From 86611442bd7818cd31e8409a7855913b2d1c47ef Mon Sep 17 00:00:00 2001 From: mickmister Date: Fri, 13 Dec 2019 17:41:38 -0500 Subject: [PATCH 01/18] `availability` slash command updates user status based on getSchedule --- go.mod | 1 + go.sum | 2 + server/api/api.go | 4 + server/api/availability.go | 88 +++++++++++++++++++ server/api/oauth2.go | 1 + server/plugin/command/availability.go | 17 ++++ server/plugin/command/command.go | 2 + server/plugin/plugin.go | 1 + server/remote/client.go | 1 + server/remote/common.go | 4 +- server/remote/msgraph/get_me.go | 2 + server/remote/msgraph/get_schedule.go | 79 +++++++++++++++++ server/remote/schedule.go | 23 +++++ server/remote/user.go | 1 + server/store/store.go | 3 + server/store/user_store.go | 50 +++++++++++ server/testdata/get_me_response.json | 8 ++ .../testdata/get_schedule_response_busy.json | 39 ++++++++ .../testdata/get_schedule_response_free.json | 24 +++++ .../get_schedule_response_invalid_email.json | 12 +++ .../testdata/webhook_event_ notification.json | 18 ++++ 21 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 server/api/availability.go create mode 100644 server/plugin/command/availability.go create mode 100644 server/remote/msgraph/get_schedule.go create mode 100644 server/remote/schedule.go create mode 100644 server/testdata/get_me_response.json create mode 100644 server/testdata/get_schedule_response_busy.json create mode 100644 server/testdata/get_schedule_response_free.json create mode 100644 server/testdata/get_schedule_response_invalid_email.json create mode 100644 server/testdata/webhook_event_ notification.json diff --git a/go.mod b/go.mod index 2d4bb189..9c4914af 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/jarcoal/httpmock v1.0.4 github.com/mattermost/mattermost-server/v5 v5.18.0-rc.test github.com/pkg/errors v0.8.1 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.4.0 github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 diff --git a/go.sum b/go.sum index 4feb8da8..61841499 100644 --- a/go.sum +++ b/go.sum @@ -343,6 +343,7 @@ github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmq github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= @@ -369,6 +370,7 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2 h1:37LbK2gAU+1oaWKC5NTz+fNOsR2LgdRj/SAFVMucgss= github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2/go.mod h1:tso14hwzqX4VbnWTNsxiL0DvMb2OwbGISFA7jDibdWc= +github.com/yaegashi/msgraph.go v0.0.0-20191206184644-860e82e7ce3b h1:cDzhOBSEXM4yhv5oBG13+PxlVhIzwtcS5affFh7VNJk= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= diff --git a/server/api/api.go b/server/api/api.go index a3291e29..4fd28796 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -7,6 +7,8 @@ import ( "context" "time" + "github.com/mattermost/mattermost-server/v5/plugin" + "github.com/mattermost/mattermost-plugin-msoffice/server/config" "github.com/mattermost/mattermost-plugin-msoffice/server/remote" "github.com/mattermost/mattermost-plugin-msoffice/server/store" @@ -20,6 +22,7 @@ type OAuth2 interface { type Subscriptions interface { CreateUserEventSubscription() (*store.Subscription, error) + GetUserAvailability() (string, error) RenewUserEventSubscription() (*store.Subscription, error) DeleteOrphanedSubscription(ID string) error DeleteUserEventSubscription() error @@ -65,6 +68,7 @@ type Dependencies struct { Poster bot.Poster Remote remote.Remote IsAuthorizedAdmin func(userId string) (bool, error) + API plugin.API } type Config struct { diff --git a/server/api/availability.go b/server/api/availability.go new file mode 100644 index 00000000..04cb5bd6 --- /dev/null +++ b/server/api/availability.go @@ -0,0 +1,88 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package api + +import ( + "time" + + "github.com/mattermost/mattermost-plugin-msoffice/server/remote" + "github.com/mattermost/mattermost-plugin-msoffice/server/utils" +) + +const ( + AVAILABILITY_VIEW_FREE = '0' + AVAILABILITY_VIEW_TENTATIVE = '1' + AVAILABILITY_VIEW_BUSY = '2' + AVAILABILITY_VIEW_OUT_OF_OFFICE = '3' + AVAILABILITY_VIEW_WORKING_ELSEWHERE = '4' +) + +func (api *api) GetUserAvailability() (string, error) { + client, err := api.MakeClient() + if err != nil { + return "", err + } + + users, err := api.UserStore.LoadAllUsers() + if err != nil { + return "", err + } + + scheduleIDs := []string{} + for _, u := range users { + scheduleIDs = append(scheduleIDs, u.Email) + } + + start := remote.NewDateTime(time.Now()) + end := remote.NewDateTime(time.Now().Add(15 * time.Minute)) + timeWindow := 15 // minutes + sched, err := client.GetSchedule(scheduleIDs, start, end, timeWindow) + if err != nil { + return "", err + } + + userID := users[0].MattermostUserID + av := sched[0].AvailabilityView + + setUserStatusFromAvailability(api, userID, av[0]) + + return utils.JSONBlock(sched), err +} + +func setUserStatusFromAvailability(api *api, mattermostUserID string, av byte) { + currentStatus, _ := api.API.GetUserStatus(mattermostUserID) + + switch av { + case AVAILABILITY_VIEW_FREE: + if currentStatus.Status == "dnd" { + api.Logger.Debugf("Setting user to online") + api.API.UpdateUserStatus(mattermostUserID, "online") + } else { + api.Logger.Debugf("User is already online") + } + case AVAILABILITY_VIEW_TENTATIVE, AVAILABILITY_VIEW_BUSY: + if currentStatus.Status != "dnd" { + api.Logger.Debugf("Setting user to dnd") + api.API.UpdateUserStatus(mattermostUserID, "dnd") + } else { + api.Logger.Debugf("User is already dnd") + } + case AVAILABILITY_VIEW_OUT_OF_OFFICE: + if currentStatus.Status != "offline" { + api.Logger.Debugf("Setting user to out of office") + api.API.UpdateUserStatus(mattermostUserID, "offline") + } else { + api.Logger.Debugf("User is already offline") + } + case AVAILABILITY_VIEW_WORKING_ELSEWHERE: + if currentStatus.Status != "dnd" { + api.Logger.Debugf("Setting user to working elsewhere") + api.API.UpdateUserStatus(mattermostUserID, "online") + } else { + api.Logger.Debugf("User is already online") + } + default: + api.Logger.Debugf("Availability view doesn't match", "av", av) + } +} diff --git a/server/api/oauth2.go b/server/api/oauth2.go index 9b89fdc7..1f74b9aa 100644 --- a/server/api/oauth2.go +++ b/server/api/oauth2.go @@ -60,6 +60,7 @@ func (api *api) CompleteOAuth2(authedUserID, code, state string) error { u := &store.User{ PluginVersion: api.Config.PluginVersion, MattermostUserID: mattermostUserID, + Email: me.Mail, Remote: me, OAuth2Token: tok, } diff --git a/server/plugin/command/availability.go b/server/plugin/command/availability.go new file mode 100644 index 00000000..ca20d946 --- /dev/null +++ b/server/plugin/command/availability.go @@ -0,0 +1,17 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package command + +func (c *Command) availability(parameters ...string) (string, error) { + switch { + case len(parameters) == 0: + resString, err := c.API.GetUserAvailability() + if err != nil { + return "", err + } + + return resString, nil + } + return "bad syntax", nil +} diff --git a/server/plugin/command/command.go b/server/plugin/command/command.go index 1a5183d2..39705fdb 100644 --- a/server/plugin/command/command.go +++ b/server/plugin/command/command.go @@ -77,6 +77,8 @@ func (c *Command) Handle() (string, error) { handler = c.findMeetings case "showcals": handler = c.showCalendars + case "availability": + handler = c.availability } out, err := handler(parameters...) if err != nil { diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 67229e2c..bc083c3a 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -175,6 +175,7 @@ func (p *Plugin) newAPIConfig() api.Config { Logger: bot, Poster: bot, Remote: remote.Makers[msgraph.Kind](conf, bot), + API: p.API, }, } } diff --git a/server/remote/client.go b/server/remote/client.go index 8c44dbfa..d2a49ecb 100644 --- a/server/remote/client.go +++ b/server/remote/client.go @@ -18,6 +18,7 @@ type Client interface { GetMe() (*User, error) GetNotificationData(*Notification) (*Notification, error) GetUserCalendars(userID string) ([]*Calendar, error) + GetSchedule(scheduleIDs []string, startTime, endTime *DateTime, availabilityViewInterval int) ([]*ScheduleInformation, error) GetUserDefaultCalendarView(userID string, startTime, endTime time.Time) ([]*Event, error) GetUserEvent(userID, eventID string) (*Event, error) ListSubscriptions() ([]*Subscription, error) diff --git a/server/remote/common.go b/server/remote/common.go index ce4cc9bf..b305a02e 100644 --- a/server/remote/common.go +++ b/server/remote/common.go @@ -19,7 +19,9 @@ const RFC3339NanoNoTimezone = "2006-01-02T15:04:05.999999999" func NewDateTime(t time.Time) *DateTime { return &DateTime{ DateTime: t.Format(RFC3339NanoNoTimezone), - TimeZone: t.Format("MST"), + // TimeZone: t.Format("MST"), + TimeZone: "Eastern Standard Time", + } } diff --git a/server/remote/msgraph/get_me.go b/server/remote/msgraph/get_me.go index ef269997..8cbeca26 100644 --- a/server/remote/msgraph/get_me.go +++ b/server/remote/msgraph/get_me.go @@ -14,6 +14,8 @@ func (c *client) GetMe() (*remote.User, error) { ID: *graphUser.ID, DisplayName: *graphUser.DisplayName, UserPrincipalName: *graphUser.UserPrincipalName, + Mail: *graphUser.Mail, } + return user, nil } diff --git a/server/remote/msgraph/get_schedule.go b/server/remote/msgraph/get_schedule.go new file mode 100644 index 00000000..69d676c5 --- /dev/null +++ b/server/remote/msgraph/get_schedule.go @@ -0,0 +1,79 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package msgraph + +import ( + "io/ioutil" + "encoding/json" + + "github.com/mattermost/mattermost-plugin-msoffice/server/remote" + + msgraph "github.com/yaegashi/msgraph.go/v1.0" +) + +type GetScheduleRequest struct { + // List of emails of users that we want to check + ScheduleIDs []string `json:"schedules"` + + // Overall start and end of entire search window + StartTime remote.DateTime `json:"startTime"` + EndTime remote.DateTime `json:"endTime"` + + // Size of each chunk of time we want to check + // This can be equal to end - start if we want, or we can get more granular results by making it shorter. + // For the graph API: The default is 30 minutes, minimum is 6, maximum is 1440 + // 15 is currently being used on our end + AvailabilityViewInterval int `json:"availabilityViewInterval"` +} + +func (c *client) GetSchedule(scheduleIDs []string, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { + req := &msgraph.CalendarGetScheduleRequestParameter{ + Schedules: scheduleIDs, + StartTime: &msgraph.DateTimeTimeZone{ + DateTime: &startTime.DateTime, + TimeZone: &startTime.TimeZone, + }, + EndTime: &msgraph.DateTimeTimeZone{ + DateTime: &endTime.DateTime, + TimeZone: &endTime.TimeZone, + }, + AvailabilityViewInterval: &availabilityViewInterval, + } + + // req := &GetScheduleRequest{ + // Schedules: scheduleIDs, + // StartTime: startTime, + // EndTime: endTime, + // AvailabilityViewInterval: availabilityViewInterval, + // } + + r2 := c.rbuilder.Me().Calendar().GetSchedule(req).Request() + r, err := r2.NewJSONRequest("POST", "", req) + if err != nil { + return nil, err + } + r = r.WithContext(c.ctx) + res, err := r2.Client().Do(r) + + if err != nil { + return nil, err + } + + var resBody remote.GetScheduleResponse + b, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(b, &resBody) + if err != nil { + return nil, err + } + + if err != nil { + return nil, err + } + + return resBody.Value, nil +} diff --git a/server/remote/schedule.go b/server/remote/schedule.go new file mode 100644 index 00000000..fca62cbe --- /dev/null +++ b/server/remote/schedule.go @@ -0,0 +1,23 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package remote + +// ScheduleInformation undocumented +type ScheduleInformation struct { + // Email of user + ScheduleID string `json:"scheduleId,omitempty"` + + // 0= free, 1= tentative, 2= busy, 3= out of office, 4= working elsewhere. + // example "0010", which means free for first and second block, tentative for third, and free for fourth + AvailabilityView string `json:"availabilityView,omitempty"` + + // ScheduleItems []interface{} `json:"scheduleItems,omitempty"` + // WorkingHours interface{} `json:"workingHours,omitempty"` + // Error *FreeBusyError `json:"error,omitempty"` +} + +type GetScheduleResponse struct { + Value []*ScheduleInformation `json:"value,omitempty"` +} + diff --git a/server/remote/user.go b/server/remote/user.go index 94a7d6a9..d02335ed 100644 --- a/server/remote/user.go +++ b/server/remote/user.go @@ -7,4 +7,5 @@ type User struct { ID string `json:"id"` DisplayName string `json:"displayName,omitempty"` UserPrincipalName string `json:"userPrincipalName,omitempty"` + Mail string `json:"mail,omitempty"` } diff --git a/server/store/store.go b/server/store/store.go index 999b5273..8cd5503a 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -14,6 +14,7 @@ import ( const ( UserKeyPrefix = "user_" + AllUsersKeyPrefix = "allusers_" MattermostUserIDKeyPrefix = "mmuid_" OAuth2KeyPrefix = "oauth2_" SubscriptionKeyPrefix = "sub_" @@ -36,6 +37,7 @@ type pluginStore struct { oauth2KV kvstore.KVStore userKV kvstore.KVStore mattermostUserIDKV kvstore.KVStore + allUsersKV kvstore.KVStore subscriptionKV kvstore.KVStore eventKV kvstore.KVStore Logger bot.Logger @@ -46,6 +48,7 @@ func NewPluginStore(api plugin.API, logger bot.Logger) Store { return &pluginStore{ basicKV: basicKV, userKV: kvstore.NewHashedKeyStore(basicKV, UserKeyPrefix), + allUsersKV: kvstore.NewHashedKeyStore(basicKV, AllUsersKeyPrefix), mattermostUserIDKV: kvstore.NewHashedKeyStore(basicKV, MattermostUserIDKeyPrefix), subscriptionKV: kvstore.NewHashedKeyStore(basicKV, SubscriptionKeyPrefix), eventKV: kvstore.NewHashedKeyStore(basicKV, EventKeyPrefix), diff --git a/server/store/user_store.go b/server/store/user_store.go index 06819e9d..61dfb33d 100644 --- a/server/store/user_store.go +++ b/server/store/user_store.go @@ -14,14 +14,23 @@ import ( type UserStore interface { LoadUser(mattermostUserId string) (*User, error) LoadMattermostUserId(remoteUserId string) (string, error) + LoadAllUsers() ([]*UserShort, error) StoreUser(user *User) error DeleteUser(mattermostUserId string) error } + +type UserShort struct { + MattermostUserID string `json:"mm_id"` + RemoteID string `json:"remote_id"` + Email string `json:"email"` +} + type User struct { PluginVersion string Remote *remote.User MattermostUserID string + Email string `json:"mattermostSettings,omitempty"` OAuth2Token *oauth2.Token Settings Settings `json:"mattermostSettings,omitempty"` } @@ -55,6 +64,15 @@ func (s *pluginStore) LoadMattermostUserId(remoteUserId string) (string, error) return string(data), nil } +func (s *pluginStore) LoadAllUsers() ([]*UserShort, error) { + users := []*UserShort{} + err := kvstore.LoadJSON(s.allUsersKV, "", &users) + if err != nil { + return nil, err + } + return users, nil +} + func (s *pluginStore) StoreUser(user *User) error { err := kvstore.StoreJSON(s.userKV, user.MattermostUserID, user) if err != nil { @@ -67,6 +85,38 @@ func (s *pluginStore) StoreUser(user *User) error { return err } + var allUsers []*UserShort + err = kvstore.LoadJSON(s.allUsersKV, "", &allUsers) + if err != nil { + allUsers = []*UserShort{} + } + + newUser := &UserShort{ + MattermostUserID: user.MattermostUserID, + RemoteID: user.Remote.ID, + Email: user.Email, + } + + found := false + filtered := []*UserShort{} + for _, u := range allUsers { + if u.MattermostUserID == user.MattermostUserID && u.RemoteID == user.Remote.ID { + found = true + filtered = append(filtered, newUser) + } else { + filtered = append(filtered, u) + } + } + + if !found { + filtered = append(filtered, newUser) + } + + err = kvstore.StoreJSON(s.allUsersKV, "", &filtered) + if err != nil { + return err + } + return nil } diff --git a/server/testdata/get_me_response.json b/server/testdata/get_me_response.json new file mode 100644 index 00000000..3277693f --- /dev/null +++ b/server/testdata/get_me_response.json @@ -0,0 +1,8 @@ +{ + "id": "fb10ac13-e441-4611-8431-8ee3b6403673", + "displayName": "Test1 User1", + "givenName": "First test", + "mail": "test1@mattermost.onmicrosoft.com", + "surname": "Last test", + "userPrincipalName": "test1@mattermost.onmicrosoft.com" +} diff --git a/server/testdata/get_schedule_response_busy.json b/server/testdata/get_schedule_response_busy.json new file mode 100644 index 00000000..8fd14a42 --- /dev/null +++ b/server/testdata/get_schedule_response_busy.json @@ -0,0 +1,39 @@ +{ + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.scheduleInformation)", + "value": [ + { + "scheduleId": "test1@mattermost.onmicrosoft.com", + "availabilityView": "22", + "scheduleItems": [ + { + "isPrivate": false, + "status": "busy", + "subject": "Busy Event", + "location": "", + "start": { + "dateTime": "2019-12-13T22:00:00.0000000", + "timeZone": "UTC" + }, + "end": { + "dateTime": "2019-12-13T23:00:00.0000000", + "timeZone": "UTC" + } + } + ], + "workingHours": { + "daysOfWeek": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday" + ], + "startTime": "08:00:00.0000000", + "endTime": "17:00:00.0000000", + "timeZone": { + "name": "Pacific Standard Time" + } + } + } +] +} diff --git a/server/testdata/get_schedule_response_free.json b/server/testdata/get_schedule_response_free.json new file mode 100644 index 00000000..60fdfb34 --- /dev/null +++ b/server/testdata/get_schedule_response_free.json @@ -0,0 +1,24 @@ +{ + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.scheduleInformation)", + "value": [ + { + "scheduleId": "test1@mattermost.onmicrosoft.com", + "availabilityView": "00", + "scheduleItems": [], + "workingHours": { + "daysOfWeek": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday" + ], + "startTime": "08:00:00.0000000", + "endTime": "17:00:00.0000000", + "timeZone": { + "name": "Pacific Standard Time" + } + } + } + ] +} diff --git a/server/testdata/get_schedule_response_invalid_email.json b/server/testdata/get_schedule_response_invalid_email.json new file mode 100644 index 00000000..7bd2a56d --- /dev/null +++ b/server/testdata/get_schedule_response_invalid_email.json @@ -0,0 +1,12 @@ +{ + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.scheduleInformation)", + "value": [ + { + "scheduleId": "fb10ac13-e441-4611-8431-8ee3b6403673", + "error": { + "message": "Microsoft.Exchange.InfoWorker.Common.Availability.InvalidSmtpAddressException: The email address provided is not a valid SMTP address.\r\n. Name of the server where exception originated: CY4PR2201MB1333. LID: 43916", + "responseCode": "ErrorInvalidSmtpAddress" + } + } + ] +} diff --git a/server/testdata/webhook_event_ notification.json b/server/testdata/webhook_event_ notification.json new file mode 100644 index 00000000..4b1d33b2 --- /dev/null +++ b/server/testdata/webhook_event_ notification.json @@ -0,0 +1,18 @@ +{ + "value": [ + { + "subscriptionId": "47b6cd58-254e-4c4b-a21c-b6d107c6a303", + "subscriptionExpirationDateTime": "2019-12-11T02:47:21+00:00", + "tenantId": "7419f71d-0b07-4d0a-89b8-3a4be2ec8627", + "changeType": "updated", + "resource": "Users/fb10ac13-e441-4611-8431-8ee3b6403673/Events/AAMkADcwOTc5YzBkLWE1MmUtNDNiOC05YzY4LWM4ZWU0YjQxZjgzZABGAAAAAAAnHVHuz_bvSbx04Ybn9aeIBwAUwKIBYedWTJ4asFPDXY-xAAAAAAENAAAUwKIBYedWTJ4asFPDXY-xAAAC8-8-AAA=", + "resourceData": { + "@odata.type": "#Microsoft.Graph.Event", + "@odata.id": "Users/fb10ac13-e441-4611-8431-8ee3b6403673/Events/AAMkADcwOTc5YzBkLWE1MmUtNDNiOC05YzY4LWM4ZWU0YjQxZjgzZABGAAAAAAAnHVHuz_bvSbx04Ybn9aeIBwAUwKIBYedWTJ4asFPDXY-xAAAAAAENAAAUwKIBYedWTJ4asFPDXY-xAAAC8-8-AAA=", + "@odata.etag": "W/\"DwAAABYAAAAUwKIBYedWTJ4asFPDXY/xAAAC8RJ0\"", + "id": "AAMkADcwOTc5YzBkLWE1MmUtNDNiOC05YzY4LWM4ZWU0YjQxZjgzZABGAAAAAAAnHVHuz_bvSbx04Ybn9aeIBwAUwKIBYedWTJ4asFPDXY-xAAAAAAENAAAUwKIBYedWTJ4asFPDXY-xAAAC8-8-AAA=" + }, + "clientState": null + } + ] +} From c4696c7111f7104723500ba5805b98210925b5d1 Mon Sep 17 00:00:00 2001 From: mickmister Date: Sun, 15 Dec 2019 12:24:39 -0500 Subject: [PATCH 02/18] app-only working. batching needs to be done now --- server/api/api.go | 4 ++ server/api/availability.go | 37 ++++++++++++++--- server/job/recurring_job.go | 5 +++ server/plugin/plugin.go | 7 +++- server/remote/client.go | 12 ++++-- server/remote/msgraph/batch_request.go | 41 +++++++++++++++++++ server/remote/msgraph/call.go | 37 +++++++++++++---- server/remote/msgraph/client.go | 12 ++++++ .../remote/msgraph/get_notification_data.go | 2 +- server/remote/msgraph/get_schedule.go | 14 +++---- server/remote/msgraph/get_schedule_batched.go | 32 +++++++++++++++ server/remote/msgraph/remote.go | 16 +++++++- server/remote/remote.go | 1 + 13 files changed, 192 insertions(+), 28 deletions(-) create mode 100644 server/job/recurring_job.go create mode 100644 server/remote/msgraph/batch_request.go create mode 100644 server/remote/msgraph/get_schedule_batched.go diff --git a/server/api/api.go b/server/api/api.go index 4fd28796..e0ceeb0f 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -100,6 +100,10 @@ func (api *api) MakeClient() (remote.Client, error) { return api.Remote.NewClient(context.Background(), api.user.OAuth2Token), nil } +func (api *api) MakeAppClient() (remote.AppLevelClient, error) { + return api.Remote.NewAppLevelClient(context.Background()), nil +} + func (api *api) Filter(filters ...filterf) error { for _, filter := range filters { err := filter(api) diff --git a/server/api/availability.go b/server/api/availability.go index 04cb5bd6..562dc81c 100644 --- a/server/api/availability.go +++ b/server/api/availability.go @@ -6,6 +6,9 @@ package api import ( "time" + "github.com/robfig/cron/v3" + + "github.com/mattermost/mattermost-plugin-msoffice/server/job" "github.com/mattermost/mattermost-plugin-msoffice/server/remote" "github.com/mattermost/mattermost-plugin-msoffice/server/utils" ) @@ -18,8 +21,28 @@ const ( AVAILABILITY_VIEW_WORKING_ELSEWHERE = '4' ) +type availabilityJob struct { + api API +} + +func NewAvailabilityJob(api API) job.RecurringJob { + return &availabilityJob{api: api} +} + +func (j *availabilityJob) Run() { + c := cron.New() + c.AddFunc("* * * * *", j.Work) + c.Start() + j.Work() +} + +func (j *availabilityJob) Work() { + j.api.GetUserAvailability() + j.api.(*api).Logger.Debugf("Just ran the job") +} + func (api *api) GetUserAvailability() (string, error) { - client, err := api.MakeClient() + client, err := api.MakeAppClient() if err != nil { return "", err } @@ -37,20 +60,22 @@ func (api *api) GetUserAvailability() (string, error) { start := remote.NewDateTime(time.Now()) end := remote.NewDateTime(time.Now().Add(15 * time.Minute)) timeWindow := 15 // minutes + sched, err := client.GetSchedule(scheduleIDs, start, end, timeWindow) if err != nil { return "", err } - userID := users[0].MattermostUserID - av := sched[0].AvailabilityView - - setUserStatusFromAvailability(api, userID, av[0]) + for i, s := range sched { + userID := users[i].MattermostUserID + av := s.AvailabilityView + api.setUserStatusFromAvailability(userID, av[0]) + } return utils.JSONBlock(sched), err } -func setUserStatusFromAvailability(api *api, mattermostUserID string, av byte) { +func (api *api) setUserStatusFromAvailability(mattermostUserID string, av byte) { currentStatus, _ := api.API.GetUserStatus(mattermostUserID) switch av { diff --git a/server/job/recurring_job.go b/server/job/recurring_job.go new file mode 100644 index 00000000..67c8bed2 --- /dev/null +++ b/server/job/recurring_job.go @@ -0,0 +1,5 @@ +package job + +type RecurringJob interface { + Run() +} diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index bc083c3a..1d4b8c2a 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -67,10 +67,15 @@ func (p *Plugin) OnActivate() error { } p.httpHandler = http.NewHandler() - p.notificationHandler = api.NewNotificationHandler(p.newAPIConfig()) + + conf := p.newAPIConfig() + p.notificationHandler = api.NewNotificationHandler(conf) command.Register(p.API.RegisterCommand) + // j := api.NewAvailabilityJob(api.New(conf, "")) + // go j.Run() + p.API.LogInfo(p.config.PluginID + " activated") return nil } diff --git a/server/remote/client.go b/server/remote/client.go index d2a49ecb..acb05905 100644 --- a/server/remote/client.go +++ b/server/remote/client.go @@ -3,11 +3,17 @@ package remote -import "time" +import ( + "time" +) + +type AppLevelClient interface { + GetSchedule(scheduleIDs []string, startTime, endTime *DateTime, availabilityViewInterval int) ([]*ScheduleInformation, error) +} type Client interface { AcceptUserEvent(userID, eventID string) error - Call(method, path string, in, out interface{}) (responseData []byte, err error) + Call(method, path string, token string, in, out interface{}) (responseData []byte, err error) CreateSubscription(notificationURL string) (*Subscription, error) DeclineUserEvent(userID, eventID string) error DeleteSubscription(subscriptionID string) error @@ -18,7 +24,7 @@ type Client interface { GetMe() (*User, error) GetNotificationData(*Notification) (*Notification, error) GetUserCalendars(userID string) ([]*Calendar, error) - GetSchedule(scheduleIDs []string, startTime, endTime *DateTime, availabilityViewInterval int) ([]*ScheduleInformation, error) + GetSchedule(schedules []string, startTime, endTime *DateTime, availabilityViewInterval int) ([]*ScheduleInformation, error) GetUserDefaultCalendarView(userID string, startTime, endTime time.Time) ([]*Event, error) GetUserEvent(userID, eventID string) (*Event, error) ListSubscriptions() ([]*Subscription, error) diff --git a/server/remote/msgraph/batch_request.go b/server/remote/msgraph/batch_request.go new file mode 100644 index 00000000..1823a122 --- /dev/null +++ b/server/remote/msgraph/batch_request.go @@ -0,0 +1,41 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package msgraph + +import ( + "net/http" + "net/url" +) + + +type AuthResponse struct { + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + AccessToken string `json:"access_token"` +} + +func (c *appClient) getAppLevelToken() (string, error) { + params := map[string]string{ + "client_id": c.conf.OAuth2ClientID, + "scope": "https://graph.microsoft.com/.default", + "client_secret": c.conf.OAuth2ClientSecret, + "grant_type": "client_credentials", + } + + u := "https://login.microsoftonline.com/" + c.conf.OAuth2Authority + "/oauth2/v2.0/token" + res := AuthResponse{} + + data := url.Values{} + data.Set("client_id", params["client_id"]) + data.Set("scope", params["scope"]) + data.Set("client_secret", params["client_secret"]) + data.Set("grant_type", params["grant_type"]) + + _, err := c.Call(http.MethodPost, u, "", data, &res) + if err != nil { + return "", err + } + + return res.AccessToken, nil +} diff --git a/server/remote/msgraph/call.go b/server/remote/msgraph/call.go index bcecac97..c0b6f953 100644 --- a/server/remote/msgraph/call.go +++ b/server/remote/msgraph/call.go @@ -11,12 +11,13 @@ import ( "io/ioutil" "net/http" "net/url" + "strings" "github.com/pkg/errors" msgraph "github.com/yaegashi/msgraph.go/v1.0" ) -func (c *client) Call(method, path string, in, out interface{}) (responseData []byte, err error) { +func (c *client) Call(method, path string, token string, in, out interface{}) (responseData []byte, err error) { errContext := fmt.Sprintf("msgraph: Call failed: method:%s, path:%s", method, path) pathURL, err := url.Parse(path) if err != nil { @@ -36,26 +37,43 @@ func (c *client) Call(method, path string, in, out interface{}) (responseData [] } var inBody io.Reader + var contentType string if in != nil { - buf := &bytes.Buffer{} - err = json.NewEncoder(buf).Encode(in) - if err != nil { - return nil, err + v, ok := in.(url.Values) + if ok { + contentType = "application/x-www-form-urlencoded" + inBody = strings.NewReader(v.Encode()) + } else { + contentType = "application/json" + buf := &bytes.Buffer{} + err = json.NewEncoder(buf).Encode(in) + if err != nil { + return nil, err + } + inBody = buf } - inBody = buf } + req, err := http.NewRequest(method, path, inBody) if err != nil { return nil, err } - if inBody != nil { - req.Header.Add("Content-Type", "application/json") + if contentType != "" { + req.Header.Add("Content-Type", contentType) } + if token != "" { + req.Header.Add("Authorization", "Bearer " + token) + } + if c.ctx != nil { req = req.WithContext(c.ctx) } - resp, err := c.httpClient.Do(req) + httpClient := c.httpClient + if httpClient == nil { + httpClient = &http.Client{} + } + resp, err := httpClient.Do(req) if err != nil { return nil, err } @@ -68,6 +86,7 @@ func (c *client) Call(method, path string, in, out interface{}) (responseData [] if err != nil { return nil, err } + switch resp.StatusCode { case http.StatusOK, http.StatusCreated: if out != nil { diff --git a/server/remote/msgraph/client.go b/server/remote/msgraph/client.go index 9bcace60..41ea1739 100644 --- a/server/remote/msgraph/client.go +++ b/server/remote/msgraph/client.go @@ -24,3 +24,15 @@ type client struct { conf *config.Config bot.Logger } + +type appClient struct { + client + + Token string + + rbuilder *msgraph.GraphServiceRequestBuilder + ctx context.Context + httpClient *http.Client + conf *config.Config + bot.Logger +} \ No newline at end of file diff --git a/server/remote/msgraph/get_notification_data.go b/server/remote/msgraph/get_notification_data.go index 45246418..15b8e63b 100644 --- a/server/remote/msgraph/get_notification_data.go +++ b/server/remote/msgraph/get_notification_data.go @@ -17,7 +17,7 @@ func (c *client) GetNotificationData(orig *remote.Notification) (*remote.Notific switch wh.ResourceData.DataType { case "#Microsoft.Graph.Event": event := remote.Event{} - _, err := c.Call(http.MethodGet, wh.Resource, nil, &event) + _, err := c.Call(http.MethodGet, wh.Resource, "", nil, &event) if err != nil { c.Logger.With(bot.LogContext{ "Resource": wh.Resource, diff --git a/server/remote/msgraph/get_schedule.go b/server/remote/msgraph/get_schedule.go index 69d676c5..d2b5d323 100644 --- a/server/remote/msgraph/get_schedule.go +++ b/server/remote/msgraph/get_schedule.go @@ -4,8 +4,8 @@ package msgraph import ( - "io/ioutil" "encoding/json" + "io/ioutil" "github.com/mattermost/mattermost-plugin-msoffice/server/remote" @@ -14,22 +14,22 @@ import ( type GetScheduleRequest struct { // List of emails of users that we want to check - ScheduleIDs []string `json:"schedules"` + Schedules []string `json:"schedules"` // Overall start and end of entire search window - StartTime remote.DateTime `json:"startTime"` - EndTime remote.DateTime `json:"endTime"` + StartTime *remote.DateTime `json:"startTime"` + EndTime *remote.DateTime `json:"endTime"` // Size of each chunk of time we want to check // This can be equal to end - start if we want, or we can get more granular results by making it shorter. // For the graph API: The default is 30 minutes, minimum is 6, maximum is 1440 // 15 is currently being used on our end - AvailabilityViewInterval int `json:"availabilityViewInterval"` + AvailabilityViewInterval int `json:"availabilityViewInterval"` } -func (c *client) GetSchedule(scheduleIDs []string, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { +func (c *client) GetSchedule(schedules []string, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { req := &msgraph.CalendarGetScheduleRequestParameter{ - Schedules: scheduleIDs, + Schedules: schedules, StartTime: &msgraph.DateTimeTimeZone{ DateTime: &startTime.DateTime, TimeZone: &startTime.TimeZone, diff --git a/server/remote/msgraph/get_schedule_batched.go b/server/remote/msgraph/get_schedule_batched.go new file mode 100644 index 00000000..c2cfba27 --- /dev/null +++ b/server/remote/msgraph/get_schedule_batched.go @@ -0,0 +1,32 @@ +package msgraph + +import ( + "net/http" + + "github.com/mattermost/mattermost-plugin-msoffice/server/remote" +) + +func (c *appClient) GetSchedule(schedules []string, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { + token, err := c.getAppLevelToken() + if err != nil { + return nil, err + } + + var res remote.GetScheduleResponse + + params := &GetScheduleRequest{ + Schedules: schedules, + StartTime: startTime, + EndTime: endTime, + AvailabilityViewInterval: availabilityViewInterval, + } + + uid := "fb10ac13-e441-4611-8431-8ee3b6403673" + u := "https://graph.microsoft.com/v1.0/Users/" + uid + "/calendar/getSchedule" + _, err = c.Call(http.MethodPost, u, token, params, &res) + if err != nil { + return nil, err + } + + return res.Value, nil +} \ No newline at end of file diff --git a/server/remote/msgraph/remote.go b/server/remote/msgraph/remote.go index 6dd18de6..998199cd 100644 --- a/server/remote/msgraph/remote.go +++ b/server/remote/msgraph/remote.go @@ -5,6 +5,7 @@ package msgraph import ( "context" + "net/http" "golang.org/x/oauth2" "golang.org/x/oauth2/microsoft" @@ -34,7 +35,7 @@ func NewRemote(conf *config.Config, logger bot.Logger) remote.Remote { } } -// NewClient creates a new client. +// NewClient creates a new client for user-delegated permissions. func (r *impl) NewClient(ctx context.Context, token *oauth2.Token) remote.Client { httpClient := r.NewOAuth2Config().Client(ctx, token) c := &client{ @@ -47,6 +48,19 @@ func (r *impl) NewClient(ctx context.Context, token *oauth2.Token) remote.Client return c } +// NewAppLevekClient creates a new client used for app-only permissions. +func (r *impl) NewAppLevelClient(ctx context.Context) remote.AppLevelClient { + httpClient := &http.Client{} + c := &appClient{ + conf: r.conf, + ctx: ctx, + httpClient: httpClient, + Logger: r.logger, + rbuilder: msgraph.NewClient(httpClient), + } + return c +} + func (r *impl) NewOAuth2Config() *oauth2.Config { return &oauth2.Config{ ClientID: r.conf.OAuth2ClientID, diff --git a/server/remote/remote.go b/server/remote/remote.go index 4fe80200..480923af 100644 --- a/server/remote/remote.go +++ b/server/remote/remote.go @@ -15,6 +15,7 @@ import ( type Remote interface { NewClient(context.Context, *oauth2.Token) Client + NewAppLevelClient(context.Context) AppLevelClient NewOAuth2Config() *oauth2.Config HandleWebhook(http.ResponseWriter, *http.Request) []*Notification } From 21ea2c61a0cd1c1246223dbe4828fb17bcfd19d5 Mon Sep 17 00:00:00 2001 From: mickmister Date: Wed, 18 Dec 2019 23:20:40 -0500 Subject: [PATCH 03/18] batch request works --- assets/profile.png | Bin 2881 -> 19737 bytes go.mod | 1 + go.sum | 3 + plugin.json | 6 +- server/api/availability.go | 104 +++++++++++++----- server/api/mock_api/mock_calendar.go | 74 +++++++++++++ server/api/mock_api/mock_subscriptions.go | 15 +++ server/api/oauth2.go | 1 - server/config/const.go | 6 +- server/plugin/command/command.go | 8 +- server/plugin/command/help.go | 2 +- server/remote/client.go | 4 +- server/remote/mock_remote/mock_client.go | 82 +++++++++++++- server/remote/mock_remote/mock_remote.go | 14 +++ server/remote/msgraph/batch_request.go | 53 +++++++-- server/remote/msgraph/call.go | 3 +- server/remote/msgraph/get_schedule.go | 2 +- server/remote/msgraph/get_schedule_batched.go | 82 +++++++++++--- server/store/mock_store/mock_user_store.go | 15 +++ server/store/user_store.go | 10 +- server/testdata/batch_requests.json | 68 ++++++++++++ .../testdata/get_schedule_response_batch.json | 38 +++++++ 22 files changed, 518 insertions(+), 73 deletions(-) create mode 100644 server/testdata/batch_requests.json create mode 100644 server/testdata/get_schedule_response_batch.json diff --git a/assets/profile.png b/assets/profile.png index 592a5f111ac8b9b0dd2de79d044b0bc6b8af6206..eb2fea2edb2ba25c8fe97f2b80ea167ea6327beb 100644 GIT binary patch literal 19737 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 F_z$iiP!|9I literal 2881 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 Date: Thu, 19 Dec 2019 00:17:28 -0500 Subject: [PATCH 04/18] get rid of AppLevelClient struct/interface --- server/api/api.go | 3 +- server/api/availability.go | 2 +- server/api/mock_api/mock_subscriptions.go | 15 ++++ server/plugin/command/availability.go | 7 ++ server/remote/client.go | 6 +- server/remote/mock_remote/mock_client.go | 16 ++-- server/remote/mock_remote/mock_remote.go | 4 +- server/remote/msgraph/batch_request.go | 13 +-- server/remote/msgraph/call.go | 5 +- .../remote/msgraph/get_notification_data.go | 2 +- server/remote/msgraph/get_schedule.go | 79 ------------------- server/remote/msgraph/get_schedule_batched.go | 26 +++++- server/remote/msgraph/remote.go | 18 ++++- server/remote/remote.go | 2 +- 14 files changed, 80 insertions(+), 118 deletions(-) delete mode 100644 server/remote/msgraph/get_schedule.go diff --git a/server/api/api.go b/server/api/api.go index e0ceeb0f..dc917af0 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -23,6 +23,7 @@ type OAuth2 interface { type Subscriptions interface { CreateUserEventSubscription() (*store.Subscription, error) GetUserAvailability() (string, error) + GetAllUsersAvailability() (string, error) RenewUserEventSubscription() (*store.Subscription, error) DeleteOrphanedSubscription(ID string) error DeleteUserEventSubscription() error @@ -100,7 +101,7 @@ func (api *api) MakeClient() (remote.Client, error) { return api.Remote.NewClient(context.Background(), api.user.OAuth2Token), nil } -func (api *api) MakeAppClient() (remote.AppLevelClient, error) { +func (api *api) MakeAppClient() (remote.Client, error) { return api.Remote.NewAppLevelClient(context.Background()), nil } diff --git a/server/api/availability.go b/server/api/availability.go index 91cf5913..33834ab9 100644 --- a/server/api/availability.go +++ b/server/api/availability.go @@ -54,7 +54,7 @@ func (j *availabilityJob) Work() { } func (api *api) GetUserAvailability() (string, error) { - client, err := api.MakeAppClient() + client, err := api.MakeClient() if err != nil { return "", err } diff --git a/server/api/mock_api/mock_subscriptions.go b/server/api/mock_api/mock_subscriptions.go index b289d5e5..348a13dd 100644 --- a/server/api/mock_api/mock_subscriptions.go +++ b/server/api/mock_api/mock_subscriptions.go @@ -77,6 +77,21 @@ func (mr *MockSubscriptionsMockRecorder) DeleteUserEventSubscription() *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserEventSubscription", reflect.TypeOf((*MockSubscriptions)(nil).DeleteUserEventSubscription)) } +// GetAllUsersAvailability mocks base method +func (m *MockSubscriptions) GetAllUsersAvailability() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllUsersAvailability") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllUsersAvailability indicates an expected call of GetAllUsersAvailability +func (mr *MockSubscriptionsMockRecorder) GetAllUsersAvailability() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllUsersAvailability", reflect.TypeOf((*MockSubscriptions)(nil).GetAllUsersAvailability)) +} + // GetUserAvailability mocks base method func (m *MockSubscriptions) GetUserAvailability() (string, error) { m.ctrl.T.Helper() diff --git a/server/plugin/command/availability.go b/server/plugin/command/availability.go index ca20d946..01871c9d 100644 --- a/server/plugin/command/availability.go +++ b/server/plugin/command/availability.go @@ -11,6 +11,13 @@ func (c *Command) availability(parameters ...string) (string, error) { return "", err } + return resString, nil + case len(parameters) == 1 && parameters[0] == "all": + resString, err := c.API.GetAllUsersAvailability() + if err != nil { + return "", err + } + return resString, nil } return "bad syntax", nil diff --git a/server/remote/client.go b/server/remote/client.go index 5a55f1a7..4405d1bf 100644 --- a/server/remote/client.go +++ b/server/remote/client.go @@ -7,13 +7,9 @@ import ( "time" ) -type AppLevelClient interface { - GetSchedule(remoteUserID string, schedules []string, startTime, endTime *DateTime, availabilityViewInterval int) ([]*ScheduleInformation, error) -} - type Client interface { AcceptUserEvent(userID, eventID string) error - Call(method, path string, token string, in, out interface{}) (responseData []byte, err error) + Call(method, path string, in, out interface{}) (responseData []byte, err error) CreateSubscription(notificationURL string) (*Subscription, error) DeclineUserEvent(userID, eventID string) error DeleteSubscription(subscriptionID string) error diff --git a/server/remote/mock_remote/mock_client.go b/server/remote/mock_remote/mock_client.go index 2b0c69b7..083bb132 100644 --- a/server/remote/mock_remote/mock_client.go +++ b/server/remote/mock_remote/mock_client.go @@ -49,18 +49,18 @@ func (mr *MockClientMockRecorder) AcceptUserEvent(arg0, arg1 interface{}) *gomoc } // Call mocks base method -func (m *MockClient) Call(arg0, arg1, arg2 string, arg3, arg4 interface{}) ([]byte, error) { +func (m *MockClient) Call(arg0, arg1 string, arg2, arg3 interface{}) ([]byte, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Call", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "Call", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } // Call indicates an expected call of Call -func (mr *MockClientMockRecorder) Call(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) Call(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockClient)(nil).Call), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockClient)(nil).Call), arg0, arg1, arg2, arg3) } // CreateCalendar mocks base method @@ -196,18 +196,18 @@ func (mr *MockClientMockRecorder) GetNotificationData(arg0 interface{}) *gomock. } // GetSchedule mocks base method -func (m *MockClient) GetSchedule(arg0 []string, arg1, arg2 *remote.DateTime, arg3 int) ([]*remote.ScheduleInformation, error) { +func (m *MockClient) GetSchedule(arg0 string, arg1 []string, arg2, arg3 *remote.DateTime, arg4 int) ([]*remote.ScheduleInformation, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSchedule", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "GetSchedule", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]*remote.ScheduleInformation) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSchedule indicates an expected call of GetSchedule -func (mr *MockClientMockRecorder) GetSchedule(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) GetSchedule(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchedule", reflect.TypeOf((*MockClient)(nil).GetSchedule), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchedule", reflect.TypeOf((*MockClient)(nil).GetSchedule), arg0, arg1, arg2, arg3, arg4) } // GetUserCalendars mocks base method diff --git a/server/remote/mock_remote/mock_remote.go b/server/remote/mock_remote/mock_remote.go index 1ea16dd0..e5b5e738 100644 --- a/server/remote/mock_remote/mock_remote.go +++ b/server/remote/mock_remote/mock_remote.go @@ -51,10 +51,10 @@ func (mr *MockRemoteMockRecorder) HandleWebhook(arg0, arg1 interface{}) *gomock. } // NewAppLevelClient mocks base method -func (m *MockRemote) NewAppLevelClient(arg0 context.Context) remote.AppLevelClient { +func (m *MockRemote) NewAppLevelClient(arg0 context.Context) remote.Client { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NewAppLevelClient", arg0) - ret0, _ := ret[0].(remote.AppLevelClient) + ret0, _ := ret[0].(remote.Client) return ret0 } diff --git a/server/remote/msgraph/batch_request.go b/server/remote/msgraph/batch_request.go index ae97b262..5610f1a6 100644 --- a/server/remote/msgraph/batch_request.go +++ b/server/remote/msgraph/batch_request.go @@ -14,7 +14,7 @@ type AuthResponse struct { AccessToken string `json:"access_token"` } -func (c *appClient) getAppLevelToken() (string, error) { +func (c *client) getAppLevelToken() (string, error) { params := map[string]string{ "client_id": c.conf.OAuth2ClientID, "scope": "https://graph.microsoft.com/.default", @@ -31,7 +31,7 @@ func (c *appClient) getAppLevelToken() (string, error) { data.Set("client_secret", params["client_secret"]) data.Set("grant_type", params["grant_type"]) - _, err := c.Call(http.MethodPost, u, "", data, &res) + _, err := c.Call(http.MethodPost, u, data, &res) if err != nil { return "", err } @@ -62,16 +62,11 @@ type FullBatchRequest struct { Requests []*SingleRequest `json:"requests"` } -func (c *appClient) batchRequest(requests []*SingleRequest, out interface{}) error { - token, err := c.getAppLevelToken() - if err != nil { - return err - } - +func (c *client) batchRequest(requests []*SingleRequest, out interface{}) error { batchReq := FullBatchRequest{Requests: requests} u := "https://graph.microsoft.com/v1.0/$batch" - _, err = c.Call(http.MethodPost, u, token, batchReq, out) + _, err := c.Call(http.MethodPost, u, batchReq, out) if err != nil { return err } diff --git a/server/remote/msgraph/call.go b/server/remote/msgraph/call.go index 37333353..31841667 100644 --- a/server/remote/msgraph/call.go +++ b/server/remote/msgraph/call.go @@ -17,7 +17,7 @@ import ( msgraph "github.com/yaegashi/msgraph.go/v1.0" ) -func (c *client) Call(method, path string, token string, in, out interface{}) (responseData []byte, err error) { +func (c *client) Call(method, path string, in, out interface{}) (responseData []byte, err error) { errContext := fmt.Sprintf("msgraph: Call failed: method:%s, path:%s", method, path) pathURL, err := url.Parse(path) if err != nil { @@ -61,9 +61,6 @@ func (c *client) Call(method, path string, token string, in, out interface{}) (r if contentType != "" { req.Header.Add("Content-Type", contentType) } - if token != "" { - req.Header.Add("Authorization", "Bearer "+token) - } if c.ctx != nil { req = req.WithContext(c.ctx) diff --git a/server/remote/msgraph/get_notification_data.go b/server/remote/msgraph/get_notification_data.go index 15b8e63b..45246418 100644 --- a/server/remote/msgraph/get_notification_data.go +++ b/server/remote/msgraph/get_notification_data.go @@ -17,7 +17,7 @@ func (c *client) GetNotificationData(orig *remote.Notification) (*remote.Notific switch wh.ResourceData.DataType { case "#Microsoft.Graph.Event": event := remote.Event{} - _, err := c.Call(http.MethodGet, wh.Resource, "", nil, &event) + _, err := c.Call(http.MethodGet, wh.Resource, nil, &event) if err != nil { c.Logger.With(bot.LogContext{ "Resource": wh.Resource, diff --git a/server/remote/msgraph/get_schedule.go b/server/remote/msgraph/get_schedule.go deleted file mode 100644 index 0c616747..00000000 --- a/server/remote/msgraph/get_schedule.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -package msgraph - -import ( - "encoding/json" - "io/ioutil" - - "github.com/mattermost/mattermost-plugin-msoffice/server/remote" - - msgraph "github.com/yaegashi/msgraph.go/v1.0" -) - -type GetScheduleRequest struct { - // List of emails of users that we want to check - Schedules []string `json:"schedules"` - - // Overall start and end of entire search window - StartTime *remote.DateTime `json:"startTime"` - EndTime *remote.DateTime `json:"endTime"` - - // Size of each chunk of time we want to check - // This can be equal to end - start if we want, or we can get more granular results by making it shorter. - // For the graph API: The default is 30 minutes, minimum is 6, maximum is 1440 - // 15 is currently being used on our end - AvailabilityViewInterval int `json:"availabilityViewInterval"` -} - -func (c *client) GetSchedule(remoteUserID string, schedules []string, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { - req := &msgraph.CalendarGetScheduleRequestParameter{ - Schedules: schedules, - StartTime: &msgraph.DateTimeTimeZone{ - DateTime: &startTime.DateTime, - TimeZone: &startTime.TimeZone, - }, - EndTime: &msgraph.DateTimeTimeZone{ - DateTime: &endTime.DateTime, - TimeZone: &endTime.TimeZone, - }, - AvailabilityViewInterval: &availabilityViewInterval, - } - - // req := &GetScheduleRequest{ - // Schedules: scheduleIDs, - // StartTime: startTime, - // EndTime: endTime, - // AvailabilityViewInterval: availabilityViewInterval, - // } - - r2 := c.rbuilder.Me().Calendar().GetSchedule(req).Request() - r, err := r2.NewJSONRequest("POST", "", req) - if err != nil { - return nil, err - } - r = r.WithContext(c.ctx) - res, err := r2.Client().Do(r) - - if err != nil { - return nil, err - } - - var resBody remote.GetScheduleResponse - b, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, err - } - - err = json.Unmarshal(b, &resBody) - if err != nil { - return nil, err - } - - if err != nil { - return nil, err - } - - return resBody.Value, nil -} diff --git a/server/remote/msgraph/get_schedule_batched.go b/server/remote/msgraph/get_schedule_batched.go index 2abf7588..4b6ca918 100644 --- a/server/remote/msgraph/get_schedule_batched.go +++ b/server/remote/msgraph/get_schedule_batched.go @@ -17,9 +17,23 @@ type GetScheduleBatchResponse struct { Responses []*GetScheduleSingleResponse `json:"responses"` } -func (c *appClient) GetSchedule(remoteUserID string, schedules []string, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { +type GetScheduleRequest struct { + // List of emails of users that we want to check + Schedules []string `json:"schedules"` + + // Overall start and end of entire search window + StartTime *remote.DateTime `json:"startTime"` + EndTime *remote.DateTime `json:"endTime"` + + // Size of each chunk of time we want to check + // This can be equal to end - start if we want, or we can get more granular results by making it shorter. + // For the graph API: The default is 30 minutes, minimum is 6, maximum is 1440 + // 15 is currently being used on our end + AvailabilityViewInterval int `json:"availabilityViewInterval"` +} + +func (c *client) GetSchedule(remoteUserID string, schedules []string, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { params := &GetScheduleRequest{ - Schedules: schedules, // need to chunk these per 20 etc StartTime: startTime, EndTime: endTime, AvailabilityViewInterval: availabilityViewInterval, @@ -53,10 +67,16 @@ func getFullBatchRequest(remoteUserID string, schedules []string, params *GetSch u := "/Users/" + remoteUserID + "/calendar/getSchedule" makeRequest := func() *SingleRequest { + p := &GetScheduleRequest{ + Schedules: schedules, // need to chunk these out + StartTime: params.StartTime, + EndTime: params.EndTime, + AvailabilityViewInterval: params.AvailabilityViewInterval, + } req := &SingleRequest{ URL: u, Method: "POST", - Body: params, + Body: p, Headers: map[string]string{ "Content-Type": "application/json", }, diff --git a/server/remote/msgraph/remote.go b/server/remote/msgraph/remote.go index 998199cd..84dea93c 100644 --- a/server/remote/msgraph/remote.go +++ b/server/remote/msgraph/remote.go @@ -48,17 +48,27 @@ func (r *impl) NewClient(ctx context.Context, token *oauth2.Token) remote.Client return c } -// NewAppLevekClient creates a new client used for app-only permissions. -func (r *impl) NewAppLevelClient(ctx context.Context) remote.AppLevelClient { +// NewAppLevelClient creates a new client used for app-only permissions. +func (r *impl) NewAppLevelClient(ctx context.Context) remote.Client { httpClient := &http.Client{} - c := &appClient{ + + // Hack. getAppLevelToken should just use http and not a client + c := &client{ conf: r.conf, ctx: ctx, httpClient: httpClient, Logger: r.logger, rbuilder: msgraph.NewClient(httpClient), } - return c + + token, _ := c.getAppLevelToken() + + o := &oauth2.Token{ + AccessToken: token, + TokenType: "Bearer", + } + + return r.NewClient(ctx, o) } func (r *impl) NewOAuth2Config() *oauth2.Config { diff --git a/server/remote/remote.go b/server/remote/remote.go index 480923af..d0cf3ec3 100644 --- a/server/remote/remote.go +++ b/server/remote/remote.go @@ -15,7 +15,7 @@ import ( type Remote interface { NewClient(context.Context, *oauth2.Token) Client - NewAppLevelClient(context.Context) AppLevelClient + NewAppLevelClient(context.Context) Client NewOAuth2Config() *oauth2.Config HandleWebhook(http.ResponseWriter, *http.Request) []*Notification } From 562957f4ba2e9579b9ef6c80429a4b2b1f89524c Mon Sep 17 00:00:00 2001 From: mickmister Date: Thu, 19 Dec 2019 00:37:09 -0500 Subject: [PATCH 05/18] remove dead code --- server/remote/msgraph/client.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/server/remote/msgraph/client.go b/server/remote/msgraph/client.go index 41ea1739..9bcace60 100644 --- a/server/remote/msgraph/client.go +++ b/server/remote/msgraph/client.go @@ -24,15 +24,3 @@ type client struct { conf *config.Config bot.Logger } - -type appClient struct { - client - - Token string - - rbuilder *msgraph.GraphServiceRequestBuilder - ctx context.Context - httpClient *http.Client - conf *config.Config - bot.Logger -} \ No newline at end of file From 62eb1181e4131ce01115fe7d42be5d42cd3fc19c Mon Sep 17 00:00:00 2001 From: mickmister Date: Thu, 19 Dec 2019 00:47:01 -0500 Subject: [PATCH 06/18] lint --- server/remote/common.go | 1 - server/remote/schedule.go | 1 - server/remote/user.go | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/server/remote/common.go b/server/remote/common.go index b305a02e..0bf67963 100644 --- a/server/remote/common.go +++ b/server/remote/common.go @@ -21,7 +21,6 @@ func NewDateTime(t time.Time) *DateTime { DateTime: t.Format(RFC3339NanoNoTimezone), // TimeZone: t.Format("MST"), TimeZone: "Eastern Standard Time", - } } diff --git a/server/remote/schedule.go b/server/remote/schedule.go index fca62cbe..f013f862 100644 --- a/server/remote/schedule.go +++ b/server/remote/schedule.go @@ -20,4 +20,3 @@ type ScheduleInformation struct { type GetScheduleResponse struct { Value []*ScheduleInformation `json:"value,omitempty"` } - diff --git a/server/remote/user.go b/server/remote/user.go index d02335ed..5a7c032e 100644 --- a/server/remote/user.go +++ b/server/remote/user.go @@ -7,5 +7,5 @@ type User struct { ID string `json:"id"` DisplayName string `json:"displayName,omitempty"` UserPrincipalName string `json:"userPrincipalName,omitempty"` - Mail string `json:"mail,omitempty"` + Mail string `json:"mail,omitempty"` } From ba38c04b2060a2c2284ccad55f86bdc966138e7d Mon Sep 17 00:00:00 2001 From: mickmister Date: Thu, 19 Dec 2019 01:00:10 -0500 Subject: [PATCH 07/18] move app-level user auth token logic into its own file --- server/remote/msgraph/batch_request.go | 32 ---------------- server/remote/msgraph/get_app_level_token.go | 40 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 32 deletions(-) create mode 100644 server/remote/msgraph/get_app_level_token.go diff --git a/server/remote/msgraph/batch_request.go b/server/remote/msgraph/batch_request.go index 5610f1a6..582089c0 100644 --- a/server/remote/msgraph/batch_request.go +++ b/server/remote/msgraph/batch_request.go @@ -5,40 +5,8 @@ package msgraph import ( "net/http" - "net/url" ) -type AuthResponse struct { - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - AccessToken string `json:"access_token"` -} - -func (c *client) getAppLevelToken() (string, error) { - params := map[string]string{ - "client_id": c.conf.OAuth2ClientID, - "scope": "https://graph.microsoft.com/.default", - "client_secret": c.conf.OAuth2ClientSecret, - "grant_type": "client_credentials", - } - - u := "https://login.microsoftonline.com/" + c.conf.OAuth2Authority + "/oauth2/v2.0/token" - res := AuthResponse{} - - data := url.Values{} - data.Set("client_id", params["client_id"]) - data.Set("scope", params["scope"]) - data.Set("client_secret", params["client_secret"]) - data.Set("grant_type", params["grant_type"]) - - _, err := c.Call(http.MethodPost, u, data, &res) - if err != nil { - return "", err - } - - return res.AccessToken, nil -} - type SingleRequest struct { ID string `json:"id"` URL string `json:"url"` diff --git a/server/remote/msgraph/get_app_level_token.go b/server/remote/msgraph/get_app_level_token.go new file mode 100644 index 00000000..f0056cd7 --- /dev/null +++ b/server/remote/msgraph/get_app_level_token.go @@ -0,0 +1,40 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package msgraph + +import ( + "net/http" + "net/url" +) + +type AuthResponse struct { + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + AccessToken string `json:"access_token"` +} + +func (c *client) getAppLevelToken() (string, error) { + params := map[string]string{ + "client_id": c.conf.OAuth2ClientID, + "scope": "https://graph.microsoft.com/.default", + "client_secret": c.conf.OAuth2ClientSecret, + "grant_type": "client_credentials", + } + + u := "https://login.microsoftonline.com/" + c.conf.OAuth2Authority + "/oauth2/v2.0/token" + res := AuthResponse{} + + data := url.Values{} + data.Set("client_id", params["client_id"]) + data.Set("scope", params["scope"]) + data.Set("client_secret", params["client_secret"]) + data.Set("grant_type", params["grant_type"]) + + _, err := c.Call(http.MethodPost, u, data, &res) + if err != nil { + return "", err + } + + return res.AccessToken, nil +} From d9572cdfef928f850b9a1b2c8a2f80702c905abd Mon Sep 17 00:00:00 2001 From: mickmister Date: Thu, 19 Dec 2019 01:02:28 -0500 Subject: [PATCH 08/18] lint --- server/remote/msgraph/get_me.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/remote/msgraph/get_me.go b/server/remote/msgraph/get_me.go index 8cbeca26..eff56bc0 100644 --- a/server/remote/msgraph/get_me.go +++ b/server/remote/msgraph/get_me.go @@ -14,7 +14,7 @@ func (c *client) GetMe() (*remote.User, error) { ID: *graphUser.ID, DisplayName: *graphUser.DisplayName, UserPrincipalName: *graphUser.UserPrincipalName, - Mail: *graphUser.Mail, + Mail: *graphUser.Mail, } return user, nil From cb184463fb86d3b3bf40ae953d2685bf11128fdb Mon Sep 17 00:00:00 2001 From: mickmister Date: Wed, 8 Jan 2020 14:10:11 -0500 Subject: [PATCH 09/18] make job cancelable through system console. rename status sync api methods --- Makefile | 1 + go.mod | 1 - go.sum | 3 - plugin.json | 7 ++ server/api/api.go | 9 ++- server/api/availability.go | 93 ++++++----------------- server/api/mock_api/mock_availability.go | 79 +++++++++++++++++++ server/api/mock_api/mock_subscriptions.go | 30 -------- server/api/status_sync_job.go | 71 +++++++++++++++++ server/config/config.go | 2 + server/job/recurring_job.go | 5 -- server/plugin/command/availability.go | 4 +- server/plugin/plugin.go | 37 +++++++-- 13 files changed, 225 insertions(+), 117 deletions(-) create mode 100644 server/api/mock_api/mock_availability.go create mode 100644 server/api/status_sync_job.go delete mode 100644 server/job/recurring_job.go diff --git a/Makefile b/Makefile index 834191bb..3dd09e3a 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,7 @@ ifneq ($(HAS_SERVER),) mockgen -destination server/api/mock_api/mock_subscriptions.go github.com/mattermost/mattermost-plugin-msoffice/server/api Subscriptions mockgen -destination server/api/mock_api/mock_calendar.go github.com/mattermost/mattermost-plugin-msoffice/server/api Calendar mockgen -destination server/api/mock_api/mock_client.go github.com/mattermost/mattermost-plugin-msoffice/server/api Client + mockgen -destination server/api/mock_api/mock_availability.go github.com/mattermost/mattermost-plugin-msoffice/server/api Availability mockgen -destination server/remote/mock_remote/mock_remote.go github.com/mattermost/mattermost-plugin-msoffice/server/remote Remote mockgen -destination server/remote/mock_remote/mock_client.go github.com/mattermost/mattermost-plugin-msoffice/server/remote Client mockgen -destination server/utils/bot/mock_bot/mock_poster.go github.com/mattermost/mattermost-plugin-msoffice/server/utils/bot Poster diff --git a/go.mod b/go.mod index 3ae8e0bf..9c4914af 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/jarcoal/httpmock v1.0.4 github.com/mattermost/mattermost-server/v5 v5.18.0-rc.test github.com/pkg/errors v0.8.1 - github.com/robfig/cron/v3 v3.0.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.4.0 github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2 diff --git a/go.sum b/go.sum index 38299783..61841499 100644 --- a/go.sum +++ b/go.sum @@ -312,9 +312,6 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= -github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= -github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= diff --git a/plugin.json b/plugin.json index bf70b044..94e91a9d 100644 --- a/plugin.json +++ b/plugin.json @@ -79,6 +79,13 @@ "type": "text", "help_text": "Microsoft Office Client Secret.", "default": "" + }, + { + "key": "EnableStatusSyncJob", + "display_name": "Enable User Status Sync Job", + "type": "bool", + "help_text": "When enabled, a Mattermost user's status will automatically update based on their Microsoft Calendar availability. This runs every 5 minutes.", + "default": false } ] } diff --git a/server/api/api.go b/server/api/api.go index dc917af0..3c48d3ad 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -22,8 +22,6 @@ type OAuth2 interface { type Subscriptions interface { CreateUserEventSubscription() (*store.Subscription, error) - GetUserAvailability() (string, error) - GetAllUsersAvailability() (string, error) RenewUserEventSubscription() (*store.Subscription, error) DeleteOrphanedSubscription(ID string) error DeleteUserEventSubscription() error @@ -47,11 +45,18 @@ type Event interface { RespondToEvent(eventID, response string) error } +type Availability interface { + GetUserAvailabilities(remoteUserID string, scheduleIDs []string) ([]*remote.ScheduleInformation, error) + SyncStatusForSingleUser() (string, error) + SyncStatusForAllUsers() (string, error) +} + type Client interface { MakeClient() (remote.Client, error) } type API interface { + Availability Calendar Client Event diff --git a/server/api/availability.go b/server/api/availability.go index 33834ab9..f3f109ae 100644 --- a/server/api/availability.go +++ b/server/api/availability.go @@ -7,72 +7,32 @@ import ( "fmt" "time" - "github.com/robfig/cron/v3" - - "github.com/mattermost/mattermost-plugin-msoffice/server/job" "github.com/mattermost/mattermost-plugin-msoffice/server/remote" "github.com/mattermost/mattermost-plugin-msoffice/server/utils" - "github.com/mattermost/mattermost-plugin-msoffice/server/utils/bot" ) const ( - AVAILABILITY_VIEW_FREE = '0' - AVAILABILITY_VIEW_TENTATIVE = '1' - AVAILABILITY_VIEW_BUSY = '2' - AVAILABILITY_VIEW_OUT_OF_OFFICE = '3' - AVAILABILITY_VIEW_WORKING_ELSEWHERE = '4' -) + availabilityTimeWindowSize = 15 -type availabilityJob struct { - api API -} - -func NewAvailabilityJob(api API) job.RecurringJob { - return &availabilityJob{api: api} -} - -func (j *availabilityJob) Run() { - c := cron.New() - c.AddFunc("* * * * *", j.Work) - c.Start() -} - -func (j *availabilityJob) getLogger() bot.Logger { - return j.api.(*api).Logger -} - -func (j *availabilityJob) Work() { - log := j.getLogger() - log.Debugf("Availability job beginning") - - _, err := j.api.GetUserAvailability() - if err != nil { - log.Errorf("Error during Availability job", "error", err.Error()) - } - - log.Debugf("Availability job finished") -} - -func (api *api) GetUserAvailability() (string, error) { - client, err := api.MakeClient() - if err != nil { - return "", err - } + availabilityViewFree = '0' + availabilityViewTentative = '1' + availabilityViewBusy = '2' + availabilityViewOutOfOffice = '3' + availabilityViewWorkingElsewhere = '4' +) +func (api *api) SyncStatusForSingleUser() (string, error) { u, err := api.UserStore.LoadUser(api.mattermostUserID) if err != nil { return "", err } scheduleIDs := []string{u.Remote.Mail} + sched, err := api.GetUserAvailabilities(u.Remote.ID, scheduleIDs) - start, end, timeWindow := getTimeInfoForAvailability() - - sched, err := client.GetSchedule(u.Remote.ID, scheduleIDs, start, end, timeWindow) if err != nil { return "", err } - if len(sched) == 0 { return "No schedule info found", nil } @@ -82,12 +42,7 @@ func (api *api) GetUserAvailability() (string, error) { return api.setUserStatusFromAvailability(api.mattermostUserID, av), nil } -func (api *api) GetAllUsersAvailability() (string, error) { - client, err := api.MakeAppClient() - if err != nil { - return "", err - } - +func (api *api) SyncStatusForAllUsers() (string, error) { users, err := api.UserStore.LoadAllUsers() if err != nil { return "", err @@ -102,13 +57,10 @@ func (api *api) GetAllUsersAvailability() (string, error) { scheduleIDs = append(scheduleIDs, u.Email) } - start, end, timeWindow := getTimeInfoForAvailability() - - sched, err := client.GetSchedule(users[0].RemoteID, scheduleIDs, start, end, timeWindow) + sched, err := api.GetUserAvailabilities(users[0].RemoteID, scheduleIDs) if err != nil { return "", err } - if len(sched) == 0 { return "No schedule info found", nil } @@ -127,39 +79,44 @@ func (api *api) GetAllUsersAvailability() (string, error) { return utils.JSONBlock(sched), nil } -func getTimeInfoForAvailability() (start, end *remote.DateTime, timeWindow int) { - start = remote.NewDateTime(time.Now()) - end = remote.NewDateTime(time.Now().Add(15 * time.Minute)) - timeWindow = 15 // minutes - return start, end, timeWindow +func (api *api) GetUserAvailabilities(remoteUserID string, scheduleIDs []string) ([]*remote.ScheduleInformation, error) { + client, err := api.MakeAppClient() + if err != nil { + return nil, err + } + + 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 string, av byte) string { currentStatus, _ := api.API.GetUserStatus(mattermostUserID) switch av { - case AVAILABILITY_VIEW_FREE: + case availabilityViewFree: if currentStatus.Status == "dnd" { api.API.UpdateUserStatus(mattermostUserID, "online") return fmt.Sprintf("User is free. Setting user from %s to online.", currentStatus.Status) } else { return fmt.Sprintf("User is free, and is already set to %s.", currentStatus.Status) } - case AVAILABILITY_VIEW_TENTATIVE, AVAILABILITY_VIEW_BUSY: + case availabilityViewTentative, availabilityViewBusy: if currentStatus.Status != "dnd" { api.API.UpdateUserStatus(mattermostUserID, "dnd") return fmt.Sprintf("User is busy. Setting user from %s to dnd.", currentStatus.Status) } else { return fmt.Sprintf("User is busy, and is already set to %s.", currentStatus.Status) } - case AVAILABILITY_VIEW_OUT_OF_OFFICE: + case availabilityViewOutOfOffice: if currentStatus.Status != "offline" { api.API.UpdateUserStatus(mattermostUserID, "offline") return fmt.Sprintf("User is out of office. Setting user from %s to offline", currentStatus.Status) } else { return fmt.Sprintf("User is out of office, and is already set to %s.", currentStatus.Status) } - case AVAILABILITY_VIEW_WORKING_ELSEWHERE: + case availabilityViewWorkingElsewhere: return fmt.Sprintf("User is working elsewhere. Pending implementation.") } diff --git a/server/api/mock_api/mock_availability.go b/server/api/mock_api/mock_availability.go new file mode 100644 index 00000000..78a78cb6 --- /dev/null +++ b/server/api/mock_api/mock_availability.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-msoffice/server/api (interfaces: Availability) + +// Package mock_api is a generated GoMock package. +package mock_api + +import ( + gomock "github.com/golang/mock/gomock" + remote "github.com/mattermost/mattermost-plugin-msoffice/server/remote" + reflect "reflect" +) + +// MockAvailability is a mock of Availability interface +type MockAvailability struct { + ctrl *gomock.Controller + recorder *MockAvailabilityMockRecorder +} + +// MockAvailabilityMockRecorder is the mock recorder for MockAvailability +type MockAvailabilityMockRecorder struct { + mock *MockAvailability +} + +// NewMockAvailability creates a new mock instance +func NewMockAvailability(ctrl *gomock.Controller) *MockAvailability { + mock := &MockAvailability{ctrl: ctrl} + mock.recorder = &MockAvailabilityMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockAvailability) EXPECT() *MockAvailabilityMockRecorder { + return m.recorder +} + +// GetUserAvailabilities mocks base method +func (m *MockAvailability) GetUserAvailabilities(arg0 string, arg1 []string) ([]*remote.ScheduleInformation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserAvailabilities", arg0, arg1) + ret0, _ := ret[0].([]*remote.ScheduleInformation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserAvailabilities indicates an expected call of GetUserAvailabilities +func (mr *MockAvailabilityMockRecorder) GetUserAvailabilities(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserAvailabilities", reflect.TypeOf((*MockAvailability)(nil).GetUserAvailabilities), arg0, arg1) +} + +// SyncStatusForAllUsers mocks base method +func (m *MockAvailability) SyncStatusForAllUsers() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SyncStatusForAllUsers") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SyncStatusForAllUsers indicates an expected call of SyncStatusForAllUsers +func (mr *MockAvailabilityMockRecorder) SyncStatusForAllUsers() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncStatusForAllUsers", reflect.TypeOf((*MockAvailability)(nil).SyncStatusForAllUsers)) +} + +// SyncStatusForSingleUser mocks base method +func (m *MockAvailability) SyncStatusForSingleUser() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SyncStatusForSingleUser") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SyncStatusForSingleUser indicates an expected call of SyncStatusForSingleUser +func (mr *MockAvailabilityMockRecorder) SyncStatusForSingleUser() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncStatusForSingleUser", reflect.TypeOf((*MockAvailability)(nil).SyncStatusForSingleUser)) +} diff --git a/server/api/mock_api/mock_subscriptions.go b/server/api/mock_api/mock_subscriptions.go index 348a13dd..13c9eb74 100644 --- a/server/api/mock_api/mock_subscriptions.go +++ b/server/api/mock_api/mock_subscriptions.go @@ -77,36 +77,6 @@ func (mr *MockSubscriptionsMockRecorder) DeleteUserEventSubscription() *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserEventSubscription", reflect.TypeOf((*MockSubscriptions)(nil).DeleteUserEventSubscription)) } -// GetAllUsersAvailability mocks base method -func (m *MockSubscriptions) GetAllUsersAvailability() (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAllUsersAvailability") - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAllUsersAvailability indicates an expected call of GetAllUsersAvailability -func (mr *MockSubscriptionsMockRecorder) GetAllUsersAvailability() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllUsersAvailability", reflect.TypeOf((*MockSubscriptions)(nil).GetAllUsersAvailability)) -} - -// GetUserAvailability mocks base method -func (m *MockSubscriptions) GetUserAvailability() (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserAvailability") - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserAvailability indicates an expected call of GetUserAvailability -func (mr *MockSubscriptionsMockRecorder) GetUserAvailability() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserAvailability", reflect.TypeOf((*MockSubscriptions)(nil).GetUserAvailability)) -} - // ListRemoteSubscriptions mocks base method func (m *MockSubscriptions) ListRemoteSubscriptions() ([]*remote.Subscription, error) { m.ctrl.T.Helper() diff --git a/server/api/status_sync_job.go b/server/api/status_sync_job.go new file mode 100644 index 00000000..bb5cfc12 --- /dev/null +++ b/server/api/status_sync_job.go @@ -0,0 +1,71 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package api + +import ( + "sync" + "time" + + "github.com/mattermost/mattermost-plugin-msoffice/server/utils/bot" +) + +type StatusSyncJob struct { + api API + cancel chan struct{} + cancelled chan struct{} + cancelOnce sync.Once +} + +func (j *StatusSyncJob) getLogger() bot.Logger { + return j.api.(*api).Logger +} + +func (j *StatusSyncJob) work() { + log := j.getLogger() + log.Debugf("User status sync job beginning") + + _, err := j.api.SyncStatusForAllUsers() + if err != nil { + log.Errorf("Error during user status sync job", "error", err.Error()) + } + + log.Debugf("User status sync job finished") +} + +func NewStatusSyncJob(api API) *StatusSyncJob { + return &StatusSyncJob{ + cancel: make(chan struct{}), + cancelled: make(chan struct{}), + api: api, + } +} + +const JOB_INTERVAL = 1 * time.Minute + +func (job *StatusSyncJob) Start() { + go func() { + defer close(job.cancelled) + + ticker := time.NewTicker(JOB_INTERVAL) + defer func() { + ticker.Stop() + }() + + for { + select { + case <-ticker.C: + job.work() + case <-job.cancel: + return + } + } + }() +} + +func (job *StatusSyncJob) Cancel() { + job.cancelOnce.Do(func() { + close(job.cancel) + }) + <-job.cancelled +} diff --git a/server/config/config.go b/server/config/config.go index 90c6f739..818028be 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -9,6 +9,8 @@ type StoredConfig struct { OAuth2ClientID string OAuth2ClientSecret string + EnableStatusSyncJob bool + bot.BotConfig } diff --git a/server/job/recurring_job.go b/server/job/recurring_job.go deleted file mode 100644 index 67c8bed2..00000000 --- a/server/job/recurring_job.go +++ /dev/null @@ -1,5 +0,0 @@ -package job - -type RecurringJob interface { - Run() -} diff --git a/server/plugin/command/availability.go b/server/plugin/command/availability.go index 01871c9d..915b1464 100644 --- a/server/plugin/command/availability.go +++ b/server/plugin/command/availability.go @@ -6,14 +6,14 @@ package command func (c *Command) availability(parameters ...string) (string, error) { switch { case len(parameters) == 0: - resString, err := c.API.GetUserAvailability() + resString, err := c.API.SyncStatusForSingleUser() if err != nil { return "", err } return resString, nil case len(parameters) == 1 && parameters[0] == "all": - resString, err := c.API.GetAllUsersAvailability() + resString, err := c.API.SyncStatusForAllUsers() if err != nil { return "", err } diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 1d4b8c2a..d82ec9af 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -29,8 +29,9 @@ import ( type Plugin struct { plugin.MattermostPlugin - configLock *sync.RWMutex - config *config.Config + configLock *sync.RWMutex + config *config.Config + statusSyncJob *api.StatusSyncJob httpHandler *http.Handler notificationHandler api.NotificationHandler @@ -68,13 +69,11 @@ func (p *Plugin) OnActivate() error { p.httpHandler = http.NewHandler() - conf := p.newAPIConfig() - p.notificationHandler = api.NewNotificationHandler(conf) + p.notificationHandler = api.NewNotificationHandler(p.newAPIConfig()) command.Register(p.API.RegisterCommand) - // j := api.NewAvailabilityJob(api.New(conf, "")) - // go j.Run() + p.initUserStatusSyncJob() p.API.LogInfo(p.config.PluginID + " activated") return nil @@ -117,6 +116,9 @@ func (p *Plugin) OnConfigurationChange() error { if p.notificationHandler != nil { p.notificationHandler.Configure(p.newAPIConfig()) } + + p.initUserStatusSyncJob() + return nil } @@ -213,3 +215,26 @@ func (p *Plugin) loadTemplates(bundlePath string) error { p.Templates = templates return nil } + +func (p *Plugin) initUserStatusSyncJob() { + conf := p.newAPIConfig() + enable := p.getConfig().EnableStatusSyncJob + logger := conf.Dependencies.Logger + + // Config is set to enable. No job exists, start a new job. + if enable && p.statusSyncJob == nil { + logger.Debugf("Enabling user status sync job") + + job := api.NewStatusSyncJob(api.New(conf, "")) + p.statusSyncJob = job + go job.Start() + } + + // Config is set to disable. Job exists, kill existing job. + if !enable && p.statusSyncJob != nil { + logger.Debugf("Disabling user status sync job") + + p.statusSyncJob.Cancel() + p.statusSyncJob = nil + } +} From 517ce031da2c7c659cbed145504980814da5d7b9 Mon Sep 17 00:00:00 2001 From: mickmister Date: Wed, 8 Jan 2020 16:38:24 -0500 Subject: [PATCH 10/18] rename AppLevelClient to SuperuserClient --- server/api/api.go | 4 ++-- server/api/availability.go | 2 +- server/api/status_sync_job.go | 4 ++-- .../{get_app_level_token.go => get_super_user_token.go} | 2 +- server/remote/msgraph/remote.go | 8 ++++---- server/remote/remote.go | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) rename server/remote/msgraph/{get_app_level_token.go => get_super_user_token.go} (94%) diff --git a/server/api/api.go b/server/api/api.go index 3c48d3ad..29600ae9 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -106,8 +106,8 @@ func (api *api) MakeClient() (remote.Client, error) { return api.Remote.NewClient(context.Background(), api.user.OAuth2Token), nil } -func (api *api) MakeAppClient() (remote.Client, error) { - return api.Remote.NewAppLevelClient(context.Background()), nil +func (api *api) MakeSuperuserClient() (remote.Client, error) { + return api.Remote.NewSuperuserClient(context.Background()), nil } func (api *api) Filter(filters ...filterf) error { diff --git a/server/api/availability.go b/server/api/availability.go index f3f109ae..1db93a70 100644 --- a/server/api/availability.go +++ b/server/api/availability.go @@ -80,7 +80,7 @@ func (api *api) SyncStatusForAllUsers() (string, error) { } func (api *api) GetUserAvailabilities(remoteUserID string, scheduleIDs []string) ([]*remote.ScheduleInformation, error) { - client, err := api.MakeAppClient() + client, err := api.MakeSuperuserClient() if err != nil { return nil, err } diff --git a/server/api/status_sync_job.go b/server/api/status_sync_job.go index bb5cfc12..d24833a8 100644 --- a/server/api/status_sync_job.go +++ b/server/api/status_sync_job.go @@ -10,6 +10,8 @@ import ( "github.com/mattermost/mattermost-plugin-msoffice/server/utils/bot" ) +const JOB_INTERVAL = 1 * time.Minute + type StatusSyncJob struct { api API cancel chan struct{} @@ -41,8 +43,6 @@ func NewStatusSyncJob(api API) *StatusSyncJob { } } -const JOB_INTERVAL = 1 * time.Minute - func (job *StatusSyncJob) Start() { go func() { defer close(job.cancelled) diff --git a/server/remote/msgraph/get_app_level_token.go b/server/remote/msgraph/get_super_user_token.go similarity index 94% rename from server/remote/msgraph/get_app_level_token.go rename to server/remote/msgraph/get_super_user_token.go index f0056cd7..f115a1bb 100644 --- a/server/remote/msgraph/get_app_level_token.go +++ b/server/remote/msgraph/get_super_user_token.go @@ -14,7 +14,7 @@ type AuthResponse struct { AccessToken string `json:"access_token"` } -func (c *client) getAppLevelToken() (string, error) { +func (c *client) getSuperuserToken() (string, error) { params := map[string]string{ "client_id": c.conf.OAuth2ClientID, "scope": "https://graph.microsoft.com/.default", diff --git a/server/remote/msgraph/remote.go b/server/remote/msgraph/remote.go index 84dea93c..4e5c4af0 100644 --- a/server/remote/msgraph/remote.go +++ b/server/remote/msgraph/remote.go @@ -48,11 +48,11 @@ func (r *impl) NewClient(ctx context.Context, token *oauth2.Token) remote.Client return c } -// NewAppLevelClient creates a new client used for app-only permissions. -func (r *impl) NewAppLevelClient(ctx context.Context) remote.Client { +// NewSuperuserClient creates a new client used for app-only permissions. +func (r *impl) NewSuperuserClient(ctx context.Context) remote.Client { httpClient := &http.Client{} - // Hack. getAppLevelToken should just use http and not a client + // TODO: getSuperuserToken should just use http and not a client c := &client{ conf: r.conf, ctx: ctx, @@ -61,7 +61,7 @@ func (r *impl) NewAppLevelClient(ctx context.Context) remote.Client { rbuilder: msgraph.NewClient(httpClient), } - token, _ := c.getAppLevelToken() + token, _ := c.getSuperuserToken() o := &oauth2.Token{ AccessToken: token, diff --git a/server/remote/remote.go b/server/remote/remote.go index d0cf3ec3..e6dae59a 100644 --- a/server/remote/remote.go +++ b/server/remote/remote.go @@ -15,7 +15,7 @@ import ( type Remote interface { NewClient(context.Context, *oauth2.Token) Client - NewAppLevelClient(context.Context) Client + NewSuperuserClient(context.Context) Client NewOAuth2Config() *oauth2.Config HandleWebhook(http.ResponseWriter, *http.Request) []*Notification } From c7ef85279cc97a5fdb51d3f10fbd527c663e7a2b Mon Sep 17 00:00:00 2001 From: mickmister Date: Wed, 8 Jan 2020 16:39:55 -0500 Subject: [PATCH 11/18] batch requests properly to handle > 400 users --- go.mod | 1 + server/remote/msgraph/batch_request.go | 54 +++++++-- server/remote/msgraph/call.go | 6 +- server/remote/msgraph/get_schedule_batched.go | 107 +++++++++++------- .../msgraph/get_schedule_batched_test.go | 86 ++++++++++++++ server/remote/schedule.go | 4 - 6 files changed, 195 insertions(+), 63 deletions(-) create mode 100644 server/remote/msgraph/get_schedule_batched_test.go diff --git a/go.mod b/go.mod index 9c4914af..422598bf 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gorilla/mux v1.7.3 github.com/jarcoal/httpmock v1.0.4 github.com/mattermost/mattermost-server/v5 v5.18.0-rc.test + github.com/mitchellh/mapstructure v1.1.2 github.com/pkg/errors v0.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.4.0 diff --git a/server/remote/msgraph/batch_request.go b/server/remote/msgraph/batch_request.go index 582089c0..852d8ded 100644 --- a/server/remote/msgraph/batch_request.go +++ b/server/remote/msgraph/batch_request.go @@ -7,7 +7,9 @@ import ( "net/http" ) -type SingleRequest struct { +const maxNumRequestsPerBatch = 20 + +type singleRequest struct { ID string `json:"id"` URL string `json:"url"` Method string `json:"method"` @@ -15,29 +17,57 @@ type SingleRequest struct { Headers map[string]string `json:"headers"` } -type SingleResponse struct { +type singleResponse struct { ID string `json:"id"` Status int `json:"status"` Body interface{} `json:"body"` Headers map[string]string `json:"headers"` } -type FullBatchResponse struct { - Responses []*SingleResponse `json:"responses"` +type fullBatchResponse struct { + Responses []interface{} `json:"responses"` } -type FullBatchRequest struct { - Requests []*SingleRequest `json:"requests"` +type fullBatchRequest struct { + Requests []*singleRequest `json:"requests"` } -func (c *client) batchRequest(requests []*SingleRequest, out interface{}) error { - batchReq := FullBatchRequest{Requests: requests} +func (c *client) batchRequest(requests []*singleRequest) (error, []*fullBatchResponse) { u := "https://graph.microsoft.com/v1.0/$batch" - _, err := c.Call(http.MethodPost, u, batchReq, out) - if err != nil { - return err + batchRequests := prepareBatchRequests(requests) + result := []*fullBatchResponse{} + for _, req := range batchRequests { + res := &fullBatchResponse{} + _, err := c.Call(http.MethodPost, u, req, res) + if err != nil { + return err, nil + } + result = append(result, res) + } + + return nil, result +} + +func prepareBatchRequests(requests []*singleRequest) []fullBatchRequest { + numFullRequests := len(requests) / maxNumRequestsPerBatch + if len(requests)%maxNumRequestsPerBatch != 0 { + numFullRequests += 1 + } + + result := []fullBatchRequest{} + + for i := 0; i < numFullRequests; i++ { + startIdx := i * maxNumRequestsPerBatch + endIdx := startIdx + maxNumRequestsPerBatch + if i == numFullRequests-1 { + endIdx = len(requests) + } + + slice := requests[startIdx:endIdx] + batchReq := fullBatchRequest{Requests: slice} + result = append(result, batchReq) } - return nil + return result } diff --git a/server/remote/msgraph/call.go b/server/remote/msgraph/call.go index 31841667..00f4d8f5 100644 --- a/server/remote/msgraph/call.go +++ b/server/remote/msgraph/call.go @@ -66,11 +66,7 @@ func (c *client) Call(method, path string, in, out interface{}) (responseData [] req = req.WithContext(c.ctx) } - httpClient := c.httpClient - if httpClient == nil { - httpClient = &http.Client{} - } - resp, err := httpClient.Do(req) + resp, err := c.httpClient.Do(req) if err != nil { return nil, err } diff --git a/server/remote/msgraph/get_schedule_batched.go b/server/remote/msgraph/get_schedule_batched.go index 4b6ca918..f79d4b7f 100644 --- a/server/remote/msgraph/get_schedule_batched.go +++ b/server/remote/msgraph/get_schedule_batched.go @@ -3,21 +3,25 @@ package msgraph import ( "strconv" + "github.com/mitchellh/mapstructure" + "github.com/mattermost/mattermost-plugin-msoffice/server/remote" ) -type GetScheduleSingleResponse struct { - ID string `json:"id"` - Status int `json:"status"` - Body *remote.GetScheduleResponse `json:"body"` - Headers map[string]string `json:"headers"` +const maxNumUsersPerRequest = 20 + +type getScheduleSingleResponse struct { + ID string `json:"id"` + Status int `json:"status"` + Body getScheduleResponse `json:"body"` + Headers map[string]string `json:"headers"` } -type GetScheduleBatchResponse struct { - Responses []*GetScheduleSingleResponse `json:"responses"` +type getScheduleBatchResponse struct { + Responses []*getScheduleSingleResponse `json:"responses"` } -type GetScheduleRequest struct { +type getScheduleRequest struct { // List of emails of users that we want to check Schedules []string `json:"schedules"` @@ -32,51 +36,65 @@ type GetScheduleRequest struct { AvailabilityViewInterval int `json:"availabilityViewInterval"` } +type getScheduleResponse struct { + Value []*remote.ScheduleInformation `json:"value,omitempty"` +} + func (c *client) GetSchedule(remoteUserID string, schedules []string, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { - params := &GetScheduleRequest{ + params := &getScheduleRequest{ StartTime: startTime, EndTime: endTime, AvailabilityViewInterval: availabilityViewInterval, } - allRequests := getFullBatchRequest(remoteUserID, schedules, params) + allRequests := prepareGetScheduleRequests(remoteUserID, schedules, params) - batchRes := GetScheduleBatchResponse{} - err := c.batchRequest(allRequests, &batchRes) + err, batchResponses := c.batchRequest(allRequests) if err != nil { return nil, err } - sorted := make([]*GetScheduleSingleResponse, len(allRequests)) - for _, r := range batchRes.Responses { - id, _ := strconv.Atoi(r.ID) - sorted[id] = r - } - result := []*remote.ScheduleInformation{} - for _, r := range sorted { - for _, sched := range r.Body.Value { - result = append(result, sched) + + for i, batchRes := range batchResponses { + length := maxNumRequestsPerBatch + if i == len(batchResponses)-1 { + length = len(allRequests) % maxNumRequestsPerBatch + } + + sorted := make([]*getScheduleSingleResponse, length) + + for _, r := range batchRes.Responses { + res := &getScheduleSingleResponse{} + mapstructure.Decode(r, res) // TODO: handle request error case. response may look different if this single request had an error + + id, _ := strconv.Atoi(res.ID) + sorted[id] = res + } + + for _, r := range sorted { + for _, sched := range r.Body.Value { + result = append(result, sched) + } } } return result, nil } -func getFullBatchRequest(remoteUserID string, schedules []string, params *GetScheduleRequest) []*SingleRequest { +func prepareGetScheduleRequests(remoteUserID string, schedules []string, params *getScheduleRequest) []*singleRequest { u := "/Users/" + remoteUserID + "/calendar/getSchedule" - makeRequest := func() *SingleRequest { - p := &GetScheduleRequest{ - Schedules: schedules, // need to chunk these out - StartTime: params.StartTime, - EndTime: params.EndTime, - AvailabilityViewInterval: params.AvailabilityViewInterval, - } - req := &SingleRequest{ + makeRequest := func(schedBatch []string) *singleRequest { + req := &singleRequest{ URL: u, Method: "POST", - Body: p, + Body: &getScheduleRequest{ + Schedules: schedBatch, + StartTime: params.StartTime, + EndTime: params.EndTime, + AvailabilityViewInterval: params.AvailabilityViewInterval, + }, Headers: map[string]string{ "Content-Type": "application/json", }, @@ -84,20 +102,25 @@ func getFullBatchRequest(remoteUserID string, schedules []string, params *GetSch return req } - // This is where we can simulate large batches - // TODO: Split up emails given into different batches properly - allRequests := []*SingleRequest{} - // allRequests = append(allRequests, makeRequest()) - - numRequestsInBatch := 1 - // numRequestsInBatch := 20 + allRequests := []*singleRequest{} - for i := 0; i < numRequestsInBatch; i++ { - allRequests = append(allRequests, makeRequest()) + numUsers := len(schedules) + numRequests := numUsers / maxNumUsersPerRequest + if numUsers%maxNumUsersPerRequest != 0 { + numRequests += 1 } - for i, r := range allRequests { - r.ID = strconv.Itoa(i) + for i := 0; i < numRequests; i++ { + startIdx := i * maxNumUsersPerRequest + endIdx := startIdx + maxNumUsersPerRequest + if i == numRequests-1 { + endIdx = len(schedules) + } + + slice := schedules[startIdx:endIdx] + req := makeRequest(slice) + req.ID = strconv.Itoa(i) + allRequests = append(allRequests, req) } return allRequests diff --git a/server/remote/msgraph/get_schedule_batched_test.go b/server/remote/msgraph/get_schedule_batched_test.go new file mode 100644 index 00000000..bf721ac3 --- /dev/null +++ b/server/remote/msgraph/get_schedule_batched_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package msgraph + +import ( + "testing" + "time" + + "github.com/mattermost/mattermost-plugin-msoffice/server/remote" + "github.com/stretchr/testify/require" +) + +func TestPrepareGetScheduleRequests(t *testing.T) { + for name, tc := range map[string]struct { + schedules []string + runAssertions func(t *testing.T, out []*singleRequest) + }{ + "5 emails": { + schedules: []string{"a", "b", "c", "d", "e"}, + runAssertions: func(t *testing.T, out []*singleRequest) { + sched1 := []string{"a", "b", "c", "d", "e"} + + require.Equal(t, 1, len(out)) + require.Equal(t, "0", out[0].ID) + body := out[0].Body.(*getScheduleRequest) + require.Equal(t, 5, len(body.Schedules)) + require.Equal(t, sched1, body.Schedules) + }, + }, + "20 emails": { + schedules: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t"}, + runAssertions: func(t *testing.T, out []*singleRequest) { + sched1 := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t"} + + require.Equal(t, 1, len(out)) + require.Equal(t, "0", out[0].ID) + body := out[0].Body.(*getScheduleRequest) + require.Equal(t, 20, len(body.Schedules)) + require.Equal(t, sched1, body.Schedules) + }, + }, + "26 emails": { + schedules: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}, + runAssertions: func(t *testing.T, out []*singleRequest) { + sched1 := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t"} + sched2 := []string{"u", "v", "w", "x", "y", "z"} + + require.Equal(t, 2, len(out)) + require.Equal(t, "0", out[0].ID) + body := out[0].Body.(*getScheduleRequest) + require.Equal(t, 20, len(body.Schedules)) + require.Equal(t, sched1, body.Schedules) + + require.Equal(t, "1", out[1].ID) + body = out[1].Body.(*getScheduleRequest) + require.Equal(t, 6, len(body.Schedules)) + require.Equal(t, sched2, body.Schedules) + }, + }, + } { + t.Run(name, func(t *testing.T) { + userID := "xyz" + start := time.Now() + end := time.Now().Add(20) + params := &getScheduleRequest{ + StartTime: remote.NewDateTime(start), + EndTime: remote.NewDateTime(end), + AvailabilityViewInterval: 15, + } + + out := prepareGetScheduleRequests(userID, tc.schedules, params) + require.Equal(t, "/Users/xyz/calendar/getSchedule", out[0].URL) + require.Equal(t, "POST", out[0].Method) + require.Equal(t, 1, len(out[0].Headers)) + require.Equal(t, "application/json", out[0].Headers["Content-Type"]) + + body := out[0].Body.(*getScheduleRequest) + require.Equal(t, params.StartTime.String(), body.StartTime.String()) + require.Equal(t, params.EndTime.String(), body.EndTime.String()) + require.Equal(t, 15, body.AvailabilityViewInterval) + + tc.runAssertions(t, out) + }) + } +} diff --git a/server/remote/schedule.go b/server/remote/schedule.go index f013f862..9f0cbd38 100644 --- a/server/remote/schedule.go +++ b/server/remote/schedule.go @@ -16,7 +16,3 @@ type ScheduleInformation struct { // WorkingHours interface{} `json:"workingHours,omitempty"` // Error *FreeBusyError `json:"error,omitempty"` } - -type GetScheduleResponse struct { - Value []*ScheduleInformation `json:"value,omitempty"` -} From b029e30eeabcccb381c61f47ef0644c3815e087a Mon Sep 17 00:00:00 2001 From: mickmister Date: Fri, 10 Jan 2020 12:08:25 -0500 Subject: [PATCH 12/18] change AllUsers name to UserIndex. handle getSchedule error. --- server/api/api.go | 10 +-- server/api/availability.go | 78 +++++++++++++------ server/api/calendar.go | 12 +-- server/api/event.go | 8 +- server/api/mock_api/mock_availability.go | 8 +- server/api/mock_api/mock_client.go | 12 +-- server/api/status_sync_job.go | 2 +- server/api/subscription.go | 8 +- server/plugin/command/availability.go | 3 +- server/remote/client.go | 6 +- server/remote/mock_remote/mock_client.go | 40 +++++++++- server/remote/mock_remote/mock_remote.go | 28 +++---- server/remote/msgraph/batch_request.go | 2 +- server/remote/msgraph/call.go | 36 ++++----- .../remote/msgraph/get_notification_data.go | 2 +- server/remote/msgraph/get_schedule_batched.go | 10 ++- server/remote/msgraph/get_super_user_token.go | 2 +- server/remote/msgraph/remote.go | 2 - server/remote/schedule.go | 7 ++ server/store/mock_store/mock_user_store.go | 30 +++---- server/store/store.go | 4 +- server/store/user_store.go | 4 +- 22 files changed, 195 insertions(+), 119 deletions(-) diff --git a/server/api/api.go b/server/api/api.go index 29600ae9..75f96a6a 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -47,12 +47,12 @@ type Event interface { type Availability interface { GetUserAvailabilities(remoteUserID string, scheduleIDs []string) ([]*remote.ScheduleInformation, error) - SyncStatusForSingleUser() (string, error) + SyncStatusForSingleUser(mattermostUserID string) (string, error) SyncStatusForAllUsers() (string, error) } type Client interface { - MakeClient() (remote.Client, error) + NewClient() (remote.Client, error) } type API interface { @@ -97,7 +97,7 @@ func New(apiConfig Config, mattermostUserID string) API { type filterf func(*api) error -func (api *api) MakeClient() (remote.Client, error) { +func (api *api) NewClient() (remote.Client, error) { err := api.Filter(withUser) if err != nil { return nil, err @@ -106,8 +106,8 @@ func (api *api) MakeClient() (remote.Client, error) { return api.Remote.NewClient(context.Background(), api.user.OAuth2Token), nil } -func (api *api) MakeSuperuserClient() (remote.Client, error) { - return api.Remote.NewSuperuserClient(context.Background()), nil +func (api *api) NewSuperuserClient() remote.Client { + return api.Remote.NewSuperuserClient(context.Background()) } func (api *api) Filter(filters ...filterf) error { diff --git a/server/api/availability.go b/server/api/availability.go index 1db93a70..43ae173b 100644 --- a/server/api/availability.go +++ b/server/api/availability.go @@ -9,6 +9,7 @@ import ( "github.com/mattermost/mattermost-plugin-msoffice/server/remote" "github.com/mattermost/mattermost-plugin-msoffice/server/utils" + "github.com/pkg/errors" ) const ( @@ -21,8 +22,8 @@ const ( availabilityViewWorkingElsewhere = '4' ) -func (api *api) SyncStatusForSingleUser() (string, error) { - u, err := api.UserStore.LoadUser(api.mattermostUserID) +func (api *api) SyncStatusForSingleUser(mattermostUserID string) (string, error) { + u, err := api.UserStore.LoadUser(mattermostUserID) if err != nil { return "", err } @@ -37,14 +38,29 @@ func (api *api) SyncStatusForSingleUser() (string, error) { return "No schedule info found", nil } + status, appErr := api.Dependencies.API.GetUserStatus(api.mattermostUserID) + if appErr != nil { + return "", appErr + } + s := sched[0] + if s.Error != nil { + return "", errors.Errorf("Error getting availability for %s: %s", s.ScheduleID, s.Error.ResponseCode) + } + if len(s.AvailabilityView) == 0 { + return "No availabilities found", nil + } + av := s.AvailabilityView[0] - return api.setUserStatusFromAvailability(api.mattermostUserID, av), nil + return api.setUserStatusFromAvailability(api.mattermostUserID, status.Status, av), nil } func (api *api) SyncStatusForAllUsers() (string, error) { - users, err := api.UserStore.LoadAllUsers() + users, err := api.UserStore.LoadUserIndex() if err != nil { + if err.Error() == "not found" { + return "No users found in user index", nil + } return "", err } @@ -53,8 +69,10 @@ func (api *api) SyncStatusForAllUsers() (string, error) { } scheduleIDs := []string{} + mattermostUserIDs := []string{} for _, u := range users { scheduleIDs = append(scheduleIDs, u.Email) + mattermostUserIDs = append(mattermostUserIDs, u.MattermostUserID) } sched, err := api.GetUserAvailabilities(users[0].RemoteID, scheduleIDs) @@ -65,11 +83,32 @@ func (api *api) SyncStatusForAllUsers() (string, error) { return "No schedule info found", nil } + statuses, appErr := api.Dependencies.API.GetUserStatusesByIds(mattermostUserIDs) + if appErr != nil { + return "", appErr + } + + statusMap := map[string]string{} + for _, s := range statuses { + statusMap[s.UserId] = s.Status + } + var res string for i, s := range sched { - userID := users[i].MattermostUserID + if s.Error != nil { + api.Logger.Errorf("Error getting availability for %s: %s", s.ScheduleID, s.Error.ResponseCode) + continue + } + av := s.AvailabilityView[0] - res = api.setUserStatusFromAvailability(userID, av) + + userID := users[i].MattermostUserID + status, ok := statusMap[userID] + if !ok { + continue + } + + res = api.setUserStatusFromAvailability(userID, status, av) } if res != "" { @@ -80,10 +119,7 @@ func (api *api) SyncStatusForAllUsers() (string, error) { } func (api *api) GetUserAvailabilities(remoteUserID string, scheduleIDs []string) ([]*remote.ScheduleInformation, error) { - client, err := api.MakeSuperuserClient() - if err != nil { - return nil, err - } + client := api.NewSuperuserClient() start := remote.NewDateTime(time.Now()) end := remote.NewDateTime(time.Now().Add(availabilityTimeWindowSize * time.Minute)) @@ -91,30 +127,28 @@ func (api *api) GetUserAvailabilities(remoteUserID string, scheduleIDs []string) return client.GetSchedule(remoteUserID, scheduleIDs, start, end, availabilityTimeWindowSize) } -func (api *api) setUserStatusFromAvailability(mattermostUserID string, av byte) string { - currentStatus, _ := api.API.GetUserStatus(mattermostUserID) - +func (api *api) setUserStatusFromAvailability(mattermostUserID, currentStatus string, av byte) string { switch av { case availabilityViewFree: - if currentStatus.Status == "dnd" { + if currentStatus == "dnd" { api.API.UpdateUserStatus(mattermostUserID, "online") - return fmt.Sprintf("User is free. Setting user from %s to online.", currentStatus.Status) + 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.Status) + return fmt.Sprintf("User is free, and is already set to %s.", currentStatus) } case availabilityViewTentative, availabilityViewBusy: - if currentStatus.Status != "dnd" { + if currentStatus != "dnd" { api.API.UpdateUserStatus(mattermostUserID, "dnd") - return fmt.Sprintf("User is busy. Setting user from %s to dnd.", currentStatus.Status) + 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.Status) + return fmt.Sprintf("User is busy, and is already set to %s.", currentStatus) } case availabilityViewOutOfOffice: - if currentStatus.Status != "offline" { + if currentStatus != "offline" { api.API.UpdateUserStatus(mattermostUserID, "offline") - return fmt.Sprintf("User is out of office. Setting user from %s to offline", currentStatus.Status) + 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.Status) + return fmt.Sprintf("User is out of office, and is already set to %s.", currentStatus) } case availabilityViewWorkingElsewhere: return fmt.Sprintf("User is working elsewhere. Pending implementation.") diff --git a/server/api/calendar.go b/server/api/calendar.go index 0b7700c3..1b7b0033 100644 --- a/server/api/calendar.go +++ b/server/api/calendar.go @@ -10,7 +10,7 @@ import ( ) func (api *api) ViewCalendar(from, to time.Time) ([]*remote.Event, error) { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return nil, err } @@ -19,7 +19,7 @@ func (api *api) ViewCalendar(from, to time.Time) ([]*remote.Event, error) { } func (api *api) CreateCalendar(calendar *remote.Calendar) (*remote.Calendar, error) { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return nil, err } @@ -28,7 +28,7 @@ func (api *api) CreateCalendar(calendar *remote.Calendar) (*remote.Calendar, err } func (api *api) CreateEvent(calendarEvent *remote.Event) (*remote.Event, error) { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return nil, err } @@ -37,7 +37,7 @@ func (api *api) CreateEvent(calendarEvent *remote.Event) (*remote.Event, error) } func (api *api) DeleteCalendar(calendarID string) error { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return err } @@ -46,7 +46,7 @@ func (api *api) DeleteCalendar(calendarID string) error { } func (api *api) FindMeetingTimes(meetingParams *remote.FindMeetingTimesParameters) (*remote.MeetingTimeSuggestionResults, error) { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return nil, err } @@ -55,7 +55,7 @@ func (api *api) FindMeetingTimes(meetingParams *remote.FindMeetingTimesParameter } func (api *api) GetUserCalendars(userID string) ([]*remote.Calendar, error) { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return nil, err } diff --git a/server/api/event.go b/server/api/event.go index cffae66b..b410f5a0 100644 --- a/server/api/event.go +++ b/server/api/event.go @@ -6,7 +6,7 @@ package api import "github.com/pkg/errors" func (api *api) AcceptEvent(eventID string) error { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return err } @@ -15,7 +15,7 @@ func (api *api) AcceptEvent(eventID string) error { } func (api *api) DeclineEvent(eventID string) error { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return err } @@ -24,7 +24,7 @@ func (api *api) DeclineEvent(eventID string) error { } func (api *api) TentativelyAcceptEvent(eventID string) error { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return err } @@ -37,7 +37,7 @@ func (api *api) RespondToEvent(eventID, response string) error { return errors.New("Not responded is not a valid response") } - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return err } diff --git a/server/api/mock_api/mock_availability.go b/server/api/mock_api/mock_availability.go index 78a78cb6..9b04521b 100644 --- a/server/api/mock_api/mock_availability.go +++ b/server/api/mock_api/mock_availability.go @@ -64,16 +64,16 @@ func (mr *MockAvailabilityMockRecorder) SyncStatusForAllUsers() *gomock.Call { } // SyncStatusForSingleUser mocks base method -func (m *MockAvailability) SyncStatusForSingleUser() (string, error) { +func (m *MockAvailability) SyncStatusForSingleUser(arg0 string) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SyncStatusForSingleUser") + ret := m.ctrl.Call(m, "SyncStatusForSingleUser", arg0) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // SyncStatusForSingleUser indicates an expected call of SyncStatusForSingleUser -func (mr *MockAvailabilityMockRecorder) SyncStatusForSingleUser() *gomock.Call { +func (mr *MockAvailabilityMockRecorder) SyncStatusForSingleUser(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncStatusForSingleUser", reflect.TypeOf((*MockAvailability)(nil).SyncStatusForSingleUser)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncStatusForSingleUser", reflect.TypeOf((*MockAvailability)(nil).SyncStatusForSingleUser), arg0) } diff --git a/server/api/mock_api/mock_client.go b/server/api/mock_api/mock_client.go index 01828dce..45e97089 100644 --- a/server/api/mock_api/mock_client.go +++ b/server/api/mock_api/mock_client.go @@ -33,17 +33,17 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { return m.recorder } -// MakeClient mocks base method -func (m *MockClient) MakeClient() (remote.Client, error) { +// NewClient mocks base method +func (m *MockClient) NewClient() (remote.Client, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MakeClient") + ret := m.ctrl.Call(m, "NewClient") ret0, _ := ret[0].(remote.Client) ret1, _ := ret[1].(error) return ret0, ret1 } -// MakeClient indicates an expected call of MakeClient -func (mr *MockClientMockRecorder) MakeClient() *gomock.Call { +// NewClient indicates an expected call of NewClient +func (mr *MockClientMockRecorder) NewClient() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeClient", reflect.TypeOf((*MockClient)(nil).MakeClient)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockClient)(nil).NewClient)) } diff --git a/server/api/status_sync_job.go b/server/api/status_sync_job.go index d24833a8..4276f75a 100644 --- a/server/api/status_sync_job.go +++ b/server/api/status_sync_job.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/mattermost-plugin-msoffice/server/utils/bot" ) -const JOB_INTERVAL = 1 * time.Minute +const JOB_INTERVAL = 5 * time.Minute type StatusSyncJob struct { api API diff --git a/server/api/subscription.go b/server/api/subscription.go index 2c2bf9e1..136532a0 100644 --- a/server/api/subscription.go +++ b/server/api/subscription.go @@ -12,7 +12,7 @@ import ( ) func (api *api) CreateUserEventSubscription() (*store.Subscription, error) { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return nil, err } @@ -49,7 +49,7 @@ func (api *api) LoadUserEventSubscription() (*store.Subscription, error) { } func (api *api) ListRemoteSubscriptions() ([]*remote.Subscription, error) { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return nil, err } @@ -61,7 +61,7 @@ func (api *api) ListRemoteSubscriptions() ([]*remote.Subscription, error) { } func (api *api) RenewUserEventSubscription() (*store.Subscription, error) { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return nil, err } @@ -105,7 +105,7 @@ func (api *api) DeleteUserEventSubscription() error { } func (api *api) DeleteOrphanedSubscription(subscriptionID string) error { - client, err := api.MakeClient() + client, err := api.NewClient() if err != nil { return err } diff --git a/server/plugin/command/availability.go b/server/plugin/command/availability.go index 915b1464..c77f0619 100644 --- a/server/plugin/command/availability.go +++ b/server/plugin/command/availability.go @@ -6,7 +6,7 @@ package command func (c *Command) availability(parameters ...string) (string, error) { switch { case len(parameters) == 0: - resString, err := c.API.SyncStatusForSingleUser() + resString, err := c.API.SyncStatusForSingleUser(c.Args.UserId) if err != nil { return "", err } @@ -20,5 +20,6 @@ func (c *Command) availability(parameters ...string) (string, error) { return resString, nil } + return "bad syntax", nil } diff --git a/server/remote/client.go b/server/remote/client.go index 4405d1bf..a1ad958d 100644 --- a/server/remote/client.go +++ b/server/remote/client.go @@ -4,12 +4,16 @@ package remote import ( + "io" + "net/url" "time" ) type Client interface { AcceptUserEvent(userID, eventID string) error - Call(method, path string, in, out interface{}) (responseData []byte, err error) + Call(method, path, contentType string, in io.Reader, out interface{}) (responseData []byte, err error) + CallJSON(method, path string, in, out interface{}) (responseData []byte, err error) + CallURLEncodedForm(method, path string, in url.Values, out interface{}) (responseData []byte, err error) CreateSubscription(notificationURL string) (*Subscription, error) DeclineUserEvent(userID, eventID string) error DeleteSubscription(subscriptionID string) error diff --git a/server/remote/mock_remote/mock_client.go b/server/remote/mock_remote/mock_client.go index 083bb132..67c609c9 100644 --- a/server/remote/mock_remote/mock_client.go +++ b/server/remote/mock_remote/mock_client.go @@ -7,6 +7,8 @@ package mock_remote import ( gomock "github.com/golang/mock/gomock" remote "github.com/mattermost/mattermost-plugin-msoffice/server/remote" + io "io" + url "net/url" reflect "reflect" time "time" ) @@ -49,18 +51,48 @@ func (mr *MockClientMockRecorder) AcceptUserEvent(arg0, arg1 interface{}) *gomoc } // Call mocks base method -func (m *MockClient) Call(arg0, arg1 string, arg2, arg3 interface{}) ([]byte, error) { +func (m *MockClient) Call(arg0, arg1, arg2 string, arg3 io.Reader, arg4 interface{}) ([]byte, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Call", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "Call", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } // Call indicates an expected call of Call -func (mr *MockClientMockRecorder) Call(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) Call(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockClient)(nil).Call), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockClient)(nil).Call), arg0, arg1, arg2, arg3, arg4) +} + +// CallJSON mocks base method +func (m *MockClient) CallJSON(arg0, arg1 string, arg2, arg3 interface{}) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CallJSON", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CallJSON indicates an expected call of CallJSON +func (mr *MockClientMockRecorder) CallJSON(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallJSON", reflect.TypeOf((*MockClient)(nil).CallJSON), arg0, arg1, arg2, arg3) +} + +// CallURLEncodedForm mocks base method +func (m *MockClient) CallURLEncodedForm(arg0, arg1 string, arg2 url.Values, arg3 interface{}) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CallURLEncodedForm", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CallURLEncodedForm indicates an expected call of CallURLEncodedForm +func (mr *MockClientMockRecorder) CallURLEncodedForm(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallURLEncodedForm", reflect.TypeOf((*MockClient)(nil).CallURLEncodedForm), arg0, arg1, arg2, arg3) } // CreateCalendar mocks base method diff --git a/server/remote/mock_remote/mock_remote.go b/server/remote/mock_remote/mock_remote.go index e5b5e738..3e54988d 100644 --- a/server/remote/mock_remote/mock_remote.go +++ b/server/remote/mock_remote/mock_remote.go @@ -50,20 +50,6 @@ func (mr *MockRemoteMockRecorder) HandleWebhook(arg0, arg1 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleWebhook", reflect.TypeOf((*MockRemote)(nil).HandleWebhook), arg0, arg1) } -// NewAppLevelClient mocks base method -func (m *MockRemote) NewAppLevelClient(arg0 context.Context) remote.Client { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewAppLevelClient", arg0) - ret0, _ := ret[0].(remote.Client) - return ret0 -} - -// NewAppLevelClient indicates an expected call of NewAppLevelClient -func (mr *MockRemoteMockRecorder) NewAppLevelClient(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewAppLevelClient", reflect.TypeOf((*MockRemote)(nil).NewAppLevelClient), arg0) -} - // NewClient mocks base method func (m *MockRemote) NewClient(arg0 context.Context, arg1 *oauth2.Token) remote.Client { m.ctrl.T.Helper() @@ -91,3 +77,17 @@ func (mr *MockRemoteMockRecorder) NewOAuth2Config() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewOAuth2Config", reflect.TypeOf((*MockRemote)(nil).NewOAuth2Config)) } + +// NewSuperuserClient mocks base method +func (m *MockRemote) NewSuperuserClient(arg0 context.Context) remote.Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewSuperuserClient", arg0) + ret0, _ := ret[0].(remote.Client) + return ret0 +} + +// NewSuperuserClient indicates an expected call of NewSuperuserClient +func (mr *MockRemoteMockRecorder) NewSuperuserClient(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewSuperuserClient", reflect.TypeOf((*MockRemote)(nil).NewSuperuserClient), arg0) +} diff --git a/server/remote/msgraph/batch_request.go b/server/remote/msgraph/batch_request.go index 852d8ded..782e8d45 100644 --- a/server/remote/msgraph/batch_request.go +++ b/server/remote/msgraph/batch_request.go @@ -39,7 +39,7 @@ func (c *client) batchRequest(requests []*singleRequest) (error, []*fullBatchRes result := []*fullBatchResponse{} for _, req := range batchRequests { res := &fullBatchResponse{} - _, err := c.Call(http.MethodPost, u, req, res) + _, err := c.CallJSON(http.MethodPost, u, req, res) if err != nil { return err, nil } diff --git a/server/remote/msgraph/call.go b/server/remote/msgraph/call.go index 00f4d8f5..a76266ea 100644 --- a/server/remote/msgraph/call.go +++ b/server/remote/msgraph/call.go @@ -17,7 +17,23 @@ import ( msgraph "github.com/yaegashi/msgraph.go/v1.0" ) -func (c *client) Call(method, path string, in, out interface{}) (responseData []byte, err error) { +func (c *client) CallJSON(method, path string, in, out interface{}) (responseData []byte, err error) { + contentType := "application/json" + buf := &bytes.Buffer{} + err = json.NewEncoder(buf).Encode(in) + if err != nil { + return nil, err + } + return c.Call(method, path, contentType, buf, out) +} + +func (c *client) CallURLEncodedForm(method, path string, in url.Values, out interface{}) (responseData []byte, err error) { + contentType := "application/x-www-form-urlencoded" + buf := strings.NewReader(in.Encode()) + return c.Call(method, path, contentType, buf, out) +} + +func (c *client) Call(method, path, contentType string, inBody io.Reader, out interface{}) (responseData []byte, err error) { errContext := fmt.Sprintf("msgraph: Call failed: method:%s, path:%s", method, path) pathURL, err := url.Parse(path) if err != nil { @@ -36,24 +52,6 @@ func (c *client) Call(method, path string, in, out interface{}) (responseData [] path = baseURL.String() + path } - var inBody io.Reader - var contentType string - if in != nil { - v, ok := in.(url.Values) - if ok { - contentType = "application/x-www-form-urlencoded" - inBody = strings.NewReader(v.Encode()) - } else { - contentType = "application/json" - buf := &bytes.Buffer{} - err = json.NewEncoder(buf).Encode(in) - if err != nil { - return nil, err - } - inBody = buf - } - } - req, err := http.NewRequest(method, path, inBody) if err != nil { return nil, err diff --git a/server/remote/msgraph/get_notification_data.go b/server/remote/msgraph/get_notification_data.go index 45246418..80a00be8 100644 --- a/server/remote/msgraph/get_notification_data.go +++ b/server/remote/msgraph/get_notification_data.go @@ -17,7 +17,7 @@ func (c *client) GetNotificationData(orig *remote.Notification) (*remote.Notific switch wh.ResourceData.DataType { case "#Microsoft.Graph.Event": event := remote.Event{} - _, err := c.Call(http.MethodGet, wh.Resource, nil, &event) + _, err := c.CallJSON(http.MethodGet, wh.Resource, nil, &event) if err != nil { c.Logger.With(bot.LogContext{ "Resource": wh.Resource, diff --git a/server/remote/msgraph/get_schedule_batched.go b/server/remote/msgraph/get_schedule_batched.go index f79d4b7f..8bf20cb8 100644 --- a/server/remote/msgraph/get_schedule_batched.go +++ b/server/remote/msgraph/get_schedule_batched.go @@ -29,10 +29,12 @@ type getScheduleRequest struct { StartTime *remote.DateTime `json:"startTime"` EndTime *remote.DateTime `json:"endTime"` - // Size of each chunk of time we want to check - // This can be equal to end - start if we want, or we can get more granular results by making it shorter. - // For the graph API: The default is 30 minutes, minimum is 6, maximum is 1440 - // 15 is currently being used on our end + /* + Size of each chunk of time we want to check + This can be equal to end - start if we want, or we can get more granular results by making it shorter. + For the graph API: The default is 30 minutes, minimum is 6, maximum is 1440 + 15 is currently being used on our end + */ AvailabilityViewInterval int `json:"availabilityViewInterval"` } diff --git a/server/remote/msgraph/get_super_user_token.go b/server/remote/msgraph/get_super_user_token.go index f115a1bb..145f2ebb 100644 --- a/server/remote/msgraph/get_super_user_token.go +++ b/server/remote/msgraph/get_super_user_token.go @@ -31,7 +31,7 @@ func (c *client) getSuperuserToken() (string, error) { data.Set("client_secret", params["client_secret"]) data.Set("grant_type", params["grant_type"]) - _, err := c.Call(http.MethodPost, u, data, &res) + _, err := c.CallURLEncodedForm(http.MethodPost, u, data, &res) if err != nil { return "", err } diff --git a/server/remote/msgraph/remote.go b/server/remote/msgraph/remote.go index 4e5c4af0..956e0577 100644 --- a/server/remote/msgraph/remote.go +++ b/server/remote/msgraph/remote.go @@ -51,8 +51,6 @@ func (r *impl) NewClient(ctx context.Context, token *oauth2.Token) remote.Client // NewSuperuserClient creates a new client used for app-only permissions. func (r *impl) NewSuperuserClient(ctx context.Context) remote.Client { httpClient := &http.Client{} - - // TODO: getSuperuserToken should just use http and not a client c := &client{ conf: r.conf, ctx: ctx, diff --git a/server/remote/schedule.go b/server/remote/schedule.go index 9f0cbd38..f575c4e8 100644 --- a/server/remote/schedule.go +++ b/server/remote/schedule.go @@ -3,6 +3,11 @@ package remote +type ScheduleInformationError struct { + Message string `json:"message"` + ResponseCode string `json:"responseCode"` +} + // ScheduleInformation undocumented type ScheduleInformation struct { // Email of user @@ -12,6 +17,8 @@ type ScheduleInformation struct { // example "0010", which means free for first and second block, tentative for third, and free for fourth AvailabilityView string `json:"availabilityView,omitempty"` + Error *ScheduleInformationError `json:"error"` + // ScheduleItems []interface{} `json:"scheduleItems,omitempty"` // WorkingHours interface{} `json:"workingHours,omitempty"` // Error *FreeBusyError `json:"error,omitempty"` diff --git a/server/store/mock_store/mock_user_store.go b/server/store/mock_store/mock_user_store.go index 0164dd0d..b3de44e3 100644 --- a/server/store/mock_store/mock_user_store.go +++ b/server/store/mock_store/mock_user_store.go @@ -47,21 +47,6 @@ func (mr *MockUserStoreMockRecorder) DeleteUser(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockUserStore)(nil).DeleteUser), arg0) } -// LoadAllUsers mocks base method -func (m *MockUserStore) LoadAllUsers() ([]*store.UserShort, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LoadAllUsers") - ret0, _ := ret[0].([]*store.UserShort) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// LoadAllUsers indicates an expected call of LoadAllUsers -func (mr *MockUserStoreMockRecorder) LoadAllUsers() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadAllUsers", reflect.TypeOf((*MockUserStore)(nil).LoadAllUsers)) -} - // LoadMattermostUserId mocks base method func (m *MockUserStore) LoadMattermostUserId(arg0 string) (string, error) { m.ctrl.T.Helper() @@ -92,6 +77,21 @@ func (mr *MockUserStoreMockRecorder) LoadUser(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUser", reflect.TypeOf((*MockUserStore)(nil).LoadUser), arg0) } +// LoadUserIndex mocks base method +func (m *MockUserStore) LoadUserIndex() ([]*store.UserShort, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadUserIndex") + ret0, _ := ret[0].([]*store.UserShort) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadUserIndex indicates an expected call of LoadUserIndex +func (mr *MockUserStoreMockRecorder) LoadUserIndex() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserIndex", reflect.TypeOf((*MockUserStore)(nil).LoadUserIndex)) +} + // StoreUser mocks base method func (m *MockUserStore) StoreUser(arg0 *store.User) error { m.ctrl.T.Helper() diff --git a/server/store/store.go b/server/store/store.go index 8cd5503a..385c174d 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -14,7 +14,7 @@ import ( const ( UserKeyPrefix = "user_" - AllUsersKeyPrefix = "allusers_" + UserIndexKeyPrefix = "userindex_" MattermostUserIDKeyPrefix = "mmuid_" OAuth2KeyPrefix = "oauth2_" SubscriptionKeyPrefix = "sub_" @@ -48,7 +48,7 @@ func NewPluginStore(api plugin.API, logger bot.Logger) Store { return &pluginStore{ basicKV: basicKV, userKV: kvstore.NewHashedKeyStore(basicKV, UserKeyPrefix), - allUsersKV: kvstore.NewHashedKeyStore(basicKV, AllUsersKeyPrefix), + allUsersKV: kvstore.NewHashedKeyStore(basicKV, UserIndexKeyPrefix), mattermostUserIDKV: kvstore.NewHashedKeyStore(basicKV, MattermostUserIDKeyPrefix), subscriptionKV: kvstore.NewHashedKeyStore(basicKV, SubscriptionKeyPrefix), eventKV: kvstore.NewHashedKeyStore(basicKV, EventKeyPrefix), diff --git a/server/store/user_store.go b/server/store/user_store.go index 77858198..f00ba11d 100644 --- a/server/store/user_store.go +++ b/server/store/user_store.go @@ -14,7 +14,7 @@ import ( type UserStore interface { LoadUser(mattermostUserId string) (*User, error) LoadMattermostUserId(remoteUserId string) (string, error) - LoadAllUsers() ([]*UserShort, error) + LoadUserIndex() ([]*UserShort, error) StoreUser(user *User) error DeleteUser(mattermostUserId string) error } @@ -62,7 +62,7 @@ func (s *pluginStore) LoadMattermostUserId(remoteUserId string) (string, error) return string(data), nil } -func (s *pluginStore) LoadAllUsers() ([]*UserShort, error) { +func (s *pluginStore) LoadUserIndex() ([]*UserShort, error) { users := []*UserShort{} err := kvstore.LoadJSON(s.allUsersKV, "", &users) if err != nil { From 95e9d6f7c86d28c906512e2b182af2989eac2751 Mon Sep 17 00:00:00 2001 From: mickmister Date: Fri, 10 Jan 2020 14:38:56 -0500 Subject: [PATCH 13/18] clean up batch response unmarshaling --- go.mod | 1 - server/remote/msgraph/batch_request.go | 16 ++-------- server/remote/msgraph/get_schedule_batched.go | 29 ++++++++++--------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 96cd18f2..15ef6328 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/gorilla/mux v1.7.3 github.com/jarcoal/httpmock v1.0.4 github.com/mattermost/mattermost-server/v5 v5.18.0-rc.test - github.com/mitchellh/mapstructure v1.1.2 github.com/pkg/errors v0.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.4.0 diff --git a/server/remote/msgraph/batch_request.go b/server/remote/msgraph/batch_request.go index 782e8d45..5824cebc 100644 --- a/server/remote/msgraph/batch_request.go +++ b/server/remote/msgraph/batch_request.go @@ -32,21 +32,11 @@ type fullBatchRequest struct { Requests []*singleRequest `json:"requests"` } -func (c *client) batchRequest(requests []*singleRequest) (error, []*fullBatchResponse) { +func (c *client) batchRequest(req fullBatchRequest, out interface{}) error { u := "https://graph.microsoft.com/v1.0/$batch" - batchRequests := prepareBatchRequests(requests) - result := []*fullBatchResponse{} - for _, req := range batchRequests { - res := &fullBatchResponse{} - _, err := c.CallJSON(http.MethodPost, u, req, res) - if err != nil { - return err, nil - } - result = append(result, res) - } - - return nil, result + _, err := c.CallJSON(http.MethodPost, u, req, out) + return err } func prepareBatchRequests(requests []*singleRequest) []fullBatchRequest { diff --git a/server/remote/msgraph/get_schedule_batched.go b/server/remote/msgraph/get_schedule_batched.go index 36a22912..288b73f7 100644 --- a/server/remote/msgraph/get_schedule_batched.go +++ b/server/remote/msgraph/get_schedule_batched.go @@ -3,13 +3,15 @@ package msgraph import ( "strconv" - "github.com/mitchellh/mapstructure" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" ) const maxNumUsersPerRequest = 20 +type getScheduleResponse struct { + Value []*remote.ScheduleInformation `json:"value,omitempty"` +} + type getScheduleSingleResponse struct { ID string `json:"id"` Status int `json:"status"` @@ -38,10 +40,6 @@ type getScheduleRequest struct { AvailabilityViewInterval int `json:"availabilityViewInterval"` } -type getScheduleResponse struct { - Value []*remote.ScheduleInformation `json:"value,omitempty"` -} - func (c *client) GetSchedule(remoteUserID string, schedules []string, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { params := &getScheduleRequest{ StartTime: startTime, @@ -50,10 +48,18 @@ func (c *client) GetSchedule(remoteUserID string, schedules []string, startTime, } allRequests := prepareGetScheduleRequests(remoteUserID, schedules, params) + batchRequests := prepareBatchRequests(allRequests) + + var batchResponses []*getScheduleBatchResponse - err, batchResponses := c.batchRequest(allRequests) - if err != nil { - return nil, err + for _, req := range batchRequests { + res := &getScheduleBatchResponse{} + err := c.batchRequest(req, res) + if err != nil { + return nil, err + } + + batchResponses = append(batchResponses, res) } result := []*remote.ScheduleInformation{} @@ -66,10 +72,7 @@ func (c *client) GetSchedule(remoteUserID string, schedules []string, startTime, sorted := make([]*getScheduleSingleResponse, length) - for _, r := range batchRes.Responses { - res := &getScheduleSingleResponse{} - mapstructure.Decode(r, res) - + for _, res := range batchRes.Responses { id, _ := strconv.Atoi(res.ID) sorted[id] = res } From d37666a792f91961e8639663d9f06b1c608abe1c Mon Sep 17 00:00:00 2001 From: mickmister Date: Sun, 12 Jan 2020 20:04:38 -0500 Subject: [PATCH 14/18] 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 --- plugin.json | 2 +- server/api/api.go | 10 ++-- server/api/availability.go | 22 ++++---- server/api/calendar.go | 12 ++--- server/api/event.go | 8 +-- server/api/notification.go | 2 +- server/api/oauth2.go | 2 +- server/api/subscription.go | 8 +-- server/config/config.go | 2 +- server/plugin/plugin.go | 4 +- server/remote/client.go | 4 +- server/remote/msgraph/call.go | 8 +-- server/remote/msgraph/get_super_user_token.go | 2 +- server/remote/msgraph/remote.go | 10 ++-- server/remote/remote.go | 4 +- server/remote/schedule.go | 8 +++ server/store/store.go | 4 +- server/store/user_store.go | 50 +++++++++++++++---- 18 files changed, 97 insertions(+), 65 deletions(-) diff --git a/plugin.json b/plugin.json index b6125c61..acaa5aca 100644 --- a/plugin.json +++ b/plugin.json @@ -81,7 +81,7 @@ "default": "" }, { - "key": "EnableStatusSyncJob", + "key": "EnableStatusSync", "display_name": "Enable User Status Sync Job", "type": "bool", "help_text": "When enabled, a Mattermost user's status will automatically update based on their Microsoft Calendar availability. This runs every 5 minutes.", diff --git a/server/api/api.go b/server/api/api.go index 0e7f9e7d..c8ad3cc2 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -52,7 +52,7 @@ type Availability interface { } type Client interface { - NewClient() (remote.Client, error) + MakeClient() (remote.Client, error) } type API interface { @@ -97,17 +97,17 @@ func New(apiConfig Config, mattermostUserID string) API { type filterf func(*api) error -func (api *api) NewClient() (remote.Client, error) { +func (api *api) MakeClient() (remote.Client, error) { err := api.Filter(withUser) if err != nil { return nil, err } - return api.Remote.NewClient(context.Background(), api.user.OAuth2Token), nil + return api.Remote.MakeClient(context.Background(), api.user.OAuth2Token), nil } -func (api *api) NewSuperuserClient() remote.Client { - return api.Remote.NewSuperuserClient(context.Background()) +func (api *api) MakeSuperuserClient() remote.Client { + return api.Remote.MakeSuperuserClient(context.Background()) } func (api *api) Filter(filters ...filterf) error { diff --git a/server/api/availability.go b/server/api/availability.go index b03f897a..54cde64d 100644 --- a/server/api/availability.go +++ b/server/api/availability.go @@ -14,12 +14,6 @@ import ( const ( availabilityTimeWindowSize = 15 - - availabilityViewFree = '0' - availabilityViewTentative = '1' - availabilityViewBusy = '2' - availabilityViewOutOfOffice = '3' - availabilityViewWorkingElsewhere = '4' ) func (api *api) SyncStatusForSingleUser(mattermostUserID string) (string, error) { @@ -93,8 +87,10 @@ func (api *api) SyncStatusForAllUsers() (string, error) { statusMap[s.UserId] = s.Status } + usersByEmail := users.ByEmail() + var res string - for i, s := range sched { + for _, s := range sched { if s.Error != nil { api.Logger.Errorf("Error getting availability for %s: %s", s.ScheduleID, s.Error.ResponseCode) continue @@ -102,7 +98,7 @@ func (api *api) SyncStatusForAllUsers() (string, error) { av := s.AvailabilityView[0] - userID := users[i].MattermostUserID + userID := usersByEmail[s.ScheduleID].MattermostUserID status, ok := statusMap[userID] if !ok { continue @@ -119,7 +115,7 @@ func (api *api) SyncStatusForAllUsers() (string, error) { } func (api *api) GetUserAvailabilities(remoteUserID string, scheduleIDs []string) ([]*remote.ScheduleInformation, error) { - client := api.NewSuperuserClient() + client := api.MakeSuperuserClient() start := remote.NewDateTime(time.Now()) end := remote.NewDateTime(time.Now().Add(availabilityTimeWindowSize * time.Minute)) @@ -129,28 +125,28 @@ func (api *api) GetUserAvailabilities(remoteUserID string, scheduleIDs []string) func (api *api) setUserStatusFromAvailability(mattermostUserID, currentStatus string, av byte) string { switch av { - case availabilityViewFree: + case remote.AvailabilityViewFree: if currentStatus == "dnd" { api.API.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 availabilityViewTentative, availabilityViewBusy: + case remote.AvailabilityViewTentative, remote.AvailabilityViewBusy: if currentStatus != "dnd" { api.API.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 availabilityViewOutOfOffice: + case remote.AvailabilityViewOutOfOffice: if currentStatus != "offline" { api.API.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 availabilityViewWorkingElsewhere: + case remote.AvailabilityViewWorkingElsewhere: return fmt.Sprintf("User is working elsewhere. Pending implementation.") } diff --git a/server/api/calendar.go b/server/api/calendar.go index ecd40b92..0613e5f5 100644 --- a/server/api/calendar.go +++ b/server/api/calendar.go @@ -10,7 +10,7 @@ import ( ) func (api *api) ViewCalendar(from, to time.Time) ([]*remote.Event, error) { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return nil, err } @@ -19,7 +19,7 @@ func (api *api) ViewCalendar(from, to time.Time) ([]*remote.Event, error) { } func (api *api) CreateCalendar(calendar *remote.Calendar) (*remote.Calendar, error) { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return nil, err } @@ -40,7 +40,7 @@ func (api *api) CreateEvent(event *remote.Event, mattermostUserIDs []string) (*r } } - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return nil, err } @@ -49,7 +49,7 @@ func (api *api) CreateEvent(event *remote.Event, mattermostUserIDs []string) (*r } func (api *api) DeleteCalendar(calendarID string) error { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return err } @@ -58,7 +58,7 @@ func (api *api) DeleteCalendar(calendarID string) error { } func (api *api) FindMeetingTimes(meetingParams *remote.FindMeetingTimesParameters) (*remote.MeetingTimeSuggestionResults, error) { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return nil, err } @@ -67,7 +67,7 @@ func (api *api) FindMeetingTimes(meetingParams *remote.FindMeetingTimesParameter } func (api *api) GetUserCalendars(userID string) ([]*remote.Calendar, error) { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return nil, err } diff --git a/server/api/event.go b/server/api/event.go index b410f5a0..cffae66b 100644 --- a/server/api/event.go +++ b/server/api/event.go @@ -6,7 +6,7 @@ package api import "github.com/pkg/errors" func (api *api) AcceptEvent(eventID string) error { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return err } @@ -15,7 +15,7 @@ func (api *api) AcceptEvent(eventID string) error { } func (api *api) DeclineEvent(eventID string) error { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return err } @@ -24,7 +24,7 @@ func (api *api) DeclineEvent(eventID string) error { } func (api *api) TentativelyAcceptEvent(eventID string) error { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return err } @@ -37,7 +37,7 @@ func (api *api) RespondToEvent(eventID, response string) error { return errors.New("Not responded is not a valid response") } - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return err } diff --git a/server/api/notification.go b/server/api/notification.go index edebe603..e104eb2a 100644 --- a/server/api/notification.go +++ b/server/api/notification.go @@ -133,7 +133,7 @@ func (h *notificationHandler) processNotification(n *remote.Notification) error var client remote.Client if !n.RecommendRenew || n.IsBare { - client = h.Remote.NewClient(context.Background(), creator.OAuth2Token) + client = h.Remote.MakeClient(context.Background(), creator.OAuth2Token) } if n.RecommendRenew { diff --git a/server/api/oauth2.go b/server/api/oauth2.go index 95ad0ea1..3443401b 100644 --- a/server/api/oauth2.go +++ b/server/api/oauth2.go @@ -51,7 +51,7 @@ func (api *api) CompleteOAuth2(authedUserID, code, state string) error { return err } - client := api.Remote.NewClient(ctx, tok) + client := api.Remote.MakeClient(ctx, tok) me, err := client.GetMe() if err != nil { return err diff --git a/server/api/subscription.go b/server/api/subscription.go index c0e96846..6bd66496 100644 --- a/server/api/subscription.go +++ b/server/api/subscription.go @@ -12,7 +12,7 @@ import ( ) func (api *api) CreateUserEventSubscription() (*store.Subscription, error) { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return nil, err } @@ -49,7 +49,7 @@ func (api *api) LoadUserEventSubscription() (*store.Subscription, error) { } func (api *api) ListRemoteSubscriptions() ([]*remote.Subscription, error) { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return nil, err } @@ -61,7 +61,7 @@ func (api *api) ListRemoteSubscriptions() ([]*remote.Subscription, error) { } func (api *api) RenewUserEventSubscription() (*store.Subscription, error) { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return nil, err } @@ -105,7 +105,7 @@ func (api *api) DeleteUserEventSubscription() error { } func (api *api) DeleteOrphanedSubscription(subscriptionID string) error { - client, err := api.NewClient() + client, err := api.MakeClient() if err != nil { return err } diff --git a/server/config/config.go b/server/config/config.go index 08d3df19..94b6965b 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -9,7 +9,7 @@ type StoredConfig struct { OAuth2ClientID string OAuth2ClientSecret string - EnableStatusSyncJob bool + EnableStatusSync bool bot.BotConfig } diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 76bc67df..4e507e4b 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -73,8 +73,6 @@ func (p *Plugin) OnActivate() error { command.Register(p.API.RegisterCommand) - p.initUserStatusSyncJob() - p.API.LogInfo(p.config.PluginID + " activated") return nil } @@ -218,7 +216,7 @@ func (p *Plugin) loadTemplates(bundlePath string) error { func (p *Plugin) initUserStatusSyncJob() { conf := p.newAPIConfig() - enable := p.getConfig().EnableStatusSyncJob + enable := p.getConfig().EnableStatusSync logger := conf.Dependencies.Logger // Config is set to enable. No job exists, start a new job. diff --git a/server/remote/client.go b/server/remote/client.go index a1ad958d..b4cd2b5d 100644 --- a/server/remote/client.go +++ b/server/remote/client.go @@ -4,16 +4,14 @@ package remote import ( - "io" "net/url" "time" ) type Client interface { AcceptUserEvent(userID, eventID string) error - Call(method, path, contentType string, in io.Reader, out interface{}) (responseData []byte, err error) CallJSON(method, path string, in, out interface{}) (responseData []byte, err error) - CallURLEncodedForm(method, path string, in url.Values, out interface{}) (responseData []byte, err error) + CallFormPost(method, path string, in url.Values, out interface{}) (responseData []byte, err error) CreateSubscription(notificationURL string) (*Subscription, error) DeclineUserEvent(userID, eventID string) error DeleteSubscription(subscriptionID string) error diff --git a/server/remote/msgraph/call.go b/server/remote/msgraph/call.go index a76266ea..6ec546ac 100644 --- a/server/remote/msgraph/call.go +++ b/server/remote/msgraph/call.go @@ -24,16 +24,16 @@ func (c *client) CallJSON(method, path string, in, out interface{}) (responseDat if err != nil { return nil, err } - return c.Call(method, path, contentType, buf, out) + return c.call(method, path, contentType, buf, out) } -func (c *client) CallURLEncodedForm(method, path string, in url.Values, out interface{}) (responseData []byte, err error) { +func (c *client) CallFormPost(method, path string, in url.Values, out interface{}) (responseData []byte, err error) { contentType := "application/x-www-form-urlencoded" buf := strings.NewReader(in.Encode()) - return c.Call(method, path, contentType, buf, out) + return c.call(method, path, contentType, buf, out) } -func (c *client) Call(method, path, contentType string, inBody io.Reader, out interface{}) (responseData []byte, err error) { +func (c *client) call(method, path, contentType string, inBody io.Reader, out interface{}) (responseData []byte, err error) { errContext := fmt.Sprintf("msgraph: Call failed: method:%s, path:%s", method, path) pathURL, err := url.Parse(path) if err != nil { diff --git a/server/remote/msgraph/get_super_user_token.go b/server/remote/msgraph/get_super_user_token.go index 145f2ebb..016ebd1c 100644 --- a/server/remote/msgraph/get_super_user_token.go +++ b/server/remote/msgraph/get_super_user_token.go @@ -31,7 +31,7 @@ func (c *client) getSuperuserToken() (string, error) { data.Set("client_secret", params["client_secret"]) data.Set("grant_type", params["grant_type"]) - _, err := c.CallURLEncodedForm(http.MethodPost, u, data, &res) + _, err := c.CallFormPost(http.MethodPost, u, data, &res) if err != nil { return "", err } diff --git a/server/remote/msgraph/remote.go b/server/remote/msgraph/remote.go index cec34abb..255b4690 100644 --- a/server/remote/msgraph/remote.go +++ b/server/remote/msgraph/remote.go @@ -35,8 +35,8 @@ func NewRemote(conf *config.Config, logger bot.Logger) remote.Remote { } } -// NewClient creates a new client for user-delegated permissions. -func (r *impl) NewClient(ctx context.Context, token *oauth2.Token) remote.Client { +// MakeClient creates a new client for user-delegated permissions. +func (r *impl) MakeClient(ctx context.Context, token *oauth2.Token) remote.Client { httpClient := r.NewOAuth2Config().Client(ctx, token) c := &client{ conf: r.conf, @@ -48,8 +48,8 @@ func (r *impl) NewClient(ctx context.Context, token *oauth2.Token) remote.Client return c } -// NewSuperuserClient creates a new client used for app-only permissions. -func (r *impl) NewSuperuserClient(ctx context.Context) remote.Client { +// MakeSuperuserClient creates a new client used for app-only permissions. +func (r *impl) MakeSuperuserClient(ctx context.Context) remote.Client { httpClient := &http.Client{} c := &client{ conf: r.conf, @@ -66,7 +66,7 @@ func (r *impl) NewSuperuserClient(ctx context.Context) remote.Client { TokenType: "Bearer", } - return r.NewClient(ctx, o) + return r.MakeClient(ctx, o) } func (r *impl) NewOAuth2Config() *oauth2.Config { diff --git a/server/remote/remote.go b/server/remote/remote.go index 4ae08ee2..2e0b1521 100644 --- a/server/remote/remote.go +++ b/server/remote/remote.go @@ -14,8 +14,8 @@ import ( ) type Remote interface { - NewClient(context.Context, *oauth2.Token) Client - NewSuperuserClient(context.Context) Client + MakeClient(context.Context, *oauth2.Token) Client + MakeSuperuserClient(context.Context) Client NewOAuth2Config() *oauth2.Config HandleWebhook(http.ResponseWriter, *http.Request) []*Notification } diff --git a/server/remote/schedule.go b/server/remote/schedule.go index f575c4e8..01d754c4 100644 --- a/server/remote/schedule.go +++ b/server/remote/schedule.go @@ -3,6 +3,14 @@ package remote +const ( + AvailabilityViewFree = '0' + AvailabilityViewTentative = '1' + AvailabilityViewBusy = '2' + AvailabilityViewOutOfOffice = '3' + AvailabilityViewWorkingElsewhere = '4' +) + type ScheduleInformationError struct { Message string `json:"message"` ResponseCode string `json:"responseCode"` diff --git a/server/store/store.go b/server/store/store.go index 469b96dd..9e15f428 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -37,7 +37,7 @@ type pluginStore struct { oauth2KV kvstore.KVStore userKV kvstore.KVStore mattermostUserIDKV kvstore.KVStore - allUsersKV kvstore.KVStore + userIndexKV kvstore.KVStore subscriptionKV kvstore.KVStore eventKV kvstore.KVStore Logger bot.Logger @@ -48,7 +48,7 @@ func NewPluginStore(api plugin.API, logger bot.Logger) Store { return &pluginStore{ basicKV: basicKV, userKV: kvstore.NewHashedKeyStore(basicKV, UserKeyPrefix), - allUsersKV: kvstore.NewHashedKeyStore(basicKV, UserIndexKeyPrefix), + userIndexKV: kvstore.NewHashedKeyStore(basicKV, UserIndexKeyPrefix), mattermostUserIDKV: kvstore.NewHashedKeyStore(basicKV, MattermostUserIDKeyPrefix), subscriptionKV: kvstore.NewHashedKeyStore(basicKV, SubscriptionKeyPrefix), eventKV: kvstore.NewHashedKeyStore(basicKV, EventKeyPrefix), diff --git a/server/store/user_store.go b/server/store/user_store.go index 5a305f12..15876bab 100644 --- a/server/store/user_store.go +++ b/server/store/user_store.go @@ -14,11 +14,13 @@ import ( type UserStore interface { LoadUser(mattermostUserId string) (*User, error) LoadMattermostUserId(remoteUserId string) (string, error) - LoadUserIndex() ([]*UserShort, error) + LoadUserIndex() (UserIndex, error) StoreUser(user *User) error DeleteUser(mattermostUserId string) error } +type UserIndex []*UserShort + type UserShort struct { MattermostUserID string `json:"mm_id"` RemoteID string `json:"remote_id"` @@ -62,9 +64,9 @@ func (s *pluginStore) LoadMattermostUserId(remoteUserId string) (string, error) return string(data), nil } -func (s *pluginStore) LoadUserIndex() ([]*UserShort, error) { - users := []*UserShort{} - err := kvstore.LoadJSON(s.allUsersKV, "", &users) +func (s *pluginStore) LoadUserIndex() (UserIndex, error) { + users := UserIndex{} + err := kvstore.LoadJSON(s.userIndexKV, "", &users) if err != nil { return nil, err } @@ -83,10 +85,10 @@ func (s *pluginStore) StoreUser(user *User) error { return err } - var allUsers []*UserShort - err = kvstore.LoadJSON(s.allUsersKV, "", &allUsers) + var userIndex []*UserShort + err = kvstore.LoadJSON(s.userIndexKV, "", &userIndex) if err != nil { - allUsers = []*UserShort{} + userIndex = []*UserShort{} } newUser := &UserShort{ @@ -97,7 +99,7 @@ func (s *pluginStore) StoreUser(user *User) error { found := false filtered := []*UserShort{} - for _, u := range allUsers { + for _, u := range userIndex { if u.MattermostUserID == user.MattermostUserID && u.RemoteID == user.Remote.ID { found = true filtered = append(filtered, newUser) @@ -110,7 +112,7 @@ func (s *pluginStore) StoreUser(user *User) error { filtered = append(filtered, newUser) } - err = kvstore.StoreJSON(s.allUsersKV, "", &filtered) + err = kvstore.StoreJSON(s.userIndexKV, "", &filtered) if err != nil { return err } @@ -133,3 +135,33 @@ func (s *pluginStore) DeleteUser(mattermostUserID string) error { } return nil } + +func (index UserIndex) ByMattermostID() map[string]*UserShort { + result := map[string]*UserShort{} + + for _, u := range index { + result[u.MattermostUserID] = u + } + + return result +} + +func (index UserIndex) ByEmail() map[string]*UserShort { + result := map[string]*UserShort{} + + for _, u := range index { + result[u.Email] = u + } + + return result +} + +func (index UserIndex) ByRemoteID() map[string]*UserShort { + result := map[string]*UserShort{} + + for _, u := range index { + result[u.RemoteID] = u + } + + return result +} From c3f428b0508ad7723030adfc574f07bb46c5fef0 Mon Sep 17 00:00:00 2001 From: mickmister Date: Sun, 12 Jan 2020 20:13:19 -0500 Subject: [PATCH 15/18] rename var --- server/api/availability.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/api/availability.go b/server/api/availability.go index 54cde64d..ca73be97 100644 --- a/server/api/availability.go +++ b/server/api/availability.go @@ -50,7 +50,7 @@ func (api *api) SyncStatusForSingleUser(mattermostUserID string) (string, error) } func (api *api) SyncStatusForAllUsers() (string, error) { - users, err := api.UserStore.LoadUserIndex() + userIndex, err := api.UserStore.LoadUserIndex() if err != nil { if err.Error() == "not found" { return "No users found in user index", nil @@ -58,18 +58,18 @@ func (api *api) SyncStatusForAllUsers() (string, error) { return "", err } - if len(users) == 0 { + if len(userIndex) == 0 { return "No connected users found", nil } scheduleIDs := []string{} mattermostUserIDs := []string{} - for _, u := range users { + for _, u := range userIndex { scheduleIDs = append(scheduleIDs, u.Email) mattermostUserIDs = append(mattermostUserIDs, u.MattermostUserID) } - sched, err := api.GetUserAvailabilities(users[0].RemoteID, scheduleIDs) + sched, err := api.GetUserAvailabilities(userIndex[0].RemoteID, scheduleIDs) if err != nil { return "", err } @@ -87,7 +87,7 @@ func (api *api) SyncStatusForAllUsers() (string, error) { statusMap[s.UserId] = s.Status } - usersByEmail := users.ByEmail() + usersByEmail := userIndex.ByEmail() var res string for _, s := range sched { From 7fd1a696c03d697b9c607f22df529abf9b98a301 Mon Sep 17 00:00:00 2001 From: mickmister Date: Sun, 12 Jan 2020 20:14:33 -0500 Subject: [PATCH 16/18] reorder methods --- server/store/user_store.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/store/user_store.go b/server/store/user_store.go index 15876bab..5bd26c2f 100644 --- a/server/store/user_store.go +++ b/server/store/user_store.go @@ -146,21 +146,21 @@ func (index UserIndex) ByMattermostID() map[string]*UserShort { return result } -func (index UserIndex) ByEmail() map[string]*UserShort { +func (index UserIndex) ByRemoteID() map[string]*UserShort { result := map[string]*UserShort{} for _, u := range index { - result[u.Email] = u + result[u.RemoteID] = u } return result } -func (index UserIndex) ByRemoteID() map[string]*UserShort { +func (index UserIndex) ByEmail() map[string]*UserShort { result := map[string]*UserShort{} for _, u := range index { - result[u.RemoteID] = u + result[u.Email] = u } return result From c2f1e71494dc3bbaf3e8fd47c8dc58185c0c7c23 Mon Sep 17 00:00:00 2001 From: mickmister Date: Sun, 12 Jan 2020 22:34:40 -0500 Subject: [PATCH 17/18] update mocks --- server/api/mock_api/mock_client.go | 12 +++---- server/remote/mock_remote/mock_client.go | 28 ++++----------- server/remote/mock_remote/mock_remote.go | 40 +++++++++++----------- server/store/mock_store/mock_user_store.go | 4 +-- 4 files changed, 34 insertions(+), 50 deletions(-) diff --git a/server/api/mock_api/mock_client.go b/server/api/mock_api/mock_client.go index 68836636..374a7c0a 100644 --- a/server/api/mock_api/mock_client.go +++ b/server/api/mock_api/mock_client.go @@ -33,17 +33,17 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { return m.recorder } -// NewClient mocks base method -func (m *MockClient) NewClient() (remote.Client, error) { +// MakeClient mocks base method +func (m *MockClient) MakeClient() (remote.Client, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewClient") + ret := m.ctrl.Call(m, "MakeClient") ret0, _ := ret[0].(remote.Client) ret1, _ := ret[1].(error) return ret0, ret1 } -// NewClient indicates an expected call of NewClient -func (mr *MockClientMockRecorder) NewClient() *gomock.Call { +// MakeClient indicates an expected call of MakeClient +func (mr *MockClientMockRecorder) MakeClient() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockClient)(nil).NewClient)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeClient", reflect.TypeOf((*MockClient)(nil).MakeClient)) } diff --git a/server/remote/mock_remote/mock_client.go b/server/remote/mock_remote/mock_client.go index cb1bfcad..8145ef78 100644 --- a/server/remote/mock_remote/mock_client.go +++ b/server/remote/mock_remote/mock_client.go @@ -7,7 +7,6 @@ package mock_remote import ( gomock "github.com/golang/mock/gomock" remote "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - io "io" url "net/url" reflect "reflect" time "time" @@ -50,19 +49,19 @@ func (mr *MockClientMockRecorder) AcceptUserEvent(arg0, arg1 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptUserEvent", reflect.TypeOf((*MockClient)(nil).AcceptUserEvent), arg0, arg1) } -// Call mocks base method -func (m *MockClient) Call(arg0, arg1, arg2 string, arg3 io.Reader, arg4 interface{}) ([]byte, error) { +// CallFormPost mocks base method +func (m *MockClient) CallFormPost(arg0, arg1 string, arg2 url.Values, arg3 interface{}) ([]byte, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Call", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "CallFormPost", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } -// Call indicates an expected call of Call -func (mr *MockClientMockRecorder) Call(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +// CallFormPost indicates an expected call of CallFormPost +func (mr *MockClientMockRecorder) CallFormPost(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockClient)(nil).Call), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallFormPost", reflect.TypeOf((*MockClient)(nil).CallFormPost), arg0, arg1, arg2, arg3) } // CallJSON mocks base method @@ -80,21 +79,6 @@ func (mr *MockClientMockRecorder) CallJSON(arg0, arg1, arg2, arg3 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallJSON", reflect.TypeOf((*MockClient)(nil).CallJSON), arg0, arg1, arg2, arg3) } -// CallURLEncodedForm mocks base method -func (m *MockClient) CallURLEncodedForm(arg0, arg1 string, arg2 url.Values, arg3 interface{}) ([]byte, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CallURLEncodedForm", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CallURLEncodedForm indicates an expected call of CallURLEncodedForm -func (mr *MockClientMockRecorder) CallURLEncodedForm(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallURLEncodedForm", reflect.TypeOf((*MockClient)(nil).CallURLEncodedForm), arg0, arg1, arg2, arg3) -} - // CreateCalendar mocks base method func (m *MockClient) CreateCalendar(arg0 *remote.Calendar) (*remote.Calendar, error) { m.ctrl.T.Helper() diff --git a/server/remote/mock_remote/mock_remote.go b/server/remote/mock_remote/mock_remote.go index d51ac692..456488a4 100644 --- a/server/remote/mock_remote/mock_remote.go +++ b/server/remote/mock_remote/mock_remote.go @@ -50,44 +50,44 @@ func (mr *MockRemoteMockRecorder) HandleWebhook(arg0, arg1 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleWebhook", reflect.TypeOf((*MockRemote)(nil).HandleWebhook), arg0, arg1) } -// NewClient mocks base method -func (m *MockRemote) NewClient(arg0 context.Context, arg1 *oauth2.Token) remote.Client { +// MakeClient mocks base method +func (m *MockRemote) MakeClient(arg0 context.Context, arg1 *oauth2.Token) remote.Client { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewClient", arg0, arg1) + ret := m.ctrl.Call(m, "MakeClient", arg0, arg1) ret0, _ := ret[0].(remote.Client) return ret0 } -// NewClient indicates an expected call of NewClient -func (mr *MockRemoteMockRecorder) NewClient(arg0, arg1 interface{}) *gomock.Call { +// MakeClient indicates an expected call of MakeClient +func (mr *MockRemoteMockRecorder) MakeClient(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockRemote)(nil).NewClient), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeClient", reflect.TypeOf((*MockRemote)(nil).MakeClient), arg0, arg1) } -// NewOAuth2Config mocks base method -func (m *MockRemote) NewOAuth2Config() *oauth2.Config { +// MakeSuperuserClient mocks base method +func (m *MockRemote) MakeSuperuserClient(arg0 context.Context) remote.Client { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewOAuth2Config") - ret0, _ := ret[0].(*oauth2.Config) + ret := m.ctrl.Call(m, "MakeSuperuserClient", arg0) + ret0, _ := ret[0].(remote.Client) return ret0 } -// NewOAuth2Config indicates an expected call of NewOAuth2Config -func (mr *MockRemoteMockRecorder) NewOAuth2Config() *gomock.Call { +// MakeSuperuserClient indicates an expected call of MakeSuperuserClient +func (mr *MockRemoteMockRecorder) MakeSuperuserClient(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewOAuth2Config", reflect.TypeOf((*MockRemote)(nil).NewOAuth2Config)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeSuperuserClient", reflect.TypeOf((*MockRemote)(nil).MakeSuperuserClient), arg0) } -// NewSuperuserClient mocks base method -func (m *MockRemote) NewSuperuserClient(arg0 context.Context) remote.Client { +// NewOAuth2Config mocks base method +func (m *MockRemote) NewOAuth2Config() *oauth2.Config { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewSuperuserClient", arg0) - ret0, _ := ret[0].(remote.Client) + ret := m.ctrl.Call(m, "NewOAuth2Config") + ret0, _ := ret[0].(*oauth2.Config) return ret0 } -// NewSuperuserClient indicates an expected call of NewSuperuserClient -func (mr *MockRemoteMockRecorder) NewSuperuserClient(arg0 interface{}) *gomock.Call { +// NewOAuth2Config indicates an expected call of NewOAuth2Config +func (mr *MockRemoteMockRecorder) NewOAuth2Config() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewSuperuserClient", reflect.TypeOf((*MockRemote)(nil).NewSuperuserClient), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewOAuth2Config", reflect.TypeOf((*MockRemote)(nil).NewOAuth2Config)) } diff --git a/server/store/mock_store/mock_user_store.go b/server/store/mock_store/mock_user_store.go index 8dce38f6..afebbe00 100644 --- a/server/store/mock_store/mock_user_store.go +++ b/server/store/mock_store/mock_user_store.go @@ -78,10 +78,10 @@ func (mr *MockUserStoreMockRecorder) LoadUser(arg0 interface{}) *gomock.Call { } // LoadUserIndex mocks base method -func (m *MockUserStore) LoadUserIndex() ([]*store.UserShort, error) { +func (m *MockUserStore) LoadUserIndex() (store.UserIndex, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "LoadUserIndex") - ret0, _ := ret[0].([]*store.UserShort) + ret0, _ := ret[0].(store.UserIndex) ret1, _ := ret[1].(error) return ret0, ret1 } From dcc16ef82f281ef4a27d51456c3dce5b50d5636c Mon Sep 17 00:00:00 2001 From: mickmister Date: Tue, 14 Jan 2020 03:17:36 -0500 Subject: [PATCH 18/18] 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 --- Makefile | 3 + server/api/api.go | 5 +- server/api/availability.go | 92 ++++++------ server/api/availability_test.go | 138 ++++++++++++++++++ server/api/plugin_api.go | 22 +++ server/plugin/plugin.go | 8 +- server/remote/schedule.go | 4 +- server/store/mock_store/mock_event_store.go | 77 ++++++++++ server/store/user_store.go | 10 ++ server/utils/bot/mock_bot/mock_logger.go | 116 +++++++++++++++ .../mock_plugin_api/mock_plugin_api.go | 79 ++++++++++ server/utils/plugin_api/plugin_api.go | 14 ++ 12 files changed, 511 insertions(+), 57 deletions(-) create mode 100644 server/api/availability_test.go create mode 100644 server/api/plugin_api.go create mode 100644 server/store/mock_store/mock_event_store.go create mode 100644 server/utils/bot/mock_bot/mock_logger.go create mode 100644 server/utils/plugin_api/mock_plugin_api/mock_plugin_api.go create mode 100644 server/utils/plugin_api/plugin_api.go diff --git a/Makefile b/Makefile index dd6a3cbb..afdff7d3 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,9 @@ ifneq ($(HAS_SERVER),) mockgen -destination server/remote/mock_remote/mock_client.go github.com/mattermost/mattermost-plugin-mscalendar/server/remote Client mockgen -destination server/utils/bot/mock_bot/mock_poster.go github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot Poster mockgen -destination server/utils/bot/mock_bot/mock_admin.go github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot Admin + mockgen -destination server/utils/bot/mock_bot/mock_logger.go github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot Logger + mockgen -destination server/utils/plugin_api/mock_plugin_api/mock_plugin_api.go github.com/mattermost/mattermost-plugin-mscalendar/server/utils/plugin_api PluginAPI + mockgen -destination server/store/mock_store/mock_event_store.go github.com/mattermost/mattermost-plugin-mscalendar/server/store EventStore mockgen -destination server/store/mock_store/mock_oauth2_store.go github.com/mattermost/mattermost-plugin-mscalendar/server/store OAuth2StateStore mockgen -destination server/store/mock_store/mock_subscription_store.go github.com/mattermost/mattermost-plugin-mscalendar/server/store SubscriptionStore mockgen -destination server/store/mock_store/mock_user_store.go github.com/mattermost/mattermost-plugin-mscalendar/server/store UserStore diff --git a/server/api/api.go b/server/api/api.go index c8ad3cc2..dc4d55a7 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -7,12 +7,11 @@ import ( "context" "time" - "github.com/mattermost/mattermost-server/v5/plugin" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" "github.com/mattermost/mattermost-plugin-mscalendar/server/store" "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/plugin_api" ) type OAuth2 interface { @@ -74,7 +73,7 @@ type Dependencies struct { Poster bot.Poster Remote remote.Remote IsAuthorizedAdmin func(userId string) (bool, error) - API plugin.API + PluginAPI plugin_api.PluginAPI } type Config struct { diff --git a/server/api/availability.go b/server/api/availability.go index ca73be97..763e6998 100644 --- a/server/api/availability.go +++ b/server/api/availability.go @@ -8,8 +8,8 @@ import ( "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" - "github.com/pkg/errors" ) const ( @@ -17,40 +17,24 @@ const ( ) func (api *api) SyncStatusForSingleUser(mattermostUserID string) (string, error) { - u, err := api.UserStore.LoadUser(mattermostUserID) - if err != nil { - return "", err - } - - scheduleIDs := []string{u.Remote.Mail} - sched, err := api.GetUserAvailabilities(u.Remote.ID, scheduleIDs) + 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 } - if len(sched) == 0 { - return "No schedule info found", nil - } - - status, appErr := api.Dependencies.API.GetUserStatus(api.mattermostUserID) - if appErr != nil { - return "", appErr - } - s := sched[0] - if s.Error != nil { - return "", errors.Errorf("Error getting availability for %s: %s", s.ScheduleID, s.Error.ResponseCode) - } - if len(s.AvailabilityView) == 0 { - return "No availabilities found", nil - } - - av := s.AvailabilityView[0] - return api.setUserStatusFromAvailability(api.mattermostUserID, status.Status, av), nil + mmIDs := userIndex.GetMattermostUserIDs() + return api.syncStatusForUsers(mmIDs) } -func (api *api) SyncStatusForAllUsers() (string, error) { - userIndex, err := api.UserStore.LoadUserIndex() +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 @@ -58,60 +42,66 @@ func (api *api) SyncStatusForAllUsers() (string, error) { return "", err } - if len(userIndex) == 0 { + 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{} - mattermostUserIDs := []string{} - for _, u := range userIndex { + for _, u := range filteredUsers { scheduleIDs = append(scheduleIDs, u.Email) - mattermostUserIDs = append(mattermostUserIDs, u.MattermostUserID) } - sched, err := api.GetUserAvailabilities(userIndex[0].RemoteID, scheduleIDs) + schedules, err := api.GetUserAvailabilities(filteredUsers[0].RemoteID, scheduleIDs) if err != nil { return "", err } - if len(sched) == 0 { + if len(schedules) == 0 { return "No schedule info found", nil } - statuses, appErr := api.Dependencies.API.GetUserStatusesByIds(mattermostUserIDs) + 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 := userIndex.ByEmail() - + usersByEmail := filteredUsers.ByEmail() var res string - for _, s := range sched { + for _, s := range schedules { if s.Error != nil { api.Logger.Errorf("Error getting availability for %s: %s", s.ScheduleID, s.Error.ResponseCode) continue } - av := s.AvailabilityView[0] - userID := usersByEmail[s.ScheduleID].MattermostUserID status, ok := statusMap[userID] if !ok { continue } - res = api.setUserStatusFromAvailability(userID, status, av) + res = api.setUserStatusFromAvailability(userID, status, s.AvailabilityView) } - if res != "" { return res, nil } - return utils.JSONBlock(sched), nil + return utils.JSONBlock(schedules), nil } func (api *api) GetUserAvailabilities(remoteUserID string, scheduleIDs []string) ([]*remote.ScheduleInformation, error) { @@ -123,25 +113,27 @@ func (api *api) GetUserAvailabilities(remoteUserID string, scheduleIDs []string) return client.GetSchedule(remoteUserID, scheduleIDs, start, end, availabilityTimeWindowSize) } -func (api *api) setUserStatusFromAvailability(mattermostUserID, currentStatus string, av byte) string { - switch av { +func (api *api) setUserStatusFromAvailability(mattermostUserID, currentStatus string, av remote.AvailabilityView) string { + currentAvailability := av[0] + + switch currentAvailability { case remote.AvailabilityViewFree: if currentStatus == "dnd" { - api.API.UpdateUserStatus(mattermostUserID, "online") + 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.API.UpdateUserStatus(mattermostUserID, "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.API.UpdateUserStatus(mattermostUserID, "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) @@ -150,5 +142,5 @@ func (api *api) setUserStatusFromAvailability(mattermostUserID, currentStatus st return fmt.Sprintf("User is working elsewhere. Pending implementation.") } - return fmt.Sprintf("Availability view doesn't match %d", av) + return fmt.Sprintf("Availability view doesn't match %d", currentAvailability) } diff --git a/server/api/availability_test.go b/server/api/availability_test.go new file mode 100644 index 00000000..d3b8caec --- /dev/null +++ b/server/api/availability_test.go @@ -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: "some_email@example.com", + AvailabilityView: "0", + }, + currentStatus: "dnd", + newStatus: "online", + }, + "User is busy but online, mark as dnd": { + sched: &remote.ScheduleInformation{ + ScheduleID: "some_email@example.com", + AvailabilityView: "2", + }, + currentStatus: "online", + newStatus: "dnd", + }, + "User is free and online, do not change status": { + sched: &remote.ScheduleInformation{ + ScheduleID: "some_email@example.com", + AvailabilityView: "0", + }, + currentStatus: "online", + newStatus: "", + }, + "User is busy and dnd, do not change status": { + sched: &remote.ScheduleInformation{ + ScheduleID: "some_email@example.com", + 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: "some_email@example.com", + }, + }, nil).AnyTimes() + + mockRemote.EXPECT().MakeSuperuserClient(context.Background()).Return(mockClient) + + mockClient.EXPECT().GetSchedule("some_remote_id", []string{"some_email@example.com"}, 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() + }) + } +} diff --git a/server/api/plugin_api.go b/server/api/plugin_api.go new file mode 100644 index 00000000..965ef403 --- /dev/null +++ b/server/api/plugin_api.go @@ -0,0 +1,22 @@ +package api + +import ( + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/plugin" +) + +type PluginAPIImpl struct { + pluginAPI plugin.API +} + +func (impl *PluginAPIImpl) GetUserStatus(userID string) (*model.Status, *model.AppError) { + return impl.pluginAPI.GetUserStatus(userID) +} + +func (impl *PluginAPIImpl) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) { + return impl.pluginAPI.GetUserStatusesByIds(userIDs) +} + +func (impl *PluginAPIImpl) UpdateUserStatus(userID, status string) (*model.Status, *model.AppError) { + return impl.pluginAPI.UpdateUserStatus(userID, status) +} diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 4e507e4b..0bce4462 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -115,7 +115,7 @@ func (p *Plugin) OnConfigurationChange() error { p.notificationHandler.Configure(p.newAPIConfig()) } - p.initUserStatusSyncJob() + p.POC_initUserStatusSyncJob() return nil } @@ -180,7 +180,7 @@ func (p *Plugin) newAPIConfig() api.Config { Logger: bot, Poster: bot, Remote: remote.Makers[msgraph.Kind](conf, bot), - API: p.API, + PluginAPI: p.API, }, } } @@ -214,7 +214,9 @@ func (p *Plugin) loadTemplates(bundlePath string) error { return nil } -func (p *Plugin) initUserStatusSyncJob() { +// POC_initUserStatusSyncJob begins a job that runs every 5 minutes to update the MM user's status based on their status in their Microsoft calendar +// This needs to be improved to run on a single node in the HA environment. Hence why the name is currently prefixed with POC +func (p *Plugin) POC_initUserStatusSyncJob() { conf := p.newAPIConfig() enable := p.getConfig().EnableStatusSync logger := conf.Dependencies.Logger diff --git a/server/remote/schedule.go b/server/remote/schedule.go index 01d754c4..7ca65d8e 100644 --- a/server/remote/schedule.go +++ b/server/remote/schedule.go @@ -16,6 +16,8 @@ type ScheduleInformationError struct { ResponseCode string `json:"responseCode"` } +type AvailabilityView string + // ScheduleInformation undocumented type ScheduleInformation struct { // Email of user @@ -23,7 +25,7 @@ type ScheduleInformation struct { // 0= free, 1= tentative, 2= busy, 3= out of office, 4= working elsewhere. // example "0010", which means free for first and second block, tentative for third, and free for fourth - AvailabilityView string `json:"availabilityView,omitempty"` + AvailabilityView AvailabilityView `json:"availabilityView,omitempty"` Error *ScheduleInformationError `json:"error"` diff --git a/server/store/mock_store/mock_event_store.go b/server/store/mock_store/mock_event_store.go new file mode 100644 index 00000000..8ec96c79 --- /dev/null +++ b/server/store/mock_store/mock_event_store.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/store (interfaces: EventStore) + +// Package mock_store is a generated GoMock package. +package mock_store + +import ( + gomock "github.com/golang/mock/gomock" + store "github.com/mattermost/mattermost-plugin-mscalendar/server/store" + reflect "reflect" +) + +// MockEventStore is a mock of EventStore interface +type MockEventStore struct { + ctrl *gomock.Controller + recorder *MockEventStoreMockRecorder +} + +// MockEventStoreMockRecorder is the mock recorder for MockEventStore +type MockEventStoreMockRecorder struct { + mock *MockEventStore +} + +// NewMockEventStore creates a new mock instance +func NewMockEventStore(ctrl *gomock.Controller) *MockEventStore { + mock := &MockEventStore{ctrl: ctrl} + mock.recorder = &MockEventStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockEventStore) EXPECT() *MockEventStoreMockRecorder { + return m.recorder +} + +// DeleteUserEvent mocks base method +func (m *MockEventStore) DeleteUserEvent(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUserEvent", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteUserEvent indicates an expected call of DeleteUserEvent +func (mr *MockEventStoreMockRecorder) DeleteUserEvent(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserEvent", reflect.TypeOf((*MockEventStore)(nil).DeleteUserEvent), arg0, arg1) +} + +// LoadUserEvent mocks base method +func (m *MockEventStore) LoadUserEvent(arg0, arg1 string) (*store.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadUserEvent", arg0, arg1) + ret0, _ := ret[0].(*store.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadUserEvent indicates an expected call of LoadUserEvent +func (mr *MockEventStoreMockRecorder) LoadUserEvent(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserEvent", reflect.TypeOf((*MockEventStore)(nil).LoadUserEvent), arg0, arg1) +} + +// StoreUserEvent mocks base method +func (m *MockEventStore) StoreUserEvent(arg0 string, arg1 *store.Event) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StoreUserEvent", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// StoreUserEvent indicates an expected call of StoreUserEvent +func (mr *MockEventStoreMockRecorder) StoreUserEvent(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreUserEvent", reflect.TypeOf((*MockEventStore)(nil).StoreUserEvent), arg0, arg1) +} diff --git a/server/store/user_store.go b/server/store/user_store.go index 5bd26c2f..28d5c812 100644 --- a/server/store/user_store.go +++ b/server/store/user_store.go @@ -165,3 +165,13 @@ func (index UserIndex) ByEmail() map[string]*UserShort { return result } + +func (index UserIndex) GetMattermostUserIDs() []string { + result := []string{} + + for _, u := range index { + result = append(result, u.MattermostUserID) + } + + return result +} diff --git a/server/utils/bot/mock_bot/mock_logger.go b/server/utils/bot/mock_bot/mock_logger.go new file mode 100644 index 00000000..b6039ba4 --- /dev/null +++ b/server/utils/bot/mock_bot/mock_logger.go @@ -0,0 +1,116 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot (interfaces: Logger) + +// Package mock_bot is a generated GoMock package. +package mock_bot + +import ( + gomock "github.com/golang/mock/gomock" + bot "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + reflect "reflect" +) + +// MockLogger is a mock of Logger interface +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +// Debugf mocks base method +func (m *MockLogger) Debugf(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf +func (mr *MockLoggerMockRecorder) Debugf(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + +// Errorf mocks base method +func (m *MockLogger) Errorf(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Errorf", varargs...) +} + +// Errorf indicates an expected call of Errorf +func (mr *MockLoggerMockRecorder) Errorf(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...) +} + +// Infof mocks base method +func (m *MockLogger) Infof(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Infof", varargs...) +} + +// Infof indicates an expected call of Infof +func (mr *MockLoggerMockRecorder) Infof(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...) +} + +// Warnf mocks base method +func (m *MockLogger) Warnf(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warnf", varargs...) +} + +// Warnf indicates an expected call of Warnf +func (mr *MockLoggerMockRecorder) Warnf(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*MockLogger)(nil).Warnf), varargs...) +} + +// With mocks base method +func (m *MockLogger) With(arg0 bot.LogContext) bot.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "With", arg0) + ret0, _ := ret[0].(bot.Logger) + return ret0 +} + +// With indicates an expected call of With +func (mr *MockLoggerMockRecorder) With(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockLogger)(nil).With), arg0) +} diff --git a/server/utils/plugin_api/mock_plugin_api/mock_plugin_api.go b/server/utils/plugin_api/mock_plugin_api/mock_plugin_api.go new file mode 100644 index 00000000..8766a149 --- /dev/null +++ b/server/utils/plugin_api/mock_plugin_api/mock_plugin_api.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/utils/plugin_api (interfaces: PluginAPI) + +// Package mock_plugin_api is a generated GoMock package. +package mock_plugin_api + +import ( + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/mattermost-server/v5/model" + reflect "reflect" +) + +// MockPluginAPI is a mock of PluginAPI interface +type MockPluginAPI struct { + ctrl *gomock.Controller + recorder *MockPluginAPIMockRecorder +} + +// MockPluginAPIMockRecorder is the mock recorder for MockPluginAPI +type MockPluginAPIMockRecorder struct { + mock *MockPluginAPI +} + +// NewMockPluginAPI creates a new mock instance +func NewMockPluginAPI(ctrl *gomock.Controller) *MockPluginAPI { + mock := &MockPluginAPI{ctrl: ctrl} + mock.recorder = &MockPluginAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockPluginAPI) EXPECT() *MockPluginAPIMockRecorder { + return m.recorder +} + +// GetUserStatus mocks base method +func (m *MockPluginAPI) GetUserStatus(arg0 string) (*model.Status, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserStatus", arg0) + ret0, _ := ret[0].(*model.Status) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUserStatus indicates an expected call of GetUserStatus +func (mr *MockPluginAPIMockRecorder) GetUserStatus(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatus", reflect.TypeOf((*MockPluginAPI)(nil).GetUserStatus), arg0) +} + +// GetUserStatusesByIds mocks base method +func (m *MockPluginAPI) GetUserStatusesByIds(arg0 []string) ([]*model.Status, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserStatusesByIds", arg0) + ret0, _ := ret[0].([]*model.Status) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// GetUserStatusesByIds indicates an expected call of GetUserStatusesByIds +func (mr *MockPluginAPIMockRecorder) GetUserStatusesByIds(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatusesByIds", reflect.TypeOf((*MockPluginAPI)(nil).GetUserStatusesByIds), arg0) +} + +// UpdateUserStatus mocks base method +func (m *MockPluginAPI) UpdateUserStatus(arg0, arg1 string) (*model.Status, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserStatus", arg0, arg1) + ret0, _ := ret[0].(*model.Status) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// UpdateUserStatus indicates an expected call of UpdateUserStatus +func (mr *MockPluginAPIMockRecorder) UpdateUserStatus(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockPluginAPI)(nil).UpdateUserStatus), arg0, arg1) +} diff --git a/server/utils/plugin_api/plugin_api.go b/server/utils/plugin_api/plugin_api.go new file mode 100644 index 00000000..c4c4d817 --- /dev/null +++ b/server/utils/plugin_api/plugin_api.go @@ -0,0 +1,14 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package plugin_api + +import ( + "github.com/mattermost/mattermost-server/v5/model" +) + +type PluginAPI interface { + GetUserStatus(userID string) (*model.Status, *model.AppError) + GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) + UpdateUserStatus(userID, status string) (*model.Status, *model.AppError) +}