Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions server/plugin/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -802,10 +802,32 @@ func (p *Plugin) handleSettings(_ *plugin.Context, _ *model.CommandArgs, paramet
default:
return "Invalid value. Accepted values are: \"on\" or \"off\" or \"on-change\" ."
}
case settingVacationSync:
switch settingValue {
case settingOn:
userInfo.Settings.SyncVacationStatus = true
case settingOff:
userInfo.Settings.SyncVacationStatus = false
default:
return "Invalid value. Accepted values are: \"on\" or \"off\"."
}
default:
return "Unknown setting " + setting
}

if setting == settingVacationSync {
onVacation := false
if userInfo.Settings.SyncVacationStatus {
user, err := p.client.User.Get(userInfo.UserID)
if err != nil {
p.client.Log.Warn("Failed to get user for vacation status sync", "userID", userInfo.UserID, "error", err.Error())
} else {
onVacation = isOnVacation(user.GetCustomStatus())
}
}
p.applyVacationStatus(context.Background(), userInfo, onVacation)
}

if setting == settingNotifications {
if userInfo.Settings.Notifications {
err := p.storeGitHubToUserIDMapping(userInfo.GitHubUsername, userInfo.UserID)
Expand Down Expand Up @@ -1301,6 +1323,17 @@ func getAutocompleteData(config *Configuration) *model.AutocompleteData {
remainderNotifications.AddStaticListArgument("", true, settingValue)
settings.AddCommand(remainderNotifications)

settingVacationSyncData := model.NewAutocompleteData("vacation-sync", "", "Set your GitHub status to Busy while your Mattermost status is \"On a vacation\"")
settingValue = []model.AutocompleteListItem{{
HelpText: "Turn vacation sync on",
Item: "on",
}, {
HelpText: "Turn vacation sync off",
Item: "off",
}}
settingVacationSyncData.AddStaticListArgument("", true, settingValue)
settings.AddCommand(settingVacationSyncData)

github.AddCommand(settings)

setup := model.NewAutocompleteData("setup", "[command]", "Available commands: oauth, webhook, announcement")
Expand Down
43 changes: 43 additions & 0 deletions server/plugin/graphql/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package graphql

import (
"context"

"github.com/pkg/errors"
"github.com/shurcooL/githubv4"
)

type changeUserStatusMutation struct {
ChangeUserStatus struct {
Status struct {
Message githubv4.String
}
} `graphql:"changeUserStatus(input: $input)"`
}

// SetBusyStatus sets the user's GitHub status to Busy, which excludes them from automatic PR assignment.
func (c *Client) SetBusyStatus(ctx context.Context, message, emoji string) error {
return c.changeUserStatus(ctx, githubv4.ChangeUserStatusInput{
LimitedAvailability: githubv4.NewBoolean(true),
Message: githubv4.NewString(githubv4.String(message)),
Emoji: githubv4.NewString(githubv4.String(emoji)),
})
}

// ClearStatus removes the user's GitHub status, including the Busy indicator.
func (c *Client) ClearStatus(ctx context.Context) error {
return c.changeUserStatus(ctx, githubv4.ChangeUserStatusInput{
LimitedAvailability: githubv4.NewBoolean(false),
})
}

func (c *Client) changeUserStatus(ctx context.Context, input githubv4.ChangeUserStatusInput) error {
var m changeUserStatusMutation
if err := c.client.Mutate(ctx, &m, input, nil); err != nil {
return errors.Wrap(err, "error in changing user status")
}
return nil
}
4 changes: 3 additions & 1 deletion server/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const (
settingButtonsTeam = "team"
settingNotifications = "notifications"
settingReminders = "reminders"
settingVacationSync = "vacation-sync"
settingOn = "on"
settingOff = "off"
settingOnChange = "on-change"
Expand Down Expand Up @@ -544,7 +545,7 @@ func (p *Plugin) getOAuthConfig(privateAllowed bool) (*oauth2.Config, error) {
// means that asks scope for private repositories
repo = github.ScopeRepo
}
scopes := []string{string(repo), string(github.ScopeNotifications), string(github.ScopeReadOrg), string(github.ScopeAdminOrgHook)}
scopes := []string{string(repo), string(github.ScopeNotifications), string(github.ScopeReadOrg), string(github.ScopeAdminOrgHook), string(github.ScopeUser)}

if config.UsePreregisteredApplication {
p.client.Log.Debug("Using Chimera Proxy OAuth configuration")
Expand Down Expand Up @@ -655,6 +656,7 @@ type UserSettings struct {
DailyReminder bool `json:"daily_reminder"`
DailyReminderOnChange bool `json:"daily_reminder_on_change"`
Notifications bool `json:"notifications"`
SyncVacationStatus bool `json:"sync_vacation_status"`
}

func (p *Plugin) storeGitHubUserInfo(info *GitHubUserInfo, encryptionKey string) error {
Expand Down
3 changes: 2 additions & 1 deletion server/plugin/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,9 @@ Reviewers: {{range $i, $el := .RequestedReviewers -}} {{- if $i}}, {{end}}{{temp
"* `/github subscriptions delete owner[/repo]` - Unsubscribe the current channel from a repository\n" +
"* `/github me` - Display the connected GitHub account\n" +
"* `/github settings [setting] [value]` - Update your user settings\n" +
" * `setting` can be `notifications` or `reminders`\n" +
" * `setting` can be `notifications`, `reminders` or `vacation-sync`\n" +
" * `value` can be `on` or `off`\n" +
" * `vacation-sync` sets your GitHub status to Busy (so you are not auto-assigned PRs) while your Mattermost custom status is \"On a vacation\". Off by default.\n" +
"* `/github setup` - Setup your Github plugin\n" +
"* `/github mute` - Managed muted GitHub users. You'll not receive notifications for comments in your PRs and issues from those users.\n" +
" * `/github mute list` - list your muted GitHub users\n" +
Expand Down
97 changes: 97 additions & 0 deletions server/plugin/vacation_sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package plugin

import (
"context"
"encoding/json"
"time"

"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
)

const (
vacationSyncKeyPrefix = "vacation_sync_"
vacationSyncTimeout = 30 * time.Second
vacationCustomStatusIcon = "palm_tree"
vacationGitHubEmoji = ":palm_tree:"
vacationGitHubMessage = "On a vacation"
)

// vacationSyncState is stored per user so a GitHub status the plugin did not set is never cleared.
type vacationSyncState struct {
Applied bool `json:"applied"`
}

// PreferencesHaveChanged reacts to custom status changes: the server saves the "recent custom
// statuses" preference whenever a user sets a custom status, and the newest one is first.
func (p *Plugin) PreferencesHaveChanged(_ *plugin.Context, preferences []model.Preference) {
for _, preference := range preferences {
if preference.Category != model.PreferenceCategoryCustomStatus || preference.Name != model.PreferenceNameRecentCustomStatuses {
continue
}

var recent model.RecentCustomStatuses
if err := json.Unmarshal([]byte(preference.Value), &recent); err != nil {
p.client.Log.Warn("Failed to parse recent custom statuses", "userID", preference.UserId, "error", err.Error())
continue
}
if len(recent) == 0 {
continue
}

info, apiErr := p.getGitHubUserInfo(preference.UserId)
if apiErr != nil || info.Settings == nil || !info.Settings.SyncVacationStatus {
continue
}

ctx, cancel := context.WithTimeout(context.Background(), vacationSyncTimeout)
p.applyVacationStatus(ctx, info, isOnVacation(&recent[0]))
cancel()
}
}

func isOnVacation(cs *model.CustomStatus) bool {
if cs == nil || cs.Emoji != vacationCustomStatusIcon {
return false
}
return cs.ExpiresAt.IsZero() || cs.ExpiresAt.After(time.Now())
}

// applyVacationStatus sets the user's GitHub status to Busy when they go on vacation and clears it
// when they come back, but only if the plugin was the one that set it.
func (p *Plugin) applyVacationStatus(ctx context.Context, info *GitHubUserInfo, onVacation bool) {
key := vacationSyncKeyPrefix + info.UserID

var state vacationSyncState
if err := p.store.Get(key, &state); err != nil {
p.client.Log.Warn("Failed to get vacation sync state", "userID", info.UserID, "error", err.Error())
return
}

switch {
case onVacation && !state.Applied:
if err := p.graphQLConnect(info).SetBusyStatus(ctx, vacationGitHubMessage, vacationGitHubEmoji); err != nil {
p.client.Log.Warn("Failed to set GitHub status to busy", "userID", info.UserID, "error", err.Error())
p.CreateBotDMPost(info.UserID,
"Failed to set your GitHub status to Busy while you are on vacation. Your GitHub connection may be missing the `user` permission. Please reconnect your account using `/github disconnect` followed by `/github connect`.",
"custom_git_vacation_sync")
return
}
state.Applied = true
case !onVacation && state.Applied:
if err := p.graphQLConnect(info).ClearStatus(ctx); err != nil {
p.client.Log.Warn("Failed to clear GitHub busy status", "userID", info.UserID, "error", err.Error())
return
}
state.Applied = false
default:
return
}

if _, err := p.store.Set(key, state); err != nil {
p.client.Log.Warn("Failed to store vacation sync state", "userID", info.UserID, "error", err.Error())
}
}
Loading
Loading