From 365b8b465e8a53ebb2da2bf3aef659ac81a2bc6a Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Wed, 19 Oct 2016 14:49:25 -0400 Subject: Merging performance branch into master (#4268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve performance on sendNotifications * Fix SQL queries * Remove get direct profiles, not needed anymore * Add raw data to error details if AppError fails to decode * men * Fix decode (#4052) * Fixing json decode * Adding unit test * Initial work for client scaling (#4051) * Begin adding paging to profiles API * Added more paging functionality * Finish hooking up admin console user lists * Add API for searching users and add searching to all user lists * Add lazy loading of profiles * Revert config.json * Fix unit tests and some style issues * Add GetProfilesFromList to Go driver and fix web unit test * Update etag for GetProfiles * Updating ui for filters and pagination (#4044) * Updating UI for pagination * Adjusting margins for filter row * Adjusting margin for specific modals * Adding relative padding to system console * Adjusting responsive view * Update client user tests * Minor fixes for direct messages modal (#4056) * Remove some unneeded initial load calls (#4057) * UX updates to user lists, added smart counts and bug fixes (#4059) * Improved getExplicitMentions and unit tests (#4064) * Refactor getting posts to lazy load profiles correctly (#4062) * Comment out SetActiveChannel test (#4066) * Profiler cpu, block, and memory profiler. (#4081) * Fix TestSetActiveChannel unit test (#4071) * Fixing build failure caused by dependancies updating (#4076) * Adding profiler * Fix admin_team_member_dropdown eslint errors * Bumping session cache size (#4077) * Bumping session cache size * Bumping status cache * Refactor how the client handles channel members to be large team friendly (#4106) * Refactor how the client handles channel members to be large team friendly * Change Id to ChannelId in ChannelStats model * Updated getChannelMember and getProfilesByIds routes to match proposal * Performance improvements (#4100) * Performance improvements * Fixing re-connect issue * Fixing error message * Some other minor perf tweaks * Some other minor perf tweaks * Fixing config file * Fixing buffer size * Fixing web socket send message * adding some error logging * fix getMe to be user required * Fix websocket event for new user * Fixing shutting down * Reverting web socket changes * Fixing logging lvl * Adding caching to GetMember * Adding some logging * Fixing caching * Fixing caching invalidate * Fixing direct message caching * Fixing caching * Fixing caching * Remove GetDirectProfiles from initial load * Adding logging and fixing websocket client * Adding back caching from bad merge. * Explicitly close go driver requests (#4162) * Refactored how the client handles team members to be more large team friendly (#4159) * Refactor getProfilesForDirectMessageList API into getAllProfiles API * Refactored how the client handles team members to be more large team friendly * Fix js error when receiving a notification * Fix JS error caused by current user being overwritten with sanitized version (#4165) * Adding error message to status failure (#4167) * Fix a few bugs caused by client scaling refactoring (#4170) * When there is no read replica, don't open a second set of connections to the master database (#4173) * Adding connection tacking to stats (#4174) * Reduce DB writes for statuses and other status related changes (#4175) * Fix bug preventing opening of DM channels from more modal (#4181) * Fixing socket timing error (#4183) * Fixing ping/pong handler * Fixing socket timing error * Commenting out status broadcasting * Removing user status changes * Removing user status changes * Removing user status changes * Removing user status changes * Adding DoPreComputeJson() * Performance improvements (#4194) * * Fix System Console Analytics queries * Add db.SetConnMaxLifetime to 15 minutes * Add "net/http/pprof" for profiling * Add FreeOSMemory() to manually release memory on reload config * Add flag to enable http profiler * Fix memory leak (#4197) * Fix memory leak * removed unneeded nil assignment * Fixing go routine leak (#4208) * Merge fixes * Merge fix * Refactored statuses to be queried by the client rather than broadcast by the server (#4212) * Refactored server code to reduce status broadcasts and to allow getting statuses by IDs * Refactor client code to periodically fetch statuses * Add store unit test for getting statuses by ids * Fix status unit test * Add getStatusesByIds REST API and move the client over to use that instead of the WebSocket * Adding multiple threads to websocket hub (#4230) * Adding multiple threads to websocket hub * Fixing unit tests * Fixing so websocket connections from the same user end up in the sameā€¦ (#4240) * Fixing so websocket connections from the same user end up in the same list * Removing old comment * Refactor user autocomplete to query the server (#4239) * Add API for autocompleting users * Converted at mention autocomplete to query server * Converted user search autocomplete to query server * Switch autocomplete API naming to use term instead of username * Split autocomplete API into two, one for channels and for teams * Fix copy/paste error * Some final client scaling fixes (#4246) * Add lazy loading of profiles to integration pages * Add lazy loading of profiles to emoji page * Fix JS error when receiving post in select team menu and also clean up channel store --- model/autocomplete.go | 58 ++++++++++++++++ model/channel.go | 4 +- model/channel_extra.go | 49 -------------- model/channel_stats.go | 34 ++++++++++ model/client.go | 164 ++++++++++++++++++++++++++++++++++++++++----- model/initial_load.go | 15 ++--- model/session.go | 2 +- model/status.go | 5 +- model/team_stats.go | 34 ++++++++++ model/user.go | 20 ++++++ model/utils.go | 30 ++++++++- model/utils_test.go | 7 ++ model/websocket_client.go | 21 ++++++ model/websocket_message.go | 44 ++++++++++-- 14 files changed, 400 insertions(+), 87 deletions(-) create mode 100644 model/autocomplete.go delete mode 100644 model/channel_extra.go create mode 100644 model/channel_stats.go create mode 100644 model/team_stats.go (limited to 'model') diff --git a/model/autocomplete.go b/model/autocomplete.go new file mode 100644 index 000000000..b7449a792 --- /dev/null +++ b/model/autocomplete.go @@ -0,0 +1,58 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type UserAutocompleteInChannel struct { + InChannel []*User `json:"in_channel"` + OutOfChannel []*User `json:"out_of_channel"` +} + +type UserAutocompleteInTeam struct { + InTeam []*User `json:"in_team"` +} + +func (o *UserAutocompleteInChannel) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func UserAutocompleteInChannelFromJson(data io.Reader) *UserAutocompleteInChannel { + decoder := json.NewDecoder(data) + var o UserAutocompleteInChannel + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *UserAutocompleteInTeam) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func UserAutocompleteInTeamFromJson(data io.Reader) *UserAutocompleteInTeam { + decoder := json.NewDecoder(data) + var o UserAutocompleteInTeam + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/channel.go b/model/channel.go index 3da9f4feb..7dee079c5 100644 --- a/model/channel.go +++ b/model/channel.go @@ -57,8 +57,8 @@ func (o *Channel) Etag() string { return Etag(o.Id, o.UpdateAt) } -func (o *Channel) ExtraEtag(memberLimit int) string { - return Etag(o.Id, o.ExtraUpdateAt, memberLimit) +func (o *Channel) StatsEtag() string { + return Etag(o.Id, o.ExtraUpdateAt) } func (o *Channel) IsValid() *AppError { diff --git a/model/channel_extra.go b/model/channel_extra.go deleted file mode 100644 index 55da588af..000000000 --- a/model/channel_extra.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -import ( - "encoding/json" - "io" -) - -type ExtraMember struct { - Id string `json:"id"` - Nickname string `json:"nickname"` - Email string `json:"email"` - Roles string `json:"roles"` - Username string `json:"username"` -} - -func (o *ExtraMember) Sanitize(options map[string]bool) { - if len(options) == 0 || !options["email"] { - o.Email = "" - } -} - -type ChannelExtra struct { - Id string `json:"id"` - Members []ExtraMember `json:"members"` - MemberCount int64 `json:"member_count"` -} - -func (o *ChannelExtra) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } -} - -func ChannelExtraFromJson(data io.Reader) *ChannelExtra { - decoder := json.NewDecoder(data) - var o ChannelExtra - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } -} diff --git a/model/channel_stats.go b/model/channel_stats.go new file mode 100644 index 000000000..079769eb0 --- /dev/null +++ b/model/channel_stats.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ChannelStats struct { + ChannelId string `json:"channel_id"` + MemberCount int64 `json:"member_count"` +} + +func (o *ChannelStats) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ChannelStatsFromJson(data io.Reader) *ChannelStats { + decoder := json.NewDecoder(data) + var o ChannelStats + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/client.go b/model/client.go index f9a56b86e..f5aeea4db 100644 --- a/model/client.go +++ b/model/client.go @@ -131,6 +131,7 @@ func (c *Client) GetFileRoute(fileId string) string { func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) { rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data)) rq.Header.Set("Content-Type", contentType) + rq.Close = true if rp, err := c.HttpClient.Do(rq); err != nil { return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error()) @@ -144,6 +145,7 @@ func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppErro func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) { rq, _ := http.NewRequest("POST", c.ApiUrl+url, strings.NewReader(data)) + rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) @@ -161,6 +163,7 @@ func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) func (c *Client) DoApiGet(url string, data string, etag string) (*http.Response, *AppError) { rq, _ := http.NewRequest("GET", c.ApiUrl+url, strings.NewReader(data)) + rq.Close = true if len(etag) > 0 { rq.Header.Set(HEADER_ETAG_CLIENT, etag) @@ -508,10 +511,9 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) { } } -// GetProfilesForDirectMessageList returns a map of users for a team that can be direct -// messaged, using user id as the key. Must be authenticated. -func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppError) { - if r, err := c.DoApiGet("/users/profiles_for_dm_list/"+teamId, "", ""); err != nil { +// GetProfiles returns a map of users using user id as the key. Must be authenticated. +func (c *Client) GetProfiles(offset int, limit int, etag string) (*Result, *AppError) { + if r, err := c.DoApiGet(fmt.Sprintf("/users/%v/%v", offset, limit), "", etag); err != nil { return nil, err } else { defer closeBody(r) @@ -520,10 +522,10 @@ func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppEr } } -// GetProfiles returns a map of users for a team using user id as the key. Must +// GetProfilesInTeam returns a map of users for a team using user id as the key. Must // be authenticated. -func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil { +func (c *Client) GetProfilesInTeam(teamId string, offset int, limit int, etag string) (*Result, *AppError) { + if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/users/%v/%v", teamId, offset, limit), "", etag); err != nil { return nil, err } else { defer closeBody(r) @@ -532,10 +534,22 @@ func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) { } } -// GetDirectProfiles gets a map of users that are currently shown in the sidebar, -// using user id as the key. Must be authenticated. -func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/users/direct_profiles", "", etag); err != nil { +// GetProfilesInChannel returns a map of users for a channel using user id as the key. Must +// be authenticated. +func (c *Client) GetProfilesInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) { + if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/%v/%v", offset, limit), "", etag); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil + } +} + +// GetProfilesNotInChannel returns a map of users not in a channel but on the team using user id as the key. Must +// be authenticated. +func (c *Client) GetProfilesNotInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) { + if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/not_in_channel/%v/%v", offset, limit), "", etag); err != nil { return nil, err } else { defer closeBody(r) @@ -544,6 +558,60 @@ func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) { } } +// GetProfilesByIds returns a map of users based on the user ids provided. Must +// be authenticated. +func (c *Client) GetProfilesByIds(userIds []string) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/ids", ArrayToJson(userIds)); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil + } +} + +// SearchUsers returns a list of users that have a username matching or similar to the search term. Must +// be authenticated. +func (c *Client) SearchUsers(term string, teamId string, options map[string]string) (*Result, *AppError) { + options["term"] = term + options["team_id"] = teamId + if r, err := c.DoApiPost("/users/search", MapToJson(options)); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserListFromJson(r.Body)}, nil + } +} + +// AutocompleteUsersInChannel returns two lists for autocompletion of users in a channel. The first list "in_channel", +// specifies users in the channel. The second list "out_of_channel" specifies users outside of the +// channel. Term, the string to search against, is required, channel id is also required. Must be authenticated. +func (c *Client) AutocompleteUsersInChannel(term string, channelId string) (*Result, *AppError) { + url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetChannelRoute(channelId), url.QueryEscape(term)) + if r, err := c.DoApiGet(url, "", ""); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInChannelFromJson(r.Body)}, nil + } +} + +// AutocompleteUsersInTeam returns a list for autocompletion of users in a team. The list "in_team" specifies +// the users in the team that match the provided term, matching against username, full name and +// nickname. Must be authenticated. +func (c *Client) AutocompleteUsersInTeam(term string) (*Result, *AppError) { + url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetTeamRoute(), url.QueryEscape(term)) + if r, err := c.DoApiGet(url, "", ""); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInTeamFromJson(r.Body)}, nil + } +} + // LoginById authenticates a user by user id and password. func (c *Client) LoginById(id string, password string) (*Result, *AppError) { m := make(map[string]string) @@ -942,6 +1010,7 @@ func (c *Client) SaveComplianceReport(job *Compliance) (*Result, *AppError) { func (c *Client) DownloadComplianceReport(id string) (*Result, *AppError) { var rq *http.Request rq, _ = http.NewRequest("GET", c.ApiUrl+"/admin/download_compliance_report/"+id, nil) + rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) @@ -1174,13 +1243,23 @@ func (c *Client) UpdateLastViewedAt(channelId string, active bool) (*Result, *Ap } } -func (c *Client) GetChannelExtraInfo(id string, memberLimit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/extra_info/"+strconv.FormatInt(int64(memberLimit), 10), "", etag); err != nil { +func (c *Client) GetChannelStats(id string, etag string) (*Result, *AppError) { + if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/stats", "", etag); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelStatsFromJson(r.Body)}, nil + } +} + +func (c *Client) GetChannelMember(channelId string, userId string) (*Result, *AppError) { + if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/members/"+userId, "", ""); err != nil { return nil, err } else { defer closeBody(r) return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelExtraFromJson(r.Body)}, nil + r.Header.Get(HEADER_ETAG_SERVER), ChannelMemberFromJson(r.Body)}, nil } } @@ -1325,6 +1404,7 @@ func (c *Client) UploadPostAttachment(data []byte, channelId string, filename st func (c *Client) uploadFile(url string, data []byte, contentType string) (*Result, *AppError) { rq, _ := http.NewRequest("POST", url, bytes.NewReader(data)) rq.Header.Set("Content-Type", contentType) + rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) @@ -1525,6 +1605,18 @@ func (c *Client) GetStatuses() (*Result, *AppError) { } } +// GetStatusesByIds returns a map of string statuses using user id as the key, +// based on the provided user ids +func (c *Client) GetStatusesByIds(userIds []string) (*Result, *AppError) { + if r, err := c.DoApiPost("/users/status/ids", ArrayToJson(userIds)); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + // SetActiveChannel sets the the channel id the user is currently viewing. // The channelId key is required but the value can be blank. Returns standard // response. @@ -1550,8 +1642,46 @@ func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { } } -func (c *Client) GetTeamMembers(teamId string) (*Result, *AppError) { - if r, err := c.DoApiGet("/teams/members/"+teamId, "", ""); err != nil { +// GetTeamMembers will return a page of team member objects as an array paged based on the +// team id, offset and limit provided. Must be authenticated. +func (c *Client) GetTeamMembers(teamId string, offset int, limit int) (*Result, *AppError) { + if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v/%v", teamId, offset, limit), "", ""); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamMembersFromJson(r.Body)}, nil + } +} + +// GetTeamMember will return a team member object based on the team id and user id provided. +// Must be authenticated. +func (c *Client) GetTeamMember(teamId string, userId string) (*Result, *AppError) { + if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v", teamId, userId), "", ""); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamMemberFromJson(r.Body)}, nil + } +} + +// GetTeamStats will return a team stats object containing the number of users on the team +// based on the team id provided. Must be authenticated. +func (c *Client) GetTeamStats(teamId string) (*Result, *AppError) { + if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/stats", teamId), "", ""); err != nil { + return nil, err + } else { + defer closeBody(r) + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamStatsFromJson(r.Body)}, nil + } +} + +// GetTeamMembersByIds will return team member objects as an array based on the +// team id and a list of user ids provided. Must be authenticated. +func (c *Client) GetTeamMembersByIds(teamId string, userIds []string) (*Result, *AppError) { + if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v/members/ids", teamId), ArrayToJson(userIds)); err != nil { return nil, err } else { defer closeBody(r) @@ -1866,6 +1996,7 @@ func (c *Client) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoj rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetEmojiRoute()+"/create", body) rq.Header.Set("Content-Type", writer.FormDataContentType()) + rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) @@ -1908,6 +2039,7 @@ func (c *Client) UploadCertificateFile(data []byte, contentType string) *AppErro url := c.ApiUrl + "/admin/add_certificate" rq, _ := http.NewRequest("POST", url, bytes.NewReader(data)) rq.Header.Set("Content-Type", contentType) + rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) diff --git a/model/initial_load.go b/model/initial_load.go index d7587e6d4..afb0a2762 100644 --- a/model/initial_load.go +++ b/model/initial_load.go @@ -9,14 +9,13 @@ import ( ) type InitialLoad struct { - User *User `json:"user"` - TeamMembers []*TeamMember `json:"team_members"` - Teams []*Team `json:"teams"` - DirectProfiles map[string]*User `json:"direct_profiles"` - Preferences Preferences `json:"preferences"` - ClientCfg map[string]string `json:"client_cfg"` - LicenseCfg map[string]string `json:"license_cfg"` - NoAccounts bool `json:"no_accounts"` + User *User `json:"user"` + TeamMembers []*TeamMember `json:"team_members"` + Teams []*Team `json:"teams"` + Preferences Preferences `json:"preferences"` + ClientCfg map[string]string `json:"client_cfg"` + LicenseCfg map[string]string `json:"license_cfg"` + NoAccounts bool `json:"no_accounts"` } func (me *InitialLoad) ToJson() string { diff --git a/model/session.go b/model/session.go index c3171ed7c..a6a753e7f 100644 --- a/model/session.go +++ b/model/session.go @@ -11,7 +11,7 @@ import ( const ( SESSION_COOKIE_TOKEN = "MMAUTHTOKEN" - SESSION_CACHE_SIZE = 10000 + SESSION_CACHE_SIZE = 25000 SESSION_PROP_PLATFORM = "platform" SESSION_PROP_OS = "os" SESSION_PROP_BROWSER = "browser" diff --git a/model/status.go b/model/status.go index f4ad8e775..324866427 100644 --- a/model/status.go +++ b/model/status.go @@ -12,8 +12,9 @@ const ( STATUS_OFFLINE = "offline" STATUS_AWAY = "away" STATUS_ONLINE = "online" - STATUS_CACHE_SIZE = 10000 - STATUS_CHANNEL_TIMEOUT = 20000 // 20 seconds + STATUS_CACHE_SIZE = 25000 + STATUS_CHANNEL_TIMEOUT = 20000 // 20 seconds + STATUS_MIN_UPDATE_TIME = 120000 // 2 minutes ) type Status struct { diff --git a/model/team_stats.go b/model/team_stats.go new file mode 100644 index 000000000..8634c3d1b --- /dev/null +++ b/model/team_stats.go @@ -0,0 +1,34 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type TeamStats struct { + TeamId string `json:"team_id"` + MemberCount int64 `json:"member_count"` +} + +func (o *TeamStats) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func TeamStatsFromJson(data io.Reader) *TeamStats { + decoder := json.NewDecoder(data) + var o TeamStats + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/user.go b/model/user.go index 1b51171ad..e713444ba 100644 --- a/model/user.go +++ b/model/user.go @@ -413,6 +413,26 @@ func UserMapFromJson(data io.Reader) map[string]*User { } } +func UserListToJson(u []*User) string { + b, err := json.Marshal(u) + if err != nil { + return "" + } else { + return string(b) + } +} + +func UserListFromJson(data io.Reader) []*User { + decoder := json.NewDecoder(data) + var users []*User + err := decoder.Decode(&users) + if err == nil { + return users + } else { + return nil + } +} + // HashPassword generates a hash using the bcrypt.GenerateFromPassword func HashPassword(password string) string { hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) diff --git a/model/utils.go b/model/utils.go index a9c441fee..4ebd23939 100644 --- a/model/utils.go +++ b/model/utils.go @@ -10,6 +10,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/mail" "net/url" "regexp" @@ -74,13 +75,21 @@ func (er *AppError) ToJson() string { // AppErrorFromJson will decode the input and return an AppError func AppErrorFromJson(data io.Reader) *AppError { - decoder := json.NewDecoder(data) + str := "" + bytes, rerr := ioutil.ReadAll(data) + if rerr != nil { + str = rerr.Error() + } else { + str = string(bytes) + } + + decoder := json.NewDecoder(strings.NewReader(str)) var er AppError err := decoder.Decode(&er) if err == nil { return &er } else { - return NewLocAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, err.Error()) + return NewLocAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, "body: "+str) } } @@ -166,6 +175,23 @@ func ArrayFromJson(data io.Reader) []string { } } +func ArrayFromInterface(data interface{}) []string { + stringArray := []string{} + + dataArray, ok := data.([]interface{}) + if !ok { + return stringArray + } + + for _, v := range dataArray { + if str, ok := v.(string); ok { + stringArray = append(stringArray, str) + } + } + + return stringArray +} + func StringInterfaceToJson(objmap map[string]interface{}) string { if b, err := json.Marshal(objmap); err != nil { return "" diff --git a/model/utils_test.go b/model/utils_test.go index 5d9289a9a..dbe1e59be 100644 --- a/model/utils_test.go +++ b/model/utils_test.go @@ -37,6 +37,13 @@ func TestAppError(t *testing.T) { err.Error() } +func TestAppErrorJunk(t *testing.T) { + rerr := AppErrorFromJson(strings.NewReader("This is a broken test")) + if "body: This is a broken test" != rerr.DetailedError { + t.Fatal() + } +} + func TestMapJson(t *testing.T) { m := make(map[string]string) diff --git a/model/websocket_client.go b/model/websocket_client.go index a048bd855..a4983e385 100644 --- a/model/websocket_client.go +++ b/model/websocket_client.go @@ -17,6 +17,7 @@ type WebSocketClient struct { Sequence int64 // The ever-incrementing sequence attached to each WebSocket action EventChannel chan *WebSocketEvent ResponseChannel chan *WebSocketResponse + ListenError *AppError } // NewWebSocketClient constructs a new WebSocket client with convienence @@ -37,6 +38,7 @@ func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) { 1, make(chan *WebSocketEvent, 100), make(chan *WebSocketResponse, 100), + nil, }, nil } @@ -59,10 +61,20 @@ func (wsc *WebSocketClient) Close() { func (wsc *WebSocketClient) Listen() { go func() { + defer func() { + wsc.Conn.Close() + close(wsc.EventChannel) + close(wsc.ResponseChannel) + }() + for { var rawMsg json.RawMessage var err error if _, rawMsg, err = wsc.Conn.ReadMessage(); err != nil { + if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + wsc.ListenError = NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error()) + } + return } @@ -107,3 +119,12 @@ func (wsc *WebSocketClient) UserTyping(channelId, parentId string) { func (wsc *WebSocketClient) GetStatuses() { wsc.SendMessage("get_statuses", nil) } + +// GetStatusesByIds will fetch certain user statuses based on ids and return +// a map of string statuses using user id as the key +func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) { + data := map[string]interface{}{ + "user_ids": userIds, + } + wsc.SendMessage("get_statuses_by_ids", data) +} diff --git a/model/websocket_message.go b/model/websocket_message.go index 9bce1e825..c7ab04857 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -31,6 +31,8 @@ const ( type WebSocketMessage interface { ToJson() string IsValid() bool + DoPreComputeJson() + GetPreComputeJson() []byte } type WebsocketBroadcast struct { @@ -41,9 +43,10 @@ type WebsocketBroadcast struct { } type WebSocketEvent struct { - Event string `json:"event"` - Data map[string]interface{} `json:"data"` - Broadcast *WebsocketBroadcast `json:"broadcast"` + Event string `json:"event"` + Data map[string]interface{} `json:"data"` + Broadcast *WebsocketBroadcast `json:"broadcast"` + PreComputeJson []byte `json:"-"` } func (m *WebSocketEvent) Add(key string, value interface{}) { @@ -59,6 +62,19 @@ func (o *WebSocketEvent) IsValid() bool { return o.Event != "" } +func (o *WebSocketEvent) DoPreComputeJson() { + b, err := json.Marshal(o) + if err != nil { + o.PreComputeJson = []byte("") + } else { + o.PreComputeJson = b + } +} + +func (o *WebSocketEvent) GetPreComputeJson() []byte { + return o.PreComputeJson +} + func (o *WebSocketEvent) ToJson() string { b, err := json.Marshal(o) if err != nil { @@ -80,10 +96,11 @@ func WebSocketEventFromJson(data io.Reader) *WebSocketEvent { } type WebSocketResponse struct { - Status string `json:"status"` - SeqReply int64 `json:"seq_reply,omitempty"` - Data map[string]interface{} `json:"data,omitempty"` - Error *AppError `json:"error,omitempty"` + Status string `json:"status"` + SeqReply int64 `json:"seq_reply,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` + Error *AppError `json:"error,omitempty"` + PreComputeJson []byte `json:"-"` } func (m *WebSocketResponse) Add(key string, value interface{}) { @@ -111,6 +128,19 @@ func (o *WebSocketResponse) ToJson() string { } } +func (o *WebSocketResponse) DoPreComputeJson() { + b, err := json.Marshal(o) + if err != nil { + o.PreComputeJson = []byte("") + } else { + o.PreComputeJson = b + } +} + +func (o *WebSocketResponse) GetPreComputeJson() []byte { + return o.PreComputeJson +} + func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse { decoder := json.NewDecoder(data) var o WebSocketResponse -- cgit v1.2.3-1-g7c22