summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/command.go728
-rw-r--r--api/command_echo.go81
-rw-r--r--api/command_echo_test.go42
-rw-r--r--api/command_join.go54
-rw-r--r--api/command_join_test.go71
-rw-r--r--api/command_loadtest.go357
-rw-r--r--api/command_loadtest_test.go220
-rw-r--r--api/command_logout.go29
-rw-r--r--api/command_logout_test.go32
-rw-r--r--api/command_me.go29
-rw-r--r--api/command_me_test.go47
-rw-r--r--api/command_shrug.go34
-rw-r--r--api/command_shrug_test.go47
-rw-r--r--api/command_test.go274
-rw-r--r--api/user.go4
-rw-r--r--api/webhook.go62
-rw-r--r--api/webhook_test.go84
-rw-r--r--config/config.json10
-rw-r--r--docker/dev/config_docker.json2
-rw-r--r--docker/local/config_docker.json2
-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.go44
-rw-r--r--model/utils.go6
-rw-r--r--model/version.go10
-rw-r--r--store/sql_command_store.go174
-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/create_post.jsx7
-rw-r--r--web/react/components/user_settings/manage_command_hooks.jsx442
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx35
-rw-r--r--web/react/utils/async_client.jsx20
-rw-r--r--web/react/utils/client.jsx76
-rw-r--r--web/web_test.go4
41 files changed, 2762 insertions, 831 deletions
diff --git a/api/command.go b/api/command.go
index 00293cf16..6e2133f34 100644
--- a/api/command.go
+++ b/api/command.go
@@ -4,12 +4,8 @@
package api
import (
- "io"
"net/http"
- "path"
- "strconv"
"strings"
- "time"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
@@ -17,630 +13,268 @@ 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.NewAppError("checkCommand", "Command not implemented", "")
-)
-var echoSem chan bool
-
-func InitCommand(r *mux.Router) {
- l4g.Debug("Initializing command api routes")
- r.Handle("/command", ApiUserRequired(command)).Methods("POST")
+type CommandProvider interface {
+ GetCommand() *model.Command
+ DoCommand(c *Context, channelId string, message string) *model.CommandResponse
}
-func command(c *Context, w http.ResponseWriter, r *http.Request) {
+var commandProviders = make(map[string]CommandProvider)
- props := model.MapFromJson(r.Body)
+func RegisterCommandProvider(newProvider CommandProvider) {
+ commandProviders[newProvider.GetCommand().Trigger] = newProvider
+}
- command := &model.Command{
- Command: strings.TrimSpace(props["command"]),
- ChannelId: strings.TrimSpace(props["channelId"]),
- Suggest: props["suggest"] == "true",
- Suggestions: make([]*model.SuggestCommand, 0, 128),
+func GetCommandProvidersProvider(name string) CommandProvider {
+ provider, ok := commandProviders[name]
+ if ok {
+ return provider
}
- 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()))
- }
+ return nil
}
-func checkCommand(c *Context, command *model.Command) bool {
+func InitCommand(r *mux.Router) {
+ l4g.Debug("Initializing command api routes")
- if len(command.Command) == 0 || strings.Index(command.Command, "/") != 0 {
- c.Err = model.NewAppError("checkCommand", "Command must start with /", "")
- return false
- }
+ sr := r.PathPrefix("/commands").Subrouter()
- if len(command.ChannelId) > 0 {
- cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, command.ChannelId, c.Session.UserId)
+ sr.Handle("/execute", ApiUserRequired(executeCommand)).Methods("POST")
+ sr.Handle("/list", ApiUserRequired(listCommands)).Methods("GET")
- if !c.HasPermissionsToChannel(cchan, "checkCommand") {
- 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")
+}
- 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
+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()
+ if cpy.AutoComplete && !seen[cpy.Id] {
+ cpy.Sanatize()
+ seen[cpy.Trigger] = true
+ commands = append(commands, &cpy)
}
}
- for _, v := range commands {
-
- if v(c, command) || c.Err != nil {
- return true
+ 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.Sanatize()
+ seen[cmd.Trigger] = true
+ commands = append(commands, cmd)
+ }
}
}
- return false
+ w.Write([]byte(model.CommandListToJson(commands)))
}
-func logoutCommand(c *Context, command *model.Command) bool {
+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"])
- cmd := cmds["logoutCommand"]
+ if len(command) <= 1 || strings.Index(command, "/") != 0 {
+ c.Err = model.NewAppError("command", "Command must start with /", "")
+ return
+ }
- if strings.Index(command.Command, cmd) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Logout"})
+ if len(channelId) > 0 {
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId)
- if !command.Suggest {
- command.GotoLocation = "/logout"
- command.Response = model.RESP_EXECUTED
- return true
+ if !c.HasPermissionsToChannel(cchan, "checkCommand") {
+ return
}
-
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Logout"})
}
- return false
-}
+ parts := strings.Split(command, " ")
+ trigger := parts[0][1:]
+ provider := GetCommandProvidersProvider(trigger)
-func echoCommand(c *Context, command *model.Command) bool {
- cmd := cmds["echoCommand"]
- maxThreads := 100
+ if provider != nil {
+ message := strings.Join(parts[1:], " ")
+ response := provider.DoCommand(c, channelId, message)
- 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 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.NewAppError("command", "An error while saving the command response to the channel", "")
}
- }
-
- if delay > 10000 {
- c.Err = model.NewAppError("echoCommand", "Delays must be under 10000 seconds", "")
- 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.NewAppError("echoCommand", "High volume of echo request, cannot process request", "")
- return false
- }
-
- echoSem <- true
- go func() {
- defer func() { <-echoSem }()
+ } else if response.ResponseType == model.COMMAND_RESPONSE_TYPE_EPHEMERAL {
post := &model.Post{}
- post.ChannelId = command.ChannelId
- post.Message = message
-
- time.Sleep(time.Duration(delay) * time.Second)
-
+ post.ChannelId = channelId
+ post.Message = "TODO_EPHEMERAL: " + response.Text
if _, err := CreatePost(c, post, true); err != nil {
- l4g.Error("Unable to create /echo post, err=%v", err)
+ c.Err = model.NewAppError("command", "An error while saving the command response to the channel", "")
}
- }()
-
- command.Response = model.RESP_EXECUTED
- return true
-
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Echo back text from your account, /echo \"message\" [delay in seconds]"})
- }
-
- return false
-}
-
-func meCommand(c *Context, command *model.Command) bool {
- cmd := cmds["meCommand"]
-
- if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
- message := ""
-
- 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("Unable to create /me post post, err=%v", err)
- return false
}
- command.Response = model.RESP_EXECUTED
- return true
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Do an action, /me [message]"})
+ w.Write([]byte(response.ToJson()))
+ } else {
+ c.Err = model.NewAppError("command", "Command with a trigger of '"+trigger+"' not found", "")
}
-
- return false
}
-func shrugCommand(c *Context, command *model.Command) bool {
- cmd := cmds["shrugCommand"]
-
- if !command.Suggest && strings.Index(command.Command, cmd) == 0 {
- message := `¯\\\_(ツ)_/¯`
-
- 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("Unable to create /shrug post post, err=%v", err)
- return false
- }
- command.Response = model.RESP_EXECUTED
- return true
-
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Adds ¯\\_(ツ)_/¯ to your message, /shrug [message]"})
+func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCommands {
+ c.Err = model.NewAppError("createCommand", "Commands have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
}
- return false
-}
-
-func joinCommand(c *Context, command *model.Command) bool {
-
- // looks for "/join channel-name"
- cmd := cmds["joinCommand"]
-
- if strings.Index(command.Command, cmd) == 0 {
-
- parts := strings.Split(command.Command, " ")
-
- startsWith := ""
-
- if len(parts) == 2 {
- startsWith = parts[1]
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
}
+ }
- 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)
-
- for _, v := range channels.Channels {
-
- if v.Name == startsWith && !command.Suggest {
-
- if v.Type == model.CHANNEL_DIRECT {
- return false
- }
+ c.LogAudit("attempt")
- JoinChannel(c, v.Id, "")
+ cmd := model.CommandFromJson(r.Body)
- if c.Err != nil {
- return false
- }
+ if cmd == nil {
+ c.SetInvalidParam("createCommand", "command")
+ return
+ }
- command.GotoLocation = c.GetTeamURL() + "/channels/" + v.Name
- command.Response = model.RESP_EXECUTED
- return true
- }
+ cmd.CreatorId = c.Session.UserId
+ cmd.TeamId = c.Session.TeamId
- if len(startsWith) == 0 || strings.Index(v.Name, startsWith) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd + " " + v.Name, Description: "Join the open channel"})
- }
- }
- }
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Join an open channel"})
+ 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 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
+func listTeamCommands(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCommands {
+ c.Err = model.NewAppError("createCommand", "Commands have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
}
- 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
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
}
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Debug Load Testing"})
}
- return false
-}
-
-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
+ 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)))
}
- return utils.Range{begin, end}, true
}
-func contains(items []string, token string) bool {
- for _, elem := range items {
- if elem == token {
- return true
- }
+func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCommands {
+ c.Err = model.NewAppError("createCommand", "Commands have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
}
- return false
-}
-func loadTestSetupCommand(c *Context, command *model.Command) bool {
- cmd := cmds["loadTestCommand"] + " setup"
-
- 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")
-
- 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 {
- l4g.Error("Failed to create testing environment")
- 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("Failed to create testing environment")
- return true
- } 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)
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
}
- return true
- } else if strings.Index(cmd, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{
- Suggestion: cmd,
- Description: "Creates a testing environment in current team. [teams] [fuzz] <Num Channels> <Num Users> <NumPosts>"})
}
- return false
-}
+ c.LogAudit("attempt")
-func loadTestUsersCommand(c *Context, command *model.Command) bool {
- cmd1 := cmds["loadTestCommand"] + " users"
- cmd2 := cmds["loadTestCommand"] + " users 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 {
- doFuzz = true
- cmd = cmd2
- }
- usersr, err := parseRange(command.Command, cmd)
- if err == false {
- usersr = utils.Range{10, 15}
- }
- 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: "Add a specified number of random users to current team <Min Users> <Max Users>"})
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add a specified number of random users with fuzz text to current team <Min Users> <Max Users>"})
- } else if strings.Index(cmd2, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add a specified number of random users with fuzz text to current team <Min Users> <Max Users>"})
+ id := props["id"]
+ if len(id) == 0 {
+ c.SetInvalidParam("regenCommandToken", "id")
+ return
}
- return false
-}
-
-func loadTestChannelsCommand(c *Context, command *model.Command) bool {
- cmd1 := cmds["loadTestCommand"] + " channels"
- cmd2 := cmds["loadTestCommand"] + " channels fuzz"
+ 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)
- 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 c.Session.TeamId != cmd.TeamId && c.Session.UserId != cmd.CreatorId && !c.IsTeamAdmin() {
+ c.LogAudit("fail - inappropriate permissions")
+ c.Err = model.NewAppError("regenToken", "Inappropriate permissions to regenerate command token", "user_id="+c.Session.UserId)
+ 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: "Add a specified number of random channels to current team <MinChannels> <MaxChannels>"})
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add a specified number of random channels with fuzz text to current team <Min Channels> <Max Channels>"})
- } else if strings.Index(cmd2, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add a specified number of random channels with fuzz text to current team <Min Channels> <Max Channels>"})
}
- return false
-}
-
-func loadTestPostsCommand(c *Context, command *model.Command) bool {
- cmd1 := cmds["loadTestCommand"] + " posts"
- cmd2 := cmds["loadTestCommand"] + " posts fuzz"
-
- if strings.Index(command.Command, cmd1) == 0 && !command.Suggest {
- cmd := cmd1
- doFuzz := false
- if strings.Index(command.Command, cmd2) == 0 {
- cmd = cmd2
- doFuzz = true
- }
-
- postsr, err := parseRange(command.Command, cmd)
- if err == false {
- postsr = utils.Range{20, 30}
- }
+ cmd.Token = model.NewId()
- 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}
- }
- }
-
- 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, 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: "Add some random posts to current channel <Min Posts> <Max Posts> <Min Images> <Max Images>"})
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add some random posts with fuzz text to current channel <Min Posts> <Max Posts> <Min Images> <Max Images>"})
- } else if strings.Index(cmd2, command.Command) == 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: "Add some random posts with fuzz text to current channel <Min Posts> <Max Posts> <Min Images> <Max Images>"})
+ 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 := ""
-
- parameters := strings.SplitN(command.Command, " ", 3)
- if len(parameters) != 3 {
- c.Err = model.NewAppError("loadTestUrlCommand", "Command must contain a url", "")
- return true
- } else {
- url = parameters[2]
- }
-
- // 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"
- }
- }
+func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCommands {
+ c.Err = model.NewAppError("createCommand", "Commands have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
- var contents io.ReadCloser
- if r, err := http.Get(url); err != nil {
- c.Err = model.NewAppError("loadTestUrlCommand", "Unable to get file", err.Error())
- return false
- } else if r.StatusCode > 400 {
- c.Err = model.NewAppError("loadTestUrlCommand", "Unable to get file", r.Status)
- return false
- } else {
- contents = r.Body
+ if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
+ if !(c.IsSystemAdmin() || c.IsTeamAdmin()) {
+ c.Err = model.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
}
+ }
- bytes := make([]byte, 4000)
-
- // break contents into 4000 byte posts
- for {
- length, err := contents.Read(bytes)
- if err != nil && err != io.EOF {
- c.Err = model.NewAppError("loadTestUrlCommand", "Encountered error reading file", err.Error())
- return false
- }
+ c.LogAudit("attempt")
- if length == 0 {
- break
- }
+ props := model.MapFromJson(r.Body)
- post := &model.Post{}
- post.Message = string(bytes[:length])
- post.ChannelId = command.ChannelId
+ id := props["id"]
+ if len(id) == 0 {
+ c.SetInvalidParam("deleteCommand", "id")
+ return
+ }
- if _, err := CreatePost(c, post, false); err != nil {
- l4g.Error("Unable to create post, err=%v", err)
- 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.NewAppError("deleteCommand", "Inappropriate permissions to delete command", "user_id="+c.Session.UserId)
+ return
}
+ }
- command.Response = model.RESP_EXECUTED
-
- return true
- } else if strings.Index(cmd, command.Command) == 0 && strings.Index(command.Command, "/loadtest posts") != 0 {
- command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Add a post containing the text from a given url to current channel <Url>"})
+ if err := (<-Srv.Store.Command().Delete(id, model.GetMillis())).Err; err != nil {
+ c.Err = err
+ return
}
- return false
+ c.LogAudit("success")
+ w.Write([]byte(model.MapToJson(props)))
}
diff --git a/api/command_echo.go b/api/command_echo.go
new file mode 100644
index 000000000..a5042c9ea
--- /dev/null
+++ b/api/command_echo.go
@@ -0,0 +1,81 @@
+// 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 {
+}
+
+func init() {
+ RegisterCommandProvider(&EchoProvider{})
+}
+
+func (me *EchoProvider) GetCommand() *model.Command {
+ return &model.Command{
+ Trigger: "echo",
+ AutoComplete: true,
+ AutoCompleteDesc: "Echo back text from your account",
+ AutoCompleteHint: "\"message\" [delay in seconds]",
+ DisplayName: "echo",
+ }
+}
+
+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: "Delays must be under 10000 seconds", 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: "High volume of echo request, cannot process request", 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("Unable to create /echo post, err=%v", 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..67c1c1ad1
--- /dev/null
+++ b/api/command_join.go
@@ -0,0 +1,54 @@
+// 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 {
+}
+
+func init() {
+ RegisterCommandProvider(&JoinProvider{})
+}
+
+func (me *JoinProvider) GetCommand() *model.Command {
+ return &model.Command{
+ Trigger: "join",
+ AutoComplete: true,
+ AutoCompleteDesc: "Join the open channel",
+ AutoCompleteHint: "[channel-name]",
+ DisplayName: "join",
+ }
+}
+
+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: "An error occured while listing channels.", 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: "An error occured while joining the channel.", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ JoinChannel(c, v.Id, "")
+
+ if c.Err != nil {
+ c.Err = nil
+ return &model.CommandResponse{Text: "An error occured while joining the channel.", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ return &model.CommandResponse{GotoLocation: c.GetTeamURL() + "/channels/" + v.Name, Text: "Joined channel.", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+ }
+ }
+
+ return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: "We couldn't find the channel"}
+}
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..8f79006b4
--- /dev/null
+++ b/api/command_loadtest.go
@@ -0,0 +1,357 @@
+// 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
+
+
+`
+
+type LoadTestProvider struct {
+}
+
+func init() {
+ if !utils.Cfg.ServiceSettings.EnableTesting {
+ RegisterCommandProvider(&LoadTestProvider{})
+ }
+}
+
+func (me *LoadTestProvider) GetCommand() *model.Command {
+ return &model.Command{
+ Trigger: "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 url...", 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..4af2a636a
--- /dev/null
+++ b/api/command_loadtest_test.go
@@ -0,0 +1,220 @@
+// 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://www.hopefullynonexistent.file/path/asdf/qwerty"
+ if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Unable to get file" {
+ 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 url..." {
+ 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 url..." {
+ 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 url..." {
+ 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..01e81aaf0
--- /dev/null
+++ b/api/command_logout.go
@@ -0,0 +1,29 @@
+// 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 {
+}
+
+func init() {
+ RegisterCommandProvider(&LogoutProvider{})
+}
+
+func (me *LogoutProvider) GetCommand() *model.Command {
+ return &model.Command{
+ Trigger: "logout",
+ AutoComplete: true,
+ AutoCompleteDesc: "Logout",
+ AutoCompleteHint: "",
+ DisplayName: "logout",
+ }
+}
+
+func (me *LogoutProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
+ return &model.CommandResponse{GotoLocation: "/logout", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: "Logging out..."}
+}
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..f0154fe53
--- /dev/null
+++ b/api/command_me.go
@@ -0,0 +1,29 @@
+// 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 {
+}
+
+func init() {
+ RegisterCommandProvider(&MeProvider{})
+}
+
+func (me *MeProvider) GetCommand() *model.Command {
+ return &model.Command{
+ Trigger: "me",
+ AutoComplete: true,
+ AutoCompleteDesc: "Do an action",
+ AutoCompleteHint: "[message]",
+ DisplayName: "me",
+ }
+}
+
+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..c49bd46ae
--- /dev/null
+++ b/api/command_shrug.go
@@ -0,0 +1,34 @@
+// 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 {
+}
+
+func init() {
+ RegisterCommandProvider(&ShrugProvider{})
+}
+
+func (me *ShrugProvider) GetCommand() *model.Command {
+ return &model.Command{
+ Trigger: "shrug",
+ AutoComplete: true,
+ AutoCompleteDesc: `Adds ¯\_(ツ)_/¯ to your message`,
+ AutoCompleteHint: "[message]",
+ DisplayName: "shrug",
+ }
+}
+
+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 f38cf1397..e5e954170 100644
--- a/api/command_test.go
+++ b/api/command_test.go
@@ -4,16 +4,14 @@
package api
import (
- "strings"
"testing"
- "time"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"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 +23,160 @@ func TestSuggestRootCommands(t *testing.T) {
Client.LoginByEmail(team.Name, user1.Email, "pwd")
- if _, err := Client.Command("", "", true); err == nil {
- t.Fatal("Should fail")
- }
-
- rs1 := Client.Must(Client.Command("", "/", true)).Data.(*model.Command)
+ if results, err := Client.ListCommands(); err != nil {
+ t.Fatal(err)
+ } else {
+ commands := results.Data.([]*model.Command)
+ foundEcho := false
- hasLogout := false
- for _, v := range rs1.Suggestions {
- if v.Suggestion == "/logout" {
- hasLogout = true
+ for _, command := range commands {
+ if command.Trigger == "echo" {
+ foundEcho = true
+ }
}
- }
-
- if !hasLogout {
- t.Log(rs1.Suggestions)
- t.Fatal("should have logout cmd")
- }
-
- rs2 := Client.Must(Client.Command("", "/log", true)).Data.(*model.Command)
-
- if rs2.Suggestions[0].Suggestion != "/logout" {
- t.Fatal("should have logout cmd")
- }
-
- rs3 := Client.Must(Client.Command("", "/joi", true)).Data.(*model.Command)
-
- if rs3.Suggestions[0].Suggestion != "/join" {
- t.Fatal("should have join cmd")
- }
- rs4 := Client.Must(Client.Command("", "/ech", true)).Data.(*model.Command)
-
- if rs4.Suggestions[0].Suggestion != "/echo" {
- t.Fatal("should have echo cmd")
+ if !foundEcho {
+ t.Fatal("Couldn't find echo command")
+ }
}
}
-func TestLogoutCommands(t *testing.T) {
+func TestCreateCommand(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")
-
- rs1 := Client.Must(Client.Command("", "/logout", false)).Data.(*model.Command)
- if rs1.GotoLocation != "/logout" {
- t.Fatal("failed to logout")
- }
-}
-
-func TestJoinCommands(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() + "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")
+ 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))
- 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))
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
- 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))
+ cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST}
- 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))
+ if _, err := Client.CreateCommand(cmd); err == nil {
+ t.Fatal("should have failed because not admin")
+ }
- data := make(map[string]string)
- data["user_id"] = user2.Id
- channel3 := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.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")
- 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")
+ var rcmd *model.Command
+ if result, err := Client.CreateCommand(cmd); err != nil {
+ t.Fatal(err)
+ } else {
+ rcmd = 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 rcmd.CreatorId != user.Id {
+ t.Fatal("user ids didn't match")
}
- rs3 := Client.Must(Client.Command("", "/join", true)).Data.(*model.Command)
- if len(rs3.Suggestions) != 2 {
- t.Fatal("should have 2 join cmd")
+ if rcmd.TeamId != team.Id {
+ t.Fatal("team ids didn't match")
}
- rs4 := Client.Must(Client.Command("", "/join ", true)).Data.(*model.Command)
- if len(rs4.Suggestions) != 2 {
- t.Fatal("should have 2 join cmd")
+ 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")
+ }
}
+}
- 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")
- }
+func TestListTeamCommands(t *testing.T) {
+ Setup()
+ enableCommands := *utils.Cfg.ServiceSettings.EnableCommands
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableCommands = &enableCommands
+ }()
+ *utils.Cfg.ServiceSettings.EnableCommands = true
- 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")
- }
+ 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)
- c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
+ 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 len(c1.Channels) != 4 { // 4 because of town-square, off-topic and direct
- t.Fatal("didn't join 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")
+
+ cmd1 := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST}
+ cmd1 = Client.Must(Client.CreateCommand(cmd1)).Data.(*model.Command)
+
+ if result, err := Client.ListTeamCommands(); err != nil {
+ t.Fatal(err)
+ } else {
+ cmds := result.Data.([]*model.Command)
- found := false
- for _, c := range c1.Channels {
- if c.Name == channel2.Name {
- found = true
- break
+ if len(cmds) != 1 {
+ t.Fatal("incorrect number of cmd")
}
}
- if !found {
- t.Fatal("didn't join channel")
- }
}
-func TestEchoCommand(t *testing.T) {
+func TestRegenToken(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() + "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)
+ 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))
- echoTestString := "/echo test"
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
- 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")
- }
+ cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST}
+ cmd = Client.Must(Client.CreateCommand(cmd)).Data.(*model.Command)
- time.Sleep(100 * time.Millisecond)
+ data := make(map[string]string)
+ data["id"] = cmd.Id
- p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList)
- if len(p1.Order) != 1 {
- t.Fatal("Echo command failed to send")
+ 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")
+ }
}
}
-func TestLoadTestUrlCommand(t *testing.T) {
+func TestDeleteCommand(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 +185,24 @@ 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)
-
- command := "/loadtest url "
- if _, err := Client.Command(channel.Id, command, false); err == nil {
- t.Fatal("/loadtest url with no url should've failed")
- }
-
- 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")
- }
-
- 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")
- }
+ cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST}
+ cmd = Client.Must(Client.CreateCommand(cmd)).Data.(*model.Command)
- 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")
- }
+ data := make(map[string]string)
+ data["id"] = cmd.Id
- 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")
+ if _, err := Client.DeleteCommand(data); err != nil {
+ t.Fatal(err)
}
- 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?")
+ cmds := Client.Must(Client.ListTeamCommands()).Data.([]*model.Command)
+ if len(cmds) != 0 {
+ t.Fatal("delete didn't work properly")
}
}
diff --git a/api/user.go b/api/user.go
index a6b4fb654..ab64759cf 100644
--- a/api/user.go
+++ b/api/user.go
@@ -1450,6 +1450,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 a9a88b7b8..33e7f957a 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.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ 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.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ 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.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ 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.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ 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.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ 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.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ 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.NewAppError("createCommand", "Integrations have been limited to admins only.", "")
+ 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.NewAppError("regenOutgoingHookToken", "Inappropriate permissions to regenerate outcoming webhook token", "user_id="+c.Session.UserId)
return
diff --git a/api/webhook_test.go b/api/webhook_test.go
index 85117ec18..89c06317f 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 b00dbc201..1a4156c8f 100644
--- a/config/config.json
+++ b/config/config.json
@@ -5,10 +5,12 @@
"SegmentDeveloperKey": "",
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
- "EnableIncomingWebhooks": false,
- "EnableOutgoingWebhooks": false,
- "EnablePostUsernameOverride": false,
- "EnablePostIconOverride": false,
+ "EnableIncomingWebhooks": true,
+ "EnableOutgoingWebhooks": true,
+ "EnableCommands": true,
+ "EnableOnlyAdminIntegrations": true,
+ "EnablePostUsernameOverride": true,
+ "EnablePostIconOverride": true,
"EnableTesting": false,
"EnableDeveloper": false,
"EnableSecurityFixAlert": true,
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index 1aa2ee843..cbe617d3a 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": false,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index 1aa2ee843..cbe617d3a 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": false,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
diff --git a/model/client.go b/model/client.go
index 75b93c971..a4da6d513 100644
--- a/model/client.go
+++ b/model/client.go
@@ -372,7 +372,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),
@@ -380,6 +416,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..c917a46ea 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 NewAppError("Command.IsValid", "Invalid Id", "")
+ }
+
+ if len(o.Token) != 26 {
+ return NewAppError("Command.IsValid", "Invalid token", "")
+ }
+
+ if o.CreateAt == 0 {
+ return NewAppError("Command.IsValid", "Create at must be a valid time", "id="+o.Id)
+ }
+
+ if o.UpdateAt == 0 {
+ return NewAppError("Command.IsValid", "Update at must be a valid time", "id="+o.Id)
+ }
+
+ if len(o.CreatorId) != 26 {
+ return NewAppError("Command.IsValid", "Invalid user id", "")
+ }
+
+ if len(o.TeamId) != 26 {
+ return NewAppError("Command.IsValid", "Invalid team id", "")
+ }
+
+ if len(o.Trigger) > 1024 {
+ return NewAppError("Command.IsValid", "Invalid trigger", "")
+ }
+
+ if len(o.URL) == 0 || len(o.URL) > 1024 {
+ return NewAppError("Command.IsValid", "Invalid url", "")
+ }
+
+ if !IsValidHttpUrl(o.URL) {
+ return NewAppError("Command.IsValid", "Invalid URL. Must be a valid URL and start with http:// or https://", "")
+ }
+
+ if !(o.Method == COMMAND_METHOD_GET || o.Method == COMMAND_METHOD_POST) {
+ return NewAppError("Command.IsValid", "Invalid Method", "")
+ }
+
+ 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) Sanatize() {
+ 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 ed56ed0c7..7d9ff41f1 100644
--- a/model/config.go
+++ b/model/config.go
@@ -24,22 +24,24 @@ 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
+ 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
}
type SSOSettings struct {
@@ -330,6 +332,16 @@ 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
+ }
}
func (o *Config) IsValid() *AppError {
diff --git a/model/utils.go b/model/utils.go
index 617c95efd..301e36f59 100644
--- a/model/utils.go
+++ b/model/utils.go
@@ -54,7 +54,11 @@ func AppErrorFromJson(data io.Reader) *AppError {
if err == nil {
return &er
} else {
- return NewAppError("AppErrorFromJson", "could not decode", err.Error())
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(data)
+ s := buf.String()
+
+ return NewAppError("AppErrorFromJson", "could not decode", err.Error()+" "+s)
}
}
diff --git a/model/version.go b/model/version.go
index 88334ceea..4a642d017 100644
--- a/model/version.go
+++ b/model/version.go
@@ -25,10 +25,10 @@ var versions = []string{
}
var CurrentVersion string = versions[0]
-var BuildNumber = "_BUILD_NUMBER_"
-var BuildDate = "_BUILD_DATE_"
-var BuildHash = "_BUILD_HASH_"
-var BuildEnterpriseReady = "_BUILD_ENTERPRISE_READY_"
+var BuildNumber = "dev"
+var BuildDate = "Fri Jan 8 14:19:26 UTC 2016"
+var BuildHash = "001a4448ca5fb0018eeb442915b473b121c04bf3"
+var BuildEnterpriseReady = "false"
func SplitVersion(version string) (int64, int64, int64) {
parts := strings.Split(version, ".")
@@ -74,7 +74,7 @@ func GetPreviousVersion(currentVersion string) (int64, int64) {
}
func IsOfficalBuild() bool {
- return BuildNumber != "_BUILD_NUMBER_"
+ return BuildNumber != "dev"
}
func IsCurrentVersion(versionToCheck string) bool {
diff --git a/store/sql_command_store.go b/store/sql_command_store.go
new file mode 100644
index 000000000..cb817d8f8
--- /dev/null
+++ b/store/sql_command_store.go
@@ -0,0 +1,174 @@
+// 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.NewAppError("SqlCommandStore.Save",
+ "You cannot overwrite an existing Command", "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.NewAppError("SqlCommandStore.Save", "We couldn't save the Command", "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.NewAppError("SqlCommandStore.Get", "We couldn't get the command", "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.NewAppError("SqlCommandStore.GetByTeam", "We couldn't get the commands", "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.NewAppError("SqlCommandStore.Delete", "We couldn't delete the command", "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.NewAppError("SqlCommandStore.DeleteByUser", "We couldn't delete the command", "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.NewAppError("SqlCommandStore.Update", "We couldn't update the command", "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 d0471fa1e..5ed715c2c 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
}
@@ -119,6 +120,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()
@@ -135,6 +137,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()
@@ -146,6 +149,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()
@@ -530,6 +534,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 b7bf0615f..c65384ec1 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.NewAppError("SqlWebhookStore.GetIncomingByUser", "We couldn't get the webhook", "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.NewAppError("SqlWebhookStore.GetIncomingByUser", "We couldn't get the webhook", "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.NewAppError("SqlWebhookStore.GetOutgoingByCreator", "We couldn't get the webhooks", "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 1a9d5be3b..5b43d0730 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 179cfecd7..3a865d52a 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()
@@ -168,13 +169,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
@@ -182,6 +182,15 @@ type WebhookStore interface {
UpdateOutgoing(hook *model.OutgoingWebhook) 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 12d03b5de..024c28d16 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -196,6 +196,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/create_post.jsx b/web/react/components/create_post.jsx
index a476863a3..d2f62334e 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -133,15 +133,10 @@ export default 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..1fb3bf599
--- /dev/null
+++ b/web/react/components/user_settings/manage_command_hooks.jsx
@@ -0,0 +1,442 @@
+// 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';
+
+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};
+ }
+
+ 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>{'Trigger: '}</strong>{cmd.trigger}
+ </div>
+ );
+ }
+
+ cmds.push(
+ <div
+ key={cmd.id}
+ className='webcmd__item'
+ >
+ <div className='padding-top x2'>
+ <strong>{'Display Name: '}</strong><span className='word-break--all'>{cmd.display_name}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Username: '}</strong><span className='word-break--all'>{cmd.username}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Icon URL: '}</strong><span className='word-break--all'>{cmd.icon_url}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Auto Complete: '}</strong><span className='word-break--all'>{cmd.auto_complete ? 'yes' : 'no'}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Auto Complete Description: '}</strong><span className='word-break--all'>{cmd.auto_complete_desc}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Auto Complete Hint: '}</strong><span className='word-break--all'>{cmd.auto_complete_hint}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Request Type: '}</strong><span className='word-break--all'>{cmd.method === 'P' ? 'POST' : 'GET'}</span>
+ </div>
+ <div className='padding-top x2 webcmd__url'>
+ <strong>{'URL: '}</strong><span className='word-break--all'>{cmd.url}</span>
+ </div>
+ {triggerDiv}
+ <div className='padding-top'>
+ <strong>{'Token: '}</strong>{cmd.token}
+ </div>
+ <div className='padding-top'>
+ <a
+ className='text-danger'
+ href='#'
+ onClick={this.regenToken.bind(this, cmd.id)}
+ >
+ {'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'>{'None'}</div>;
+ }
+
+ const existingCmds = (
+ <div className='webcmds__container'>
+ <label className='control-label padding-top x2'>{'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'>
+ {'Create commands to send new message events to an external integration. Please see '}
+ <a
+ href='http://mattermost.org/commands'
+ target='_blank'
+ >
+ {'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'>{'Display Name:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='displayName'
+ className='form-control'
+ value={this.state.cmd.display_name}
+ onChange={this.updateDisplayName}
+ placeholder='Display Name'
+ />
+ </div>
+ <div className='padding-top'>{'Command display name.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Username:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='username'
+ className='form-control'
+ value={this.state.cmd.username}
+ onChange={this.updateUsername}
+ placeholder='Username'
+ />
+ </div>
+ <div className='padding-top'>{'The username to use when overriding the post.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'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'>{'URL to an icon'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Trigger:'}</label>
+ <div className='padding-top'>
+ {'/'}
+ <input
+ ref='trigger'
+ className='form-control'
+ value={this.state.cmd.trigger}
+ onChange={this.updateTrigger}
+ placeholder='Command trigger e.g. "hello" not including the slash'
+ />
+ </div>
+ <div className='padding-top'>{'Word to trigger on'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Autocomplete:'}</label>
+ <div className='padding-top'>
+ <label>
+ <input
+ type='checkbox'
+ checked={this.state.cmd.auto_complete}
+ onChange={this.updateAutoComplete}
+ />
+ {'A short description of what this commands does'}
+ </label>
+ </div>
+ <div className='padding-top'>{'Show this command in autocomplete list.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Autocomplete Description:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='autoCompleteDesc'
+ className='form-control'
+ value={this.state.cmd.auto_complete_desc}
+ onChange={this.updateAutoCompleteDesc}
+ placeholder='A short description of what this commands does.'
+ />
+ </div>
+ <div className='padding-top'>{'A short description of what this commands does.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Autocomplete Hint:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='autoCompleteHint'
+ className='form-control'
+ value={this.state.cmd.auto_complete_hint}
+ onChange={this.updateAutoCompleteHint}
+ placeholder='[zipcode]'
+ />
+ </div>
+ <div className='padding-top'>{'List parameters to be passed to the command.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Request Type:'}</label>
+ <div className='padding-top'>
+ <select
+ ref='method'
+ className='form-control'
+ value={this.state.cmd.method}
+ onChange={this.updateMethod}
+ >
+ <option value='P'>{'POST'}</option>
+ <option value='G'>{'GET'}</option>
+ </select>
+ </div>
+ <div className='padding-top'>{'Command request type issued to the callback URL.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Callback URL:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='URL'
+ className='form-control'
+ value={this.state.cmd.url}
+ rows={1}
+ onChange={this.updateURL}
+ placeholder='Must start with http:// or https://'
+ />
+ </div>
+ <div className='padding-top'>{'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>
+ );
+ }
+}
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index a86510eb3..bcd1be13d 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';
export default class UserSettingsIntegrationsTab extends React.Component {
constructor(props) {
@@ -20,6 +21,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
render() {
let incomingHooksSection;
let outgoingHooksSection;
+ let commandHooksSection;
var inputs = [];
if (global.window.mm_config.EnableIncomingWebhooks === 'true') {
@@ -84,6 +86,37 @@ export default 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='Commands'
+ width='medium'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ commandHooksSection = (
+ <SettingItemMin
+ title='Commands'
+ width='medium'
+ describe='Manage your commands'
+ updateSection={() => {
+ this.updateSection('command-hooks');
+ }}
+ />
+ );
+ }
+ }
+
return (
<div>
<div className='modal-header'>
@@ -114,6 +147,8 @@ export default 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 0ee89b9fa..5378a2ba6 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -748,20 +748,28 @@ 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.trigger);
+ 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 d60fea872..07982b7be 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -846,11 +846,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);
@@ -859,6 +859,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/web_test.go b/web/web_test.go
index 8d40810b5..abe5ab2f1 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -193,6 +193,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}