diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/admin.go | 10 | ||||
-rw-r--r-- | app/app.go | 18 | ||||
-rw-r--r-- | app/apptestlib.go | 24 | ||||
-rw-r--r-- | app/authorization.go | 12 | ||||
-rw-r--r-- | app/auto_constants.go | 22 | ||||
-rw-r--r-- | app/auto_posts.go | 15 | ||||
-rw-r--r-- | app/channel.go | 45 | ||||
-rw-r--r-- | app/channel_test.go | 69 | ||||
-rw-r--r-- | app/command_expand_collapse.go | 4 | ||||
-rw-r--r-- | app/config.go | 15 | ||||
-rw-r--r-- | app/config_test.go | 10 | ||||
-rw-r--r-- | app/diagnostics.go | 14 | ||||
-rw-r--r-- | app/emoji.go | 1 | ||||
-rw-r--r-- | app/login.go | 14 | ||||
-rw-r--r-- | app/notification.go | 13 | ||||
-rw-r--r-- | app/plugin.go | 33 | ||||
-rw-r--r-- | app/plugin_test.go | 25 | ||||
-rw-r--r-- | app/post.go | 18 | ||||
-rw-r--r-- | app/preference.go | 8 | ||||
-rw-r--r-- | app/server.go | 47 | ||||
-rw-r--r-- | app/server_test.go | 2 | ||||
-rw-r--r-- | app/session.go | 3 | ||||
-rw-r--r-- | app/slackimport.go | 12 | ||||
-rw-r--r-- | app/status.go | 44 | ||||
-rw-r--r-- | app/status_test.go | 40 | ||||
-rw-r--r-- | app/team.go | 99 | ||||
-rw-r--r-- | app/user.go | 31 | ||||
-rw-r--r-- | app/user_test.go | 129 | ||||
-rw-r--r-- | app/webhook.go | 2 | ||||
-rw-r--r-- | app/webhook_test.go | 58 |
30 files changed, 603 insertions, 234 deletions
diff --git a/app/admin.go b/app/admin.go index 154fa8899..22928390e 100644 --- a/app/admin.go +++ b/app/admin.go @@ -15,7 +15,6 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/store/sqlstore" "github.com/mattermost/mattermost-server/utils" ) @@ -141,10 +140,11 @@ func (a *App) InvalidateAllCachesSkipSend() { l4g.Info(utils.T("api.context.invalidate_all_caches")) a.sessionCache.Purge() ClearStatusCache() - sqlstore.ClearChannelCaches() - sqlstore.ClearUserCaches() - sqlstore.ClearPostCaches() - sqlstore.ClearWebhookCaches() + a.Srv.Store.Channel().ClearCaches() + a.Srv.Store.User().ClearCaches() + a.Srv.Store.Post().ClearCaches() + a.Srv.Store.FileInfo().ClearCaches() + a.Srv.Store.Webhook().ClearCaches() a.LoadLicense() } diff --git a/app/app.go b/app/app.go index 26aed4c73..f5e5dd21e 100644 --- a/app/app.go +++ b/app/app.go @@ -131,8 +131,24 @@ func New(options ...Option) (outApp *App, outErr error) { app.configListenerId = app.AddConfigListener(func(_, _ *model.Config) { app.configOrLicenseListener() + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CONFIG_CHANGED, "", "", "", nil) + + message.Add("config", app.ClientConfigWithNoAccounts()) + app.Go(func() { + app.Publish(message) + }) + }) + app.licenseListenerId = app.AddLicenseListener(func() { + app.configOrLicenseListener() + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LICENSE_CHANGED, "", "", "", nil) + message.Add("license", app.GetSanitizedClientLicense()) + app.Go(func() { + app.Publish(message) + }) + }) - app.licenseListenerId = app.AddLicenseListener(app.configOrLicenseListener) app.regenerateClientConfig() app.setDefaultRolesBasedOnConfig() diff --git a/app/apptestlib.go b/app/apptestlib.go index c7846c9b5..01f5b0102 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -135,10 +135,6 @@ func (me *TestHelper) InitBasic() *TestHelper { return me } -func (me *TestHelper) MakeUsername() string { - return "un_" + model.NewId() -} - func (me *TestHelper) MakeEmail() string { return "success_" + model.NewId() + "@simulator.amazonses.com" } @@ -191,10 +187,6 @@ func (me *TestHelper) CreateChannel(team *model.Team) *model.Channel { return me.createChannel(team, model.CHANNEL_OPEN) } -func (me *TestHelper) CreatePrivateChannel(team *model.Team) *model.Channel { - return me.createChannel(team, model.CHANNEL_PRIVATE) -} - func (me *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel { id := model.NewId() @@ -253,6 +245,22 @@ func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) { utils.EnableDebugLogForTest() } +func (me *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember { + utils.DisableDebugLogForTest() + + member, err := me.App.AddUserToChannel(user, channel) + if err != nil { + l4g.Error(err.Error()) + l4g.Close() + time.Sleep(time.Second) + panic(err) + } + + utils.EnableDebugLogForTest() + + return member +} + func (me *TestHelper) TearDown() { me.App.Shutdown() os.Remove(me.tempConfigPath) diff --git a/app/authorization.go b/app/authorization.go index 3a64bb717..4231cac77 100644 --- a/app/authorization.go +++ b/app/authorization.go @@ -181,18 +181,6 @@ func (a *App) HasPermissionToChannelByPost(askingUserId string, postId string, p return a.HasPermissionTo(askingUserId, permission) } -func (a *App) HasPermissionToUser(askingUserId string, userId string) bool { - if askingUserId == userId { - return true - } - - if a.HasPermissionTo(askingUserId, model.PERMISSION_EDIT_OTHER_USERS) { - return true - } - - return false -} - func (a *App) CheckIfRolesGrantPermission(roles []string, permissionId string) bool { for _, roleId := range roles { if role := a.Role(roleId); role == nil { diff --git a/app/auto_constants.go b/app/auto_constants.go index c52eb6243..520d4e363 100644 --- a/app/auto_constants.go +++ b/app/auto_constants.go @@ -9,16 +9,15 @@ import ( ) 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" + USER_PASSWORD = "passwd" + CHANNEL_TYPE = model.CHANNEL_OPEN + 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 ( @@ -29,8 +28,5 @@ var ( 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_posts.go b/app/auto_posts.go index 6d1e352e5..379c74ab7 100644 --- a/app/auto_posts.go +++ b/app/auto_posts.go @@ -90,18 +90,3 @@ func (cfg *AutoPostCreator) CreateRandomPost() (*model.Post, bool) { } 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 { - return posts, false - } - } - - return posts, true -} diff --git a/app/channel.go b/app/channel.go index 8ac1f421c..4e294abbb 100644 --- a/app/channel.go +++ b/app/channel.go @@ -225,6 +225,14 @@ func (a *App) createDirectChannel(userId string, otherUserId string) (*model.Cha } } else { channel := result.Data.(*model.Channel) + + if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(userId, channel.Id, model.GetMillis()); result.Err != nil { + l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err) + } + if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(otherUserId, channel.Id, model.GetMillis()); result.Err != nil { + l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err) + } + return channel, nil } } @@ -369,7 +377,7 @@ func (a *App) postChannelPrivacyMessage(user *model.User, channel *model.Channel })[channel.Type] post := &model.Post{ ChannelId: channel.Id, - Message: fmt.Sprintf(utils.T("api.channel.change_channel_privacy." + privacy)), + Message: utils.T("api.channel.change_channel_privacy." + privacy), Type: model.POST_CHANGE_CHANNEL_PRIVACY, UserId: user.Id, Props: model.StringInterface{ @@ -545,7 +553,6 @@ func (a *App) DeleteChannel(channel *model.Channel, userId string) *model.AppErr message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_DELETED, channel.TeamId, "", "", nil) message.Add("channel_id", channel.Id) - a.Publish(message) } @@ -1055,7 +1062,7 @@ func (a *App) LeaveChannel(channelId string, userId string) *model.AppError { return err } - if channel.Name == model.DEFAULT_CHANNEL && *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages == false { + if channel.Name == model.DEFAULT_CHANNEL && !*a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages { return nil } @@ -1093,7 +1100,9 @@ func (a *App) PostAddToChannelMessage(user *model.User, addedUser *model.User, c UserId: user.Id, RootId: postRootId, Props: model.StringInterface{ + "userId": user.Id, "username": user.Username, + "addedUserId": addedUser.Id, "addedUsername": addedUser.Username, }, } @@ -1113,7 +1122,9 @@ func (a *App) postAddToTeamMessage(user *model.User, addedUser *model.User, chan UserId: user.Id, RootId: postRootId, Props: model.StringInterface{ + "userId": user.Id, "username": user.Username, + "addedUserId": addedUser.Id, "addedUsername": addedUser.Username, }, } @@ -1132,6 +1143,7 @@ func (a *App) postRemoveFromChannelMessage(removerUserId string, removedUser *mo Type: model.POST_REMOVE_FROM_CHANNEL, UserId: removerUserId, Props: model.StringInterface{ + "removedUserId": removedUser.Id, "removedUsername": removedUser.Username, }, } @@ -1166,17 +1178,13 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string, message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil) message.Add("user_id", userIdToRemove) message.Add("remover_id", removerUserId) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) // because the removed user no longer belongs to the channel we need to send a separate websocket event userMsg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", "", userIdToRemove, nil) userMsg.Add("channel_id", channel.Id) userMsg.Add("remover_id", removerUserId) - a.Go(func() { - a.Publish(userMsg) - }) + a.Publish(userMsg) return nil } @@ -1246,9 +1254,7 @@ func (a *App) UpdateChannelLastViewedAt(channelIds []string, userId string) *mod for _, channelId := range channelIds { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, "", "", userId, nil) message.Add("channel_id", channelId) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } } @@ -1325,9 +1331,7 @@ func (a *App) ViewChannel(view *model.ChannelView, userId string, clearPushNotif if *a.Config().ServiceSettings.EnableChannelViewedMessages && model.IsValidId(view.ChannelId) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, "", "", userId, nil) message.Add("channel_id", view.ChannelId) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } return times, nil @@ -1430,7 +1434,16 @@ func (a *App) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model. } a.InvalidateCacheForUser(userId1) a.InvalidateCacheForUser(userId2) - return result.Data.(*model.Channel), nil + + channel := result.Data.(*model.Channel) + if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(userId1, channel.Id, model.GetMillis()); result.Err != nil { + l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err) + } + if result := <-a.Srv.Store.ChannelMemberHistory().LogJoinEvent(userId2, channel.Id, model.GetMillis()); result.Err != nil { + l4g.Warn("Failed to update ChannelMemberHistory table %v", result.Err) + } + + return channel, nil } else if result.Err != nil { return nil, model.NewAppError("GetOrCreateDMChannel", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message, result.Err.StatusCode) } diff --git a/app/channel_test.go b/app/channel_test.go index e4a0e4320..69efaeca7 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -110,7 +110,7 @@ func TestMoveChannel(t *testing.T) { } } -func TestJoinDefaultChannelsTownSquare(t *testing.T) { +func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordTownSquare(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -136,7 +136,7 @@ func TestJoinDefaultChannelsTownSquare(t *testing.T) { assert.True(t, found) } -func TestJoinDefaultChannelsOffTopic(t *testing.T) { +func TestJoinDefaultChannelsCreatesChannelMemberHistoryRecordOffTopic(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -162,7 +162,7 @@ func TestJoinDefaultChannelsOffTopic(t *testing.T) { assert.True(t, found) } -func TestCreateChannelPublic(t *testing.T) { +func TestCreateChannelPublicCreatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -176,7 +176,7 @@ func TestCreateChannelPublic(t *testing.T) { assert.Equal(t, publicChannel.Id, histories[0].ChannelId) } -func TestCreateChannelPrivate(t *testing.T) { +func TestCreateChannelPrivateCreatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -205,7 +205,7 @@ func TestUpdateChannelPrivacy(t *testing.T) { } } -func TestCreateGroupChannel(t *testing.T) { +func TestCreateGroupChannelCreatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -233,7 +233,62 @@ func TestCreateGroupChannel(t *testing.T) { } } -func TestAddUserToChannel(t *testing.T) { +func TestCreateDirectChannelCreatesChannelMemberHistoryRecord(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user1 := th.CreateUser() + user2 := th.CreateUser() + + if channel, err := th.App.CreateDirectChannel(user1.Id, user2.Id); err != nil { + t.Fatal("Failed to create direct channel. Error: " + err.Message) + } else { + // there should be a ChannelMemberHistory record for both users + histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)).([]*model.ChannelMemberHistoryResult) + assert.Len(t, histories, 2) + + historyId0 := histories[0].UserId + historyId1 := histories[1].UserId + switch historyId0 { + case user1.Id: + assert.Equal(t, user2.Id, historyId1) + case user2.Id: + assert.Equal(t, user1.Id, historyId1) + default: + t.Fatal("Unexpected user id " + historyId0 + " in ChannelMemberHistory table") + } + } +} + +func TestGetDirectChannelCreatesChannelMemberHistoryRecord(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user1 := th.CreateUser() + user2 := th.CreateUser() + + // this function call implicitly creates a direct channel between the two users if one doesn't already exist + if channel, err := th.App.GetDirectChannel(user1.Id, user2.Id); err != nil { + t.Fatal("Failed to create direct channel. Error: " + err.Message) + } else { + // there should be a ChannelMemberHistory record for both users + histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)).([]*model.ChannelMemberHistoryResult) + assert.Len(t, histories, 2) + + historyId0 := histories[0].UserId + historyId1 := histories[1].UserId + switch historyId0 { + case user1.Id: + assert.Equal(t, user2.Id, historyId1) + case user2.Id: + assert.Equal(t, user1.Id, historyId1) + default: + t.Fatal("Unexpected user id " + historyId0 + " in ChannelMemberHistory table") + } + } +} + +func TestAddUserToChannelCreatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -263,7 +318,7 @@ func TestAddUserToChannel(t *testing.T) { assert.Equal(t, groupUserIds, channelMemberHistoryUserIds) } -func TestRemoveUserFromChannel(t *testing.T) { +func TestRemoveUserFromChannelUpdatesChannelMemberHistoryRecord(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/command_expand_collapse.go b/app/command_expand_collapse.go index a8eb3bc1f..638490c6c 100644 --- a/app/command_expand_collapse.go +++ b/app/command_expand_collapse.go @@ -74,9 +74,7 @@ func (a *App) setCollapsePreference(args *model.CommandArgs, isCollapse bool) *m socketMessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", args.UserId, nil) socketMessage.Add("preference", pref.ToJson()) - a.Go(func() { - a.Publish(socketMessage) - }) + a.Publish(socketMessage) var rmsg string diff --git a/app/config.go b/app/config.go index 35a0c9a3f..460d580d8 100644 --- a/app/config.go +++ b/app/config.go @@ -14,6 +14,7 @@ import ( "fmt" "net/url" "runtime/debug" + "strconv" "strings" l4g "github.com/alecthomas/log4go" @@ -34,6 +35,7 @@ func (a *App) UpdateConfig(f func(*model.Config)) { updated := old.Clone() f(updated) a.config.Store(updated) + a.InvokeConfigListeners(old, updated) } @@ -269,3 +271,16 @@ func (a *App) GetCookieDomain() string { func (a *App) GetSiteURL() string { return a.siteURL } + +// ClientConfigWithNoAccounts gets the configuration in a format suitable for sending to the client. +func (a *App) ClientConfigWithNoAccounts() map[string]string { + respCfg := map[string]string{} + for k, v := range a.ClientConfig() { + respCfg[k] = v + } + + // NoAccounts is not actually part of the configuration, but is expected by the client. + respCfg["NoAccounts"] = strconv.FormatBool(a.IsFirstUserAccount()) + + return respCfg +} diff --git a/app/config_test.go b/app/config_test.go index 5ee999f0f..051fa8fd8 100644 --- a/app/config_test.go +++ b/app/config_test.go @@ -63,3 +63,13 @@ func TestAsymmetricSigningKey(t *testing.T) { assert.NotNil(t, th.App.AsymmetricSigningKey()) assert.NotEmpty(t, th.App.ClientConfig()["AsymmetricSigningPublicKey"]) } + +func TestClientConfigWithNoAccounts(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + config := th.App.ClientConfigWithNoAccounts() + if _, ok := config["NoAccounts"]; !ok { + t.Fatal("expected NoAccounts in returned config") + } +} diff --git a/app/diagnostics.go b/app/diagnostics.go index 12553afc8..4cff5f02a 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -502,11 +502,15 @@ func (a *App) trackConfig() { }) a.SendDiagnostic(TRACK_CONFIG_MESSAGE_EXPORT, map[string]interface{}{ - "enable_message_export": *cfg.MessageExportSettings.EnableExport, - "export_format": *cfg.MessageExportSettings.ExportFormat, - "daily_run_time": *cfg.MessageExportSettings.DailyRunTime, - "default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp, - "batch_size": *cfg.MessageExportSettings.BatchSize, + "enable_message_export": *cfg.MessageExportSettings.EnableExport, + "export_format": *cfg.MessageExportSettings.ExportFormat, + "daily_run_time": *cfg.MessageExportSettings.DailyRunTime, + "default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp, + "batch_size": *cfg.MessageExportSettings.BatchSize, + "global_relay_customer_type": *cfg.MessageExportSettings.GlobalRelaySettings.CustomerType, + "is_default_global_relay_smtp_username": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SmtpUsername, ""), + "is_default_global_relay_smtp_password": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.SmtpPassword, ""), + "is_default_global_relay_email_address": isDefault(*cfg.MessageExportSettings.GlobalRelaySettings.EmailAddress, ""), }) } diff --git a/app/emoji.go b/app/emoji.go index 20d4bb44d..eebe59ccf 100644 --- a/app/emoji.go +++ b/app/emoji.go @@ -60,7 +60,6 @@ func (a *App) CreateEmoji(sessionUserId string, emoji *model.Emoji, multiPartIma } else { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EMOJI_ADDED, "", "", "", nil) message.Add("emoji", emoji.ToJson()) - a.Publish(message) return result.Data.(*model.Emoji), nil } diff --git a/app/login.go b/app/login.go index e01566bcd..43b022749 100644 --- a/app/login.go +++ b/app/login.go @@ -9,8 +9,8 @@ import ( "strings" "time" + "github.com/avct/uasurfer" "github.com/mattermost/mattermost-server/model" - "github.com/mssola/user_agent" ) func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId string, ldapOnly bool) (*model.User, *model.AppError) { @@ -71,19 +71,19 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, session.SetExpireInDays(*a.Config().ServiceSettings.SessionLengthWebInDays) } - ua := user_agent.New(r.UserAgent()) + ua := uasurfer.Parse(r.UserAgent()) - plat := ua.Platform() + plat := ua.OS.Platform.String() if plat == "" { plat = "unknown" } - os := ua.OS() + os := ua.OS.Name.String() if os == "" { os = "unknown" } - bname, bversion := ua.Browser() + bname := ua.Browser.Name.String() if bname == "" { bname = "unknown" } @@ -92,9 +92,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, bname = "Desktop App" } - if bversion == "" { - bversion = "0.0" - } + bversion := ua.Browser.Version session.AddProp(model.SESSION_PROP_PLATFORM, plat) session.AddProp(model.SESSION_PROP_OS, os) diff --git a/app/notification.go b/app/notification.go index 9a8096bbf..bb0c8703f 100644 --- a/app/notification.go +++ b/app/notification.go @@ -55,10 +55,15 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod if channel.Type == model.CHANNEL_DIRECT { var otherUserId string - if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { - otherUserId = userIds[1] - } else { - otherUserId = userIds[0] + + userIds := strings.Split(channel.Name, "__") + + if userIds[0] != userIds[1] { + if userIds[0] == post.UserId { + otherUserId = userIds[1] + } else { + otherUserId = userIds[0] + } } if _, ok := profileMap[otherUserId]; ok { diff --git a/app/plugin.go b/app/plugin.go index fe671d26a..6702e9227 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -91,18 +91,11 @@ func (a *App) ActivatePlugins() { active := a.PluginEnv.IsPluginActive(id) if pluginState.Enable && !active { - if err := a.PluginEnv.ActivatePlugin(id); err != nil { - l4g.Error(err.Error()) + if err := a.activatePlugin(plugin.Manifest); err != nil { + l4g.Error("%v plugin enabled in config.json but failing to activate err=%v", plugin.Manifest.Id, err.DetailedError) continue } - if plugin.Manifest.HasClient() { - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil) - message.Add("manifest", plugin.Manifest.ClientManifest()) - a.Publish(message) - } - - l4g.Info("Activated %v plugin", id) } else if !pluginState.Enable && active { if err := a.deactivatePlugin(plugin.Manifest); err != nil { l4g.Error(err.Error()) @@ -111,6 +104,21 @@ func (a *App) ActivatePlugins() { } } +func (a *App) activatePlugin(manifest *model.Manifest) *model.AppError { + if err := a.PluginEnv.ActivatePlugin(manifest.Id); err != nil { + return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if manifest.HasClient() { + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil) + message.Add("manifest", manifest.ClientManifest()) + a.Publish(message) + } + + l4g.Info("Activated %v plugin", manifest.Id) + return nil +} + func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError { if err := a.PluginEnv.DeactivatePlugin(manifest.Id); err != nil { return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) @@ -301,11 +309,18 @@ func (a *App) EnablePlugin(id string) *model.AppError { return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest) } + if err := a.activatePlugin(manifest); err != nil { + return err + } + a.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true} }) if err := a.SaveConfig(a.Config(), true); err != nil { + if err.Id == "ent.cluster.save_config.error" { + return model.NewAppError("EnablePlugin", "app.plugin.cluster.save_config.app_error", nil, "", http.StatusInternalServerError) + } return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) } diff --git a/app/plugin_test.go b/app/plugin_test.go index 4794d2704..9ad5dc1fa 100644 --- a/app/plugin_test.go +++ b/app/plugin_test.go @@ -4,8 +4,10 @@ package app import ( + "errors" "net/http" "net/http/httptest" + "strings" "testing" "github.com/gorilla/mux" @@ -195,3 +197,26 @@ func TestPluginCommands(t *testing.T) { require.NotNil(t, err) assert.Equal(t, http.StatusNotFound, err.StatusCode) } + +type pluginBadActivation struct { + testPlugin +} + +func (p *pluginBadActivation) OnActivate(api plugin.API) error { + return errors.New("won't activate for some reason") +} + +func TestPluginBadActivation(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.InstallPlugin(&model.Manifest{ + Id: "foo", + }, &pluginBadActivation{}) + + t.Run("EnablePlugin bad activation", func(t *testing.T) { + err := th.App.EnablePlugin("foo") + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.DetailedError, "won't activate for some reason")) + }) +} diff --git a/app/post.go b/app/post.go index a541797fa..5067777ab 100644 --- a/app/post.go +++ b/app/post.go @@ -84,9 +84,7 @@ func (a *App) CreatePostAsUser(post *model.Post) (*model.Post, *model.AppError) if *a.Config().ServiceSettings.EnableChannelViewedMessages { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, "", "", post.UserId, nil) message.Add("channel_id", post.ChannelId) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } } @@ -314,10 +312,7 @@ func (a *App) SendEphemeralPost(userId string, post *model.Post) *model.Post { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil) message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) - - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) return post } @@ -424,10 +419,7 @@ func (a *App) PatchPost(postId string, patch *model.PostPatch) (*model.Post, *mo func (a *App) sendUpdatedPostEvent(post *model.Post) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, "", nil) message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) - - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } func (a *App) GetPostsPage(channelId string, page int, perPage int) (*model.PostList, *model.AppError) { @@ -567,11 +559,9 @@ func (a *App) DeletePost(postId string) (*model.Post, *model.AppError) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil) message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) + a.Publish(message) a.Go(func() { - a.Publish(message) - }) - a.Go(func() { a.DeletePostFiles(post) }) a.Go(func() { diff --git a/app/preference.go b/app/preference.go index 9ca1f474c..eb41992da 100644 --- a/app/preference.go +++ b/app/preference.go @@ -55,9 +55,7 @@ func (a *App) UpdatePreferences(userId string, preferences model.Preferences) *m message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCES_CHANGED, "", "", userId, nil) message.Add("preferences", preferences.ToJson()) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) return nil } @@ -80,9 +78,7 @@ func (a *App) DeletePreferences(userId string, preferences model.Preferences) *m message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCES_DELETED, "", "", userId, nil) message.Add("preferences", preferences.ToJson()) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) return nil } diff --git a/app/server.go b/app/server.go index 93804a372..0c6c25ba5 100644 --- a/app/server.go +++ b/app/server.go @@ -84,28 +84,6 @@ func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second -type VaryBy struct { - useIP bool - useAuth bool -} - -func (m *VaryBy) Key(r *http.Request) string { - key := "" - - if m.useAuth { - token, tokenLocation := ParseAuthTokenFromRequest(r) - if tokenLocation != TokenLocationNotFound { - key += token - } else if m.useIP { // If we don't find an authentication token and IP based is enabled, fall back to IP - key += utils.GetIpAddress(r) - } - } else if m.useIP { // Only if Auth based is not enabed do we use a plain IP based - key = utils.GetIpAddress(r) - } - - return key -} - func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) { if r.Host == "" { http.Error(w, "Not Found", http.StatusNotFound) @@ -223,31 +201,6 @@ func (a *App) StartServer() error { return nil } -type tcpKeepAliveListener struct { - *net.TCPListener -} - -func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { - tc, err := ln.AcceptTCP() - if err != nil { - return - } - tc.SetKeepAlive(true) - tc.SetKeepAlivePeriod(3 * time.Minute) - return tc, nil -} - -func (a *App) Listen(addr string) (net.Listener, error) { - if addr == "" { - addr = ":http" - } - ln, err := net.Listen("tcp", addr) - if err != nil { - return nil, err - } - return tcpKeepAliveListener{ln.(*net.TCPListener)}, nil -} - func (a *App) StopServer() { if a.Srv.Server != nil { ctx, cancel := context.WithTimeout(context.Background(), TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN) diff --git a/app/server_test.go b/app/server_test.go index de358b976..94771a44e 100644 --- a/app/server_test.go +++ b/app/server_test.go @@ -26,7 +26,7 @@ func TestStartServerRateLimiterCriticalError(t *testing.T) { // Attempt to use Rate Limiter with an invalid config a.UpdateConfig(func(cfg *model.Config) { - *cfg.RateLimitSettings.Enable = true + *cfg.RateLimitSettings.Enable = true *cfg.RateLimitSettings.MaxBurst = -100 }) diff --git a/app/session.go b/app/session.go index 459618439..88f52477f 100644 --- a/app/session.go +++ b/app/session.go @@ -138,6 +138,9 @@ func (a *App) ClearSessionCacheForUserSkipClusterSend(userId string) { session := ts.(*model.Session) if session.UserId == userId { a.sessionCache.Remove(key) + if a.Metrics != nil { + a.Metrics.IncrementMemCacheInvalidationCounterSession() + } } } } diff --git a/app/slackimport.go b/app/slackimport.go index 9d1b4cf9c..ed522671a 100644 --- a/app/slackimport.go +++ b/app/slackimport.go @@ -109,13 +109,11 @@ func SlackParseUsers(data io.Reader) ([]SlackUser, error) { decoder := json.NewDecoder(data) var users []SlackUser - if err := decoder.Decode(&users); err != nil { - // This actually returns errors that are ignored. - // In this case it is erroring because of a null that Slack - // introduced. So we just return the users here. - return users, err - } - return users, nil + err := decoder.Decode(&users) + // This actually returns errors that are ignored. + // In this case it is erroring because of a null that Slack + // introduced. So we just return the users here. + return users, err } func SlackParsePosts(data io.Reader) ([]SlackPost, error) { diff --git a/app/status.go b/app/status.go index 1ef7aef0f..c8bff0d1a 100644 --- a/app/status.go +++ b/app/status.go @@ -221,9 +221,7 @@ func (a *App) BroadcastStatus(status *model.Status) { event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) event.Add("status", status.Status) event.Add("user_id", status.UserId) - a.Go(func() { - a.Publish(event) - }) + a.Publish(event) } func (a *App) SetStatusOffline(userId string, manual bool) { @@ -238,18 +236,7 @@ func (a *App) SetStatusOffline(userId string, manual bool) { status = &model.Status{UserId: userId, Status: model.STATUS_OFFLINE, Manual: manual, LastActivityAt: model.GetMillis(), ActiveChannel: ""} - a.AddStatusCache(status) - - if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) - } - - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) - event.Add("status", model.STATUS_OFFLINE) - event.Add("user_id", status.UserId) - a.Go(func() { - a.Publish(event) - }) + a.SaveAndBroadcastStatus(status) } func (a *App) SetStatusAwayIfNeeded(userId string, manual bool) { @@ -281,18 +268,7 @@ func (a *App) SetStatusAwayIfNeeded(userId string, manual bool) { status.Manual = manual status.ActiveChannel = "" - a.AddStatusCache(status) - - if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) - } - - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) - event.Add("status", model.STATUS_AWAY) - event.Add("user_id", status.UserId) - a.Go(func() { - a.Publish(event) - }) + a.SaveAndBroadcastStatus(status) } func (a *App) SetStatusDoNotDisturb(userId string) { @@ -309,18 +285,22 @@ func (a *App) SetStatusDoNotDisturb(userId string) { status.Status = model.STATUS_DND status.Manual = true + a.SaveAndBroadcastStatus(status) +} + +func (a *App) SaveAndBroadcastStatus(status *model.Status) *model.AppError { a.AddStatusCache(status) if result := <-a.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) + l4g.Error(utils.T("api.status.save_status.error"), status.UserId, result.Err) } event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) - event.Add("status", model.STATUS_DND) + event.Add("status", status.Status) event.Add("user_id", status.UserId) - a.Go(func() { - a.Publish(event) - }) + a.Publish(event) + + return nil } func GetStatusFromCache(userId string) *model.Status { diff --git a/app/status_test.go b/app/status_test.go new file mode 100644 index 000000000..bf5736a48 --- /dev/null +++ b/app/status_test.go @@ -0,0 +1,40 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + + "github.com/mattermost/mattermost-server/model" +) + +func TestSaveStatus(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user := th.BasicUser + + for _, statusString := range []string{ + model.STATUS_ONLINE, + model.STATUS_AWAY, + model.STATUS_DND, + model.STATUS_OFFLINE, + } { + t.Run(statusString, func(t *testing.T) { + status := &model.Status{ + UserId: user.Id, + Status: statusString, + } + + th.App.SaveAndBroadcastStatus(status) + + after, err := th.App.GetStatus(user.Id) + if err != nil { + t.Fatalf("failed to get status after save: %v", err) + } else if after.Status != statusString { + t.Fatalf("failed to save status, got %v, expected %v", after.Status, statusString) + } + }) + } +} diff --git a/app/team.go b/app/team.go index d8750bfbb..239ce4369 100644 --- a/app/team.go +++ b/app/team.go @@ -4,13 +4,18 @@ package app import ( + "bytes" "fmt" + "image" + "image/png" + "mime/multipart" "net/http" "net/url" "strconv" "strings" l4g "github.com/alecthomas/log4go" + "github.com/disintegration/imaging" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" @@ -134,9 +139,7 @@ func (a *App) sendTeamEvent(team *model.Team, event string) { message := model.NewWebSocketEvent(event, "", "", "", nil) message.Add("team", sanitizedTeam.ToJson()) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } func (a *App) UpdateTeamMemberRoles(teamId string, userId string, newRoles string) (*model.TeamMember, *model.AppError) { @@ -173,10 +176,7 @@ func (a *App) UpdateTeamMemberRoles(teamId string, userId string, newRoles strin func (a *App) sendUpdatedMemberRoleEvent(userId string, member *model.TeamMember) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_MEMBERROLE_UPDATED, "", "", userId, nil) message.Add("member", member.ToJson()) - - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } func (a *App) AddUserToTeam(teamId string, userId string, userRequestorId string) (*model.Team, *model.AppError) { @@ -919,3 +919,88 @@ func (a *App) SanitizeTeams(session model.Session, teams []*model.Team) []*model return teams } + +func (a *App) GetTeamIcon(team *model.Team) ([]byte, *model.AppError) { + if len(*a.Config().FileSettings.DriverName) == 0 { + return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.filesettings_no_driver.app_error", nil, "", http.StatusNotImplemented) + } else { + path := "teams/" + team.Id + "/teamIcon.png" + if data, err := a.ReadFile(path); err != nil { + return nil, model.NewAppError("GetTeamIcon", "api.team.get_team_icon.read_file.app_error", nil, err.Error(), http.StatusNotFound) + } else { + return data, nil + } + } +} + +func (a *App) SetTeamIcon(teamId string, imageData *multipart.FileHeader) *model.AppError { + file, err := imageData.Open() + if err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.open.app_error", nil, err.Error(), http.StatusBadRequest) + } + defer file.Close() + return a.SetTeamIconFromFile(teamId, file) +} + +func (a *App) SetTeamIconFromFile(teamId string, file multipart.File) *model.AppError { + + team, getTeamErr := a.GetTeam(teamId) + + if getTeamErr != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.get_team.app_error", nil, getTeamErr.Error(), http.StatusBadRequest) + } + + if len(*a.Config().FileSettings.DriverName) == 0 { + return model.NewAppError("setTeamIcon", "api.team.set_team_icon.storage.app_error", nil, "", http.StatusNotImplemented) + } + + // Decode image config first to check dimensions before loading the whole thing into memory later on + config, _, err := image.DecodeConfig(file) + if err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode_config.app_error", nil, err.Error(), http.StatusBadRequest) + } else if config.Width*config.Height > model.MaxImageSize { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.too_large.app_error", nil, err.Error(), http.StatusBadRequest) + } + + file.Seek(0, 0) + + // Decode image into Image object + img, _, err := image.Decode(file) + if err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.decode.app_error", nil, err.Error(), http.StatusBadRequest) + } + + file.Seek(0, 0) + + orientation, _ := getImageOrientation(file) + img = makeImageUpright(img, orientation) + + // Scale team icon + teamIconWidthAndHeight := 128 + img = imaging.Fill(img, teamIconWidthAndHeight, teamIconWidthAndHeight, imaging.Center, imaging.Lanczos) + + buf := new(bytes.Buffer) + err = png.Encode(buf, img) + if err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.encode.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + path := "teams/" + teamId + "/teamIcon.png" + + if err := a.WriteFile(buf.Bytes(), path); err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.write_file.app_error", nil, "", http.StatusInternalServerError) + } + + curTime := model.GetMillis() + + if result := <-a.Srv.Store.Team().UpdateLastTeamIconUpdate(teamId, curTime); result.Err != nil { + return model.NewAppError("SetTeamIcon", "api.team.set_team_icon.update.app_error", nil, result.Err.Error(), http.StatusBadRequest) + } + + // manually set time to avoid possible cluster inconsistencies + team.LastTeamIconUpdate = curTime + + a.sendTeamEvent(team, model.WEBSOCKET_EVENT_UPDATE_TEAM) + + return nil +} diff --git a/app/user.go b/app/user.go index f915f35cb..7a6dc0b49 100644 --- a/app/user.go +++ b/app/user.go @@ -34,7 +34,6 @@ const ( TOKEN_TYPE_PASSWORD_RECOVERY = "password_recovery" TOKEN_TYPE_VERIFY_EMAIL = "verify_email" PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour - VERIFY_EMAIL_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour IMAGE_PROFILE_PIXEL_DIMENSION = 128 ) @@ -202,9 +201,7 @@ func (a *App) CreateUser(user *model.User) (*model.User, *model.AppError) { // This message goes to everyone, so the teamId, channelId and userId are irrelevant message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil) message.Add("user_id", ruser.Id) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) return ruser, nil } @@ -508,6 +505,14 @@ func (a *App) GetUsersInChannel(channelId string, offset int, limit int) ([]*mod } } +func (a *App) GetUsersInChannelByStatus(channelId string, offset int, limit int) ([]*model.User, *model.AppError) { + if result := <-a.Srv.Store.User().GetProfilesInChannelByStatus(channelId, offset, limit); result.Err != nil { + return nil, result.Err + } else { + return result.Data.([]*model.User), nil + } +} + func (a *App) GetUsersInChannelMap(channelId string, offset int, limit int, asAdmin bool) (map[string]*model.User, *model.AppError) { users, err := a.GetUsersInChannel(channelId, offset, limit) if err != nil { @@ -533,6 +538,15 @@ func (a *App) GetUsersInChannelPage(channelId string, page int, perPage int, asA return a.sanitizeProfiles(users, asAdmin), nil } +func (a *App) GetUsersInChannelPageByStatus(channelId string, page int, perPage int, asAdmin bool) ([]*model.User, *model.AppError) { + users, err := a.GetUsersInChannelByStatus(channelId, page*perPage, perPage) + if err != nil { + return nil, err + } + + return a.sanitizeProfiles(users, asAdmin), nil +} + func (a *App) GetUsersNotInChannel(teamId string, channelId string, offset int, limit int) ([]*model.User, *model.AppError) { if result := <-a.Srv.Store.User().GetProfilesNotInChannel(teamId, channelId, offset, limit); result.Err != nil { return nil, result.Err @@ -832,7 +846,6 @@ func (a *App) SetProfileImageFromFile(userId string, file multipart.File) *model message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil) message.Add("user", user) - a.Publish(message) } @@ -901,10 +914,6 @@ func (a *App) UpdateActive(user *model.User, active bool) (*model.User, *model.A } } - if extra := <-a.Srv.Store.Channel().ExtraUpdateByUser(user.Id, model.GetMillis()); extra.Err != nil { - return nil, extra.Err - } - ruser := result.Data.([2]*model.User)[0] options := a.Config().GetSanitizeOptions() options["passwordupdate"] = false @@ -1002,9 +1011,7 @@ func (a *App) sendUpdatedUserEvent(user model.User, asAdmin bool) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", nil) message.Add("user", user) - a.Go(func() { - a.Publish(message) - }) + a.Publish(message) } func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) { diff --git a/app/user_test.go b/app/user_test.go index 38ff286b3..94052da61 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -299,3 +299,132 @@ func createGitlabUser(t *testing.T, a *App, email string, username string) (*mod return user, gitlabUserObj } + +func TestGetUsersByStatus(t *testing.T) { + th := Setup() + defer th.TearDown() + + team := th.CreateTeam() + channel, err := th.App.CreateChannel(&model.Channel{ + DisplayName: "dn_" + model.NewId(), + Name: "name_" + model.NewId(), + Type: model.CHANNEL_OPEN, + TeamId: team.Id, + CreatorId: model.NewId(), + }, false) + if err != nil { + t.Fatalf("failed to create channel: %v", err) + } + + createUserWithStatus := func(username string, status string) *model.User { + id := model.NewId() + + user, err := th.App.CreateUser(&model.User{ + Email: "success+" + id + "@simulator.amazonses.com", + Username: "un_" + username + "_" + id, + Nickname: "nn_" + id, + Password: "Password1", + }) + if err != nil { + t.Fatalf("failed to create user: %v", err) + } + + th.LinkUserToTeam(user, team) + th.AddUserToChannel(user, channel) + + th.App.SaveAndBroadcastStatus(&model.Status{ + UserId: user.Id, + Status: status, + Manual: true, + }) + + return user + } + + // Creating these out of order in case that affects results + awayUser1 := createUserWithStatus("away1", model.STATUS_AWAY) + awayUser2 := createUserWithStatus("away2", model.STATUS_AWAY) + dndUser1 := createUserWithStatus("dnd1", model.STATUS_DND) + dndUser2 := createUserWithStatus("dnd2", model.STATUS_DND) + offlineUser1 := createUserWithStatus("offline1", model.STATUS_OFFLINE) + offlineUser2 := createUserWithStatus("offline2", model.STATUS_OFFLINE) + onlineUser1 := createUserWithStatus("online1", model.STATUS_ONLINE) + onlineUser2 := createUserWithStatus("online2", model.STATUS_ONLINE) + + t.Run("sorting by status then alphabetical", func(t *testing.T) { + usersByStatus, err := th.App.GetUsersInChannelPageByStatus(channel.Id, 0, 8, true) + if err != nil { + t.Fatal(err) + } + + expectedUsersByStatus := []*model.User{ + onlineUser1, + onlineUser2, + awayUser1, + awayUser2, + dndUser1, + dndUser2, + offlineUser1, + offlineUser2, + } + + if len(usersByStatus) != len(expectedUsersByStatus) { + t.Fatalf("received only %v users, expected %v", len(usersByStatus), len(expectedUsersByStatus)) + } + + for i := range usersByStatus { + if usersByStatus[i].Id != expectedUsersByStatus[i].Id { + t.Fatalf("received user %v at index %v, expected %v", usersByStatus[i].Username, i, expectedUsersByStatus[i].Username) + } + } + }) + + t.Run("paging", func(t *testing.T) { + usersByStatus, err := th.App.GetUsersInChannelPageByStatus(channel.Id, 0, 3, true) + if err != nil { + t.Fatal(err) + } + + if len(usersByStatus) != 3 { + t.Fatal("received too many users") + } + + if usersByStatus[0].Id != onlineUser1.Id && usersByStatus[1].Id != onlineUser2.Id { + t.Fatal("expected to receive online users first") + } + + if usersByStatus[2].Id != awayUser1.Id { + t.Fatal("expected to receive away users second") + } + + usersByStatus, err = th.App.GetUsersInChannelPageByStatus(channel.Id, 1, 3, true) + if err != nil { + t.Fatal(err) + } + + if usersByStatus[0].Id != awayUser2.Id { + t.Fatal("expected to receive away users second") + } + + if usersByStatus[1].Id != dndUser1.Id && usersByStatus[2].Id != dndUser2.Id { + t.Fatal("expected to receive dnd users third") + } + + usersByStatus, err = th.App.GetUsersInChannelPageByStatus(channel.Id, 1, 4, true) + if err != nil { + t.Fatal(err) + } + + if len(usersByStatus) != 4 { + t.Fatal("received too many users") + } + + if usersByStatus[0].Id != dndUser1.Id && usersByStatus[1].Id != dndUser2.Id { + t.Fatal("expected to receive dnd users third") + } + + if usersByStatus[2].Id != offlineUser1.Id && usersByStatus[3].Id != offlineUser2.Id { + t.Fatal("expected to receive offline users last") + } + }) +} diff --git a/app/webhook.go b/app/webhook.go index f3777ab48..abfc388b5 100644 --- a/app/webhook.go +++ b/app/webhook.go @@ -225,7 +225,7 @@ func SplitWebhookPost(post *model.Post) ([]*model.Post, *model.AppError) { func (a *App) CreateWebhookPost(userId string, channel *model.Channel, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) { // parse links into Markdown format - linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) + linkWithTextRegex := regexp.MustCompile(`<([^\n<\|>]+)\|([^\n>]+)>`) text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") post := &model.Post{UserId: userId, ChannelId: channel.Id, Message: text, Type: postType, RootId: postRootId} diff --git a/app/webhook_test.go b/app/webhook_test.go index 850e74efc..4d2bc58fa 100644 --- a/app/webhook_test.go +++ b/app/webhook_test.go @@ -317,6 +317,64 @@ func TestCreateWebhookPost(t *testing.T) { if err == nil { t.Fatal("should have failed - bad post type") } + + expectedText := "`<>|<>|`" + post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{ + "attachments": []*model.SlackAttachment{ + { + Text: "text", + }, + }, + "webhook_display_name": hook.DisplayName, + }, model.POST_SLACK_ATTACHMENT, "") + if err != nil { + t.Fatal(err.Error()) + } + assert.Equal(t, expectedText, post.Message) + + expectedText = "< | \n|\n>" + post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{ + "attachments": []*model.SlackAttachment{ + { + Text: "text", + }, + }, + "webhook_display_name": hook.DisplayName, + }, model.POST_SLACK_ATTACHMENT, "") + if err != nil { + t.Fatal(err.Error()) + } + assert.Equal(t, expectedText, post.Message) + + expectedText = `commit bc95839e4a430ace453e8b209a3723c000c1729a +Author: foo <foo@example.org> +Date: Thu Mar 1 19:46:54 2018 +0300 + + commit message 2 + + test | 1 + + 1 file changed, 1 insertion(+) + +commit 5df78b7139b543997838071cd912e375d8bd69b2 +Author: foo <foo@example.org> +Date: Thu Mar 1 19:46:48 2018 +0300 + + commit message 1 + + test | 3 +++ + 1 file changed, 3 insertions(+)` + post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{ + "attachments": []*model.SlackAttachment{ + { + Text: "text", + }, + }, + "webhook_display_name": hook.DisplayName, + }, model.POST_SLACK_ATTACHMENT, "") + if err != nil { + t.Fatal(err.Error()) + } + assert.Equal(t, expectedText, post.Message) } func TestSplitWebhookPost(t *testing.T) { |