Landscape
Command Provider
所有 CommandProvider 保存在包全局映射 commandProviders 中,提供 RegisterCommandProvider 方法,向 commandProviders 中注册新的 CommandProvider,key 为 CommandProvider.GetTrigger 返回的字符串。
Operations
Command 相关操作在 api4.InitCommand 中注册,以 HTTP 方式触发,注册代码如下
func (api *API) InitCommand() {api.BaseRoutes.Commands.Handle("", api.ApiSessionRequired(createCommand)).Methods("POST")api.BaseRoutes.Commands.Handle("", api.ApiSessionRequired(listCommands)).Methods("GET")api.BaseRoutes.Commands.Handle("/execute", api.ApiSessionRequired(executeCommand)).Methods("POST")api.BaseRoutes.Command.Handle("", api.ApiSessionRequired(updateCommand)).Methods("PUT")api.BaseRoutes.Command.Handle("", api.ApiSessionRequired(deleteCommand)).Methods("DELETE")api.BaseRoutes.Team.Handle("/commands/autocomplete", api.ApiSessionRequired(listAutocompleteCommands)).Methods("GET")api.BaseRoutes.Command.Handle("/regen_token", api.ApiSessionRequired(regenCommandToken)).Methods("PUT")}
后续操作中,会大量使用 model.Command 结构,其定义如下
type Command struct {Id string `json:"id"`Token string `json:"token"`CreateAt int64 `json:"create_at"`UpdateAt int64 `json:"update_at"`DeleteAt int64 `json:"delete_at"`CreatorId string `json:"creator_id"`TeamId string `json:"team_id"`Trigger string `json:"trigger"`Method string `json:"method"`Username string `json:"username"`IconURL string `json:"icon_url"`AutoComplete bool `json:"auto_complete"`AutoCompleteDesc string `json:"auto_complete_desc"`AutoCompleteHint string `json:"auto_complete_hint"`DisplayName string `json:"display_name"`Description string `json:"description"`URL string `json:"url"`}
Command Creation
根据上面的路由信息,创建 Command 的 Handler 方法为 createCommand,下面我们来看具体的执行步骤。首先,需要从 http.Request 中解析出需要的 Command 结构信息。
func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {cmd := model.CommandFromJson(r.Body)// ...}
然后,根据 cmd 中指定的 TeamId 及 App 所在 Session,判断是否有创建命令操作权限,如下所示
if !c.App.SessionHasPermissionToTeam(c.App.Session, cmd.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) {c.SetPermissionError(model.PERMISSION_MANAGE_SLASH_COMMANDS)return}
如果有权限,设置 Command 的创建者 ID
cmd.CreatorId = c.App.Session.UserId
执行创建操作,创建操作仍然需要 App 为核心
rcmd, err := c.App.CreateCommand(cmd)
App.CreateCommand
检查 Team 全部命令与新 Command 是否冲突
cmd.Trigger = strings.ToLower(cmd.Trigger)teamCmds, err := a.Srv.Store.Command().GetByTeam(cmd.TeamId)if err != nil {return nil, err}for _, existingCommand := range teamCmds {if cmd.Trigger == existingCommand.Trigger {return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)}}
然后,再检查是否与全局内建的命令冲突
for _, builtInProvider := range commandProviders {builtInCommand := builtInProvider.GetCommand(a, utils.T)if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger {return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)}}
如果一切正常,则存储命令
return a.Srv.Store.Command().Save(cmd)
Command 与 Users、Teams 的关系如下所示
Command Execution
首先,获取要执行命令的参数
commandArgs := model.CommandArgsFromJson(r.Body)
CommandArgs 定义如下
type CommandArgs struct {UserId string `json:"user_id"`ChannelId string `json:"channel_id"`TeamId string `json:"team_id"`RootId string `json:"root_id"`ParentId string `json:"parent_id"`TriggerId string `json:"trigger_id,omitempty"`Command string `json:"command"`SiteURL string `json:"-"`T goi18n.TranslateFunc `json:"-"`Session Session `json:"-"`}
检查权限
if !c.App.SessionHasPermissionToChannel(c.App.Session, commandArgs.ChannelId, model.PERMISSION_USE_SLASH_COMMANDS) {c.SetPermissionError(model.PERMISSION_USE_SLASH_COMMANDS)return}
获取 Channel、检查权限
channel, err := c.App.GetChannel(commandArgs.ChannelId)if err != nil {c.Err = errreturn}if channel.Type != model.CHANNEL_DIRECT && channel.Type != model.CHANNEL_GROUP {// if this isn't a DM or GM, the team id is implicitly taken from the channel so that slash commands created on// some other team can't be run against this onecommandArgs.TeamId = channel.TeamId} else {// if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that// they can't just execute slash commands against arbitrary teamsif c.App.Session.GetTeamByTeamId(commandArgs.TeamId) == nil {if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_USE_SLASH_COMMANDS) {c.SetPermissionError(model.PERMISSION_USE_SLASH_COMMANDS)return}}}
完善 CommanArgs 并执行
commandArgs.UserId = c.App.Session.UserIdcommandArgs.T = c.App.TcommandArgs.Session = c.App.SessioncommandArgs.SiteURL = c.GetSiteURLHeader()response, err := c.App.ExecuteCommand(commandArgs)
ExecuteCommand
首先尝试执行内建命令
cmd, response := a.tryExecuteBuiltInCommand(args, trigger, message)if cmd != nil && response != nil {return a.HandleCommandResponse(cmd, args, response, true)}
再尝试执行 plugin 引入的命令
cmd, response, appErr = a.tryExecutePluginCommand(args)if appErr != nil {return nil, appErr} else if cmd != nil && response != nil {response.TriggerId = clientTriggerIdreturn a.HandleCommandResponse(cmd, args, response, true)}
最后执行自定义命令
cmd, response, appErr = a.tryExecuteCustomCommand(args, trigger, message)
Command Webhook
在执行自定义命令时,会注册一个 webhook,用于命令向 mattermost 返回结果
hook, appErr := a.CreateCommandWebhook(cmd.Id, args)if appErr != nil {return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, appErr.Error(), http.StatusInternalServerError)}p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)return a.doCommandRequest(cmd, p)
/hooks/commands 路由处理函数在 web/webhook.go 中注册
func (w *Web) InitWebhooks() {w.MainRouter.Handle("/hooks/commands/{id:[A-Za-z0-9]+}", w.NewHandler(commandWebhook)).Methods("POST")w.MainRouter.Handle("/hooks/{id:[A-Za-z0-9]+}", w.NewHandler(incomingWebhook)).Methods("POST")}
Build-in Commands
Location
内置的 Command 位于 app/command_xxx.go 中,在 init 方法中注册,以 command_open.go 为例
func init() {RegisterCommandProvider(&OpenProvider{})}
