diff options
Diffstat (limited to 'api')
-rw-r--r-- | api/admin.go | 37 | ||||
-rw-r--r-- | api/admin_test.go | 10 | ||||
-rw-r--r-- | api/apitestlib.go | 4 | ||||
-rw-r--r-- | api/authorization.go | 14 | ||||
-rw-r--r-- | api/auto_users.go | 7 | ||||
-rw-r--r-- | api/channel.go | 92 | ||||
-rw-r--r-- | api/channel_test.go | 149 | ||||
-rw-r--r-- | api/cli_test.go | 10 | ||||
-rw-r--r-- | api/command_loadtest.go | 2 | ||||
-rw-r--r-- | api/command_msg.go | 10 | ||||
-rw-r--r-- | api/command_statuses_test.go | 2 | ||||
-rw-r--r-- | api/context.go | 6 | ||||
-rw-r--r-- | api/post.go | 145 | ||||
-rw-r--r-- | api/post_test.go | 67 | ||||
-rw-r--r-- | api/server.go | 22 | ||||
-rw-r--r-- | api/status.go | 131 | ||||
-rw-r--r-- | api/status_test.go | 106 | ||||
-rw-r--r-- | api/team.go | 100 | ||||
-rw-r--r-- | api/team_test.go | 109 | ||||
-rw-r--r-- | api/user.go | 301 | ||||
-rw-r--r-- | api/user_test.go | 438 | ||||
-rw-r--r-- | api/web_conn.go | 149 | ||||
-rw-r--r-- | api/web_hub.go | 152 | ||||
-rw-r--r-- | api/websocket.go | 12 | ||||
-rw-r--r-- | api/websocket_handler.go | 10 | ||||
-rw-r--r-- | api/websocket_router.go | 1 | ||||
-rw-r--r-- | api/websocket_test.go | 3 |
27 files changed, 1520 insertions, 569 deletions
diff --git a/api/admin.go b/api/admin.go index 9ac071e6d..0edfb246b 100644 --- a/api/admin.go +++ b/api/admin.go @@ -20,6 +20,7 @@ import ( "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" + "runtime/debug" ) func InitAdmin() { @@ -48,7 +49,7 @@ func InitAdmin() { BaseRoutes.Admin.Handle("/remove_certificate", ApiAdminSystemRequired(removeCertificate)).Methods("POST") BaseRoutes.Admin.Handle("/saml_cert_status", ApiAdminSystemRequired(samlCertificateStatus)).Methods("GET") BaseRoutes.Admin.Handle("/cluster_status", ApiAdminSystemRequired(getClusterStatus)).Methods("GET") - BaseRoutes.Admin.Handle("/recently_active_users/{team_id:[A-Za-z0-9]+}", ApiUserRequiredActivity(getRecentlyActiveUsers, false)).Methods("GET") + BaseRoutes.Admin.Handle("/recently_active_users/{team_id:[A-Za-z0-9]+}", ApiUserRequired(getRecentlyActiveUsers)).Methods("GET") } func getLogs(c *Context, w http.ResponseWriter, r *http.Request) { @@ -134,6 +135,7 @@ func getConfig(c *Context, w http.ResponseWriter, r *http.Request) { } func reloadConfig(c *Context, w http.ResponseWriter, r *http.Request) { + debug.FreeOSMemory() utils.LoadConfig(utils.CfgFileName) // start/restart email batching job if necessary @@ -338,12 +340,15 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { name := params["name"] if name == "standard" { - var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 5) + var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 8) rows[0] = &model.AnalyticsRow{"channel_open_count", 0} rows[1] = &model.AnalyticsRow{"channel_private_count", 0} rows[2] = &model.AnalyticsRow{"post_count", 0} rows[3] = &model.AnalyticsRow{"unique_user_count", 0} rows[4] = &model.AnalyticsRow{"team_count", 0} + rows[5] = &model.AnalyticsRow{"total_websocket_connections", 0} + rows[6] = &model.AnalyticsRow{"total_master_db_connections", 0} + rows[7] = &model.AnalyticsRow{"total_read_db_connections", 0} openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN) privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE) @@ -386,6 +391,10 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { rows[4].Value = float64(r.Data.(int64)) } + rows[5].Value = float64(TotalWebsocketConnections()) + rows[6].Value = float64(Srv.Store.TotalMasterDbConnections()) + rows[7].Value = float64(Srv.Store.TotalReadDbConnections()) + w.Write([]byte(rows.ToJson())) } else if name == "post_counts_day" { if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil { @@ -706,32 +715,14 @@ func samlCertificateStatus(c *Context, w http.ResponseWriter, r *http.Request) { } func getRecentlyActiveUsers(c *Context, w http.ResponseWriter, r *http.Request) { - statusMap := map[string]interface{}{} - - if result := <-Srv.Store.Status().GetAllFromTeam(c.TeamId); result.Err != nil { - c.Err = result.Err - return - } else { - statuses := result.Data.([]*model.Status) - for _, s := range statuses { - statusMap[s.UserId] = s.LastActivityAt - } - } - - if result := <-Srv.Store.User().GetProfiles(c.TeamId); result.Err != nil { + if result := <-Srv.Store.User().GetRecentlyActiveUsersForTeam(c.TeamId); result.Err != nil { c.Err = result.Err return } else { profiles := result.Data.(map[string]*model.User) - for k, p := range profiles { - p = sanitizeProfile(c, p) - - if lastActivityAt, ok := statusMap[p.Id].(int64); ok { - p.LastActivityAt = lastActivityAt - } - - profiles[k] = p + for _, p := range profiles { + sanitizeProfile(c, p) } w.Write([]byte(model.UserMapToJson(profiles))) diff --git a/api/admin_test.go b/api/admin_test.go index 445d2de38..e1520877c 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -527,17 +527,13 @@ func TestAdminLdapSyncNow(t *testing.T) { } } +// Needs more work func TestGetRecentlyActiveUsers(t *testing.T) { th := Setup().InitBasic() - user1Id := th.BasicUser.Id - user2Id := th.BasicUser2.Id - if userMap, err := th.BasicClient.GetRecentlyActiveUsers(th.BasicTeam.Id); err != nil { t.Fatal(err) - } else if len(userMap.Data.(map[string]*model.User)) != 2 { - t.Fatal("should have been 2") - } else if userMap.Data.(map[string]*model.User)[user1Id].Id != user1Id || userMap.Data.(map[string]*model.User)[user2Id].Id != user2Id { - t.Fatal("should have been valid") + } else if len(userMap.Data.(map[string]*model.User)) >= 2 { + t.Fatal("should have been at least 2") } } diff --git a/api/apitestlib.go b/api/apitestlib.go index 9345d3fc4..37367b71d 100644 --- a/api/apitestlib.go +++ b/api/apitestlib.go @@ -36,7 +36,7 @@ func SetupEnterprise() *TestHelper { *utils.Cfg.RateLimitSettings.Enable = false utils.DisableDebugLogForTest() utils.License.Features.SetDefaults() - NewServer() + NewServer(false) StartServer() utils.InitHTML() InitApi() @@ -57,7 +57,7 @@ func Setup() *TestHelper { utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 *utils.Cfg.RateLimitSettings.Enable = false utils.DisableDebugLogForTest() - NewServer() + NewServer(false) StartServer() InitApi() utils.EnableDebugLogForTest() diff --git a/api/authorization.go b/api/authorization.go index 5badf244b..8b3140b0f 100644 --- a/api/authorization.go +++ b/api/authorization.go @@ -5,6 +5,7 @@ package api import ( "net/http" + "strings" l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/model" @@ -65,15 +66,16 @@ func HasPermissionToTeam(user *model.User, teamMember *model.TeamMember, permiss } func HasPermissionToChannelContext(c *Context, channelId string, permission *model.Permission) bool { - cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId) + cmc := Srv.Store.Channel().GetAllChannelMembersForUser(c.Session.UserId, true) var channelRoles []string if cmcresult := <-cmc; cmcresult.Err == nil { - channelMember := cmcresult.Data.(model.ChannelMember) - channelRoles = channelMember.GetRoles() - - if CheckIfRolesGrantPermission(channelRoles, permission.Id) { - return true + ids := cmcresult.Data.(map[string]string) + if roles, ok := ids[channelId]; ok { + channelRoles = strings.Fields(roles) + if CheckIfRolesGrantPermission(channelRoles, permission.Id) { + return true + } } } diff --git a/api/auto_users.go b/api/auto_users.go index a23b76246..7439de96e 100644 --- a/api/auto_users.go +++ b/api/auto_users.go @@ -80,6 +80,13 @@ func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) { ruser := result.Data.(*model.User) + status := &model.Status{ruser.Id, model.STATUS_ONLINE, false, model.GetMillis(), ""} + 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)) diff --git a/api/channel.go b/api/channel.go index f40c979ca..9cc8976c2 100644 --- a/api/channel.go +++ b/api/channel.go @@ -6,7 +6,6 @@ package api import ( "fmt" "net/http" - "strconv" "strings" l4g "github.com/alecthomas/log4go" @@ -16,16 +15,12 @@ import ( "github.com/mattermost/platform/utils" ) -const ( - defaultExtraMemberLimit = 100 -) - func InitChannel() { l4g.Debug(utils.T("api.channel.init.debug")) - BaseRoutes.Channels.Handle("/", ApiUserRequiredActivity(getChannels, false)).Methods("GET") + BaseRoutes.Channels.Handle("/", ApiUserRequired(getChannels)).Methods("GET") BaseRoutes.Channels.Handle("/more", ApiUserRequired(getMoreChannels)).Methods("GET") - BaseRoutes.Channels.Handle("/counts", ApiUserRequiredActivity(getChannelCounts, false)).Methods("GET") + BaseRoutes.Channels.Handle("/counts", ApiUserRequired(getChannelCounts)).Methods("GET") BaseRoutes.Channels.Handle("/create", ApiUserRequired(createChannel)).Methods("POST") BaseRoutes.Channels.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST") BaseRoutes.Channels.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST") @@ -35,9 +30,9 @@ func InitChannel() { BaseRoutes.NeedChannelName.Handle("/join", ApiUserRequired(join)).Methods("POST") - BaseRoutes.NeedChannel.Handle("/", ApiUserRequiredActivity(getChannel, false)).Methods("GET") - BaseRoutes.NeedChannel.Handle("/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET") - BaseRoutes.NeedChannel.Handle("/extra_info/{member_limit:-?[0-9]+}", ApiUserRequired(getChannelExtraInfo)).Methods("GET") + BaseRoutes.NeedChannel.Handle("/", ApiUserRequired(getChannel)).Methods("GET") + BaseRoutes.NeedChannel.Handle("/stats", ApiUserRequired(getChannelStats)).Methods("GET") + BaseRoutes.NeedChannel.Handle("/members/{user_id:[A-Za-z0-9]+}", ApiUserRequired(getChannelMember)).Methods("GET") BaseRoutes.NeedChannel.Handle("/join", ApiUserRequired(join)).Methods("POST") BaseRoutes.NeedChannel.Handle("/leave", ApiUserRequired(leave)).Methods("POST") BaseRoutes.NeedChannel.Handle("/delete", ApiUserRequired(deleteChannel)).Methods("POST") @@ -150,11 +145,14 @@ func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *mo } else { channel := result.Data.(*model.Channel) + InvalidateCacheForUser(userId) + InvalidateCacheForUser(otherUserId) + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil) message.Add("teammate_id", otherUserId) go Publish(message) - return result.Data.(*model.Channel), nil + return channel, nil } } @@ -566,6 +564,7 @@ func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelM go func() { InvalidateCacheForUser(user.Id) + Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil) message.Add("user_id", user.Id) @@ -609,6 +608,8 @@ func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *m if _, err := CreatePost(fakeContext, post, false); err != nil { l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) } + + Srv.Store.User().InvalidateProfilesInChannelCache(result.Data.(*model.Channel).Id) } if result := <-Srv.Store.Channel().GetByName(teamId, "off-topic"); result.Err != nil { @@ -631,6 +632,8 @@ func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *m if _, err := CreatePost(fakeContext, post, false); err != nil { l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) } + + Srv.Store.User().InvalidateProfilesInChannelCache(result.Data.(*model.Channel).Id) } return err @@ -778,9 +781,9 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("name=" + channel.Name) go func() { - InvalidateCacheForChannel(channel.Id) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_DELETED, c.TeamId, "", "", nil) message.Add("channel_id", channel.Id) + go Publish(message) post := &model.Post{ @@ -917,54 +920,27 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) { } -func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { +func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["channel_id"] - var memberLimit int - if memberLimitString, ok := params["member_limit"]; !ok { - memberLimit = defaultExtraMemberLimit - } else if memberLimitInt64, err := strconv.ParseInt(memberLimitString, 10, 0); err != nil { - c.Err = model.NewLocAppError("getChannelExtraInfo", "api.channel.get_channel_extra_info.member_limit.app_error", nil, err.Error()) - return - } else { - memberLimit = int(memberLimitInt64) - } - sc := Srv.Store.Channel().Get(id) var channel *model.Channel - if cresult := <-sc; cresult.Err != nil { - c.Err = cresult.Err + if result := <-sc; result.Err != nil { + c.Err = result.Err return } else { - channel = cresult.Data.(*model.Channel) + channel = result.Data.(*model.Channel) } - extraEtag := channel.ExtraEtag(memberLimit) - if HandleEtag(extraEtag, w, r) { - return - } - - scm := Srv.Store.Channel().GetMember(id, c.Session.UserId) - ecm := Srv.Store.Channel().GetExtraMembers(id, memberLimit) - ccm := Srv.Store.Channel().GetMemberCount(id) - - if cmresult := <-scm; cmresult.Err != nil { - c.Err = cmresult.Err - return - } else if ecmresult := <-ecm; ecmresult.Err != nil { - c.Err = ecmresult.Err - return - } else if ccmresult := <-ccm; ccmresult.Err != nil { - c.Err = ccmresult.Err + if result := <-Srv.Store.Channel().GetMemberCount(id); result.Err != nil { + c.Err = result.Err return } else { - //member := cmresult.Data.(model.ChannelMember) - extraMembers := ecmresult.Data.([]model.ExtraMember) - memberCount := ccmresult.Data.(int64) + memberCount := result.Data.(int64) if channel.DeleteAt > 0 { - c.Err = model.NewLocAppError("getChannelExtraInfo", "api.channel.get_channel_extra_info.deleted.app_error", nil, "") + c.Err = model.NewLocAppError("getChannelStats", "api.channel.get_channel_extra_info.deleted.app_error", nil, "") c.Err.StatusCode = http.StatusBadRequest return } @@ -973,12 +949,29 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { return } - data := model.ChannelExtra{Id: channel.Id, Members: extraMembers, MemberCount: memberCount} - w.Header().Set(model.HEADER_ETAG_SERVER, extraEtag) + data := model.ChannelStats{ChannelId: channel.Id, MemberCount: memberCount} w.Write([]byte(data.ToJson())) } } +func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + channelId := params["channel_id"] + userId := params["user_id"] + + if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { + return + } + + if result := <-Srv.Store.Channel().GetMember(channelId, userId); result.Err != nil { + c.Err = result.Err + return + } else { + member := result.Data.(model.ChannelMember) + w.Write([]byte(member.ToJson())) + } +} + func addMember(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["channel_id"] @@ -1101,6 +1094,7 @@ func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel } InvalidateCacheForUser(userIdToRemove) + Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil) message.Add("user_id", userIdToRemove) diff --git a/api/channel_test.go b/api/channel_test.go index 1d0f0270d..4835ee9b7 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) @@ -1106,7 +1105,7 @@ func TestDeleteChannel(t *testing.T) { } } -func TestGetChannelExtraInfo(t *testing.T) { +func TestGetChannelStats(t *testing.T) { th := Setup().InitBasic() Client := th.BasicClient team := th.BasicTeam @@ -1114,115 +1113,13 @@ func TestGetChannelExtraInfo(t *testing.T) { channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) - rget := Client.Must(Client.GetChannelExtraInfo(channel1.Id, -1, "")) - data := rget.Data.(*model.ChannelExtra) - if data.Id != channel1.Id { + rget := Client.Must(Client.GetChannelStats(channel1.Id, "")) + data := rget.Data.(*model.ChannelStats) + if data.ChannelId != channel1.Id { t.Fatal("couldnt't get extra info") - } else if len(data.Members) != 1 { - t.Fatal("got incorrect members") } else if data.MemberCount != 1 { t.Fatal("got incorrect member count") } - - // - // Testing etag caching - // - - currentEtag := rget.Etag - - if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil { - t.Fatal(err) - } else if cache_result.Data.(*model.ChannelExtra) != nil { - t.Log(cache_result.Data) - t.Fatal("response should be empty") - } else { - currentEtag = cache_result.Etag - } - - Client2 := model.NewClient("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress) - - user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Tester 2", Password: "passwd1"} - user2 = Client2.Must(Client2.CreateUser(user2, "")).Data.(*model.User) - LinkUserToTeam(user2, team) - Client2.SetTeamId(team.Id) - store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - - Client2.Login(user2.Email, "passwd1") - Client2.Must(Client2.JoinChannel(channel1.Id)) - - if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil { - t.Fatal(err) - } else if cache_result.Data.(*model.ChannelExtra) == nil { - t.Log(cache_result.Data) - t.Fatal("response should not be empty") - } else { - currentEtag = cache_result.Etag - } - - if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil { - t.Fatal(err) - } else if cache_result.Data.(*model.ChannelExtra) != nil { - t.Log(cache_result.Data) - t.Fatal("response should be empty") - } else { - currentEtag = cache_result.Etag - } - - Client2.Must(Client2.LeaveChannel(channel1.Id)) - - if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil { - t.Fatal(err) - } else if cache_result.Data.(*model.ChannelExtra) == nil { - t.Log(cache_result.Data) - t.Fatal("response should not be empty") - } else { - currentEtag = cache_result.Etag - } - - if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, -1, currentEtag); err != nil { - t.Fatal(err) - } else if cache_result.Data.(*model.ChannelExtra) != nil { - t.Log(cache_result.Data) - t.Fatal("response should be empty") - } else { - currentEtag = cache_result.Etag - } - - Client2.Must(Client2.JoinChannel(channel1.Id)) - - if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 2, currentEtag); err != nil { - t.Fatal(err) - } else if extra := cache_result.Data.(*model.ChannelExtra); extra == nil { - t.Fatal("response should not be empty") - } else if len(extra.Members) != 2 { - t.Fatal("should've returned 2 members") - } else if extra.MemberCount != 2 { - t.Fatal("should've returned member count of 2") - } else { - currentEtag = cache_result.Etag - } - - if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 1, currentEtag); err != nil { - t.Fatal(err) - } else if extra := cache_result.Data.(*model.ChannelExtra); extra == nil { - t.Fatal("response should not be empty") - } else if len(extra.Members) != 1 { - t.Fatal("should've returned only 1 member") - } else if extra.MemberCount != 2 { - t.Fatal("should've returned member count of 2") - } else { - currentEtag = cache_result.Etag - } - - if cache_result, err := Client.GetChannelExtraInfo(channel1.Id, 1, currentEtag); err != nil { - t.Fatal(err) - } else if cache_result.Data.(*model.ChannelExtra) != nil { - t.Log(cache_result.Data) - t.Fatal("response should be empty") - } else { - currentEtag = cache_result.Etag - } - } func TestAddChannelMember(t *testing.T) { @@ -1495,3 +1392,41 @@ func TestFuzzyChannel(t *testing.T) { } } } + +func TestGetChannelMember(t *testing.T) { + th := Setup().InitBasic() + Client := th.BasicClient + team := th.BasicTeam + + channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + if result, err := Client.GetChannelMember(channel1.Id, th.BasicUser.Id); err != nil { + t.Fatal(err) + } else { + cm := result.Data.(*model.ChannelMember) + + if cm.UserId != th.BasicUser.Id { + t.Fatal("user ids didn't match") + } + if cm.ChannelId != channel1.Id { + t.Fatal("channel ids didn't match") + } + } + + if _, err := Client.GetChannelMember(channel1.Id, th.BasicUser2.Id); err == nil { + t.Fatal("should have failed - user not in channel") + } + + if _, err := Client.GetChannelMember("junk", th.BasicUser2.Id); err == nil { + t.Fatal("should have failed - bad channel id") + } + + if _, err := Client.GetChannelMember(channel1.Id, "junk"); err == nil { + t.Fatal("should have failed - bad user id") + } + + if _, err := Client.GetChannelMember("junk", "junk"); err == nil { + t.Fatal("should have failed - bad channel and user id") + } +} diff --git a/api/cli_test.go b/api/cli_test.go index de2347058..4613988f3 100644 --- a/api/cli_test.go +++ b/api/cli_test.go @@ -68,7 +68,7 @@ func TestCliCreateUserWithTeam(t *testing.T) { t.Fatal(err) } - profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfiles(th.SystemAdminTeam.Id, "")).Data.(map[string]*model.User) + profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfilesInTeam(th.SystemAdminTeam.Id, 0, 1000, "")).Data.(map[string]*model.User) found := false @@ -318,7 +318,7 @@ func TestCliJoinTeam(t *testing.T) { t.Fatal(err) } - profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfiles(th.SystemAdminTeam.Id, "")).Data.(map[string]*model.User) + profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfilesInTeam(th.SystemAdminTeam.Id, 0, 1000, "")).Data.(map[string]*model.User) found := false @@ -348,7 +348,7 @@ func TestCliLeaveTeam(t *testing.T) { t.Fatal(err) } - profiles := th.BasicClient.Must(th.BasicClient.GetProfiles(th.BasicTeam.Id, "")).Data.(map[string]*model.User) + profiles := th.BasicClient.Must(th.BasicClient.GetProfilesInTeam(th.BasicTeam.Id, 0, 1000, "")).Data.(map[string]*model.User) found := false @@ -359,8 +359,8 @@ func TestCliLeaveTeam(t *testing.T) { } - if !found { - t.Fatal("profile still should be in team even if deleted") + if found { + t.Fatal("profile should not be on team") } if result := <-Srv.Store.Team().GetTeamsByUserId(th.BasicUser.Id); result.Err != nil { diff --git a/api/command_loadtest.go b/api/command_loadtest.go index beb831b22..8f5163a66 100644 --- a/api/command_loadtest.go +++ b/api/command_loadtest.go @@ -288,7 +288,7 @@ func (me *LoadTestProvider) PostsCommand(c *Context, channelId string, message s } var usernames []string - if result := <-Srv.Store.User().GetProfiles(c.TeamId); result.Err == nil { + if result := <-Srv.Store.User().GetProfiles(c.TeamId, 0, 1000); result.Err == nil { profileUsers := result.Data.(map[string]*model.User) usernames = make([]string, len(profileUsers)) i := 0 diff --git a/api/command_msg.go b/api/command_msg.go index aac657385..2e0e25397 100644 --- a/api/command_msg.go +++ b/api/command_msg.go @@ -47,20 +47,22 @@ func (me *msgProvider) DoCommand(c *Context, channelId string, message string) * targetUser = strings.SplitN(message, " ", 2)[0] targetUser = strings.TrimPrefix(targetUser, "@") - if profileList := <-Srv.Store.User().GetAllProfiles(); profileList.Err != nil { + // FIX ME + // Why isn't this selecting by username since we have that? + if profileList := <-Srv.Store.User().GetAll(); profileList.Err != nil { c.Err = profileList.Err return &model.CommandResponse{Text: c.T("api.command_msg.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { - profileUsers := profileList.Data.(map[string]*model.User) + profileUsers := profileList.Data.([]*model.User) for _, userProfile := range profileUsers { - //Don't let users open DMs with themselves. It probably won't work out well. + // Don't let users open DMs with themselves. It probably won't work out well. if userProfile.Id == c.Session.UserId { continue } if userProfile.Username == targetUser { targetChannelId := "" - //Find the channel based on this user + // Find the channel based on this user channelName := model.GetDMNameFromIds(c.Session.UserId, userProfile.Id) if channel := <-Srv.Store.Channel().GetByName(c.TeamId, channelName); channel.Err != nil { diff --git a/api/command_statuses_test.go b/api/command_statuses_test.go index 1c8026a9f..73c6354f4 100644 --- a/api/command_statuses_test.go +++ b/api/command_statuses_test.go @@ -27,7 +27,7 @@ func commandAndTest(t *testing.T, th *TestHelper, status string) { t.Fatal("Command failed to execute") } - time.Sleep(300 * time.Millisecond) + time.Sleep(500 * time.Millisecond) statuses := Client.Must(Client.GetStatuses()).Data.(map[string]string) diff --git a/api/context.go b/api/context.go index 524ccf402..257f43174 100644 --- a/api/context.go +++ b/api/context.go @@ -57,7 +57,7 @@ func AppHandlerIndependent(h func(*Context, http.ResponseWriter, *http.Request)) } func ApiUserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, true, false, true, true, false, false} + return &handler{h, true, false, true, false, false, false} } func ApiUserRequiredActivity(h func(*Context, http.ResponseWriter, *http.Request), isUserActivity bool) http.Handler { @@ -85,7 +85,7 @@ func ApiAppHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Req } func ApiUserRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, true, false, true, true, false, true} + return &handler{h, true, false, true, false, false, true} } func ApiAppHandlerTrustRequesterIndependent(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -220,7 +220,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.LogError(c.Err) c.Err.Where = r.URL.Path - // Block out detailed error whenn not in developer mode + // Block out detailed error when not in developer mode if !*utils.Cfg.ServiceSettings.EnableDeveloper { c.Err.DetailedError = "" } diff --git a/api/post.go b/api/post.go index 498f5b363..5ae5f60db 100644 --- a/api/post.go +++ b/api/post.go @@ -35,18 +35,18 @@ const ( func InitPost() { l4g.Debug(utils.T("api.post.init.debug")) - BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("POST") - BaseRoutes.NeedTeam.Handle("/posts/flagged/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getFlaggedPosts, false)).Methods("GET") + BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequiredActivity(searchPosts, true)).Methods("POST") + BaseRoutes.NeedTeam.Handle("/posts/flagged/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getFlaggedPosts)).Methods("GET") BaseRoutes.NeedTeam.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET") BaseRoutes.NeedTeam.Handle("/pltmp/{post_id}", ApiUserRequired(getPermalinkTmp)).Methods("GET") - BaseRoutes.Posts.Handle("/create", ApiUserRequired(createPost)).Methods("POST") - BaseRoutes.Posts.Handle("/update", ApiUserRequired(updatePost)).Methods("POST") - BaseRoutes.Posts.Handle("/page/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET") - BaseRoutes.Posts.Handle("/since/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, false)).Methods("GET") + BaseRoutes.Posts.Handle("/create", ApiUserRequiredActivity(createPost, true)).Methods("POST") + BaseRoutes.Posts.Handle("/update", ApiUserRequiredActivity(updatePost, true)).Methods("POST") + BaseRoutes.Posts.Handle("/page/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getPosts)).Methods("GET") + BaseRoutes.Posts.Handle("/since/{time:[0-9]+}", ApiUserRequired(getPostsSince)).Methods("GET") BaseRoutes.NeedPost.Handle("/get", ApiUserRequired(getPost)).Methods("GET") - BaseRoutes.NeedPost.Handle("/delete", ApiUserRequired(deletePost)).Methods("POST") + BaseRoutes.NeedPost.Handle("/delete", ApiUserRequiredActivity(deletePost, true)).Methods("POST") BaseRoutes.NeedPost.Handle("/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET") BaseRoutes.NeedPost.Handle("/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET") BaseRoutes.NeedPost.Handle("/get_file_infos", ApiUserRequired(getFileInfosForPost)).Methods("GET") @@ -154,7 +154,7 @@ func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post } } - go handlePostEvents(c, rpost, triggerWebhooks) + handlePostEvents(c, rpost, triggerWebhooks) return rpost, nil } @@ -250,7 +250,7 @@ func handlePostEvents(c *Context, post *model.Post, triggerWebhooks bool) { channel = result.Data.(*model.Channel) } - go sendNotifications(c, post, team, channel) + sendNotifications(c, post, team, channel) var user *model.User if result := <-uchan; result.Err != nil { @@ -441,40 +441,31 @@ func handleWebhookEvents(c *Context, post *model.Post, team *model.Team, channel } } -// Given a map of user IDs to profiles and a map of user IDs of channel members, returns a list of mention -// keywords for all users on the team. Users that are members of the channel will have all their mention -// keywords returned while users that aren't in the channel will only have their @mentions returned. -func getMentionKeywords(profiles map[string]*model.User, members map[string]string) map[string][]string { +// Given a map of user IDs to profiles, returns a list of mention +// keywords for all users in the channel. +func getMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]string { keywords := make(map[string][]string) for id, profile := range profiles { - _, inChannel := members[id] - - if inChannel { - if len(profile.NotifyProps["mention_keys"]) > 0 { - // Add all the user's mention keys - splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") - for _, k := range splitKeys { - // note that these are made lower case so that we can do a case insensitive check for them - key := strings.ToLower(k) - keywords[key] = append(keywords[key], id) - } + if len(profile.NotifyProps["mention_keys"]) > 0 { + // Add all the user's mention keys + splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") + for _, k := range splitKeys { + // note that these are made lower case so that we can do a case insensitive check for them + key := strings.ToLower(k) + keywords[key] = append(keywords[key], id) } + } - // If turned on, add the user's case sensitive first name - if profile.NotifyProps["first_name"] == "true" { - keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id) - } + // If turned on, add the user's case sensitive first name + if profile.NotifyProps["first_name"] == "true" { + keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id) + } - // Add @channel and @all to keywords if user has them turned on - if profile.NotifyProps["channel"] == "true" { - keywords["@channel"] = append(keywords["@channel"], profile.Id) - keywords["@all"] = append(keywords["@all"], profile.Id) - } - } else { - // user isn't in channel, so just look for @mentions - key := "@" + strings.ToLower(profile.Username) - keywords[key] = append(keywords[key], id) + // Add @channel and @all to keywords if user has them turned on + if profile.NotifyProps["channel"] == "true" { + keywords["@channel"] = append(keywords["@channel"], profile.Id) + keywords["@all"] = append(keywords["@all"], profile.Id) } } @@ -482,9 +473,11 @@ func getMentionKeywords(profiles map[string]*model.User, members map[string]stri } // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned -// users and whether or not @here was mentioned. -func getExplicitMentions(message string, keywords map[string][]string) (map[string]bool, bool) { +// users and a slice of potencial mention users not in the channel and whether or not @here was mentioned. +func getExplicitMentions(message string, keywords map[string][]string) (map[string]bool, []string, bool) { mentioned := make(map[string]bool) + potentialOthersMentioned := make([]string, 0) + systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true} hereMentioned := false addMentionedUsers := func(ids []string) { @@ -510,6 +503,9 @@ func getExplicitMentions(message string, keywords map[string][]string) (map[stri if ids, match := keywords[word]; match { addMentionedUsers(ids) isMention = true + } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { + potentialOthersMentioned = append(potentialOthersMentioned, word[1:]) + continue } if !isMention { @@ -532,19 +528,19 @@ func getExplicitMentions(message string, keywords map[string][]string) (map[stri // Case-sensitive check for first name if ids, match := keywords[splitWord]; match { addMentionedUsers(ids) + } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { + username := word[1:len(splitWord)] + potentialOthersMentioned = append(potentialOthersMentioned, username) } } } } - return mentioned, hereMentioned + return mentioned, potentialOthersMentioned, hereMentioned } func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) { - // get profiles for all users we could be mentioning - pchan := Srv.Store.User().GetProfiles(c.TeamId) - dpchan := Srv.Store.User().GetDirectProfiles(c.Session.UserId) - mchan := Srv.Store.Channel().GetMembers(post.ChannelId) + pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true) fchan := Srv.Store.FileInfo().GetForPost(post.Id) var profileMap map[string]*model.User @@ -555,30 +551,11 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * profileMap = result.Data.(map[string]*model.User) } - if result := <-dpchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err) - return - } else { - dps := result.Data.(map[string]*model.User) - for k, v := range dps { - profileMap[k] = v - } - } - + // If the user who made the post is mention don't send a notification if _, ok := profileMap[post.UserId]; !ok { l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) return } - // using a map as a pseudo-set since we're checking for containment a lot - members := make(map[string]string) - if result := <-mchan; result.Err != nil { - l4g.Error(utils.T("api.post.handle_post_events_and_forget.members.error"), post.ChannelId, result.Err) - return - } else { - for _, member := range result.Data.([]model.ChannelMember) { - members[member.UserId] = member.UserId - } - } mentionedUserIds := make(map[string]bool) allActivityPushUserIds := []string{} @@ -595,11 +572,11 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * mentionedUserIds[otherUserId] = true } else { - keywords := getMentionKeywords(profileMap, members) + keywords := getMentionKeywordsInChannel(profileMap) - // get users that are explicitly mentioned var mentioned map[string]bool - mentioned, hereNotification = getExplicitMentions(post.Message, keywords) + var potentialOtherMentions []string + mentioned, potentialOtherMentions, hereNotification = getExplicitMentions(post.Message, keywords) // get users that have comment thread mentions enabled if len(post.RootId) > 0 { @@ -623,25 +600,15 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * delete(mentioned, post.UserId) } - outOfChannelMentions := make(map[string]bool) - for id := range mentioned { - if _, inChannel := members[id]; inChannel { - mentionedUserIds[id] = true - } else { - outOfChannelMentions[id] = true + if len(potentialOtherMentions) > 0 { + if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil { + outOfChannelMentions := result.Data.(map[string]*model.User) + go sendOutOfChannelMentions(c, post, outOfChannelMentions) } } - go sendOutOfChannelMentions(c, post, profileMap, outOfChannelMentions) - // find which users in the channel are set up to always receive mobile notifications - for id := range members { - profile := profileMap[id] - if profile == nil { - l4g.Warn(utils.T("api.post.notification.member_profile.warn"), id) - continue - } - + for _, profile := range profileMap { if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && (post.UserId != profile.Id || post.Props["from_webhook"] == "true") && !post.IsSystemMessage() { @@ -699,10 +666,9 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * } _, profileFound := profileMap[status.UserId] - _, isChannelMember := members[status.UserId] _, alreadyMentioned := mentionedUserIds[status.UserId] - if status.Status == model.STATUS_ONLINE && profileFound && isChannelMember && !alreadyMentioned { + if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned { mentionedUsersList = append(mentionedUsersList, status.UserId) updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId)) } @@ -787,7 +753,8 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * message.Add("mentions", model.ArrayToJson(mentionedUsersList)) } - go Publish(message) + Publish(message) + return } func sendNotificationEmail(c *Context, post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) { @@ -1045,14 +1012,14 @@ func getMobileAppSession(userId string) *model.Session { return nil } -func sendOutOfChannelMentions(c *Context, post *model.Post, profiles map[string]*model.User, outOfChannelMentions map[string]bool) { - if len(outOfChannelMentions) == 0 { +func sendOutOfChannelMentions(c *Context, post *model.Post, profiles map[string]*model.User) { + if len(profiles) == 0 { return } var usernames []string - for id := range outOfChannelMentions { - usernames = append(usernames, profiles[id].Username) + for _, user := range profiles { + usernames = append(usernames, user.Username) } sort.Strings(usernames) diff --git a/api/post_test.go b/api/post_test.go index bdc5278e4..3c917aec3 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -883,10 +883,9 @@ func TestGetMentionKeywords(t *testing.T) { } profiles := map[string]*model.User{user1.Id: user1} - members := map[string]string{user1.Id: user1.Id} - mentions := getMentionKeywords(profiles, members) + mentions := getMentionKeywordsInChannel(profiles) if len(mentions) != 3 { - t.Fatal("should've returned two mention keywords") + t.Fatal("should've returned three mention keywords") } else if ids, ok := mentions["user"]; !ok || ids[0] != user1.Id { t.Fatal("should've returned mention key of user") } else if ids, ok := mentions["@user"]; !ok || ids[0] != user1.Id { @@ -906,8 +905,7 @@ func TestGetMentionKeywords(t *testing.T) { } profiles = map[string]*model.User{user2.Id: user2} - members = map[string]string{user2.Id: user2.Id} - mentions = getMentionKeywords(profiles, members) + mentions = getMentionKeywordsInChannel(profiles) if len(mentions) != 1 { t.Fatal("should've returned one mention keyword") } else if ids, ok := mentions["First"]; !ok || ids[0] != user2.Id { @@ -925,8 +923,7 @@ func TestGetMentionKeywords(t *testing.T) { } profiles = map[string]*model.User{user3.Id: user3} - members = map[string]string{user3.Id: user3.Id} - mentions = getMentionKeywords(profiles, members) + mentions = getMentionKeywordsInChannel(profiles) if len(mentions) != 2 { t.Fatal("should've returned two mention keywords") } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user3.Id { @@ -948,8 +945,7 @@ func TestGetMentionKeywords(t *testing.T) { } profiles = map[string]*model.User{user4.Id: user4} - members = map[string]string{user4.Id: user4.Id} - mentions = getMentionKeywords(profiles, members) + mentions = getMentionKeywordsInChannel(profiles) if len(mentions) != 6 { t.Fatal("should've returned six mention keywords") } else if ids, ok := mentions["user"]; !ok || ids[0] != user4.Id { @@ -973,13 +969,7 @@ func TestGetMentionKeywords(t *testing.T) { user3.Id: user3, user4.Id: user4, } - members = map[string]string{ - user1.Id: user1.Id, - user2.Id: user2.Id, - user3.Id: user3.Id, - user4.Id: user4.Id, - } - mentions = getMentionKeywords(profiles, members) + mentions = getMentionKeywordsInChannel(profiles) if len(mentions) != 6 { t.Fatal("should've returned six mention keywords") } else if ids, ok := mentions["user"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { @@ -995,16 +985,6 @@ func TestGetMentionKeywords(t *testing.T) { } else if ids, ok := mentions["@all"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) { t.Fatal("should've mentioned user3 and user4 with @all") } - - // a user that's not in the channel - profiles = map[string]*model.User{user4.Id: user4} - members = map[string]string{} - mentions = getMentionKeywords(profiles, members) - if len(mentions) != 1 { - t.Fatal("should've returned one mention keyword") - } else if ids, ok := mentions["@user"]; !ok || len(ids) != 1 || ids[0] != user4.Id { - t.Fatal("should've returned mention key of @user") - } } func TestGetExplicitMentionsAtHere(t *testing.T) { @@ -1051,7 +1031,7 @@ func TestGetExplicitMentionsAtHere(t *testing.T) { } for message, shouldMention := range cases { - if _, hereMentioned := getExplicitMentions(message, nil); hereMentioned && !shouldMention { + if _, _, hereMentioned := getExplicitMentions(message, nil); hereMentioned && !shouldMention { t.Fatalf("shouldn't have mentioned @here with \"%v\"", message) } else if !hereMentioned && shouldMention { t.Fatalf("should've have mentioned @here with \"%v\"", message) @@ -1060,10 +1040,12 @@ func TestGetExplicitMentionsAtHere(t *testing.T) { // mentioning @here and someone id := model.NewId() - if mentions, hereMentioned := getExplicitMentions("@here @user", map[string][]string{"@user": {id}}); !hereMentioned { + if mentions, potential, hereMentioned := getExplicitMentions("@here @user @potential", map[string][]string{"@user": {id}}); !hereMentioned { t.Fatal("should've mentioned @here with \"@here @user\"") } else if len(mentions) != 1 || !mentions[id] { t.Fatal("should've mentioned @user with \"@here @user\"") + } else if len(potential) > 1 { + t.Fatal("should've potential mentions for @potential") } } @@ -1074,69 +1056,76 @@ func TestGetExplicitMentions(t *testing.T) { // not mentioning anybody message := "this is a message" keywords := map[string][]string{} - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 0 { - t.Fatal("shouldn't have mentioned anybody") + if mentions, potential, _ := getExplicitMentions(message, keywords); len(mentions) != 0 || len(potential) != 0 { + t.Fatal("shouldn't have mentioned anybody or have any potencial mentions") } // mentioning a user that doesn't exist message = "this is a message for @user" - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 0 { + if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 0 { t.Fatal("shouldn't have mentioned user that doesn't exist") } // mentioning one person keywords = map[string][]string{"@user": {id1}} - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { + if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { t.Fatal("should've mentioned @user") } // mentioning one person without an @mention message = "this is a message for @user" keywords = map[string][]string{"this": {id1}} - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { + if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] { t.Fatal("should've mentioned this") } // mentioning multiple people with one word message = "this is a message for @user" keywords = map[string][]string{"@user": {id1, id2}} - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { + if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { t.Fatal("should've mentioned two users with @user") } // mentioning only one of multiple people keywords = map[string][]string{"@user": {id1}, "@mention": {id2}} - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { + if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { t.Fatal("should've mentioned @user and not @mention") } // mentioning multiple people with multiple words message = "this is an @mention for @user" keywords = map[string][]string{"@user": {id1}, "@mention": {id2}} - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { + if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { t.Fatal("should've mentioned two users with @user and @mention") } // mentioning @channel (not a special case, but it's good to double check) message = "this is an message for @channel" keywords = map[string][]string{"@channel": {id1, id2}} - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { + if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { t.Fatal("should've mentioned two users with @channel") } // mentioning @all (not a special case, but it's good to double check) message = "this is an message for @all" keywords = map[string][]string{"@all": {id1, id2}} - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { + if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] { t.Fatal("should've mentioned two users with @all") } // mentioning user.period without mentioning user (PLT-3222) message = "user.period doesn't complicate things at all by including periods in their username" keywords = map[string][]string{"user.period": {id1}, "user": {id2}} - if mentions, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { + if mentions, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] { t.Fatal("should've mentioned user.period and not user") } + + // mentioning a potential out of channel user + message = "this is an message for @potential and @user" + keywords = map[string][]string{"@user": {id1}} + if mentions, potential, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || len(potential) != 1 { + t.Fatal("should've mentioned user and have a potential not in channel") + } } func TestGetFlaggedPosts(t *testing.T) { diff --git a/api/server.go b/api/server.go index a7b9716c4..fee74e373 100644 --- a/api/server.go +++ b/api/server.go @@ -7,6 +7,7 @@ import ( "crypto/tls" "net" "net/http" + "net/http/pprof" "strings" "time" @@ -36,7 +37,20 @@ const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second var Srv *Server -func NewServer() { +func AttachProfiler(router *mux.Router) { + router.HandleFunc("/debug/pprof/", pprof.Index) + router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + router.HandleFunc("/debug/pprof/profile", pprof.Profile) + router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + + // Manually add support for paths linked to by index page at /debug/pprof/ + router.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) + router.Handle("/debug/pprof/heap", pprof.Handler("heap")) + router.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")) + router.Handle("/debug/pprof/block", pprof.Handler("block")) +} + +func NewServer(enableProfiler bool) { l4g.Info(utils.T("api.server.new_server.init.info")) @@ -44,6 +58,10 @@ func NewServer() { Srv.Store = store.NewSqlStore() Srv.Router = mux.NewRouter() + if enableProfiler { + AttachProfiler(Srv.Router) + l4g.Info("Enabled HTTP Profiler") + } Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404) } @@ -177,7 +195,7 @@ func StopServer() { Srv.GracefulServer.Stop(TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN) Srv.Store.Close() - hub.Stop() + HubStop() l4g.Info(utils.T("api.server.stop_server.stopped.info")) } diff --git a/api/status.go b/api/status.go index a1a5ac496..897102a4b 100644 --- a/api/status.go +++ b/api/status.go @@ -31,9 +31,11 @@ func AddStatusCache(status *model.Status) { func InitStatus() { l4g.Debug(utils.T("api.status.init.debug")) - BaseRoutes.Users.Handle("/status", ApiUserRequiredActivity(getStatusesHttp, false)).Methods("GET") - BaseRoutes.Users.Handle("/status/set_active_channel", ApiUserRequiredActivity(setActiveChannel, false)).Methods("POST") + BaseRoutes.Users.Handle("/status", ApiUserRequired(getStatusesHttp)).Methods("GET") + BaseRoutes.Users.Handle("/status/ids", ApiUserRequired(getStatusesByIdsHttp)).Methods("POST") + BaseRoutes.Users.Handle("/status/set_active_channel", ApiUserRequired(setActiveChannel)).Methods("POST") BaseRoutes.WebSocket.Handle("get_statuses", ApiWebSocketHandler(getStatusesWebSocket)) + BaseRoutes.WebSocket.Handle("get_statuses_by_ids", ApiWebSocketHandler(getStatusesByIdsWebSocket)) } func getStatusesHttp(c *Context, w http.ResponseWriter, r *http.Request) { @@ -55,6 +57,7 @@ func getStatusesWebSocket(req *model.WebSocketRequest) (map[string]interface{}, return statusMap, nil } +// Only returns 300 statuses max func GetAllStatuses() (map[string]interface{}, *model.AppError) { if result := <-Srv.Store.Status().GetOnlineAway(); result.Err != nil { return nil, result.Err @@ -70,11 +73,82 @@ func GetAllStatuses() (map[string]interface{}, *model.AppError) { } } +func getStatusesByIdsHttp(c *Context, w http.ResponseWriter, r *http.Request) { + userIds := model.ArrayFromJson(r.Body) + + if len(userIds) == 0 { + c.SetInvalidParam("getStatusesByIdsHttp", "user_ids") + return + } + + statusMap, err := GetStatusesByIds(userIds) + if err != nil { + c.Err = err + return + } + + w.Write([]byte(model.StringInterfaceToJson(statusMap))) +} + +func getStatusesByIdsWebSocket(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { + var userIds []string + if userIds = model.ArrayFromInterface(req.Data["user_ids"]); len(userIds) == 0 { + l4g.Error(model.StringInterfaceToJson(req.Data)) + return nil, NewInvalidWebSocketParamError(req.Action, "user_ids") + } + + statusMap, err := GetStatusesByIds(userIds) + if err != nil { + return nil, err + } + + return statusMap, nil +} + +func GetStatusesByIds(userIds []string) (map[string]interface{}, *model.AppError) { + statusMap := map[string]interface{}{} + + missingUserIds := []string{} + for _, userId := range userIds { + if result, ok := statusCache.Get(userId); ok { + statusMap[userId] = result.(*model.Status).Status + } else { + missingUserIds = append(missingUserIds, userId) + } + } + + if len(missingUserIds) > 0 { + if result := <-Srv.Store.Status().GetByIds(missingUserIds); result.Err != nil { + return nil, result.Err + } else { + statuses := result.Data.([]*model.Status) + + for _, s := range statuses { + AddStatusCache(s) + statusMap[s.UserId] = s.Status + } + } + } + + // For the case where the user does not have a row in the Status table and cache + for _, userId := range missingUserIds { + if _, ok := statusMap[userId]; !ok { + statusMap[userId] = model.STATUS_OFFLINE + } + } + + return statusMap, nil +} + func SetStatusOnline(userId string, sessionId string, manual bool) { broadcast := false + var oldStatus string = model.STATUS_OFFLINE + var oldTime int64 = 0 + var oldManual bool = false var status *model.Status var err *model.AppError + if status, err = GetStatus(userId); err != nil { status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), ""} broadcast = true @@ -82,35 +156,45 @@ func SetStatusOnline(userId string, sessionId string, manual bool) { if status.Manual && !manual { return // manually set status always overrides non-manual one } + if status.Status != model.STATUS_ONLINE { broadcast = true } + + oldStatus = status.Status + oldTime = status.LastActivityAt + oldManual = status.Manual + status.Status = model.STATUS_ONLINE - status.Manual = false // for "online" there's no manually or auto set + status.Manual = false // for "online" there's no manual setting status.LastActivityAt = model.GetMillis() } AddStatusCache(status) - achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, model.GetMillis()) + // Only update the database if the status has changed, the status has been manually set, + // or enough time has passed since the previous action + if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.STATUS_MIN_UPDATE_TIME { + achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, status.LastActivityAt) - var schan store.StoreChannel - if broadcast { - schan = Srv.Store.Status().SaveOrUpdate(status) - } else { - schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt) - } + var schan store.StoreChannel + if broadcast { + schan = Srv.Store.Status().SaveOrUpdate(status) + } else { + schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt) + } - if result := <-achan; result.Err != nil { - l4g.Error(utils.T("api.status.last_activity.error"), userId, sessionId, result.Err) - } + if result := <-achan; result.Err != nil { + l4g.Error(utils.T("api.status.last_activity.error"), userId, sessionId, result.Err) + } - if result := <-schan; result.Err != nil { - l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) + if result := <-schan; result.Err != nil { + l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) + } } if broadcast { - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", "", nil) + event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) event.Add("status", model.STATUS_ONLINE) event.Add("user_id", status.UserId) go Publish(event) @@ -131,7 +215,7 @@ func SetStatusOffline(userId string, manual bool) { l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) } - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", "", nil) + event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) event.Add("status", model.STATUS_OFFLINE) event.Add("user_id", status.UserId) go Publish(event) @@ -168,15 +252,18 @@ func SetStatusAwayIfNeeded(userId string, manual bool) { l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err) } - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", "", nil) + event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil) event.Add("status", model.STATUS_AWAY) event.Add("user_id", status.UserId) go Publish(event) } func GetStatus(userId string) (*model.Status, *model.AppError) { - if status, ok := statusCache.Get(userId); ok { - return status.(*model.Status), nil + if result, ok := statusCache.Get(userId); ok { + status := result.(*model.Status) + statusCopy := &model.Status{} + *statusCopy = *status + return statusCopy, nil } if result := <-Srv.Store.Status().Get(userId); result.Err != nil { @@ -232,6 +319,10 @@ func SetActiveChannel(userId string, channelId string) *model.AppError { status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), channelId} } else { status.ActiveChannel = channelId + if !status.Manual { + status.Status = model.STATUS_ONLINE + } + status.LastActivityAt = model.GetMillis() } AddStatusCache(status) diff --git a/api/status_test.go b/api/status_test.go index f15c239db..ffc946817 100644 --- a/api/status_test.go +++ b/api/status_test.go @@ -4,13 +4,12 @@ package api import ( - "strings" - "testing" - "time" - "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" + "strings" + "testing" + "time" ) func TestStatuses(t *testing.T) { @@ -59,7 +58,7 @@ func TestStatuses(t *testing.T) { t.Fatal(err2) } - time.Sleep(300 * time.Millisecond) + time.Sleep(500 * time.Millisecond) WebSocketClient.GetStatuses() if resp := <-WebSocketClient.ResponseChannel; resp.Error != nil { @@ -76,6 +75,7 @@ func TestStatuses(t *testing.T) { } if status, ok := resp.Data[th.BasicUser2.Id]; !ok { + t.Log(len(resp.Data)) t.Fatal("should have had user status") } else if status != model.STATUS_ONLINE { t.Log(status) @@ -83,7 +83,55 @@ func TestStatuses(t *testing.T) { } } - SetStatusAwayIfNeeded(th.BasicUser2.Id, false) + WebSocketClient.GetStatusesByIds([]string{th.BasicUser2.Id}) + if resp := <-WebSocketClient.ResponseChannel; resp.Error != nil { + t.Fatal(resp.Error) + } else { + if resp.SeqReply != WebSocketClient.Sequence-1 { + t.Fatal("bad sequence number") + } + + for _, status := range resp.Data { + if status != model.STATUS_OFFLINE && status != model.STATUS_AWAY && status != model.STATUS_ONLINE { + t.Fatal("one of the statuses had an invalid value") + } + } + + if status, ok := resp.Data[th.BasicUser2.Id]; !ok { + t.Log(len(resp.Data)) + t.Fatal("should have had user status") + } else if status != model.STATUS_ONLINE { + t.Log(status) + t.Fatal("status should have been online") + } else if len(resp.Data) != 1 { + t.Fatal("only 1 status should be returned") + } + } + + WebSocketClient.GetStatusesByIds([]string{ruser2.Id, "junk"}) + if resp := <-WebSocketClient.ResponseChannel; resp.Error != nil { + t.Fatal(resp.Error) + } else { + if resp.SeqReply != WebSocketClient.Sequence-1 { + t.Fatal("bad sequence number") + } + + if len(resp.Data) != 2 { + t.Fatal("2 statuses should be returned") + } + } + + WebSocketClient.GetStatusesByIds([]string{}) + if resp := <-WebSocketClient.ResponseChannel; resp.Error == nil { + if resp.SeqReply != WebSocketClient.Sequence-1 { + t.Fatal("bad sequence number") + } + t.Fatal("should have errored - empty user ids") + } + + WebSocketClient2.Close() + + SetStatusAwayIfNeeded(th.BasicUser.Id, false) awayTimeout := *utils.Cfg.TeamSettings.UserStatusAwayTimeout defer func() { @@ -93,10 +141,9 @@ func TestStatuses(t *testing.T) { time.Sleep(1500 * time.Millisecond) - SetStatusAwayIfNeeded(th.BasicUser2.Id, false) - SetStatusAwayIfNeeded(th.BasicUser2.Id, false) + SetStatusAwayIfNeeded(th.BasicUser.Id, false) + SetStatusOnline(th.BasicUser.Id, "junk", false) - WebSocketClient2.Close() time.Sleep(300 * time.Millisecond) WebSocketClient.GetStatuses() @@ -115,20 +162,17 @@ func TestStatuses(t *testing.T) { stop := make(chan bool) onlineHit := false awayHit := false - offlineHit := false go func() { for { select { case resp := <-WebSocketClient.EventChannel: - if resp.Event == model.WEBSOCKET_EVENT_STATUS_CHANGE && resp.Data["user_id"].(string) == th.BasicUser2.Id { + if resp.Event == model.WEBSOCKET_EVENT_STATUS_CHANGE && resp.Data["user_id"].(string) == th.BasicUser.Id { status := resp.Data["status"].(string) if status == model.STATUS_ONLINE { onlineHit = true } else if status == model.STATUS_AWAY { awayHit = true - } else if status == model.STATUS_OFFLINE { - offlineHit = true } } case <-stop: @@ -147,11 +191,40 @@ func TestStatuses(t *testing.T) { if !awayHit { t.Fatal("didn't get away event") } - if !offlineHit { - t.Fatal("didn't get offline event") + + time.Sleep(500 * time.Millisecond) + + WebSocketClient.Close() +} + +func TestGetStatusesByIds(t *testing.T) { + th := Setup().InitBasic() + Client := th.BasicClient + + if result, err := Client.GetStatusesByIds([]string{th.BasicUser.Id}); err != nil { + t.Fatal(err) + } else { + statuses := result.Data.(map[string]string) + if len(statuses) != 1 { + t.Fatal("should only have 1 status") + } + } + + if result, err := Client.GetStatusesByIds([]string{th.BasicUser.Id, th.BasicUser2.Id, "junk"}); err != nil { + t.Fatal(err) + } else { + statuses := result.Data.(map[string]string) + if len(statuses) != 3 { + t.Fatal("should have 3 statuses") + } + } + + if _, err := Client.GetStatusesByIds([]string{}); err == nil { + t.Fatal("should have errored") } } +/* func TestSetActiveChannel(t *testing.T) { th := Setup().InitBasic() Client := th.BasicClient @@ -185,8 +258,9 @@ func TestSetActiveChannel(t *testing.T) { time.Sleep(500 * time.Millisecond) status, _ = GetStatus(th.BasicUser.Id) - // need to check if offline to catch race + need to check if offline to catch race if status.Status != model.STATUS_OFFLINE && status.ActiveChannel != th.BasicChannel.Id { t.Fatal("active channel should be set") } } +*/ diff --git a/api/team.go b/api/team.go index 2be7b8545..c57591d46 100644 --- a/api/team.go +++ b/api/team.go @@ -31,9 +31,12 @@ func InitTeam() { BaseRoutes.Teams.Handle("/all_team_listings", ApiUserRequired(GetAllTeamListings)).Methods("GET") BaseRoutes.Teams.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST") BaseRoutes.Teams.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST") - BaseRoutes.Teams.Handle("/members/{id:[A-Za-z0-9]+}", ApiUserRequired(getMembers)).Methods("GET") BaseRoutes.NeedTeam.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET") + BaseRoutes.NeedTeam.Handle("/stats", ApiUserRequired(getTeamStats)).Methods("GET") + BaseRoutes.NeedTeam.Handle("/members/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getTeamMembers)).Methods("GET") + BaseRoutes.NeedTeam.Handle("/members/ids", ApiUserRequired(getTeamMembersByIds)).Methods("POST") + BaseRoutes.NeedTeam.Handle("/members/{user_id:[A-Za-z0-9]+}", ApiUserRequired(getTeamMember)).Methods("GET") BaseRoutes.NeedTeam.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST") BaseRoutes.NeedTeam.Handle("/update_member_roles", ApiUserRequired(updateMemberRoles)).Methods("POST") @@ -305,7 +308,9 @@ func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { InvalidateCacheForUser(user.Id) // This message goes to everyone, so the teamId, channelId and userId are irrelevant - go Publish(model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil)) + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil) + message.Add("user_id", user.Id) + go Publish(message) return nil } @@ -335,11 +340,10 @@ func LeaveTeam(team *model.Team, user *model.User) *model.AppError { for _, channel := range channelMembers.Channels { if channel.Type != model.CHANNEL_DIRECT { + Srv.Store.User().InvalidateProfilesInChannelCache(channel.Id) if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil { return result.Err } - - InvalidateCacheForChannel(channel.Id) } } @@ -889,6 +893,25 @@ func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getTeamStats(c *Context, w http.ResponseWriter, r *http.Request) { + if c.Session.GetTeamByTeamId(c.TeamId) == nil { + if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { + return + } + } + + if result := <-Srv.Store.Team().GetMemberCount(c.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + stats := &model.TeamStats{} + stats.MemberCount = result.Data.(int64) + stats.TeamId = c.TeamId + w.Write([]byte(stats.ToJson())) + return + } +} + func importTeam(c *Context, w http.ResponseWriter, r *http.Request) { if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_IMPORT_TEAM) { c.Err = model.NewLocAppError("importTeam", "api.team.import_team.admin.app_error", nil, "userId="+c.Session.UserId) @@ -982,17 +1005,76 @@ func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { } } -func getMembers(c *Context, w http.ResponseWriter, r *http.Request) { +func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] - if c.Session.GetTeamByTeamId(id) == nil { - if !HasPermissionToTeamContext(c, id, model.PERMISSION_MANAGE_SYSTEM) { + offset, err := strconv.Atoi(params["offset"]) + if err != nil { + c.SetInvalidParam("getTeamMembers", "offset") + return + } + + limit, err := strconv.Atoi(params["limit"]) + if err != nil { + c.SetInvalidParam("getTeamMembers", "limit") + return + } + + if c.Session.GetTeamByTeamId(c.TeamId) == nil { + if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) { + return + } + } + + if result := <-Srv.Store.Team().GetMembers(c.TeamId, offset, limit); result.Err != nil { + c.Err = result.Err + return + } else { + members := result.Data.([]*model.TeamMember) + w.Write([]byte(model.TeamMembersToJson(members))) + return + } +} + +func getTeamMember(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + + userId := params["user_id"] + if len(userId) < 26 { + c.SetInvalidParam("getTeamMember", "user_id") + return + } + + if c.Session.GetTeamByTeamId(c.TeamId) == nil { + if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) { + return + } + } + + if result := <-Srv.Store.Team().GetMember(c.TeamId, userId); result.Err != nil { + c.Err = result.Err + return + } else { + member := result.Data.(model.TeamMember) + w.Write([]byte(member.ToJson())) + return + } +} + +func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) { + userIds := model.ArrayFromJson(r.Body) + if len(userIds) == 0 { + c.SetInvalidParam("getTeamMembersByIds", "user_ids") + return + } + + if c.Session.GetTeamByTeamId(c.TeamId) == nil { + if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) { return } } - if result := <-Srv.Store.Team().GetMembers(id); result.Err != nil { + if result := <-Srv.Store.Team().GetMembersByIds(c.TeamId, userIds); result.Err != nil { c.Err = result.Err return } else { diff --git a/api/team_test.go b/api/team_test.go index 1a66a826f..a58710145 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -560,14 +560,80 @@ func TestGetMyTeam(t *testing.T) { func TestGetTeamMembers(t *testing.T) { th := Setup().InitBasic() - if result, err := th.BasicClient.GetTeamMembers(th.BasicTeam.Id); err != nil { + if result, err := th.BasicClient.GetTeamMembers(th.BasicTeam.Id, 0, 100); err != nil { t.Fatal(err) } else { members := result.Data.([]*model.TeamMember) - if members == nil { + if len(members) == 0 { + t.Fatal("should have results") + } + } + + if _, err := th.BasicClient.GetTeamMembers("junk", 0, 100); err == nil { + t.Fatal("should have errored - bad team id") + } +} + +func TestGetTeamMember(t *testing.T) { + th := Setup().InitBasic() + + if result, err := th.BasicClient.GetTeamMember(th.BasicTeam.Id, th.BasicUser.Id); err != nil { + t.Fatal(err) + } else { + member := result.Data.(*model.TeamMember) + if member == nil { t.Fatal("should be valid") } } + + if _, err := th.BasicClient.GetTeamMember("junk", th.BasicUser.Id); err == nil { + t.Fatal("should have errored - bad team id") + } + + if _, err := th.BasicClient.GetTeamMember(th.BasicTeam.Id, ""); err == nil { + t.Fatal("should have errored - blank user id") + } + + if _, err := th.BasicClient.GetTeamMember(th.BasicTeam.Id, "junk"); err == nil { + t.Fatal("should have errored - bad user id") + } + + if _, err := th.BasicClient.GetTeamMember(th.BasicTeam.Id, "12345678901234567890123456"); err == nil { + t.Fatal("should have errored - bad user id") + } +} + +func TestGetTeamMembersByIds(t *testing.T) { + th := Setup().InitBasic() + + if result, err := th.BasicClient.GetTeamMembersByIds(th.BasicTeam.Id, []string{th.BasicUser.Id}); err != nil { + t.Fatal(err) + } else { + member := result.Data.([]*model.TeamMember)[0] + if member.UserId != th.BasicUser.Id { + t.Fatal("user id did not match") + } + if member.TeamId != th.BasicTeam.Id { + t.Fatal("team id did not match") + } + } + + if result, err := th.BasicClient.GetTeamMembersByIds(th.BasicTeam.Id, []string{th.BasicUser.Id, th.BasicUser2.Id, model.NewId()}); err != nil { + t.Fatal(err) + } else { + members := result.Data.([]*model.TeamMember) + if len(members) != 2 { + t.Fatal("length should have been 2") + } + } + + if _, err := th.BasicClient.GetTeamMembersByIds("junk", []string{th.BasicUser.Id}); err == nil { + t.Fatal("should have errored - bad team id") + } + + if _, err := th.BasicClient.GetTeamMembersByIds(th.BasicTeam.Id, []string{}); err == nil { + t.Fatal("should have errored - empty user ids") + } } func TestUpdateTeamMemberRoles(t *testing.T) { @@ -632,3 +698,42 @@ func TestUpdateTeamMemberRoles(t *testing.T) { t.Fatal("Should have worked, user is team admin") } } + +func TestGetTeamStats(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + Client := th.BasicClient + + if result, err := th.SystemAdminClient.GetTeamStats(th.BasicTeam.Id); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.TeamStats).MemberCount != 2 { + t.Fatal("wrong count") + } + } + + if result, err := th.SystemAdminClient.GetTeamStats("junk"); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.TeamStats).MemberCount != 0 { + t.Fatal("wrong count") + } + } + + if result, err := th.SystemAdminClient.GetTeamStats(th.BasicTeam.Id); err != nil { + t.Fatal(err) + } else { + if result.Data.(*model.TeamStats).MemberCount != 2 { + t.Fatal("wrong count") + } + } + + user := model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} + ruser, _ := Client.CreateUser(&user, "") + store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id)) + + Client.Login(user.Email, user.Password) + + if _, err := Client.GetTeamStats(th.BasicTeam.Id); err == nil { + t.Fatal("should have errored - not on team") + } +} diff --git a/api/user.go b/api/user.go index 5f7e3ad10..12e57a33f 100644 --- a/api/user.go +++ b/api/user.go @@ -53,9 +53,15 @@ func InitUser() { BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") BaseRoutes.Users.Handle("/me", ApiUserRequired(getMe)).Methods("GET") BaseRoutes.Users.Handle("/initial_load", ApiAppHandler(getInitialLoad)).Methods("GET") - BaseRoutes.Users.Handle("/direct_profiles", ApiUserRequired(getDirectProfiles)).Methods("GET") - BaseRoutes.Users.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") - BaseRoutes.Users.Handle("/profiles_for_dm_list/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfilesForDirectMessageList)).Methods("GET") + BaseRoutes.Users.Handle("/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") + BaseRoutes.NeedTeam.Handle("/users/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getProfilesInTeam)).Methods("GET") + BaseRoutes.NeedChannel.Handle("/users/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getProfilesInChannel)).Methods("GET") + BaseRoutes.NeedChannel.Handle("/users/not_in_channel/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getProfilesNotInChannel)).Methods("GET") + BaseRoutes.Users.Handle("/search", ApiUserRequired(searchUsers)).Methods("POST") + BaseRoutes.Users.Handle("/ids", ApiUserRequired(getProfilesByIds)).Methods("POST") + + BaseRoutes.NeedTeam.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInTeam)).Methods("GET") + BaseRoutes.NeedChannel.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInChannel)).Methods("GET") BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST") BaseRoutes.Users.Handle("/generate_mfa_qr", ApiUserRequiredTrustRequester(generateMfaQrCode)).Methods("GET") @@ -270,7 +276,9 @@ func CreateUser(user *model.User) (*model.User, *model.AppError) { ruser.Sanitize(map[string]bool{}) // This message goes to everyone, so the teamId, channelId and userId are irrelevant - go Publish(model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil)) + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil) + message.Add("user_id", ruser.Id) + go Publish(message) return ruser, nil } @@ -379,7 +387,7 @@ func sendWelcomeEmail(c *Context, userId string, email string, siteURL string, v func addDirectChannels(teamId string, user *model.User) { var profiles map[string]*model.User - if result := <-Srv.Store.User().GetProfiles(teamId); result.Err != nil { + if result := <-Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil { l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error()) return } else { @@ -875,7 +883,6 @@ func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) { uchan := Srv.Store.User().Get(c.Session.UserId) pchan := Srv.Store.Preference().GetAll(c.Session.UserId) tchan := Srv.Store.Team().GetTeamsByUserId(c.Session.UserId) - dpchan := Srv.Store.User().GetDirectProfiles(c.Session.UserId) il.TeamMembers = c.Session.TeamMembers @@ -904,19 +911,6 @@ func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) { team.Sanitize() } } - - if dp := <-dpchan; dp.Err != nil { - c.Err = dp.Err - return - } else { - profiles := dp.Data.(map[string]*model.User) - - for k, p := range profiles { - profiles[k] = sanitizeProfile(c, p) - } - - il.DirectProfiles = profiles - } } if cchan != nil { @@ -960,25 +954,27 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) { } } -func getProfilesForDirectMessageList(c *Context, w http.ResponseWriter, r *http.Request) { +func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] - var pchan store.StoreChannel + offset, err := strconv.Atoi(params["offset"]) + if err != nil { + c.SetInvalidParam("getProfiles", "offset") + return + } - if *utils.Cfg.TeamSettings.RestrictDirectMessage == model.DIRECT_MESSAGE_TEAM { - if c.Session.GetTeamByTeamId(id) == nil { - if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { - return - } - } + limit, err := strconv.Atoi(params["limit"]) + if err != nil { + c.SetInvalidParam("getProfiles", "limit") + return + } - pchan = Srv.Store.User().GetProfiles(id) - } else { - pchan = Srv.Store.User().GetAllProfiles() + etag := (<-Srv.Store.User().GetEtagForAllProfiles()).Data.(string) + if HandleEtag(etag, w, r) { + return } - if result := <-pchan; result.Err != nil { + if result := <-Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -988,26 +984,39 @@ func getProfilesForDirectMessageList(c *Context, w http.ResponseWriter, r *http. profiles[k] = sanitizeProfile(c, p) } + w.Header().Set(model.HEADER_ETAG_SERVER, etag) w.Write([]byte(model.UserMapToJson(profiles))) } } -func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { +func getProfilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - id := params["id"] + teamId := params["team_id"] - if c.Session.GetTeamByTeamId(id) == nil { + if c.Session.GetTeamByTeamId(teamId) == nil { if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { return } } - etag := (<-Srv.Store.User().GetEtagForProfiles(id)).Data.(string) + offset, err := strconv.Atoi(params["offset"]) + if err != nil { + c.SetInvalidParam("getProfilesInTeam", "offset") + return + } + + limit, err := strconv.Atoi(params["limit"]) + if err != nil { + c.SetInvalidParam("getProfilesInTeam", "limit") + return + } + + etag := (<-Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string) if HandleEtag(etag, w, r) { return } - if result := <-Srv.Store.User().GetProfiles(id); result.Err != nil { + if result := <-Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -1022,13 +1031,73 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) { } } -func getDirectProfiles(c *Context, w http.ResponseWriter, r *http.Request) { - etag := (<-Srv.Store.User().GetEtagForDirectProfiles(c.Session.UserId)).Data.(string) - if HandleEtag(etag, w, r) { +func getProfilesInChannel(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + channelId := params["channel_id"] + + if c.Session.GetTeamByTeamId(c.TeamId) == nil { + if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { + return + } + } + + if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { + return + } + + offset, err := strconv.Atoi(params["offset"]) + if err != nil { + c.SetInvalidParam("getProfiles", "offset") return } - if result := <-Srv.Store.User().GetDirectProfiles(c.Session.UserId); result.Err != nil { + limit, err := strconv.Atoi(params["limit"]) + if err != nil { + c.SetInvalidParam("getProfiles", "limit") + return + } + + if result := <-Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil { + c.Err = result.Err + return + } else { + profiles := result.Data.(map[string]*model.User) + + for k, p := range profiles { + profiles[k] = sanitizeProfile(c, p) + } + + w.Write([]byte(model.UserMapToJson(profiles))) + } +} + +func getProfilesNotInChannel(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + channelId := params["channel_id"] + + if c.Session.GetTeamByTeamId(c.TeamId) == nil { + if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { + return + } + } + + if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { + return + } + + offset, err := strconv.Atoi(params["offset"]) + if err != nil { + c.SetInvalidParam("getProfiles", "offset") + return + } + + limit, err := strconv.Atoi(params["limit"]) + if err != nil { + c.SetInvalidParam("getProfiles", "limit") + return + } + + if result := <-Srv.Store.User().GetProfilesNotInChannel(c.TeamId, channelId, offset, limit); result.Err != nil { c.Err = result.Err return } else { @@ -1038,7 +1107,6 @@ func getDirectProfiles(c *Context, w http.ResponseWriter, r *http.Request) { profiles[k] = sanitizeProfile(c, p) } - w.Header().Set(model.HEADER_ETAG_SERVER, etag) w.Write([]byte(model.UserMapToJson(profiles))) } } @@ -2522,3 +2590,152 @@ func sanitizeProfile(c *Context, user *model.User) *model.User { return user } + +func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + term := props["term"] + if len(term) == 0 { + c.SetInvalidParam("searchUsers", "term") + return + } + + teamId := props["team_id"] + inChannelId := props["in_channel"] + notInChannelId := props["not_in_channel"] + + if inChannelId != "" && !HasPermissionToChannelContext(c, inChannelId, model.PERMISSION_READ_CHANNEL) { + return + } + + if notInChannelId != "" && !HasPermissionToChannelContext(c, notInChannelId, model.PERMISSION_READ_CHANNEL) { + return + } + + var uchan store.StoreChannel + if inChannelId != "" { + uchan = Srv.Store.User().SearchInChannel(inChannelId, term, store.USER_SEARCH_TYPE_USERNAME) + } else if notInChannelId != "" { + uchan = Srv.Store.User().SearchNotInChannel(teamId, notInChannelId, term, store.USER_SEARCH_TYPE_USERNAME) + } else { + uchan = Srv.Store.User().Search(teamId, term, store.USER_SEARCH_TYPE_USERNAME) + } + + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return + } else { + profiles := result.Data.([]*model.User) + + for _, p := range profiles { + sanitizeProfile(c, p) + } + + w.Write([]byte(model.UserListToJson(profiles))) + } +} + +func getProfilesByIds(c *Context, w http.ResponseWriter, r *http.Request) { + userIds := model.ArrayFromJson(r.Body) + + if len(userIds) == 0 { + c.SetInvalidParam("getProfilesByIds", "user_ids") + return + } + + if result := <-Srv.Store.User().GetProfileByIds(userIds); result.Err != nil { + c.Err = result.Err + return + } else { + profiles := result.Data.(map[string]*model.User) + + for _, p := range profiles { + sanitizeProfile(c, p) + } + + w.Write([]byte(model.UserMapToJson(profiles))) + } +} + +func autocompleteUsersInChannel(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + channelId := params["channel_id"] + teamId := params["team_id"] + + term := r.URL.Query().Get("term") + + if c.Session.GetTeamByTeamId(teamId) == nil { + if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { + return + } + } + + if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { + return + } + + uchan := Srv.Store.User().SearchInChannel(channelId, term, store.USER_SEARCH_TYPE_ALL) + nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, store.USER_SEARCH_TYPE_ALL) + + autocomplete := &model.UserAutocompleteInChannel{} + + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return + } else { + profiles := result.Data.([]*model.User) + + for _, p := range profiles { + sanitizeProfile(c, p) + } + + autocomplete.InChannel = profiles + } + + if result := <-nuchan; result.Err != nil { + c.Err = result.Err + return + } else { + profiles := result.Data.([]*model.User) + + for _, p := range profiles { + sanitizeProfile(c, p) + } + + autocomplete.OutOfChannel = profiles + } + + w.Write([]byte(autocomplete.ToJson())) +} + +func autocompleteUsersInTeam(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + teamId := params["team_id"] + + term := r.URL.Query().Get("term") + + if c.Session.GetTeamByTeamId(teamId) == nil { + if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { + return + } + } + + uchan := Srv.Store.User().Search(teamId, term, store.USER_SEARCH_TYPE_ALL) + + autocomplete := &model.UserAutocompleteInTeam{} + + if result := <-uchan; result.Err != nil { + c.Err = result.Err + return + } else { + profiles := result.Data.([]*model.User) + + for _, p := range profiles { + sanitizeProfile(c, p) + } + + autocomplete.InTeam = profiles + } + + w.Write([]byte(autocomplete.ToJson())) +} diff --git a/api/user_test.go b/api/user_test.go index 3800f5d9e..3b6fcb1fb 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -435,7 +435,7 @@ func TestGetUser(t *testing.T) { } } - if userMap, err := Client.GetProfiles(rteam.Data.(*model.Team).Id, ""); err != nil { + if userMap, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 0, 100, ""); err != nil { t.Fatal(err) } else if len(userMap.Data.(map[string]*model.User)) != 2 { t.Fatal("should have been 2") @@ -444,7 +444,7 @@ func TestGetUser(t *testing.T) { } else { // test etag caching - if cache_result, err := Client.GetProfiles(rteam.Data.(*model.Team).Id, userMap.Etag); err != nil { + if cache_result, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 0, 100, userMap.Etag); err != nil { t.Fatal(err) } else if cache_result.Data.(map[string]*model.User) != nil { t.Log(cache_result.Data) @@ -452,7 +452,25 @@ func TestGetUser(t *testing.T) { } } - if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err == nil { + if userMap, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 0, 1, ""); err != nil { + t.Fatal(err) + } else if len(userMap.Data.(map[string]*model.User)) != 1 { + t.Fatal("should have been 1") + } + + if userMap, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 1, 1, ""); err != nil { + t.Fatal(err) + } else if len(userMap.Data.(map[string]*model.User)) != 1 { + t.Fatal("should have been 1") + } + + if userMap, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 10, 10, ""); err != nil { + t.Fatal(err) + } else if len(userMap.Data.(map[string]*model.User)) != 0 { + t.Fatal("should have been 0") + } + + if _, err := Client.GetProfilesInTeam(rteam2.Data.(*model.Team).Id, 0, 100, ""); err == nil { t.Fatal("shouldn't have access") } @@ -468,12 +486,12 @@ func TestGetUser(t *testing.T) { Client.Login(user.Email, "passwd1") - if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err != nil { + if _, err := Client.GetProfilesInTeam(rteam2.Data.(*model.Team).Id, 0, 100, ""); err != nil { t.Fatal(err) } } -func TestGetDirectProfiles(t *testing.T) { +func TestGetProfiles(t *testing.T) { th := Setup().InitBasic() th.BasicClient.Must(th.BasicClient.CreateDirectChannel(th.BasicUser2.Id)) @@ -485,41 +503,42 @@ func TestGetDirectProfiles(t *testing.T) { utils.Cfg.PrivacySettings.ShowEmailAddress = true - if result, err := th.BasicClient.GetDirectProfiles(""); err != nil { + if result, err := th.BasicClient.GetProfiles(0, 100, ""); err != nil { t.Fatal(err) } else { users := result.Data.(map[string]*model.User) - if len(users) != 1 { + if len(users) < 1 { t.Fatal("map was wrong length") } - if users[th.BasicUser2.Id] == nil { - t.Fatal("missing expected user") - } - for _, user := range users { if user.Email == "" { t.Fatal("problem with show email") } } + + // test etag caching + if cache_result, err := th.BasicClient.GetProfiles(0, 100, result.Etag); err != nil { + t.Fatal(err) + } else if cache_result.Data.(map[string]*model.User) != nil { + t.Log(cache_result.Etag) + t.Log(result.Etag) + t.Fatal("cache should be empty") + } } utils.Cfg.PrivacySettings.ShowEmailAddress = false - if result, err := th.BasicClient.GetDirectProfiles(""); err != nil { + if result, err := th.BasicClient.GetProfiles(0, 100, ""); err != nil { t.Fatal(err) } else { users := result.Data.(map[string]*model.User) - if len(users) != 1 { + if len(users) < 1 { t.Fatal("map was wrong length") } - if users[th.BasicUser2.Id] == nil { - t.Fatal("missing expected user") - } - for _, user := range users { if user.Email != "" { t.Fatal("problem with show email") @@ -528,11 +547,9 @@ func TestGetDirectProfiles(t *testing.T) { } } -func TestGetProfilesForDirectMessageList(t *testing.T) { +func TestGetProfilesByIds(t *testing.T) { th := Setup().InitBasic() - th.BasicClient.Must(th.BasicClient.CreateDirectChannel(th.BasicUser2.Id)) - prevShowEmail := utils.Cfg.PrivacySettings.ShowEmailAddress defer func() { utils.Cfg.PrivacySettings.ShowEmailAddress = prevShowEmail @@ -540,12 +557,12 @@ func TestGetProfilesForDirectMessageList(t *testing.T) { utils.Cfg.PrivacySettings.ShowEmailAddress = true - if result, err := th.BasicClient.GetProfilesForDirectMessageList(th.BasicTeam.Id); err != nil { + if result, err := th.BasicClient.GetProfilesByIds([]string{th.BasicUser.Id}); err != nil { t.Fatal(err) } else { users := result.Data.(map[string]*model.User) - if len(users) < 1 { + if len(users) != 1 { t.Fatal("map was wrong length") } @@ -558,12 +575,12 @@ func TestGetProfilesForDirectMessageList(t *testing.T) { utils.Cfg.PrivacySettings.ShowEmailAddress = false - if result, err := th.BasicClient.GetProfilesForDirectMessageList(th.BasicTeam.Id); err != nil { + if result, err := th.BasicClient.GetProfilesByIds([]string{th.BasicUser.Id}); err != nil { t.Fatal(err) } else { users := result.Data.(map[string]*model.User) - if len(users) < 1 { + if len(users) != 1 { t.Fatal("map was wrong length") } @@ -573,6 +590,16 @@ func TestGetProfilesForDirectMessageList(t *testing.T) { } } } + + if result, err := th.BasicClient.GetProfilesByIds([]string{th.BasicUser.Id, th.BasicUser2.Id}); err != nil { + t.Fatal(err) + } else { + users := result.Data.(map[string]*model.User) + + if len(users) != 2 { + t.Fatal("map was wrong length") + } + } } func TestGetAudits(t *testing.T) { @@ -1837,3 +1864,366 @@ func TestUserTyping(t *testing.T) { t.Fatal("did not receive typing event") } } + +func TestGetProfilesInChannel(t *testing.T) { + th := Setup().InitBasic() + Client := th.BasicClient + + prevShowEmail := utils.Cfg.PrivacySettings.ShowEmailAddress + defer func() { + utils.Cfg.PrivacySettings.ShowEmailAddress = prevShowEmail + }() + + utils.Cfg.PrivacySettings.ShowEmailAddress = true + + if result, err := Client.GetProfilesInChannel(th.BasicChannel.Id, 0, 100, ""); err != nil { + t.Fatal(err) + } else { + users := result.Data.(map[string]*model.User) + + if len(users) < 1 { + t.Fatal("map was wrong length") + } + + for _, user := range users { + if user.Email == "" { + t.Fatal("problem with show email") + } + } + } + + th.LoginBasic2() + + if _, err := Client.GetProfilesInChannel(th.BasicChannel.Id, 0, 100, ""); err == nil { + t.Fatal("should not have access") + } + + Client.Must(Client.JoinChannel(th.BasicChannel.Id)) + + utils.Cfg.PrivacySettings.ShowEmailAddress = false + + if result, err := Client.GetProfilesInChannel(th.BasicChannel.Id, 0, 100, ""); err != nil { + t.Fatal(err) + } else { + users := result.Data.(map[string]*model.User) + + if len(users) < 1 { + t.Fatal("map was wrong length") + } + + found := false + for _, user := range users { + if user.Email != "" { + t.Fatal("problem with show email") + } + if user.Id == th.BasicUser2.Id { + found = true + } + } + + if !found { + t.Fatal("should have found profile") + } + } + + user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} + Client.Must(Client.CreateUser(&user, "")) + + Client.Login(user.Email, "passwd1") + Client.SetTeamId("junk") + + if _, err := Client.GetProfilesInChannel(th.BasicChannel.Id, 0, 100, ""); err == nil { + t.Fatal("should not have access") + } +} + +func TestGetProfilesNotInChannel(t *testing.T) { + th := Setup().InitBasic() + Client := th.BasicClient + + prevShowEmail := utils.Cfg.PrivacySettings.ShowEmailAddress + defer func() { + utils.Cfg.PrivacySettings.ShowEmailAddress = prevShowEmail + }() + + utils.Cfg.PrivacySettings.ShowEmailAddress = true + + if result, err := Client.GetProfilesNotInChannel(th.BasicChannel.Id, 0, 100, ""); err != nil { + t.Fatal(err) + } else { + users := result.Data.(map[string]*model.User) + + if len(users) < 1 { + t.Fatal("map was wrong length") + } + + found := false + for _, user := range users { + if user.Email == "" { + t.Fatal("problem with show email") + } + if user.Id == th.BasicUser2.Id { + found = true + } + } + + if !found { + t.Fatal("should have found profile") + } + } + + user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + LinkUserToTeam(user, th.BasicTeam) + + th.LoginBasic2() + + if _, err := Client.GetProfilesNotInChannel(th.BasicChannel.Id, 0, 100, ""); err == nil { + t.Fatal("should not have access") + } + + Client.Must(Client.JoinChannel(th.BasicChannel.Id)) + + utils.Cfg.PrivacySettings.ShowEmailAddress = false + + if result, err := Client.GetProfilesNotInChannel(th.BasicChannel.Id, 0, 100, ""); err != nil { + t.Fatal(err) + } else { + users := result.Data.(map[string]*model.User) + + if len(users) < 1 { + t.Fatal("map was wrong length") + } + + found := false + for _, user := range users { + if user.Email != "" { + t.Fatal("problem with show email") + } + if user.Id == th.BasicUser2.Id { + found = true + } + } + + if found { + t.Fatal("should not have found profile") + } + } + + user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"} + Client.Must(Client.CreateUser(&user2, "")) + + Client.Login(user2.Email, "passwd1") + Client.SetTeamId(th.BasicTeam.Id) + + if _, err := Client.GetProfilesNotInChannel(th.BasicChannel.Id, 0, 100, ""); err == nil { + t.Fatal("should not have access") + } +} + +func TestSearchUsers(t *testing.T) { + th := Setup().InitBasic() + Client := th.BasicClient + + if result, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + found := false + for _, user := range users { + if user.Id == th.BasicUser.Id { + found = true + } + } + + if !found { + t.Fatal("should have found profile") + } + } + + if result, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{"in_channel": th.BasicChannel.Id}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + if len(users) != 1 { + t.Fatal("map was wrong length") + } + + found := false + for _, user := range users { + if user.Id == th.BasicUser.Id { + found = true + } + } + + if !found { + t.Fatal("should have found profile") + } + } + + if result, err := Client.SearchUsers(th.BasicUser2.Username, "", map[string]string{"not_in_channel": th.BasicChannel.Id}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + if len(users) != 1 { + t.Fatal("map was wrong length") + } + + found1 := false + found2 := false + for _, user := range users { + if user.Id == th.BasicUser.Id { + found1 = true + } else if user.Id == th.BasicUser2.Id { + found2 = true + } + } + + if found1 { + t.Fatal("should not have found profile") + } + if !found2 { + t.Fatal("should have found profile") + } + } + + if result, err := Client.SearchUsers(th.BasicUser2.Username, th.BasicTeam.Id, map[string]string{"not_in_channel": th.BasicChannel.Id}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + if len(users) != 1 { + t.Fatal("map was wrong length") + } + + found1 := false + found2 := false + for _, user := range users { + if user.Id == th.BasicUser.Id { + found1 = true + } else if user.Id == th.BasicUser2.Id { + found2 = true + } + } + + if found1 { + t.Fatal("should not have found profile") + } + if !found2 { + t.Fatal("should have found profile") + } + } + + if result, err := Client.SearchUsers(th.BasicUser.Username, "junk", map[string]string{"not_in_channel": th.BasicChannel.Id}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + if len(users) != 0 { + t.Fatal("map was wrong length") + } + } + + th.LoginBasic2() + + if result, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{}); err != nil { + t.Fatal(err) + } else { + users := result.Data.([]*model.User) + + found := false + for _, user := range users { + if user.Id == th.BasicUser.Id { + found = true + } + } + + if !found { + t.Fatal("should have found profile") + } + } + + if _, err := Client.SearchUsers("", "", map[string]string{}); err == nil { + t.Fatal("should have errored - blank term") + } + + if _, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{"in_channel": th.BasicChannel.Id}); err == nil { + t.Fatal("should not have access") + } + + if _, err := Client.SearchUsers(th.BasicUser.Username, "", map[string]string{"not_in_channel": th.BasicChannel.Id}); err == nil { + t.Fatal("should not have access") + } +} + +func TestAutocompleteUsers(t *testing.T) { + th := Setup().InitBasic() + Client := th.BasicClient + + if result, err := Client.AutocompleteUsersInTeam(th.BasicUser.Username); err != nil { + t.Fatal(err) + } else { + autocomplete := result.Data.(*model.UserAutocompleteInTeam) + if len(autocomplete.InTeam) != 1 { + t.Fatal("should have returned 1 user in") + } + } + + if result, err := Client.AutocompleteUsersInTeam(th.BasicUser.Username[0:5]); err != nil { + t.Fatal(err) + } else { + autocomplete := result.Data.(*model.UserAutocompleteInTeam) + if len(autocomplete.InTeam) < 1 { + t.Fatal("should have returned at least 1 user in") + } + } + + if result, err := Client.AutocompleteUsersInChannel(th.BasicUser.Username, th.BasicChannel.Id); err != nil { + t.Fatal(err) + } else { + autocomplete := result.Data.(*model.UserAutocompleteInChannel) + if len(autocomplete.InChannel) != 1 { + t.Fatal("should have returned 1 user in") + } + if len(autocomplete.OutOfChannel) != 0 { + t.Fatal("should have returned no users out") + } + } + + if result, err := Client.AutocompleteUsersInChannel("", th.BasicChannel.Id); err != nil { + t.Fatal(err) + } else { + autocomplete := result.Data.(*model.UserAutocompleteInChannel) + if len(autocomplete.InChannel) != 1 && autocomplete.InChannel[0].Id != th.BasicUser2.Id { + t.Fatal("should have returned at 1 user in") + } + if len(autocomplete.OutOfChannel) != 1 && autocomplete.OutOfChannel[0].Id != th.BasicUser2.Id { + t.Fatal("should have returned 1 user out") + } + } + + if result, err := Client.AutocompleteUsersInTeam(""); err != nil { + t.Fatal(err) + } else { + autocomplete := result.Data.(*model.UserAutocompleteInTeam) + if len(autocomplete.InTeam) != 2 { + t.Fatal("should have returned 2 users in") + } + } + + if _, err := Client.AutocompleteUsersInChannel("", "junk"); err == nil { + t.Fatal("should have errored - bad channel id") + } + + Client.SetTeamId("junk") + if _, err := Client.AutocompleteUsersInChannel("", th.BasicChannel.Id); err == nil { + t.Fatal("should have errored - bad team id") + } + + if _, err := Client.AutocompleteUsersInTeam(""); err == nil { + t.Fatal("should have errored - bad team id") + } +} diff --git a/api/web_conn.go b/api/web_conn.go index f4bd493bb..7f3c1f875 100644 --- a/api/web_conn.go +++ b/api/web_conn.go @@ -4,54 +4,52 @@ package api import ( + "fmt" "time" "github.com/mattermost/platform/model" + l4g "github.com/alecthomas/log4go" "github.com/gorilla/websocket" goi18n "github.com/nicksnyder/go-i18n/i18n" ) const ( - WRITE_WAIT = 10 * time.Second - PONG_WAIT = 60 * time.Second - PING_PERIOD = (PONG_WAIT * 9) / 10 - MAX_SIZE = 512 - REDIS_WAIT = 60 * time.Second + WRITE_WAIT = 30 * time.Second + PONG_WAIT = 100 * time.Second + PING_PERIOD = (PONG_WAIT * 6) / 10 ) type WebConn struct { - WebSocket *websocket.Conn - Send chan model.WebSocketMessage - SessionToken string - UserId string - T goi18n.TranslateFunc - Locale string - isMemberOfChannel map[string]bool - isMemberOfTeam map[string]bool + WebSocket *websocket.Conn + Send chan model.WebSocketMessage + SessionToken string + UserId string + T goi18n.TranslateFunc + Locale string + AllChannelMembers map[string]string + LastAllChannelMembersTime int64 } func NewWebConn(c *Context, ws *websocket.Conn) *WebConn { go SetStatusOnline(c.Session.UserId, c.Session.Id, false) return &WebConn{ - Send: make(chan model.WebSocketMessage, 64), - WebSocket: ws, - UserId: c.Session.UserId, - SessionToken: c.Session.Token, - T: c.T, - Locale: c.Locale, - isMemberOfChannel: make(map[string]bool), - isMemberOfTeam: make(map[string]bool), + Send: make(chan model.WebSocketMessage, 256), + WebSocket: ws, + UserId: c.Session.UserId, + SessionToken: c.Session.Token, + T: c.T, + Locale: c.Locale, } } func (c *WebConn) readPump() { defer func() { - hub.Unregister(c) + HubUnregister(c) c.WebSocket.Close() }() - c.WebSocket.SetReadLimit(MAX_SIZE) + c.WebSocket.SetReadLimit(SOCKET_MAX_MESSAGE_SIZE_KB) c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) c.WebSocket.SetPongHandler(func(string) error { c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) @@ -62,6 +60,13 @@ func (c *WebConn) readPump() { for { var req model.WebSocketRequest if err := c.WebSocket.ReadJSON(&req); err != nil { + // browsers will appear as CloseNoStatusReceived + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + l4g.Debug(fmt.Sprintf("websocket.read: client side closed socket userId=%v", c.UserId)) + } else { + l4g.Debug(fmt.Sprintf("websocket.read: cannot read, closing websocket for userId=%v error=%v", c.UserId, err.Error())) + } + return } else { BaseRoutes.WebSocket.ServeWebSocket(c, &req) @@ -87,63 +92,97 @@ func (c *WebConn) writePump() { } c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) - if err := c.WebSocket.WriteJSON(msg); err != nil { + if err := c.WebSocket.WriteMessage(websocket.TextMessage, msg.GetPreComputeJson()); err != nil { + // browsers will appear as CloseNoStatusReceived + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + l4g.Debug(fmt.Sprintf("websocket.send: client side closed socket userId=%v", c.UserId)) + } else { + l4g.Debug(fmt.Sprintf("websocket.send: cannot send, closing websocket for userId=%v, error=%v", c.UserId, err.Error())) + } + return } case <-ticker.C: c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) if err := c.WebSocket.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + // browsers will appear as CloseNoStatusReceived + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + l4g.Debug(fmt.Sprintf("websocket.ticker: client side closed socket userId=%v", c.UserId)) + } else { + l4g.Debug(fmt.Sprintf("websocket.ticker: cannot read, closing websocket for userId=%v error=%v", c.UserId, err.Error())) + } + return } } } } -func (c *WebConn) InvalidateCache() { - c.isMemberOfTeam = make(map[string]bool) - c.isMemberOfChannel = make(map[string]bool) -} +func (webCon *WebConn) InvalidateCache() { + webCon.AllChannelMembers = nil + webCon.LastAllChannelMembersTime = 0 -func (c *WebConn) InvalidateCacheForChannel(channelId string) { - delete(c.isMemberOfChannel, channelId) } -func (c *WebConn) IsMemberOfTeam(teamId string) bool { - isMember, ok := c.isMemberOfTeam[teamId] - if !ok { - session := GetSession(c.SessionToken) - if session == nil { - isMember = false - c.isMemberOfTeam[teamId] = isMember - } else { - member := session.GetTeamByTeamId(teamId) +func (webCon *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool { + // If the event is destined to a specific user + if len(msg.Broadcast.UserId) > 0 && webCon.UserId != msg.Broadcast.UserId { + return false + } + + // if the user is omitted don't send the message + if len(msg.Broadcast.OmitUsers) > 0 { + if _, ok := msg.Broadcast.OmitUsers[webCon.UserId]; ok { + return false + } + } - if member != nil { - isMember = true - c.isMemberOfTeam[teamId] = isMember + // Only report events to users who are in the channel for the event + if len(msg.Broadcast.ChannelId) > 0 { + + if model.GetMillis()-webCon.LastAllChannelMembersTime > 1000*60*15 { // 15 minutes + webCon.AllChannelMembers = nil + webCon.LastAllChannelMembersTime = 0 + } + + if webCon.AllChannelMembers == nil { + if result := <-Srv.Store.Channel().GetAllChannelMembersForUser(webCon.UserId, true); result.Err != nil { + l4g.Error("webhub.shouldSendEvent: " + result.Err.Error()) + return false } else { - isMember = true - c.isMemberOfTeam[teamId] = isMember + webCon.AllChannelMembers = result.Data.(map[string]string) + webCon.LastAllChannelMembersTime = model.GetMillis() } + } + if _, ok := webCon.AllChannelMembers[msg.Broadcast.ChannelId]; ok { + return true + } else { + return false } } - return isMember + // Only report events to users who are in the team for the event + if len(msg.Broadcast.TeamId) > 0 { + return webCon.IsMemberOfTeam(msg.Broadcast.TeamId) + + } + + return true } -func (c *WebConn) IsMemberOfChannel(channelId string) bool { - isMember, ok := c.isMemberOfChannel[channelId] - if !ok { - if cresult := <-Srv.Store.Channel().GetMember(channelId, c.UserId); cresult.Err != nil { - isMember = false - c.isMemberOfChannel[channelId] = isMember +func (webCon *WebConn) IsMemberOfTeam(teamId string) bool { + session := GetSession(webCon.SessionToken) + if session == nil { + return false + } else { + member := session.GetTeamByTeamId(teamId) + + if member != nil { + return true } else { - isMember = true - c.isMemberOfChannel[channelId] = isMember + return false } } - - return isMember } diff --git a/api/web_hub.go b/api/web_hub.go index 4a9719d80..5f480880e 100644 --- a/api/web_hub.go +++ b/api/web_hub.go @@ -5,6 +5,8 @@ package api import ( "fmt" + "hash/fnv" + "runtime" l4g "github.com/alecthomas/log4go" @@ -14,27 +16,77 @@ import ( ) type Hub struct { - connections map[*WebConn]bool - register chan *WebConn - unregister chan *WebConn - broadcast chan *model.WebSocketEvent - stop chan string - invalidateUser chan string - invalidateChannel chan string + connections map[*WebConn]bool + register chan *WebConn + unregister chan *WebConn + broadcast chan *model.WebSocketEvent + stop chan string + invalidateUser chan string } -var hub = &Hub{ - register: make(chan *WebConn), - unregister: make(chan *WebConn), - connections: make(map[*WebConn]bool), - broadcast: make(chan *model.WebSocketEvent), - stop: make(chan string), - invalidateUser: make(chan string), - invalidateChannel: make(chan string), +var hubs []*Hub = make([]*Hub, 0) + +func NewWebHub() *Hub { + return &Hub{ + register: make(chan *WebConn), + unregister: make(chan *WebConn), + connections: make(map[*WebConn]bool, model.SESSION_CACHE_SIZE), + broadcast: make(chan *model.WebSocketEvent, 4096), + stop: make(chan string), + invalidateUser: make(chan string), + } +} + +func TotalWebsocketConnections() int { + // XXX TODO FIXME, this is racy and needs to be fixed + count := 0 + for _, hub := range hubs { + count = count + len(hub.connections) + } + + return count +} + +func HubStart() { + l4g.Info(utils.T("api.web_hub.start.starting.debug"), runtime.NumCPU()*2) + + // Total number of hubs is twice the number of CPUs. + hubs = make([]*Hub, runtime.NumCPU()*2) + + for i := 0; i < len(hubs); i++ { + hubs[i] = NewWebHub() + hubs[i].Start() + } +} + +func HubStop() { + l4g.Info(utils.T("api.web_hub.start.stopping.debug")) + + for _, hub := range hubs { + hub.Stop() + } + + hubs = make([]*Hub, 0) +} + +func HubRegister(webConn *WebConn) { + hash := fnv.New32a() + hash.Write([]byte(webConn.UserId)) + index := hash.Sum32() % uint32(len(hubs)) + hubs[index].Register(webConn) +} + +func HubUnregister(webConn *WebConn) { + for _, hub := range hubs { + hub.Unregister(webConn) + } } func Publish(message *model.WebSocketEvent) { - hub.Broadcast(message) + message.DoPreComputeJson() + for _, hub := range hubs { + hub.Broadcast(message) + } if einterfaces.GetClusterInterface() != nil { einterfaces.GetClusterInterface().Publish(message) @@ -42,11 +94,19 @@ func Publish(message *model.WebSocketEvent) { } func PublishSkipClusterSend(message *model.WebSocketEvent) { - hub.Broadcast(message) + message.DoPreComputeJson() + for _, hub := range hubs { + hub.Broadcast(message) + } } func InvalidateCacheForUser(userId string) { - hub.invalidateUser <- userId + + Srv.Store.Channel().InvalidateAllChannelMembersForUser(userId) + + for _, hub := range hubs { + hub.InvalidateUser(userId) + } if einterfaces.GetClusterInterface() != nil { einterfaces.GetClusterInterface().InvalidateCacheForUser(userId) @@ -54,11 +114,17 @@ func InvalidateCacheForUser(userId string) { } func InvalidateCacheForChannel(channelId string) { - hub.invalidateChannel <- channelId - if einterfaces.GetClusterInterface() != nil { - einterfaces.GetClusterInterface().InvalidateCacheForChannel(channelId) - } + // XXX TODO FIX ME + // This can be removed, but the performance branch + // needs to be merged into master so it can be removed + // from the enterprise repo as well. + + // hub.invalidateChannel <- channelId + + // if einterfaces.GetClusterInterface() != nil { + // einterfaces.GetClusterInterface().InvalidateCacheForChannel(channelId) + // } } func (h *Hub) Register(webConn *WebConn) { @@ -79,6 +145,10 @@ func (h *Hub) Broadcast(message *model.WebSocketEvent) { } } +func (h *Hub) InvalidateUser(userId string) { + h.invalidateUser <- userId +} + func (h *Hub) Stop() { h.stop <- "all" } @@ -108,6 +178,7 @@ func (h *Hub) Start() { if !found { go SetStatusOffline(userId, false) } + case userId := <-h.invalidateUser: for webCon := range h.connections { if webCon.UserId == userId { @@ -115,26 +186,20 @@ func (h *Hub) Start() { } } - case channelId := <-h.invalidateChannel: - for webCon := range h.connections { - webCon.InvalidateCacheForChannel(channelId) - } - case msg := <-h.broadcast: for webCon := range h.connections { - if shouldSendEvent(webCon, msg) { + if webCon.ShouldSendEvent(msg) { select { case webCon.Send <- msg: default: + l4g.Error(fmt.Sprintf("webhub.broadcast: cannot send, closing websocket for userId=%v", webCon.UserId)) close(webCon.Send) delete(h.connections, webCon) } } } - case s := <-h.stop: - l4g.Debug(utils.T("api.web_hub.start.stopping.debug"), s) - + case <-h.stop: for webCon := range h.connections { webCon.WebSocket.Close() } @@ -144,28 +209,3 @@ func (h *Hub) Start() { } }() } - -func shouldSendEvent(webCon *WebConn, msg *model.WebSocketEvent) bool { - // If the event is destined to a specific user - if len(msg.Broadcast.UserId) > 0 && webCon.UserId != msg.Broadcast.UserId { - return false - } - - // if the user is omitted don't send the message - if _, ok := msg.Broadcast.OmitUsers[webCon.UserId]; ok { - return false - } - - // Only report events to users who are in the channel for the event - if len(msg.Broadcast.ChannelId) > 0 { - return webCon.IsMemberOfChannel(msg.Broadcast.ChannelId) - } - - // Only report events to users who are in the team for the event - if len(msg.Broadcast.TeamId) > 0 { - return webCon.IsMemberOfTeam(msg.Broadcast.TeamId) - - } - - return true -} diff --git a/api/websocket.go b/api/websocket.go index fe9fa0bf9..34d95f705 100644 --- a/api/websocket.go +++ b/api/websocket.go @@ -11,16 +11,20 @@ import ( "net/http" ) +const ( + SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB +) + func InitWebSocket() { l4g.Debug(utils.T("api.web_socket.init.debug")) BaseRoutes.Users.Handle("/websocket", ApiUserRequiredTrustRequester(connect)).Methods("GET") - hub.Start() + HubStart() } func connect(c *Context, w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, + ReadBufferSize: SOCKET_MAX_MESSAGE_SIZE_KB, + WriteBufferSize: SOCKET_MAX_MESSAGE_SIZE_KB, CheckOrigin: func(r *http.Request) bool { return true }, @@ -34,7 +38,7 @@ func connect(c *Context, w http.ResponseWriter, r *http.Request) { } wc := NewWebConn(c, ws) - hub.Register(wc) + HubRegister(wc) go wc.writePump() wc.readPump() } diff --git a/api/websocket_handler.go b/api/websocket_handler.go index 5a313fe13..95aad8fee 100644 --- a/api/websocket_handler.go +++ b/api/websocket_handler.go @@ -31,11 +31,17 @@ func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequ if data, err = wh.handlerFunc(r); err != nil { l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, r.Session.UserId, err.SystemMessage(utils.T), err.DetailedError) err.DetailedError = "" - conn.Send <- model.NewWebSocketError(r.Seq, err) + errResp := model.NewWebSocketError(r.Seq, err) + errResp.DoPreComputeJson() + + conn.Send <- errResp return } - conn.Send <- model.NewWebSocketResponse(model.STATUS_OK, r.Seq, data) + resp := model.NewWebSocketResponse(model.STATUS_OK, r.Seq, data) + resp.DoPreComputeJson() + + conn.Send <- resp } func NewInvalidWebSocketParamError(action string, name string) *model.AppError { diff --git a/api/websocket_router.go b/api/websocket_router.go index cd3ff4d1a..34b576464 100644 --- a/api/websocket_router.go +++ b/api/websocket_router.go @@ -54,6 +54,7 @@ func (wr *WebSocketRouter) ReturnWebSocketError(conn *WebConn, r *model.WebSocke err.DetailedError = "" errorResp := model.NewWebSocketError(r.Seq, err) + errorResp.DoPreComputeJson() conn.Send <- errorResp } diff --git a/api/websocket_test.go b/api/websocket_test.go index dcbc8e0f4..b7ca4b691 100644 --- a/api/websocket_test.go +++ b/api/websocket_test.go @@ -82,7 +82,8 @@ func TestWebSocketEvent(t *testing.T) { omitUser["somerandomid"] = true evt1 := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", th.BasicChannel.Id, "", omitUser) evt1.Add("user_id", "somerandomid") - go Publish(evt1) + Publish(evt1) + time.Sleep(300 * time.Millisecond) stop := make(chan bool) |