summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/command.go807
-rw-r--r--api/command_echo.go89
-rw-r--r--api/command_echo_test.go42
-rw-r--r--api/command_join.go62
-rw-r--r--api/command_join_test.go71
-rw-r--r--api/command_loadtest.go365
-rw-r--r--api/command_loadtest_test.go221
-rw-r--r--api/command_logout.go37
-rw-r--r--api/command_logout_test.go32
-rw-r--r--api/command_me.go37
-rw-r--r--api/command_me_test.go47
-rw-r--r--api/command_shrug.go42
-rw-r--r--api/command_shrug_test.go47
-rw-r--r--api/command_test.go306
-rw-r--r--api/user.go4
-rw-r--r--api/webhook.go62
-rw-r--r--api/webhook_test.go84
-rw-r--r--config/config.json2
-rw-r--r--docker/dev/config_docker.json2
-rw-r--r--docker/local/config_docker.json2
-rw-r--r--i18n/en.json208
-rw-r--r--i18n/es.json120
-rw-r--r--model/client.go47
-rw-r--r--model/command.go130
-rw-r--r--model/command_response.go41
-rw-r--r--model/command_response_test.go19
-rw-r--r--model/command_test.go90
-rw-r--r--model/config.go50
-rw-r--r--store/sql_command_store.go173
-rw-r--r--store/sql_command_store_test.go155
-rw-r--r--store/sql_store.go8
-rw-r--r--store/sql_webhook_store.go27
-rw-r--r--store/sql_webhook_store_test.go34
-rw-r--r--store/store.go13
-rw-r--r--utils/config.go2
-rw-r--r--web/react/components/admin_console/service_settings.jsx104
-rw-r--r--web/react/components/create_post.jsx7
-rw-r--r--web/react/components/user_settings/manage_command_hooks.jsx652
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx43
-rw-r--r--web/react/utils/async_client.jsx19
-rw-r--r--web/react/utils/client.jsx76
-rw-r--r--web/static/i18n/en.json45
-rw-r--r--web/static/i18n/es.json4
-rw-r--r--web/web_test.go4
44 files changed, 3453 insertions, 979 deletions
diff --git a/api/command.go b/api/command.go
index ab63a15a7..a8573cdcc 100644
--- a/api/command.go
+++ b/api/command.go
@@ -4,12 +4,11 @@
package api
import (
- "io"
+ "fmt"
+ "io/ioutil"
"net/http"
- "path"
- "strconv"
+ "net/url"
"strings"
- "time"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
@@ -17,630 +16,386 @@ import (
"github.com/mattermost/platform/utils"
)
-type commandHandler func(c *Context, command *model.Command) bool
-
-var (
- cmds = map[string]string{
- "logoutCommand": "/logout",
- "joinCommand": "/join",
- "loadTestCommand": "/loadtest",
- "echoCommand": "/echo",
- "shrugCommand": "/shrug",
- "meCommand": "/me",
- }
- commands = []commandHandler{
- logoutCommand,
- joinCommand,
- loadTestCommand,
- echoCommand,
- shrugCommand,
- meCommand,
- }
- commandNotImplementedErr = model.NewLocAppError("checkCommand", "api.command.no_implemented.app_error", nil, "")
-)
-var echoSem chan bool
-
-func InitCommand(r *mux.Router) {
- l4g.Debug(utils.T("api.command.init.debug"))
- r.Handle("/command", ApiUserRequired(command)).Methods("POST")
+type CommandProvider interface {
+ GetTrigger() string
+ GetCommand(c *Context) *model.Command
+ DoCommand(c *Context, channelId string, message string) *model.CommandResponse
}
-func command(c *Context, w http.ResponseWriter, r *http.Request) {
-
- props := model.MapFromJson(r.Body)
-
- command := &model.Command{
- Command: strings.TrimSpace(props["command"]),
- ChannelId: strings.TrimSpace(props["channelId"]),
- Suggest: props["suggest"] == "true",
- Suggestions: make([]*model.SuggestCommand, 0, 128),
- }
+var commandProviders = make(map[string]CommandProvider)
- checkCommand(c, command)
- if c.Err != nil {
- if c.Err != commandNotImplementedErr {
- return
- } else {
- c.Err = nil
- command.Response = model.RESP_NOT_IMPLEMENTED
- w.Write([]byte(command.ToJson()))
- return
- }
- } else {
- w.Write([]byte(command.ToJson()))
- }
+func RegisterCommandProvider(newProvider CommandProvider) {
+ commandProviders[newProvider.GetTrigger()] = newProvider
}
-func checkCommand(c *Context, command *model.Command) bool {
-
- if len(command.Command) == 0 || strings.Index(command.Command, "/") != 0 {
- c.Err = model.NewLocAppError("checkCommand", "api.command.check_command.start.app_error", nil, "")
- return false
+func GetCommandProvider(name string) CommandProvider {
+ provider, ok := commandProviders[name]
+ if ok {
+ return provider
}
- if len(command.ChannelId) > 0 {
- cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, command.ChannelId, c.Session.UserId)
+ return nil
+}
- if !c.HasPermissionsToChannel(cchan, "checkCommand") {
- return true
- }
- }
+func InitCommand(r *mux.Router) {
+ l4g.Debug(utils.T("api.command.init.debug"))
- if !command.Suggest {
- implemented := false
- for _, cmd := range cmds {
- bounds := len(cmd)
- if len(command.Command) < bounds {
- continue
- }
- if command.Command[:bounds] == cmd {
- implemented = true
- }
- }
- if !implemented {
- c.Err = commandNotImplementedErr
- return false
- }
- }
+ sr := r.PathPrefix("/commands").Subrouter()
- for _, v := range commands {
+ sr.Handle("/execute", ApiUserRequired(executeCommand)).Methods("POST")
+ sr.Handle("/list", ApiUserRequired(listCommands)).Methods("GET")
- if v(c, command) || c.Err != nil {
- return true
- }
- }
+ sr.Handle("/create", ApiUserRequired(createCommand)).Methods("POST")
+ sr.Handle("/list_team_commands", ApiUserRequired(listTeamCommands)).Methods("GET")
+ sr.Handle("/regen_token", ApiUserRequired(regenCommandToken)).Methods("POST")
+ sr.Handle("/delete", ApiUserRequired(deleteCommand)).Methods("POST")
- return false
+ sr.Handle("/test", ApiAppHandler(testCommand)).Methods("POST")
+ sr.Handle("/test", ApiAppHandler(testCommand)).Methods("GET")
}
-func logoutCommand(c *Context, command *model.Command) bool {
-
- cmd := cmds["logoutCommand"]
-
- if strings.Index(command.Command, cmd) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.logout_command.description")})
-
- if !command.Suggest {
- command.GotoLocation = "/logout"
- command.Response = model.RESP_EXECUTED
- return true
+func listCommands(c *Context, w http.ResponseWriter, r *http.Request) {
+ commands := make([]*model.Command, 0, 32)
+ seen := make(map[string]bool)
+ for _, value := range commandProviders {
+ cpy := *value.GetCommand(c)
+ if cpy.AutoComplete && !seen[cpy.Id] {
+ cpy.Sanitize()
+ seen[cpy.Trigger] = true
+ commands = append(commands, &cpy)
}
-
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.logout_command.description")})
}
- return false
-}
-
-func echoCommand(c *Context, command *model.Command) bool {
- cmd := cmds["echoCommand"]
- maxThreads := 100
-
- if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
- parameters := strings.SplitN(command.Command, " ", 2)
- if len(parameters) != 2 || len(parameters[1]) == 0 {
- return false
- }
- message := strings.Trim(parameters[1], " ")
- delay := 0
- if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 {
- if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil {
- delay = checkDelay
- }
- message = message[1:endMsg]
- } else if strings.Index(message, " ") > -1 {
- delayIdx := strings.LastIndex(message, " ")
- delayStr := strings.Trim(message[delayIdx:], " ")
-
- if checkDelay, err := strconv.Atoi(delayStr); err == nil {
- delay = checkDelay
- message = message[:delayIdx]
+ if result := <-Srv.Store.Command().GetByTeam(c.Session.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ teamCmds := result.Data.([]*model.Command)
+ for _, cmd := range teamCmds {
+ if cmd.AutoComplete && !seen[cmd.Id] {
+ cmd.Sanitize()
+ seen[cmd.Trigger] = true
+ commands = append(commands, cmd)
}
}
-
- if delay > 10000 {
- c.Err = model.NewLocAppError("echoCommand", "api.command.echo_command.under.app_error", nil, "")
- return false
- }
-
- if echoSem == nil {
- // We want one additional thread allowed so we never reach channel lockup
- echoSem = make(chan bool, maxThreads+1)
- }
-
- if len(echoSem) >= maxThreads {
- c.Err = model.NewLocAppError("echoCommand", "api.command.echo_command.high_volume.app_error", nil, "")
- return false
- }
-
- echoSem <- true
- go func() {
- defer func() { <-echoSem }()
- post := &model.Post{}
- post.ChannelId = command.ChannelId
- post.Message = message
-
- time.Sleep(time.Duration(delay) * time.Second)
-
- if _, err := CreatePost(c, post, true); err != nil {
- l4g.Error(utils.T("api.command.echo_command.create.error"), err)
- }
- }()
-
- command.Response = model.RESP_EXECUTED
- return true
-
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.echo_command.description")})
}
- return false
+ w.Write([]byte(model.CommandListToJson(commands)))
}
-func meCommand(c *Context, command *model.Command) bool {
- cmd := cmds["meCommand"]
+func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+ command := strings.TrimSpace(props["command"])
+ channelId := strings.TrimSpace(props["channelId"])
- if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
- message := ""
+ if len(command) <= 1 || strings.Index(command, "/") != 0 {
+ c.Err = model.NewLocAppError("executeCommand", "api.command.execute_command.start.app_error", nil, "")
+ return
+ }
- parameters := strings.SplitN(command.Command, " ", 2)
- if len(parameters) > 1 {
- message += "*" + parameters[1] + "*"
- }
+ if len(channelId) > 0 {
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId)
- post := &model.Post{}
- post.Message = message
- post.ChannelId = command.ChannelId
- if _, err := CreatePost(c, post, false); err != nil {
- l4g.Error(utils.T("api.command.me_command.create.error"), err)
- return false
+ if !c.HasPermissionsToChannel(cchan, "checkCommand") {
+ return
}
- command.Response = model.RESP_EXECUTED
- return true
-
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.me_command.description")})
}
- return false
-}
-
-func shrugCommand(c *Context, command *model.Command) bool {
- cmd := cmds["shrugCommand"]
+ parts := strings.Split(command, " ")
+ trigger := parts[0][1:]
+ message := strings.Join(parts[1:], " ")
+ provider := GetCommandProvider(trigger)
- if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
- message := `¯\\\_(ツ)_/¯`
+ if provider != nil {
- parameters := strings.SplitN(command.Command, " ", 2)
- if len(parameters) > 1 {
- message += " " + parameters[1]
- }
-
- post := &model.Post{}
- post.Message = message
- post.ChannelId = command.ChannelId
- if _, err := CreatePost(c, post, false); err != nil {
- l4g.Error(utils.T("api.command.shrug_command.create.error"), err)
- return false
- }
- command.Response = model.RESP_EXECUTED
- return true
+ response := provider.DoCommand(c, channelId, message)
+ handleResponse(c, w, response, channelId)
+ return
+ } else {
+ chanChan := Srv.Store.Channel().Get(channelId)
+ teamChan := Srv.Store.Team().Get(c.Session.TeamId)
+ userChan := Srv.Store.User().Get(c.Session.UserId)
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.shrug_command.description")})
- }
+ if result := <-Srv.Store.Command().GetByTeam(c.Session.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
- return false
-}
+ var team *model.Team
+ if tr := <-teamChan; tr.Err != nil {
+ c.Err = tr.Err
+ return
+ } else {
+ team = tr.Data.(*model.Team)
-func joinCommand(c *Context, command *model.Command) bool {
+ }
- // looks for "/join channel-name"
- cmd := cmds["joinCommand"]
+ var user *model.User
+ if ur := <-userChan; ur.Err != nil {
+ c.Err = ur.Err
+ return
+ } else {
+ user = ur.Data.(*model.User)
+ }
- if strings.Index(command.Command, cmd) == 0 {
+ var channel *model.Channel
+ if cr := <-chanChan; cr.Err != nil {
+ c.Err = cr.Err
+ return
+ } else {
+ channel = cr.Data.(*model.Channel)
+ }
- parts := strings.Split(command.Command, " ")
+ teamCmds := result.Data.([]*model.Command)
+ for _, cmd := range teamCmds {
+ if trigger == cmd.Trigger {
+ l4g.Debug(fmt.Sprintf(utils.T("api.command.execute_command.debug"), trigger, c.Session.UserId))
- startsWith := ""
+ p := url.Values{}
+ p.Set("token", cmd.Token)
- if len(parts) == 2 {
- startsWith = parts[1]
- }
+ p.Set("team_id", cmd.TeamId)
+ p.Set("team_domain", team.Name)
- if result := <-Srv.Store.Channel().GetMoreChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
- return false
- } else {
- channels := result.Data.(*model.ChannelList)
+ p.Set("channel_id", channelId)
+ p.Set("channel_name", channel.Name)
- for _, v := range channels.Channels {
+ p.Set("user_id", c.Session.UserId)
+ p.Set("user_name", user.Username)
- if v.Name == startsWith && !command.Suggest {
+ p.Set("command", "/"+trigger)
+ p.Set("text", message)
+ p.Set("response_url", "not supported yet")
- if v.Type == model.CHANNEL_DIRECT {
- return false
+ method := "POST"
+ if cmd.Method == model.COMMAND_METHOD_GET {
+ method = "GET"
}
- JoinChannel(c, v.Id, "")
-
- if c.Err != nil {
- return false
+ client := &http.Client{}
+ req, _ := http.NewRequest(method, cmd.URL, strings.NewReader(p.Encode()))
+ req.Header.Set("Accept", "application/json")
+ if cmd.Method == model.COMMAND_METHOD_POST {
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
- command.GotoLocation = c.GetTeamURL() + "/channels/" + v.Name
- command.Response = model.RESP_EXECUTED
- return true
- }
+ if resp, err := client.Do(req); err != nil {
+ c.Err = model.NewLocAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error())
+ } else {
+ if resp.StatusCode == http.StatusOK {
+ response := model.CommandResponseFromJson(resp.Body)
+ if response == nil {
+ c.Err = model.NewLocAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": trigger}, "")
+ } else {
+ handleResponse(c, w, response, channelId)
+ }
+ } else {
+ body, _ := ioutil.ReadAll(resp.Body)
+ c.Err = model.NewLocAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]interface{}{"Trigger": trigger, "Status": resp.Status}, string(body))
+ }
+ }
- if len(startsWith) == 0 || strings.Index(v.Name, startsWith) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd + " " + v.Name, Description: c.T("api.commmand.join_command.description")})
+ return
}
}
+
}
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.commmand.join_command.description")})
}
- return false
+ c.Err = model.NewLocAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "")
}
-func loadTestCommand(c *Context, command *model.Command) bool {
- cmd := cmds["loadTestCommand"]
-
- // This command is only available when EnableTesting is true
- if !utils.Cfg.ServiceSettings.EnableTesting {
- return false
- }
-
- if strings.Index(command.Command, cmd) == 0 {
- if loadTestSetupCommand(c, command) {
- return true
- }
- if loadTestUsersCommand(c, command) {
- return true
- }
- if loadTestChannelsCommand(c, command) {
- return true
- }
- if loadTestPostsCommand(c, command) {
- return true
- }
- if loadTestUrlCommand(c, command) {
- return true
- }
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.load_test_command.description")})
+func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandResponse, channelId string) {
+ if response.ResponseType == model.COMMAND_RESPONSE_TYPE_IN_CHANNEL {
+ post := &model.Post{}
+ post.ChannelId = channelId
+ post.Message = response.Text
+ if _, err := CreatePost(c, post, true); err != nil {
+ c.Err = model.NewLocAppError("command", "api.command.execute_command.save.app_error", nil, "")
+ }
+ } else if response.ResponseType == model.COMMAND_RESPONSE_TYPE_EPHEMERAL {
+ // post := &model.Post{}
+ // post.ChannelId = channelId
+ // post.Message = "TODO_EPHEMERAL: " + response.Text
+ // if _, err := CreatePost(c, post, true); err != nil {
+ // c.Err = model.NewLocAppError("command", "api.command.execute_command.save.app_error", nil, "")
+ // }
}
- return false
+ w.Write([]byte(response.ToJson()))
}
-func parseRange(command string, cmd string) (utils.Range, bool) {
- tokens := strings.Fields(strings.TrimPrefix(command, cmd))
- var begin int
- var end int
- var err1 error
- var err2 error
- switch {
- case len(tokens) == 1:
- begin, err1 = strconv.Atoi(tokens[0])
- end = begin
- if err1 != nil {
- return utils.Range{0, 0}, false
- }
- case len(tokens) >= 2:
- begin, err1 = strconv.Atoi(tokens[0])
- end, err2 = strconv.Atoi(tokens[1])
- if err1 != nil || err2 != nil {
- return utils.Range{0, 0}, false
- }
- default:
- return utils.Range{0, 0}, false
+func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCommands {
+ c.Err = model.NewLocAppError("createCommand", "api.command.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
}
- return utils.Range{begin, end}, true
-}
-func contains(items []string, token string) bool {
- for _, elem := range items {
- if elem == token {
- return true
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("createCommand", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
}
}
- return false
-}
-func loadTestSetupCommand(c *Context, command *model.Command) bool {
- cmd := cmds["loadTestCommand"] + " setup"
+ c.LogAudit("attempt")
- if strings.Index(command.Command, cmd) == 0 && !command.Suggest {
- tokens := strings.Fields(strings.TrimPrefix(command.Command, cmd))
- doTeams := contains(tokens, "teams")
- doFuzz := contains(tokens, "fuzz")
+ cmd := model.CommandFromJson(r.Body)
- numArgs := 0
- if doTeams {
- numArgs++
- }
- if doFuzz {
- numArgs++
- }
+ if cmd == nil {
+ c.SetInvalidParam("createCommand", "command")
+ return
+ }
- var numTeams int
- var numChannels int
- var numUsers int
- var numPosts int
-
- // Defaults
- numTeams = 10
- numChannels = 10
- numUsers = 10
- numPosts = 10
-
- if doTeams {
- if (len(tokens) - numArgs) >= 4 {
- numTeams, _ = strconv.Atoi(tokens[numArgs+0])
- numChannels, _ = strconv.Atoi(tokens[numArgs+1])
- numUsers, _ = strconv.Atoi(tokens[numArgs+2])
- numPosts, _ = strconv.Atoi(tokens[numArgs+3])
- }
- } else {
- if (len(tokens) - numArgs) >= 3 {
- numChannels, _ = strconv.Atoi(tokens[numArgs+0])
- numUsers, _ = strconv.Atoi(tokens[numArgs+1])
- numPosts, _ = strconv.Atoi(tokens[numArgs+2])
- }
- }
- client := model.NewClient(c.GetSiteURL())
+ cmd.CreatorId = c.Session.UserId
+ cmd.TeamId = c.Session.TeamId
- if doTeams {
- if err := CreateBasicUser(client); err != nil {
- l4g.Error(utils.T("api.command.load_test_setup_command.create.error"))
- return true
- }
- client.LoginByEmail(BTEST_TEAM_NAME, BTEST_USER_EMAIL, BTEST_USER_PASSWORD)
- environment, err := CreateTestEnvironmentWithTeams(
- client,
- utils.Range{numTeams, numTeams},
- utils.Range{numChannels, numChannels},
- utils.Range{numUsers, numUsers},
- utils.Range{numPosts, numPosts},
- doFuzz)
- if err != true {
- l4g.Error(utils.T("api.command.load_test_setup_command.create.error"))
- return true
- } else {
- l4g.Info("Testing environment created")
- for i := 0; i < len(environment.Teams); i++ {
- l4g.Info(utils.T("api.command.load_test_setup_command.created.info"), environment.Teams[i].Name)
- l4g.Info(utils.T("api.command.load_test_setup_command.login.info"), environment.Environments[i].Users[0].Email, USER_PASSWORD)
- }
- }
- } else {
- client.MockSession(c.Session.Token)
- CreateTestEnvironmentInTeam(
- client,
- c.Session.TeamId,
- utils.Range{numChannels, numChannels},
- utils.Range{numUsers, numUsers},
- utils.Range{numPosts, numPosts},
- doFuzz)
- }
- return true
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{
- Suggestion: cmd,
- Description: c.T("api.command.load_test_setup_command.description")})
+ if result := <-Srv.Store.Command().Save(cmd); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ c.LogAudit("success")
+ rcmd := result.Data.(*model.Command)
+ w.Write([]byte(rcmd.ToJson()))
}
-
- return false
}
-func loadTestUsersCommand(c *Context, command *model.Command) bool {
- cmd1 := cmds["loadTestCommand"] + " users"
- cmd2 := cmds["loadTestCommand"] + " users fuzz"
+func listTeamCommands(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCommands {
+ c.Err = model.NewLocAppError("listTeamCommands", "api.command.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
- if strings.Index(command.Command, cmd1) == 0 && !command.Suggest {
- cmd := cmd1
- doFuzz := false
- if strings.Index(command.Command, cmd2) == 0 {
- doFuzz = true
- cmd = cmd2
- }
- usersr, err := parseRange(command.Command, cmd)
- if err == false {
- usersr = utils.Range{10, 15}
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("listTeamCommands", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
}
- client := model.NewClient(c.GetSiteURL())
- userCreator := NewAutoUserCreator(client, c.Session.TeamId)
- userCreator.Fuzzy = doFuzz
- userCreator.CreateTestUsers(usersr)
- return true
- } else if strings.Index(cmd1, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd1, Description: c.T("api.command.load_test_users_command.users.description")})
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_users_command.fuzz.description")})
- } else if strings.Index(cmd2, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_users_command.fuzz.description")})
}
- return false
+ if result := <-Srv.Store.Command().GetByTeam(c.Session.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ cmds := result.Data.([]*model.Command)
+ w.Write([]byte(model.CommandListToJson(cmds)))
+ }
}
-func loadTestChannelsCommand(c *Context, command *model.Command) bool {
- cmd1 := cmds["loadTestCommand"] + " channels"
- cmd2 := cmds["loadTestCommand"] + " channels fuzz"
+func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCommands {
+ c.Err = model.NewLocAppError("regenCommandToken", "api.command.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
- if strings.Index(command.Command, cmd1) == 0 && !command.Suggest {
- cmd := cmd1
- doFuzz := false
- if strings.Index(command.Command, cmd2) == 0 {
- doFuzz = true
- cmd = cmd2
- }
- channelsr, err := parseRange(command.Command, cmd)
- if err == false {
- channelsr = utils.Range{20, 30}
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("regenCommandToken", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
}
- client := model.NewClient(c.GetSiteURL())
- client.MockSession(c.Session.Token)
- channelCreator := NewAutoChannelCreator(client, c.Session.TeamId)
- channelCreator.Fuzzy = doFuzz
- channelCreator.CreateTestChannels(channelsr)
- return true
- } else if strings.Index(cmd1, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd1, Description: c.T("api.command.load_test_channels_command.channel.description")})
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_channels_command.fuzz.description")})
- } else if strings.Index(cmd2, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_channels_command.fuzz.description")})
}
- return false
-}
+ c.LogAudit("attempt")
-func loadTestPostsCommand(c *Context, command *model.Command) bool {
- cmd1 := cmds["loadTestCommand"] + " posts"
- cmd2 := cmds["loadTestCommand"] + " posts fuzz"
+ props := model.MapFromJson(r.Body)
- if strings.Index(command.Command, cmd1) == 0 && !command.Suggest {
- cmd := cmd1
- doFuzz := false
- if strings.Index(command.Command, cmd2) == 0 {
- cmd = cmd2
- doFuzz = true
- }
+ id := props["id"]
+ if len(id) == 0 {
+ c.SetInvalidParam("regenCommandToken", "id")
+ return
+ }
- postsr, err := parseRange(command.Command, cmd)
- if err == false {
- postsr = utils.Range{20, 30}
- }
+ var cmd *model.Command
+ if result := <-Srv.Store.Command().Get(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ cmd = result.Data.(*model.Command)
- tokens := strings.Fields(strings.TrimPrefix(command.Command, cmd))
- rimages := utils.Range{0, 0}
- if len(tokens) >= 3 {
- if numImages, err := strconv.Atoi(tokens[2]); err == nil {
- rimages = utils.Range{numImages, numImages}
- }
+ if c.Session.TeamId != cmd.TeamId || (c.Session.UserId != cmd.CreatorId && !c.IsTeamAdmin()) {
+ c.LogAudit("fail - inappropriate permissions")
+ c.Err = model.NewLocAppError("regenToken", "api.command.regen.app_error", nil, "user_id="+c.Session.UserId)
+ return
}
+ }
- var usernames []string
- if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err == nil {
- profileUsers := result.Data.(map[string]*model.User)
- usernames = make([]string, len(profileUsers))
- i := 0
- for _, userprof := range profileUsers {
- usernames[i] = userprof.Username
- i++
- }
- }
+ cmd.Token = model.NewId()
- client := model.NewClient(c.GetSiteURL())
- client.MockSession(c.Session.Token)
- testPoster := NewAutoPostCreator(client, command.ChannelId)
- testPoster.Fuzzy = doFuzz
- testPoster.Users = usernames
-
- numImages := utils.RandIntFromRange(rimages)
- numPosts := utils.RandIntFromRange(postsr)
- for i := 0; i < numPosts; i++ {
- testPoster.HasImage = (i < numImages)
- testPoster.CreateRandomPost()
- }
- return true
- } else if strings.Index(cmd1, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd1, Description: c.T("api.command.load_test_posts_command.posts.description")})
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_posts_command.fuzz.description")})
- } else if strings.Index(cmd2, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_posts_command.fuzz.description")})
+ if result := <-Srv.Store.Command().Update(cmd); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ w.Write([]byte(result.Data.(*model.Command).ToJson()))
}
-
- return false
}
-func loadTestUrlCommand(c *Context, command *model.Command) bool {
- cmd := cmds["loadTestCommand"] + " url"
-
- if strings.Index(command.Command, cmd) == 0 && !command.Suggest {
- url := ""
+func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCommands {
+ c.Err = model.NewLocAppError("deleteCommand", "api.command.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
- parameters := strings.SplitN(command.Command, " ", 3)
- if len(parameters) != 3 {
- c.Err = model.NewLocAppError("loadTestUrlCommand", "api.command.load_test_url_command.url.app_error", nil, "")
- return true
- } else {
- url = parameters[2]
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("deleteCommand", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
}
+ }
- // provide a shortcut to easily access tests stored in doc/developer/tests
- if !strings.HasPrefix(url, "http") {
- url = "https://raw.githubusercontent.com/mattermost/platform/master/doc/developer/tests/" + url
+ c.LogAudit("attempt")
- if path.Ext(url) == "" {
- url += ".md"
- }
- }
-
- var contents io.ReadCloser
- if r, err := http.Get(url); err != nil {
- c.Err = model.NewLocAppError("loadTestUrlCommand", "api.command.load_test_url_command.file.app_error", nil, err.Error())
- return false
- } else if r.StatusCode > 400 {
- c.Err = model.NewLocAppError("loadTestUrlCommand", "api.command.load_test_url_command.file.app_error", nil, r.Status)
- return false
- } else {
- contents = r.Body
- }
+ props := model.MapFromJson(r.Body)
- bytes := make([]byte, 4000)
+ id := props["id"]
+ if len(id) == 0 {
+ c.SetInvalidParam("deleteCommand", "id")
+ return
+ }
- // break contents into 4000 byte posts
- for {
- length, err := contents.Read(bytes)
- if err != nil && err != io.EOF {
- c.Err = model.NewLocAppError("loadTestUrlCommand", "api.command.load_test_url_command.reading.app_error", nil, err.Error())
- return false
- }
+ if result := <-Srv.Store.Command().Get(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ if c.Session.TeamId != result.Data.(*model.Command).TeamId || (c.Session.UserId != result.Data.(*model.Command).CreatorId && !c.IsTeamAdmin()) {
+ c.LogAudit("fail - inappropriate permissions")
+ c.Err = model.NewLocAppError("deleteCommand", "api.command.delete.app_error", nil, "user_id="+c.Session.UserId)
+ return
+ }
+ }
- if length == 0 {
- break
- }
+ if err := (<-Srv.Store.Command().Delete(id, model.GetMillis())).Err; err != nil {
+ c.Err = err
+ return
+ }
- post := &model.Post{}
- post.Message = string(bytes[:length])
- post.ChannelId = command.ChannelId
+ c.LogAudit("success")
+ w.Write([]byte(model.MapToJson(props)))
+}
- if _, err := CreatePost(c, post, false); err != nil {
- l4g.Error(utils.T("api.command.load_test_url_command.create.error"), err)
- return false
- }
- }
+func testCommand(c *Context, w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
- command.Response = model.RESP_EXECUTED
+ msg := ""
+ if r.Method == "POST" {
+ msg = msg + "\ntoken=" + r.FormValue("token")
+ msg = msg + "\nteam_domain=" + r.FormValue("team_domain")
+ } else {
+ body, _ := ioutil.ReadAll(r.Body)
+ msg = string(body)
+ }
- return true
- } else if strings.Index(cmd, command.Command) == 0 && strings.Index(command.Command, "/loadtest posts") != 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.load_test_url_command.description")})
+ rc := &model.CommandResponse{
+ Text: "test command response " + msg,
+ ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
}
- return false
+ w.Write([]byte(rc.ToJson()))
}
diff --git a/api/command_echo.go b/api/command_echo.go
new file mode 100644
index 000000000..805db7ad2
--- /dev/null
+++ b/api/command_echo.go
@@ -0,0 +1,89 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+)
+
+var echoSem chan bool
+
+type EchoProvider struct {
+}
+
+const (
+ CMD_ECHO = "echo"
+)
+
+func init() {
+ RegisterCommandProvider(&EchoProvider{})
+}
+
+func (me *EchoProvider) GetTrigger() string {
+ return CMD_ECHO
+}
+
+func (me *EchoProvider) GetCommand(c *Context) *model.Command {
+ return &model.Command{
+ Trigger: CMD_ECHO,
+ AutoComplete: true,
+ AutoCompleteDesc: c.T("api.command_echo.desc"),
+ AutoCompleteHint: c.T("api.command_echo.hint"),
+ DisplayName: c.T("api.command_echo.name"),
+ }
+}
+
+func (me *EchoProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ maxThreads := 100
+
+ delay := 0
+ if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 {
+ if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil {
+ delay = checkDelay
+ }
+ message = message[1:endMsg]
+ } else if strings.Index(message, " ") > -1 {
+ delayIdx := strings.LastIndex(message, " ")
+ delayStr := strings.Trim(message[delayIdx:], " ")
+
+ if checkDelay, err := strconv.Atoi(delayStr); err == nil {
+ delay = checkDelay
+ message = message[:delayIdx]
+ }
+ }
+
+ if delay > 10000 {
+ return &model.CommandResponse{Text: c.T("api.command_echo.delay.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ if echoSem == nil {
+ // We want one additional thread allowed so we never reach channel lockup
+ echoSem = make(chan bool, maxThreads+1)
+ }
+
+ if len(echoSem) >= maxThreads {
+ return &model.CommandResponse{Text: c.T("api.command_echo.high_volume.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ echoSem <- true
+ go func() {
+ defer func() { <-echoSem }()
+ post := &model.Post{}
+ post.ChannelId = channelId
+ post.Message = message
+
+ time.Sleep(time.Duration(delay) * time.Second)
+
+ if _, err := CreatePost(c, post, true); err != nil {
+ l4g.Error(c.T("api.command_echo.create.app_error"), err)
+ }
+ }()
+
+ return &model.CommandResponse{}
+}
diff --git a/api/command_echo_test.go b/api/command_echo_test.go
new file mode 100644
index 000000000..3bfaa0279
--- /dev/null
+++ b/api/command_echo_test.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "testing"
+ "time"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+)
+
+func TestEchoCommand(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ echoTestString := "/echo test"
+
+ r1 := Client.Must(Client.Command(channel1.Id, echoTestString, false)).Data.(*model.CommandResponse)
+ if r1 == nil {
+ t.Fatal("Echo command failed to execute")
+ }
+
+ time.Sleep(100 * time.Millisecond)
+
+ p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
+ if len(p1.Order) != 1 {
+ t.Fatal("Echo command failed to send")
+ }
+}
diff --git a/api/command_join.go b/api/command_join.go
new file mode 100644
index 000000000..ba3b0041e
--- /dev/null
+++ b/api/command_join.go
@@ -0,0 +1,62 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+type JoinProvider struct {
+}
+
+const (
+ CMD_JOIN = "join"
+)
+
+func init() {
+ RegisterCommandProvider(&JoinProvider{})
+}
+
+func (me *JoinProvider) GetTrigger() string {
+ return CMD_JOIN
+}
+
+func (me *JoinProvider) GetCommand(c *Context) *model.Command {
+ return &model.Command{
+ Trigger: CMD_JOIN,
+ AutoComplete: true,
+ AutoCompleteDesc: c.T("api.command_join.desc"),
+ AutoCompleteHint: c.T("api.command_join.hint"),
+ DisplayName: c.T("api.command_join.name"),
+ }
+}
+
+func (me *JoinProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ if result := <-Srv.Store.Channel().GetMoreChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil {
+ return &model.CommandResponse{Text: c.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ } else {
+ channels := result.Data.(*model.ChannelList)
+
+ for _, v := range channels.Channels {
+
+ if v.Name == message {
+
+ if v.Type == model.CHANNEL_DIRECT {
+ return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ JoinChannel(c, v.Id, "")
+
+ if c.Err != nil {
+ c.Err = nil
+ return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ return &model.CommandResponse{GotoLocation: c.GetTeamURL() + "/channels/" + v.Name, Text: c.T("api.command_join.success"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+ }
+ }
+
+ return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command_join.missing.app_error")}
+}
diff --git a/api/command_join_test.go b/api/command_join_test.go
new file mode 100644
index 000000000..7260915a6
--- /dev/null
+++ b/api/command_join_test.go
@@ -0,0 +1,71 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+)
+
+func TestJoinCommands(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel0 := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel0 = Client.Must(Client.CreateChannel(channel0)).Data.(*model.Channel)
+
+ channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+ Client.Must(Client.LeaveChannel(channel1.Id))
+
+ channel2 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
+ Client.Must(Client.LeaveChannel(channel2.Id))
+
+ user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ data := make(map[string]string)
+ data["user_id"] = user2.Id
+ channel3 := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.Channel)
+
+ rs5 := Client.Must(Client.Command(channel0.Id, "/join "+channel2.Name, false)).Data.(*model.CommandResponse)
+ if !strings.HasSuffix(rs5.GotoLocation, "/"+team.Name+"/channels/"+channel2.Name) {
+ t.Fatal("failed to join channel")
+ }
+
+ rs6 := Client.Must(Client.Command(channel0.Id, "/join "+channel3.Name, false)).Data.(*model.CommandResponse)
+ if strings.HasSuffix(rs6.GotoLocation, "/"+team.Name+"/channels/"+channel3.Name) {
+ t.Fatal("should not have joined direct message channel")
+ }
+
+ c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
+
+ if len(c1.Channels) != 5 { // 4 because of town-square, off-topic and direct
+ t.Fatal("didn't join channel")
+ }
+
+ found := false
+ for _, c := range c1.Channels {
+ if c.Name == channel2.Name {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Fatal("didn't join channel")
+ }
+}
diff --git a/api/command_loadtest.go b/api/command_loadtest.go
new file mode 100644
index 000000000..c7c4f98f5
--- /dev/null
+++ b/api/command_loadtest.go
@@ -0,0 +1,365 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "io"
+ "net/http"
+ "path"
+ "strconv"
+ "strings"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+var usage = `Mattermost load testing commands to help configure the system
+
+ COMMANDS:
+
+ Setup - Creates a testing environment in current team.
+ /loadtest setup [teams] [fuzz] <Num Channels> <Num Users> <NumPosts>
+
+ Example:
+ /loadtest setup teams fuzz 10 20 50
+
+ Users - Add a specified number of random users with fuzz text to current team.
+ /loadtest users [fuzz] <Min Users> <Max Users>
+
+ Example:
+ /loadtest users fuzz 5 10
+
+ Channels - Add a specified number of random channels with fuzz text to current team.
+ /loadtest channels [fuzz] <Min Channels> <Max Channels>
+
+ Example:
+ /loadtest channels fuzz 5 10
+
+ Posts - Add some random posts with fuzz text to current channel.
+ /loadtest posts [fuzz] <Min Posts> <Max Posts> <Max Images>
+
+ Example:
+ /loadtest posts fuzz 5 10 3
+
+ Url - Add a post containing the text from a given url to current channel.
+ /loadtest url
+
+ Example:
+ /loadtest http://www.example.com/sample_file.md
+
+
+`
+
+const (
+ CMD_LOADTEST = "loadtest"
+)
+
+type LoadTestProvider struct {
+}
+
+func init() {
+ if !utils.Cfg.ServiceSettings.EnableTesting {
+ RegisterCommandProvider(&LoadTestProvider{})
+ }
+}
+
+func (me *LoadTestProvider) GetTrigger() string {
+ return CMD_LOADTEST
+}
+
+func (me *LoadTestProvider) GetCommand(c *Context) *model.Command {
+ return &model.Command{
+ Trigger: CMD_LOADTEST,
+ AutoComplete: false,
+ AutoCompleteDesc: "Debug Load Testing",
+ AutoCompleteHint: "help",
+ DisplayName: "loadtest",
+ }
+}
+
+func (me *LoadTestProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
+
+ //This command is only available when EnableTesting is true
+ if !utils.Cfg.ServiceSettings.EnableTesting {
+ return &model.CommandResponse{}
+ }
+
+ if strings.HasPrefix(message, "setup") {
+ return me.SetupCommand(c, channelId, message)
+ }
+
+ if strings.HasPrefix(message, "users") {
+ return me.UsersCommand(c, channelId, message)
+ }
+
+ if strings.HasPrefix(message, "channels") {
+ return me.ChannelsCommand(c, channelId, message)
+ }
+
+ if strings.HasPrefix(message, "posts") {
+ return me.PostsCommand(c, channelId, message)
+ }
+
+ if strings.HasPrefix(message, "url") {
+ return me.UrlCommand(c, channelId, message)
+ }
+
+ return me.HelpCommand(c, channelId, message)
+}
+
+func (me *LoadTestProvider) HelpCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ return &model.CommandResponse{Text: usage, ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+}
+
+func (me *LoadTestProvider) SetupCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ tokens := strings.Fields(strings.TrimPrefix(message, "setup"))
+ doTeams := contains(tokens, "teams")
+ doFuzz := contains(tokens, "fuzz")
+
+ numArgs := 0
+ if doTeams {
+ numArgs++
+ }
+ if doFuzz {
+ numArgs++
+ }
+
+ var numTeams int
+ var numChannels int
+ var numUsers int
+ var numPosts int
+
+ // Defaults
+ numTeams = 10
+ numChannels = 10
+ numUsers = 10
+ numPosts = 10
+
+ if doTeams {
+ if (len(tokens) - numArgs) >= 4 {
+ numTeams, _ = strconv.Atoi(tokens[numArgs+0])
+ numChannels, _ = strconv.Atoi(tokens[numArgs+1])
+ numUsers, _ = strconv.Atoi(tokens[numArgs+2])
+ numPosts, _ = strconv.Atoi(tokens[numArgs+3])
+ }
+ } else {
+ if (len(tokens) - numArgs) >= 3 {
+ numChannels, _ = strconv.Atoi(tokens[numArgs+0])
+ numUsers, _ = strconv.Atoi(tokens[numArgs+1])
+ numPosts, _ = strconv.Atoi(tokens[numArgs+2])
+ }
+ }
+ client := model.NewClient(c.GetSiteURL())
+
+ if doTeams {
+ if err := CreateBasicUser(client); err != nil {
+ return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+ client.LoginByEmail(BTEST_TEAM_NAME, BTEST_USER_EMAIL, BTEST_USER_PASSWORD)
+ environment, err := CreateTestEnvironmentWithTeams(
+ client,
+ utils.Range{numTeams, numTeams},
+ utils.Range{numChannels, numChannels},
+ utils.Range{numUsers, numUsers},
+ utils.Range{numPosts, numPosts},
+ doFuzz)
+ if err != true {
+ return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ } else {
+ l4g.Info("Testing environment created")
+ for i := 0; i < len(environment.Teams); i++ {
+ l4g.Info("Team Created: " + environment.Teams[i].Name)
+ l4g.Info("\t User to login: " + environment.Environments[i].Users[0].Email + ", " + USER_PASSWORD)
+ }
+ }
+ } else {
+ client.MockSession(c.Session.Token)
+ CreateTestEnvironmentInTeam(
+ client,
+ c.Session.TeamId,
+ utils.Range{numChannels, numChannels},
+ utils.Range{numUsers, numUsers},
+ utils.Range{numPosts, numPosts},
+ doFuzz)
+ }
+
+ return &model.CommandResponse{Text: "Creating enviroment...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+}
+
+func (me *LoadTestProvider) UsersCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ cmd := strings.TrimSpace(strings.TrimPrefix(message, "users"))
+
+ doFuzz := false
+ if strings.Index(cmd, "fuzz") == 0 {
+ doFuzz = true
+ cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "fuzz"))
+ }
+
+ usersr, err := parseRange(cmd, "")
+ if err == false {
+ usersr = utils.Range{2, 5}
+ }
+
+ client := model.NewClient(c.GetSiteURL())
+ userCreator := NewAutoUserCreator(client, c.Session.TeamId)
+ userCreator.Fuzzy = doFuzz
+ userCreator.CreateTestUsers(usersr)
+
+ return &model.CommandResponse{Text: "Adding users...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+}
+
+func (me *LoadTestProvider) ChannelsCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ cmd := strings.TrimSpace(strings.TrimPrefix(message, "channels"))
+
+ doFuzz := false
+ if strings.Index(cmd, "fuzz") == 0 {
+ doFuzz = true
+ cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "fuzz"))
+ }
+
+ channelsr, err := parseRange(cmd, "")
+ if err == false {
+ channelsr = utils.Range{2, 5}
+ }
+ client := model.NewClient(c.GetSiteURL())
+ client.MockSession(c.Session.Token)
+ channelCreator := NewAutoChannelCreator(client, c.Session.TeamId)
+ channelCreator.Fuzzy = doFuzz
+ channelCreator.CreateTestChannels(channelsr)
+
+ return &model.CommandResponse{Text: "Adding channels...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+}
+
+func (me *LoadTestProvider) PostsCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ cmd := strings.TrimSpace(strings.TrimPrefix(message, "posts"))
+
+ doFuzz := false
+ if strings.Index(cmd, "fuzz") == 0 {
+ doFuzz = true
+ cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "fuzz"))
+ }
+
+ postsr, err := parseRange(cmd, "")
+ if err == false {
+ postsr = utils.Range{20, 30}
+ }
+
+ tokens := strings.Fields(cmd)
+ rimages := utils.Range{0, 0}
+ if len(tokens) >= 3 {
+ if numImages, err := strconv.Atoi(tokens[2]); err == nil {
+ rimages = utils.Range{numImages, numImages}
+ }
+ }
+
+ var usernames []string
+ if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err == nil {
+ profileUsers := result.Data.(map[string]*model.User)
+ usernames = make([]string, len(profileUsers))
+ i := 0
+ for _, userprof := range profileUsers {
+ usernames[i] = userprof.Username
+ i++
+ }
+ }
+
+ client := model.NewClient(c.GetSiteURL())
+ client.MockSession(c.Session.Token)
+ testPoster := NewAutoPostCreator(client, channelId)
+ testPoster.Fuzzy = doFuzz
+ testPoster.Users = usernames
+
+ numImages := utils.RandIntFromRange(rimages)
+ numPosts := utils.RandIntFromRange(postsr)
+ for i := 0; i < numPosts; i++ {
+ testPoster.HasImage = (i < numImages)
+ testPoster.CreateRandomPost()
+ }
+
+ return &model.CommandResponse{Text: "Adding posts...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+}
+
+func (me *LoadTestProvider) UrlCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ url := strings.TrimSpace(strings.TrimPrefix(message, "url"))
+ if len(url) == 0 {
+ return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ // provide a shortcut to easily access tests stored in doc/developer/tests
+ if !strings.HasPrefix(url, "http") {
+ url = "https://raw.githubusercontent.com/mattermost/platform/master/doc/developer/tests/" + url
+
+ if path.Ext(url) == "" {
+ url += ".md"
+ }
+ }
+
+ var contents io.ReadCloser
+ if r, err := http.Get(url); err != nil {
+ return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ } else if r.StatusCode > 400 {
+ return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ } else {
+ contents = r.Body
+ }
+
+ bytes := make([]byte, 4000)
+
+ // break contents into 4000 byte posts
+ for {
+ length, err := contents.Read(bytes)
+ if err != nil && err != io.EOF {
+ return &model.CommandResponse{Text: "Encountered error reading file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ if length == 0 {
+ break
+ }
+
+ post := &model.Post{}
+ post.Message = string(bytes[:length])
+ post.ChannelId = channelId
+
+ if _, err := CreatePost(c, post, false); err != nil {
+ return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+ }
+
+ return &model.CommandResponse{Text: "Loading data...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+}
+
+func parseRange(command string, cmd string) (utils.Range, bool) {
+ tokens := strings.Fields(strings.TrimPrefix(command, cmd))
+ var begin int
+ var end int
+ var err1 error
+ var err2 error
+ switch {
+ case len(tokens) == 1:
+ begin, err1 = strconv.Atoi(tokens[0])
+ end = begin
+ if err1 != nil {
+ return utils.Range{0, 0}, false
+ }
+ case len(tokens) >= 2:
+ begin, err1 = strconv.Atoi(tokens[0])
+ end, err2 = strconv.Atoi(tokens[1])
+ if err1 != nil || err2 != nil {
+ return utils.Range{0, 0}, false
+ }
+ default:
+ return utils.Range{0, 0}, false
+ }
+ return utils.Range{begin, end}, true
+}
+
+func contains(items []string, token string) bool {
+ for _, elem := range items {
+ if elem == token {
+ return true
+ }
+ }
+ return false
+}
diff --git a/api/command_loadtest_test.go b/api/command_loadtest_test.go
new file mode 100644
index 000000000..7cb77cf18
--- /dev/null
+++ b/api/command_loadtest_test.go
@@ -0,0 +1,221 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+)
+
+func TestLoadTestHelpCommands(t *testing.T) {
+ Setup()
+ // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json
+ enableTesting := utils.Cfg.ServiceSettings.EnableTesting
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableTesting = enableTesting
+ }()
+
+ utils.Cfg.ServiceSettings.EnableTesting = true
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel)
+
+ rs := Client.Must(Client.Command(channel.Id, "/loadtest help", false)).Data.(*model.CommandResponse)
+ if !strings.Contains(rs.Text, "Mattermost load testing commands to help") {
+ t.Fatal(rs.Text)
+ }
+
+ time.Sleep(2 * time.Second)
+}
+
+func TestLoadTestSetupCommands(t *testing.T) {
+ Setup()
+ // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json
+ enableTesting := utils.Cfg.ServiceSettings.EnableTesting
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableTesting = enableTesting
+ }()
+
+ utils.Cfg.ServiceSettings.EnableTesting = true
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel)
+
+ rs := Client.Must(Client.Command(channel.Id, "/loadtest setup fuzz 1 1 1", false)).Data.(*model.CommandResponse)
+ if rs.Text != "Creating enviroment..." {
+ t.Fatal(rs.Text)
+ }
+
+ time.Sleep(2 * time.Second)
+}
+
+func TestLoadTestUsersCommands(t *testing.T) {
+ Setup()
+ // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json
+ enableTesting := utils.Cfg.ServiceSettings.EnableTesting
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableTesting = enableTesting
+ }()
+
+ utils.Cfg.ServiceSettings.EnableTesting = true
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel)
+
+ rs := Client.Must(Client.Command(channel.Id, "/loadtest users fuzz 1 2", false)).Data.(*model.CommandResponse)
+ if rs.Text != "Adding users..." {
+ t.Fatal(rs.Text)
+ }
+
+ time.Sleep(2 * time.Second)
+}
+
+func TestLoadTestChannelsCommands(t *testing.T) {
+ Setup()
+ // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json
+ enableTesting := utils.Cfg.ServiceSettings.EnableTesting
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableTesting = enableTesting
+ }()
+
+ utils.Cfg.ServiceSettings.EnableTesting = true
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel)
+
+ rs := Client.Must(Client.Command(channel.Id, "/loadtest channels fuzz 1 2", false)).Data.(*model.CommandResponse)
+ if rs.Text != "Adding channels..." {
+ t.Fatal(rs.Text)
+ }
+
+ time.Sleep(2 * time.Second)
+}
+
+func TestLoadTestPostsCommands(t *testing.T) {
+ Setup()
+ // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json
+ enableTesting := utils.Cfg.ServiceSettings.EnableTesting
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableTesting = enableTesting
+ }()
+
+ utils.Cfg.ServiceSettings.EnableTesting = true
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel)
+
+ rs := Client.Must(Client.Command(channel.Id, "/loadtest posts fuzz 2 3 2", false)).Data.(*model.CommandResponse)
+ if rs.Text != "Adding posts..." {
+ t.Fatal(rs.Text)
+ }
+
+ time.Sleep(2 * time.Second)
+}
+
+func TestLoadTestUrlCommands(t *testing.T) {
+ Setup()
+ // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json
+ enableTesting := utils.Cfg.ServiceSettings.EnableTesting
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableTesting = enableTesting
+ }()
+
+ utils.Cfg.ServiceSettings.EnableTesting = true
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel)
+
+ command := "/loadtest url "
+ if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Command must contain a url" {
+ t.Fatal("/loadtest url with no url should've failed")
+ }
+
+ command = "/loadtest url http://missingfiletonwhere/path/asdf/qwerty"
+ if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Unable to get file" {
+ t.Log(r.Text)
+ t.Fatal("/loadtest url with invalid url should've failed")
+ }
+
+ command = "/loadtest url https://raw.githubusercontent.com/mattermost/platform/master/README.md"
+ if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Loading data..." {
+ t.Fatal("/loadtest url for README.md should've executed")
+ }
+
+ command = "/loadtest url test-emoticons.md"
+ if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Loading data..." {
+ t.Fatal("/loadtest url for test-emoticons.md should've executed")
+ }
+
+ command = "/loadtest url test-emoticons"
+ if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Loading data..." {
+ t.Fatal("/loadtest url for test-emoticons should've executed")
+ }
+
+ posts := Client.Must(Client.GetPosts(channel.Id, 0, 5, "")).Data.(*model.PostList)
+ // note that this may make more than 3 posts if files are too long to fit in an individual post
+ if len(posts.Order) < 3 {
+ t.Fatal("/loadtest url made too few posts, perhaps there needs to be a delay before GetPosts in the test?")
+ }
+
+ time.Sleep(2 * time.Second)
+}
diff --git a/api/command_logout.go b/api/command_logout.go
new file mode 100644
index 000000000..fb69b4f85
--- /dev/null
+++ b/api/command_logout.go
@@ -0,0 +1,37 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+type LogoutProvider struct {
+}
+
+const (
+ CMD_LOGOUT = "logout"
+)
+
+func init() {
+ RegisterCommandProvider(&LogoutProvider{})
+}
+
+func (me *LogoutProvider) GetTrigger() string {
+ return CMD_LOGOUT
+}
+
+func (me *LogoutProvider) GetCommand(c *Context) *model.Command {
+ return &model.Command{
+ Trigger: CMD_LOGOUT,
+ AutoComplete: true,
+ AutoCompleteDesc: c.T("api.command_logout.desc"),
+ AutoCompleteHint: "",
+ DisplayName: c.T("api.command_logout.name"),
+ }
+}
+
+func (me *LogoutProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ return &model.CommandResponse{GotoLocation: "/logout", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command_logout.success_message")}
+}
diff --git a/api/command_logout_test.go b/api/command_logout_test.go
new file mode 100644
index 000000000..86979316b
--- /dev/null
+++ b/api/command_logout_test.go
@@ -0,0 +1,32 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "testing"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+)
+
+func TestLogoutTestCommand(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ rs1 := Client.Must(Client.Command(channel1.Id, "/logout", false)).Data.(*model.CommandResponse)
+ if rs1.GotoLocation != "/logout" {
+ t.Fatal("failed to logout")
+ }
+}
diff --git a/api/command_me.go b/api/command_me.go
new file mode 100644
index 000000000..c6147278b
--- /dev/null
+++ b/api/command_me.go
@@ -0,0 +1,37 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+type MeProvider struct {
+}
+
+const (
+ CMD_ME = "me"
+)
+
+func init() {
+ RegisterCommandProvider(&MeProvider{})
+}
+
+func (me *MeProvider) GetTrigger() string {
+ return CMD_ME
+}
+
+func (me *MeProvider) GetCommand(c *Context) *model.Command {
+ return &model.Command{
+ Trigger: CMD_ME,
+ AutoComplete: true,
+ AutoCompleteDesc: c.T("api.command_me.desc"),
+ AutoCompleteHint: c.T("api.command_me.hint"),
+ DisplayName: c.T("api.command_me.name"),
+ }
+}
+
+func (me *MeProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, Text: "*" + message + "*"}
+}
diff --git a/api/command_me_test.go b/api/command_me_test.go
new file mode 100644
index 000000000..d55a15b2c
--- /dev/null
+++ b/api/command_me_test.go
@@ -0,0 +1,47 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "testing"
+ "time"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+)
+
+func TestMeCommand(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ testString := "/me hello"
+
+ r1 := Client.Must(Client.Command(channel1.Id, testString, false)).Data.(*model.CommandResponse)
+ if r1 == nil {
+ t.Fatal("Command failed to execute")
+ }
+
+ time.Sleep(100 * time.Millisecond)
+
+ p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
+ if len(p1.Order) != 1 {
+ t.Fatal("Command failed to send")
+ } else {
+ if p1.Posts[p1.Order[0]].Message != `*hello*` {
+ t.Log(p1.Posts[p1.Order[0]].Message)
+ t.Fatal("invalid shrug reponse")
+ }
+ }
+}
diff --git a/api/command_shrug.go b/api/command_shrug.go
new file mode 100644
index 000000000..8fb5bc200
--- /dev/null
+++ b/api/command_shrug.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+type ShrugProvider struct {
+}
+
+const (
+ CMD_SHRUG = "shrug"
+)
+
+func init() {
+ RegisterCommandProvider(&ShrugProvider{})
+}
+
+func (me *ShrugProvider) GetTrigger() string {
+ return CMD_SHRUG
+}
+
+func (me *ShrugProvider) GetCommand(c *Context) *model.Command {
+ return &model.Command{
+ Trigger: CMD_SHRUG,
+ AutoComplete: true,
+ AutoCompleteDesc: c.T("api.command_shrug.desc"),
+ AutoCompleteHint: c.T("api.command_shrug.hint"),
+ DisplayName: c.T("api.command_shrug.name"),
+ }
+}
+
+func (me *ShrugProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ rmsg := `¯\\\_(ツ)\_/¯`
+ if len(message) > 0 {
+ rmsg = message + " " + rmsg
+ }
+
+ return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, Text: rmsg}
+}
diff --git a/api/command_shrug_test.go b/api/command_shrug_test.go
new file mode 100644
index 000000000..92cecf664
--- /dev/null
+++ b/api/command_shrug_test.go
@@ -0,0 +1,47 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "testing"
+ "time"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+)
+
+func TestShrugCommand(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+
+ Client.LoginByEmail(team.Name, user1.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ testString := "/shrug"
+
+ r1 := Client.Must(Client.Command(channel1.Id, testString, false)).Data.(*model.CommandResponse)
+ if r1 == nil {
+ t.Fatal("Command failed to execute")
+ }
+
+ time.Sleep(100 * time.Millisecond)
+
+ p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
+ if len(p1.Order) != 1 {
+ t.Fatal("Command failed to send")
+ } else {
+ if p1.Posts[p1.Order[0]].Message != `¯\\\_(ツ)\_/¯` {
+ t.Log(p1.Posts[p1.Order[0]].Message)
+ t.Fatal("invalid shrug reponse")
+ }
+ }
+}
diff --git a/api/command_test.go b/api/command_test.go
index 86eb297d5..22e2bd666 100644
--- a/api/command_test.go
+++ b/api/command_test.go
@@ -4,7 +4,6 @@
package api
import (
- "strings"
"testing"
"time"
@@ -13,7 +12,7 @@ import (
"github.com/mattermost/platform/utils"
)
-func TestSuggestRootCommands(t *testing.T) {
+func TestListCommands(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
@@ -25,177 +24,197 @@ func TestSuggestRootCommands(t *testing.T) {
Client.LoginByEmail(team.Name, user1.Email, "pwd")
- if _, err := Client.Command("", "", true); err == nil {
- t.Fatal("Should fail")
- }
+ if results, err := Client.ListCommands(); err != nil {
+ t.Fatal(err)
+ } else {
+ commands := results.Data.([]*model.Command)
+ foundEcho := false
- rs1 := Client.Must(Client.Command("", "/", true)).Data.(*model.Command)
+ for _, command := range commands {
+ if command.Trigger == "echo" {
+ foundEcho = true
+ }
+ }
- hasLogout := false
- for _, v := range rs1.Suggestions {
- if v.Suggestion == "/logout" {
- hasLogout = true
+ if !foundEcho {
+ t.Fatal("Couldn't find echo command")
}
}
+}
- if !hasLogout {
- t.Log(rs1.Suggestions)
- t.Fatal("should have logout cmd")
- }
+func TestCreateCommand(t *testing.T) {
+ Setup()
- rs2 := Client.Must(Client.Command("", "/log", true)).Data.(*model.Command)
+ enableCommands := *utils.Cfg.ServiceSettings.EnableCommands
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableCommands = &enableCommands
+ }()
+ *utils.Cfg.ServiceSettings.EnableCommands = true
- if rs2.Suggestions[0].Suggestion != "/logout" {
- t.Fatal("should have logout cmd")
- }
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
- rs3 := Client.Must(Client.Command("", "/joi", true)).Data.(*model.Command)
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
- if rs3.Suggestions[0].Suggestion != "/join" {
- t.Fatal("should have join cmd")
- }
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
- rs4 := Client.Must(Client.Command("", "/ech", true)).Data.(*model.Command)
+ cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST}
- if rs4.Suggestions[0].Suggestion != "/echo" {
- t.Fatal("should have echo cmd")
+ if _, err := Client.CreateCommand(cmd); err == nil {
+ t.Fatal("should have failed because not admin")
}
-}
-func TestLogoutCommands(t *testing.T) {
- Setup()
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
- team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ var rcmd *model.Command
+ if result, err := Client.CreateCommand(cmd); err != nil {
+ t.Fatal(err)
+ } else {
+ rcmd = result.Data.(*model.Command)
+ }
- user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
- user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+ if rcmd.CreatorId != user.Id {
+ t.Fatal("user ids didn't match")
+ }
- Client.LoginByEmail(team.Name, user1.Email, "pwd")
+ if rcmd.TeamId != team.Id {
+ t.Fatal("team ids didn't match")
+ }
- rs1 := Client.Must(Client.Command("", "/logout", false)).Data.(*model.Command)
- if rs1.GotoLocation != "/logout" {
- t.Fatal("failed to logout")
+ cmd = &model.Command{CreatorId: "123", TeamId: "456", URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST}
+ if result, err := Client.CreateCommand(cmd); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.Command).CreatorId != user.Id {
+ t.Fatal("bad user id wasn't overwritten")
+ }
+ if result.Data.(*model.Command).TeamId != team.Id {
+ t.Fatal("bad team id wasn't overwritten")
+ }
}
}
-func TestJoinCommands(t *testing.T) {
+func TestListTeamCommands(t *testing.T) {
Setup()
+ enableCommands := *utils.Cfg.ServiceSettings.EnableCommands
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableCommands = &enableCommands
+ }()
+ *utils.Cfg.ServiceSettings.EnableCommands = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
- user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
- user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user1.Id))
-
- Client.LoginByEmail(team.Name, user1.Email, "pwd")
-
- channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
- channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
- Client.Must(Client.LeaveChannel(channel1.Id))
-
- channel2 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
- channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
- Client.Must(Client.LeaveChannel(channel2.Id))
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
- user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
- user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
- data := make(map[string]string)
- data["user_id"] = user2.Id
- channel3 := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.Channel)
+ cmd1 := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST}
+ cmd1 = Client.Must(Client.CreateCommand(cmd1)).Data.(*model.Command)
- rs1 := Client.Must(Client.Command("", "/join aa", true)).Data.(*model.Command)
- if rs1.Suggestions[0].Suggestion != "/join "+channel1.Name {
- t.Fatal("should have join cmd")
- }
+ if result, err := Client.ListTeamCommands(); err != nil {
+ t.Fatal(err)
+ } else {
+ cmds := result.Data.([]*model.Command)
- rs2 := Client.Must(Client.Command("", "/join bb", true)).Data.(*model.Command)
- if rs2.Suggestions[0].Suggestion != "/join "+channel2.Name {
- t.Fatal("should have join cmd")
+ if len(cmds) != 1 {
+ t.Fatal("incorrect number of cmd")
+ }
}
+}
- rs3 := Client.Must(Client.Command("", "/join", true)).Data.(*model.Command)
- if len(rs3.Suggestions) != 2 {
- t.Fatal("should have 2 join cmd")
- }
+func TestRegenToken(t *testing.T) {
+ Setup()
+ enableCommands := *utils.Cfg.ServiceSettings.EnableCommands
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableCommands = &enableCommands
+ }()
+ *utils.Cfg.ServiceSettings.EnableCommands = true
- rs4 := Client.Must(Client.Command("", "/join ", true)).Data.(*model.Command)
- if len(rs4.Suggestions) != 2 {
- t.Fatal("should have 2 join cmd")
- }
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
- rs5 := Client.Must(Client.Command("", "/join "+channel2.Name, false)).Data.(*model.Command)
- if !strings.HasSuffix(rs5.GotoLocation, "/"+team.Name+"/channels/"+channel2.Name) {
- t.Fatal("failed to join channel")
- }
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
- rs6 := Client.Must(Client.Command("", "/join "+channel3.Name, false)).Data.(*model.Command)
- if strings.HasSuffix(rs6.GotoLocation, "/"+team.Name+"/channels/"+channel3.Name) {
- t.Fatal("should not have joined direct message channel")
- }
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
- c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
+ cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST}
+ cmd = Client.Must(Client.CreateCommand(cmd)).Data.(*model.Command)
- if len(c1.Channels) != 4 { // 4 because of town-square, off-topic and direct
- t.Fatal("didn't join channel")
- }
+ data := make(map[string]string)
+ data["id"] = cmd.Id
- found := false
- for _, c := range c1.Channels {
- if c.Name == channel2.Name {
- found = true
- break
+ if result, err := Client.RegenCommandToken(data); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.Command).Token == cmd.Token {
+ t.Fatal("regen didn't work properly")
}
}
- if !found {
- t.Fatal("didn't join channel")
- }
}
-func TestEchoCommand(t *testing.T) {
+func TestDeleteCommand(t *testing.T) {
Setup()
+ enableCommands := *utils.Cfg.ServiceSettings.EnableCommands
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableCommands = &enableCommands
+ }()
+ *utils.Cfg.ServiceSettings.EnableCommands = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
- user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
- user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
- Client.LoginByEmail(team.Name, user1.Email, "pwd")
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
- channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
- channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+ cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST}
+ cmd = Client.Must(Client.CreateCommand(cmd)).Data.(*model.Command)
- echoTestString := "/echo test"
+ data := make(map[string]string)
+ data["id"] = cmd.Id
- r1 := Client.Must(Client.Command(channel1.Id, echoTestString, false)).Data.(*model.Command)
- if r1.Response != model.RESP_EXECUTED {
- t.Fatal("Echo command failed to execute")
+ if _, err := Client.DeleteCommand(data); err != nil {
+ t.Fatal(err)
}
- time.Sleep(100 * time.Millisecond)
-
- p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
- if len(p1.Order) != 1 {
- t.Fatal("Echo command failed to send")
+ cmds := Client.Must(Client.ListTeamCommands()).Data.([]*model.Command)
+ if len(cmds) != 0 {
+ t.Fatal("delete didn't work properly")
}
}
-func TestLoadTestUrlCommand(t *testing.T) {
+func TestTestCommand(t *testing.T) {
Setup()
-
- // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json
- enableTesting := utils.Cfg.ServiceSettings.EnableTesting
+ enableCommands := *utils.Cfg.ServiceSettings.EnableCommands
defer func() {
- utils.Cfg.ServiceSettings.EnableTesting = enableTesting
+ utils.Cfg.ServiceSettings.EnableCommands = &enableCommands
}()
-
- utils.Cfg.ServiceSettings.EnableTesting = true
+ *utils.Cfg.ServiceSettings.EnableCommands = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -204,39 +223,52 @@ func TestLoadTestUrlCommand(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
- channel := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
- channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel)
+ channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
- command := "/loadtest url "
- if _, err := Client.Command(channel.Id, command, false); err == nil {
- t.Fatal("/loadtest url with no url should've failed")
+ cmd1 := &model.Command{
+ URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + "/api/v1/commands/test",
+ Method: model.COMMAND_METHOD_POST,
+ Trigger: "test",
}
- // command = "/loadtest url http://www.hopefullynonexistent.file/path/asdf/qwerty"
- // if _, err := Client.Command(channel.Id, command, false); err == nil {
- // t.Fatal("/loadtest url with invalid url should've failed")
- // }
+ cmd1 = Client.Must(Client.CreateCommand(cmd1)).Data.(*model.Command)
- command = "/loadtest url https://raw.githubusercontent.com/mattermost/platform/master/README.md"
- if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.Command); r.Response != model.RESP_EXECUTED {
- t.Fatal("/loadtest url for README.md should've executed")
+ r1 := Client.Must(Client.Command(channel1.Id, "/test", false)).Data.(*model.CommandResponse)
+ if r1 == nil {
+ t.Fatal("Test command failed to execute")
}
- command = "/loadtest url test-emoticons.md"
- if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.Command); r.Response != model.RESP_EXECUTED {
- t.Fatal("/loadtest url for test-emoticons.md should've executed")
+ time.Sleep(100 * time.Millisecond)
+
+ p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
+ if len(p1.Order) != 1 {
+ t.Fatal("Test command failed to send")
}
- command = "/loadtest url test-emoticons"
- if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.Command); r.Response != model.RESP_EXECUTED {
- t.Fatal("/loadtest url for test-emoticons should've executed")
+ cmd2 := &model.Command{
+ URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + "/api/v1/commands/test",
+ Method: model.COMMAND_METHOD_GET,
+ Trigger: "test2",
}
- posts := Client.Must(Client.GetPosts(channel.Id, 0, 5, "")).Data.(*model.PostList)
- // note that this may make more than 3 posts if files are too long to fit in an individual post
- if len(posts.Order) < 3 {
- t.Fatal("/loadtest url made too few posts, perhaps there needs to be a delay before GetPosts in the test?")
+ cmd2 = Client.Must(Client.CreateCommand(cmd2)).Data.(*model.Command)
+
+ r2 := Client.Must(Client.Command(channel1.Id, "/test2", false)).Data.(*model.CommandResponse)
+ if r2 == nil {
+ t.Fatal("Test2 command failed to execute")
+ }
+
+ time.Sleep(100 * time.Millisecond)
+
+ p2 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
+ if len(p2.Order) != 2 {
+ t.Fatal("Test command failed to send")
}
}
diff --git a/api/user.go b/api/user.go
index 8b2df7143..9926f3ff3 100644
--- a/api/user.go
+++ b/api/user.go
@@ -1542,6 +1542,10 @@ func PermanentDeleteUser(c *Context, user *model.User) *model.AppError {
return result.Err
}
+ if result := <-Srv.Store.Command().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
if result := <-Srv.Store.Preference().PermanentDeleteByUser(user.Id); result.Err != nil {
return result.Err
}
diff --git a/api/webhook.go b/api/webhook.go
index 1372fe335..3906d09be 100644
--- a/api/webhook.go
+++ b/api/webhook.go
@@ -32,6 +32,14 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("createIncomingHook", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ }
+
c.LogAudit("attempt")
hook := model.IncomingWebhookFromJson(r.Body)
@@ -79,6 +87,14 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("deleteIncomingHook", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ }
+
c.LogAudit("attempt")
props := model.MapFromJson(r.Body)
@@ -116,7 +132,15 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Webhook().GetIncomingByUser(c.Session.UserId); result.Err != nil {
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("getIncomingHooks", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ }
+
+ if result := <-Srv.Store.Webhook().GetIncomingByTeam(c.Session.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -132,6 +156,14 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("createOutgoingHook", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ }
+
c.LogAudit("attempt")
hook := model.OutgoingWebhookFromJson(r.Body)
@@ -188,7 +220,15 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Webhook().GetOutgoingByCreator(c.Session.UserId); result.Err != nil {
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("getOutgoingHooks", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ }
+
+ if result := <-Srv.Store.Webhook().GetOutgoingByTeam(c.Session.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -204,6 +244,14 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("deleteOutgoingHook", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ }
+
c.LogAudit("attempt")
props := model.MapFromJson(r.Body)
@@ -241,6 +289,14 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request)
return
}
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ }
+
c.LogAudit("attempt")
props := model.MapFromJson(r.Body)
@@ -258,7 +314,7 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request)
} else {
hook = result.Data.(*model.OutgoingWebhook)
- if c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() {
+ if c.Session.TeamId != hook.TeamId && c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.webhook.regen_outgoing_token.permissions.app_error", nil, "user_id="+c.Session.UserId)
return
diff --git a/api/webhook_test.go b/api/webhook_test.go
index 0a464656b..4f85d178d 100644
--- a/api/webhook_test.go
+++ b/api/webhook_test.go
@@ -13,6 +13,14 @@ import (
func TestCreateIncomingHook(t *testing.T) {
Setup()
+ enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks
+ enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks
+ }()
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -21,6 +29,10 @@ func TestCreateIncomingHook(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
@@ -76,6 +88,14 @@ func TestCreateIncomingHook(t *testing.T) {
func TestListIncomingHooks(t *testing.T) {
Setup()
+ enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks
+ enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks
+ }()
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -84,6 +104,10 @@ func TestListIncomingHooks(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
@@ -114,6 +138,14 @@ func TestListIncomingHooks(t *testing.T) {
func TestDeleteIncomingHook(t *testing.T) {
Setup()
+ enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks
+ enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks
+ }()
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -122,6 +154,10 @@ func TestDeleteIncomingHook(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
@@ -154,6 +190,14 @@ func TestDeleteIncomingHook(t *testing.T) {
func TestCreateOutgoingHook(t *testing.T) {
Setup()
+ enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks
+ enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks
+ }()
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -162,6 +206,10 @@ func TestCreateOutgoingHook(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
@@ -217,6 +265,14 @@ func TestCreateOutgoingHook(t *testing.T) {
func TestListOutgoingHooks(t *testing.T) {
Setup()
+ enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks
+ enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks
+ }()
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -225,6 +281,10 @@ func TestListOutgoingHooks(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
@@ -255,6 +315,14 @@ func TestListOutgoingHooks(t *testing.T) {
func TestDeleteOutgoingHook(t *testing.T) {
Setup()
+ enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks
+ enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks
+ }()
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -263,6 +331,10 @@ func TestDeleteOutgoingHook(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
@@ -295,6 +367,14 @@ func TestDeleteOutgoingHook(t *testing.T) {
func TestRegenOutgoingHookToken(t *testing.T) {
Setup()
+ enableIncomingHooks := utils.Cfg.ServiceSettings.EnableIncomingWebhooks
+ enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = enableIncomingHooks
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks
+ }()
+ utils.Cfg.ServiceSettings.EnableIncomingWebhooks = true
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -303,6 +383,10 @@ func TestRegenOutgoingHookToken(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
diff --git a/config/config.json b/config/config.json
index 560073ad2..5ed05fecd 100644
--- a/config/config.json
+++ b/config/config.json
@@ -7,6 +7,8 @@
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": false,
"EnableOutgoingWebhooks": false,
+ "EnableCommands": false,
+ "EnableOnlyAdminIntegrations": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index 80b99b66d..e831bbb3a 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -7,6 +7,8 @@
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": false,
"EnableOutgoingWebhooks": false,
+ "EnableCommands": false,
+ "EnableOnlyAdminIntegrations": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index 80b99b66d..e831bbb3a 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -7,6 +7,8 @@
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": false,
"EnableOutgoingWebhooks": false,
+ "EnableCommands": false,
+ "EnableOnlyAdminIntegrations": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
diff --git a/i18n/en.json b/i18n/en.json
index 2d86e1ee5..d72d6dca5 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -244,120 +244,140 @@
"translation": "Tried to perform an invalid update of the default channel {{.Channel}}"
},
{
- "id": "api.command.check_command.start.app_error",
- "translation": "Command must start with /"
+ "id": "api.command.init.debug",
+ "translation": "Initializing command api routes"
},
{
- "id": "api.command.echo_command.create.error",
- "translation": "Unable to create /echo post, err=%v"
+ "id": "api.command.execute_command.start.app_error",
+ "translation": "No command trigger found"
},
{
- "id": "api.command.echo_command.description",
- "translation": "Echo back text from your account, /echo \"message\" [delay in seconds]"
+ "id": "api.command.execute_command.debug",
+ "translation": "Executing cmd=%v userId=%v"
},
{
- "id": "api.command.echo_command.high_volume.app_error",
- "translation": "High volume of echo request, cannot process request"
+ "id": "api.command.execute_command.failed.app_error",
+ "translation": "Command with a trigger of '{{.Trigger}}' failed"
},
{
- "id": "api.command.echo_command.under.app_error",
- "translation": "Delays must be under 10000 seconds"
+ "id": "api.command.execute_command.failed_empty.app_error",
+ "translation": "Command with a trigger of '{{.Trigger}}' returned an empty response"
},
{
- "id": "api.command.init.debug",
- "translation": "Initializing command api routes"
+ "id": "api.command.execute_command.failed_resp.app_error",
+ "translation": "Command with a trigger of '{{.Trigger}}' returned response {{.Status}}"
},
{
- "id": "api.command.load_test_channels_command.channel.description",
- "translation": "Add a specified number of random channels to current team <MinChannels> <MaxChannels>"
+ "id": "api.command.execute_command.not_found.app_error",
+ "translation": "Command with a trigger of '{{.Trigger}}' not found"
},
{
- "id": "api.command.load_test_channels_command.fuzz.description",
- "translation": "Add a specified number of random channels with fuzz text to current team <Min Channels> <Max Channels>"
+ "id": "api.command.execute_command.save.app_error",
+ "translation": "An error while saving the command response to the channel"
},
{
- "id": "api.command.load_test_command.description",
- "translation": "Debug Load Testing"
+ "id": "api.command.disabled.app_error",
+ "translation": "Commands have been disabled by the system admin."
},
{
- "id": "api.command.load_test_posts_command.fuzz.description",
- "translation": "Add some random posts with fuzz text to current channel <Min Posts> <Max Posts> <Min Images> <Max Images>"
+ "id": "api.command.admin_only.app_error",
+ "translation": "Integrations have been limited to admins only."
},
{
- "id": "api.command.load_test_posts_command.posts.description",
- "translation": "Add some random posts to current channel <Min Posts> <Max Posts> <Min Images> <Max Images>"
+ "id": "api.command.regen.app_error",
+ "translation": "Inappropriate permissions to regenerate command token"
},
{
- "id": "api.command.load_test_setup_command.create.error",
- "translation": "Failed to create testing environment"
+ "id": "api.command.delete.app_error",
+ "translation": "Inappropriate permissions to delete command"
},
{
- "id": "api.command.load_test_setup_command.created.info",
- "translation": "Team Created: %v"
+ "id": "api.command_echo.desc",
+ "translation": "Echo back text from your account"
},
{
- "id": "api.command.load_test_setup_command.description",
- "translation": "Creates a testing environment in current team. [teams] [fuzz] <Num Channels> <Num Users> <NumPosts>"
+ "id": "api.command_echo.hint",
+ "translation": "'message' [delay in seconds]"
},
{
- "id": "api.command.load_test_setup_command.login.info",
- "translation": "\t User to login: %v, %v"
+ "id": "api.command_echo.name",
+ "translation": "echo"
},
{
- "id": "api.command.load_test_url_command.create.error",
- "translation": "Unable to create post, err=%v"
+ "id": "api.command_echo.delay.app_error",
+ "translation": "Delays must be under 10000 seconds"
},
{
- "id": "api.command.load_test_url_command.description",
- "translation": "Add a post containing the text from a given url to current channel <Url>"
+ "id": "api.command_echo.high_volume.app_error",
+ "translation": "High volume of echo request, cannot process request"
},
{
- "id": "api.command.load_test_url_command.file.app_error",
- "translation": "Unable to get file"
+ "id": "api.command_echo.create.app_error",
+ "translation": "Unable to create /echo post, err=%v"
},
{
- "id": "api.command.load_test_url_command.reading.app_error",
- "translation": "Encountered error reading file"
+ "id": "api.command_join.desc",
+ "translation": "Join the open channel"
},
{
- "id": "api.command.load_test_url_command.url.app_error",
- "translation": "Command must contain a url"
+ "id": "api.command_join.hint",
+ "translation": "[channel-name]"
},
{
- "id": "api.command.load_test_users_command.fuzz.description",
- "translation": "Add a specified number of random users with fuzz text to current team <Min Users> <Max Users>"
+ "id": "api.command_join.name",
+ "translation": "join"
},
{
- "id": "api.command.load_test_users_command.users.description",
- "translation": "Add a specified number of random users to current team <Min Users> <Max Users>"
+ "id": "api.command_join.list.app_error",
+ "translation": "An error occured while listing channels."
},
{
- "id": "api.command.logout_command.description",
- "translation": "Logout"
+ "id": "api.command_join.fail.app_error",
+ "translation": "An error occured while joining the channel."
},
{
- "id": "api.command.me_command.create.error",
- "translation": "Unable to create /me post post, err=%v"
+ "id": "api.command_join.success",
+ "translation": "Joined channel."
},
{
- "id": "api.command.me_command.description",
- "translation": "Do an action, /me [message]"
+ "id": "api.command_join.missing.app_error",
+ "translation": "We couldn't find the channel"
},
{
- "id": "api.command.no_implemented.app_error",
- "translation": "Command not implemented"
+ "id": "api.command_logout.desc",
+ "translation": "Logout of Mattermost"
},
{
- "id": "api.command.shrug_command.create.error",
- "translation": "Unable to create /shrug post post, err=%v"
+ "id": "api.command_logout.name",
+ "translation": "logout"
},
{
- "id": "api.command.shrug_command.description",
- "translation": "Adds ¯\\_(ツ)_/¯ to your message, /shrug [message]"
+ "id": "api.command_logout.success_message",
+ "translation": "Logging out..."
},
{
- "id": "api.commmand.join_command.description",
- "translation": "Join the open channel"
+ "id": "api.command_me.desc",
+ "translation": "Do an action"
+ },
+ {
+ "id": "api.command_me.hint",
+ "translation": "[message]"
+ },
+ {
+ "id": "api.command_me.name",
+ "translation": "me"
+ },
+ {
+ "id": "api.command_shrug.desc",
+ "translation": "Adds ¯\\_(ツ)_/¯ to your message"
+ },
+ {
+ "id": "api.command_shrug.hint",
+ "translation": "[message]"
+ },
+ {
+ "id": "api.command_shrug.name",
+ "translation": "shrug"
},
{
"id": "api.context.404.app_error",
@@ -1808,6 +1828,46 @@
"translation": "Name must be 2 or more lowercase alphanumeric characters"
},
{
+ "id": "model.command.is_valid.id.app_error",
+ "translation": "Invalid Id"
+ },
+ {
+ "id": "model.command.is_valid.token.app_error",
+ "translation": "Invalid token"
+ },
+ {
+ "id": "model.command.is_valid.create_at.app_error",
+ "translation": "Create at must be a valid time"
+ },
+ {
+ "id": "model.command.is_valid.update_at.app_error",
+ "translation": "Update at must be a valid time"
+ },
+ {
+ "id": "model.command.is_valid.user_id.app_error",
+ "translation": "Invalid user id"
+ },
+ {
+ "id": "model.command.is_valid.team_id.app_error",
+ "translation": "Invalid team id"
+ },
+ {
+ "id": "model.command.is_valid.trigger.app_error",
+ "translation": "Invalid trigger"
+ },
+ {
+ "id": "model.command.is_valid.url.app_error",
+ "translation": "Invalid url"
+ },
+ {
+ "id": "model.command.is_valid.url_http.app_error",
+ "translation": "Invalid URL. Must be a valid URL and start with http:// or https://"
+ },
+ {
+ "id": "model.command.is_valid.method.app_error",
+ "translation": "Invalid Method"
+ },
+ {
"id": "model.channel.is_valid.create_at.app_error",
"translation": "Create at must be a valid time"
},
@@ -2376,6 +2436,34 @@
"translation": "We encountered an error saving the audit"
},
{
+ "id": "store.sql_command.save.saving_overwrite.app_error",
+ "translation": "You cannot overwrite an existing Command"
+ },
+ {
+ "id": "store.sql_command.save.saving.app_error",
+ "translation": "We couldn't save the Command"
+ },
+ {
+ "id": "store.sql_command.save.get.app_error",
+ "translation": "We couldn't get the command"
+ },
+ {
+ "id": "store.sql_command.save.get_team.app_error",
+ "translation": "We couldn't get the commands"
+ },
+ {
+ "id": "store.sql_command.save.delete.app_error",
+ "translation": "We couldn't delete the command"
+ },
+ {
+ "id": "store.sql_command.save.delete_perm.app_error",
+ "translation": "We couldn't delete the command"
+ },
+ {
+ "id": "store.sql_command.save.update.app_error",
+ "translation": "We couldn't update the command"
+ },
+ {
"id": "store.sql_channel.analytics_type_count.app_error",
"translation": "We couldn't get channel type counts"
},
@@ -3044,10 +3132,6 @@
"translation": "We couldn't get the webhooks"
},
{
- "id": "store.sql_webhooks.get_outgoing_by_creator.app_error",
- "translation": "We couldn't get the webhooks"
- },
- {
"id": "store.sql_webhooks.get_outgoing_by_team.app_error",
"translation": "We couldn't get the webhooks"
},
diff --git a/i18n/es.json b/i18n/es.json
index 9599fe879..5c1998234 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -244,122 +244,6 @@
"translation": "Intento de realizar una actualización inválida al canal predeterminado {{.Channel}}"
},
{
- "id": "api.command.check_command.start.app_error",
- "translation": "El comando debe comenzar con /"
- },
- {
- "id": "api.command.echo_command.create.error",
- "translation": "No se puede crear /echo mensaje, err=%v"
- },
- {
- "id": "api.command.echo_command.description",
- "translation": "Echo del texto desde tu cuenta, /echo \"mensaje\" [retraso en segundos]"
- },
- {
- "id": "api.command.echo_command.high_volume.app_error",
- "translation": "Volumen alto de solicitudes echo, no se puede procesar la solicitud"
- },
- {
- "id": "api.command.echo_command.under.app_error",
- "translation": "El retraso debe ser menor a 10000 segundos"
- },
- {
- "id": "api.command.init.debug",
- "translation": "Inicializando rutas del API para los comandos"
- },
- {
- "id": "api.command.load_test_channels_command.channel.description",
- "translation": "Agrega un número específico de canales aleatorios al equipo actual <MinChannels> <MaxChannels>"
- },
- {
- "id": "api.command.load_test_channels_command.fuzz.description",
- "translation": "Agrega un número específico de canales aleatorios con el texto fuzz al equipo actual <Min Channels> <Max Channels>"
- },
- {
- "id": "api.command.load_test_command.description",
- "translation": "Depurar pruebas de carga"
- },
- {
- "id": "api.command.load_test_posts_command.fuzz.description",
- "translation": "Agrega algunos mensajes aleatorios con el texto fuzz al canal actual <Min Mensajes> <Max Mensajes> <Min Images> <Max Imágenes>"
- },
- {
- "id": "api.command.load_test_posts_command.posts.description",
- "translation": "Agrega algunos mensajes aleatorios al canal actual <Min Mensajes> <Max Mensajes> <Min Images> <Max Imágenes>"
- },
- {
- "id": "api.command.load_test_setup_command.create.error",
- "translation": "Falla al crear el entorno de pruebas"
- },
- {
- "id": "api.command.load_test_setup_command.created.info",
- "translation": "Equipo Creado: %v"
- },
- {
- "id": "api.command.load_test_setup_command.description",
- "translation": "Crea un entorno de prueba para el equipo actual. [equipos] [fuzz] <Cant Canales> <Cant Usuarios> <Num Mensajes>"
- },
- {
- "id": "api.command.load_test_setup_command.login.info",
- "translation": "\t Usuario que inicia sesión: %v, %v"
- },
- {
- "id": "api.command.load_test_url_command.create.error",
- "translation": "No se pudo crear el mensaje, err=%v"
- },
- {
- "id": "api.command.load_test_url_command.description",
- "translation": "Agraga un mensaje que contiene el texto de un url dado al canal actual <Url>"
- },
- {
- "id": "api.command.load_test_url_command.file.app_error",
- "translation": "No se puede obtener el archivo"
- },
- {
- "id": "api.command.load_test_url_command.reading.app_error",
- "translation": "Se encontró un error leyendo el archivo"
- },
- {
- "id": "api.command.load_test_url_command.url.app_error",
- "translation": "El comando debe contener un url"
- },
- {
- "id": "api.command.load_test_users_command.fuzz.description",
- "translation": "Agraga un número específico de usuarios aleatorios con el texto fuzz al equipo actual <Min Users> <Max Users>"
- },
- {
- "id": "api.command.load_test_users_command.users.description",
- "translation": "Agrega un número específico de usuarios aleatorios al equipo actual <Min Users> <Max Users>"
- },
- {
- "id": "api.command.logout_command.description",
- "translation": "Cerrar sesión"
- },
- {
- "id": "api.command.me_command.create.error",
- "translation": "No se pudo crear el mensaje /me mensaje, err=%v"
- },
- {
- "id": "api.command.me_command.description",
- "translation": "Realiza una acción, /me [mensaje]"
- },
- {
- "id": "api.command.no_implemented.app_error",
- "translation": "Comando no implementado"
- },
- {
- "id": "api.command.shrug_command.create.error",
- "translation": "No se pudo crear el mensaje /shrug mensaje, err=%v"
- },
- {
- "id": "api.command.shrug_command.description",
- "translation": "Agrega ¯\\_(ツ)_/¯ a tu mensaje, /shrug [mensaje]"
- },
- {
- "id": "api.commmand.join_command.description",
- "translation": "Unirme al canal abierto"
- },
- {
"id": "api.context.404.app_error",
"translation": "Lo sentimos, pero no pudimos encontrar la página."
},
@@ -3052,10 +2936,6 @@
"translation": "No pudimos obtener los webhooks"
},
{
- "id": "store.sql_webhooks.get_outgoing_by_creator.app_error",
- "translation": "No pudimos obtener los webhooks"
- },
- {
"id": "store.sql_webhooks.get_outgoing_by_team.app_error",
"translation": "No pudimos obtener los webhooks"
},
diff --git a/model/client.go b/model/client.go
index a6a527963..560e47b76 100644
--- a/model/client.go
+++ b/model/client.go
@@ -380,7 +380,43 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul
m["command"] = command
m["channelId"] = channelId
m["suggest"] = strconv.FormatBool(suggest)
- if r, err := c.DoApiPost("/command", MapToJson(m)); err != nil {
+ if r, err := c.DoApiPost("/commands/execute", MapToJson(m)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), CommandResponseFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) ListCommands() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/commands/list", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), CommandListFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) ListTeamCommands() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/commands/list_team_commands", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), CommandListFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) CreateCommand(cmd *Command) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/commands/create", cmd.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), CommandFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) RegenCommandToken(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/commands/regen_token", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -388,6 +424,15 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul
}
}
+func (c *Client) DeleteCommand(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/commands/delete", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) {
if r, err := c.DoApiGet("/users/"+id+"/audits", "", etag); err != nil {
return nil, err
diff --git a/model/command.go b/model/command.go
index 5aec5f534..56d88f13c 100644
--- a/model/command.go
+++ b/model/command.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
@@ -9,28 +9,27 @@ import (
)
const (
- RESP_EXECUTED = "executed"
- RESP_NOT_IMPLEMENTED = "not implemented"
+ COMMAND_METHOD_POST = "P"
+ COMMAND_METHOD_GET = "G"
)
type Command struct {
- Command string `json:"command"`
- Response string `json:"response"`
- GotoLocation string `json:"goto_location"`
- ChannelId string `json:"channel_id"`
- Suggest bool `json:"-"`
- Suggestions []*SuggestCommand `json:"suggestions"`
-}
-
-func (o *Command) AddSuggestion(suggest *SuggestCommand) {
-
- if o.Suggest {
- if o.Suggestions == nil {
- o.Suggestions = make([]*SuggestCommand, 0, 128)
- }
-
- o.Suggestions = append(o.Suggestions, suggest)
- }
+ Id string `json:"id"`
+ Token string `json:"token"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ CreatorId string `json:"creator_id"`
+ TeamId string `json:"team_id"`
+ Trigger string `json:"trigger"`
+ Method string `json:"method"`
+ Username string `json:"username"`
+ IconURL string `json:"icon_url"`
+ AutoComplete bool `json:"auto_complete"`
+ AutoCompleteDesc string `json:"auto_complete_desc"`
+ AutoCompleteHint string `json:"auto_complete_hint"`
+ DisplayName string `json:"display_name"`
+ URL string `json:"url"`
}
func (o *Command) ToJson() string {
@@ -52,3 +51,94 @@ func CommandFromJson(data io.Reader) *Command {
return nil
}
}
+
+func CommandListToJson(l []*Command) string {
+ b, err := json.Marshal(l)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func CommandListFromJson(data io.Reader) []*Command {
+ decoder := json.NewDecoder(data)
+ var o []*Command
+ err := decoder.Decode(&o)
+ if err == nil {
+ return o
+ } else {
+ return nil
+ }
+}
+
+func (o *Command) IsValid() *AppError {
+
+ if len(o.Id) != 26 {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.id.app_error", nil, "")
+ }
+
+ if len(o.Token) != 26 {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.token.app_error", nil, "")
+ }
+
+ if o.CreateAt == 0 {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.create_at.app_error", nil, "")
+ }
+
+ if o.UpdateAt == 0 {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.update_at.app_error", nil, "")
+ }
+
+ if len(o.CreatorId) != 26 {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.user_id.app_error", nil, "")
+ }
+
+ if len(o.TeamId) != 26 {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.team_id.app_error", nil, "")
+ }
+
+ if len(o.Trigger) > 1024 {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.trigger.app_error", nil, "")
+ }
+
+ if len(o.URL) == 0 || len(o.URL) > 1024 {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.url.app_error", nil, "")
+ }
+
+ if !IsValidHttpUrl(o.URL) {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.url_http.app_error", nil, "")
+ }
+
+ if !(o.Method == COMMAND_METHOD_GET || o.Method == COMMAND_METHOD_POST) {
+ return NewLocAppError("Command.IsValid", "model.command.is_valid.method.app_error", nil, "")
+ }
+
+ return nil
+}
+
+func (o *Command) PreSave() {
+ if o.Id == "" {
+ o.Id = NewId()
+ }
+
+ if o.Token == "" {
+ o.Token = NewId()
+ }
+
+ o.CreateAt = GetMillis()
+ o.UpdateAt = o.CreateAt
+}
+
+func (o *Command) PreUpdate() {
+ o.UpdateAt = GetMillis()
+}
+
+func (o *Command) Sanitize() {
+ o.Token = ""
+ o.CreatorId = ""
+ o.Method = ""
+ o.URL = ""
+ o.Username = ""
+ o.IconURL = ""
+}
diff --git a/model/command_response.go b/model/command_response.go
new file mode 100644
index 000000000..9314f38ef
--- /dev/null
+++ b/model/command_response.go
@@ -0,0 +1,41 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+const (
+ COMMAND_RESPONSE_TYPE_IN_CHANNEL = "in_channel"
+ COMMAND_RESPONSE_TYPE_EPHEMERAL = "ephemeral"
+)
+
+type CommandResponse struct {
+ ResponseType string `json:"response_type"`
+ Text string `json:"text"`
+ GotoLocation string `json:"goto_location"`
+ Attachments interface{} `json:"attachments"`
+}
+
+func (o *CommandResponse) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func CommandResponseFromJson(data io.Reader) *CommandResponse {
+ decoder := json.NewDecoder(data)
+ var o CommandResponse
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
diff --git a/model/command_response_test.go b/model/command_response_test.go
new file mode 100644
index 000000000..7aa3e984b
--- /dev/null
+++ b/model/command_response_test.go
@@ -0,0 +1,19 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestCommandResponseJson(t *testing.T) {
+ o := CommandResponse{Text: "test"}
+ json := o.ToJson()
+ ro := CommandResponseFromJson(strings.NewReader(json))
+
+ if o.Text != ro.Text {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/model/command_test.go b/model/command_test.go
index 61302ea10..0581625d9 100644
--- a/model/command_test.go
+++ b/model/command_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
@@ -9,17 +9,89 @@ import (
)
func TestCommandJson(t *testing.T) {
+ o := Command{Id: NewId()}
+ json := o.ToJson()
+ ro := CommandFromJson(strings.NewReader(json))
- command := &Command{Command: NewId(), Suggest: true}
- command.AddSuggestion(&SuggestCommand{Suggestion: NewId()})
- json := command.ToJson()
- result := CommandFromJson(strings.NewReader(json))
-
- if command.Command != result.Command {
+ if o.Id != ro.Id {
t.Fatal("Ids do not match")
}
+}
- if command.Suggestions[0].Suggestion != result.Suggestions[0].Suggestion {
- t.Fatal("Ids do not match")
+func TestCommandIsValid(t *testing.T) {
+ o := Command{}
+
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Id = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CreateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.UpdateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CreatorId = "123"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CreatorId = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Token = "123"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
}
+
+ o.Token = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.TeamId = "123"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.TeamId = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.URL = "nowhere.com/"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.URL = "http://nowhere.com/"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Method = COMMAND_METHOD_GET
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestCommandPreSave(t *testing.T) {
+ o := Command{}
+ o.PreSave()
+}
+
+func TestCommandPreUpdate(t *testing.T) {
+ o := Command{}
+ o.PreUpdate()
}
diff --git a/model/config.go b/model/config.go
index a6d1c21dc..acb525abf 100644
--- a/model/config.go
+++ b/model/config.go
@@ -24,24 +24,26 @@ const (
)
type ServiceSettings struct {
- ListenAddress string
- MaximumLoginAttempts int
- SegmentDeveloperKey string
- GoogleDeveloperKey string
- EnableOAuthServiceProvider bool
- EnableIncomingWebhooks bool
- EnableOutgoingWebhooks bool
- EnablePostUsernameOverride bool
- EnablePostIconOverride bool
- EnableTesting bool
- EnableDeveloper *bool
- EnableSecurityFixAlert *bool
- SessionLengthWebInDays *int
- SessionLengthMobileInDays *int
- SessionLengthSSOInDays *int
- SessionCacheInMinutes *int
- WebsocketSecurePort *int
- WebsocketPort *int
+ ListenAddress string
+ MaximumLoginAttempts int
+ SegmentDeveloperKey string
+ GoogleDeveloperKey string
+ EnableOAuthServiceProvider bool
+ EnableIncomingWebhooks bool
+ EnableOutgoingWebhooks bool
+ EnableCommands *bool
+ EnableOnlyAdminIntegrations *bool
+ EnablePostUsernameOverride bool
+ EnablePostIconOverride bool
+ EnableTesting bool
+ EnableDeveloper *bool
+ EnableSecurityFixAlert *bool
+ SessionLengthWebInDays *int
+ SessionLengthMobileInDays *int
+ SessionLengthSSOInDays *int
+ SessionCacheInMinutes *int
+ WebsocketSecurePort *int
+ WebsocketPort *int
}
type SSOSettings struct {
@@ -349,10 +351,22 @@ func (o *Config) SetDefaults() {
o.ServiceSettings.SessionCacheInMinutes = new(int)
*o.ServiceSettings.SessionCacheInMinutes = 10
}
+
+ if o.ServiceSettings.EnableCommands == nil {
+ o.ServiceSettings.EnableCommands = new(bool)
+ *o.ServiceSettings.EnableCommands = false
+ }
+
+ if o.ServiceSettings.EnableOnlyAdminIntegrations == nil {
+ o.ServiceSettings.EnableOnlyAdminIntegrations = new(bool)
+ *o.ServiceSettings.EnableOnlyAdminIntegrations = true
+ }
+
if o.ServiceSettings.WebsocketPort == nil {
o.ServiceSettings.WebsocketPort = new(int)
*o.ServiceSettings.WebsocketPort = 80
}
+
if o.ServiceSettings.WebsocketSecurePort == nil {
o.ServiceSettings.WebsocketSecurePort = new(int)
*o.ServiceSettings.WebsocketSecurePort = 443
diff --git a/store/sql_command_store.go b/store/sql_command_store.go
new file mode 100644
index 000000000..760235e10
--- /dev/null
+++ b/store/sql_command_store.go
@@ -0,0 +1,173 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+type SqlCommandStore struct {
+ *SqlStore
+}
+
+func NewSqlCommandStore(sqlStore *SqlStore) CommandStore {
+ s := &SqlCommandStore{sqlStore}
+
+ for _, db := range sqlStore.GetAllConns() {
+ tableo := db.AddTableWithName(model.Command{}, "Commands").SetKeys(false, "Id")
+ tableo.ColMap("Id").SetMaxSize(26)
+ tableo.ColMap("Token").SetMaxSize(26)
+ tableo.ColMap("CreatorId").SetMaxSize(26)
+ tableo.ColMap("TeamId").SetMaxSize(26)
+ tableo.ColMap("Trigger").SetMaxSize(128)
+ tableo.ColMap("URL").SetMaxSize(1024)
+ tableo.ColMap("Method").SetMaxSize(1)
+ tableo.ColMap("Username").SetMaxSize(64)
+ tableo.ColMap("IconURL").SetMaxSize(1024)
+ tableo.ColMap("AutoCompleteDesc").SetMaxSize(1024)
+ tableo.ColMap("AutoCompleteHint").SetMaxSize(1024)
+ tableo.ColMap("DisplayName").SetMaxSize(64)
+ }
+
+ return s
+}
+
+func (s SqlCommandStore) UpgradeSchemaIfNeeded() {
+}
+
+func (s SqlCommandStore) CreateIndexesIfNotExists() {
+ s.CreateIndexIfNotExists("idx_command_team_id", "Commands", "TeamId")
+}
+
+func (s SqlCommandStore) Save(command *model.Command) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if len(command.Id) > 0 {
+ result.Err = model.NewLocAppError("SqlCommandStore.Save", "store.sql_command.save.saving_overwrite.app_error", nil, "id="+command.Id)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ command.PreSave()
+ if result.Err = command.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if err := s.GetMaster().Insert(command); err != nil {
+ result.Err = model.NewLocAppError("SqlCommandStore.Save", "store.sql_command.save.saving.app_error", nil, "id="+command.Id+", "+err.Error())
+ } else {
+ result.Data = command
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlCommandStore) Get(id string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var command model.Command
+
+ if err := s.GetReplica().SelectOne(&command, "SELECT * FROM Commands WHERE Id = :Id AND DeleteAt = 0", map[string]interface{}{"Id": id}); err != nil {
+ result.Err = model.NewLocAppError("SqlCommandStore.Get", "store.sql_command.save.get.app_error", nil, "id="+id+", err="+err.Error())
+ }
+
+ result.Data = &command
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlCommandStore) GetByTeam(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var commands []*model.Command
+
+ if _, err := s.GetReplica().Select(&commands, "SELECT * FROM Commands WHERE TeamId = :TeamId AND DeleteAt = 0", map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewLocAppError("SqlCommandStore.GetByTeam", "store.sql_command.save.get_team.app_error", nil, "teamId="+teamId+", err="+err.Error())
+ }
+
+ result.Data = commands
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlCommandStore) Delete(commandId string, time int64) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("Update Commands SET DeleteAt = :DeleteAt, UpdateAt = :UpdateAt WHERE Id = :Id", map[string]interface{}{"DeleteAt": time, "UpdateAt": time, "Id": commandId})
+ if err != nil {
+ result.Err = model.NewLocAppError("SqlCommandStore.Delete", "store.sql_command.save.delete.app_error", nil, "id="+commandId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlCommandStore) PermanentDeleteByUser(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("DELETE FROM Commands WHERE CreatorId = :UserId", map[string]interface{}{"UserId": userId})
+ if err != nil {
+ result.Err = model.NewLocAppError("SqlCommandStore.DeleteByUser", "store.sql_command.save.delete_perm.app_error", nil, "id="+userId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlCommandStore) Update(hook *model.Command) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ hook.UpdateAt = model.GetMillis()
+
+ if _, err := s.GetMaster().Update(hook); err != nil {
+ result.Err = model.NewLocAppError("SqlCommandStore.Update", "store.sql_command.save.update.app_error", nil, "id="+hook.Id+", "+err.Error())
+ } else {
+ result.Data = hook
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_command_store_test.go b/store/sql_command_store_test.go
new file mode 100644
index 000000000..b4610d4aa
--- /dev/null
+++ b/store/sql_command_store_test.go
@@ -0,0 +1,155 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "testing"
+)
+
+func TestCommandStoreSave(t *testing.T) {
+ Setup()
+
+ o1 := model.Command{}
+ o1.CreatorId = model.NewId()
+ o1.Method = model.COMMAND_METHOD_POST
+ o1.TeamId = model.NewId()
+ o1.URL = "http://nowhere.com/"
+
+ if err := (<-store.Command().Save(&o1)).Err; err != nil {
+ t.Fatal("couldn't save item", err)
+ }
+
+ if err := (<-store.Command().Save(&o1)).Err; err == nil {
+ t.Fatal("shouldn't be able to update from save")
+ }
+}
+
+func TestCommandStoreGet(t *testing.T) {
+ Setup()
+
+ o1 := &model.Command{}
+ o1.CreatorId = model.NewId()
+ o1.Method = model.COMMAND_METHOD_POST
+ o1.TeamId = model.NewId()
+ o1.URL = "http://nowhere.com/"
+
+ o1 = (<-store.Command().Save(o1)).Data.(*model.Command)
+
+ if r1 := <-store.Command().Get(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Command).CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned command")
+ }
+ }
+
+ if err := (<-store.Command().Get("123")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestCommandStoreGetByTeam(t *testing.T) {
+ Setup()
+
+ o1 := &model.Command{}
+ o1.CreatorId = model.NewId()
+ o1.Method = model.COMMAND_METHOD_POST
+ o1.TeamId = model.NewId()
+ o1.URL = "http://nowhere.com/"
+
+ o1 = (<-store.Command().Save(o1)).Data.(*model.Command)
+
+ if r1 := <-store.Command().GetByTeam(o1.TeamId); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.([]*model.Command)[0].CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned command")
+ }
+ }
+
+ if result := <-store.Command().GetByTeam("123"); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ if len(result.Data.([]*model.Command)) != 0 {
+ t.Fatal("no commands should have returned")
+ }
+ }
+}
+
+func TestCommandStoreDelete(t *testing.T) {
+ Setup()
+
+ o1 := &model.Command{}
+ o1.CreatorId = model.NewId()
+ o1.Method = model.COMMAND_METHOD_POST
+ o1.TeamId = model.NewId()
+ o1.URL = "http://nowhere.com/"
+
+ o1 = (<-store.Command().Save(o1)).Data.(*model.Command)
+
+ if r1 := <-store.Command().Get(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Command).CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned command")
+ }
+ }
+
+ if r2 := <-store.Command().Delete(o1.Id, model.GetMillis()); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Command().Get(o1.Id)); r3.Err == nil {
+ t.Log(r3.Data)
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestCommandStoreDeleteByUser(t *testing.T) {
+ Setup()
+
+ o1 := &model.Command{}
+ o1.CreatorId = model.NewId()
+ o1.Method = model.COMMAND_METHOD_POST
+ o1.TeamId = model.NewId()
+ o1.URL = "http://nowhere.com/"
+
+ o1 = (<-store.Command().Save(o1)).Data.(*model.Command)
+
+ if r1 := <-store.Command().Get(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Command).CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned command")
+ }
+ }
+
+ if r2 := <-store.Command().PermanentDeleteByUser(o1.CreatorId); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Command().Get(o1.Id)); r3.Err == nil {
+ t.Log(r3.Data)
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestCommandStoreUpdate(t *testing.T) {
+ Setup()
+
+ o1 := &model.Command{}
+ o1.CreatorId = model.NewId()
+ o1.Method = model.COMMAND_METHOD_POST
+ o1.TeamId = model.NewId()
+ o1.URL = "http://nowhere.com/"
+
+ o1 = (<-store.Command().Save(o1)).Data.(*model.Command)
+
+ o1.Token = model.NewId()
+
+ if r2 := <-store.Command().Update(o1); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+}
diff --git a/store/sql_store.go b/store/sql_store.go
index 335cb207c..8517eb1a2 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -47,6 +47,7 @@ type SqlStore struct {
oauth OAuthStore
system SystemStore
webhook WebhookStore
+ command CommandStore
preference PreferenceStore
}
@@ -100,6 +101,7 @@ func NewSqlStore() Store {
sqlStore.oauth = NewSqlOAuthStore(sqlStore)
sqlStore.system = NewSqlSystemStore(sqlStore)
sqlStore.webhook = NewSqlWebhookStore(sqlStore)
+ sqlStore.command = NewSqlCommandStore(sqlStore)
sqlStore.preference = NewSqlPreferenceStore(sqlStore)
err := sqlStore.master.CreateTablesIfNotExists()
@@ -116,6 +118,7 @@ func NewSqlStore() Store {
sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded()
sqlStore.system.(*SqlSystemStore).UpgradeSchemaIfNeeded()
sqlStore.webhook.(*SqlWebhookStore).UpgradeSchemaIfNeeded()
+ sqlStore.command.(*SqlCommandStore).UpgradeSchemaIfNeeded()
sqlStore.preference.(*SqlPreferenceStore).UpgradeSchemaIfNeeded()
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
@@ -127,6 +130,7 @@ func NewSqlStore() Store {
sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists()
sqlStore.system.(*SqlSystemStore).CreateIndexesIfNotExists()
sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists()
+ sqlStore.command.(*SqlCommandStore).CreateIndexesIfNotExists()
sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists()
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
@@ -511,6 +515,10 @@ func (ss SqlStore) Webhook() WebhookStore {
return ss.webhook
}
+func (ss SqlStore) Command() CommandStore {
+ return ss.command
+}
+
func (ss SqlStore) Preference() PreferenceStore {
return ss.preference
}
diff --git a/store/sql_webhook_store.go b/store/sql_webhook_store.go
index 5298b0b94..740c9a33f 100644
--- a/store/sql_webhook_store.go
+++ b/store/sql_webhook_store.go
@@ -134,7 +134,7 @@ func (s SqlWebhookStore) PermanentDeleteIncomingByUser(userId string) StoreChann
return storeChannel
}
-func (s SqlWebhookStore) GetIncomingByUser(userId string) StoreChannel {
+func (s SqlWebhookStore) GetIncomingByTeam(teamId string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
@@ -142,8 +142,8 @@ func (s SqlWebhookStore) GetIncomingByUser(userId string) StoreChannel {
var webhooks []*model.IncomingWebhook
- if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM IncomingWebhooks WHERE UserId = :UserId AND DeleteAt = 0", map[string]interface{}{"UserId": userId}); err != nil {
- result.Err = model.NewLocAppError("SqlWebhookStore.GetIncomingByUser", "store.sql_webhooks.get_incoming_by_user.app_error", nil, "userId="+userId+", err="+err.Error())
+ if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM IncomingWebhooks WHERE TeamId = :TeamId AND DeleteAt = 0", map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewLocAppError("SqlWebhookStore.GetIncomingByUser", "store.sql_webhooks.get_incoming_by_user.app_error", nil, "teamId="+teamId+", err="+err.Error())
}
result.Data = webhooks
@@ -231,27 +231,6 @@ func (s SqlWebhookStore) GetOutgoing(id string) StoreChannel {
return storeChannel
}
-func (s SqlWebhookStore) GetOutgoingByCreator(userId string) StoreChannel {
- storeChannel := make(StoreChannel)
-
- go func() {
- result := StoreResult{}
-
- var webhooks []*model.OutgoingWebhook
-
- if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM OutgoingWebhooks WHERE CreatorId = :UserId AND DeleteAt = 0", map[string]interface{}{"UserId": userId}); err != nil {
- result.Err = model.NewLocAppError("SqlWebhookStore.GetOutgoingByCreator", "store.sql_webhooks.get_outgoing_by_creator.app_error", nil, "userId="+userId+", err="+err.Error())
- }
-
- result.Data = webhooks
-
- storeChannel <- result
- close(storeChannel)
- }()
-
- return storeChannel
-}
-
func (s SqlWebhookStore) GetOutgoingByChannel(channelId string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_webhook_store_test.go b/store/sql_webhook_store_test.go
index 089e38244..251ecf597 100644
--- a/store/sql_webhook_store_test.go
+++ b/store/sql_webhook_store_test.go
@@ -48,7 +48,7 @@ func TestWebhookStoreGetIncoming(t *testing.T) {
}
}
-func TestWebhookStoreGetIncomingByUser(t *testing.T) {
+func TestWebhookStoreGetIncomingByTeam(t *testing.T) {
Setup()
o1 := &model.IncomingWebhook{}
@@ -58,7 +58,7 @@ func TestWebhookStoreGetIncomingByUser(t *testing.T) {
o1 = (<-store.Webhook().SaveIncoming(o1)).Data.(*model.IncomingWebhook)
- if r1 := <-store.Webhook().GetIncomingByUser(o1.UserId); r1.Err != nil {
+ if r1 := <-store.Webhook().GetIncomingByTeam(o1.TeamId); r1.Err != nil {
t.Fatal(r1.Err)
} else {
if r1.Data.([]*model.IncomingWebhook)[0].CreateAt != o1.CreateAt {
@@ -66,7 +66,7 @@ func TestWebhookStoreGetIncomingByUser(t *testing.T) {
}
}
- if result := <-store.Webhook().GetIncomingByUser("123"); result.Err != nil {
+ if result := <-store.Webhook().GetIncomingByTeam("123"); result.Err != nil {
t.Fatal(result.Err)
} else {
if len(result.Data.([]*model.IncomingWebhook)) != 0 {
@@ -201,34 +201,6 @@ func TestWebhookStoreGetOutgoingByChannel(t *testing.T) {
}
}
-func TestWebhookStoreGetOutgoingByCreator(t *testing.T) {
- Setup()
-
- o1 := &model.OutgoingWebhook{}
- o1.ChannelId = model.NewId()
- o1.CreatorId = model.NewId()
- o1.TeamId = model.NewId()
- o1.CallbackURLs = []string{"http://nowhere.com/"}
-
- o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
-
- if r1 := <-store.Webhook().GetOutgoingByCreator(o1.CreatorId); r1.Err != nil {
- t.Fatal(r1.Err)
- } else {
- if r1.Data.([]*model.OutgoingWebhook)[0].CreateAt != o1.CreateAt {
- t.Fatal("invalid returned webhook")
- }
- }
-
- if result := <-store.Webhook().GetOutgoingByCreator("123"); result.Err != nil {
- t.Fatal(result.Err)
- } else {
- if len(result.Data.([]*model.OutgoingWebhook)) != 0 {
- t.Fatal("no webhooks should have returned")
- }
- }
-}
-
func TestWebhookStoreGetOutgoingByTeam(t *testing.T) {
Setup()
diff --git a/store/store.go b/store/store.go
index 2aa627734..b6b86e0d9 100644
--- a/store/store.go
+++ b/store/store.go
@@ -37,6 +37,7 @@ type Store interface {
OAuth() OAuthStore
System() SystemStore
Webhook() WebhookStore
+ Command() CommandStore
Preference() PreferenceStore
MarkSystemRanUnitTests()
Close()
@@ -170,13 +171,12 @@ type SystemStore interface {
type WebhookStore interface {
SaveIncoming(webhook *model.IncomingWebhook) StoreChannel
GetIncoming(id string) StoreChannel
- GetIncomingByUser(userId string) StoreChannel
+ GetIncomingByTeam(teamId string) StoreChannel
GetIncomingByChannel(channelId string) StoreChannel
DeleteIncoming(webhookId string, time int64) StoreChannel
PermanentDeleteIncomingByUser(userId string) StoreChannel
SaveOutgoing(webhook *model.OutgoingWebhook) StoreChannel
GetOutgoing(id string) StoreChannel
- GetOutgoingByCreator(userId string) StoreChannel
GetOutgoingByChannel(channelId string) StoreChannel
GetOutgoingByTeam(teamId string) StoreChannel
DeleteOutgoing(webhookId string, time int64) StoreChannel
@@ -186,6 +186,15 @@ type WebhookStore interface {
AnalyticsOutgoingCount(teamId string) StoreChannel
}
+type CommandStore interface {
+ Save(webhook *model.Command) StoreChannel
+ Get(id string) StoreChannel
+ GetByTeam(teamId string) StoreChannel
+ Delete(commandId string, time int64) StoreChannel
+ PermanentDeleteByUser(userId string) StoreChannel
+ Update(hook *model.Command) StoreChannel
+}
+
type PreferenceStore interface {
Save(preferences *model.Preferences) StoreChannel
Get(userId string, category string, name string) StoreChannel
diff --git a/utils/config.go b/utils/config.go
index e9b7e1878..3e4ba5c5b 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -202,6 +202,8 @@ func getClientConfig(c *model.Config) map[string]string {
props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks)
props["EnableOutgoingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableOutgoingWebhooks)
+ props["EnableCommands"] = strconv.FormatBool(*c.ServiceSettings.EnableCommands)
+ props["EnableOnlyAdminIntegrations"] = strconv.FormatBool(*c.ServiceSettings.EnableOnlyAdminIntegrations)
props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride)
props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride)
props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper)
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 7021900eb..2cc68d1ed 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -75,6 +75,8 @@ class ServiceSettings extends React.Component {
config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked;
config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked;
config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
+ config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked;
+ config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked;
//config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
@@ -389,11 +391,105 @@ class ServiceSettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
+ htmlFor='EnableCommands'
+ >
+ <FormattedMessage
+ id='admin.service.cmdsTitle'
+ defaultMessage='Enable Slash Commands: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableCommands'
+ value='true'
+ ref='EnableCommands'
+ defaultChecked={this.props.config.ServiceSettings.EnableCommands}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableCommands'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableCommands}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.service.cmdsDesc'
+ defaultMessage='When true, user created slash commands will be allowed.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableOnlyAdminIntegrations'
+ >
+ <FormattedMessage
+ id='admin.service.integrationAdmin'
+ defaultMessage='Enable Integrations for Admin Only: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOnlyAdminIntegrations'
+ value='true'
+ ref='EnableOnlyAdminIntegrations'
+ defaultChecked={this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOnlyAdminIntegrations'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.service.integrationAdminDesc'
+ defaultMessage='When true, user created integrations can only be created by admins.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
htmlFor='EnablePostUsernameOverride'
>
<FormattedMessage
id='admin.service.overrideTitle'
- defaultMessage='Enable Overriding Usernames from Webhooks: '
+ defaultMessage='Enable Overriding Usernames from Webhooks and Slash Commands: '
/>
</label>
<div className='col-sm-8'>
@@ -427,7 +523,7 @@ class ServiceSettings extends React.Component {
<p className='help-text'>
<FormattedMessage
id='admin.service.overrideDescription'
- defaultMessage='When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'
+ defaultMessage='When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'
/>
</p>
</div>
@@ -440,7 +536,7 @@ class ServiceSettings extends React.Component {
>
<FormattedMessage
id='admin.service.iconTitle'
- defaultMessage='Enable Overriding Icon from Webhooks: '
+ defaultMessage='Enable Overriding Icon from Webhooks and Slash Commands: '
/>
</label>
<div className='col-sm-8'>
@@ -474,7 +570,7 @@ class ServiceSettings extends React.Component {
<p className='help-text'>
<FormattedMessage
id='admin.service.iconDescription'
- defaultMessage='When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'
+ defaultMessage='When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'
/>
</p>
</div>
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index ed672cd34..20892898e 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -155,15 +155,10 @@ class CreatePost extends React.Component {
post.message,
false,
(data) => {
- if (data.response === 'not implemented') {
- this.sendMessage(post);
- return;
- }
-
PostStore.storeDraft(data.channel_id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
- if (data.goto_location.length > 0) {
+ if (data.goto_location && data.goto_location.length > 0) {
window.location.href = data.goto_location;
}
},
diff --git a/web/react/components/user_settings/manage_command_hooks.jsx b/web/react/components/user_settings/manage_command_hooks.jsx
new file mode 100644
index 000000000..bcf0a6c82
--- /dev/null
+++ b/web/react/components/user_settings/manage_command_hooks.jsx
@@ -0,0 +1,652 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LoadingScreen from '../loading_screen.jsx';
+
+import * as Client from '../../utils/client.jsx';
+
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
+
+const holders = defineMessages({
+ requestTypePost: {
+ id: 'user.settings.cmds.request_type_post',
+ defaultMessage: 'POST'
+ },
+ requestTypeGet: {
+ id: 'user.settings.cmds.request_type_get',
+ defaultMessage: 'GET'
+ },
+ addDisplayNamePlaceholder: {
+ id: 'user.settings.cmds.add_display_name.placeholder',
+ defaultMessage: 'Display Name'
+ },
+ addUsernamePlaceholder: {
+ id: 'user.settings.cmds.add_username.placeholder',
+ defaultMessage: 'Username'
+ },
+ addTriggerPlaceholder: {
+ id: 'user.settings.cmds.add_trigger.placeholder',
+ defaultMessage: 'Command trigger e.g. "hello" not including the slash'
+ },
+ addAutoCompleteDescPlaceholder: {
+ id: 'user.settings.cmds.auto_complete_desc.placeholder',
+ defaultMessage: 'A short description of what this commands does.'
+ },
+ addAutoCompleteHintPlaceholder: {
+ id: 'user.settings.cmds.auto_complete_hint.placeholder',
+ defaultMessage: '[zipcode]'
+ },
+ adUrlPlaceholder: {
+ id: 'user.settings.cmds.url.placeholder',
+ defaultMessage: 'Must start with http:// or https://'
+ }
+});
+
+export default class ManageCommandCmds extends React.Component {
+ constructor() {
+ super();
+
+ this.getCmds = this.getCmds.bind(this);
+ this.addNewCmd = this.addNewCmd.bind(this);
+ this.emptyCmd = this.emptyCmd.bind(this);
+ this.updateTrigger = this.updateTrigger.bind(this);
+ this.updateURL = this.updateURL.bind(this);
+ this.updateMethod = this.updateMethod.bind(this);
+ this.updateUsername = this.updateUsername.bind(this);
+ this.updateIconURL = this.updateIconURL.bind(this);
+ this.updateDisplayName = this.updateDisplayName.bind(this);
+ this.updateAutoComplete = this.updateAutoComplete.bind(this);
+ this.updateAutoCompleteDesc = this.updateAutoCompleteDesc.bind(this);
+ this.updateAutoCompleteHint = this.updateAutoCompleteHint.bind(this);
+
+ this.state = {cmds: [], cmd: this.emptyCmd(), getCmdsComplete: false};
+ }
+
+ static propTypes() {
+ return {
+ intl: intlShape.isRequired
+ };
+ }
+
+ emptyCmd() {
+ var cmd = {};
+ cmd.url = '';
+ cmd.trigger = '';
+ cmd.method = 'P';
+ cmd.username = '';
+ cmd.icon_url = '';
+ cmd.auto_complete = false;
+ cmd.auto_complete_desc = '';
+ cmd.auto_complete_hint = '';
+ cmd.display_name = '';
+ return cmd;
+ }
+
+ componentDidMount() {
+ this.getCmds();
+ }
+
+ addNewCmd(e) {
+ e.preventDefault();
+
+ if (this.state.cmd.trigger === '' || this.state.cmd.url === '') {
+ return;
+ }
+
+ var cmd = this.state.cmd;
+ if (cmd.trigger.length !== 0) {
+ cmd.trigger = cmd.trigger.trim();
+ }
+ cmd.url = cmd.url.trim();
+
+ Client.addCommand(
+ cmd,
+ (data) => {
+ let cmds = Object.assign([], this.state.cmds);
+ if (!cmds) {
+ cmds = [];
+ }
+ cmds.push(data);
+ this.setState({cmds, addError: null, cmd: this.emptyCmd()});
+ },
+ (err) => {
+ this.setState({addError: err.message});
+ }
+ );
+ }
+
+ removeCmd(id) {
+ const data = {};
+ data.id = id;
+
+ Client.deleteCommand(
+ data,
+ () => {
+ const cmds = this.state.cmds;
+ let index = -1;
+ for (let i = 0; i < cmds.length; i++) {
+ if (cmds[i].id === id) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ cmds.splice(index, 1);
+ }
+
+ this.setState({cmds});
+ },
+ (err) => {
+ this.setState({editError: err.message});
+ }
+ );
+ }
+
+ regenToken(id) {
+ const regenData = {};
+ regenData.id = id;
+
+ Client.regenCommandToken(
+ regenData,
+ (data) => {
+ const cmds = Object.assign([], this.state.cmds);
+ for (let i = 0; i < cmds.length; i++) {
+ if (cmds[i].id === id) {
+ cmds[i] = data;
+ break;
+ }
+ }
+
+ this.setState({cmds, editError: null});
+ },
+ (err) => {
+ this.setState({editError: err.message});
+ }
+ );
+ }
+
+ getCmds() {
+ Client.listTeamCommands(
+ (data) => {
+ if (data) {
+ this.setState({cmds: data, getCmdsComplete: true, editError: null});
+ }
+ },
+ (err) => {
+ this.setState({editError: err.message});
+ }
+ );
+ }
+
+ updateTrigger(e) {
+ var cmd = this.state.cmd;
+ cmd.trigger = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateURL(e) {
+ var cmd = this.state.cmd;
+ cmd.url = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateMethod(e) {
+ var cmd = this.state.cmd;
+ cmd.method = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateUsername(e) {
+ var cmd = this.state.cmd;
+ cmd.username = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateIconURL(e) {
+ var cmd = this.state.cmd;
+ cmd.icon_url = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateDisplayName(e) {
+ var cmd = this.state.cmd;
+ cmd.display_name = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateAutoComplete(e) {
+ var cmd = this.state.cmd;
+ cmd.auto_complete = e.target.checked;
+ this.setState(cmd);
+ }
+
+ updateAutoCompleteDesc(e) {
+ var cmd = this.state.cmd;
+ cmd.auto_complete_desc = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateAutoCompleteHint(e) {
+ var cmd = this.state.cmd;
+ cmd.auto_complete_hint = e.target.value;
+ this.setState(cmd);
+ }
+
+ render() {
+ let addError;
+ if (this.state.addError) {
+ addError = <label className='has-error'>{this.state.addError}</label>;
+ }
+
+ let editError;
+ if (this.state.editError) {
+ addError = <label className='has-error'>{this.state.editError}</label>;
+ }
+
+ const cmds = [];
+ this.state.cmds.forEach((cmd) => {
+ let triggerDiv;
+ if (cmd.trigger && cmd.trigger.length !== 0) {
+ triggerDiv = (
+ <div className='padding-top'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.trigger'
+ defaultMessage='Trigger: '
+ />
+ </strong>{cmd.trigger}
+ </div>
+ );
+ }
+
+ cmds.push(
+ <div
+ key={cmd.id}
+ className='webcmd__item'
+ >
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.display_name'
+ defaultMessage='Display Name: '
+ />
+ </strong><span className='word-break--all'>{cmd.display_name}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.username'
+ defaultMessage='Username: '
+ />
+ </strong><span className='word-break--all'>{cmd.username}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.icon_url'
+ defaultMessage='Icon URL: '
+ />
+ </strong><span className='word-break--all'>{cmd.icon_url}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete'
+ defaultMessage='Auto Complete: '
+ />
+ </strong><span className='word-break--all'>{cmd.auto_complete ? 'yes' : 'no'}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_desc'
+ defaultMessage='Auto Complete Description: '
+ />
+ </strong><span className='word-break--all'>{cmd.auto_complete_desc}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_hint'
+ defaultMessage='Auto Complete Hint: '
+ />
+ </strong><span className='word-break--all'>{cmd.auto_complete_hint}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.request_type'
+ defaultMessage='Request Type: '
+ />
+ </strong>
+ <span className='word-break--all'>
+ {
+ cmd.method === 'P' ?
+ <FormattedMessage
+ id='user.settings.cmds.request_type_post'
+ defaultMessage='POST'
+ /> :
+ <FormattedMessage
+ id='user.settings.cmds.request_type_get'
+ defaultMessage='GET'
+ />
+ }
+ </span>
+ </div>
+ <div className='padding-top x2 webcmd__url'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.url'
+ defaultMessage='URL: '
+ />
+ </strong><span className='word-break--all'>{cmd.url}</span>
+ </div>
+ {triggerDiv}
+ <div className='padding-top'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.token'
+ defaultMessage='Token: '
+ />
+ </strong>{cmd.token}
+ </div>
+ <div className='padding-top'>
+ <a
+ className='text-danger'
+ href='#'
+ onClick={this.regenToken.bind(this, cmd.id)}
+ >
+ <FormattedMessage
+ id='user.settings.cmds.regen'
+ defaultMessage='Regen Token'
+ />
+ </a>
+ <a
+ className='webcmd__remove'
+ href='#'
+ onClick={this.removeCmd.bind(this, cmd.id)}
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </a>
+ </div>
+ <div className='padding-top x2 divider-light'></div>
+ </div>
+ );
+ });
+
+ let displayCmds;
+ if (!this.state.getCmdsComplete) {
+ displayCmds = <LoadingScreen/>;
+ } else if (cmds.length > 0) {
+ displayCmds = cmds;
+ } else {
+ displayCmds = (
+ <div className='padding-top x2'>
+ <FormattedMessage
+ id='user.settings.cmds.none'
+ defaultMessage='None'
+ />
+ </div>
+ );
+ }
+
+ const existingCmds = (
+ <div className='webcmds__container'>
+ <label className='control-label padding-top x2'>
+ <FormattedMessage
+ id='user.settings.cmds.existing'
+ defaultMessage='Existing commands'
+ />
+ </label>
+ <div className='padding-top divider-light'></div>
+ <div className='webcmds__list'>
+ {displayCmds}
+ </div>
+ </div>
+ );
+
+ const disableButton = this.state.cmd.trigger === '' || this.state.cmd.url === '';
+
+ return (
+ <div key='addCommandCmd'>
+ <FormattedHTMLMessage
+ id='user.settings.cmds.add_desc'
+ defaultMessage='Create commands to send message events to an external integration. Please see <a href="http://mattermost.org/commands">http://mattermost.org/commands</a> to learn more.'
+ />
+ <div><label className='control-label padding-top x2'>{'Add a new command'}</label></div>
+ <div className='padding-top divider-light'></div>
+ <div className='padding-top'>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.display_name'
+ defaultMessage='Display Name: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='displayName'
+ className='form-control'
+ value={this.state.cmd.display_name}
+ onChange={this.updateDisplayName}
+ placeholder={this.props.intl.formatMessage(holders.addDisplayNamePlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>{'Command display name.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.username'
+ defaultMessage='Username: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='username'
+ className='form-control'
+ value={this.state.cmd.username}
+ onChange={this.updateUsername}
+ placeholder={this.props.intl.formatMessage(holders.addUsernamePlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.username_desc'
+ defaultMessage='The username to use when overriding the post.'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.icon_url'
+ defaultMessage='Icon URL: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='iconURL'
+ className='form-control'
+ value={this.state.cmd.icon_url}
+ onChange={this.updateIconURL}
+ placeholder='https://www.example.com/myicon.png'
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.icon_url_desc'
+ defaultMessage='URL to an icon'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.trigger'
+ defaultMessage='Trigger: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='trigger'
+ className='form-control'
+ value={this.state.cmd.trigger}
+ onChange={this.updateTrigger}
+ placeholder={this.props.intl.formatMessage(holders.addTriggerPlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.trigger_desc'
+ defaultMessage='Word to trigger on'
+ />
+ {''}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete'
+ defaultMessage='Auto Complete: '
+ />
+ </label>
+ <div className='padding-top'>
+ <label>
+ <input
+ type='checkbox'
+ checked={this.state.cmd.auto_complete}
+ onChange={this.updateAutoComplete}
+ />
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_desc_desc'
+ defaultMessage='A short description of what this commands does'
+ />
+ </label>
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_help'
+ defaultMessage='Show this command in autocomplete list.'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_desc'
+ defaultMessage='Auto Complete Description: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='autoCompleteDesc'
+ className='form-control'
+ value={this.state.cmd.auto_complete_desc}
+ onChange={this.updateAutoCompleteDesc}
+ placeholder={this.props.intl.formatMessage(holders.addAutoCompleteDescPlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_desc_desc'
+ defaultMessage='A short description of what this commands does'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_hint'
+ defaultMessage='Auto Complete Hint: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='autoCompleteHint'
+ className='form-control'
+ value={this.state.cmd.auto_complete_hint}
+ onChange={this.updateAutoCompleteHint}
+ placeholder={this.props.intl.formatMessage(holders.addAutoCompleteHintPlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_hint_desc'
+ defaultMessage='List parameters to be passed to the command.'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.request_type'
+ defaultMessage='Request Type: '
+ />
+ </label>
+ <div className='padding-top'>
+ <select
+ ref='method'
+ className='form-control'
+ value={this.state.cmd.method}
+ onChange={this.updateMethod}
+ >
+ <option value='P'>
+ {this.props.intl.formatMessage(holders.requestTypePost)}
+ </option>
+ <option value='G'>
+ {this.props.intl.formatMessage(holders.requestTypeGet)}
+ </option>
+ </select>
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.request_type_desc'
+ defaultMessage='Command request type issued to the callback URL.'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.url'
+ defaultMessage='URL: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='URL'
+ className='form-control'
+ value={this.state.cmd.url}
+ rows={1}
+ onChange={this.updateURL}
+ placeholder={this.props.intl.formatMessage(holders.adUrlPlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.url_desc'
+ defaultMessage='URL that will receive the HTTP POST or GET event'
+ />
+ </div>
+ {addError}
+ </div>
+ <div className='padding-top padding-bottom'>
+ <a
+ className={'btn btn-sm btn-primary'}
+ href='#'
+ disabled={disableButton}
+ onClick={this.addNewCmd}
+ >
+ {'Add'}
+ </a>
+ </div>
+ </div>
+ {existingCmds}
+ {editError}
+ </div>
+ );
+ }
+}
+
+export default injectIntl(ManageCommandCmds);
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index abd04a301..1a9edab03 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -5,6 +5,7 @@ import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import ManageIncomingHooks from './manage_incoming_hooks.jsx';
import ManageOutgoingHooks from './manage_outgoing_hooks.jsx';
+import ManageCommandHooks from './manage_command_hooks.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
@@ -24,6 +25,14 @@ const holders = defineMessages({
outDesc: {
id: 'user.settings.integrations.outWebhooksDescription',
defaultMessage: 'Manage your outgoing webhooks'
+ },
+ cmdName: {
+ id: 'user.settings.integrations.commands',
+ defaultMessage: 'Commands'
+ },
+ cmdDesc: {
+ id: 'user.settings.integrations.commandsDescription',
+ defaultMessage: 'Manage your commands'
}
});
@@ -41,6 +50,7 @@ class UserSettingsIntegrationsTab extends React.Component {
render() {
let incomingHooksSection;
let outgoingHooksSection;
+ let commandHooksSection;
var inputs = [];
const {formatMessage} = this.props.intl;
@@ -106,6 +116,37 @@ class UserSettingsIntegrationsTab extends React.Component {
}
}
+ if (global.window.mm_config.EnableCommands === 'true') {
+ if (this.props.activeSection === 'command-hooks') {
+ inputs.push(
+ <ManageCommandHooks key='command-hook-ui' />
+ );
+
+ commandHooksSection = (
+ <SettingItemMax
+ title={formatMessage(holders.cmdName)}
+ width='medium'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ commandHooksSection = (
+ <SettingItemMin
+ title={formatMessage(holders.cmdName)}
+ width='medium'
+ describe={formatMessage(holders.cmdDesc)}
+ updateSection={() => {
+ this.updateSection('command-hooks');
+ }}
+ />
+ );
+ }
+ }
+
return (
<div>
<div className='modal-header'>
@@ -144,6 +185,8 @@ class UserSettingsIntegrationsTab extends React.Component {
<div className='divider-light'/>
{outgoingHooksSection}
<div className='divider-dark'/>
+ {commandHooksSection}
+ <div className='divider-dark'/>
</div>
</div>
);
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index d615e02c7..328a7a7f2 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -774,20 +774,27 @@ export function savePreferences(preferences, success, error) {
}
export function getSuggestedCommands(command, suggestionId, component) {
- client.executeCommand(
- '',
- command,
- true,
+ client.listCommands(
(data) => {
+ var matches = [];
+ data.forEach((cmd) => {
+ if (('/' + cmd.trigger).indexOf(command) === 0) {
+ matches.push({
+ suggestion: '/' + cmd.trigger + ' ' + cmd.auto_complete_hint,
+ description: cmd.auto_complete_desc
+ });
+ }
+ });
+
// pull out the suggested commands from the returned data
- const terms = data.suggestions.map((suggestion) => suggestion.suggestion);
+ const terms = matches.map((suggestion) => suggestion.suggestion);
AppDispatcher.handleServerAction({
type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
id: suggestionId,
matchedPretext: command,
terms,
- items: data.suggestions,
+ items: matches,
component
});
},
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 33eb4cd47..992337671 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -908,11 +908,11 @@ export function getChannelExtraInfo(id, memberLimit, success, error) {
export function executeCommand(channelId, command, suggest, success, error) {
$.ajax({
- url: '/api/v1/command',
+ url: '/api/v1/commands/execute',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- data: JSON.stringify({channelId: channelId, command: command, suggest: '' + suggest}),
+ data: JSON.stringify({channelId, command, suggest: '' + suggest}),
success,
error: function onError(xhr, status, err) {
var e = handleError('executeCommand', xhr, status, err);
@@ -921,6 +921,78 @@ export function executeCommand(channelId, command, suggest, success, error) {
});
}
+export function addCommand(cmd, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(cmd),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('addCommand', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function deleteCommand(data, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('deleteCommand', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listTeamCommands(success, error) {
+ $.ajax({
+ url: '/api/v1/commands/list_team_commands',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('listTeamCommands', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function regenCommandToken(data, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/regen_token',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('regenCommandToken', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listCommands(success, error) {
+ $.ajax({
+ url: '/api/v1/commands/list',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('listCommands', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getPostsPage(channelId, offset, limit, success, error, complete) {
$.ajax({
cache: false,
diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json
index aaffc6ea7..a160aa58f 100644
--- a/web/static/i18n/en.json
+++ b/web/static/i18n/en.json
@@ -351,10 +351,14 @@
"admin.service.webhooksDescription": "When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.",
"admin.service.outWebhooksTitle": "Enable Outgoing Webhooks: ",
"admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed.",
- "admin.service.overrideTitle": "Enable Overriding Usernames from Webhooks: ",
- "admin.service.overrideDescription": "When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.",
- "admin.service.iconTitle": "Enable Overriding Icon from Webhooks: ",
- "admin.service.iconDescription": "When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.",
+ "admin.service.cmdsTitle": "Enable Slash Commands: ",
+ "admin.service.cmdsDesc": "When true, user created slash commands will be allowed.",
+ "admin.service.integrationAdmin": "Enable Integrations for Admin Only: ",
+ "admin.service.integrationAdminDesc": "When true, user created integrations can only be created by admins.",
+ "admin.service.overrideTitle": "Enable Overriding Usernames from Webhooks and Salsh Commands: ",
+ "admin.service.overrideDescription": "When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.",
+ "admin.service.iconTitle": "Enable Overriding Icon from Webhooks and Slash Commands: ",
+ "admin.service.iconDescription": "When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.",
"admin.service.testingTitle": "Enable Testing: ",
"admin.service.testingDescription": "(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.",
"admin.service.developerTitle": "Enable Developer Mode: ",
@@ -1148,7 +1152,40 @@
"user.settings.integrations.incomingWebhooksDescription": "Manage your incoming webhooks",
"user.settings.integrations.outWebhooks": "Outgoing Webhooks",
"user.settings.integrations.outWebhooksDescription": "Manage your outgoing webhooks",
+ "user.settings.integrations.commands": "Commands",
+ "user.settings.integrations.commandsDescription": "Manage your commands",
"user.settings.integrations.title": "Integration Settings",
+ "user.settings.cmds.trigger": "Trigger: ",
+ "user.settings.cmds.display_name": "Display Name: ",
+ "user.settings.cmds.username": "Username: ",
+ "user.settings.cmds.icon_url": "Icon URL: ",
+ "user.settings.cmds.auto_complete": "Auto Complete: ",
+ "user.settings.cmds.auto_complete_desc": "Auto Complete Description: ",
+ "user.settings.cmds.auto_complete_hint": "Auto Complete Hint: ",
+ "user.settings.cmds.request_type": "Request Type: ",
+ "user.settings.cmds.request_type_post": "POST",
+ "user.settings.cmds.request_type_get": "GET",
+ "user.settings.cmds.url": "URL: ",
+ "user.settings.cmds.token": "Token: ",
+ "user.settings.cmds.regen": "Regen Token",
+ "user.settings.cmds.none": "None",
+ "Existing commands": "Existing commands",
+ "user.settings.cmds.add_desc": "Create commands to send message events to an external integration. Please see <a href=\"http://mattermost.org/commands\">http://mattermost.org/commands</a> to learn more.",
+ "user.settings.cmds.add_display_name.placeholder": "Display Name",
+ "user.settings.cmds.existing": "Existing commands",
+ "user.settings.cmds.add_username.placeholder": "Username",
+ "user.settings.cmds.username_desc": "The username to use when overriding the post.",
+ "user.settings.cmds.icon_url_desc": "URL to an icon",
+ "user.settings.cmds.trigger_desc": "Word to trigger on",
+ "user.settings.cmds.add_trigger.placeholder": "Command trigger e.g. \"hello\" not including the slash",
+ "user.settings.cmds.auto_complete_desc_desc": "A short description of what this commands does",
+ "user.settings.cmds.auto_complete_help": "Show this command in autocomplete list.",
+ "user.settings.cmds.auto_complete_desc.placeholder": "A short description of what this commands does.",
+ "user.settings.cmds.auto_complete_hint.placeholder": "[zipcode]",
+ "user.settings.cmds.auto_complete_hint_desc": "List parameters to be passed to the command.",
+ "user.settings.cmds.request_type_desc": "Command request type issued to the callback URL.",
+ "user.settings.cmds.url_desc": "URL that will receive the HTTP POST or GET event",
+ "user.settings.cmds.url.placeholder": "Must start with http:// or https://",
"user.settings.modal.general": "General",
"user.settings.modal.security": "Security",
"user.settings.modal.notifications": "Notifications",
diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json
index 981e1f06f..0a780c442 100644
--- a/web/static/i18n/es.json
+++ b/web/static/i18n/es.json
@@ -299,8 +299,6 @@
"admin.service.googleDescription": "Asigna una llave a este campo para habilitar la previsualización de videos de YouTube tomados de los enlaces que aparecen en los mensajes o comentarios. Las instrucciones de como obtener una llave está disponible en <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Al dejar este campo en blanco deshabilita la generación de previsualizaciones de videos de YouTube desde los enlaces.",
"admin.service.googleExample": "Ej \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"",
"admin.service.googleTitle": "Llave de desarrolador Google:",
- "admin.service.iconDescription": "Cuando es verdadero, se le permitirá cambiar el icono del mensaje desde webhooks. Nota, en combinación con permitir el cambio de nombre de usuario, podría exponer a los usuarios a sufrir ataques de phishing.",
- "admin.service.iconTitle": "Habilitar el cambio de icono desde los Webhooks: ",
"admin.service.listenAddress": "Dirección de escucha:",
"admin.service.listenDescription": "La dirección a la que se unirá y escuchará. Ingresar \":8065\" se podrá unir a todas las interfaces o podrá seleccionar una como ej: \"127.0.0.1:8065\". Cambiando este valor es necesario reiniciar el servidor.",
"admin.service.listenExample": "Ej \":8065\"",
@@ -308,8 +306,6 @@
"admin.service.mobileSessionDaysDesc": "La sesión nativa de los dispositivos moviles expirará luego de transcurrido el numero de días especificado y se solicitará al usuario que inicie sesión nuevamente.",
"admin.service.outWebhooksDesc": "Cuando es verdadero, los webhooks de salida serán permitidos.",
"admin.service.outWebhooksTitle": "Habilitar Webhooks de Salida: ",
- "admin.service.overrideDescription": "Cuando es verdadero, se le permitirá cambiar el nombre de usuario desde webhooks. Nota, en conjunto con cambio de icono, podría exponer a los usuarios a sufrir ataques de phishing.",
- "admin.service.overrideTitle": "Habilitar el cambio de nombres de usuario desde los Webhooks: ",
"admin.service.save": "Guardar",
"admin.service.saving": "Guardando....",
"admin.service.securityDesc": "Cuando es verdadero, Los Administradores del Sistema serán notificados por correo electrónico se han anunciado alertas de seguridad relevantes en las últimas 12 horas. Requiere que los correos estén habilitados.",
diff --git a/web/web_test.go b/web/web_test.go
index cc7d22559..7617ae54a 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -194,6 +194,10 @@ func TestIncomingWebhook(t *testing.T) {
user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User)
store.Must(api.Srv.Store.User().VerifyEmail(user.Id))
+ c := &api.Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ api.UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
ApiClient.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}