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 }