github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/model/command_autocomplete.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package model 5 6 import ( 7 "encoding/json" 8 "io" 9 "net/url" 10 "path" 11 "reflect" 12 "strings" 13 14 "github.com/pkg/errors" 15 ) 16 17 // AutocompleteArgType describes autocomplete argument type 18 type AutocompleteArgType string 19 20 // Argument types 21 const ( 22 AutocompleteArgTypeText AutocompleteArgType = "TextInput" 23 AutocompleteArgTypeStaticList AutocompleteArgType = "StaticList" 24 AutocompleteArgTypeDynamicList AutocompleteArgType = "DynamicList" 25 ) 26 27 // AutocompleteData describes slash command autocomplete information. 28 type AutocompleteData struct { 29 // Trigger of the command 30 Trigger string 31 // Hint of a command 32 Hint string 33 // Text displayed to the user to help with the autocomplete description 34 HelpText string 35 // Role of the user who should be able to see the autocomplete info of this command 36 RoleID string 37 // Arguments of the command. Arguments can be named or positional. 38 // If they are positional order in the list matters, if they are named order does not matter. 39 // All arguments should be either named or positional, no mixing allowed. 40 Arguments []*AutocompleteArg 41 // Subcommands of the command 42 SubCommands []*AutocompleteData 43 } 44 45 // AutocompleteArg describes an argument of the command. Arguments can be named or positional. 46 // If Name is empty string Argument is positional otherwise it is named argument. 47 // Named arguments are passed as --Name Argument_Value. 48 type AutocompleteArg struct { 49 // Name of the argument 50 Name string 51 // Text displayed to the user to help with the autocomplete 52 HelpText string 53 // Type of the argument 54 Type AutocompleteArgType 55 // Required determins if argument is optional or not. 56 Required bool 57 // Actual data of the argument (depends on the Type) 58 Data interface{} 59 } 60 61 // AutocompleteTextArg describes text user can input as an argument. 62 type AutocompleteTextArg struct { 63 // Hint of the input text 64 Hint string 65 // Regex pattern to match 66 Pattern string 67 } 68 69 // AutocompleteListItem describes an item in the AutocompleteStaticListArg. 70 type AutocompleteListItem struct { 71 Item string 72 Hint string 73 HelpText string 74 } 75 76 // AutocompleteStaticListArg is used to input one of the arguments from the list, 77 // for example [yes, no], [on, off], and so on. 78 type AutocompleteStaticListArg struct { 79 PossibleArguments []AutocompleteListItem 80 } 81 82 // AutocompleteDynamicListArg is used when user wants to download possible argument list from the URL. 83 type AutocompleteDynamicListArg struct { 84 FetchURL string 85 } 86 87 // AutocompleteSuggestion describes a single suggestion item sent to the front-end 88 // Example: for user input `/jira cre` - 89 // Complete might be `/jira create` 90 // Suggestion might be `create`, 91 // Hint might be `[issue text]`, 92 // Description might be `Create a new Issue` 93 type AutocompleteSuggestion struct { 94 // Complete describes completed suggestion 95 Complete string 96 // Suggestion describes what user might want to input next 97 Suggestion string 98 // Hint describes a hint about the suggested input 99 Hint string 100 // Description of the command or a suggestion 101 Description string 102 // IconData is base64 encoded svg image 103 IconData string 104 } 105 106 // NewAutocompleteData returns new Autocomplete data. 107 func NewAutocompleteData(trigger, hint, helpText string) *AutocompleteData { 108 return &AutocompleteData{ 109 Trigger: trigger, 110 Hint: hint, 111 HelpText: helpText, 112 RoleID: SYSTEM_USER_ROLE_ID, 113 Arguments: []*AutocompleteArg{}, 114 SubCommands: []*AutocompleteData{}, 115 } 116 } 117 118 // AddCommand adds a subcommand to the autocomplete data. 119 func (ad *AutocompleteData) AddCommand(command *AutocompleteData) { 120 ad.SubCommands = append(ad.SubCommands, command) 121 } 122 123 // AddTextArgument adds positional AutocompleteArgTypeText argument to the command. 124 func (ad *AutocompleteData) AddTextArgument(helpText, hint, pattern string) { 125 ad.AddNamedTextArgument("", helpText, hint, pattern, true) 126 } 127 128 // AddNamedTextArgument adds named AutocompleteArgTypeText argument to the command. 129 func (ad *AutocompleteData) AddNamedTextArgument(name, helpText, hint, pattern string, required bool) { 130 argument := AutocompleteArg{ 131 Name: name, 132 HelpText: helpText, 133 Type: AutocompleteArgTypeText, 134 Required: required, 135 Data: &AutocompleteTextArg{Hint: hint, Pattern: pattern}, 136 } 137 ad.Arguments = append(ad.Arguments, &argument) 138 } 139 140 // AddStaticListArgument adds positional AutocompleteArgTypeStaticList argument to the command. 141 func (ad *AutocompleteData) AddStaticListArgument(helpText string, required bool, items []AutocompleteListItem) { 142 ad.AddNamedStaticListArgument("", helpText, required, items) 143 } 144 145 // AddNamedStaticListArgument adds named AutocompleteArgTypeStaticList argument to the command. 146 func (ad *AutocompleteData) AddNamedStaticListArgument(name, helpText string, required bool, items []AutocompleteListItem) { 147 argument := AutocompleteArg{ 148 Name: name, 149 HelpText: helpText, 150 Type: AutocompleteArgTypeStaticList, 151 Required: required, 152 Data: &AutocompleteStaticListArg{PossibleArguments: items}, 153 } 154 ad.Arguments = append(ad.Arguments, &argument) 155 } 156 157 // AddDynamicListArgument adds positional AutocompleteArgTypeDynamicList argument to the command. 158 func (ad *AutocompleteData) AddDynamicListArgument(helpText, url string, required bool) { 159 ad.AddNamedDynamicListArgument("", helpText, url, required) 160 } 161 162 // AddNamedDynamicListArgument adds named AutocompleteArgTypeDynamicList argument to the command. 163 func (ad *AutocompleteData) AddNamedDynamicListArgument(name, helpText, url string, required bool) { 164 argument := AutocompleteArg{ 165 Name: name, 166 HelpText: helpText, 167 Type: AutocompleteArgTypeDynamicList, 168 Required: required, 169 Data: &AutocompleteDynamicListArg{FetchURL: url}, 170 } 171 ad.Arguments = append(ad.Arguments, &argument) 172 } 173 174 // Equals method checks if command is the same. 175 func (ad *AutocompleteData) Equals(command *AutocompleteData) bool { 176 if !(ad.Trigger == command.Trigger && ad.HelpText == command.HelpText && ad.RoleID == command.RoleID && ad.Hint == command.Hint) { 177 return false 178 } 179 if len(ad.Arguments) != len(command.Arguments) || len(ad.SubCommands) != len(command.SubCommands) { 180 return false 181 } 182 for i := range ad.Arguments { 183 if !ad.Arguments[i].Equals(command.Arguments[i]) { 184 return false 185 } 186 } 187 for i := range ad.SubCommands { 188 if !ad.SubCommands[i].Equals(command.SubCommands[i]) { 189 return false 190 } 191 } 192 return true 193 } 194 195 // UpdateRelativeURLsForPluginCommands method updates relative urls for plugin commands 196 func (ad *AutocompleteData) UpdateRelativeURLsForPluginCommands(baseURL *url.URL) error { 197 for _, arg := range ad.Arguments { 198 if arg.Type != AutocompleteArgTypeDynamicList { 199 continue 200 } 201 dynamicList, ok := arg.Data.(*AutocompleteDynamicListArg) 202 if !ok { 203 return errors.New("Not a proper DynamicList type argument") 204 } 205 dynamicListURL, err := url.Parse(dynamicList.FetchURL) 206 if err != nil { 207 return errors.Wrapf(err, "FetchURL is not a proper url") 208 } 209 if !dynamicListURL.IsAbs() { 210 absURL := &url.URL{} 211 *absURL = *baseURL 212 absURL.Path = path.Join(absURL.Path, dynamicList.FetchURL) 213 dynamicList.FetchURL = absURL.String() 214 } 215 216 } 217 for _, command := range ad.SubCommands { 218 err := command.UpdateRelativeURLsForPluginCommands(baseURL) 219 if err != nil { 220 return err 221 } 222 } 223 return nil 224 } 225 226 // IsValid method checks if autocomplete data is valid. 227 func (ad *AutocompleteData) IsValid() error { 228 if ad == nil { 229 return errors.New("No nil commands are allowed in AutocompleteData") 230 } 231 if ad.Trigger == "" { 232 return errors.New("An empty command name in the autocomplete data") 233 } 234 if strings.ToLower(ad.Trigger) != ad.Trigger { 235 return errors.New("Command should be lowercase") 236 } 237 roles := []string{SYSTEM_ADMIN_ROLE_ID, SYSTEM_USER_ROLE_ID, ""} 238 if stringNotInSlice(ad.RoleID, roles) { 239 return errors.New("Wrong role in the autocomplete data") 240 } 241 if len(ad.Arguments) > 0 && len(ad.SubCommands) > 0 { 242 return errors.New("Command can't have arguments and subcommands") 243 } 244 if len(ad.Arguments) > 0 { 245 namedArgumentIndex := -1 246 for i, arg := range ad.Arguments { 247 if arg.Name != "" { // it's a named argument 248 if namedArgumentIndex == -1 { // first named argument 249 namedArgumentIndex = i 250 } 251 } else { // it's a positional argument 252 if namedArgumentIndex != -1 { 253 return errors.New("Named argument should not be before positional argument") 254 } 255 } 256 if arg.Type == AutocompleteArgTypeDynamicList { 257 dynamicList, ok := arg.Data.(*AutocompleteDynamicListArg) 258 if !ok { 259 return errors.New("Not a proper DynamicList type argument") 260 } 261 _, err := url.Parse(dynamicList.FetchURL) 262 if err != nil { 263 return errors.Wrapf(err, "FetchURL is not a proper url") 264 } 265 } else if arg.Type == AutocompleteArgTypeStaticList { 266 staticList, ok := arg.Data.(*AutocompleteStaticListArg) 267 if !ok { 268 return errors.New("Not a proper StaticList type argument") 269 } 270 for _, arg := range staticList.PossibleArguments { 271 if arg.Item == "" { 272 return errors.New("Possible argument name not set in StaticList argument") 273 } 274 } 275 } else if arg.Type == AutocompleteArgTypeText { 276 if _, ok := arg.Data.(*AutocompleteTextArg); !ok { 277 return errors.New("Not a proper TextInput type argument") 278 } 279 if arg.Name == "" && !arg.Required { 280 return errors.New("Positional argument can not be optional") 281 } 282 } 283 } 284 } 285 for _, command := range ad.SubCommands { 286 err := command.IsValid() 287 if err != nil { 288 return err 289 } 290 } 291 return nil 292 } 293 294 // ToJSON encodes AutocompleteData struct to the json 295 func (ad *AutocompleteData) ToJSON() ([]byte, error) { 296 b, err := json.Marshal(ad) 297 if err != nil { 298 return nil, errors.Wrapf(err, "can't marshal slash command %s", ad.Trigger) 299 } 300 return b, nil 301 } 302 303 // AutocompleteDataFromJSON decodes AutocompleteData struct from the json 304 func AutocompleteDataFromJSON(data []byte) (*AutocompleteData, error) { 305 var ad AutocompleteData 306 if err := json.Unmarshal(data, &ad); err != nil { 307 return nil, errors.Wrap(err, "can't unmarshal AutocompleteData") 308 } 309 return &ad, nil 310 } 311 312 // Equals method checks if argument is the same. 313 func (a *AutocompleteArg) Equals(arg *AutocompleteArg) bool { 314 if a.Name != arg.Name || 315 a.HelpText != arg.HelpText || 316 a.Type != arg.Type || 317 a.Required != arg.Required || 318 !reflect.DeepEqual(a.Data, arg.Data) { 319 return false 320 } 321 return true 322 } 323 324 // UnmarshalJSON will unmarshal argument 325 func (a *AutocompleteArg) UnmarshalJSON(b []byte) error { 326 var arg map[string]interface{} 327 if err := json.Unmarshal(b, &arg); err != nil { 328 return errors.Wrapf(err, "Can't unmarshal argument %s", string(b)) 329 } 330 var ok bool 331 a.Name, ok = arg["Name"].(string) 332 if !ok { 333 return errors.Errorf("No field Name in the argument %s", string(b)) 334 } 335 336 a.HelpText, ok = arg["HelpText"].(string) 337 if !ok { 338 return errors.Errorf("No field HelpText in the argument %s", string(b)) 339 } 340 341 t, ok := arg["Type"].(string) 342 if !ok { 343 return errors.Errorf("No field Type in the argument %s", string(b)) 344 } 345 a.Type = AutocompleteArgType(t) 346 347 a.Required, ok = arg["Required"].(bool) 348 if !ok { 349 return errors.Errorf("No field Required in the argument %s", string(b)) 350 } 351 352 data, ok := arg["Data"] 353 if !ok { 354 return errors.Errorf("No field Data in the argument %s", string(b)) 355 } 356 357 if a.Type == AutocompleteArgTypeText { 358 m, ok := data.(map[string]interface{}) 359 if !ok { 360 return errors.Errorf("Wrong Data type in the TextInput argument %s", string(b)) 361 } 362 pattern, ok := m["Pattern"].(string) 363 if !ok { 364 return errors.Errorf("No field Pattern in the TextInput argument %s", string(b)) 365 } 366 hint, ok := m["Hint"].(string) 367 if !ok { 368 return errors.Errorf("No field Hint in the TextInput argument %s", string(b)) 369 } 370 a.Data = &AutocompleteTextArg{Hint: hint, Pattern: pattern} 371 } else if a.Type == AutocompleteArgTypeStaticList { 372 m, ok := data.(map[string]interface{}) 373 if !ok { 374 return errors.Errorf("Wrong Data type in the StaticList argument %s", string(b)) 375 } 376 list, ok := m["PossibleArguments"].([]interface{}) 377 if !ok { 378 return errors.Errorf("No field PossibleArguments in the StaticList argument %s", string(b)) 379 } 380 381 possibleArguments := []AutocompleteListItem{} 382 for i := range list { 383 args, ok := list[i].(map[string]interface{}) 384 if !ok { 385 return errors.Errorf("Wrong AutocompleteStaticListItem type in the StaticList argument %s", string(b)) 386 } 387 item, ok := args["Item"].(string) 388 if !ok { 389 return errors.Errorf("No field Item in the StaticList's possible arguments %s", string(b)) 390 } 391 392 hint, ok := args["Hint"].(string) 393 if !ok { 394 return errors.Errorf("No field Hint in the StaticList's possible arguments %s", string(b)) 395 } 396 helpText, ok := args["HelpText"].(string) 397 if !ok { 398 return errors.Errorf("No field Hint in the StaticList's possible arguments %s", string(b)) 399 } 400 401 possibleArguments = append(possibleArguments, AutocompleteListItem{ 402 Item: item, 403 Hint: hint, 404 HelpText: helpText, 405 }) 406 } 407 a.Data = &AutocompleteStaticListArg{PossibleArguments: possibleArguments} 408 } else if a.Type == AutocompleteArgTypeDynamicList { 409 m, ok := data.(map[string]interface{}) 410 if !ok { 411 return errors.Errorf("Wrong type in the DynamicList argument %s", string(b)) 412 } 413 url, ok := m["FetchURL"].(string) 414 if !ok { 415 return errors.Errorf("No field FetchURL in the DynamicList's argument %s", string(b)) 416 } 417 a.Data = &AutocompleteDynamicListArg{FetchURL: url} 418 } 419 return nil 420 } 421 422 // AutocompleteSuggestionsToJSON returns json for a list of AutocompleteSuggestion objects 423 func AutocompleteSuggestionsToJSON(suggestions []AutocompleteSuggestion) []byte { 424 b, _ := json.Marshal(suggestions) 425 return b 426 } 427 428 // AutocompleteSuggestionsFromJSON returns list of AutocompleteSuggestions from json. 429 func AutocompleteSuggestionsFromJSON(data io.Reader) []AutocompleteSuggestion { 430 var o []AutocompleteSuggestion 431 json.NewDecoder(data).Decode(&o) 432 return o 433 } 434 435 // AutocompleteStaticListItemsToJSON returns json for a list of AutocompleteStaticListItem objects 436 func AutocompleteStaticListItemsToJSON(items []AutocompleteListItem) []byte { 437 b, _ := json.Marshal(items) 438 return b 439 } 440 441 // AutocompleteStaticListItemsFromJSON returns list of AutocompleteStaticListItem from json. 442 func AutocompleteStaticListItemsFromJSON(data io.Reader) []AutocompleteListItem { 443 var o []AutocompleteListItem 444 json.NewDecoder(data).Decode(&o) 445 return o 446 } 447 448 func stringNotInSlice(a string, slice []string) bool { 449 for _, b := range slice { 450 if b == a { 451 return false 452 } 453 } 454 return true 455 }