Landscape

Command Provider

command-landscape.svg

所有 CommandProvider 保存在包全局映射 commandProviders 中,提供 RegisterCommandProvider 方法,向 commandProviders 中注册新的 CommandProvider,key 为 CommandProvider.GetTrigger 返回的字符串。

Operations

Command 相关操作在 api4.InitCommand 中注册,以 HTTP 方式触发,注册代码如下

  1. func (api *API) InitCommand() {
  2. api.BaseRoutes.Commands.Handle("", api.ApiSessionRequired(createCommand)).Methods("POST")
  3. api.BaseRoutes.Commands.Handle("", api.ApiSessionRequired(listCommands)).Methods("GET")
  4. api.BaseRoutes.Commands.Handle("/execute", api.ApiSessionRequired(executeCommand)).Methods("POST")
  5. api.BaseRoutes.Command.Handle("", api.ApiSessionRequired(updateCommand)).Methods("PUT")
  6. api.BaseRoutes.Command.Handle("", api.ApiSessionRequired(deleteCommand)).Methods("DELETE")
  7. api.BaseRoutes.Team.Handle("/commands/autocomplete", api.ApiSessionRequired(listAutocompleteCommands)).Methods("GET")
  8. api.BaseRoutes.Command.Handle("/regen_token", api.ApiSessionRequired(regenCommandToken)).Methods("PUT")
  9. }

后续操作中,会大量使用 model.Command 结构,其定义如下

  1. type Command struct {
  2. Id string `json:"id"`
  3. Token string `json:"token"`
  4. CreateAt int64 `json:"create_at"`
  5. UpdateAt int64 `json:"update_at"`
  6. DeleteAt int64 `json:"delete_at"`
  7. CreatorId string `json:"creator_id"`
  8. TeamId string `json:"team_id"`
  9. Trigger string `json:"trigger"`
  10. Method string `json:"method"`
  11. Username string `json:"username"`
  12. IconURL string `json:"icon_url"`
  13. AutoComplete bool `json:"auto_complete"`
  14. AutoCompleteDesc string `json:"auto_complete_desc"`
  15. AutoCompleteHint string `json:"auto_complete_hint"`
  16. DisplayName string `json:"display_name"`
  17. Description string `json:"description"`
  18. URL string `json:"url"`
  19. }

Command Creation

根据上面的路由信息,创建 Command 的 Handler 方法为 createCommand,下面我们来看具体的执行步骤。首先,需要从 http.Request 中解析出需要的 Command 结构信息。

  1. func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
  2. cmd := model.CommandFromJson(r.Body)
  3. // ...
  4. }

然后,根据 cmd 中指定的 TeamId 及 App 所在 Session,判断是否有创建命令操作权限,如下所示

  1. if !c.App.SessionHasPermissionToTeam(c.App.Session, cmd.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
  2. c.SetPermissionError(model.PERMISSION_MANAGE_SLASH_COMMANDS)
  3. return
  4. }

如果有权限,设置 Command 的创建者 ID

  1. cmd.CreatorId = c.App.Session.UserId

执行创建操作,创建操作仍然需要 App 为核心

  1. rcmd, err := c.App.CreateCommand(cmd)

App.CreateCommand

检查 Team 全部命令与新 Command 是否冲突

  1. cmd.Trigger = strings.ToLower(cmd.Trigger)
  2. teamCmds, err := a.Srv.Store.Command().GetByTeam(cmd.TeamId)
  3. if err != nil {
  4. return nil, err
  5. }
  6. for _, existingCommand := range teamCmds {
  7. if cmd.Trigger == existingCommand.Trigger {
  8. return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
  9. }
  10. }

然后,再检查是否与全局内建的命令冲突

  1. for _, builtInProvider := range commandProviders {
  2. builtInCommand := builtInProvider.GetCommand(a, utils.T)
  3. if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger {
  4. return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
  5. }
  6. }

如果一切正常,则存储命令

  1. return a.Srv.Store.Command().Save(cmd)

Command 与 Users、Teams 的关系如下所示
command-db-relation.svg

Command Execution

首先,获取要执行命令的参数

  1. commandArgs := model.CommandArgsFromJson(r.Body)

CommandArgs 定义如下

  1. type CommandArgs struct {
  2. UserId string `json:"user_id"`
  3. ChannelId string `json:"channel_id"`
  4. TeamId string `json:"team_id"`
  5. RootId string `json:"root_id"`
  6. ParentId string `json:"parent_id"`
  7. TriggerId string `json:"trigger_id,omitempty"`
  8. Command string `json:"command"`
  9. SiteURL string `json:"-"`
  10. T goi18n.TranslateFunc `json:"-"`
  11. Session Session `json:"-"`
  12. }

检查权限

  1. if !c.App.SessionHasPermissionToChannel(c.App.Session, commandArgs.ChannelId, model.PERMISSION_USE_SLASH_COMMANDS) {
  2. c.SetPermissionError(model.PERMISSION_USE_SLASH_COMMANDS)
  3. return
  4. }

获取 Channel、检查权限

  1. channel, err := c.App.GetChannel(commandArgs.ChannelId)
  2. if err != nil {
  3. c.Err = err
  4. return
  5. }
  6. if channel.Type != model.CHANNEL_DIRECT && channel.Type != model.CHANNEL_GROUP {
  7. // if this isn't a DM or GM, the team id is implicitly taken from the channel so that slash commands created on
  8. // some other team can't be run against this one
  9. commandArgs.TeamId = channel.TeamId
  10. } else {
  11. // if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that
  12. // they can't just execute slash commands against arbitrary teams
  13. if c.App.Session.GetTeamByTeamId(commandArgs.TeamId) == nil {
  14. if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_USE_SLASH_COMMANDS) {
  15. c.SetPermissionError(model.PERMISSION_USE_SLASH_COMMANDS)
  16. return
  17. }
  18. }
  19. }

完善 CommanArgs 并执行

  1. commandArgs.UserId = c.App.Session.UserId
  2. commandArgs.T = c.App.T
  3. commandArgs.Session = c.App.Session
  4. commandArgs.SiteURL = c.GetSiteURLHeader()
  5. response, err := c.App.ExecuteCommand(commandArgs)

ExecuteCommand

首先尝试执行内建命令

  1. cmd, response := a.tryExecuteBuiltInCommand(args, trigger, message)
  2. if cmd != nil && response != nil {
  3. return a.HandleCommandResponse(cmd, args, response, true)
  4. }

再尝试执行 plugin 引入的命令

  1. cmd, response, appErr = a.tryExecutePluginCommand(args)
  2. if appErr != nil {
  3. return nil, appErr
  4. } else if cmd != nil && response != nil {
  5. response.TriggerId = clientTriggerId
  6. return a.HandleCommandResponse(cmd, args, response, true)
  7. }

最后执行自定义命令

  1. cmd, response, appErr = a.tryExecuteCustomCommand(args, trigger, message)

Command Webhook

在执行自定义命令时,会注册一个 webhook,用于命令向 mattermost 返回结果

  1. hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
  2. if appErr != nil {
  3. return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, appErr.Error(), http.StatusInternalServerError)
  4. }
  5. p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
  6. return a.doCommandRequest(cmd, p)

/hooks/commands 路由处理函数在 web/webhook.go 中注册

  1. func (w *Web) InitWebhooks() {
  2. w.MainRouter.Handle("/hooks/commands/{id:[A-Za-z0-9]+}", w.NewHandler(commandWebhook)).Methods("POST")
  3. w.MainRouter.Handle("/hooks/{id:[A-Za-z0-9]+}", w.NewHandler(incomingWebhook)).Methods("POST")
  4. }

Build-in Commands

Location

内置的 Command 位于 app/command_xxx.go 中,在 init 方法中注册,以 command_open.go 为例

  1. func init() {
  2. RegisterCommandProvider(&OpenProvider{})
  3. }