diff --git a/plugin.json b/plugin.json index ff5289d46..5f90b2ed5 100644 --- a/plugin.json +++ b/plugin.json @@ -7,13 +7,13 @@ "icon_path": "assets/icon.svg", "min_server_version": "7.1.0", "server": { - "executables": { - "linux-amd64": "server/dist/plugin-linux-amd64", - "linux-arm64": "server/dist/plugin-linux-arm64", - "darwin-amd64": "server/dist/plugin-darwin-amd64", - "darwin-arm64": "server/dist/plugin-darwin-arm64", - "windows-amd64": "server/dist/plugin-windows-amd64.exe" - } + "executables": { + "linux-amd64": "server/dist/plugin-linux-amd64", + "linux-arm64": "server/dist/plugin-linux-arm64", + "darwin-amd64": "server/dist/plugin-darwin-amd64", + "darwin-arm64": "server/dist/plugin-darwin-arm64", + "windows-amd64": "server/dist/plugin-windows-amd64.exe" + } }, "webapp": { "bundle_path": "webapp/dist/main.js" @@ -138,4 +138,4 @@ ], "footer": "* To report an issue, make a suggestion or a contribution, [check the repository](https://github.com/mattermost/mattermost-plugin-github)." } -} +} \ No newline at end of file diff --git a/server/plugin/command.go b/server/plugin/command.go index 87f80692d..0f25afc54 100644 --- a/server/plugin/command.go +++ b/server/plugin/command.go @@ -771,6 +771,15 @@ func (p *Plugin) handleSettings(_ *plugin.Context, _ *model.CommandArgs, paramet default: return "Invalid value. Accepted values are: \"on\" or \"off\" or \"on-change\" ." } + case "status": + switch settingValue { + case settingOn: + userInfo.Settings.StatusSync = true + case settingOff: + userInfo.Settings.StatusSync = false + default: + return "Invalid value. Accepted values are: \"on\" or \"off\"." + } default: return "Unknown setting " + setting } diff --git a/server/plugin/command_test.go b/server/plugin/command_test.go index 3789baad1..d3534c0a6 100644 --- a/server/plugin/command_test.go +++ b/server/plugin/command_test.go @@ -1453,6 +1453,43 @@ func TestHandleSettings(t *testing.T) { }, expectedResult: "Unknown setting unknownSetting", }, + { + name: "Successfully set status to on", + parameters: []string{ + "status", settingOn, + }, + setup: func() { + mockKvStore.EXPECT().Set(userInfo.UserID+githubTokenKey, gomock.Any()).Return(true, nil).Times(1) + }, + assertions: func(result string) { + assert.Equal(t, result, "Settings updated.") + }, + expectedResult: "Settings updated.", + }, + { + name: "Successfully set status to off", + parameters: []string{ + "status", settingOff, + }, + setup: func() { + mockKvStore.EXPECT().Set(userInfo.UserID+githubTokenKey, gomock.Any()).Return(true, nil).Times(1) + }, + assertions: func(result string) { + assert.Equal(t, result, "Settings updated.") + }, + expectedResult: "Settings updated.", + }, + { + name: "Invalid setting value for status", + parameters: []string{ + "status", "invalid", + }, + setup: func() {}, + assertions: func(result string) { + assert.Equal(t, result, "Invalid value. Accepted values are: \"on\" or \"off\".") + }, + expectedResult: "Invalid value. Accepted values are: \"on\" or \"off\".", + }, { name: "Error while storing settings", parameters: []string{ diff --git a/server/plugin/graphql/client.go b/server/plugin/graphql/client.go index 53083b03a..d3f9e2b04 100644 --- a/server/plugin/graphql/client.go +++ b/server/plugin/graphql/client.go @@ -64,3 +64,52 @@ func (c *Client) executeQuery(ctx context.Context, qry interface{}, params map[s return nil } + +type changeUserStatusMutation struct { + ChangeUserStatus struct { + Status struct { + Message githubv4.String + Emoji githubv4.String + } + } `graphql:"changeUserStatus(input: $input)"` +} + +func (c *Client) UpdateUserStatus(ctx context.Context, emoji, message string, busy bool) (string, error) { + var mutation changeUserStatusMutation + input := githubv4.ChangeUserStatusInput{ + Emoji: githubv4.NewString(githubv4.String(emoji)), + Message: githubv4.NewString(githubv4.String(message)), + LimitedAvailability: githubv4.NewBoolean(githubv4.Boolean(busy)), + } + + err := c.client.Mutate(ctx, &mutation, input, nil) + if err != nil { + return "", errors.Wrap(err, "UpdateUserStatus mutate failed") + } + + return string(mutation.ChangeUserStatus.Status.Message), nil +} + +type getUserStatusQuery struct { + User struct { + Status struct { + Message githubv4.String + Emoji githubv4.String + LimitedAvailability githubv4.Boolean + } + } `graphql:"user(login: $login)"` +} + +func (c *Client) GetUserStatus(ctx context.Context, login string) (string, string, bool, error) { + var query getUserStatusQuery + variables := map[string]interface{}{ + "login": githubv4.String(login), + } + + err := c.client.Query(ctx, &query, variables) + if err != nil { + return "", "", false, errors.Wrap(err, "GetUserStatus query failed") + } + + return string(query.User.Status.Message), string(query.User.Status.Emoji), bool(query.User.Status.LimitedAvailability), nil +} diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 82ce05b6e..38020da58 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -625,6 +625,13 @@ type UserSettings struct { DailyReminder bool `json:"daily_reminder"` DailyReminderOnChange bool `json:"daily_reminder_on_change"` Notifications bool `json:"notifications"` + StatusSync bool `json:"status_sync"` +} + +type GithubStatus struct { + Message string `json:"message"` + Emoji string `json:"emoji"` + Busy bool `json:"busy"` } func (p *Plugin) storeGitHubUserInfo(info *GitHubUserInfo) error { @@ -711,6 +718,10 @@ func (p *Plugin) disconnectGitHubAccount(userID string) { p.client.Log.Warn("Failed to delete github token from KV store", "userID", userID, "error", err.Error()) } + if err := p.store.Delete(userID + "_github_status"); err != nil { + p.client.Log.Warn("Failed to delete github status from KV store", "userID", userID, "error", err.Error()) + } + user, err := p.client.User.Get(userID) if err != nil { p.client.Log.Warn("Failed to get user props", "userID", userID, "error", err.Error()) @@ -1192,3 +1203,71 @@ func (p *Plugin) handleRevokedToken(info *GitHubUserInfo) { p.disconnectGitHubAccount(info.UserID) p.CreateBotDMPost(info.UserID, "Your Github account was disconnected due to an invalid or revoked authorization token. Reconnect your account using the `/github connect` command.", "custom_git_revoked_token") } + +func (p *Plugin) UserStatusHasChanged(c *plugin.Context, userStatus *model.Status) { + // Check if status sync is enabled in configuration + + userInfo, apiErr := p.getGitHubUserInfo(userStatus.UserId) + if apiErr != nil { + return + } + + if !userInfo.Settings.StatusSync { + return + } + + if userStatus.Status == model.StatusOutOfOffice { + graphQLClient := p.graphQLConnect(userInfo) + message, emoji, busy, err := graphQLClient.GetUserStatus(context.Background(), userInfo.GitHubUsername) + if err != nil { + p.client.Log.Error("failed to get user status", "error", err) + return + } + + // Don't overwrite if GitHub status is already set to "out of office" (busy) + if busy { + p.client.Log.Debug("GitHub status is already set to busy/OOO, skipping update") + return + } + + githubStatus := &GithubStatus{ + Message: message, + Emoji: emoji, + Busy: busy, + } + + githubStatusJSON, err := json.Marshal(githubStatus) + if err != nil { + p.client.Log.Error("failed to marshal github status", "error", err) + return + } + + p.store.Set(userInfo.UserID+"_github_status", githubStatusJSON) + + _, err = graphQLClient.UpdateUserStatus(context.Background(), ":house_with_garden:", "Out of office", true) + if err != nil { + p.client.Log.Error("failed to update user status", "error", err) + return + } + + p.CreateBotDMPost(userInfo.UserID, "Your GitHub status has been updated to Out of office.", "custom_git_status_sync") + } else { + var oldStatus []byte + if err := p.store.Get(userInfo.UserID+"_github_status", &oldStatus); err == nil && len(oldStatus) > 0 { + var githubStatus GithubStatus + if err := json.Unmarshal(oldStatus, &githubStatus); err != nil { + p.client.Log.Error("failed to unmarshal github status", "error", err) + return + } + + graphQLClient := p.graphQLConnect(userInfo) + _, err := graphQLClient.UpdateUserStatus(context.Background(), githubStatus.Emoji, githubStatus.Message, githubStatus.Busy) + if err != nil { + p.client.Log.Error("failed to update user status", "error", err) + return + } + p.store.Delete(userInfo.UserID + "_github_status") + p.CreateBotDMPost(userInfo.UserID, "Your GitHub status has been restored.", "custom_git_status_sync") + } + } +} diff --git a/webapp/src/types/github_types.ts b/webapp/src/types/github_types.ts index 2bc95d5ea..12e31cc9e 100644 --- a/webapp/src/types/github_types.ts +++ b/webapp/src/types/github_types.ts @@ -59,6 +59,7 @@ export type UserSettingsData = { sidebar_buttons: string; daily_reminder: boolean; notifications: boolean; + status_sync?: boolean; } export type ConnectedData = {