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  }