From 275731578e72d2c6e12cfb2fc315d3446474faec Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 16 Jul 2018 15:49:26 -0400 Subject: MM-10254 Add plugin APIs for getting/updating user statuses (#9101) * Add plugin APIs for getting/updating user statuses * Add and update tests * Updates per feedback --- api4/post.go | 2 +- api4/status.go | 2 +- api4/status_test.go | 6 +-- api4/user_test.go | 2 +- api4/websocket_test.go | 2 +- app/apptestlib.go | 8 ++++ app/auto_responder.go | 2 +- app/auto_responder_test.go | 4 +- app/command_dnd.go | 2 +- app/command_online.go | 2 +- app/plugin_api.go | 26 +++++++++++++ app/plugin_api_test.go | 32 +++++++++++++++ app/status.go | 2 +- app/web_conn.go | 2 +- app/websocket_router.go | 2 +- plugin/api.go | 10 +++++ plugin/client_rpc_generated.go | 88 ++++++++++++++++++++++++++++++++++++++++++ plugin/mock_api_test.go | 77 +++++++++++++++++++++++++++++++++++- plugin/plugintest/api.go | 77 +++++++++++++++++++++++++++++++++++- plugin/plugintest/hooks.go | 2 +- 20 files changed, 332 insertions(+), 18 deletions(-) create mode 100644 app/plugin_api_test.go diff --git a/api4/post.go b/api4/post.go index 27ecd1584..b76e89964 100644 --- a/api4/post.go +++ b/api4/post.go @@ -64,7 +64,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - c.App.SetStatusOnline(c.Session.UserId, c.Session.Id, false) + c.App.SetStatusOnline(c.Session.UserId, false) c.App.UpdateLastActivityAtIfNeeded(c.Session) w.WriteHeader(http.StatusCreated) diff --git a/api4/status.go b/api4/status.go index 627ddaca6..30e2140f1 100644 --- a/api4/status.go +++ b/api4/status.go @@ -78,7 +78,7 @@ func updateUserStatus(c *Context, w http.ResponseWriter, r *http.Request) { switch status.Status { case "online": - c.App.SetStatusOnline(c.Params.UserId, "", true) + c.App.SetStatusOnline(c.Params.UserId, true) case "offline": c.App.SetStatusOffline(c.Params.UserId, true) case "away": diff --git a/api4/status_test.go b/api4/status_test.go index 7049bedef..9b3583c1e 100644 --- a/api4/status_test.go +++ b/api4/status_test.go @@ -17,7 +17,7 @@ func TestGetUserStatus(t *testing.T) { t.Fatal("Should return offline status") } - th.App.SetStatusOnline(th.BasicUser.Id, "", true) + th.App.SetStatusOnline(th.BasicUser.Id, true) userStatus, resp = Client.GetUserStatus(th.BasicUser.Id, "") CheckNoError(t, resp) if userStatus.Status != "online" { @@ -80,8 +80,8 @@ func TestGetUsersStatusesByIds(t *testing.T) { } } - th.App.SetStatusOnline(th.BasicUser.Id, "", true) - th.App.SetStatusOnline(th.BasicUser2.Id, "", true) + th.App.SetStatusOnline(th.BasicUser.Id, true) + th.App.SetStatusOnline(th.BasicUser2.Id, true) usersStatuses, resp = Client.GetUsersStatusesByIds(usersIds) CheckNoError(t, resp) for _, userStatus := range usersStatuses { diff --git a/api4/user_test.go b/api4/user_test.go index 4cbaf449a..78693e05f 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -1501,7 +1501,7 @@ func TestGetRecentlyActiveUsersInTeam(t *testing.T) { Client := th.Client teamId := th.BasicTeam.Id - th.App.SetStatusOnline(th.BasicUser.Id, "", true) + th.App.SetStatusOnline(th.BasicUser.Id, true) rusers, resp := Client.GetRecentlyActiveUsersInTeam(teamId, 0, 60, "") CheckNoError(t, resp) diff --git a/api4/websocket_test.go b/api4/websocket_test.go index 9e4c1da73..8ca194133 100644 --- a/api4/websocket_test.go +++ b/api4/websocket_test.go @@ -404,7 +404,7 @@ func TestWebSocketStatuses(t *testing.T) { time.Sleep(1500 * time.Millisecond) th.App.SetStatusAwayIfNeeded(th.BasicUser.Id, false) - th.App.SetStatusOnline(th.BasicUser.Id, "junk", false) + th.App.SetStatusOnline(th.BasicUser.Id, false) time.Sleep(1500 * time.Millisecond) diff --git a/app/apptestlib.go b/app/apptestlib.go index 12b01c5e5..48783f49c 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -463,6 +463,14 @@ func (me *TestHelper) SetupChannelScheme() *model.Scheme { } } +func (me *TestHelper) SetupPluginAPI() *PluginAPI { + manifest := &model.Manifest{ + Id: "pluginid", + } + + return NewPluginAPI(me.App, manifest) +} + type FakeClusterInterface struct { clusterMessageHandler einterfaces.ClusterMessageHandler } diff --git a/app/auto_responder.go b/app/auto_responder.go index aa7f243c4..a57a53f79 100644 --- a/app/auto_responder.go +++ b/app/auto_responder.go @@ -42,7 +42,7 @@ func (a *App) SetAutoResponderStatus(user *model.User, oldNotifyProps model.Stri if autoResponderEnabled { a.SetStatusOutOfOffice(user.Id) } else if autoResponderDisabled { - a.SetStatusOnline(user.Id, "", true) + a.SetStatusOnline(user.Id, true) } } diff --git a/app/auto_responder_test.go b/app/auto_responder_test.go index 65b466c92..f78bbc669 100644 --- a/app/auto_responder_test.go +++ b/app/auto_responder_test.go @@ -18,7 +18,7 @@ func TestSetAutoResponderStatus(t *testing.T) { user := th.CreateUser() defer th.App.PermanentDeleteUser(user) - th.App.SetStatusOnline(user.Id, "", true) + th.App.SetStatusOnline(user.Id, true) patch := &model.UserPatch{} patch.NotifyProps = make(map[string]string) @@ -57,7 +57,7 @@ func TestDisableAutoResponder(t *testing.T) { user := th.CreateUser() defer th.App.PermanentDeleteUser(user) - th.App.SetStatusOnline(user.Id, "", true) + th.App.SetStatusOnline(user.Id, true) patch := &model.UserPatch{} patch.NotifyProps = make(map[string]string) diff --git a/app/command_dnd.go b/app/command_dnd.go index cd3764fdf..96135194d 100644 --- a/app/command_dnd.go +++ b/app/command_dnd.go @@ -38,7 +38,7 @@ func (me *DndProvider) DoCommand(a *App, args *model.CommandArgs, message string return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command_dnd.error")} } else { if status.Status == "dnd" { - a.SetStatusOnline(args.UserId, args.Session.Id, true) + a.SetStatusOnline(args.UserId, true) return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command_dnd.disabled")} } } diff --git a/app/command_online.go b/app/command_online.go index 358bb8209..f5b14aff4 100644 --- a/app/command_online.go +++ b/app/command_online.go @@ -33,7 +33,7 @@ func (me *OnlineProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Comm } func (me *OnlineProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse { - a.SetStatusOnline(args.UserId, args.Session.Id, true) + a.SetStatusOnline(args.UserId, true) return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command_online.success")} } diff --git a/app/plugin_api.go b/app/plugin_api.go index 4130fc4b2..d7b116c0f 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -6,6 +6,7 @@ package app import ( "encoding/json" "fmt" + "net/http" "strings" "github.com/mattermost/mattermost-server/mlog" @@ -144,6 +145,31 @@ func (api *PluginAPI) UpdateUser(user *model.User) (*model.User, *model.AppError return api.app.UpdateUser(user, true) } +func (api *PluginAPI) GetUserStatus(userId string) (*model.Status, *model.AppError) { + return api.app.GetStatus(userId) +} + +func (api *PluginAPI) GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) { + return api.app.GetUserStatusesByIds(userIds) +} + +func (api *PluginAPI) UpdateUserStatus(userId, status string) (*model.Status, *model.AppError) { + switch status { + case model.STATUS_ONLINE: + api.app.SetStatusOnline(userId, true) + case model.STATUS_OFFLINE: + api.app.SetStatusOffline(userId, true) + case model.STATUS_AWAY: + api.app.SetStatusAwayIfNeeded(userId, true) + case model.STATUS_DND: + api.app.SetStatusDoNotDisturb(userId) + default: + return nil, model.NewAppError("UpdateUserStatus", "plugin.api.update_user_status.bad_status", nil, "unrecognized status", http.StatusBadRequest) + } + + return api.app.GetStatus(userId) +} + func (api *PluginAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { return api.app.CreateChannel(channel, false) } diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go new file mode 100644 index 000000000..56507a8f7 --- /dev/null +++ b/app/plugin_api_test.go @@ -0,0 +1,32 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" +) + +func TestPluginAPIUpdateUserStatus(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + api := th.SetupPluginAPI() + + statuses := []string{model.STATUS_ONLINE, model.STATUS_AWAY, model.STATUS_DND, model.STATUS_OFFLINE} + + for _, s := range statuses { + status, err := api.UpdateUserStatus(th.BasicUser.Id, s) + require.Nil(t, err) + require.NotNil(t, status) + assert.Equal(t, s, status.Status) + } + + status, err := api.UpdateUserStatus(th.BasicUser.Id, "notrealstatus") + assert.NotNil(t, err) + assert.Nil(t, status) +} diff --git a/app/status.go b/app/status.go index 16c43160d..bfeb5c77e 100644 --- a/app/status.go +++ b/app/status.go @@ -177,7 +177,7 @@ func (a *App) SetStatusLastActivityAt(userId string, activityAt int64) { a.SetStatusAwayIfNeeded(userId, false) } -func (a *App) SetStatusOnline(userId string, sessionId string, manual bool) { +func (a *App) SetStatusOnline(userId string, manual bool) { if !*a.Config().ServiceSettings.EnableUserStatuses { return } diff --git a/app/web_conn.go b/app/web_conn.go index dd01a8e31..47fae24c3 100644 --- a/app/web_conn.go +++ b/app/web_conn.go @@ -47,7 +47,7 @@ type WebConn struct { func (a *App) NewWebConn(ws *websocket.Conn, session model.Session, t goi18n.TranslateFunc, locale string) *WebConn { if len(session.UserId) > 0 { a.Go(func() { - a.SetStatusOnline(session.UserId, session.Id, false) + a.SetStatusOnline(session.UserId, false) a.UpdateLastActivityAtIfNeeded(session) }) } diff --git a/app/websocket_router.go b/app/websocket_router.go index 6c5e142a1..da5f03602 100644 --- a/app/websocket_router.go +++ b/app/websocket_router.go @@ -55,7 +55,7 @@ func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketReque conn.WebSocket.Close() } else { wr.app.Go(func() { - wr.app.SetStatusOnline(session.UserId, session.Id, false) + wr.app.SetStatusOnline(session.UserId, false) wr.app.UpdateLastActivityAtIfNeeded(*session) }) diff --git a/plugin/api.go b/plugin/api.go index 76df9377a..70a3e7ab5 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -49,6 +49,16 @@ type API interface { // UpdateUser updates a user. UpdateUser(user *model.User) (*model.User, *model.AppError) + // GetUserStatus will get a user's status. + GetUserStatus(userId string) (*model.Status, *model.AppError) + + // GetUserStatusesByIds will return a list of user statuses based on the provided slice of user IDs. + GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) + + // UpdateUserStatus will set a user's status until the user, or another integration/plugin, sets it back to online. + // The status parameter can be: "online", "away", "dnd", or "offline". + UpdateUserStatus(userId, status string) (*model.Status, *model.AppError) + // CreateTeam creates a team. CreateTeam(team *model.Team) (*model.Team, *model.AppError) diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index ab884e647..a96ff33e5 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -716,6 +716,94 @@ func (s *apiRPCServer) UpdateUser(args *Z_UpdateUserArgs, returns *Z_UpdateUserR return nil } +type Z_GetUserStatusArgs struct { + A string +} + +type Z_GetUserStatusReturns struct { + A *model.Status + B *model.AppError +} + +func (g *apiRPCClient) GetUserStatus(userId string) (*model.Status, *model.AppError) { + _args := &Z_GetUserStatusArgs{userId} + _returns := &Z_GetUserStatusReturns{} + if err := g.client.Call("Plugin.GetUserStatus", _args, _returns); err != nil { + g.log.Error("RPC call to GetUserStatus API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetUserStatus(args *Z_GetUserStatusArgs, returns *Z_GetUserStatusReturns) error { + if hook, ok := s.impl.(interface { + GetUserStatus(userId string) (*model.Status, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetUserStatus(args.A) + } else { + return fmt.Errorf("API GetUserStatus called but not implemented.") + } + return nil +} + +type Z_GetUserStatusesByIdsArgs struct { + A []string +} + +type Z_GetUserStatusesByIdsReturns struct { + A []*model.Status + B *model.AppError +} + +func (g *apiRPCClient) GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) { + _args := &Z_GetUserStatusesByIdsArgs{userIds} + _returns := &Z_GetUserStatusesByIdsReturns{} + if err := g.client.Call("Plugin.GetUserStatusesByIds", _args, _returns); err != nil { + g.log.Error("RPC call to GetUserStatusesByIds API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetUserStatusesByIds(args *Z_GetUserStatusesByIdsArgs, returns *Z_GetUserStatusesByIdsReturns) error { + if hook, ok := s.impl.(interface { + GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetUserStatusesByIds(args.A) + } else { + return fmt.Errorf("API GetUserStatusesByIds called but not implemented.") + } + return nil +} + +type Z_UpdateUserStatusArgs struct { + A string + B string +} + +type Z_UpdateUserStatusReturns struct { + A *model.Status + B *model.AppError +} + +func (g *apiRPCClient) UpdateUserStatus(userId, status string) (*model.Status, *model.AppError) { + _args := &Z_UpdateUserStatusArgs{userId, status} + _returns := &Z_UpdateUserStatusReturns{} + if err := g.client.Call("Plugin.UpdateUserStatus", _args, _returns); err != nil { + g.log.Error("RPC call to UpdateUserStatus API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) UpdateUserStatus(args *Z_UpdateUserStatusArgs, returns *Z_UpdateUserStatusReturns) error { + if hook, ok := s.impl.(interface { + UpdateUserStatus(userId, status string) (*model.Status, *model.AppError) + }); ok { + returns.A, returns.B = hook.UpdateUserStatus(args.A, args.B) + } else { + return fmt.Errorf("API UpdateUserStatus called but not implemented.") + } + return nil +} + type Z_CreateTeamArgs struct { A *model.Team } diff --git a/plugin/mock_api_test.go b/plugin/mock_api_test.go index 1ffa3aa46..07b1c1277 100644 --- a/plugin/mock_api_test.go +++ b/plugin/mock_api_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery v1.0.0 // Regenerate this file using `make plugin-mocks`. @@ -674,6 +674,56 @@ func (_m *MockAPI) GetUserByUsername(name string) (*model.User, *model.AppError) return r0, r1 } +// GetUserStatus provides a mock function with given fields: userId +func (_m *MockAPI) GetUserStatus(userId string) (*model.Status, *model.AppError) { + ret := _m.Called(userId) + + var r0 *model.Status + if rf, ok := ret.Get(0).(func(string) *model.Status); ok { + r0 = rf(userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Status) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(userId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetUserStatusesByIds provides a mock function with given fields: userIds +func (_m *MockAPI) GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) { + ret := _m.Called(userIds) + + var r0 []*model.Status + if rf, ok := ret.Get(0).(func([]string) []*model.Status); ok { + r0 = rf(userIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Status) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func([]string) *model.AppError); ok { + r1 = rf(userIds) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // KVDelete provides a mock function with given fields: key func (_m *MockAPI) KVDelete(key string) *model.AppError { ret := _m.Called(key) @@ -1016,3 +1066,28 @@ func (_m *MockAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) { return r0, r1 } + +// UpdateUserStatus provides a mock function with given fields: status, userId +func (_m *MockAPI) UpdateUserStatus(status string, userId string) (*model.Status, *model.AppError) { + ret := _m.Called(status, userId) + + var r0 *model.Status + if rf, ok := ret.Get(0).(func(string, string) *model.Status); ok { + r0 = rf(status, userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Status) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok { + r1 = rf(status, userId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index 3ce1d0145..06ab02560 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery v1.0.0 // Regenerate this file using `make plugin-mocks`. @@ -674,6 +674,56 @@ func (_m *API) GetUserByUsername(name string) (*model.User, *model.AppError) { return r0, r1 } +// GetUserStatus provides a mock function with given fields: userId +func (_m *API) GetUserStatus(userId string) (*model.Status, *model.AppError) { + ret := _m.Called(userId) + + var r0 *model.Status + if rf, ok := ret.Get(0).(func(string) *model.Status); ok { + r0 = rf(userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Status) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(userId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetUserStatusesByIds provides a mock function with given fields: userIds +func (_m *API) GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) { + ret := _m.Called(userIds) + + var r0 []*model.Status + if rf, ok := ret.Get(0).(func([]string) []*model.Status); ok { + r0 = rf(userIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Status) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func([]string) *model.AppError); ok { + r1 = rf(userIds) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // KVDelete provides a mock function with given fields: key func (_m *API) KVDelete(key string) *model.AppError { ret := _m.Called(key) @@ -1016,3 +1066,28 @@ func (_m *API) UpdateUser(user *model.User) (*model.User, *model.AppError) { return r0, r1 } + +// UpdateUserStatus provides a mock function with given fields: status, userId +func (_m *API) UpdateUserStatus(status string, userId string) (*model.Status, *model.AppError) { + ret := _m.Called(status, userId) + + var r0 *model.Status + if rf, ok := ret.Get(0).(func(string, string) *model.Status); ok { + r0 = rf(status, userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Status) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok { + r1 = rf(status, userId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} diff --git a/plugin/plugintest/hooks.go b/plugin/plugintest/hooks.go index d88792f58..61268c299 100644 --- a/plugin/plugintest/hooks.go +++ b/plugin/plugintest/hooks.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery v1.0.0 // Regenerate this file using `make plugin-mocks`. -- cgit v1.2.3-1-g7c22