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 = err
return
}
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 one
commandArgs.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 teams
if 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.UserId
commandArgs.T = c.App.T
commandArgs.Session = c.App.Session
commandArgs.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 = clientTriggerId
return 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{})
}