PLT-1527 Add a slash command to set yourself away (#3752)

* added handlers for slash commands

* added manual status persistance

* added tests

* removed extra debug output and comments

* rebase - fixing the PR

* making echo messages after slash commands ephemeral
Этот коммит содержится в:
Dmitri Aizenberg
2016-08-31 06:24:14 -07:00
коммит произвёл Joram Wilander
родитель db660bdf9c
Коммит dc09b7781a
15 изменённых файлов: 260 добавлений и 26 удалений

42
api/command_away.go Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"github.com/mattermost/platform/model"
)
type AwayProvider struct {
}
const (
CMD_AWAY = "away"
)
func init() {
RegisterCommandProvider(&AwayProvider{})
}
func (me *AwayProvider) GetTrigger() string {
return CMD_AWAY
}
func (me *AwayProvider) GetCommand(c *Context) *model.Command {
return &model.Command{
Trigger: CMD_AWAY,
AutoComplete: true,
AutoCompleteDesc: c.T("api.command_away.desc"),
DisplayName: c.T("api.command_away.name"),
}
}
func (me *AwayProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
rmsg := c.T("api.command_away.success")
if len(message) > 0 {
rmsg = message + " " + rmsg
}
SetStatusAwayIfNeeded(c.Session.UserId, true)
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg}
}

42
api/command_offline.go Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"github.com/mattermost/platform/model"
)
type OfflineProvider struct {
}
const (
CMD_OFFLINE = "offline"
)
func init() {
RegisterCommandProvider(&OfflineProvider{})
}
func (me *OfflineProvider) GetTrigger() string {
return CMD_OFFLINE
}
func (me *OfflineProvider) GetCommand(c *Context) *model.Command {
return &model.Command{
Trigger: CMD_OFFLINE,
AutoComplete: true,
AutoCompleteDesc: c.T("api.command_offline.desc"),
DisplayName: c.T("api.command_offline.name"),
}
}
func (me *OfflineProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
rmsg := c.T("api.command_offline.success")
if len(message) > 0 {
rmsg = message + " " + rmsg
}
SetStatusOffline(c.Session.UserId, true)
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg}
}

42
api/command_online.go Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"github.com/mattermost/platform/model"
)
type OnlineProvider struct {
}
const (
CMD_ONLINE = "online"
)
func init() {
RegisterCommandProvider(&OnlineProvider{})
}
func (me *OnlineProvider) GetTrigger() string {
return CMD_ONLINE
}
func (me *OnlineProvider) GetCommand(c *Context) *model.Command {
return &model.Command{
Trigger: CMD_ONLINE,
AutoComplete: true,
AutoCompleteDesc: c.T("api.command_online.desc"),
DisplayName: c.T("api.command_online.name"),
}
}
func (me *OnlineProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
rmsg := c.T("api.command_online.success")
if len(message) > 0 {
rmsg = message + " " + rmsg
}
SetStatusOnline(c.Session.UserId, c.Session.Id, true)
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg}
}

40
api/command_statuses_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,40 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"testing"
"time"
"github.com/mattermost/platform/model"
)
func TestStatusCommands(t *testing.T) {
th := Setup().InitBasic()
commandAndTest(t, th, "away")
commandAndTest(t, th, "offline")
commandAndTest(t, th, "online")
}
func commandAndTest(t *testing.T, th *TestHelper, status string) {
Client := th.BasicClient
channel := th.BasicChannel
user := th.BasicUser
r1 := Client.Must(Client.Command(channel.Id, "/"+status, false)).Data.(*model.CommandResponse)
if r1 == nil {
t.Fatal("Command failed to execute")
}
time.Sleep(300 * time.Millisecond)
statuses := Client.Must(Client.GetStatuses()).Data.(map[string]string)
if status == "offline" {
status = ""
}
if statuses[user.Id] != status {
t.Fatal("Error setting status " + status)
}
}

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

@@ -207,7 +207,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 {
SetStatusOnline(c.Session.UserId, c.Session.Id) SetStatusOnline(c.Session.UserId, c.Session.Id, false)
} }
if c.Err == nil { if c.Err == nil {

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

@@ -679,7 +679,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
var status *model.Status var status *model.Status
var err *model.AppError var err *model.AppError
if status, err = GetStatus(id); err != nil { if status, err = GetStatus(id); err != nil {
status = &model.Status{id, model.STATUS_OFFLINE, 0} status = &model.Status{id, model.STATUS_OFFLINE, false, 0}
} }
if userAllowsEmails && status.Status != model.STATUS_ONLINE { if userAllowsEmails && status.Status != model.STATUS_ONLINE {
@@ -727,7 +727,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
var status *model.Status var status *model.Status
var err *model.AppError var err *model.AppError
if status, err = GetStatus(id); err != nil { if status, err = GetStatus(id); err != nil {
status = &model.Status{id, model.STATUS_OFFLINE, 0} status = &model.Status{id, model.STATUS_OFFLINE, false, 0}
} }
if profileMap[id].StatusAllowsPushNotification(status) { if profileMap[id].StatusAllowsPushNotification(status) {
@@ -740,7 +740,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
var status *model.Status var status *model.Status
var err *model.AppError var err *model.AppError
if status, err = GetStatus(id); err != nil { if status, err = GetStatus(id); err != nil {
status = &model.Status{id, model.STATUS_OFFLINE, 0} status = &model.Status{id, model.STATUS_OFFLINE, false, 0}
} }
if profileMap[id].StatusAllowsPushNotification(status) { if profileMap[id].StatusAllowsPushNotification(status) {

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

@@ -65,19 +65,24 @@ func GetAllStatuses() (map[string]interface{}, *model.AppError) {
} }
} }
func SetStatusOnline(userId string, sessionId string) { func SetStatusOnline(userId string, sessionId string, manual bool) {
l4g.Debug(userId, "online")
broadcast := false broadcast := false
var status *model.Status var status *model.Status
var err *model.AppError var err *model.AppError
if status, err = GetStatus(userId); err != nil { if status, err = GetStatus(userId); err != nil {
status = &model.Status{userId, model.STATUS_ONLINE, model.GetMillis()} status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis()}
broadcast = true broadcast = true
} else { } else {
if status.Manual && !manual {
return // manually set status always overrides non-manual one
}
if status.Status != model.STATUS_ONLINE { if status.Status != model.STATUS_ONLINE {
broadcast = true broadcast = true
} }
status.Status = model.STATUS_ONLINE status.Status = model.STATUS_ONLINE
status.Manual = false // for "online" there's no manually or auto set
status.LastActivityAt = model.GetMillis() status.LastActivityAt = model.GetMillis()
} }
@@ -107,8 +112,14 @@ func SetStatusOnline(userId string, sessionId string) {
} }
} }
func SetStatusOffline(userId string) { func SetStatusOffline(userId string, manual bool) {
status := &model.Status{userId, model.STATUS_OFFLINE, model.GetMillis()} l4g.Debug(userId, "offline")
status, err := GetStatus(userId)
if err == nil && status.Manual && !manual {
return // manually set status always overrides non-manual one
}
status = &model.Status{userId, model.STATUS_OFFLINE, manual, model.GetMillis()}
AddStatusCache(status) AddStatusCache(status)
@@ -121,21 +132,30 @@ func SetStatusOffline(userId string) {
go Publish(event) go Publish(event)
} }
func SetStatusAwayIfNeeded(userId string) { func SetStatusAwayIfNeeded(userId string, manual bool) {
l4g.Debug(userId, "away")
status, err := GetStatus(userId) status, err := GetStatus(userId)
if err != nil { if err != nil {
status = &model.Status{userId, model.STATUS_OFFLINE, 0} status = &model.Status{userId, model.STATUS_OFFLINE, manual, 0}
} }
if status.Status == model.STATUS_AWAY { if !manual && status.Manual {
return return // manually set status always overrides non-manual one
} }
if !IsUserAway(status.LastActivityAt) { if !manual {
return if status.Status == model.STATUS_AWAY {
return
}
if !IsUserAway(status.LastActivityAt) {
return
}
} }
status.Status = model.STATUS_AWAY status.Status = model.STATUS_AWAY
status.Manual = manual
AddStatusCache(status) AddStatusCache(status)

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

@@ -83,7 +83,7 @@ func TestStatuses(t *testing.T) {
} }
} }
SetStatusAwayIfNeeded(th.BasicUser2.Id) SetStatusAwayIfNeeded(th.BasicUser2.Id, false)
awayTimeout := *utils.Cfg.TeamSettings.UserStatusAwayTimeout awayTimeout := *utils.Cfg.TeamSettings.UserStatusAwayTimeout
defer func() { defer func() {
@@ -93,8 +93,8 @@ func TestStatuses(t *testing.T) {
time.Sleep(1500 * time.Millisecond) time.Sleep(1500 * time.Millisecond)
SetStatusAwayIfNeeded(th.BasicUser2.Id) SetStatusAwayIfNeeded(th.BasicUser2.Id, false)
SetStatusAwayIfNeeded(th.BasicUser2.Id) SetStatusAwayIfNeeded(th.BasicUser2.Id, false)
WebSocketClient2.Close() WebSocketClient2.Close()
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)

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

@@ -32,7 +32,7 @@ type WebConn struct {
} }
func NewWebConn(c *Context, ws *websocket.Conn) *WebConn { func NewWebConn(c *Context, ws *websocket.Conn) *WebConn {
go SetStatusOnline(c.Session.UserId, c.Session.Id) go SetStatusOnline(c.Session.UserId, c.Session.Id, false)
return &WebConn{ return &WebConn{
Send: make(chan model.WebSocketMessage, 64), Send: make(chan model.WebSocketMessage, 64),
@@ -55,7 +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 SetStatusAwayIfNeeded(c.UserId, false)
return nil return nil
}) })

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

@@ -100,7 +100,7 @@ func (h *Hub) Start() {
} }
if !found { if !found {
go SetStatusOffline(userId) go SetStatusOffline(userId, false)
} }
case userId := <-h.invalidateUser: case userId := <-h.invalidateUser:
for webCon := range h.connections { for webCon := range h.connections {

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

@@ -431,6 +431,42 @@
"id": "api.command.regen.app_error", "id": "api.command.regen.app_error",
"translation": "Inappropriate permissions to regenerate command token" "translation": "Inappropriate permissions to regenerate command token"
}, },
{
"id": "api.command_away.desc",
"translation": "Set your status away"
},
{
"id": "api.command_away.name",
"translation": "away"
},
{
"id": "api.command_away.success",
"translation": "You are now away"
},
{
"id": "api.command_online.desc",
"translation": "Set your status online"
},
{
"id": "api.command_online.name",
"translation": "online"
},
{
"id": "api.command_online.success",
"translation": "You are now online"
},
{
"id": "api.command_offline.desc",
"translation": "Set your status offline"
},
{
"id": "api.command_offline.name",
"translation": "offline"
},
{
"id": "api.command_offline.success",
"translation": "You are now offline"
},
{ {
"id": "api.command_collapse.desc", "id": "api.command_collapse.desc",
"translation": "Turn on auto-collapsing of image previews" "translation": "Turn on auto-collapsing of image previews"

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

@@ -18,6 +18,7 @@ const (
type Status struct { type Status struct {
UserId string `json:"user_id"` UserId string `json:"user_id"`
Status string `json:"status"` Status string `json:"status"`
Manual bool `json:"manual"`
LastActivityAt int64 `json:"last_activity_at"` LastActivityAt int64 `json:"last_activity_at"`
} }

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

@@ -9,7 +9,7 @@ import (
) )
func TestStatus(t *testing.T) { func TestStatus(t *testing.T) {
status := Status{NewId(), STATUS_ONLINE, 0} status := Status{NewId(), STATUS_ONLINE, true, 0}
json := status.ToJson() json := status.ToJson()
status2 := StatusFromJson(strings.NewReader(json)) status2 := StatusFromJson(strings.NewReader(json))
@@ -24,4 +24,8 @@ func TestStatus(t *testing.T) {
if status.LastActivityAt != status2.LastActivityAt { if status.LastActivityAt != status2.LastActivityAt {
t.Fatal("LastActivityAt should have matched") t.Fatal("LastActivityAt should have matched")
} }
if status.Manual != status2.Manual {
t.Fatal("Manual should have matched")
}
} }

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

@@ -5,6 +5,7 @@ package store
import ( import (
"database/sql" "database/sql"
"github.com/mattermost/platform/model" "github.com/mattermost/platform/model"
) )
@@ -22,12 +23,17 @@ func NewSqlStatusStore(sqlStore *SqlStore) StatusStore {
for _, db := range sqlStore.GetAllConns() { for _, db := range sqlStore.GetAllConns() {
table := db.AddTableWithName(model.Status{}, "Status").SetKeys(false, "UserId") table := db.AddTableWithName(model.Status{}, "Status").SetKeys(false, "UserId")
table.ColMap("UserId").SetMaxSize(26) table.ColMap("UserId").SetMaxSize(26)
table.ColMap("Manual")
table.ColMap("Status").SetMaxSize(32) table.ColMap("Status").SetMaxSize(32)
} }
return s return s
} }
func (s SqlStatusStore) UpgradeSchemaIfNeeded() {
s.CreateColumnIfNotExists("Status", "Manual", "BOOLEAN", "BOOLEAN", "0")
}
func (s SqlStatusStore) CreateIndexesIfNotExists() { func (s SqlStatusStore) CreateIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_status_user_id", "Status", "UserId") s.CreateIndexIfNotExists("idx_status_user_id", "Status", "UserId")
s.CreateIndexIfNotExists("idx_status_status", "Status", "Status") s.CreateIndexIfNotExists("idx_status_status", "Status", "Status")

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

@@ -4,14 +4,15 @@
package store package store
import ( import (
"github.com/mattermost/platform/model"
"testing" "testing"
"github.com/mattermost/platform/model"
) )
func TestSqlStatusStore(t *testing.T) { func TestSqlStatusStore(t *testing.T) {
Setup() Setup()
status := &model.Status{model.NewId(), model.STATUS_ONLINE, 0} status := &model.Status{model.NewId(), model.STATUS_ONLINE, false, 0}
if err := (<-store.Status().SaveOrUpdate(status)).Err; err != nil { if err := (<-store.Status().SaveOrUpdate(status)).Err; err != nil {
t.Fatal(err) t.Fatal(err)
@@ -27,12 +28,12 @@ func TestSqlStatusStore(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
status2 := &model.Status{model.NewId(), model.STATUS_AWAY, 0} status2 := &model.Status{model.NewId(), model.STATUS_AWAY, false, 0}
if err := (<-store.Status().SaveOrUpdate(status2)).Err; err != nil { if err := (<-store.Status().SaveOrUpdate(status2)).Err; err != nil {
t.Fatal(err) t.Fatal(err)
} }
status3 := &model.Status{model.NewId(), model.STATUS_OFFLINE, 0} status3 := &model.Status{model.NewId(), model.STATUS_OFFLINE, false, 0}
if err := (<-store.Status().SaveOrUpdate(status3)).Err; err != nil { if err := (<-store.Status().SaveOrUpdate(status3)).Err; err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -80,7 +81,7 @@ func TestSqlStatusStore(t *testing.T) {
func TestActiveUserCount(t *testing.T) { func TestActiveUserCount(t *testing.T) {
Setup() Setup()
status := &model.Status{model.NewId(), model.STATUS_ONLINE, model.GetMillis()} status := &model.Status{model.NewId(), model.STATUS_ONLINE, false, model.GetMillis()}
Must(store.Status().SaveOrUpdate(status)) Must(store.Status().SaveOrUpdate(status))
if result := <-store.Status().GetTotalActiveUsersCount(); result.Err != nil { if result := <-store.Status().GetTotalActiveUsersCount(); result.Err != nil {