Merging performance branch into master (#4268)

* 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
This commit is contained in:
Joram Wilander
2016-10-19 14:49:25 -04:00
committed by GitHub
parent 0512bd26ee
commit 365b8b465e
129 changed files with 6411 additions and 2530 deletions

View File

@@ -361,7 +361,7 @@ run-server: prepare-enterprise start-docker
run-cli: prepare-enterprise start-docker
@echo Running mattermost for development
@echo Example should be like >'make ARGS="-version" run-cli'
@echo Example should be like 'make ARGS="-version" run-cli'
$(GO) run $(GOFLAGS) $(GO_LINKER_FLAGS) *.go ${ARGS}

View File

@@ -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)))

View File

@@ -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")
}
}

View File

@@ -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()

View File

@@ -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
}
}
}

View File

@@ -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))

View File

@@ -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)

View File

@@ -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")
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 = ""
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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"))
}

View File

@@ -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)

View File

@@ -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")
}
}
*/

View File

@@ -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(id); result.Err != nil {
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().GetMembersByIds(c.TeamId, userIds); result.Err != nil {
c.Err = result.Err
return
} else {

View File

@@ -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")
}
}

View File

@@ -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
if *utils.Cfg.TeamSettings.RestrictDirectMessage == model.DIRECT_MESSAGE_TEAM {
if c.Session.GetTeamByTeamId(id) == nil {
if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
return
}
}
pchan = Srv.Store.User().GetProfiles(id)
} else {
pchan = Srv.Store.User().GetAllProfiles()
offset, err := strconv.Atoi(params["offset"])
if err != nil {
c.SetInvalidParam("getProfiles", "offset")
return
}
if result := <-pchan; result.Err != nil {
limit, err := strconv.Atoi(params["limit"])
if err != nil {
c.SetInvalidParam("getProfiles", "limit")
return
}
etag := (<-Srv.Store.User().GetEtagForAllProfiles()).Data.(string)
if HandleEtag(etag, w, r) {
return
}
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
}
if result := <-Srv.Store.User().GetDirectProfiles(c.Session.UserId); result.Err != nil {
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().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()))
}

View File

@@ -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,62 +503,7 @@ func TestGetDirectProfiles(t *testing.T) {
utils.Cfg.PrivacySettings.ShowEmailAddress = true
if result, err := th.BasicClient.GetDirectProfiles(""); err != nil {
t.Fatal(err)
} else {
users := result.Data.(map[string]*model.User)
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")
}
}
}
utils.Cfg.PrivacySettings.ShowEmailAddress = false
if result, err := th.BasicClient.GetDirectProfiles(""); err != nil {
t.Fatal(err)
} else {
users := result.Data.(map[string]*model.User)
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")
}
}
}
}
func TestGetProfilesForDirectMessageList(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
}()
utils.Cfg.PrivacySettings.ShowEmailAddress = true
if result, err := th.BasicClient.GetProfilesForDirectMessageList(th.BasicTeam.Id); err != nil {
if result, err := th.BasicClient.GetProfiles(0, 100, ""); err != nil {
t.Fatal(err)
} else {
users := result.Data.(map[string]*model.User)
@@ -554,11 +517,20 @@ func TestGetProfilesForDirectMessageList(t *testing.T) {
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.GetProfilesForDirectMessageList(th.BasicTeam.Id); err != nil {
if result, err := th.BasicClient.GetProfiles(0, 100, ""); err != nil {
t.Fatal(err)
} else {
users := result.Data.(map[string]*model.User)
@@ -575,6 +547,61 @@ func TestGetProfilesForDirectMessageList(t *testing.T) {
}
}
func TestGetProfilesByIds(t *testing.T) {
th := Setup().InitBasic()
prevShowEmail := utils.Cfg.PrivacySettings.ShowEmailAddress
defer func() {
utils.Cfg.PrivacySettings.ShowEmailAddress = prevShowEmail
}()
utils.Cfg.PrivacySettings.ShowEmailAddress = true
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 {
t.Fatal("map was wrong length")
}
for _, user := range users {
if user.Email == "" {
t.Fatal("problem with show email")
}
}
}
utils.Cfg.PrivacySettings.ShowEmailAddress = false
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 {
t.Fatal("map was wrong length")
}
for _, user := range users {
if user.Email != "" {
t.Fatal("problem with show email")
}
}
}
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) {
th := Setup()
Client := th.CreateClient()
@@ -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")
}
}

View File

@@ -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 (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
}
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)
// 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()
}
}
}
return isMember
}
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
if _, ok := webCon.AllChannelMembers[msg.Broadcast.ChannelId]; ok {
return true
} else {
isMember = true
c.isMemberOfChannel[channelId] = isMember
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 (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 {
return false
}
}
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)

View File

@@ -64,7 +64,7 @@
},
"LogSettings": {
"EnableConsole": true,
"ConsoleLevel": "DEBUG",
"ConsoleLevel": "INFO",
"EnableFile": true,
"FileLevel": "INFO",
"FileFormat": "",
@@ -240,4 +240,4 @@
"TurnUsername": "",
"TurnSharedKey": ""
}
}
}

View File

@@ -2403,9 +2403,13 @@
"id": "api.user.verify_email.bad_link.app_error",
"translation": "Bad verify email link."
},
{
"id": "api.web_hub.start.starting.debug",
"translation": "Starting %v websocket hubs"
},
{
"id": "api.web_hub.start.stopping.debug",
"translation": "stopping %v connections"
"translation": "stopping websocket hub connections"
},
{
"id": "api.web_socket.connect.error",
@@ -4627,6 +4631,10 @@
"id": "store.sql_team.get_member.app_error",
"translation": "We couldn't get the team member"
},
{
"id": "store.sql_team.get_members_by_ids.app_error",
"translation": "We couldn't get the team members"
},
{
"id": "store.sql_team.get_member.missing.app_error",
"translation": "No team member found for that user id and team id"
@@ -4635,6 +4643,10 @@
"id": "store.sql_team.get_members.app_error",
"translation": "We couldn't get the team members"
},
{
"id": "store.sql_team.get_member_count.app_error",
"translation": "We couldn't count the team members"
},
{
"id": "store.sql_team.get_teams_for_email.app_error",
"translation": "We encountered a problem when looking up teams"
@@ -4723,6 +4735,10 @@
"id": "store.sql_user.get_profiles.app_error",
"translation": "We encountered an error while finding user profiles"
},
{
"id": "store.sql_user.get_recently_active_users.app_error",
"translation": "We encountered an error while finding the recently active users"
},
{
"id": "store.sql_user.get_sysadmin_profiles.app_error",
"translation": "We encountered an error while finding user profiles"

View File

@@ -14,6 +14,7 @@ import (
"os/exec"
"os/signal"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"syscall"
@@ -83,6 +84,10 @@ var flagChannelHeader string
var flagChannelPurpose string
var flagUserSetInactive bool
var flagImportArchive string
var flagCpuProfile bool
var flagMemProfile bool
var flagBlockProfile bool
var flagHttpProfiler bool
func doLoadConfig(filename string) (err string) {
defer func() {
@@ -122,7 +127,26 @@ func main() {
cmdUpdateDb30()
api.NewServer()
if flagCpuProfile {
f, err := os.Create(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".cpu.prof")
if err != nil {
l4g.Error("Error creating cpu profile log: " + err.Error())
}
l4g.Info("CPU Profiler is logging to " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".cpu.prof")
pprof.StartCPUProfile(f)
}
if flagBlockProfile {
l4g.Info("Block Profiler is logging to " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".blk.prof")
runtime.SetBlockProfileRate(1)
}
if flagMemProfile {
l4g.Info("Memory Profiler is logging to " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".mem.prof")
}
api.NewServer(flagHttpProfiler)
api.InitApi()
web.InitWeb()
@@ -169,6 +193,37 @@ func main() {
}
api.StopServer()
if flagCpuProfile {
l4g.Info("Closing CPU Profiler")
pprof.StopCPUProfile()
}
if flagBlockProfile {
f, err := os.Create(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".blk.prof")
if err != nil {
l4g.Error("Error creating block profile log: " + err.Error())
}
l4g.Info("Writing Block Profiler to: " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".blk.prof")
pprof.Lookup("block").WriteTo(f, 0)
f.Close()
runtime.SetBlockProfileRate(0)
}
if flagMemProfile {
f, err := os.Create(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".mem.prof")
if err != nil {
l4g.Error("Error creating memory profile file: " + err.Error())
}
l4g.Info("Writing Memory Profiler to: " + utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation) + ".mem.prof")
runtime.GC()
if err := pprof.WriteHeapProfile(f); err != nil {
l4g.Error("Error creating memory profile: " + err.Error())
}
f.Close()
}
}
}
@@ -380,6 +435,10 @@ func parseCmds() {
flag.BoolVar(&flagCmdActivateUser, "activate_user", false, "")
flag.BoolVar(&flagCmdSlackImport, "slack_import", false, "")
flag.BoolVar(&flagUserSetInactive, "inactive", false, "")
flag.BoolVar(&flagCpuProfile, "cpuprofile", false, "")
flag.BoolVar(&flagMemProfile, "memprofile", false, "")
flag.BoolVar(&flagBlockProfile, "blkprofile", false, "")
flag.BoolVar(&flagHttpProfiler, "httpprofiler", false, "")
flag.Parse()

58
model/autocomplete.go Normal file
View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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
}
}

34
model/channel_stats.go Normal file
View File

@@ -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
}
}

View File

@@ -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,10 @@ 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)
@@ -544,6 +546,72 @@ func (c *Client) GetDirectProfiles(etag string) (*Result, *AppError) {
}
}
// 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)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil
}
}
// 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), ChannelExtraFromJson(r.Body)}, nil
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), 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)

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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 {

34
model/team_stats.go Normal file
View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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 ""

View File

@@ -37,6 +37,13 @@ func TestAppError(t *testing.T) {
err.Error()
}
func TestAppErrorJunk(t *testing.T) {
rerr := AppErrorFromJson(strings.NewReader("<html><body>This is a broken test</body></html>"))
if "body: <html><body>This is a broken test</body></html>" != rerr.DetailedError {
t.Fatal()
}
}
func TestMapJson(t *testing.T) {
m := make(map[string]string)

View File

@@ -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)
}

View File

@@ -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

View File

@@ -6,21 +6,26 @@ package store
import (
"database/sql"
l4g "github.com/alecthomas/log4go"
"github.com/go-gorp/gorp"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
const (
MISSING_CHANNEL_ERROR = "store.sql_channel.get_by_name.missing.app_error"
MISSING_CHANNEL_MEMBER_ERROR = "store.sql_channel.get_member.missing.app_error"
CHANNEL_EXISTS_ERROR = "store.sql_channel.save_channel.exists.app_error"
MISSING_CHANNEL_ERROR = "store.sql_channel.get_by_name.missing.app_error"
MISSING_CHANNEL_MEMBER_ERROR = "store.sql_channel.get_member.missing.app_error"
CHANNEL_EXISTS_ERROR = "store.sql_channel.save_channel.exists.app_error"
ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE = model.SESSION_CACHE_SIZE
ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SEC = 900 // 15 mins
)
type SqlChannelStore struct {
*SqlStore
}
var allChannelMembersForUserCache *utils.Cache = utils.NewLru(ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE)
func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
s := &SqlChannelStore{sqlStore}
@@ -517,6 +522,8 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
}
}
s.InvalidateAllChannelMembersForUser(member.UserId)
storeChannel <- result
close(storeChannel)
}()
@@ -619,6 +626,33 @@ func (s SqlChannelStore) GetMember(channelId string, userId string) StoreChannel
return storeChannel
}
func (us SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) {
allChannelMembersForUserCache.Remove(userId)
}
func (us SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool {
if cacheItem, ok := allChannelMembersForUserCache.Get(userId); ok {
ids := cacheItem.(map[string]string)
if _, ok := ids[channelId]; ok {
return true
} else {
return false
}
}
if result := <-us.GetAllChannelMembersForUser(userId, true); result.Err != nil {
l4g.Error("SqlChannelStore.IsUserInChannelUseCache: " + result.Err.Error())
return false
} else {
ids := result.Data.(map[string]string)
if _, ok := ids[channelId]; ok {
return true
} else {
return false
}
}
}
func (s SqlChannelStore) GetMemberForPost(postId string, userId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -649,6 +683,52 @@ func (s SqlChannelStore) GetMemberForPost(postId string, userId string) StoreCha
return storeChannel
}
type allChannelMember struct {
ChannelId string
Roles string
}
func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCache bool) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
if allowFromCache {
if cacheItem, ok := allChannelMembersForUserCache.Get(userId); ok {
result.Data = cacheItem.(map[string]string)
storeChannel <- result
close(storeChannel)
return
}
}
var data []allChannelMember
_, err := s.GetReplica().Select(&data, "SELECT ChannelId, Roles FROM Channels, ChannelMembers WHERE Channels.Id = ChannelMembers.ChannelId AND ChannelMembers.UserId = :UserId AND Channels.DeleteAt = 0", map[string]interface{}{"UserId": userId})
if err != nil {
result.Err = model.NewLocAppError("SqlChannelStore.GetAllChannelMembersForUser", "store.sql_channel.get_channels.get.app_error", nil, "userId="+userId+", err="+err.Error())
} else {
ids := make(map[string]string)
for i := range data {
ids[data[i].ChannelId] = data[i].Roles
}
result.Data = ids
if allowFromCache {
allChannelMembersForUserCache.AddWithExpiresInSecs(userId, ids, ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SEC)
}
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlChannelStore) GetMemberCount(channelId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -678,64 +758,6 @@ func (s SqlChannelStore) GetMemberCount(channelId string) StoreChannel {
return storeChannel
}
func (s SqlChannelStore) GetExtraMembers(channelId string, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
var members []model.ExtraMember
var err error
if limit != -1 {
_, err = s.GetReplica().Select(&members, `
SELECT
Id,
Nickname,
Email,
ChannelMembers.Roles,
Username
FROM
ChannelMembers,
Users
WHERE
ChannelMembers.UserId = Users.Id
AND Users.DeleteAt = 0
AND ChannelId = :ChannelId
LIMIT :Limit`, map[string]interface{}{"ChannelId": channelId, "Limit": limit})
} else {
_, err = s.GetReplica().Select(&members, `
SELECT
Id,
Nickname,
Email,
ChannelMembers.Roles,
Username
FROM
ChannelMembers,
Users
WHERE
ChannelMembers.UserId = Users.Id
AND Users.DeleteAt = 0
AND ChannelId = :ChannelId`, map[string]interface{}{"ChannelId": channelId})
}
if err != nil {
result.Err = model.NewLocAppError("SqlChannelStore.GetExtraMembers", "store.sql_channel.get_extra_members.app_error", nil, "channel_id="+channelId+", "+err.Error())
} else {
for i := range members {
members[i].Sanitize(utils.Cfg.GetSanitizeOptions())
}
result.Data = members
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlChannelStore) RemoveMember(channelId string, userId string) StoreChannel {
storeChannel := make(StoreChannel, 1)

View File

@@ -408,11 +408,6 @@ func TestChannelMemberStore(t *testing.T) {
t.Fatal("should have go member")
}
extraMembers := (<-store.Channel().GetExtraMembers(o1.ChannelId, 20)).Data.([]model.ExtraMember)
if len(extraMembers) != 1 {
t.Fatal("should have 1 extra members")
}
if err := (<-store.Channel().SaveMember(&o1)).Err; err == nil {
t.Fatal("Should have been a duplicate")
}
@@ -422,18 +417,6 @@ func TestChannelMemberStore(t *testing.T) {
if t4 != t3 {
t.Fatal("Should not update time upon failure")
}
// rejoin the channel and make sure that an inactive user isn't returned by GetExtraMambers
Must(store.Channel().SaveMember(&o2))
u2.DeleteAt = 1000
Must(store.User().Update(&u2, true))
if result := <-store.Channel().GetExtraMembers(o1.ChannelId, 20); result.Err != nil {
t.Fatal(result.Err)
} else if extraMembers := result.Data.([]model.ExtraMember); len(extraMembers) != 1 {
t.Fatal("should have 1 extra members")
}
}
func TestChannelDeleteMemberStore(t *testing.T) {
@@ -534,6 +517,42 @@ func TestChannelStoreGetChannels(t *testing.T) {
if list.Channels[0].Id != o1.Id {
t.Fatal("missing channel")
}
acresult := <-store.Channel().GetAllChannelMembersForUser(m1.UserId, false)
ids := acresult.Data.(map[string]string)
if _, ok := ids[o1.Id]; !ok {
t.Fatal("missing channel")
}
acresult2 := <-store.Channel().GetAllChannelMembersForUser(m1.UserId, true)
ids2 := acresult2.Data.(map[string]string)
if _, ok := ids2[o1.Id]; !ok {
t.Fatal("missing channel")
}
acresult3 := <-store.Channel().GetAllChannelMembersForUser(m1.UserId, true)
ids3 := acresult3.Data.(map[string]string)
if _, ok := ids3[o1.Id]; !ok {
t.Fatal("missing channel")
}
if !store.Channel().IsUserInChannelUseCache(m1.UserId, o1.Id) {
t.Fatal("missing channel")
}
if store.Channel().IsUserInChannelUseCache(m1.UserId, o2.Id) {
t.Fatal("missing channel")
}
if store.Channel().IsUserInChannelUseCache(m1.UserId, "blahblah") {
t.Fatal("missing channel")
}
if store.Channel().IsUserInChannelUseCache("blahblah", "blahblah") {
t.Fatal("missing channel")
}
store.Channel().InvalidateAllChannelMembersForUser(m1.UserId)
}
func TestChannelStoreGetMoreChannels(t *testing.T) {
@@ -974,22 +993,10 @@ func TestUpdateExtrasByUser(t *testing.T) {
t.Fatal("failed to update extras by user: %v", result.Err)
}
if result := <-store.Channel().GetExtraMembers(c1.Id, -1); result.Err != nil {
t.Fatal("failed to get extras: %v", result.Err)
} else if len(result.Data.([]model.ExtraMember)) != 0 {
t.Fatal("got incorrect member count %v", len(result.Data.([]model.ExtraMember)))
}
u1.DeleteAt = 0
Must(store.User().Update(u1, true))
if result := <-store.Channel().ExtraUpdateByUser(u1.Id, u1.DeleteAt); result.Err != nil {
t.Fatal("failed to update extras by user: %v", result.Err)
}
if result := <-store.Channel().GetExtraMembers(c1.Id, -1); result.Err != nil {
t.Fatal("failed to get extras: %v", result.Err)
} else if len(result.Data.([]model.ExtraMember)) != 1 {
t.Fatal("got incorrect member count %v", len(result.Data.([]model.ExtraMember)))
}
}

View File

@@ -43,6 +43,7 @@ func (s SqlPostStore) CreateIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_posts_create_at", "Posts", "CreateAt")
s.CreateIndexIfNotExists("idx_posts_channel_id", "Posts", "ChannelId")
s.CreateIndexIfNotExists("idx_posts_root_id", "Posts", "RootId")
s.CreateIndexIfNotExists("idx_posts_user_id", "Posts", "UserId")
s.CreateFullTextIndexIfNotExists("idx_posts_message_txt", "Posts", "Message")
s.CreateFullTextIndexIfNotExists("idx_posts_hashtags_txt", "Posts", "Hashtags")
@@ -811,47 +812,36 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
result := StoreResult{}
query :=
`SELECT
t1.Name, COUNT(t1.UserId) AS Value
FROM
(SELECT DISTINCT
`SELECT DISTINCT
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
Posts.UserId
FROM
Posts, Channels
WHERE
Posts.ChannelId = Channels.Id`
COUNT(DISTINCT Posts.UserId) AS Value
FROM Posts
INNER JOIN Channels
ON Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt >= :StartTime AND Posts.CreateAt <= :EndTime
ORDER BY Name DESC) AS t1
GROUP BY Name
GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
query =
`SELECT
TO_CHAR(t1.Name, 'YYYY-MM-DD') AS Name, COUNT(t1.UserId) AS Value
FROM
(SELECT DISTINCT
DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)) AS Name,
Posts.UserId
FROM
Posts, Channels
WHERE
Posts.ChannelId = Channels.Id`
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, COUNT(DISTINCT Posts.UserId) AS Value
FROM Posts
INNER JOIN Channels
ON Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt >= :StartTime AND Posts.CreateAt <= :EndTime
ORDER BY Name DESC) AS t1
GROUP BY Name
GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
}
@@ -884,15 +874,12 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
result := StoreResult{}
query :=
`SELECT
Name, COUNT(Value) AS Value
FROM
(SELECT
`SELECT
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
'1' AS Value
FROM
Posts, Channels
WHERE
COUNT(Posts.Id) AS Value
FROM Posts
INNER JOIN Channels
ON
Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
@@ -900,31 +887,26 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
}
query += ` AND Posts.CreateAt <= :EndTime
AND Posts.CreateAt >= :StartTime) AS t1
GROUP BY Name
AND Posts.CreateAt >= :StartTime
GROUP BY DATE(FROM_UNIXTIME(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
query =
`SELECT
Name, COUNT(Value) AS Value
FROM
(SELECT
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name,
'1' AS Value
FROM
Posts, Channels
WHERE
Posts.ChannelId = Channels.Id`
`SELECT
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name, Count(Posts.Id) AS Value
FROM Posts
INNER JOIN Channels
ON Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt <= :EndTime
AND Posts.CreateAt >= :StartTime) AS t1
GROUP BY Name
AND Posts.CreateAt >= :StartTime
GROUP BY DATE(TO_TIMESTAMP(Posts.CreateAt / 1000))
ORDER BY Name DESC
LIMIT 30`
}

View File

@@ -5,6 +5,7 @@ package store
import (
"database/sql"
"strconv"
"github.com/mattermost/platform/model"
)
@@ -43,11 +44,11 @@ func (s SqlStatusStore) SaveOrUpdate(status *model.Status) StoreChannel {
if err := s.GetReplica().SelectOne(&model.Status{}, "SELECT * FROM Status WHERE UserId = :UserId", map[string]interface{}{"UserId": status.UserId}); err == nil {
if _, err := s.GetMaster().Update(status); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.update.app_error", nil, "")
result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.update.app_error", nil, err.Error())
}
} else {
if err := s.GetMaster().Insert(status); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.save.app_error", nil, "")
result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.save.app_error", nil, err.Error())
}
}
@@ -89,6 +90,38 @@ func (s SqlStatusStore) Get(userId string) StoreChannel {
return storeChannel
}
func (s SqlStatusStore) GetByIds(userIds []string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
props := make(map[string]interface{})
idQuery := ""
for index, userId := range userIds {
if len(idQuery) > 0 {
idQuery += ", "
}
props["userId"+strconv.Itoa(index)] = userId
idQuery += ":userId" + strconv.Itoa(index)
}
var statuses []*model.Status
if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE UserId IN ("+idQuery+")", props); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.GetByIds", "store.sql_status.get.app_error", nil, err.Error())
} else {
result.Data = statuses
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlStatusStore) GetOnlineAway() StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -96,7 +129,7 @@ func (s SqlStatusStore) GetOnlineAway() StoreChannel {
result := StoreResult{}
var statuses []*model.Status
if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE Status = :Online OR Status = :Away", map[string]interface{}{"Online": model.STATUS_ONLINE, "Away": model.STATUS_AWAY}); err != nil {
if _, err := s.GetReplica().Select(&statuses, "SELECT * FROM Status WHERE Status = :Online OR Status = :Away LIMIT 300", map[string]interface{}{"Online": model.STATUS_ONLINE, "Away": model.STATUS_AWAY}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.GetOnlineAway", "store.sql_status.get_online_away.app_error", nil, err.Error())
} else {
result.Data = statuses
@@ -157,7 +190,7 @@ func (s SqlStatusStore) ResetAll() StoreChannel {
go func() {
result := StoreResult{}
if _, err := s.GetMaster().Exec("UPDATE Status SET Status = :Status", map[string]interface{}{"Status": model.STATUS_OFFLINE}); err != nil {
if _, err := s.GetMaster().Exec("UPDATE Status SET Status = :Status WHERE Manual = 0", map[string]interface{}{"Status": model.STATUS_OFFLINE}); err != nil {
result.Err = model.NewLocAppError("SqlStatusStore.ResetAll", "store.sql_status.reset_all.app_error", nil, "")
}

View File

@@ -60,6 +60,15 @@ func TestSqlStatusStore(t *testing.T) {
}
}
if result := <-store.Status().GetByIds([]string{status.UserId, "junk"}); result.Err != nil {
t.Fatal(result.Err)
} else {
statuses := result.Data.([]*model.Status)
if len(statuses) != 1 {
t.Fatal("should only have 1 status")
}
}
if err := (<-store.Status().ResetAll()).Err; err != nil {
t.Fatal(err)
}

View File

@@ -33,6 +33,7 @@ import (
const (
INDEX_TYPE_FULL_TEXT = "full_text"
INDEX_TYPE_DEFAULT = "default"
MAX_DB_CONN_LIFETIME = 15
)
const (
@@ -94,9 +95,7 @@ func initConnection() *SqlStore {
if len(utils.Cfg.SqlSettings.DataSourceReplicas) == 0 {
sqlStore.replicas = make([]*gorp.DbMap, 1)
sqlStore.replicas[0] = setupConnection(fmt.Sprintf("replica-%v", 0), utils.Cfg.SqlSettings.DriverName, utils.Cfg.SqlSettings.DataSource,
utils.Cfg.SqlSettings.MaxIdleConns, utils.Cfg.SqlSettings.MaxOpenConns,
utils.Cfg.SqlSettings.Trace)
sqlStore.replicas[0] = sqlStore.master
} else {
sqlStore.replicas = make([]*gorp.DbMap, len(utils.Cfg.SqlSettings.DataSourceReplicas))
for i, replica := range utils.Cfg.SqlSettings.DataSourceReplicas {
@@ -183,6 +182,7 @@ func setupConnection(con_type string, driver string, dataSource string, maxIdle
db.SetMaxIdleConns(maxIdle)
db.SetMaxOpenConns(maxOpen)
db.SetConnMaxLifetime(time.Duration(MAX_DB_CONN_LIFETIME) * time.Minute)
var dbmap *gorp.DbMap
@@ -205,6 +205,26 @@ func setupConnection(con_type string, driver string, dataSource string, maxIdle
return dbmap
}
func (ss SqlStore) TotalMasterDbConnections() int {
return ss.GetMaster().Db.Stats().OpenConnections
}
func (ss SqlStore) TotalReadDbConnections() int {
if len(utils.Cfg.SqlSettings.DataSourceReplicas) == 0 {
return 0
} else {
count := 0
for _, db := range ss.replicas {
count = count + db.Db.Stats().OpenConnections
}
return count
}
return 0
}
func (ss SqlStore) GetCurrentSchemaVersion() string {
version, _ := ss.GetMaster().SelectStr("SELECT Value FROM Systems WHERE Name='Version'")
return version

View File

@@ -5,6 +5,7 @@ package store
import (
"database/sql"
"strconv"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
@@ -441,14 +442,14 @@ func (s SqlTeamStore) GetMember(teamId string, userId string) StoreChannel {
return storeChannel
}
func (s SqlTeamStore) GetMembers(teamId string) StoreChannel {
func (s SqlTeamStore) GetMembers(teamId string, offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
var members []*model.TeamMember
_, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId})
_, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId AND DeleteAt = 0 LIMIT :Limit OFFSET :Offset", map[string]interface{}{"TeamId": teamId, "Offset": offset, "Limit": limit})
if err != nil {
result.Err = model.NewLocAppError("SqlTeamStore.GetMembers", "store.sql_team.get_members.app_error", nil, "teamId="+teamId+" "+err.Error())
} else {
@@ -462,6 +463,70 @@ func (s SqlTeamStore) GetMembers(teamId string) StoreChannel {
return storeChannel
}
func (s SqlTeamStore) GetMemberCount(teamId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
count, err := s.GetReplica().SelectInt(`
SELECT
count(*)
FROM
TeamMembers,
Users
WHERE
TeamMembers.UserId = Users.Id
AND TeamMembers.TeamId = :TeamId
AND TeamMembers.DeleteAt = 0
AND Users.DeleteAt = 0`, map[string]interface{}{"TeamId": teamId})
if err != nil {
result.Err = model.NewLocAppError("SqlTeamStore.GetMemberCount", "store.sql_team.get_member_count.app_error", nil, "teamId="+teamId+" "+err.Error())
} else {
result.Data = count
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlTeamStore) GetMembersByIds(teamId string, userIds []string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
var members []*model.TeamMember
props := make(map[string]interface{})
idQuery := ""
for index, userId := range userIds {
if len(idQuery) > 0 {
idQuery += ", "
}
props["userId"+strconv.Itoa(index)] = userId
idQuery += ":userId" + strconv.Itoa(index)
}
props["TeamId"] = teamId
if _, err := s.GetReplica().Select(&members, "SELECT * FROM TeamMembers WHERE TeamId = :TeamId AND UserId IN ("+idQuery+") AND DeleteAt = 0", props); err != nil {
result.Err = model.NewLocAppError("SqlTeamStore.GetMembersByIds", "store.sql_team.get_members_by_ids.app_error", nil, "teamId="+teamId+" "+err.Error())
} else {
result.Data = members
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlTeamStore) GetTeamsForUser(userId string) StoreChannel {
storeChannel := make(StoreChannel, 1)

View File

@@ -298,7 +298,7 @@ func TestTeamMembers(t *testing.T) {
Must(store.Team().SaveMember(m2))
Must(store.Team().SaveMember(m3))
if r1 := <-store.Team().GetMembers(teamId1); r1.Err != nil {
if r1 := <-store.Team().GetMembers(teamId1, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
ms := r1.Data.([]*model.TeamMember)
@@ -308,7 +308,7 @@ func TestTeamMembers(t *testing.T) {
}
}
if r1 := <-store.Team().GetMembers(teamId2); r1.Err != nil {
if r1 := <-store.Team().GetMembers(teamId2, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
ms := r1.Data.([]*model.TeamMember)
@@ -342,7 +342,7 @@ func TestTeamMembers(t *testing.T) {
t.Fatal(r1.Err)
}
if r1 := <-store.Team().GetMembers(teamId1); r1.Err != nil {
if r1 := <-store.Team().GetMembers(teamId1, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
ms := r1.Data.([]*model.TeamMember)
@@ -363,7 +363,7 @@ func TestTeamMembers(t *testing.T) {
t.Fatal(r1.Err)
}
if r1 := <-store.Team().GetMembers(teamId1); r1.Err != nil {
if r1 := <-store.Team().GetMembers(teamId1, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
ms := r1.Data.([]*model.TeamMember)
@@ -434,3 +434,74 @@ func TestGetTeamMember(t *testing.T) {
t.Fatal("empty team id - should have failed")
}
}
func TestGetTeamMembersByIds(t *testing.T) {
Setup()
teamId1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
Must(store.Team().SaveMember(m1))
if r := <-store.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId}); r.Err != nil {
t.Fatal(r.Err)
} else {
rm1 := r.Data.([]*model.TeamMember)[0]
if rm1.TeamId != m1.TeamId {
t.Fatal("bad team id")
}
if rm1.UserId != m1.UserId {
t.Fatal("bad user id")
}
}
m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
Must(store.Team().SaveMember(m2))
if r := <-store.Team().GetMembersByIds(m1.TeamId, []string{m1.UserId, m2.UserId, model.NewId()}); r.Err != nil {
t.Fatal(r.Err)
} else {
rm := r.Data.([]*model.TeamMember)
if len(rm) != 2 {
t.Fatal("return wrong number of results")
}
}
if r := <-store.Team().GetMembersByIds(m1.TeamId, []string{}); r.Err == nil {
t.Fatal("empty user ids - should have failed")
}
}
func TestTeamStoreMemberCount(t *testing.T) {
Setup()
u1 := &model.User{}
u1.Email = model.NewId()
Must(store.User().Save(u1))
teamId1 := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: u1.Id}
Must(store.Team().SaveMember(m1))
if result := <-store.Team().GetMemberCount(teamId1); result.Err != nil {
t.Fatal(result.Err)
} else {
if result.Data.(int64) != 1 {
t.Fatal("wrong count")
}
}
m2 := &model.TeamMember{TeamId: teamId1, UserId: model.NewId()}
Must(store.Team().SaveMember(m2))
if result := <-store.Team().GetMemberCount(teamId1); result.Err != nil {
t.Fatal(result.Err)
} else {
if result.Data.(int64) != 1 {
t.Fatal("wrong count")
}
}
}

View File

@@ -15,14 +15,20 @@ import (
)
const (
MISSING_ACCOUNT_ERROR = "store.sql_user.missing_account.const"
MISSING_AUTH_ACCOUNT_ERROR = "store.sql_user.get_by_auth.missing_account.app_error"
MISSING_ACCOUNT_ERROR = "store.sql_user.missing_account.const"
MISSING_AUTH_ACCOUNT_ERROR = "store.sql_user.get_by_auth.missing_account.app_error"
PROFILES_IN_CHANNEL_CACHE_SIZE = 5000
PROFILES_IN_CHANNEL_CACHE_SEC = 900 // 15 mins
USER_SEARCH_TYPE_ALL = "Username, FirstName, LastName, Nickname"
USER_SEARCH_TYPE_USERNAME = "Username"
)
type SqlUserStore struct {
*SqlStore
}
var profilesInChannelCache *utils.Cache = utils.NewLru(PROFILES_IN_CHANNEL_CACHE_SIZE)
func NewSqlUserStore(sqlStore *SqlStore) UserStore {
us := &SqlUserStore{sqlStore}
@@ -49,6 +55,9 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
func (us SqlUserStore) CreateIndexesIfNotExists() {
us.CreateIndexIfNotExists("idx_users_email", "Users", "Email")
us.CreateFullTextIndexIfNotExists("idx_users_username_txt", "Users", USER_SEARCH_TYPE_USERNAME)
us.CreateFullTextIndexIfNotExists("idx_users_all_names_txt", "Users", USER_SEARCH_TYPE_ALL)
}
func (us SqlUserStore) Save(user *model.User) StoreChannel {
@@ -457,7 +466,7 @@ func (s SqlUserStore) GetEtagForAllProfiles() StoreChannel {
return storeChannel
}
func (us SqlUserStore) GetAllProfiles() StoreChannel {
func (us SqlUserStore) GetAllProfiles(offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -466,8 +475,8 @@ func (us SqlUserStore) GetAllProfiles() StoreChannel {
var users []*model.User
if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users"); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users ORDER BY Username ASC LIMIT :Limit OFFSET :Offset", map[string]interface{}{"Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetAllProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {
userMap := make(map[string]*model.User)
@@ -509,7 +518,7 @@ func (s SqlUserStore) GetEtagForProfiles(teamId string) StoreChannel {
return storeChannel
}
func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
func (us SqlUserStore) GetProfiles(teamId string, offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -518,7 +527,7 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
var users []*model.User
if _, err := us.GetReplica().Select(&users, "SELECT Users.* FROM Users, TeamMembers WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId", map[string]interface{}{"TeamId": teamId}); err != nil {
if _, err := us.GetReplica().Select(&users, "SELECT Users.* FROM Users, TeamMembers WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId AND TeamMembers.DeleteAt = 0 ORDER BY Users.Username ASC LIMIT :Limit OFFSET :Offset", map[string]interface{}{"TeamId": teamId, "Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {
@@ -541,45 +550,36 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel {
return storeChannel
}
func (us SqlUserStore) GetDirectProfiles(userId string) StoreChannel {
func (us SqlUserStore) InvalidateProfilesInChannelCache(channelId string) {
profilesInChannelCache.Remove(channelId)
}
storeChannel := make(StoreChannel, 1)
func (us SqlUserStore) GetProfilesInChannel(channelId string, offset int, limit int, allowFromCache bool) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if allowFromCache && offset == -1 && limit == -1 {
if cacheItem, ok := profilesInChannelCache.Get(channelId); ok {
result.Data = cacheItem.(map[string]*model.User)
storeChannel <- result
close(storeChannel)
return
}
}
var users []*model.User
if _, err := us.GetReplica().Select(&users, `
SELECT
Users.*
FROM
Users
WHERE
Id IN (SELECT DISTINCT
UserId
FROM
ChannelMembers
WHERE
ChannelMembers.UserId != :UserId
AND ChannelMembers.ChannelId IN (SELECT
Channels.Id
FROM
Channels,
ChannelMembers
WHERE
Channels.Type = 'D'
AND Channels.Id = ChannelMembers.ChannelId
AND ChannelMembers.UserId = :UserId))
OR Id IN (SELECT
Name
FROM
Preferences
WHERE
UserId = :UserId
AND Category = 'direct_channel_show')
`, map[string]interface{}{"UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetDirectProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
query := "SELECT Users.* FROM Users, ChannelMembers WHERE ChannelMembers.ChannelId = :ChannelId AND Users.Id = ChannelMembers.UserId AND Users.DeleteAt = 0"
if limit >= 0 && offset >= 0 {
query += " ORDER BY Users.Username ASC LIMIT :Limit OFFSET :Offset"
}
if _, err := us.GetReplica().Select(&users, query, map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetProfilesInChannel", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {
userMap := make(map[string]*model.User)
@@ -592,6 +592,148 @@ func (us SqlUserStore) GetDirectProfiles(userId string) StoreChannel {
}
result.Data = userMap
if allowFromCache && offset == -1 && limit == -1 {
profilesInChannelCache.AddWithExpiresInSecs(channelId, userMap, PROFILES_IN_CHANNEL_CACHE_SEC)
}
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var users []*model.User
if _, err := us.GetReplica().Select(&users, `
SELECT
u.*
FROM Users u
INNER JOIN TeamMembers tm
ON tm.UserId = u.Id
AND tm.TeamId = :TeamId
LEFT JOIN ChannelMembers cm
ON cm.UserId = u.Id
AND cm.ChannelId = :ChannelId
WHERE cm.UserId IS NULL
ORDER BY u.Username ASC
LIMIT :Limit OFFSET :Offset
`, map[string]interface{}{"TeamId": teamId, "ChannelId": channelId, "Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetProfilesNotInChannel", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {
userMap := make(map[string]*model.User)
for _, u := range users {
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
userMap[u.Id] = u
}
result.Data = userMap
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) GetProfilesByUsernames(usernames []string, teamId string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var users []*model.User
props := make(map[string]interface{})
idQuery := ""
for index, usernames := range usernames {
if len(idQuery) > 0 {
idQuery += ", "
}
props["username"+strconv.Itoa(index)] = usernames
idQuery += ":username" + strconv.Itoa(index)
}
props["TeamId"] = teamId
if _, err := us.GetReplica().Select(&users, `SELECT Users.* FROM Users INNER JOIN TeamMembers ON
Users.Id = TeamMembers.UserId AND Users.Username IN (`+idQuery+`) AND TeamMembers.TeamId = :TeamId `, props); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetProfilesByUsernames", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {
userMap := make(map[string]*model.User)
for _, u := range users {
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
userMap[u.Id] = u
}
result.Data = userMap
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
type UserWithLastActivityAt struct {
model.User
LastActivityAt int64
}
func (us SqlUserStore) GetRecentlyActiveUsersForTeam(teamId string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var users []*UserWithLastActivityAt
if _, err := us.GetReplica().Select(&users, `
SELECT
u.*,
s.LastActivityAt
FROM Users AS u
INNER JOIN TeamMembers AS t ON u.Id = t.UserId
INNER JOIN Status AS s ON s.UserId = t.UserId
WHERE t.TeamId = :TeamId
ORDER BY s.LastActivityAt DESC
LIMIT 100
`, map[string]interface{}{"TeamId": teamId}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetRecentlyActiveUsers", "store.sql_user.get_recently_active_users.app_error", nil, err.Error())
} else {
userMap := make(map[string]*model.User)
for _, userWithLastActivityAt := range users {
u := userWithLastActivityAt.User
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
u.LastActivityAt = userWithLastActivityAt.LastActivityAt
userMap[u.Id] = &u
}
result.Data = userMap
}
storeChannel <- result
@@ -938,3 +1080,144 @@ func (us SqlUserStore) GetUnreadCountForChannel(userId string, channelId string)
return storeChannel
}
func (us SqlUserStore) Search(teamId string, term string, searchType string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
searchQuery := ""
if teamId == "" {
searchQuery = `
SELECT
*
FROM
Users
WHERE
DeleteAt = 0
SEARCH_CLAUSE
ORDER BY Username ASC
LIMIT 50`
} else {
searchQuery = `
SELECT
Users.*
FROM
Users, TeamMembers
WHERE
TeamMembers.TeamId = :TeamId
AND Users.Id = TeamMembers.UserId
AND Users.DeleteAt = 0
AND TeamMembers.DeleteAt = 0
SEARCH_CLAUSE
ORDER BY Users.Username ASC
LIMIT 100`
}
storeChannel <- us.performSearch(searchQuery, term, searchType, map[string]interface{}{"TeamId": teamId})
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, searchType string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
searchQuery := ""
if teamId == "" {
searchQuery = `
SELECT
u.*
FROM Users u
LEFT JOIN ChannelMembers cm
ON cm.UserId = u.Id
AND cm.ChannelId = :ChannelId
WHERE cm.UserId IS NULL
SEARCH_CLAUSE
ORDER BY u.Username ASC
LIMIT 100`
} else {
searchQuery = `
SELECT
u.*
FROM Users u
INNER JOIN TeamMembers tm
ON tm.UserId = u.Id
AND tm.TeamId = :TeamId
LEFT JOIN ChannelMembers cm
ON cm.UserId = u.Id
AND cm.ChannelId = :ChannelId
WHERE cm.UserId IS NULL
SEARCH_CLAUSE
ORDER BY u.Username ASC
LIMIT 100`
}
storeChannel <- us.performSearch(searchQuery, term, searchType, map[string]interface{}{"TeamId": teamId, "ChannelId": channelId})
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) SearchInChannel(channelId string, term string, searchType string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
searchQuery := `
SELECT
Users.*
FROM
Users, ChannelMembers
WHERE
ChannelMembers.ChannelId = :ChannelId
AND ChannelMembers.UserId = Users.Id
AND Users.DeleteAt = 0
SEARCH_CLAUSE
ORDER BY Users.Username ASC
LIMIT 100`
storeChannel <- us.performSearch(searchQuery, term, searchType, map[string]interface{}{"ChannelId": channelId})
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) performSearch(searchQuery string, term string, searchType string, parameters map[string]interface{}) StoreResult {
result := StoreResult{}
if term == "" {
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
term = term + ":*"
searchClause := fmt.Sprintf("AND (%s) @@ to_tsquery(:Term)", searchType)
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
term = term + "*"
searchClause := fmt.Sprintf("AND MATCH(%s) AGAINST (:Term IN BOOLEAN MODE)", searchType)
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
}
var users []*model.User
parameters["Term"] = term
if _, err := us.GetReplica().Select(&users, searchQuery, parameters); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.Search", "store.sql_user.search.app_error", nil, "term="+term+", "+"search_type="+searchType+", "+err.Error())
} else {
for _, u := range users {
u.Password = ""
u.AuthData = new(string)
*u.AuthData = ""
}
result.Data = users
}
return result
}

View File

@@ -205,7 +205,7 @@ func TestUserStoreGetAllProfiles(t *testing.T) {
Must(store.User().Save(u2))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
if r1 := <-store.User().GetAllProfiles(); r1.Err != nil {
if r1 := <-store.User().GetAllProfiles(0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
@@ -213,6 +213,15 @@ func TestUserStoreGetAllProfiles(t *testing.T) {
t.Fatal("invalid returned users")
}
}
if r2 := <-store.User().GetAllProfiles(0, 1); r2.Err != nil {
t.Fatal(r2.Err)
} else {
users := r2.Data.(map[string]*model.User)
if len(users) != 1 {
t.Fatal("invalid returned users, limit did not work")
}
}
}
func TestUserStoreGetProfiles(t *testing.T) {
@@ -230,7 +239,7 @@ func TestUserStoreGetProfiles(t *testing.T) {
Must(store.User().Save(u2))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
if r1 := <-store.User().GetProfiles(teamId); r1.Err != nil {
if r1 := <-store.User().GetProfiles(teamId, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
@@ -243,7 +252,7 @@ func TestUserStoreGetProfiles(t *testing.T) {
}
}
if r2 := <-store.User().GetProfiles("123"); r2.Err != nil {
if r2 := <-store.User().GetProfiles("123", 0, 100); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if len(r2.Data.(map[string]*model.User)) != 0 {
@@ -252,7 +261,7 @@ func TestUserStoreGetProfiles(t *testing.T) {
}
}
func TestUserStoreGetDirectProfiles(t *testing.T) {
func TestUserStoreGetProfilesInChannel(t *testing.T) {
Setup()
teamId := model.NewId()
@@ -267,7 +276,151 @@ func TestUserStoreGetDirectProfiles(t *testing.T) {
Must(store.User().Save(u2))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
if r1 := <-store.User().GetDirectProfiles(u1.Id); r1.Err != nil {
c1 := model.Channel{}
c1.TeamId = teamId
c1.DisplayName = "Profiles in channel"
c1.Name = "profiles-" + model.NewId()
c1.Type = model.CHANNEL_OPEN
c2 := model.Channel{}
c2.TeamId = teamId
c2.DisplayName = "Profiles in private"
c2.Name = "profiles-" + model.NewId()
c2.Type = model.CHANNEL_PRIVATE
Must(store.Channel().Save(&c1))
Must(store.Channel().Save(&c2))
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = c1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
m3 := model.ChannelMember{}
m3.ChannelId = c2.Id
m3.UserId = u1.Id
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
Must(store.Channel().SaveMember(&m2))
Must(store.Channel().SaveMember(&m3))
if r1 := <-store.User().GetProfilesInChannel(c1.Id, -1, -1, false); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
if len(users) != 2 {
t.Fatal("invalid returned users")
}
if users[u1.Id].Id != u1.Id {
t.Fatal("invalid returned user")
}
}
if r2 := <-store.User().GetProfilesInChannel(c2.Id, -1, -1, false); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if len(r2.Data.(map[string]*model.User)) != 1 {
t.Fatal("should have returned empty map")
}
}
if r2 := <-store.User().GetProfilesInChannel(c2.Id, -1, -1, true); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if len(r2.Data.(map[string]*model.User)) != 1 {
t.Fatal("should have returned empty map")
}
}
if r2 := <-store.User().GetProfilesInChannel(c2.Id, -1, -1, true); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if len(r2.Data.(map[string]*model.User)) != 1 {
t.Fatal("should have returned empty map")
}
}
store.User().InvalidateProfilesInChannelCache(c2.Id)
}
func TestUserStoreGetProfilesNotInChannel(t *testing.T) {
Setup()
teamId := model.NewId()
u1 := &model.User{}
u1.Email = model.NewId()
Must(store.User().Save(u1))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}))
u2 := &model.User{}
u2.Email = model.NewId()
Must(store.User().Save(u2))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
c1 := model.Channel{}
c1.TeamId = teamId
c1.DisplayName = "Profiles in channel"
c1.Name = "profiles-" + model.NewId()
c1.Type = model.CHANNEL_OPEN
c2 := model.Channel{}
c2.TeamId = teamId
c2.DisplayName = "Profiles in private"
c2.Name = "profiles-" + model.NewId()
c2.Type = model.CHANNEL_PRIVATE
Must(store.Channel().Save(&c1))
Must(store.Channel().Save(&c2))
if r1 := <-store.User().GetProfilesNotInChannel(teamId, c1.Id, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
if len(users) != 2 {
t.Fatal("invalid returned users")
}
if users[u1.Id].Id != u1.Id {
t.Fatal("invalid returned user")
}
}
if r2 := <-store.User().GetProfilesNotInChannel(teamId, c2.Id, 0, 100); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if len(r2.Data.(map[string]*model.User)) != 2 {
t.Fatal("invalid returned users")
}
}
m1 := model.ChannelMember{}
m1.ChannelId = c1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = c1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
m3 := model.ChannelMember{}
m3.ChannelId = c2.Id
m3.UserId = u1.Id
m3.NotifyProps = model.GetDefaultChannelNotifyProps()
Must(store.Channel().SaveMember(&m1))
Must(store.Channel().SaveMember(&m2))
Must(store.Channel().SaveMember(&m3))
if r1 := <-store.User().GetProfilesNotInChannel(teamId, c1.Id, 0, 100); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
@@ -276,11 +429,11 @@ func TestUserStoreGetDirectProfiles(t *testing.T) {
}
}
if r2 := <-store.User().GetDirectProfiles("123"); r2.Err != nil {
if r2 := <-store.User().GetProfilesNotInChannel(teamId, c2.Id, 0, 100); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if len(r2.Data.(map[string]*model.User)) != 0 {
t.Fatal("should have returned empty map")
if len(r2.Data.(map[string]*model.User)) != 1 {
t.Fatal("should have had 1 user not in channel")
}
}
}
@@ -326,7 +479,7 @@ func TestUserStoreGetProfilesByIds(t *testing.T) {
}
}
if r2 := <-store.User().GetProfiles("123"); r2.Err != nil {
if r2 := <-store.User().GetProfiles("123", 0, 100); r2.Err != nil {
t.Fatal(r2.Err)
} else {
if len(r2.Data.(map[string]*model.User)) != 0 {
@@ -335,6 +488,50 @@ func TestUserStoreGetProfilesByIds(t *testing.T) {
}
}
func TestUserStoreGetProfilesByUsernames(t *testing.T) {
Setup()
teamId := model.NewId()
u1 := &model.User{}
u1.Email = model.NewId()
u1.Username = "username1" + model.NewId()
Must(store.User().Save(u1))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}))
u2 := &model.User{}
u2.Email = model.NewId()
u2.Username = "username2" + model.NewId()
Must(store.User().Save(u2))
Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
if r1 := <-store.User().GetProfilesByUsernames([]string{u1.Username, u2.Username}, teamId); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
if len(users) != 2 {
t.Fatal("invalid returned users")
}
if users[u1.Id].Id != u1.Id {
t.Fatal("invalid returned user")
}
}
if r1 := <-store.User().GetProfilesByUsernames([]string{u1.Username}, teamId); r1.Err != nil {
t.Fatal(r1.Err)
} else {
users := r1.Data.(map[string]*model.User)
if len(users) != 1 {
t.Fatal("invalid returned users")
}
if users[u1.Id].Id != u1.Id {
t.Fatal("invalid returned user")
}
}
}
func TestUserStoreGetSystemAdminProfiles(t *testing.T) {
Setup()
@@ -713,3 +910,216 @@ func TestUserStoreUpdateMfaActive(t *testing.T) {
t.Fatal(err)
}
}
func TestUserStoreGetRecentlyActiveUsersForTeam(t *testing.T) {
Setup()
u1 := &model.User{}
u1.Email = model.NewId()
Must(store.User().Save(u1))
Must(store.Status().SaveOrUpdate(&model.Status{u1.Id, model.STATUS_ONLINE, false, model.GetMillis(), ""}))
tid := model.NewId()
Must(store.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}))
if r1 := <-store.User().GetRecentlyActiveUsersForTeam(tid); r1.Err != nil {
t.Fatal(r1.Err)
}
}
func TestUserStoreSearch(t *testing.T) {
Setup()
u1 := &model.User{}
u1.Username = "jimbo" + model.NewId()
u1.FirstName = "Tim"
u1.LastName = "Bill"
u1.Nickname = "Rob"
u1.Email = model.NewId()
Must(store.User().Save(u1))
tid := model.NewId()
Must(store.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}))
if r1 := <-store.User().Search(tid, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if !found {
t.Fatal("should have found user")
}
}
if r1 := <-store.User().Search("", "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if !found {
t.Fatal("should have found user")
}
}
if r1 := <-store.User().Search(tid, "", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
t.Fatal(r1.Err)
}
c1 := model.Channel{}
c1.TeamId = tid
c1.DisplayName = "NameName"
c1.Name = "a" + model.NewId() + "b"
c1.Type = model.CHANNEL_OPEN
c1 = *Must(store.Channel().Save(&c1)).(*model.Channel)
if r1 := <-store.User().SearchNotInChannel(tid, c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if !found {
t.Fatal("should have found user")
}
}
if r1 := <-store.User().SearchNotInChannel("", c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if !found {
t.Fatal("should have found user")
}
}
if r1 := <-store.User().SearchNotInChannel("junk", c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if found {
t.Fatal("should not have found user")
}
}
if r1 := <-store.User().SearchInChannel(c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if found {
t.Fatal("should not have found user")
}
}
Must(store.Channel().SaveMember(&model.ChannelMember{ChannelId: c1.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}))
if r1 := <-store.User().SearchInChannel(c1.Id, "jimb", USER_SEARCH_TYPE_USERNAME); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if !found {
t.Fatal("should have found user")
}
}
if r1 := <-store.User().Search(tid, "Tim", USER_SEARCH_TYPE_ALL); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if !found {
t.Fatal("should have found user")
}
}
if r1 := <-store.User().Search(tid, "Bill", USER_SEARCH_TYPE_ALL); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if !found {
t.Fatal("should have found user")
}
}
if r1 := <-store.User().Search(tid, "Rob", USER_SEARCH_TYPE_ALL); r1.Err != nil {
t.Fatal(r1.Err)
} else {
profiles := r1.Data.([]*model.User)
found := false
for _, profile := range profiles {
if profile.Id == u1.Id {
found = true
break
}
}
if !found {
t.Fatal("should have found user")
}
}
}

View File

@@ -49,6 +49,8 @@ type Store interface {
MarkSystemRanUnitTests()
Close()
DropAllTables()
TotalMasterDbConnections() int
TotalReadDbConnections() int
}
type TeamStore interface {
@@ -66,7 +68,9 @@ type TeamStore interface {
SaveMember(member *model.TeamMember) StoreChannel
UpdateMember(member *model.TeamMember) StoreChannel
GetMember(teamId string, userId string) StoreChannel
GetMembers(teamId string) StoreChannel
GetMembers(teamId string, offset int, limit int) StoreChannel
GetMembersByIds(teamId string, userIds []string) StoreChannel
GetMemberCount(teamId string) StoreChannel
GetTeamsForUser(userId string) StoreChannel
RemoveMember(teamId string, userId string) StoreChannel
RemoveAllMembersByTeam(teamId string) StoreChannel
@@ -89,16 +93,17 @@ type ChannelStore interface {
GetChannelCounts(teamId string, userId string) StoreChannel
GetAll(teamId string) StoreChannel
GetForPost(postId string) StoreChannel
SaveMember(member *model.ChannelMember) StoreChannel
UpdateMember(member *model.ChannelMember) StoreChannel
GetMembers(channelId string) StoreChannel
GetMember(channelId string, userId string) StoreChannel
GetAllChannelMembersForUser(userId string, allowFromCache bool) StoreChannel
InvalidateAllChannelMembersForUser(userId string)
IsUserInChannelUseCache(userId string, channelId string) bool
GetMemberForPost(postId string, userId string) StoreChannel
GetMemberCount(channelId string) StoreChannel
RemoveMember(channelId string, userId string) StoreChannel
PermanentDeleteMembersByUser(userId string) StoreChannel
GetExtraMembers(channelId string, limit int) StoreChannel
UpdateLastViewedAt(channelId string, userId string) StoreChannel
SetLastViewedAt(channelId string, userId string, newLastViewedAt int64) StoreChannel
IncrementMentionCount(channelId string, userId string) StoreChannel
@@ -135,9 +140,12 @@ type UserStore interface {
UpdateMfaActive(userId string, active bool) StoreChannel
Get(id string) StoreChannel
GetAll() StoreChannel
GetAllProfiles() StoreChannel
GetProfiles(teamId string) StoreChannel
GetDirectProfiles(userId string) StoreChannel
InvalidateProfilesInChannelCache(channelId string)
GetProfilesInChannel(channelId string, offset int, limit int, allowFromCache bool) StoreChannel
GetProfilesNotInChannel(teamId string, channelId string, offset int, limit int) StoreChannel
GetProfilesByUsernames(usernames []string, teamId string) StoreChannel
GetAllProfiles(offset int, limit int) StoreChannel
GetProfiles(teamId string, offset int, limit int) StoreChannel
GetProfileByIds(userId []string) StoreChannel
GetByEmail(email string) StoreChannel
GetByAuth(authData *string, authService string) StoreChannel
@@ -147,7 +155,6 @@ type UserStore interface {
VerifyEmail(userId string) StoreChannel
GetEtagForAllProfiles() StoreChannel
GetEtagForProfiles(teamId string) StoreChannel
GetEtagForDirectProfiles(userId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetTotalUsersCount() StoreChannel
GetSystemAdminProfiles() StoreChannel
@@ -155,6 +162,10 @@ type UserStore interface {
AnalyticsUniqueUserCount(teamId string) StoreChannel
GetUnreadCount(userId string) StoreChannel
GetUnreadCountForChannel(userId string, channelId string) StoreChannel
GetRecentlyActiveUsersForTeam(teamId string) StoreChannel
Search(teamId string, term string, searchType string) StoreChannel
SearchInChannel(channelId string, term string, searchType string) StoreChannel
SearchNotInChannel(teamId string, channelId string, term string, searchType string) StoreChannel
}
type SessionStore interface {
@@ -274,6 +285,7 @@ type EmojiStore interface {
type StatusStore interface {
SaveOrUpdate(status *model.Status) StoreChannel
Get(userId string) StoreChannel
GetByIds(userIds []string) StoreChannel
GetOnlineAway() StoreChannel
GetOnline() StoreChannel
GetAllFromTeam(teamId string) StoreChannel

View File

@@ -23,7 +23,7 @@ func Setup() {
utils.TranslationsPreInit()
utils.LoadConfig("config.json")
utils.InitTranslations(utils.Cfg.LocalizationSettings)
api.NewServer()
api.NewServer(false)
api.StartServer()
api.InitApi()
InitWeb()
@@ -161,7 +161,7 @@ func TestGetAccessToken(t *testing.T) {
}
}
if result, err := ApiClient.DoApiGet("/users/profiles/"+teamId+"?access_token="+token, "", ""); err != nil {
if result, err := ApiClient.DoApiGet("/teams/"+teamId+"/users/0/100?access_token="+token, "", ""); err != nil {
t.Fatal(err)
} else {
userMap := model.UserMapFromJson(result.Body)
@@ -170,16 +170,16 @@ func TestGetAccessToken(t *testing.T) {
}
}
if _, err := ApiClient.DoApiGet("/users/profiles/"+teamId, "", ""); err == nil {
if _, err := ApiClient.DoApiGet("/teams/"+teamId+"/users/0/100", "", ""); err == nil {
t.Fatal("should have failed - no access token provided")
}
if _, err := ApiClient.DoApiGet("/users/profiles/"+teamId+"?access_token=junk", "", ""); err == nil {
if _, err := ApiClient.DoApiGet("/teams/"+teamId+"/users/0/100?access_token=junk", "", ""); err == nil {
t.Fatal("should have failed - bad access token provided")
}
ApiClient.SetOAuthToken(token)
if result, err := ApiClient.DoApiGet("/users/profiles/"+teamId, "", ""); err != nil {
if result, err := ApiClient.DoApiGet("/teams/"+teamId+"/users/0/100", "", ""); err != nil {
t.Fatal(err)
} else {
userMap := model.UserMapFromJson(result.Body)

View File

@@ -1,18 +1,26 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {browserHistory} from 'react-router/es6';
import * as Utils from 'utils/utils.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {Preferences, ActionTypes} from 'utils/constants.jsx';
import {browserHistory} from 'react-router/es6';
export function goToChannel(channel) {
if (channel.fake) {
Utils.openDirectChannelToUser(
openDirectChannelToUser(
UserStore.getProfileByUsername(channel.display_name),
() => {
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
@@ -53,3 +61,124 @@ export function setChannelAsRead(channelIdParam) {
ChannelStore.emitLastViewed(Number.MAX_VALUE, false);
}
}
export function addUserToChannel(channelId, userId, success, error) {
Client.addChannelMember(
channelId,
userId,
(data) => {
UserStore.removeProfileNotInChannel(channelId, userId);
const profile = UserStore.getProfile(userId);
if (profile) {
UserStore.saveProfileInChannel(channelId, profile);
UserStore.emitInChannelChange();
}
UserStore.emitNotInChannelChange();
if (success) {
success(data);
}
},
(err) => {
AsyncClient.dispatchError(err, 'addChannelMember');
if (error) {
error(err);
}
}
);
}
export function removeUserFromChannel(channelId, userId, success, error) {
Client.removeChannelMember(
channelId,
userId,
(data) => {
UserStore.removeProfileInChannel(channelId, userId);
const profile = UserStore.getProfile(userId);
if (profile) {
UserStore.saveProfileNotInChannel(channelId, profile);
UserStore.emitNotInChannelChange();
}
UserStore.emitInChannelChange();
if (success) {
success(data);
}
},
(err) => {
AsyncClient.dispatchError(err, 'removeChannelMember');
if (error) {
error(err);
}
}
);
}
export function openDirectChannelToUser(user, success, error) {
const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), user.id);
let channel = ChannelStore.getByName(channelName);
if (channel) {
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
loadProfilesAndTeamMembersForDMSidebar();
AsyncClient.savePreference(
Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
user.id,
'true'
);
if (success) {
success(channel, true);
}
return;
}
channel = {
name: channelName,
last_post_at: 0,
total_msg_count: 0,
type: 'D',
display_name: user.username,
teammate_id: user.id,
status: UserStore.getStatus(user.id)
};
Client.createDirectChannel(
user.id,
(data) => {
Client.getChannel(
data.id,
(data2) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_CHANNEL,
channel: data2.channel,
member: data2.member
});
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
loadProfilesAndTeamMembersForDMSidebar();
AsyncClient.savePreference(
Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
user.id,
'true'
);
if (success) {
success(data2.channel, false);
}
}
);
},
() => {
browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/' + channelName);
if (error) {
error();
}
}
);
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import UserStore from 'stores/user_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import {ActionTypes} from 'utils/constants.jsx';
export function loadEmoji(getProfiles = true) {
Client.listEmoji(
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_CUSTOM_EMOJIS,
emojis: data
});
if (getProfiles) {
loadProfilesForEmoji(data);
}
},
(err) => {
AsyncClient.dispatchError(err, 'listEmoji');
}
);
}
function loadProfilesForEmoji(emojiList) {
const profilesToLoad = {};
for (let i = 0; i < emojiList.length; i++) {
const emoji = emojiList[i];
if (!UserStore.hasProfile(emoji.creator_id)) {
profilesToLoad[emoji.creator_id] = true;
}
}
const list = Object.keys(profilesToLoad);
if (list.length === 0) {
return;
}
AsyncClient.getProfilesByIds(list);
}

View File

@@ -12,7 +12,8 @@ import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import SearchStore from 'stores/search_store.jsx';
import {handleNewPost} from 'actions/post_actions.jsx';
import {handleNewPost, loadPosts, loadPostsBefore, loadPostsAfter} from 'actions/post_actions.jsx';
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
@@ -43,9 +44,9 @@ export function emitChannelClickEvent(channel) {
function switchToChannel(chan) {
AsyncClient.getChannels(true);
AsyncClient.getMoreChannels(true);
AsyncClient.getChannelExtraInfo(chan.id);
AsyncClient.getChannelStats(chan.id);
AsyncClient.updateLastViewedAt(chan.id);
AsyncClient.getPosts(chan.id);
loadPosts(chan.id);
trackPage();
AppDispatcher.handleViewAction({
@@ -108,7 +109,7 @@ export function emitInitialLoad(callback) {
if (data.team_members) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_TEAM_MEMBERS,
type: ActionTypes.RECEIVED_MY_TEAM_MEMBERS,
team_members: data.team_members
});
}
@@ -143,9 +144,9 @@ export function doFocusPost(channelId, postId, data) {
});
AsyncClient.getChannels(true);
AsyncClient.getMoreChannels(true);
AsyncClient.getChannelExtraInfo(channelId);
AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true);
AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true);
AsyncClient.getChannelStats(channelId);
loadPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true);
loadPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS, true);
}
export function emitPostFocusEvent(postId, onSuccess) {
@@ -246,14 +247,14 @@ export function emitLoadMorePostsFocusedTopEvent() {
export function loadMorePostsTop(id, isFocusPost) {
const earliestPostId = PostStore.getEarliestPost(id).id;
if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) {
AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE, isFocusPost);
loadPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE, isFocusPost);
}
}
export function emitLoadMorePostsFocusedBottomEvent() {
const id = PostStore.getFocusedPostId();
const latestPostId = PostStore.getLatestPost(id).id;
AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE, Boolean(id));
loadPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE, Boolean(id));
}
export function emitUserPostedEvent(post) {
@@ -362,7 +363,7 @@ export function emitClearSuggestions(suggestionId) {
export function emitPreferenceChangedEvent(preference) {
if (preference.category === Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW) {
AsyncClient.getDirectProfiles();
loadProfilesAndTeamMembersForDMSidebar();
}
AppDispatcher.handleServerAction({
@@ -437,7 +438,7 @@ export function loadDefaultLocale() {
export function viewLoggedIn() {
AsyncClient.getChannels();
AsyncClient.getMoreChannels();
AsyncClient.getChannelExtraInfo();
AsyncClient.getChannelStats();
// Clear pending posts (shouldn't have pending posts if we are loading)
PostStore.clearPendingPosts();

View File

@@ -0,0 +1,114 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import {ActionTypes} from 'utils/constants.jsx';
export function loadIncomingHooks() {
Client.listIncomingHooks(
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_INCOMING_WEBHOOKS,
teamId: TeamStore.getCurrentId(),
incomingWebhooks: data
});
loadProfilesForIncomingHooks(data);
},
(err) => {
AsyncClient.dispatchError(err, 'listIncomingHooks');
}
);
}
function loadProfilesForIncomingHooks(hooks) {
const profilesToLoad = {};
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i];
if (!UserStore.hasProfile(hook.user_id)) {
profilesToLoad[hook.user_id] = true;
}
}
const list = Object.keys(profilesToLoad);
if (list.length === 0) {
return;
}
AsyncClient.getProfilesByIds(list);
}
export function loadOutgoingHooks() {
Client.listOutgoingHooks(
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_OUTGOING_WEBHOOKS,
teamId: TeamStore.getCurrentId(),
outgoingWebhooks: data
});
loadProfilesForOutgoingHooks(data);
},
(err) => {
AsyncClient.dispatchError(err, 'listOutgoingHooks');
}
);
}
function loadProfilesForOutgoingHooks(hooks) {
const profilesToLoad = {};
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i];
if (!UserStore.hasProfile(hook.creator_id)) {
profilesToLoad[hook.creator_id] = true;
}
}
const list = Object.keys(profilesToLoad);
if (list.length === 0) {
return;
}
AsyncClient.getProfilesByIds(list);
}
export function loadTeamCommands() {
Client.listTeamCommands(
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_COMMANDS,
teamId: Client.teamId,
commands: data
});
loadProfilesForCommands(data);
},
(err) => {
AsyncClient.dispatchError(err, 'loadTeamCommands');
}
);
}
function loadProfilesForCommands(commands) {
const profilesToLoad = {};
for (let i = 0; i < commands.length; i++) {
const command = commands[i];
if (!UserStore.hasProfile(command.creator_id)) {
profilesToLoad[command.creator_id] = true;
}
}
const list = Object.keys(profilesToLoad);
if (list.length === 0) {
return;
}
AsyncClient.getProfilesByIds(list);
}

View File

@@ -8,6 +8,8 @@ import PostStore from 'stores/post_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {loadStatusesForChannel} from 'actions/status_actions.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
@@ -52,6 +54,8 @@ export function handleNewPost(post, msg) {
post,
websocketMessageProps
});
loadProfilesForPosts(data.posts);
},
(err) => {
AsyncClient.dispatchError(err, 'getPost');
@@ -115,7 +119,7 @@ export function setUnreadPost(channelId, postId) {
member.last_viewed_at = lastViewed;
member.msg_count = channel.total_msg_count - unreadPosts;
member.mention_count = 0;
ChannelStore.setChannelMember(member);
ChannelStore.storeMyChannelMember(member);
ChannelStore.setUnreadCount(channelId);
AsyncClient.setLastViewedAt(lastViewed, channelId);
}
@@ -153,9 +157,156 @@ export function getFlaggedPosts() {
results: data,
is_flagged_posts: true
});
loadProfilesForPosts(data.posts);
},
(err) => {
AsyncClient.dispatchError(err, 'getFlaggedPosts');
}
);
}
export function loadPosts(channelId = ChannelStore.getCurrentId()) {
const postList = PostStore.getAllPosts(channelId);
const latestPostTime = PostStore.getLatestPostFromPageTime(channelId);
if (!postList || Object.keys(postList).length === 0 || postList.order.length < Constants.POST_CHUNK_SIZE || latestPostTime === 0) {
loadPostsPage(channelId, Constants.POST_CHUNK_SIZE);
return;
}
Client.getPosts(
channelId,
latestPostTime,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POSTS,
id: channelId,
before: true,
numRequested: 0,
post_list: data
});
loadProfilesForPosts(data.posts);
loadStatusesForChannel(channelId);
},
(err) => {
AsyncClient.dispatchError(err, 'loadPosts');
}
);
}
export function loadPostsPage(channelId = ChannelStore.getCurrentId(), max = Constants.POST_CHUNK_SIZE) {
const postList = PostStore.getAllPosts(channelId);
// if we already have more than POST_CHUNK_SIZE posts,
// let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE,
// with a max
let numPosts = Math.min(max, Constants.POST_CHUNK_SIZE);
if (postList && postList.order.length > 0) {
numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE));
}
Client.getPostsPage(
channelId,
0,
numPosts,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POSTS,
id: channelId,
before: true,
numRequested: numPosts,
checkLatest: true,
post_list: data
});
loadProfilesForPosts(data.posts);
loadStatusesForChannel(channelId);
},
(err) => {
AsyncClient.dispatchError(err, 'loadPostsPage');
}
);
}
export function loadPostsBefore(postId, offset, numPost, isPost) {
const channelId = ChannelStore.getCurrentId();
if (channelId == null) {
return;
}
Client.getPostsBefore(
channelId,
postId,
offset,
numPost,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POSTS,
id: channelId,
before: true,
numRequested: numPost,
post_list: data,
isPost
});
loadProfilesForPosts(data.posts);
loadStatusesForChannel(channelId);
},
(err) => {
AsyncClient.dispatchError(err, 'loadPostsBefore');
}
);
}
export function loadPostsAfter(postId, offset, numPost, isPost) {
const channelId = ChannelStore.getCurrentId();
if (channelId == null) {
return;
}
Client.getPostsAfter(
channelId,
postId,
offset,
numPost,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POSTS,
id: channelId,
before: false,
numRequested: numPost,
post_list: data,
isPost
});
loadProfilesForPosts(data.posts);
loadStatusesForChannel(channelId);
},
(err) => {
AsyncClient.dispatchError(err, 'loadPostsAfter');
}
);
}
function loadProfilesForPosts(posts) {
const profilesToLoad = {};
for (const pid in posts) {
if (!posts.hasOwnProperty(pid)) {
continue;
}
const post = posts[pid];
if (!UserStore.hasProfile(post.user_id)) {
profilesToLoad[post.user_id] = true;
}
}
const list = Object.keys(profilesToLoad);
if (list.length === 0) {
return;
}
AsyncClient.getProfilesByIds(list);
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import Client from 'client/web_client.jsx';
import {ActionTypes, Preferences, Constants} from 'utils/constants.jsx';
export function loadStatusesForChannel(channelId = ChannelStore.getCurrentId()) {
const postList = PostStore.getVisiblePosts(channelId);
if (!postList || !postList.posts) {
return;
}
const statusesToLoad = {};
for (const pid in postList.posts) {
if (!postList.posts.hasOwnProperty(pid)) {
continue;
}
const post = postList.posts[pid];
statusesToLoad[post.user_id] = true;
}
loadStatusesByIds(Object.keys(statusesToLoad));
}
export function loadStatusesForDMSidebar() {
const dmPrefs = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
const statusesToLoad = [];
for (const [key, value] of dmPrefs) {
if (value === 'true') {
statusesToLoad.push(key);
}
}
loadStatusesByIds(statusesToLoad);
}
export function loadStatusesForChannelAndSidebar() {
const statusesToLoad = {};
const channelId = ChannelStore.getCurrentId();
const postList = PostStore.getVisiblePosts(channelId);
if (postList && postList.posts) {
for (const pid in postList.posts) {
if (!postList.posts.hasOwnProperty(pid)) {
continue;
}
const post = postList.posts[pid];
statusesToLoad[post.user_id] = true;
}
}
const dmPrefs = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
for (const [key, value] of dmPrefs) {
if (value === 'true') {
statusesToLoad[key] = true;
}
}
loadStatusesByIds(Object.keys(statusesToLoad));
}
export function loadStatusesForProfilesList(users) {
if (users == null) {
return;
}
const statusesToLoad = [];
for (let i = 0; i < users.length; i++) {
statusesToLoad.push(users[i].id);
}
loadStatusesByIds(statusesToLoad);
}
export function loadStatusesForProfilesMap(users) {
if (users == null) {
return;
}
const statusesToLoad = [];
for (const userId in users) {
if (!users.hasOwnProperty(userId)) {
return;
}
statusesToLoad.push(userId);
}
loadStatusesByIds(statusesToLoad);
}
export function loadStatusesByIds(userIds) {
if (userIds.length === 0) {
return;
}
Client.getStatusesByIds(
userIds,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_STATUSES,
statuses: data
});
}
);
}
let intervalId = '';
export function startPeriodicStatusUpdates() {
clearInterval(intervalId);
intervalId = setInterval(
() => {
loadStatusesForChannelAndSidebar();
},
Constants.STATUS_INTERVAL
);
}
export function stopPeriodicStatusUpdates() {
clearInterval(intervalId);
}

View File

@@ -2,6 +2,7 @@
// See License.txt for license information.
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
@@ -19,8 +20,6 @@ export function checkIfTeamExists(teamName, onSuccess, onError) {
export function createTeam(team, onSuccess, onError) {
Client.createTeam(team,
(rteam) => {
AsyncClient.getDirectProfiles();
AppDispatcher.handleServerAction({
type: ActionTypes.CREATED_TEAM,
team: rteam,
@@ -36,3 +35,25 @@ export function createTeam(team, onSuccess, onError) {
onError
);
}
export function removeUserFromTeam(teamId, userId, success, error) {
Client.removeUserFromTeam(
teamId,
userId,
() => {
TeamStore.removeMemberInTeam(teamId, userId);
AsyncClient.getUser(userId);
if (success) {
success();
}
},
(err) => {
AsyncClient.dispatchError(err, 'removeUserFromTeam');
if (error) {
error(err);
}
}
);
}

View File

@@ -2,12 +2,17 @@
// See License.txt for license information.
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import {loadStatusesForProfilesList, loadStatusesForProfilesMap} from 'actions/status_actions.jsx';
import {getDirectChannelName} from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import {ActionTypes, Preferences} from 'utils/constants.jsx';
@@ -29,9 +34,179 @@ export function switchFromLdapToEmail(email, password, ldapPassword, onSuccess,
);
}
export function getMoreDmList() {
AsyncClient.getTeamMembers(TeamStore.getCurrentId());
AsyncClient.getProfilesForDirectMessageList();
export function loadProfilesAndTeamMembers(offset, limit, teamId = TeamStore.getCurrentId(), success, error) {
Client.getProfilesInTeam(
teamId,
offset,
limit,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_PROFILES_IN_TEAM,
profiles: data,
team_id: teamId,
offset,
count: Object.keys(data).length
});
loadTeamMembersForProfilesMap(data, teamId, success, error);
loadStatusesForProfilesMap(data);
},
(err) => {
AsyncClient.dispatchError(err, 'getProfilesInTeam');
}
);
}
export function loadTeamMembersForProfilesMap(profiles, teamId = TeamStore.getCurrentId(), success, error) {
const membersToLoad = {};
for (const pid in profiles) {
if (!profiles.hasOwnProperty(pid)) {
continue;
}
if (!TeamStore.hasActiveMemberInTeam(teamId, pid)) {
membersToLoad[pid] = true;
}
}
const list = Object.keys(membersToLoad);
if (list.length === 0) {
if (success) {
success({});
}
return;
}
loadTeamMembersForProfiles(list, teamId, success, error);
}
export function loadTeamMembersForProfilesList(profiles, teamId = TeamStore.getCurrentId(), success, error) {
const membersToLoad = {};
for (let i = 0; i < profiles.length; i++) {
const pid = profiles[i].id;
if (!TeamStore.hasActiveMemberInTeam(teamId, pid)) {
membersToLoad[pid] = true;
}
}
const list = Object.keys(membersToLoad);
if (list.length === 0) {
if (success) {
success({});
}
return;
}
loadTeamMembersForProfiles(list, teamId, success, error);
}
function loadTeamMembersForProfiles(userIds, teamId, success, error) {
Client.getTeamMembersByIds(
teamId,
userIds,
(data) => {
const memberMap = {};
for (let i = 0; i < data.length; i++) {
memberMap[data[i].user_id] = data[i];
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_MEMBERS_IN_TEAM,
team_id: teamId,
team_members: memberMap
});
if (success) {
success(data);
}
},
(err) => {
AsyncClient.dispatchError(err, 'getTeamMembersByIds');
if (error) {
error(err);
}
}
);
}
function populateDMChannelsWithProfiles(userIds) {
const currentUserId = UserStore.getCurrentId();
for (let i = 0; i < userIds.length; i++) {
const channelName = getDirectChannelName(currentUserId, userIds[i]);
const channel = ChannelStore.getByName(channelName);
if (channel) {
UserStore.saveUserIdInChannel(channel.id, userIds[i]);
}
}
}
export function loadProfilesAndTeamMembersForDMSidebar() {
const dmPrefs = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
const teamId = TeamStore.getCurrentId();
const profilesToLoad = [];
const membersToLoad = [];
for (const [key, value] of dmPrefs) {
if (value === 'true') {
if (!UserStore.hasProfile(key)) {
profilesToLoad.push(key);
}
membersToLoad.push(key);
}
}
if (profilesToLoad.length > 0) {
Client.getProfilesByIds(
profilesToLoad,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_PROFILES,
profiles: data
});
// Use membersToLoad so we get all the DM profiles even if they were already loaded
populateDMChannelsWithProfiles(membersToLoad);
},
(err) => {
AsyncClient.dispatchError(err, 'getProfilesByIds');
}
);
} else {
populateDMChannelsWithProfiles(membersToLoad);
}
if (membersToLoad.length > 0) {
Client.getTeamMembersByIds(
teamId,
membersToLoad,
(data) => {
const memberMap = {};
for (let i = 0; i < data.length; i++) {
memberMap[data[i].user_id] = data[i];
}
const nonMembersMap = {};
for (let i = 0; i < membersToLoad.length; i++) {
if (!memberMap[membersToLoad[i]]) {
nonMembersMap[membersToLoad[i]] = true;
}
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_MEMBERS_IN_TEAM,
team_id: teamId,
team_members: memberMap,
non_team_members: nonMembersMap
});
},
(err) => {
AsyncClient.dispatchError(err, 'getTeamMembersByIds');
}
);
}
}
export function saveTheme(teamId, theme, onSuccess, onError) {
@@ -82,3 +257,62 @@ function onThemeSaved(teamId, theme, onSuccess) {
onSuccess();
}
export function searchUsers(term, teamId = TeamStore.getCurrentId(), options = {}, success, error) {
Client.searchUsers(
term,
teamId,
options,
(data) => {
loadStatusesForProfilesList(data);
if (success) {
success(data);
}
},
(err) => {
AsyncClient.dispatchError(err, 'searchUsers');
if (error) {
error(err);
}
}
);
}
export function autocompleteUsersInChannel(username, channelId, success, error) {
Client.autocompleteUsersInChannel(
username,
channelId,
(data) => {
if (success) {
success(data);
}
},
(err) => {
AsyncClient.dispatchError(err, 'autocompleteUsersInChannel');
if (error) {
error(err);
}
}
);
}
export function autocompleteUsersInTeam(username, success, error) {
Client.autocompleteUsersInTeam(
username,
(data) => {
if (success) {
success(data);
}
},
(err) => {
AsyncClient.dispatchError(err, 'autocompleteUsersInTeam');
if (error) {
error(err);
}
}
);
}

View File

@@ -3,8 +3,6 @@
import $ from 'jquery';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PostStore from 'stores/post_store.jsx';
@@ -20,10 +18,11 @@ import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import * as UserActions from 'actions/user_actions.jsx';
import {handleNewPost} from 'actions/post_actions.jsx';
import {handleNewPost, loadPosts} from 'actions/post_actions.jsx';
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
import * as StatusActions from 'actions/status_actions.jsx';
import {Constants, SocketEvents, ActionTypes} from 'utils/constants.jsx';
import {Constants, SocketEvents, UserStatuses} from 'utils/constants.jsx';
import {browserHistory} from 'react-router/es6';
@@ -53,6 +52,7 @@ export function initialize() {
connUrl += Client.getUsersRoute() + '/websocket';
WebSocketClient.setEventCallback(handleEvent);
WebSocketClient.setFirstConnectCallback(handleFirstConnect);
WebSocketClient.setReconnectCallback(handleReconnect);
WebSocketClient.setCloseCallback(handleClose);
WebSocketClient.initialize(connUrl);
@@ -64,22 +64,19 @@ export function close() {
}
export function getStatuses() {
WebSocketClient.getStatuses(
(resp) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_STATUSES,
statuses: resp.data
});
}
);
StatusActions.loadStatusesForChannelAndSidebar();
}
function handleFirstConnect() {
getStatuses();
ErrorStore.clearLastError();
ErrorStore.emitChange();
}
function handleReconnect() {
if (Client.teamId) {
AsyncClient.getChannels();
AsyncClient.getPosts(ChannelStore.getCurrentId());
AsyncClient.getTeamMembers(TeamStore.getCurrentId());
AsyncClient.getProfiles();
loadPosts(ChannelStore.getCurrentId());
}
getStatuses();
@@ -112,7 +109,7 @@ function handleEvent(msg) {
break;
case SocketEvents.NEW_USER:
handleNewUserEvent();
handleNewUserEvent(msg);
break;
case SocketEvents.LEAVE_TEAM:
@@ -170,6 +167,10 @@ function handleEvent(msg) {
function handleNewPostEvent(msg) {
const post = JSON.parse(msg.data.post);
handleNewPost(post, msg);
if (UserStore.getStatus(post.user_id) !== UserStatuses.ONLINE) {
StatusActions.loadStatusesByIds([post.user_id]);
}
}
function handlePostEditEvent(msg) {
@@ -196,36 +197,33 @@ function handlePostDeleteEvent(msg) {
}
}
function handleNewUserEvent() {
AsyncClient.getTeamMembers(TeamStore.getCurrentId());
AsyncClient.getProfiles();
AsyncClient.getDirectProfiles();
AsyncClient.getChannelExtraInfo();
function handleNewUserEvent(msg) {
AsyncClient.getUser(msg.user_id);
AsyncClient.getChannelStats();
loadProfilesAndTeamMembersForDMSidebar();
}
function handleLeaveTeamEvent(msg) {
if (UserStore.getCurrentId() === msg.data.user_id) {
TeamStore.removeTeamMember(msg.broadcast.team_id);
TeamStore.removeMyTeamMember(msg.broadcast.team_id);
// if the are on the team begin removed redirect them to the root
// if they are on the team being removed redirect them to the root
if (TeamStore.getCurrentId() === msg.broadcast.team_id) {
TeamStore.setCurrentId('');
Client.setTeamId('');
browserHistory.push('/');
}
} else if (TeamStore.getCurrentId() === msg.broadcast.team_id) {
UserActions.getMoreDmList();
}
}
function handleDirectAddedEvent(msg) {
AsyncClient.getChannel(msg.broadcast.channel_id);
AsyncClient.getDirectProfiles();
loadProfilesAndTeamMembersForDMSidebar();
}
function handleUserAddedEvent(msg) {
if (ChannelStore.getCurrentId() === msg.broadcast.channel_id) {
AsyncClient.getChannelExtraInfo();
AsyncClient.getChannelStats();
}
if (TeamStore.getCurrentId() === msg.data.team_id && UserStore.getCurrentId() === msg.data.user_id) {
@@ -248,7 +246,7 @@ function handleUserRemovedEvent(msg) {
$('#removed_from_channel').modal('show');
}
} else if (ChannelStore.getCurrentId() === msg.broadcast.channel_id) {
AsyncClient.getChannelExtraInfo();
AsyncClient.getChannelStats();
}
}
@@ -287,6 +285,10 @@ function handlePreferenceChangedEvent(msg) {
function handleUserTypingEvent(msg) {
GlobalActions.emitRemoteUserTypingEvent(msg.broadcast.channel_id, msg.data.user_id, msg.data.parent_id);
if (UserStore.getStatus(msg.data.user_id) !== UserStatuses.ONLINE) {
StatusActions.loadStatusesByIds([msg.data.user_id]);
}
}
function handleStatusChangedEvent(msg) {
@@ -301,4 +303,4 @@ function handleHelloEvent(msg) {
function handleWebrtc(msg) {
const data = msg.data;
return WebrtcActions.handle(data);
}
}

View File

@@ -73,11 +73,7 @@ export default class Client {
return `${this.url}${this.urlVersion}/teams`;
}
getTeamNeededRoute() {
return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}`;
}
getTeamNeededManualRoute(teamId) {
getTeamNeededRoute(teamId = this.getTeamId()) {
return `${this.url}${this.urlVersion}/teams/${teamId}`;
}
@@ -565,15 +561,43 @@ export default class Client {
end(this.handleResponse.bind(this, 'getMyTeam', success, error));
}
getTeamMembers(teamId, success, error) {
getTeamMembers(teamId, offset, limit, success, error) {
request.
get(`${this.getTeamsRoute()}/members/${teamId}`).
get(`${this.getTeamNeededRoute(teamId)}/members/${offset}/${limit}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getTeamMembers', success, error));
}
getTeamMember(teamId, userId, success, error) {
request.
get(`${this.getTeamNeededRoute(teamId)}/members/${userId}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getTeamMember', success, error));
}
getTeamMembersByIds(teamId, userIds, success, error) {
request.
post(`${this.getTeamNeededRoute(teamId)}/members/ids`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
send(userIds).
end(this.handleResponse.bind(this, 'getTeamMembersByIds', success, error));
}
getTeamStats(teamId, success, error) {
request.
get(`${this.getTeamNeededRoute(teamId)}/stats`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getTeamStats', success, error));
}
inviteMembers(data, success, error) {
request.
post(`${this.getTeamNeededRoute()}/invite_members`).
@@ -740,7 +764,7 @@ export default class Client {
};
request.
post(`${this.getTeamNeededManualRoute(teamId)}/update_member_roles`).
post(`${this.getTeamNeededRoute(teamId)}/update_member_roles`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
@@ -1003,40 +1027,78 @@ export default class Client {
end(this.handleResponse.bind(this, 'getRecentlyActiveUsers', success, error));
}
getDirectProfiles(success, error) {
getProfiles(offset, limit, success, error) {
request.
get(`${this.getUsersRoute()}/direct_profiles`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getDirectProfiles', success, error));
}
getProfiles(success, error) {
request.
get(`${this.getUsersRoute()}/profiles/${this.getTeamId()}`).
get(`${this.getUsersRoute()}/${offset}/${limit}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getProfiles', success, error));
}
getProfilesForTeam(teamId, success, error) {
getProfilesInTeam(teamId, offset, limit, success, error) {
request.
get(`${this.getUsersRoute()}/profiles/${teamId}`).
get(`${this.getTeamNeededRoute(teamId)}/users/${offset}/${limit}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getProfilesForTeam', success, error));
end(this.handleResponse.bind(this, 'getProfilesInTeam', success, error));
}
getProfilesForDirectMessageList(success, error) {
getProfilesInChannel(channelId, offset, limit, success, error) {
request.
get(`${this.getUsersRoute()}/profiles_for_dm_list/${this.getTeamId()}`).
get(`${this.getChannelNeededRoute(channelId)}/users/${offset}/${limit}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getProfilesForDirectMessageList', success, error));
end(this.handleResponse.bind(this, 'getProfilesInChannel', success, error));
}
getProfilesNotInChannel(channelId, offset, limit, success, error) {
request.
get(`${this.getChannelNeededRoute(channelId)}/users/not_in_channel/${offset}/${limit}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getProfilesNotInChannel', success, error));
}
getProfilesByIds(userIds, success, error) {
request.
post(`${this.getUsersRoute()}/ids`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
send(userIds).
end(this.handleResponse.bind(this, 'getProfilesByIds', success, error));
}
searchUsers(term, teamId, options, success, error) {
request.
post(`${this.getUsersRoute()}/search`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
send({term, team_id: teamId, ...options}).
end(this.handleResponse.bind(this, 'searchUsers', success, error));
}
autocompleteUsersInChannel(term, channelId, success, error) {
request.
get(`${this.getChannelNeededRoute(channelId)}/users/autocomplete?term=${encodeURIComponent(term)}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'autocompleteUsers', success, error));
}
autocompleteUsersInTeam(term, success, error) {
request.
get(`${this.getTeamNeededRoute()}/users/autocomplete?term=${encodeURIComponent(term)}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'autocompleteUsers', success, error));
}
getStatuses(success, error) {
@@ -1048,6 +1110,16 @@ export default class Client {
end(this.handleResponse.bind(this, 'getStatuses', success, error));
}
getStatusesByIds(userIds, success, error) {
request.
post(`${this.getUsersRoute()}/status/ids`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
send(userIds).
end(this.handleResponse.bind(this, 'getStatuses', success, error));
}
setActiveChannel(id, success, error) {
request.
post(`${this.getUsersRoute()}/status/set_active_channel`).
@@ -1285,18 +1357,22 @@ export default class Client {
end(this.handleResponse.bind(this, 'getChannelCounts', success, error));
}
getChannelExtraInfo(channelId, memberLimit, success, error) {
var url = `${this.getChannelNeededRoute(channelId)}/extra_info`;
if (memberLimit) {
url += '/' + memberLimit;
}
getChannelStats(channelId, success, error) {
request.
get(url).
get(`${this.getChannelNeededRoute(channelId)}/stats`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getChannelExtraInfo', success, error));
end(this.handleResponse.bind(this, 'getChannelStats', success, error));
}
getChannelMember(channelId, userId, success, error) {
request.
get(`${this.getChannelNeededRoute(channelId)}/members/${userId}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getChannelMember', success, error));
}
addChannelMember(channelId, userId, success, error) {

View File

@@ -12,6 +12,7 @@ export default class WebSocketClient {
this.connectFailCount = 0;
this.eventCallback = null;
this.responseCallbacks = {};
this.firstConnectCallback = null;
this.reconnectCallback = null;
this.errorCallback = null;
this.closeCallback = null;
@@ -29,12 +30,13 @@ export default class WebSocketClient {
this.conn = new WebSocket(connectionUrl);
this.conn.onopen = () => {
if (this.reconnectCallback) {
this.reconnectCallback();
}
if (this.connectFailCount > 0) {
console.log('websocket re-established connection'); //eslint-disable-line no-console
if (this.reconnectCallback) {
this.reconnectCallback();
}
} else if (this.firstConnectCallback) {
this.firstConnectCallback();
}
this.connectFailCount = 0;
@@ -104,6 +106,10 @@ export default class WebSocketClient {
this.eventCallback = callback;
}
setFirstConnectCallback(callback) {
this.firstConnectCallback = callback;
}
setReconnectCallback(callback) {
this.reconnectCallback = callback;
}
@@ -157,4 +163,10 @@ export default class WebSocketClient {
getStatuses(callback) {
this.sendMessage('get_statuses', null, callback);
}
getStatusesByIds(userIds, callback) {
const data = {};
data.user_ids = userIds;
this.sendMessage('get_statuses_by_ids', data, callback);
}
}

View File

@@ -22,7 +22,7 @@ export default class AdminNavbarDropdown extends React.Component {
this.state = {
teams: TeamStore.getAll(),
teamMembers: TeamStore.getTeamMembers()
teamMembers: TeamStore.getMyTeamMembers()
};
}
@@ -45,7 +45,7 @@ export default class AdminNavbarDropdown extends React.Component {
onTeamChange() {
this.setState({
teams: TeamStore.getAll(),
teamMembers: TeamStore.getTeamMembers()
teamMembers: TeamStore.getMyTeamMembers()
});
}

View File

@@ -8,11 +8,11 @@ import UserStore from 'stores/user_store.jsx';
import ConfirmModal from '../confirm_modal.jsx';
import TeamStore from 'stores/team_store.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import {FormattedMessage} from 'react-intl';
import React from 'react';
export default class UserItem extends React.Component {
export default class AdminTeamMembersDropdown extends React.Component {
constructor(props) {
super(props);
@@ -50,7 +50,7 @@ export default class UserItem extends React.Component {
}
);
Client.updateTeamMemberRoles(
this.props.team.id,
this.props.teamMember.team_id,
this.props.user.id,
'team_user',
() => {
@@ -74,7 +74,7 @@ export default class UserItem extends React.Component {
handleRemoveFromTeam() {
Client.removeUserFromTeam(
this.props.team.id,
this.props.teamMember.team_id,
this.props.user.id,
() => {
this.props.refreshProfiles();
@@ -111,7 +111,7 @@ export default class UserItem extends React.Component {
doMakeTeamAdmin() {
Client.updateTeamMemberRoles(
this.props.team.id,
this.props.teamMember.team_id,
this.props.user.id,
'team_user team_admin',
() => {
@@ -241,7 +241,6 @@ export default class UserItem extends React.Component {
}
const me = UserStore.getCurrentUser();
const email = user.email;
let showMakeMember = Utils.isAdmin(teamMember.roles) || Utils.isSystemAdmin(user.roles);
let showMakeAdmin = !Utils.isAdmin(teamMember.roles) && !Utils.isSystemAdmin(user.roles);
let showMakeSystemAdmin = !Utils.isSystemAdmin(user.roles);
@@ -406,39 +405,8 @@ export default class UserItem extends React.Component {
);
}
let mfaActiveText;
if (mfaEnabled) {
if (user.mfa_active) {
mfaActiveText = (
<FormattedHTMLMessage
id='admin.user_item.mfaYes'
defaultMessage=', <strong>MFA</strong>: Yes'
/>
);
} else {
mfaActiveText = (
<FormattedHTMLMessage
id='admin.user_item.mfaNo'
defaultMessage=', <strong>MFA</strong>: No'
/>
);
}
}
let authServiceText;
let passwordReset;
if (user.auth_service) {
const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service);
authServiceText = (
<FormattedHTMLMessage
id='admin.user_item.authServiceNotEmail'
defaultMessage=', <strong>Sign-in Method:</strong> {service}'
values={{
service
}}
/>
);
passwordReset = (
<li role='presentation'>
<a
@@ -454,13 +422,6 @@ export default class UserItem extends React.Component {
</li>
);
} else {
authServiceText = (
<FormattedHTMLMessage
id='admin.user_item.authServiceEmail'
defaultMessage=', <strong>Sign-in Method:</strong> Email'
/>
);
passwordReset = (
<li role='presentation'>
<a
@@ -531,63 +492,38 @@ export default class UserItem extends React.Component {
}
return (
<div className='more-modal__row'>
<img
className='more-modal__image pull-left'
src={`${Client.getUsersRoute()}/${user.id}/image?time=${user.update_at}`}
height='36'
width='36'
/>
<div className='more-modal__details'>
<div className='more-modal__name'>{displayedName}</div>
<div className='more-modal__description'>
<FormattedHTMLMessage
id='admin.user_item.emailTitle'
defaultMessage='<strong>Email:</strong> {email}'
values={{
email
}}
/>
{authServiceText}
{mfaActiveText}
</div>
{serverError}
</div>
<div className='more-modal__actions'>
<div className='dropdown member-drop'>
<a
href='#'
className='dropdown-toggle theme'
type='button'
data-toggle='dropdown'
aria-expanded='true'
>
<span>{currentRoles} </span>
<span className='caret'/>
</a>
<ul
className='dropdown-menu member-menu'
role='menu'
>
{removeFromTeam}
{makeAdmin}
{makeMember}
{makeActive}
{makeNotActive}
{makeSystemAdmin}
{mfaReset}
{passwordReset}
</ul>
</div>
</div>
<div className='dropdown member-drop'>
<a
href='#'
className='dropdown-toggle theme'
type='button'
data-toggle='dropdown'
aria-expanded='true'
>
<span>{currentRoles} </span>
<span className='caret'/>
</a>
<ul
className='dropdown-menu member-menu'
role='menu'
>
{removeFromTeam}
{makeAdmin}
{makeMember}
{makeActive}
{makeNotActive}
{makeSystemAdmin}
{mfaReset}
{passwordReset}
</ul>
{makeDemoteModal}
{serverError}
</div>
);
}
}
UserItem.propTypes = {
team: React.PropTypes.object.isRequired,
AdminTeamMembersDropdown.propTypes = {
user: React.PropTypes.object.isRequired,
teamMember: React.PropTypes.object.isRequired,
refreshProfiles: React.PropTypes.func.isRequired,

View File

@@ -1,16 +1,25 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AdminStore from 'stores/admin_store.jsx';
import Client from 'client/web_client.jsx';
import FormError from 'components/form_error.jsx';
import LoadingScreen from '../loading_screen.jsx';
import UserItem from './user_item.jsx';
import SearchableUserList from 'components/searchable_user_list.jsx';
import AdminTeamMembersDropdown from './admin_team_members_dropdown.jsx';
import ResetPasswordModal from './reset_password_modal.jsx';
import FormError from 'components/form_error.jsx';
import {FormattedMessage} from 'react-intl';
import AdminStore from 'stores/admin_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {searchUsers, loadProfilesAndTeamMembers, loadTeamMembersForProfilesList} from 'actions/user_actions.jsx';
import {getTeamStats} from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
const USERS_PER_PAGE = 50;
export default class UserList extends React.Component {
static get propTypes() {
@@ -23,34 +32,49 @@ export default class UserList extends React.Component {
super(props);
this.onAllTeamsChange = this.onAllTeamsChange.bind(this);
this.onStatsChange = this.onStatsChange.bind(this);
this.onUsersChange = this.onUsersChange.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.getTeamProfiles = this.getTeamProfiles.bind(this);
this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
this.doPasswordReset = this.doPasswordReset.bind(this);
this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
this.getTeamMemberForUser = this.getTeamMemberForUser.bind(this);
this.nextPage = this.nextPage.bind(this);
this.search = this.search.bind(this);
this.loadComplete = this.loadComplete.bind(this);
const stats = TeamStore.getStats(this.props.params.team);
this.state = {
team: AdminStore.getTeam(this.props.params.team),
users: null,
teamMembers: null,
users: [],
teamMembers: TeamStore.getMembersInTeam(this.props.params.team),
total: stats.member_count,
serverError: null,
showPasswordModal: false,
loading: true,
user: null
};
}
componentDidMount() {
this.getCurrentTeamProfiles();
AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange);
UserStore.addInTeamChangeListener(this.onUsersChange);
TeamStore.addChangeListener(this.onTeamChange);
TeamStore.addStatsChangeListener(this.onStatsChange);
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, this.props.params.team, this.loadComplete);
getTeamStats(this.props.params.team);
}
componentWillReceiveProps(nextProps) {
if (nextProps.params.team !== this.props.params.team) {
const stats = TeamStore.getStats(nextProps.params.team);
this.setState({
team: AdminStore.getTeam(nextProps.params.team)
team: AdminStore.getTeam(nextProps.params.team),
users: [],
teamMembers: TeamStore.getMembersInTeam(nextProps.params.team),
total: stats.member_count
});
this.getTeamProfiles(nextProps.params.team);
@@ -59,6 +83,13 @@ export default class UserList extends React.Component {
componentWillUnmount() {
AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange);
UserStore.removeInTeamChangeListener(this.onUsersChange);
TeamStore.removeChangeListener(this.onTeamChange);
TeamStore.removeStatsChangeListener(this.onStatsChange);
}
loadComplete() {
this.setState({loading: false});
}
onAllTeamsChange() {
@@ -67,59 +98,21 @@ export default class UserList extends React.Component {
});
}
getCurrentTeamProfiles() {
this.getTeamProfiles(this.props.params.team);
onStatsChange() {
const stats = TeamStore.getStats(this.props.params.team);
this.setState({total: stats.member_count});
}
getTeamProfiles(teamId) {
Client.getTeamMembers(
teamId,
(data) => {
this.setState({
teamMembers: data
});
},
(err) => {
this.setState({
teamMembers: null,
serverError: err.message
});
}
);
onUsersChange() {
this.setState({users: UserStore.getProfileListInTeam(this.props.params.team)});
}
Client.getProfilesForTeam(
teamId,
(users) => {
var memberList = [];
for (var id in users) {
if (users.hasOwnProperty(id)) {
memberList.push(users[id]);
}
}
onTeamChange() {
this.setState({teamMembers: TeamStore.getMembersInTeam(this.props.params.team)});
}
memberList.sort((a, b) => {
if (a.username < b.username) {
return -1;
}
if (a.username > b.username) {
return 1;
}
return 0;
});
this.setState({
users: memberList
});
},
(err) => {
this.setState({
users: null,
serverError: err.message
});
}
);
nextPage(page) {
loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.props.params.team);
}
doPasswordReset(user) {
@@ -144,20 +137,21 @@ export default class UserList extends React.Component {
});
}
getTeamMemberForUser(userId) {
if (this.state.teamMembers) {
for (const index in this.state.teamMembers) {
if (this.state.teamMembers.hasOwnProperty(index)) {
var teamMember = this.state.teamMembers[index];
if (teamMember.user_id === userId) {
return teamMember;
}
}
}
search(term) {
if (term === '') {
this.setState({search: false, users: UserStore.getProfileListInTeam(this.props.params.team)});
return;
}
return null;
searchUsers(
term,
this.props.params.team,
{},
(users) => {
this.setState({loading: true, search: true, users});
loadTeamMembersForProfilesList(users, this.props.params.team, this.loadComplete);
}
);
}
render() {
@@ -165,41 +159,71 @@ export default class UserList extends React.Component {
return null;
}
if (this.state.users == null || this.state.teamMembers == null) {
return (
<div className='wrapper--fixed'>
<h3>
<FormattedMessage
id='admin.userList.title'
defaultMessage='Users for {team}'
values={{
team: this.state.team.name
}}
/>
</h3>
<FormError error={this.state.serverError}/>
<LoadingScreen/>
</div>
);
}
const teamMembers = this.state.teamMembers;
const users = this.state.users;
const actionUserProps = {};
const extraInfo = {};
const mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true';
var memberList = this.state.users.map((user) => {
var teamMember = this.getTeamMemberForUser(user.id);
let usersToDisplay;
if (this.state.loading) {
usersToDisplay = null;
} else {
usersToDisplay = [];
if (!teamMember || teamMember.delete_at > 0) {
return null;
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (teamMembers[user.id]) {
usersToDisplay.push(user);
actionUserProps[user.id] = {
teamMember: teamMembers[user.id]
};
const info = [];
if (user.auth_service) {
const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service);
info.push(
<FormattedHTMLMessage
id='admin.user_item.authServiceNotEmail'
defaultMessage='<strong>Sign-in Method:</strong> {service}'
values={{
service
}}
/>
);
} else {
info.push(
<FormattedHTMLMessage
id='admin.user_item.authServiceEmail'
defaultMessage='<strong>Sign-in Method:</strong> Email'
/>
);
}
if (mfaEnabled) {
if (user.mfa_active) {
info.push(
<FormattedHTMLMessage
id='admin.user_item.mfaYes'
defaultMessage='<strong>MFA</strong>: Yes'
/>
);
} else {
info.push(
<FormattedHTMLMessage
id='admin.user_item.mfaNo'
defaultMessage='<strong>MFA</strong>: No'
/>
);
}
}
extraInfo[user.id] = info;
}
}
return (
<UserItem
team={this.state.team}
key={'user_' + user.id}
user={user}
teamMember={teamMember}
refreshProfiles={this.getCurrentTeamProfiles}
doPasswordReset={this.doPasswordReset}
/>);
});
}
return (
<div className='wrapper--fixed'>
@@ -209,7 +233,7 @@ export default class UserList extends React.Component {
defaultMessage='Users for {team} ({count})'
values={{
team: this.state.team.name,
count: this.state.users.length
count: this.state.total
}}
/>
</h3>
@@ -219,7 +243,20 @@ export default class UserList extends React.Component {
role='form'
>
<div className='more-modal__list member-list-holder'>
{memberList}
<SearchableUserList
users={usersToDisplay}
usersPerPage={USERS_PER_PAGE}
total={this.state.total}
extraInfo={extraInfo}
nextPage={this.nextPage}
search={this.search}
actions={[AdminTeamMembersDropdown]}
actionProps={{
refreshProfiles: this.getCurrentTeamProfiles,
doPasswordReset: this.doPasswordReset
}}
actionUserProps={actionUserProps}
/>
</div>
</form>
<ResetPasswordModal

View File

@@ -82,6 +82,7 @@ class SystemAnalytics extends React.Component {
const stats = this.state.stats;
let advancedCounts;
let advancedStats;
let advancedGraphs;
let banner;
if (global.window.mm_license.IsLicensed === 'true') {
@@ -130,6 +131,41 @@ class SystemAnalytics extends React.Component {
</div>
);
advancedStats = (
<div className='row'>
<StatisticCount
title={
<FormattedMessage
id='analytics.system.totalWebsockets'
defaultMessage='Websocket Conns'
/>
}
icon='fa-user'
count={stats[StatTypes.TOTAL_WEBSOCKET_CONNECTIONS]}
/>
<StatisticCount
title={
<FormattedMessage
id='analytics.system.totalMasterDbConnections'
defaultMessage='Master DB Conns'
/>
}
icon='fa-terminal'
count={stats[StatTypes.TOTAL_MASTER_DB_CONNECTIONS]}
/>
<StatisticCount
title={
<FormattedMessage
id='analytics.system.totalReadDbConnections'
defaultMessage='Replica DB Conns'
/>
}
icon='fa-terminal'
count={stats[StatTypes.TOTAL_READ_DB_CONNECTIONS]}
/>
</div>
);
const channelTypeData = formatChannelDoughtnutData(stats[StatTypes.TOTAL_PUBLIC_CHANNELS], stats[StatTypes.TOTAL_PRIVATE_GROUPS], this.props.intl);
const postTypeData = formatPostDoughtnutData(stats[StatTypes.TOTAL_FILE_POSTS], stats[StatTypes.TOTAL_HASHTAG_POSTS], stats[StatTypes.TOTAL_POSTS], this.props.intl);
@@ -246,6 +282,7 @@ class SystemAnalytics extends React.Component {
/>
</div>
{advancedCounts}
{advancedStats}
{advancedGraphs}
<div className='row'>
<LineChart

View File

@@ -63,13 +63,15 @@ export default class ChannelHeader extends React.Component {
}
getStateFromStores() {
const extraInfo = ChannelStore.getExtraInfo(this.props.channelId);
const stats = ChannelStore.getStats(this.props.channelId);
const users = UserStore.getProfileListInChannel(this.props.channelId);
return {
channel: ChannelStore.get(this.props.channelId),
memberChannel: ChannelStore.getMember(this.props.channelId),
users: extraInfo.members,
userCount: extraInfo.member_count,
memberChannel: ChannelStore.getMyMember(this.props.channelId),
users,
userCount: stats.member_count,
currentUser: UserStore.getCurrentUser(),
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
isBusy: WebrtcStore.isBusy()
@@ -89,10 +91,10 @@ export default class ChannelHeader extends React.Component {
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
ChannelStore.addStatsChangeListener(this.onListenerChange);
SearchStore.addSearchChangeListener(this.onListenerChange);
PreferenceStore.addChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
UserStore.addInChannelChangeListener(this.onListenerChange);
UserStore.addStatusesChangeListener(this.onListenerChange);
WebrtcStore.addChangedListener(this.onListenerChange);
WebrtcStore.addBusyListener(this.onBusy);
@@ -102,10 +104,10 @@ export default class ChannelHeader extends React.Component {
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
ChannelStore.removeStatsChangeListener(this.onListenerChange);
SearchStore.removeSearchChangeListener(this.onListenerChange);
PreferenceStore.removeChangeListener(this.onListenerChange);
UserStore.removeChangeListener(this.onListenerChange);
UserStore.removeInChannelChangeListener(this.onListenerChange);
UserStore.removeStatusesChangeListener(this.onListenerChange);
WebrtcStore.removeChangedListener(this.onListenerChange);
WebrtcStore.removeBusyListener(this.onBusy);
@@ -117,10 +119,7 @@ export default class ChannelHeader extends React.Component {
}
onListenerChange() {
const newState = this.getStateFromStores();
if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
this.setState(this.getStateFromStores());
}
handleLeave() {
@@ -265,7 +264,6 @@ export default class ChannelHeader extends React.Component {
</Popover>
);
let channelTitle = channel.display_name;
const currentId = this.state.currentUser.id;
const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
const isSystemAdmin = UserStore.isSystemAdminForCurrentUser();
const isDirect = (this.state.channel.type === 'D');
@@ -273,13 +271,8 @@ export default class ChannelHeader extends React.Component {
if (isDirect) {
const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
let contact;
if (this.state.users.length > 1) {
if (this.state.users[0].id === currentId) {
contact = this.state.users[1];
} else {
contact = this.state.users[0];
}
const contact = this.state.users[0];
if (contact) {
channelTitle = Utils.displayUsername(contact.id);
}

View File

@@ -1,14 +1,13 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import {FormattedMessage} from 'react-intl';
import SpinnerButton from 'components/spinner_button.jsx';
import {addUserToChannel} from 'actions/channel_actions.jsx';
import React from 'react';
import {FormattedMessage} from 'react-intl';
export default class ChannelInviteButton extends React.Component {
static get propTypes() {
return {
@@ -37,7 +36,7 @@ export default class ChannelInviteButton extends React.Component {
addingUser: true
});
Client.addChannelMember(
addUserToChannel(
this.props.channel.id,
this.props.user.id,
() => {
@@ -46,7 +45,6 @@ export default class ChannelInviteButton extends React.Component {
});
this.props.onInviteError(null);
AsyncClient.getChannelExtraInfo();
},
(err) => {
this.setState({

View File

@@ -1,124 +1,85 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import $ from 'jquery';
import ChannelInviteButton from './channel_invite_button.jsx';
import FilteredUserList from './filtered_user_list.jsx';
import SearchableUserList from './searchable_user_list.jsx';
import LoadingScreen from './loading_screen.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import {searchUsers} from 'actions/user_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
import React from 'react';
const USERS_PER_PAGE = 50;
export default class ChannelInviteModal extends React.Component {
constructor(props) {
super(props);
this.onListenerChange = this.onListenerChange.bind(this);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onChange = this.onChange.bind(this);
this.handleInviteError = this.handleInviteError.bind(this);
this.nextPage = this.nextPage.bind(this);
this.search = this.search.bind(this);
this.state = this.getStateFromStores();
}
shouldComponentUpdate(nextProps, nextState) {
if (!this.props.show && !nextProps.show) {
return false;
}
this.term = '';
if (!Utils.areObjectsEqual(this.props, nextProps)) {
return true;
}
const channelStats = ChannelStore.getStats(props.channel.id);
const teamStats = TeamStore.getCurrentStats();
if (!Utils.areObjectsEqual(this.state, nextState)) {
return true;
}
return false;
}
getStateFromStores() {
const users = UserStore.getActiveOnlyProfiles();
if ($.isEmptyObject(users)) {
return {
loading: true
};
}
// make sure we have all members of this channel before rendering
const extraInfo = ChannelStore.getCurrentExtraInfo();
if (extraInfo.member_count !== extraInfo.members.length) {
AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
return {
loading: true
};
}
const currentUser = UserStore.getCurrentUser();
if (!currentUser) {
return {
loading: true
};
}
const currentMember = ChannelStore.getCurrentMember();
if (!currentMember) {
return {
loading: true
};
}
const memberIds = extraInfo.members.map((user) => user.id);
var nonmembers = [];
for (var id in users) {
if (memberIds.indexOf(id) === -1) {
nonmembers.push(users[id]);
}
}
nonmembers.sort((a, b) => {
return a.username.localeCompare(b.username);
});
return {
nonmembers,
loading: false,
currentUser,
currentMember
this.state = {
users: [],
total: teamStats.member_count - channelStats.member_count,
search: false
};
}
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
ChannelStore.addChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
this.onListenerChange();
TeamStore.addStatsChangeListener(this.onChange);
ChannelStore.addStatsChangeListener(this.onChange);
UserStore.addNotInChannelChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
this.onChange();
AsyncClient.getProfilesNotInChannel(this.props.channel.id, 0);
AsyncClient.getTeamStats(TeamStore.getCurrentId());
} else if (this.props.show && !nextProps.show) {
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
ChannelStore.removeChangeListener(this.onListenerChange);
UserStore.removeChangeListener(this.onListenerChange);
TeamStore.removeStatsChangeListener(this.onChange);
ChannelStore.removeStatsChangeListener(this.onChange);
UserStore.removeNotInChannelChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
}
}
componentWillUnmount() {
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
ChannelStore.removeChangeListener(this.onListenerChange);
UserStore.removeChangeListener(this.onListenerChange);
ChannelStore.removeStatsChangeListener(this.onChange);
ChannelStore.removeChangeListener(this.onChange);
UserStore.removeNotInChannelChangeListener(this.onChange);
}
onListenerChange() {
var newState = this.getStateFromStores();
if (!Utils.areObjectsEqual(this.state, newState)) {
this.setState(newState);
onChange() {
if (this.state.search) {
this.search(this.term);
return;
}
const channelStats = ChannelStore.getStats(this.props.channel.id);
const teamStats = TeamStore.getCurrentStats();
this.setState({
users: UserStore.getProfileListNotInChannel(this.props.channel.id),
total: teamStats.member_count - channelStats.member_count
});
}
handleInviteError(err) {
if (err) {
this.setState({
@@ -130,6 +91,29 @@ export default class ChannelInviteModal extends React.Component {
});
}
}
nextPage(page) {
AsyncClient.getProfilesNotInChannel(this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
}
search(term) {
this.term = term;
if (term === '') {
this.setState({users: UserStore.getProfileListNotInChannel(), search: false});
return;
}
searchUsers(
term,
TeamStore.getCurrentId(),
{not_in_channel: this.props.channel.id},
(users) => {
this.setState({search: true, users});
}
);
}
render() {
var inviteError = null;
if (this.state.inviteError) {
@@ -145,9 +129,13 @@ export default class ChannelInviteModal extends React.Component {
maxHeight = Utils.windowHeight() - 300;
}
content = (
<FilteredUserList
<SearchableUserList
style={{maxHeight}}
users={this.state.nonmembers}
users={this.state.users}
usersPerPage={USERS_PER_PAGE}
total={this.state.total}
nextPage={this.nextPage}
search={this.search}
actions={[ChannelInviteButton]}
actionProps={{
channel: this.props.channel,

View File

@@ -1,122 +1,89 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import FilteredUserList from './filtered_user_list.jsx';
import SearchableUserList from './searchable_user_list.jsx';
import LoadingScreen from './loading_screen.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import {searchUsers} from 'actions/user_actions.jsx';
import {removeUserFromChannel} from 'actions/channel_actions.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
import React from 'react';
const USERS_PER_PAGE = 50;
export default class ChannelMembersModal extends React.Component {
constructor(props) {
super(props);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onChange = this.onChange.bind(this);
this.handleRemove = this.handleRemove.bind(this);
this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this);
this.search = this.search.bind(this);
this.nextPage = this.nextPage.bind(this);
this.term = '';
const stats = ChannelStore.getStats(props.channel.id);
// the rest of the state gets populated when the modal is shown
this.state = {
showInviteModal: false
users: [],
total: stats.member_count,
showInviteModal: false,
search: false
};
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(this.props, nextProps)) {
return true;
}
if (!Utils.areObjectsEqual(this.state, nextState)) {
return true;
}
return false;
}
getStateFromStores() {
const extraInfo = ChannelStore.getCurrentExtraInfo();
const profiles = UserStore.getActiveOnlyProfiles();
if (extraInfo.member_count !== extraInfo.members.length) {
AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
return {
loading: true
};
}
const memberList = extraInfo.members.map((member) => {
return profiles[member.id];
});
function compareByUsername(a, b) {
if (a.username < b.username) {
return -1;
} else if (a.username > b.username) {
return 1;
}
return 0;
}
memberList.sort(compareByUsername);
return {
memberList,
loading: false
};
}
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
ChannelStore.addExtraInfoChangeListener(this.onChange);
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addStatsChangeListener(this.onChange);
UserStore.addInChannelChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
this.onChange();
AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
} else if (this.props.show && !nextProps.show) {
ChannelStore.removeExtraInfoChangeListener(this.onChange);
ChannelStore.removeChangeListener(this.onChange);
ChannelStore.removeStatsChangeListener(this.onChange);
UserStore.removeInChannelChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
}
}
onChange() {
const newState = this.getStateFromStores();
if (!Utils.areObjectsEqual(this.state, newState)) {
this.setState(newState);
if (this.state.search) {
this.search(this.term);
return;
}
const stats = ChannelStore.getStats(this.props.channel.id);
this.setState({
users: UserStore.getProfileListInChannel(this.props.channel.id),
total: stats.member_count
});
}
handleRemove(user) {
const userId = user.id;
Client.removeChannelMember(
ChannelStore.getCurrentId(),
removeUserFromChannel(
this.props.channel.id,
userId,
() => {
const memberList = this.state.memberList.slice();
for (let i = 0; i < memberList.length; i++) {
if (userId === memberList[i].id) {
memberList.splice(i, 1);
break;
}
}
this.setState({memberList});
AsyncClient.getChannelExtraInfo();
},
null,
(err) => {
this.setState({inviteError: err.message});
}
);
}
createRemoveMemberButton({user}) {
if (user.id === UserStore.getCurrentId()) {
return null;
@@ -135,6 +102,29 @@ export default class ChannelMembersModal extends React.Component {
</button>
);
}
nextPage(page) {
AsyncClient.getProfilesInChannel(this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
}
search(term) {
this.term = term;
if (term === '') {
this.setState({users: UserStore.getProfileListInChannel(this.props.channel.id), search: false});
return;
}
searchUsers(
term,
TeamStore.getCurrentId(),
{in_channel: this.props.channel.id},
(users) => {
this.setState({search: true, users});
}
);
}
render() {
let content;
if (this.state.loading) {
@@ -151,9 +141,13 @@ export default class ChannelMembersModal extends React.Component {
}
content = (
<FilteredUserList
<SearchableUserList
style={{maxHeight}}
users={this.state.memberList}
users={this.state.users}
usersPerPage={USERS_PER_PAGE}
total={this.state.total}
nextPage={this.nextPage}
search={this.search}
actions={removeButton}
/>
);

View File

@@ -65,9 +65,9 @@ export default class ChannelNotificationsModal extends React.Component {
Client.updateChannelNotifyProps(data,
() => {
// YUCK
var member = ChannelStore.getMember(channelId);
var member = ChannelStore.getMyMember(channelId);
member.notify_props.desktop = notifyLevel;
ChannelStore.setChannelMember(member);
ChannelStore.storeMyChannelMember(member);
this.updateSection('');
},
(err) => {
@@ -256,13 +256,13 @@ export default class ChannelNotificationsModal extends React.Component {
mark_unread: markUnreadLevel
};
//TODO: This should be fixed, moved to event_helpers
//TODO: This should be fixed, moved to actions
Client.updateChannelNotifyProps(data,
() => {
// Yuck...
var member = ChannelStore.getMember(channelId);
var member = ChannelStore.getMyMember(channelId);
member.notify_props.mark_unread = markUnreadLevel;
ChannelStore.setChannelMember(member);
ChannelStore.storeMyChannelMember(member);
this.updateSection('');
},
(err) => {

View File

@@ -8,12 +8,13 @@ import SwitchChannelProvider from './suggestion/switch_channel_provider.jsx';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
import {goToChannel, openDirectChannelToUser} from 'actions/channel_actions.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
import React from 'react';
import $ from 'jquery';
@@ -27,30 +28,14 @@ export default class SwitchChannelModal extends React.Component {
this.onExited = this.onExited.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleDmUserChange = this.handleDmUserChange.bind(this);
this.suggestionProviders = [new SwitchChannelProvider()];
this.state = {
dmUsers: UserStore.getDirectProfiles(),
text: '',
error: ''
};
}
componentDidMount() {
UserStore.addDmListChangeListener(this.handleDmUserChange);
}
componentWillUnmount() {
UserStore.removeDmListChangeListener(this.handleDmUserChange);
}
handleDmUserChange() {
this.setState({
dmUsers: UserStore.getDirectProfiles()
});
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
const textbox = this.refs.search.getTextbox();
@@ -97,18 +82,13 @@ export default class SwitchChannelModal extends React.Component {
const name = this.state.text.trim();
let channel = null;
// TODO: Replace this hack with something reasonable
if (name.indexOf(Utils.localizeMessage('channel_switch_modal.dm', '(Direct Message)')) > 0) {
const dmUsername = name.substr(0, name.indexOf(Utils.localizeMessage('channel_switch_modal.dm', '(Direct Message)')) - 1);
let user = null;
for (const id in this.state.dmUsers) {
if (this.state.dmUsers[id].username === dmUsername) {
user = this.state.dmUsers[id];
break;
}
}
const user = UserStore.getProfileByUsername(dmUsername);
if (user) {
Utils.openDirectChannelToUser(
openDirectChannelToUser(
user,
(ch) => {
channel = ch;
@@ -123,7 +103,7 @@ export default class SwitchChannelModal extends React.Component {
}
if (channel !== null) {
ChannelActions.goToChannel(channel);
goToChannel(channel);
this.onHide();
} else if (this.state.text !== '') {
this.setState({

View File

@@ -1,26 +1,27 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import $ from 'jquery';
import ReactDOM from 'react-dom';
import Client from 'client/web_client.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import Textbox from './textbox.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import PostStore from 'stores/post_store.jsx';
import MessageHistoryStore from 'stores/message_history_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {loadPosts} from 'actions/post_actions.jsx';
import Client from 'client/web_client.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
const KeyCodes = Constants.KeyCodes;
import {FormattedMessage} from 'react-intl';
var KeyCodes = Constants.KeyCodes;
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
export default class EditPostModal extends React.Component {
constructor(props) {
@@ -77,7 +78,7 @@ export default class EditPostModal extends React.Component {
Client.updatePost(
updatedPost,
() => {
AsyncClient.getPosts(updatedPost.channel_id);
loadPosts(updatedPost.channel_id);
window.scrollTo(0, 0);
},
(err) => {

View File

@@ -1,16 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import EmojiListItem from './emoji_list_item.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
import EmojiStore from 'stores/emoji_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {loadEmoji} from 'actions/emoji_actions.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import EmojiStore from 'stores/emoji_store.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
import EmojiListItem from './emoji_list_item.jsx';
import React from 'react';
import {Link} from 'react-router';
import LoadingScreen from 'components/loading_screen.jsx';
import {FormattedMessage} from 'react-intl';
export default class EmojiList extends React.Component {
static get propTypes() {
@@ -24,28 +28,30 @@ export default class EmojiList extends React.Component {
super(props);
this.handleEmojiChange = this.handleEmojiChange.bind(this);
this.handleUserChange = this.handleUserChange.bind(this);
this.deleteEmoji = this.deleteEmoji.bind(this);
this.updateFilter = this.updateFilter.bind(this);
this.state = {
emojis: EmojiStore.getCustomEmojiMap(),
loading: !EmojiStore.hasReceivedCustomEmojis(),
filter: ''
filter: '',
users: UserStore.getProfiles()
};
}
componentDidMount() {
EmojiStore.addChangeListener(this.handleEmojiChange);
UserStore.addChangeListener(this.handleUserChange);
if (window.mm_config.EnableCustomEmoji === 'true') {
AsyncClient.listEmoji();
loadEmoji();
}
}
componentWillUnmount() {
EmojiStore.removeChangeListener(this.handleEmojiChange);
UserStore.removeChangeListener(this.handleUserChange);
}
handleEmojiChange() {
@@ -55,6 +61,10 @@ export default class EmojiList extends React.Component {
});
}
handleUserChange() {
this.setState({users: UserStore.getProfiles()});
}
updateFilter(e) {
this.setState({
filter: e.target.value
@@ -98,6 +108,7 @@ export default class EmojiList extends React.Component {
emoji={emoji}
onDelete={onDelete}
filter={filter}
creator={this.state.users[emoji.creator_id] || {}}
/>
);
}

View File

@@ -4,7 +4,7 @@
import React from 'react';
import EmojiStore from 'stores/emoji_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
@@ -14,7 +14,8 @@ export default class EmojiListItem extends React.Component {
return {
emoji: React.PropTypes.object.isRequired,
onDelete: React.PropTypes.func.isRequired,
filter: React.PropTypes.string
filter: React.PropTypes.string,
creator: React.PropTypes.object.isRequired
};
}
@@ -22,10 +23,6 @@ export default class EmojiListItem extends React.Component {
super(props);
this.handleDelete = this.handleDelete.bind(this);
this.state = {
creator: UserStore.getProfile(this.props.emoji.creator_id)
};
}
handleDelete(e) {
@@ -57,7 +54,7 @@ export default class EmojiListItem extends React.Component {
render() {
const emoji = this.props.emoji;
const creator = this.state.creator;
const creator = this.props.creator;
const filter = this.props.filter ? this.props.filter.toLowerCase() : '';
if (!this.matchesFilter(emoji, creator, filter)) {

View File

@@ -2,9 +2,6 @@
// See License.txt for license information.
import React from 'react';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
export default class InstalledCommand extends React.Component {
@@ -13,7 +10,8 @@ export default class InstalledCommand extends React.Component {
command: React.PropTypes.object.isRequired,
onRegenToken: React.PropTypes.func.isRequired,
onDelete: React.PropTypes.func.isRequired,
filter: React.PropTypes.string
filter: React.PropTypes.string,
creator: React.PropTypes.object.isRequired
};
}
@@ -113,7 +111,7 @@ export default class InstalledCommand extends React.Component {
id='installed_integrations.creation'
defaultMessage='Created by {creator} on {createAt, date, full}'
values={{
creator: Utils.displayUsername(command.creator_id),
creator: this.props.creator.username,
createAt: command.create_at
}}
/>

View File

@@ -1,16 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import BackstageList from 'components/backstage/components/backstage_list.jsx';
import InstalledCommand from './installed_command.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import IntegrationStore from 'stores/integration_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {loadTeamCommands} from 'actions/integration_actions.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import BackstageList from 'components/backstage/components/backstage_list.jsx';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import InstalledCommand from './installed_command.jsx';
export default class InstalledCommands extends React.Component {
static get propTypes() {
@@ -23,7 +27,7 @@ export default class InstalledCommands extends React.Component {
super(props);
this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
this.handleUserChange = this.handleUserChange.bind(this);
this.regenCommandToken = this.regenCommandToken.bind(this);
this.deleteCommand = this.deleteCommand.bind(this);
@@ -31,20 +35,23 @@ export default class InstalledCommands extends React.Component {
this.state = {
commands: IntegrationStore.getCommands(teamId),
loading: !IntegrationStore.hasReceivedCommands(teamId)
loading: !IntegrationStore.hasReceivedCommands(teamId),
users: UserStore.getProfiles()
};
}
componentDidMount() {
IntegrationStore.addChangeListener(this.handleIntegrationChange);
UserStore.addChangeListener(this.handleUserChange);
if (window.mm_config.EnableCommands === 'true') {
AsyncClient.listTeamCommands();
loadTeamCommands();
}
}
componentWillUnmount() {
IntegrationStore.removeChangeListener(this.handleIntegrationChange);
UserStore.removeChangeListener(this.handleUserChange);
}
handleIntegrationChange() {
@@ -56,6 +63,10 @@ export default class InstalledCommands extends React.Component {
});
}
handleUserChange() {
this.setState({users: UserStore.getProfiles()});
}
regenCommandToken(command) {
AsyncClient.regenCommandToken(command.id);
}
@@ -72,6 +83,7 @@ export default class InstalledCommands extends React.Component {
command={command}
onRegenToken={this.regenCommandToken}
onDelete={this.deleteCommand}
creator={this.state.users[command.creator_id] || {}}
/>
);
});

View File

@@ -13,7 +13,8 @@ export default class InstalledIncomingWebhook extends React.Component {
return {
incomingWebhook: React.PropTypes.object.isRequired,
onDelete: React.PropTypes.func.isRequired,
filter: React.PropTypes.string
filter: React.PropTypes.string,
creator: React.PropTypes.object.isRequired
};
}
@@ -108,7 +109,7 @@ export default class InstalledIncomingWebhook extends React.Component {
id='installed_integrations.creation'
defaultMessage='Created by {creator} on {createAt, date, full}'
values={{
creator: Utils.displayUsername(incomingWebhook.user_id),
creator: this.props.creator.username,
createAt: incomingWebhook.create_at
}}
/>

View File

@@ -1,16 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import BackstageList from 'components/backstage/components/backstage_list.jsx';
import InstalledIncomingWebhook from './installed_incoming_webhook.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import IntegrationStore from 'stores/integration_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {loadIncomingHooks} from 'actions/integration_actions.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import BackstageList from 'components/backstage/components/backstage_list.jsx';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import InstalledIncomingWebhook from './installed_incoming_webhook.jsx';
export default class InstalledIncomingWebhooks extends React.Component {
static get propTypes() {
@@ -23,27 +27,30 @@ export default class InstalledIncomingWebhooks extends React.Component {
super(props);
this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
this.handleUserChange = this.handleUserChange.bind(this);
this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this);
const teamId = TeamStore.getCurrentId();
this.state = {
incomingWebhooks: IntegrationStore.getIncomingWebhooks(teamId),
loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId)
loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId),
users: UserStore.getProfiles()
};
}
componentDidMount() {
IntegrationStore.addChangeListener(this.handleIntegrationChange);
UserStore.addChangeListener(this.handleUserChange);
if (window.mm_config.EnableIncomingWebhooks === 'true') {
AsyncClient.listIncomingHooks();
loadIncomingHooks();
}
}
componentWillUnmount() {
IntegrationStore.removeChangeListener(this.handleIntegrationChange);
UserStore.removeChangeListener(this.handleUserChange);
}
handleIntegrationChange() {
@@ -55,6 +62,12 @@ export default class InstalledIncomingWebhooks extends React.Component {
});
}
handleUserChange() {
this.setState({
users: UserStore.getProfiles()
});
}
deleteIncomingWebhook(incomingWebhook) {
AsyncClient.deleteIncomingHook(incomingWebhook.id);
}
@@ -66,6 +79,7 @@ export default class InstalledIncomingWebhooks extends React.Component {
key={incomingWebhook.id}
incomingWebhook={incomingWebhook}
onDelete={this.deleteIncomingWebhook}
creator={this.state.users[incomingWebhook.user_id] || {}}
/>
);
});

View File

@@ -4,7 +4,6 @@
import React from 'react';
import ChannelStore from 'stores/channel_store.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
@@ -14,7 +13,8 @@ export default class InstalledOutgoingWebhook extends React.Component {
outgoingWebhook: React.PropTypes.object.isRequired,
onRegenToken: React.PropTypes.func.isRequired,
onDelete: React.PropTypes.func.isRequired,
filter: React.PropTypes.string
filter: React.PropTypes.string,
creator: React.PropTypes.object.isRequired
};
}
@@ -195,7 +195,7 @@ export default class InstalledOutgoingWebhook extends React.Component {
id='installed_integrations.creation'
defaultMessage='Created by {creator} on {createAt, date, full}'
values={{
creator: Utils.displayUsername(outgoingWebhook.creator_id),
creator: this.props.creator.username,
createAt: outgoingWebhook.create_at
}}
/>

View File

@@ -1,16 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import BackstageList from 'components/backstage/components/backstage_list.jsx';
import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import IntegrationStore from 'stores/integration_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import * as Utils from 'utils/utils.jsx';
import UserStore from 'stores/user_store.jsx';
import BackstageList from 'components/backstage/components/backstage_list.jsx';
import {loadOutgoingHooks} from 'actions/integration_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx';
export default class InstalledOutgoingWebhooks extends React.Component {
static get propTypes() {
@@ -23,7 +27,7 @@ export default class InstalledOutgoingWebhooks extends React.Component {
super(props);
this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
this.handleUserChange = this.handleUserChange.bind(this);
this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this);
this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this);
@@ -31,20 +35,23 @@ export default class InstalledOutgoingWebhooks extends React.Component {
this.state = {
outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(teamId),
loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId)
loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId),
users: UserStore.getProfiles()
};
}
componentDidMount() {
IntegrationStore.addChangeListener(this.handleIntegrationChange);
UserStore.addChangeListener(this.handleUserChange);
if (window.mm_config.EnableOutgoingWebhooks === 'true') {
AsyncClient.listOutgoingHooks();
loadOutgoingHooks();
}
}
componentWillUnmount() {
IntegrationStore.removeChangeListener(this.handleIntegrationChange);
UserStore.removeChangeListener(this.handleUserChange);
}
handleIntegrationChange() {
@@ -56,6 +63,10 @@ export default class InstalledOutgoingWebhooks extends React.Component {
});
}
handleUserChange() {
this.setState({users: UserStore.getProfiles()});
}
regenOutgoingWebhookToken(outgoingWebhook) {
AsyncClient.regenOutgoingHookToken(outgoingWebhook.id);
}
@@ -72,6 +83,7 @@ export default class InstalledOutgoingWebhooks extends React.Component {
outgoingWebhook={outgoingWebhook}
onRegenToken={this.regenOutgoingWebhookToken}
onDelete={this.deleteOutgoingWebhook}
creator={this.state.users[outgoingWebhook.creator_id] || {}}
/>
);
});

View File

@@ -1,21 +1,24 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import $ from 'jquery';
import LoadingScreen from 'components/loading_screen.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import UserStore from 'stores/user_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import * as WebSocketActions from 'actions/websocket_actions.jsx';
import {loadEmoji} from 'actions/emoji_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import {browserHistory} from 'react-router/es6';
const BACKSPACE_CHAR = 8;
import $ from 'jquery';
import React from 'react';
// import the EmojiStore so that it'll register to receive the results of the listEmojis call further down
@@ -148,7 +151,7 @@ export default class LoggedIn extends React.Component {
// Get custom emoji from the server
if (window.mm_config.EnableCustomEmoji === 'true') {
AsyncClient.listEmoji();
loadEmoji(false);
}
}

View File

@@ -1,62 +1,94 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import FilteredUserList from './filtered_user_list.jsx';
import TeamMembersDropdown from './team_members_dropdown.jsx';
import SearchableUserList from 'components/searchable_user_list.jsx';
import TeamMembersDropdown from 'components/team_members_dropdown.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import {searchUsers, loadProfilesAndTeamMembers, loadTeamMembersForProfilesList} from 'actions/user_actions.jsx';
import {getTeamStats} from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
import React from 'react';
const USERS_PER_PAGE = 50;
export default class MemberListTeam extends React.Component {
constructor(props) {
super(props);
this.getUsers = this.getUsers.bind(this);
this.onChange = this.onChange.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.onStatsChange = this.onStatsChange.bind(this);
this.search = this.search.bind(this);
this.loadComplete = this.loadComplete.bind(this);
const stats = TeamStore.getCurrentStats();
this.state = {
users: this.getUsers(),
teamMembers: TeamStore.getMembersForTeam()
users: UserStore.getProfileListInTeam(),
teamMembers: Object.assign([], TeamStore.getMembersInTeam()),
total: stats.member_count,
search: false,
loading: true
};
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
TeamStore.addChangeListener(this.onTeamChange);
AsyncClient.getTeamMembers(TeamStore.getCurrentId());
UserStore.addInTeamChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
TeamStore.addChangeListener(this.onChange);
TeamStore.addStatsChangeListener(this.onStatsChange);
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, TeamStore.getCurrentId(), this.loadComplete);
getTeamStats(TeamStore.getCurrentId());
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
TeamStore.removeChangeListener(this.onTeamChange);
UserStore.removeInTeamChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
TeamStore.removeChangeListener(this.onChange);
TeamStore.removeStatsChangeListener(this.onStatsChange);
}
getUsers() {
const profiles = UserStore.getProfiles();
const users = [];
for (const id of Object.keys(profiles)) {
users.push(profiles[id]);
}
users.sort((a, b) => a.username.localeCompare(b.username));
return users;
loadComplete() {
this.setState({loading: false});
}
onChange() {
this.setState({
users: this.getUsers()
});
if (!this.state.search) {
this.setState({users: UserStore.getProfileListInTeam()});
}
this.setState({teamMembers: Object.assign([], TeamStore.getMembersInTeam())});
}
onTeamChange() {
this.setState({
teamMembers: TeamStore.getMembersForTeam()
});
onStatsChange() {
const stats = TeamStore.getCurrentStats();
this.setState({total: stats.member_count});
}
nextPage(page) {
loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
}
search(term) {
if (term === '') {
this.setState({search: false, users: UserStore.getProfileListInTeam()});
return;
}
searchUsers(
term,
TeamStore.getCurrentId(),
{},
(users) => {
this.setState({loading: true, search: true, users});
loadTeamMembersForProfilesList(users, TeamStore.getCurrentId(), this.loadComplete);
}
);
}
render() {
@@ -65,12 +97,38 @@ export default class MemberListTeam extends React.Component {
teamMembersDropdown = [TeamMembersDropdown];
}
const teamMembers = this.state.teamMembers;
const users = this.state.users;
const actionUserProps = {};
let usersToDisplay;
if (this.state.loading) {
usersToDisplay = null;
} else {
usersToDisplay = [];
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (teamMembers[user.id]) {
usersToDisplay.push(user);
actionUserProps[user.id] = {
teamMember: teamMembers[user.id]
};
}
}
}
return (
<FilteredUserList
<SearchableUserList
style={this.props.style}
users={this.state.users}
teamMembers={this.state.teamMembers}
users={usersToDisplay}
usersPerPage={USERS_PER_PAGE}
total={this.state.total}
nextPage={this.nextPage}
search={this.search}
actions={teamMembersDropdown}
actionUserProps={actionUserProps}
/>
);
}

View File

@@ -1,73 +1,67 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import FilteredUserList from 'components/filtered_user_list.jsx';
import SearchableUserList from 'components/searchable_user_list.jsx';
import SpinnerButton from 'components/spinner_button.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
import {getMoreDmList} from 'actions/user_actions.jsx';
import {searchUsers} from 'actions/user_actions.jsx';
import {openDirectChannelToUser} from 'actions/channel_actions.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
const USERS_PER_PAGE = 50;
export default class MoreDirectChannels extends React.Component {
constructor(props) {
super(props);
this.handleHide = this.handleHide.bind(this);
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.handleUserChange = this.handleUserChange.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.onChange = this.onChange.bind(this);
this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this);
this.toggleList = this.toggleList.bind(this);
this.nextPage = this.nextPage.bind(this);
this.search = this.search.bind(this);
this.loadComplete = this.loadComplete.bind(this);
this.state = {
users: UserStore.getProfilesForDmList(),
teamMembers: TeamStore.getMembersForTeam(),
users: UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true),
loadingDMChannel: -1,
usersLoaded: false,
teamMembersLoaded: false
listType: 'team',
loading: false,
search: false
};
}
componentDidMount() {
UserStore.addDmListChangeListener(this.handleUserChange);
TeamStore.addChangeListener(this.onTeamChange);
UserStore.addChangeListener(this.onChange);
UserStore.addInTeamChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
TeamStore.addChangeListener(this.onChange);
AsyncClient.getProfiles(0, Constants.PROFILE_CHUNK_SIZE);
AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, Constants.PROFILE_CHUNK_SIZE);
}
componentWillUnmount() {
UserStore.removeDmListChangeListener(this.handleUserChange);
TeamStore.removeChangeListener(this.onTeamChange);
UserStore.removeChangeListener(this.onChange);
UserStore.removeInTeamChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
TeamStore.removeChangeListener(this.onChange);
}
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.show !== this.props.show) {
return true;
}
if (nextProps.onModalDismissed.toString() !== this.props.onModalDismissed.toString()) {
return true;
}
if (nextState.loadingDMChannel !== this.state.loadingDMChannel) {
return true;
}
if (!Utils.areObjectsEqual(nextState.users, this.state.users)) {
return true;
}
if (!Utils.areObjectsEqual(nextState.teamMembers, this.state.teamMembers)) {
return true;
}
return false;
loadComplete() {
this.setState({loading: false});
}
handleHide() {
@@ -84,7 +78,7 @@ export default class MoreDirectChannels extends React.Component {
}
this.setState({loadingDMChannel: teammate.id});
Utils.openDirectChannelToUser(
openDirectChannelToUser(
teammate,
(channel) => {
browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name);
@@ -97,17 +91,35 @@ export default class MoreDirectChannels extends React.Component {
);
}
handleUserChange() {
onChange(force) {
if (this.state.search && !force) {
return;
}
let users;
if (this.state.listType === 'any') {
users = UserStore.getProfileList();
} else {
users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true);
}
this.setState({
users: UserStore.getProfilesForDmList(),
usersLoaded: true
users
});
}
onTeamChange() {
toggleList(e) {
const listType = e.target.value;
let users;
if (listType === 'any') {
users = UserStore.getProfileList();
} else {
users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true);
}
this.setState({
teamMembers: TeamStore.getMembersForTeam(),
teamMembersLoaded: true
users,
listType
});
}
@@ -126,38 +138,96 @@ export default class MoreDirectChannels extends React.Component {
);
}
nextPage(page) {
if (this.state.listType === 'any') {
AsyncClient.getProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
} else {
AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
}
}
search(term) {
if (term === '') {
this.onChange(true);
this.setState({search: false});
return;
}
let teamId;
if (this.state.listType === 'any') {
teamId = '';
} else {
teamId = TeamStore.getCurrentId();
}
searchUsers(
term,
teamId,
{},
(users) => {
for (let i = 0; i < users.length; i++) {
if (users[i].id === UserStore.getCurrentId()) {
users.splice(i, 1);
break;
}
}
this.setState({search: true, users});
}
);
}
render() {
let maxHeight = 1000;
if (Utils.windowHeight() <= 1200) {
maxHeight = Utils.windowHeight() - 300;
}
var body = null;
if (!this.state.usersLoaded || !this.state.teamMembersLoaded) {
body = (<LoadingScreen/>);
} else {
var showTeamToggle = false;
if (global.window.mm_config.RestrictDirectMessage === 'any') {
showTeamToggle = true;
}
body = (
<FilteredUserList
style={{maxHeight}}
users={this.state.users}
teamMembers={this.state.teamMembers}
actions={[this.createJoinDirectChannelButton]}
showTeamToggle={showTeamToggle}
/>
let teamToggle;
if (global.window.mm_config.RestrictDirectMessage === 'any') {
teamToggle = (
<div className='member-select__container'>
<select
className='form-control'
id='restrictList'
ref='restrictList'
defaultValue='team'
onChange={this.toggleList}
>
<option value='any'>
<FormattedMessage
id='filtered_user_list.any_team'
defaultMessage='All Users'
/>
</option>
<option value='team'>
<FormattedMessage
id='filtered_user_list.team_only'
defaultMessage='Members of this Team'
/>
</option>
</select>
<span
className='member-show'
>
<FormattedMessage
id='filtered_user_list.show'
defaultMessage='Filter:'
/>
</span>
</div>
);
}
let users = this.state.users;
if (this.state.loading) {
users = null;
}
return (
<Modal
dialogClassName='more-modal more-direct-channels'
show={this.props.show}
onHide={this.handleHide}
onEntered={getMoreDmList}
>
<Modal.Header closeButton={true}>
<Modal.Title>
@@ -168,7 +238,16 @@ export default class MoreDirectChannels extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
{body}
{teamToggle}
<SearchableUserList
key={'moreDirectChannelsList_' + this.state.listType}
style={{maxHeight}}
users={users}
usersPerPage={USERS_PER_PAGE}
nextPage={this.nextPage}
search={this.search}
actions={[this.createJoinDirectChannelButton]}
/>
</Modal.Body>
<Modal.Footer>
<button

View File

@@ -69,8 +69,8 @@ export default class Navbar extends React.Component {
return {
channel: ChannelStore.getCurrent(),
member: ChannelStore.getCurrentMember(),
users: ChannelStore.getCurrentExtraInfo().members,
userCount: ChannelStore.getCurrentExtraInfo().member_count,
users: [],
userCount: ChannelStore.getCurrentStats().member_count,
currentUser: UserStore.getCurrentUser()
};
}
@@ -81,7 +81,7 @@ export default class Navbar extends React.Component {
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange);
ChannelStore.addStatsChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
$('.inner-wrap').click(this.hideSidebars);
document.addEventListener('keydown', this.showChannelSwitchModal);
@@ -89,7 +89,7 @@ export default class Navbar extends React.Component {
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
ChannelStore.removeExtraInfoChangeListener(this.onChange);
ChannelStore.removeStatsChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
document.removeEventListener('keydown', this.showChannelSwitchModal);
}

View File

@@ -13,6 +13,7 @@ import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
import Constants from 'utils/constants.jsx';
const TutorialSteps = Constants.TutorialSteps;
const Preferences = Constants.Preferences;
@@ -80,6 +81,7 @@ export default class NeedsTeam extends React.Component {
if (tutorialStep <= TutorialSteps.INTRO_SCREENS) {
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/tutorial');
}
stopPeriodicStatusUpdates();
}
componentDidMount() {
@@ -89,6 +91,8 @@ export default class NeedsTeam extends React.Component {
// Emit view action
GlobalActions.viewLoggedIn();
startPeriodicStatusUpdates();
// Set up tracking for whether the window is active
window.isActive = true;
$(window).on('focus', () => {

View File

@@ -7,7 +7,7 @@ import ChannelStore from 'stores/channel_store.jsx';
function getCountsStateFromStores() {
var count = 0;
var channels = ChannelStore.getAll();
var members = ChannelStore.getAllMembers();
var members = ChannelStore.getMyMembers();
channels.forEach((channel) => {
var channelMember = members[channel.id];

View File

@@ -6,9 +6,11 @@ import ProfilePicture from 'components/profile_picture.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {openDirectChannelToUser} from 'actions/channel_actions.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import $ from 'jquery';
import React from 'react';
@@ -22,20 +24,18 @@ export default class PopoverListMembers extends React.Component {
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.closePopover = this.closePopover.bind(this);
this.state = {showPopover: false};
}
componentDidUpdate() {
$('.member-list__popover .popover-content').perfectScrollbar();
}
componentWillMount() {
this.setState({showPopover: false});
}
handleShowDirectChannel(teammate, e) {
e.preventDefault();
Utils.openDirectChannelToUser(
openDirectChannelToUser(
teammate,
(channel, channelAlreadyExisted) => {
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
@@ -90,12 +90,6 @@ export default class PopoverListMembers extends React.Component {
}
if (name) {
let status;
if (m.status) {
status = m.status;
} else {
status = UserStore.getStatus(m.id);
}
popoverHtml.push(
<div
className='more-modal__row'
@@ -103,7 +97,6 @@ export default class PopoverListMembers extends React.Component {
>
<ProfilePicture
src={`${Client.getUsersRoute()}/${m.id}/image?time=${m.update_at}`}
status={status}
width='26'
height='26'
/>
@@ -123,19 +116,27 @@ export default class PopoverListMembers extends React.Component {
);
}
});
popoverHtml.push(
<div
className='more-modal__row'
key={'popover-member-more'}
>
<div className='col-sm-5'/>
<div className='more-modal__details'>
<div
className='more-modal__name'
>
{'...'}
</div>
</div>
</div>
);
}
let count = this.props.memberCount;
const count = this.props.memberCount;
let countText = '-';
// fall back to checking the length of the member list if the count isn't set
if (!count && members) {
count = members.length;
}
if (count > Constants.MAX_CHANNEL_POPOVER_COUNT) {
countText = Constants.MAX_CHANNEL_POPOVER_COUNT + '+';
} else if (count > 0) {
if (count > 0) {
countText = count.toString();
}
@@ -151,7 +152,10 @@ export default class PopoverListMembers extends React.Component {
id='member_popover'
className='member-popover__trigger'
ref='member_popover_target'
onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})}
onClick={(e) => {
this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover});
AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
}}
>
<div>
{countText}

View File

@@ -4,11 +4,10 @@
import PostStore from 'stores/post_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import {loadPosts} from 'actions/post_actions.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
@@ -29,13 +28,13 @@ export default class PendingPostOptions extends React.Component {
var post = this.props.post;
Client.createPost(post,
(data) => {
AsyncClient.getPosts(post.channel_id);
loadPosts(post.channel_id);
var channel = ChannelStore.get(post.channel_id);
var member = ChannelStore.getMember(post.channel_id);
var member = ChannelStore.getMyMember(post.channel_id);
member.msg_count = channel.total_msg_count;
member.last_viewed_at = (new Date()).getTime();
ChannelStore.setChannelMember(member);
ChannelStore.storeMyChannelMember(member);
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST,

View File

@@ -66,6 +66,16 @@ export default class PostList extends React.Component {
}
}
componentWillReceiveProps(nextProps) {
// TODO: Clean-up intro text creation
if (this.props.channel && this.props.channel.type === Constants.DM_CHANNEL) {
const teammateId = Utils.getUserIdFromChannelName(this.props.channel);
if (!this.props.profiles[teammateId] && nextProps.profiles[teammateId]) {
this.introText = createChannelIntroMessage(this.props.channel, this.state.fullWidthIntro);
}
}
}
handleKeyDown(e) {
if (e.which === Constants.KeyCodes.ESCAPE && $('.popover.in,.modal.in').length === 0) {
e.preventDefault();

View File

@@ -35,10 +35,7 @@ export default class PostFocusView extends React.Component {
const focusedPostId = PostStore.getFocusedPostId();
const channel = ChannelStore.getCurrent();
let profiles = UserStore.getProfiles();
if (channel && channel.type === Constants.DM_CHANNEL) {
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
}
const profiles = UserStore.getProfiles();
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);
@@ -115,12 +112,7 @@ export default class PostFocusView extends React.Component {
}
onUserChange() {
const channel = ChannelStore.getCurrent();
let profiles = UserStore.getProfiles();
if (channel && channel.type === Constants.DM_CHANNEL) {
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
}
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))});
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
}
onStatusChange() {

View File

@@ -34,13 +34,10 @@ export default class PostViewController extends React.Component {
this.onBusy = this.onBusy.bind(this);
const channel = props.channel;
let profiles = UserStore.getProfiles();
if (channel && channel.type === Constants.DM_CHANNEL) {
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
}
const profiles = UserStore.getProfiles();
let lastViewed = Number.MAX_VALUE;
const member = ChannelStore.getMember(channel.id);
const member = ChannelStore.getMyMember(channel.id);
if (member != null) {
lastViewed = member.last_viewed_at;
}
@@ -107,12 +104,7 @@ export default class PostViewController extends React.Component {
}
onUserChange() {
const channel = this.state.channel;
let profiles = UserStore.getProfiles();
if (channel && channel.type === Constants.DM_CHANNEL) {
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
}
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))});
this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
}
onPostsChange() {
@@ -165,15 +157,12 @@ export default class PostViewController extends React.Component {
const channel = nextProps.channel;
let lastViewed = Number.MAX_VALUE;
const member = ChannelStore.getMember(channel.id);
const member = ChannelStore.getMyMember(channel.id);
if (member != null) {
lastViewed = member.last_viewed_at;
}
let profiles = UserStore.getProfiles();
if (channel && channel.type === Constants.DM_CHANNEL) {
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
}
const profiles = UserStore.getProfiles();
const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true);

View File

@@ -61,6 +61,10 @@ export default class RhsRootPost extends React.Component {
return true;
}
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
return true;
}
if (!Utils.areObjectsEqual(nextProps.currentUser, this.props.currentUser)) {
return true;
}
@@ -85,7 +89,7 @@ export default class RhsRootPost extends React.Component {
var isOwner = this.props.currentUser.id === post.user_id;
var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
var timestamp = UserStore.getProfile(post.user_id).update_at;
var timestamp = user.update_at;
var channel = ChannelStore.get(post.channel_id);
const flagIcon = Constants.FLAG_ICON_SVG;

View File

@@ -8,7 +8,6 @@ import RootPost from './rhs_root_post.jsx';
import Comment from './rhs_comment.jsx';
import FileUploadOverlay from './file_upload_overlay.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
@@ -238,12 +237,7 @@ export default class RhsThread extends React.Component {
render() {
const postsArray = this.state.postsArray;
const selected = this.state.selected;
const channel = ChannelStore.get(this.state.selected.channel_id);
let profiles = this.state.profiles || {};
if (channel && channel.type === Constants.DM_CHANNEL) {
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles());
}
const profiles = this.state.profiles || {};
if (postsArray == null || selected == null) {
return (

Some files were not shown because too many files have changed in this diff Show More