diff --git a/api/command.go b/api/command.go index ab63a15a7f..a8573cdcc2 100644 --- a/api/command.go +++ b/api/command.go @@ -4,12 +4,11 @@ package api import ( - "io" + "fmt" + "io/ioutil" "net/http" - "path" - "strconv" + "net/url" "strings" - "time" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" @@ -17,630 +16,386 @@ import ( "github.com/mattermost/platform/utils" ) -type commandHandler func(c *Context, command *model.Command) bool +type CommandProvider interface { + GetTrigger() string + GetCommand(c *Context) *model.Command + DoCommand(c *Context, channelId string, message string) *model.CommandResponse +} -var ( - cmds = map[string]string{ - "logoutCommand": "/logout", - "joinCommand": "/join", - "loadTestCommand": "/loadtest", - "echoCommand": "/echo", - "shrugCommand": "/shrug", - "meCommand": "/me", +var commandProviders = make(map[string]CommandProvider) + +func RegisterCommandProvider(newProvider CommandProvider) { + commandProviders[newProvider.GetTrigger()] = newProvider +} + +func GetCommandProvider(name string) CommandProvider { + provider, ok := commandProviders[name] + if ok { + return provider } - commands = []commandHandler{ - logoutCommand, - joinCommand, - loadTestCommand, - echoCommand, - shrugCommand, - meCommand, - } - commandNotImplementedErr = model.NewLocAppError("checkCommand", "api.command.no_implemented.app_error", nil, "") -) -var echoSem chan bool + + return nil +} func InitCommand(r *mux.Router) { l4g.Debug(utils.T("api.command.init.debug")) - r.Handle("/command", ApiUserRequired(command)).Methods("POST") + + sr := r.PathPrefix("/commands").Subrouter() + + sr.Handle("/execute", ApiUserRequired(executeCommand)).Methods("POST") + sr.Handle("/list", ApiUserRequired(listCommands)).Methods("GET") + + 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") + + sr.Handle("/test", ApiAppHandler(testCommand)).Methods("POST") + sr.Handle("/test", ApiAppHandler(testCommand)).Methods("GET") } -func command(c *Context, w http.ResponseWriter, r *http.Request) { +func listCommands(c *Context, w http.ResponseWriter, r *http.Request) { + commands := make([]*model.Command, 0, 32) + seen := make(map[string]bool) + for _, value := range commandProviders { + cpy := *value.GetCommand(c) + if cpy.AutoComplete && !seen[cpy.Id] { + cpy.Sanitize() + seen[cpy.Trigger] = true + commands = append(commands, &cpy) + } + } + + if result := <-Srv.Store.Command().GetByTeam(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + teamCmds := result.Data.([]*model.Command) + for _, cmd := range teamCmds { + if cmd.AutoComplete && !seen[cmd.Id] { + cmd.Sanitize() + seen[cmd.Trigger] = true + commands = append(commands, cmd) + } + } + } + + w.Write([]byte(model.CommandListToJson(commands))) +} + +func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + command := strings.TrimSpace(props["command"]) + channelId := strings.TrimSpace(props["channelId"]) + + if len(command) <= 1 || strings.Index(command, "/") != 0 { + c.Err = model.NewLocAppError("executeCommand", "api.command.execute_command.start.app_error", nil, "") + return + } + + if len(channelId) > 0 { + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + + if !c.HasPermissionsToChannel(cchan, "checkCommand") { + return + } + } + + parts := strings.Split(command, " ") + trigger := parts[0][1:] + message := strings.Join(parts[1:], " ") + provider := GetCommandProvider(trigger) + + if provider != nil { + + response := provider.DoCommand(c, channelId, message) + handleResponse(c, w, response, channelId) + return + } else { + chanChan := Srv.Store.Channel().Get(channelId) + teamChan := Srv.Store.Team().Get(c.Session.TeamId) + userChan := Srv.Store.User().Get(c.Session.UserId) + + if result := <-Srv.Store.Command().GetByTeam(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + + var team *model.Team + if tr := <-teamChan; tr.Err != nil { + c.Err = tr.Err + return + } else { + team = tr.Data.(*model.Team) + + } + + var user *model.User + if ur := <-userChan; ur.Err != nil { + c.Err = ur.Err + return + } else { + user = ur.Data.(*model.User) + } + + var channel *model.Channel + if cr := <-chanChan; cr.Err != nil { + c.Err = cr.Err + return + } else { + channel = cr.Data.(*model.Channel) + } + + teamCmds := result.Data.([]*model.Command) + for _, cmd := range teamCmds { + if trigger == cmd.Trigger { + l4g.Debug(fmt.Sprintf(utils.T("api.command.execute_command.debug"), trigger, c.Session.UserId)) + + p := url.Values{} + p.Set("token", cmd.Token) + + p.Set("team_id", cmd.TeamId) + p.Set("team_domain", team.Name) + + p.Set("channel_id", channelId) + p.Set("channel_name", channel.Name) + + p.Set("user_id", c.Session.UserId) + p.Set("user_name", user.Username) + + p.Set("command", "/"+trigger) + p.Set("text", message) + p.Set("response_url", "not supported yet") + + method := "POST" + if cmd.Method == model.COMMAND_METHOD_GET { + method = "GET" + } + + client := &http.Client{} + req, _ := http.NewRequest(method, cmd.URL, strings.NewReader(p.Encode())) + req.Header.Set("Accept", "application/json") + if cmd.Method == model.COMMAND_METHOD_POST { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + if resp, err := client.Do(req); err != nil { + c.Err = model.NewLocAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error()) + } else { + if resp.StatusCode == http.StatusOK { + response := model.CommandResponseFromJson(resp.Body) + if response == nil { + c.Err = model.NewLocAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": trigger}, "") + } else { + handleResponse(c, w, response, channelId) + } + } else { + body, _ := ioutil.ReadAll(resp.Body) + c.Err = model.NewLocAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]interface{}{"Trigger": trigger, "Status": resp.Status}, string(body)) + } + } + + return + } + } + + } + } + + c.Err = model.NewLocAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "") +} + +func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandResponse, channelId string) { + if response.ResponseType == model.COMMAND_RESPONSE_TYPE_IN_CHANNEL { + post := &model.Post{} + post.ChannelId = channelId + post.Message = response.Text + if _, err := CreatePost(c, post, true); err != nil { + c.Err = model.NewLocAppError("command", "api.command.execute_command.save.app_error", nil, "") + } + } else if response.ResponseType == model.COMMAND_RESPONSE_TYPE_EPHEMERAL { + // post := &model.Post{} + // post.ChannelId = channelId + // post.Message = "TODO_EPHEMERAL: " + response.Text + // if _, err := CreatePost(c, post, true); err != nil { + // c.Err = model.NewLocAppError("command", "api.command.execute_command.save.app_error", nil, "") + // } + } + + w.Write([]byte(response.ToJson())) +} + +func createCommand(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + c.Err = model.NewLocAppError("createCommand", "api.command.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("createCommand", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + + c.LogAudit("attempt") + + cmd := model.CommandFromJson(r.Body) + + if cmd == nil { + c.SetInvalidParam("createCommand", "command") + return + } + + cmd.CreatorId = c.Session.UserId + cmd.TeamId = c.Session.TeamId + + 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())) + } +} + +func listTeamCommands(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + c.Err = model.NewLocAppError("listTeamCommands", "api.command.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("listTeamCommands", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + + if result := <-Srv.Store.Command().GetByTeam(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + cmds := result.Data.([]*model.Command) + w.Write([]byte(model.CommandListToJson(cmds))) + } +} + +func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + c.Err = model.NewLocAppError("regenCommandToken", "api.command.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("regenCommandToken", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + + c.LogAudit("attempt") props := model.MapFromJson(r.Body) - command := &model.Command{ - Command: strings.TrimSpace(props["command"]), - ChannelId: strings.TrimSpace(props["channelId"]), - Suggest: props["suggest"] == "true", - Suggestions: make([]*model.SuggestCommand, 0, 128), + id := props["id"] + if len(id) == 0 { + c.SetInvalidParam("regenCommandToken", "id") + return } - 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 - } + var cmd *model.Command + if result := <-Srv.Store.Command().Get(id); result.Err != nil { + c.Err = result.Err + return } else { - w.Write([]byte(command.ToJson())) + cmd = result.Data.(*model.Command) + + if c.Session.TeamId != cmd.TeamId || (c.Session.UserId != cmd.CreatorId && !c.IsTeamAdmin()) { + c.LogAudit("fail - inappropriate permissions") + c.Err = model.NewLocAppError("regenToken", "api.command.regen.app_error", nil, "user_id="+c.Session.UserId) + return + } + } + + cmd.Token = model.NewId() + + if result := <-Srv.Store.Command().Update(cmd); result.Err != nil { + c.Err = result.Err + return + } else { + w.Write([]byte(result.Data.(*model.Command).ToJson())) } } -func checkCommand(c *Context, command *model.Command) bool { - - if len(command.Command) == 0 || strings.Index(command.Command, "/") != 0 { - c.Err = model.NewLocAppError("checkCommand", "api.command.check_command.start.app_error", nil, "") - return false +func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + c.Err = model.NewLocAppError("deleteCommand", "api.command.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return } - if len(command.ChannelId) > 0 { - cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, command.ChannelId, c.Session.UserId) - - if !c.HasPermissionsToChannel(cchan, "checkCommand") { - return true + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("deleteCommand", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return } } - 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 + c.LogAudit("attempt") + + props := model.MapFromJson(r.Body) + + id := props["id"] + if len(id) == 0 { + c.SetInvalidParam("deleteCommand", "id") + return + } + + if result := <-Srv.Store.Command().Get(id); result.Err != nil { + c.Err = result.Err + return + } else { + if c.Session.TeamId != result.Data.(*model.Command).TeamId || (c.Session.UserId != result.Data.(*model.Command).CreatorId && !c.IsTeamAdmin()) { + c.LogAudit("fail - inappropriate permissions") + c.Err = model.NewLocAppError("deleteCommand", "api.command.delete.app_error", nil, "user_id="+c.Session.UserId) + return } } - for _, v := range commands { - - if v(c, command) || c.Err != nil { - return true - } + 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))) } -func logoutCommand(c *Context, command *model.Command) bool { +func testCommand(c *Context, w http.ResponseWriter, r *http.Request) { + r.ParseForm() - cmd := cmds["logoutCommand"] - - if strings.Index(command.Command, cmd) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.logout_command.description")}) - - if !command.Suggest { - command.GotoLocation = "/logout" - command.Response = model.RESP_EXECUTED - return true - } - - } else if strings.Index(cmd, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.logout_command.description")}) + msg := "" + if r.Method == "POST" { + msg = msg + "\ntoken=" + r.FormValue("token") + msg = msg + "\nteam_domain=" + r.FormValue("team_domain") + } else { + body, _ := ioutil.ReadAll(r.Body) + msg = string(body) } - return false -} - -func echoCommand(c *Context, command *model.Command) bool { - cmd := cmds["echoCommand"] - maxThreads := 100 - - if !command.Suggest && strings.Index(command.Command, cmd) == 0 { - parameters := strings.SplitN(command.Command, " ", 2) - if len(parameters) != 2 || len(parameters[1]) == 0 { - return false - } - message := strings.Trim(parameters[1], " ") - delay := 0 - if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 { - if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil { - delay = checkDelay - } - message = message[1:endMsg] - } else if strings.Index(message, " ") > -1 { - delayIdx := strings.LastIndex(message, " ") - delayStr := strings.Trim(message[delayIdx:], " ") - - if checkDelay, err := strconv.Atoi(delayStr); err == nil { - delay = checkDelay - message = message[:delayIdx] - } - } - - if delay > 10000 { - c.Err = model.NewLocAppError("echoCommand", "api.command.echo_command.under.app_error", nil, "") - return false - } - - if echoSem == nil { - // We want one additional thread allowed so we never reach channel lockup - echoSem = make(chan bool, maxThreads+1) - } - - if len(echoSem) >= maxThreads { - c.Err = model.NewLocAppError("echoCommand", "api.command.echo_command.high_volume.app_error", nil, "") - return false - } - - echoSem <- true - go func() { - defer func() { <-echoSem }() - post := &model.Post{} - post.ChannelId = command.ChannelId - post.Message = message - - time.Sleep(time.Duration(delay) * time.Second) - - if _, err := CreatePost(c, post, true); err != nil { - l4g.Error(utils.T("api.command.echo_command.create.error"), err) - } - }() - - command.Response = model.RESP_EXECUTED - return true - - } else if strings.Index(cmd, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.echo_command.description")}) - } - - return false -} - -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(utils.T("api.command.me_command.create.error"), 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: c.T("api.command.me_command.description")}) - } - - 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(utils.T("api.command.shrug_command.create.error"), 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: c.T("api.command.shrug_command.description")}) - } - - 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 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 - } - - JoinChannel(c, v.Id, "") - - if c.Err != nil { - return false - } - - command.GotoLocation = c.GetTeamURL() + "/channels/" + v.Name - command.Response = model.RESP_EXECUTED - return true - } - - if len(startsWith) == 0 || strings.Index(v.Name, startsWith) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd + " " + v.Name, Description: c.T("api.commmand.join_command.description")}) - } - } - } - } else if strings.Index(cmd, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.commmand.join_command.description")}) - } - - 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 - } - - if strings.Index(command.Command, cmd) == 0 { - if loadTestSetupCommand(c, command) { - return true - } - if loadTestUsersCommand(c, command) { - return true - } - if loadTestChannelsCommand(c, command) { - return true - } - if loadTestPostsCommand(c, command) { - return true - } - if loadTestUrlCommand(c, command) { - return true - } - } else if strings.Index(cmd, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: c.T("api.command.load_test_command.description")}) - } - - 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 - } - return utils.Range{begin, end}, true -} - -func contains(items []string, token string) bool { - for _, elem := range items { - if elem == token { - return true - } - } - 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(utils.T("api.command.load_test_setup_command.create.error")) - return true - } - client.LoginByEmail(BTEST_TEAM_NAME, BTEST_USER_EMAIL, BTEST_USER_PASSWORD) - environment, err := CreateTestEnvironmentWithTeams( - client, - utils.Range{numTeams, numTeams}, - utils.Range{numChannels, numChannels}, - utils.Range{numUsers, numUsers}, - utils.Range{numPosts, numPosts}, - doFuzz) - if err != true { - l4g.Error(utils.T("api.command.load_test_setup_command.create.error")) - return true - } else { - l4g.Info("Testing environment created") - for i := 0; i < len(environment.Teams); i++ { - l4g.Info(utils.T("api.command.load_test_setup_command.created.info"), environment.Teams[i].Name) - l4g.Info(utils.T("api.command.load_test_setup_command.login.info"), environment.Environments[i].Users[0].Email, USER_PASSWORD) - } - } - } else { - client.MockSession(c.Session.Token) - CreateTestEnvironmentInTeam( - client, - c.Session.TeamId, - utils.Range{numChannels, numChannels}, - utils.Range{numUsers, numUsers}, - utils.Range{numPosts, numPosts}, - doFuzz) - } - return true - } else if strings.Index(cmd, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{ - Suggestion: cmd, - Description: c.T("api.command.load_test_setup_command.description")}) - } - - return false -} - -func loadTestUsersCommand(c *Context, command *model.Command) bool { - cmd1 := cmds["loadTestCommand"] + " users" - cmd2 := cmds["loadTestCommand"] + " users fuzz" - - 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: c.T("api.command.load_test_users_command.users.description")}) - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_users_command.fuzz.description")}) - } else if strings.Index(cmd2, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_users_command.fuzz.description")}) - } - - return false -} - -func loadTestChannelsCommand(c *Context, command *model.Command) bool { - cmd1 := cmds["loadTestCommand"] + " channels" - cmd2 := cmds["loadTestCommand"] + " channels fuzz" - - 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} - } - client := model.NewClient(c.GetSiteURL()) - client.MockSession(c.Session.Token) - channelCreator := NewAutoChannelCreator(client, c.Session.TeamId) - channelCreator.Fuzzy = doFuzz - channelCreator.CreateTestChannels(channelsr) - return true - } else if strings.Index(cmd1, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd1, Description: c.T("api.command.load_test_channels_command.channel.description")}) - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_channels_command.fuzz.description")}) - } else if strings.Index(cmd2, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_channels_command.fuzz.description")}) - } - - return false -} - -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} - } - - 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: c.T("api.command.load_test_posts_command.posts.description")}) - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_posts_command.fuzz.description")}) - } else if strings.Index(cmd2, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd2, Description: c.T("api.command.load_test_posts_command.fuzz.description")}) - } - - 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.NewLocAppError("loadTestUrlCommand", "api.command.load_test_url_command.url.app_error", nil, "") - 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" - } - } - - var contents io.ReadCloser - if r, err := http.Get(url); err != nil { - c.Err = model.NewLocAppError("loadTestUrlCommand", "api.command.load_test_url_command.file.app_error", nil, err.Error()) - return false - } else if r.StatusCode > 400 { - c.Err = model.NewLocAppError("loadTestUrlCommand", "api.command.load_test_url_command.file.app_error", nil, r.Status) - return false - } else { - contents = r.Body - } - - 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.NewLocAppError("loadTestUrlCommand", "api.command.load_test_url_command.reading.app_error", nil, err.Error()) - return false - } - - if length == 0 { - break - } - - post := &model.Post{} - post.Message = string(bytes[:length]) - post.ChannelId = command.ChannelId - - if _, err := CreatePost(c, post, false); err != nil { - l4g.Error(utils.T("api.command.load_test_url_command.create.error"), err) - return false - } - } - - 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: c.T("api.command.load_test_url_command.description")}) - } - - return false + rc := &model.CommandResponse{ + Text: "test command response " + msg, + ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, + } + + w.Write([]byte(rc.ToJson())) } diff --git a/api/command_echo.go b/api/command_echo.go new file mode 100644 index 0000000000..805db7ad23 --- /dev/null +++ b/api/command_echo.go @@ -0,0 +1,89 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "strconv" + "strings" + "time" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" +) + +var echoSem chan bool + +type EchoProvider struct { +} + +const ( + CMD_ECHO = "echo" +) + +func init() { + RegisterCommandProvider(&EchoProvider{}) +} + +func (me *EchoProvider) GetTrigger() string { + return CMD_ECHO +} + +func (me *EchoProvider) GetCommand(c *Context) *model.Command { + return &model.Command{ + Trigger: CMD_ECHO, + AutoComplete: true, + AutoCompleteDesc: c.T("api.command_echo.desc"), + AutoCompleteHint: c.T("api.command_echo.hint"), + DisplayName: c.T("api.command_echo.name"), + } +} + +func (me *EchoProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse { + maxThreads := 100 + + delay := 0 + if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 { + if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil { + delay = checkDelay + } + message = message[1:endMsg] + } else if strings.Index(message, " ") > -1 { + delayIdx := strings.LastIndex(message, " ") + delayStr := strings.Trim(message[delayIdx:], " ") + + if checkDelay, err := strconv.Atoi(delayStr); err == nil { + delay = checkDelay + message = message[:delayIdx] + } + } + + if delay > 10000 { + return &model.CommandResponse{Text: c.T("api.command_echo.delay.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + if echoSem == nil { + // We want one additional thread allowed so we never reach channel lockup + echoSem = make(chan bool, maxThreads+1) + } + + if len(echoSem) >= maxThreads { + return &model.CommandResponse{Text: c.T("api.command_echo.high_volume.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + echoSem <- true + go func() { + defer func() { <-echoSem }() + post := &model.Post{} + post.ChannelId = channelId + post.Message = message + + time.Sleep(time.Duration(delay) * time.Second) + + if _, err := CreatePost(c, post, true); err != nil { + l4g.Error(c.T("api.command_echo.create.app_error"), err) + } + }() + + return &model.CommandResponse{} +} diff --git a/api/command_echo_test.go b/api/command_echo_test.go new file mode 100644 index 0000000000..3bfaa02794 --- /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 0000000000..ba3b0041e9 --- /dev/null +++ b/api/command_join.go @@ -0,0 +1,62 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" +) + +type JoinProvider struct { +} + +const ( + CMD_JOIN = "join" +) + +func init() { + RegisterCommandProvider(&JoinProvider{}) +} + +func (me *JoinProvider) GetTrigger() string { + return CMD_JOIN +} + +func (me *JoinProvider) GetCommand(c *Context) *model.Command { + return &model.Command{ + Trigger: CMD_JOIN, + AutoComplete: true, + AutoCompleteDesc: c.T("api.command_join.desc"), + AutoCompleteHint: c.T("api.command_join.hint"), + DisplayName: c.T("api.command_join.name"), + } +} + +func (me *JoinProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse { + if result := <-Srv.Store.Channel().GetMoreChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { + return &model.CommandResponse{Text: c.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + channels := result.Data.(*model.ChannelList) + + for _, v := range channels.Channels { + + if v.Name == message { + + if v.Type == model.CHANNEL_DIRECT { + return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + JoinChannel(c, v.Id, "") + + if c.Err != nil { + c.Err = nil + return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + return &model.CommandResponse{GotoLocation: c.GetTeamURL() + "/channels/" + v.Name, Text: c.T("api.command_join.success"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + } + } + + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command_join.missing.app_error")} +} diff --git a/api/command_join_test.go b/api/command_join_test.go new file mode 100644 index 0000000000..7260915a6a --- /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 0000000000..c7c4f98f50 --- /dev/null +++ b/api/command_loadtest.go @@ -0,0 +1,365 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "io" + "net/http" + "path" + "strconv" + "strings" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +var usage = `Mattermost load testing commands to help configure the system + + COMMANDS: + + Setup - Creates a testing environment in current team. + /loadtest setup [teams] [fuzz] + + 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] + + Example: + /loadtest users fuzz 5 10 + + Channels - Add a specified number of random channels with fuzz text to current team. + /loadtest channels [fuzz] + + Example: + /loadtest channels fuzz 5 10 + + Posts - Add some random posts with fuzz text to current channel. + /loadtest posts [fuzz] + + Example: + /loadtest posts fuzz 5 10 3 + + Url - Add a post containing the text from a given url to current channel. + /loadtest url + + Example: + /loadtest http://www.example.com/sample_file.md + + +` + +const ( + CMD_LOADTEST = "loadtest" +) + +type LoadTestProvider struct { +} + +func init() { + if !utils.Cfg.ServiceSettings.EnableTesting { + RegisterCommandProvider(&LoadTestProvider{}) + } +} + +func (me *LoadTestProvider) GetTrigger() string { + return CMD_LOADTEST +} + +func (me *LoadTestProvider) GetCommand(c *Context) *model.Command { + return &model.Command{ + Trigger: CMD_LOADTEST, + AutoComplete: false, + AutoCompleteDesc: "Debug Load Testing", + AutoCompleteHint: "help", + DisplayName: "loadtest", + } +} + +func (me *LoadTestProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse { + + //This command is only available when EnableTesting is true + if !utils.Cfg.ServiceSettings.EnableTesting { + return &model.CommandResponse{} + } + + if strings.HasPrefix(message, "setup") { + return me.SetupCommand(c, channelId, message) + } + + if strings.HasPrefix(message, "users") { + return me.UsersCommand(c, channelId, message) + } + + if strings.HasPrefix(message, "channels") { + return me.ChannelsCommand(c, channelId, message) + } + + if strings.HasPrefix(message, "posts") { + return me.PostsCommand(c, channelId, message) + } + + if strings.HasPrefix(message, "url") { + return me.UrlCommand(c, channelId, message) + } + + return me.HelpCommand(c, channelId, message) +} + +func (me *LoadTestProvider) HelpCommand(c *Context, channelId string, message string) *model.CommandResponse { + return &model.CommandResponse{Text: usage, ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) SetupCommand(c *Context, channelId string, message string) *model.CommandResponse { + tokens := strings.Fields(strings.TrimPrefix(message, "setup")) + doTeams := contains(tokens, "teams") + doFuzz := contains(tokens, "fuzz") + + numArgs := 0 + if doTeams { + numArgs++ + } + if doFuzz { + numArgs++ + } + + var numTeams int + var numChannels int + var numUsers int + var numPosts int + + // Defaults + numTeams = 10 + numChannels = 10 + numUsers = 10 + numPosts = 10 + + if doTeams { + if (len(tokens) - numArgs) >= 4 { + numTeams, _ = strconv.Atoi(tokens[numArgs+0]) + numChannels, _ = strconv.Atoi(tokens[numArgs+1]) + numUsers, _ = strconv.Atoi(tokens[numArgs+2]) + numPosts, _ = strconv.Atoi(tokens[numArgs+3]) + } + } else { + if (len(tokens) - numArgs) >= 3 { + numChannels, _ = strconv.Atoi(tokens[numArgs+0]) + numUsers, _ = strconv.Atoi(tokens[numArgs+1]) + numPosts, _ = strconv.Atoi(tokens[numArgs+2]) + } + } + client := model.NewClient(c.GetSiteURL()) + + if doTeams { + if err := CreateBasicUser(client); err != nil { + return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + client.LoginByEmail(BTEST_TEAM_NAME, BTEST_USER_EMAIL, BTEST_USER_PASSWORD) + environment, err := CreateTestEnvironmentWithTeams( + client, + utils.Range{numTeams, numTeams}, + utils.Range{numChannels, numChannels}, + utils.Range{numUsers, numUsers}, + utils.Range{numPosts, numPosts}, + doFuzz) + if err != true { + return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + l4g.Info("Testing environment created") + for i := 0; i < len(environment.Teams); i++ { + l4g.Info("Team Created: " + environment.Teams[i].Name) + l4g.Info("\t User to login: " + environment.Environments[i].Users[0].Email + ", " + USER_PASSWORD) + } + } + } else { + client.MockSession(c.Session.Token) + CreateTestEnvironmentInTeam( + client, + c.Session.TeamId, + utils.Range{numChannels, numChannels}, + utils.Range{numUsers, numUsers}, + utils.Range{numPosts, numPosts}, + doFuzz) + } + + return &model.CommandResponse{Text: "Creating enviroment...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) UsersCommand(c *Context, channelId string, message string) *model.CommandResponse { + cmd := strings.TrimSpace(strings.TrimPrefix(message, "users")) + + doFuzz := false + if strings.Index(cmd, "fuzz") == 0 { + doFuzz = true + cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "fuzz")) + } + + usersr, err := parseRange(cmd, "") + if err == false { + usersr = utils.Range{2, 5} + } + + client := model.NewClient(c.GetSiteURL()) + userCreator := NewAutoUserCreator(client, c.Session.TeamId) + userCreator.Fuzzy = doFuzz + userCreator.CreateTestUsers(usersr) + + return &model.CommandResponse{Text: "Adding users...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) ChannelsCommand(c *Context, channelId string, message string) *model.CommandResponse { + cmd := strings.TrimSpace(strings.TrimPrefix(message, "channels")) + + doFuzz := false + if strings.Index(cmd, "fuzz") == 0 { + doFuzz = true + cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "fuzz")) + } + + channelsr, err := parseRange(cmd, "") + if err == false { + channelsr = utils.Range{2, 5} + } + client := model.NewClient(c.GetSiteURL()) + client.MockSession(c.Session.Token) + channelCreator := NewAutoChannelCreator(client, c.Session.TeamId) + channelCreator.Fuzzy = doFuzz + channelCreator.CreateTestChannels(channelsr) + + return &model.CommandResponse{Text: "Adding channels...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) PostsCommand(c *Context, channelId string, message string) *model.CommandResponse { + cmd := strings.TrimSpace(strings.TrimPrefix(message, "posts")) + + doFuzz := false + if strings.Index(cmd, "fuzz") == 0 { + doFuzz = true + cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "fuzz")) + } + + postsr, err := parseRange(cmd, "") + if err == false { + postsr = utils.Range{20, 30} + } + + tokens := strings.Fields(cmd) + rimages := utils.Range{0, 0} + if len(tokens) >= 3 { + if numImages, err := strconv.Atoi(tokens[2]); err == nil { + rimages = utils.Range{numImages, numImages} + } + } + + var usernames []string + if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err == nil { + profileUsers := result.Data.(map[string]*model.User) + usernames = make([]string, len(profileUsers)) + i := 0 + for _, userprof := range profileUsers { + usernames[i] = userprof.Username + i++ + } + } + + client := model.NewClient(c.GetSiteURL()) + client.MockSession(c.Session.Token) + testPoster := NewAutoPostCreator(client, channelId) + testPoster.Fuzzy = doFuzz + testPoster.Users = usernames + + numImages := utils.RandIntFromRange(rimages) + numPosts := utils.RandIntFromRange(postsr) + for i := 0; i < numPosts; i++ { + testPoster.HasImage = (i < numImages) + testPoster.CreateRandomPost() + } + + return &model.CommandResponse{Text: "Adding posts...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) UrlCommand(c *Context, channelId string, message string) *model.CommandResponse { + url := strings.TrimSpace(strings.TrimPrefix(message, "url")) + if len(url) == 0 { + return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + // provide a shortcut to easily access tests stored in doc/developer/tests + if !strings.HasPrefix(url, "http") { + url = "https://raw.githubusercontent.com/mattermost/platform/master/doc/developer/tests/" + url + + if path.Ext(url) == "" { + url += ".md" + } + } + + var contents io.ReadCloser + if r, err := http.Get(url); err != nil { + return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else if r.StatusCode > 400 { + return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + contents = r.Body + } + + bytes := make([]byte, 4000) + + // break contents into 4000 byte posts + for { + length, err := contents.Read(bytes) + if err != nil && err != io.EOF { + return &model.CommandResponse{Text: "Encountered error reading file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + if length == 0 { + break + } + + post := &model.Post{} + post.Message = string(bytes[:length]) + post.ChannelId = channelId + + if _, err := CreatePost(c, post, false); err != nil { + return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + } + + return &model.CommandResponse{Text: "Loading data...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func parseRange(command string, cmd string) (utils.Range, bool) { + tokens := strings.Fields(strings.TrimPrefix(command, cmd)) + var begin int + var end int + var err1 error + var err2 error + switch { + case len(tokens) == 1: + begin, err1 = strconv.Atoi(tokens[0]) + end = begin + if err1 != nil { + return utils.Range{0, 0}, false + } + case len(tokens) >= 2: + begin, err1 = strconv.Atoi(tokens[0]) + end, err2 = strconv.Atoi(tokens[1]) + if err1 != nil || err2 != nil { + return utils.Range{0, 0}, false + } + default: + return utils.Range{0, 0}, false + } + return utils.Range{begin, end}, true +} + +func contains(items []string, token string) bool { + for _, elem := range items { + if elem == token { + return true + } + } + return false +} diff --git a/api/command_loadtest_test.go b/api/command_loadtest_test.go new file mode 100644 index 0000000000..7cb77cf180 --- /dev/null +++ b/api/command_loadtest_test.go @@ -0,0 +1,221 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "strings" + "testing" + "time" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" +) + +func TestLoadTestHelpCommands(t *testing.T) { + Setup() + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json + enableTesting := utils.Cfg.ServiceSettings.EnableTesting + defer func() { + utils.Cfg.ServiceSettings.EnableTesting = enableTesting + }() + + utils.Cfg.ServiceSettings.EnableTesting = true + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + rs := Client.Must(Client.Command(channel.Id, "/loadtest help", false)).Data.(*model.CommandResponse) + if !strings.Contains(rs.Text, "Mattermost load testing commands to help") { + t.Fatal(rs.Text) + } + + time.Sleep(2 * time.Second) +} + +func TestLoadTestSetupCommands(t *testing.T) { + Setup() + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json + enableTesting := utils.Cfg.ServiceSettings.EnableTesting + defer func() { + utils.Cfg.ServiceSettings.EnableTesting = enableTesting + }() + + utils.Cfg.ServiceSettings.EnableTesting = true + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + rs := Client.Must(Client.Command(channel.Id, "/loadtest setup fuzz 1 1 1", false)).Data.(*model.CommandResponse) + if rs.Text != "Creating enviroment..." { + t.Fatal(rs.Text) + } + + time.Sleep(2 * time.Second) +} + +func TestLoadTestUsersCommands(t *testing.T) { + Setup() + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json + enableTesting := utils.Cfg.ServiceSettings.EnableTesting + defer func() { + utils.Cfg.ServiceSettings.EnableTesting = enableTesting + }() + + utils.Cfg.ServiceSettings.EnableTesting = true + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + rs := Client.Must(Client.Command(channel.Id, "/loadtest users fuzz 1 2", false)).Data.(*model.CommandResponse) + if rs.Text != "Adding users..." { + t.Fatal(rs.Text) + } + + time.Sleep(2 * time.Second) +} + +func TestLoadTestChannelsCommands(t *testing.T) { + Setup() + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json + enableTesting := utils.Cfg.ServiceSettings.EnableTesting + defer func() { + utils.Cfg.ServiceSettings.EnableTesting = enableTesting + }() + + utils.Cfg.ServiceSettings.EnableTesting = true + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + rs := Client.Must(Client.Command(channel.Id, "/loadtest channels fuzz 1 2", false)).Data.(*model.CommandResponse) + if rs.Text != "Adding channels..." { + t.Fatal(rs.Text) + } + + time.Sleep(2 * time.Second) +} + +func TestLoadTestPostsCommands(t *testing.T) { + Setup() + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json + enableTesting := utils.Cfg.ServiceSettings.EnableTesting + defer func() { + utils.Cfg.ServiceSettings.EnableTesting = enableTesting + }() + + utils.Cfg.ServiceSettings.EnableTesting = true + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + rs := Client.Must(Client.Command(channel.Id, "/loadtest posts fuzz 2 3 2", false)).Data.(*model.CommandResponse) + if rs.Text != "Adding posts..." { + t.Fatal(rs.Text) + } + + time.Sleep(2 * time.Second) +} + +func TestLoadTestUrlCommands(t *testing.T) { + Setup() + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json + enableTesting := utils.Cfg.ServiceSettings.EnableTesting + defer func() { + utils.Cfg.ServiceSettings.EnableTesting = enableTesting + }() + + utils.Cfg.ServiceSettings.EnableTesting = true + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + command := "/loadtest url " + if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Command must contain a url" { + t.Fatal("/loadtest url with no url should've failed") + } + + command = "/loadtest url http://missingfiletonwhere/path/asdf/qwerty" + if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Unable to get file" { + t.Log(r.Text) + t.Fatal("/loadtest url with invalid url should've failed") + } + + command = "/loadtest url https://raw.githubusercontent.com/mattermost/platform/master/README.md" + if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Loading data..." { + t.Fatal("/loadtest url for README.md should've executed") + } + + command = "/loadtest url test-emoticons.md" + if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Loading data..." { + t.Fatal("/loadtest url for test-emoticons.md should've executed") + } + + command = "/loadtest url test-emoticons" + if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Loading data..." { + t.Fatal("/loadtest url for test-emoticons should've executed") + } + + posts := Client.Must(Client.GetPosts(channel.Id, 0, 5, "")).Data.(*model.PostList) + // note that this may make more than 3 posts if files are too long to fit in an individual post + if len(posts.Order) < 3 { + t.Fatal("/loadtest url made too few posts, perhaps there needs to be a delay before GetPosts in the test?") + } + + time.Sleep(2 * time.Second) +} diff --git a/api/command_logout.go b/api/command_logout.go new file mode 100644 index 0000000000..fb69b4f850 --- /dev/null +++ b/api/command_logout.go @@ -0,0 +1,37 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" +) + +type LogoutProvider struct { +} + +const ( + CMD_LOGOUT = "logout" +) + +func init() { + RegisterCommandProvider(&LogoutProvider{}) +} + +func (me *LogoutProvider) GetTrigger() string { + return CMD_LOGOUT +} + +func (me *LogoutProvider) GetCommand(c *Context) *model.Command { + return &model.Command{ + Trigger: CMD_LOGOUT, + AutoComplete: true, + AutoCompleteDesc: c.T("api.command_logout.desc"), + AutoCompleteHint: "", + DisplayName: c.T("api.command_logout.name"), + } +} + +func (me *LogoutProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse { + return &model.CommandResponse{GotoLocation: "/logout", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command_logout.success_message")} +} diff --git a/api/command_logout_test.go b/api/command_logout_test.go new file mode 100644 index 0000000000..86979316b7 --- /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 0000000000..c6147278bf --- /dev/null +++ b/api/command_me.go @@ -0,0 +1,37 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" +) + +type MeProvider struct { +} + +const ( + CMD_ME = "me" +) + +func init() { + RegisterCommandProvider(&MeProvider{}) +} + +func (me *MeProvider) GetTrigger() string { + return CMD_ME +} + +func (me *MeProvider) GetCommand(c *Context) *model.Command { + return &model.Command{ + Trigger: CMD_ME, + AutoComplete: true, + AutoCompleteDesc: c.T("api.command_me.desc"), + AutoCompleteHint: c.T("api.command_me.hint"), + DisplayName: c.T("api.command_me.name"), + } +} + +func (me *MeProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse { + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, Text: "*" + message + "*"} +} diff --git a/api/command_me_test.go b/api/command_me_test.go new file mode 100644 index 0000000000..d55a15b2c6 --- /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 0000000000..8fb5bc2001 --- /dev/null +++ b/api/command_shrug.go @@ -0,0 +1,42 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" +) + +type ShrugProvider struct { +} + +const ( + CMD_SHRUG = "shrug" +) + +func init() { + RegisterCommandProvider(&ShrugProvider{}) +} + +func (me *ShrugProvider) GetTrigger() string { + return CMD_SHRUG +} + +func (me *ShrugProvider) GetCommand(c *Context) *model.Command { + return &model.Command{ + Trigger: CMD_SHRUG, + AutoComplete: true, + AutoCompleteDesc: c.T("api.command_shrug.desc"), + AutoCompleteHint: c.T("api.command_shrug.hint"), + DisplayName: c.T("api.command_shrug.name"), + } +} + +func (me *ShrugProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse { + rmsg := `¯\\\_(ツ)\_/¯` + if len(message) > 0 { + rmsg = message + " " + rmsg + } + + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, Text: rmsg} +} diff --git a/api/command_shrug_test.go b/api/command_shrug_test.go new file mode 100644 index 0000000000..92cecf6648 --- /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 86eb297d5d..22e2bd6669 100644 --- a/api/command_test.go +++ b/api/command_test.go @@ -4,7 +4,6 @@ package api import ( - "strings" "testing" "time" @@ -13,7 +12,7 @@ import ( "github.com/mattermost/platform/utils" ) -func TestSuggestRootCommands(t *testing.T) { +func TestListCommands(t *testing.T) { Setup() team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} @@ -25,177 +24,89 @@ func TestSuggestRootCommands(t *testing.T) { Client.LoginByEmail(team.Name, user1.Email, "pwd") - if _, err := Client.Command("", "", true); err == nil { - t.Fatal("Should fail") - } + if results, err := Client.ListCommands(); err != nil { + t.Fatal(err) + } else { + commands := results.Data.([]*model.Command) + foundEcho := false - rs1 := Client.Must(Client.Command("", "/", true)).Data.(*model.Command) + for _, command := range commands { + if command.Trigger == "echo" { + foundEcho = true + } + } - hasLogout := false - for _, v := range rs1.Suggestions { - if v.Suggestion == "/logout" { - hasLogout = true + if !foundEcho { + t.Fatal("Couldn't find echo command") } } - - if !hasLogout { - t.Log(rs1.Suggestions) - t.Fatal("should have logout cmd") - } - - 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") - } } -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() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - 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() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - Client.Must(Client.LeaveChannel(channel1.Id)) - - channel2 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) - Client.Must(Client.LeaveChannel(channel2.Id)) - - user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - data := make(map[string]string) - data["user_id"] = user2.Id - channel3 := Client.Must(Client.CreateDirectChannel(data)).Data.(*model.Channel) - - 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") - } - - 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") - } - - rs3 := Client.Must(Client.Command("", "/join", true)).Data.(*model.Command) - if len(rs3.Suggestions) != 2 { - t.Fatal("should have 2 join cmd") - } - - rs4 := Client.Must(Client.Command("", "/join ", true)).Data.(*model.Command) - if len(rs4.Suggestions) != 2 { - t.Fatal("should have 2 join cmd") - } - - 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") - } - - 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") - } - - c1 := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList) - - if len(c1.Channels) != 4 { // 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") - } -} - -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() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - - Client.LoginByEmail(team.Name, user1.Email, "pwd") - - channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} - channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - - echoTestString := "/echo test" - - 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") - } - - 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") - } -} - -func TestLoadTestUrlCommand(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.EnableCommands = true - 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) + + 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)) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + + cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + + if _, err := Client.CreateCommand(cmd); err == nil { + t.Fatal("should have failed because not admin") + } + + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) + Client.LoginByEmail(team.Name, user.Email, "pwd") + + var rcmd *model.Command + if result, err := Client.CreateCommand(cmd); err != nil { + t.Fatal(err) + } else { + rcmd = result.Data.(*model.Command) + } + + if rcmd.CreatorId != user.Id { + t.Fatal("user ids didn't match") + } + + if rcmd.TeamId != team.Id { + t.Fatal("team ids didn't match") + } + + cmd = &model.Command{CreatorId: "123", TeamId: "456", URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + if result, err := Client.CreateCommand(cmd); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.Command).CreatorId != user.Id { + t.Fatal("bad user id wasn't overwritten") + } + if result.Data.(*model.Command).TeamId != team.Id { + t.Fatal("bad team id wasn't overwritten") + } + } +} + +func TestListTeamCommands(t *testing.T) { + Setup() + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands + defer func() { + utils.Cfg.ServiceSettings.EnableCommands = &enableCommands + }() + *utils.Cfg.ServiceSettings.EnableCommands = true team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) @@ -204,39 +115,160 @@ 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) + cmd1 := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + cmd1 = Client.Must(Client.CreateCommand(cmd1)).Data.(*model.Command) - command := "/loadtest url " - if _, err := Client.Command(channel.Id, command, false); err == nil { - t.Fatal("/loadtest url with no url should've failed") - } + if result, err := Client.ListTeamCommands(); err != nil { + t.Fatal(err) + } else { + cmds := result.Data.([]*model.Command) - // 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") - } - - 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") - } - - 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") - } - - 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?") + if len(cmds) != 1 { + t.Fatal("incorrect number of cmd") + } + } +} + +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) + + 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)) + + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) + Client.LoginByEmail(team.Name, user.Email, "pwd") + + cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + cmd = Client.Must(Client.CreateCommand(cmd)).Data.(*model.Command) + + data := make(map[string]string) + data["id"] = cmd.Id + + 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 TestDeleteCommand(t *testing.T) { + Setup() + enableCommands := *utils.Cfg.ServiceSettings.EnableCommands + defer func() { + utils.Cfg.ServiceSettings.EnableCommands = &enableCommands + }() + *utils.Cfg.ServiceSettings.EnableCommands = true + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) + Client.LoginByEmail(team.Name, user.Email, "pwd") + + cmd := &model.Command{URL: "http://nowhere.com", Method: model.COMMAND_METHOD_POST} + cmd = Client.Must(Client.CreateCommand(cmd)).Data.(*model.Command) + + data := make(map[string]string) + data["id"] = cmd.Id + + if _, err := Client.DeleteCommand(data); err != nil { + t.Fatal(err) + } + + cmds := Client.Must(Client.ListTeamCommands()).Data.([]*model.Command) + if len(cmds) != 0 { + t.Fatal("delete didn't work properly") + } +} + +func TestTestCommand(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) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) + Client.LoginByEmail(team.Name, user.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + cmd1 := &model.Command{ + URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + "/api/v1/commands/test", + Method: model.COMMAND_METHOD_POST, + Trigger: "test", + } + + cmd1 = Client.Must(Client.CreateCommand(cmd1)).Data.(*model.Command) + + r1 := Client.Must(Client.Command(channel1.Id, "/test", false)).Data.(*model.CommandResponse) + if r1 == nil { + t.Fatal("Test 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("Test command failed to send") + } + + cmd2 := &model.Command{ + URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + "/api/v1/commands/test", + Method: model.COMMAND_METHOD_GET, + Trigger: "test2", + } + + cmd2 = Client.Must(Client.CreateCommand(cmd2)).Data.(*model.Command) + + r2 := Client.Must(Client.Command(channel1.Id, "/test2", false)).Data.(*model.CommandResponse) + if r2 == nil { + t.Fatal("Test2 command failed to execute") + } + + time.Sleep(100 * time.Millisecond) + + p2 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList) + if len(p2.Order) != 2 { + t.Fatal("Test command failed to send") } } diff --git a/api/user.go b/api/user.go index 8b2df7143e..9926f3ff34 100644 --- a/api/user.go +++ b/api/user.go @@ -1542,6 +1542,10 @@ func PermanentDeleteUser(c *Context, user *model.User) *model.AppError { return result.Err } + if result := <-Srv.Store.Command().PermanentDeleteByUser(user.Id); result.Err != nil { + return result.Err + } + if result := <-Srv.Store.Preference().PermanentDeleteByUser(user.Id); result.Err != nil { return result.Err } diff --git a/api/webhook.go b/api/webhook.go index 1372fe3353..3906d09be6 100644 --- a/api/webhook.go +++ b/api/webhook.go @@ -32,6 +32,14 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("createIncomingHook", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + c.LogAudit("attempt") hook := model.IncomingWebhookFromJson(r.Body) @@ -79,6 +87,14 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("deleteIncomingHook", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + c.LogAudit("attempt") props := model.MapFromJson(r.Body) @@ -116,7 +132,15 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Webhook().GetIncomingByUser(c.Session.UserId); result.Err != nil { + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("getIncomingHooks", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + + if result := <-Srv.Store.Webhook().GetIncomingByTeam(c.Session.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -132,6 +156,14 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("createOutgoingHook", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + c.LogAudit("attempt") hook := model.OutgoingWebhookFromJson(r.Body) @@ -188,7 +220,15 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-Srv.Store.Webhook().GetOutgoingByCreator(c.Session.UserId); result.Err != nil { + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("getOutgoingHooks", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + + if result := <-Srv.Store.Webhook().GetOutgoingByTeam(c.Session.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -204,6 +244,14 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { return } + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("deleteOutgoingHook", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + c.LogAudit("attempt") props := model.MapFromJson(r.Body) @@ -241,6 +289,14 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) return } + if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations { + if !(c.IsSystemAdmin() || c.IsTeamAdmin()) { + c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.command.admin_only.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + } + c.LogAudit("attempt") props := model.MapFromJson(r.Body) @@ -258,7 +314,7 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) } else { hook = result.Data.(*model.OutgoingWebhook) - if c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() { + if c.Session.TeamId != hook.TeamId && c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() { c.LogAudit("fail - inappropriate permissions") c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.webhook.regen_outgoing_token.permissions.app_error", nil, "user_id="+c.Session.UserId) return diff --git a/api/webhook_test.go b/api/webhook_test.go index 0a464656b8..4f85d178d5 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 560073ad22..5ed05fecde 100644 --- a/config/config.json +++ b/config/config.json @@ -7,6 +7,8 @@ "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": false, "EnableOutgoingWebhooks": false, + "EnableCommands": false, + "EnableOnlyAdminIntegrations": true, "EnablePostUsernameOverride": false, "EnablePostIconOverride": false, "EnableTesting": false, diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index 80b99b66dc..e831bbb3a4 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -7,6 +7,8 @@ "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": false, "EnableOutgoingWebhooks": false, + "EnableCommands": false, + "EnableOnlyAdminIntegrations": true, "EnablePostUsernameOverride": false, "EnablePostIconOverride": false, "EnableTesting": false, diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index 80b99b66dc..e831bbb3a4 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -7,6 +7,8 @@ "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": false, "EnableOutgoingWebhooks": false, + "EnableCommands": false, + "EnableOnlyAdminIntegrations": true, "EnablePostUsernameOverride": false, "EnablePostIconOverride": false, "EnableTesting": false, diff --git a/i18n/en.json b/i18n/en.json index 2d86e1ee51..d72d6dca57 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -243,122 +243,142 @@ "id": "api.channel.update_channel.tried.app_error", "translation": "Tried to perform an invalid update of the default channel {{.Channel}}" }, - { - "id": "api.command.check_command.start.app_error", - "translation": "Command must start with /" - }, - { - "id": "api.command.echo_command.create.error", - "translation": "Unable to create /echo post, err=%v" - }, - { - "id": "api.command.echo_command.description", - "translation": "Echo back text from your account, /echo \"message\" [delay in seconds]" - }, - { - "id": "api.command.echo_command.high_volume.app_error", - "translation": "High volume of echo request, cannot process request" - }, - { - "id": "api.command.echo_command.under.app_error", - "translation": "Delays must be under 10000 seconds" - }, { "id": "api.command.init.debug", "translation": "Initializing command api routes" }, { - "id": "api.command.load_test_channels_command.channel.description", - "translation": "Add a specified number of random channels to current team " + "id": "api.command.execute_command.start.app_error", + "translation": "No command trigger found" }, { - "id": "api.command.load_test_channels_command.fuzz.description", - "translation": "Add a specified number of random channels with fuzz text to current team " + "id": "api.command.execute_command.debug", + "translation": "Executing cmd=%v userId=%v" }, { - "id": "api.command.load_test_command.description", - "translation": "Debug Load Testing" + "id": "api.command.execute_command.failed.app_error", + "translation": "Command with a trigger of '{{.Trigger}}' failed" }, { - "id": "api.command.load_test_posts_command.fuzz.description", - "translation": "Add some random posts with fuzz text to current channel " + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "Command with a trigger of '{{.Trigger}}' returned an empty response" }, { - "id": "api.command.load_test_posts_command.posts.description", - "translation": "Add some random posts to current channel " + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "Command with a trigger of '{{.Trigger}}' returned response {{.Status}}" }, { - "id": "api.command.load_test_setup_command.create.error", - "translation": "Failed to create testing environment" + "id": "api.command.execute_command.not_found.app_error", + "translation": "Command with a trigger of '{{.Trigger}}' not found" }, { - "id": "api.command.load_test_setup_command.created.info", - "translation": "Team Created: %v" + "id": "api.command.execute_command.save.app_error", + "translation": "An error while saving the command response to the channel" }, { - "id": "api.command.load_test_setup_command.description", - "translation": "Creates a testing environment in current team. [teams] [fuzz] " + "id": "api.command.disabled.app_error", + "translation": "Commands have been disabled by the system admin." }, { - "id": "api.command.load_test_setup_command.login.info", - "translation": "\t User to login: %v, %v" + "id": "api.command.admin_only.app_error", + "translation": "Integrations have been limited to admins only." }, { - "id": "api.command.load_test_url_command.create.error", - "translation": "Unable to create post, err=%v" + "id": "api.command.regen.app_error", + "translation": "Inappropriate permissions to regenerate command token" }, { - "id": "api.command.load_test_url_command.description", - "translation": "Add a post containing the text from a given url to current channel " + "id": "api.command.delete.app_error", + "translation": "Inappropriate permissions to delete command" }, { - "id": "api.command.load_test_url_command.file.app_error", - "translation": "Unable to get file" + "id": "api.command_echo.desc", + "translation": "Echo back text from your account" }, { - "id": "api.command.load_test_url_command.reading.app_error", - "translation": "Encountered error reading file" + "id": "api.command_echo.hint", + "translation": "'message' [delay in seconds]" }, { - "id": "api.command.load_test_url_command.url.app_error", - "translation": "Command must contain a url" + "id": "api.command_echo.name", + "translation": "echo" }, { - "id": "api.command.load_test_users_command.fuzz.description", - "translation": "Add a specified number of random users with fuzz text to current team " + "id": "api.command_echo.delay.app_error", + "translation": "Delays must be under 10000 seconds" }, { - "id": "api.command.load_test_users_command.users.description", - "translation": "Add a specified number of random users to current team " + "id": "api.command_echo.high_volume.app_error", + "translation": "High volume of echo request, cannot process request" }, { - "id": "api.command.logout_command.description", - "translation": "Logout" + "id": "api.command_echo.create.app_error", + "translation": "Unable to create /echo post, err=%v" }, { - "id": "api.command.me_command.create.error", - "translation": "Unable to create /me post post, err=%v" - }, - { - "id": "api.command.me_command.description", - "translation": "Do an action, /me [message]" - }, - { - "id": "api.command.no_implemented.app_error", - "translation": "Command not implemented" - }, - { - "id": "api.command.shrug_command.create.error", - "translation": "Unable to create /shrug post post, err=%v" - }, - { - "id": "api.command.shrug_command.description", - "translation": "Adds ¯\\_(ツ)_/¯ to your message, /shrug [message]" - }, - { - "id": "api.commmand.join_command.description", + "id": "api.command_join.desc", "translation": "Join the open channel" }, + { + "id": "api.command_join.hint", + "translation": "[channel-name]" + }, + { + "id": "api.command_join.name", + "translation": "join" + }, + { + "id": "api.command_join.list.app_error", + "translation": "An error occured while listing channels." + }, + { + "id": "api.command_join.fail.app_error", + "translation": "An error occured while joining the channel." + }, + { + "id": "api.command_join.success", + "translation": "Joined channel." + }, + { + "id": "api.command_join.missing.app_error", + "translation": "We couldn't find the channel" + }, + { + "id": "api.command_logout.desc", + "translation": "Logout of Mattermost" + }, + { + "id": "api.command_logout.name", + "translation": "logout" + }, + { + "id": "api.command_logout.success_message", + "translation": "Logging out..." + }, + { + "id": "api.command_me.desc", + "translation": "Do an action" + }, + { + "id": "api.command_me.hint", + "translation": "[message]" + }, + { + "id": "api.command_me.name", + "translation": "me" + }, + { + "id": "api.command_shrug.desc", + "translation": "Adds ¯\\_(ツ)_/¯ to your message" + }, + { + "id": "api.command_shrug.hint", + "translation": "[message]" + }, + { + "id": "api.command_shrug.name", + "translation": "shrug" + }, { "id": "api.context.404.app_error", "translation": "Sorry, we could not find the page." @@ -1807,6 +1827,46 @@ "id": "model.channel.is_valid.2_or_more.app_error", "translation": "Name must be 2 or more lowercase alphanumeric characters" }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "Invalid Id" + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "Invalid token" + }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "Update at must be a valid time" + }, + { + "id": "model.command.is_valid.user_id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "Invalid team id" + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "Invalid trigger" + }, + { + "id": "model.command.is_valid.url.app_error", + "translation": "Invalid url" + }, + { + "id": "model.command.is_valid.url_http.app_error", + "translation": "Invalid URL. Must be a valid URL and start with http:// or https://" + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "Invalid Method" + }, { "id": "model.channel.is_valid.create_at.app_error", "translation": "Create at must be a valid time" @@ -2375,6 +2435,34 @@ "id": "store.sql_audit.save.saving.app_error", "translation": "We encountered an error saving the audit" }, + { + "id": "store.sql_command.save.saving_overwrite.app_error", + "translation": "You cannot overwrite an existing Command" + }, + { + "id": "store.sql_command.save.saving.app_error", + "translation": "We couldn't save the Command" + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "We couldn't get the command" + }, + { + "id": "store.sql_command.save.get_team.app_error", + "translation": "We couldn't get the commands" + }, + { + "id": "store.sql_command.save.delete.app_error", + "translation": "We couldn't delete the command" + }, + { + "id": "store.sql_command.save.delete_perm.app_error", + "translation": "We couldn't delete the command" + }, + { + "id": "store.sql_command.save.update.app_error", + "translation": "We couldn't update the command" + }, { "id": "store.sql_channel.analytics_type_count.app_error", "translation": "We couldn't get channel type counts" @@ -3043,10 +3131,6 @@ "id": "store.sql_webhooks.get_outgoing_by_channel.app_error", "translation": "We couldn't get the webhooks" }, - { - "id": "store.sql_webhooks.get_outgoing_by_creator.app_error", - "translation": "We couldn't get the webhooks" - }, { "id": "store.sql_webhooks.get_outgoing_by_team.app_error", "translation": "We couldn't get the webhooks" diff --git a/i18n/es.json b/i18n/es.json index 9599fe8791..5c1998234f 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -243,122 +243,6 @@ "id": "api.channel.update_channel.tried.app_error", "translation": "Intento de realizar una actualización inválida al canal predeterminado {{.Channel}}" }, - { - "id": "api.command.check_command.start.app_error", - "translation": "El comando debe comenzar con /" - }, - { - "id": "api.command.echo_command.create.error", - "translation": "No se puede crear /echo mensaje, err=%v" - }, - { - "id": "api.command.echo_command.description", - "translation": "Echo del texto desde tu cuenta, /echo \"mensaje\" [retraso en segundos]" - }, - { - "id": "api.command.echo_command.high_volume.app_error", - "translation": "Volumen alto de solicitudes echo, no se puede procesar la solicitud" - }, - { - "id": "api.command.echo_command.under.app_error", - "translation": "El retraso debe ser menor a 10000 segundos" - }, - { - "id": "api.command.init.debug", - "translation": "Inicializando rutas del API para los comandos" - }, - { - "id": "api.command.load_test_channels_command.channel.description", - "translation": "Agrega un número específico de canales aleatorios al equipo actual " - }, - { - "id": "api.command.load_test_channels_command.fuzz.description", - "translation": "Agrega un número específico de canales aleatorios con el texto fuzz al equipo actual " - }, - { - "id": "api.command.load_test_command.description", - "translation": "Depurar pruebas de carga" - }, - { - "id": "api.command.load_test_posts_command.fuzz.description", - "translation": "Agrega algunos mensajes aleatorios con el texto fuzz al canal actual " - }, - { - "id": "api.command.load_test_posts_command.posts.description", - "translation": "Agrega algunos mensajes aleatorios al canal actual " - }, - { - "id": "api.command.load_test_setup_command.create.error", - "translation": "Falla al crear el entorno de pruebas" - }, - { - "id": "api.command.load_test_setup_command.created.info", - "translation": "Equipo Creado: %v" - }, - { - "id": "api.command.load_test_setup_command.description", - "translation": "Crea un entorno de prueba para el equipo actual. [equipos] [fuzz] " - }, - { - "id": "api.command.load_test_setup_command.login.info", - "translation": "\t Usuario que inicia sesión: %v, %v" - }, - { - "id": "api.command.load_test_url_command.create.error", - "translation": "No se pudo crear el mensaje, err=%v" - }, - { - "id": "api.command.load_test_url_command.description", - "translation": "Agraga un mensaje que contiene el texto de un url dado al canal actual " - }, - { - "id": "api.command.load_test_url_command.file.app_error", - "translation": "No se puede obtener el archivo" - }, - { - "id": "api.command.load_test_url_command.reading.app_error", - "translation": "Se encontró un error leyendo el archivo" - }, - { - "id": "api.command.load_test_url_command.url.app_error", - "translation": "El comando debe contener un url" - }, - { - "id": "api.command.load_test_users_command.fuzz.description", - "translation": "Agraga un número específico de usuarios aleatorios con el texto fuzz al equipo actual " - }, - { - "id": "api.command.load_test_users_command.users.description", - "translation": "Agrega un número específico de usuarios aleatorios al equipo actual " - }, - { - "id": "api.command.logout_command.description", - "translation": "Cerrar sesión" - }, - { - "id": "api.command.me_command.create.error", - "translation": "No se pudo crear el mensaje /me mensaje, err=%v" - }, - { - "id": "api.command.me_command.description", - "translation": "Realiza una acción, /me [mensaje]" - }, - { - "id": "api.command.no_implemented.app_error", - "translation": "Comando no implementado" - }, - { - "id": "api.command.shrug_command.create.error", - "translation": "No se pudo crear el mensaje /shrug mensaje, err=%v" - }, - { - "id": "api.command.shrug_command.description", - "translation": "Agrega ¯\\_(ツ)_/¯ a tu mensaje, /shrug [mensaje]" - }, - { - "id": "api.commmand.join_command.description", - "translation": "Unirme al canal abierto" - }, { "id": "api.context.404.app_error", "translation": "Lo sentimos, pero no pudimos encontrar la página." @@ -3051,10 +2935,6 @@ "id": "store.sql_webhooks.get_outgoing_by_channel.app_error", "translation": "No pudimos obtener los webhooks" }, - { - "id": "store.sql_webhooks.get_outgoing_by_creator.app_error", - "translation": "No pudimos obtener los webhooks" - }, { "id": "store.sql_webhooks.get_outgoing_by_team.app_error", "translation": "No pudimos obtener los webhooks" diff --git a/model/client.go b/model/client.go index a6a527963a..560e47b76c 100644 --- a/model/client.go +++ b/model/client.go @@ -380,7 +380,34 @@ 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), @@ -388,6 +415,24 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul } } +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), + r.Header.Get(HEADER_ETAG_SERVER), CommandFromJson(r.Body)}, nil + } +} + +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 5aec5f5343..56d88f13c5 100644 --- a/model/command.go +++ b/model/command.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package model @@ -9,28 +9,27 @@ import ( ) const ( - RESP_EXECUTED = "executed" - RESP_NOT_IMPLEMENTED = "not implemented" + COMMAND_METHOD_POST = "P" + COMMAND_METHOD_GET = "G" ) type Command struct { - Command string `json:"command"` - Response string `json:"response"` - GotoLocation string `json:"goto_location"` - ChannelId string `json:"channel_id"` - Suggest bool `json:"-"` - Suggestions []*SuggestCommand `json:"suggestions"` -} - -func (o *Command) AddSuggestion(suggest *SuggestCommand) { - - if o.Suggest { - if o.Suggestions == nil { - o.Suggestions = make([]*SuggestCommand, 0, 128) - } - - o.Suggestions = append(o.Suggestions, suggest) - } + Id string `json:"id"` + Token string `json:"token"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + CreatorId string `json:"creator_id"` + TeamId string `json:"team_id"` + Trigger string `json:"trigger"` + Method string `json:"method"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + AutoComplete bool `json:"auto_complete"` + AutoCompleteDesc string `json:"auto_complete_desc"` + AutoCompleteHint string `json:"auto_complete_hint"` + DisplayName string `json:"display_name"` + URL string `json:"url"` } func (o *Command) ToJson() string { @@ -52,3 +51,94 @@ func CommandFromJson(data io.Reader) *Command { return nil } } + +func CommandListToJson(l []*Command) string { + b, err := json.Marshal(l) + if err != nil { + return "" + } else { + return string(b) + } +} + +func CommandListFromJson(data io.Reader) []*Command { + decoder := json.NewDecoder(data) + var o []*Command + err := decoder.Decode(&o) + if err == nil { + return o + } else { + return nil + } +} + +func (o *Command) IsValid() *AppError { + + if len(o.Id) != 26 { + return NewLocAppError("Command.IsValid", "model.command.is_valid.id.app_error", nil, "") + } + + if len(o.Token) != 26 { + return NewLocAppError("Command.IsValid", "model.command.is_valid.token.app_error", nil, "") + } + + if o.CreateAt == 0 { + return NewLocAppError("Command.IsValid", "model.command.is_valid.create_at.app_error", nil, "") + } + + if o.UpdateAt == 0 { + return NewLocAppError("Command.IsValid", "model.command.is_valid.update_at.app_error", nil, "") + } + + if len(o.CreatorId) != 26 { + return NewLocAppError("Command.IsValid", "model.command.is_valid.user_id.app_error", nil, "") + } + + if len(o.TeamId) != 26 { + return NewLocAppError("Command.IsValid", "model.command.is_valid.team_id.app_error", nil, "") + } + + if len(o.Trigger) > 1024 { + return NewLocAppError("Command.IsValid", "model.command.is_valid.trigger.app_error", nil, "") + } + + if len(o.URL) == 0 || len(o.URL) > 1024 { + return NewLocAppError("Command.IsValid", "model.command.is_valid.url.app_error", nil, "") + } + + if !IsValidHttpUrl(o.URL) { + return NewLocAppError("Command.IsValid", "model.command.is_valid.url_http.app_error", nil, "") + } + + if !(o.Method == COMMAND_METHOD_GET || o.Method == COMMAND_METHOD_POST) { + return NewLocAppError("Command.IsValid", "model.command.is_valid.method.app_error", nil, "") + } + + return nil +} + +func (o *Command) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + if o.Token == "" { + o.Token = NewId() + } + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt +} + +func (o *Command) PreUpdate() { + o.UpdateAt = GetMillis() +} + +func (o *Command) Sanitize() { + o.Token = "" + o.CreatorId = "" + o.Method = "" + o.URL = "" + o.Username = "" + o.IconURL = "" +} diff --git a/model/command_response.go b/model/command_response.go new file mode 100644 index 0000000000..9314f38ef3 --- /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 0000000000..7aa3e984b6 --- /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 61302ea107..0581625d91 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 { - t.Fatal("Ids do not match") - } - - if command.Suggestions[0].Suggestion != result.Suggestions[0].Suggestion { + if o.Id != ro.Id { 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 a6d1c21dc7..acb525abf7 100644 --- a/model/config.go +++ b/model/config.go @@ -24,24 +24,26 @@ const ( ) type ServiceSettings struct { - ListenAddress string - MaximumLoginAttempts int - SegmentDeveloperKey string - GoogleDeveloperKey string - EnableOAuthServiceProvider bool - EnableIncomingWebhooks bool - EnableOutgoingWebhooks bool - EnablePostUsernameOverride bool - EnablePostIconOverride bool - EnableTesting bool - EnableDeveloper *bool - EnableSecurityFixAlert *bool - SessionLengthWebInDays *int - SessionLengthMobileInDays *int - SessionLengthSSOInDays *int - SessionCacheInMinutes *int - WebsocketSecurePort *int - WebsocketPort *int + ListenAddress string + MaximumLoginAttempts int + SegmentDeveloperKey string + GoogleDeveloperKey string + EnableOAuthServiceProvider bool + EnableIncomingWebhooks bool + EnableOutgoingWebhooks bool + EnableCommands *bool + EnableOnlyAdminIntegrations *bool + EnablePostUsernameOverride bool + EnablePostIconOverride bool + EnableTesting bool + EnableDeveloper *bool + EnableSecurityFixAlert *bool + SessionLengthWebInDays *int + SessionLengthMobileInDays *int + SessionLengthSSOInDays *int + SessionCacheInMinutes *int + WebsocketSecurePort *int + WebsocketPort *int } type SSOSettings struct { @@ -349,10 +351,22 @@ func (o *Config) SetDefaults() { o.ServiceSettings.SessionCacheInMinutes = new(int) *o.ServiceSettings.SessionCacheInMinutes = 10 } + + if o.ServiceSettings.EnableCommands == nil { + o.ServiceSettings.EnableCommands = new(bool) + *o.ServiceSettings.EnableCommands = false + } + + if o.ServiceSettings.EnableOnlyAdminIntegrations == nil { + o.ServiceSettings.EnableOnlyAdminIntegrations = new(bool) + *o.ServiceSettings.EnableOnlyAdminIntegrations = true + } + if o.ServiceSettings.WebsocketPort == nil { o.ServiceSettings.WebsocketPort = new(int) *o.ServiceSettings.WebsocketPort = 80 } + if o.ServiceSettings.WebsocketSecurePort == nil { o.ServiceSettings.WebsocketSecurePort = new(int) *o.ServiceSettings.WebsocketSecurePort = 443 diff --git a/store/sql_command_store.go b/store/sql_command_store.go new file mode 100644 index 0000000000..760235e103 --- /dev/null +++ b/store/sql_command_store.go @@ -0,0 +1,173 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" +) + +type SqlCommandStore struct { + *SqlStore +} + +func NewSqlCommandStore(sqlStore *SqlStore) CommandStore { + s := &SqlCommandStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + tableo := db.AddTableWithName(model.Command{}, "Commands").SetKeys(false, "Id") + tableo.ColMap("Id").SetMaxSize(26) + tableo.ColMap("Token").SetMaxSize(26) + tableo.ColMap("CreatorId").SetMaxSize(26) + tableo.ColMap("TeamId").SetMaxSize(26) + tableo.ColMap("Trigger").SetMaxSize(128) + tableo.ColMap("URL").SetMaxSize(1024) + tableo.ColMap("Method").SetMaxSize(1) + tableo.ColMap("Username").SetMaxSize(64) + tableo.ColMap("IconURL").SetMaxSize(1024) + tableo.ColMap("AutoCompleteDesc").SetMaxSize(1024) + tableo.ColMap("AutoCompleteHint").SetMaxSize(1024) + tableo.ColMap("DisplayName").SetMaxSize(64) + } + + return s +} + +func (s SqlCommandStore) UpgradeSchemaIfNeeded() { +} + +func (s SqlCommandStore) CreateIndexesIfNotExists() { + s.CreateIndexIfNotExists("idx_command_team_id", "Commands", "TeamId") +} + +func (s SqlCommandStore) Save(command *model.Command) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if len(command.Id) > 0 { + result.Err = model.NewLocAppError("SqlCommandStore.Save", "store.sql_command.save.saving_overwrite.app_error", nil, "id="+command.Id) + storeChannel <- result + close(storeChannel) + return + } + + command.PreSave() + if result.Err = command.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetMaster().Insert(command); err != nil { + result.Err = model.NewLocAppError("SqlCommandStore.Save", "store.sql_command.save.saving.app_error", nil, "id="+command.Id+", "+err.Error()) + } else { + result.Data = command + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlCommandStore) Get(id string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var command model.Command + + if err := s.GetReplica().SelectOne(&command, "SELECT * FROM Commands WHERE Id = :Id AND DeleteAt = 0", map[string]interface{}{"Id": id}); err != nil { + result.Err = model.NewLocAppError("SqlCommandStore.Get", "store.sql_command.save.get.app_error", nil, "id="+id+", err="+err.Error()) + } + + result.Data = &command + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlCommandStore) GetByTeam(teamId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var commands []*model.Command + + if _, err := s.GetReplica().Select(&commands, "SELECT * FROM Commands WHERE TeamId = :TeamId AND DeleteAt = 0", map[string]interface{}{"TeamId": teamId}); err != nil { + result.Err = model.NewLocAppError("SqlCommandStore.GetByTeam", "store.sql_command.save.get_team.app_error", nil, "teamId="+teamId+", err="+err.Error()) + } + + result.Data = commands + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlCommandStore) Delete(commandId string, time int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec("Update Commands SET DeleteAt = :DeleteAt, UpdateAt = :UpdateAt WHERE Id = :Id", map[string]interface{}{"DeleteAt": time, "UpdateAt": time, "Id": commandId}) + if err != nil { + result.Err = model.NewLocAppError("SqlCommandStore.Delete", "store.sql_command.save.delete.app_error", nil, "id="+commandId+", err="+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlCommandStore) PermanentDeleteByUser(userId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + _, err := s.GetMaster().Exec("DELETE FROM Commands WHERE CreatorId = :UserId", map[string]interface{}{"UserId": userId}) + if err != nil { + result.Err = model.NewLocAppError("SqlCommandStore.DeleteByUser", "store.sql_command.save.delete_perm.app_error", nil, "id="+userId+", err="+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlCommandStore) Update(hook *model.Command) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + hook.UpdateAt = model.GetMillis() + + if _, err := s.GetMaster().Update(hook); err != nil { + result.Err = model.NewLocAppError("SqlCommandStore.Update", "store.sql_command.save.update.app_error", nil, "id="+hook.Id+", "+err.Error()) + } else { + result.Data = hook + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_command_store_test.go b/store/sql_command_store_test.go new file mode 100644 index 0000000000..b4610d4aa1 --- /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 335cb207c1..8517eb1a24 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -47,6 +47,7 @@ type SqlStore struct { oauth OAuthStore system SystemStore webhook WebhookStore + command CommandStore preference PreferenceStore } @@ -100,6 +101,7 @@ func NewSqlStore() Store { sqlStore.oauth = NewSqlOAuthStore(sqlStore) sqlStore.system = NewSqlSystemStore(sqlStore) sqlStore.webhook = NewSqlWebhookStore(sqlStore) + sqlStore.command = NewSqlCommandStore(sqlStore) sqlStore.preference = NewSqlPreferenceStore(sqlStore) err := sqlStore.master.CreateTablesIfNotExists() @@ -116,6 +118,7 @@ func NewSqlStore() Store { sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded() sqlStore.system.(*SqlSystemStore).UpgradeSchemaIfNeeded() sqlStore.webhook.(*SqlWebhookStore).UpgradeSchemaIfNeeded() + sqlStore.command.(*SqlCommandStore).UpgradeSchemaIfNeeded() sqlStore.preference.(*SqlPreferenceStore).UpgradeSchemaIfNeeded() sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists() @@ -127,6 +130,7 @@ func NewSqlStore() Store { sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists() sqlStore.system.(*SqlSystemStore).CreateIndexesIfNotExists() sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists() + sqlStore.command.(*SqlCommandStore).CreateIndexesIfNotExists() sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists() sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures() @@ -511,6 +515,10 @@ func (ss SqlStore) Webhook() WebhookStore { return ss.webhook } +func (ss SqlStore) Command() CommandStore { + return ss.command +} + func (ss SqlStore) Preference() PreferenceStore { return ss.preference } diff --git a/store/sql_webhook_store.go b/store/sql_webhook_store.go index 5298b0b942..740c9a33f0 100644 --- a/store/sql_webhook_store.go +++ b/store/sql_webhook_store.go @@ -134,7 +134,7 @@ func (s SqlWebhookStore) PermanentDeleteIncomingByUser(userId string) StoreChann return storeChannel } -func (s SqlWebhookStore) GetIncomingByUser(userId string) StoreChannel { +func (s SqlWebhookStore) GetIncomingByTeam(teamId string) StoreChannel { storeChannel := make(StoreChannel) go func() { @@ -142,8 +142,8 @@ func (s SqlWebhookStore) GetIncomingByUser(userId string) StoreChannel { var webhooks []*model.IncomingWebhook - if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM IncomingWebhooks WHERE UserId = :UserId AND DeleteAt = 0", map[string]interface{}{"UserId": userId}); err != nil { - result.Err = model.NewLocAppError("SqlWebhookStore.GetIncomingByUser", "store.sql_webhooks.get_incoming_by_user.app_error", nil, "userId="+userId+", err="+err.Error()) + if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM IncomingWebhooks WHERE TeamId = :TeamId AND DeleteAt = 0", map[string]interface{}{"TeamId": teamId}); err != nil { + result.Err = model.NewLocAppError("SqlWebhookStore.GetIncomingByUser", "store.sql_webhooks.get_incoming_by_user.app_error", nil, "teamId="+teamId+", err="+err.Error()) } result.Data = webhooks @@ -231,27 +231,6 @@ func (s SqlWebhookStore) GetOutgoing(id string) StoreChannel { return storeChannel } -func (s SqlWebhookStore) GetOutgoingByCreator(userId string) StoreChannel { - storeChannel := make(StoreChannel) - - go func() { - result := StoreResult{} - - var webhooks []*model.OutgoingWebhook - - if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM OutgoingWebhooks WHERE CreatorId = :UserId AND DeleteAt = 0", map[string]interface{}{"UserId": userId}); err != nil { - result.Err = model.NewLocAppError("SqlWebhookStore.GetOutgoingByCreator", "store.sql_webhooks.get_outgoing_by_creator.app_error", nil, "userId="+userId+", err="+err.Error()) - } - - result.Data = webhooks - - storeChannel <- result - close(storeChannel) - }() - - return storeChannel -} - func (s SqlWebhookStore) GetOutgoingByChannel(channelId string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_webhook_store_test.go b/store/sql_webhook_store_test.go index 089e382449..251ecf5971 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 2aa6277340..b6b86e0d92 100644 --- a/store/store.go +++ b/store/store.go @@ -37,6 +37,7 @@ type Store interface { OAuth() OAuthStore System() SystemStore Webhook() WebhookStore + Command() CommandStore Preference() PreferenceStore MarkSystemRanUnitTests() Close() @@ -170,13 +171,12 @@ type SystemStore interface { type WebhookStore interface { SaveIncoming(webhook *model.IncomingWebhook) StoreChannel GetIncoming(id string) StoreChannel - GetIncomingByUser(userId string) StoreChannel + GetIncomingByTeam(teamId string) StoreChannel GetIncomingByChannel(channelId string) StoreChannel DeleteIncoming(webhookId string, time int64) StoreChannel PermanentDeleteIncomingByUser(userId string) StoreChannel SaveOutgoing(webhook *model.OutgoingWebhook) StoreChannel GetOutgoing(id string) StoreChannel - GetOutgoingByCreator(userId string) StoreChannel GetOutgoingByChannel(channelId string) StoreChannel GetOutgoingByTeam(teamId string) StoreChannel DeleteOutgoing(webhookId string, time int64) StoreChannel @@ -186,6 +186,15 @@ type WebhookStore interface { AnalyticsOutgoingCount(teamId string) StoreChannel } +type CommandStore interface { + Save(webhook *model.Command) StoreChannel + Get(id string) StoreChannel + GetByTeam(teamId string) StoreChannel + Delete(commandId string, time int64) StoreChannel + PermanentDeleteByUser(userId string) StoreChannel + Update(hook *model.Command) StoreChannel +} + type PreferenceStore interface { Save(preferences *model.Preferences) StoreChannel Get(userId string, category string, name string) StoreChannel diff --git a/utils/config.go b/utils/config.go index e9b7e18781..3e4ba5c5bd 100644 --- a/utils/config.go +++ b/utils/config.go @@ -202,6 +202,8 @@ func getClientConfig(c *model.Config) map[string]string { props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks) props["EnableOutgoingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableOutgoingWebhooks) + props["EnableCommands"] = strconv.FormatBool(*c.ServiceSettings.EnableCommands) + props["EnableOnlyAdminIntegrations"] = strconv.FormatBool(*c.ServiceSettings.EnableOnlyAdminIntegrations) props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride) props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride) props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper) diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx index 7021900eb5..2cc68d1ed1 100644 --- a/web/react/components/admin_console/service_settings.jsx +++ b/web/react/components/admin_console/service_settings.jsx @@ -75,6 +75,8 @@ class ServiceSettings extends React.Component { config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked; config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked; config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked; + config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked; + config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked; //config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked; @@ -386,6 +388,100 @@ class ServiceSettings extends React.Component { +
+ +
+ + +

+ +

+
+
+ +
+ +
+ + +

+ +

+
+
+
@@ -427,7 +523,7 @@ class ServiceSettings extends React.Component {

@@ -440,7 +536,7 @@ class ServiceSettings extends React.Component { >
@@ -474,7 +570,7 @@ class ServiceSettings extends React.Component {

diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index ed672cd347..20892898ec 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -155,15 +155,10 @@ class CreatePost extends React.Component { post.message, false, (data) => { - if (data.response === 'not implemented') { - this.sendMessage(post); - return; - } - PostStore.storeDraft(data.channel_id, null); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); - if (data.goto_location.length > 0) { + if (data.goto_location && data.goto_location.length > 0) { window.location.href = data.goto_location; } }, diff --git a/web/react/components/user_settings/manage_command_hooks.jsx b/web/react/components/user_settings/manage_command_hooks.jsx new file mode 100644 index 0000000000..bcf0a6c828 --- /dev/null +++ b/web/react/components/user_settings/manage_command_hooks.jsx @@ -0,0 +1,652 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LoadingScreen from '../loading_screen.jsx'; + +import * as Client from '../../utils/client.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; + +const holders = defineMessages({ + requestTypePost: { + id: 'user.settings.cmds.request_type_post', + defaultMessage: 'POST' + }, + requestTypeGet: { + id: 'user.settings.cmds.request_type_get', + defaultMessage: 'GET' + }, + addDisplayNamePlaceholder: { + id: 'user.settings.cmds.add_display_name.placeholder', + defaultMessage: 'Display Name' + }, + addUsernamePlaceholder: { + id: 'user.settings.cmds.add_username.placeholder', + defaultMessage: 'Username' + }, + addTriggerPlaceholder: { + id: 'user.settings.cmds.add_trigger.placeholder', + defaultMessage: 'Command trigger e.g. "hello" not including the slash' + }, + addAutoCompleteDescPlaceholder: { + id: 'user.settings.cmds.auto_complete_desc.placeholder', + defaultMessage: 'A short description of what this commands does.' + }, + addAutoCompleteHintPlaceholder: { + id: 'user.settings.cmds.auto_complete_hint.placeholder', + defaultMessage: '[zipcode]' + }, + adUrlPlaceholder: { + id: 'user.settings.cmds.url.placeholder', + defaultMessage: 'Must start with http:// or https://' + } +}); + +export default class ManageCommandCmds extends React.Component { + constructor() { + super(); + + this.getCmds = this.getCmds.bind(this); + this.addNewCmd = this.addNewCmd.bind(this); + this.emptyCmd = this.emptyCmd.bind(this); + this.updateTrigger = this.updateTrigger.bind(this); + this.updateURL = this.updateURL.bind(this); + this.updateMethod = this.updateMethod.bind(this); + this.updateUsername = this.updateUsername.bind(this); + this.updateIconURL = this.updateIconURL.bind(this); + this.updateDisplayName = this.updateDisplayName.bind(this); + this.updateAutoComplete = this.updateAutoComplete.bind(this); + this.updateAutoCompleteDesc = this.updateAutoCompleteDesc.bind(this); + this.updateAutoCompleteHint = this.updateAutoCompleteHint.bind(this); + + this.state = {cmds: [], cmd: this.emptyCmd(), getCmdsComplete: false}; + } + + static propTypes() { + return { + intl: intlShape.isRequired + }; + } + + emptyCmd() { + var cmd = {}; + cmd.url = ''; + cmd.trigger = ''; + cmd.method = 'P'; + cmd.username = ''; + cmd.icon_url = ''; + cmd.auto_complete = false; + cmd.auto_complete_desc = ''; + cmd.auto_complete_hint = ''; + cmd.display_name = ''; + return cmd; + } + + componentDidMount() { + this.getCmds(); + } + + addNewCmd(e) { + e.preventDefault(); + + if (this.state.cmd.trigger === '' || this.state.cmd.url === '') { + return; + } + + var cmd = this.state.cmd; + if (cmd.trigger.length !== 0) { + cmd.trigger = cmd.trigger.trim(); + } + cmd.url = cmd.url.trim(); + + Client.addCommand( + cmd, + (data) => { + let cmds = Object.assign([], this.state.cmds); + if (!cmds) { + cmds = []; + } + cmds.push(data); + this.setState({cmds, addError: null, cmd: this.emptyCmd()}); + }, + (err) => { + this.setState({addError: err.message}); + } + ); + } + + removeCmd(id) { + const data = {}; + data.id = id; + + Client.deleteCommand( + data, + () => { + const cmds = this.state.cmds; + let index = -1; + for (let i = 0; i < cmds.length; i++) { + if (cmds[i].id === id) { + index = i; + break; + } + } + + if (index !== -1) { + cmds.splice(index, 1); + } + + this.setState({cmds}); + }, + (err) => { + this.setState({editError: err.message}); + } + ); + } + + regenToken(id) { + const regenData = {}; + regenData.id = id; + + Client.regenCommandToken( + regenData, + (data) => { + const cmds = Object.assign([], this.state.cmds); + for (let i = 0; i < cmds.length; i++) { + if (cmds[i].id === id) { + cmds[i] = data; + break; + } + } + + this.setState({cmds, editError: null}); + }, + (err) => { + this.setState({editError: err.message}); + } + ); + } + + getCmds() { + Client.listTeamCommands( + (data) => { + if (data) { + this.setState({cmds: data, getCmdsComplete: true, editError: null}); + } + }, + (err) => { + this.setState({editError: err.message}); + } + ); + } + + updateTrigger(e) { + var cmd = this.state.cmd; + cmd.trigger = e.target.value; + this.setState(cmd); + } + + updateURL(e) { + var cmd = this.state.cmd; + cmd.url = e.target.value; + this.setState(cmd); + } + + updateMethod(e) { + var cmd = this.state.cmd; + cmd.method = e.target.value; + this.setState(cmd); + } + + updateUsername(e) { + var cmd = this.state.cmd; + cmd.username = e.target.value; + this.setState(cmd); + } + + updateIconURL(e) { + var cmd = this.state.cmd; + cmd.icon_url = e.target.value; + this.setState(cmd); + } + + updateDisplayName(e) { + var cmd = this.state.cmd; + cmd.display_name = e.target.value; + this.setState(cmd); + } + + updateAutoComplete(e) { + var cmd = this.state.cmd; + cmd.auto_complete = e.target.checked; + this.setState(cmd); + } + + updateAutoCompleteDesc(e) { + var cmd = this.state.cmd; + cmd.auto_complete_desc = e.target.value; + this.setState(cmd); + } + + updateAutoCompleteHint(e) { + var cmd = this.state.cmd; + cmd.auto_complete_hint = e.target.value; + this.setState(cmd); + } + + render() { + let addError; + if (this.state.addError) { + addError = ; + } + + let editError; + if (this.state.editError) { + addError = ; + } + + const cmds = []; + this.state.cmds.forEach((cmd) => { + let triggerDiv; + if (cmd.trigger && cmd.trigger.length !== 0) { + triggerDiv = ( +
+ + + {cmd.trigger} +
+ ); + } + + cmds.push( +
+
+ + + {cmd.display_name} +
+
+ + + {cmd.username} +
+
+ + + {cmd.icon_url} +
+
+ + + {cmd.auto_complete ? 'yes' : 'no'} +
+
+ + + {cmd.auto_complete_desc} +
+
+ + + {cmd.auto_complete_hint} +
+
+ + + + + { + cmd.method === 'P' ? + : + + } + +
+
+ + + {cmd.url} +
+ {triggerDiv} +
+ + + {cmd.token} +
+ +
+
+ ); + }); + + let displayCmds; + if (!this.state.getCmdsComplete) { + displayCmds = ; + } else if (cmds.length > 0) { + displayCmds = cmds; + } else { + displayCmds = ( +
+ +
+ ); + } + + const existingCmds = ( +
+ +
+
+ {displayCmds} +
+
+ ); + + const disableButton = this.state.cmd.trigger === '' || this.state.cmd.url === ''; + + return ( +
+ +
+
+
+
+ +
+ +
+
{'Command display name.'}
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ + {''}
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+ {addError} +
+ +
+ {existingCmds} + {editError} +
+ ); + } +} + +export default injectIntl(ManageCommandCmds); diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index abd04a3012..1a9edab037 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -5,6 +5,7 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; import ManageIncomingHooks from './manage_incoming_hooks.jsx'; import ManageOutgoingHooks from './manage_outgoing_hooks.jsx'; +import ManageCommandHooks from './manage_command_hooks.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -24,6 +25,14 @@ const holders = defineMessages({ outDesc: { id: 'user.settings.integrations.outWebhooksDescription', defaultMessage: 'Manage your outgoing webhooks' + }, + cmdName: { + id: 'user.settings.integrations.commands', + defaultMessage: 'Commands' + }, + cmdDesc: { + id: 'user.settings.integrations.commandsDescription', + defaultMessage: 'Manage your commands' } }); @@ -41,6 +50,7 @@ class UserSettingsIntegrationsTab extends React.Component { render() { let incomingHooksSection; let outgoingHooksSection; + let commandHooksSection; var inputs = []; const {formatMessage} = this.props.intl; @@ -106,6 +116,37 @@ class UserSettingsIntegrationsTab extends React.Component { } } + if (global.window.mm_config.EnableCommands === 'true') { + if (this.props.activeSection === 'command-hooks') { + inputs.push( + + ); + + commandHooksSection = ( + { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + commandHooksSection = ( + { + this.updateSection('command-hooks'); + }} + /> + ); + } + } + return (
@@ -144,6 +185,8 @@ class UserSettingsIntegrationsTab extends React.Component {
{outgoingHooksSection}
+ {commandHooksSection} +
); diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index d615e02c74..328a7a7f2e 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -774,20 +774,27 @@ export function savePreferences(preferences, success, error) { } export function getSuggestedCommands(command, suggestionId, component) { - client.executeCommand( - '', - command, - true, + client.listCommands( (data) => { + var matches = []; + data.forEach((cmd) => { + if (('/' + cmd.trigger).indexOf(command) === 0) { + matches.push({ + suggestion: '/' + cmd.trigger + ' ' + cmd.auto_complete_hint, + description: cmd.auto_complete_desc + }); + } + }); + // pull out the suggested commands from the returned data - const terms = data.suggestions.map((suggestion) => suggestion.suggestion); + const terms = matches.map((suggestion) => suggestion.suggestion); AppDispatcher.handleServerAction({ type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS, id: suggestionId, matchedPretext: command, terms, - items: data.suggestions, + items: matches, component }); }, diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 33eb4cd474..9923376719 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -908,11 +908,11 @@ export function getChannelExtraInfo(id, memberLimit, success, error) { export function executeCommand(channelId, command, suggest, success, error) { $.ajax({ - url: '/api/v1/command', + url: '/api/v1/commands/execute', dataType: 'json', contentType: 'application/json', type: 'POST', - data: JSON.stringify({channelId: channelId, command: command, suggest: '' + suggest}), + data: JSON.stringify({channelId, command, suggest: '' + suggest}), success, error: function onError(xhr, status, err) { var e = handleError('executeCommand', xhr, status, err); @@ -921,6 +921,78 @@ export function executeCommand(channelId, command, suggest, success, error) { }); } +export function addCommand(cmd, success, error) { + $.ajax({ + url: '/api/v1/commands/create', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(cmd), + success, + error: (xhr, status, err) => { + var e = handleError('addCommand', xhr, status, err); + error(e); + } + }); +} + +export function deleteCommand(data, success, error) { + $.ajax({ + url: '/api/v1/commands/delete', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: (xhr, status, err) => { + var e = handleError('deleteCommand', xhr, status, err); + error(e); + } + }); +} + +export function listTeamCommands(success, error) { + $.ajax({ + url: '/api/v1/commands/list_team_commands', + dataType: 'json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('listTeamCommands', xhr, status, err); + error(e); + } + }); +} + +export function regenCommandToken(data, success, error) { + $.ajax({ + url: '/api/v1/commands/regen_token', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: (xhr, status, err) => { + var e = handleError('regenCommandToken', xhr, status, err); + error(e); + } + }); +} + +export function listCommands(success, error) { + $.ajax({ + url: '/api/v1/commands/list', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('listCommands', xhr, status, err); + error(e); + } + }); +} + export function getPostsPage(channelId, offset, limit, success, error, complete) { $.ajax({ cache: false, diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index aaffc6ea77..a160aa58f8 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -351,10 +351,14 @@ "admin.service.webhooksDescription": "When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.", "admin.service.outWebhooksTitle": "Enable Outgoing Webhooks: ", "admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed.", - "admin.service.overrideTitle": "Enable Overriding Usernames from Webhooks: ", - "admin.service.overrideDescription": "When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.", - "admin.service.iconTitle": "Enable Overriding Icon from Webhooks: ", - "admin.service.iconDescription": "When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.", + "admin.service.cmdsTitle": "Enable Slash Commands: ", + "admin.service.cmdsDesc": "When true, user created slash commands will be allowed.", + "admin.service.integrationAdmin": "Enable Integrations for Admin Only: ", + "admin.service.integrationAdminDesc": "When true, user created integrations can only be created by admins.", + "admin.service.overrideTitle": "Enable Overriding Usernames from Webhooks and Salsh Commands: ", + "admin.service.overrideDescription": "When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.", + "admin.service.iconTitle": "Enable Overriding Icon from Webhooks and Slash Commands: ", + "admin.service.iconDescription": "When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.", "admin.service.testingTitle": "Enable Testing: ", "admin.service.testingDescription": "(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.", "admin.service.developerTitle": "Enable Developer Mode: ", @@ -1148,7 +1152,40 @@ "user.settings.integrations.incomingWebhooksDescription": "Manage your incoming webhooks", "user.settings.integrations.outWebhooks": "Outgoing Webhooks", "user.settings.integrations.outWebhooksDescription": "Manage your outgoing webhooks", + "user.settings.integrations.commands": "Commands", + "user.settings.integrations.commandsDescription": "Manage your commands", "user.settings.integrations.title": "Integration Settings", + "user.settings.cmds.trigger": "Trigger: ", + "user.settings.cmds.display_name": "Display Name: ", + "user.settings.cmds.username": "Username: ", + "user.settings.cmds.icon_url": "Icon URL: ", + "user.settings.cmds.auto_complete": "Auto Complete: ", + "user.settings.cmds.auto_complete_desc": "Auto Complete Description: ", + "user.settings.cmds.auto_complete_hint": "Auto Complete Hint: ", + "user.settings.cmds.request_type": "Request Type: ", + "user.settings.cmds.request_type_post": "POST", + "user.settings.cmds.request_type_get": "GET", + "user.settings.cmds.url": "URL: ", + "user.settings.cmds.token": "Token: ", + "user.settings.cmds.regen": "Regen Token", + "user.settings.cmds.none": "None", + "Existing commands": "Existing commands", + "user.settings.cmds.add_desc": "Create commands to send message events to an external integration. Please see http://mattermost.org/commands to learn more.", + "user.settings.cmds.add_display_name.placeholder": "Display Name", + "user.settings.cmds.existing": "Existing commands", + "user.settings.cmds.add_username.placeholder": "Username", + "user.settings.cmds.username_desc": "The username to use when overriding the post.", + "user.settings.cmds.icon_url_desc": "URL to an icon", + "user.settings.cmds.trigger_desc": "Word to trigger on", + "user.settings.cmds.add_trigger.placeholder": "Command trigger e.g. \"hello\" not including the slash", + "user.settings.cmds.auto_complete_desc_desc": "A short description of what this commands does", + "user.settings.cmds.auto_complete_help": "Show this command in autocomplete list.", + "user.settings.cmds.auto_complete_desc.placeholder": "A short description of what this commands does.", + "user.settings.cmds.auto_complete_hint.placeholder": "[zipcode]", + "user.settings.cmds.auto_complete_hint_desc": "List parameters to be passed to the command.", + "user.settings.cmds.request_type_desc": "Command request type issued to the callback URL.", + "user.settings.cmds.url_desc": "URL that will receive the HTTP POST or GET event", + "user.settings.cmds.url.placeholder": "Must start with http:// or https://", "user.settings.modal.general": "General", "user.settings.modal.security": "Security", "user.settings.modal.notifications": "Notifications", diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index 981e1f06f3..0a780c4424 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -299,8 +299,6 @@ "admin.service.googleDescription": "Asigna una llave a este campo para habilitar la previsualización de videos de YouTube tomados de los enlaces que aparecen en los mensajes o comentarios. Las instrucciones de como obtener una llave está disponible en https://www.youtube.com/watch?v=Im69kzhpR3I. Al dejar este campo en blanco deshabilita la generación de previsualizaciones de videos de YouTube desde los enlaces.", "admin.service.googleExample": "Ej \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"", "admin.service.googleTitle": "Llave de desarrolador Google:", - "admin.service.iconDescription": "Cuando es verdadero, se le permitirá cambiar el icono del mensaje desde webhooks. Nota, en combinación con permitir el cambio de nombre de usuario, podría exponer a los usuarios a sufrir ataques de phishing.", - "admin.service.iconTitle": "Habilitar el cambio de icono desde los Webhooks: ", "admin.service.listenAddress": "Dirección de escucha:", "admin.service.listenDescription": "La dirección a la que se unirá y escuchará. Ingresar \":8065\" se podrá unir a todas las interfaces o podrá seleccionar una como ej: \"127.0.0.1:8065\". Cambiando este valor es necesario reiniciar el servidor.", "admin.service.listenExample": "Ej \":8065\"", @@ -308,8 +306,6 @@ "admin.service.mobileSessionDaysDesc": "La sesión nativa de los dispositivos moviles expirará luego de transcurrido el numero de días especificado y se solicitará al usuario que inicie sesión nuevamente.", "admin.service.outWebhooksDesc": "Cuando es verdadero, los webhooks de salida serán permitidos.", "admin.service.outWebhooksTitle": "Habilitar Webhooks de Salida: ", - "admin.service.overrideDescription": "Cuando es verdadero, se le permitirá cambiar el nombre de usuario desde webhooks. Nota, en conjunto con cambio de icono, podría exponer a los usuarios a sufrir ataques de phishing.", - "admin.service.overrideTitle": "Habilitar el cambio de nombres de usuario desde los Webhooks: ", "admin.service.save": "Guardar", "admin.service.saving": "Guardando....", "admin.service.securityDesc": "Cuando es verdadero, Los Administradores del Sistema serán notificados por correo electrónico se han anunciado alertas de seguridad relevantes en las últimas 12 horas. Requiere que los correos estén habilitados.", diff --git a/web/web_test.go b/web/web_test.go index cc7d225597..7617ae54ae 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -194,6 +194,10 @@ func TestIncomingWebhook(t *testing.T) { user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User) store.Must(api.Srv.Store.User().VerifyEmail(user.Id)) + c := &api.Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + api.UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) ApiClient.LoginByEmail(team.Name, user.Email, "pwd") channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}