diff --git a/plugin.json b/plugin.json index ff5289d46..184ce3638 100644 --- a/plugin.json +++ b/plugin.json @@ -134,6 +134,13 @@ "type": "bool", "help_text": "When set to 'true' you will get a notification with less details when a draft pull request is created and a notification with complete details when they are marked as ready for review. When set to 'false' no notifications are delivered for draft pull requests.", "default": false + }, + { + "key": "EnableStatusSync", + "display_name": "Enable Status Synchronization:", + "type": "bool", + "help_text": "(Optional) When enabled, users can opt-in to automatically sync their Mattermost 'Out of Office' status to GitHub. When a user sets their status to OOO in Mattermost, their GitHub status will be updated to 'Out of office' with busy indicator. The status is restored when they return. The feature respects manually set GitHub statuses and will not overwrite them.", + "default": false } ], "footer": "* To report an issue, make a suggestion or a contribution, [check the repository](https://github.com/mattermost/mattermost-plugin-github)." diff --git a/server/plugin/configuration.go b/server/plugin/configuration.go index 38dcc3dcf..902e9c037 100644 --- a/server/plugin/configuration.go +++ b/server/plugin/configuration.go @@ -43,6 +43,7 @@ type Configuration struct { UsePreregisteredApplication bool `json:"usepreregisteredapplication"` ShowAuthorInCommitNotification bool `json:"showauthorincommitnotification"` GetNotificationForDraftPRs bool `json:"getnotificationfordraftprs"` + EnableStatusSync bool `json:"enablestatussync"` } func (c *Configuration) ToMap() (map[string]interface{}, error) { 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..6b1e0c8ae 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -627,6 +627,12 @@ type UserSettings struct { Notifications bool `json:"notifications"` } +type GithubStatus struct { + Message string `json:"message"` + Emoji string `json:"emoji"` + Busy bool `json:"busy"` +} + func (p *Plugin) storeGitHubUserInfo(info *GitHubUserInfo) error { config := p.getConfiguration() @@ -711,6 +717,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 +1202,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 + config := p.getConfiguration() + if !config.EnableStatusSync { + return + } + + userInfo, apiErr := p.getGitHubUserInfo(userStatus.UserId) + if apiErr != nil { + return + } + + if userStatus.Status == "ooo" { + 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") + } + } +}