github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/app/command.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package app 5 6 import ( 7 "io" 8 "io/ioutil" 9 "net/http" 10 "net/url" 11 "strings" 12 "sync" 13 "unicode" 14 15 goi18n "github.com/mattermost/go-i18n/i18n" 16 "github.com/vnforks/kid/v5/mlog" 17 "github.com/vnforks/kid/v5/model" 18 "github.com/vnforks/kid/v5/store" 19 "github.com/vnforks/kid/v5/utils" 20 ) 21 22 type CommandProvider interface { 23 GetTrigger() string 24 GetCommand(a *App, T goi18n.TranslateFunc) *model.Command 25 DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse 26 } 27 28 var commandProviders = make(map[string]CommandProvider) 29 30 func RegisterCommandProvider(newProvider CommandProvider) { 31 commandProviders[newProvider.GetTrigger()] = newProvider 32 } 33 34 func GetCommandProvider(name string) CommandProvider { 35 provider, ok := commandProviders[name] 36 if ok { 37 return provider 38 } 39 40 return nil 41 } 42 43 // @openTracingParams branchId 44 // previous ListCommands now ListAutocompleteCommands 45 func (a *App) ListAutocompleteCommands(branchId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) { 46 commands := make([]*model.Command, 0, 32) 47 seen := make(map[string]bool) 48 for _, value := range commandProviders { 49 if cmd := value.GetCommand(a, T); cmd != nil { 50 cpy := *cmd 51 if cpy.AutoComplete && !seen[cpy.Id] { 52 cpy.Sanitize() 53 seen[cpy.Trigger] = true 54 commands = append(commands, &cpy) 55 } 56 } 57 } 58 59 if *a.Config().ServiceSettings.EnableCommands { 60 branchCmds, err := a.Srv().Store.Command().GetByBranch(branchId) 61 if err != nil { 62 return nil, err 63 } 64 65 for _, cmd := range branchCmds { 66 if cmd.AutoComplete && !seen[cmd.Id] { 67 cmd.Sanitize() 68 seen[cmd.Trigger] = true 69 commands = append(commands, cmd) 70 } 71 } 72 } 73 74 return commands, nil 75 } 76 77 func (a *App) ListBranchCommands(branchId string) ([]*model.Command, *model.AppError) { 78 if !*a.Config().ServiceSettings.EnableCommands { 79 return nil, model.NewAppError("ListBranchCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 80 } 81 82 return a.Srv().Store.Command().GetByBranch(branchId) 83 } 84 85 func (a *App) ListAllCommands(branchId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) { 86 commands := make([]*model.Command, 0, 32) 87 seen := make(map[string]bool) 88 for _, value := range commandProviders { 89 if cmd := value.GetCommand(a, T); cmd != nil { 90 cpy := *cmd 91 if cpy.AutoComplete && !seen[cpy.Trigger] { 92 cpy.Sanitize() 93 seen[cpy.Trigger] = true 94 commands = append(commands, &cpy) 95 } 96 } 97 } 98 99 if *a.Config().ServiceSettings.EnableCommands { 100 branchCmds, err := a.Srv().Store.Command().GetByBranch(branchId) 101 if err != nil { 102 return nil, err 103 } 104 for _, cmd := range branchCmds { 105 if !seen[cmd.Trigger] { 106 cmd.Sanitize() 107 seen[cmd.Trigger] = true 108 commands = append(commands, cmd) 109 } 110 } 111 } 112 113 return commands, nil 114 } 115 116 // @openTracingParams args 117 func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { 118 trigger := "" 119 message := "" 120 index := strings.IndexFunc(args.Command, unicode.IsSpace) 121 if index != -1 { 122 trigger = args.Command[:index] 123 message = args.Command[index+1:] 124 } else { 125 trigger = args.Command 126 } 127 trigger = strings.ToLower(trigger) 128 if !strings.HasPrefix(trigger, "/") { 129 return nil, model.NewAppError("command", "api.command.execute_command.format.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusBadRequest) 130 } 131 trigger = strings.TrimPrefix(trigger, "/") 132 133 clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey()) 134 if appErr != nil { 135 mlog.Error("error occurred in generating trigger Id for a user ", mlog.Err(appErr)) 136 } 137 138 args.TriggerId = triggerId 139 140 cmd, response := a.tryExecuteBuiltInCommand(args, trigger, message) 141 if cmd != nil && response != nil { 142 return a.HandleCommandResponse(cmd, args, response, true) 143 } 144 145 if cmd != nil && response != nil { 146 response.TriggerId = clientTriggerId 147 return a.HandleCommandResponse(cmd, args, response, true) 148 } 149 150 cmd, response, appErr = a.tryExecuteCustomCommand(args, trigger, message) 151 if appErr != nil { 152 return nil, appErr 153 } else if cmd != nil && response != nil { 154 response.TriggerId = clientTriggerId 155 return a.HandleCommandResponse(cmd, args, response, false) 156 } 157 158 return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusNotFound) 159 } 160 161 // mentionsToBranchMembers returns all the @ mentions found in message that 162 // belong to users in the specified branch, linking them to their users 163 func (a *App) mentionsToBranchMembers(message, branchId string) model.UserMentionMap { 164 type mentionMapItem struct { 165 Name string 166 Id string 167 } 168 169 possibleMentions := model.PossibleAtMentions(message) 170 mentionChan := make(chan *mentionMapItem, len(possibleMentions)) 171 172 var wg sync.WaitGroup 173 for _, mention := range possibleMentions { 174 wg.Add(1) 175 go func(mention string) { 176 defer wg.Done() 177 user, err := a.Srv().Store.User().GetByUsername(mention) 178 179 if err != nil && err.StatusCode != http.StatusNotFound { 180 mlog.Warn("Failed to retrieve user @"+mention, mlog.Err(err)) 181 return 182 } 183 184 // If it's a http.StatusNotFound error, check for usernames in substrings 185 // without trailing punctuation 186 if err != nil { 187 trimmed, ok := model.TrimUsernameSpecialChar(mention) 188 for ; ok; trimmed, ok = model.TrimUsernameSpecialChar(trimmed) { 189 userFromTrimmed, userErr := a.Srv().Store.User().GetByUsername(trimmed) 190 if userErr != nil && err.StatusCode != http.StatusNotFound { 191 return 192 } 193 194 if userErr != nil { 195 continue 196 } 197 198 _, err = a.GetBranchMember(branchId, userFromTrimmed.Id) 199 if err != nil { 200 // The user is not in the branch, so we should ignore it 201 return 202 } 203 204 mentionChan <- &mentionMapItem{trimmed, userFromTrimmed.Id} 205 return 206 } 207 208 return 209 } 210 211 _, err = a.GetBranchMember(branchId, user.Id) 212 if err != nil { 213 // The user is not in the branch, so we should ignore it 214 return 215 } 216 217 mentionChan <- &mentionMapItem{mention, user.Id} 218 }(mention) 219 } 220 221 wg.Wait() 222 close(mentionChan) 223 224 atMentionMap := make(model.UserMentionMap) 225 for mention := range mentionChan { 226 atMentionMap[mention.Name] = mention.Id 227 } 228 229 return atMentionMap 230 } 231 232 // tryExecuteBuiltInCommand attempts to run a built in command based on the given arguments. If no such command can be 233 // found, returns nil for all arguments. 234 func (a *App) tryExecuteBuiltInCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) { 235 provider := GetCommandProvider(trigger) 236 if provider == nil { 237 return nil, nil 238 } 239 240 cmd := provider.GetCommand(a, args.T) 241 if cmd == nil { 242 return nil, nil 243 } 244 245 return cmd, provider.DoCommand(a, args, message) 246 } 247 248 // tryExecuteCustomCommand attempts to run a custom command based on the given arguments. If no such command can be 249 // found, returns nil for all arguments. 250 func (a *App) tryExecuteCustomCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse, *model.AppError) { 251 // Handle custom commands 252 if !*a.Config().ServiceSettings.EnableCommands { 253 return nil, nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 254 } 255 256 chanChan := make(chan store.StoreResult, 1) 257 go func() { 258 class, err := a.Srv().Store.Class().Get(args.ClassId, true) 259 chanChan <- store.StoreResult{Data: class, Err: err} 260 close(chanChan) 261 }() 262 263 branchChan := make(chan store.StoreResult, 1) 264 go func() { 265 branch, err := a.Srv().Store.Branch().Get(args.BranchId) 266 branchChan <- store.StoreResult{Data: branch, Err: err} 267 close(branchChan) 268 }() 269 270 userChan := make(chan store.StoreResult, 1) 271 go func() { 272 user, err := a.Srv().Store.User().Get(args.UserId) 273 userChan <- store.StoreResult{Data: user, Err: err} 274 close(userChan) 275 }() 276 277 branchCmds, err := a.Srv().Store.Command().GetByBranch(args.BranchId) 278 if err != nil { 279 return nil, nil, err 280 } 281 282 tr := <-branchChan 283 if tr.Err != nil { 284 return nil, nil, tr.Err 285 } 286 branch := tr.Data.(*model.Branch) 287 288 ur := <-userChan 289 if ur.Err != nil { 290 return nil, nil, ur.Err 291 } 292 user := ur.Data.(*model.User) 293 294 cr := <-chanChan 295 if cr.Err != nil { 296 return nil, nil, cr.Err 297 } 298 class := cr.Data.(*model.Class) 299 300 var cmd *model.Command 301 302 for _, branchCmd := range branchCmds { 303 if trigger == branchCmd.Trigger { 304 cmd = branchCmd 305 } 306 } 307 308 if cmd == nil { 309 return nil, nil, nil 310 } 311 312 mlog.Debug("Executing command", mlog.String("command", trigger), mlog.String("user_id", args.UserId)) 313 314 p := url.Values{} 315 p.Set("token", cmd.Token) 316 317 p.Set("branch_id", cmd.BranchId) 318 p.Set("branch_domain", branch.Name) 319 320 p.Set("class_id", args.ClassId) 321 p.Set("class_name", class.Name) 322 323 p.Set("user_id", args.UserId) 324 p.Set("user_name", user.Username) 325 326 p.Set("command", "/"+trigger) 327 p.Set("text", message) 328 329 p.Set("trigger_id", args.TriggerId) 330 331 userMentionMap := a.mentionsToBranchMembers(message, branch.Id) 332 for key, values := range userMentionMap.ToURLValues() { 333 p[key] = values 334 } 335 336 return a.doCommandRequest(cmd, p) 337 } 338 339 func (a *App) doCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) { 340 // Prepare the request 341 var req *http.Request 342 var err error 343 if cmd.Method == model.COMMAND_METHOD_GET { 344 req, err = http.NewRequest(http.MethodGet, cmd.URL, nil) 345 } else { 346 req, err = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode())) 347 } 348 349 if err != nil { 350 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError) 351 } 352 353 if cmd.Method == model.COMMAND_METHOD_GET { 354 if req.URL.RawQuery != "" { 355 req.URL.RawQuery += "&" 356 } 357 req.URL.RawQuery += p.Encode() 358 } 359 360 req.Header.Set("Accept", "application/json") 361 req.Header.Set("Authorization", "Token "+cmd.Token) 362 if cmd.Method == model.COMMAND_METHOD_POST { 363 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 364 } 365 366 // Send the request 367 resp, err := a.HTTPService().MakeClient(false).Do(req) 368 if err != nil { 369 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError) 370 } 371 372 defer resp.Body.Close() 373 374 // Handle the response 375 body := io.LimitReader(resp.Body, 1024*1024) 376 377 if resp.StatusCode != http.StatusOK { 378 // Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil 379 bodyBytes, _ := ioutil.ReadAll(body) 380 381 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) 382 } 383 384 response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), body) 385 if err != nil { 386 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError) 387 } else if response == nil { 388 return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError) 389 } 390 391 return cmd, response, nil 392 } 393 394 func (a *App) HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) { 395 trigger := "" 396 if len(args.Command) != 0 { 397 parts := strings.Split(args.Command, " ") 398 trigger = parts[0][1:] 399 trigger = strings.ToLower(trigger) 400 } 401 402 return response, nil 403 } 404 405 func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) { 406 if !*a.Config().ServiceSettings.EnableCommands { 407 return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 408 } 409 410 cmd.Trigger = strings.ToLower(cmd.Trigger) 411 412 branchCmds, err := a.Srv().Store.Command().GetByBranch(cmd.BranchId) 413 if err != nil { 414 return nil, err 415 } 416 417 for _, existingCommand := range branchCmds { 418 if cmd.Trigger == existingCommand.Trigger { 419 return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) 420 } 421 } 422 423 for _, builtInProvider := range commandProviders { 424 builtInCommand := builtInProvider.GetCommand(a, utils.T) 425 if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger { 426 return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) 427 } 428 } 429 430 return a.Srv().Store.Command().Save(cmd) 431 } 432 433 func (a *App) GetCommand(commandId string) (*model.Command, *model.AppError) { 434 if !*a.Config().ServiceSettings.EnableCommands { 435 return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 436 } 437 438 cmd, err := a.Srv().Store.Command().Get(commandId) 439 if err != nil { 440 err.StatusCode = http.StatusNotFound 441 return nil, err 442 } 443 444 return cmd, nil 445 } 446 447 func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) { 448 if !*a.Config().ServiceSettings.EnableCommands { 449 return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 450 } 451 452 updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger) 453 updatedCmd.Id = oldCmd.Id 454 updatedCmd.Token = oldCmd.Token 455 updatedCmd.CreateAt = oldCmd.CreateAt 456 updatedCmd.UpdateAt = model.GetMillis() 457 updatedCmd.DeleteAt = oldCmd.DeleteAt 458 updatedCmd.CreatorId = oldCmd.CreatorId 459 updatedCmd.BranchId = oldCmd.BranchId 460 461 return a.Srv().Store.Command().Update(updatedCmd) 462 } 463 464 func (a *App) MoveCommand(branch *model.Branch, command *model.Command) *model.AppError { 465 command.BranchId = branch.Id 466 467 _, err := a.Srv().Store.Command().Update(command) 468 if err != nil { 469 return err 470 } 471 472 return nil 473 } 474 475 func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) { 476 if !*a.Config().ServiceSettings.EnableCommands { 477 return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 478 } 479 480 cmd.Token = model.NewId() 481 482 return a.Srv().Store.Command().Update(cmd) 483 } 484 485 func (a *App) DeleteCommand(commandId string) *model.AppError { 486 if !*a.Config().ServiceSettings.EnableCommands { 487 return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) 488 } 489 490 return a.Srv().Store.Command().Delete(commandId, model.GetMillis()) 491 }