diff --git a/app/app_iface.go b/app/app_iface.go index 4e822e271d..d9733e9ef4 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -587,6 +587,7 @@ type AppIface interface { GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) GetComplianceReports(page, perPage int) (model.Compliances, *model.AppError) GetCookieDomain() string + GetCustomStatus(userID string) (*model.CustomStatus, *model.AppError) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) GetDeletedChannels(teamID string, offset int, limit int, userID string) (model.ChannelList, *model.AppError) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index c7024708b4..1f4b8525d5 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -5449,6 +5449,28 @@ func (a *OpenTracingAppLayer) GetCookieDomain() string { return resultVar0 } +func (a *OpenTracingAppLayer) GetCustomStatus(userID string) (*model.CustomStatus, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCustomStatus") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetCustomStatus(userID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDefaultProfileImage") diff --git a/app/plugin_api.go b/app/plugin_api.go index dfbbc04ded..b21ca7d8d2 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -344,6 +344,18 @@ func (api *PluginAPI) SetUserStatusTimedDND(userID string, endTime int64) (*mode return api.app.GetStatus(userID) } +func (api *PluginAPI) UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError { + return api.app.SetCustomStatus(userID, customStatus) +} + +func (api *PluginAPI) RemoveUserCustomStatus(userID string) *model.AppError { + return api.app.RemoveCustomStatus(userID) +} + +func (api *PluginAPI) GetUserCustomStatus(userID string) (*model.CustomStatus, *model.AppError) { + return api.app.GetCustomStatus(userID) +} + func (api *PluginAPI) GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) { switch sortBy { case model.ChannelSortByUsername: diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index 2860aabac1..3397ed2f67 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -494,6 +494,59 @@ func TestPluginAPIGetUsersInTeam(t *testing.T) { } } +func TestPluginAPIUserCustomStatus(t *testing.T) { + th := Setup(t) + defer th.TearDown() + api := th.SetupPluginAPI() + + user1, err := th.App.CreateUser(th.Context, &model.User{ + Email: strings.ToLower(model.NewId()) + "success+test@example.com", + Username: "user_" + model.NewId(), + Password: "password", + }) + require.Nil(t, err) + defer th.App.PermanentDeleteUser(th.Context, user1) + + custom := &model.CustomStatus{ + Emoji: ":tada:", + Text: "honk", + } + + err = api.UpdateUserCustomStatus(user1.Id, custom) + assert.Nil(t, err) + userCs, err := th.App.GetCustomStatus(user1.Id) + assert.Nil(t, err) + assert.Equal(t, custom, userCs) + + custom.Text = "" + err = api.UpdateUserCustomStatus(user1.Id, custom) + assert.Nil(t, err) + userCs, err = th.App.GetCustomStatus(user1.Id) + assert.Nil(t, err) + assert.Equal(t, custom, userCs) + + custom.Text = "honk" + custom.Emoji = "" + err = api.UpdateUserCustomStatus(user1.Id, custom) + assert.Nil(t, err) + userCs, err = th.App.GetCustomStatus(user1.Id) + assert.Nil(t, err) + assert.Equal(t, custom, userCs) + + custom.Text = "" + err = api.UpdateUserCustomStatus(user1.Id, custom) + assert.NotNil(t, err) + assert.Equal(t, err.Error(), "SetCustomStatus: Failed to update the custom status. Please add either emoji or custom text status or both., ") + + // Remove custom status + err = api.RemoveUserCustomStatus(user1.Id) + assert.Nil(t, err) + var csClear *model.CustomStatus + userCs, err = th.App.GetCustomStatus(user1.Id) + assert.Nil(t, err) + assert.Equal(t, csClear, userCs) +} + func TestPluginAPIGetFile(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() @@ -715,7 +768,6 @@ func TestPluginAPILoadPluginConfiguration(t *testing.T) { ] }}`) require.NoError(t, err) - } func TestPluginAPILoadPluginConfigurationDefaults(t *testing.T) { @@ -755,7 +807,6 @@ func TestPluginAPILoadPluginConfigurationDefaults(t *testing.T) { }`) require.NoError(t, err) - } func TestPluginAPIGetPlugins(t *testing.T) { @@ -1084,6 +1135,7 @@ func pluginAPIHookTest(t *testing.T, th *TestHelper, fileName string, id string, if ret != "OK" { return errors.New(ret) } + return nil } @@ -1284,7 +1336,6 @@ func TestPluginCreateBot(t *testing.T) { Description: "bot2", }) require.NotNil(t, err) - } func TestPluginCreatePostWithUploadedFile(t *testing.T) { @@ -1748,6 +1799,7 @@ type MockSlashCommandProvider struct { func (*MockSlashCommandProvider) GetTrigger() string { return "mock" } + func (*MockSlashCommandProvider) GetCommand(a *App, T i18n.TranslateFunc) *model.Command { return &model.Command{ Trigger: "mock", @@ -1757,6 +1809,7 @@ func (*MockSlashCommandProvider) GetCommand(a *App, T i18n.TranslateFunc) *model DisplayName: "mock", } } + func (mscp *MockSlashCommandProvider) DoCommand(a *App, c *request.Context, args *model.CommandArgs, message string) *model.CommandResponse { mscp.Args = args mscp.Message = message @@ -1920,7 +1973,6 @@ func TestPluginAPIUpdateCommand(t *testing.T) { require.NoError(t, appErr) require.Equal(t, "anothernewtriggeragain", newCmd4.Trigger) require.Equal(t, team1.Id, newCmd4.TeamId) - } func TestPluginAPIIsEnterpriseReady(t *testing.T) { diff --git a/app/status.go b/app/status.go index b9ec12e480..2b21b4750f 100644 --- a/app/status.go +++ b/app/status.go @@ -406,6 +406,10 @@ func (a *App) UpdateDNDStatusOfUsers() { } func (a *App) SetCustomStatus(userID string, cs *model.CustomStatus) *model.AppError { + if cs == nil || (cs.Emoji == "" && cs.Text == "") { + return model.NewAppError("SetCustomStatus", "api.custom_status.set_custom_statuses.update.app_error", nil, "", http.StatusBadRequest) + } + user, err := a.GetUser(userID) if err != nil { return err @@ -439,6 +443,15 @@ func (a *App) RemoveCustomStatus(userID string) *model.AppError { return nil } +func (a *App) GetCustomStatus(userID string) (*model.CustomStatus, *model.AppError) { + user, err := a.GetUser(userID) + if err != nil { + return &model.CustomStatus{}, err + } + + return user.GetCustomStatus(), nil +} + func (a *App) addRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError { var newRCS model.RecentCustomStatuses diff --git a/app/status_test.go b/app/status_test.go index d2d8c0e7dd..c7f3a45a26 100644 --- a/app/status_test.go +++ b/app/status_test.go @@ -37,3 +37,30 @@ func TestSaveStatus(t *testing.T) { }) } } + +func TestCustomStatus(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + user := th.BasicUser + + cs := &model.CustomStatus{ + Emoji: ":smile:", + Text: "honk!", + } + + err := th.App.SetCustomStatus(user.Id, cs) + require.Nil(t, err, "failed to set custom status %v", err) + + csSaved, err := th.App.GetCustomStatus(user.Id) + require.Nil(t, err, "failed to get custom status after save %v", err) + require.Equal(t, cs, csSaved) + + err = th.App.RemoveCustomStatus(user.Id) + require.Nil(t, err, "failed to to clear custom status %v", err) + + var csClear *model.CustomStatus + csSaved, err = th.App.GetCustomStatus(user.Id) + require.Nil(t, err, "failed to get custom status after clear %v", err) + require.Equal(t, csClear, csSaved) +} diff --git a/i18n/en.json b/i18n/en.json index 9351d1a669..5bd952ecb5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1566,6 +1566,10 @@ "id": "api.custom_status.recent_custom_statuses.delete.app_error", "translation": "Failed to delete the recent status. Please try adding the status first or contact your system administrator for details." }, + { + "id": "api.custom_status.set_custom_statuses.update.app_error", + "translation": "Failed to update the custom status. Please add either emoji or custom text status or both." + }, { "id": "api.email.send_warn_metric_ack.failure.app_error", "translation": "Failure to send admin acknowledgment email" diff --git a/model/client4.go b/model/client4.go index 51ad590736..c416266273 100644 --- a/model/client4.go +++ b/model/client4.go @@ -6154,6 +6154,36 @@ func (c *Client4) UpdateUserStatus(userId string, userStatus *Status) (*Status, return &s, BuildResponse(r), nil } +// UpdateUserCustomStatus sets a user's custom status based on the provided user id string. +func (c *Client4) UpdateUserCustomStatus(userId string, userCustomStatus *CustomStatus) (*CustomStatus, *Response) { + r, err := c.DoApiPut(c.GetUserStatusRoute(userId)+"/custom", userCustomStatus.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CustomStatusFromJson(r.Body), BuildResponse(r) +} + +// RemoveUserCustomStatus remove a user's custom status based on the provided user id string. +func (c *Client4) RemoveUserCustomStatus(userId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetUserStatusRoute(userId) + "/custom") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// RemoveRecentUserCustomStatus remove a recent user's custom status based on the provided user id string. +func (c *Client4) RemoveRecentUserCustomStatus(userId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetUserStatusRoute(userId) + "/custom/recent") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + // Emoji Section // CreateEmoji will save an emoji to the server if the current user has permission diff --git a/model/user.go b/model/user.go index 271ccd8155..0b691c6cb9 100644 --- a/model/user.go +++ b/model/user.go @@ -621,6 +621,15 @@ func (u *User) SetCustomStatus(cs *CustomStatus) error { return nil } +func (u *User) GetCustomStatus() *CustomStatus { + var o *CustomStatus + + data := u.Props[UserPropsKeyCustomStatus] + _ = json.Unmarshal([]byte(data), &o) + + return o +} + func (u *User) ClearCustomStatus() { u.MakeNonNil() u.Props[UserPropsKeyCustomStatus] = "" diff --git a/plugin/api.go b/plugin/api.go index 23c1a1bd23..0f70ab566f 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -257,6 +257,19 @@ type API interface { // Minimum server version: 5.8 UpdateUserActive(userID string, active bool) *model.AppError + // UpdateUserCustomStatus will set a user's custom status until the user, or another integration/plugin, clear it or update the custom status. + // The custom status have two parameters: emoji icon and custom text. + // + // @tag User + // Minimum server version: 5.36 + UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError + + // RemoveUserCustomStatus will remove a user's custom status. + // + // @tag User + // Minimum server version: 5.36 + RemoveUserCustomStatus(userID string) *model.AppError + // GetUsersInChannel returns a page of users in a channel. Page counting starts at 0. // The sortBy parameter can be: "username" or "status". // diff --git a/plugin/api_timer_layer_generated.go b/plugin/api_timer_layer_generated.go index 15a4113144..111c059261 100644 --- a/plugin/api_timer_layer_generated.go +++ b/plugin/api_timer_layer_generated.go @@ -301,6 +301,20 @@ func (api *apiTimerLayer) UpdateUserActive(userID string, active bool) *model.Ap return _returnsA } +func (api *apiTimerLayer) UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError { + startTime := timePkg.Now() + _returnsA := api.apiImpl.UpdateUserCustomStatus(userID, customStatus) + api.recordTime(startTime, "UpdateUserCustomStatus", _returnsA == nil) + return _returnsA +} + +func (api *apiTimerLayer) RemoveUserCustomStatus(userID string) *model.AppError { + startTime := timePkg.Now() + _returnsA := api.apiImpl.RemoveUserCustomStatus(userID) + api.recordTime(startTime, "RemoveUserCustomStatus", _returnsA == nil) + return _returnsA +} + func (api *apiTimerLayer) GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) { startTime := timePkg.Now() _returnsA, _returnsB := api.apiImpl.GetUsersInChannel(channelID, sortBy, page, perPage) diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 68786a8dd4..06914133c6 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -1755,6 +1755,63 @@ func (s *apiRPCServer) UpdateUserActive(args *Z_UpdateUserActiveArgs, returns *Z return nil } +type Z_UpdateUserCustomStatusArgs struct { + A string + B *model.CustomStatus +} + +type Z_UpdateUserCustomStatusReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError { + _args := &Z_UpdateUserCustomStatusArgs{userID, customStatus} + _returns := &Z_UpdateUserCustomStatusReturns{} + if err := g.client.Call("Plugin.UpdateUserCustomStatus", _args, _returns); err != nil { + log.Printf("RPC call to UpdateUserCustomStatus API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) UpdateUserCustomStatus(args *Z_UpdateUserCustomStatusArgs, returns *Z_UpdateUserCustomStatusReturns) error { + if hook, ok := s.impl.(interface { + UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError + }); ok { + returns.A = hook.UpdateUserCustomStatus(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API UpdateUserCustomStatus called but not implemented.")) + } + return nil +} + +type Z_RemoveUserCustomStatusArgs struct { + A string +} + +type Z_RemoveUserCustomStatusReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) RemoveUserCustomStatus(userID string) *model.AppError { + _args := &Z_RemoveUserCustomStatusArgs{userID} + _returns := &Z_RemoveUserCustomStatusReturns{} + if err := g.client.Call("Plugin.RemoveUserCustomStatus", _args, _returns); err != nil { + log.Printf("RPC call to RemoveUserCustomStatus API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) RemoveUserCustomStatus(args *Z_RemoveUserCustomStatusArgs, returns *Z_RemoveUserCustomStatusReturns) error { + if hook, ok := s.impl.(interface { + RemoveUserCustomStatus(userID string) *model.AppError + }); ok { + returns.A = hook.RemoveUserCustomStatus(args.A) + } else { + return encodableError(fmt.Errorf("API RemoveUserCustomStatus called but not implemented.")) + } + return nil +} + type Z_GetUsersInChannelArgs struct { A string B string diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index 888441d528..57229c23a0 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -2918,6 +2918,22 @@ func (_m *API) RemoveTeamIcon(teamID string) *model.AppError { return r0 } +// RemoveUserCustomStatus provides a mock function with given fields: userID +func (_m *API) RemoveUserCustomStatus(userID string) *model.AppError { + ret := _m.Called(userID) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string) *model.AppError); ok { + r0 = rf(userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // RequestTrialLicense provides a mock function with given fields: requesterID, users, termsAccepted, receiveEmailsAccepted func (_m *API) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError { ret := _m.Called(requesterID, users, termsAccepted, receiveEmailsAccepted) @@ -3531,6 +3547,22 @@ func (_m *API) UpdateUserActive(userID string, active bool) *model.AppError { return r0 } +// UpdateUserCustomStatus provides a mock function with given fields: userID, customStatus +func (_m *API) UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError { + ret := _m.Called(userID, customStatus) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string, *model.CustomStatus) *model.AppError); ok { + r0 = rf(userID, customStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // UpdateUserStatus provides a mock function with given fields: userID, status func (_m *API) UpdateUserStatus(userID string, status string) (*model.Status, *model.AppError) { ret := _m.Called(userID, status)