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 }