PLT-2241 Refactored statuses into a more real-time system (#3573)
* Refactored statuses into a more real-time system * Updated package.json with correct commit and fixed minor bug * Minor updates to statuses based on feedback * When setting status online, update only LastActivityAt if status already exists
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
180adc79af
Коммит
c0ab2636d6
2
Makefile
2
Makefile
@@ -159,7 +159,7 @@ test-server: start-docker prepare-enterprise
|
|||||||
rm -f cover.out
|
rm -f cover.out
|
||||||
echo "mode: count" > cover.out
|
echo "mode: count" > cover.out
|
||||||
|
|
||||||
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=340s -covermode=count -coverprofile=capi.out ./api || exit 1
|
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=440s -covermode=count -coverprofile=capi.out ./api || exit 1
|
||||||
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1
|
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1
|
||||||
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1
|
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1
|
||||||
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cutils.out ./utils || exit 1
|
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cutils.out ./utils || exit 1
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ func InitApi() {
|
|||||||
InitPreference()
|
InitPreference()
|
||||||
InitLicense()
|
InitLicense()
|
||||||
InitEmoji()
|
InitEmoji()
|
||||||
|
InitStatus()
|
||||||
|
|
||||||
// 404 on any api route before web.go has a chance to serve it
|
// 404 on any api route before web.go has a chance to serve it
|
||||||
Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))
|
Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
|
var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
|
||||||
|
var statusCache *utils.Cache = utils.NewLru(model.STATUS_CACHE_SIZE)
|
||||||
|
|
||||||
var allowedMethods []string = []string{
|
var allowedMethods []string = []string{
|
||||||
"POST",
|
"POST",
|
||||||
@@ -196,11 +197,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 {
|
if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 {
|
||||||
go func() {
|
SetStatusOnline(c.Session.UserId, c.Session.Id)
|
||||||
if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, c.Session.Id, model.GetMillis())).Err; err != nil {
|
|
||||||
l4g.Error(utils.T("api.context.last_activity_at.error"), c.Session.UserId, c.Session.Id, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Err == nil {
|
if c.Err == nil {
|
||||||
|
|||||||
@@ -73,7 +73,12 @@ func ping(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(model.MapToJson(m)))
|
w.Write([]byte(model.MapToJson(m)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func webSocketPing(req *model.WebSocketRequest, responseData map[string]interface{}) *model.AppError {
|
func webSocketPing(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
|
||||||
responseData["text"] = "pong"
|
data := map[string]interface{}{}
|
||||||
return nil
|
data["text"] = "pong"
|
||||||
|
data["version"] = model.CurrentVersion
|
||||||
|
data["server_time"] = model.GetMillis()
|
||||||
|
data["node_id"] = ""
|
||||||
|
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -597,7 +597,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
|
|||||||
for _, id := range mentionedUsersList {
|
for _, id := range mentionedUsersList {
|
||||||
userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"
|
userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"
|
||||||
|
|
||||||
if userAllowsEmails && (profileMap[id].IsAway() || profileMap[id].IsOffline()) {
|
var status *model.Status
|
||||||
|
var err *model.AppError
|
||||||
|
if status, err = GetStatus(id); err != nil {
|
||||||
|
status = &model.Status{id, model.STATUS_OFFLINE, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
if userAllowsEmails && status.Status != model.STATUS_ONLINE {
|
||||||
sendNotificationEmail(c, post, profileMap[id], channel, team, senderName)
|
sendNotificationEmail(c, post, profileMap[id], channel, team, senderName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
155
api/status.go
Обычный файл
155
api/status.go
Обычный файл
@@ -0,0 +1,155 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
l4g "github.com/alecthomas/log4go"
|
||||||
|
"github.com/mattermost/platform/model"
|
||||||
|
"github.com/mattermost/platform/store"
|
||||||
|
"github.com/mattermost/platform/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitStatus() {
|
||||||
|
l4g.Debug(utils.T("api.status.init.debug"))
|
||||||
|
|
||||||
|
BaseRoutes.Users.Handle("/status", ApiUserRequiredActivity(getStatusesHttp, false)).Methods("GET")
|
||||||
|
BaseRoutes.WebSocket.Handle("get_statuses", ApiWebSocketHandler(getStatusesWebSocket))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatusesHttp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
statusMap, err := GetAllStatuses()
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(model.StringInterfaceToJson(statusMap)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatusesWebSocket(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
|
||||||
|
statusMap, err := GetAllStatuses()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllStatuses() (map[string]interface{}, *model.AppError) {
|
||||||
|
if result := <-Srv.Store.Status().GetOnlineAway(); result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
} else {
|
||||||
|
statuses := result.Data.([]*model.Status)
|
||||||
|
|
||||||
|
statusMap := map[string]interface{}{}
|
||||||
|
for _, s := range statuses {
|
||||||
|
statusMap[s.UserId] = s.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusMap, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetStatusOnline(userId string, sessionId string) {
|
||||||
|
broadcast := false
|
||||||
|
saveStatus := false
|
||||||
|
|
||||||
|
var status *model.Status
|
||||||
|
var err *model.AppError
|
||||||
|
if status, err = GetStatus(userId); err != nil {
|
||||||
|
status = &model.Status{userId, model.STATUS_ONLINE, model.GetMillis()}
|
||||||
|
broadcast = true
|
||||||
|
saveStatus = true
|
||||||
|
} else {
|
||||||
|
if status.Status != model.STATUS_ONLINE {
|
||||||
|
broadcast = true
|
||||||
|
}
|
||||||
|
status.Status = model.STATUS_ONLINE
|
||||||
|
status.LastActivityAt = model.GetMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCache.Add(status.UserId, status)
|
||||||
|
|
||||||
|
achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, model.GetMillis())
|
||||||
|
|
||||||
|
var schan store.StoreChannel
|
||||||
|
if saveStatus {
|
||||||
|
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 := <-schan; result.Err != nil {
|
||||||
|
l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if broadcast {
|
||||||
|
event := model.NewWebSocketEvent("", "", status.UserId, model.WEBSOCKET_EVENT_STATUS_CHANGE)
|
||||||
|
event.Add("status", model.STATUS_ONLINE)
|
||||||
|
go Publish(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetStatusOffline(userId string) {
|
||||||
|
status := &model.Status{userId, model.STATUS_OFFLINE, model.GetMillis()}
|
||||||
|
|
||||||
|
statusCache.Add(status.UserId, status)
|
||||||
|
|
||||||
|
if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
|
||||||
|
l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
event := model.NewWebSocketEvent("", "", status.UserId, model.WEBSOCKET_EVENT_STATUS_CHANGE)
|
||||||
|
event.Add("status", model.STATUS_OFFLINE)
|
||||||
|
go Publish(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetStatusAwayIfNeeded(userId string) {
|
||||||
|
status, err := GetStatus(userId)
|
||||||
|
if err != nil {
|
||||||
|
status = &model.Status{userId, model.STATUS_OFFLINE, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Status == model.STATUS_AWAY {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsUserAway(status.LastActivityAt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status.Status = model.STATUS_AWAY
|
||||||
|
|
||||||
|
statusCache.Add(status.UserId, status)
|
||||||
|
|
||||||
|
if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
|
||||||
|
l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
event := model.NewWebSocketEvent("", "", status.UserId, model.WEBSOCKET_EVENT_STATUS_CHANGE)
|
||||||
|
event.Add("status", model.STATUS_AWAY)
|
||||||
|
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 := <-Srv.Store.Status().Get(userId); result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
} else {
|
||||||
|
return result.Data.(*model.Status), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsUserAway(lastActivityAt int64) bool {
|
||||||
|
return model.GetMillis()-lastActivityAt >= *utils.Cfg.TeamSettings.UserStatusAwayTimeout*1000
|
||||||
|
}
|
||||||
153
api/status_test.go
Обычный файл
153
api/status_test.go
Обычный файл
@@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mattermost/platform/model"
|
||||||
|
"github.com/mattermost/platform/store"
|
||||||
|
"github.com/mattermost/platform/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatuses(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
Client := th.BasicClient
|
||||||
|
WebSocketClient, err := th.CreateWebSocketClient()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer WebSocketClient.Close()
|
||||||
|
WebSocketClient.Listen()
|
||||||
|
|
||||||
|
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||||
|
rteam, _ := Client.CreateTeam(&team)
|
||||||
|
|
||||||
|
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
|
||||||
|
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
|
||||||
|
LinkUserToTeam(ruser, rteam.Data.(*model.Team))
|
||||||
|
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
|
||||||
|
|
||||||
|
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
|
||||||
|
ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User)
|
||||||
|
LinkUserToTeam(ruser2, rteam.Data.(*model.Team))
|
||||||
|
store.Must(Srv.Store.User().VerifyEmail(ruser2.Id))
|
||||||
|
|
||||||
|
Client.Login(user.Email, user.Password)
|
||||||
|
Client.SetTeamId(team.Id)
|
||||||
|
|
||||||
|
r1, err := Client.GetStatuses()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses := r1.Data.(map[string]string)
|
||||||
|
|
||||||
|
for _, status := range statuses {
|
||||||
|
if status != model.STATUS_OFFLINE && status != model.STATUS_AWAY && status != model.STATUS_ONLINE {
|
||||||
|
t.Fatal("one of the statuses had an invalid value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th.LoginBasic2()
|
||||||
|
|
||||||
|
WebSocketClient2, err2 := th.CreateWebSocketClient()
|
||||||
|
if err2 != nil {
|
||||||
|
t.Fatal(err2)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
|
||||||
|
WebSocketClient.GetStatuses()
|
||||||
|
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.Fatal("should have had user status")
|
||||||
|
} else if status != model.STATUS_ONLINE {
|
||||||
|
t.Log(status)
|
||||||
|
t.Fatal("status should have been online")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStatusAwayIfNeeded(th.BasicUser2.Id)
|
||||||
|
|
||||||
|
awayTimeout := *utils.Cfg.TeamSettings.UserStatusAwayTimeout
|
||||||
|
defer func() {
|
||||||
|
*utils.Cfg.TeamSettings.UserStatusAwayTimeout = awayTimeout
|
||||||
|
}()
|
||||||
|
*utils.Cfg.TeamSettings.UserStatusAwayTimeout = 1
|
||||||
|
|
||||||
|
time.Sleep(1500 * time.Millisecond)
|
||||||
|
|
||||||
|
SetStatusAwayIfNeeded(th.BasicUser2.Id)
|
||||||
|
SetStatusAwayIfNeeded(th.BasicUser2.Id)
|
||||||
|
|
||||||
|
WebSocketClient2.Close()
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
|
||||||
|
WebSocketClient.GetStatuses()
|
||||||
|
if resp := <-WebSocketClient.ResponseChannel; resp.Error != nil {
|
||||||
|
t.Fatal(resp.Error)
|
||||||
|
} else {
|
||||||
|
if resp.SeqReply != WebSocketClient.Sequence-1 {
|
||||||
|
t.Fatal("bad sequence number")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := resp.Data[th.BasicUser2.Id]; ok {
|
||||||
|
t.Fatal("should not have had user status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.UserId == th.BasicUser2.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:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
stop <- true
|
||||||
|
|
||||||
|
if !onlineHit {
|
||||||
|
t.Fatal("didn't get online event")
|
||||||
|
}
|
||||||
|
if !awayHit {
|
||||||
|
t.Fatal("didn't get away event")
|
||||||
|
}
|
||||||
|
if !offlineHit {
|
||||||
|
t.Fatal("didn't get offline event")
|
||||||
|
}
|
||||||
|
}
|
||||||
38
api/user.go
38
api/user.go
@@ -54,7 +54,6 @@ func InitUser() {
|
|||||||
BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
|
BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
|
||||||
BaseRoutes.Users.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
|
BaseRoutes.Users.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
|
||||||
BaseRoutes.Users.Handle("/initial_load", ApiAppHandler(getInitialLoad)).Methods("GET")
|
BaseRoutes.Users.Handle("/initial_load", ApiAppHandler(getInitialLoad)).Methods("GET")
|
||||||
BaseRoutes.Users.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST")
|
|
||||||
BaseRoutes.Users.Handle("/direct_profiles", ApiUserRequired(getDirectProfiles)).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/{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("/profiles_for_dm_list/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfilesForDirectMessageList)).Methods("GET")
|
||||||
@@ -1955,35 +1954,6 @@ func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
userIds := model.ArrayFromJson(r.Body)
|
|
||||||
if len(userIds) == 0 {
|
|
||||||
c.SetInvalidParam("getStatuses", "userIds")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if result := <-Srv.Store.User().GetProfileByIds(userIds); result.Err != nil {
|
|
||||||
c.Err = result.Err
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
profiles := result.Data.(map[string]*model.User)
|
|
||||||
|
|
||||||
statuses := map[string]string{}
|
|
||||||
for _, profile := range profiles {
|
|
||||||
if profile.IsOffline() {
|
|
||||||
statuses[profile.Id] = model.USER_OFFLINE
|
|
||||||
} else if profile.IsAway() {
|
|
||||||
statuses[profile.Id] = model.USER_AWAY
|
|
||||||
} else {
|
|
||||||
statuses[profile.Id] = model.USER_ONLINE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(model.MapToJson(statuses)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsUsernameTaken(name string) bool {
|
func IsUsernameTaken(name string) bool {
|
||||||
|
|
||||||
if !model.IsValidUsername(name) {
|
if !model.IsValidUsername(name) {
|
||||||
@@ -2312,7 +2282,7 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.Err = error
|
c.Err = error
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
if user.LastActivityAt > 0 {
|
if _, err := GetStatus(user.Id); err != nil {
|
||||||
go SendEmailChangeVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
|
go SendEmailChangeVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
|
||||||
} else {
|
} else {
|
||||||
go SendVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
|
go SendVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
|
||||||
@@ -2551,11 +2521,11 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func userTyping(req *model.WebSocketRequest, responseData map[string]interface{}) *model.AppError {
|
func userTyping(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
|
||||||
var ok bool
|
var ok bool
|
||||||
var channelId string
|
var channelId string
|
||||||
if channelId, ok = req.Data["channel_id"].(string); !ok || len(channelId) != 26 {
|
if channelId, ok = req.Data["channel_id"].(string); !ok || len(channelId) != 26 {
|
||||||
return NewInvalidWebSocketParamError(req.Action, "channel_id")
|
return nil, NewInvalidWebSocketParamError(req.Action, "channel_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
var parentId string
|
var parentId string
|
||||||
@@ -2567,5 +2537,5 @@ func userTyping(req *model.WebSocketRequest, responseData map[string]interface{}
|
|||||||
event.Add("parent_id", parentId)
|
event.Add("parent_id", parentId)
|
||||||
go Publish(event)
|
go Publish(event)
|
||||||
|
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -669,9 +669,7 @@ func TestUserUpdate(t *testing.T) {
|
|||||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||||
|
|
||||||
time1 := model.GetMillis()
|
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", Roles: ""}
|
||||||
|
|
||||||
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", LastActivityAt: time1, LastPingAt: time1, Roles: ""}
|
|
||||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||||
LinkUserToTeam(user, team)
|
LinkUserToTeam(user, team)
|
||||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||||
@@ -683,15 +681,7 @@ func TestUserUpdate(t *testing.T) {
|
|||||||
Client.Login(user.Email, "passwd1")
|
Client.Login(user.Email, "passwd1")
|
||||||
Client.SetTeamId(team.Id)
|
Client.SetTeamId(team.Id)
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
time2 := model.GetMillis()
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
user.Nickname = "Jim Jimmy"
|
user.Nickname = "Jim Jimmy"
|
||||||
user.LastActivityAt = time2
|
|
||||||
user.LastPingAt = time2
|
|
||||||
user.Roles = model.ROLE_TEAM_ADMIN
|
user.Roles = model.ROLE_TEAM_ADMIN
|
||||||
user.LastPasswordUpdate = 123
|
user.LastPasswordUpdate = 123
|
||||||
|
|
||||||
@@ -701,12 +691,6 @@ func TestUserUpdate(t *testing.T) {
|
|||||||
if result.Data.(*model.User).Nickname != "Jim Jimmy" {
|
if result.Data.(*model.User).Nickname != "Jim Jimmy" {
|
||||||
t.Fatal("Nickname did not update properly")
|
t.Fatal("Nickname did not update properly")
|
||||||
}
|
}
|
||||||
if result.Data.(*model.User).LastActivityAt == time2 {
|
|
||||||
t.Fatal("LastActivityAt should not have updated")
|
|
||||||
}
|
|
||||||
if result.Data.(*model.User).LastPingAt == time2 {
|
|
||||||
t.Fatal("LastPingAt should not have updated")
|
|
||||||
}
|
|
||||||
if result.Data.(*model.User).Roles != "" {
|
if result.Data.(*model.User).Roles != "" {
|
||||||
t.Fatal("Roles should not have updated")
|
t.Fatal("Roles should not have updated")
|
||||||
}
|
}
|
||||||
@@ -1347,48 +1331,6 @@ func TestFuzzyUserCreate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatuses(t *testing.T) {
|
|
||||||
th := Setup()
|
|
||||||
Client := th.CreateClient()
|
|
||||||
|
|
||||||
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
|
||||||
rteam, _ := Client.CreateTeam(&team)
|
|
||||||
|
|
||||||
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
|
|
||||||
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
|
|
||||||
LinkUserToTeam(ruser, rteam.Data.(*model.Team))
|
|
||||||
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
|
|
||||||
|
|
||||||
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
|
|
||||||
ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User)
|
|
||||||
LinkUserToTeam(ruser2, rteam.Data.(*model.Team))
|
|
||||||
store.Must(Srv.Store.User().VerifyEmail(ruser2.Id))
|
|
||||||
|
|
||||||
Client.Login(user.Email, user.Password)
|
|
||||||
Client.SetTeamId(team.Id)
|
|
||||||
|
|
||||||
userIds := []string{ruser2.Id}
|
|
||||||
|
|
||||||
r1, err := Client.GetStatuses(userIds)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
statuses := r1.Data.(map[string]string)
|
|
||||||
|
|
||||||
if len(statuses) != 1 {
|
|
||||||
t.Log(statuses)
|
|
||||||
t.Fatal("invalid number of statuses")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, status := range statuses {
|
|
||||||
if status != model.USER_OFFLINE && status != model.USER_AWAY && status != model.USER_ONLINE {
|
|
||||||
t.Fatal("one of the statuses had an invalid value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmailToOAuth(t *testing.T) {
|
func TestEmailToOAuth(t *testing.T) {
|
||||||
th := Setup()
|
th := Setup()
|
||||||
Client := th.CreateClient()
|
Client := th.CreateClient()
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/platform/model"
|
"github.com/mattermost/platform/model"
|
||||||
"github.com/mattermost/platform/utils"
|
|
||||||
|
|
||||||
l4g "github.com/alecthomas/log4go"
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
goi18n "github.com/nicksnyder/go-i18n/i18n"
|
goi18n "github.com/nicksnyder/go-i18n/i18n"
|
||||||
)
|
)
|
||||||
@@ -34,18 +32,7 @@ type WebConn struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewWebConn(c *Context, ws *websocket.Conn) *WebConn {
|
func NewWebConn(c *Context, ws *websocket.Conn) *WebConn {
|
||||||
go func() {
|
go SetStatusOnline(c.Session.UserId, c.Session.Id)
|
||||||
achan := Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, c.Session.Token, model.GetMillis())
|
|
||||||
pchan := Srv.Store.User().UpdateLastPingAt(c.Session.UserId, model.GetMillis())
|
|
||||||
|
|
||||||
if result := <-achan; result.Err != nil {
|
|
||||||
l4g.Error(utils.T("api.web_conn.new_web_conn.last_activity.error"), c.Session.UserId, c.Session.Token, result.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result := <-pchan; result.Err != nil {
|
|
||||||
l4g.Error(utils.T("api.web_conn.new_web_conn.last_ping.error"), c.Session.UserId, result.Err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return &WebConn{
|
return &WebConn{
|
||||||
Send: make(chan model.WebSocketMessage, 64),
|
Send: make(chan model.WebSocketMessage, 64),
|
||||||
@@ -68,13 +55,7 @@ func (c *WebConn) readPump() {
|
|||||||
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
|
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
|
||||||
c.WebSocket.SetPongHandler(func(string) error {
|
c.WebSocket.SetPongHandler(func(string) error {
|
||||||
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
|
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
|
||||||
|
go SetStatusAwayIfNeeded(c.UserId)
|
||||||
go func() {
|
|
||||||
if result := <-Srv.Store.User().UpdateLastPingAt(c.UserId, model.GetMillis()); result.Err != nil {
|
|
||||||
l4g.Error(utils.T("api.web_conn.new_web_conn.last_ping.error"), c.UserId, result.Err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -67,10 +67,23 @@ func (h *Hub) Start() {
|
|||||||
h.connections[webCon] = true
|
h.connections[webCon] = true
|
||||||
|
|
||||||
case webCon := <-h.unregister:
|
case webCon := <-h.unregister:
|
||||||
|
userId := webCon.UserId
|
||||||
if _, ok := h.connections[webCon]; ok {
|
if _, ok := h.connections[webCon]; ok {
|
||||||
delete(h.connections, webCon)
|
delete(h.connections, webCon)
|
||||||
close(webCon.Send)
|
close(webCon.Send)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for webCon := range h.connections {
|
||||||
|
if userId == webCon.UserId {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
go SetStatusOffline(userId)
|
||||||
|
}
|
||||||
case userId := <-h.invalidateUser:
|
case userId := <-h.invalidateUser:
|
||||||
for webCon := range h.connections {
|
for webCon := range h.connections {
|
||||||
if webCon.UserId == userId {
|
if webCon.UserId == userId {
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import (
|
|||||||
"github.com/mattermost/platform/utils"
|
"github.com/mattermost/platform/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ApiWebSocketHandler(wh func(*model.WebSocketRequest, map[string]interface{}) *model.AppError) *webSocketHandler {
|
func ApiWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)) *webSocketHandler {
|
||||||
return &webSocketHandler{wh}
|
return &webSocketHandler{wh}
|
||||||
}
|
}
|
||||||
|
|
||||||
type webSocketHandler struct {
|
type webSocketHandler struct {
|
||||||
handlerFunc func(*model.WebSocketRequest, map[string]interface{}) *model.AppError
|
handlerFunc func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) {
|
func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) {
|
||||||
@@ -25,9 +25,10 @@ func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequ
|
|||||||
r.T = conn.T
|
r.T = conn.T
|
||||||
r.Locale = conn.Locale
|
r.Locale = conn.Locale
|
||||||
|
|
||||||
data := make(map[string]interface{})
|
var data map[string]interface{}
|
||||||
|
var err *model.AppError
|
||||||
|
|
||||||
if err := wh.handlerFunc(r, data); err != nil {
|
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)
|
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 = ""
|
err.DetailedError = ""
|
||||||
conn.Send <- model.NewWebSocketError(r.Seq, err)
|
conn.Send <- model.NewWebSocketError(r.Seq, err)
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
"RestrictDirectMessage": "any",
|
"RestrictDirectMessage": "any",
|
||||||
"RestrictTeamInvite": "all",
|
"RestrictTeamInvite": "all",
|
||||||
"RestrictPublicChannelManagement": "all",
|
"RestrictPublicChannelManagement": "all",
|
||||||
"RestrictPrivateChannelManagement": "all"
|
"RestrictPrivateChannelManagement": "all",
|
||||||
|
"UserStatusAwayTimeout": 300
|
||||||
},
|
},
|
||||||
"SqlSettings": {
|
"SqlSettings": {
|
||||||
"DriverName": "mysql",
|
"DriverName": "mysql",
|
||||||
|
|||||||
34
i18n/en.json
34
i18n/en.json
@@ -1980,12 +1980,12 @@
|
|||||||
"translation": "Bad verify email link."
|
"translation": "Bad verify email link."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.web_conn.new_web_conn.last_activity.error",
|
"id": "api.status.last_activity.error",
|
||||||
"translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v"
|
"translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.web_conn.new_web_conn.last_ping.error",
|
"id": "api.status.save_status.error",
|
||||||
"translation": "Failed to update LastPingAt for user_id=%v, err=%v"
|
"translation": "Failed to save status for user_id=%v, err=%v"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api.web_hub.start.stopping.debug",
|
"id": "api.web_hub.start.stopping.debug",
|
||||||
@@ -4036,8 +4036,32 @@
|
|||||||
"translation": "We encountered an error while finding user profiles"
|
"translation": "We encountered an error while finding user profiles"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_user.get_total_active_users_count.app_error",
|
"id": "store.sql_status.get_total_active_users_count.app_error",
|
||||||
"translation": "We could not count the users"
|
"translation": "We could not count the active users"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_status.reset_all.app_error",
|
||||||
|
"translation": "Encountered an error resetting all the statuses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_status.get_online_away.app_error",
|
||||||
|
"translation": "Encountered an error retrieving all the online/away statuses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_status.get.app_error",
|
||||||
|
"translation": "Encountered an error retrieving the status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_status.get.missing.app_error",
|
||||||
|
"translation": "No entry for that status exists"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_status.update.app_error",
|
||||||
|
"translation": "Encountered an error updating the status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_status.save.app_error",
|
||||||
|
"translation": "Encountered an error saving the status"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_user.get_total_users_count.app_error",
|
"id": "store.sql_user.get_total_users_count.app_error",
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ func main() {
|
|||||||
if flagRunCmds {
|
if flagRunCmds {
|
||||||
runCmds()
|
runCmds()
|
||||||
} else {
|
} else {
|
||||||
|
resetStatuses()
|
||||||
|
|
||||||
api.StartServer()
|
api.StartServer()
|
||||||
|
|
||||||
// If we allow testing then listen for manual testing URL hits
|
// If we allow testing then listen for manual testing URL hits
|
||||||
@@ -149,6 +151,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resetStatuses() {
|
||||||
|
if result := <-api.Srv.Store.Status().ResetAll(); result.Err != nil {
|
||||||
|
l4g.Error(utils.T("mattermost.reset_status.error"), result.Err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setDiagnosticId() {
|
func setDiagnosticId() {
|
||||||
if result := <-api.Srv.Store.System().Get(); result.Err == nil {
|
if result := <-api.Srv.Store.System().Get(); result.Err == nil {
|
||||||
props := result.Data.(model.StringMap)
|
props := result.Data.(model.StringMap)
|
||||||
@@ -200,7 +208,7 @@ func doSecurityAndDiagnostics() {
|
|||||||
v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
|
v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ucr := <-api.Srv.Store.User().GetTotalActiveUsersCount(); ucr.Err == nil {
|
if ucr := <-api.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil {
|
||||||
v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
|
v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1415,8 +1415,9 @@ func (c *Client) AdminResetPassword(userId, newPassword string) (*Result, *AppEr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetStatuses(data []string) (*Result, *AppError) {
|
// GetStatuses returns a map of string statuses using user id as the key
|
||||||
if r, err := c.DoApiPost("/users/status", ArrayToJson(data)); err != nil {
|
func (c *Client) GetStatuses() (*Result, *AppError) {
|
||||||
|
if r, err := c.DoApiGet("/users/status", "", ""); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
defer closeBody(r)
|
defer closeBody(r)
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ type TeamSettings struct {
|
|||||||
RestrictTeamInvite *string
|
RestrictTeamInvite *string
|
||||||
RestrictPublicChannelManagement *string
|
RestrictPublicChannelManagement *string
|
||||||
RestrictPrivateChannelManagement *string
|
RestrictPrivateChannelManagement *string
|
||||||
|
UserStatusAwayTimeout *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type LdapSettings struct {
|
type LdapSettings struct {
|
||||||
@@ -435,6 +436,11 @@ func (o *Config) SetDefaults() {
|
|||||||
*o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL
|
*o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.TeamSettings.UserStatusAwayTimeout == nil {
|
||||||
|
o.TeamSettings.UserStatusAwayTimeout = new(int64)
|
||||||
|
*o.TeamSettings.UserStatusAwayTimeout = 300
|
||||||
|
}
|
||||||
|
|
||||||
if o.EmailSettings.EnableSignInWithEmail == nil {
|
if o.EmailSettings.EnableSignInWithEmail == nil {
|
||||||
o.EmailSettings.EnableSignInWithEmail = new(bool)
|
o.EmailSettings.EnableSignInWithEmail = new(bool)
|
||||||
|
|
||||||
|
|||||||
42
model/status.go
Обычный файл
42
model/status.go
Обычный файл
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
STATUS_OFFLINE = "offline"
|
||||||
|
STATUS_AWAY = "away"
|
||||||
|
STATUS_ONLINE = "online"
|
||||||
|
STATUS_CACHE_SIZE = 10000
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
LastActivityAt int64 `json:"last_activity_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Status) ToJson() string {
|
||||||
|
b, err := json.Marshal(o)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StatusFromJson(data io.Reader) *Status {
|
||||||
|
decoder := json.NewDecoder(data)
|
||||||
|
var o Status
|
||||||
|
err := decoder.Decode(&o)
|
||||||
|
if err == nil {
|
||||||
|
return &o
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
27
model/status_test.go
Обычный файл
27
model/status_test.go
Обычный файл
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatus(t *testing.T) {
|
||||||
|
status := Status{NewId(), STATUS_ONLINE, 0}
|
||||||
|
json := status.ToJson()
|
||||||
|
status2 := StatusFromJson(strings.NewReader(json))
|
||||||
|
|
||||||
|
if status.UserId != status2.UserId {
|
||||||
|
t.Fatal("UserId should have matched")
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Status != status2.Status {
|
||||||
|
t.Fatal("Status should have matched")
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.LastActivityAt != status2.LastActivityAt {
|
||||||
|
t.Fatal("LastActivityAt should have matched")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,6 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
ROLE_SYSTEM_ADMIN = "system_admin"
|
ROLE_SYSTEM_ADMIN = "system_admin"
|
||||||
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
|
|
||||||
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
|
|
||||||
USER_OFFLINE = "offline"
|
|
||||||
USER_AWAY = "away"
|
|
||||||
USER_ONLINE = "online"
|
|
||||||
USER_NOTIFY_ALL = "all"
|
USER_NOTIFY_ALL = "all"
|
||||||
USER_NOTIFY_MENTION = "mention"
|
USER_NOTIFY_MENTION = "mention"
|
||||||
USER_NOTIFY_NONE = "none"
|
USER_NOTIFY_NONE = "none"
|
||||||
@@ -44,8 +39,6 @@ type User struct {
|
|||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"`
|
||||||
LastName string `json:"last_name"`
|
LastName string `json:"last_name"`
|
||||||
Roles string `json:"roles"`
|
Roles string `json:"roles"`
|
||||||
LastActivityAt int64 `json:"last_activity_at,omitempty"`
|
|
||||||
LastPingAt int64 `json:"last_ping_at,omitempty"`
|
|
||||||
AllowMarketing bool `json:"allow_marketing,omitempty"`
|
AllowMarketing bool `json:"allow_marketing,omitempty"`
|
||||||
Props StringMap `json:"props,omitempty"`
|
Props StringMap `json:"props,omitempty"`
|
||||||
NotifyProps StringMap `json:"notify_props,omitempty"`
|
NotifyProps StringMap `json:"notify_props,omitempty"`
|
||||||
@@ -222,14 +215,6 @@ func (u *User) Etag(showFullName, showEmail bool) string {
|
|||||||
return Etag(u.Id, u.UpdateAt, showFullName, showEmail)
|
return Etag(u.Id, u.UpdateAt, showFullName, showEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) IsOffline() bool {
|
|
||||||
return (GetMillis()-u.LastPingAt) > USER_OFFLINE_TIMEOUT && (GetMillis()-u.LastActivityAt) > USER_OFFLINE_TIMEOUT
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) IsAway() bool {
|
|
||||||
return (GetMillis() - u.LastActivityAt) > USER_AWAY_TIMEOUT
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any private data from the user object
|
// Remove any private data from the user object
|
||||||
func (u *User) Sanitize(options map[string]bool) {
|
func (u *User) Sanitize(options map[string]bool) {
|
||||||
u.Password = ""
|
u.Password = ""
|
||||||
@@ -258,7 +243,6 @@ func (u *User) ClearNonProfileFields() {
|
|||||||
u.MfaActive = false
|
u.MfaActive = false
|
||||||
u.MfaSecret = ""
|
u.MfaSecret = ""
|
||||||
u.EmailVerified = false
|
u.EmailVerified = false
|
||||||
u.LastPingAt = 0
|
|
||||||
u.AllowMarketing = false
|
u.AllowMarketing = false
|
||||||
u.Props = StringMap{}
|
u.Props = StringMap{}
|
||||||
u.NotifyProps = StringMap{}
|
u.NotifyProps = StringMap{}
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface
|
|||||||
wsc.Conn.WriteJSON(req)
|
wsc.Conn.WriteJSON(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserTyping will push a user_typing event out to all connected users
|
||||||
|
// who are in the specified channel
|
||||||
func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
|
func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"channel_id": channelId,
|
"channel_id": channelId,
|
||||||
@@ -100,3 +102,8 @@ func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
|
|||||||
|
|
||||||
wsc.SendMessage("user_typing", data)
|
wsc.SendMessage("user_typing", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetStatuses will return a map of string statuses using user id as the key
|
||||||
|
func (wsc *WebSocketClient) GetStatuses() {
|
||||||
|
wsc.SendMessage("get_statuses", nil)
|
||||||
|
}
|
||||||
|
|||||||
166
store/sql_status_store.go
Обычный файл
166
store/sql_status_store.go
Обычный файл
@@ -0,0 +1,166 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"github.com/mattermost/platform/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MISSING_STATUS_ERROR = "store.sql_status.get.missing.app_error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqlStatusStore struct {
|
||||||
|
*SqlStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlStatusStore(sqlStore *SqlStore) StatusStore {
|
||||||
|
s := &SqlStatusStore{sqlStore}
|
||||||
|
|
||||||
|
for _, db := range sqlStore.GetAllConns() {
|
||||||
|
table := db.AddTableWithName(model.Status{}, "Status").SetKeys(false, "UserId")
|
||||||
|
table.ColMap("UserId").SetMaxSize(26)
|
||||||
|
table.ColMap("Status").SetMaxSize(32)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlStatusStore) UpgradeSchemaIfNeeded() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlStatusStore) CreateIndexesIfNotExists() {
|
||||||
|
s.CreateIndexIfNotExists("idx_status_user_id", "Status", "UserId")
|
||||||
|
s.CreateIndexIfNotExists("idx_status_status", "Status", "Status")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlStatusStore) SaveOrUpdate(status *model.Status) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
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, "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := s.GetMaster().Insert(status); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlStatusStore.SaveOrUpdate", "store.sql_status.save.app_error", nil, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlStatusStore) Get(userId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
var status model.Status
|
||||||
|
|
||||||
|
if err := s.GetReplica().SelectOne(&status,
|
||||||
|
`SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
Status
|
||||||
|
WHERE
|
||||||
|
UserId = :UserId`, map[string]interface{}{"UserId": userId}); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
result.Err = model.NewLocAppError("SqlStatusStore.Get", MISSING_STATUS_ERROR, nil, err.Error())
|
||||||
|
} else {
|
||||||
|
result.Err = model.NewLocAppError("SqlStatusStore.Get", "store.sql_status.get.app_error", nil, err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.Data = &status
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlStatusStore) GetOnlineAway() StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
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 {
|
||||||
|
result.Err = model.NewLocAppError("SqlStatusStore.GetOnline", "store.sql_status.get_online_away.app_error", nil, err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = statuses
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlStatusStore) ResetAll() StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
if _, err := s.GetMaster().Exec("UPDATE Status SET Status = :Status", map[string]interface{}{"Status": model.STATUS_OFFLINE}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlStatusStore.ResetAll", "store.sql_status.reset_all.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlStatusStore) GetTotalActiveUsersCount() StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
time := model.GetMillis() - (1000 * 60 * 60 * 24)
|
||||||
|
|
||||||
|
if count, err := s.GetReplica().SelectInt("SELECT COUNT(UserId) FROM Status WHERE LastActivityAt > :Time", map[string]interface{}{"Time": time}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlStatusStore.GetTotalActiveUsersCount", "store.sql_status.get_total_active_users_count.app_error", nil, err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = count
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlStatusStore) UpdateLastActivityAt(userId string, lastActivityAt int64) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
if _, err := s.GetMaster().Exec("UPDATE Status SET LastActivityAt = :Time WHERE UserId = :UserId", map[string]interface{}{"UserId": userId, "Time": lastActivityAt}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlStatusStore.UpdateLastActivityAt", "store.sql_status.update_last_activity_at.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
83
store/sql_status_store_test.go
Обычный файл
83
store/sql_status_store_test.go
Обычный файл
@@ -0,0 +1,83 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mattermost/platform/model"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSqlStatusStore(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
status := &model.Status{model.NewId(), model.STATUS_ONLINE, 0}
|
||||||
|
|
||||||
|
if err := (<-store.Status().SaveOrUpdate(status)).Err; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status.LastActivityAt = 10
|
||||||
|
|
||||||
|
if err := (<-store.Status().SaveOrUpdate(status)).Err; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := (<-store.Status().Get(status.UserId)).Err; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status2 := &model.Status{model.NewId(), model.STATUS_AWAY, 0}
|
||||||
|
if err := (<-store.Status().SaveOrUpdate(status2)).Err; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status3 := &model.Status{model.NewId(), model.STATUS_OFFLINE, 0}
|
||||||
|
if err := (<-store.Status().SaveOrUpdate(status3)).Err; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := <-store.Status().GetOnlineAway(); result.Err != nil {
|
||||||
|
t.Fatal(result.Err)
|
||||||
|
} else {
|
||||||
|
statuses := result.Data.([]*model.Status)
|
||||||
|
for _, status := range statuses {
|
||||||
|
if status.Status == model.STATUS_OFFLINE {
|
||||||
|
t.Fatal("should not have returned offline statuses")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := (<-store.Status().ResetAll()).Err; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := <-store.Status().Get(status.UserId); result.Err != nil {
|
||||||
|
t.Fatal(result.Err)
|
||||||
|
} else {
|
||||||
|
status := result.Data.(*model.Status)
|
||||||
|
if status.Status != model.STATUS_OFFLINE {
|
||||||
|
t.Fatal("should be offline")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := <-store.Status().UpdateLastActivityAt(status.UserId, 10); result.Err != nil {
|
||||||
|
t.Fatal(result.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActiveUserCount(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
status := &model.Status{model.NewId(), model.STATUS_ONLINE, model.GetMillis()}
|
||||||
|
Must(store.Status().SaveOrUpdate(status))
|
||||||
|
|
||||||
|
if result := <-store.Status().GetTotalActiveUsersCount(); result.Err != nil {
|
||||||
|
t.Fatal(result.Err)
|
||||||
|
} else {
|
||||||
|
count := result.Data.(int64)
|
||||||
|
if count <= 0 {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@ type SqlStore struct {
|
|||||||
license LicenseStore
|
license LicenseStore
|
||||||
recovery PasswordRecoveryStore
|
recovery PasswordRecoveryStore
|
||||||
emoji EmojiStore
|
emoji EmojiStore
|
||||||
|
status StatusStore
|
||||||
SchemaVersion string
|
SchemaVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +130,7 @@ func NewSqlStore() Store {
|
|||||||
sqlStore.license = NewSqlLicenseStore(sqlStore)
|
sqlStore.license = NewSqlLicenseStore(sqlStore)
|
||||||
sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore)
|
sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore)
|
||||||
sqlStore.emoji = NewSqlEmojiStore(sqlStore)
|
sqlStore.emoji = NewSqlEmojiStore(sqlStore)
|
||||||
|
sqlStore.status = NewSqlStatusStore(sqlStore)
|
||||||
|
|
||||||
err := sqlStore.master.CreateTablesIfNotExists()
|
err := sqlStore.master.CreateTablesIfNotExists()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -152,6 +154,7 @@ func NewSqlStore() Store {
|
|||||||
sqlStore.license.(*SqlLicenseStore).UpgradeSchemaIfNeeded()
|
sqlStore.license.(*SqlLicenseStore).UpgradeSchemaIfNeeded()
|
||||||
sqlStore.recovery.(*SqlPasswordRecoveryStore).UpgradeSchemaIfNeeded()
|
sqlStore.recovery.(*SqlPasswordRecoveryStore).UpgradeSchemaIfNeeded()
|
||||||
sqlStore.emoji.(*SqlEmojiStore).UpgradeSchemaIfNeeded()
|
sqlStore.emoji.(*SqlEmojiStore).UpgradeSchemaIfNeeded()
|
||||||
|
sqlStore.status.(*SqlStatusStore).UpgradeSchemaIfNeeded()
|
||||||
|
|
||||||
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
|
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
|
||||||
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
|
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
|
||||||
@@ -168,6 +171,7 @@ func NewSqlStore() Store {
|
|||||||
sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists()
|
sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists()
|
||||||
sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists()
|
sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists()
|
||||||
sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists()
|
sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists()
|
||||||
|
sqlStore.status.(*SqlStatusStore).CreateIndexesIfNotExists()
|
||||||
|
|
||||||
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
|
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
|
||||||
|
|
||||||
@@ -696,6 +700,10 @@ func (ss SqlStore) Emoji() EmojiStore {
|
|||||||
return ss.emoji
|
return ss.emoji
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ss SqlStore) Status() StatusStore {
|
||||||
|
return ss.status
|
||||||
|
}
|
||||||
|
|
||||||
func (ss SqlStore) DropAllTables() {
|
func (ss SqlStore) DropAllTables() {
|
||||||
ss.master.TruncateTables()
|
ss.master.TruncateTables()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() {
|
|||||||
us.Preference().Save(&data)
|
us.Preference().Save(&data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADDED for 3.3 remove for 3.7
|
||||||
|
us.RemoveColumnIfExists("Users", "LastActivityAt")
|
||||||
|
us.RemoveColumnIfExists("Users", "LastPingAt")
|
||||||
}
|
}
|
||||||
|
|
||||||
func themeMigrationFailed(err error) {
|
func themeMigrationFailed(err error) {
|
||||||
@@ -187,8 +191,6 @@ func (us SqlUserStore) Update(user *model.User, trustedUpdateData bool) StoreCha
|
|||||||
user.Password = oldUser.Password
|
user.Password = oldUser.Password
|
||||||
user.LastPasswordUpdate = oldUser.LastPasswordUpdate
|
user.LastPasswordUpdate = oldUser.LastPasswordUpdate
|
||||||
user.LastPictureUpdate = oldUser.LastPictureUpdate
|
user.LastPictureUpdate = oldUser.LastPictureUpdate
|
||||||
user.LastActivityAt = oldUser.LastActivityAt
|
|
||||||
user.LastPingAt = oldUser.LastPingAt
|
|
||||||
user.EmailVerified = oldUser.EmailVerified
|
user.EmailVerified = oldUser.EmailVerified
|
||||||
user.FailedAttempts = oldUser.FailedAttempts
|
user.FailedAttempts = oldUser.FailedAttempts
|
||||||
user.MfaSecret = oldUser.MfaSecret
|
user.MfaSecret = oldUser.MfaSecret
|
||||||
@@ -283,65 +285,6 @@ func (us SqlUserStore) UpdateUpdateAt(userId string) StoreChannel {
|
|||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (us SqlUserStore) UpdateLastPingAt(userId string, time int64) StoreChannel {
|
|
||||||
storeChannel := make(StoreChannel)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
result := StoreResult{}
|
|
||||||
|
|
||||||
if _, err := us.GetMaster().Exec("UPDATE Users SET LastPingAt = :LastPingAt WHERE Id = :UserId", map[string]interface{}{"LastPingAt": time, "UserId": userId}); err != nil {
|
|
||||||
result.Err = model.NewLocAppError("SqlUserStore.UpdateLastPingAt", "store.sql_user.update_last_ping.app_error", nil, "user_id="+userId)
|
|
||||||
} else {
|
|
||||||
result.Data = userId
|
|
||||||
}
|
|
||||||
|
|
||||||
storeChannel <- result
|
|
||||||
close(storeChannel)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return storeChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (us SqlUserStore) UpdateLastActivityAt(userId string, time int64) StoreChannel {
|
|
||||||
storeChannel := make(StoreChannel)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
result := StoreResult{}
|
|
||||||
|
|
||||||
if _, err := us.GetMaster().Exec("UPDATE Users SET LastActivityAt = :LastActivityAt WHERE Id = :UserId", map[string]interface{}{"LastActivityAt": time, "UserId": userId}); err != nil {
|
|
||||||
result.Err = model.NewLocAppError("SqlUserStore.UpdateLastActivityAt", "store.sql_user.update_last_activity.app_error", nil, "user_id="+userId)
|
|
||||||
} else {
|
|
||||||
result.Data = userId
|
|
||||||
}
|
|
||||||
|
|
||||||
storeChannel <- result
|
|
||||||
close(storeChannel)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return storeChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (us SqlUserStore) UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel {
|
|
||||||
storeChannel := make(StoreChannel)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
result := StoreResult{}
|
|
||||||
|
|
||||||
if _, err := us.GetMaster().Exec("UPDATE Users SET LastActivityAt = :UserLastActivityAt WHERE Id = :UserId", map[string]interface{}{"UserLastActivityAt": time, "UserId": userId}); err != nil {
|
|
||||||
result.Err = model.NewLocAppError("SqlUserStore.UpdateLastActivityAt", "store.sql_user.update_last_activity.app_error", nil, "1 user_id="+userId+" session_id="+sessionId+" err="+err.Error())
|
|
||||||
} else if _, err := us.GetMaster().Exec("UPDATE Sessions SET LastActivityAt = :SessionLastActivityAt WHERE Id = :SessionId", map[string]interface{}{"SessionLastActivityAt": time, "SessionId": sessionId}); err != nil {
|
|
||||||
result.Err = model.NewLocAppError("SqlUserStore.UpdateLastActivityAt", "store.sql_user.update_last_activity.app_error", nil, "2 user_id="+userId+" session_id="+sessionId+" err="+err.Error())
|
|
||||||
} else {
|
|
||||||
result.Data = userId
|
|
||||||
}
|
|
||||||
|
|
||||||
storeChannel <- result
|
|
||||||
close(storeChannel)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return storeChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChannel {
|
func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChannel {
|
||||||
|
|
||||||
storeChannel := make(StoreChannel)
|
storeChannel := make(StoreChannel)
|
||||||
@@ -988,27 +931,6 @@ func (us SqlUserStore) GetTotalUsersCount() StoreChannel {
|
|||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (us SqlUserStore) GetTotalActiveUsersCount() StoreChannel {
|
|
||||||
storeChannel := make(StoreChannel)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
result := StoreResult{}
|
|
||||||
|
|
||||||
time := model.GetMillis() - (1000 * 60 * 60 * 24)
|
|
||||||
|
|
||||||
if count, err := us.GetReplica().SelectInt("SELECT COUNT(Id) FROM Users WHERE LastActivityAt > :Time", map[string]interface{}{"Time": time}); err != nil {
|
|
||||||
result.Err = model.NewLocAppError("SqlUserStore.GetTotalActiveUsersCount", "store.sql_user.get_total_active_users_count.app_error", nil, err.Error())
|
|
||||||
} else {
|
|
||||||
result.Data = count
|
|
||||||
}
|
|
||||||
|
|
||||||
storeChannel <- result
|
|
||||||
close(storeChannel)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return storeChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (us SqlUserStore) PermanentDelete(userId string) StoreChannel {
|
func (us SqlUserStore) PermanentDelete(userId string) StoreChannel {
|
||||||
|
|
||||||
storeChannel := make(StoreChannel)
|
storeChannel := make(StoreChannel)
|
||||||
|
|||||||
@@ -129,50 +129,6 @@ func TestUserStoreUpdateUpdateAt(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserStoreUpdateLastPingAt(t *testing.T) {
|
|
||||||
Setup()
|
|
||||||
|
|
||||||
u1 := &model.User{}
|
|
||||||
u1.Email = model.NewId()
|
|
||||||
Must(store.User().Save(u1))
|
|
||||||
Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}))
|
|
||||||
|
|
||||||
if err := (<-store.User().UpdateLastPingAt(u1.Id, 1234567890)).Err; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
|
|
||||||
t.Fatal(r1.Err)
|
|
||||||
} else {
|
|
||||||
if r1.Data.(*model.User).LastPingAt != 1234567890 {
|
|
||||||
t.Fatal("LastPingAt not updated correctly")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserStoreUpdateLastActivityAt(t *testing.T) {
|
|
||||||
Setup()
|
|
||||||
|
|
||||||
u1 := &model.User{}
|
|
||||||
u1.Email = model.NewId()
|
|
||||||
Must(store.User().Save(u1))
|
|
||||||
Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}))
|
|
||||||
|
|
||||||
if err := (<-store.User().UpdateLastActivityAt(u1.Id, 1234567890)).Err; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
|
|
||||||
t.Fatal(r1.Err)
|
|
||||||
} else {
|
|
||||||
if r1.Data.(*model.User).LastActivityAt != 1234567890 {
|
|
||||||
t.Fatal("LastActivityAt not updated correctly")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) {
|
func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) {
|
||||||
Setup()
|
Setup()
|
||||||
|
|
||||||
@@ -189,41 +145,7 @@ func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) {
|
|||||||
t.Fatal(r1.Err)
|
t.Fatal(r1.Err)
|
||||||
} else {
|
} else {
|
||||||
if r1.Data.(*model.User).FailedAttempts != 3 {
|
if r1.Data.(*model.User).FailedAttempts != 3 {
|
||||||
t.Fatal("LastActivityAt not updated correctly")
|
t.Fatal("FailedAttempts not updated correctly")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserStoreUpdateUserAndSessionActivity(t *testing.T) {
|
|
||||||
Setup()
|
|
||||||
|
|
||||||
u1 := &model.User{}
|
|
||||||
u1.Email = model.NewId()
|
|
||||||
Must(store.User().Save(u1))
|
|
||||||
Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}))
|
|
||||||
|
|
||||||
s1 := model.Session{}
|
|
||||||
s1.UserId = u1.Id
|
|
||||||
Must(store.Session().Save(&s1))
|
|
||||||
|
|
||||||
if err := (<-store.User().UpdateUserAndSessionActivity(u1.Id, s1.Id, 1234567890)).Err; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r1 := <-store.User().Get(u1.Id); r1.Err != nil {
|
|
||||||
t.Fatal(r1.Err)
|
|
||||||
} else {
|
|
||||||
if r1.Data.(*model.User).LastActivityAt != 1234567890 {
|
|
||||||
t.Fatal("LastActivityAt not updated correctly for user")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r2 := <-store.Session().Get(s1.Id); r2.Err != nil {
|
|
||||||
t.Fatal(r2.Err)
|
|
||||||
} else {
|
|
||||||
if r2.Data.(*model.Session).LastActivityAt != 1234567890 {
|
|
||||||
t.Fatal("LastActivityAt not updated correctly for session")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,25 +190,6 @@ func TestUserCount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActiveUserCount(t *testing.T) {
|
|
||||||
Setup()
|
|
||||||
|
|
||||||
u1 := &model.User{}
|
|
||||||
u1.Email = model.NewId()
|
|
||||||
Must(store.User().Save(u1))
|
|
||||||
Must(store.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}))
|
|
||||||
<-store.User().UpdateLastActivityAt(u1.Id, model.GetMillis())
|
|
||||||
|
|
||||||
if result := <-store.User().GetTotalActiveUsersCount(); result.Err != nil {
|
|
||||||
t.Fatal(result.Err)
|
|
||||||
} else {
|
|
||||||
count := result.Data.(int64)
|
|
||||||
if count <= 0 {
|
|
||||||
t.Fatal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserStoreGetAllProfiles(t *testing.T) {
|
func TestUserStoreGetAllProfiles(t *testing.T) {
|
||||||
Setup()
|
Setup()
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type Store interface {
|
|||||||
License() LicenseStore
|
License() LicenseStore
|
||||||
PasswordRecovery() PasswordRecoveryStore
|
PasswordRecovery() PasswordRecoveryStore
|
||||||
Emoji() EmojiStore
|
Emoji() EmojiStore
|
||||||
|
Status() StatusStore
|
||||||
MarkSystemRanUnitTests()
|
MarkSystemRanUnitTests()
|
||||||
Close()
|
Close()
|
||||||
DropAllTables()
|
DropAllTables()
|
||||||
@@ -126,9 +127,6 @@ type UserStore interface {
|
|||||||
Update(user *model.User, allowRoleUpdate bool) StoreChannel
|
Update(user *model.User, allowRoleUpdate bool) StoreChannel
|
||||||
UpdateLastPictureUpdate(userId string) StoreChannel
|
UpdateLastPictureUpdate(userId string) StoreChannel
|
||||||
UpdateUpdateAt(userId string) StoreChannel
|
UpdateUpdateAt(userId string) StoreChannel
|
||||||
UpdateLastPingAt(userId string, time int64) StoreChannel
|
|
||||||
UpdateLastActivityAt(userId string, time int64) StoreChannel
|
|
||||||
UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel
|
|
||||||
UpdatePassword(userId, newPassword string) StoreChannel
|
UpdatePassword(userId, newPassword string) StoreChannel
|
||||||
UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel
|
UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel
|
||||||
UpdateMfaSecret(userId, secret string) StoreChannel
|
UpdateMfaSecret(userId, secret string) StoreChannel
|
||||||
@@ -150,7 +148,6 @@ type UserStore interface {
|
|||||||
GetEtagForDirectProfiles(userId string) StoreChannel
|
GetEtagForDirectProfiles(userId string) StoreChannel
|
||||||
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
|
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
|
||||||
GetTotalUsersCount() StoreChannel
|
GetTotalUsersCount() StoreChannel
|
||||||
GetTotalActiveUsersCount() StoreChannel
|
|
||||||
GetSystemAdminProfiles() StoreChannel
|
GetSystemAdminProfiles() StoreChannel
|
||||||
PermanentDelete(userId string) StoreChannel
|
PermanentDelete(userId string) StoreChannel
|
||||||
AnalyticsUniqueUserCount(teamId string) StoreChannel
|
AnalyticsUniqueUserCount(teamId string) StoreChannel
|
||||||
@@ -264,3 +261,12 @@ type EmojiStore interface {
|
|||||||
GetAll() StoreChannel
|
GetAll() StoreChannel
|
||||||
Delete(id string, time int64) StoreChannel
|
Delete(id string, time int64) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StatusStore interface {
|
||||||
|
SaveOrUpdate(status *model.Status) StoreChannel
|
||||||
|
Get(userId string) StoreChannel
|
||||||
|
GetOnlineAway() StoreChannel
|
||||||
|
ResetAll() StoreChannel
|
||||||
|
GetTotalActiveUsersCount() StoreChannel
|
||||||
|
UpdateLastActivityAt(userId string, lastActivityAt int64) StoreChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
import PostStore from 'stores/post_store.jsx';
|
import PostStore from 'stores/post_store.jsx';
|
||||||
@@ -14,12 +17,14 @@ import Client from 'utils/web_client.jsx';
|
|||||||
import WebSocketClient from 'utils/websocket_client.jsx';
|
import WebSocketClient from 'utils/websocket_client.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
import * as AsyncClient from 'utils/async_client.jsx';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
import * as UserActions from 'actions/user_actions.jsx';
|
import * as UserActions from 'actions/user_actions.jsx';
|
||||||
import {handleNewPost} from 'actions/post_actions.jsx';
|
import {handleNewPost} from 'actions/post_actions.jsx';
|
||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
const SocketEvents = Constants.SocketEvents;
|
const SocketEvents = Constants.SocketEvents;
|
||||||
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
import {browserHistory} from 'react-router/es6';
|
import {browserHistory} from 'react-router/es6';
|
||||||
|
|
||||||
@@ -34,10 +39,10 @@ export function initialize() {
|
|||||||
|
|
||||||
const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + Client.getUsersRoute() + '/websocket';
|
const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + Client.getUsersRoute() + '/websocket';
|
||||||
|
|
||||||
WebSocketClient.initialize(connUrl);
|
|
||||||
WebSocketClient.setEventCallback(handleEvent);
|
WebSocketClient.setEventCallback(handleEvent);
|
||||||
WebSocketClient.setReconnectCallback(handleReconnect);
|
WebSocketClient.setReconnectCallback(handleReconnect);
|
||||||
WebSocketClient.setCloseCallback(handleClose);
|
WebSocketClient.setCloseCallback(handleClose);
|
||||||
|
WebSocketClient.initialize(connUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,9 +50,21 @@ export function close() {
|
|||||||
WebSocketClient.close();
|
WebSocketClient.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStatuses() {
|
||||||
|
WebSocketClient.getStatuses(
|
||||||
|
(resp) => {
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_STATUSES,
|
||||||
|
statuses: resp.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function handleReconnect() {
|
function handleReconnect() {
|
||||||
AsyncClient.getChannels();
|
AsyncClient.getChannels();
|
||||||
AsyncClient.getPosts(ChannelStore.getCurrentId());
|
AsyncClient.getPosts(ChannelStore.getCurrentId());
|
||||||
|
getStatuses();
|
||||||
ErrorStore.clearLastError();
|
ErrorStore.clearLastError();
|
||||||
ErrorStore.emitChange();
|
ErrorStore.emitChange();
|
||||||
}
|
}
|
||||||
@@ -112,6 +129,10 @@ function handleEvent(msg) {
|
|||||||
handleUserTypingEvent(msg);
|
handleUserTypingEvent(msg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case SocketEvents.STATUS_CHANGED:
|
||||||
|
handleStatusChangedEvent(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,3 +239,7 @@ function handlePreferenceChangedEvent(msg) {
|
|||||||
function handleUserTypingEvent(msg) {
|
function handleUserTypingEvent(msg) {
|
||||||
GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.data.parent_id);
|
GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.data.parent_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStatusChangedEvent(msg) {
|
||||||
|
UserStore.setStatus(msg.user_id, msg.data.status);
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import * as TextFormatting from 'utils/text_formatting.jsx';
|
|||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
import * as AsyncClient from 'utils/async_client.jsx';
|
||||||
import Client from 'utils/web_client.jsx';
|
import Client from 'utils/web_client.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
|
const UserStatuses = Constants.UserStatuses;
|
||||||
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import {browserHistory} from 'react-router/es6';
|
import {browserHistory} from 'react-router/es6';
|
||||||
@@ -189,6 +190,7 @@ export default class ChannelHeader extends React.Component {
|
|||||||
if (teammate) {
|
if (teammate) {
|
||||||
return UserStore.getStatus(teammate.id);
|
return UserStore.getStatus(teammate.id);
|
||||||
}
|
}
|
||||||
|
return UserStatuses.OFFLINE;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ import UserStore from 'stores/user_store.jsx';
|
|||||||
import BrowserStore from 'stores/browser_store.jsx';
|
import BrowserStore from 'stores/browser_store.jsx';
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import * as Websockets from 'actions/websocket_actions.jsx';
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
|
import * as WebSocketActions from 'actions/websocket_actions.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
|
|
||||||
import {browserHistory} from 'react-router/es6';
|
import {browserHistory} from 'react-router/es6';
|
||||||
|
|
||||||
const CLIENT_STATUS_INTERVAL = 30000;
|
|
||||||
const BACKSPACE_CHAR = 8;
|
const BACKSPACE_CHAR = 8;
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -26,8 +25,8 @@ export default class LoggedIn extends React.Component {
|
|||||||
this.onUserChanged = this.onUserChanged.bind(this);
|
this.onUserChanged = this.onUserChanged.bind(this);
|
||||||
this.setupUser = this.setupUser.bind(this);
|
this.setupUser = this.setupUser.bind(this);
|
||||||
|
|
||||||
// Initalize websockets
|
// Initalize websocket
|
||||||
Websockets.initialize();
|
WebSocketActions.initialize();
|
||||||
|
|
||||||
// Force logout of all tabs if one tab is logged out
|
// Force logout of all tabs if one tab is logged out
|
||||||
$(window).bind('storage', (e) => {
|
$(window).bind('storage', (e) => {
|
||||||
@@ -109,10 +108,6 @@ export default class LoggedIn extends React.Component {
|
|||||||
// Listen for user
|
// Listen for user
|
||||||
UserStore.addChangeListener(this.onUserChanged);
|
UserStore.addChangeListener(this.onUserChanged);
|
||||||
|
|
||||||
// Get all statuses regularally. (Soon to be switched to websocket)
|
|
||||||
AsyncClient.getStatuses();
|
|
||||||
this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL);
|
|
||||||
|
|
||||||
// ???
|
// ???
|
||||||
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
|
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
|
||||||
if (ev.type === 'mouseenter') {
|
if (ev.type === 'mouseenter') {
|
||||||
@@ -144,7 +139,7 @@ export default class LoggedIn extends React.Component {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pervent backspace from navigating back a page
|
// Prevent backspace from navigating back a page
|
||||||
$(window).on('keydown.preventBackspace', (e) => {
|
$(window).on('keydown.preventBackspace', (e) => {
|
||||||
if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
|
if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -159,9 +154,8 @@ export default class LoggedIn extends React.Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
$('#root').attr('class', '');
|
$('#root').attr('class', '');
|
||||||
clearInterval(this.intervalId);
|
|
||||||
|
|
||||||
Websockets.close();
|
WebSocketActions.close();
|
||||||
UserStore.removeChangeListener(this.onUserChanged);
|
UserStore.removeChangeListener(this.onUserChanged);
|
||||||
|
|
||||||
$('body').off('click.userpopover');
|
$('body').off('click.userpopover');
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export default class Sidebar extends React.Component {
|
|||||||
|
|
||||||
directChannel.display_name = Utils.displayUsername(teammateId);
|
directChannel.display_name = Utils.displayUsername(teammateId);
|
||||||
directChannel.teammate_id = teammateId;
|
directChannel.teammate_id = teammateId;
|
||||||
directChannel.status = UserStore.getStatus(teammateId);
|
directChannel.status = UserStore.getStatus(teammateId) || 'offline';
|
||||||
|
|
||||||
if (UserStore.hasTeamProfile(teammateId)) {
|
if (UserStore.hasTeamProfile(teammateId)) {
|
||||||
directChannels.push(directChannel);
|
directChannels.push(directChannel);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"keymirror": "0.1.1",
|
"keymirror": "0.1.1",
|
||||||
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
|
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
|
||||||
"match-at": "0.1.0",
|
"match-at": "0.1.0",
|
||||||
"mattermost": "mattermost/mattermost-javascript#4cdaeba22ff82bf93dc417af1ab4e89e3248d624",
|
"mattermost": "mattermost/mattermost-javascript#84b6f1ebf33aa4b5d8e7ddd7be97d3f5bff5ed17",
|
||||||
"object-assign": "4.1.0",
|
"object-assign": "4.1.0",
|
||||||
"perfect-scrollbar": "0.6.12",
|
"perfect-scrollbar": "0.6.12",
|
||||||
"react": "15.2.1",
|
"react": "15.2.1",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import LocalizationStore from './localization_store.jsx';
|
|||||||
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
const UserStatuses = Constants.UserStatuses;
|
||||||
|
|
||||||
const CHANGE_EVENT_DM_LIST = 'change_dm_list';
|
const CHANGE_EVENT_DM_LIST = 'change_dm_list';
|
||||||
const CHANGE_EVENT = 'change';
|
const CHANGE_EVENT = 'change';
|
||||||
@@ -292,18 +293,11 @@ class UserStoreClass extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setStatuses(statuses) {
|
setStatuses(statuses) {
|
||||||
this.pSetStatuses(statuses);
|
this.statuses = Object.assign(this.statuses, statuses);
|
||||||
this.emitStatusesChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
pSetStatuses(statuses) {
|
|
||||||
this.statuses = statuses;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(userId, status) {
|
setStatus(userId, status) {
|
||||||
var statuses = this.getStatuses();
|
this.statuses[userId] = status;
|
||||||
statuses[userId] = status;
|
|
||||||
this.pSetStatuses(statuses);
|
|
||||||
this.emitStatusesChange();
|
this.emitStatusesChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +306,7 @@ class UserStoreClass extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStatus(id) {
|
getStatus(id) {
|
||||||
return this.getStatuses()[id];
|
return this.getStatuses()[id] || UserStatuses.OFFLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoAccounts() {
|
getNoAccounts() {
|
||||||
@@ -370,7 +364,7 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
|
|||||||
UserStore.emitAuditsChange();
|
UserStore.emitAuditsChange();
|
||||||
break;
|
break;
|
||||||
case ActionTypes.RECEIVED_STATUSES:
|
case ActionTypes.RECEIVED_STATUSES:
|
||||||
UserStore.pSetStatuses(action.statuses);
|
UserStore.setStatuses(action.statuses);
|
||||||
UserStore.emitStatusesChange();
|
UserStore.emitStatusesChange();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import * as GlobalActions from 'actions/global_actions.jsx';
|
|||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
import BrowserStore from 'stores/browser_store.jsx';
|
import BrowserStore from 'stores/browser_store.jsx';
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
|
||||||
import PostStore from 'stores/post_store.jsx';
|
import PostStore from 'stores/post_store.jsx';
|
||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import * as utils from './utils.jsx';
|
import * as utils from './utils.jsx';
|
||||||
@@ -744,21 +743,12 @@ export function getMe() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getStatuses() {
|
export function getStatuses() {
|
||||||
const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
|
if (isCallInProgress('getStatuses')) {
|
||||||
|
|
||||||
const teammateIds = [];
|
|
||||||
for (const [name, value] of preferences) {
|
|
||||||
if (value === 'true') {
|
|
||||||
teammateIds.push(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCallInProgress('getStatuses') || teammateIds.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
callTracker.getStatuses = utils.getTimestamp();
|
callTracker.getStatuses = utils.getTimestamp();
|
||||||
Client.getStatuses(teammateIds,
|
Client.getStatuses(
|
||||||
(data) => {
|
(data) => {
|
||||||
callTracker.getStatuses = 0;
|
callTracker.getStatuses = 0;
|
||||||
|
|
||||||
@@ -1535,4 +1525,4 @@ export function deleteEmoji(id) {
|
|||||||
dispatchError(err, 'deleteEmoji');
|
dispatchError(err, 'deleteEmoji');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ export const Constants = {
|
|||||||
USER_REMOVED: 'user_removed',
|
USER_REMOVED: 'user_removed',
|
||||||
TYPING: 'typing',
|
TYPING: 'typing',
|
||||||
PREFERENCE_CHANGED: 'preference_changed',
|
PREFERENCE_CHANGED: 'preference_changed',
|
||||||
EPHEMERAL_MESSAGE: 'ephemeral_message'
|
EPHEMERAL_MESSAGE: 'ephemeral_message',
|
||||||
|
STATUS_CHANGED: 'status_change'
|
||||||
},
|
},
|
||||||
|
|
||||||
UserUpdateEvents: {
|
UserUpdateEvents: {
|
||||||
@@ -210,6 +211,12 @@ export const Constants = {
|
|||||||
POST: 5
|
POST: 5
|
||||||
},
|
},
|
||||||
|
|
||||||
|
UserStatuses: {
|
||||||
|
OFFLINE: 'offline',
|
||||||
|
AWAY: 'away',
|
||||||
|
ONLINE: 'online'
|
||||||
|
},
|
||||||
|
|
||||||
SPECIAL_MENTIONS: ['all', 'channel'],
|
SPECIAL_MENTIONS: ['all', 'channel'],
|
||||||
CHARACTER_LIMIT: 4000,
|
CHARACTER_LIMIT: 4000,
|
||||||
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
|
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user