github.com/crspeller/mattermost-server@v0.0.0-20190328001957-a200beb3d111/app/command.go (about) 1 // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package app 5 6 import ( 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "strings" 13 14 "github.com/crspeller/mattermost-server/mlog" 15 "github.com/crspeller/mattermost-server/model" 16 "github.com/crspeller/mattermost-server/utils" 17 goi18n "github.com/nicksnyder/go-i18n/i18n" 18 ) 19 20 type CommandProvider interface { 21 GetTrigger() string 22 GetCommand(a *App, T goi18n.TranslateFunc) *model.Command 23 DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse 24 } 25 26 var commandProviders = make(map[string]CommandProvider) 27 28 func RegisterCommandProvider(newProvider CommandProvider) { 29 commandProviders[newProvider.GetTrigger()] = newProvider 30 } 31 32 func GetCommandProvider(name string) CommandProvider { 33 provider, ok := commandProviders[name] 34 if ok { 35 return provider 36 } 37 38 return nil 39 } 40 41 func (a *App) CreateCommandPost(post *model.Post, teamId string, response *model.CommandResponse) (*model.Post, *model.AppError) { 42 post.Message = model.ParseSlackLinksToMarkdown(response.Text) 43 post.CreateAt = model.GetMillis() 44 45 if strings.HasPrefix(post.Type, model.POST_SYSTEM_MESSAGE_PREFIX) { 46 err := model.NewAppError("CreateCommandPost", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "post.type"}, "", http.StatusBadRequest) 47 return nil, err 48 } 49 50 if response.Attachments != nil { 51 model.ParseSlackAttachment(post, response.Attachments) 52 } 53 54 if response.ResponseType == model.COMMAND_RESPONSE_TYPE_IN_CHANNEL { 55 return a.CreatePostMissingChannel(post, true) 56 } 57 58 if (response.ResponseType == "" || response.ResponseType == model.COMMAND_RESPONSE_TYPE_EPHEMERAL) && (response.Text != "" || response.Attachments != nil) { 59 post.ParentId = "" 60 a.SendEphemeralPost(post.UserId, post) 61 } 62 63 return post, nil 64 } 65 66 // previous ListCommands now ListAutocompleteCommands 67 func (a *App) ListAutocompleteCommands(teamId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) { 68 commands := make([]*model.Command, 0, 32) 69 seen := make(map[string]bool) 70 for _, value := range commandProviders { 71 if cmd := value.GetCommand(a, T); cmd != nil { 72 cpy := *cmd 73 if cpy.AutoComplete && !seen[cpy.Id] { 74 cpy.Sanitize() 75 seen[cpy.Trigger] = true 76 commands = append(commands, &cpy) 77 } 78 } 79 } 80 81 for _, cmd := range a.PluginCommandsForTeam(teamId) { 82 if cmd.AutoComplete && !seen[cmd.Trigger] { 83 seen[cmd.Trigger] = true 84 commands = append(commands, cmd) 85 } 86 } 87 88 if *a.Config().ServiceSettings.EnableCommands { 89 result := <-a.Srv.Store.Command().GetByTeam(teamId) 90 if result.Err != nil { 91 return nil, result.Err 92 } 93 94 teamCmds := result.Data.([]*model.Command) 95 for _, cmd := range teamCmds { 96 if cmd.AutoComplete && !seen[cmd.Id] { 97 cmd.Sanitize() 98 seen[cmd.Trigger] = true 99 commands = append(commands, cmd) 100 } 101 } 102 } 103 104 return commands, nil 105 } 106 107 func (a *App) ListTeamCommands(teamId string) ([]*model.Command, *model.AppError) { 108 if !*a.Config().ServiceSettings.EnableCommands { 109 return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 110 } 111 112 result := <-a.Srv.Store.Command().GetByTeam(teamId) 113 if result.Err != nil { 114 return nil, result.Err 115 } 116 117 return result.Data.([]*model.Command), nil 118 } 119 120 func (a *App) ListAllCommands(teamId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) { 121 commands := make([]*model.Command, 0, 32) 122 seen := make(map[string]bool) 123 for _, value := range commandProviders { 124 if cmd := value.GetCommand(a, T); cmd != nil { 125 cpy := *cmd 126 if cpy.AutoComplete && !seen[cpy.Trigger] { 127 cpy.Sanitize() 128 seen[cpy.Trigger] = true 129 commands = append(commands, &cpy) 130 } 131 } 132 } 133 134 for _, cmd := range a.PluginCommandsForTeam(teamId) { 135 if !seen[cmd.Trigger] { 136 seen[cmd.Trigger] = true 137 commands = append(commands, cmd) 138 } 139 } 140 141 if *a.Config().ServiceSettings.EnableCommands { 142 result := <-a.Srv.Store.Command().GetByTeam(teamId) 143 if result.Err != nil { 144 return nil, result.Err 145 } 146 teamCmds := result.Data.([]*model.Command) 147 for _, cmd := range teamCmds { 148 if !seen[cmd.Trigger] { 149 cmd.Sanitize() 150 seen[cmd.Trigger] = true 151 commands = append(commands, cmd) 152 } 153 } 154 } 155 156 return commands, nil 157 } 158 159 func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { 160 parts := strings.Split(args.Command, " ") 161 trigger := parts[0][1:] 162 trigger = strings.ToLower(trigger) 163 message := strings.Join(parts[1:], " ") 164 165 clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey()) 166 if appErr != nil { 167 mlog.Error(appErr.Error()) 168 } 169 170 args.TriggerId = triggerId 171 172 cmd, response := a.tryExecuteBuiltInCommand(args, trigger, message) 173 if cmd != nil && response != nil { 174 return a.HandleCommandResponse(cmd, args, response, true) 175 } 176 177 cmd, response, appErr = a.tryExecutePluginCommand(args) 178 if appErr != nil { 179 return nil, appErr 180 } else if cmd != nil && response != nil { 181 response.TriggerId = clientTriggerId 182 return a.HandleCommandResponse(cmd, args, response, true) 183 } 184 185 cmd, response, appErr = a.tryExecuteCustomCommand(args, trigger, message) 186 if appErr != nil { 187 return nil, appErr 188 } else if cmd != nil && response != nil { 189 response.TriggerId = clientTriggerId 190 return a.HandleCommandResponse(cmd, args, response, false) 191 } 192 193 return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusNotFound) 194 } 195 196 // tryExecutePluginCommand attempts to run a built in command based on the given arguments. If no such command can be 197 // found, returns nil for all arguments. 198 func (a *App) tryExecuteBuiltInCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) { 199 provider := GetCommandProvider(trigger) 200 if provider == nil { 201 return nil, nil 202 } 203 204 cmd := provider.GetCommand(a, args.T) 205 if cmd == nil { 206 return nil, nil 207 } 208 209 return cmd, provider.DoCommand(a, args, message) 210 } 211 212 // tryExecuteCustomCommand attempts to run a custom command based on the given arguments. If no such command can be 213 // found, returns nil for all arguments. 214 func (a *App) tryExecuteCustomCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse, *model.AppError) { 215 // Handle custom commands 216 if !*a.Config().ServiceSettings.EnableCommands { 217 return nil, nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 218 } 219 220 chanChan := a.Srv.Store.Channel().Get(args.ChannelId, true) 221 teamChan := a.Srv.Store.Team().Get(args.TeamId) 222 userChan := a.Srv.Store.User().Get(args.UserId) 223 224 result := <-a.Srv.Store.Command().GetByTeam(args.TeamId) 225 if result.Err != nil { 226 return nil, nil, result.Err 227 } 228 229 tr := <-teamChan 230 if tr.Err != nil { 231 return nil, nil, tr.Err 232 } 233 team := tr.Data.(*model.Team) 234 235 ur := <-userChan 236 if ur.Err != nil { 237 return nil, nil, ur.Err 238 } 239 user := ur.Data.(*model.User) 240 241 cr := <-chanChan 242 if cr.Err != nil { 243 return nil, nil, cr.Err 244 } 245 channel := cr.Data.(*model.Channel) 246 247 var cmd *model.Command 248 249 teamCmds := result.Data.([]*model.Command) 250 for _, teamCmd := range teamCmds { 251 if trigger == teamCmd.Trigger { 252 cmd = teamCmd 253 } 254 } 255 256 if cmd == nil { 257 return nil, nil, nil 258 } 259 260 mlog.Debug(fmt.Sprintf(utils.T("api.command.execute_command.debug"), trigger, args.UserId)) 261 262 p := url.Values{} 263 p.Set("token", cmd.Token) 264 265 p.Set("team_id", cmd.TeamId) 266 p.Set("team_domain", team.Name) 267 268 p.Set("channel_id", args.ChannelId) 269 p.Set("channel_name", channel.Name) 270 271 p.Set("user_id", args.UserId) 272 p.Set("user_name", user.Username) 273 274 p.Set("command", "/"+trigger) 275 p.Set("text", message) 276 277 p.Set("trigger_id", args.TriggerId) 278 279 hook, appErr := a.CreateCommandWebhook(cmd.Id, args) 280 if appErr != nil { 281 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, appErr.Error(), http.StatusInternalServerError) 282 } 283 p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id) 284 285 return a.doCommandRequest(cmd, p) 286 } 287 288 func (a *App) doCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) { 289 // Prepare the request 290 var req *http.Request 291 var err error 292 if cmd.Method == model.COMMAND_METHOD_GET { 293 req, err = http.NewRequest(http.MethodGet, cmd.URL, nil) 294 } else { 295 req, err = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode())) 296 } 297 298 if err != nil { 299 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError) 300 } 301 302 if cmd.Method == model.COMMAND_METHOD_GET { 303 if req.URL.RawQuery != "" { 304 req.URL.RawQuery += "&" 305 } 306 req.URL.RawQuery += p.Encode() 307 } 308 309 req.Header.Set("Accept", "application/json") 310 req.Header.Set("Authorization", "Token "+cmd.Token) 311 if cmd.Method == model.COMMAND_METHOD_POST { 312 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 313 } 314 315 // Send the request 316 resp, err := a.HTTPService.MakeClient(false).Do(req) 317 if err != nil { 318 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError) 319 } 320 321 defer resp.Body.Close() 322 323 // Handle the response 324 body := io.LimitReader(resp.Body, MaxIntegrationResponseSize) 325 326 if resp.StatusCode != http.StatusOK { 327 // Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil 328 bodyBytes, _ := ioutil.ReadAll(body) 329 330 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]interface{}{"Trigger": cmd.Trigger, "Status": resp.Status}, string(bodyBytes), http.StatusInternalServerError) 331 } 332 333 response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), body) 334 if err != nil { 335 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError) 336 } else if response == nil { 337 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError) 338 } 339 340 return cmd, response, nil 341 } 342 343 func (a *App) HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) { 344 trigger := "" 345 if len(args.Command) != 0 { 346 parts := strings.Split(args.Command, " ") 347 trigger = parts[0][1:] 348 trigger = strings.ToLower(trigger) 349 } 350 351 var lastError *model.AppError 352 _, err := a.HandleCommandResponsePost(command, args, response, builtIn) 353 354 if err != nil { 355 mlog.Error(err.Error()) 356 lastError = err 357 } 358 359 if response.ExtraResponses != nil { 360 for _, resp := range response.ExtraResponses { 361 _, err := a.HandleCommandResponsePost(command, args, resp, builtIn) 362 363 if err != nil { 364 mlog.Error(err.Error()) 365 lastError = err 366 } 367 } 368 } 369 370 if lastError != nil { 371 return response, model.NewAppError("command", "api.command.execute_command.create_post_failed.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusInternalServerError) 372 } 373 374 return response, nil 375 } 376 377 func (a *App) HandleCommandResponsePost(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) { 378 post := &model.Post{} 379 post.ChannelId = args.ChannelId 380 post.RootId = args.RootId 381 post.ParentId = args.ParentId 382 post.UserId = args.UserId 383 post.Type = response.Type 384 post.Props = response.Props 385 386 if len(response.ChannelId) != 0 { 387 _, err := a.GetChannelMember(response.ChannelId, args.UserId) 388 if err != nil { 389 err = model.NewAppError("HandleCommandResponsePost", "api.command.command_post.forbidden.app_error", nil, err.Error(), http.StatusForbidden) 390 return nil, err 391 } 392 post.ChannelId = response.ChannelId 393 } 394 395 isBotPost := !builtIn 396 397 if *a.Config().ServiceSettings.EnablePostUsernameOverride { 398 if len(command.Username) != 0 { 399 post.AddProp("override_username", command.Username) 400 isBotPost = true 401 } else if len(response.Username) != 0 { 402 post.AddProp("override_username", response.Username) 403 isBotPost = true 404 } 405 } 406 407 if *a.Config().ServiceSettings.EnablePostIconOverride { 408 if len(command.IconURL) != 0 { 409 post.AddProp("override_icon_url", command.IconURL) 410 isBotPost = true 411 } else if len(response.IconURL) != 0 { 412 post.AddProp("override_icon_url", response.IconURL) 413 isBotPost = true 414 } else { 415 post.AddProp("override_icon_url", "") 416 } 417 } 418 419 if isBotPost { 420 post.AddProp("from_webhook", "true") 421 } 422 423 // Process Slack text replacements 424 response.Text = a.ProcessSlackText(response.Text) 425 response.Attachments = a.ProcessSlackAttachments(response.Attachments) 426 427 if _, err := a.CreateCommandPost(post, args.TeamId, response); err != nil { 428 return post, err 429 } 430 431 return post, nil 432 } 433 434 func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) { 435 if !*a.Config().ServiceSettings.EnableCommands { 436 return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 437 } 438 439 cmd.Trigger = strings.ToLower(cmd.Trigger) 440 441 result := <-a.Srv.Store.Command().GetByTeam(cmd.TeamId) 442 if result.Err != nil { 443 return nil, result.Err 444 } 445 446 teamCmds := result.Data.([]*model.Command) 447 for _, existingCommand := range teamCmds { 448 if cmd.Trigger == existingCommand.Trigger { 449 return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) 450 } 451 } 452 453 for _, builtInProvider := range commandProviders { 454 builtInCommand := builtInProvider.GetCommand(a, utils.T) 455 if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger { 456 return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) 457 } 458 } 459 460 result = <-a.Srv.Store.Command().Save(cmd) 461 if result.Err != nil { 462 return nil, result.Err 463 } 464 465 return result.Data.(*model.Command), nil 466 } 467 468 func (a *App) GetCommand(commandId string) (*model.Command, *model.AppError) { 469 if !*a.Config().ServiceSettings.EnableCommands { 470 return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 471 } 472 473 result := <-a.Srv.Store.Command().Get(commandId) 474 if result.Err != nil { 475 result.Err.StatusCode = http.StatusNotFound 476 return nil, result.Err 477 } 478 479 return result.Data.(*model.Command), nil 480 } 481 482 func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) { 483 if !*a.Config().ServiceSettings.EnableCommands { 484 return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 485 } 486 487 updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger) 488 updatedCmd.Id = oldCmd.Id 489 updatedCmd.Token = oldCmd.Token 490 updatedCmd.CreateAt = oldCmd.CreateAt 491 updatedCmd.UpdateAt = model.GetMillis() 492 updatedCmd.DeleteAt = oldCmd.DeleteAt 493 updatedCmd.CreatorId = oldCmd.CreatorId 494 updatedCmd.TeamId = oldCmd.TeamId 495 496 result := <-a.Srv.Store.Command().Update(updatedCmd) 497 if result.Err != nil { 498 return nil, result.Err 499 } 500 return result.Data.(*model.Command), nil 501 } 502 503 func (a *App) MoveCommand(team *model.Team, command *model.Command) *model.AppError { 504 command.TeamId = team.Id 505 506 result := <-a.Srv.Store.Command().Update(command) 507 if result.Err != nil { 508 return result.Err 509 } 510 511 return nil 512 } 513 514 func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) { 515 if !*a.Config().ServiceSettings.EnableCommands { 516 return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 517 } 518 519 cmd.Token = model.NewId() 520 521 result := <-a.Srv.Store.Command().Update(cmd) 522 if result.Err != nil { 523 return nil, result.Err 524 } 525 526 return result.Data.(*model.Command), nil 527 } 528 529 func (a *App) DeleteCommand(commandId string) *model.AppError { 530 if !*a.Config().ServiceSettings.EnableCommands { 531 return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 532 } 533 result := <-a.Srv.Store.Command().Delete(commandId, model.GetMillis()) 534 if result.Err != nil { 535 return result.Err 536 } 537 return nil 538 }