github.com/haalcala/mattermost-server-change-repo@v0.0.0-20210713015153-16753fbeee5f/app/command_autocomplete.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  	"errors"
     8  	"fmt"
     9  	"net/url"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/mattermost/mattermost-server/v5/mlog"
    14  	"github.com/mattermost/mattermost-server/v5/model"
    15  )
    16  
    17  // AutocompleteDynamicArgProvider dynamically provides auto-completion args for built-in commands.
    18  type AutocompleteDynamicArgProvider interface {
    19  	GetAutoCompleteListItems(a *App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error)
    20  }
    21  
    22  // GetSuggestions returns suggestions for user input.
    23  func (a *App) GetSuggestions(commandArgs *model.CommandArgs, commands []*model.Command, roleID string) []model.AutocompleteSuggestion {
    24  	sort.Slice(commands, func(i, j int) bool {
    25  		return strings.Compare(strings.ToLower(commands[i].Trigger), strings.ToLower(commands[j].Trigger)) < 0
    26  	})
    27  
    28  	autocompleteData := []*model.AutocompleteData{}
    29  	for _, command := range commands {
    30  		if command.AutocompleteData == nil {
    31  			command.AutocompleteData = model.NewAutocompleteData(command.Trigger, command.AutoCompleteHint, command.AutoCompleteDesc)
    32  		}
    33  		autocompleteData = append(autocompleteData, command.AutocompleteData)
    34  	}
    35  
    36  	userInput := commandArgs.Command
    37  	suggestions := a.getSuggestions(commandArgs, autocompleteData, "", userInput, roleID)
    38  	for i, suggestion := range suggestions {
    39  		for _, command := range commands {
    40  			if strings.HasPrefix(suggestion.Complete, command.Trigger) {
    41  				suggestions[i].IconData = command.AutocompleteIconData
    42  				break
    43  			}
    44  		}
    45  	}
    46  
    47  	return suggestions
    48  }
    49  
    50  func (a *App) getSuggestions(commandArgs *model.CommandArgs, commands []*model.AutocompleteData, inputParsed, inputToBeParsed, roleID string) []model.AutocompleteSuggestion {
    51  	suggestions := []model.AutocompleteSuggestion{}
    52  	index := strings.Index(inputToBeParsed, " ")
    53  
    54  	if index == -1 { // no space in input
    55  		for _, command := range commands {
    56  			if strings.HasPrefix(command.Trigger, strings.ToLower(inputToBeParsed)) && (command.RoleID == roleID || roleID == model.SYSTEM_ADMIN_ROLE_ID || roleID == "") {
    57  				s := model.AutocompleteSuggestion{
    58  					Complete:    inputParsed + command.Trigger,
    59  					Suggestion:  command.Trigger,
    60  					Description: command.HelpText,
    61  					Hint:        command.Hint,
    62  				}
    63  				suggestions = append(suggestions, s)
    64  			}
    65  		}
    66  		return suggestions
    67  	}
    68  
    69  	for _, command := range commands {
    70  		if command.Trigger != strings.ToLower(inputToBeParsed[:index]) {
    71  			continue
    72  		}
    73  		if roleID != "" && roleID != model.SYSTEM_ADMIN_ROLE_ID && roleID != command.RoleID {
    74  			continue
    75  		}
    76  		toBeParsed := inputToBeParsed[index+1:]
    77  		parsed := inputParsed + inputToBeParsed[:index+1]
    78  
    79  		if len(command.Arguments) == 0 {
    80  			// Seek recursively in subcommands
    81  			subSuggestions := a.getSuggestions(commandArgs, command.SubCommands, parsed, toBeParsed, roleID)
    82  			suggestions = append(suggestions, subSuggestions...)
    83  			continue
    84  		}
    85  
    86  		found, _, _, suggestion := a.parseArguments(commandArgs, command.Arguments, parsed, toBeParsed)
    87  		if found {
    88  			suggestions = append(suggestions, suggestion...)
    89  		}
    90  	}
    91  
    92  	return suggestions
    93  }
    94  
    95  func (a *App) parseArguments(commandArgs *model.CommandArgs, args []*model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
    96  	if len(args) == 0 {
    97  		return false, parsed, toBeParsed, suggestions
    98  	}
    99  
   100  	if args[0].Required {
   101  		found, changedParsed, changedToBeParsed, suggestion := a.parseArgument(commandArgs, args[0], parsed, toBeParsed)
   102  		if found {
   103  			suggestions = append(suggestions, suggestion...)
   104  			return true, changedParsed, changedToBeParsed, suggestions
   105  		}
   106  		return a.parseArguments(commandArgs, args[1:], changedParsed, changedToBeParsed)
   107  	}
   108  
   109  	// Handling optional arguments. Optional argument can be inputted or not,
   110  	// so we have to pase both cases recursively and output combined suggestions.
   111  	foundWithOptional, changedParsedWithOptional, changedToBeParsedWithOptional, suggestionsWithOptional := a.parseArgument(commandArgs, args[0], parsed, toBeParsed)
   112  	if foundWithOptional {
   113  		suggestions = append(suggestions, suggestionsWithOptional...)
   114  	} else {
   115  		foundWithOptionalRest, changedParsedWithOptionalRest, changedToBeParsedWithOptionalRest, suggestionsWithOptionalRest := a.parseArguments(commandArgs, args[1:], changedParsedWithOptional, changedToBeParsedWithOptional)
   116  		if foundWithOptionalRest {
   117  			suggestions = append(suggestions, suggestionsWithOptionalRest...)
   118  		}
   119  		foundWithOptional = foundWithOptionalRest
   120  		changedParsedWithOptional = changedParsedWithOptionalRest
   121  		changedToBeParsedWithOptional = changedToBeParsedWithOptionalRest
   122  	}
   123  
   124  	foundWithoutOptional, changedParsedWithoutOptional, changedToBeParsedWithoutOptional, suggestionsWithoutOptional := a.parseArguments(commandArgs, args[1:], parsed, toBeParsed)
   125  	if foundWithoutOptional {
   126  		suggestions = append(suggestions, suggestionsWithoutOptional...)
   127  	}
   128  
   129  	// if suggestions were found we can return them
   130  	if foundWithOptional || foundWithoutOptional {
   131  		return true, parsed + toBeParsed, "", suggestions
   132  	}
   133  
   134  	// no suggestions found yet, check if optional argument was inputted
   135  	if changedParsedWithOptional != parsed && changedToBeParsedWithOptional != toBeParsed {
   136  		return false, changedParsedWithOptional, changedToBeParsedWithOptional, suggestions
   137  	}
   138  
   139  	// no suggestions and optional argument was not inputted
   140  	return foundWithoutOptional, changedParsedWithoutOptional, changedToBeParsedWithoutOptional, suggestions
   141  }
   142  
   143  func (a *App) parseArgument(commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
   144  	if arg.Name != "" { //Parse the --name first
   145  		found, changedParsed, changedToBeParsed, suggestion := parseNamedArgument(arg, parsed, toBeParsed)
   146  		if found {
   147  			suggestions = append(suggestions, suggestion)
   148  			return true, changedParsed, changedToBeParsed, suggestions
   149  		}
   150  		if changedToBeParsed == "" {
   151  			return true, changedParsed, changedToBeParsed, suggestions
   152  		}
   153  		if changedToBeParsed == " " {
   154  			changedToBeParsed = ""
   155  		}
   156  		parsed = changedParsed
   157  		toBeParsed = changedToBeParsed
   158  	}
   159  
   160  	if arg.Type == model.AutocompleteArgTypeText {
   161  		found, changedParsed, changedToBeParsed, suggestion := parseInputTextArgument(arg, parsed, toBeParsed)
   162  		if found {
   163  			suggestions = append(suggestions, suggestion)
   164  			return true, changedParsed, changedToBeParsed, suggestions
   165  		}
   166  		parsed = changedParsed
   167  		toBeParsed = changedToBeParsed
   168  	} else if arg.Type == model.AutocompleteArgTypeStaticList {
   169  		found, changedParsed, changedToBeParsed, staticListSuggestions := parseStaticListArgument(arg, parsed, toBeParsed)
   170  		if found {
   171  			suggestions = append(suggestions, staticListSuggestions...)
   172  			return true, changedParsed, changedToBeParsed, suggestions
   173  		}
   174  		parsed = changedParsed
   175  		toBeParsed = changedToBeParsed
   176  	} else if arg.Type == model.AutocompleteArgTypeDynamicList {
   177  		found, changedParsed, changedToBeParsed, dynamicListSuggestions := a.getDynamicListArgument(commandArgs, arg, parsed, toBeParsed)
   178  		if found {
   179  			suggestions = append(suggestions, dynamicListSuggestions...)
   180  			return true, changedParsed, changedToBeParsed, suggestions
   181  		}
   182  		parsed = changedParsed
   183  		toBeParsed = changedToBeParsed
   184  	}
   185  
   186  	return false, parsed, toBeParsed, suggestions
   187  }
   188  
   189  func parseNamedArgument(arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestion model.AutocompleteSuggestion) {
   190  	in := strings.TrimPrefix(toBeParsed, " ")
   191  	namedArg := "--" + arg.Name
   192  	if in == "" { //The user has not started typing the argument.
   193  		return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed + namedArg + " ", Suggestion: namedArg, Hint: "", Description: arg.HelpText}
   194  	}
   195  	if strings.HasPrefix(strings.ToLower(namedArg), strings.ToLower(in)) {
   196  		return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed + namedArg[len(in):] + " ", Suggestion: namedArg, Hint: "", Description: arg.HelpText}
   197  	}
   198  
   199  	if !strings.HasPrefix(strings.ToLower(in), strings.ToLower(namedArg)+" ") {
   200  		return false, parsed + toBeParsed, "", model.AutocompleteSuggestion{}
   201  	}
   202  	if strings.ToLower(in) == strings.ToLower(namedArg)+" " {
   203  		return false, parsed + namedArg + " ", " ", model.AutocompleteSuggestion{}
   204  	}
   205  	return false, parsed + namedArg + " ", in[len(namedArg)+1:], model.AutocompleteSuggestion{}
   206  }
   207  
   208  func parseInputTextArgument(arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestion model.AutocompleteSuggestion) {
   209  	in := strings.TrimPrefix(toBeParsed, " ")
   210  	a := arg.Data.(*model.AutocompleteTextArg)
   211  	if in == "" { //The user has not started typing the argument.
   212  		return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed, Suggestion: "", Hint: a.Hint, Description: arg.HelpText}
   213  	}
   214  	if in[0] == '"' { //input with multiple words
   215  		indexOfSecondQuote := strings.Index(in[1:], `"`)
   216  		if indexOfSecondQuote == -1 { //typing of the multiple word argument is not finished
   217  			return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed, Suggestion: "", Hint: a.Hint, Description: arg.HelpText}
   218  		}
   219  		// this argument is typed already
   220  		offset := 2
   221  		if len(in) > indexOfSecondQuote+2 && in[indexOfSecondQuote+2] == ' ' {
   222  			offset++
   223  		}
   224  		return false, parsed + in[:indexOfSecondQuote+offset], in[indexOfSecondQuote+offset:], model.AutocompleteSuggestion{}
   225  	}
   226  	// input with a single word
   227  	index := strings.Index(in, " ")
   228  	if index == -1 { // typing of the single word argument is not finished
   229  		return true, parsed + toBeParsed, "", model.AutocompleteSuggestion{Complete: parsed + toBeParsed, Suggestion: "", Hint: a.Hint, Description: arg.HelpText}
   230  	}
   231  	// single word argument already typed
   232  	return false, parsed + in[:index+1], in[index+1:], model.AutocompleteSuggestion{}
   233  }
   234  
   235  func parseStaticListArgument(arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
   236  	a := arg.Data.(*model.AutocompleteStaticListArg)
   237  	return parseListItems(a.PossibleArguments, parsed, toBeParsed)
   238  }
   239  
   240  func (a *App) getDynamicListArgument(commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) (found bool, alreadyParsed string, yetToBeParsed string, suggestions []model.AutocompleteSuggestion) {
   241  	dynamicArg := arg.Data.(*model.AutocompleteDynamicListArg)
   242  
   243  	if strings.HasPrefix(dynamicArg.FetchURL, "builtin:") {
   244  		listItems, err := a.getBuiltinDynamicListArgument(commandArgs, arg, parsed, toBeParsed)
   245  		if err != nil {
   246  			a.Log().Error("Can't fetch dynamic list arguments for", mlog.String("url", dynamicArg.FetchURL), mlog.Err(err))
   247  			return false, parsed, toBeParsed, []model.AutocompleteSuggestion{}
   248  		}
   249  		return parseListItems(listItems, parsed, toBeParsed)
   250  	}
   251  
   252  	params := url.Values{}
   253  	params.Add("user_input", parsed+toBeParsed)
   254  	params.Add("parsed", parsed)
   255  
   256  	// Encode the information normally provided to a plugin slash command handler into the request parameters
   257  	// Encode PluginContext:
   258  	pluginContext := a.PluginContext()
   259  	params.Add("request_id", pluginContext.RequestId)
   260  	params.Add("session_id", pluginContext.SessionId)
   261  	params.Add("ip_address", pluginContext.IpAddress)
   262  	params.Add("accept_language", pluginContext.AcceptLanguage)
   263  	params.Add("user_agent", pluginContext.UserAgent)
   264  
   265  	// Encode CommandArgs:
   266  	params.Add("channel_id", commandArgs.ChannelId)
   267  	params.Add("team_id", commandArgs.TeamId)
   268  	params.Add("root_id", commandArgs.RootId)
   269  	params.Add("parent_id", commandArgs.ParentId)
   270  	params.Add("user_id", commandArgs.UserId)
   271  	params.Add("site_url", commandArgs.SiteURL)
   272  
   273  	resp, err := a.doPluginRequest("GET", dynamicArg.FetchURL, params, nil)
   274  
   275  	if err != nil {
   276  		a.Log().Error("Can't fetch dynamic list arguments for", mlog.String("url", dynamicArg.FetchURL), mlog.Err(err))
   277  		return false, parsed, toBeParsed, []model.AutocompleteSuggestion{}
   278  	}
   279  
   280  	listItems := model.AutocompleteStaticListItemsFromJSON(resp.Body)
   281  
   282  	return parseListItems(listItems, parsed, toBeParsed)
   283  }
   284  
   285  func parseListItems(items []model.AutocompleteListItem, parsed, toBeParsed string) (bool, string, string, []model.AutocompleteSuggestion) {
   286  	in := strings.TrimPrefix(toBeParsed, " ")
   287  	suggestions := []model.AutocompleteSuggestion{}
   288  	maxPrefix := ""
   289  	for _, arg := range items {
   290  		if strings.HasPrefix(strings.ToLower(in), strings.ToLower(arg.Item)+" ") && len(maxPrefix) < len(arg.Item)+1 {
   291  			maxPrefix = arg.Item + " "
   292  		}
   293  	}
   294  	if maxPrefix != "" { //typing of an argument finished
   295  		return false, parsed + in[:len(maxPrefix)], in[len(maxPrefix):], []model.AutocompleteSuggestion{}
   296  	}
   297  	// user has not finished typing static argument
   298  	for _, arg := range items {
   299  		if strings.HasPrefix(strings.ToLower(arg.Item), strings.ToLower(in)) {
   300  			suggestions = append(suggestions, model.AutocompleteSuggestion{Complete: parsed + arg.Item, Suggestion: arg.Item, Hint: arg.Hint, Description: arg.HelpText})
   301  		}
   302  	}
   303  	return true, parsed + toBeParsed, "", suggestions
   304  }
   305  
   306  func (a *App) getBuiltinDynamicListArgument(commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
   307  	dynamicArg := arg.Data.(*model.AutocompleteDynamicListArg)
   308  	arr := strings.Split(dynamicArg.FetchURL, ":")
   309  	if len(arr) < 2 {
   310  		return nil, errors.New("Dynamic list URL missing built-in command name")
   311  	}
   312  	cmdName := arr[1]
   313  
   314  	provider := GetCommandProvider(cmdName)
   315  	if provider == nil {
   316  		return nil, fmt.Errorf("No command provider for %s", cmdName)
   317  	}
   318  
   319  	dp, ok := provider.(AutocompleteDynamicArgProvider)
   320  	if !ok {
   321  		return nil, fmt.Errorf("Auto-completion not available for built-in command %s", cmdName)
   322  	}
   323  
   324  	return dp.GetAutoCompleteListItems(a, commandArgs, arg, parsed, toBeParsed)
   325  }