github.com/ashishbhate/mattermost-server@v5.11.1+incompatible/app/command.go (about)

     1  // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  
    14  	"github.com/mattermost/mattermost-server/mlog"
    15  	"github.com/mattermost/mattermost-server/model"
    16  	"github.com/mattermost/mattermost-server/utils"
    17  	goi18n "github.com/nicksnyder/go-i18n/i18n"
    18  )
    19  
    20  type CommandProvider interface {
    21  	GetTrigger() string
    22  	GetCommand(a *App, T goi18n.TranslateFunc) *model.Command
    23  	DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse
    24  }
    25  
    26  var commandProviders = make(map[string]CommandProvider)
    27  
    28  func RegisterCommandProvider(newProvider CommandProvider) {
    29  	commandProviders[newProvider.GetTrigger()] = newProvider
    30  }
    31  
    32  func GetCommandProvider(name string) CommandProvider {
    33  	provider, ok := commandProviders[name]
    34  	if ok {
    35  		return provider
    36  	}
    37  
    38  	return nil
    39  }
    40  
    41  func (a *App) CreateCommandPost(post *model.Post, teamId string, response *model.CommandResponse) (*model.Post, *model.AppError) {
    42  	post.Message = model.ParseSlackLinksToMarkdown(response.Text)
    43  	post.CreateAt = model.GetMillis()
    44  
    45  	if strings.HasPrefix(post.Type, model.POST_SYSTEM_MESSAGE_PREFIX) {
    46  		err := model.NewAppError("CreateCommandPost", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "post.type"}, "", http.StatusBadRequest)
    47  		return nil, err
    48  	}
    49  
    50  	if response.Attachments != nil {
    51  		model.ParseSlackAttachment(post, response.Attachments)
    52  	}
    53  
    54  	if response.ResponseType == model.COMMAND_RESPONSE_TYPE_IN_CHANNEL {
    55  		return a.CreatePostMissingChannel(post, true)
    56  	}
    57  
    58  	if (response.ResponseType == "" || response.ResponseType == model.COMMAND_RESPONSE_TYPE_EPHEMERAL) && (response.Text != "" || response.Attachments != nil) {
    59  		post.ParentId = ""
    60  		a.SendEphemeralPost(post.UserId, post)
    61  	}
    62  
    63  	return post, nil
    64  }
    65  
    66  // previous ListCommands now ListAutocompleteCommands
    67  func (a *App) ListAutocompleteCommands(teamId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) {
    68  	commands := make([]*model.Command, 0, 32)
    69  	seen := make(map[string]bool)
    70  	for _, value := range commandProviders {
    71  		if cmd := value.GetCommand(a, T); cmd != nil {
    72  			cpy := *cmd
    73  			if cpy.AutoComplete && !seen[cpy.Id] {
    74  				cpy.Sanitize()
    75  				seen[cpy.Trigger] = true
    76  				commands = append(commands, &cpy)
    77  			}
    78  		}
    79  	}
    80  
    81  	for _, cmd := range a.PluginCommandsForTeam(teamId) {
    82  		if cmd.AutoComplete && !seen[cmd.Trigger] {
    83  			seen[cmd.Trigger] = true
    84  			commands = append(commands, cmd)
    85  		}
    86  	}
    87  
    88  	if *a.Config().ServiceSettings.EnableCommands {
    89  		result := <-a.Srv.Store.Command().GetByTeam(teamId)
    90  		if result.Err != nil {
    91  			return nil, result.Err
    92  		}
    93  
    94  		teamCmds := result.Data.([]*model.Command)
    95  		for _, cmd := range teamCmds {
    96  			if cmd.AutoComplete && !seen[cmd.Id] {
    97  				cmd.Sanitize()
    98  				seen[cmd.Trigger] = true
    99  				commands = append(commands, cmd)
   100  			}
   101  		}
   102  	}
   103  
   104  	return commands, nil
   105  }
   106  
   107  func (a *App) ListTeamCommands(teamId string) ([]*model.Command, *model.AppError) {
   108  	if !*a.Config().ServiceSettings.EnableCommands {
   109  		return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   110  	}
   111  
   112  	result := <-a.Srv.Store.Command().GetByTeam(teamId)
   113  	if result.Err != nil {
   114  		return nil, result.Err
   115  	}
   116  
   117  	return result.Data.([]*model.Command), nil
   118  }
   119  
   120  func (a *App) ListAllCommands(teamId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) {
   121  	commands := make([]*model.Command, 0, 32)
   122  	seen := make(map[string]bool)
   123  	for _, value := range commandProviders {
   124  		if cmd := value.GetCommand(a, T); cmd != nil {
   125  			cpy := *cmd
   126  			if cpy.AutoComplete && !seen[cpy.Trigger] {
   127  				cpy.Sanitize()
   128  				seen[cpy.Trigger] = true
   129  				commands = append(commands, &cpy)
   130  			}
   131  		}
   132  	}
   133  
   134  	for _, cmd := range a.PluginCommandsForTeam(teamId) {
   135  		if !seen[cmd.Trigger] {
   136  			seen[cmd.Trigger] = true
   137  			commands = append(commands, cmd)
   138  		}
   139  	}
   140  
   141  	if *a.Config().ServiceSettings.EnableCommands {
   142  		result := <-a.Srv.Store.Command().GetByTeam(teamId)
   143  		if result.Err != nil {
   144  			return nil, result.Err
   145  		}
   146  		teamCmds := result.Data.([]*model.Command)
   147  		for _, cmd := range teamCmds {
   148  			if !seen[cmd.Trigger] {
   149  				cmd.Sanitize()
   150  				seen[cmd.Trigger] = true
   151  				commands = append(commands, cmd)
   152  			}
   153  		}
   154  	}
   155  
   156  	return commands, nil
   157  }
   158  
   159  func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
   160  	parts := strings.Split(args.Command, " ")
   161  	trigger := parts[0][1:]
   162  	trigger = strings.ToLower(trigger)
   163  	message := strings.Join(parts[1:], " ")
   164  
   165  	clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey())
   166  	if appErr != nil {
   167  		mlog.Error(appErr.Error())
   168  	}
   169  
   170  	args.TriggerId = triggerId
   171  
   172  	cmd, response := a.tryExecuteBuiltInCommand(args, trigger, message)
   173  	if cmd != nil && response != nil {
   174  		return a.HandleCommandResponse(cmd, args, response, true)
   175  	}
   176  
   177  	cmd, response, appErr = a.tryExecutePluginCommand(args)
   178  	if appErr != nil {
   179  		return nil, appErr
   180  	} else if cmd != nil && response != nil {
   181  		response.TriggerId = clientTriggerId
   182  		return a.HandleCommandResponse(cmd, args, response, true)
   183  	}
   184  
   185  	cmd, response, appErr = a.tryExecuteCustomCommand(args, trigger, message)
   186  	if appErr != nil {
   187  		return nil, appErr
   188  	} else if cmd != nil && response != nil {
   189  		response.TriggerId = clientTriggerId
   190  		return a.HandleCommandResponse(cmd, args, response, false)
   191  	}
   192  
   193  	return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusNotFound)
   194  }
   195  
   196  // tryExecutePluginCommand attempts to run a built in command based on the given arguments. If no such command can be
   197  // found, returns nil for all arguments.
   198  func (a *App) tryExecuteBuiltInCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) {
   199  	provider := GetCommandProvider(trigger)
   200  	if provider == nil {
   201  		return nil, nil
   202  	}
   203  
   204  	cmd := provider.GetCommand(a, args.T)
   205  	if cmd == nil {
   206  		return nil, nil
   207  	}
   208  
   209  	return cmd, provider.DoCommand(a, args, message)
   210  }
   211  
   212  // tryExecuteCustomCommand attempts to run a custom command based on the given arguments. If no such command can be
   213  // found, returns nil for all arguments.
   214  func (a *App) tryExecuteCustomCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse, *model.AppError) {
   215  	// Handle custom commands
   216  	if !*a.Config().ServiceSettings.EnableCommands {
   217  		return nil, nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   218  	}
   219  
   220  	chanChan := a.Srv.Store.Channel().Get(args.ChannelId, true)
   221  	teamChan := a.Srv.Store.Team().Get(args.TeamId)
   222  	userChan := a.Srv.Store.User().Get(args.UserId)
   223  
   224  	result := <-a.Srv.Store.Command().GetByTeam(args.TeamId)
   225  	if result.Err != nil {
   226  		return nil, nil, result.Err
   227  	}
   228  
   229  	tr := <-teamChan
   230  	if tr.Err != nil {
   231  		return nil, nil, tr.Err
   232  	}
   233  	team := tr.Data.(*model.Team)
   234  
   235  	ur := <-userChan
   236  	if ur.Err != nil {
   237  		return nil, nil, ur.Err
   238  	}
   239  	user := ur.Data.(*model.User)
   240  
   241  	cr := <-chanChan
   242  	if cr.Err != nil {
   243  		return nil, nil, cr.Err
   244  	}
   245  	channel := cr.Data.(*model.Channel)
   246  
   247  	var cmd *model.Command
   248  
   249  	teamCmds := result.Data.([]*model.Command)
   250  	for _, teamCmd := range teamCmds {
   251  		if trigger == teamCmd.Trigger {
   252  			cmd = teamCmd
   253  		}
   254  	}
   255  
   256  	if cmd == nil {
   257  		return nil, nil, nil
   258  	}
   259  
   260  	mlog.Debug(fmt.Sprintf(utils.T("api.command.execute_command.debug"), trigger, args.UserId))
   261  
   262  	p := url.Values{}
   263  	p.Set("token", cmd.Token)
   264  
   265  	p.Set("team_id", cmd.TeamId)
   266  	p.Set("team_domain", team.Name)
   267  
   268  	p.Set("channel_id", args.ChannelId)
   269  	p.Set("channel_name", channel.Name)
   270  
   271  	p.Set("user_id", args.UserId)
   272  	p.Set("user_name", user.Username)
   273  
   274  	p.Set("command", "/"+trigger)
   275  	p.Set("text", message)
   276  
   277  	p.Set("trigger_id", args.TriggerId)
   278  
   279  	hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
   280  	if appErr != nil {
   281  		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, appErr.Error(), http.StatusInternalServerError)
   282  	}
   283  	p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
   284  
   285  	return a.doCommandRequest(cmd, p)
   286  }
   287  
   288  func (a *App) doCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
   289  	// Prepare the request
   290  	var req *http.Request
   291  	var err error
   292  	if cmd.Method == model.COMMAND_METHOD_GET {
   293  		req, err = http.NewRequest(http.MethodGet, cmd.URL, nil)
   294  	} else {
   295  		req, err = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
   296  	}
   297  
   298  	if err != nil {
   299  		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError)
   300  	}
   301  
   302  	if cmd.Method == model.COMMAND_METHOD_GET {
   303  		if req.URL.RawQuery != "" {
   304  			req.URL.RawQuery += "&"
   305  		}
   306  		req.URL.RawQuery += p.Encode()
   307  	}
   308  
   309  	req.Header.Set("Accept", "application/json")
   310  	req.Header.Set("Authorization", "Token "+cmd.Token)
   311  	if cmd.Method == model.COMMAND_METHOD_POST {
   312  		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   313  	}
   314  
   315  	// Send the request
   316  	resp, err := a.HTTPService.MakeClient(false).Do(req)
   317  	if err != nil {
   318  		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError)
   319  	}
   320  
   321  	defer resp.Body.Close()
   322  
   323  	// Handle the response
   324  	body := io.LimitReader(resp.Body, MaxIntegrationResponseSize)
   325  
   326  	if resp.StatusCode != http.StatusOK {
   327  		// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
   328  		bodyBytes, _ := ioutil.ReadAll(body)
   329  
   330  		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)
   331  	}
   332  
   333  	response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), body)
   334  	if err != nil {
   335  		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError)
   336  	} else if response == nil {
   337  		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError)
   338  	}
   339  
   340  	return cmd, response, nil
   341  }
   342  
   343  func (a *App) HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
   344  	trigger := ""
   345  	if len(args.Command) != 0 {
   346  		parts := strings.Split(args.Command, " ")
   347  		trigger = parts[0][1:]
   348  		trigger = strings.ToLower(trigger)
   349  	}
   350  
   351  	var lastError *model.AppError
   352  	_, err := a.HandleCommandResponsePost(command, args, response, builtIn)
   353  
   354  	if err != nil {
   355  		mlog.Error(err.Error())
   356  		lastError = err
   357  	}
   358  
   359  	if response.ExtraResponses != nil {
   360  		for _, resp := range response.ExtraResponses {
   361  			_, err := a.HandleCommandResponsePost(command, args, resp, builtIn)
   362  
   363  			if err != nil {
   364  				mlog.Error(err.Error())
   365  				lastError = err
   366  			}
   367  		}
   368  	}
   369  
   370  	if lastError != nil {
   371  		return response, model.NewAppError("command", "api.command.execute_command.create_post_failed.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusInternalServerError)
   372  	}
   373  
   374  	return response, nil
   375  }
   376  
   377  func (a *App) HandleCommandResponsePost(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.Post, *model.AppError) {
   378  	post := &model.Post{}
   379  	post.ChannelId = args.ChannelId
   380  	post.RootId = args.RootId
   381  	post.ParentId = args.ParentId
   382  	post.UserId = args.UserId
   383  	post.Type = response.Type
   384  	post.Props = response.Props
   385  
   386  	if len(response.ChannelId) != 0 {
   387  		_, err := a.GetChannelMember(response.ChannelId, args.UserId)
   388  		if err != nil {
   389  			err = model.NewAppError("HandleCommandResponsePost", "api.command.command_post.forbidden.app_error", nil, err.Error(), http.StatusForbidden)
   390  			return nil, err
   391  		}
   392  		post.ChannelId = response.ChannelId
   393  	}
   394  
   395  	isBotPost := !builtIn
   396  
   397  	if *a.Config().ServiceSettings.EnablePostUsernameOverride {
   398  		if len(command.Username) != 0 {
   399  			post.AddProp("override_username", command.Username)
   400  			isBotPost = true
   401  		} else if len(response.Username) != 0 {
   402  			post.AddProp("override_username", response.Username)
   403  			isBotPost = true
   404  		}
   405  	}
   406  
   407  	if *a.Config().ServiceSettings.EnablePostIconOverride {
   408  		if len(command.IconURL) != 0 {
   409  			post.AddProp("override_icon_url", command.IconURL)
   410  			isBotPost = true
   411  		} else if len(response.IconURL) != 0 {
   412  			post.AddProp("override_icon_url", response.IconURL)
   413  			isBotPost = true
   414  		} else {
   415  			post.AddProp("override_icon_url", "")
   416  		}
   417  	}
   418  
   419  	if isBotPost {
   420  		post.AddProp("from_webhook", "true")
   421  	}
   422  
   423  	// Process Slack text replacements
   424  	response.Text = a.ProcessSlackText(response.Text)
   425  	response.Attachments = a.ProcessSlackAttachments(response.Attachments)
   426  
   427  	if _, err := a.CreateCommandPost(post, args.TeamId, response); err != nil {
   428  		return post, err
   429  	}
   430  
   431  	return post, nil
   432  }
   433  
   434  func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
   435  	if !*a.Config().ServiceSettings.EnableCommands {
   436  		return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   437  	}
   438  
   439  	cmd.Trigger = strings.ToLower(cmd.Trigger)
   440  
   441  	result := <-a.Srv.Store.Command().GetByTeam(cmd.TeamId)
   442  	if result.Err != nil {
   443  		return nil, result.Err
   444  	}
   445  
   446  	teamCmds := result.Data.([]*model.Command)
   447  	for _, existingCommand := range teamCmds {
   448  		if cmd.Trigger == existingCommand.Trigger {
   449  			return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
   450  		}
   451  	}
   452  
   453  	for _, builtInProvider := range commandProviders {
   454  		builtInCommand := builtInProvider.GetCommand(a, utils.T)
   455  		if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger {
   456  			return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
   457  		}
   458  	}
   459  
   460  	result = <-a.Srv.Store.Command().Save(cmd)
   461  	if result.Err != nil {
   462  		return nil, result.Err
   463  	}
   464  
   465  	return result.Data.(*model.Command), nil
   466  }
   467  
   468  func (a *App) GetCommand(commandId string) (*model.Command, *model.AppError) {
   469  	if !*a.Config().ServiceSettings.EnableCommands {
   470  		return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   471  	}
   472  
   473  	result := <-a.Srv.Store.Command().Get(commandId)
   474  	if result.Err != nil {
   475  		result.Err.StatusCode = http.StatusNotFound
   476  		return nil, result.Err
   477  	}
   478  
   479  	return result.Data.(*model.Command), nil
   480  }
   481  
   482  func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) {
   483  	if !*a.Config().ServiceSettings.EnableCommands {
   484  		return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   485  	}
   486  
   487  	updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
   488  	updatedCmd.Id = oldCmd.Id
   489  	updatedCmd.Token = oldCmd.Token
   490  	updatedCmd.CreateAt = oldCmd.CreateAt
   491  	updatedCmd.UpdateAt = model.GetMillis()
   492  	updatedCmd.DeleteAt = oldCmd.DeleteAt
   493  	updatedCmd.CreatorId = oldCmd.CreatorId
   494  	updatedCmd.TeamId = oldCmd.TeamId
   495  
   496  	result := <-a.Srv.Store.Command().Update(updatedCmd)
   497  	if result.Err != nil {
   498  		return nil, result.Err
   499  	}
   500  	return result.Data.(*model.Command), nil
   501  }
   502  
   503  func (a *App) MoveCommand(team *model.Team, command *model.Command) *model.AppError {
   504  	command.TeamId = team.Id
   505  
   506  	result := <-a.Srv.Store.Command().Update(command)
   507  	if result.Err != nil {
   508  		return result.Err
   509  	}
   510  
   511  	return nil
   512  }
   513  
   514  func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
   515  	if !*a.Config().ServiceSettings.EnableCommands {
   516  		return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   517  	}
   518  
   519  	cmd.Token = model.NewId()
   520  
   521  	result := <-a.Srv.Store.Command().Update(cmd)
   522  	if result.Err != nil {
   523  		return nil, result.Err
   524  	}
   525  
   526  	return result.Data.(*model.Command), nil
   527  }
   528  
   529  func (a *App) DeleteCommand(commandId string) *model.AppError {
   530  	if !*a.Config().ServiceSettings.EnableCommands {
   531  		return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   532  	}
   533  	result := <-a.Srv.Store.Command().Delete(commandId, model.GetMillis())
   534  	if result.Err != nil {
   535  		return result.Err
   536  	}
   537  	return nil
   538  }