generated from mattermost/mattermost-plugin-starter-template
-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[MM-19893] Outlook user status sync intial implementation (#18)
* `availability` slash command updates user status based on getSchedule * app-only working. batching needs to be done now * batch request works * get rid of AppLevelClient struct/interface * remove dead code * lint * move app-level user auth token logic into its own file * lint * make job cancelable through system console. rename status sync api methods * rename AppLevelClient to SuperuserClient * batch requests properly to handle > 400 users * change AllUsers name to UserIndex. handle getSchedule error. * clean up batch response unmarshaling * PR feedback * rename NewClient to MakeClient * rename EnableStatusSyncJob to EnableStatusSync * rename CallURLEncodedForm to CallFormPost * rename allUsers to userIndex * Move availability view constants to remote package * Implement UserIndex methods to access as a map * Remove call to status sync job in OnActivate * Remove redundant Call method on remote client interface * rename var * reorder methods * update mocks * update for PR feedback: * extract PluginAPI interface * refactor sync status code * write test for user status sync * comment on POC_initStatusSyncJob * type alias for remote AvailabilityView string
- Loading branch information
1 parent
bde3526
commit 6040591
Showing
47 changed files
with
1,761 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. | ||
// See License for license information. | ||
|
||
package api | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
|
||
"github.com/mattermost/mattermost-plugin-mscalendar/server/remote" | ||
"github.com/mattermost/mattermost-plugin-mscalendar/server/store" | ||
"github.com/mattermost/mattermost-plugin-mscalendar/server/utils" | ||
) | ||
|
||
const ( | ||
availabilityTimeWindowSize = 15 | ||
) | ||
|
||
func (api *api) SyncStatusForSingleUser(mattermostUserID string) (string, error) { | ||
return api.syncStatusForUsers([]string{mattermostUserID}) | ||
} | ||
|
||
func (api *api) SyncStatusForAllUsers() (string, error) { | ||
userIndex, err := api.UserStore.LoadUserIndex() | ||
if err != nil { | ||
if err.Error() == "not found" { | ||
return "No users found in user index", nil | ||
} | ||
return "", err | ||
} | ||
|
||
mmIDs := userIndex.GetMattermostUserIDs() | ||
return api.syncStatusForUsers(mmIDs) | ||
} | ||
|
||
func (api *api) syncStatusForUsers(mattermostUserIDs []string) (string, error) { | ||
fullUserIndex, err := api.UserStore.LoadUserIndex() | ||
if err != nil { | ||
if err.Error() == "not found" { | ||
return "No users found in user index", nil | ||
} | ||
return "", err | ||
} | ||
|
||
filteredUsers := store.UserIndex{} | ||
indexByMattermostUserID := fullUserIndex.ByMattermostID() | ||
|
||
for _, mattermostUserID := range mattermostUserIDs { | ||
if u, ok := indexByMattermostUserID[mattermostUserID]; ok { | ||
filteredUsers = append(filteredUsers, u) | ||
} | ||
} | ||
|
||
if len(filteredUsers) == 0 { | ||
return "No connected users found", nil | ||
} | ||
|
||
scheduleIDs := []string{} | ||
for _, u := range filteredUsers { | ||
scheduleIDs = append(scheduleIDs, u.Email) | ||
} | ||
|
||
schedules, err := api.GetUserAvailabilities(filteredUsers[0].RemoteID, scheduleIDs) | ||
if err != nil { | ||
return "", err | ||
} | ||
if len(schedules) == 0 { | ||
return "No schedule info found", nil | ||
} | ||
|
||
return api.setUserStatuses(filteredUsers, schedules, mattermostUserIDs) | ||
} | ||
|
||
func (api *api) setUserStatuses(filteredUsers store.UserIndex, schedules []*remote.ScheduleInformation, mattermostUserIDs []string) (string, error) { | ||
statuses, appErr := api.Dependencies.PluginAPI.GetUserStatusesByIds(mattermostUserIDs) | ||
if appErr != nil { | ||
return "", appErr | ||
} | ||
statusMap := map[string]string{} | ||
for _, s := range statuses { | ||
statusMap[s.UserId] = s.Status | ||
} | ||
|
||
usersByEmail := filteredUsers.ByEmail() | ||
var res string | ||
for _, s := range schedules { | ||
if s.Error != nil { | ||
api.Logger.Errorf("Error getting availability for %s: %s", s.ScheduleID, s.Error.ResponseCode) | ||
continue | ||
} | ||
|
||
userID := usersByEmail[s.ScheduleID].MattermostUserID | ||
status, ok := statusMap[userID] | ||
if !ok { | ||
continue | ||
} | ||
|
||
res = api.setUserStatusFromAvailability(userID, status, s.AvailabilityView) | ||
} | ||
if res != "" { | ||
return res, nil | ||
} | ||
|
||
return utils.JSONBlock(schedules), nil | ||
} | ||
|
||
func (api *api) GetUserAvailabilities(remoteUserID string, scheduleIDs []string) ([]*remote.ScheduleInformation, error) { | ||
client := api.MakeSuperuserClient() | ||
|
||
start := remote.NewDateTime(time.Now()) | ||
end := remote.NewDateTime(time.Now().Add(availabilityTimeWindowSize * time.Minute)) | ||
|
||
return client.GetSchedule(remoteUserID, scheduleIDs, start, end, availabilityTimeWindowSize) | ||
} | ||
|
||
func (api *api) setUserStatusFromAvailability(mattermostUserID, currentStatus string, av remote.AvailabilityView) string { | ||
currentAvailability := av[0] | ||
|
||
switch currentAvailability { | ||
case remote.AvailabilityViewFree: | ||
if currentStatus == "dnd" { | ||
api.PluginAPI.UpdateUserStatus(mattermostUserID, "online") | ||
return fmt.Sprintf("User is free. Setting user from %s to online.", currentStatus) | ||
} else { | ||
return fmt.Sprintf("User is free, and is already set to %s.", currentStatus) | ||
} | ||
case remote.AvailabilityViewTentative, remote.AvailabilityViewBusy: | ||
if currentStatus != "dnd" { | ||
api.PluginAPI.UpdateUserStatus(mattermostUserID, "dnd") | ||
return fmt.Sprintf("User is busy. Setting user from %s to dnd.", currentStatus) | ||
} else { | ||
return fmt.Sprintf("User is busy, and is already set to %s.", currentStatus) | ||
} | ||
case remote.AvailabilityViewOutOfOffice: | ||
if currentStatus != "offline" { | ||
api.PluginAPI.UpdateUserStatus(mattermostUserID, "offline") | ||
return fmt.Sprintf("User is out of office. Setting user from %s to offline", currentStatus) | ||
} else { | ||
return fmt.Sprintf("User is out of office, and is already set to %s.", currentStatus) | ||
} | ||
case remote.AvailabilityViewWorkingElsewhere: | ||
return fmt.Sprintf("User is working elsewhere. Pending implementation.") | ||
} | ||
|
||
return fmt.Sprintf("Availability view doesn't match %d", currentAvailability) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. | ||
// See License for license information. | ||
|
||
package api | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/golang/mock/gomock" | ||
|
||
"github.com/mattermost/mattermost-plugin-mscalendar/server/config" | ||
"github.com/mattermost/mattermost-plugin-mscalendar/server/remote" | ||
"github.com/mattermost/mattermost-plugin-mscalendar/server/remote/mock_remote" | ||
"github.com/mattermost/mattermost-plugin-mscalendar/server/store" | ||
"github.com/mattermost/mattermost-plugin-mscalendar/server/store/mock_store" | ||
"github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot/mock_bot" | ||
"github.com/mattermost/mattermost-plugin-mscalendar/server/utils/plugin_api/mock_plugin_api" | ||
"github.com/mattermost/mattermost-server/v5/model" | ||
) | ||
|
||
func TestSyncStatusForAllUsers(t *testing.T) { | ||
for name, tc := range map[string]struct { | ||
sched *remote.ScheduleInformation | ||
currentStatus string | ||
newStatus string | ||
}{ | ||
"User is free but dnd, mark user as online": { | ||
sched: &remote.ScheduleInformation{ | ||
ScheduleID: "[email protected]", | ||
AvailabilityView: "0", | ||
}, | ||
currentStatus: "dnd", | ||
newStatus: "online", | ||
}, | ||
"User is busy but online, mark as dnd": { | ||
sched: &remote.ScheduleInformation{ | ||
ScheduleID: "[email protected]", | ||
AvailabilityView: "2", | ||
}, | ||
currentStatus: "online", | ||
newStatus: "dnd", | ||
}, | ||
"User is free and online, do not change status": { | ||
sched: &remote.ScheduleInformation{ | ||
ScheduleID: "[email protected]", | ||
AvailabilityView: "0", | ||
}, | ||
currentStatus: "online", | ||
newStatus: "", | ||
}, | ||
"User is busy and dnd, do not change status": { | ||
sched: &remote.ScheduleInformation{ | ||
ScheduleID: "[email protected]", | ||
AvailabilityView: "2", | ||
}, | ||
currentStatus: "dnd", | ||
newStatus: "", | ||
}, | ||
} { | ||
t.Run(name, func(t *testing.T) { | ||
userStoreCtrl := gomock.NewController(t) | ||
defer userStoreCtrl.Finish() | ||
userStore := mock_store.NewMockUserStore(userStoreCtrl) | ||
|
||
oauthStoreCtrl := gomock.NewController(t) | ||
defer oauthStoreCtrl.Finish() | ||
oauthStore := mock_store.NewMockOAuth2StateStore(oauthStoreCtrl) | ||
|
||
subsStoreCtrl := gomock.NewController(t) | ||
defer subsStoreCtrl.Finish() | ||
subsStore := mock_store.NewMockSubscriptionStore(subsStoreCtrl) | ||
|
||
eventStoreCtrl := gomock.NewController(t) | ||
defer eventStoreCtrl.Finish() | ||
eventStore := mock_store.NewMockEventStore(eventStoreCtrl) | ||
|
||
conf := &config.Config{} | ||
|
||
posterCtrl := gomock.NewController(t) | ||
defer posterCtrl.Finish() | ||
poster := mock_bot.NewMockPoster(posterCtrl) | ||
|
||
loggerCtrl := gomock.NewController(t) | ||
defer loggerCtrl.Finish() | ||
logger := mock_bot.NewMockLogger(loggerCtrl) | ||
|
||
remoteCtrl := gomock.NewController(t) | ||
defer remoteCtrl.Finish() | ||
mockRemote := mock_remote.NewMockRemote(remoteCtrl) | ||
|
||
clientCtrl := gomock.NewController(t) | ||
defer clientCtrl.Finish() | ||
mockClient := mock_remote.NewMockClient(clientCtrl) | ||
|
||
pluginAPICtrl := gomock.NewController(t) | ||
defer pluginAPICtrl.Finish() | ||
mockPluginAPI := mock_plugin_api.NewMockPluginAPI(pluginAPICtrl) | ||
|
||
apiConfig := Config{ | ||
Config: conf, | ||
Dependencies: &Dependencies{ | ||
UserStore: userStore, | ||
OAuth2StateStore: oauthStore, | ||
SubscriptionStore: subsStore, | ||
EventStore: eventStore, | ||
Logger: logger, | ||
Poster: poster, | ||
Remote: mockRemote, | ||
PluginAPI: mockPluginAPI, | ||
}, | ||
} | ||
|
||
userStore.EXPECT().LoadUserIndex().Return(store.UserIndex{ | ||
&store.UserShort{ | ||
MattermostUserID: "some_mm_id", | ||
RemoteID: "some_remote_id", | ||
Email: "[email protected]", | ||
}, | ||
}, nil).AnyTimes() | ||
|
||
mockRemote.EXPECT().MakeSuperuserClient(context.Background()).Return(mockClient) | ||
|
||
mockClient.EXPECT().GetSchedule("some_remote_id", []string{"[email protected]"}, gomock.Any(), gomock.Any(), 15).Return([]*remote.ScheduleInformation{tc.sched}, nil) | ||
|
||
mockPluginAPI.EXPECT().GetUserStatusesByIds([]string{"some_mm_id"}).Return([]*model.Status{&model.Status{Status: tc.currentStatus, UserId: "some_mm_id"}}, nil) | ||
|
||
if tc.newStatus == "" { | ||
mockPluginAPI.EXPECT().UpdateUserStatus("some_mm_id", gomock.Any()).Times(0) | ||
} else { | ||
mockPluginAPI.EXPECT().UpdateUserStatus("some_mm_id", tc.newStatus).Times(1) | ||
} | ||
|
||
a := New(apiConfig, "") | ||
a.SyncStatusForAllUsers() | ||
}) | ||
} | ||
} |
Oops, something went wrong.