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
Этот коммит содержится в:
Joram Wilander
2016-07-18 11:10:03 -04:00
коммит произвёл GitHub
родитель 180adc79af
Коммит c0ab2636d6
35 изменённых файлов: 800 добавлений и 376 удалений

Просмотреть файл

@@ -159,7 +159,7 @@ test-server: start-docker prepare-enterprise
rm -f 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=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

Просмотреть файл

@@ -94,6 +94,7 @@ func InitApi() {
InitPreference()
InitLicense()
InitEmoji()
InitStatus()
// 404 on any api route before web.go has a chance to serve it
Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))

Просмотреть файл

@@ -20,6 +20,7 @@ import (
)
var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
var statusCache *utils.Cache = utils.NewLru(model.STATUS_CACHE_SIZE)
var allowedMethods []string = []string{
"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 {
go func() {
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)
}
}()
SetStatusOnline(c.Session.UserId, c.Session.Id)
}
if c.Err == nil {

Просмотреть файл

@@ -73,7 +73,12 @@ func ping(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(m)))
}
func webSocketPing(req *model.WebSocketRequest, responseData map[string]interface{}) *model.AppError {
responseData["text"] = "pong"
return nil
func webSocketPing(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
data := map[string]interface{}{}
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 {
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)
}
}

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 Обычный файл
Просмотреть файл

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

Просмотреть файл

@@ -54,7 +54,6 @@ func InitUser() {
BaseRoutes.Users.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
BaseRoutes.Users.Handle("/me", ApiAppHandler(getMe)).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("/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")
@@ -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 {
if !model.IsValidUsername(name) {
@@ -2312,7 +2282,7 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
c.Err = error
return
} else {
if user.LastActivityAt > 0 {
if _, err := GetStatus(user.Id); err != nil {
go SendEmailChangeVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
} else {
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 channelId string
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
@@ -2567,5 +2537,5 @@ func userTyping(req *model.WebSocketRequest, responseData map[string]interface{}
event.Add("parent_id", parentId)
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 = 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", LastActivityAt: time1, LastPingAt: time1, Roles: ""}
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", Roles: ""}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
@@ -683,15 +681,7 @@ func TestUserUpdate(t *testing.T) {
Client.Login(user.Email, "passwd1")
Client.SetTeamId(team.Id)
time.Sleep(100 * time.Millisecond)
time2 := model.GetMillis()
time.Sleep(100 * time.Millisecond)
user.Nickname = "Jim Jimmy"
user.LastActivityAt = time2
user.LastPingAt = time2
user.Roles = model.ROLE_TEAM_ADMIN
user.LastPasswordUpdate = 123
@@ -701,12 +691,6 @@ func TestUserUpdate(t *testing.T) {
if result.Data.(*model.User).Nickname != "Jim Jimmy" {
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 != "" {
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) {
th := Setup()
Client := th.CreateClient()

Просмотреть файл

@@ -7,9 +7,7 @@ import (
"time"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/websocket"
goi18n "github.com/nicksnyder/go-i18n/i18n"
)
@@ -34,18 +32,7 @@ type WebConn struct {
}
func NewWebConn(c *Context, ws *websocket.Conn) *WebConn {
go func() {
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)
}
}()
go SetStatusOnline(c.Session.UserId, c.Session.Id)
return &WebConn{
Send: make(chan model.WebSocketMessage, 64),
@@ -68,13 +55,7 @@ func (c *WebConn) readPump() {
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
c.WebSocket.SetPongHandler(func(string) error {
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
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)
}
}()
go SetStatusAwayIfNeeded(c.UserId)
return nil
})

Просмотреть файл

@@ -67,10 +67,23 @@ func (h *Hub) Start() {
h.connections[webCon] = true
case webCon := <-h.unregister:
userId := webCon.UserId
if _, ok := h.connections[webCon]; ok {
delete(h.connections, webCon)
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:
for webCon := range h.connections {
if webCon.UserId == userId {

Просмотреть файл

@@ -10,12 +10,12 @@ import (
"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}
}
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) {
@@ -25,9 +25,10 @@ func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequ
r.T = conn.T
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)
err.DetailedError = ""
conn.Send <- model.NewWebSocketError(r.Seq, err)

Просмотреть файл

@@ -40,7 +40,8 @@
"RestrictDirectMessage": "any",
"RestrictTeamInvite": "all",
"RestrictPublicChannelManagement": "all",
"RestrictPrivateChannelManagement": "all"
"RestrictPrivateChannelManagement": "all",
"UserStatusAwayTimeout": 300
},
"SqlSettings": {
"DriverName": "mysql",

Просмотреть файл

@@ -1980,12 +1980,12 @@
"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"
},
{
"id": "api.web_conn.new_web_conn.last_ping.error",
"translation": "Failed to update LastPingAt for user_id=%v, err=%v"
"id": "api.status.save_status.error",
"translation": "Failed to save status for user_id=%v, err=%v"
},
{
"id": "api.web_hub.start.stopping.debug",
@@ -4036,8 +4036,32 @@
"translation": "We encountered an error while finding user profiles"
},
{
"id": "store.sql_user.get_total_active_users_count.app_error",
"translation": "We could not count the users"
"id": "store.sql_status.get_total_active_users_count.app_error",
"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",

Просмотреть файл

@@ -125,6 +125,8 @@ func main() {
if flagRunCmds {
runCmds()
} else {
resetStatuses()
api.StartServer()
// 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() {
if result := <-api.Srv.Store.System().Get(); result.Err == nil {
props := result.Data.(model.StringMap)
@@ -200,7 +208,7 @@ func doSecurityAndDiagnostics() {
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))
}

Просмотреть файл

@@ -1415,8 +1415,9 @@ func (c *Client) AdminResetPassword(userId, newPassword string) (*Result, *AppEr
}
}
func (c *Client) GetStatuses(data []string) (*Result, *AppError) {
if r, err := c.DoApiPost("/users/status", ArrayToJson(data)); err != nil {
// GetStatuses returns a map of string statuses using user id as the key
func (c *Client) GetStatuses() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/status", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)

Просмотреть файл

@@ -195,6 +195,7 @@ type TeamSettings struct {
RestrictTeamInvite *string
RestrictPublicChannelManagement *string
RestrictPrivateChannelManagement *string
UserStatusAwayTimeout *int64
}
type LdapSettings struct {
@@ -435,6 +436,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.RestrictPrivateChannelManagement = PERMISSIONS_ALL
}
if o.TeamSettings.UserStatusAwayTimeout == nil {
o.TeamSettings.UserStatusAwayTimeout = new(int64)
*o.TeamSettings.UserStatusAwayTimeout = 300
}
if o.EmailSettings.EnableSignInWithEmail == nil {
o.EmailSettings.EnableSignInWithEmail = new(bool)

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 Обычный файл
Просмотреть файл

@@ -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 (
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_MENTION = "mention"
USER_NOTIFY_NONE = "none"
@@ -44,8 +39,6 @@ type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Roles string `json:"roles"`
LastActivityAt int64 `json:"last_activity_at,omitempty"`
LastPingAt int64 `json:"last_ping_at,omitempty"`
AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"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)
}
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
func (u *User) Sanitize(options map[string]bool) {
u.Password = ""
@@ -258,7 +243,6 @@ func (u *User) ClearNonProfileFields() {
u.MfaActive = false
u.MfaSecret = ""
u.EmailVerified = false
u.LastPingAt = 0
u.AllowMarketing = false
u.Props = StringMap{}
u.NotifyProps = StringMap{}

Просмотреть файл

@@ -92,6 +92,8 @@ func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface
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) {
data := map[string]interface{}{
"channel_id": channelId,
@@ -100,3 +102,8 @@ func (wsc *WebSocketClient) UserTyping(channelId, parentId string) {
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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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
recovery PasswordRecoveryStore
emoji EmojiStore
status StatusStore
SchemaVersion string
}
@@ -129,6 +130,7 @@ func NewSqlStore() Store {
sqlStore.license = NewSqlLicenseStore(sqlStore)
sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore)
sqlStore.emoji = NewSqlEmojiStore(sqlStore)
sqlStore.status = NewSqlStatusStore(sqlStore)
err := sqlStore.master.CreateTablesIfNotExists()
if err != nil {
@@ -152,6 +154,7 @@ func NewSqlStore() Store {
sqlStore.license.(*SqlLicenseStore).UpgradeSchemaIfNeeded()
sqlStore.recovery.(*SqlPasswordRecoveryStore).UpgradeSchemaIfNeeded()
sqlStore.emoji.(*SqlEmojiStore).UpgradeSchemaIfNeeded()
sqlStore.status.(*SqlStatusStore).UpgradeSchemaIfNeeded()
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
@@ -168,6 +171,7 @@ func NewSqlStore() Store {
sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists()
sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists()
sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists()
sqlStore.status.(*SqlStatusStore).CreateIndexesIfNotExists()
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
@@ -696,6 +700,10 @@ func (ss SqlStore) Emoji() EmojiStore {
return ss.emoji
}
func (ss SqlStore) Status() StatusStore {
return ss.status
}
func (ss SqlStore) DropAllTables() {
ss.master.TruncateTables()
}

Просмотреть файл

@@ -108,6 +108,10 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() {
us.Preference().Save(&data)
}
}
// ADDED for 3.3 remove for 3.7
us.RemoveColumnIfExists("Users", "LastActivityAt")
us.RemoveColumnIfExists("Users", "LastPingAt")
}
func themeMigrationFailed(err error) {
@@ -187,8 +191,6 @@ func (us SqlUserStore) Update(user *model.User, trustedUpdateData bool) StoreCha
user.Password = oldUser.Password
user.LastPasswordUpdate = oldUser.LastPasswordUpdate
user.LastPictureUpdate = oldUser.LastPictureUpdate
user.LastActivityAt = oldUser.LastActivityAt
user.LastPingAt = oldUser.LastPingAt
user.EmailVerified = oldUser.EmailVerified
user.FailedAttempts = oldUser.FailedAttempts
user.MfaSecret = oldUser.MfaSecret
@@ -283,65 +285,6 @@ func (us SqlUserStore) UpdateUpdateAt(userId string) 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 {
storeChannel := make(StoreChannel)
@@ -988,27 +931,6 @@ func (us SqlUserStore) GetTotalUsersCount() 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 {
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) {
Setup()
@@ -189,41 +145,7 @@ func TestUserStoreUpdateFailedPasswordAttempts(t *testing.T) {
t.Fatal(r1.Err)
} else {
if r1.Data.(*model.User).FailedAttempts != 3 {
t.Fatal("LastActivityAt 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")
t.Fatal("FailedAttempts not updated correctly")
}
}
@@ -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) {
Setup()

Просмотреть файл

@@ -43,6 +43,7 @@ type Store interface {
License() LicenseStore
PasswordRecovery() PasswordRecoveryStore
Emoji() EmojiStore
Status() StatusStore
MarkSystemRanUnitTests()
Close()
DropAllTables()
@@ -126,9 +127,6 @@ type UserStore interface {
Update(user *model.User, allowRoleUpdate bool) StoreChannel
UpdateLastPictureUpdate(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
UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel
UpdateMfaSecret(userId, secret string) StoreChannel
@@ -150,7 +148,6 @@ type UserStore interface {
GetEtagForDirectProfiles(userId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetTotalUsersCount() StoreChannel
GetTotalActiveUsersCount() StoreChannel
GetSystemAdminProfiles() StoreChannel
PermanentDelete(userId string) StoreChannel
AnalyticsUniqueUserCount(teamId string) StoreChannel
@@ -264,3 +261,12 @@ type EmojiStore interface {
GetAll() 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.
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';
@@ -14,12 +17,14 @@ import Client from 'utils/web_client.jsx';
import WebSocketClient from 'utils/websocket_client.jsx';
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 Constants from 'utils/constants.jsx';
const SocketEvents = Constants.SocketEvents;
const ActionTypes = Constants.ActionTypes;
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';
WebSocketClient.initialize(connUrl);
WebSocketClient.setEventCallback(handleEvent);
WebSocketClient.setReconnectCallback(handleReconnect);
WebSocketClient.setCloseCallback(handleClose);
WebSocketClient.initialize(connUrl);
}
}
@@ -45,9 +50,21 @@ export function close() {
WebSocketClient.close();
}
export function getStatuses() {
WebSocketClient.getStatuses(
(resp) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_STATUSES,
statuses: resp.data
});
}
);
}
function handleReconnect() {
AsyncClient.getChannels();
AsyncClient.getPosts(ChannelStore.getCurrentId());
getStatuses();
ErrorStore.clearLastError();
ErrorStore.emitChange();
}
@@ -112,6 +129,10 @@ function handleEvent(msg) {
handleUserTypingEvent(msg);
break;
case SocketEvents.STATUS_CHANGED:
handleStatusChangedEvent(msg);
break;
default:
}
}
@@ -218,3 +239,7 @@ function handlePreferenceChangedEvent(msg) {
function handleUserTypingEvent(msg) {
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 Client from 'utils/web_client.jsx';
import Constants from 'utils/constants.jsx';
const UserStatuses = Constants.UserStatuses;
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
@@ -189,6 +190,7 @@ export default class ChannelHeader extends React.Component {
if (teammate) {
return UserStore.getStatus(teammate.id);
}
return UserStatuses.OFFLINE;
}
return null;
}

Просмотреть файл

@@ -8,13 +8,12 @@ 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 Websockets from 'actions/websocket_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 {browserHistory} from 'react-router/es6';
const CLIENT_STATUS_INTERVAL = 30000;
const BACKSPACE_CHAR = 8;
import React from 'react';
@@ -26,8 +25,8 @@ export default class LoggedIn extends React.Component {
this.onUserChanged = this.onUserChanged.bind(this);
this.setupUser = this.setupUser.bind(this);
// Initalize websockets
Websockets.initialize();
// Initalize websocket
WebSocketActions.initialize();
// Force logout of all tabs if one tab is logged out
$(window).bind('storage', (e) => {
@@ -109,10 +108,6 @@ export default class LoggedIn extends React.Component {
// Listen for user
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) {
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) => {
if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
e.preventDefault();
@@ -159,9 +154,8 @@ export default class LoggedIn extends React.Component {
componentWillUnmount() {
$('#root').attr('class', '');
clearInterval(this.intervalId);
Websockets.close();
WebSocketActions.close();
UserStore.removeChangeListener(this.onUserChanged);
$('body').off('click.userpopover');

Просмотреть файл

@@ -125,7 +125,7 @@ export default class Sidebar extends React.Component {
directChannel.display_name = Utils.displayUsername(teammateId);
directChannel.teammate_id = teammateId;
directChannel.status = UserStore.getStatus(teammateId);
directChannel.status = UserStore.getStatus(teammateId) || 'offline';
if (UserStore.hasTeamProfile(teammateId)) {
directChannels.push(directChannel);

Просмотреть файл

@@ -18,7 +18,7 @@
"keymirror": "0.1.1",
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
"match-at": "0.1.0",
"mattermost": "mattermost/mattermost-javascript#4cdaeba22ff82bf93dc417af1ab4e89e3248d624",
"mattermost": "mattermost/mattermost-javascript#84b6f1ebf33aa4b5d8e7ddd7be97d3f5bff5ed17",
"object-assign": "4.1.0",
"perfect-scrollbar": "0.6.12",
"react": "15.2.1",

Просмотреть файл

@@ -9,6 +9,7 @@ import LocalizationStore from './localization_store.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const UserStatuses = Constants.UserStatuses;
const CHANGE_EVENT_DM_LIST = 'change_dm_list';
const CHANGE_EVENT = 'change';
@@ -292,18 +293,11 @@ class UserStoreClass extends EventEmitter {
}
setStatuses(statuses) {
this.pSetStatuses(statuses);
this.emitStatusesChange();
}
pSetStatuses(statuses) {
this.statuses = statuses;
this.statuses = Object.assign(this.statuses, statuses);
}
setStatus(userId, status) {
var statuses = this.getStatuses();
statuses[userId] = status;
this.pSetStatuses(statuses);
this.statuses[userId] = status;
this.emitStatusesChange();
}
@@ -312,7 +306,7 @@ class UserStoreClass extends EventEmitter {
}
getStatus(id) {
return this.getStatuses()[id];
return this.getStatuses()[id] || UserStatuses.OFFLINE;
}
getNoAccounts() {
@@ -370,7 +364,7 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
UserStore.emitAuditsChange();
break;
case ActionTypes.RECEIVED_STATUSES:
UserStore.pSetStatuses(action.statuses);
UserStore.setStatuses(action.statuses);
UserStore.emitStatusesChange();
break;
default:

Просмотреть файл

@@ -7,7 +7,6 @@ import * as GlobalActions from 'actions/global_actions.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as utils from './utils.jsx';
@@ -744,21 +743,12 @@ export function getMe() {
}
export function getStatuses() {
const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
const teammateIds = [];
for (const [name, value] of preferences) {
if (value === 'true') {
teammateIds.push(name);
}
}
if (isCallInProgress('getStatuses') || teammateIds.length === 0) {
if (isCallInProgress('getStatuses')) {
return;
}
callTracker.getStatuses = utils.getTimestamp();
Client.getStatuses(teammateIds,
Client.getStatuses(
(data) => {
callTracker.getStatuses = 0;
@@ -1535,4 +1525,4 @@ export function deleteEmoji(id) {
dispatchError(err, 'deleteEmoji');
}
);
}
}

Просмотреть файл

@@ -191,7 +191,8 @@ export const Constants = {
USER_REMOVED: 'user_removed',
TYPING: 'typing',
PREFERENCE_CHANGED: 'preference_changed',
EPHEMERAL_MESSAGE: 'ephemeral_message'
EPHEMERAL_MESSAGE: 'ephemeral_message',
STATUS_CHANGED: 'status_change'
},
UserUpdateEvents: {
@@ -210,6 +211,12 @@ export const Constants = {
POST: 5
},
UserStatuses: {
OFFLINE: 'offline',
AWAY: 'away',
ONLINE: 'online'
},
SPECIAL_MENTIONS: ['all', 'channel'],
CHARACTER_LIMIT: 4000,
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],