github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/app/command.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  	"sync"
    13  	"unicode"
    14  
    15  	goi18n "github.com/mattermost/go-i18n/i18n"
    16  	"github.com/vnforks/kid/v5/mlog"
    17  	"github.com/vnforks/kid/v5/model"
    18  	"github.com/vnforks/kid/v5/store"
    19  	"github.com/vnforks/kid/v5/utils"
    20  )
    21  
    22  type CommandProvider interface {
    23  	GetTrigger() string
    24  	GetCommand(a *App, T goi18n.TranslateFunc) *model.Command
    25  	DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse
    26  }
    27  
    28  var commandProviders = make(map[string]CommandProvider)
    29  
    30  func RegisterCommandProvider(newProvider CommandProvider) {
    31  	commandProviders[newProvider.GetTrigger()] = newProvider
    32  }
    33  
    34  func GetCommandProvider(name string) CommandProvider {
    35  	provider, ok := commandProviders[name]
    36  	if ok {
    37  		return provider
    38  	}
    39  
    40  	return nil
    41  }
    42  
    43  // @openTracingParams branchId
    44  // previous ListCommands now ListAutocompleteCommands
    45  func (a *App) ListAutocompleteCommands(branchId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) {
    46  	commands := make([]*model.Command, 0, 32)
    47  	seen := make(map[string]bool)
    48  	for _, value := range commandProviders {
    49  		if cmd := value.GetCommand(a, T); cmd != nil {
    50  			cpy := *cmd
    51  			if cpy.AutoComplete && !seen[cpy.Id] {
    52  				cpy.Sanitize()
    53  				seen[cpy.Trigger] = true
    54  				commands = append(commands, &cpy)
    55  			}
    56  		}
    57  	}
    58  
    59  	if *a.Config().ServiceSettings.EnableCommands {
    60  		branchCmds, err := a.Srv().Store.Command().GetByBranch(branchId)
    61  		if err != nil {
    62  			return nil, err
    63  		}
    64  
    65  		for _, cmd := range branchCmds {
    66  			if cmd.AutoComplete && !seen[cmd.Id] {
    67  				cmd.Sanitize()
    68  				seen[cmd.Trigger] = true
    69  				commands = append(commands, cmd)
    70  			}
    71  		}
    72  	}
    73  
    74  	return commands, nil
    75  }
    76  
    77  func (a *App) ListBranchCommands(branchId string) ([]*model.Command, *model.AppError) {
    78  	if !*a.Config().ServiceSettings.EnableCommands {
    79  		return nil, model.NewAppError("ListBranchCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
    80  	}
    81  
    82  	return a.Srv().Store.Command().GetByBranch(branchId)
    83  }
    84  
    85  func (a *App) ListAllCommands(branchId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) {
    86  	commands := make([]*model.Command, 0, 32)
    87  	seen := make(map[string]bool)
    88  	for _, value := range commandProviders {
    89  		if cmd := value.GetCommand(a, T); cmd != nil {
    90  			cpy := *cmd
    91  			if cpy.AutoComplete && !seen[cpy.Trigger] {
    92  				cpy.Sanitize()
    93  				seen[cpy.Trigger] = true
    94  				commands = append(commands, &cpy)
    95  			}
    96  		}
    97  	}
    98  
    99  	if *a.Config().ServiceSettings.EnableCommands {
   100  		branchCmds, err := a.Srv().Store.Command().GetByBranch(branchId)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		for _, cmd := range branchCmds {
   105  			if !seen[cmd.Trigger] {
   106  				cmd.Sanitize()
   107  				seen[cmd.Trigger] = true
   108  				commands = append(commands, cmd)
   109  			}
   110  		}
   111  	}
   112  
   113  	return commands, nil
   114  }
   115  
   116  // @openTracingParams args
   117  func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
   118  	trigger := ""
   119  	message := ""
   120  	index := strings.IndexFunc(args.Command, unicode.IsSpace)
   121  	if index != -1 {
   122  		trigger = args.Command[:index]
   123  		message = args.Command[index+1:]
   124  	} else {
   125  		trigger = args.Command
   126  	}
   127  	trigger = strings.ToLower(trigger)
   128  	if !strings.HasPrefix(trigger, "/") {
   129  		return nil, model.NewAppError("command", "api.command.execute_command.format.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusBadRequest)
   130  	}
   131  	trigger = strings.TrimPrefix(trigger, "/")
   132  
   133  	clientTriggerId, triggerId, appErr := model.GenerateTriggerId(args.UserId, a.AsymmetricSigningKey())
   134  	if appErr != nil {
   135  		mlog.Error("error occurred in generating trigger Id for a user ", mlog.Err(appErr))
   136  	}
   137  
   138  	args.TriggerId = triggerId
   139  
   140  	cmd, response := a.tryExecuteBuiltInCommand(args, trigger, message)
   141  	if cmd != nil && response != nil {
   142  		return a.HandleCommandResponse(cmd, args, response, true)
   143  	}
   144  
   145  	if cmd != nil && response != nil {
   146  		response.TriggerId = clientTriggerId
   147  		return a.HandleCommandResponse(cmd, args, response, true)
   148  	}
   149  
   150  	cmd, response, appErr = a.tryExecuteCustomCommand(args, trigger, message)
   151  	if appErr != nil {
   152  		return nil, appErr
   153  	} else if cmd != nil && response != nil {
   154  		response.TriggerId = clientTriggerId
   155  		return a.HandleCommandResponse(cmd, args, response, false)
   156  	}
   157  
   158  	return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusNotFound)
   159  }
   160  
   161  // mentionsToBranchMembers returns all the @ mentions found in message that
   162  // belong to users in the specified branch, linking them to their users
   163  func (a *App) mentionsToBranchMembers(message, branchId string) model.UserMentionMap {
   164  	type mentionMapItem struct {
   165  		Name string
   166  		Id   string
   167  	}
   168  
   169  	possibleMentions := model.PossibleAtMentions(message)
   170  	mentionChan := make(chan *mentionMapItem, len(possibleMentions))
   171  
   172  	var wg sync.WaitGroup
   173  	for _, mention := range possibleMentions {
   174  		wg.Add(1)
   175  		go func(mention string) {
   176  			defer wg.Done()
   177  			user, err := a.Srv().Store.User().GetByUsername(mention)
   178  
   179  			if err != nil && err.StatusCode != http.StatusNotFound {
   180  				mlog.Warn("Failed to retrieve user @"+mention, mlog.Err(err))
   181  				return
   182  			}
   183  
   184  			// If it's a http.StatusNotFound error, check for usernames in substrings
   185  			// without trailing punctuation
   186  			if err != nil {
   187  				trimmed, ok := model.TrimUsernameSpecialChar(mention)
   188  				for ; ok; trimmed, ok = model.TrimUsernameSpecialChar(trimmed) {
   189  					userFromTrimmed, userErr := a.Srv().Store.User().GetByUsername(trimmed)
   190  					if userErr != nil && err.StatusCode != http.StatusNotFound {
   191  						return
   192  					}
   193  
   194  					if userErr != nil {
   195  						continue
   196  					}
   197  
   198  					_, err = a.GetBranchMember(branchId, userFromTrimmed.Id)
   199  					if err != nil {
   200  						// The user is not in the branch, so we should ignore it
   201  						return
   202  					}
   203  
   204  					mentionChan <- &mentionMapItem{trimmed, userFromTrimmed.Id}
   205  					return
   206  				}
   207  
   208  				return
   209  			}
   210  
   211  			_, err = a.GetBranchMember(branchId, user.Id)
   212  			if err != nil {
   213  				// The user is not in the branch, so we should ignore it
   214  				return
   215  			}
   216  
   217  			mentionChan <- &mentionMapItem{mention, user.Id}
   218  		}(mention)
   219  	}
   220  
   221  	wg.Wait()
   222  	close(mentionChan)
   223  
   224  	atMentionMap := make(model.UserMentionMap)
   225  	for mention := range mentionChan {
   226  		atMentionMap[mention.Name] = mention.Id
   227  	}
   228  
   229  	return atMentionMap
   230  }
   231  
   232  // tryExecuteBuiltInCommand attempts to run a built in command based on the given arguments. If no such command can be
   233  // found, returns nil for all arguments.
   234  func (a *App) tryExecuteBuiltInCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) {
   235  	provider := GetCommandProvider(trigger)
   236  	if provider == nil {
   237  		return nil, nil
   238  	}
   239  
   240  	cmd := provider.GetCommand(a, args.T)
   241  	if cmd == nil {
   242  		return nil, nil
   243  	}
   244  
   245  	return cmd, provider.DoCommand(a, args, message)
   246  }
   247  
   248  // tryExecuteCustomCommand attempts to run a custom command based on the given arguments. If no such command can be
   249  // found, returns nil for all arguments.
   250  func (a *App) tryExecuteCustomCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse, *model.AppError) {
   251  	// Handle custom commands
   252  	if !*a.Config().ServiceSettings.EnableCommands {
   253  		return nil, nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   254  	}
   255  
   256  	chanChan := make(chan store.StoreResult, 1)
   257  	go func() {
   258  		class, err := a.Srv().Store.Class().Get(args.ClassId, true)
   259  		chanChan <- store.StoreResult{Data: class, Err: err}
   260  		close(chanChan)
   261  	}()
   262  
   263  	branchChan := make(chan store.StoreResult, 1)
   264  	go func() {
   265  		branch, err := a.Srv().Store.Branch().Get(args.BranchId)
   266  		branchChan <- store.StoreResult{Data: branch, Err: err}
   267  		close(branchChan)
   268  	}()
   269  
   270  	userChan := make(chan store.StoreResult, 1)
   271  	go func() {
   272  		user, err := a.Srv().Store.User().Get(args.UserId)
   273  		userChan <- store.StoreResult{Data: user, Err: err}
   274  		close(userChan)
   275  	}()
   276  
   277  	branchCmds, err := a.Srv().Store.Command().GetByBranch(args.BranchId)
   278  	if err != nil {
   279  		return nil, nil, err
   280  	}
   281  
   282  	tr := <-branchChan
   283  	if tr.Err != nil {
   284  		return nil, nil, tr.Err
   285  	}
   286  	branch := tr.Data.(*model.Branch)
   287  
   288  	ur := <-userChan
   289  	if ur.Err != nil {
   290  		return nil, nil, ur.Err
   291  	}
   292  	user := ur.Data.(*model.User)
   293  
   294  	cr := <-chanChan
   295  	if cr.Err != nil {
   296  		return nil, nil, cr.Err
   297  	}
   298  	class := cr.Data.(*model.Class)
   299  
   300  	var cmd *model.Command
   301  
   302  	for _, branchCmd := range branchCmds {
   303  		if trigger == branchCmd.Trigger {
   304  			cmd = branchCmd
   305  		}
   306  	}
   307  
   308  	if cmd == nil {
   309  		return nil, nil, nil
   310  	}
   311  
   312  	mlog.Debug("Executing command", mlog.String("command", trigger), mlog.String("user_id", args.UserId))
   313  
   314  	p := url.Values{}
   315  	p.Set("token", cmd.Token)
   316  
   317  	p.Set("branch_id", cmd.BranchId)
   318  	p.Set("branch_domain", branch.Name)
   319  
   320  	p.Set("class_id", args.ClassId)
   321  	p.Set("class_name", class.Name)
   322  
   323  	p.Set("user_id", args.UserId)
   324  	p.Set("user_name", user.Username)
   325  
   326  	p.Set("command", "/"+trigger)
   327  	p.Set("text", message)
   328  
   329  	p.Set("trigger_id", args.TriggerId)
   330  
   331  	userMentionMap := a.mentionsToBranchMembers(message, branch.Id)
   332  	for key, values := range userMentionMap.ToURLValues() {
   333  		p[key] = values
   334  	}
   335  
   336  	return a.doCommandRequest(cmd, p)
   337  }
   338  
   339  func (a *App) doCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
   340  	// Prepare the request
   341  	var req *http.Request
   342  	var err error
   343  	if cmd.Method == model.COMMAND_METHOD_GET {
   344  		req, err = http.NewRequest(http.MethodGet, cmd.URL, nil)
   345  	} else {
   346  		req, err = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
   347  	}
   348  
   349  	if err != nil {
   350  		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError)
   351  	}
   352  
   353  	if cmd.Method == model.COMMAND_METHOD_GET {
   354  		if req.URL.RawQuery != "" {
   355  			req.URL.RawQuery += "&"
   356  		}
   357  		req.URL.RawQuery += p.Encode()
   358  	}
   359  
   360  	req.Header.Set("Accept", "application/json")
   361  	req.Header.Set("Authorization", "Token "+cmd.Token)
   362  	if cmd.Method == model.COMMAND_METHOD_POST {
   363  		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   364  	}
   365  
   366  	// Send the request
   367  	resp, err := a.HTTPService().MakeClient(false).Do(req)
   368  	if err != nil {
   369  		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError)
   370  	}
   371  
   372  	defer resp.Body.Close()
   373  
   374  	// Handle the response
   375  	body := io.LimitReader(resp.Body, 1024*1024)
   376  
   377  	if resp.StatusCode != http.StatusOK {
   378  		// Ignore the error below because the resulting string will just be the empty string if bodyBytes is nil
   379  		bodyBytes, _ := ioutil.ReadAll(body)
   380  
   381  		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)
   382  	}
   383  
   384  	response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), body)
   385  	if err != nil {
   386  		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, err.Error(), http.StatusInternalServerError)
   387  	} else if response == nil {
   388  		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError)
   389  	}
   390  
   391  	return cmd, response, nil
   392  }
   393  
   394  func (a *App) HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) {
   395  	trigger := ""
   396  	if len(args.Command) != 0 {
   397  		parts := strings.Split(args.Command, " ")
   398  		trigger = parts[0][1:]
   399  		trigger = strings.ToLower(trigger)
   400  	}
   401  
   402  	return response, nil
   403  }
   404  
   405  func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) {
   406  	if !*a.Config().ServiceSettings.EnableCommands {
   407  		return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   408  	}
   409  
   410  	cmd.Trigger = strings.ToLower(cmd.Trigger)
   411  
   412  	branchCmds, err := a.Srv().Store.Command().GetByBranch(cmd.BranchId)
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	for _, existingCommand := range branchCmds {
   418  		if cmd.Trigger == existingCommand.Trigger {
   419  			return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
   420  		}
   421  	}
   422  
   423  	for _, builtInProvider := range commandProviders {
   424  		builtInCommand := builtInProvider.GetCommand(a, utils.T)
   425  		if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger {
   426  			return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest)
   427  		}
   428  	}
   429  
   430  	return a.Srv().Store.Command().Save(cmd)
   431  }
   432  
   433  func (a *App) GetCommand(commandId string) (*model.Command, *model.AppError) {
   434  	if !*a.Config().ServiceSettings.EnableCommands {
   435  		return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   436  	}
   437  
   438  	cmd, err := a.Srv().Store.Command().Get(commandId)
   439  	if err != nil {
   440  		err.StatusCode = http.StatusNotFound
   441  		return nil, err
   442  	}
   443  
   444  	return cmd, nil
   445  }
   446  
   447  func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) {
   448  	if !*a.Config().ServiceSettings.EnableCommands {
   449  		return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   450  	}
   451  
   452  	updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger)
   453  	updatedCmd.Id = oldCmd.Id
   454  	updatedCmd.Token = oldCmd.Token
   455  	updatedCmd.CreateAt = oldCmd.CreateAt
   456  	updatedCmd.UpdateAt = model.GetMillis()
   457  	updatedCmd.DeleteAt = oldCmd.DeleteAt
   458  	updatedCmd.CreatorId = oldCmd.CreatorId
   459  	updatedCmd.BranchId = oldCmd.BranchId
   460  
   461  	return a.Srv().Store.Command().Update(updatedCmd)
   462  }
   463  
   464  func (a *App) MoveCommand(branch *model.Branch, command *model.Command) *model.AppError {
   465  	command.BranchId = branch.Id
   466  
   467  	_, err := a.Srv().Store.Command().Update(command)
   468  	if err != nil {
   469  		return err
   470  	}
   471  
   472  	return nil
   473  }
   474  
   475  func (a *App) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {
   476  	if !*a.Config().ServiceSettings.EnableCommands {
   477  		return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   478  	}
   479  
   480  	cmd.Token = model.NewId()
   481  
   482  	return a.Srv().Store.Command().Update(cmd)
   483  }
   484  
   485  func (a *App) DeleteCommand(commandId string) *model.AppError {
   486  	if !*a.Config().ServiceSettings.EnableCommands {
   487  		return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
   488  	}
   489  
   490  	return a.Srv().Store.Command().Delete(commandId, model.GetMillis())
   491  }