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 91c8c022a5..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 } @@ -2087,13 +2091,16 @@ func switchToSSO(c *Context, w http.ResponseWriter, r *http.Request) { func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team, email string) { authData := "" + ssoEmail := "" provider := einterfaces.GetOauthProvider(service) if provider == nil { c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.unavailable.app_error", map[string]interface{}{"Service": service}, "") return } else { - authData = provider.GetAuthDataFromJson(userData) + ssoUser := provider.GetUserFromJson(userData) + authData = ssoUser.AuthData + ssoEmail = ssoUser.Email } if len(authData) == 0 { @@ -2120,7 +2127,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, return } - if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData); result.Err != nil { + if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData, ssoEmail); result.Err != nil { c.Err = result.Err return } 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..74a873204a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -244,121 +244,141 @@ "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.admin_only.app_error", + "translation": "Integrations have been limited to admins only." }, { - "id": "api.command.echo_command.create.error", - "translation": "Unable to create /echo post, err=%v" + "id": "api.command.delete.app_error", + "translation": "Inappropriate permissions to delete command" }, { - "id": "api.command.echo_command.description", - "translation": "Echo back text from your account, /echo \"message\" [delay in seconds]" + "id": "api.command.disabled.app_error", + "translation": "Commands have been disabled by the system admin." }, { - "id": "api.command.echo_command.high_volume.app_error", - "translation": "High volume of echo request, cannot process request" + "id": "api.command.execute_command.debug", + "translation": "Executing cmd=%v userId=%v" }, { - "id": "api.command.echo_command.under.app_error", - "translation": "Delays must be under 10000 seconds" + "id": "api.command.execute_command.failed.app_error", + "translation": "Command with a trigger of '{{.Trigger}}' failed" + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "Command with a trigger of '{{.Trigger}}' returned an empty response" + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "Command with a trigger of '{{.Trigger}}' returned response {{.Status}}" + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "Command with a trigger of '{{.Trigger}}' not found" + }, + { + "id": "api.command.execute_command.save.app_error", + "translation": "An error while saving the command response to the channel" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "No command trigger found" }, { "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.regen.app_error", + "translation": "Inappropriate permissions to regenerate command token" }, { - "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_echo.create.app_error", + "translation": "Unable to create /echo post, err=%v" }, { - "id": "api.command.load_test_command.description", - "translation": "Debug Load Testing" + "id": "api.command_echo.delay.app_error", + "translation": "Delays must be under 10000 seconds" }, { - "id": "api.command.load_test_posts_command.fuzz.description", - "translation": "Add some random posts with fuzz text to current channel " + "id": "api.command_echo.desc", + "translation": "Echo back text from your account" }, { - "id": "api.command.load_test_posts_command.posts.description", - "translation": "Add some random posts to current channel " + "id": "api.command_echo.high_volume.app_error", + "translation": "High volume of echo request, cannot process request" }, { - "id": "api.command.load_test_setup_command.create.error", - "translation": "Failed to create testing environment" + "id": "api.command_echo.hint", + "translation": "'message' [delay in seconds]" }, { - "id": "api.command.load_test_setup_command.created.info", - "translation": "Team Created: %v" + "id": "api.command_echo.name", + "translation": "echo" }, { - "id": "api.command.load_test_setup_command.description", - "translation": "Creates a testing environment in current team. [teams] [fuzz] " - }, - { - "id": "api.command.load_test_setup_command.login.info", - "translation": "\t User to login: %v, %v" - }, - { - "id": "api.command.load_test_url_command.create.error", - "translation": "Unable to create post, err=%v" - }, - { - "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.load_test_url_command.file.app_error", - "translation": "Unable to get file" - }, - { - "id": "api.command.load_test_url_command.reading.app_error", - "translation": "Encountered error reading file" - }, - { - "id": "api.command.load_test_url_command.url.app_error", - "translation": "Command must contain a url" - }, - { - "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.load_test_users_command.users.description", - "translation": "Add a specified number of random users to current team " - }, - { - "id": "api.command.logout_command.description", - "translation": "Logout" - }, - { - "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.fail.app_error", + "translation": "An error occured while joining the channel." + }, + { + "id": "api.command_join.hint", + "translation": "[channel-name]" + }, + { + "id": "api.command_join.list.app_error", + "translation": "An error occured while listing channels." + }, + { + "id": "api.command_join.missing.app_error", + "translation": "We couldn't find the channel" + }, + { + "id": "api.command_join.name", + "translation": "join" + }, + { + "id": "api.command_join.success", + "translation": "Joined 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." @@ -1871,6 +1891,46 @@ "id": "model.client.login.app_error", "translation": "Authentication tokens didn't match" }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "Invalid Id" + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "Invalid Method" + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "Invalid team id" + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "Invalid token" + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "Invalid trigger" + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "Update at must be a valid time" + }, + { + "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.user_id.app_error", + "translation": "Invalid user id" + }, { "id": "model.config.is_valid.email_reset_salt.app_error", "translation": "Invalid password reset salt for email settings. Must be 32 chars or more." @@ -2555,6 +2615,34 @@ "id": "store.sql_channel.update_member.app_error", "translation": "We encountered an error updating the channel member" }, + { + "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.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.saving.app_error", + "translation": "We couldn't save the Command" + }, + { + "id": "store.sql_command.save.saving_overwrite.app_error", + "translation": "You cannot overwrite an existing Command" + }, + { + "id": "store.sql_command.save.update.app_error", + "translation": "We couldn't update the command" + }, { "id": "store.sql_oauth.get_access_data.app_error", "translation": "We encountered an error finding the access token" @@ -3015,6 +3103,14 @@ "id": "store.sql_user.verify_email.app_error", "translation": "Unable to update verify email field" }, + { + "id": "store.sql_webhooks.analytics_incoming_count.app_error", + "translation": "We couldn't count the incoming webhooks" + }, + { + "id": "store.sql_webhooks.analytics_outgoing_count.app_error", + "translation": "We couldn't count the outgoing webhooks" + }, { "id": "store.sql_webhooks.delete_incoming.app_error", "translation": "We couldn't delete the webhook" @@ -3043,10 +3139,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" @@ -3079,14 +3171,6 @@ "id": "store.sql_webhooks.update_outgoing.app_error", "translation": "We couldn't update the webhook" }, - { - "id": "store.sql_webhooks.analytics_incoming_count.app_error", - "translation": "We couldn't count the incoming webhooks" - }, - { - "id": "store.sql_webhooks.analytics_outgoing_count.app_error", - "translation": "We couldn't count the outgoing webhooks" - }, { "id": "utils.config.load_config.decoding.panic", "translation": "Error decoding config file={{.Filename}}, err={{.Error}}" diff --git a/i18n/es.json b/i18n/es.json index 9599fe8791..2fe02b2b05 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -244,120 +244,140 @@ "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.admin_only.app_error", + "translation": "Las ingtegraciones solo pueden ser utilizadas por adminitradores." }, { - "id": "api.command.echo_command.create.error", - "translation": "No se puede crear /echo mensaje, err=%v" + "id": "api.command.delete.app_error", + "translation": "Permisos inapropiados para utilizar el comando de borrar" }, { - "id": "api.command.echo_command.description", - "translation": "Echo del texto desde tu cuenta, /echo \"mensaje\" [retraso en segundos]" + "id": "api.command.disabled.app_error", + "translation": "Los comandos han sido deshabilitados por el administrador de sistema." }, { - "id": "api.command.echo_command.high_volume.app_error", - "translation": "Volumen alto de solicitudes echo, no se puede procesar la solicitud" + "id": "api.command.execute_command.debug", + "translation": "Ejecutando cmd=%v userId=%v" }, { - "id": "api.command.echo_command.under.app_error", - "translation": "El retraso debe ser menor a 10000 segundos" + "id": "api.command.execute_command.failed.app_error", + "translation": "El Comando con el gatillador '{{.Trigger}}' falló" + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "El Comando con el gatillador '{{.Trigger}}' retorno una respuesta vacía" + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "El Comando con el gatillador '{{.Trigger}}' retorno la respuesta {{.Status}}" + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "No se encontró el Comando con el gatillador '{{.Trigger}}'" + }, + { + "id": "api.command.execute_command.save.app_error", + "translation": "Ocurrió un error mientras se guardaba la respuesta del comando en el canal" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "No se encontró un gatillador para el comando" }, { "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.regen.app_error", + "translation": "Permisos inapropiados para utilizar le comando de regenerar token" }, { - "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_echo.create.app_error", + "translation": "No se pudo crear el /echo mensaje, err=%v" }, { - "id": "api.command.load_test_command.description", - "translation": "Depurar pruebas de carga" + "id": "api.command_echo.delay.app_error", + "translation": "El retraso debe ser menor a 10000 segundos" }, { - "id": "api.command.load_test_posts_command.fuzz.description", - "translation": "Agrega algunos mensajes aleatorios con el texto fuzz al canal actual " + "id": "api.command_echo.desc", + "translation": "Eco de vuelta un texto utilizando tu cuenta" }, { - "id": "api.command.load_test_posts_command.posts.description", - "translation": "Agrega algunos mensajes aleatorios al canal actual " + "id": "api.command_echo.high_volume.app_error", + "translation": "Demasiadas solicitudes de echo, no pudimos procesar la solicitud" }, { - "id": "api.command.load_test_setup_command.create.error", - "translation": "Falla al crear el entorno de pruebas" + "id": "api.command_echo.hint", + "translation": "'mensaje' [retraso en segundos]" }, { - "id": "api.command.load_test_setup_command.created.info", - "translation": "Equipo Creado: %v" + "id": "api.command_echo.name", + "translation": "echo" }, { - "id": "api.command.load_test_setup_command.description", - "translation": "Crea un entorno de prueba para el equipo actual. [equipos] [fuzz] " + "id": "api.command_join.desc", + "translation": "Unirte a un canal público" }, { - "id": "api.command.load_test_setup_command.login.info", - "translation": "\t Usuario que inicia sesión: %v, %v" + "id": "api.command_join.fail.app_error", + "translation": "Ocurrió un error al unirse al canal." }, { - "id": "api.command.load_test_url_command.create.error", - "translation": "No se pudo crear el mensaje, err=%v" + "id": "api.command_join.hint", + "translation": "[nombre del canal]" }, { - "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_join.list.app_error", + "translation": "Ocurrió un error al listar los canales." }, { - "id": "api.command.load_test_url_command.file.app_error", - "translation": "No se puede obtener el archivo" + "id": "api.command_join.missing.app_error", + "translation": "No pudimos encontrar el canal" }, { - "id": "api.command.load_test_url_command.reading.app_error", - "translation": "Se encontró un error leyendo el archivo" + "id": "api.command_join.name", + "translation": "unir" }, { - "id": "api.command.load_test_url_command.url.app_error", - "translation": "El comando debe contener un url" + "id": "api.command_join.success", + "translation": "Te uniste al canal." }, { - "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_logout.desc", + "translation": "Salir de Mattermost" }, { - "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.name", + "translation": "salir" }, { - "id": "api.command.logout_command.description", - "translation": "Cerrar sesión" + "id": "api.command_logout.success_message", + "translation": "Cerrando sesión..." }, { - "id": "api.command.me_command.create.error", - "translation": "No se pudo crear el mensaje /me mensaje, err=%v" + "id": "api.command_me.desc", + "translation": "Hacer una acción" }, { - "id": "api.command.me_command.description", - "translation": "Realiza una acción, /me [mensaje]" + "id": "api.command_me.hint", + "translation": "[mensaje]" }, { - "id": "api.command.no_implemented.app_error", - "translation": "Comando no implementado" + "id": "api.command_me.name", + "translation": "yo" }, { - "id": "api.command.shrug_command.create.error", - "translation": "No se pudo crear el mensaje /shrug mensaje, err=%v" + "id": "api.command_shrug.desc", + "translation": "Agrega ¯\\_(ツ)_/¯ a tu mensaje" }, { - "id": "api.command.shrug_command.description", - "translation": "Agrega ¯\\_(ツ)_/¯ a tu mensaje, /shrug [mensaje]" + "id": "api.command_shrug.hint", + "translation": "[mensaje]" }, { - "id": "api.commmand.join_command.description", - "translation": "Unirme al canal abierto" + "id": "api.command_shrug.name", + "translation": "npi" }, { "id": "api.context.404.app_error", @@ -1871,6 +1891,46 @@ "id": "model.client.login.app_error", "translation": "Token de autenticación no coincidió" }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "Fecha de Creación debe ser válida" + }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "Id inválido" + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "Método Inválido" + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "Id del equipo inválido" + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "Token inválido" + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "gatillador inválido" + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "Fecha de Actualización debe ser válida" + }, + { + "id": "model.command.is_valid.url.app_error", + "translation": "URL inválido" + }, + { + "id": "model.command.is_valid.url_http.app_error", + "translation": "URL inválido. Debe ser un URL válido y comenzar con http:// o https://" + }, + { + "id": "model.command.is_valid.user_id.app_error", + "translation": "Id de usuario inválido" + }, { "id": "model.config.is_valid.email_reset_salt.app_error", "translation": "Salt para restablecer contraseñas en la configuración de correos es inválido. Debe ser de 32 caracteres o más." @@ -2555,6 +2615,34 @@ "id": "store.sql_channel.update_member.app_error", "translation": "Encontramos un error actualizando el miembro del canal" }, + { + "id": "store.sql_command.save.delete.app_error", + "translation": "No pudimos eliminar el comando" + }, + { + "id": "store.sql_command.save.delete_perm.app_error", + "translation": "No pudimos eliminar el comando" + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "No pudimos obtener el comando" + }, + { + "id": "store.sql_command.save.get_team.app_error", + "translation": "No pudimos obtener los comandos" + }, + { + "id": "store.sql_command.save.saving.app_error", + "translation": "No pudimos guardar el Comando" + }, + { + "id": "store.sql_command.save.saving_overwrite.app_error", + "translation": "No puedes sobreescribir un comando existente" + }, + { + "id": "store.sql_command.save.update.app_error", + "translation": "No pudimos actualizar el comando" + }, { "id": "store.sql_oauth.get_access_data.app_error", "translation": "Encontramos un error buscando el token de acceso" @@ -3051,10 +3139,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_user_store.go b/store/sql_user_store.go index 0b6970c96d..b1544289d8 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -305,7 +305,7 @@ func (us SqlUserStore) UpdateFailedPasswordAttempts(userId string, attempts int) return storeChannel } -func (us SqlUserStore) UpdateAuthData(userId, service, authData string) StoreChannel { +func (us SqlUserStore) UpdateAuthData(userId, service, authData, email string) StoreChannel { storeChannel := make(StoreChannel) @@ -314,7 +314,24 @@ func (us SqlUserStore) UpdateAuthData(userId, service, authData string) StoreCha updateAt := model.GetMillis() - if _, err := us.GetMaster().Exec("UPDATE Users SET Password = '', LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, FailedAttempts = 0, AuthService = :AuthService, AuthData = :AuthData WHERE Id = :UserId", map[string]interface{}{"LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId, "AuthService": service, "AuthData": authData}); err != nil { + query := ` + UPDATE + Users + SET + Password = '', + LastPasswordUpdate = :LastPasswordUpdate, + UpdateAt = :UpdateAt, + FailedAttempts = 0, + AuthService = :AuthService, + AuthData = :AuthData` + + if len(email) != 0 { + query += ", Email = :Email" + } + + query += " WHERE Id = :UserId" + + if _, err := us.GetMaster().Exec(query, map[string]interface{}{"LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId, "AuthService": service, "AuthData": authData, "Email": email}); err != nil { result.Err = model.NewLocAppError("SqlUserStore.UpdateAuthData", "store.sql_user.update_auth_data.app_error", nil, "id="+userId+", "+err.Error()) } else { result.Data = userId diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index d1ee5e6470..2350bad309 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -402,7 +402,7 @@ func TestUserStoreUpdateAuthData(t *testing.T) { service := "someservice" authData := "1" - if err := (<-store.User().UpdateAuthData(u1.Id, service, authData)).Err; err != nil { + if err := (<-store.User().UpdateAuthData(u1.Id, service, authData, "")).Err; err != nil { t.Fatal(err) } 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 cfc679706a..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() @@ -111,7 +112,7 @@ type UserStore interface { UpdateLastActivityAt(userId string, time int64) StoreChannel UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel UpdatePassword(userId, newPassword string) StoreChannel - UpdateAuthData(userId, service, authData string) StoreChannel + UpdateAuthData(userId, service, authData, email string) StoreChannel Get(id string) StoreChannel GetProfiles(teamId string) StoreChannel GetByEmail(teamId string, email string) StoreChannel @@ -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/channel_loader.jsx b/web/react/components/channel_loader.jsx index 15571ad934..174c8c4e15 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -128,6 +128,16 @@ class ChannelLoader extends React.Component { } }); + $('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before'); + } else { + $(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before'); + } + }); + $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) { if (ev.type === 'mouseenter') { $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment'); diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 1b552838a6..8c49315e76 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -202,8 +202,7 @@ class CreateComment extends React.Component { if (e.keyCode === KeyCodes.UP && this.state.messageText === '') { e.preventDefault(); - const channelId = ChannelStore.getCurrentId(); - const lastPost = PostStore.getCurrentUsersLatestPost(channelId, this.props.rootId); + const lastPost = PostStore.getCurrentUsersLatestPost(this.props.channelId, this.props.rootId); if (!lastPost) { return; } @@ -402,4 +401,4 @@ CreateComment.propTypes = { rootId: React.PropTypes.string.isRequired }; -export default injectIntl(CreateComment); \ No newline at end of file +export default injectIntl(CreateComment); 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/file_upload.jsx b/web/react/components/file_upload.jsx index 626dbc5b3c..746289653d 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -101,9 +101,9 @@ class FileUpload extends React.Component { } else if (tooLargeFiles.length > 1) { var tooLargeFilenames = tooLargeFiles.map((file) => file.name).join(', '); - this.props.onUploadError(formatMessage(holders.filesAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), files: tooLargeFilenames})); + this.props.onUploadError(formatMessage(holders.filesAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), filenames: tooLargeFilenames})); } else if (tooLargeFiles.length > 0) { - this.props.onUploadError(formatMessage(holders.fileAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), file: tooLargeFiles[0].name})); + this.props.onUploadError(formatMessage(holders.fileAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), filename: tooLargeFiles[0].name})); } } @@ -329,4 +329,4 @@ FileUpload.propTypes = { postType: React.PropTypes.string }; -export default injectIntl(FileUpload); \ No newline at end of file +export default injectIntl(FileUpload); diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 0ad091d5b7..544ba920a3 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -59,64 +59,74 @@ export default class SearchResultsItem extends React.Component { }; return ( -
-
{channelName}
-
-
- +
+
+
+
-
- -
- +
+
{channelName}
+
+
+
+
+ +
+ +
+
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_display.jsx b/web/react/components/user_settings/user_settings_display.jsx index 3b2a2065b8..776bde4426 100644 --- a/web/react/components/user_settings/user_settings_display.jsx +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -297,7 +297,7 @@ class UserSettingsDisplay extends React.Component { if (this.state.nameFormat === 'username') { describe = formatMessage(holders.showUsername); } else if (this.state.nameFormat === 'full_name') { - describe = formatMessage(holders.showFullName); + describe = formatMessage(holders.showFullname); } else { describe = formatMessage(holders.showNickname); } 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/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index e73680b387..4a4de5c3b1 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -173,6 +173,7 @@ .team__name { line-height: 22px; margin-top: -2px; + float: left; } .user__name { @include single-transition(all, 0.1s, linear); diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 73c7bd9cb4..2ff49c9b70 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -764,12 +764,13 @@ body.ios { } svg { + height: 17px; width: 17px; } .comment-icon { display: inline-block; - top: 3px; + top: 2px; position: relative; margin-right: 3px; fill: inherit; diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss index cb125bff0b..693c59a31e 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -90,6 +90,7 @@ -webkit-overflow-scrolling: touch; @include flex(1 1 auto); height: calc(100% - 56px); + padding-top: 10px; } .search-results-header { @@ -104,19 +105,22 @@ border-bottom: $border-gray; } -.search-item-container { - border-top: $border-gray; - padding: 10px 1em; - margin: 0; +.search-item__container { - &:first-child { - border: none; + .post { + padding: 0 1em 1em; + margin: 0; + + &:first-child { + border: none; + } + + .search-channel__name { + font-weight: 600; + margin: 0 0 10px 0; + } } - .search-channel__name { - font-weight: 600; - margin: 0 0 10px 0; - } } .search-item__jump { 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 28d9336e6d..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.", @@ -571,8 +567,8 @@ "claim.email_to_sso.pwd": "Contraseña", "claim.email_to_sso.pwdError": "Por favor introduce tu contraseña.", "claim.email_to_sso.ssoType": "Al reclamar tu cuenta, sólo podrás iniciar sesión con {type} SSO", - "claim.email_to_sso.switchTo": "Cambiar cuenta a ", - "claim.email_to_sso.title": "Cambiar Cuenta de Correo/Contraseña a ", + "claim.email_to_sso.switchTo": "Cambiar cuenta a {uiType}", + "claim.email_to_sso.title": "Cambiar Cuenta de Correo/Contraseña a {uiType}", "claim.sso_to_email.confirm": "Confirmar Contraseña", "claim.sso_to_email.description": "Al cambiar el tipo de cuenta, sólo podrás iniciar sesión con tu correo electrónico y contraseña.", "claim.sso_to_email.enterPwd": "Por favor ingresa una contraseña.", 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}