diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/admin.go | 21 | ||||
-rw-r--r-- | app/auto_channels.go | 74 | ||||
-rw-r--r-- | app/auto_constants.go | 36 | ||||
-rw-r--r-- | app/auto_environment.go | 99 | ||||
-rw-r--r-- | app/auto_posts.go | 103 | ||||
-rw-r--r-- | app/auto_teams.go | 81 | ||||
-rw-r--r-- | app/auto_users.go | 109 | ||||
-rw-r--r-- | app/brand.go | 16 | ||||
-rw-r--r-- | app/channel.go | 28 | ||||
-rw-r--r-- | app/command.go | 304 | ||||
-rw-r--r-- | app/command_away.go | 43 | ||||
-rw-r--r-- | app/command_echo.go | 97 | ||||
-rw-r--r-- | app/command_expand_collapse.go | 87 | ||||
-rw-r--r-- | app/command_invite_people.go | 64 | ||||
-rw-r--r-- | app/command_join.go | 62 | ||||
-rw-r--r-- | app/command_loadtest.go | 437 | ||||
-rw-r--r-- | app/command_logout.go | 48 | ||||
-rw-r--r-- | app/command_me.go | 38 | ||||
-rw-r--r-- | app/command_msg.go | 110 | ||||
-rw-r--r-- | app/command_offline.go | 43 | ||||
-rw-r--r-- | app/command_online.go | 43 | ||||
-rw-r--r-- | app/command_shortcuts.go | 95 | ||||
-rw-r--r-- | app/command_shrug.go | 43 | ||||
-rw-r--r-- | app/compliance.go | 4 | ||||
-rw-r--r-- | app/email_test.go | 443 | ||||
-rw-r--r-- | app/file.go | 5 | ||||
-rw-r--r-- | app/saml.go | 143 | ||||
-rw-r--r-- | app/team.go | 30 | ||||
-rw-r--r-- | app/user.go | 86 | ||||
-rw-r--r-- | app/webhook.go | 33 |
30 files changed, 2629 insertions, 196 deletions
diff --git a/app/admin.go b/app/admin.go index c551da50c..509d81840 100644 --- a/app/admin.go +++ b/app/admin.go @@ -18,14 +18,14 @@ import ( "github.com/mattermost/platform/utils" ) -func GetLogs() ([]string, *model.AppError) { - lines, err := GetLogsSkipSend() +func GetLogs(page, perPage int) ([]string, *model.AppError) { + lines, err := GetLogsSkipSend(page, perPage) if err != nil { return nil, err } if einterfaces.GetClusterInterface() != nil { - clines, err := einterfaces.GetClusterInterface().GetLogs() + clines, err := einterfaces.GetClusterInterface().GetLogs(page, perPage) if err != nil { return nil, err } @@ -36,7 +36,7 @@ func GetLogs() ([]string, *model.AppError) { return lines, nil } -func GetLogsSkipSend() ([]string, *model.AppError) { +func GetLogsSkipSend(page, perPage int) ([]string, *model.AppError) { var lines []string if utils.Cfg.LogSettings.EnableFile { @@ -47,9 +47,20 @@ func GetLogsSkipSend() ([]string, *model.AppError) { defer file.Close() + offsetCount := 0 + limitCount := 0 scanner := bufio.NewScanner(file) for scanner.Scan() { - lines = append(lines, scanner.Text()) + if limitCount >= perPage { + break + } + + if offsetCount >= page*perPage { + lines = append(lines, scanner.Text()) + limitCount++ + } else { + offsetCount++ + } } } else { lines = append(lines, "") 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/brand.go b/app/brand.go index aeecc6972..9b3df3145 100644 --- a/app/brand.go +++ b/app/brand.go @@ -13,11 +13,13 @@ import ( ) func SaveBrandImage(imageData *multipart.FileHeader) *model.AppError { + if len(utils.Cfg.FileSettings.DriverName) == 0 { + return model.NewAppError("SaveBrandImage", "api.admin.upload_brand_image.storage.app_error", nil, "", http.StatusNotImplemented) + } + brandInterface := einterfaces.GetBrandInterface() if brandInterface == nil { - err := model.NewLocAppError("SaveBrandImage", "api.admin.upload_brand_image.not_available.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented - return err + return model.NewAppError("SaveBrandImage", "api.admin.upload_brand_image.not_available.app_error", nil, "", http.StatusNotImplemented) } if err := brandInterface.SaveBrandImage(imageData); err != nil { @@ -29,16 +31,12 @@ func SaveBrandImage(imageData *multipart.FileHeader) *model.AppError { func GetBrandImage() ([]byte, *model.AppError) { if len(utils.Cfg.FileSettings.DriverName) == 0 { - err := model.NewLocAppError("GetBrandImage", "api.admin.get_brand_image.storage.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented - return nil, err + return nil, model.NewAppError("GetBrandImage", "api.admin.get_brand_image.storage.app_error", nil, "", http.StatusNotImplemented) } brandInterface := einterfaces.GetBrandInterface() if brandInterface == nil { - err := model.NewLocAppError("GetBrandImage", "api.admin.get_brand_image.not_available.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented - return nil, err + return nil, model.NewAppError("GetBrandImage", "api.admin.get_brand_image.not_available.app_error", nil, "", http.StatusNotImplemented) } if img, err := brandInterface.GetBrandImage(); err != nil { diff --git a/app/channel.go b/app/channel.go index 7c63cbc6b..af7596ae1 100644 --- a/app/channel.go +++ b/app/channel.go @@ -313,7 +313,7 @@ func UpdateChannelMemberNotifyProps(data map[string]string, channelId string, us func DeleteChannel(channel *model.Channel, userId string, siteURL string) *model.AppError { uc := Srv.Store.User().Get(userId) ihc := Srv.Store.Webhook().GetIncomingByChannel(channel.Id) - ohc := Srv.Store.Webhook().GetOutgoingByChannel(channel.Id) + ohc := Srv.Store.Webhook().GetOutgoingByChannel(channel.Id, -1, -1) if uresult := <-uc; uresult.Err != nil { return uresult.Err @@ -646,6 +646,14 @@ func GetChannelsUserNotIn(teamId string, userId string, offset int, limit int) ( } } +func GetPublicChannelsForTeam(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError) { + if result := <-Srv.Store.Channel().GetPublicChannelsForTeam(teamId, offset, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.ChannelList), nil + } +} + func GetChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) { if result := <-Srv.Store.Channel().GetMember(channelId, userId); result.Err != nil { return nil, result.Err @@ -694,7 +702,25 @@ func GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, *mode } } +func GetChannelUnread(channelId, userId string) (*model.ChannelUnread, *model.AppError) { + result := <-Srv.Store.Channel().GetChannelUnread(channelId, userId) + if result.Err != nil { + return nil, result.Err + } + channelUnread := result.Data.(*model.ChannelUnread) + + if channelUnread.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_MARK_UNREAD_MENTION { + channelUnread.MsgCount = 0 + } + + return channelUnread, nil +} + func JoinChannel(channel *model.Channel, userId string, siteURL string) *model.AppError { + if channel.DeleteAt > 0 { + return model.NewLocAppError("JoinChannel", "api.channel.join_channel.already_deleted.app_error", nil, "") + } + userChan := Srv.Store.User().Get(userId) memberChan := Srv.Store.Channel().GetMember(channel.Id, userId) diff --git a/app/command.go b/app/command.go index 4d344915d..4583cf81b 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, siteURL string) (*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, args.SiteURL); 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..3bfe67cd7 --- /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, args.SiteURL); 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..e7d25aa16 --- /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, args.SiteURL); 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..b1d89826b --- /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] <Num Channels> <Num Users> <NumPosts> + + Example: + /loadtest setup teams fuzz 10 20 50 + + Users - Add a specified number of random users with fuzz text to current team. + /loadtest users [fuzz] <Min Users> <Max Users> + + Example: + /loadtest users fuzz 5 10 + + Channels - Add a specified number of random channels with fuzz text to current team. + /loadtest channels [fuzz] <Min Channels> <Max Channels> + + Example: + /loadtest channels fuzz 5 10 + + Posts - Add some random posts with fuzz text to current channel. + /loadtest posts [fuzz] <Min Posts> <Max Posts> <Max Images> + + Example: + /loadtest posts fuzz 5 10 3 + + Url - Add a post containing the text from a given url to current channel. + /loadtest url + + Example: + /loadtest http://www.example.com/sample_file.md + + 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, args.SiteURL); 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, args.SiteURL); 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..06b17fdc4 --- /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, args.SiteURL); 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} +} diff --git a/app/compliance.go b/app/compliance.go index ffef69b44..966b9b523 100644 --- a/app/compliance.go +++ b/app/compliance.go @@ -11,12 +11,12 @@ import ( "github.com/mattermost/platform/utils" ) -func GetComplianceReports() (model.Compliances, *model.AppError) { +func GetComplianceReports(page, perPage int) (model.Compliances, *model.AppError) { if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance { return nil, model.NewLocAppError("GetComplianceReports", "ent.compliance.licence_disable.app_error", nil, "") } - if result := <-Srv.Store.Compliance().GetAll(); result.Err != nil { + if result := <-Srv.Store.Compliance().GetAll(page*perPage, perPage); result.Err != nil { return nil, result.Err } else { return result.Data.(model.Compliances), nil diff --git a/app/email_test.go b/app/email_test.go index 6cd46fee0..17b892585 100644 --- a/app/email_test.go +++ b/app/email_test.go @@ -30,17 +30,29 @@ func TestSendChangeUsernameEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(emailTo); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], emailTo) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID); err == nil { - if resultsEmail.Subject != expectedSubject { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(emailTo) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], emailTo) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } } } } @@ -66,21 +78,33 @@ func TestSendEmailChangeVerifyEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(newUserEmail); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], newUserEmail) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(newUserEmail, resultsMailbox[0].ID); err == nil { - if resultsEmail.Subject != expectedSubject { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") - } - if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(newUserEmail)) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong new email in the message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(newUserEmail) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], newUserEmail) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(newUserEmail, resultsMailbox[0].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } + if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(newUserEmail)) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong new email in the message") + } } } } @@ -106,17 +130,29 @@ func TestSendEmailChangeEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(oldEmail); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], oldEmail) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(oldEmail, resultsMailbox[0].ID); err == nil { - if resultsEmail.Subject != expectedSubject { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(oldEmail) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], oldEmail) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(oldEmail, resultsMailbox[0].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } } } } @@ -142,21 +178,33 @@ func TestSendVerifyEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(userEmail); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], userEmail) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(userEmail, resultsMailbox[0].ID); err == nil { - if resultsEmail.Subject != expectedSubject { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") - } - if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(userEmail)) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong new email in the message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(userEmail) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], userEmail) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(userEmail, resultsMailbox[0].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } + if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(userEmail)) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong new email in the message") + } } } } @@ -182,17 +230,29 @@ func TestSendSignInChangeEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { - if resultsEmail.Subject != expectedSubject { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(email) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], email) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } } } } @@ -219,17 +279,29 @@ func TestSendWelcomeEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { - if resultsEmail.Subject != expectedSubject { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(email) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], email) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } } } } @@ -244,25 +316,37 @@ func TestSendWelcomeEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { - if !strings.Contains(resultsEmail.Subject, expectedSubject) { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") - } - if !strings.Contains(resultsEmail.Body.Text, expectedVerifyEmail) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") - } - if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(email)) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong email in the message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(email) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], email) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { + if !strings.Contains(resultsEmail.Subject, expectedSubject) { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } + if !strings.Contains(resultsEmail.Body.Text, expectedVerifyEmail) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } + if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(email)) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong email in the message") + } } } } @@ -288,17 +372,29 @@ func TestSendPasswordChangeEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { - if resultsEmail.Subject != expectedSubject { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(email) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], email) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } } } } @@ -324,17 +420,29 @@ func TestSendMfaChangeEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { - if resultsEmail.Subject != expectedSubject { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(email) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], email) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } } } } @@ -349,17 +457,29 @@ func TestSendMfaChangeEmail(t *testing.T) { t.Fatal("Should send change username email") } else { //Check if the email was send to the rigth email address - if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { - if !strings.Contains(resultsEmail.Subject, expectedSubject) { - t.Log(resultsEmail.Subject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(email) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], email) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil { + if !strings.Contains(resultsEmail.Subject, expectedSubject) { + t.Log(resultsEmail.Subject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } } } } @@ -385,35 +505,58 @@ func TestSendInviteEmails(t *testing.T) { SendInviteEmails(th.BasicTeam, senderName, invites, siteURL) //Check if the email was send to the rigth email address to email1 - if resultsMailbox, err := utils.GetMailBox(email1); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email1) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(email1, resultsMailbox[0].ID); err == nil { - if resultsEmail.Subject != expectedSubject { - t.Log(resultsEmail.Subject) - t.Log(expectedSubject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") + var resultsMailbox utils.JSONMessageHeaderInbucket + err := utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(email1) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], email1) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(email1, resultsMailbox[0].ID); err == nil { + if resultsEmail.Subject != expectedSubject { + t.Log(resultsEmail.Subject) + t.Log(expectedSubject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } } } } //Check if the email was send to the rigth email address to email2 - if resultsMailbox, err := utils.GetMailBox(email2); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email2) { - t.Fatal("Wrong To recipient") - } else { - if resultsEmail, err := utils.GetMessageFromMailbox(email2, resultsMailbox[0].ID); err == nil { - if !strings.Contains(resultsEmail.Subject, expectedSubject) { - t.Log(resultsEmail.Subject) - t.Log(expectedSubject) - t.Fatal("Wrong Subject") - } - if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { - t.Log(resultsEmail.Body.Text) - t.Fatal("Wrong Body message") + err = utils.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = utils.GetMailBox(email2) + return err + }) + if err != nil { + t.Log(err) + t.Log("No email was received, maybe due load on the server. Disabling this verification") + } + if err == nil && len(resultsMailbox) > 0 { + if !strings.ContainsAny(resultsMailbox[0].To[0], email2) { + t.Fatal("Wrong To recipient") + } else { + if resultsEmail, err := utils.GetMessageFromMailbox(email2, resultsMailbox[0].ID); err == nil { + if !strings.Contains(resultsEmail.Subject, expectedSubject) { + t.Log(resultsEmail.Subject) + t.Log(expectedSubject) + t.Fatal("Wrong Subject") + } + if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) { + t.Log(resultsEmail.Body.Text) + t.Fatal("Wrong Body message") + } } } } diff --git a/app/file.go b/app/file.go index b678475c9..8c0960fe8 100644 --- a/app/file.go +++ b/app/file.go @@ -360,6 +360,11 @@ func MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo { func GeneratePublicLink(siteURL string, info *model.FileInfo) string { hash := GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt) + return fmt.Sprintf("%s/files/%v/public?h=%s", siteURL, info.Id, hash) +} + +func GeneratePublicLinkV3(siteURL string, info *model.FileInfo) string { + hash := GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt) return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX_V3, info.Id, hash) } diff --git a/app/saml.go b/app/saml.go index cc39d4540..92f0e1f0e 100644 --- a/app/saml.go +++ b/app/saml.go @@ -16,21 +16,19 @@ import ( func GetSamlMetadata() (string, *model.AppError) { samlInterface := einterfaces.GetSamlInterface() - if samlInterface == nil { - err := model.NewLocAppError("GetSamlMetadata", "api.admin.saml.not_available.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented + err := model.NewAppError("GetSamlMetadata", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented) return "", err } if result, err := samlInterface.GetMetadata(); err != nil { - return "", model.NewLocAppError("GetSamlMetadata", "api.admin.saml.metadata.app_error", nil, "err="+err.Message) + return "", model.NewAppError("GetSamlMetadata", "api.admin.saml.metadata.app_error", nil, "err="+err.Message, err.StatusCode) } else { return result, nil } } -func AddSamlCertificate(fileData *multipart.FileHeader) *model.AppError { +func WriteSamlFile(fileData *multipart.FileHeader) *model.AppError { file, err := fileData.Open() defer file.Close() if err != nil { @@ -47,7 +45,67 @@ func AddSamlCertificate(fileData *multipart.FileHeader) *model.AppError { return nil } -func RemoveSamlCertificate(filename string) *model.AppError { +func AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError { + if err := WriteSamlFile(fileData); err != nil { + return err + } + + cfg := &model.Config{} + *cfg = *utils.Cfg + + *cfg.SamlSettings.PublicCertificateFile = fileData.Filename + + if err := cfg.IsValid(); err != nil { + return err + } + + utils.SaveConfig(utils.CfgFileName, cfg) + utils.LoadConfig(utils.CfgFileName) + + return nil +} + +func AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError { + if err := WriteSamlFile(fileData); err != nil { + return err + } + + cfg := &model.Config{} + *cfg = *utils.Cfg + + *cfg.SamlSettings.PrivateKeyFile = fileData.Filename + + if err := cfg.IsValid(); err != nil { + return err + } + + utils.SaveConfig(utils.CfgFileName, cfg) + utils.LoadConfig(utils.CfgFileName) + + return nil +} + +func AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError { + if err := WriteSamlFile(fileData); err != nil { + return err + } + + cfg := &model.Config{} + *cfg = *utils.Cfg + + *cfg.SamlSettings.IdpCertificateFile = fileData.Filename + + if err := cfg.IsValid(); err != nil { + return err + } + + utils.SaveConfig(utils.CfgFileName, cfg) + utils.LoadConfig(utils.CfgFileName) + + return nil +} + +func RemoveSamlFile(filename string) *model.AppError { if err := os.Remove(utils.FindConfigFile(filename)); err != nil { return model.NewLocAppError("removeCertificate", "api.admin.remove_certificate.delete.app_error", map[string]interface{}{"Filename": filename}, err.Error()) @@ -56,12 +114,75 @@ func RemoveSamlCertificate(filename string) *model.AppError { return nil } -func GetSamlCertificateStatus() map[string]interface{} { - status := make(map[string]interface{}) +func RemoveSamlPublicCertificate() *model.AppError { + if err := RemoveSamlFile(*utils.Cfg.SamlSettings.PublicCertificateFile); err != nil { + return err + } + + cfg := &model.Config{} + *cfg = *utils.Cfg + + *cfg.SamlSettings.PublicCertificateFile = "" + *cfg.SamlSettings.Encrypt = false + + if err := cfg.IsValid(); err != nil { + return err + } + + utils.SaveConfig(utils.CfgFileName, cfg) + utils.LoadConfig(utils.CfgFileName) + + return nil +} + +func RemoveSamlPrivateCertificate() *model.AppError { + if err := RemoveSamlFile(*utils.Cfg.SamlSettings.PrivateKeyFile); err != nil { + return err + } + + cfg := &model.Config{} + *cfg = *utils.Cfg + + *cfg.SamlSettings.PrivateKeyFile = "" + *cfg.SamlSettings.Encrypt = false + + if err := cfg.IsValid(); err != nil { + return err + } + + utils.SaveConfig(utils.CfgFileName, cfg) + utils.LoadConfig(utils.CfgFileName) + + return nil +} + +func RemoveSamlIdpCertificate() *model.AppError { + if err := RemoveSamlFile(*utils.Cfg.SamlSettings.IdpCertificateFile); err != nil { + return err + } + + cfg := &model.Config{} + *cfg = *utils.Cfg + + *cfg.SamlSettings.IdpCertificateFile = "" + *cfg.SamlSettings.Enable = false + + if err := cfg.IsValid(); err != nil { + return err + } + + utils.SaveConfig(utils.CfgFileName, cfg) + utils.LoadConfig(utils.CfgFileName) + + return nil +} + +func GetSamlCertificateStatus() *model.SamlCertificateStatus { + status := &model.SamlCertificateStatus{} - status["IdpCertificateFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.IdpCertificateFile) - status["PrivateKeyFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PrivateKeyFile) - status["PublicCertificateFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PublicCertificateFile) + status.IdpCertificateFile = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.IdpCertificateFile) + status.PrivateKeyFile = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PrivateKeyFile) + status.PublicCertificateFile = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PublicCertificateFile) return status } diff --git a/app/team.go b/app/team.go index 875dedfd0..d0d907731 100644 --- a/app/team.go +++ b/app/team.go @@ -391,6 +391,30 @@ func GetTeamMembersByIds(teamId string, userIds []string) ([]*model.TeamMember, } } +func GetTeamUnread(teamId, userId string) (*model.TeamUnread, *model.AppError) { + result := <-Srv.Store.Team().GetChannelUnreadsForTeam(teamId, userId) + if result.Err != nil { + return nil, result.Err + } + + channelUnreads := result.Data.([]*model.ChannelUnread) + var teamUnread = &model.TeamUnread{ + MsgCount: 0, + MentionCount: 0, + TeamId: teamId, + } + + for _, cu := range channelUnreads { + teamUnread.MentionCount += cu.MentionCount + + if cu.NotifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_MENTION { + teamUnread.MsgCount += cu.MsgCount + } + } + + return teamUnread, nil +} + func RemoveUserFromTeam(teamId string, userId string) *model.AppError { tchan := Srv.Store.Team().Get(teamId) uchan := Srv.Store.User().Get(userId) @@ -511,8 +535,8 @@ func FindTeamByName(name string) bool { } } -func GetTeamsUnreadForUser(teamId string, userId string) ([]*model.TeamUnread, *model.AppError) { - if result := <-Srv.Store.Team().GetTeamsUnreadForUser(teamId, userId); result.Err != nil { +func GetTeamsUnreadForUser(excludeTeamId string, userId string) ([]*model.TeamUnread, *model.AppError) { + if result := <-Srv.Store.Team().GetChannelUnreadsForAllTeams(excludeTeamId, userId); result.Err != nil { return nil, result.Err } else { data := result.Data.([]*model.ChannelUnread) @@ -523,7 +547,7 @@ func GetTeamsUnreadForUser(teamId string, userId string) ([]*model.TeamUnread, * tu.MentionCount += cu.MentionCount if cu.NotifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_MENTION { - tu.MsgCount += (cu.TotalMsgCount - cu.MsgCount) + tu.MsgCount += cu.MsgCount } return tu diff --git a/app/user.go b/app/user.go index d050e1524..c877640d6 100644 --- a/app/user.go +++ b/app/user.go @@ -949,6 +949,34 @@ func UpdateUserNotifyProps(userId string, props map[string]string, siteURL strin return ruser, nil } +func UpdateMfa(activate bool, userId, token, siteUrl string) *model.AppError { + if activate { + if err := ActivateMfa(userId, token); err != nil { + return err + } + } else { + if err := DeactivateMfa(userId); err != nil { + return err + } + } + + go func() { + var user *model.User + var err *model.AppError + + if user, err = GetUser(userId); err != nil { + l4g.Error(err.Error()) + return + } + + if err := SendMfaChangeEmail(user.Email, activate, user.Locale, siteUrl); err != nil { + l4g.Error(err.Error()) + } + }() + + return nil +} + func UpdatePasswordByUserIdSendEmail(userId, newPassword, method, siteURL string) *model.AppError { var user *model.User var err *model.AppError @@ -1183,31 +1211,49 @@ func VerifyUserEmail(userId string) *model.AppError { return nil } -func SearchUsersInChannel(channelId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) { +func SearchUsersInChannel(channelId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { if result := <-Srv.Store.User().SearchInChannel(channelId, term, searchOptions); result.Err != nil { return nil, result.Err } else { - return result.Data.([]*model.User), nil + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + return users, nil } } -func SearchUsersNotInChannel(teamId string, channelId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) { +func SearchUsersNotInChannel(teamId string, channelId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { if result := <-Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions); result.Err != nil { return nil, result.Err } else { - return result.Data.([]*model.User), nil + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + return users, nil } } -func SearchUsersInTeam(teamId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) { +func SearchUsersInTeam(teamId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { if result := <-Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil { return nil, result.Err } else { - return result.Data.([]*model.User), nil + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + return users, nil } } -func AutocompleteUsersInChannel(teamId string, channelId string, term string, searchOptions map[string]bool) (*model.UserAutocompleteInChannel, *model.AppError) { +func AutocompleteUsersInChannel(teamId string, channelId string, term string, searchOptions map[string]bool, asAdmin bool) (*model.UserAutocompleteInChannel, *model.AppError) { uchan := Srv.Store.User().SearchInChannel(channelId, term, searchOptions) nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions) @@ -1216,25 +1262,43 @@ func AutocompleteUsersInChannel(teamId string, channelId string, term string, se if result := <-uchan; result.Err != nil { return nil, result.Err } else { - autocomplete.InChannel = result.Data.([]*model.User) + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + autocomplete.InChannel = users } if result := <-nuchan; result.Err != nil { return nil, result.Err } else { - autocomplete.OutOfChannel = result.Data.([]*model.User) + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + autocomplete.OutOfChannel = users } return autocomplete, nil } -func AutocompleteUsersInTeam(teamId string, term string, searchOptions map[string]bool) (*model.UserAutocompleteInTeam, *model.AppError) { +func AutocompleteUsersInTeam(teamId string, term string, searchOptions map[string]bool, asAdmin bool) (*model.UserAutocompleteInTeam, *model.AppError) { autocomplete := &model.UserAutocompleteInTeam{} if result := <-Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil { return nil, result.Err } else { - autocomplete.InTeam = result.Data.([]*model.User) + users := result.Data.([]*model.User) + + for _, user := range users { + SanitizeProfile(user, asAdmin) + } + + autocomplete.InTeam = users } return autocomplete, nil diff --git a/app/webhook.go b/app/webhook.go index 98e1080b6..c3853b97b 100644 --- a/app/webhook.go +++ b/app/webhook.go @@ -33,7 +33,7 @@ func handleWebhookEvents(post *model.Post, team *model.Team, channel *model.Chan return nil } - hchan := Srv.Store.Webhook().GetOutgoingByTeam(team.Id) + hchan := Srv.Store.Webhook().GetOutgoingByTeam(team.Id, -1, -1) result := <-hchan if result.Err != nil { return result.Err @@ -186,6 +186,7 @@ func UpdateIncomingWebhook(oldHook, updatedHook *model.IncomingWebhook) (*model. return nil, model.NewAppError("UpdateIncomingWebhook", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented) } + updatedHook.Id = oldHook.Id updatedHook.UserId = oldHook.UserId updatedHook.CreateAt = oldHook.CreateAt updatedHook.UpdateAt = model.GetMillis() @@ -275,7 +276,7 @@ func CreateOutgoingWebhook(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, return nil, model.NewAppError("CreateOutgoingWebhook", "api.webhook.create_outgoing.triggers.app_error", nil, "", http.StatusBadRequest) } - if result := <-Srv.Store.Webhook().GetOutgoingByTeam(hook.TeamId); result.Err != nil { + if result := <-Srv.Store.Webhook().GetOutgoingByTeam(hook.TeamId, -1, -1); result.Err != nil { return nil, result.Err } else { allHooks := result.Data.([]*model.OutgoingWebhook) @@ -320,7 +321,7 @@ func UpdateOutgoingWebhook(oldHook, updatedHook *model.OutgoingWebhook) (*model. } var result store.StoreResult - if result = <-Srv.Store.Webhook().GetOutgoingByTeam(oldHook.TeamId); result.Err != nil { + if result = <-Srv.Store.Webhook().GetOutgoingByTeam(oldHook.TeamId, -1, -1); result.Err != nil { return nil, result.Err } @@ -360,12 +361,36 @@ func GetOutgoingWebhook(hookId string) (*model.OutgoingWebhook, *model.AppError) } } +func GetOutgoingWebhooksPage(page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { + return nil, model.NewAppError("GetOutgoingWebhooksPage", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + if result := <-Srv.Store.Webhook().GetOutgoingList(page*perPage, perPage); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.OutgoingWebhook), nil + } +} + +func GetOutgoingWebhooksForChannelPage(channelId string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) { + if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { + return nil, model.NewAppError("GetOutgoingWebhooksForChannelPage", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + if result := <-Srv.Store.Webhook().GetOutgoingByChannel(channelId, page*perPage, perPage); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.OutgoingWebhook), nil + } +} + func GetOutgoingWebhooksForTeamPage(teamId string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) { if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { return nil, model.NewAppError("GetOutgoingWebhooksForTeamPage", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented) } - if result := <-Srv.Store.Webhook().GetOutgoingByTeam(teamId); result.Err != nil { + if result := <-Srv.Store.Webhook().GetOutgoingByTeam(teamId, page*perPage, perPage); result.Err != nil { return nil, result.Err } else { return result.Data.([]*model.OutgoingWebhook), nil |