Files
mostlymatter/app/command.go
Jesse Hallam 6d50d836f5 MM-10232, MM-10259: Improve error handling from invalid json (#8668)
* MM-10232: improve error handling from malformed slash command responses

Switch to json.Unmarshal, which doesn't obscure JSON parse failures like
json.Decode. The latter is primarily designed for streams of JSON, not
necessarily unmarshalling just a single object.

* rework HumanizedJsonError to expose Line and Character discretely

* MM-10259: pinpoint line and character where json config error occurs

* tweak HumanizeJsonError to accept err first
2018-04-26 11:19:25 -04:00

417 строки
13 KiB
Go

// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package app
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
goi18n "github.com/nicksnyder/go-i18n/i18n"
)
type CommandProvider interface {
GetTrigger() string
GetCommand(a *App, T goi18n.TranslateFunc) *model.Command
DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse
}
var commandProviders = make(map[string]CommandProvider)
func RegisterCommandProvider(newProvider CommandProvider) {
commandProviders[newProvider.GetTrigger()] = newProvider
}
func GetCommandProvider(name string) CommandProvider {
provider, ok := commandProviders[name]
if ok {
return provider
}
return nil
}
func (a *App) CreateCommandPost(post *model.Post, teamId string, response *model.CommandResponse) (*model.Post, *model.AppError) {
post.Message = parseSlackLinksToMarkdown(response.Text)
post.CreateAt = model.GetMillis()
if strings.HasPrefix(post.Type, model.POST_SYSTEM_MESSAGE_PREFIX) {
err := model.NewAppError("CreateCommandPost", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "post.type"}, "", http.StatusBadRequest)
return nil, err
}
if response.Attachments != nil {
parseSlackAttachment(post, response.Attachments)
}
if response.ResponseType == model.COMMAND_RESPONSE_TYPE_IN_CHANNEL {
return a.CreatePostMissingChannel(post, true)
} else if (response.ResponseType == "" || response.ResponseType == model.COMMAND_RESPONSE_TYPE_EPHEMERAL) && (response.Text != "" || response.Attachments != nil) {
post.ParentId = ""
a.SendEphemeralPost(post.UserId, post)
}
return post, nil
}
// previous ListCommands now ListAutocompleteCommands
func (a *App) ListAutocompleteCommands(teamId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) {
commands := make([]*model.Command, 0, 32)
seen := make(map[string]bool)
for _, value := range commandProviders {
if cmd := value.GetCommand(a, T); cmd != nil {
cpy := *cmd
if cpy.AutoComplete && !seen[cpy.Id] {
cpy.Sanitize()
seen[cpy.Trigger] = true
commands = append(commands, &cpy)
}
}
}
for _, cmd := range a.PluginCommandsForTeam(teamId) {
if cmd.AutoComplete && !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
if *a.Config().ServiceSettings.EnableCommands {
if result := <-a.Srv.Store.Command().GetByTeam(teamId); result.Err != nil {
return nil, result.Err
} else {
teamCmds := result.Data.([]*model.Command)
for _, cmd := range teamCmds {
if cmd.AutoComplete && !seen[cmd.Id] {
cmd.Sanitize()
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
}
}
return commands, nil
}
func (a *App) ListTeamCommands(teamId string) ([]*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if result := <-a.Srv.Store.Command().GetByTeam(teamId); result.Err != nil {
return nil, result.Err
} else {
return result.Data.([]*model.Command), nil
}
}
func (a *App) ListAllCommands(teamId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) {
commands := make([]*model.Command, 0, 32)
seen := make(map[string]bool)
for _, value := range commandProviders {
if cmd := value.GetCommand(a, T); cmd != nil {
cpy := *cmd
if cpy.AutoComplete && !seen[cpy.Trigger] {
cpy.Sanitize()
seen[cpy.Trigger] = true
commands = append(commands, &cpy)
}
}
}
for _, cmd := range a.PluginCommandsForTeam(teamId) {
if !seen[cmd.Trigger] {
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
if *a.Config().ServiceSettings.EnableCommands {
if result := <-a.Srv.Store.Command().GetByTeam(teamId); result.Err != nil {
return nil, result.Err
} else {
teamCmds := result.Data.([]*model.Command)
for _, cmd := range teamCmds {
if !seen[cmd.Trigger] {
cmd.Sanitize()
seen[cmd.Trigger] = true
commands = append(commands, cmd)
}
}
}
}
return commands, nil
}
func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
parts := strings.Split(args.Command, " ")
trigger := parts[0][1:]
trigger = strings.ToLower(trigger)
message := strings.Join(parts[1:], " ")
provider := GetCommandProvider(trigger)
if provider != nil {
if cmd := provider.GetCommand(a, args.T); cmd != nil {
response := provider.DoCommand(a, args, message)
return a.HandleCommandResponse(cmd, args, response, true)
}
}
if cmd, response, err := a.ExecutePluginCommand(args); err != nil {
return nil, err
} else if cmd != nil {
return a.HandleCommandResponse(cmd, args, response, true)
}
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
chanChan := a.Srv.Store.Channel().Get(args.ChannelId, true)
teamChan := a.Srv.Store.Team().Get(args.TeamId)
userChan := a.Srv.Store.User().Get(args.UserId)
if result := <-a.Srv.Store.Command().GetByTeam(args.TeamId); result.Err != nil {
return nil, result.Err
} else {
var team *model.Team
if tr := <-teamChan; tr.Err != nil {
return nil, tr.Err
} else {
team = tr.Data.(*model.Team)
}
var user *model.User
if ur := <-userChan; ur.Err != nil {
return nil, ur.Err
} else {
user = ur.Data.(*model.User)
}
var channel *model.Channel
if cr := <-chanChan; cr.Err != nil {
return nil, cr.Err
} else {
channel = cr.Data.(*model.Channel)
}
teamCmds := result.Data.([]*model.Command)
for _, cmd := range teamCmds {
if trigger == cmd.Trigger {
l4g.Debug(fmt.Sprintf(utils.T("api.command.execute_command.debug"), trigger, args.UserId))
p := url.Values{}
p.Set("token", cmd.Token)
p.Set("team_id", cmd.TeamId)
p.Set("team_domain", team.Name)
p.Set("channel_id", args.ChannelId)
p.Set("channel_name", channel.Name)
p.Set("user_id", args.UserId)
p.Set("user_name", user.Username)
p.Set("command", "/"+trigger)
p.Set("text", message)
if hook, err := a.CreateCommandWebhook(cmd.Id, args); err != nil {
return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error(), http.StatusInternalServerError)
} else {
p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
}
method := "POST"
if cmd.Method == model.COMMAND_METHOD_GET {
method = "GET"
}
req, _ := http.NewRequest(method, cmd.URL, strings.NewReader(p.Encode()))
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Token "+cmd.Token)
if cmd.Method == model.COMMAND_METHOD_POST {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
if resp, err := a.HTTPClient(false).Do(req); err != nil {
return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error(), http.StatusInternalServerError)
} else {
if resp.StatusCode == http.StatusOK {
if response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), resp.Body); err != nil {
return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error(), http.StatusInternalServerError)
} else if response == nil {
return nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusInternalServerError)
} else {
return a.HandleCommandResponse(cmd, args, response, false)
}
} else {
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
return nil, model.NewAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]interface{}{"Trigger": trigger, "Status": resp.Status}, string(body), http.StatusInternalServerError)
}
}
}
}
}
return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusNotFound)
}
func (a *App) HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
post := &model.Post{}
post.ChannelId = args.ChannelId
post.RootId = args.RootId
post.ParentId = args.ParentId
post.UserId = args.UserId
post.Type = response.Type
post.Props = response.Props
isBotPost := !builtIn
if a.Config().ServiceSettings.EnablePostUsernameOverride {
if len(command.Username) != 0 {
post.AddProp("override_username", command.Username)
isBotPost = true
} else if len(response.Username) != 0 {
post.AddProp("override_username", response.Username)
isBotPost = true
}
}
if a.Config().ServiceSettings.EnablePostIconOverride {
if len(command.IconURL) != 0 {
post.AddProp("override_icon_url", command.IconURL)
isBotPost = true
} else if len(response.IconURL) != 0 {
post.AddProp("override_icon_url", response.IconURL)
isBotPost = true
} else {
post.AddProp("override_icon_url", "")
}
}
if isBotPost {
post.AddProp("from_webhook", "true")
}
// Process Slack text replacements
response.Text = a.ProcessSlackText(response.Text)
response.Attachments = a.ProcessSlackAttachments(response.Attachments)
if _, err := a.CreateCommandPost(post, args.TeamId, response); err != nil {
l4g.Error(err.Error())
}
return response, nil
}
func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
cmd.Trigger = strings.ToLower(cmd.Trigger)
if result := <-a.Srv.Store.Command().GetByTeam(cmd.TeamId); result.Err != nil {
return nil, result.Err
} else {
teamCmds := result.Data.([]*model.Command)
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)
}
}
}
if result := <-a.Srv.Store.Command().Save(cmd); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.Command), nil
}
}
func (a *App) GetCommand(commandId string) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if result := <-a.Srv.Store.Command().Get(commandId); result.Err != nil {
result.Err.StatusCode = http.StatusNotFound
return nil, result.Err
} else {
return result.Data.(*model.Command), nil
}
}
func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
updatedCmd.Id = oldCmd.Id
updatedCmd.Token = oldCmd.Token
updatedCmd.CreateAt = oldCmd.CreateAt
updatedCmd.UpdateAt = model.GetMillis()
updatedCmd.DeleteAt = oldCmd.DeleteAt
updatedCmd.CreatorId = oldCmd.CreatorId
updatedCmd.TeamId = oldCmd.TeamId
if result := <-a.Srv.Store.Command().Update(updatedCmd); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.Command), nil
}
}
func (a *App) MoveCommand(team *model.Team, command *model.Command) *model.AppError {
command.TeamId = team.Id
if result := <-a.Srv.Store.Command().Update(command); result.Err != nil {
return result.Err
}
return nil
}
func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCommands {
return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
cmd.Token = model.NewId()
if result := <-a.Srv.Store.Command().Update(cmd); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.Command), nil
}
}
func (a *App) DeleteCommand(commandId string) *model.AppError {
if !*a.Config().ServiceSettings.EnableCommands {
return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if err := (<-a.Srv.Store.Command().Delete(commandId, model.GetMillis())).Err; err != nil {
return err
}
return nil
}