From e9c6cc269b5c9fe82e5f38d63344a07365bccd6b Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 13 Mar 2017 09:23:16 -0400 Subject: Move command logic into app layer (#5617) --- app/auto_channels.go | 74 +++++++ app/auto_constants.go | 36 ++++ app/auto_environment.go | 99 ++++++++++ app/auto_posts.go | 103 ++++++++++ app/auto_teams.go | 81 ++++++++ app/auto_users.go | 109 ++++++++++ app/command.go | 304 ++++++++++++++++++++++++++++ app/command_away.go | 43 ++++ app/command_echo.go | 97 +++++++++ app/command_expand_collapse.go | 87 ++++++++ app/command_invite_people.go | 64 ++++++ app/command_join.go | 62 ++++++ app/command_loadtest.go | 437 +++++++++++++++++++++++++++++++++++++++++ app/command_logout.go | 48 +++++ app/command_me.go | 38 ++++ app/command_msg.go | 110 +++++++++++ app/command_offline.go | 43 ++++ app/command_online.go | 43 ++++ app/command_shortcuts.go | 95 +++++++++ app/command_shrug.go | 43 ++++ 20 files changed, 2016 insertions(+) create mode 100644 app/auto_channels.go create mode 100644 app/auto_constants.go create mode 100644 app/auto_environment.go create mode 100644 app/auto_posts.go create mode 100644 app/auto_teams.go create mode 100644 app/auto_users.go create mode 100644 app/command_away.go create mode 100644 app/command_echo.go create mode 100644 app/command_expand_collapse.go create mode 100644 app/command_invite_people.go create mode 100644 app/command_join.go create mode 100644 app/command_loadtest.go create mode 100644 app/command_logout.go create mode 100644 app/command_me.go create mode 100644 app/command_msg.go create mode 100644 app/command_offline.go create mode 100644 app/command_online.go create mode 100644 app/command_shortcuts.go create mode 100644 app/command_shrug.go (limited to 'app') diff --git a/app/auto_channels.go b/app/auto_channels.go new file mode 100644 index 000000000..3945a5a4f --- /dev/null +++ b/app/auto_channels.go @@ -0,0 +1,74 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +type AutoChannelCreator struct { + client *model.Client + team *model.Team + Fuzzy bool + DisplayNameLen utils.Range + DisplayNameCharset string + NameLen utils.Range + NameCharset string + ChannelType string +} + +func NewAutoChannelCreator(client *model.Client, team *model.Team) *AutoChannelCreator { + return &AutoChannelCreator{ + client: client, + team: team, + Fuzzy: false, + DisplayNameLen: CHANNEL_DISPLAY_NAME_LEN, + DisplayNameCharset: utils.ALPHANUMERIC, + NameLen: CHANNEL_NAME_LEN, + NameCharset: utils.LOWERCASE, + ChannelType: CHANNEL_TYPE, + } +} + +func (cfg *AutoChannelCreator) createRandomChannel() (*model.Channel, bool) { + var displayName string + if cfg.Fuzzy { + displayName = utils.FuzzName() + } else { + displayName = utils.RandomName(cfg.NameLen, cfg.NameCharset) + } + name := utils.RandomName(cfg.NameLen, cfg.NameCharset) + + channel := &model.Channel{ + TeamId: cfg.team.Id, + DisplayName: displayName, + Name: name, + Type: cfg.ChannelType} + + println(cfg.client.GetTeamRoute()) + result, err := cfg.client.CreateChannel(channel) + if err != nil { + err.Translate(utils.T) + println(err.Error()) + println(err.DetailedError) + return nil, false + } + return result.Data.(*model.Channel), true +} + +func (cfg *AutoChannelCreator) CreateTestChannels(num utils.Range) ([]*model.Channel, bool) { + numChannels := utils.RandIntFromRange(num) + channels := make([]*model.Channel, numChannels) + + for i := 0; i < numChannels; i++ { + var err bool + channels[i], err = cfg.createRandomChannel() + if err != true { + return channels, false + } + } + + return channels, true +} diff --git a/app/auto_constants.go b/app/auto_constants.go new file mode 100644 index 000000000..c8c903e32 --- /dev/null +++ b/app/auto_constants.go @@ -0,0 +1,36 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +const ( + USER_PASSWORD = "passwd" + CHANNEL_TYPE = model.CHANNEL_OPEN + FUZZ_USER_EMAIL_PREFIX_LEN = 10 + BTEST_TEAM_DISPLAY_NAME = "TestTeam" + BTEST_TEAM_NAME = "z-z-testdomaina" + BTEST_TEAM_EMAIL = "test@nowhere.com" + BTEST_TEAM_TYPE = model.TEAM_OPEN + BTEST_USER_NAME = "Mr. Testing Tester" + BTEST_USER_EMAIL = "success+ttester@simulator.amazonses.com" + BTEST_USER_PASSWORD = "passwd" +) + +var ( + TEAM_NAME_LEN = utils.Range{Begin: 10, End: 20} + TEAM_DOMAIN_NAME_LEN = utils.Range{Begin: 10, End: 20} + TEAM_EMAIL_LEN = utils.Range{Begin: 15, End: 30} + USER_NAME_LEN = utils.Range{Begin: 5, End: 20} + USER_EMAIL_LEN = utils.Range{Begin: 15, End: 30} + CHANNEL_DISPLAY_NAME_LEN = utils.Range{Begin: 10, End: 20} + CHANNEL_NAME_LEN = utils.Range{Begin: 5, End: 20} + POST_MESSAGE_LEN = utils.Range{Begin: 100, End: 400} + POST_HASHTAGS_NUM = utils.Range{Begin: 5, End: 10} + POST_MENTIONS_NUM = utils.Range{Begin: 0, End: 3} + TEST_IMAGE_FILENAMES = []string{"test.png", "testjpg.jpg", "testgif.gif"} +) diff --git a/app/auto_environment.go b/app/auto_environment.go new file mode 100644 index 000000000..b0a4f54b8 --- /dev/null +++ b/app/auto_environment.go @@ -0,0 +1,99 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "math/rand" + "time" +) + +type TestEnvironment struct { + Teams []*model.Team + Environments []TeamEnvironment +} + +func CreateTestEnvironmentWithTeams(client *model.Client, rangeTeams utils.Range, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TestEnvironment, bool) { + rand.Seed(time.Now().UTC().UnixNano()) + + teamCreator := NewAutoTeamCreator(client) + teamCreator.Fuzzy = fuzzy + teams, err := teamCreator.CreateTestTeams(rangeTeams) + if err != true { + return TestEnvironment{}, false + } + + environment := TestEnvironment{teams, make([]TeamEnvironment, len(teams))} + + for i, team := range teams { + userCreator := NewAutoUserCreator(client, team) + userCreator.Fuzzy = fuzzy + randomUser, err := userCreator.createRandomUser() + if err != true { + return TestEnvironment{}, false + } + client.LoginById(randomUser.Id, USER_PASSWORD) + client.SetTeamId(team.Id) + teamEnvironment, err := CreateTestEnvironmentInTeam(client, team, rangeChannels, rangeUsers, rangePosts, fuzzy) + if err != true { + return TestEnvironment{}, false + } + environment.Environments[i] = teamEnvironment + } + + return environment, true +} + +func CreateTestEnvironmentInTeam(client *model.Client, team *model.Team, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TeamEnvironment, bool) { + rand.Seed(time.Now().UTC().UnixNano()) + + // We need to create at least one user + if rangeUsers.Begin <= 0 { + rangeUsers.Begin = 1 + } + + userCreator := NewAutoUserCreator(client, team) + userCreator.Fuzzy = fuzzy + users, err := userCreator.CreateTestUsers(rangeUsers) + if err != true { + return TeamEnvironment{}, false + } + usernames := make([]string, len(users)) + for i, user := range users { + usernames[i] = user.Username + } + + channelCreator := NewAutoChannelCreator(client, team) + channelCreator.Fuzzy = fuzzy + channels, err := channelCreator.CreateTestChannels(rangeChannels) + + // Have every user join every channel + for _, user := range users { + for _, channel := range channels { + client.LoginById(user.Id, USER_PASSWORD) + client.JoinChannel(channel.Id) + } + } + + if err != true { + return TeamEnvironment{}, false + } + + numPosts := utils.RandIntFromRange(rangePosts) + numImages := utils.RandIntFromRange(rangePosts) / 4 + for j := 0; j < numPosts; j++ { + user := users[utils.RandIntFromRange(utils.Range{Begin: 0, End: len(users) - 1})] + client.LoginById(user.Id, USER_PASSWORD) + for i, channel := range channels { + postCreator := NewAutoPostCreator(client, channel.Id) + postCreator.HasImage = i < numImages + postCreator.Users = usernames + postCreator.Fuzzy = fuzzy + postCreator.CreateRandomPost() + } + } + + return TeamEnvironment{users, channels}, true +} diff --git a/app/auto_posts.go b/app/auto_posts.go new file mode 100644 index 000000000..b32407539 --- /dev/null +++ b/app/auto_posts.go @@ -0,0 +1,103 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "bytes" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "io" + "os" +) + +type AutoPostCreator struct { + client *model.Client + channelid string + Fuzzy bool + TextLength utils.Range + HasImage bool + ImageFilenames []string + Users []string + Mentions utils.Range + Tags utils.Range +} + +// Automatic poster used for testing +func NewAutoPostCreator(client *model.Client, channelid string) *AutoPostCreator { + return &AutoPostCreator{ + client: client, + channelid: channelid, + Fuzzy: false, + TextLength: utils.Range{Begin: 100, End: 200}, + HasImage: false, + ImageFilenames: TEST_IMAGE_FILENAMES, + Users: []string{}, + Mentions: utils.Range{Begin: 0, End: 5}, + Tags: utils.Range{Begin: 0, End: 7}, + } +} + +func (cfg *AutoPostCreator) UploadTestFile() ([]string, bool) { + filename := cfg.ImageFilenames[utils.RandIntFromRange(utils.Range{Begin: 0, End: len(cfg.ImageFilenames) - 1})] + + path := utils.FindDir("web/static/images") + file, err := os.Open(path + "/" + filename) + defer file.Close() + + data := &bytes.Buffer{} + _, err = io.Copy(data, file) + if err != nil { + return nil, false + } + + resp, appErr := cfg.client.UploadPostAttachment(data.Bytes(), cfg.channelid, filename) + if appErr != nil { + return nil, false + } + + return []string{resp.FileInfos[0].Id}, true +} + +func (cfg *AutoPostCreator) CreateRandomPost() (*model.Post, bool) { + var fileIds []string + if cfg.HasImage { + var err1 bool + fileIds, err1 = cfg.UploadTestFile() + if err1 == false { + return nil, false + } + } + + var postText string + if cfg.Fuzzy { + postText = utils.FuzzPost() + } else { + postText = utils.RandomText(cfg.TextLength, cfg.Tags, cfg.Mentions, cfg.Users) + } + + post := &model.Post{ + ChannelId: cfg.channelid, + Message: postText, + FileIds: fileIds} + result, err2 := cfg.client.CreatePost(post) + if err2 != nil { + return nil, false + } + return result.Data.(*model.Post), true +} + +func (cfg *AutoPostCreator) CreateTestPosts(rangePosts utils.Range) ([]*model.Post, bool) { + numPosts := utils.RandIntFromRange(rangePosts) + posts := make([]*model.Post, numPosts) + + for i := 0; i < numPosts; i++ { + var err bool + posts[i], err = cfg.CreateRandomPost() + if err != true { + return posts, false + } + } + + return posts, true +} diff --git a/app/auto_teams.go b/app/auto_teams.go new file mode 100644 index 000000000..6e66f4446 --- /dev/null +++ b/app/auto_teams.go @@ -0,0 +1,81 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +type TeamEnvironment struct { + Users []*model.User + Channels []*model.Channel +} + +type AutoTeamCreator struct { + client *model.Client + Fuzzy bool + NameLength utils.Range + NameCharset string + DomainLength utils.Range + DomainCharset string + EmailLength utils.Range + EmailCharset string +} + +func NewAutoTeamCreator(client *model.Client) *AutoTeamCreator { + return &AutoTeamCreator{ + client: client, + Fuzzy: false, + NameLength: TEAM_NAME_LEN, + NameCharset: utils.LOWERCASE, + DomainLength: TEAM_DOMAIN_NAME_LEN, + DomainCharset: utils.LOWERCASE, + EmailLength: TEAM_EMAIL_LEN, + EmailCharset: utils.LOWERCASE, + } +} + +func (cfg *AutoTeamCreator) createRandomTeam() (*model.Team, bool) { + var teamEmail string + var teamDisplayName string + var teamName string + if cfg.Fuzzy { + teamEmail = "success+" + model.NewId() + "simulator.amazonses.com" + teamDisplayName = utils.FuzzName() + teamName = utils.FuzzName() + } else { + teamEmail = "success+" + model.NewId() + "simulator.amazonses.com" + teamDisplayName = utils.RandomName(cfg.NameLength, cfg.NameCharset) + teamName = utils.RandomName(cfg.NameLength, cfg.NameCharset) + model.NewId() + } + team := &model.Team{ + DisplayName: teamDisplayName, + Name: teamName, + Email: teamEmail, + Type: model.TEAM_OPEN, + } + + result, err := cfg.client.CreateTeam(team) + if err != nil { + return nil, false + } + createdTeam := result.Data.(*model.Team) + return createdTeam, true +} + +func (cfg *AutoTeamCreator) CreateTestTeams(num utils.Range) ([]*model.Team, bool) { + numTeams := utils.RandIntFromRange(num) + teams := make([]*model.Team, numTeams) + + for i := 0; i < numTeams; i++ { + var err bool + teams[i], err = cfg.createRandomTeam() + if err != true { + return teams, false + } + } + + return teams, true +} diff --git a/app/auto_users.go b/app/auto_users.go new file mode 100644 index 000000000..7a99cc90b --- /dev/null +++ b/app/auto_users.go @@ -0,0 +1,109 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + + l4g "github.com/alecthomas/log4go" +) + +type AutoUserCreator struct { + client *model.Client + team *model.Team + EmailLength utils.Range + EmailCharset string + NameLength utils.Range + NameCharset string + Fuzzy bool +} + +func NewAutoUserCreator(client *model.Client, team *model.Team) *AutoUserCreator { + return &AutoUserCreator{ + client: client, + team: team, + EmailLength: USER_EMAIL_LEN, + EmailCharset: utils.LOWERCASE, + NameLength: USER_NAME_LEN, + NameCharset: utils.LOWERCASE, + Fuzzy: false, + } +} + +// Basic test team and user so you always know one +func CreateBasicUser(client *model.Client) *model.AppError { + result, _ := client.FindTeamByName(BTEST_TEAM_NAME) + if result.Data.(bool) == false { + newteam := &model.Team{DisplayName: BTEST_TEAM_DISPLAY_NAME, Name: BTEST_TEAM_NAME, Email: BTEST_TEAM_EMAIL, Type: BTEST_TEAM_TYPE} + result, err := client.CreateTeam(newteam) + if err != nil { + return err + } + basicteam := result.Data.(*model.Team) + newuser := &model.User{Email: BTEST_USER_EMAIL, Nickname: BTEST_USER_NAME, Password: BTEST_USER_PASSWORD} + result, err = client.CreateUser(newuser, "") + if err != nil { + return err + } + ruser := result.Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + store.Must(Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id})) + } + return nil +} + +func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) { + var userEmail string + var userName string + if cfg.Fuzzy { + userEmail = "success+" + model.NewId() + "simulator.amazonses.com" + userName = utils.FuzzName() + } else { + userEmail = "success+" + model.NewId() + "simulator.amazonses.com" + userName = utils.RandomName(cfg.NameLength, cfg.NameCharset) + } + + user := &model.User{ + Email: userEmail, + Nickname: userName, + Password: USER_PASSWORD} + + result, err := cfg.client.CreateUserWithInvite(user, "", "", cfg.team.InviteId) + if err != nil { + err.Translate(utils.T) + l4g.Error(err.Error()) + return nil, false + } + + ruser := result.Data.(*model.User) + + status := &model.Status{UserId: ruser.Id, Status: model.STATUS_ONLINE, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""} + if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { + result.Err.Translate(utils.T) + l4g.Error(result.Err.Error()) + return nil, false + } + + // We need to cheat to verify the user's email + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + return result.Data.(*model.User), true +} + +func (cfg *AutoUserCreator) CreateTestUsers(num utils.Range) ([]*model.User, bool) { + numUsers := utils.RandIntFromRange(num) + users := make([]*model.User, numUsers) + + for i := 0; i < numUsers; i++ { + var err bool + users[i], err = cfg.createRandomUser() + if err != true { + return users, false + } + } + + return users, true +} diff --git a/app/command.go b/app/command.go index 2d5861206..491813efe 100644 --- a/app/command.go +++ b/app/command.go @@ -4,9 +4,40 @@ package app import ( + "crypto/tls" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + goi18n "github.com/nicksnyder/go-i18n/i18n" ) +type CommandProvider interface { + GetTrigger() string + GetCommand(T goi18n.TranslateFunc) *model.Command + DoCommand(args *model.CommandArgs, message string) *model.CommandResponse +} + +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 + } + + return nil +} + func CreateCommandPost(post *model.Post, teamId string, response *model.CommandResponse) (*model.Post, *model.AppError) { post.Message = parseSlackLinksToMarkdown(response.Text) post.CreateAt = model.GetMillis() @@ -29,3 +60,276 @@ func CreateCommandPost(post *model.Post, teamId string, response *model.CommandR return post, nil } + +func ListCommands(teamId string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError) { + commands := make([]*model.Command, 0, 32) + seen := make(map[string]bool) + for _, value := range commandProviders { + cpy := *value.GetCommand(T) + if cpy.AutoComplete && !seen[cpy.Id] { + cpy.Sanitize() + seen[cpy.Trigger] = true + commands = append(commands, &cpy) + } + } + + if *utils.Cfg.ServiceSettings.EnableCommands { + if result := <-Srv.Store.Command().GetByTeam(teamId); result.Err != nil { + return nil, result.Err + } 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) + } + } + } + } + + return commands, nil +} + +func ListTeamCommands(teamId string) ([]*model.Command, *model.AppError) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + return nil, model.NewAppError("ListTeamCommands", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + if result := <-Srv.Store.Command().GetByTeam(teamId); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.Command), nil + } +} + +func ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + parts := strings.Split(args.Command, " ") + trigger := parts[0][1:] + trigger = strings.ToLower(trigger) + message := strings.Join(parts[1:], " ") + provider := GetCommandProvider(trigger) + + if provider != nil { + response := provider.DoCommand(args, message) + return HandleCommandResponse(provider.GetCommand(args.T), args, response, true) + } else { + if !*utils.Cfg.ServiceSettings.EnableCommands { + return nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + chanChan := Srv.Store.Channel().Get(args.ChannelId, true) + teamChan := Srv.Store.Team().Get(args.TeamId) + userChan := Srv.Store.User().Get(args.UserId) + + if result := <-Srv.Store.Command().GetByTeam(args.TeamId); result.Err != nil { + return nil, result.Err + } else { + + var team *model.Team + if tr := <-teamChan; tr.Err != nil { + return nil, tr.Err + } else { + team = tr.Data.(*model.Team) + } + + var user *model.User + if ur := <-userChan; ur.Err != nil { + return nil, ur.Err + } else { + user = ur.Data.(*model.User) + } + + var channel *model.Channel + if cr := <-chanChan; cr.Err != nil { + return nil, cr.Err + } 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, args.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", args.ChannelId) + p.Set("channel_name", channel.Name) + + p.Set("user_id", args.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" + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + client := &http.Client{Transport: tr} + + 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 { + return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error(), http.StatusInternalServerError) + } else { + if resp.StatusCode == http.StatusOK { + response := model.CommandResponseFromJson(resp.Body) + if response == nil { + return nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusInternalServerError) + } else { + return HandleCommandResponse(cmd, args, response, false) + } + } else { + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + return nil, model.NewAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]interface{}{"Trigger": trigger, "Status": resp.Status}, string(body), http.StatusInternalServerError) + } + } + } + } + } + } + + return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusNotFound) +} + +func HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError) { + post := &model.Post{} + post.ChannelId = args.ChannelId + post.RootId = args.RootId + post.ParentId = args.ParentId + post.UserId = args.UserId + + if !builtIn { + post.AddProp("from_webhook", "true") + } + + if utils.Cfg.ServiceSettings.EnablePostUsernameOverride { + if len(command.Username) != 0 { + post.AddProp("override_username", command.Username) + } else if len(response.Username) != 0 { + post.AddProp("override_username", response.Username) + } + } + + if utils.Cfg.ServiceSettings.EnablePostIconOverride { + if len(command.IconURL) != 0 { + post.AddProp("override_icon_url", command.IconURL) + } else if len(response.IconURL) != 0 { + post.AddProp("override_icon_url", response.IconURL) + } else { + post.AddProp("override_icon_url", "") + } + } + + if _, err := CreateCommandPost(post, args.TeamId, response); err != nil { + l4g.Error(err.Error()) + } + + return response, nil +} + +func CreateCommand(cmd *model.Command) (*model.Command, *model.AppError) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + return nil, model.NewAppError("CreateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + cmd.Trigger = strings.ToLower(cmd.Trigger) + + if result := <-Srv.Store.Command().GetByTeam(cmd.TeamId); result.Err != nil { + return nil, result.Err + } else { + teamCmds := result.Data.([]*model.Command) + for _, existingCommand := range teamCmds { + if cmd.Trigger == existingCommand.Trigger { + return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) + } + } + for _, builtInProvider := range commandProviders { + builtInCommand := *builtInProvider.GetCommand(utils.T) + if cmd.Trigger == builtInCommand.Trigger { + return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) + } + } + } + + if result := <-Srv.Store.Command().Save(cmd); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.Command), nil + } +} + +func GetCommand(commandId string) (*model.Command, *model.AppError) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + if result := <-Srv.Store.Command().Get(commandId); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.Command), nil + } +} + +func UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + return nil, model.NewAppError("UpdateCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + updatedCmd.Trigger = strings.ToLower(updatedCmd.Trigger) + updatedCmd.Id = oldCmd.Id + updatedCmd.Token = oldCmd.Token + updatedCmd.CreateAt = oldCmd.CreateAt + updatedCmd.UpdateAt = model.GetMillis() + updatedCmd.DeleteAt = oldCmd.DeleteAt + updatedCmd.CreatorId = oldCmd.CreatorId + updatedCmd.TeamId = oldCmd.TeamId + + if result := <-Srv.Store.Command().Update(updatedCmd); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.Command), nil + } +} + +func RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) { + if !*utils.Cfg.ServiceSettings.EnableCommands { + return nil, model.NewAppError("RegenCommandToken", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + cmd.Token = model.NewId() + + if result := <-Srv.Store.Command().Update(cmd); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.Command), nil + } +} + +func DeleteCommand(commandId string) *model.AppError { + if !*utils.Cfg.ServiceSettings.EnableCommands { + return model.NewAppError("DeleteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + if err := (<-Srv.Store.Command().Delete(commandId, model.GetMillis())).Err; err != nil { + return err + } + + return nil +} diff --git a/app/command_away.go b/app/command_away.go new file mode 100644 index 000000000..55553fa3f --- /dev/null +++ b/app/command_away.go @@ -0,0 +1,43 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type AwayProvider struct { +} + +const ( + CMD_AWAY = "away" +) + +func init() { + RegisterCommandProvider(&AwayProvider{}) +} + +func (me *AwayProvider) GetTrigger() string { + return CMD_AWAY +} + +func (me *AwayProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_AWAY, + AutoComplete: true, + AutoCompleteDesc: T("api.command_away.desc"), + DisplayName: T("api.command_away.name"), + } +} + +func (me *AwayProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + rmsg := args.T("api.command_away.success") + if len(message) > 0 { + rmsg = message + " " + rmsg + } + SetStatusAwayIfNeeded(args.UserId, true) + + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg} +} diff --git a/app/command_echo.go b/app/command_echo.go new file mode 100644 index 000000000..40d70e54a --- /dev/null +++ b/app/command_echo.go @@ -0,0 +1,97 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "strconv" + "strings" + "time" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +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(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_ECHO, + AutoComplete: true, + AutoCompleteDesc: T("api.command_echo.desc"), + AutoCompleteHint: T("api.command_echo.hint"), + DisplayName: T("api.command_echo.name"), + } +} + +func (me *EchoProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + if len(message) == 0 { + return &model.CommandResponse{Text: args.T("api.command_echo.message.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + 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: args.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: args.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 = args.ChannelId + post.RootId = args.RootId + post.ParentId = args.ParentId + post.Message = message + post.UserId = args.UserId + + time.Sleep(time.Duration(delay) * time.Second) + + if _, err := CreatePost(post, args.TeamId, true); err != nil { + l4g.Error(args.T("api.command_echo.create.app_error"), err) + } + }() + + return &model.CommandResponse{} +} diff --git a/app/command_expand_collapse.go b/app/command_expand_collapse.go new file mode 100644 index 000000000..a4a152c60 --- /dev/null +++ b/app/command_expand_collapse.go @@ -0,0 +1,87 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "strconv" + + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type ExpandProvider struct { +} + +type CollapseProvider struct { +} + +const ( + CMD_EXPAND = "expand" + CMD_COLLAPSE = "collapse" +) + +func init() { + RegisterCommandProvider(&ExpandProvider{}) + RegisterCommandProvider(&CollapseProvider{}) +} + +func (me *ExpandProvider) GetTrigger() string { + return CMD_EXPAND +} + +func (me *CollapseProvider) GetTrigger() string { + return CMD_COLLAPSE +} + +func (me *ExpandProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_EXPAND, + AutoComplete: true, + AutoCompleteDesc: T("api.command_expand.desc"), + DisplayName: T("api.command_expand.name"), + } +} + +func (me *CollapseProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_COLLAPSE, + AutoComplete: true, + AutoCompleteDesc: T("api.command_collapse.desc"), + DisplayName: T("api.command_collapse.name"), + } +} + +func (me *ExpandProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + return setCollapsePreference(args, false) +} + +func (me *CollapseProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + return setCollapsePreference(args, true) +} + +func setCollapsePreference(args *model.CommandArgs, isCollapse bool) *model.CommandResponse { + pref := model.Preference{ + UserId: args.UserId, + Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, + Name: model.PREFERENCE_NAME_COLLAPSE_SETTING, + Value: strconv.FormatBool(isCollapse), + } + + if result := <-Srv.Store.Preference().Save(&model.Preferences{pref}); result.Err != nil { + return &model.CommandResponse{Text: args.T("api.command_expand_collapse.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + socketMessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", args.UserId, nil) + socketMessage.Add("preference", pref.ToJson()) + go Publish(socketMessage) + + var rmsg string + + if isCollapse { + rmsg = args.T("api.command_collapse.success") + } else { + rmsg = args.T("api.command_expand.success") + } + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg} +} diff --git a/app/command_invite_people.go b/app/command_invite_people.go new file mode 100644 index 000000000..12ef03f45 --- /dev/null +++ b/app/command_invite_people.go @@ -0,0 +1,64 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "strings" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type InvitePeopleProvider struct { +} + +const ( + CMD_INVITE_PEOPLE = "invite_people" +) + +func init() { + RegisterCommandProvider(&InvitePeopleProvider{}) +} + +func (me *InvitePeopleProvider) GetTrigger() string { + return CMD_INVITE_PEOPLE +} + +func (me *InvitePeopleProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_INVITE_PEOPLE, + AutoComplete: true, + AutoCompleteDesc: T("api.command.invite_people.desc"), + AutoCompleteHint: T("api.command.invite_people.hint"), + DisplayName: T("api.command.invite_people.name"), + } +} + +func (me *InvitePeopleProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + if !utils.Cfg.EmailSettings.SendEmailNotifications { + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command.invite_people.email_off")} + } + + emailList := strings.Fields(message) + + for i := len(emailList) - 1; i >= 0; i-- { + emailList[i] = strings.Trim(emailList[i], ",") + if !strings.Contains(emailList[i], "@") { + emailList = append(emailList[:i], emailList[i+1:]...) + } + } + + if len(emailList) == 0 { + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command.invite_people.no_email")} + } + + if err := InviteNewUsersToTeam(emailList, args.TeamId, args.UserId, args.SiteURL); err != nil { + l4g.Error(err.Error()) + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command.invite_people.fail")} + } + + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command.invite_people.sent")} +} diff --git a/app/command_join.go b/app/command_join.go new file mode 100644 index 000000000..5b19dd7a0 --- /dev/null +++ b/app/command_join.go @@ -0,0 +1,62 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type JoinProvider struct { +} + +const ( + CMD_JOIN = "join" +) + +func init() { + RegisterCommandProvider(&JoinProvider{}) +} + +func (me *JoinProvider) GetTrigger() string { + return CMD_JOIN +} + +func (me *JoinProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_JOIN, + AutoComplete: true, + AutoCompleteDesc: T("api.command_join.desc"), + AutoCompleteHint: T("api.command_join.hint"), + DisplayName: T("api.command_join.name"), + } +} + +func (me *JoinProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + if result := <-Srv.Store.Channel().GetByName(args.TeamId, message, true); result.Err != nil { + return &model.CommandResponse{Text: args.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + channel := result.Data.(*model.Channel) + + if channel.Name == message { + + if channel.Type != model.CHANNEL_OPEN { + return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + if err := JoinChannel(channel, args.UserId); err != nil { + return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + team, err := GetTeam(channel.TeamId) + if err != nil { + return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name, Text: args.T("api.command_join.success"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + } + + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command_join.missing.app_error")} +} diff --git a/app/command_loadtest.go b/app/command_loadtest.go new file mode 100644 index 000000000..d3c7474ae --- /dev/null +++ b/app/command_loadtest.go @@ -0,0 +1,437 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "io" + "net/http" + "path" + "strconv" + "strings" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +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 + + Json - Add a post using the JSON file as payload to the current channel. + /loadtest json url + + Example + /loadtest json http://www.example.com/sample_body.json + +` + +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(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_LOADTEST, + AutoComplete: false, + AutoCompleteDesc: "Debug Load Testing", + AutoCompleteHint: "help", + DisplayName: "loadtest", + } +} + +func (me *LoadTestProvider) DoCommand(args *model.CommandArgs, 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(args, message) + } + + if strings.HasPrefix(message, "users") { + return me.UsersCommand(args, message) + } + + if strings.HasPrefix(message, "channels") { + return me.ChannelsCommand(args, message) + } + + if strings.HasPrefix(message, "posts") { + return me.PostsCommand(args, message) + } + + if strings.HasPrefix(message, "url") { + return me.UrlCommand(args, message) + } + if strings.HasPrefix(message, "json") { + return me.JsonCommand(args, message) + } + return me.HelpCommand(args, message) +} + +func (me *LoadTestProvider) HelpCommand(args *model.CommandArgs, message string) *model.CommandResponse { + return &model.CommandResponse{Text: usage, ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) SetupCommand(args *model.CommandArgs, 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(args.SiteURL) + + if doTeams { + if err := CreateBasicUser(client); err != nil { + return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + client.Login(BTEST_USER_EMAIL, BTEST_USER_PASSWORD) + environment, err := CreateTestEnvironmentWithTeams( + client, + utils.Range{Begin: numTeams, End: numTeams}, + utils.Range{Begin: numChannels, End: numChannels}, + utils.Range{Begin: numUsers, End: numUsers}, + utils.Range{Begin: numPosts, End: 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 { + + var team *model.Team + if tr := <-Srv.Store.Team().Get(args.TeamId); tr.Err != nil { + return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + team = tr.Data.(*model.Team) + } + + client.MockSession(args.Session.Token) + client.SetTeamId(args.TeamId) + CreateTestEnvironmentInTeam( + client, + team, + utils.Range{Begin: numChannels, End: numChannels}, + utils.Range{Begin: numUsers, End: numUsers}, + utils.Range{Begin: numPosts, End: numPosts}, + doFuzz) + } + + return &model.CommandResponse{Text: "Created enviroment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) UsersCommand(args *model.CommandArgs, 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{Begin: 2, End: 5} + } + + var team *model.Team + if tr := <-Srv.Store.Team().Get(args.TeamId); tr.Err != nil { + return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + team = tr.Data.(*model.Team) + } + + client := model.NewClient(args.SiteURL) + client.SetTeamId(team.Id) + userCreator := NewAutoUserCreator(client, team) + userCreator.Fuzzy = doFuzz + userCreator.CreateTestUsers(usersr) + + return &model.CommandResponse{Text: "Added users", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) ChannelsCommand(args *model.CommandArgs, 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{Begin: 2, End: 5} + } + + var team *model.Team + if tr := <-Srv.Store.Team().Get(args.TeamId); tr.Err != nil { + return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + team = tr.Data.(*model.Team) + } + + client := model.NewClient(args.SiteURL) + client.SetTeamId(team.Id) + client.MockSession(args.Session.Token) + channelCreator := NewAutoChannelCreator(client, team) + channelCreator.Fuzzy = doFuzz + channelCreator.CreateTestChannels(channelsr) + + return &model.CommandResponse{Text: "Added channels", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) PostsCommand(args *model.CommandArgs, 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{Begin: 20, End: 30} + } + + tokens := strings.Fields(cmd) + rimages := utils.Range{Begin: 0, End: 0} + if len(tokens) >= 3 { + if numImages, err := strconv.Atoi(tokens[2]); err == nil { + rimages = utils.Range{Begin: numImages, End: numImages} + } + } + + var usernames []string + if result := <-Srv.Store.User().GetProfiles(args.TeamId, 0, 1000); result.Err == nil { + profileUsers := result.Data.([]*model.User) + usernames = make([]string, len(profileUsers)) + i := 0 + for _, userprof := range profileUsers { + usernames[i] = userprof.Username + i++ + } + } + + client := model.NewClient(args.SiteURL) + client.SetTeamId(args.TeamId) + client.MockSession(args.Session.Token) + testPoster := NewAutoPostCreator(client, args.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: "Added posts", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) UrlCommand(args *model.CommandArgs, 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/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 = args.ChannelId + post.UserId = args.UserId + + if _, err := CreatePost(post, args.TeamId, false); err != nil { + return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + } + + return &model.CommandResponse{Text: "Loaded data", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + +func (me *LoadTestProvider) JsonCommand(args *model.CommandArgs, message string) *model.CommandResponse { + url := strings.TrimSpace(strings.TrimPrefix(message, "json")) + 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/tests/" + url + + if path.Ext(url) == "" { + url += ".json" + } + } + + 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 + } + + post := model.PostFromJson(contents) + post.ChannelId = args.ChannelId + post.UserId = args.UserId + if post.Message == "" { + post.Message = message + } + + if _, err := CreatePost(post, args.TeamId, false); err != nil { + return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + return &model.CommandResponse{Text: "Loaded 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{Begin: 0, End: 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{Begin: 0, End: 0}, false + } + default: + return utils.Range{Begin: 0, End: 0}, false + } + return utils.Range{Begin: begin, End: end}, true +} + +func contains(items []string, token string) bool { + for _, elem := range items { + if elem == token { + return true + } + } + return false +} diff --git a/app/command_logout.go b/app/command_logout.go new file mode 100644 index 000000000..1a353056e --- /dev/null +++ b/app/command_logout.go @@ -0,0 +1,48 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type LogoutProvider struct { +} + +const ( + CMD_LOGOUT = "logout" +) + +func init() { + RegisterCommandProvider(&LogoutProvider{}) +} + +func (me *LogoutProvider) GetTrigger() string { + return CMD_LOGOUT +} + +func (me *LogoutProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_LOGOUT, + AutoComplete: true, + AutoCompleteDesc: T("api.command_logout.desc"), + AutoCompleteHint: "", + DisplayName: T("api.command_logout.name"), + } +} + +func (me *LogoutProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + FAIL := &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command_logout.fail_message")} + SUCCESS := &model.CommandResponse{GotoLocation: "/login"} + + // We can't actually remove the user's cookie from here so we just dump their session and let the browser figure it out + if args.Session.Id != "" { + if err := RevokeSessionById(args.Session.Id); err != nil { + return FAIL + } + return SUCCESS + } + return FAIL +} diff --git a/app/command_me.go b/app/command_me.go new file mode 100644 index 000000000..bb29ec1e0 --- /dev/null +++ b/app/command_me.go @@ -0,0 +1,38 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type MeProvider struct { +} + +const ( + CMD_ME = "me" +) + +func init() { + RegisterCommandProvider(&MeProvider{}) +} + +func (me *MeProvider) GetTrigger() string { + return CMD_ME +} + +func (me *MeProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_ME, + AutoComplete: true, + AutoCompleteDesc: T("api.command_me.desc"), + AutoCompleteHint: T("api.command_me.hint"), + DisplayName: T("api.command_me.name"), + } +} + +func (me *MeProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, Text: "*" + message + "*"} +} diff --git a/app/command_msg.go b/app/command_msg.go new file mode 100644 index 000000000..fd4ace61a --- /dev/null +++ b/app/command_msg.go @@ -0,0 +1,110 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "strings" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type msgProvider struct { +} + +const ( + CMD_MSG = "msg" +) + +func init() { + RegisterCommandProvider(&msgProvider{}) +} + +func (me *msgProvider) GetTrigger() string { + return CMD_MSG +} + +func (me *msgProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_MSG, + AutoComplete: true, + AutoCompleteDesc: T("api.command_msg.desc"), + AutoCompleteHint: T("api.command_msg.hint"), + DisplayName: T("api.command_msg.name"), + } +} + +func (me *msgProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + + splitMessage := strings.SplitN(message, " ", 2) + + parsedMessage := "" + targetUsername := "" + teamId := "" + + if len(splitMessage) > 1 { + parsedMessage = strings.SplitN(message, " ", 2)[1] + } + targetUsername = strings.SplitN(message, " ", 2)[0] + targetUsername = strings.TrimPrefix(targetUsername, "@") + + var userProfile *model.User + if result := <-Srv.Store.User().GetByUsername(targetUsername); result.Err != nil { + l4g.Error(result.Err.Error()) + return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + userProfile = result.Data.(*model.User) + } + + if userProfile.Id == args.UserId { + return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + // Find the channel based on this user + channelName := model.GetDMNameFromIds(args.UserId, userProfile.Id) + + targetChannelId := "" + if channel := <-Srv.Store.Channel().GetByName(args.TeamId, channelName, true); channel.Err != nil { + if channel.Err.Id == "store.sql_channel.get_by_name.missing.app_error" { + if directChannel, err := CreateDirectChannel(args.UserId, userProfile.Id); err != nil { + l4g.Error(err.Error()) + return &model.CommandResponse{Text: args.T("api.command_msg.dm_fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + targetChannelId = directChannel.Id + } + } else { + l4g.Error(channel.Err.Error()) + return &model.CommandResponse{Text: args.T("api.command_msg.dm_fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + } else { + channel := channel.Data.(*model.Channel) + targetChannelId = channel.Id + teamId = channel.TeamId + } + + if len(parsedMessage) > 0 { + post := &model.Post{} + post.Message = parsedMessage + post.ChannelId = targetChannelId + post.UserId = args.UserId + if _, err := CreatePost(post, args.TeamId, true); err != nil { + return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + } + + if teamId == "" { + if len(args.Session.TeamMembers) == 0 { + return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + teamId = args.Session.TeamMembers[0].TeamId + } + + team, err := GetTeam(teamId) + if err != nil { + return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channelName, Text: "", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} diff --git a/app/command_offline.go b/app/command_offline.go new file mode 100644 index 000000000..6e2c125f8 --- /dev/null +++ b/app/command_offline.go @@ -0,0 +1,43 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type OfflineProvider struct { +} + +const ( + CMD_OFFLINE = "offline" +) + +func init() { + RegisterCommandProvider(&OfflineProvider{}) +} + +func (me *OfflineProvider) GetTrigger() string { + return CMD_OFFLINE +} + +func (me *OfflineProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_OFFLINE, + AutoComplete: true, + AutoCompleteDesc: T("api.command_offline.desc"), + DisplayName: T("api.command_offline.name"), + } +} + +func (me *OfflineProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + rmsg := args.T("api.command_offline.success") + if len(message) > 0 { + rmsg = message + " " + rmsg + } + SetStatusOffline(args.UserId, true) + + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg} +} diff --git a/app/command_online.go b/app/command_online.go new file mode 100644 index 000000000..bd6fbab60 --- /dev/null +++ b/app/command_online.go @@ -0,0 +1,43 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type OnlineProvider struct { +} + +const ( + CMD_ONLINE = "online" +) + +func init() { + RegisterCommandProvider(&OnlineProvider{}) +} + +func (me *OnlineProvider) GetTrigger() string { + return CMD_ONLINE +} + +func (me *OnlineProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_ONLINE, + AutoComplete: true, + AutoCompleteDesc: T("api.command_online.desc"), + DisplayName: T("api.command_online.name"), + } +} + +func (me *OnlineProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + rmsg := args.T("api.command_online.success") + if len(message) > 0 { + rmsg = message + " " + rmsg + } + SetStatusOnline(args.UserId, args.Session.Id, true) + + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg} +} diff --git a/app/command_shortcuts.go b/app/command_shortcuts.go new file mode 100644 index 000000000..93e5f0f51 --- /dev/null +++ b/app/command_shortcuts.go @@ -0,0 +1,95 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "bytes" + "strings" + + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type ShortcutsProvider struct { +} + +const ( + CMD_SHORTCUTS = "shortcuts" +) + +func init() { + RegisterCommandProvider(&ShortcutsProvider{}) +} + +func (me *ShortcutsProvider) GetTrigger() string { + return CMD_SHORTCUTS +} + +func (me *ShortcutsProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_SHORTCUTS, + AutoComplete: true, + AutoCompleteDesc: T("api.command_shortcuts.desc"), + AutoCompleteHint: "", + DisplayName: T("api.command_shortcuts.name"), + } +} + +func (me *ShortcutsProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + shortcutIds := [28]string{ + "api.command_shortcuts.header", + // Nav shortcuts + "api.command_shortcuts.nav.header", + "api.command_shortcuts.nav.prev", + "api.command_shortcuts.nav.next", + "api.command_shortcuts.nav.unread_prev", + "api.command_shortcuts.nav.unread_next", + "api.command_shortcuts.nav.switcher", + "api.command_shortcuts.nav.settings", + "api.command_shortcuts.nav.recent_mentions", + // Files shortcuts + "api.command_shortcuts.files.header", + "api.command_shortcuts.files.upload", + // Msg shortcuts + "api.command_shortcuts.msgs.header", + "api.command_shortcuts.msgs.mark_as_read", + "api.command_shortcuts.msgs.reprint_prev", + "api.command_shortcuts.msgs.reprint_next", + "api.command_shortcuts.msgs.edit", + "api.command_shortcuts.msgs.comp_username", + "api.command_shortcuts.msgs.comp_channel", + "api.command_shortcuts.msgs.comp_emoji", + // Browser shortcuts + "api.command_shortcuts.browser.header", + "api.command_shortcuts.browser.channel_prev", + "api.command_shortcuts.browser.channel_next", + "api.command_shortcuts.browser.font_increase", + "api.command_shortcuts.browser.font_decrease", + "api.command_shortcuts.browser.highlight_prev", + "api.command_shortcuts.browser.highlight_next", + "api.command_shortcuts.browser.newline", + } + + var osDependentWords map[string]interface{} + if strings.Contains(message, "mac") { + osDependentWords = map[string]interface{}{ + "CmdOrCtrl": args.T("api.command_shortcuts.cmd"), + "ChannelPrevCmd": args.T("api.command_shortcuts.browser.channel_prev.cmd_mac"), + "ChannelNextCmd": args.T("api.command_shortcuts.browser.channel_next.cmd_mac"), + } + } else { + osDependentWords = map[string]interface{}{ + "CmdOrCtrl": args.T("api.command_shortcuts.ctrl"), + "ChannelPrevCmd": args.T("api.command_shortcuts.browser.channel_prev.cmd"), + "ChannelNextCmd": args.T("api.command_shortcuts.browser.channel_next.cmd"), + } + } + + var buffer bytes.Buffer + for _, element := range shortcutIds { + buffer.WriteString(args.T(element, osDependentWords)) + } + + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: buffer.String()} +} diff --git a/app/command_shrug.go b/app/command_shrug.go new file mode 100644 index 000000000..12d1039ec --- /dev/null +++ b/app/command_shrug.go @@ -0,0 +1,43 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/platform/model" + goi18n "github.com/nicksnyder/go-i18n/i18n" +) + +type ShrugProvider struct { +} + +const ( + CMD_SHRUG = "shrug" +) + +func init() { + RegisterCommandProvider(&ShrugProvider{}) +} + +func (me *ShrugProvider) GetTrigger() string { + return CMD_SHRUG +} + +func (me *ShrugProvider) GetCommand(T goi18n.TranslateFunc) *model.Command { + return &model.Command{ + Trigger: CMD_SHRUG, + AutoComplete: true, + AutoCompleteDesc: T("api.command_shrug.desc"), + AutoCompleteHint: T("api.command_shrug.hint"), + DisplayName: T("api.command_shrug.name"), + } +} + +func (me *ShrugProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse { + rmsg := `¯\\\_(ツ)\_/¯` + if len(message) > 0 { + rmsg = message + " " + rmsg + } + + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, Text: rmsg} +} -- cgit v1.2.3-1-g7c22