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