From 7f1bbcd544c37b71db94265931517247b5479374 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Thu, 29 Feb 2024 23:02:35 -0500 Subject: [PATCH] Refactor calendar engine into its own package (#267) * oauth works * autocomplete * viewcal works * add unimplemented if blocks * fix tests and lint * implement CreateMySubscription, RenewSubscription, and DeleteSubscription gcal methods * WIP gcal subs * setup-attach make target * fix lint and tests * chore: more compatible tar command * add review comments * [GCAL] Create event logic (#269) * remote logic * allow datetimes without timezone * add calendar settings readonly scope to get tz * event conversion * scopes * revert datetime.time * bug: get timezone from datetime instead of event data (#271) * [GCAL] Allow building two different plugins under the same codebase (#268) * plugin profile images * receive provider flag * manifests * retrieve manifest id from file * Build time configuration * revert manifest * using go static configuration for providers * removed unused build flags * revert manifest * Added basic Google Calendar instructions * Update providers/GoogleCalendar.md Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * Update providers/GoogleCalendar.md Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * Update server/plugin/plugin.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * remoove log --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * [GCAL] Endpoint to autocomplete connected calendar users (#275) * user_store: more information fields on the index * feat: endpoint to autocomplete calendar connected users * revert test event store hashed * Update server/api/autocomplete.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * Update server/api/autocomplete.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * dont expose private data in endpoint * lint * Update server/store/user_store.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * [GCAL] Refactor Microsoft Calendar references to provider references (#276) * chore: replace provider display name * chore: replace command trigger references * revert previous changes * [GCAL] Enable notifications and reminders when a superuser token is not supported (#272) * nil control * use subscriptions default ttl * return specific error when no superuser token can be used * notification formatting logic to other file * Client.GetEventsBetweenDates * typo: import * shorter command name :) * availability without super user token * delete store subs only if we successfuly do on remote * comment * ignore missing subs * update sync method * allow switching acting user on the fly * daily summary without super user token * formatted imports * calendar -> calendarEvents * processAllDailySummaryByUser -> processAllDailySummaryWithIndividualCredentials * revert silenced error under notifications * msgraph GetEventsBetweenDates * Update server/mscalendar/daily_summary.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * [GCAL] Create event logic (#269) * remote logic * allow datetimes without timezone * add calendar settings readonly scope to get tz * event conversion * scopes * revert datetime.time * Client.GetEventsBetweenDates * fix: store last post time * refactored availability logic * availability test * refactored daily summary logic * test: availability * daily summary tests (wip) * daily summary test * fix: merge duplications * slack attachment for notifications * lint and test * goimports * Update server/mscalendar/availability.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * remove logges * nullify client when changing acting user * move withActiveUser filter to fenced code outside of general method * tests: handling more scenarios * Fixed test after bugfix * fixed test assertions * allow engine copy to perform safe state mutations --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * [GCAL] Added encrypted key value storage (#270) * Added encrypted kvstore * encrypt only oauth2 kv store * retrieve encryption setting from provider config * explicitly disable encrypted store for mscalendar * remove old comment * [GCAL] today/tomorrow commands with styling (#273) * feat: added today and tomorrow commands with attachments * remove unused function * linted * now -> day * remove unused code * today/tomorrow autocomplete * return tables on today/tomorrow commands * check for nil location * revert tomorrow day times four * fix markdown table render and test * [GCAL] Fix test in main branch from PR merges (#277) * fix: test from merges * add meeting url * [GCAL] Move configuration readyness logic to remotes (#279) * Move configuration logic to remotes * check encryption key as well * Update server/plugin/plugin.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * [GCAL] Remove unused code (#278) * remove batch requests from gcal code * More info in readme * [GCAL] Fix event notifications not working due to missing scope permissions plus internal state (#282) * fix: notifications scope and acting user logic * remove log * avoid calling notifications if the engine fails to load * [GCAL] Unsubscriptions (#283) * fix: notifications scope and acting user logic * fix: gcal unsubscribe requiring more parameters * update gcal manifest (#285) * [GCAL] Better logging information (#287) * fix: better log information * log missing error * use correct manifest file on manifest calls (#290) * [GCAL] Embed tzdata and correctly parse location from event datetime (#288) * fix: embed tzdata to prevent errors on systems without it installed * move to main * avoid overwritting location if empty * use tags instead of import * [GCAL] Event notifications behind a feature flag (#292) * put event notifications behind a feature flag * admin only un/subscribe commands * properly fence notification processor * ignore notifications if there's no processor (#293) * [GCAL] Remove join event column (broken from merges) (#294) * remove join column from merges * revert manifest * [GCAL] Channels reminder underlying logic (#274) * nil control * use subscriptions default ttl * return specific error when no superuser token can be used * notification formatting logic to other file * Client.GetEventsBetweenDates * typo: import * shorter command name :) * availability without super user token * delete store subs only if we successfuly do on remote * comment * ignore missing subs * update sync method * allow switching acting user on the fly * daily summary without super user token * formatted imports * calendar -> calendarEvents * processAllDailySummaryByUser -> processAllDailySummaryWithIndividualCredentials * revert silenced error under notifications * msgraph GetEventsBetweenDates * Update server/mscalendar/daily_summary.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * [GCAL] Create event logic (#269) * remote logic * allow datetimes without timezone * add calendar settings readonly scope to get tz * event conversion * scopes * revert datetime.time * Client.GetEventsBetweenDates * fix: store last post time * refactored availability logic * availability test * refactored daily summary logic * test: availability * daily summary tests (wip) * daily summary test * fix: merge duplications * slack attachment for notifications * lint and test * goimports * Allow poster to create posts * Allow store event metadata * styling * store recurring event id * deliver channel notifications * test: fixed for base case * test: channel reminders * test: recurring events * move from slice to map * Added store methods to interact with event metadata * lint * lint imports * restore logic lost in merge * duplicated test * Update server/mscalendar/availability_test.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * remove recurrent event id field --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * Updated google calendar readme * [GCAL] Summary command fixes (#286) * command trigger shown * added autocomplete to summary command * remove join meeting column * fix event subjects with table separator * fixed merge import * replace more characters in subject * [GCAL] Add a modal to create events (#281) * add webapp folder * WIP * opening create event modal from slash command works * add time selectors and initial impl of attendee selector * delete .npminstall * ignore .npminstall * [GCAL/MSCAL] Add endpoint to create events from the frontend (#291) * add remoteid to event attendee * api endpoint * conversion fixes * api logic * frontend logic to test * check if user belongs to channel * updated interface * use api/v1 prefix * implement getchannelmember * all day events * pluginAPI.CanLinkEventToChannel * autocomplete users * create event fetch call * channel autocomplete * improved error messages displayed to users * better error handling * channel selector store channel id * properly set email addresses for attendees * event location as string * response messages * little refactor * attendee selector can invite arbitrary emails * uppercase error dynamically * lowercase error * remove punctuation * linted ts files * goimports * fixed import * link events on creation * Add channel action to create events * add channel_id to payload * time selector every 15 minutes * time selector start/end with limits * capitalized error messages * don't allow 0 minute events * display timezone in channel reminders * utc time parsing * remove nullfunc * properly initialize variables * address some comments * npm run fix * Update webapp/src/components/time_selector.tsx Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * Update webapp/src/components/time_selector.tsx Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * refactor options with useMemo * capitalization in function for clarity * npm run fix * get plugin id from manifest * remove no-ops * refactor createEvent * fix channel autocomplete url * fix lint * fix user string typo * make it so `make apply` assumes gcal for now * more careful error handling * reorganize frontend error handling * default to CALENDAR_PROVIDER=gcal * use Client4.autocompleteChannels instead of custom plugin endpoint * fix manifest test --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * gofmt --------- Co-authored-by: Felipe Martin Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> * [GCAL/MSCAL] Store linked events per user to remove links on disconnection (#296) * add webapp folder * WIP * opening create event modal from slash command works * add time selectors and initial impl of attendee selector * delete .npminstall * ignore .npminstall * add remoteid to event attendee * api endpoint * conversion fixes * api logic * frontend logic to test * check if user belongs to channel * updated interface * use api/v1 prefix * implement getchannelmember * all day events * pluginAPI.CanLinkEventToChannel * autocomplete users * create event fetch call * channel autocomplete * improved error messages displayed to users * better error handling * channel selector store channel id * properly set email addresses for attendees * event location as string * response messages * little refactor * attendee selector can invite arbitrary emails * uppercase error dynamically * lowercase error * remove punctuation * linted ts files * goimports * fixed import * link events on creation * Add channel action to create events * add channel_id to payload * time selector every 15 minutes * time selector start/end with limits * capitalized error messages * don't allow 0 minute events * display timezone in channel reminders * utc time parsing * remove nullfunc * properly initialize variables * address some comments * npm run fix * store linked events for user to remove links on disconnection * typo * remove unused items after merge --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * [GCAL/MSCAL] Remind only accepted events (#295) * remind only accepted events * fixed tests * excludeDeclinedEvents * normalize msgraph event response * Remove GoogleDomainVerifyKey setting (#299) * fix: encrypt user store (#297) * feat: send notifications when an event is created (#300) * typo: user already connected message (#301) * typo: user already connected message * test: fix change * typo: user already connected message (#301) * typo: user already connected message * test: fix change * [GCAL/MSCAL] Exclude rejected events from agenda commands (#302) * exclude rejected events from agenda commands * test: check that declied event are excluded * fix possible nil pointer * [GCAL/MSCAL] Create event modal only for connected accounts (#303) * only connected users chan use slash command * prevent channel menu action from allowing too * npm run fix * lint errors * retrieve provider configuration to use in messages * websocket events * add message to mobile users * npm run fix * Update webapp/src/plugin_hooks.ts Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * register modal in setup * events create * trigger from provide config * using dispatch * missing semicolon * Update webapp/src/plugin_hooks.ts Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * singular --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * updated settings to use style and removed current value line (#305) * show link to connect if not connected (#306) * [GCAL/MSCAL] Reduce welcome steps (#308) * reduce welcome steps and edit defaults * Update server/mscalendar/welcome_flow.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * [GCAL/MSCAL] Control start/date times if the selected date is today (#307) * improvements to create event datetimes * reset starttime endtime on date change * repopulate times if date changed * npm run fix * database replication workaround (#312) * [GCAL/MSCAL] Catch errors when using `findmeetings` commands without enough parameters (#313) * fix: handle error in find meetings parameters command * changed loop variable usage * Replace Equals with ElementsMatch * makefile dist build for production (#314) * [GCAL/MSCAL] Create event UX improvements (#309) * Optional fields * time picker defaults * Add custom icon to Create Event menu item * add custom icon to date selector * npm run fix * header menu icon multiline * use scss * using proper typeRoots * scoped css style * [GCAL] Store conference data (#298) * extract conference data into new attribute * removed log * removed unused fuunction * tests * Update server/remote/gcal/get_default_calendar_view_test.go Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * more realistic datetime values --------- Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * Common code refactor, back to mscalendar plugin (#334) * refactor: mscalendar -> engine * remove gcal assets * refactored common code outside server package * check-style * go mod tidy * REPO_URL -> REPOSITORY_URL * delete unknown file * updated mocks * apped user to handle after all actions (#335) * refactor msgraph to mscalendar * goimports * removed apply command from merge * remove "REVIEW:" comments --------- Co-authored-by: Felipe Martin Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> --- .gitignore | 2 + Makefile | 111 ++++++- .../{profile.png => profile-mscalendar.png} | Bin .../{profile.svg => profile-mscalendar.svg} | 0 build/custom.mk | 36 +-- build/manifest/main.go | 12 +- build/setup.mk | 9 +- calendar/README.md | 3 + {server => calendar}/api/api.go | 28 +- calendar/api/autocomplete.go | 34 +++ calendar/api/connected_user.go | 30 ++ calendar/api/events.go | 274 +++++++++++++++++ {server => calendar}/api/get_authorized.go | 2 +- calendar/api/notification.go | 21 ++ {server => calendar}/api/post_action.go | 32 +- {server => calendar}/command/availability.go | 4 +- {server => calendar}/command/command.go | 100 ++++--- {server => calendar}/command/connect.go | 10 +- {server => calendar}/command/connect_test.go | 36 ++- .../command/create_calendar.go | 4 +- {server => calendar}/command/create_event.go | 8 +- calendar/command/daily_summary.go | 88 ++++++ .../command/delete_calendar.go | 2 +- {server => calendar}/command/disconnect.go | 6 +- .../command/disconnect_test.go | 45 +-- calendar/command/event.go | 16 + .../command/find_meeting_times.go | 15 +- calendar/command/get_calendars.go | 13 + {server => calendar}/command/help.go | 7 +- {server => calendar}/command/info.go | 9 +- {server => calendar}/command/settings.go | 4 +- {server => calendar}/command/subscribe.go | 8 +- {server => calendar}/command/unsubscribe.go | 6 +- {server => calendar}/command/view_calendar.go | 8 +- calendar/config/config.go | 56 ++++ calendar/config/const.go | 38 +++ .../engine}/availability.go | 191 +++++++++--- .../engine}/availability_test.go | 278 ++++++++++++++++-- .../engine}/calendar.go | 16 +- .../mscalendar => calendar/engine}/client.go | 4 +- .../engine}/daily_summary.go | 97 ++++-- .../engine}/daily_summary_test.go | 214 +++++++++++++- .../engine}/event_responder.go | 2 +- .../mscalendar => calendar/engine}/filters.go | 22 +- .../engine/mock_engine/mock_engine.go | 272 ++++++++--------- .../mock_plugin_api/mock_plugin_api.go | 58 +++- .../engine}/mock_welcomer/mock_welcomer.go | 2 +- .../engine}/mscalendar.go | 33 ++- calendar/engine/mscalendar_test.go | 23 ++ calendar/engine/notification.go | 217 ++++++++++++++ .../engine/notification_format.go | 219 +------------- .../engine}/notification_test.go | 48 +-- .../mscalendar => calendar/engine}/oauth2.go | 34 ++- .../engine}/oauth2_test.go | 43 ++- .../engine}/settings.go | 15 +- .../engine}/settings_daily_summary.go | 25 +- .../engine}/settings_notifications.go | 8 +- .../engine}/subscription.go | 41 ++- .../mscalendar => calendar/engine}/user.go | 42 ++- calendar/engine/views/calendar.go | 237 +++++++++++++++ .../views/status_change_notification.go | 2 +- calendar/engine/welcome_flow.go | 115 ++++++++ .../engine}/welcomer.go | 23 +- .../jobs/daily_summary_job.go | 8 +- {server => calendar}/jobs/job_manager.go | 12 +- calendar/jobs/mock_cluster/mock_cluster.go | 111 +++++++ {server => calendar}/jobs/renew_job.go | 7 +- {server => calendar}/jobs/status_sync_job.go | 10 +- {server => calendar}/plugin/plugin.go | 96 +++--- {server => calendar}/remote/calendar.go | 0 {server => calendar}/remote/client.go | 54 +++- {server => calendar}/remote/date_time.go | 13 +- {server => calendar}/remote/date_time_test.go | 0 {server => calendar}/remote/event.go | 14 + {server => calendar}/remote/meeting.go | 0 .../remote/mock_remote/mock_client.go | 52 ++-- .../remote/mock_remote/mock_remote.go | 19 +- {server => calendar}/remote/notification.go | 0 {server => calendar}/remote/remote.go | 11 +- {server => calendar}/remote/schedule.go | 0 {server => calendar}/remote/subscription.go | 1 + {server => calendar}/remote/user.go | 0 {server => calendar}/store/event_store.go | 69 ++++- {server => calendar}/store/flow_store.go | 0 .../store/mock_store/mock_store.go | 104 ++++++- {server => calendar}/store/oauth2_store.go | 0 {server => calendar}/store/setting_store.go | 5 +- {server => calendar}/store/store.go | 24 +- .../store/subscription_store.go | 6 +- {server => calendar}/store/user_store.go | 111 ++++++- {server => calendar}/store/welcome_store.go | 2 +- {server => calendar}/telemetry/logger.go | 0 {server => calendar}/telemetry/rudder.go | 0 {server => calendar}/telemetry/tracker.go | 0 .../testdata/batch_requests.json | 0 .../testdata/get_calendar_view_response.json | 0 .../get_mainbox_settings_response.json | 0 .../testdata/get_me_response.json | 0 .../testdata/get_schedule_response_batch.json | 0 .../testdata/get_schedule_response_busy.json | 0 .../testdata/get_schedule_response_free.json | 0 .../get_schedule_response_invalid_email.json | 0 .../testdata/webhook_event_ notification.json | 0 {server => calendar}/tracker/tracker.go | 2 +- {server => calendar}/utils/bot/admin.go | 0 {server => calendar}/utils/bot/bot.go | 2 +- {server => calendar}/utils/bot/bot_config.go | 0 .../utils/bot/flow_controller.go | 2 +- {server => calendar}/utils/bot/logger.go | 14 +- .../utils/bot/mock_bot/mock_admin.go | 2 +- .../utils/bot/mock_bot/mock_logger.go | 4 +- .../utils/bot/mock_bot/mock_poster.go | 36 ++- {server => calendar}/utils/bot/poster.go | 23 ++ {server => calendar}/utils/byte_size.go | 0 {server => calendar}/utils/byte_size_test.go | 0 {server => calendar}/utils/fields/fields.go | 0 {server => calendar}/utils/fields/value.go | 0 {server => calendar}/utils/flow/empty_step.go | 6 +- {server => calendar}/utils/flow/flow.go | 0 {server => calendar}/utils/flow/handler.go | 9 +- .../utils/httputils/handler.go | 0 .../utils/httputils/limited_readcloser.go | 2 +- .../httputils/limited_readcloser_test.go | 2 +- {server => calendar}/utils/httputils/utils.go | 16 + .../utils/httputils/utils_test.go | 0 calendar/utils/kvstore/crypt.go | 130 ++++++++ calendar/utils/kvstore/crypt_test.go | 109 +++++++ .../utils/kvstore/hashed_key.go | 0 .../utils/kvstore/hashed_key_test.go | 0 {server => calendar}/utils/kvstore/keys.go | 0 {server => calendar}/utils/kvstore/kvstore.go | 0 {server => calendar}/utils/kvstore/ots.go | 0 .../utils/kvstore/plugin_store.go | 0 {server => calendar}/utils/map.go | 0 {server => calendar}/utils/markdown.go | 0 .../utils/oauth2connect/oauth2.go | 10 +- .../utils/oauth2connect/oauth2_complete.go | 7 +- .../utils/oauth2connect/oauth2_connect.go | 2 +- {server => calendar}/utils/pluginapi/api.go | 35 ++- .../utils/settingspanel/bool_setting.go | 25 +- .../utils/settingspanel/empty_setting.go | 8 +- .../utils/settingspanel/handler.go | 9 +- .../utils/settingspanel/option_setting.go | 2 +- .../utils/settingspanel/read_only_setting.go | 4 +- .../utils/settingspanel/settings.go | 4 +- .../utils/settingspanel/utils.go | 0 .../utils/slack_attachments.go | 0 calendar/utils/test/gomock.go | 54 ++++ {server => calendar}/utils/tz/conversion.go | 0 {server => calendar}/utils/tz/data.go | 0 {server => calendar}/utils/tz/tz_test.go | 0 calendar/utils/url.go | 8 + go.mod | 3 +- go.sum | 4 +- .../msgraph => msgraph}/batch_request.go | 0 {server/remote/msgraph => msgraph}/call.go | 0 {server/remote/msgraph => msgraph}/client.go | 4 +- .../msgraph => msgraph}/create_calendar.go | 4 +- .../msgraph => msgraph}/create_event.go | 2 +- .../msgraph => msgraph}/delete_calendar.go | 2 +- msgraph/event.go | 90 ++++++ .../msgraph => msgraph}/find_meeting_times.go | 2 +- .../msgraph => msgraph}/get_calendars.go | 4 +- .../get_default_calendar_view.go | 15 +- .../get_mailbox_settings.go | 2 +- {server/remote/msgraph => msgraph}/get_me.go | 2 +- .../get_notification_data.go | 4 +- .../get_schedule_batched.go | 4 +- .../get_schedule_batched_test.go | 4 +- .../get_super_user_token.go | 0 .../msgraph => msgraph}/handle_webhook.go | 4 +- msgraph/msgraph.go | 31 ++ {server/remote/msgraph => msgraph}/remote.go | 17 +- .../msgraph => msgraph}/subscription.go | 18 +- plugin.json | 2 +- server/api/notification.go | 19 -- server/command/daily_summary.go | 77 ----- server/command/get_calendars.go | 13 - server/config/config.go | 29 -- server/config/const.go | 32 -- server/main.go | 15 +- server/mscalendar/views/calendar.go | 111 ------- server/mscalendar/welcome_flow.go | 101 ------- server/remote/msgraph/event.go | 51 ---- 184 files changed, 3863 insertions(+), 1417 deletions(-) rename assets/{profile.png => profile-mscalendar.png} (100%) rename assets/{profile.svg => profile-mscalendar.svg} (100%) create mode 100644 calendar/README.md rename {server => calendar}/api/api.go (52%) create mode 100644 calendar/api/autocomplete.go create mode 100644 calendar/api/connected_user.go create mode 100644 calendar/api/events.go rename {server => calendar}/api/get_authorized.go (80%) create mode 100644 calendar/api/notification.go rename {server => calendar}/api/post_action.go (87%) rename {server => calendar}/command/availability.go (82%) rename {server => calendar}/command/command.go (56%) rename {server => calendar}/command/connect.go (69%) rename {server => calendar}/command/connect_test.go (60%) rename {server => calendar}/command/create_calendar.go (73%) rename {server => calendar}/command/create_event.go (94%) create mode 100644 calendar/command/daily_summary.go rename {server => calendar}/command/delete_calendar.go (81%) rename {server => calendar}/command/disconnect.go (57%) rename {server => calendar}/command/disconnect_test.go (63%) create mode 100644 calendar/command/event.go rename {server => calendar}/command/find_meeting_times.go (72%) create mode 100644 calendar/command/get_calendars.go rename {server => calendar}/command/help.go (65%) rename {server => calendar}/command/info.go (57%) rename {server => calendar}/command/settings.go (56%) rename {server => calendar}/command/subscribe.go (74%) rename {server => calendar}/command/unsubscribe.go (63%) rename {server => calendar}/command/view_calendar.go (51%) create mode 100644 calendar/config/config.go create mode 100644 calendar/config/const.go rename {server/mscalendar => calendar/engine}/availability.go (69%) rename {server/mscalendar => calendar/engine}/availability_test.go (52%) rename {server/mscalendar => calendar/engine}/calendar.go (84%) rename {server/mscalendar => calendar/engine}/client.go (87%) rename {server/mscalendar => calendar/engine}/daily_summary.go (65%) rename {server/mscalendar => calendar/engine}/daily_summary_test.go (54%) rename {server/mscalendar => calendar/engine}/event_responder.go (98%) rename {server/mscalendar => calendar/engine}/filters.go (65%) rename server/mscalendar/mock_mscalendar/mock_mscalendar.go => calendar/engine/mock_engine/mock_engine.go (56%) rename {server/mscalendar => calendar/engine}/mock_plugin_api/mock_plugin_api.go (66%) rename {server/mscalendar => calendar/engine}/mock_welcomer/mock_welcomer.go (96%) rename {server/mscalendar => calendar/engine}/mscalendar.go (56%) create mode 100644 calendar/engine/mscalendar_test.go create mode 100644 calendar/engine/notification.go rename server/mscalendar/notification.go => calendar/engine/notification_format.go (54%) rename {server/mscalendar => calendar/engine}/notification_test.go (75%) rename {server/mscalendar => calendar/engine}/oauth2.go (69%) rename {server/mscalendar => calendar/engine}/oauth2_test.go (87%) rename {server/mscalendar => calendar/engine}/settings.go (77%) rename {server/mscalendar => calendar/engine}/settings_daily_summary.go (88%) rename {server/mscalendar => calendar/engine}/settings_notifications.go (92%) rename {server/mscalendar => calendar/engine}/subscription.go (76%) rename {server/mscalendar => calendar/engine}/user.go (71%) create mode 100644 calendar/engine/views/calendar.go rename {server/mscalendar => calendar/engine}/views/status_change_notification.go (97%) create mode 100644 calendar/engine/welcome_flow.go rename {server/mscalendar => calendar/engine}/welcomer.go (82%) rename {server => calendar}/jobs/daily_summary_job.go (76%) rename {server => calendar}/jobs/job_manager.go (89%) create mode 100644 calendar/jobs/mock_cluster/mock_cluster.go rename {server => calendar}/jobs/renew_job.go (84%) rename {server => calendar}/jobs/status_sync_job.go (80%) rename {server => calendar}/plugin/plugin.go (71%) rename {server => calendar}/remote/calendar.go (100%) rename {server => calendar}/remote/client.go (71%) rename {server => calendar}/remote/date_time.go (80%) rename {server => calendar}/remote/date_time_test.go (100%) rename {server => calendar}/remote/event.go (86%) rename {server => calendar}/remote/meeting.go (100%) rename {server => calendar}/remote/mock_remote/mock_client.go (90%) rename {server => calendar}/remote/mock_remote/mock_remote.go (79%) rename {server => calendar}/remote/notification.go (100%) rename {server => calendar}/remote/remote.go (60%) rename {server => calendar}/remote/schedule.go (100%) rename {server => calendar}/remote/subscription.go (91%) rename {server => calendar}/remote/user.go (100%) rename {server => calendar}/store/event_store.go (52%) rename {server => calendar}/store/flow_store.go (100%) rename {server => calendar}/store/mock_store/mock_store.go (81%) rename {server => calendar}/store/oauth2_store.go (100%) rename {server => calendar}/store/setting_store.go (97%) rename {server => calendar}/store/store.go (69%) rename {server => calendar}/store/subscription_store.go (90%) rename {server => calendar}/store/user_store.go (67%) rename {server => calendar}/store/welcome_store.go (91%) rename {server => calendar}/telemetry/logger.go (100%) rename {server => calendar}/telemetry/rudder.go (100%) rename {server => calendar}/telemetry/tracker.go (100%) rename {server => calendar}/testdata/batch_requests.json (100%) rename {server => calendar}/testdata/get_calendar_view_response.json (100%) rename {server => calendar}/testdata/get_mainbox_settings_response.json (100%) rename {server => calendar}/testdata/get_me_response.json (100%) rename {server => calendar}/testdata/get_schedule_response_batch.json (100%) rename {server => calendar}/testdata/get_schedule_response_busy.json (100%) rename {server => calendar}/testdata/get_schedule_response_free.json (100%) rename {server => calendar}/testdata/get_schedule_response_invalid_email.json (100%) rename {server => calendar}/testdata/webhook_event_ notification.json (100%) rename {server => calendar}/tracker/tracker.go (96%) rename {server => calendar}/utils/bot/admin.go (100%) rename {server => calendar}/utils/bot/bot.go (95%) rename {server => calendar}/utils/bot/bot_config.go (100%) rename {server => calendar}/utils/bot/flow_controller.go (96%) rename {server => calendar}/utils/bot/logger.go (88%) rename {server => calendar}/utils/bot/mock_bot/mock_admin.go (92%) rename {server => calendar}/utils/bot/mock_bot/mock_logger.go (95%) rename {server => calendar}/utils/bot/mock_bot/mock_poster.go (75%) rename {server => calendar}/utils/bot/poster.go (79%) rename {server => calendar}/utils/byte_size.go (100%) rename {server => calendar}/utils/byte_size_test.go (100%) rename {server => calendar}/utils/fields/fields.go (100%) rename {server => calendar}/utils/fields/value.go (100%) rename {server => calendar}/utils/flow/empty_step.go (66%) rename {server => calendar}/utils/flow/flow.go (100%) rename {server => calendar}/utils/flow/handler.go (85%) rename {server => calendar}/utils/httputils/handler.go (100%) rename {server => calendar}/utils/httputils/limited_readcloser.go (91%) rename {server => calendar}/utils/httputils/limited_readcloser_test.go (93%) rename {server => calendar}/utils/httputils/utils.go (83%) rename {server => calendar}/utils/httputils/utils_test.go (100%) create mode 100644 calendar/utils/kvstore/crypt.go create mode 100644 calendar/utils/kvstore/crypt_test.go rename {server => calendar}/utils/kvstore/hashed_key.go (100%) rename {server => calendar}/utils/kvstore/hashed_key_test.go (100%) rename {server => calendar}/utils/kvstore/keys.go (100%) rename {server => calendar}/utils/kvstore/kvstore.go (100%) rename {server => calendar}/utils/kvstore/ots.go (100%) rename {server => calendar}/utils/kvstore/plugin_store.go (100%) rename {server => calendar}/utils/map.go (100%) rename {server => calendar}/utils/markdown.go (100%) rename {server => calendar}/utils/oauth2connect/oauth2.go (63%) rename {server => calendar}/utils/oauth2connect/oauth2_complete.go (80%) rename {server => calendar}/utils/oauth2connect/oauth2_connect.go (87%) rename {server => calendar}/utils/pluginapi/api.go (70%) rename {server => calendar}/utils/settingspanel/bool_setting.go (85%) rename {server => calendar}/utils/settingspanel/empty_setting.go (72%) rename {server => calendar}/utils/settingspanel/handler.go (84%) rename {server => calendar}/utils/settingspanel/option_setting.go (96%) rename {server => calendar}/utils/settingspanel/read_only_setting.go (89%) rename {server => calendar}/utils/settingspanel/settings.go (96%) rename {server => calendar}/utils/settingspanel/utils.go (100%) rename {server => calendar}/utils/slack_attachments.go (100%) create mode 100644 calendar/utils/test/gomock.go rename {server => calendar}/utils/tz/conversion.go (100%) rename {server => calendar}/utils/tz/data.go (100%) rename {server => calendar}/utils/tz/tz_test.go (100%) create mode 100644 calendar/utils/url.go rename {server/remote/msgraph => msgraph}/batch_request.go (100%) rename {server/remote/msgraph => msgraph}/call.go (100%) rename {server/remote/msgraph => msgraph}/client.go (76%) rename {server/remote/msgraph => msgraph}/create_calendar.go (82%) rename {server/remote/msgraph => msgraph}/create_event.go (88%) rename {server/remote/msgraph => msgraph}/delete_calendar.go (87%) create mode 100644 msgraph/event.go rename {server/remote/msgraph => msgraph}/find_meeting_times.go (90%) rename {server/remote/msgraph => msgraph}/get_calendars.go (83%) rename {server/remote/msgraph => msgraph}/get_default_calendar_view.go (84%) rename {server/remote/msgraph => msgraph}/get_mailbox_settings.go (88%) rename {server/remote/msgraph => msgraph}/get_me.go (93%) rename {server/remote/msgraph => msgraph}/get_notification_data.go (88%) rename {server/remote/msgraph => msgraph}/get_schedule_batched.go (95%) rename {server/remote/msgraph => msgraph}/get_schedule_batched_test.go (88%) rename {server/remote/msgraph => msgraph}/get_super_user_token.go (100%) rename {server/remote/msgraph => msgraph}/handle_webhook.go (94%) create mode 100644 msgraph/msgraph.go rename {server/remote/msgraph => msgraph}/remote.go (78%) rename {server/remote/msgraph => msgraph}/subscription.go (75%) delete mode 100644 server/api/notification.go delete mode 100644 server/command/daily_summary.go delete mode 100644 server/command/get_calendars.go delete mode 100644 server/config/config.go delete mode 100644 server/config/const.go delete mode 100644 server/mscalendar/views/calendar.go delete mode 100644 server/mscalendar/welcome_flow.go delete mode 100644 server/remote/msgraph/event.go diff --git a/.gitignore b/.gitignore index 1521c8b7..9bfe167f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ dist +.DS_Store +.npminstall diff --git a/Makefile b/Makefile index fc2c58c4..dc5a8e7c 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,11 @@ CURL ?= $(shell command -v curl 2> /dev/null) MM_DEBUG ?= MANIFEST_FILE ?= plugin.json GOPATH ?= $(shell go env GOPATH) + +MANIFEST_FILE ?= plugin.json + GO_TEST_FLAGS ?= -race -GO_BUILD_FLAGS ?= +GO_BUILD_FLAGS ?= -tags timetzdata MM_UTILITIES_DIR ?= ../mattermost-utilities DLV_DEBUG_PORT := 2346 DEFAULT_GOOS := $(shell go env GOOS) @@ -13,9 +16,14 @@ DEFAULT_GOARCH := $(shell go env GOARCH) export GO111MODULE=on +GO_PACKAGES ?= ./server/... ./calendar/... ./msgraph/... + # You can include assets this directory into the bundle. This can be e.g. used to include profile pictures. ASSETS_DIR ?= assets +# Repository URL +REPOSITORY_URL ?= github.com/mattermost/mattermost-plugin-mscalendar + ## Define the default target (make all) .PHONY: default default: all @@ -41,7 +49,7 @@ endif .PHONY: all all: check-style test dist -## Runs eslint and golangci-lint +## Runs golangci-lint and eslint. .PHONY: check-style check-style: webapp/node_modules @echo Checking for style guide compliance @@ -94,17 +102,59 @@ webapp: webapp/node_modules ifneq ($(HAS_WEBAPP),) ifeq ($(MM_DEBUG),) cd webapp && $(NPM) run build; +endif + +## Builds the webapp in debug mode, if it exists. +.PHONY: webapp-debug +webapp-debug: webapp/.npminstall +ifneq ($(HAS_WEBAPP),) + cd webapp && \ + $(NPM) run debug; +endif + +# server-debug builds and deploys a debug version of the plugin for your architecture. +# Then resets the plugin to pick up the changes. +.PHONY: server-debug +server-debug: server-debug-deploy reset + +.PHONY: server-debug-deploy +server-debug-deploy: validate-go-version + mkdir -p server/dist +ifeq ($(OS),Darwin) + cd server && env GOOS=darwin GOARCH=amd64 $(GOBUILD) -gcflags "all=-N -l" -o dist/plugin-darwin-amd64; +else ifeq ($(OS),Linux) + cd server && env GOOS=linux GOARCH=amd64 $(GOBUILD) -gcflags "all=-N -l" -o dist/plugin-linux-amd64; +else ifeq ($(OS),Windows_NT) + cd server && env GOOS=windows GOARCH=amd64 $(GOBUILD) -gcflags "all=-N -l" -o dist/plugin-windows-amd64.exe; else cd webapp && $(NPM) run debug; endif endif + rm -rf dist/ + mkdir -p dist/$(PLUGIN_ID)/server/dist + cp $(MANIFEST_FILE) dist/$(PLUGIN_ID)/plugin.json + cp -r server/dist/* dist/$(PLUGIN_ID)/server/dist/ + mkdir -p ../mattermost-server/plugins + cp -r dist/* ../mattermost-server/plugins/ + +.PHONY: validate-go-version +validate-go-version: ## Validates the installed version of go against Mattermost's minimum requirement. + @if [ $(GO_MAJOR_VERSION) -gt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \ + exit 0 ;\ + elif [ $(GO_MAJOR_VERSION) -lt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \ + echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\ + exit 1; \ + elif [ $(GO_MINOR_VERSION) -lt $(MINIMUM_SUPPORTED_GO_MINOR_VERSION) ] ; then \ + echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\ + exit 1; \ + fi ## Generates a tar bundle of the plugin for install. .PHONY: bundle bundle: rm -rf dist/ mkdir -p dist/$(PLUGIN_ID) - cp $(MANIFEST_FILE) dist/$(PLUGIN_ID)/ + cp $(MANIFEST_FILE) dist/$(PLUGIN_ID)/plugin.json ifneq ($(wildcard $(ASSETS_DIR)/.),) cp -r $(ASSETS_DIR) dist/$(PLUGIN_ID)/ endif @@ -119,7 +169,7 @@ ifneq ($(HAS_WEBAPP),) mkdir -p dist/$(PLUGIN_ID)/webapp cp -r webapp/dist dist/$(PLUGIN_ID)/webapp/ endif - cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID) + cd dist && tar -cvzf $(BUNDLE_NAME) -C $(PLUGIN_ID) . @echo plugin built at: dist/$(BUNDLE_NAME) @@ -190,7 +240,7 @@ detach: setup-attach .PHONY: test test: webapp/node_modules ifneq ($(HAS_SERVER),) - $(GO) test -v $(GO_TEST_FLAGS) ./server/... + $(GO) test -v $(GO_TEST_FLAGS) $(GO_PACKAGES) endif ifneq ($(HAS_WEBAPP),) cd webapp && $(NPM) run test; @@ -200,7 +250,7 @@ endif .PHONY: coverage coverage: webapp/node_modules ifneq ($(HAS_SERVER),) - $(GO) test $(GO_TEST_FLAGS) -coverprofile=server/coverage.txt ./server/... + $(GO) test $(GO_TEST_FLAGS) -coverprofile=server/coverage.txt $(GO_PACKAGES) $(GO) tool cover -html=server/coverage.txt endif @@ -255,6 +305,55 @@ ifneq ($(HAS_WEBAPP),) endif rm -fr build/bin/ +## Setup dlv for attaching, identifying the plugin PID for other targets. +.PHONY: setup-attach +setup-attach: + $(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}')) + $(eval NUM_PID := $(shell echo -n ${PLUGIN_PID} | wc -w)) + + @if [ ${NUM_PID} -gt 2 ]; then \ + echo "** There is more than 1 plugin process running. Run 'make kill reset' to restart just one."; \ + exit 1; \ + fi + +## Check if setup-attach succeeded. +.PHONY: check-attach +check-attach: + @if [ -z ${PLUGIN_PID} ]; then \ + echo "Could not find plugin PID; the plugin is not running. Exiting."; \ + exit 1; \ + else \ + echo "Located Plugin running with PID: ${PLUGIN_PID}"; \ + fi + +## Attach dlv to an existing plugin instance. +.PHONY: attach +attach: setup-attach check-attach + dlv attach ${PLUGIN_PID} + +## Attach dlv to an existing plugin instance, exposing a headless instance on $DLV_DEBUG_PORT. +.PHONY: attach-headless +attach-headless: setup-attach check-attach + dlv attach ${PLUGIN_PID} --listen :$(DLV_DEBUG_PORT) --headless=true --api-version=2 --accept-multiclient + +## Detach dlv from an existing plugin instance, if previously attached. +.PHONY: detach +detach: setup-attach + @DELVE_PID=$(shell ps aux | grep "dlv attach ${PLUGIN_PID}" | grep -v "grep" | awk -F " " '{print $$2}') && \ + if [ "$$DELVE_PID" -gt 0 ] > /dev/null 2>&1 ; then \ + echo "Located existing delve process running with PID: $$DELVE_PID. Killing." ; \ + kill -9 $$DELVE_PID ; \ + fi + +## Kill all instances of the plugin, detaching any existing dlv instance. +.PHONY: kill +kill: detach + $(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}')) + + @for PID in ${PLUGIN_PID}; do \ + echo "Killing plugin pid $$PID"; \ + kill -9 $$PID; \ + done; \ # Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html help: @cat Makefile build/*.mk | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//" | sed -e "s/^## //" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort diff --git a/assets/profile.png b/assets/profile-mscalendar.png similarity index 100% rename from assets/profile.png rename to assets/profile-mscalendar.png diff --git a/assets/profile.svg b/assets/profile-mscalendar.svg similarity index 100% rename from assets/profile.svg rename to assets/profile-mscalendar.svg diff --git a/build/custom.mk b/build/custom.mk index 6e95582d..dcc42e9b 100644 --- a/build/custom.mk +++ b/build/custom.mk @@ -5,7 +5,7 @@ RUDDER_WRITE_KEY = 1d5bMvdrfWClLxgK1FvV3s4U1tg ifdef MM_RUDDER_PLUGINS_PROD RUDDER_WRITE_KEY = $(MM_RUDDER_PLUGINS_PROD) endif -LDFLAGS += -X "github.com/mattermost/mattermost-plugin-mscalendar/server/telemetry.rudderWriteKey=$(RUDDER_WRITE_KEY)" +LDFLAGS += -X "$(REPOSITORY_URL)/server/telemetry.rudderWriteKey=$(RUDDER_WRITE_KEY)" # Build info BUILD_DATE = $(shell date -u) @@ -21,25 +21,25 @@ GO_BUILD_FLAGS = -ldflags '$(LDFLAGS)' mock: ifneq ($(HAS_SERVER),) go install github.com/golang/mock/mockgen@v1.6.0 - mockgen -destination server/jobs/mock_cluster/mock_cluster.go github.com/mattermost/mattermost/server/public/pluginapi/cluster JobPluginAPI - mockgen -destination server/mscalendar/mock_mscalendar/mock_mscalendar.go github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar MSCalendar - mockgen -destination server/mscalendar/mock_welcomer/mock_welcomer.go -package mock_welcomer github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar Welcomer - mockgen -destination server/mscalendar/mock_plugin_api/mock_plugin_api.go -package mock_plugin_api github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar PluginAPI - mockgen -destination server/remote/mock_remote/mock_remote.go github.com/mattermost/mattermost-plugin-mscalendar/server/remote Remote - 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/store/mock_store/mock_store.go github.com/mattermost/mattermost-plugin-mscalendar/server/store Store + mockgen -destination calendar/jobs/mock_cluster/mock_cluster.go github.com/mattermost/mattermost-plugin-api/cluster JobPluginAPI + mockgen -destination calendar/engine/mock_engine/mock_engine.go $(REPOSITORY_URL)/calendar/engine Engine + mockgen -destination calendar/engine/mock_welcomer/mock_welcomer.go -package mock_welcomer $(REPOSITORY_URL)/calendar/engine Welcomer + mockgen -destination calendar/engine/mock_plugin_api/mock_plugin_api.go -package mock_plugin_api $(REPOSITORY_URL)/calendar/engine PluginAPI + mockgen -destination calendar/remote/mock_remote/mock_remote.go $(REPOSITORY_URL)/calendar/remote Remote + mockgen -destination calendar/remote/mock_remote/mock_client.go $(REPOSITORY_URL)/calendar/remote Client + mockgen -destination calendar/utils/bot/mock_bot/mock_poster.go $(REPOSITORY_URL)/calendar/utils/bot Poster + mockgen -destination calendar/utils/bot/mock_bot/mock_admin.go $(REPOSITORY_URL)/calendar/utils/bot Admin + mockgen -destination calendar/utils/bot/mock_bot/mock_logger.go $(REPOSITORY_URL)/calendar/utils/bot Logger + mockgen -destination calendar/store/mock_store/mock_store.go $(REPOSITORY_URL)/calendar/store Store endif clean_mock: ifneq ($(HAS_SERVER),) - rm -rf ./server/jobs/mock_cluster - rm -rf ./server/mscalendar/mock_mscalendar - rm -rf ./server/mscalendar/mock_welcomer - rm -rf ./server/mscalendar/mock_plugin_api - rm -rf ./server/remote/mock_remote - rm -rf ./server/utils/bot/mock_bot - rm -rf ./server/store/mock_store + rm -rf ./calendar/jobs/mock_cluster + rm -rf ./calendar/engine/mock_engine + rm -rf ./calendar/engine/mock_welcomer + rm -rf ./calendar/engine/mock_plugin_api + rm -rf ./calendar/remote/mock_remote + rm -rf ./calendar/utils/bot/mock_bot + rm -rf ./calendar/store/mock_store endif diff --git a/build/manifest/main.go b/build/manifest/main.go index 957a5002..ee365ebf 100644 --- a/build/manifest/main.go +++ b/build/manifest/main.go @@ -43,10 +43,16 @@ func main() { } func findManifest() (*model.Manifest, error) { - _, manifestFilePath, err := model.FindManifest(".") - if err != nil { - return nil, errors.Wrap(err, "failed to find manifest in current working directory") + manifestFilePath := os.Getenv("MANIFEST_FILE") + + if manifestFilePath == "" { + var err error + _, manifestFilePath, err = model.FindManifest(".") + if err != nil { + return nil, errors.Wrap(err, "failed to find manifest in current working directory") + } } + manifestFile, err := os.Open(manifestFilePath) if err != nil { return nil, errors.Wrapf(err, "failed to open %s", manifestFilePath) diff --git a/build/setup.mk b/build/setup.mk index 493b06fc..1c932f2d 100644 --- a/build/setup.mk +++ b/build/setup.mk @@ -11,22 +11,23 @@ $(shell cd build/manifest && $(GO) build -o ../bin/manifest) $(shell cd build/pluginctl && $(GO) build -o ../bin/pluginctl) # Extract the plugin id from the manifest. -PLUGIN_ID ?= $(shell build/bin/manifest id) +# TODO: Not working +PLUGIN_ID ?= $(shell MANIFEST_FILE=$(MANIFEST_FILE) build/bin/manifest id) ifeq ($(PLUGIN_ID),) $(error "Cannot parse id from $(MANIFEST_FILE)") endif # Extract the plugin version from the manifest. -PLUGIN_VERSION ?= $(shell build/bin/manifest version) +PLUGIN_VERSION ?= $(shell MANIFEST_FILE=$(MANIFEST_FILE) build/bin/manifest version) ifeq ($(PLUGIN_VERSION),) $(error "Cannot parse version from $(MANIFEST_FILE)") endif # Determine if a server is defined in the manifest. -HAS_SERVER ?= $(shell build/bin/manifest has_server) +HAS_SERVER ?= $(shell MANIFEST_FILE=$(MANIFEST_FILE) build/bin/manifest has_server) # Determine if a webapp is defined in the manifest. -HAS_WEBAPP ?= $(shell build/bin/manifest has_webapp) +HAS_WEBAPP ?= $(shell MANIFEST_FILE=$(MANIFEST_FILE) build/bin/manifest has_webapp) # Determine if a /public folder is in use HAS_PUBLIC ?= $(wildcard public/.) diff --git a/calendar/README.md b/calendar/README.md new file mode 100644 index 00000000..26f2e2da --- /dev/null +++ b/calendar/README.md @@ -0,0 +1,3 @@ +# Calendar Plugin API + +This file contain the common logic for creating Calendar plugins for Mattermost. diff --git a/server/api/api.go b/calendar/api/api.go similarity index 52% rename from server/api/api.go rename to calendar/api/api.go index 19fdcfbe..89e7b184 100644 --- a/server/api/api.go +++ b/calendar/api/api.go @@ -4,22 +4,25 @@ package api import ( - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" + "net/http" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" ) type api struct { - mscalendar.Env - mscalendar.NotificationProcessor + engine.Env + engine.NotificationProcessor } // Init initializes the router. -func Init(h *httputils.Handler, env mscalendar.Env, notificationProcessor mscalendar.NotificationProcessor) { +func Init(h *httputils.Handler, env engine.Env, notificationProcessor engine.NotificationProcessor) { api := &api{ Env: env, NotificationProcessor: notificationProcessor, } + apiRouter := h.Router.PathPrefix(config.PathAPI).Subrouter() apiRouter.HandleFunc("/authorized", api.getAuthorized).Methods("GET") @@ -32,4 +35,17 @@ func Init(h *httputils.Handler, env mscalendar.Env, notificationProcessor mscale postActionRouter.HandleFunc(config.PathTentative, api.postActionTentative).Methods("POST") postActionRouter.HandleFunc(config.PathRespond, api.postActionRespond).Methods("POST") postActionRouter.HandleFunc(config.PathConfirmStatusChange, api.postActionConfirmStatusChange).Methods("POST") + + dialogRouter := h.Router.PathPrefix(config.PathAutocomplete).Subrouter() + dialogRouter.HandleFunc(config.PathUsers, api.autocompleteConnectedUsers) + + apiRoutes := h.Router.PathPrefix(config.InternalAPIPath).Subrouter() + eventsRouter := apiRoutes.PathPrefix(config.PathEvents).Subrouter() + eventsRouter.HandleFunc(config.PathCreate, api.createEvent).Methods("POST") + apiRoutes.HandleFunc(config.PathConnectedUser, api.connectedUserHandler) + + // Returns provider information for the plugin to use + apiRoutes.HandleFunc(config.PathProvider, func(w http.ResponseWriter, r *http.Request) { + httputils.WriteJSONResponse(w, config.Provider, 200) + }) } diff --git a/calendar/api/autocomplete.go b/calendar/api/autocomplete.go new file mode 100644 index 00000000..8f611502 --- /dev/null +++ b/calendar/api/autocomplete.go @@ -0,0 +1,34 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" +) + +func (api *api) autocompleteConnectedUsers(w http.ResponseWriter, r *http.Request) { + mattermostUserID := r.Header.Get("Mattermost-User-Id") + _, err := api.Store.LoadUser(mattermostUserID) + if mattermostUserID == "" || errors.Is(err, store.ErrNotFound) { + httputils.WriteUnauthorizedError(w, fmt.Errorf("unauthorized")) + return + } + + searchString := r.URL.Query().Get("search") + results, err := api.Store.SearchInUserIndex(searchString, 10) + if err != nil { + utils.SlackAttachmentError(w, "unable to search in user index: "+err.Error()) + httputils.WriteInternalServerError(w, err) + return + } + + if err := httputils.WriteJSONResponse(w, results.ToDTO(), http.StatusOK); err != nil { + api.Logger.With(bot.LogContext{"err": err.Error()}).Errorf("error sending response to user") + httputils.WriteInternalServerError(w, err) + } +} diff --git a/calendar/api/connected_user.go b/calendar/api/connected_user.go new file mode 100644 index 00000000..93b65847 --- /dev/null +++ b/calendar/api/connected_user.go @@ -0,0 +1,30 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" +) + +func (api *api) connectedUserHandler(w http.ResponseWriter, r *http.Request) { + mattermostUserID := r.Header.Get("Mattermost-User-Id") + if mattermostUserID == "" { + httputils.WriteUnauthorizedError(w, fmt.Errorf("unauthorized")) + return + } + + _, errStore := api.Store.LoadUser(mattermostUserID) + if errStore != nil && !errors.Is(errStore, store.ErrNotFound) { + httputils.WriteInternalServerError(w, errStore) + return + } + if errors.Is(errStore, store.ErrNotFound) { + httputils.WriteUnauthorizedError(w, fmt.Errorf("unauthorized")) + return + } + + w.Write([]byte(`{"is_connected": true}`)) +} diff --git a/calendar/api/events.go b/calendar/api/events.go new file mode 100644 index 00000000..c7ffbe7e --- /dev/null +++ b/calendar/api/events.go @@ -0,0 +1,274 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/mattermost/mattermost/server/public/model" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/views" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" + + "github.com/pkg/errors" +) + +const ( + createEventDateTimeFormat = "2006-01-02 15:04" + createEventDateFormat = "2006-01-02" +) + +type createEventPayload struct { + AllDay bool `json:"all_day"` + Attendees []string `json:"attendees"` + Date string `json:"date"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + // Reminder bool `json:"reminder" + Description string `json:"description,omitempty"` + Subject string `json:"subject"` + Location string `json:"location,omitempty"` + ChannelID string `json:"channel_id"` +} + +func (cep createEventPayload) ToRemoteEvent(loc *time.Location) (*remote.Event, error) { + var evt remote.Event + + evt.IsAllDay = cep.AllDay + + start, err := cep.parseStartTime(loc) + if err != nil { + return nil, errors.Wrap(err, "error parsing start time") + } + + end, err := cep.parseEndTime(loc) + if err != nil { + return nil, errors.Wrap(err, "error parsing start time") + } + + if !cep.AllDay { + evt.Start = &remote.DateTime{ + DateTime: start.Format(remote.RFC3339NanoNoTimezone), + TimeZone: loc.String(), + } + evt.End = &remote.DateTime{ + DateTime: end.Format(remote.RFC3339NanoNoTimezone), + TimeZone: loc.String(), + } + } else { + date, err := cep.parseDate(loc) + if err != nil { + return nil, errors.Wrap(err, "error parsing date") + } + + evt.Start = &remote.DateTime{ + DateTime: time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, loc).Format(remote.RFC3339NanoNoTimezone), + TimeZone: loc.String(), + } + evt.End = &remote.DateTime{ + DateTime: time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 99, loc).Format(remote.RFC3339NanoNoTimezone), + TimeZone: loc.String(), + } + } + + if cep.Description != "" { + evt.Body = &remote.ItemBody{ + Content: cep.Description, + ContentType: "text/plain", + } + } + evt.Subject = cep.Subject + if cep.Location != "" { + evt.Location = &remote.Location{ + DisplayName: cep.Location, + } + } + + return &evt, nil +} + +func (cep createEventPayload) parseStartTime(loc *time.Location) (time.Time, error) { + return time.ParseInLocation(createEventDateTimeFormat, fmt.Sprintf("%s %s", cep.Date, cep.StartTime), loc) +} + +func (cep createEventPayload) parseEndTime(loc *time.Location) (time.Time, error) { + return time.ParseInLocation(createEventDateTimeFormat, fmt.Sprintf("%s %s", cep.Date, cep.EndTime), loc) +} + +func (cep createEventPayload) parseDate(loc *time.Location) (time.Time, error) { + return time.ParseInLocation(createEventDateFormat, cep.Date, loc) +} + +func (cep createEventPayload) IsValid(loc *time.Location) error { + if cep.Subject == "" { + return fmt.Errorf("subject must not be empty") + } + + if cep.Date == "" { + return fmt.Errorf("date must not be empty") + } + + _, err := cep.parseDate(loc) + if err != nil { + return fmt.Errorf("invalid date") + } + + if cep.StartTime == "" && cep.EndTime == "" && !cep.AllDay { + return fmt.Errorf("start time/end time must be set or event should last all day") + } + + start, err := cep.parseStartTime(loc) + if err != nil { + return fmt.Errorf("please use a valid start time") + } + + if start.Before(time.Now()) { + return fmt.Errorf("please select a start date and time that is not prior to the current time") + } + + end, err := cep.parseEndTime(loc) + if err != nil { + return fmt.Errorf("please use a valid end time") + } + + if end.Before(time.Now()) { + return fmt.Errorf("please select an end date and time that is not prior to the current time") + } + + if start.After(end) { + return fmt.Errorf("end date cannot be earlier than start date") + } + + return nil +} + +func (api *api) createEvent(w http.ResponseWriter, r *http.Request) { + mattermostUserID := r.Header.Get("Mattermost-User-Id") + if mattermostUserID == "" { + httputils.WriteUnauthorizedError(w, fmt.Errorf("unauthorized")) + return + } + + user, errStore := api.Store.LoadUser(mattermostUserID) + if errStore != nil && !errors.Is(errStore, store.ErrNotFound) { + httputils.WriteInternalServerError(w, errStore) + return + } + if errors.Is(errStore, store.ErrNotFound) { + httputils.WriteUnauthorizedError(w, fmt.Errorf("unauthorized")) + return + } + + var payload createEventPayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + httputils.WriteBadRequestError(w, err) + return + } + defer r.Body.Close() + + if payload.ChannelID != "" { + if !api.PluginAPI.CanLinkEventToChannel(payload.ChannelID, user.MattermostUserID) { + httputils.WriteBadRequestError(w, fmt.Errorf("you don't have permission to link events in the selected channel")) + return + } + } + + client := api.Remote.MakeClient(context.Background(), user.OAuth2Token) + + mailbox, errMailbox := client.GetMailboxSettings(user.Remote.ID) + if errMailbox != nil { + httputils.WriteInternalServerError(w, errMailbox) + return + } + + loc, errLocation := time.LoadLocation(mailbox.TimeZone) + if errLocation != nil { + httputils.WriteInternalServerError(w, errLocation) + return + } + + if err := payload.IsValid(loc); err != nil { + httputils.WriteBadRequestError(w, err) + return + } + + event, errParse := payload.ToRemoteEvent(loc) + if errParse != nil { + httputils.WriteBadRequestError(w, errParse) + return + } + + for _, pa := range payload.Attendees { + var emailAddress string + + if strings.Contains(pa, "@") { + emailAddress = pa + } else { + attendeeUser, err := api.Store.LoadUser(pa) + if err != nil { + api.Logger.With(bot.LogContext{"attendee_mm_id": pa}).Errorf("error loading attendee from mattermost user id") + continue + } + + emailAddress = attendeeUser.Remote.Mail + } + + event.Attendees = append(event.Attendees, &remote.Attendee{ + EmailAddress: &remote.EmailAddress{ + Address: emailAddress, + }, + }) + } + + event, err := client.CreateEvent(user.Remote.ID, event) + if err != nil { + httputils.WriteInternalServerError(w, err) + return + } + + attachment, err := views.RenderEventAsAttachment(event, mailbox.TimeZone, views.ShowTimezoneOption(mailbox.TimeZone)) + if err != nil { + api.Logger.With(bot.LogContext{"err": err}).Errorf("error rendering event as slack attachment") + } + + // Event linking + if payload.ChannelID != "" { + if err := api.Store.StoreUserLinkedEvent(user.MattermostUserID, event.ICalUID, payload.ChannelID); err != nil { + api.Poster.DM(mattermostUserID, "Your event **%s** could not be linked to a channel. Please contact an administrator for more details.", event.Subject) + httputils.WriteInternalServerError(w, err) + return + } + + if err := api.Store.AddLinkedChannelToEvent(event.ICalUID, payload.ChannelID); err != nil { + api.Logger.With(bot.LogContext{"err": err}).Errorf("error linking event to channel") + defer func() { + api.Poster.DM(mattermostUserID, "You event **%s** could not be linked to a channel. Please contact an administrator for more details.", event.Subject) + }() + } else { + post := &model.Post{ + Message: fmt.Sprintf("The event **%s** was linked to this channel by @%s", event.Subject, user.MattermostUsername), + ChannelId: payload.ChannelID, + } + if attachment != nil { + model.ParseSlackAttachment(post, []*model.SlackAttachment{attachment}) + } + if err := api.Poster.CreatePost(post); err != nil { + api.Logger.With(bot.LogContext{"err": err}).Errorf("error sending post to channel about linked event") + } + } + } else { + if attachment == nil { + api.Poster.DM(mattermostUserID, "Your event: **%s** was created successfully.", event.Subject) + } else { + api.Poster.DMWithMessageAndAttachments(mattermostUserID, "Your event was created successfully.", attachment) + } + } + + httputils.WriteJSONResponse(w, `{"ok": true}`, http.StatusCreated) +} diff --git a/server/api/get_authorized.go b/calendar/api/get_authorized.go similarity index 80% rename from server/api/get_authorized.go rename to calendar/api/get_authorized.go index 1d4863e3..9c98f203 100644 --- a/server/api/get_authorized.go +++ b/calendar/api/get_authorized.go @@ -5,7 +5,7 @@ package api import "net/http" -func (api *api) getAuthorized(w http.ResponseWriter, r *http.Request) { +func (api *api) getAuthorized(w http.ResponseWriter, _ *http.Request) { // if we've made it here, we're authorized. w.WriteHeader(http.StatusOK) w.Write([]byte(`{"is_authorized": true}`)) diff --git a/calendar/api/notification.go b/calendar/api/notification.go new file mode 100644 index 00000000..5af3b848 --- /dev/null +++ b/calendar/api/notification.go @@ -0,0 +1,21 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package api + +import ( + "net/http" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" +) + +func (api *api) notification(w http.ResponseWriter, req *http.Request) { + if api.NotificationProcessor != nil { + err := api.NotificationProcessor.Enqueue( + api.Env.Remote.HandleWebhook(w, req)...) + if err != nil { + httputils.WriteInternalServerError(w, err) + return + } + } +} diff --git a/server/api/post_action.go b/calendar/api/post_action.go similarity index 87% rename from server/api/post_action.go rename to calendar/api/post_action.go index beacea67..ce326f0e 100644 --- a/server/api/post_action.go +++ b/calendar/api/post_action.go @@ -13,13 +13,13 @@ import ( "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/views" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/views" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" ) -func (api *api) preprocessAction(w http.ResponseWriter, req *http.Request) (mscal mscalendar.MSCalendar, user *mscalendar.User, eventID string, option string, postID string) { +func (api *api) preprocessAction(w http.ResponseWriter, req *http.Request) (mscal engine.Engine, user *engine.User, eventID string, option string, postID string) { mattermostUserID := req.Header.Get("Mattermost-User-ID") if mattermostUserID == "" { utils.SlackAttachmentError(w, "Error: not authorized") @@ -38,17 +38,17 @@ func (api *api) preprocessAction(w http.ResponseWriter, req *http.Request) (msca return nil, nil, "", "", "" } option, _ = request.Context["selected_option"].(string) - mscal = mscalendar.New(api.Env, mattermostUserID) + mscal = engine.New(api.Env, mattermostUserID) - return mscal, mscalendar.NewUser(mattermostUserID), eventID, option, request.PostId + return mscal, engine.NewUser(mattermostUserID), eventID, option, request.PostId } func (api *api) postActionAccept(w http.ResponseWriter, req *http.Request) { - mscalendar, user, eventID, _, _ := api.preprocessAction(w, req) + localEngine, user, eventID, _, _ := api.preprocessAction(w, req) if eventID == "" { return } - err := mscalendar.AcceptEvent(user, eventID) + err := localEngine.AcceptEvent(user, eventID) if err != nil { api.Logger.Warnf("Failed to accept event. err=%v", err) utils.SlackAttachmentError(w, "Error: Failed to accept event: "+err.Error()) @@ -57,11 +57,11 @@ func (api *api) postActionAccept(w http.ResponseWriter, req *http.Request) { } func (api *api) postActionDecline(w http.ResponseWriter, req *http.Request) { - mscalendar, user, eventID, _, _ := api.preprocessAction(w, req) + localEngine, user, eventID, _, _ := api.preprocessAction(w, req) if eventID == "" { return } - err := mscalendar.DeclineEvent(user, eventID) + err := localEngine.DeclineEvent(user, eventID) if err != nil { utils.SlackAttachmentError(w, "Error: Failed to decline event: "+err.Error()) return @@ -69,11 +69,11 @@ func (api *api) postActionDecline(w http.ResponseWriter, req *http.Request) { } func (api *api) postActionTentative(w http.ResponseWriter, req *http.Request) { - mscalendar, user, eventID, _, _ := api.preprocessAction(w, req) + localEngine, user, eventID, _, _ := api.preprocessAction(w, req) if eventID == "" { return } - err := mscalendar.TentativelyAcceptEvent(user, eventID) + err := localEngine.TentativelyAcceptEvent(user, eventID) if err != nil { utils.SlackAttachmentError(w, "Error: Failed to tentatively accept event: "+err.Error()) return @@ -135,11 +135,11 @@ func (api *api) postActionRespond(w http.ResponseWriter, req *http.Request) { func prettyOption(option string) string { switch option { - case mscalendar.OptionYes: + case engine.OptionYes: return "accepted" - case mscalendar.OptionNo: + case engine.OptionNo: return "declined" - case mscalendar.OptionMaybe: + case engine.OptionMaybe: return "tentatively accepted" default: return "" diff --git a/server/command/availability.go b/calendar/command/availability.go similarity index 82% rename from server/command/availability.go rename to calendar/command/availability.go index 5bca44d3..f8a2a0ae 100644 --- a/server/command/availability.go +++ b/calendar/command/availability.go @@ -6,14 +6,14 @@ package command func (c *Command) debugAvailability(parameters ...string) (string, bool, error) { switch { case len(parameters) == 0: - resString, _, err := c.MSCalendar.Sync(c.Args.UserId) + resString, _, err := c.Engine.Sync(c.Args.UserId) if err != nil { return "", false, err } return resString, false, nil case len(parameters) == 1 && parameters[0] == "all": - resString, _, err := c.MSCalendar.SyncAll() + resString, _, err := c.Engine.SyncAll() if err != nil { return "", false, err } diff --git a/server/command/command.go b/calendar/command/command.go similarity index 56% rename from server/command/command.go rename to calendar/command/command.go index a7949db3..fe0ebc8c 100644 --- a/server/command/command.go +++ b/calendar/command/command.go @@ -13,34 +13,58 @@ import ( "github.com/mattermost/mattermost/server/public/pluginapi/experimental/command" "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" - "github.com/mattermost/mattermost-plugin-mscalendar/server/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" ) // Handler handles commands type Command struct { - MSCalendar mscalendar.MSCalendar - Context *plugin.Context - Args *model.CommandArgs - Config *config.Config - ChannelID string + Engine engine.Engine + Context *plugin.Context + Args *model.CommandArgs + Config *config.Config + ChannelID string } -func getNotConnectedText() string { - return fmt.Sprintf("It looks like your Mattermost account is not connected to a Microsoft account. Please connect your account using `/%s connect`.", config.CommandTrigger) +func getNotConnectedText(pluginURL string) string { + return fmt.Sprintf( + "It looks like your Mattermost account is not connected to a %s account. [Click here to connect your account](%s/oauth2/connect) or use `/%s connect`.", + config.Provider.DisplayName, + pluginURL, + config.Provider.CommandTrigger, + ) } type handleFunc func(parameters ...string) (string, bool, error) var cmds = []*model.AutocompleteData{ - model.NewAutocompleteData("connect", "", "Connect to your Microsoft account"), - model.NewAutocompleteData("disconnect", "", "Disconnect from your Microsoft Account"), - model.NewAutocompleteData("summary", "", "View your events for today, or edit the settings for your daily summary."), + model.NewAutocompleteData("connect", "", fmt.Sprintf("Connect to your %s account", config.Provider.DisplayName)), + model.NewAutocompleteData("disconnect", "", fmt.Sprintf("Disconnect from your %s account", config.Provider.DisplayName)), + { // Summary + Trigger: "summary", + HelpText: "View your events for today, or edit the settings for your daily summary.", + SubCommands: []*model.AutocompleteData{ + model.NewAutocompleteData("view", "", "View your daily summary."), + model.NewAutocompleteData("today", "", "Display today's events."), + model.NewAutocompleteData("tomorrow", "", "Display tomorrow's events."), + model.NewAutocompleteData("settings", "", "View your settings for the daily summary."), + model.NewAutocompleteData("time", "", "Set the time you would like to receive your daily summary."), + model.NewAutocompleteData("enable", "", "Enable your daily summary."), + model.NewAutocompleteData("disable", "", "Disable your daily summary."), + }, + }, model.NewAutocompleteData("viewcal", "", "View your events for the upcoming week."), + { // Create + Trigger: "event", + HelpText: "Manage events.", + SubCommands: []*model.AutocompleteData{ + model.NewAutocompleteData("create", "", "Creates a new event (desktop only)."), + }, + }, + model.NewAutocompleteData("today", "", "Display today's events."), + model.NewAutocompleteData("tomorrow", "", "Display tomorrow's events."), model.NewAutocompleteData("settings", "", "Edit your user personal settings."), - model.NewAutocompleteData("subscribe", "", "Enable notifications for event invitations and updates."), - model.NewAutocompleteData("unsubscribe", "", "Disable notifications for event invitations and updates."), model.NewAutocompleteData("info", "", "Read information about this version of the plugin."), model.NewAutocompleteData("help", "", "Read help text for the commands"), } @@ -54,18 +78,18 @@ func Register(client *pluginapilicense.Client) error { hint := "[" + strings.Join(names[:4], "|") + "...]" - cmd := model.NewAutocompleteData(config.CommandTrigger, hint, "Interact with your Outlook calendar.") + cmd := model.NewAutocompleteData(config.Provider.CommandTrigger, hint, fmt.Sprintf("Interact with your %s calendar.", config.Provider.DisplayName)) cmd.SubCommands = cmds - iconData, err := command.GetIconData(&client.System, "assets/profile.svg") + iconData, err := command.GetIconData(&client.System, fmt.Sprintf("assets/profile-%s.svg", config.Provider.Name)) if err != nil { return errors.Wrap(err, "failed to get icon data") } return client.SlashCommand.Register(&model.Command{ - Trigger: config.CommandTrigger, - DisplayName: "Microsoft Calendar", - Description: "Interact with your Outlook calendar.", + Trigger: config.Provider.CommandTrigger, + DisplayName: config.Provider.DisplayName, + Description: fmt.Sprintf("Interact with your %s calendar.", config.Provider.DisplayName), AutoComplete: true, AutoCompleteDesc: strings.Join(names, ", "), AutoCompleteHint: "(subcommand)", @@ -99,22 +123,32 @@ func (c *Command) Handle() (string, bool, error) { handler = c.requireConnectedUser(c.createEvent) case "deletecal": handler = c.requireConnectedUser(c.deleteCalendar) - case "subscribe": - handler = c.requireConnectedUser(c.subscribe) - case "unsubscribe": - handler = c.requireConnectedUser(c.unsubscribe) case "findmeetings": handler = c.requireConnectedUser(c.findMeetings) case "showcals": handler = c.requireConnectedUser(c.showCalendars) - case "availability": - handler = c.requireConnectedUser(c.requireAdminUser(c.debugAvailability)) case "settings": handler = c.requireConnectedUser(c.settings) + case "events": + handler = c.requireConnectedUser(c.event) + // Admin only + case "avail": + handler = c.requireConnectedUser(c.requireAdminUser(c.debugAvailability)) + case "subscribe": + handler = c.requireConnectedUser(c.requireAdminUser(c.subscribe)) + case "unsubscribe": + handler = c.requireConnectedUser(c.requireAdminUser(c.unsubscribe)) + // Aliases + case "today": + parameters = []string{"today"} + handler = c.requireConnectedUser(c.dailySummary) + case "tomorrow": + parameters = []string{"tomorrow"} + handler = c.requireConnectedUser(c.dailySummary) } out, mustRedirectToDM, err := handler(parameters...) if err != nil { - return out, false, errors.WithMessagef(err, "Command /%s %s failed", config.CommandTrigger, cmd) + return out, false, errors.WithMessagef(err, "Command /%s %s failed", config.Provider.CommandTrigger, cmd) } return out, mustRedirectToDM, nil @@ -126,7 +160,7 @@ func (c *Command) isValid() (subcommand string, parameters []string, err error) } split := strings.Fields(c.Args.Command) command := split[0] - if command != "/"+config.CommandTrigger { + if command != "/"+config.Provider.CommandTrigger { return "", nil, fmt.Errorf("%q is not a supported command. Please contact your system administrator", command) } @@ -142,8 +176,8 @@ func (c *Command) isValid() (subcommand string, parameters []string, err error) return subcommand, parameters, nil } -func (c *Command) user() *mscalendar.User { - return mscalendar.NewUser(c.Args.UserId) +func (c *Command) user() *engine.User { + return engine.NewUser(c.Args.UserId) } func (c *Command) requireConnectedUser(handle handleFunc) handleFunc { @@ -154,7 +188,7 @@ func (c *Command) requireConnectedUser(handle handleFunc) handleFunc { } if !connected { - return getNotConnectedText(), false, nil + return getNotConnectedText(c.Config.PluginURL), false, nil } return handle(parameters...) } @@ -162,7 +196,7 @@ func (c *Command) requireConnectedUser(handle handleFunc) handleFunc { func (c *Command) requireAdminUser(handle handleFunc) handleFunc { return func(parameters ...string) (string, bool, error) { - authorized, err := c.MSCalendar.IsAuthorizedAdmin(c.Args.UserId) + authorized, err := c.Engine.IsAuthorizedAdmin(c.Args.UserId) if err != nil { return "", false, err } @@ -175,7 +209,7 @@ func (c *Command) requireAdminUser(handle handleFunc) handleFunc { } func (c *Command) isConnected() (bool, error) { - _, err := c.MSCalendar.GetRemoteUser(c.Args.UserId) + _, err := c.Engine.GetRemoteUser(c.Args.UserId) if err == store.ErrNotFound { return false, nil } diff --git a/server/command/connect.go b/calendar/command/connect.go similarity index 69% rename from server/command/connect.go rename to calendar/command/connect.go index 781c5fe7..c633368f 100644 --- a/server/command/connect.go +++ b/calendar/command/connect.go @@ -6,7 +6,7 @@ package command import ( "fmt" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" ) const ( @@ -16,15 +16,15 @@ const ( ConnectErrorMessage = "There has been a problem while trying to connect. err=" ) -func (c *Command) connect(parameters ...string) (string, bool, error) { - ru, err := c.MSCalendar.GetRemoteUser(c.Args.UserId) +func (c *Command) connect(_ ...string) (string, bool, error) { + ru, err := c.Engine.GetRemoteUser(c.Args.UserId) if err == nil { - return fmt.Sprintf(ConnectAlreadyConnectedTemplate, config.ApplicationName, ru.Mail, config.CommandTrigger), false, nil + return fmt.Sprintf(ConnectAlreadyConnectedTemplate, config.Provider.DisplayName, ru.Mail, config.Provider.CommandTrigger), false, nil } out := "" - err = c.MSCalendar.Welcome(c.Args.UserId) + err = c.Engine.Welcome(c.Args.UserId) if err != nil { out = ConnectErrorMessage + err.Error() } diff --git a/server/command/connect_test.go b/calendar/command/connect_test.go similarity index 60% rename from server/command/connect_test.go rename to calendar/command/connect_test.go index eed0edcc..0c702262 100644 --- a/server/command/connect_test.go +++ b/calendar/command/connect_test.go @@ -1,6 +1,7 @@ package command import ( + "fmt" "testing" "github.com/golang/mock/gomock" @@ -9,35 +10,38 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/require" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/mock_mscalendar" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/mock_engine" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) func TestConnect(t *testing.T) { tcs := []struct { name string command string - setup func(m mscalendar.MSCalendar) + setup func(m engine.Engine) expectedOutput string expectedError string }{ { name: "user already connected", command: "connect", - setup: func(m mscalendar.MSCalendar) { - mscal := m.(*mock_mscalendar.MockMSCalendar) + setup: func(m engine.Engine) { + mscal := m.(*mock_engine.MockEngine) mscal.EXPECT().GetRemoteUser("user_id").Return(&remote.User{Mail: "user@email.com"}, nil).Times(1) }, - expectedOutput: "Your Mattermost account is already connected to Microsoft Calendar account `user@email.com`. To connect to a different account, first run `/mscalendar disconnect`.", - expectedError: "", + expectedOutput: fmt.Sprintf( + "Your Mattermost account is already connected to %s account `user@email.com`. To connect to a different account, first run `/%s disconnect`.", + config.Provider.DisplayName, config.Provider.CommandTrigger, + ), + expectedError: "", }, { name: "user not connected", command: "connect", - setup: func(m mscalendar.MSCalendar) { - mscal := m.(*mock_mscalendar.MockMSCalendar) + setup: func(m engine.Engine) { + mscal := m.(*mock_engine.MockEngine) mscal.EXPECT().GetRemoteUser("user_id").Return(nil, errors.New("remote user not found")).Times(1) mscal.EXPECT().Welcome("user_id").Return(nil) }, @@ -55,16 +59,16 @@ func TestConnect(t *testing.T) { PluginURL: "http://localhost", } - mscal := mock_mscalendar.NewMockMSCalendar(ctrl) + mscal := mock_engine.NewMockEngine(ctrl) command := Command{ Context: &plugin.Context{}, Args: &model.CommandArgs{ - Command: "/mscalendar " + tc.command, + Command: fmt.Sprintf("/%s %s", config.Provider.CommandTrigger, tc.command), UserId: "user_id", }, - ChannelID: "channel_id", - Config: conf, - MSCalendar: mscal, + ChannelID: "channel_id", + Config: conf, + Engine: mscal, } if tc.setup != nil { diff --git a/server/command/create_calendar.go b/calendar/command/create_calendar.go similarity index 73% rename from server/command/create_calendar.go rename to calendar/command/create_calendar.go index e4b7be61..c3aa854a 100644 --- a/server/command/create_calendar.go +++ b/calendar/command/create_calendar.go @@ -1,7 +1,7 @@ package command import ( - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) func (c *Command) createCalendar(parameters ...string) (string, bool, error) { @@ -13,7 +13,7 @@ func (c *Command) createCalendar(parameters ...string) (string, bool, error) { Name: parameters[0], } - _, err := c.MSCalendar.CreateCalendar(c.user(), calIn) + _, err := c.Engine.CreateCalendar(c.user(), calIn) if err != nil { return "", false, err } diff --git a/server/command/create_event.go b/calendar/command/create_event.go similarity index 94% rename from server/command/create_event.go rename to calendar/command/create_event.go index 7fd911af..211d0272 100644 --- a/server/command/create_event.go +++ b/calendar/command/create_event.go @@ -9,8 +9,8 @@ import ( "github.com/pkg/errors" flag "github.com/spf13/pflag" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" ) func getCreateEventFlagSet() *flag.FlagSet { @@ -33,7 +33,7 @@ func (c *Command) createEvent(parameters ...string) (string, bool, error) { return getCreateEventFlagSet().FlagUsages(), false, nil } - tz, err := c.MSCalendar.GetTimezone(c.user()) + tz, err := c.Engine.GetTimezone(c.user()) if err != nil { return "", false, nil } @@ -54,7 +54,7 @@ func (c *Command) createEvent(parameters ...string) (string, bool, error) { return "", false, err } - calEvent, err := c.MSCalendar.CreateEvent(c.user(), event, mattermostUserIDs) + calEvent, err := c.Engine.CreateEvent(c.user(), event, mattermostUserIDs) if err != nil { return "", false, err } diff --git a/calendar/command/daily_summary.go b/calendar/command/daily_summary.go new file mode 100644 index 00000000..6a03dec5 --- /dev/null +++ b/calendar/command/daily_summary.go @@ -0,0 +1,88 @@ +package command + +import ( + "fmt" + "time" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" +) + +func getDailySummaryHelp() string { + return "### Daily summary commands:\n" + + fmt.Sprintf("`/%s summary view` - View your daily summary\n", config.Provider.CommandTrigger) + + fmt.Sprintf("`/%s summary settings` - View your settings for the daily summary\n", config.Provider.CommandTrigger) + + fmt.Sprintf("`/%s summary time 8:00AM` - Set the time you would like to receive your daily summary\n", config.Provider.CommandTrigger) + + fmt.Sprintf("`/%s summary enable` - Enable your daily summary\n", config.Provider.CommandTrigger) + + fmt.Sprintf("`/%s summary disable` - Disable your daily summary", config.Provider.CommandTrigger) +} + +func getDailySummarySetTimeErrorMessage() string { + return fmt.Sprintf("Please enter a time, for example:\n`/%s summary time 8:00AM`", config.Provider.CommandTrigger) +} + +func (c *Command) dailySummary(parameters ...string) (string, bool, error) { + if len(parameters) == 0 { + return getDailySummaryHelp(), false, nil + } + + switch parameters[0] { + case "view", "today": + postStr, err := c.Engine.GetDaySummaryForUser(time.Now(), c.user()) + if err != nil { + return err.Error(), false, err + } + return postStr, false, nil + case "tomorrow": + postStr, err := c.Engine.GetDaySummaryForUser(time.Now().Add(time.Hour*24), c.user()) + if err != nil { + return err.Error(), false, err + } + return postStr, false, nil + case "time": + if len(parameters) != 2 { + return getDailySummarySetTimeErrorMessage(), false, nil + } + val := parameters[1] + + dsum, err := c.Engine.SetDailySummaryPostTime(c.user(), val) + if err != nil { + return err.Error() + "\n" + getDailySummarySetTimeErrorMessage(), false, nil + } + + return dailySummaryResponse(dsum), false, nil + case "settings": + dsum, err := c.Engine.GetDailySummarySettingsForUser(c.user()) + if err != nil { + return err.Error() + "\nYou may need to configure your daily summary using the commands below.\n" + getDailySummaryHelp(), false, nil + } + + return dailySummaryResponse(dsum), false, nil + case "enable": + dsum, err := c.Engine.SetDailySummaryEnabled(c.user(), true) + if err != nil { + return err.Error(), false, err + } + + return dailySummaryResponse(dsum), false, nil + case "disable": + dsum, err := c.Engine.SetDailySummaryEnabled(c.user(), false) + if err != nil { + return err.Error(), false, err + } + return dailySummaryResponse(dsum), false, nil + } + return "Invalid command. Please try again\n\n" + getDailySummaryHelp(), false, nil +} + +func dailySummaryResponse(dsum *store.DailySummaryUserSettings) string { + if dsum.PostTime == "" { + return "Your daily summary time is not yet configured.\n" + getDailySummarySetTimeErrorMessage() + } + + enableStr := "" + if !dsum.Enable { + enableStr = fmt.Sprintf(", but is disabled. Enable it with `/%s summary enable`", config.Provider.CommandTrigger) + } + return fmt.Sprintf("Your daily summary is configured to show at %s %s%s.", dsum.PostTime, dsum.Timezone, enableStr) +} diff --git a/server/command/delete_calendar.go b/calendar/command/delete_calendar.go similarity index 81% rename from server/command/delete_calendar.go rename to calendar/command/delete_calendar.go index ad532898..94f6ee3b 100644 --- a/server/command/delete_calendar.go +++ b/calendar/command/delete_calendar.go @@ -5,7 +5,7 @@ func (c *Command) deleteCalendar(parameters ...string) (string, bool, error) { return "Please provide the ID of only one calendar ", false, nil } - err := c.MSCalendar.DeleteCalendar(c.user(), parameters[0]) + err := c.Engine.DeleteCalendar(c.user(), parameters[0]) if err != nil { return "", false, err } diff --git a/server/command/disconnect.go b/calendar/command/disconnect.go similarity index 57% rename from server/command/disconnect.go rename to calendar/command/disconnect.go index b350d8f3..a25cf912 100644 --- a/server/command/disconnect.go +++ b/calendar/command/disconnect.go @@ -3,12 +3,12 @@ package command -func (c *Command) disconnect(parameters ...string) (string, bool, error) { - err := c.MSCalendar.DisconnectUser(c.Args.UserId) +func (c *Command) disconnect(_ ...string) (string, bool, error) { + err := c.Engine.DisconnectUser(c.Args.UserId) if err != nil { return "", false, err } - c.MSCalendar.ClearSettingsPosts(c.Args.UserId) + c.Engine.ClearSettingsPosts(c.Args.UserId) return "Successfully disconnected your account", false, nil } diff --git a/server/command/disconnect_test.go b/calendar/command/disconnect_test.go similarity index 63% rename from server/command/disconnect_test.go rename to calendar/command/disconnect_test.go index 864debfd..914c912e 100644 --- a/server/command/disconnect_test.go +++ b/calendar/command/disconnect_test.go @@ -1,6 +1,7 @@ package command import ( + "fmt" "testing" "github.com/golang/mock/gomock" @@ -9,57 +10,57 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/require" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/mock_mscalendar" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/mock_engine" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" ) func TestDisconnect(t *testing.T) { tcs := []struct { name string command string - setup func(mscalendar.MSCalendar) + setup func(engine.Engine) expectedOutput string expectedError string }{ { name: "user not connected", command: "disconnect", - setup: func(m mscalendar.MSCalendar) { - mscal := m.(*mock_mscalendar.MockMSCalendar) + setup: func(m engine.Engine) { + mscal := m.(*mock_engine.MockEngine) mscal.EXPECT().GetRemoteUser("user_id").Return(&remote.User{}, store.ErrNotFound).Times(1) }, - expectedOutput: getNotConnectedText(), + expectedOutput: getNotConnectedText("http://localhost"), expectedError: "", }, { name: "error fetching user", command: "disconnect", - setup: func(m mscalendar.MSCalendar) { - mscal := m.(*mock_mscalendar.MockMSCalendar) + setup: func(m engine.Engine) { + mscal := m.(*mock_engine.MockEngine) mscal.EXPECT().GetRemoteUser("user_id").Return(&remote.User{}, errors.New("some error")).Times(1) }, expectedOutput: "", - expectedError: "Command /mscalendar disconnect failed: some error", + expectedError: fmt.Sprintf("Command /%s disconnect failed: some error", config.Provider.CommandTrigger), }, { name: "disconnect failed", command: "disconnect", - setup: func(m mscalendar.MSCalendar) { - mscal := m.(*mock_mscalendar.MockMSCalendar) + setup: func(m engine.Engine) { + mscal := m.(*mock_engine.MockEngine) mscal.EXPECT().GetRemoteUser("user_id").Return(&remote.User{}, nil).Times(1) mscal.EXPECT().DisconnectUser("user_id").Return(errors.New("some error")).Times(1) }, expectedOutput: "", - expectedError: "Command /mscalendar disconnect failed: some error", + expectedError: fmt.Sprintf("Command /%s disconnect failed: some error", config.Provider.CommandTrigger), }, { name: "disconnect successful", command: "disconnect", - setup: func(m mscalendar.MSCalendar) { - mscal := m.(*mock_mscalendar.MockMSCalendar) + setup: func(m engine.Engine) { + mscal := m.(*mock_engine.MockEngine) mscal.EXPECT().GetRemoteUser("user_id").Return(&remote.User{}, nil).Times(1) mscal.EXPECT().DisconnectUser("user_id").Return(nil).Times(1) mscal.EXPECT().ClearSettingsPosts("user_id").Return().Times(1) @@ -78,16 +79,16 @@ func TestDisconnect(t *testing.T) { PluginURL: "http://localhost", } - mscal := mock_mscalendar.NewMockMSCalendar(ctrl) + mscal := mock_engine.NewMockEngine(ctrl) command := Command{ Context: &plugin.Context{}, Args: &model.CommandArgs{ - Command: "/mscalendar " + tc.command, + Command: fmt.Sprintf("/%s %s", config.Provider.CommandTrigger, tc.command), UserId: "user_id", }, - ChannelID: "channel_id", - Config: conf, - MSCalendar: mscal, + ChannelID: "channel_id", + Config: conf, + Engine: mscal, } if tc.setup != nil { diff --git a/calendar/command/event.go b/calendar/command/event.go new file mode 100644 index 00000000..f7083b67 --- /dev/null +++ b/calendar/command/event.go @@ -0,0 +1,16 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package command + +func (c *Command) event(parameters ...string) (string, bool, error) { + if len(parameters) == 0 { + return getDailySummaryHelp(), false, nil + } + + if parameters[0] == "create" { + return "Creating events is only supported on desktop.", false, nil + } + + return "", false, nil +} diff --git a/server/command/find_meeting_times.go b/calendar/command/find_meeting_times.go similarity index 72% rename from server/command/find_meeting_times.go rename to calendar/command/find_meeting_times.go index 292d4a82..abafd19b 100644 --- a/server/command/find_meeting_times.go +++ b/calendar/command/find_meeting_times.go @@ -7,16 +7,19 @@ import ( "fmt" "strings" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" ) func (c *Command) findMeetings(parameters ...string) (string, bool, error) { meetingParams := &remote.FindMeetingTimesParameters{} var attendees []remote.Attendee - for a := range parameters { - s := strings.Split(parameters[a], ":") + for _, parameter := range parameters { + s := strings.Split(parameter, ":") + if len(s) != 2 { + return "", false, fmt.Errorf("error in parameter %s", parameter) + } t, email := s[0], s[1] attendee := remote.Attendee{ Type: t, @@ -28,12 +31,12 @@ func (c *Command) findMeetings(parameters ...string) (string, bool, error) { } meetingParams.Attendees = attendees - meetings, err := c.MSCalendar.FindMeetingTimes(c.user(), meetingParams) + meetings, err := c.Engine.FindMeetingTimes(c.user(), meetingParams) if err != nil { return "", false, err } - timeZone, _ := c.MSCalendar.GetTimezone(c.user()) + timeZone, _ := c.Engine.GetTimezone(c.user()) resp := "" for _, m := range meetings.MeetingTimeSuggestions { if timeZone != "" { diff --git a/calendar/command/get_calendars.go b/calendar/command/get_calendars.go new file mode 100644 index 00000000..01179952 --- /dev/null +++ b/calendar/command/get_calendars.go @@ -0,0 +1,13 @@ +package command + +import ( + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" +) + +func (c *Command) showCalendars(_ ...string) (string, bool, error) { + resp, err := c.Engine.GetCalendars(c.user()) + if err != nil { + return "", false, err + } + return utils.JSONBlock(resp), false, nil +} diff --git a/server/command/help.go b/calendar/command/help.go similarity index 65% rename from server/command/help.go rename to calendar/command/help.go index 9a3ab8a3..efaee10b 100644 --- a/server/command/help.go +++ b/calendar/command/help.go @@ -6,10 +6,10 @@ package command import ( "fmt" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" ) -func (c *Command) help(parameters ...string) (string, bool, error) { +func (c *Command) help(_ ...string) (string, bool, error) { resp := "" for _, cmd := range cmds { desc := cmd.Trigger @@ -18,9 +18,10 @@ func (c *Command) help(parameters ...string) (string, bool, error) { } resp += getCommandText(desc) } + return resp, false, nil } func getCommandText(s string) string { - return fmt.Sprintf("/%s %s\n", config.CommandTrigger, s) + return fmt.Sprintf("/%s %s\n", config.Provider.CommandTrigger, s) } diff --git a/server/command/info.go b/calendar/command/info.go similarity index 57% rename from server/command/info.go rename to calendar/command/info.go index c0fde2b2..72942be0 100644 --- a/server/command/info.go +++ b/calendar/command/info.go @@ -6,15 +6,16 @@ package command import ( "fmt" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" ) -func (c *Command) info(parameters ...string) (string, bool, error) { - resp := fmt.Sprintf("Mattermost Microsoft Calendar plugin version: %s, "+ +func (c *Command) info(_ ...string) (string, bool, error) { + resp := fmt.Sprintf("Mattermost %s plugin version: %s, "+ "[%s](https://github.com/mattermost/%s/commit/%s), built %s\n", + c.Config.Provider.DisplayName, c.Config.PluginVersion, c.Config.BuildHashShort, - config.Repository, + config.Provider.Repository, c.Config.BuildHash, c.Config.BuildDate) return resp, false, nil diff --git a/server/command/settings.go b/calendar/command/settings.go similarity index 56% rename from server/command/settings.go rename to calendar/command/settings.go index 7ee56d15..29250122 100644 --- a/server/command/settings.go +++ b/calendar/command/settings.go @@ -3,7 +3,7 @@ package command -func (c *Command) settings(parameters ...string) (string, bool, error) { - c.MSCalendar.PrintSettings(c.Args.UserId) +func (c *Command) settings(_ ...string) (string, bool, error) { + c.Engine.PrintSettings(c.Args.UserId) return "", true, nil } diff --git a/server/command/subscribe.go b/calendar/command/subscribe.go similarity index 74% rename from server/command/subscribe.go rename to calendar/command/subscribe.go index bc7b8fbf..7b09ca45 100644 --- a/server/command/subscribe.go +++ b/calendar/command/subscribe.go @@ -6,7 +6,7 @@ package command import ( "fmt" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" ) func (c *Command) subscribe(parameters ...string) (string, bool, error) { @@ -14,12 +14,12 @@ func (c *Command) subscribe(parameters ...string) (string, bool, error) { return c.debugList() } - _, err := c.MSCalendar.LoadMyEventSubscription() + _, err := c.Engine.LoadMyEventSubscription() if err == nil { return "You are already subscribed to events.", false, nil } - _, err = c.MSCalendar.CreateMyEventSubscription() + _, err = c.Engine.CreateMyEventSubscription() if err != nil { return "", false, err } @@ -27,7 +27,7 @@ func (c *Command) subscribe(parameters ...string) (string, bool, error) { } func (c *Command) debugList() (string, bool, error) { - subs, err := c.MSCalendar.ListRemoteSubscriptions() + subs, err := c.Engine.ListRemoteSubscriptions() if err != nil { return "", false, err } diff --git a/server/command/unsubscribe.go b/calendar/command/unsubscribe.go similarity index 63% rename from server/command/unsubscribe.go rename to calendar/command/unsubscribe.go index 346e210a..d2b6fd8f 100644 --- a/server/command/unsubscribe.go +++ b/calendar/command/unsubscribe.go @@ -3,13 +3,13 @@ package command -func (c *Command) unsubscribe(parameters ...string) (string, bool, error) { - _, err := c.MSCalendar.LoadMyEventSubscription() +func (c *Command) unsubscribe(_ ...string) (string, bool, error) { + _, err := c.Engine.LoadMyEventSubscription() if err != nil { return "You are not subscribed to events.", false, nil } - err = c.MSCalendar.DeleteMyEventSubscription() + err = c.Engine.DeleteMyEventSubscription() if err != nil { return "", false, err } diff --git a/server/command/view_calendar.go b/calendar/command/view_calendar.go similarity index 51% rename from server/command/view_calendar.go rename to calendar/command/view_calendar.go index d6d88ab7..731ea6be 100644 --- a/server/command/view_calendar.go +++ b/calendar/command/view_calendar.go @@ -6,16 +6,16 @@ package command import ( "time" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/views" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/views" ) -func (c *Command) viewCalendar(parameters ...string) (string, bool, error) { - tz, err := c.MSCalendar.GetTimezone(c.user()) +func (c *Command) viewCalendar(_ ...string) (string, bool, error) { + tz, err := c.Engine.GetTimezone(c.user()) if err != nil { return "Error: No timezone found", false, err } - events, err := c.MSCalendar.ViewCalendar(c.user(), time.Now().Add(-24*time.Hour), time.Now().Add(14*24*time.Hour)) + events, err := c.Engine.ViewCalendar(c.user(), time.Now().Add(-24*time.Hour), time.Now().Add(14*24*time.Hour)) if err != nil { return "", false, err } diff --git a/calendar/config/config.go b/calendar/config/config.go new file mode 100644 index 00000000..844e82db --- /dev/null +++ b/calendar/config/config.go @@ -0,0 +1,56 @@ +package config + +import "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + +var Provider ProviderConfig + +// StoredConfig represents the data stored in and managed with the Mattermost +// config. +type StoredConfig struct { + OAuth2Authority string + OAuth2ClientID string + OAuth2ClientSecret string + bot.Config + EnableStatusSync bool + EnableDailySummary bool + + EncryptionKey string +} + +type ProviderFeatures struct { + EncryptedStore bool + EventNotifications bool +} + +// ProviderConfig represents the specific configuration that changes when building for different +// calendar providers. +type ProviderConfig struct { + Name string + DisplayName string + Repository string + CommandTrigger string + TelemetryShortName string + BotUsername string + BotDisplayName string + Features ProviderFeatures +} + +// Config represents the the metadata handed to all request runners (command, +// http). +type Config struct { + PluginID string + BuildDate string + BuildHash string + BuildHashShort string + MattermostSiteHostname string + MattermostSiteURL string + PluginURL string + PluginURLPath string + PluginVersion string + StoredConfig + Provider ProviderConfig +} + +func (c *Config) GetNotificationURL() string { + return c.PluginURL + FullPathEventNotification +} diff --git a/calendar/config/const.go b/calendar/config/const.go new file mode 100644 index 00000000..61444dd5 --- /dev/null +++ b/calendar/config/const.go @@ -0,0 +1,38 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package config + +const ( + BotDescription = "Created by the %s Plugin." + + PathOAuth2 = "/oauth2" + PathComplete = "/complete" + PathAPI = "/api/v1" + PathDialogs = "/dialogs" + PathSetAutoRespondMessage = "/set-auto-respond-message" + PathPostAction = "/action" + PathRespond = "/respond" + PathAccept = "/accept" + PathDecline = "/decline" + PathTentative = "/tentative" + PathConfirmStatusChange = "/confirm" + PathNotification = "/notification/v1" + PathEvent = "/event" + PathVerifyDomain = "/verify" + + PathAutocomplete = "/autocomplete" + PathUsers = "/users" + PathChannels = "/channels" + + InternalAPIPath = "/api/v1" + PathEvents = "/events" + PathCreate = "/create" + PathProvider = "/provider" + PathConnectedUser = "/me" + + FullPathEventNotification = PathNotification + PathEvent + FullPathOAuth2Redirect = PathOAuth2 + PathComplete + + EventIDKey = "EventID" +) diff --git a/server/mscalendar/availability.go b/calendar/engine/availability.go similarity index 69% rename from server/mscalendar/availability.go rename to calendar/engine/availability.go index 1bc0adae..692adc69 100644 --- a/server/mscalendar/availability.go +++ b/calendar/engine/availability.go @@ -1,7 +1,7 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "fmt" @@ -10,22 +10,28 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/views" - "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/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/views" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) const ( - calendarViewTimeWindowSize = 10 * time.Minute - StatusSyncJobInterval = 5 * time.Minute - upcomingEventNotificationTime = 10 * time.Minute + calendarViewTimeWindowSize = 10 * time.Minute + StatusSyncJobInterval = 5 * time.Minute + upcomingEventNotificationTime = 10 * time.Minute + upcomingEventNotificationWindow = (StatusSyncJobInterval * 11) / 10 // 110% of the interval logTruncateMsg = "We've truncated the logs due to too many messages" logTruncateLimit = 5 ) +var ( + errNoUsersNeedToBeSynced = errors.New("no users need to be synced") +) + type StatusSyncJobSummary struct { NumberOfUsersFailedStatusChanged int NumberOfUsersStatusChanged int @@ -44,15 +50,17 @@ func (m *mscalendar) Sync(mattermostUserID string) (string, *StatusSyncJobSummar return "", nil, err } - return m.syncUsers(store.UserIndex{user}) -} + userIndex := store.UserIndex{user} -func (m *mscalendar) SyncAll() (string, *StatusSyncJobSummary, error) { - err := m.Filter(withSuperuserClient) - if err != nil { + err = m.Filter(withSuperuserClient) + if err != nil && !errors.Is(err, remote.ErrSuperUserClientNotSupported) { return "", &StatusSyncJobSummary{}, errors.Wrap(err, "not able to filter the super user client") } + return m.syncUsers(userIndex, errors.Is(err, remote.ErrSuperUserClientNotSupported)) +} + +func (m *mscalendar) SyncAll() (string, *StatusSyncJobSummary, error) { userIndex, err := m.Store.LoadUserIndex() if err != nil { if err.Error() == "not found" { @@ -61,18 +69,29 @@ func (m *mscalendar) SyncAll() (string, *StatusSyncJobSummary, error) { return "", &StatusSyncJobSummary{}, errors.Wrap(err, "not able to load the users from user index") } - return m.syncUsers(userIndex) -} + err = m.Filter(withSuperuserClient) + if err != nil && !errors.Is(err, remote.ErrSuperUserClientNotSupported) { + return "", &StatusSyncJobSummary{}, errors.Wrap(err, "not able to filter the super user client") + } -func (m *mscalendar) syncUsers(userIndex store.UserIndex) (string, *StatusSyncJobSummary, error) { - syncJobSummary := &StatusSyncJobSummary{} - if len(userIndex) == 0 { - return "No connected users found", syncJobSummary, nil + result, jobSummary, err := m.syncUsers(userIndex, errors.Is(err, remote.ErrSuperUserClientNotSupported)) + if result != "" && err != nil { + return result, jobSummary, nil } - syncJobSummary.NumberOfUsersProcessed = len(userIndex) + + return result, jobSummary, err +} + +// retrieveUsersToSync retrieves the users and their calendar data to sync up and send notifications +// The parameter fetchIndividually determines if the calendar data should be fetched while we loop the +// users (using individual credentials) or on a batch after the loop. +func (m *mscalendar) retrieveUsersToSync(userIndex store.UserIndex, syncJobSummary *StatusSyncJobSummary, fetchIndividually bool) ([]*store.User, []*remote.ViewCalendarResponse, error) { + start := time.Now().UTC() + end := time.Now().UTC().Add(calendarViewTimeWindowSize) numberOfLogs := 0 users := []*store.User{} + calendarViews := []*remote.ViewCalendarResponse{} for _, u := range userIndex { // TODO fetch users from kvstore in batches, and process in batches instead of all at once user, err := m.Store.LoadUser(u.MattermostUserID) @@ -88,23 +107,68 @@ func (m *mscalendar) syncUsers(userIndex store.UserIndex) (string, *StatusSyncJo // In case of error in loading, skip this user and continue with the next user continue } - if user.Settings.UpdateStatus || user.Settings.ReceiveReminders { - users = append(users, user) + + // If user does not have the proper features enabled, just go to the next one + if !(user.Settings.UpdateStatus || user.Settings.ReceiveReminders) { + continue + } + + if fetchIndividually { + engine, err := m.FilterCopy(withActingUser(user.MattermostUserID)) + if err != nil { + m.Logger.Warnf("Not able to enable active user %s from user index. err=%v", user.MattermostUserID, err) + continue + } + + calendarUser := newUserFromStoredUser(user) + calendarEvents, err := engine.GetCalendarEvents(calendarUser, start, end, true) + if err != nil { + syncJobSummary.NumberOfUsersFailedStatusChanged++ + m.Logger.With(bot.LogContext{ + "user": u.MattermostUserID, + "err": err, + }).Errorf("error getting calendar events") + continue + } + + calendarViews = append(calendarViews, calendarEvents) } + + users = append(users, user) } + if len(users) == 0 { - return "No users need to be synced", syncJobSummary, nil + return users, calendarViews, errNoUsersNeedToBeSynced } - calendarViews, err := m.GetCalendarViews(users) - if err != nil { - return "", syncJobSummary, errors.Wrap(err, "not able to get calendar views for connected users") + if !fetchIndividually { + var err error + calendarViews, err = m.GetCalendarViews(users) + if err != nil { + return users, calendarViews, errors.Wrap(err, "not able to get calendar views for connected users") + } } + if len(calendarViews) == 0 { - return "No calendar views found", syncJobSummary, nil + return users, calendarViews, fmt.Errorf("no calendar views found") + } + + return users, calendarViews, nil +} + +func (m *mscalendar) syncUsers(userIndex store.UserIndex, fetchIndividually bool) (string, *StatusSyncJobSummary, error) { + syncJobSummary := &StatusSyncJobSummary{} + if len(userIndex) == 0 { + return "No connected users found", syncJobSummary, nil + } + syncJobSummary.NumberOfUsersProcessed = len(userIndex) + + users, calendarViews, err := m.retrieveUsersToSync(userIndex, syncJobSummary, fetchIndividually) + if err != nil { + return err.Error(), syncJobSummary, errors.Wrapf(err, "error retrieving users to sync (individually=%v)", fetchIndividually) } - m.deliverReminders(users, calendarViews) + m.deliverReminders(users, calendarViews, fetchIndividually) out, numberOfUsersStatusChanged, numberOfUsersFailedStatusChanged, err := m.setUserStatuses(users, calendarViews) if err != nil { return "", syncJobSummary, errors.Wrap(err, "error setting the user statuses") @@ -116,7 +180,7 @@ func (m *mscalendar) syncUsers(userIndex store.UserIndex) (string, *StatusSyncJo return out, syncJobSummary, nil } -func (m *mscalendar) deliverReminders(users []*store.User, calendarViews []*remote.ViewCalendarResponse) { +func (m *mscalendar) deliverReminders(users []*store.User, calendarViews []*remote.ViewCalendarResponse, fetchIndividually bool) { numberOfLogs := 0 toNotify := []*store.User{} for _, u := range users { @@ -149,7 +213,16 @@ func (m *mscalendar) deliverReminders(users []*store.User, calendarViews []*remo } mattermostUserID := usersByRemoteID[view.RemoteUserID].MattermostUserID - m.notifyUpcomingEvents(mattermostUserID, view.Events) + if fetchIndividually { + engine, err := m.FilterCopy(withActingUser(user.MattermostUserID)) + if err != nil { + m.Logger.With(bot.LogContext{"err": err}).Errorf("error getting engine for user") + continue + } + engine.notifyUpcomingEvents(mattermostUserID, view.Events) + } else { + m.notifyUpcomingEvents(mattermostUserID, view.Events) + } } } @@ -383,6 +456,27 @@ func (m *mscalendar) setStatusOrAskUser(user *store.User, currentStatus *model.S return nil } +func (m *mscalendar) GetCalendarEvents(user *User, start, end time.Time, excludeDeclined bool) (*remote.ViewCalendarResponse, error) { + err := m.Filter(withClient) + if err != nil { + return nil, errors.Wrap(err, "errror withClient") + } + + events, err := m.client.GetEventsBetweenDates(user.Remote.ID, start, end) + if err != nil { + return nil, errors.Wrapf(err, "error getting events for user %s", user.MattermostUserID) + } + + if excludeDeclined { + events = m.excludeDeclinedEvents(events) + } + + return &remote.ViewCalendarResponse{ + RemoteUserID: user.Remote.ID, + Events: events, + }, nil +} + func (m *mscalendar) GetCalendarViews(users []*store.User) ([]*remote.ViewCalendarResponse, error) { err := m.Filter(withClient) if err != nil { @@ -424,16 +518,47 @@ func (m *mscalendar) notifyUpcomingEvents(mattermostUserID string, events []*rem } } - message, err := views.RenderUpcomingEvent(event, timezone) + _, attachment, err := views.RenderUpcomingEventAsAttachment(event, timezone) if err != nil { m.Logger.Warnf("notifyUpcomingEvent error rendering schedule item. err=%v", err) continue } - _, err = m.Poster.DM(mattermostUserID, message) + + _, err = m.Poster.DMWithAttachments(mattermostUserID, attachment) if err != nil { m.Logger.Warnf("notifyUpcomingEvents error creating DM. err=%v", err) continue } + + // Process channel reminders + eventMetadata, errMetadata := m.Store.LoadEventMetadata(event.ICalUID) + if errMetadata != nil && !errors.Is(errMetadata, store.ErrNotFound) { + m.Logger.With(bot.LogContext{ + "eventID": event.ID, + "err": errMetadata.Error(), + }).Warnf("notifyUpcomingEvents error checking store for channel notifications") + continue + } + + if eventMetadata != nil { + for channelID := range eventMetadata.LinkedChannelIDs { + post := &model.Post{ + ChannelId: channelID, + Message: "Upcoming event", + } + attachment, errRender := views.RenderEventAsAttachment(event, timezone, views.ShowTimezoneOption(timezone)) + if errRender != nil { + m.Logger.With(bot.LogContext{"err": errRender}).Errorf("notifyUpcomingEvents error rendering channel post") + continue + } + model.ParseSlackAttachment(post, []*model.SlackAttachment{attachment}) + errPoster := m.Poster.CreatePost(post) + if errPoster != nil { + m.Logger.With(bot.LogContext{"err": errPoster}).Warnf("notifyUpcomingEvents error creating post in channel") + continue + } + } + } } } } diff --git a/server/mscalendar/availability_test.go b/calendar/engine/availability_test.go similarity index 52% rename from server/mscalendar/availability_test.go rename to calendar/engine/availability_test.go index f21e83ca..e344cc0d 100644 --- a/server/mscalendar/availability_test.go +++ b/calendar/engine/availability_test.go @@ -1,7 +1,7 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "context" @@ -12,13 +12,14 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/stretchr/testify/require" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/mock_plugin_api" - "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/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/mock_plugin_api" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote/mock_remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store/mock_store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot/mock_bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/test" ) func TestSyncStatusAll(t *testing.T) { @@ -116,7 +117,15 @@ func TestSyncStatusAll(t *testing.T) { env, client := makeStatusSyncTestEnv(ctrl) deps := env.Dependencies - c, papi, s, logger := client.(*mock_remote.MockClient), deps.PluginAPI.(*mock_plugin_api.MockPluginAPI), deps.Store.(*mock_store.MockStore), deps.Logger.(*mock_bot.MockLogger) + c, r, papi, s, logger := client.(*mock_remote.MockClient), env.Remote.(*mock_remote.MockRemote), deps.PluginAPI.(*mock_plugin_api.MockPluginAPI), deps.Store.(*mock_store.MockStore), deps.Logger.(*mock_bot.MockLogger) + s.EXPECT().LoadUserIndex().Return(store.UserIndex{ + &store.UserShort{ + MattermostUserID: "user_mm_id", + RemoteID: "user_remote_id", + Email: "user_email@example.com", + }, + }, nil).Times(1) + r.EXPECT().MakeSuperuserClient(context.Background()).Return(client, nil) mockUser := &store.User{ MattermostUserID: "user_mm_id", @@ -178,8 +187,9 @@ func TestSyncStatusUserConfig(t *testing.T) { UpdateStatus: false, }, runAssertions: func(deps *Dependencies, client remote.Client) { - c := client.(*mock_remote.MockClient) + c, r := client.(*mock_remote.MockClient), deps.Remote.(*mock_remote.MockRemote) c.EXPECT().DoBatchViewCalendarRequests(gomock.Any()).Times(0) + r.EXPECT().MakeSuperuserClient(gomock.Any()) }, }, "UpdateStatus enabled and GetConfirmation enabled": { @@ -188,10 +198,10 @@ func TestSyncStatusUserConfig(t *testing.T) { GetConfirmation: true, }, runAssertions: func(deps *Dependencies, client remote.Client) { - c, papi, poster, s := client.(*mock_remote.MockClient), deps.PluginAPI.(*mock_plugin_api.MockPluginAPI), deps.Poster.(*mock_bot.MockPoster), deps.Store.(*mock_store.MockStore) + c, r, papi, poster, s := client.(*mock_remote.MockClient), deps.Remote.(*mock_remote.MockRemote), deps.PluginAPI.(*mock_plugin_api.MockPluginAPI), deps.Poster.(*mock_bot.MockPoster), deps.Store.(*mock_store.MockStore) + r.EXPECT().MakeSuperuserClient(context.Background()).Return(client, nil) moment := time.Now().UTC() busyEvent := &remote.Event{ICalUID: "event_id", Start: remote.NewDateTime(moment, "UTC"), ShowAs: "busy"} - c.EXPECT().DoBatchViewCalendarRequests(gomock.Any()).Times(1).Return([]*remote.ViewCalendarResponse{ {Events: []*remote.Event{busyEvent}, RemoteUserID: "user_remote_id"}, }, nil) @@ -211,6 +221,13 @@ func TestSyncStatusUserConfig(t *testing.T) { env, client := makeStatusSyncTestEnv(ctrl) s := env.Dependencies.Store.(*mock_store.MockStore) + s.EXPECT().LoadUserIndex().Return(store.UserIndex{ + &store.UserShort{ + MattermostUserID: "user_mm_id", + RemoteID: "user_remote_id", + Email: "user_email@example.com", + }, + }, nil).Times(1) s.EXPECT().LoadUser("user_mm_id").Return(&store.User{ MattermostUserID: "user_mm_id", Remote: &remote.User{ @@ -233,6 +250,7 @@ func TestReminders(t *testing.T) { for name, tc := range map[string]struct { apiError *remote.APIError remoteEvents []*remote.Event + eventMetadata map[string]*store.EventMetadata numReminders int shouldLogError bool }{ @@ -277,6 +295,30 @@ func TestReminders(t *testing.T) { numReminders: 2, shouldLogError: false, }, + "Remote event linked to channel in the range for the reminder. DM and channel reminders should occur.": { + remoteEvents: []*remote.Event{ + {ID: "event_id_1", ICalUID: "event_id_1", Start: remote.NewDateTime(time.Now().Add(7*time.Minute).UTC(), "UTC"), End: remote.NewDateTime(time.Now().Add(45*time.Minute).UTC(), "UTC")}, + }, + eventMetadata: map[string]*store.EventMetadata{ + "event_id_1": { + LinkedChannelIDs: map[string]struct{}{"some_channel_id": {}}, + }, + }, + numReminders: 1, + shouldLogError: false, + }, + "Remote recurring event linked to channel in the range for the reminder. DM and channel reminders should occur.": { + remoteEvents: []*remote.Event{ + {ID: "event_id_1_recurring", ICalUID: "event_id_1", Start: remote.NewDateTime(time.Now().Add(7*time.Minute).UTC(), "UTC"), End: remote.NewDateTime(time.Now().Add(45*time.Minute).UTC(), "UTC")}, + }, + eventMetadata: map[string]*store.EventMetadata{ + "event_id_1": { + LinkedChannelIDs: map[string]struct{}{"channel_id": {}}, + }, + }, + numReminders: 1, + shouldLogError: false, + }, "Remote API Error. Error should be logged.": { remoteEvents: []*remote.Event{}, numReminders: 0, @@ -291,7 +333,15 @@ func TestReminders(t *testing.T) { env, client := makeStatusSyncTestEnv(ctrl) deps := env.Dependencies - c, s, poster, logger := client.(*mock_remote.MockClient), deps.Store.(*mock_store.MockStore), deps.Poster.(*mock_bot.MockPoster), deps.Logger.(*mock_bot.MockLogger) + c, r, poster, s, logger := client.(*mock_remote.MockClient), env.Remote.(*mock_remote.MockRemote), deps.Poster.(*mock_bot.MockPoster), deps.Store.(*mock_store.MockStore), deps.Logger.(*mock_bot.MockLogger) + s.EXPECT().LoadUserIndex().Return(store.UserIndex{ + &store.UserShort{ + MattermostUserID: "user_mm_id", + RemoteID: "user_remote_id", + Email: "user_email@example.com", + }, + }, nil).Times(1) + r.EXPECT().MakeSuperuserClient(context.Background()).Return(client, nil) loadUser := s.EXPECT().LoadUser("user_mm_id").Return(&store.User{ MattermostUserID: "user_mm_id", @@ -306,9 +356,20 @@ func TestReminders(t *testing.T) { }, nil) if tc.numReminders > 0 { - poster.EXPECT().DM("user_mm_id", gomock.Any()).Times(tc.numReminders) + poster.EXPECT().DMWithAttachments("user_mm_id", gomock.Any()).Times(tc.numReminders) loadUser.Times(2) c.EXPECT().GetMailboxSettings("user_remote_id").Times(1).Return(&remote.MailboxSettings{TimeZone: "UTC"}, nil) + + // Metadata (linked channels test) + for eventID, metadata := range tc.eventMetadata { + s.EXPECT().LoadEventMetadata(eventID).Return(metadata, nil).Times(1) + for channelID := range metadata.LinkedChannelIDs { + poster.EXPECT().CreatePost(test.DoMatch(func(v *model.Post) bool { + return v.ChannelId == channelID + })).Return(nil) + } + } + s.EXPECT().LoadEventMetadata(gomock.Any()).Return(nil, store.ErrNotFound).Times(tc.numReminders - len(tc.eventMetadata)) } else { poster.EXPECT().DM(gomock.Any(), gomock.Any()).Times(0) loadUser.Times(1) @@ -328,6 +389,185 @@ func TestReminders(t *testing.T) { } } +func TestRetrieveUsersToSyncIndividually(t *testing.T) { + t.Run("no users to sync", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + env, _ := makeStatusSyncTestEnv(ctrl) + + m := New(env, "").(*mscalendar) + jobSummary := &StatusSyncJobSummary{} + + _, _, err := m.retrieveUsersToSync([]*store.UserShort{}, jobSummary, true) + require.ErrorIs(t, errNoUsersNeedToBeSynced, err) + }) + + t.Run("user reminders and status disabled", func(t *testing.T) { + testUser := newTestUser() + testUser.Settings.UpdateStatus = false + testUser.Settings.ReceiveReminders = false + + userIndex := []*store.UserShort{ + { + MattermostUserID: testUser.MattermostUserID, + RemoteID: testUser.Remote.ID, + Email: testUser.Remote.Mail, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + e, _ := makeStatusSyncTestEnv(ctrl) + + s := e.Store.(*mock_store.MockStore) + s.EXPECT().LoadUser(testUser.MattermostUserID).Return(testUser, nil) + + m := New(e, "").(*mscalendar) + jobSummary := &StatusSyncJobSummary{} + + _, _, err := m.retrieveUsersToSync(userIndex, jobSummary, true) + require.ErrorIs(t, err, errNoUsersNeedToBeSynced) + }) + + t.Run("one user should be synced", func(t *testing.T) { + testUser := newTestUser() + testUser.Settings.UpdateStatus = true + testUser.Settings.ReceiveReminders = true + + userIndex := []*store.UserShort{ + { + MattermostUserID: testUser.MattermostUserID, + RemoteID: testUser.Remote.ID, + Email: testUser.Remote.Mail, + }, + } + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + e, client := makeStatusSyncTestEnv(ctrl) + + c, r, _, s, papi := client.(*mock_remote.MockClient), e.Remote.(*mock_remote.MockRemote), e.Poster.(*mock_bot.MockPoster), e.Store.(*mock_store.MockStore), e.PluginAPI.(*mock_plugin_api.MockPluginAPI) + s.EXPECT().LoadUser(testUser.MattermostUserID).Return(testUser, nil).Times(2) + + events := []*remote.Event{newTestEvent("1", "", "test")} + papi.EXPECT().GetMattermostUser(testUser.MattermostUserID) + r.EXPECT().MakeClient(gomock.Any(), testUser.OAuth2Token).Return(client) + c.EXPECT().GetEventsBetweenDates(testUser.Remote.ID, gomock.Any(), gomock.Any()).Return(events, nil) + + m := New(e, "").(*mscalendar) + jobSummary := &StatusSyncJobSummary{} + + users, responses, err := m.retrieveUsersToSync(userIndex, jobSummary, true) + require.NoError(t, err) + require.Equal(t, []*store.User{testUser}, users) + require.Equal(t, []*remote.ViewCalendarResponse{{ + RemoteUserID: testUser.Remote.ID, + Events: events, + }}, responses) + }) + + t.Run("one user should be synced, one user shouldn't", func(t *testing.T) { + testUser := newTestUser() + testUser.Settings.UpdateStatus = true + testUser.Settings.ReceiveReminders = true + + testUser2 := newTestUserNumbered(1) + + userIndex := []*store.UserShort{ + { + MattermostUserID: testUser.MattermostUserID, + RemoteID: testUser.Remote.ID, + Email: testUser.Remote.Mail, + }, + { + MattermostUserID: testUser2.MattermostUserID, + RemoteID: testUser2.Remote.ID, + Email: testUser2.Remote.Mail, + }, + } + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + e, client := makeStatusSyncTestEnv(ctrl) + + c, r, _, s, papi := client.(*mock_remote.MockClient), e.Remote.(*mock_remote.MockRemote), e.Poster.(*mock_bot.MockPoster), e.Store.(*mock_store.MockStore), e.PluginAPI.(*mock_plugin_api.MockPluginAPI) + s.EXPECT().LoadUser(testUser.MattermostUserID).Return(testUser, nil).Times(2) + s.EXPECT().LoadUser(testUser2.MattermostUserID).Return(testUser2, nil) + + events := []*remote.Event{newTestEvent("1", "", "test")} + papi.EXPECT().GetMattermostUser(testUser.MattermostUserID) + r.EXPECT().MakeClient(gomock.Any(), testUser.OAuth2Token).Return(client) + c.EXPECT().GetEventsBetweenDates(testUser.Remote.ID, gomock.Any(), gomock.Any()).Return(events, nil) + + m := New(e, "").(*mscalendar) + jobSummary := &StatusSyncJobSummary{} + + users, responses, err := m.retrieveUsersToSync(userIndex, jobSummary, true) + require.NoError(t, err) + require.Equal(t, []*store.User{testUser}, users) + require.Equal(t, []*remote.ViewCalendarResponse{{ + RemoteUserID: testUser.Remote.ID, + Events: events, + }}, responses) + }) + + t.Run("two users should be synced", func(t *testing.T) { + testUser := newTestUserNumbered(1) + testUser.Settings.UpdateStatus = true + testUser.Settings.ReceiveReminders = true + + testUser2 := newTestUserNumbered(2) + testUser2.Settings.UpdateStatus = true + testUser2.Settings.ReceiveReminders = true + + userIndex := []*store.UserShort{ + { + MattermostUserID: testUser.MattermostUserID, + RemoteID: testUser.Remote.ID, + Email: testUser.Remote.Mail, + }, + { + MattermostUserID: testUser2.MattermostUserID, + RemoteID: testUser2.Remote.ID, + Email: testUser2.Remote.Mail, + }, + } + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + e, client := makeStatusSyncTestEnv(ctrl) + + c, r, _, s, papi := client.(*mock_remote.MockClient), e.Remote.(*mock_remote.MockRemote), e.Poster.(*mock_bot.MockPoster), e.Store.(*mock_store.MockStore), e.PluginAPI.(*mock_plugin_api.MockPluginAPI) + s.EXPECT().LoadUser(testUser.MattermostUserID).Return(testUser, nil).Times(2) + s.EXPECT().LoadUser(testUser2.MattermostUserID).Return(testUser2, nil).Times(2) + + eventsUser1 := []*remote.Event{newTestEvent("1", "", "test")} + eventsUser2 := []*remote.Event{newTestEvent("2", "", "test2")} + papi.EXPECT().GetMattermostUser(testUser.MattermostUserID) + papi.EXPECT().GetMattermostUser(testUser2.MattermostUserID) + r.EXPECT().MakeClient(gomock.Any(), testUser.OAuth2Token).Return(client) + r.EXPECT().MakeClient(gomock.Any(), testUser2.OAuth2Token).Return(client) + c.EXPECT().GetEventsBetweenDates(testUser.Remote.ID, gomock.Any(), gomock.Any()).Return(eventsUser1, nil) + c.EXPECT().GetEventsBetweenDates(testUser2.Remote.ID, gomock.Any(), gomock.Any()).Return(eventsUser2, nil) + + m := New(e, "").(*mscalendar) + jobSummary := &StatusSyncJobSummary{} + + users, responses, err := m.retrieveUsersToSync(userIndex, jobSummary, true) + require.NoError(t, err) + require.ElementsMatch(t, []*store.User{testUser, testUser2}, users) + require.ElementsMatch(t, []*remote.ViewCalendarResponse{{ + RemoteUserID: testUser.Remote.ID, + Events: eventsUser1, + }, { + RemoteUserID: testUser2.Remote.ID, + Events: eventsUser2, + }}, responses) + }) +} + func makeStatusSyncTestEnv(ctrl *gomock.Controller) (Env, remote.Client) { s := mock_store.NewMockStore(ctrl) poster := mock_bot.NewMockPoster(ctrl) @@ -347,15 +587,5 @@ func makeStatusSyncTestEnv(ctrl *gomock.Controller) (Env, remote.Client) { }, } - s.EXPECT().LoadUserIndex().Return(store.UserIndex{ - &store.UserShort{ - MattermostUserID: "user_mm_id", - RemoteID: "user_remote_id", - Email: "user_email@example.com", - }, - }, nil).Times(1) - - mockRemote.EXPECT().MakeSuperuserClient(context.Background()).Return(mockClient, nil) - return env, mockClient } diff --git a/server/mscalendar/calendar.go b/calendar/engine/calendar.go similarity index 84% rename from server/mscalendar/calendar.go rename to calendar/engine/calendar.go index cec09559..ba097633 100644 --- a/server/mscalendar/calendar.go +++ b/calendar/engine/calendar.go @@ -1,12 +1,12 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "time" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) type Calendar interface { @@ -46,6 +46,16 @@ func (m *mscalendar) getTodayCalendarEvents(user *User, now time.Time, timezone return m.client.GetDefaultCalendarView(user.Remote.ID, from, to) } +func (m *mscalendar) excludeDeclinedEvents(events []*remote.Event) (result []*remote.Event) { + for ix, evt := range events { + if evt.ResponseStatus == nil || evt.ResponseStatus.Response != remote.EventResponseStatusDeclined { + result = append(result, events[ix]) + } + } + + return +} + func (m *mscalendar) CreateCalendar(user *User, calendar *remote.Calendar) (*remote.Calendar, error) { err := m.Filter( withClient, @@ -72,7 +82,7 @@ func (m *mscalendar) CreateEvent(user *User, event *remote.Event, mattermostUser _, err := m.Store.LoadUser(mattermostUserID) if err != nil { if err.Error() == "not found" { - _, err = m.Poster.DM(mattermostUserID, "You have been invited to an Microsoft Outlook calendar event but have not linked your account. Feel free to join us by connecting your Microsoft Outlook account using `/mscalendar connect`") + _, err = m.Poster.DM(mattermostUserID, "You have been invited to a %s event but have not linked your account. Feel free to join us by connecting your %s account using `/%s connect`", m.Provider.DisplayName, m.Provider.DisplayName, m.Provider.CommandTrigger) if err != nil { m.Logger.Warnf("CreateEvent error creating DM. err=%v", err) continue diff --git a/server/mscalendar/client.go b/calendar/engine/client.go similarity index 87% rename from server/mscalendar/client.go rename to calendar/engine/client.go index bf29f0d5..47794596 100644 --- a/server/mscalendar/client.go +++ b/calendar/engine/client.go @@ -1,12 +1,12 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "context" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) type Client interface { diff --git a/server/mscalendar/daily_summary.go b/calendar/engine/daily_summary.go similarity index 65% rename from server/mscalendar/daily_summary.go rename to calendar/engine/daily_summary.go index 1e49ab5d..a2e75f49 100644 --- a/server/mscalendar/daily_summary.go +++ b/calendar/engine/daily_summary.go @@ -1,7 +1,7 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "fmt" @@ -9,10 +9,11 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/views" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/store" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/tz" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/views" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/tz" ) const dailySummaryTimeWindow = time.Minute * 2 @@ -21,7 +22,7 @@ const dailySummaryTimeWindow = time.Minute * 2 const DailySummaryJobInterval = 15 * time.Minute type DailySummary interface { - GetDailySummaryForUser(user *User) (string, error) + GetDaySummaryForUser(now time.Time, user *User) (string, error) GetDailySummarySettingsForUser(user *User) (*store.DailySummaryUserSettings, error) SetDailySummaryPostTime(user *User, timeStr string) (*store.DailySummaryUserSettings, error) SetDailySummaryEnabled(user *User, enable bool) (*store.DailySummaryUserSettings, error) @@ -102,16 +103,19 @@ func (m *mscalendar) ProcessAllDailySummary(now time.Time) error { } err = m.Filter(withSuperuserClient) - if err != nil { + if err != nil && !errors.Is(err, remote.ErrSuperUserClientNotSupported) { return err } + fetchIndividually := errors.Is(err, remote.ErrSuperUserClientNotSupported) + + calendarViews := []*remote.ViewCalendarResponse{} requests := []*remote.ViewCalendarParams{} byRemoteID := map[string]*store.User{} for _, user := range userIndex { storeUser, storeErr := m.Store.LoadUser(user.MattermostUserID) if storeErr != nil { - m.Logger.Warnf("Error loading user %s for daily summary. err=%v", user.MattermostUserID, storeErr) + m.Logger.Warnf("Error loading user %s for daily summary. err=%v", storeUser.MattermostUserID, storeErr) continue } byRemoteID[storeUser.Remote.ID] = storeUser @@ -123,28 +127,67 @@ func (m *mscalendar) ProcessAllDailySummary(now time.Time) error { shouldPost, shouldPostErr := shouldPostDailySummary(dsum, now) if shouldPostErr != nil { - m.Logger.Warnf("Error posting daily summary for user %s. err=%v", user.MattermostUserID, shouldPostErr) + m.Logger.With(bot.LogContext{"mm_user_id": storeUser.MattermostUserID, "now": now.String(), "err": shouldPostErr}).Warnf("Error checking daily summary should be posted") continue } if !shouldPost { continue } - start, end := getTodayHoursForTimezone(now, dsum.Timezone) - req := &remote.ViewCalendarParams{ - RemoteUserID: storeUser.Remote.ID, - StartTime: start, - EndTime: end, + if fetchIndividually { + u := NewUser(storeUser.MattermostUserID) + if err := m.ExpandUser(u); err != nil { + m.Logger.With(bot.LogContext{ + "mattermost_id": storeUser.MattermostUserID, + "remote_id": storeUser.Remote.ID, + "err": err, + }).Errorf("error getting user information") + continue + } + + engine, err := m.FilterCopy(withActingUser(storeUser.MattermostUserID)) + if err != nil { + m.Logger.Errorf("Error creating user engine %s. err=%v", storeUser.MattermostUserID, err) + continue + } + + tz, err := engine.GetTimezone(u) + if err != nil { + m.Logger.With(bot.LogContext{"mm_user_id": storeUser.MattermostUserID, "err": err}).Errorf("Error getting timezone for user.") + continue + } + + events, err := engine.getTodayCalendarEvents(u, now, tz) + if err != nil { + m.Logger.With(bot.LogContext{"mm_user_id": storeUser.MattermostUserID, "now": now.String(), "tz": tz, "err": err}).Errorf("Error getting calendar events for user") + continue + } + + calendarViews = append(calendarViews, &remote.ViewCalendarResponse{ + Error: nil, + RemoteUserID: storeUser.Remote.ID, + Events: events, + }) + } else { + start, end := getTodayHoursForTimezone(now, dsum.Timezone) + req := &remote.ViewCalendarParams{ + RemoteUserID: storeUser.Remote.ID, + StartTime: start, + EndTime: end, + } + requests = append(requests, req) } - requests = append(requests, req) } - responses, err := m.client.DoBatchViewCalendarRequests(requests) - if err != nil { - return err + if !fetchIndividually { + var err error + calendarViews, err = m.client.DoBatchViewCalendarRequests(requests) + if err != nil { + return err + } } - for _, res := range responses { + for _, res := range calendarViews { user := byRemoteID[res.RemoteUserID] if res.Error != nil { m.Logger.Warnf("Error rendering user %s calendar. err=%s %s", user.MattermostUserID, res.Error.Code, res.Error.Message) @@ -160,6 +203,7 @@ func (m *mscalendar) ProcessAllDailySummary(now time.Time) error { } m.Poster.DM(user.MattermostUserID, postStr) + m.Dependencies.Tracker.TrackDailySummarySent(user.MattermostUserID) dsum.LastPostTime = time.Now().Format(time.RFC3339) err = m.Store.StoreUser(user) @@ -168,22 +212,29 @@ func (m *mscalendar) ProcessAllDailySummary(now time.Time) error { } } - m.Logger.Infof("Processed daily summary for %d users", len(responses)) + m.Logger.Infof("Processed daily summary for %d users", len(calendarViews)) return nil } -func (m *mscalendar) GetDailySummaryForUser(user *User) (string, error) { +func (m *mscalendar) GetDaySummaryForUser(day time.Time, user *User) (string, error) { tz, err := m.GetTimezone(user) if err != nil { return "", err } - calendarData, err := m.getTodayCalendarEvents(user, time.Now(), tz) + calendarData, err := m.getTodayCalendarEvents(user, day, tz) if err != nil { return "Failed to get calendar events", err } - return views.RenderCalendarView(calendarData, tz) + events := m.excludeDeclinedEvents(calendarData) + + messageString, err := views.RenderCalendarView(events, tz) + if err != nil { + return "", errors.Wrap(err, "failed to render daily summary") + } + + return messageString, nil } func shouldPostDailySummary(dsum *store.DailySummaryUserSettings, now time.Time) (bool, error) { diff --git a/server/mscalendar/daily_summary_test.go b/calendar/engine/daily_summary_test.go similarity index 54% rename from server/mscalendar/daily_summary_test.go rename to calendar/engine/daily_summary_test.go index 339da405..ecb6b94f 100644 --- a/server/mscalendar/daily_summary_test.go +++ b/calendar/engine/daily_summary_test.go @@ -1,24 +1,104 @@ -package mscalendar +package engine import ( "context" + "errors" "testing" "time" "github.com/golang/mock/gomock" - "github.com/pkg/errors" "github.com/stretchr/testify/require" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/mock_plugin_api" - "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/telemetry" - "github.com/mattermost/mattermost-plugin-mscalendar/server/tracker" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot/mock_bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/mock_plugin_api" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote/mock_remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store/mock_store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/telemetry" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/tracker" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot/mock_bot" ) +func TestGetDaySummaryForUser(t *testing.T) { + t.Run("declined events are excluded", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := mock_store.NewMockStore(ctrl) + poster := mock_bot.NewMockPoster(ctrl) + mockRemote := mock_remote.NewMockRemote(ctrl) + mockClient := mock_remote.NewMockClient(ctrl) + mockPluginAPI := mock_plugin_api.NewMockPluginAPI(ctrl) + + logger := mock_bot.NewMockLogger(ctrl) + env := Env{ + Dependencies: &Dependencies{ + Store: mockStore, + Logger: logger, + Poster: poster, + Remote: mockRemote, + PluginAPI: mockPluginAPI, + Tracker: tracker.New(telemetry.NewTracker(nil, "", "", "", "", "", telemetry.TrackerConfig{}, nil)), + }, + } + + loc, err := time.LoadLocation("EST") + require.Nil(t, err) + hour, minute := 9, 0 // Time is "9:00AM" + moment := makeTime(hour, minute, loc) + + user := NewUser("user1_mm_id") + + mockStore.EXPECT().LoadUser(user.MattermostUserID).Return(&store.User{ + MattermostUserID: user.MattermostUserID, + Remote: &remote.User{ID: "user1_remote_id"}, + Settings: store.Settings{ + DailySummary: &store.DailySummaryUserSettings{ + Enable: true, + PostTime: "9:00AM", + Timezone: "Pacific Standard Time", + LastPostTime: "", + }, + }, + }, nil).Times(2) + + mockPluginAPI.EXPECT().GetMattermostUser(user.MattermostUserID) + + mockRemote.EXPECT().MakeClient(context.Background(), nil).Return(mockClient) + + mockClient.EXPECT().GetMailboxSettings("user1_remote_id").Return(&remote.MailboxSettings{ + TimeZone: "Pacific Standard Time", + }, nil) + + mockClient.EXPECT().GetDefaultCalendarView("user1_remote_id", gomock.Any(), gomock.Any()).Return([]*remote.Event{ + { + Subject: "The subject", + Start: remote.NewDateTime(moment, "Pacific Standard Time"), + End: remote.NewDateTime(moment.Add(2*time.Hour), "Pacific Standard Time"), + }, + { + Subject: "The subject for declined event", + Start: remote.NewDateTime(moment, "Pacific Standard Time"), + End: remote.NewDateTime(moment.Add(2*time.Hour), "Pacific Standard Time"), + ResponseStatus: &remote.EventResponseStatus{ + Response: remote.EventResponseStatusDeclined, + }, + }, + }, nil) + + mscalendar := New(env, "user1_mm_id") + result, err := mscalendar.GetDaySummaryForUser(moment, user) + require.NoError(t, err) + + require.Equal(t, result, `Times are shown in Pacific Standard Time +Wednesday February 12, 2020 + +| Time | Subject | +| :-- | :-- | +| 9:00AM - 11:00AM | [The subject]() |`) + }) +} + func TestProcessAllDailySummary(t *testing.T) { for _, tc := range []struct { runAssertions func(deps *Dependencies, client remote.Client) @@ -138,6 +218,9 @@ func TestProcessAllDailySummary(t *testing.T) { Subject: "The subject", Start: remote.NewDateTime(moment, "Mountain Standard Time"), End: remote.NewDateTime(moment.Add(2*time.Hour), "Mountain Standard Time"), + Location: &remote.Location{ + DisplayName: "https://zoom.us/j/123", + }, }, }}, }, nil) @@ -148,10 +231,117 @@ func TestProcessAllDailySummary(t *testing.T) { gomock.InOrder( mockPoster.EXPECT().DM("user1_mm_id", "You have no upcoming events.").Return("postID1", nil).Times(1), mockPoster.EXPECT().DM("user2_mm_id", `Times are shown in Pacific Standard Time -Wednesday February 12 +Wednesday February 12, 2020 + +| Time | Subject | +| :-- | :-- | +| 9:00AM - 11:00AM | [The subject]() |`).Return("postID2", nil).Times(1), + ) + + s.EXPECT().StoreUser(gomock.Any()).Times(2).DoAndReturn(func(u *store.User) error { + require.NotEmpty(t, u.Settings.DailySummary.LastPostTime) + return nil + }) + + mockLogger := deps.Logger.(*mock_bot.MockLogger) + mockLogger.EXPECT().Infof("Processed daily summary for %d users", 2) + }, + }, + { + name: "User receives their daily summary (individual data call)", + err: "", + runAssertions: func(deps *Dependencies, client remote.Client) { + s := deps.Store.(*mock_store.MockStore) + mockRemote := deps.Remote.(*mock_remote.MockRemote) + mockClient := client.(*mock_remote.MockClient) + papi := deps.PluginAPI.(*mock_plugin_api.MockPluginAPI) + + loc, err := time.LoadLocation("MST") + require.Nil(t, err) + hour, minute := 10, 0 // Time is "10:00AM" + moment := makeTime(hour, minute, loc) + + s.EXPECT().LoadUserIndex().Return(store.UserIndex{{ + MattermostUserID: "user1_mm_id", + RemoteID: "user1_remote_id", + }, { + MattermostUserID: "user2_mm_id", + RemoteID: "user2_remote_id", + }, { + MattermostUserID: "user3_mm_id", + RemoteID: "user3_remote_id", + }}, nil) + + mockRemote.EXPECT().MakeSuperuserClient(context.Background()).Return(nil, remote.ErrSuperUserClientNotSupported).Times(1) + + s.EXPECT().LoadUser("user1_mm_id").Return(&store.User{ + MattermostUserID: "user1_mm_id", + Remote: &remote.User{ID: "user1_remote_id"}, + Settings: store.Settings{ + DailySummary: &store.DailySummaryUserSettings{ + Enable: true, + PostTime: "9:00AM", + Timezone: "Eastern Standard Time", + LastPostTime: "", + }, + }, + }, nil).Times(3) + + s.EXPECT().LoadUser("user2_mm_id").Return(&store.User{ + MattermostUserID: "user2_mm_id", + Remote: &remote.User{ID: "user2_remote_id"}, + Settings: store.Settings{ + DailySummary: &store.DailySummaryUserSettings{ + Enable: true, + PostTime: "6:00AM", + Timezone: "Pacific Standard Time", + LastPostTime: "", + }, + }, + }, nil).Times(3) + + s.EXPECT().LoadUser("user3_mm_id").Return(&store.User{ + MattermostUserID: "user3_mm_id", + Remote: &remote.User{ID: "user3_remote_id"}, + Settings: store.Settings{ + DailySummary: &store.DailySummaryUserSettings{ + Enable: true, + PostTime: "10:00AM", // should not receive summary + Timezone: "Pacific Standard Time", + LastPostTime: "", + }, + }, + }, nil) + + papi.EXPECT().GetMattermostUser("user1_mm_id").Times(2) + papi.EXPECT().GetMattermostUser("user2_mm_id").Times(2) + + mockClient.EXPECT().GetMailboxSettings("user1_remote_id").Return(&remote.MailboxSettings{ + TimeZone: "Eastern Standard Time", + }, nil) + mockClient.EXPECT().GetMailboxSettings("user2_remote_id").Return(&remote.MailboxSettings{ + TimeZone: "Pacific Standard Time", + }, nil) + + mockRemote.EXPECT().MakeClient(context.Background(), gomock.Any()).Return(mockClient).Times(2) + + mockClient.EXPECT().GetDefaultCalendarView("user1_remote_id", gomock.Any(), gomock.Any()).Return([]*remote.Event{}, nil) + mockClient.EXPECT().GetDefaultCalendarView("user2_remote_id", gomock.Any(), gomock.Any()).Return([]*remote.Event{ + { + Subject: "The subject", + Start: remote.NewDateTime(moment, "Mountain Standard Time"), + End: remote.NewDateTime(moment.Add(2*time.Hour), "Mountain Standard Time"), + }, + }, nil) + + mockPoster := deps.Poster.(*mock_bot.MockPoster) + gomock.InOrder( + mockPoster.EXPECT().DM("user1_mm_id", "You have no upcoming events.").Return("postID1", nil).Times(1), + mockPoster.EXPECT().DM("user2_mm_id", `Times are shown in Pacific Standard Time +Wednesday February 12, 2020 | Time | Subject | -| :--: | :-- | +| :-- | :-- | | 9:00AM - 11:00AM | [The subject]() |`).Return("postID2", nil).Times(1), ) diff --git a/server/mscalendar/event_responder.go b/calendar/engine/event_responder.go similarity index 98% rename from server/mscalendar/event_responder.go rename to calendar/engine/event_responder.go index b1d1b36d..8fc4aaf3 100644 --- a/server/mscalendar/event_responder.go +++ b/calendar/engine/event_responder.go @@ -1,7 +1,7 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "github.com/pkg/errors" diff --git a/server/mscalendar/filters.go b/calendar/engine/filters.go similarity index 65% rename from server/mscalendar/filters.go rename to calendar/engine/filters.go index 7074c171..2409985a 100644 --- a/server/mscalendar/filters.go +++ b/calendar/engine/filters.go @@ -1,7 +1,9 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine + +import "github.com/pkg/errors" type filterf func(*mscalendar) error @@ -15,6 +17,16 @@ func (m *mscalendar) Filter(filters ...filterf) error { return nil } +// FilterCopy creates a copy of the calendar engine and applies filters to it +func (m *mscalendar) FilterCopy(filters ...filterf) (*mscalendar, error) { + engine := m.copy() + if err := engine.Filter(filters...); err != nil { + return nil, errors.Wrap(err, "error filtering engine copy") + } + + return engine, nil +} + func withActingUserExpanded(m *mscalendar) error { return m.ExpandUser(m.actingUser) } @@ -31,6 +43,14 @@ func withRemoteUser(user *User) func(m *mscalendar) error { } } +func withActingUser(mattermostUserID string) func(m *mscalendar) error { + return func(m *mscalendar) error { + m.actingUser = NewUser(mattermostUserID) + m.client = nil + return nil + } +} + func withClient(m *mscalendar) error { if m.client != nil { return nil diff --git a/server/mscalendar/mock_mscalendar/mock_mscalendar.go b/calendar/engine/mock_engine/mock_engine.go similarity index 56% rename from server/mscalendar/mock_mscalendar/mock_mscalendar.go rename to calendar/engine/mock_engine/mock_engine.go index 05e380aa..81ae45bd 100644 --- a/server/mscalendar/mock_mscalendar/mock_mscalendar.go +++ b/calendar/engine/mock_engine/mock_engine.go @@ -1,44 +1,44 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar (interfaces: MSCalendar) +// Source: github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine (interfaces: Engine) -// Package mock_mscalendar is a generated GoMock package. -package mock_mscalendar +// Package mock_engine is a generated GoMock package. +package mock_engine import ( reflect "reflect" time "time" gomock "github.com/golang/mock/gomock" - mscalendar "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" - remote "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - store "github.com/mattermost/mattermost-plugin-mscalendar/server/store" + engine "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" + remote "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + store "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" ) -// MockMSCalendar is a mock of MSCalendar interface. -type MockMSCalendar struct { +// MockEngine is a mock of Engine interface. +type MockEngine struct { ctrl *gomock.Controller - recorder *MockMSCalendarMockRecorder + recorder *MockEngineMockRecorder } -// MockMSCalendarMockRecorder is the mock recorder for MockMSCalendar. -type MockMSCalendarMockRecorder struct { - mock *MockMSCalendar +// MockEngineMockRecorder is the mock recorder for MockEngine. +type MockEngineMockRecorder struct { + mock *MockEngine } -// NewMockMSCalendar creates a new mock instance. -func NewMockMSCalendar(ctrl *gomock.Controller) *MockMSCalendar { - mock := &MockMSCalendar{ctrl: ctrl} - mock.recorder = &MockMSCalendarMockRecorder{mock} +// NewMockEngine creates a new mock instance. +func NewMockEngine(ctrl *gomock.Controller) *MockEngine { + mock := &MockEngine{ctrl: ctrl} + mock.recorder = &MockEngineMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMSCalendar) EXPECT() *MockMSCalendarMockRecorder { +func (m *MockEngine) EXPECT() *MockEngineMockRecorder { return m.recorder } // AcceptEvent mocks base method. -func (m *MockMSCalendar) AcceptEvent(arg0 *mscalendar.User, arg1 string) error { +func (m *MockEngine) AcceptEvent(arg0 *engine.User, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AcceptEvent", arg0, arg1) ret0, _ := ret[0].(error) @@ -46,13 +46,13 @@ func (m *MockMSCalendar) AcceptEvent(arg0 *mscalendar.User, arg1 string) error { } // AcceptEvent indicates an expected call of AcceptEvent. -func (mr *MockMSCalendarMockRecorder) AcceptEvent(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) AcceptEvent(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptEvent", reflect.TypeOf((*MockMSCalendar)(nil).AcceptEvent), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptEvent", reflect.TypeOf((*MockEngine)(nil).AcceptEvent), arg0, arg1) } // AfterDisconnect mocks base method. -func (m *MockMSCalendar) AfterDisconnect(arg0 string) error { +func (m *MockEngine) AfterDisconnect(arg0 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AfterDisconnect", arg0) ret0, _ := ret[0].(error) @@ -60,13 +60,13 @@ func (m *MockMSCalendar) AfterDisconnect(arg0 string) error { } // AfterDisconnect indicates an expected call of AfterDisconnect. -func (mr *MockMSCalendarMockRecorder) AfterDisconnect(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) AfterDisconnect(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterDisconnect", reflect.TypeOf((*MockMSCalendar)(nil).AfterDisconnect), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterDisconnect", reflect.TypeOf((*MockEngine)(nil).AfterDisconnect), arg0) } // AfterSuccessfullyConnect mocks base method. -func (m *MockMSCalendar) AfterSuccessfullyConnect(arg0, arg1 string) error { +func (m *MockEngine) AfterSuccessfullyConnect(arg0, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AfterSuccessfullyConnect", arg0, arg1) ret0, _ := ret[0].(error) @@ -74,25 +74,25 @@ func (m *MockMSCalendar) AfterSuccessfullyConnect(arg0, arg1 string) error { } // AfterSuccessfullyConnect indicates an expected call of AfterSuccessfullyConnect. -func (mr *MockMSCalendarMockRecorder) AfterSuccessfullyConnect(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) AfterSuccessfullyConnect(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterSuccessfullyConnect", reflect.TypeOf((*MockMSCalendar)(nil).AfterSuccessfullyConnect), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterSuccessfullyConnect", reflect.TypeOf((*MockEngine)(nil).AfterSuccessfullyConnect), arg0, arg1) } // ClearSettingsPosts mocks base method. -func (m *MockMSCalendar) ClearSettingsPosts(arg0 string) { +func (m *MockEngine) ClearSettingsPosts(arg0 string) { m.ctrl.T.Helper() m.ctrl.Call(m, "ClearSettingsPosts", arg0) } // ClearSettingsPosts indicates an expected call of ClearSettingsPosts. -func (mr *MockMSCalendarMockRecorder) ClearSettingsPosts(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) ClearSettingsPosts(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearSettingsPosts", reflect.TypeOf((*MockMSCalendar)(nil).ClearSettingsPosts), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearSettingsPosts", reflect.TypeOf((*MockEngine)(nil).ClearSettingsPosts), arg0) } // CreateCalendar mocks base method. -func (m *MockMSCalendar) CreateCalendar(arg0 *mscalendar.User, arg1 *remote.Calendar) (*remote.Calendar, error) { +func (m *MockEngine) CreateCalendar(arg0 *engine.User, arg1 *remote.Calendar) (*remote.Calendar, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateCalendar", arg0, arg1) ret0, _ := ret[0].(*remote.Calendar) @@ -101,13 +101,13 @@ func (m *MockMSCalendar) CreateCalendar(arg0 *mscalendar.User, arg1 *remote.Cale } // CreateCalendar indicates an expected call of CreateCalendar. -func (mr *MockMSCalendarMockRecorder) CreateCalendar(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) CreateCalendar(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCalendar", reflect.TypeOf((*MockMSCalendar)(nil).CreateCalendar), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCalendar", reflect.TypeOf((*MockEngine)(nil).CreateCalendar), arg0, arg1) } // CreateEvent mocks base method. -func (m *MockMSCalendar) CreateEvent(arg0 *mscalendar.User, arg1 *remote.Event, arg2 []string) (*remote.Event, error) { +func (m *MockEngine) CreateEvent(arg0 *engine.User, arg1 *remote.Event, arg2 []string) (*remote.Event, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateEvent", arg0, arg1, arg2) ret0, _ := ret[0].(*remote.Event) @@ -116,13 +116,13 @@ func (m *MockMSCalendar) CreateEvent(arg0 *mscalendar.User, arg1 *remote.Event, } // CreateEvent indicates an expected call of CreateEvent. -func (mr *MockMSCalendarMockRecorder) CreateEvent(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) CreateEvent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEvent", reflect.TypeOf((*MockMSCalendar)(nil).CreateEvent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEvent", reflect.TypeOf((*MockEngine)(nil).CreateEvent), arg0, arg1, arg2) } // CreateMyEventSubscription mocks base method. -func (m *MockMSCalendar) CreateMyEventSubscription() (*store.Subscription, error) { +func (m *MockEngine) CreateMyEventSubscription() (*store.Subscription, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateMyEventSubscription") ret0, _ := ret[0].(*store.Subscription) @@ -131,13 +131,13 @@ func (m *MockMSCalendar) CreateMyEventSubscription() (*store.Subscription, error } // CreateMyEventSubscription indicates an expected call of CreateMyEventSubscription. -func (mr *MockMSCalendarMockRecorder) CreateMyEventSubscription() *gomock.Call { +func (mr *MockEngineMockRecorder) CreateMyEventSubscription() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMyEventSubscription", reflect.TypeOf((*MockMSCalendar)(nil).CreateMyEventSubscription)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMyEventSubscription", reflect.TypeOf((*MockEngine)(nil).CreateMyEventSubscription)) } // DeclineEvent mocks base method. -func (m *MockMSCalendar) DeclineEvent(arg0 *mscalendar.User, arg1 string) error { +func (m *MockEngine) DeclineEvent(arg0 *engine.User, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeclineEvent", arg0, arg1) ret0, _ := ret[0].(error) @@ -145,13 +145,13 @@ func (m *MockMSCalendar) DeclineEvent(arg0 *mscalendar.User, arg1 string) error } // DeclineEvent indicates an expected call of DeclineEvent. -func (mr *MockMSCalendarMockRecorder) DeclineEvent(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) DeclineEvent(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeclineEvent", reflect.TypeOf((*MockMSCalendar)(nil).DeclineEvent), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeclineEvent", reflect.TypeOf((*MockEngine)(nil).DeclineEvent), arg0, arg1) } // DeleteCalendar mocks base method. -func (m *MockMSCalendar) DeleteCalendar(arg0 *mscalendar.User, arg1 string) error { +func (m *MockEngine) DeleteCalendar(arg0 *engine.User, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteCalendar", arg0, arg1) ret0, _ := ret[0].(error) @@ -159,13 +159,13 @@ func (m *MockMSCalendar) DeleteCalendar(arg0 *mscalendar.User, arg1 string) erro } // DeleteCalendar indicates an expected call of DeleteCalendar. -func (mr *MockMSCalendarMockRecorder) DeleteCalendar(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) DeleteCalendar(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCalendar", reflect.TypeOf((*MockMSCalendar)(nil).DeleteCalendar), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCalendar", reflect.TypeOf((*MockEngine)(nil).DeleteCalendar), arg0, arg1) } // DeleteMyEventSubscription mocks base method. -func (m *MockMSCalendar) DeleteMyEventSubscription() error { +func (m *MockEngine) DeleteMyEventSubscription() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteMyEventSubscription") ret0, _ := ret[0].(error) @@ -173,13 +173,13 @@ func (m *MockMSCalendar) DeleteMyEventSubscription() error { } // DeleteMyEventSubscription indicates an expected call of DeleteMyEventSubscription. -func (mr *MockMSCalendarMockRecorder) DeleteMyEventSubscription() *gomock.Call { +func (mr *MockEngineMockRecorder) DeleteMyEventSubscription() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMyEventSubscription", reflect.TypeOf((*MockMSCalendar)(nil).DeleteMyEventSubscription)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMyEventSubscription", reflect.TypeOf((*MockEngine)(nil).DeleteMyEventSubscription)) } // DeleteOrphanedSubscription mocks base method. -func (m *MockMSCalendar) DeleteOrphanedSubscription(arg0 string) error { +func (m *MockEngine) DeleteOrphanedSubscription(arg0 *store.Subscription) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteOrphanedSubscription", arg0) ret0, _ := ret[0].(error) @@ -187,13 +187,13 @@ func (m *MockMSCalendar) DeleteOrphanedSubscription(arg0 string) error { } // DeleteOrphanedSubscription indicates an expected call of DeleteOrphanedSubscription. -func (mr *MockMSCalendarMockRecorder) DeleteOrphanedSubscription(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) DeleteOrphanedSubscription(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrphanedSubscription", reflect.TypeOf((*MockMSCalendar)(nil).DeleteOrphanedSubscription), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrphanedSubscription", reflect.TypeOf((*MockEngine)(nil).DeleteOrphanedSubscription), arg0) } // DisconnectUser mocks base method. -func (m *MockMSCalendar) DisconnectUser(arg0 string) error { +func (m *MockEngine) DisconnectUser(arg0 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DisconnectUser", arg0) ret0, _ := ret[0].(error) @@ -201,13 +201,13 @@ func (m *MockMSCalendar) DisconnectUser(arg0 string) error { } // DisconnectUser indicates an expected call of DisconnectUser. -func (mr *MockMSCalendarMockRecorder) DisconnectUser(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) DisconnectUser(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisconnectUser", reflect.TypeOf((*MockMSCalendar)(nil).DisconnectUser), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisconnectUser", reflect.TypeOf((*MockEngine)(nil).DisconnectUser), arg0) } // FindMeetingTimes mocks base method. -func (m *MockMSCalendar) FindMeetingTimes(arg0 *mscalendar.User, arg1 *remote.FindMeetingTimesParameters) (*remote.MeetingTimeSuggestionResults, error) { +func (m *MockEngine) FindMeetingTimes(arg0 *engine.User, arg1 *remote.FindMeetingTimesParameters) (*remote.MeetingTimeSuggestionResults, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FindMeetingTimes", arg0, arg1) ret0, _ := ret[0].(*remote.MeetingTimeSuggestionResults) @@ -216,27 +216,27 @@ func (m *MockMSCalendar) FindMeetingTimes(arg0 *mscalendar.User, arg1 *remote.Fi } // FindMeetingTimes indicates an expected call of FindMeetingTimes. -func (mr *MockMSCalendarMockRecorder) FindMeetingTimes(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) FindMeetingTimes(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindMeetingTimes", reflect.TypeOf((*MockMSCalendar)(nil).FindMeetingTimes), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindMeetingTimes", reflect.TypeOf((*MockEngine)(nil).FindMeetingTimes), arg0, arg1) } // GetActingUser mocks base method. -func (m *MockMSCalendar) GetActingUser() *mscalendar.User { +func (m *MockEngine) GetActingUser() *engine.User { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetActingUser") - ret0, _ := ret[0].(*mscalendar.User) + ret0, _ := ret[0].(*engine.User) return ret0 } // GetActingUser indicates an expected call of GetActingUser. -func (mr *MockMSCalendarMockRecorder) GetActingUser() *gomock.Call { +func (mr *MockEngineMockRecorder) GetActingUser() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActingUser", reflect.TypeOf((*MockMSCalendar)(nil).GetActingUser)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActingUser", reflect.TypeOf((*MockEngine)(nil).GetActingUser)) } // GetCalendarViews mocks base method. -func (m *MockMSCalendar) GetCalendarViews(arg0 []*store.User) ([]*remote.ViewCalendarResponse, error) { +func (m *MockEngine) GetCalendarViews(arg0 []*store.User) ([]*remote.ViewCalendarResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetCalendarViews", arg0) ret0, _ := ret[0].([]*remote.ViewCalendarResponse) @@ -245,13 +245,13 @@ func (m *MockMSCalendar) GetCalendarViews(arg0 []*store.User) ([]*remote.ViewCal } // GetCalendarViews indicates an expected call of GetCalendarViews. -func (mr *MockMSCalendarMockRecorder) GetCalendarViews(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) GetCalendarViews(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCalendarViews", reflect.TypeOf((*MockMSCalendar)(nil).GetCalendarViews), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCalendarViews", reflect.TypeOf((*MockEngine)(nil).GetCalendarViews), arg0) } // GetCalendars mocks base method. -func (m *MockMSCalendar) GetCalendars(arg0 *mscalendar.User) ([]*remote.Calendar, error) { +func (m *MockEngine) GetCalendars(arg0 *engine.User) ([]*remote.Calendar, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetCalendars", arg0) ret0, _ := ret[0].([]*remote.Calendar) @@ -260,43 +260,43 @@ func (m *MockMSCalendar) GetCalendars(arg0 *mscalendar.User) ([]*remote.Calendar } // GetCalendars indicates an expected call of GetCalendars. -func (mr *MockMSCalendarMockRecorder) GetCalendars(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) GetCalendars(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCalendars", reflect.TypeOf((*MockMSCalendar)(nil).GetCalendars), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCalendars", reflect.TypeOf((*MockEngine)(nil).GetCalendars), arg0) } -// GetDailySummaryForUser mocks base method. -func (m *MockMSCalendar) GetDailySummaryForUser(arg0 *mscalendar.User) (string, error) { +// GetDailySummarySettingsForUser mocks base method. +func (m *MockEngine) GetDailySummarySettingsForUser(arg0 *engine.User) (*store.DailySummaryUserSettings, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDailySummaryForUser", arg0) - ret0, _ := ret[0].(string) + ret := m.ctrl.Call(m, "GetDailySummarySettingsForUser", arg0) + ret0, _ := ret[0].(*store.DailySummaryUserSettings) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetDailySummaryForUser indicates an expected call of GetDailySummaryForUser. -func (mr *MockMSCalendarMockRecorder) GetDailySummaryForUser(arg0 interface{}) *gomock.Call { +// GetDailySummarySettingsForUser indicates an expected call of GetDailySummarySettingsForUser. +func (mr *MockEngineMockRecorder) GetDailySummarySettingsForUser(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDailySummaryForUser", reflect.TypeOf((*MockMSCalendar)(nil).GetDailySummaryForUser), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDailySummarySettingsForUser", reflect.TypeOf((*MockEngine)(nil).GetDailySummarySettingsForUser), arg0) } -// GetDailySummarySettingsForUser mocks base method. -func (m *MockMSCalendar) GetDailySummarySettingsForUser(arg0 *mscalendar.User) (*store.DailySummaryUserSettings, error) { +// GetDaySummaryForUser mocks base method. +func (m *MockEngine) GetDaySummaryForUser(arg0 time.Time, arg1 *engine.User) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDailySummarySettingsForUser", arg0) - ret0, _ := ret[0].(*store.DailySummaryUserSettings) + ret := m.ctrl.Call(m, "GetDaySummaryForUser", arg0, arg1) + ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetDailySummarySettingsForUser indicates an expected call of GetDailySummarySettingsForUser. -func (mr *MockMSCalendarMockRecorder) GetDailySummarySettingsForUser(arg0 interface{}) *gomock.Call { +// GetDaySummaryForUser indicates an expected call of GetDaySummaryForUser. +func (mr *MockEngineMockRecorder) GetDaySummaryForUser(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDailySummarySettingsForUser", reflect.TypeOf((*MockMSCalendar)(nil).GetDailySummarySettingsForUser), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDaySummaryForUser", reflect.TypeOf((*MockEngine)(nil).GetDaySummaryForUser), arg0, arg1) } // GetRemoteUser mocks base method. -func (m *MockMSCalendar) GetRemoteUser(arg0 string) (*remote.User, error) { +func (m *MockEngine) GetRemoteUser(arg0 string) (*remote.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRemoteUser", arg0) ret0, _ := ret[0].(*remote.User) @@ -305,13 +305,13 @@ func (m *MockMSCalendar) GetRemoteUser(arg0 string) (*remote.User, error) { } // GetRemoteUser indicates an expected call of GetRemoteUser. -func (mr *MockMSCalendarMockRecorder) GetRemoteUser(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) GetRemoteUser(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRemoteUser", reflect.TypeOf((*MockMSCalendar)(nil).GetRemoteUser), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRemoteUser", reflect.TypeOf((*MockEngine)(nil).GetRemoteUser), arg0) } // GetTimezone mocks base method. -func (m *MockMSCalendar) GetTimezone(arg0 *mscalendar.User) (string, error) { +func (m *MockEngine) GetTimezone(arg0 *engine.User) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetTimezone", arg0) ret0, _ := ret[0].(string) @@ -320,13 +320,13 @@ func (m *MockMSCalendar) GetTimezone(arg0 *mscalendar.User) (string, error) { } // GetTimezone indicates an expected call of GetTimezone. -func (mr *MockMSCalendarMockRecorder) GetTimezone(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) GetTimezone(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimezone", reflect.TypeOf((*MockMSCalendar)(nil).GetTimezone), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimezone", reflect.TypeOf((*MockEngine)(nil).GetTimezone), arg0) } // GetUserSettings mocks base method. -func (m *MockMSCalendar) GetUserSettings(arg0 *mscalendar.User) (*store.Settings, error) { +func (m *MockEngine) GetUserSettings(arg0 *engine.User) (*store.Settings, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetUserSettings", arg0) ret0, _ := ret[0].(*store.Settings) @@ -335,13 +335,13 @@ func (m *MockMSCalendar) GetUserSettings(arg0 *mscalendar.User) (*store.Settings } // GetUserSettings indicates an expected call of GetUserSettings. -func (mr *MockMSCalendarMockRecorder) GetUserSettings(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) GetUserSettings(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSettings", reflect.TypeOf((*MockMSCalendar)(nil).GetUserSettings), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSettings", reflect.TypeOf((*MockEngine)(nil).GetUserSettings), arg0) } // IsAuthorizedAdmin mocks base method. -func (m *MockMSCalendar) IsAuthorizedAdmin(arg0 string) (bool, error) { +func (m *MockEngine) IsAuthorizedAdmin(arg0 string) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsAuthorizedAdmin", arg0) ret0, _ := ret[0].(bool) @@ -350,13 +350,13 @@ func (m *MockMSCalendar) IsAuthorizedAdmin(arg0 string) (bool, error) { } // IsAuthorizedAdmin indicates an expected call of IsAuthorizedAdmin. -func (mr *MockMSCalendarMockRecorder) IsAuthorizedAdmin(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) IsAuthorizedAdmin(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAuthorizedAdmin", reflect.TypeOf((*MockMSCalendar)(nil).IsAuthorizedAdmin), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAuthorizedAdmin", reflect.TypeOf((*MockEngine)(nil).IsAuthorizedAdmin), arg0) } // ListRemoteSubscriptions mocks base method. -func (m *MockMSCalendar) ListRemoteSubscriptions() ([]*remote.Subscription, error) { +func (m *MockEngine) ListRemoteSubscriptions() ([]*remote.Subscription, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListRemoteSubscriptions") ret0, _ := ret[0].([]*remote.Subscription) @@ -365,13 +365,13 @@ func (m *MockMSCalendar) ListRemoteSubscriptions() ([]*remote.Subscription, erro } // ListRemoteSubscriptions indicates an expected call of ListRemoteSubscriptions. -func (mr *MockMSCalendarMockRecorder) ListRemoteSubscriptions() *gomock.Call { +func (mr *MockEngineMockRecorder) ListRemoteSubscriptions() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRemoteSubscriptions", reflect.TypeOf((*MockMSCalendar)(nil).ListRemoteSubscriptions)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRemoteSubscriptions", reflect.TypeOf((*MockEngine)(nil).ListRemoteSubscriptions)) } // LoadMyEventSubscription mocks base method. -func (m *MockMSCalendar) LoadMyEventSubscription() (*store.Subscription, error) { +func (m *MockEngine) LoadMyEventSubscription() (*store.Subscription, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "LoadMyEventSubscription") ret0, _ := ret[0].(*store.Subscription) @@ -380,25 +380,25 @@ func (m *MockMSCalendar) LoadMyEventSubscription() (*store.Subscription, error) } // LoadMyEventSubscription indicates an expected call of LoadMyEventSubscription. -func (mr *MockMSCalendarMockRecorder) LoadMyEventSubscription() *gomock.Call { +func (mr *MockEngineMockRecorder) LoadMyEventSubscription() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadMyEventSubscription", reflect.TypeOf((*MockMSCalendar)(nil).LoadMyEventSubscription)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadMyEventSubscription", reflect.TypeOf((*MockEngine)(nil).LoadMyEventSubscription)) } // PrintSettings mocks base method. -func (m *MockMSCalendar) PrintSettings(arg0 string) { +func (m *MockEngine) PrintSettings(arg0 string) { m.ctrl.T.Helper() m.ctrl.Call(m, "PrintSettings", arg0) } // PrintSettings indicates an expected call of PrintSettings. -func (mr *MockMSCalendarMockRecorder) PrintSettings(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) PrintSettings(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintSettings", reflect.TypeOf((*MockMSCalendar)(nil).PrintSettings), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintSettings", reflect.TypeOf((*MockEngine)(nil).PrintSettings), arg0) } // ProcessAllDailySummary mocks base method. -func (m *MockMSCalendar) ProcessAllDailySummary(arg0 time.Time) error { +func (m *MockEngine) ProcessAllDailySummary(arg0 time.Time) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ProcessAllDailySummary", arg0) ret0, _ := ret[0].(error) @@ -406,13 +406,13 @@ func (m *MockMSCalendar) ProcessAllDailySummary(arg0 time.Time) error { } // ProcessAllDailySummary indicates an expected call of ProcessAllDailySummary. -func (mr *MockMSCalendarMockRecorder) ProcessAllDailySummary(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) ProcessAllDailySummary(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessAllDailySummary", reflect.TypeOf((*MockMSCalendar)(nil).ProcessAllDailySummary), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessAllDailySummary", reflect.TypeOf((*MockEngine)(nil).ProcessAllDailySummary), arg0) } // RenewMyEventSubscription mocks base method. -func (m *MockMSCalendar) RenewMyEventSubscription() (*store.Subscription, error) { +func (m *MockEngine) RenewMyEventSubscription() (*store.Subscription, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RenewMyEventSubscription") ret0, _ := ret[0].(*store.Subscription) @@ -421,13 +421,13 @@ func (m *MockMSCalendar) RenewMyEventSubscription() (*store.Subscription, error) } // RenewMyEventSubscription indicates an expected call of RenewMyEventSubscription. -func (mr *MockMSCalendarMockRecorder) RenewMyEventSubscription() *gomock.Call { +func (mr *MockEngineMockRecorder) RenewMyEventSubscription() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewMyEventSubscription", reflect.TypeOf((*MockMSCalendar)(nil).RenewMyEventSubscription)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewMyEventSubscription", reflect.TypeOf((*MockEngine)(nil).RenewMyEventSubscription)) } // RespondToEvent mocks base method. -func (m *MockMSCalendar) RespondToEvent(arg0 *mscalendar.User, arg1, arg2 string) error { +func (m *MockEngine) RespondToEvent(arg0 *engine.User, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RespondToEvent", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -435,13 +435,13 @@ func (m *MockMSCalendar) RespondToEvent(arg0 *mscalendar.User, arg1, arg2 string } // RespondToEvent indicates an expected call of RespondToEvent. -func (mr *MockMSCalendarMockRecorder) RespondToEvent(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) RespondToEvent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RespondToEvent", reflect.TypeOf((*MockMSCalendar)(nil).RespondToEvent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RespondToEvent", reflect.TypeOf((*MockEngine)(nil).RespondToEvent), arg0, arg1, arg2) } // SetDailySummaryEnabled mocks base method. -func (m *MockMSCalendar) SetDailySummaryEnabled(arg0 *mscalendar.User, arg1 bool) (*store.DailySummaryUserSettings, error) { +func (m *MockEngine) SetDailySummaryEnabled(arg0 *engine.User, arg1 bool) (*store.DailySummaryUserSettings, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetDailySummaryEnabled", arg0, arg1) ret0, _ := ret[0].(*store.DailySummaryUserSettings) @@ -450,13 +450,13 @@ func (m *MockMSCalendar) SetDailySummaryEnabled(arg0 *mscalendar.User, arg1 bool } // SetDailySummaryEnabled indicates an expected call of SetDailySummaryEnabled. -func (mr *MockMSCalendarMockRecorder) SetDailySummaryEnabled(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) SetDailySummaryEnabled(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDailySummaryEnabled", reflect.TypeOf((*MockMSCalendar)(nil).SetDailySummaryEnabled), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDailySummaryEnabled", reflect.TypeOf((*MockEngine)(nil).SetDailySummaryEnabled), arg0, arg1) } // SetDailySummaryPostTime mocks base method. -func (m *MockMSCalendar) SetDailySummaryPostTime(arg0 *mscalendar.User, arg1 string) (*store.DailySummaryUserSettings, error) { +func (m *MockEngine) SetDailySummaryPostTime(arg0 *engine.User, arg1 string) (*store.DailySummaryUserSettings, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetDailySummaryPostTime", arg0, arg1) ret0, _ := ret[0].(*store.DailySummaryUserSettings) @@ -465,45 +465,45 @@ func (m *MockMSCalendar) SetDailySummaryPostTime(arg0 *mscalendar.User, arg1 str } // SetDailySummaryPostTime indicates an expected call of SetDailySummaryPostTime. -func (mr *MockMSCalendarMockRecorder) SetDailySummaryPostTime(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) SetDailySummaryPostTime(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDailySummaryPostTime", reflect.TypeOf((*MockMSCalendar)(nil).SetDailySummaryPostTime), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDailySummaryPostTime", reflect.TypeOf((*MockEngine)(nil).SetDailySummaryPostTime), arg0, arg1) } // Sync mocks base method. -func (m *MockMSCalendar) Sync(arg0 string) (string, *mscalendar.StatusSyncJobSummary, error) { +func (m *MockEngine) Sync(arg0 string) (string, *engine.StatusSyncJobSummary, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Sync", arg0) ret0, _ := ret[0].(string) - ret1, _ := ret[1].(*mscalendar.StatusSyncJobSummary) + ret1, _ := ret[1].(*engine.StatusSyncJobSummary) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // Sync indicates an expected call of Sync. -func (mr *MockMSCalendarMockRecorder) Sync(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) Sync(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sync", reflect.TypeOf((*MockMSCalendar)(nil).Sync), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sync", reflect.TypeOf((*MockEngine)(nil).Sync), arg0) } // SyncAll mocks base method. -func (m *MockMSCalendar) SyncAll() (string, *mscalendar.StatusSyncJobSummary, error) { +func (m *MockEngine) SyncAll() (string, *engine.StatusSyncJobSummary, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SyncAll") ret0, _ := ret[0].(string) - ret1, _ := ret[1].(*mscalendar.StatusSyncJobSummary) + ret1, _ := ret[1].(*engine.StatusSyncJobSummary) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // SyncAll indicates an expected call of SyncAll. -func (mr *MockMSCalendarMockRecorder) SyncAll() *gomock.Call { +func (mr *MockEngineMockRecorder) SyncAll() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncAll", reflect.TypeOf((*MockMSCalendar)(nil).SyncAll)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncAll", reflect.TypeOf((*MockEngine)(nil).SyncAll)) } // TentativelyAcceptEvent mocks base method. -func (m *MockMSCalendar) TentativelyAcceptEvent(arg0 *mscalendar.User, arg1 string) error { +func (m *MockEngine) TentativelyAcceptEvent(arg0 *engine.User, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TentativelyAcceptEvent", arg0, arg1) ret0, _ := ret[0].(error) @@ -511,13 +511,13 @@ func (m *MockMSCalendar) TentativelyAcceptEvent(arg0 *mscalendar.User, arg1 stri } // TentativelyAcceptEvent indicates an expected call of TentativelyAcceptEvent. -func (mr *MockMSCalendarMockRecorder) TentativelyAcceptEvent(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) TentativelyAcceptEvent(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TentativelyAcceptEvent", reflect.TypeOf((*MockMSCalendar)(nil).TentativelyAcceptEvent), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TentativelyAcceptEvent", reflect.TypeOf((*MockEngine)(nil).TentativelyAcceptEvent), arg0, arg1) } // ViewCalendar mocks base method. -func (m *MockMSCalendar) ViewCalendar(arg0 *mscalendar.User, arg1, arg2 time.Time) ([]*remote.Event, error) { +func (m *MockEngine) ViewCalendar(arg0 *engine.User, arg1, arg2 time.Time) ([]*remote.Event, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ViewCalendar", arg0, arg1, arg2) ret0, _ := ret[0].([]*remote.Event) @@ -526,13 +526,13 @@ func (m *MockMSCalendar) ViewCalendar(arg0 *mscalendar.User, arg1, arg2 time.Tim } // ViewCalendar indicates an expected call of ViewCalendar. -func (mr *MockMSCalendarMockRecorder) ViewCalendar(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) ViewCalendar(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ViewCalendar", reflect.TypeOf((*MockMSCalendar)(nil).ViewCalendar), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ViewCalendar", reflect.TypeOf((*MockEngine)(nil).ViewCalendar), arg0, arg1, arg2) } // Welcome mocks base method. -func (m *MockMSCalendar) Welcome(arg0 string) error { +func (m *MockEngine) Welcome(arg0 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Welcome", arg0) ret0, _ := ret[0].(error) @@ -540,19 +540,19 @@ func (m *MockMSCalendar) Welcome(arg0 string) error { } // Welcome indicates an expected call of Welcome. -func (mr *MockMSCalendarMockRecorder) Welcome(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) Welcome(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Welcome", reflect.TypeOf((*MockMSCalendar)(nil).Welcome), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Welcome", reflect.TypeOf((*MockEngine)(nil).Welcome), arg0) } // WelcomeFlowEnd mocks base method. -func (m *MockMSCalendar) WelcomeFlowEnd(arg0 string) { +func (m *MockEngine) WelcomeFlowEnd(arg0 string) { m.ctrl.T.Helper() m.ctrl.Call(m, "WelcomeFlowEnd", arg0) } // WelcomeFlowEnd indicates an expected call of WelcomeFlowEnd. -func (mr *MockMSCalendarMockRecorder) WelcomeFlowEnd(arg0 interface{}) *gomock.Call { +func (mr *MockEngineMockRecorder) WelcomeFlowEnd(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WelcomeFlowEnd", reflect.TypeOf((*MockMSCalendar)(nil).WelcomeFlowEnd), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WelcomeFlowEnd", reflect.TypeOf((*MockEngine)(nil).WelcomeFlowEnd), arg0) } diff --git a/server/mscalendar/mock_plugin_api/mock_plugin_api.go b/calendar/engine/mock_plugin_api/mock_plugin_api.go similarity index 66% rename from server/mscalendar/mock_plugin_api/mock_plugin_api.go rename to calendar/engine/mock_plugin_api/mock_plugin_api.go index 5aa1b9a1..fc1e9967 100644 --- a/server/mscalendar/mock_plugin_api/mock_plugin_api.go +++ b/calendar/engine/mock_plugin_api/mock_plugin_api.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar (interfaces: PluginAPI) +// Source: github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine (interfaces: PluginAPI) // Package mock_plugin_api is a generated GoMock package. package mock_plugin_api @@ -34,6 +34,20 @@ func (m *MockPluginAPI) EXPECT() *MockPluginAPIMockRecorder { return m.recorder } +// CanLinkEventToChannel mocks base method. +func (m *MockPluginAPI) CanLinkEventToChannel(arg0, arg1 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CanLinkEventToChannel", arg0, arg1) + ret0, _ := ret[0].(bool) + return ret0 +} + +// CanLinkEventToChannel indicates an expected call of CanLinkEventToChannel. +func (mr *MockPluginAPIMockRecorder) CanLinkEventToChannel(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanLinkEventToChannel", reflect.TypeOf((*MockPluginAPI)(nil).CanLinkEventToChannel), arg0, arg1) +} + // GetMattermostUser mocks base method. func (m *MockPluginAPI) GetMattermostUser(arg0 string) (*model.User, error) { m.ctrl.T.Helper() @@ -94,6 +108,21 @@ func (mr *MockPluginAPIMockRecorder) GetMattermostUserStatusesByIds(arg0 interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMattermostUserStatusesByIds", reflect.TypeOf((*MockPluginAPI)(nil).GetMattermostUserStatusesByIds), arg0) } +// GetMattermostUserTeams mocks base method. +func (m *MockPluginAPI) GetMattermostUserTeams(arg0 string) ([]*model.Team, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMattermostUserTeams", arg0) + ret0, _ := ret[0].([]*model.Team) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMattermostUserTeams indicates an expected call of GetMattermostUserTeams. +func (mr *MockPluginAPIMockRecorder) GetMattermostUserTeams(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMattermostUserTeams", reflect.TypeOf((*MockPluginAPI)(nil).GetMattermostUserTeams), arg0) +} + // GetPost mocks base method. func (m *MockPluginAPI) GetPost(arg0 string) (*model.Post, error) { m.ctrl.T.Helper() @@ -124,6 +153,33 @@ func (mr *MockPluginAPIMockRecorder) IsSysAdmin(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSysAdmin", reflect.TypeOf((*MockPluginAPI)(nil).IsSysAdmin), arg0) } +// PublishWebsocketEvent mocks base method. +func (m *MockPluginAPI) PublishWebsocketEvent(arg0, arg1 string, arg2 map[string]interface{}) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "PublishWebsocketEvent", arg0, arg1, arg2) +} + +// PublishWebsocketEvent indicates an expected call of PublishWebsocketEvent. +func (mr *MockPluginAPIMockRecorder) PublishWebsocketEvent(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishWebsocketEvent", reflect.TypeOf((*MockPluginAPI)(nil).PublishWebsocketEvent), arg0, arg1, arg2) +} + +// SearchLinkableChannelForUser mocks base method. +func (m *MockPluginAPI) SearchLinkableChannelForUser(arg0, arg1, arg2 string) ([]*model.Channel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchLinkableChannelForUser", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.Channel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchLinkableChannelForUser indicates an expected call of SearchLinkableChannelForUser. +func (mr *MockPluginAPIMockRecorder) SearchLinkableChannelForUser(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchLinkableChannelForUser", reflect.TypeOf((*MockPluginAPI)(nil).SearchLinkableChannelForUser), arg0, arg1, arg2) +} + // UpdateMattermostUserStatus mocks base method. func (m *MockPluginAPI) UpdateMattermostUserStatus(arg0, arg1 string) (*model.Status, error) { m.ctrl.T.Helper() diff --git a/server/mscalendar/mock_welcomer/mock_welcomer.go b/calendar/engine/mock_welcomer/mock_welcomer.go similarity index 96% rename from server/mscalendar/mock_welcomer/mock_welcomer.go rename to calendar/engine/mock_welcomer/mock_welcomer.go index 14e53652..89303149 100644 --- a/server/mscalendar/mock_welcomer/mock_welcomer.go +++ b/calendar/engine/mock_welcomer/mock_welcomer.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar (interfaces: Welcomer) +// Source: github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine (interfaces: Welcomer) // Package mock_welcomer is a generated GoMock package. package mock_welcomer diff --git a/server/mscalendar/mscalendar.go b/calendar/engine/mscalendar.go similarity index 56% rename from server/mscalendar/mscalendar.go rename to calendar/engine/mscalendar.go index a9a9417b..f472aca0 100644 --- a/server/mscalendar/mscalendar.go +++ b/calendar/engine/mscalendar.go @@ -1,20 +1,20 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "github.com/mattermost/mattermost/server/public/model" - "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/tracker" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/settingspanel" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/tracker" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/settingspanel" ) -type MSCalendar interface { +type Engine interface { Availability Calendar EventResponder @@ -46,6 +46,10 @@ type PluginAPI interface { IsSysAdmin(mattermostUserID string) (bool, error) UpdateMattermostUserStatus(mattermostUserID, status string) (*model.Status, error) GetPost(postID string) (*model.Post, error) + CanLinkEventToChannel(channelID, userID string) bool + SearchLinkableChannelForUser(teamID, mattemostUserID, search string) ([]*model.Channel, error) + GetMattermostUserTeams(mattermostUserID string) ([]*model.Team, error) + PublishWebsocketEvent(mattermostUserID, event string, payload map[string]any) } type Env struct { @@ -60,7 +64,18 @@ type mscalendar struct { client remote.Client } -func New(env Env, actingMattermostUserID string) MSCalendar { +// copy returns a copy of the calendar engine +func (m mscalendar) copy() *mscalendar { + user := *m.actingUser + client := m.client + return &mscalendar{ + Env: m.Env, + actingUser: &user, + client: client, + } +} + +func New(env Env, actingMattermostUserID string) Engine { return &mscalendar{ Env: env, actingUser: NewUser(actingMattermostUserID), diff --git a/calendar/engine/mscalendar_test.go b/calendar/engine/mscalendar_test.go new file mode 100644 index 00000000..94965aaa --- /dev/null +++ b/calendar/engine/mscalendar_test.go @@ -0,0 +1,23 @@ +package engine + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestCopy(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + user := newTestUserNumbered(1) + env, _ := makeStatusSyncTestEnv(ctrl) + + engine := New(env, user.MattermostUserID) + + engineCopy := engine.(*mscalendar).copy() + + assert.NotSame(t, engine, engineCopy) + assert.NotSame(t, engine.(*mscalendar).actingUser, engineCopy.actingUser) + assert.NotSame(t, engine.(*mscalendar).client, engineCopy.client) +} diff --git a/calendar/engine/notification.go b/calendar/engine/notification.go new file mode 100644 index 00000000..f5b50fe5 --- /dev/null +++ b/calendar/engine/notification.go @@ -0,0 +1,217 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package engine + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" +) + +const maxQueueSize = 1024 + +const ( + FieldSubject = "Subject" + FieldBodyPreview = "BodyPreview" + FieldImportance = "Importance" + FieldDuration = "Duration" + FieldWhen = "When" + FieldLocation = "Location" + FieldAttendees = "Attendees" + FieldOrganizer = "Organizer" + FieldResponseStatus = "ResponseStatus" +) + +const ( + OptionYes = "Yes" + OptionNotResponded = "Not responded" + OptionNo = "No" + OptionMaybe = "Maybe" +) + +const ( + ResponseYes = "accepted" + ResponseMaybe = "tentativelyAccepted" + ResponseNo = "declined" + ResponseNone = "notResponded" +) + +var importantNotificationChanges = []string{FieldSubject, FieldWhen} + +var notificationFieldOrder = []string{ + FieldWhen, + FieldLocation, + FieldAttendees, + FieldImportance, +} + +type NotificationProcessor interface { + Configure(Env) + Enqueue(notifications ...*remote.Notification) error + Quit() +} + +type notificationProcessor struct { + Env + envChan chan Env + + queue chan *remote.Notification + quit chan bool +} + +func NewNotificationProcessor(env Env) NotificationProcessor { + processor := ¬ificationProcessor{ + Env: env, + envChan: make(chan (Env)), + queue: make(chan (*remote.Notification), maxQueueSize), + quit: make(chan (bool)), + } + go processor.work() + return processor +} + +func (processor *notificationProcessor) Enqueue(notifications ...*remote.Notification) error { + for _, n := range notifications { + select { + case processor.queue <- n: + default: + return fmt.Errorf("webhook notification: queue full, dropped notification") + } + } + return nil +} + +func (processor *notificationProcessor) Configure(env Env) { + processor.envChan <- env +} + +func (processor *notificationProcessor) Quit() { + processor.quit <- true +} + +func (processor *notificationProcessor) work() { + for { + select { + case n := <-processor.queue: + err := processor.processNotification(n) + if err != nil { + processor.Logger.With(bot.LogContext{ + "subscriptionID": n.SubscriptionID, + }).Infof("webhook notification: failed: `%v`.", err) + } + + case env := <-processor.envChan: + processor.Env = env + + case <-processor.quit: + return + } + } +} + +func (processor *notificationProcessor) processNotification(n *remote.Notification) error { + sub, err := processor.Store.LoadSubscription(n.SubscriptionID) + if err != nil { + return err + } + creator, err := processor.Store.LoadUser(sub.MattermostCreatorID) + if err != nil { + return err + } + if sub.Remote.ID != creator.Settings.EventSubscriptionID { + return errors.New("subscription is orphaned") + } + if sub.Remote.ClientState != "" && sub.Remote.ClientState != n.ClientState { + return errors.New("unauthorized webhook") + } + + n.Subscription = sub.Remote + n.SubscriptionCreator = creator.Remote + + client := processor.Remote.MakeClient(context.Background(), creator.OAuth2Token) + + if n.RecommendRenew { + var renewed *remote.Subscription + renewed, err = client.RenewSubscription(processor.Config.GetNotificationURL(), sub.Remote.CreatorID, n.Subscription) + if err != nil { + return err + } + + storedSub := &store.Subscription{ + Remote: renewed, + MattermostCreatorID: creator.MattermostUserID, + PluginVersion: processor.Config.PluginVersion, + } + err = processor.Store.StoreUserSubscription(creator, storedSub) + if err != nil { + return err + } + processor.Logger.With(bot.LogContext{ + "MattermostUserID": creator.MattermostUserID, + "SubscriptionID": n.SubscriptionID, + }).Debugf("webhook notification: renewed user subscription.") + } + + if n.IsBare { + n, err = client.GetNotificationData(n) + if err != nil { + return err + } + } + + var sa *model.SlackAttachment + prior, err := processor.Store.LoadUserEvent(creator.MattermostUserID, n.Event.ICalUID) + if err != nil && err != store.ErrNotFound { + return err + } + + mailSettings, err := client.GetMailboxSettings(sub.Remote.CreatorID) + if err != nil { + return err + } + timezone := mailSettings.TimeZone + + if prior != nil { + var changed bool + changed, sa = processor.updatedEventSlackAttachment(n, prior.Remote, timezone) + if !changed { + processor.Logger.With(bot.LogContext{ + "MattermostUserID": creator.MattermostUserID, + "SubscriptionID": n.SubscriptionID, + "ChangeType": n.ChangeType, + "EventID": n.Event.ID, + "EventICalUID": n.Event.ICalUID, + }).Debugf("webhook notification: no changes detected in event.") + return nil + } + } else { + sa = processor.newEventSlackAttachment(n, timezone) + prior = &store.Event{} + } + + _, err = processor.Poster.DMWithAttachments(creator.MattermostUserID, sa) + if err != nil { + return err + } + + prior.Remote = n.Event + err = processor.Store.StoreUserEvent(creator.MattermostUserID, prior) + if err != nil { + return err + } + + processor.Logger.With(bot.LogContext{ + "MattermostUserID": creator.MattermostUserID, + "SubscriptionID": n.SubscriptionID, + }).Debugf("Notified: %s.", sa.Title) + + return nil +} diff --git a/server/mscalendar/notification.go b/calendar/engine/notification_format.go similarity index 54% rename from server/mscalendar/notification.go rename to calendar/engine/notification_format.go index d6bb6b61..50244427 100644 --- a/server/mscalendar/notification.go +++ b/calendar/engine/notification_format.go @@ -1,225 +1,18 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -package mscalendar +package engine import ( - "context" "fmt" "strings" "time" - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/views" - "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/fields" -) - -const maxQueueSize = 1024 - -const ( - FieldSubject = "Subject" - FieldBodyPreview = "BodyPreview" - FieldImportance = "Importance" - FieldDuration = "Duration" - FieldWhen = "When" - FieldLocation = "Location" - FieldAttendees = "Attendees" - FieldOrganizer = "Organizer" - FieldResponseStatus = "ResponseStatus" -) - -const ( - OptionYes = "Yes" - OptionNotResponded = "Not responded" - OptionNo = "No" - OptionMaybe = "Maybe" -) + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/views" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/fields" -const ( - ResponseYes = "accepted" - ResponseMaybe = "tentativelyAccepted" - ResponseNo = "declined" - ResponseNone = "notResponded" + "github.com/mattermost/mattermost/server/public/model" ) -var importantNotificationChanges = []string{FieldSubject, FieldWhen} - -var notificationFieldOrder = []string{ - FieldWhen, - FieldLocation, - FieldAttendees, - FieldImportance, -} - -type NotificationProcessor interface { - Configure(Env) - Enqueue(notifications ...*remote.Notification) error - Quit() -} - -type notificationProcessor struct { - Env - envChan chan Env - - queue chan *remote.Notification - quit chan bool -} - -func NewNotificationProcessor(env Env) NotificationProcessor { - processor := ¬ificationProcessor{ - Env: env, - envChan: make(chan (Env)), - queue: make(chan (*remote.Notification), maxQueueSize), - quit: make(chan (bool)), - } - go processor.work() - return processor -} - -func (processor *notificationProcessor) Enqueue(notifications ...*remote.Notification) error { - for _, n := range notifications { - select { - case processor.queue <- n: - default: - return fmt.Errorf("webhook notification: queue full, dropped notification") - } - } - return nil -} - -func (processor *notificationProcessor) Configure(env Env) { - processor.envChan <- env -} - -func (processor *notificationProcessor) Quit() { - processor.quit <- true -} - -func (processor *notificationProcessor) work() { - for { - select { - case n := <-processor.queue: - err := processor.processNotification(n) - if err != nil { - processor.Logger.With(bot.LogContext{ - "subscriptionID": n.SubscriptionID, - }).Infof("webhook notification: failed: `%v`.", err) - } - - case env := <-processor.envChan: - processor.Env = env - - case <-processor.quit: - return - } - } -} - -func (processor *notificationProcessor) processNotification(n *remote.Notification) error { - sub, err := processor.Store.LoadSubscription(n.SubscriptionID) - if err != nil { - return err - } - creator, err := processor.Store.LoadUser(sub.MattermostCreatorID) - if err != nil { - return err - } - if sub.Remote.ID != creator.Settings.EventSubscriptionID { - return errors.New("subscription is orphaned") - } - if sub.Remote.ClientState != "" && sub.Remote.ClientState != n.ClientState { - return errors.New("unauthorized webhook") - } - - n.Subscription = sub.Remote - n.SubscriptionCreator = creator.Remote - - client := processor.Remote.MakeClient(context.Background(), creator.OAuth2Token) - - if n.RecommendRenew { - var renewed *remote.Subscription - renewed, err = client.RenewSubscription(n.SubscriptionID) - if err != nil { - return err - } - - storedSub := &store.Subscription{ - Remote: renewed, - MattermostCreatorID: creator.MattermostUserID, - PluginVersion: processor.Config.PluginVersion, - } - err = processor.Store.StoreUserSubscription(creator, storedSub) - if err != nil { - return err - } - processor.Logger.With(bot.LogContext{ - "MattermostUserID": creator.MattermostUserID, - "SubscriptionID": n.SubscriptionID, - }).Debugf("webhook notification: renewed user subscription.") - } - - if n.IsBare { - n, err = client.GetNotificationData(n) - if err != nil { - return err - } - } - - var sa *model.SlackAttachment - prior, err := processor.Store.LoadUserEvent(creator.MattermostUserID, n.Event.ICalUID) - if err != nil && err != store.ErrNotFound { - return err - } - - mailSettings, err := client.GetMailboxSettings(sub.Remote.CreatorID) - if err != nil { - return err - } - timezone := mailSettings.TimeZone - - if prior != nil { - var changed bool - changed, sa = processor.updatedEventSlackAttachment(n, prior.Remote, timezone) - if !changed { - processor.Logger.With(bot.LogContext{ - "MattermostUserID": creator.MattermostUserID, - "SubscriptionID": n.SubscriptionID, - "ChangeType": n.ChangeType, - "EventID": n.Event.ID, - "EventICalUID": n.Event.ICalUID, - }).Debugf("webhook notification: no changes detected in event.") - return nil - } - } else { - sa = processor.newEventSlackAttachment(n, timezone) - prior = &store.Event{} - } - - _, err = processor.Poster.DMWithAttachments(creator.MattermostUserID, sa) - if err != nil { - return err - } - - prior.Remote = n.Event - err = processor.Store.StoreUserEvent(creator.MattermostUserID, prior) - if err != nil { - return err - } - - processor.Logger.With(bot.LogContext{ - "MattermostUserID": creator.MattermostUserID, - "SubscriptionID": n.SubscriptionID, - }).Debugf("Notified: %s.", sa.Title) - - return nil -} - func (processor *notificationProcessor) newSlackAttachment(n *remote.Notification) *model.SlackAttachment { title := views.EnsureSubject(n.Event.Subject) titleLink := n.Event.Weblink diff --git a/server/mscalendar/notification_test.go b/calendar/engine/notification_test.go similarity index 75% rename from server/mscalendar/notification_test.go rename to calendar/engine/notification_test.go index d413c46a..d4f92046 100644 --- a/server/mscalendar/notification_test.go +++ b/calendar/engine/notification_test.go @@ -1,10 +1,11 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "context" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -12,14 +13,14 @@ import ( "github.com/golang/mock/gomock" "golang.org/x/oauth2" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/mock_plugin_api" - "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" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot/mock_bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/mock_plugin_api" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote/mock_remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store/mock_store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot/mock_bot" ) func newTestNotificationProcessor(env Env) NotificationProcessor { @@ -29,10 +30,10 @@ func newTestNotificationProcessor(env Env) NotificationProcessor { return processor } -func newTestEvent(locationDisplayName string, subjectDisplayName string) *remote.Event { +func newTestEvent(identifier, locationDisplayName string, subjectDisplayName string) *remote.Event { return &remote.Event{ - ID: "remote_event_id", - ICalUID: "remote_event_uid", + ID: fmt.Sprintf("remote_event_id_%s", identifier), + ICalUID: fmt.Sprintf("remote_event_uid_%s", identifier), Organizer: &remote.Attendee{ EmailAddress: &remote.EmailAddress{ Address: "event_organizer_email", @@ -43,7 +44,7 @@ func newTestEvent(locationDisplayName string, subjectDisplayName string) *remote DisplayName: locationDisplayName, }, ResponseStatus: &remote.EventResponseStatus{ - Response: "event_response", + Response: remote.EventResponseStatusAccepted, }, Weblink: "event_weblink", Subject: subjectDisplayName, @@ -65,24 +66,29 @@ func newTestSubscription() *store.Subscription { } func newTestUser() *store.User { + return newTestUserNumbered(1) +} + +func newTestUserNumbered(number int) *store.User { return &store.User{ Settings: store.Settings{ - EventSubscriptionID: "remote_subscription_id", + EventSubscriptionID: fmt.Sprintf("remote_subscription_id_%d", number), }, - Remote: &remote.User{ID: "remote_user_id"}, + Remote: &remote.User{ID: fmt.Sprintf("remote_user_id_%d", number)}, OAuth2Token: &oauth2.Token{ - AccessToken: "creator_oauth_token", + AccessToken: fmt.Sprintf("creator_oauth_token_%d", number), }, - MattermostUserID: "creator_mm_id", + MattermostUserID: fmt.Sprintf("creator_mm_id_%d", number), } } +//lint:ignore U1000 TODO gcal Ignore unused function temporarily for debugging test func newTestNotification(clientState string, recommendRenew bool) *remote.Notification { n := &remote.Notification{ SubscriptionID: "remote_subscription_id", IsBare: true, SubscriptionCreator: &remote.User{}, - Event: newTestEvent("event_location_display_name", "event_subject"), + Event: newTestEvent("1", "event_location_display_name", "event_subject"), Subscription: &remote.Subscription{}, ClientState: clientState, RecommendRenew: recommendRenew, @@ -91,6 +97,8 @@ func newTestNotification(clientState string, recommendRenew bool) *remote.Notifi } func TestProcessNotification(t *testing.T) { + t.Skip("TODO gcal implement TestProcessNotification") + tcs := []struct { notification *remote.Notification priorEvent *remote.Event @@ -111,7 +119,7 @@ func TestProcessNotification(t *testing.T) { name: "prior event exists", expectedError: "", notification: newTestNotification("stored_client_state", false), - priorEvent: newTestEvent("prior_event_location_display_name", "other_event_subject"), + priorEvent: newTestEvent("1", "prior_event_location_display_name", "other_event_subject"), }, { name: "sub renewal recommended", expectedError: "", @@ -156,7 +164,7 @@ func TestProcessNotification(t *testing.T) { mockClient.EXPECT().GetMailboxSettings(user.Remote.ID).Return(&remote.MailboxSettings{TimeZone: "Eastern Standard Time"}, nil) if tc.notification.RecommendRenew { - mockClient.EXPECT().RenewSubscription("remote_subscription_id").Return(&remote.Subscription{}, nil).Times(1) + mockClient.EXPECT().RenewSubscription("notificationurl", "remote_creator_id", "remote_subscription_id").Return(&remote.Subscription{}, nil).Times(1) mockStore.EXPECT().StoreUserSubscription(user, &store.Subscription{ Remote: &remote.Subscription{}, MattermostCreatorID: "creator_mm_id", diff --git a/server/mscalendar/oauth2.go b/calendar/engine/oauth2.go similarity index 69% rename from server/mscalendar/oauth2.go rename to calendar/engine/oauth2.go index a826b3df..e0cf52e1 100644 --- a/server/mscalendar/oauth2.go +++ b/calendar/engine/oauth2.go @@ -1,7 +1,7 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "context" @@ -12,9 +12,9 @@ import ( "github.com/pkg/errors" "golang.org/x/oauth2" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/store" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/oauth2connect" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/oauth2connect" ) const BotWelcomeMessage = "Bot user connected to account %s." @@ -45,7 +45,7 @@ func (app *oauth2App) InitOAuth2(mattermostUserID string) (url string, err error return "", err } - return conf.AuthCodeURL(state, oauth2.AccessTypeOffline), nil + return conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent")), nil } func (app *oauth2App) CompleteOAuth2(authedUserID, code, state string) error { @@ -81,20 +81,28 @@ func (app *oauth2App) CompleteOAuth2(authedUserID, code, state string) error { if err == nil { user, userErr := app.PluginAPI.GetMattermostUser(uid) if userErr == nil { - app.Poster.DM(authedUserID, RemoteUserAlreadyConnected, config.ApplicationName, me.Mail, config.CommandTrigger, user.Username) - return fmt.Errorf(RemoteUserAlreadyConnected, config.ApplicationName, me.Mail, config.CommandTrigger, user.Username) + app.Poster.DM(authedUserID, RemoteUserAlreadyConnected, config.Provider.DisplayName, me.Mail, user.Username, config.Provider.CommandTrigger) + return fmt.Errorf(RemoteUserAlreadyConnected, config.Provider.DisplayName, me.Mail, user.Username, config.Provider.CommandTrigger) } // Couldn't fetch connected MM account. Reject connect attempt. - app.Poster.DM(authedUserID, RemoteUserAlreadyConnectedNotFound, config.ApplicationName, me.Mail) - return fmt.Errorf(RemoteUserAlreadyConnectedNotFound, config.ApplicationName, me.Mail) + app.Poster.DM(authedUserID, RemoteUserAlreadyConnectedNotFound, config.Provider.DisplayName, me.Mail) + return fmt.Errorf(RemoteUserAlreadyConnectedNotFound, config.Provider.DisplayName, me.Mail) + } + + user, userErr := app.PluginAPI.GetMattermostUser(mattermostUserID) + if userErr != nil { + return fmt.Errorf("error retrieving mattermost user (%s)", mattermostUserID) } u := &store.User{ - PluginVersion: app.Config.PluginVersion, - MattermostUserID: mattermostUserID, - Remote: me, - OAuth2Token: tok, + PluginVersion: app.Config.PluginVersion, + MattermostUserID: mattermostUserID, + MattermostUsername: user.Username, + MattermostDisplayName: user.GetDisplayName(model.ShowFullName), + Remote: me, + OAuth2Token: tok, + Settings: store.DefaultSettings, } mailboxSettings, err := client.GetMailboxSettings(me.ID) diff --git a/server/mscalendar/oauth2_test.go b/calendar/engine/oauth2_test.go similarity index 87% rename from server/mscalendar/oauth2_test.go rename to calendar/engine/oauth2_test.go index ac7e8bdc..0e79b65a 100644 --- a/server/mscalendar/oauth2_test.go +++ b/calendar/engine/oauth2_test.go @@ -1,4 +1,4 @@ -package mscalendar +package engine import ( "errors" @@ -12,16 +12,16 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/stretchr/testify/require" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/mock_plugin_api" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/mock_welcomer" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote/msgraph" - "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" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot/mock_bot" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/oauth2connect" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/mock_plugin_api" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine/mock_welcomer" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store/mock_store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot/mock_bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/oauth2connect" + "github.com/mattermost/mattermost-plugin-mscalendar/msgraph" ) const ( @@ -40,6 +40,7 @@ func TestCompleteOAuth2Happy(t *testing.T) { app, env := newOAuth2TestApp(ctrl) ss := env.Dependencies.Store.(*mock_store.MockStore) + aa := env.PluginAPI.(*mock_plugin_api.MockPluginAPI) welcomer := env.Dependencies.Welcomer.(*mock_welcomer.MockWelcomer) state := "" @@ -64,6 +65,12 @@ func TestCompleteOAuth2Happy(t *testing.T) { gomock.InOrder( ss.EXPECT().VerifyOAuth2State(gomock.Eq(state)).Return(nil).Times(1), ss.EXPECT().LoadMattermostUserID(fakeRemoteID).Return("", errors.New("connected user not found")).Times(1), + aa.EXPECT().GetMattermostUser(fakeID).Return(&model.User{ + Id: fakeID, + Username: "fake_username", + FirstName: "fake first name", + LastName: "fake last name", + }, nil), ss.EXPECT().StoreUser(gomock.Any()).Return(nil).Times(1), ss.EXPECT().StoreUserInIndex(gomock.Any()).Return(nil).Times(1), welcomer.EXPECT().AfterSuccessfullyConnect(fakeID, "mail-value").Return(nil).Times(1), @@ -112,7 +119,7 @@ func TestInitOAuth2(t *testing.T) { ss.EXPECT().LoadUser(fakeID).Return(nil, errors.New("remote user not found")).Times(1) ss.EXPECT().StoreOAuth2State(gomock.Any()).Return(nil).Times(1) }, - expectURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?access_type=offline&client_id=fakeclientid&redirect_uri=http%3A%2F%2Flocalhost%2Foauth2%2Fcomplete&response_type=code&scope=offline_access+User.Read+Calendars.ReadWrite+Calendars.ReadWrite.Shared+MailboxSettings.Read%40mattermost.com", + expectURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?access_type=offline&client_id=fakeclientid&prompt=consent&redirect_uri=http%3A%2F%2Flocalhost%2Foauth2%2Fcomplete&response_type=code&scope=offline_access+User.Read+Calendars.ReadWrite+Calendars.ReadWrite.Shared+MailboxSettings.Read%40mattermost.com", }, } @@ -206,10 +213,10 @@ func TestCompleteOAuth2Errors(t *testing.T) { poster.EXPECT().DM( gomock.Eq("fake@mattermost.com"), gomock.Eq(RemoteUserAlreadyConnected), - gomock.Eq("Microsoft Calendar"), + gomock.Eq(config.Provider.DisplayName), gomock.Eq("mail-value"), - gomock.Eq("mscalendar"), gomock.Eq("sample-username"), + gomock.Eq(config.Provider.CommandTrigger), ).Return("post_id", nil).Times(1) }, }, @@ -233,8 +240,16 @@ func TestCompleteOAuth2Errors(t *testing.T) { registerResponder: statusOKGraphAPIResponder, setup: func(d *Dependencies) { ss := d.Store.(*mock_store.MockStore) + aa := d.PluginAPI.(*mock_plugin_api.MockPluginAPI) ss.EXPECT().StoreUser(gomock.Any()).Return(errors.New("forced kvstore error")).Times(1) ss.EXPECT().LoadMattermostUserID("user-remote-id").Return("", errors.New("connected user not found")).Times(1) + aa.EXPECT().GetMattermostUser(fakeID).Return(&model.User{ + Id: fakeID, + Username: "fake_username", + FirstName: "fake first name", + LastName: "fake last name", + }, nil) + ss.EXPECT() ss.EXPECT().VerifyOAuth2State(gomock.Eq("user_fake@mattermost.com")).Return(nil).Times(1) }, expectError: "forced kvstore error", diff --git a/server/mscalendar/settings.go b/calendar/engine/settings.go similarity index 77% rename from server/mscalendar/settings.go rename to calendar/engine/settings.go index a813bb4d..de622ccf 100644 --- a/server/mscalendar/settings.go +++ b/calendar/engine/settings.go @@ -1,9 +1,10 @@ -package mscalendar +package engine import ( - "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/settingspanel" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/settingspanel" ) type Settings interface { @@ -22,7 +23,7 @@ func (m *mscalendar) ClearSettingsPosts(userID string) { } } -func NewSettingsPanel(bot bot.Bot, panelStore settingspanel.PanelStore, settingStore settingspanel.SettingStore, settingsHandler, pluginURL string, getCal func(userID string) MSCalendar) settingspanel.Panel { +func NewSettingsPanel(bot bot.Bot, panelStore settingspanel.PanelStore, settingStore settingspanel.SettingStore, settingsHandler, pluginURL string, getCal func(userID string) Engine, providerFeatures config.ProviderFeatures) settingspanel.Panel { settings := []settingspanel.Setting{} settings = append(settings, settingspanel.NewBoolSetting( store.UpdateStatusSettingID, @@ -52,7 +53,9 @@ func NewSettingsPanel(bot bot.Bot, panelStore settingspanel.PanelStore, settingS "", settingStore, )) - settings = append(settings, NewNotificationsSetting(getCal)) + if providerFeatures.EventNotifications { + settings = append(settings, NewNotificationsSetting(getCal)) + } settings = append(settings, NewDailySummarySetting( settingStore, func(userID string) (string, error) { return getCal(userID).GetTimezone(NewUser(userID)) }, diff --git a/server/mscalendar/settings_daily_summary.go b/calendar/engine/settings_daily_summary.go similarity index 88% rename from server/mscalendar/settings_daily_summary.go rename to calendar/engine/settings_daily_summary.go index c445be38..caa9d27c 100644 --- a/server/mscalendar/settings_daily_summary.go +++ b/calendar/engine/settings_daily_summary.go @@ -1,4 +1,4 @@ -package mscalendar +package engine import ( "errors" @@ -7,8 +7,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost-plugin-mscalendar/server/store" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/settingspanel" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/settingspanel" ) type dailySummarySetting struct { @@ -118,7 +118,6 @@ func (s *dailySummarySetting) GetSlackAttachments(userID, settingHandler string, currentAPM := "AM" fullTime := "8:00AM" currentEnable := false - currentTextValue := "Not set." if dsum != nil { fullTime = dsum.PostTime @@ -127,21 +126,14 @@ func (s *dailySummarySetting) GetSlackAttachments(userID, settingHandler string, currentH = splitted[0] currentM = splitted[1][:2] currentAPM = splitted[1][2:] - enableText := "Disabled" - if currentEnable { - enableText = "Enabled" - } - currentTextValue = fmt.Sprintf("%s (%s) (%s)", dsum.PostTime, dsum.Timezone, enableText) } timezone, err := s.getTimezone(userID) if err != nil { - return nil, fmt.Errorf("could not load the timezone from Microsoft. err=%v", err) + return nil, fmt.Errorf("could not load the timezone. err=%v", err) } fullTime = fullTime + " " + timezone - currentValueMessage = fmt.Sprintf("Current value: %s", currentTextValue) - actionOptionsH := model.PostAction{ Name: "H:", Integration: &model.PostActionIntegration{ @@ -181,7 +173,9 @@ func (s *dailySummarySetting) GetSlackAttachments(userID, settingHandler string, DefaultOption: fullTime, } - actions = []*model.PostAction{&actionOptionsH, &actionOptionsM, &actionOptionsAPM} + if currentEnable { + actions = []*model.PostAction{&actionOptionsH, &actionOptionsM, &actionOptionsAPM} + } buttonText := "Enable" enable := "true" @@ -202,12 +196,11 @@ func (s *dailySummarySetting) GetSlackAttachments(userID, settingHandler string, actions = append(actions, &actionToggle) - text := fmt.Sprintf("%s\n%s", s.description, currentValueMessage) sa := model.SlackAttachment{ Title: title, - Text: text, + Text: s.description, Actions: actions, - Fallback: fmt.Sprintf("%s: %s", title, text), + Fallback: fmt.Sprintf("%s: %s", title, s.description), } return &sa, nil } diff --git a/server/mscalendar/settings_notifications.go b/calendar/engine/settings_notifications.go similarity index 92% rename from server/mscalendar/settings_notifications.go rename to calendar/engine/settings_notifications.go index 2012a1b1..ca0bfd4f 100644 --- a/server/mscalendar/settings_notifications.go +++ b/calendar/engine/settings_notifications.go @@ -1,22 +1,22 @@ -package mscalendar +package engine import ( "fmt" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/settingspanel" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/settingspanel" ) type notificationSetting struct { - getCal func(string) MSCalendar + getCal func(string) Engine title string description string id string dependsOn string } -func NewNotificationsSetting(getCal func(string) MSCalendar) settingspanel.Setting { +func NewNotificationsSetting(getCal func(string) Engine) settingspanel.Setting { return ¬ificationSetting{ title: "Receive notifications of new events", description: "Do you want to subscribe to new events and receive a message when they are created?", diff --git a/server/mscalendar/subscription.go b/calendar/engine/subscription.go similarity index 76% rename from server/mscalendar/subscription.go rename to calendar/engine/subscription.go index 98115d86..22d9a1c0 100644 --- a/server/mscalendar/subscription.go +++ b/calendar/engine/subscription.go @@ -1,22 +1,21 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "strings" "github.com/pkg/errors" - "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/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" ) type Subscriptions interface { CreateMyEventSubscription() (*store.Subscription, error) RenewMyEventSubscription() (*store.Subscription, error) - DeleteOrphanedSubscription(ID string) error + DeleteOrphanedSubscription(*store.Subscription) error DeleteMyEventSubscription() error ListRemoteSubscriptions() ([]*remote.Subscription, error) LoadMyEventSubscription() (*store.Subscription, error) @@ -28,8 +27,7 @@ func (m *mscalendar) CreateMyEventSubscription() (*store.Subscription, error) { return nil, err } - sub, err := m.client.CreateMySubscription( - m.Config.PluginURL + config.FullPathEventNotification) + sub, err := m.client.CreateMySubscription(m.Config.GetNotificationURL(), m.actingUser.Remote.ID) if err != nil { return nil, err } @@ -52,6 +50,9 @@ func (m *mscalendar) LoadMyEventSubscription() (*store.Subscription, error) { if err != nil { return nil, err } + + // TODO: if m.actingUser.Settings.EventSubscriptionID is empty, there's no sub + storedSub, err := m.Store.LoadSubscription(m.actingUser.Settings.EventSubscriptionID) if err != nil { return nil, err @@ -81,7 +82,13 @@ func (m *mscalendar) RenewMyEventSubscription() (*store.Subscription, error) { if subscriptionID == "" { return nil, nil } - renewed, err := m.client.RenewSubscription(subscriptionID) + + sub, err := m.Store.LoadSubscription(subscriptionID) + if err != nil { + return nil, errors.Wrap(err, "error loading subscription") + } + + renewed, err := m.client.RenewSubscription(m.Config.GetNotificationURL(), m.actingUser.Remote.ID, sub.Remote) if err != nil { if strings.Contains(err.Error(), "The object was not found") { err = m.Store.DeleteUserSubscription(m.actingUser.User, subscriptionID) @@ -116,26 +123,32 @@ func (m *mscalendar) DeleteMyEventSubscription() error { subscriptionID := m.actingUser.Settings.EventSubscriptionID - err = m.Store.DeleteUserSubscription(m.actingUser.User, subscriptionID) + sub, err := m.Store.LoadSubscription(subscriptionID) if err != nil { - return errors.WithMessagef(err, "failed to delete subscription %s", subscriptionID) + return errors.Wrap(err, "error loading subscription") } - err = m.DeleteOrphanedSubscription(subscriptionID) + err = m.DeleteOrphanedSubscription(sub) if err != nil { return err } + + err = m.Store.DeleteUserSubscription(m.actingUser.User, subscriptionID) + if err != nil { + return errors.WithMessagef(err, "failed to delete subscription %s", subscriptionID) + } + return nil } -func (m *mscalendar) DeleteOrphanedSubscription(subscriptionID string) error { +func (m *mscalendar) DeleteOrphanedSubscription(sub *store.Subscription) error { err := m.Filter(withClient) if err != nil { return err } - err = m.client.DeleteSubscription(subscriptionID) + err = m.client.DeleteSubscription(sub.Remote) if err != nil { - return errors.WithMessagef(err, "failed to delete subscription %s", subscriptionID) + return errors.WithMessagef(err, "failed to delete subscription %s", sub.Remote.ID) } return nil } diff --git a/server/mscalendar/user.go b/calendar/engine/user.go similarity index 71% rename from server/mscalendar/user.go rename to calendar/engine/user.go index dbc798d8..fd01ea69 100644 --- a/server/mscalendar/user.go +++ b/calendar/engine/user.go @@ -1,7 +1,7 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See License for license information. -package mscalendar +package engine import ( "fmt" @@ -10,8 +10,9 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) type Users interface { @@ -35,6 +36,13 @@ func NewUser(mattermostUserID string) *User { } } +func newUserFromStoredUser(u *store.User) *User { + return &User{ + User: u, + MattermostUserID: u.MattermostUserID, + } +} + func (user *User) Clone() *User { clone := *user clone.User = user.User.Clone() @@ -61,7 +69,7 @@ func (m *mscalendar) ExpandRemoteUser(user *User) error { if user.User == nil { storedUser, err := m.Store.LoadUser(user.MattermostUserID) if err != nil { - return errors.Wrap(err, "It looks like your Mattermost account is not connected to a Microsoft account. Please connect your account using `/mscalendar connect`.") //nolint:revive + return errors.Wrapf(err, "It looks like your Mattermost account is not connected to %s. Please connect your account using `/%s connect`.", m.Provider.DisplayName, m.Provider.CommandTrigger) //nolint:revive } user.User = storedUser } @@ -129,14 +137,38 @@ func (m *mscalendar) DisconnectUser(mattermostUserID string) error { return err } + // Unlink events owned by the user that is disconnecting its account + linkedEventsLeft := make(map[string]string) + for eventID, channelID := range storedUser.ChannelEvents { + if errStore := m.Store.DeleteLinkedChannelFromEvent(eventID, channelID); errStore != nil { + linkedEventsLeft[eventID] = channelID + } + } + if len(linkedEventsLeft) != 0 { + storedUser.ChannelEvents = linkedEventsLeft + if errStore := m.Store.StoreUser(storedUser); errStore != nil { + m.Logger.With(bot.LogContext{ + "err": errStore, + "mm_user_id": storedUser.MattermostDisplayName, + "linked_channels_left": linkedEventsLeft, + }).Errorf("error storing user after failing deleting linked channels from store") + } + return fmt.Errorf("error deleting linked channels from events") + } + eventSubscriptionID := storedUser.Settings.EventSubscriptionID if eventSubscriptionID != "" { + sub, errLoad := m.Store.LoadSubscription(eventSubscriptionID) + if errLoad != nil { + return errors.Wrap(errLoad, "error loading subscription") + } + err = m.Store.DeleteUserSubscription(storedUser, eventSubscriptionID) if err != nil && err != store.ErrNotFound { return errors.WithMessagef(err, "failed to delete subscription %s", eventSubscriptionID) } - err = m.client.DeleteSubscription(eventSubscriptionID) + err = m.client.DeleteSubscription(sub.Remote) if err != nil { m.Logger.Warnf("failed to delete remote subscription %s. err=%v", eventSubscriptionID, err) } diff --git a/calendar/engine/views/calendar.go b/calendar/engine/views/calendar.go new file mode 100644 index 00000000..fd324c46 --- /dev/null +++ b/calendar/engine/views/calendar.go @@ -0,0 +1,237 @@ +package views + +import ( + "fmt" + "net/url" + "sort" + "strings" + "time" + + "github.com/mattermost/mattermost/server/public/model" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" +) + +type Option interface { + Apply(remote.Event, *model.SlackAttachment) +} + +type showTimezoneOption struct { + timezone string +} + +func (tzOpt showTimezoneOption) Apply(event remote.Event, attachment *model.SlackAttachment) { + attachment.Text = fmt.Sprintf( + "%s - %s (%s)", + event.Start.In(tzOpt.timezone).Time().Format(time.Kitchen), + event.End.In(tzOpt.timezone).Time().Format(time.Kitchen), + tzOpt.timezone, + ) +} + +func ShowTimezoneOption(timezone string) Option { + if timezone == "" { + timezone = "UTC" + } + + return showTimezoneOption{ + timezone: timezone, + } +} + +var subjectReplacer = strings.NewReplacer( + "|", `\|`, + "[", `\[`, + "]", `\]`, + ">", `\>`, +) + +func RenderCalendarView(events []*remote.Event, timeZone string) (string, error) { + if len(events) == 0 { + return "You have no upcoming events.", nil + } + + if timeZone != "" { + for _, e := range events { + e.Start = e.Start.In(timeZone) + e.End = e.End.In(timeZone) + } + } + + sort.Slice(events, func(i, j int) bool { + return events[i].Start.Time().Before(events[j].Start.Time()) + }) + + resp := "Times are shown in " + events[0].Start.TimeZone + for _, group := range groupEventsByDate(events) { + resp += "\n" + group[0].Start.Time().Format("Monday January 02, 2006") + "\n\n" + resp += renderTableHeader() + for _, e := range group { + eventString, err := renderEvent(e, true, timeZone) + if err != nil { + return "", err + } + resp += fmt.Sprintf("\n%s", eventString) + } + } + + return resp, nil +} + +func RenderDaySummary(events []*remote.Event, timezone string) (string, []*model.SlackAttachment, error) { + if len(events) == 0 { + return "You have no events for that day", nil, nil + } + + if timezone != "" { + for _, e := range events { + e.Start = e.Start.In(timezone) + e.End = e.End.In(timezone) + } + } + + message := fmt.Sprintf("Agenda for %s.\nTimes are shown in %s", events[0].Start.Time().Format("Monday, 02 January"), events[0].Start.TimeZone) + + var attachments []*model.SlackAttachment + for _, event := range events { + var actions []*model.PostAction + + fields := []*model.SlackAttachmentField{} + if event.Location != nil && event.Location.DisplayName != "" { + fields = append(fields, &model.SlackAttachmentField{ + Title: "Location", + Value: event.Location.DisplayName, + Short: true, + }) + } + + attachments = append(attachments, &model.SlackAttachment{ + Title: event.Subject, + // Text: event.BodyPreview, + Text: fmt.Sprintf("(%s - %s)", event.Start.In(timezone).Time().Format(time.Kitchen), event.End.In(timezone).Time().Format(time.Kitchen)), + Fields: fields, + Actions: actions, + }) + } + + return message, attachments, nil +} + +func renderTableHeader() string { + return `| Time | Subject | +| :-- | :-- |` +} + +func renderEvent(event *remote.Event, asRow bool, timeZone string) (string, error) { + start := event.Start.In(timeZone).Time().Format(time.Kitchen) + end := event.End.In(timeZone).Time().Format(time.Kitchen) + + format := "(%s - %s) [%s](%s)" + if asRow { + format = "| %s - %s | [%s](%s) |" + } + + link, err := url.QueryUnescape(event.Weblink) + if err != nil { + return "", err + } + + subject := EnsureSubject(event.Subject) + + return fmt.Sprintf(format, start, end, subjectReplacer.Replace(subject), link), nil +} + +func RenderEventAsAttachment(event *remote.Event, timezone string, options ...Option) (*model.SlackAttachment, error) { + var actions []*model.PostAction + fields := []*model.SlackAttachmentField{} + var titleLink string + + if event.Location != nil && event.Location.DisplayName != "" { + fields = append(fields, &model.SlackAttachmentField{ + Title: "Location", + Value: event.Location.DisplayName, + Short: true, + }) + } + + if event.Conference != nil { + // Use conference URL as title link if theres conference data present + titleLink = event.Conference.URL + + title := "Meeting URL" + if event.Conference.Application != "" { + title = event.Conference.Application + } + + fields = append(fields, &model.SlackAttachmentField{ + Title: title, + Value: event.Conference.URL, + Short: true, + }) + } + + attachment := &model.SlackAttachment{ + Title: event.Subject, + TitleLink: titleLink, + Text: fmt.Sprintf("%s - %s", event.Start.In(timezone).Time().Format(time.Kitchen), event.End.In(timezone).Time().Format(time.Kitchen)), + Fields: fields, + Actions: actions, + } + + for _, opt := range options { + opt.Apply(*event, attachment) + } + + return attachment, nil +} + +func groupEventsByDate(events []*remote.Event) [][]*remote.Event { + groups := map[string][]*remote.Event{} + + for _, event := range events { + date := event.Start.Time().Format("2006-01-02") + _, ok := groups[date] + if !ok { + groups[date] = []*remote.Event{} + } + + groups[date] = append(groups[date], event) + } + + days := []string{} + for k := range groups { + days = append(days, k) + } + sort.Strings(days) + + result := [][]*remote.Event{} + for _, day := range days { + group := groups[day] + result = append(result, group) + } + return result +} + +func RenderUpcomingEvent(event *remote.Event, timeZone string) (string, error) { + message := "You have an upcoming event:\n" + eventString, err := renderEvent(event, false, timeZone) + if err != nil { + return "", err + } + + return message + eventString, nil +} + +func EnsureSubject(s string) string { + if s == "" { + return "(No subject)" + } + + return s +} + +func RenderUpcomingEventAsAttachment(event *remote.Event, timeZone string, options ...Option) (message string, attachment *model.SlackAttachment, err error) { + message = "Upcoming event:\n" + attachment, err = RenderEventAsAttachment(event, timeZone, options...) + return message, attachment, err +} diff --git a/server/mscalendar/views/status_change_notification.go b/calendar/engine/views/status_change_notification.go similarity index 97% rename from server/mscalendar/views/status_change_notification.go rename to calendar/engine/views/status_change_notification.go index 12779cf9..d4034d77 100644 --- a/server/mscalendar/views/status_change_notification.go +++ b/calendar/engine/views/status_change_notification.go @@ -8,7 +8,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) var prettyStatuses = map[string]string{ diff --git a/calendar/engine/welcome_flow.go b/calendar/engine/welcome_flow.go new file mode 100644 index 00000000..c82df172 --- /dev/null +++ b/calendar/engine/welcome_flow.go @@ -0,0 +1,115 @@ +package engine + +import ( + "fmt" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/flow" +) + +type WelcomeFlow struct { + controller bot.FlowController + onFlowDone func(userID string) + url string + providerFeatures config.ProviderFeatures + steps []flow.Step +} + +func NewWelcomeFlow(bot bot.FlowController, welcomer Welcomer, providerFeatures config.ProviderFeatures) *WelcomeFlow { + wf := WelcomeFlow{ + url: "/welcome", + controller: bot, + onFlowDone: welcomer.WelcomeFlowEnd, + providerFeatures: providerFeatures, + } + wf.makeSteps() + return &wf +} + +func (wf *WelcomeFlow) Step(i int) flow.Step { + if i < 0 { + return nil + } + if i >= len(wf.steps) { + return nil + } + return wf.steps[i] +} + +func (wf *WelcomeFlow) URL() string { + return wf.url +} + +func (wf *WelcomeFlow) Length() int { + return len(wf.steps) +} + +func (wf *WelcomeFlow) StepDone(userID string, step int, value bool) { + wf.controller.NextStep(userID, step, value) +} + +func (wf *WelcomeFlow) FlowDone(userID string) { + wf.onFlowDone(userID) +} + +func (wf *WelcomeFlow) makeSteps() { + steps := []flow.Step{ + &flow.SimpleStep{ + Title: "Update Status", + Message: "Would you like your Mattermost status to be automatically updated at the time of your events?", + PropertyName: store.UpdateStatusPropertyName, + TrueButtonMessage: "Yes - Update my status", + FalseButtonMessage: "No - Don't update my status", + TrueResponseMessage: ":thumbsup: Got it! We'll automatically update your status in Mattermost.", + FalseResponseMessage: ":thumbsup: Got it! We won't update your status in Mattermost.", + FalseSkip: 2, + }, + // &flow.SimpleStep{ + // Title: "Confirm status change", + // Message: "Do you want to receive confirmations before we update your status for each event?", + // PropertyName: store.GetConfirmationPropertyName, + // TrueButtonMessage: "Yes - I would like to get confirmations", + // FalseButtonMessage: "No - Update my status automatically", + // TrueResponseMessage: "Cool, we'll also send you confirmations before updating your status.", + // FalseResponseMessage: "Cool, we'll update your status automatically with no confirmation.", + // }, + // &flow.SimpleStep{ + // Title: "Status during meetings", + // Message: "Do you want to set your status to `Away` or to `Do not Disturb` while you are on a meeting? Setting to `Do Not Disturb` will silence notifications.", + // PropertyName: store.ReceiveNotificationsDuringMeetingName, + // TrueButtonMessage: "Away", + // FalseButtonMessage: "Do not Disturb", + // TrueResponseMessage: "Great, your status will be set to Away.", + // FalseResponseMessage: "Great, your status will be set to Do not Disturb.", + // }, + } + + if wf.providerFeatures.EventNotifications { + steps = append(steps, &flow.SimpleStep{ + Title: "Subscribe to events", + Message: "Do you want to receive notifications when you are invited to an event?", + PropertyName: store.SubscribePropertyName, + TrueButtonMessage: "Yes - I would like to receive notifications for new events", + FalseButtonMessage: "No - Do not notify me of new events", + TrueResponseMessage: "Great, you will receive a message any time you receive a new event.", + FalseResponseMessage: "Great, you will not receive any notification on new events.", + }) + } + + steps = append(steps, &flow.SimpleStep{ + Title: "Receive reminder", + Message: "Do you want to receive a reminder for upcoming events?", + PropertyName: store.ReceiveUpcomingEventReminderName, + TrueButtonMessage: "Yes - I would like to receive reminders for upcoming events", + FalseButtonMessage: "No - Do not notify me of upcoming events", + TrueResponseMessage: "Great, you will receive a message before your meetings.", + FalseResponseMessage: "Great, you will not receive any notification for upcoming events.", + }, &flow.EmptyStep{ + Title: "Daily Summary", + Message: fmt.Sprintf("Remember that you can set-up a daily summary by typing `/%s summary time 8:00AM` or using `/%s settings` to access the settings.", config.Provider.CommandTrigger, config.Provider.CommandTrigger), + }) + + wf.steps = steps +} diff --git a/server/mscalendar/welcomer.go b/calendar/engine/welcomer.go similarity index 82% rename from server/mscalendar/welcomer.go rename to calendar/engine/welcomer.go index a05230fb..991832df 100644 --- a/server/mscalendar/welcomer.go +++ b/calendar/engine/welcomer.go @@ -1,14 +1,14 @@ -package mscalendar +package engine import ( "fmt" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "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/flow" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/flow" ) type Welcomer interface { @@ -31,8 +31,7 @@ type mscBot struct { } const ( - WelcomeMessage = `Welcome to the Microsoft Calendar plugin. - [Click here to link your account.](%s/oauth2/connect)` + WelcomeMessage = `Welcome to the %s plugin. [Click here to link your account.](%s/oauth2/connect)` ) func (m *mscalendar) Welcome(userID string) error { @@ -73,6 +72,8 @@ func (bot *mscBot) Welcome(userID string) error { } func (bot *mscBot) AfterSuccessfullyConnect(userID, userLogin string) error { + bot.PluginAPI.PublishWebsocketEvent(userID, "connected", map[string]any{"action": "connected"}) + bot.Tracker.TrackUserAuthenticated(userID) postID, err := bot.Store.DeleteUserWelcomePost(userID) if err != nil { @@ -90,6 +91,8 @@ func (bot *mscBot) AfterSuccessfullyConnect(userID, userLogin string) error { } func (bot *mscBot) AfterDisconnect(userID string) error { + bot.PluginAPI.PublishWebsocketEvent(userID, "disconnected", map[string]any{"action": "disconnected"}) + bot.Tracker.TrackUserDeauthenticated(userID) errCancel := bot.Cancel(userID) errClean := bot.cleanWelcomePost(userID) @@ -110,7 +113,7 @@ func (bot *mscBot) WelcomeFlowEnd(userID string) { func (bot *mscBot) newConnectAttachment() *model.SlackAttachment { title := "Connect" - text := fmt.Sprintf(WelcomeMessage, bot.pluginURL) + text := fmt.Sprintf(WelcomeMessage, bot.Provider.DisplayName, bot.pluginURL) sa := model.SlackAttachment{ Title: title, Text: text, @@ -122,7 +125,7 @@ func (bot *mscBot) newConnectAttachment() *model.SlackAttachment { func (bot *mscBot) newConnectedAttachment(userLogin string) *model.SlackAttachment { title := "Connect" - text := ":tada: Congratulations! Your microsoft account (*" + userLogin + "*) has been connected to Mattermost." + text := ":tada: Congratulations! Your " + bot.Provider.DisplayName + " account (*" + userLogin + "*) has been connected to Mattermost." return &model.SlackAttachment{ Title: title, Text: text, @@ -131,7 +134,7 @@ func (bot *mscBot) newConnectedAttachment(userLogin string) *model.SlackAttachme } func (bot *mscBot) notifySettings(userID string) error { - _, err := bot.DM(userID, "Feel free to change these settings anytime by typing `/%s settings`", config.CommandTrigger) + _, err := bot.DM(userID, "Feel free to change these settings anytime by typing `/%s settings`", config.Provider.CommandTrigger) if err != nil { return err } diff --git a/server/jobs/daily_summary_job.go b/calendar/jobs/daily_summary_job.go similarity index 76% rename from server/jobs/daily_summary_job.go rename to calendar/jobs/daily_summary_job.go index aa6b7059..f4366532 100644 --- a/server/jobs/daily_summary_job.go +++ b/calendar/jobs/daily_summary_job.go @@ -6,13 +6,13 @@ package jobs import ( "time" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" ) // Unique id for the daily summary job const dailySummaryJobID = "daily_summary" -const dailySummaryJobInterval = mscalendar.DailySummaryJobInterval +const dailySummaryJobInterval = engine.DailySummaryJobInterval // NewDailySummaryJob creates a RegisteredJob with the parameters specific to the DailySummaryJob func NewDailySummaryJob() RegisteredJob { @@ -24,10 +24,10 @@ func NewDailySummaryJob() RegisteredJob { } // runDailySummaryJob delivers the daily calendar summary to all users who have their settings configured to receive it now -func runDailySummaryJob(env mscalendar.Env) { +func runDailySummaryJob(env engine.Env) { env.Logger.Debugf("Daily summary job beginning") - err := mscalendar.New(env, "").ProcessAllDailySummary(time.Now()) + err := engine.New(env, "").ProcessAllDailySummary(time.Now()) if err != nil { env.Logger.Errorf("Error during daily summary job. err=%v", err) } diff --git a/server/jobs/job_manager.go b/calendar/jobs/job_manager.go similarity index 89% rename from server/jobs/job_manager.go rename to calendar/jobs/job_manager.go index c61a2abd..e7c5c21a 100644 --- a/server/jobs/job_manager.go +++ b/calendar/jobs/job_manager.go @@ -12,18 +12,18 @@ import ( "github.com/mattermost/mattermost/server/public/pluginapi/cluster" "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" ) type JobManager struct { - env mscalendar.Env + env engine.Env papi cluster.JobPluginAPI registeredJobs sync.Map activeJobs sync.Map } type RegisteredJob struct { - work func(env mscalendar.Env) + work func(env engine.Env) id string interval time.Duration } @@ -47,7 +47,7 @@ func newActiveJob(ctx context.Context, rj RegisteredJob, sched io.Closer) *activ } // NewJobManager creates a JobManager for to let plugin.go coordinate with the scheduled jobs. -func NewJobManager(papi cluster.JobPluginAPI, env mscalendar.Env) *JobManager { +func NewJobManager(papi cluster.JobPluginAPI, env engine.Env) *JobManager { return &JobManager{ papi: papi, env: env, @@ -110,7 +110,7 @@ func (jm *JobManager) deactivateJob(job RegisteredJob) error { return nil } -// getEnv returns the mscalendar.Env stored on the job manager -func (jm *JobManager) getEnv() mscalendar.Env { +// getEnv returns the engine.Env stored on the job manager +func (jm *JobManager) getEnv() engine.Env { return jm.env } diff --git a/calendar/jobs/mock_cluster/mock_cluster.go b/calendar/jobs/mock_cluster/mock_cluster.go new file mode 100644 index 00000000..ce4a242e --- /dev/null +++ b/calendar/jobs/mock_cluster/mock_cluster.go @@ -0,0 +1,111 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-api/cluster (interfaces: JobPluginAPI) + +// Package mock_cluster is a generated GoMock package. +package mock_cluster + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/mattermost/server/public/model" +) + +// MockJobPluginAPI is a mock of JobPluginAPI interface. +type MockJobPluginAPI struct { + ctrl *gomock.Controller + recorder *MockJobPluginAPIMockRecorder +} + +// MockJobPluginAPIMockRecorder is the mock recorder for MockJobPluginAPI. +type MockJobPluginAPIMockRecorder struct { + mock *MockJobPluginAPI +} + +// NewMockJobPluginAPI creates a new mock instance. +func NewMockJobPluginAPI(ctrl *gomock.Controller) *MockJobPluginAPI { + mock := &MockJobPluginAPI{ctrl: ctrl} + mock.recorder = &MockJobPluginAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockJobPluginAPI) EXPECT() *MockJobPluginAPIMockRecorder { + return m.recorder +} + +// KVDelete mocks base method. +func (m *MockJobPluginAPI) KVDelete(arg0 string) *model.AppError { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVDelete", arg0) + ret0, _ := ret[0].(*model.AppError) + return ret0 +} + +// KVDelete indicates an expected call of KVDelete. +func (mr *MockJobPluginAPIMockRecorder) KVDelete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVDelete", reflect.TypeOf((*MockJobPluginAPI)(nil).KVDelete), arg0) +} + +// KVGet mocks base method. +func (m *MockJobPluginAPI) KVGet(arg0 string) ([]byte, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVGet", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// KVGet indicates an expected call of KVGet. +func (mr *MockJobPluginAPIMockRecorder) KVGet(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVGet", reflect.TypeOf((*MockJobPluginAPI)(nil).KVGet), arg0) +} + +// KVList mocks base method. +func (m *MockJobPluginAPI) KVList(arg0, arg1 int) ([]string, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVList", arg0, arg1) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// KVList indicates an expected call of KVList. +func (mr *MockJobPluginAPIMockRecorder) KVList(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVList", reflect.TypeOf((*MockJobPluginAPI)(nil).KVList), arg0, arg1) +} + +// KVSetWithOptions mocks base method. +func (m *MockJobPluginAPI) KVSetWithOptions(arg0 string, arg1 []byte, arg2 model.PluginKVSetOptions) (bool, *model.AppError) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KVSetWithOptions", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(*model.AppError) + return ret0, ret1 +} + +// KVSetWithOptions indicates an expected call of KVSetWithOptions. +func (mr *MockJobPluginAPIMockRecorder) KVSetWithOptions(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSetWithOptions", reflect.TypeOf((*MockJobPluginAPI)(nil).KVSetWithOptions), arg0, arg1, arg2) +} + +// LogError mocks base method. +func (m *MockJobPluginAPI) LogError(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "LogError", varargs...) +} + +// LogError indicates an expected call of LogError. +func (mr *MockJobPluginAPIMockRecorder) LogError(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogError", reflect.TypeOf((*MockJobPluginAPI)(nil).LogError), varargs...) +} diff --git a/server/jobs/renew_job.go b/calendar/jobs/renew_job.go similarity index 84% rename from server/jobs/renew_job.go rename to calendar/jobs/renew_job.go index f81c176e..42e2e31c 100644 --- a/server/jobs/renew_job.go +++ b/calendar/jobs/renew_job.go @@ -6,7 +6,7 @@ package jobs import ( "time" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" ) const ditherRenew = 50 * time.Millisecond @@ -20,7 +20,7 @@ func NewRenewJob() RegisteredJob { } // runRenewJob calls renews the event subscription for each connected user -func runRenewJob(env mscalendar.Env) { +func runRenewJob(env engine.Env) { uindex, err := env.Store.LoadUserIndex() if err != nil { env.Logger.Errorf("Renew job failed to load user index. err=%v", err) @@ -29,7 +29,8 @@ func runRenewJob(env mscalendar.Env) { env.Logger.Debugf("Renew job: %v users", len(uindex)) for _, u := range uindex { - asUser := mscalendar.New(env, u.MattermostUserID) + asUser := engine.New(env, u.MattermostUserID) + env.Logger.Debugf("Renewing for user: %s", u.MattermostUserID) _, err = asUser.RenewMyEventSubscription() if err != nil { diff --git a/server/jobs/status_sync_job.go b/calendar/jobs/status_sync_job.go similarity index 80% rename from server/jobs/status_sync_job.go rename to calendar/jobs/status_sync_job.go index fff9040b..f64ab966 100644 --- a/server/jobs/status_sync_job.go +++ b/calendar/jobs/status_sync_job.go @@ -3,9 +3,7 @@ package jobs -import ( - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" -) +import "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" // Unique id for the status sync job const statusSyncJobID = "status_sync" @@ -14,16 +12,16 @@ const statusSyncJobID = "status_sync" func NewStatusSyncJob() RegisteredJob { return RegisteredJob{ id: statusSyncJobID, - interval: mscalendar.StatusSyncJobInterval, + interval: engine.StatusSyncJobInterval, work: runSyncJob, } } // runSyncJob synchronizes all users' statuses between mscalendar and Mattermost. -func runSyncJob(env mscalendar.Env) { +func runSyncJob(env engine.Env) { env.Logger.Debugf("User status sync job beginning") - _, syncJobSummary, err := mscalendar.New(env, "").SyncAll() + _, syncJobSummary, err := engine.New(env, "").SyncAll() if err != nil { env.Logger.Errorf("Error during user status sync job. err=%v", err) } diff --git a/server/plugin/plugin.go b/calendar/plugin/plugin.go similarity index 71% rename from server/plugin/plugin.go rename to calendar/plugin/plugin.go index ec16fde9..112f3efc 100644 --- a/server/plugin/plugin.go +++ b/calendar/plugin/plugin.go @@ -19,29 +19,28 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/api" - "github.com/mattermost/mattermost-plugin-mscalendar/server/command" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/jobs" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote/msgraph" - "github.com/mattermost/mattermost-plugin-mscalendar/server/store" - "github.com/mattermost/mattermost-plugin-mscalendar/server/telemetry" - "github.com/mattermost/mattermost-plugin-mscalendar/server/tracker" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/flow" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/oauth2connect" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/pluginapi" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/settingspanel" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/api" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/command" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/jobs" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/telemetry" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/tracker" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/flow" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/oauth2connect" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/pluginapi" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/settingspanel" ) type Env struct { - mscalendar.Env + engine.Env bot bot.Bot jobManager *jobs.JobManager - notificationProcessor mscalendar.NotificationProcessor + notificationProcessor engine.NotificationProcessor httpHandler *httputils.Handler configError error } @@ -55,7 +54,7 @@ type Plugin struct { telemetryClient telemetry.Client } -func NewWithEnv(env mscalendar.Env) *Plugin { +func NewWithEnv(env engine.Env) *Plugin { return &Plugin{ env: Env{ Env: env, @@ -72,10 +71,8 @@ func (p *Plugin) OnActivate() error { return errors.WithMessage(err, "failed to load plugin configuration") } - if stored.OAuth2Authority == "" || - stored.OAuth2ClientID == "" || - stored.OAuth2ClientSecret == "" { - return errors.New("failed to configure: OAuth2 credentials to be set in the config") + if errConfig := p.env.Remote.CheckConfiguration(stored); errConfig != nil { + return errors.Wrap(errConfig, "failed to configure") } p.initEnv(&p.env, "") @@ -108,13 +105,13 @@ func (p *Plugin) OnActivate() error { p.API.GetServerVersion(), e.PluginID, e.PluginVersion, - config.TelemetryShortName, + config.Provider.TelemetryShortName, telemetry.NewTrackerConfig(p.API.GetConfig()), telemetry.NewLogger(p.API), ), ) e.bot = e.bot.WithConfig(stored.Config) - e.Dependencies.Store = store.NewPluginStore(p.API, e.bot, e.Dependencies.Tracker) + e.Dependencies.Store = store.NewPluginStore(p.API, e.bot, e.Dependencies.Tracker, e.Provider.Features.EncryptedStore, []byte(e.EncryptionKey)) }) return nil @@ -173,9 +170,9 @@ func (p *Plugin) OnConfigurationChange() (err error) { e.Config.PluginURLPath = pluginURLPath e.bot = e.bot.WithConfig(stored.Config) - e.Dependencies.Remote = remote.Makers[msgraph.Kind](e.Config, e.bot) + e.Dependencies.Remote = remote.Makers[config.Provider.Name](e.Config, e.bot) - mscalendarBot := mscalendar.NewMSCalendarBot(e.bot, e.Env, pluginURL) + mscalendarBot := engine.NewMSCalendarBot(e.bot, e.Env, pluginURL) e.Dependencies.Logger = e.bot @@ -186,29 +183,32 @@ func (p *Plugin) OnConfigurationChange() (err error) { e.Dependencies.Poster = e.bot e.Dependencies.Welcomer = mscalendarBot - e.Dependencies.Store = store.NewPluginStore(p.API, e.bot, e.Dependencies.Tracker) - e.Dependencies.SettingsPanel = mscalendar.NewSettingsPanel( + e.Dependencies.Store = store.NewPluginStore(p.API, e.bot, e.Dependencies.Tracker, e.Provider.Features.EncryptedStore, []byte(e.EncryptionKey)) + e.Dependencies.SettingsPanel = engine.NewSettingsPanel( e.bot, e.Dependencies.Store, e.Dependencies.Store, "/settings", pluginURL, - func(userID string) mscalendar.MSCalendar { - return mscalendar.New(e.Env, userID) + func(userID string) engine.Engine { + return engine.New(e.Env, userID) }, + e.Provider.Features, ) - welcomeFlow := mscalendar.NewWelcomeFlow(e.bot, e.Dependencies.Welcomer) + welcomeFlow := engine.NewWelcomeFlow(e.bot, e.Dependencies.Welcomer, e.Provider.Features) e.bot.RegisterFlow(welcomeFlow, mscalendarBot) - if e.notificationProcessor == nil { - e.notificationProcessor = mscalendar.NewNotificationProcessor(e.Env) - } else { - e.notificationProcessor.Configure(e.Env) + if e.Provider.Features.EventNotifications { + if e.notificationProcessor == nil { + e.notificationProcessor = engine.NewNotificationProcessor(e.Env) + } else { + e.notificationProcessor.Configure(e.Env) + } } e.httpHandler = httputils.NewHandler() - oauth2connect.Init(e.httpHandler, mscalendar.NewOAuth2App(e.Env)) + oauth2connect.Init(e.httpHandler, engine.NewOAuth2App(e.Env), config.Provider) flow.Init(e.httpHandler, welcomeFlow, mscalendarBot) settingspanel.Init(e.httpHandler, e.Dependencies.SettingsPanel) api.Init(e.httpHandler, e.Env, e.notificationProcessor) @@ -232,11 +232,11 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo } command := command.Command{ - Context: c, - Args: args, - ChannelID: args.ChannelId, - Config: env.Config, - MSCalendar: mscalendar.New(env.Env, args.UserId), + Context: c, + Args: args, + ChannelID: args.ChannelId, + Config: env.Config, + Engine: engine.New(env.Env, args.UserId), } out, mustRedirectToDM, err := command.Handle() if err != nil { @@ -254,14 +254,14 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo if appErr != nil { return nil, model.NewAppError("mscalendarplugin.ExecuteCommand", "Unable to execute command.", nil, appErr.Error(), http.StatusInternalServerError) } - dmURL := fmt.Sprintf("%s/%s/messages/@%s", env.MattermostSiteURL, t.Name, config.BotUserName) + dmURL := fmt.Sprintf("%s/%s/messages/@%s", env.MattermostSiteURL, t.Name, config.Provider.BotUsername) response.GotoLocation = dmURL } return response, nil } -func (p *Plugin) ServeHTTP(pc *plugin.Context, w http.ResponseWriter, req *http.Request) { +func (p *Plugin) ServeHTTP(_ *plugin.Context, w http.ResponseWriter, req *http.Request) { env := p.getEnv() if env.configError != nil { p.API.LogError(env.configError.Error()) @@ -322,11 +322,11 @@ func (p *Plugin) initEnv(e *Env, pluginURL string) error { e.bot = bot.New(p.API, pluginURL) err := e.bot.Ensure( &model.Bot{ - Username: config.BotUserName, - DisplayName: config.BotDisplayName, - Description: config.BotDescription, + Username: e.Provider.BotUsername, + DisplayName: e.Provider.BotDisplayName, + Description: fmt.Sprintf(config.BotDescription, e.Provider.DisplayName), }, - filepath.Join("assets", "profile.png"), + filepath.Join("assets", fmt.Sprintf("profile-%s.png", e.Provider.Name)), ) if err != nil { return errors.Wrap(err, "failed to ensure bot account") diff --git a/server/remote/calendar.go b/calendar/remote/calendar.go similarity index 100% rename from server/remote/calendar.go rename to calendar/remote/calendar.go diff --git a/server/remote/client.go b/calendar/remote/client.go similarity index 71% rename from server/remote/client.go rename to calendar/remote/client.go index 31f2c0bf..d2db0bb9 100644 --- a/server/remote/client.go +++ b/calendar/remote/client.go @@ -9,26 +9,50 @@ import ( ) type Client interface { - AcceptEvent(remoteUserID, eventID string) error - CallFormPost(method, path string, in url.Values, out interface{}) (responseData []byte, err error) - CallJSON(method, path string, in, out interface{}) (responseData []byte, err error) - CreateCalendar(remoteUserID string, calendar *Calendar) (*Calendar, error) - CreateEvent(remoteUserID string, calendarEvent *Event) (*Event, error) - CreateMySubscription(notificationURL string) (*Subscription, error) - DeclineEvent(remoteUserID, eventID string) error - DeleteCalendar(remoteUserID, calendarID string) error - DeleteSubscription(subscriptionID string) error - FindMeetingTimes(remoteUserID string, meetingParams *FindMeetingTimesParameters) (*MeetingTimeSuggestionResults, error) + Core + Calendars + Events + Subscriptions + Utils + Unsupported +} + +type Core interface { + GetMe() (*User, error) +} + +type Calendars interface { + GetEvent(remoteUserID, eventID string) (*Event, error) GetCalendars(remoteUserID string) ([]*Calendar, error) GetDefaultCalendarView(remoteUserID string, startTime, endTime time.Time) ([]*Event, error) DoBatchViewCalendarRequests([]*ViewCalendarParams) ([]*ViewCalendarResponse, error) - GetEvent(remoteUserID, eventID string) (*Event, error) GetMailboxSettings(remoteUserID string) (*MailboxSettings, error) - GetMe() (*User, error) +} + +type Events interface { + CreateEvent(remoteUserID string, calendarEvent *Event) (*Event, error) + AcceptEvent(remoteUserID, eventID string) error + DeclineEvent(remoteUserID, eventID string) error + TentativelyAcceptEvent(remoteUserID, eventID string) error + GetEventsBetweenDates(remoteUserID string, start, end time.Time) ([]*Event, error) +} + +type Subscriptions interface { + CreateMySubscription(notificationURL, remoteUserID string) (*Subscription, error) + DeleteSubscription(sub *Subscription) error GetNotificationData(*Notification) (*Notification, error) - GetSchedule(requests []*ScheduleUserInfo, startTime, endTime *DateTime, availabilityViewInterval int) ([]*ScheduleInformation, error) ListSubscriptions() ([]*Subscription, error) - RenewSubscription(subscriptionID string) (*Subscription, error) - TentativelyAcceptEvent(remoteUserID, eventID string) error + RenewSubscription(notificationURL, remoteUserID string, sub *Subscription) (*Subscription, error) +} + +type Utils interface { GetSuperuserToken() (string, error) + CallFormPost(method, path string, in url.Values, out interface{}) (responseData []byte, err error) + CallJSON(method, path string, in, out interface{}) (responseData []byte, err error) +} + +type Unsupported interface { + CreateCalendar(remoteUserID string, calendar *Calendar) (*Calendar, error) + DeleteCalendar(remoteUserID, calendarID string) error + FindMeetingTimes(remoteUserID string, meetingParams *FindMeetingTimesParameters) (*MeetingTimeSuggestionResults, error) } diff --git a/server/remote/date_time.go b/calendar/remote/date_time.go similarity index 80% rename from server/remote/date_time.go rename to calendar/remote/date_time.go index ec430dba..00cf0c8a 100644 --- a/server/remote/date_time.go +++ b/calendar/remote/date_time.go @@ -6,7 +6,7 @@ package remote import ( "time" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/tz" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/tz" ) type EmailAddress struct { @@ -18,7 +18,10 @@ type DateTime struct { TimeZone string `json:"timeZone,omitempty"` } -const RFC3339NanoNoTimezone = "2006-01-02T15:04:05.999999999" +const ( + RFC3339NanoNoTimezone = "2006-01-02T15:04:05.999999999" + UndefinedDatetime = "n/a" +) // NewDateTime creates a DateTime that is compatible with Microsoft's API. func NewDateTime(t time.Time, timeZone string) *DateTime { @@ -30,10 +33,11 @@ func NewDateTime(t time.Time, timeZone string) *DateTime { } } +// String returns the RFC3339 representation of the provided datetime. UndefinedDatetime if not set. func (dt DateTime) String() string { t := dt.Time() if t.IsZero() { - return "n/a" + return UndefinedDatetime } return t.Format(time.RFC3339) } @@ -41,7 +45,7 @@ func (dt DateTime) String() string { func (dt DateTime) PrettyString() string { t := dt.Time() if t.IsZero() { - return "n/a" + return UndefinedDatetime } return t.Format(time.RFC822) } @@ -73,5 +77,6 @@ func (dt DateTime) Time() time.Time { if err != nil { return time.Time{} } + return t } diff --git a/server/remote/date_time_test.go b/calendar/remote/date_time_test.go similarity index 100% rename from server/remote/date_time_test.go rename to calendar/remote/date_time_test.go diff --git a/server/remote/event.go b/calendar/remote/event.go similarity index 86% rename from server/remote/event.go rename to calendar/remote/event.go index 30d68c98..fe524726 100644 --- a/server/remote/event.go +++ b/calendar/remote/event.go @@ -3,9 +3,17 @@ package remote +const ( + EventResponseStatusNotAnswered = "not_answered" + EventResponseStatusAccepted = "accepted" + EventResponseStatusTentative = "tentative" + EventResponseStatusDeclined = "declined" +) + type Event struct { Start *DateTime `json:"start,omitempty"` Location *Location `json:"location,omitempty"` + Conference *Conference `json:"conference,omitempty"` End *DateTime `json:"end,omitempty"` Organizer *Attendee `json:"organizer,omitempty"` Body *ItemBody `json:"Body,omitempty"` @@ -55,7 +63,13 @@ type Coordinates struct { Longitude float32 `json:"longitude,omitempty"` } +type Conference struct { + Application string `json:"application"` + URL string `json:"url"` +} + type Attendee struct { + RemoteID string `json:"remoteId,omitempty"` Status *EventResponseStatus `json:"status,omitempty"` EmailAddress *EmailAddress `json:"emailAddress,omitempty"` Type string `json:"type,omitempty"` diff --git a/server/remote/meeting.go b/calendar/remote/meeting.go similarity index 100% rename from server/remote/meeting.go rename to calendar/remote/meeting.go diff --git a/server/remote/mock_remote/mock_client.go b/calendar/remote/mock_remote/mock_client.go similarity index 90% rename from server/remote/mock_remote/mock_client.go rename to calendar/remote/mock_remote/mock_client.go index ca6358ab..892d979d 100644 --- a/server/remote/mock_remote/mock_client.go +++ b/calendar/remote/mock_remote/mock_client.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/remote (interfaces: Client) +// Source: github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote (interfaces: Client) // Package mock_remote is a generated GoMock package. package mock_remote @@ -10,7 +10,7 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - remote "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + remote "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) // MockClient is a mock of Client interface. @@ -111,18 +111,18 @@ func (mr *MockClientMockRecorder) CreateEvent(arg0, arg1 interface{}) *gomock.Ca } // CreateMySubscription mocks base method. -func (m *MockClient) CreateMySubscription(arg0 string) (*remote.Subscription, error) { +func (m *MockClient) CreateMySubscription(arg0, arg1 string) (*remote.Subscription, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateMySubscription", arg0) + ret := m.ctrl.Call(m, "CreateMySubscription", arg0, arg1) ret0, _ := ret[0].(*remote.Subscription) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateMySubscription indicates an expected call of CreateMySubscription. -func (mr *MockClientMockRecorder) CreateMySubscription(arg0 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) CreateMySubscription(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMySubscription", reflect.TypeOf((*MockClient)(nil).CreateMySubscription), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMySubscription", reflect.TypeOf((*MockClient)(nil).CreateMySubscription), arg0, arg1) } // DeclineEvent mocks base method. @@ -154,7 +154,7 @@ func (mr *MockClientMockRecorder) DeleteCalendar(arg0, arg1 interface{}) *gomock } // DeleteSubscription mocks base method. -func (m *MockClient) DeleteSubscription(arg0 string) error { +func (m *MockClient) DeleteSubscription(arg0 *remote.Subscription) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteSubscription", arg0) ret0, _ := ret[0].(error) @@ -242,6 +242,21 @@ func (mr *MockClientMockRecorder) GetEvent(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvent", reflect.TypeOf((*MockClient)(nil).GetEvent), arg0, arg1) } +// GetEventsBetweenDates mocks base method. +func (m *MockClient) GetEventsBetweenDates(arg0 string, arg1, arg2 time.Time) ([]*remote.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEventsBetweenDates", arg0, arg1, arg2) + ret0, _ := ret[0].([]*remote.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEventsBetweenDates indicates an expected call of GetEventsBetweenDates. +func (mr *MockClientMockRecorder) GetEventsBetweenDates(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEventsBetweenDates", reflect.TypeOf((*MockClient)(nil).GetEventsBetweenDates), arg0, arg1, arg2) +} + // GetMailboxSettings mocks base method. func (m *MockClient) GetMailboxSettings(arg0 string) (*remote.MailboxSettings, error) { m.ctrl.T.Helper() @@ -287,21 +302,6 @@ func (mr *MockClientMockRecorder) GetNotificationData(arg0 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationData", reflect.TypeOf((*MockClient)(nil).GetNotificationData), arg0) } -// GetSchedule mocks base method. -func (m *MockClient) GetSchedule(arg0 []*remote.ScheduleUserInfo, arg1, arg2 *remote.DateTime, arg3 int) ([]*remote.ScheduleInformation, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSchedule", arg0, arg1, arg2, arg3) - 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 { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchedule", reflect.TypeOf((*MockClient)(nil).GetSchedule), arg0, arg1, arg2, arg3) -} - // GetSuperuserToken mocks base method. func (m *MockClient) GetSuperuserToken() (string, error) { m.ctrl.T.Helper() @@ -333,18 +333,18 @@ func (mr *MockClientMockRecorder) ListSubscriptions() *gomock.Call { } // RenewSubscription mocks base method. -func (m *MockClient) RenewSubscription(arg0 string) (*remote.Subscription, error) { +func (m *MockClient) RenewSubscription(arg0, arg1 string, arg2 *remote.Subscription) (*remote.Subscription, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RenewSubscription", arg0) + ret := m.ctrl.Call(m, "RenewSubscription", arg0, arg1, arg2) ret0, _ := ret[0].(*remote.Subscription) ret1, _ := ret[1].(error) return ret0, ret1 } // RenewSubscription indicates an expected call of RenewSubscription. -func (mr *MockClientMockRecorder) RenewSubscription(arg0 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) RenewSubscription(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewSubscription", reflect.TypeOf((*MockClient)(nil).RenewSubscription), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewSubscription", reflect.TypeOf((*MockClient)(nil).RenewSubscription), arg0, arg1, arg2) } // TentativelyAcceptEvent mocks base method. diff --git a/server/remote/mock_remote/mock_remote.go b/calendar/remote/mock_remote/mock_remote.go similarity index 79% rename from server/remote/mock_remote/mock_remote.go rename to calendar/remote/mock_remote/mock_remote.go index 934c852e..7bd13064 100644 --- a/server/remote/mock_remote/mock_remote.go +++ b/calendar/remote/mock_remote/mock_remote.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/remote (interfaces: Remote) +// Source: github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote (interfaces: Remote) // Package mock_remote is a generated GoMock package. package mock_remote @@ -10,7 +10,8 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - remote "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + config "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + remote "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" oauth2 "golang.org/x/oauth2" ) @@ -37,6 +38,20 @@ func (m *MockRemote) EXPECT() *MockRemoteMockRecorder { return m.recorder } +// CheckConfiguration mocks base method. +func (m *MockRemote) CheckConfiguration(arg0 config.StoredConfig) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckConfiguration", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckConfiguration indicates an expected call of CheckConfiguration. +func (mr *MockRemoteMockRecorder) CheckConfiguration(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckConfiguration", reflect.TypeOf((*MockRemote)(nil).CheckConfiguration), arg0) +} + // HandleWebhook mocks base method. func (m *MockRemote) HandleWebhook(arg0 http.ResponseWriter, arg1 *http.Request) []*remote.Notification { m.ctrl.T.Helper() diff --git a/server/remote/notification.go b/calendar/remote/notification.go similarity index 100% rename from server/remote/notification.go rename to calendar/remote/notification.go diff --git a/server/remote/remote.go b/calendar/remote/remote.go similarity index 60% rename from server/remote/remote.go rename to calendar/remote/remote.go index 1d77e7ba..919c40e4 100644 --- a/server/remote/remote.go +++ b/calendar/remote/remote.go @@ -5,12 +5,18 @@ package remote import ( "context" + "errors" "net/http" "golang.org/x/oauth2" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" +) + +var ( + ErrSuperUserClientNotSupported = errors.New("superuser client is not supported") + ErrNotImplemented = errors.New("not implemented") ) type Remote interface { @@ -18,6 +24,7 @@ type Remote interface { MakeSuperuserClient(ctx context.Context) (Client, error) NewOAuth2Config() *oauth2.Config HandleWebhook(http.ResponseWriter, *http.Request) []*Notification + CheckConfiguration(configuration config.StoredConfig) error } var Makers = map[string]func(*config.Config, bot.Logger) Remote{} diff --git a/server/remote/schedule.go b/calendar/remote/schedule.go similarity index 100% rename from server/remote/schedule.go rename to calendar/remote/schedule.go diff --git a/server/remote/subscription.go b/calendar/remote/subscription.go similarity index 91% rename from server/remote/subscription.go rename to calendar/remote/subscription.go index 882d05a6..c88d037c 100644 --- a/server/remote/subscription.go +++ b/calendar/remote/subscription.go @@ -5,6 +5,7 @@ package remote type Subscription struct { ID string `json:"id"` + ResourceID string `json:"resourceId,omitempty"` Resource string `json:"resource,omitempty"` ApplicationID string `json:"applicationId,omitempty"` ChangeType string `json:"changeType,omitempty"` diff --git a/server/remote/user.go b/calendar/remote/user.go similarity index 100% rename from server/remote/user.go rename to calendar/remote/user.go diff --git a/server/store/event_store.go b/calendar/store/event_store.go similarity index 52% rename from server/store/event_store.go rename to calendar/store/event_store.go index f983d8cb..fb975811 100644 --- a/server/store/event_store.go +++ b/calendar/store/event_store.go @@ -7,9 +7,11 @@ import ( "encoding/json" "time" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/kvstore" + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/kvstore" ) // If event has an end date/time, its record will be set to expire ttlAfterEventEnd @@ -18,18 +20,30 @@ import ( const ttlAfterEventEnd = 30 * 24 * time.Hour // 30 days const defaultEventTTL = 30 * 24 * time.Hour // 30 days +type EventMetadata struct { + LinkedChannelIDs map[string]struct{} +} + type Event struct { Remote *remote.Event PluginVersion string } type EventStore interface { + LoadEventMetadata(eventID string) (*EventMetadata, error) + StoreEventMetadata(eventID string, eventMeta *EventMetadata) error + DeleteEventMetadata(eventID string) error + + AddLinkedChannelToEvent(eventID, channelID string) error + DeleteLinkedChannelFromEvent(eventID, channelID string) error + LoadUserEvent(mattermostUserID, eventID string) (*Event, error) StoreUserEvent(mattermostUserID string, event *Event) error DeleteUserEvent(mattermostUserID, eventID string) error } func eventKey(mattermostUserID, eventID string) string { return mattermostUserID + "_" + eventID } +func eventMetaKey(eventID string) string { return "metadata_" + eventID } func (s *pluginStore) LoadUserEvent(mattermostUserID, eventID string) (*Event, error) { event := Event{} @@ -40,6 +54,55 @@ func (s *pluginStore) LoadUserEvent(mattermostUserID, eventID string) (*Event, e return &event, nil } +func (s *pluginStore) AddLinkedChannelToEvent(eventID, channelID string) error { + eventMeta, err := s.LoadEventMetadata(eventID) + if err != nil && !errors.Is(err, ErrNotFound) { + return err + } + + if eventMeta == nil { + eventMeta = &EventMetadata{ + LinkedChannelIDs: make(map[string]struct{}, 1), + } + } + + eventMeta.LinkedChannelIDs[channelID] = struct{}{} + + return s.StoreEventMetadata(eventID, eventMeta) +} + +func (s *pluginStore) DeleteLinkedChannelFromEvent(eventID, channelID string) error { + eventMeta, err := s.LoadEventMetadata(eventID) + if err != nil && !errors.Is(err, ErrNotFound) { + return err + } + + delete(eventMeta.LinkedChannelIDs, channelID) + + return s.StoreEventMetadata(eventID, eventMeta) +} + +func (s *pluginStore) StoreEventMetadata(eventID string, eventMeta *EventMetadata) error { + err := kvstore.StoreJSON(s.eventKV, eventMetaKey(eventID), &eventMeta) + if err != nil { + return errors.Wrap(err, "error storing event metadata") + } + return nil +} + +func (s *pluginStore) LoadEventMetadata(eventID string) (*EventMetadata, error) { + event := EventMetadata{} + err := kvstore.LoadJSON(s.eventKV, eventMetaKey(eventID), &event) + if err != nil { + return nil, err + } + return &event, nil +} + +func (s *pluginStore) DeleteEventMetadata(eventID string) error { + return s.eventKV.Delete(eventMetaKey(eventID)) +} + func (s *pluginStore) StoreUserEvent(mattermostUserID string, event *Event) error { now := time.Now() end := now.Add(defaultEventTTL) diff --git a/server/store/flow_store.go b/calendar/store/flow_store.go similarity index 100% rename from server/store/flow_store.go rename to calendar/store/flow_store.go diff --git a/server/store/mock_store/mock_store.go b/calendar/store/mock_store/mock_store.go similarity index 81% rename from server/store/mock_store/mock_store.go rename to calendar/store/mock_store/mock_store.go index bcfe3f37..8be1b1d8 100644 --- a/server/store/mock_store/mock_store.go +++ b/calendar/store/mock_store/mock_store.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/store (interfaces: Store) +// Source: github.com/mattermost/mattermost-plugin-mscalendar/calendar/store (interfaces: Store) // Package mock_store is a generated GoMock package. package mock_store @@ -8,7 +8,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - store "github.com/mattermost/mattermost-plugin-mscalendar/server/store" + store "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" ) // MockStore is a mock of Store interface. @@ -34,6 +34,20 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder { return m.recorder } +// AddLinkedChannelToEvent mocks base method. +func (m *MockStore) AddLinkedChannelToEvent(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddLinkedChannelToEvent", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddLinkedChannelToEvent indicates an expected call of AddLinkedChannelToEvent. +func (mr *MockStoreMockRecorder) AddLinkedChannelToEvent(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLinkedChannelToEvent", reflect.TypeOf((*MockStore)(nil).AddLinkedChannelToEvent), arg0, arg1) +} + // DeleteCurrentStep mocks base method. func (m *MockStore) DeleteCurrentStep(arg0 string) error { m.ctrl.T.Helper() @@ -48,6 +62,34 @@ func (mr *MockStoreMockRecorder) DeleteCurrentStep(arg0 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCurrentStep", reflect.TypeOf((*MockStore)(nil).DeleteCurrentStep), arg0) } +// DeleteEventMetadata mocks base method. +func (m *MockStore) DeleteEventMetadata(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteEventMetadata", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteEventMetadata indicates an expected call of DeleteEventMetadata. +func (mr *MockStoreMockRecorder) DeleteEventMetadata(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEventMetadata", reflect.TypeOf((*MockStore)(nil).DeleteEventMetadata), arg0) +} + +// DeleteLinkedChannelFromEvent mocks base method. +func (m *MockStore) DeleteLinkedChannelFromEvent(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteLinkedChannelFromEvent", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteLinkedChannelFromEvent indicates an expected call of DeleteLinkedChannelFromEvent. +func (mr *MockStoreMockRecorder) DeleteLinkedChannelFromEvent(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLinkedChannelFromEvent", reflect.TypeOf((*MockStore)(nil).DeleteLinkedChannelFromEvent), arg0, arg1) +} + // DeletePanelPostID mocks base method. func (m *MockStore) DeletePanelPostID(arg0 string) error { m.ctrl.T.Helper() @@ -193,6 +235,21 @@ func (mr *MockStoreMockRecorder) GetSetting(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSetting", reflect.TypeOf((*MockStore)(nil).GetSetting), arg0, arg1) } +// LoadEventMetadata mocks base method. +func (m *MockStore) LoadEventMetadata(arg0 string) (*store.EventMetadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadEventMetadata", arg0) + ret0, _ := ret[0].(*store.EventMetadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadEventMetadata indicates an expected call of LoadEventMetadata. +func (mr *MockStoreMockRecorder) LoadEventMetadata(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadEventMetadata", reflect.TypeOf((*MockStore)(nil).LoadEventMetadata), arg0) +} + // LoadMattermostUserID mocks base method. func (m *MockStore) LoadMattermostUserID(arg0 string) (string, error) { m.ctrl.T.Helper() @@ -326,6 +383,21 @@ func (mr *MockStoreMockRecorder) RemovePostID(arg0, arg1 interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePostID", reflect.TypeOf((*MockStore)(nil).RemovePostID), arg0, arg1) } +// SearchInUserIndex mocks base method. +func (m *MockStore) SearchInUserIndex(arg0 string, arg1 int) (store.UserIndex, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchInUserIndex", arg0, arg1) + ret0, _ := ret[0].(store.UserIndex) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchInUserIndex indicates an expected call of SearchInUserIndex. +func (mr *MockStoreMockRecorder) SearchInUserIndex(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchInUserIndex", reflect.TypeOf((*MockStore)(nil).SearchInUserIndex), arg0, arg1) +} + // SetCurrentStep mocks base method. func (m *MockStore) SetCurrentStep(arg0 string, arg1 int) error { m.ctrl.T.Helper() @@ -396,6 +468,20 @@ func (mr *MockStoreMockRecorder) SetSetting(arg0, arg1, arg2 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSetting", reflect.TypeOf((*MockStore)(nil).SetSetting), arg0, arg1, arg2) } +// StoreEventMetadata mocks base method. +func (m *MockStore) StoreEventMetadata(arg0 string, arg1 *store.EventMetadata) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StoreEventMetadata", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// StoreEventMetadata indicates an expected call of StoreEventMetadata. +func (mr *MockStoreMockRecorder) StoreEventMetadata(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreEventMetadata", reflect.TypeOf((*MockStore)(nil).StoreEventMetadata), arg0, arg1) +} + // StoreOAuth2State mocks base method. func (m *MockStore) StoreOAuth2State(arg0 string) error { m.ctrl.T.Helper() @@ -466,6 +552,20 @@ func (mr *MockStoreMockRecorder) StoreUserInIndex(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreUserInIndex", reflect.TypeOf((*MockStore)(nil).StoreUserInIndex), arg0) } +// StoreUserLinkedEvent mocks base method. +func (m *MockStore) StoreUserLinkedEvent(arg0, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StoreUserLinkedEvent", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// StoreUserLinkedEvent indicates an expected call of StoreUserLinkedEvent. +func (mr *MockStoreMockRecorder) StoreUserLinkedEvent(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreUserLinkedEvent", reflect.TypeOf((*MockStore)(nil).StoreUserLinkedEvent), arg0, arg1, arg2) +} + // StoreUserSubscription mocks base method. func (m *MockStore) StoreUserSubscription(arg0 *store.User, arg1 *store.Subscription) error { m.ctrl.T.Helper() diff --git a/server/store/oauth2_store.go b/calendar/store/oauth2_store.go similarity index 100% rename from server/store/oauth2_store.go rename to calendar/store/oauth2_store.go diff --git a/server/store/setting_store.go b/calendar/store/setting_store.go similarity index 97% rename from server/store/setting_store.go rename to calendar/store/setting_store.go index 94f2afb7..5f2c3850 100644 --- a/server/store/setting_store.go +++ b/calendar/store/setting_store.go @@ -26,7 +26,9 @@ func (s *pluginStore) SetSetting(userID, settingID string, value interface{}) er return fmt.Errorf("cannot read value %v for setting %s (expecting bool)", value, settingID) } user.Settings.UpdateStatus = storableValue - s.Tracker.TrackAutomaticStatusUpdate(userID, storableValue, "settings") + if s.Tracker != nil { + s.Tracker.TrackAutomaticStatusUpdate(userID, storableValue, "settings") + } case GetConfirmationSettingID: storableValue, ok := value.(bool) if !ok { @@ -85,6 +87,7 @@ func (s *pluginStore) GetSetting(userID, settingID string) (interface{}, error) func DefaultDailySummaryUserSettings() *DailySummaryUserSettings { return &DailySummaryUserSettings{ PostTime: "8:00AM", + Timezone: "Eastern Standard Time", Enable: false, } diff --git a/server/store/store.go b/calendar/store/store.go similarity index 69% rename from server/store/store.go rename to calendar/store/store.go index 2391c8ce..05b6dca6 100644 --- a/server/store/store.go +++ b/calendar/store/store.go @@ -8,11 +8,11 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" - "github.com/mattermost/mattermost-plugin-mscalendar/server/tracker" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/flow" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/kvstore" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/settingspanel" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/tracker" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/flow" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/kvstore" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/settingspanel" ) const ( @@ -55,16 +55,24 @@ type pluginStore struct { Tracker tracker.Tracker } -func NewPluginStore(api plugin.API, logger bot.Logger, tracker tracker.Tracker) Store { +func NewPluginStore(api plugin.API, logger bot.Logger, tracker tracker.Tracker, enableEncryption bool, encryptionKey []byte) Store { basicKV := kvstore.NewPluginStore(api) + oauth2KV := kvstore.NewHashedKeyStore(kvstore.NewOneTimePluginStore(api, OAuth2KeyExpiration), OAuth2KeyPrefix) + user2KV := kvstore.NewHashedKeyStore(basicKV, UserKeyPrefix) + + if enableEncryption { + oauth2KV = kvstore.NewEncryptedKeyStore(oauth2KV, encryptionKey) + user2KV = kvstore.NewEncryptedKeyStore(user2KV, encryptionKey) + } + return &pluginStore{ basicKV: basicKV, - userKV: kvstore.NewHashedKeyStore(basicKV, UserKeyPrefix), + userKV: user2KV, userIndexKV: kvstore.NewHashedKeyStore(basicKV, UserIndexKeyPrefix), mattermostUserIDKV: kvstore.NewHashedKeyStore(basicKV, MattermostUserIDKeyPrefix), subscriptionKV: kvstore.NewHashedKeyStore(basicKV, SubscriptionKeyPrefix), eventKV: kvstore.NewHashedKeyStore(basicKV, EventKeyPrefix), - oauth2KV: kvstore.NewHashedKeyStore(kvstore.NewOneTimePluginStore(api, OAuth2KeyExpiration), OAuth2KeyPrefix), + oauth2KV: oauth2KV, welcomeIndexKV: kvstore.NewHashedKeyStore(basicKV, WelcomeKeyPrefix), settingsPanelKV: kvstore.NewHashedKeyStore(basicKV, SettingsPanelPrefix), Logger: logger, diff --git a/server/store/subscription_store.go b/calendar/store/subscription_store.go similarity index 90% rename from server/store/subscription_store.go rename to calendar/store/subscription_store.go index 71be53bd..d85f5ec9 100644 --- a/server/store/subscription_store.go +++ b/calendar/store/subscription_store.go @@ -6,9 +6,9 @@ package store import ( "fmt" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/kvstore" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/kvstore" ) type SubscriptionStore interface { diff --git a/server/store/user_store.go b/calendar/store/user_store.go similarity index 67% rename from server/store/user_store.go rename to calendar/store/user_store.go index 0149b7cc..84f42288 100644 --- a/server/store/user_store.go +++ b/calendar/store/user_store.go @@ -6,17 +6,22 @@ package store import ( "encoding/json" "fmt" + "strings" + "github.com/pkg/errors" "golang.org/x/oauth2" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/kvstore" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/kvstore" ) +type ChannelEventLink map[string]string + type UserStore interface { LoadUser(mattermostUserID string) (*User, error) LoadMattermostUserID(remoteUserID string) (string, error) LoadUserIndex() (UserIndex, error) + SearchInUserIndex(term string, limit int) (UserIndex, error) StoreUser(user *User) error LoadUserFromIndex(mattermostUserID string) (*UserShort, error) DeleteUser(mattermostUserID string) error @@ -24,25 +29,54 @@ type UserStore interface { StoreUserInIndex(user *User) error DeleteUserFromIndex(mattermostUserID string) error StoreUserActiveEvents(mattermostUserID string, events []string) error + StoreUserLinkedEvent(mattermostUserID, eventID, channelID string) error } type UserIndex []*UserShort type UserShort struct { - MattermostUserID string `json:"mm_id"` - RemoteID string `json:"remote_id"` - Email string `json:"email"` + MattermostUsername string `json:"mm_username"` + MattermostDisplayName string `json:"mm_display_name"` + MattermostUserID string `json:"mm_id"` + RemoteID string `json:"remote_id"` + Email string `json:"email"` +} + +func (us UserShort) Matches(term string) bool { + return strings.Contains(us.MattermostUsername, term) || strings.Contains(us.MattermostDisplayName, term) || strings.Contains(us.Email, term) +} + +func (us UserShort) ToDTO() UserShortDTO { + return UserShortDTO{ + MattermostUserID: us.MattermostUserID, + MattermostUsername: us.MattermostUsername, + MattermostDisplayName: us.MattermostDisplayName, + } +} + +type UserShortDTO struct { + MattermostUserID string `json:"mm_id"` + MattermostUsername string `json:"mm_username"` + MattermostDisplayName string `json:"mm_display_name"` } type User struct { - Settings Settings `json:"mattermostSettings,omitempty"` - Remote *remote.User - OAuth2Token *oauth2.Token - PluginVersion string - MattermostUserID string - LastStatus string - WelcomeFlowStatus WelcomeFlowStatus `json:"mattermostFlags,omitempty"` - ActiveEvents []string `json:"events"` + Settings Settings `json:"mattermostSettings,omitempty"` + Remote *remote.User + OAuth2Token *oauth2.Token + PluginVersion string + MattermostUserID string + MattermostUsername string + MattermostDisplayName string + LastStatus string + WelcomeFlowStatus WelcomeFlowStatus `json:"mattermostFlags,omitempty"` + ActiveEvents []string `json:"events"` + ChannelEvents ChannelEventLink `json:"linkedEvents,omitempty"` +} + +var DefaultSettings = Settings{ + GetConfirmation: false, + ReceiveNotificationsDuringMeeting: true, } type Settings struct { @@ -200,9 +234,11 @@ func (s *pluginStore) ModifyUserIndex(modify func(userIndex UserIndex) (UserInde func (s *pluginStore) StoreUserInIndex(user *User) error { return s.ModifyUserIndex(func(userIndex UserIndex) (UserIndex, error) { newUser := &UserShort{ - MattermostUserID: user.MattermostUserID, - RemoteID: user.Remote.ID, - Email: user.Remote.Mail, + MattermostUserID: user.MattermostUserID, + MattermostUsername: user.MattermostUsername, + MattermostDisplayName: user.MattermostDisplayName, + RemoteID: user.Remote.ID, + Email: user.Remote.Mail, } for i, u := range userIndex { @@ -230,6 +266,26 @@ func (s *pluginStore) DeleteUserFromIndex(mattermostUserID string) error { }) } +func (s *pluginStore) SearchInUserIndex(term string, limit int) (UserIndex, error) { + userIndex, err := s.LoadUserIndex() + if err != nil { + return nil, errors.Wrap(err, "error searching user in index") + } + + result := []*UserShort{} + for idx, u := range userIndex { + if u.Matches(term) { + result = append(result, userIndex[idx]) + } + + if len(result) == limit { + break + } + } + + return result, nil +} + func (s *pluginStore) StoreUserActiveEvents(mattermostUserID string, events []string) error { u, err := s.LoadUser(mattermostUserID) if err != nil { @@ -239,6 +295,29 @@ func (s *pluginStore) StoreUserActiveEvents(mattermostUserID string, events []st return kvstore.StoreJSON(s.userKV, mattermostUserID, u) } +func (s *pluginStore) StoreUserLinkedEvent(mattermostUserID, eventID, channelID string) error { + u, err := s.LoadUser(mattermostUserID) + if err != nil { + return err + } + + if u.ChannelEvents == nil { + u.ChannelEvents = make(ChannelEventLink, 1) + } + + u.ChannelEvents[eventID] = channelID + + return kvstore.StoreJSON(s.userKV, mattermostUserID, u) +} + +func (index UserIndex) ToDTO() (result []UserShortDTO) { + for _, u := range index { + result = append(result, u.ToDTO()) + } + + return +} + func (index UserIndex) ByMattermostID() map[string]*UserShort { result := map[string]*UserShort{} diff --git a/server/store/welcome_store.go b/calendar/store/welcome_store.go similarity index 91% rename from server/store/welcome_store.go rename to calendar/store/welcome_store.go index e7c3d08e..9ea735df 100644 --- a/server/store/welcome_store.go +++ b/calendar/store/welcome_store.go @@ -1,6 +1,6 @@ package store -import "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/kvstore" +import "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/kvstore" type WelcomeStore interface { LoadUserWelcomePost(mattermostID string) (string, error) diff --git a/server/telemetry/logger.go b/calendar/telemetry/logger.go similarity index 100% rename from server/telemetry/logger.go rename to calendar/telemetry/logger.go diff --git a/server/telemetry/rudder.go b/calendar/telemetry/rudder.go similarity index 100% rename from server/telemetry/rudder.go rename to calendar/telemetry/rudder.go diff --git a/server/telemetry/tracker.go b/calendar/telemetry/tracker.go similarity index 100% rename from server/telemetry/tracker.go rename to calendar/telemetry/tracker.go diff --git a/server/testdata/batch_requests.json b/calendar/testdata/batch_requests.json similarity index 100% rename from server/testdata/batch_requests.json rename to calendar/testdata/batch_requests.json diff --git a/server/testdata/get_calendar_view_response.json b/calendar/testdata/get_calendar_view_response.json similarity index 100% rename from server/testdata/get_calendar_view_response.json rename to calendar/testdata/get_calendar_view_response.json diff --git a/server/testdata/get_mainbox_settings_response.json b/calendar/testdata/get_mainbox_settings_response.json similarity index 100% rename from server/testdata/get_mainbox_settings_response.json rename to calendar/testdata/get_mainbox_settings_response.json diff --git a/server/testdata/get_me_response.json b/calendar/testdata/get_me_response.json similarity index 100% rename from server/testdata/get_me_response.json rename to calendar/testdata/get_me_response.json diff --git a/server/testdata/get_schedule_response_batch.json b/calendar/testdata/get_schedule_response_batch.json similarity index 100% rename from server/testdata/get_schedule_response_batch.json rename to calendar/testdata/get_schedule_response_batch.json diff --git a/server/testdata/get_schedule_response_busy.json b/calendar/testdata/get_schedule_response_busy.json similarity index 100% rename from server/testdata/get_schedule_response_busy.json rename to calendar/testdata/get_schedule_response_busy.json diff --git a/server/testdata/get_schedule_response_free.json b/calendar/testdata/get_schedule_response_free.json similarity index 100% rename from server/testdata/get_schedule_response_free.json rename to calendar/testdata/get_schedule_response_free.json diff --git a/server/testdata/get_schedule_response_invalid_email.json b/calendar/testdata/get_schedule_response_invalid_email.json similarity index 100% rename from server/testdata/get_schedule_response_invalid_email.json rename to calendar/testdata/get_schedule_response_invalid_email.json diff --git a/server/testdata/webhook_event_ notification.json b/calendar/testdata/webhook_event_ notification.json similarity index 100% rename from server/testdata/webhook_event_ notification.json rename to calendar/testdata/webhook_event_ notification.json diff --git a/server/tracker/tracker.go b/calendar/tracker/tracker.go similarity index 96% rename from server/tracker/tracker.go rename to calendar/tracker/tracker.go index b0c6b185..e8a8ff85 100644 --- a/server/tracker/tracker.go +++ b/calendar/tracker/tracker.go @@ -3,7 +3,7 @@ package tracker import ( "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost-plugin-mscalendar/server/telemetry" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/telemetry" ) const ( diff --git a/server/utils/bot/admin.go b/calendar/utils/bot/admin.go similarity index 100% rename from server/utils/bot/admin.go rename to calendar/utils/bot/admin.go diff --git a/server/utils/bot/bot.go b/calendar/utils/bot/bot.go similarity index 95% rename from server/utils/bot/bot.go rename to calendar/utils/bot/bot.go index 7d8ee901..bba8afe7 100644 --- a/server/utils/bot/bot.go +++ b/calendar/utils/bot/bot.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/pluginapi" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/flow" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/flow" ) type Bot interface { diff --git a/server/utils/bot/bot_config.go b/calendar/utils/bot/bot_config.go similarity index 100% rename from server/utils/bot/bot_config.go rename to calendar/utils/bot/bot_config.go diff --git a/server/utils/bot/flow_controller.go b/calendar/utils/bot/flow_controller.go similarity index 96% rename from server/utils/bot/flow_controller.go rename to calendar/utils/bot/flow_controller.go index c33d126e..9761f450 100644 --- a/server/utils/bot/flow_controller.go +++ b/calendar/utils/bot/flow_controller.go @@ -1,6 +1,6 @@ package bot -import "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/flow" +import "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/flow" type FlowController interface { Start(userID string) error diff --git a/server/utils/bot/logger.go b/calendar/utils/bot/logger.go similarity index 88% rename from server/utils/bot/logger.go rename to calendar/utils/bot/logger.go index e2029bf4..721aa1ce 100644 --- a/server/utils/bot/logger.go +++ b/calendar/utils/bot/logger.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" ) const timed = "__since" @@ -109,12 +109,12 @@ func (bot *bot) logToAdmins(level, message string) { type NilLogger struct{} -func (l *NilLogger) With(logContext LogContext) Logger { return l } -func (l *NilLogger) Timed() Logger { return l } -func (l *NilLogger) Debugf(format string, args ...interface{}) {} -func (l *NilLogger) Errorf(format string, args ...interface{}) {} -func (l *NilLogger) Infof(format string, args ...interface{}) {} -func (l *NilLogger) Warnf(format string, args ...interface{}) {} +func (l *NilLogger) With(_ LogContext) Logger { return l } +func (l *NilLogger) Timed() Logger { return l } +func (l *NilLogger) Debugf(_ string, _ ...interface{}) {} +func (l *NilLogger) Errorf(_ string, _ ...interface{}) {} +func (l *NilLogger) Infof(_ string, _ ...interface{}) {} +func (l *NilLogger) Warnf(_ string, _ ...interface{}) {} type TestLogger struct { testing.TB diff --git a/server/utils/bot/mock_bot/mock_admin.go b/calendar/utils/bot/mock_bot/mock_admin.go similarity index 92% rename from server/utils/bot/mock_bot/mock_admin.go rename to calendar/utils/bot/mock_bot/mock_admin.go index 25219de9..25777269 100644 --- a/server/utils/bot/mock_bot/mock_admin.go +++ b/calendar/utils/bot/mock_bot/mock_admin.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot (interfaces: Admin) +// Source: github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot (interfaces: Admin) // Package mock_bot is a generated GoMock package. package mock_bot diff --git a/server/utils/bot/mock_bot/mock_logger.go b/calendar/utils/bot/mock_bot/mock_logger.go similarity index 95% rename from server/utils/bot/mock_bot/mock_logger.go rename to calendar/utils/bot/mock_bot/mock_logger.go index da470dc6..f0cd4abf 100644 --- a/server/utils/bot/mock_bot/mock_logger.go +++ b/calendar/utils/bot/mock_bot/mock_logger.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot (interfaces: Logger) +// Source: github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot (interfaces: Logger) // Package mock_bot is a generated GoMock package. package mock_bot @@ -8,7 +8,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - bot "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + bot "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) // MockLogger is a mock of Logger interface. diff --git a/server/utils/bot/mock_bot/mock_poster.go b/calendar/utils/bot/mock_bot/mock_poster.go similarity index 75% rename from server/utils/bot/mock_bot/mock_poster.go rename to calendar/utils/bot/mock_bot/mock_poster.go index 39e8ac08..df5409ea 100644 --- a/server/utils/bot/mock_bot/mock_poster.go +++ b/calendar/utils/bot/mock_bot/mock_poster.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot (interfaces: Poster) +// Source: github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot (interfaces: Poster) // Package mock_bot is a generated GoMock package. package mock_bot @@ -34,6 +34,20 @@ func (m *MockPoster) EXPECT() *MockPosterMockRecorder { return m.recorder } +// CreatePost mocks base method. +func (m *MockPoster) CreatePost(arg0 *model.Post) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePost", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreatePost indicates an expected call of CreatePost. +func (mr *MockPosterMockRecorder) CreatePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockPoster)(nil).CreatePost), arg0) +} + // DM mocks base method. func (m *MockPoster) DM(arg0, arg1 string, arg2 ...interface{}) (string, error) { m.ctrl.T.Helper() @@ -93,6 +107,26 @@ func (mr *MockPosterMockRecorder) DMWithAttachments(arg0 interface{}, arg1 ...in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DMWithAttachments", reflect.TypeOf((*MockPoster)(nil).DMWithAttachments), varargs...) } +// DMWithMessageAndAttachments mocks base method. +func (m *MockPoster) DMWithMessageAndAttachments(arg0, arg1 string, arg2 ...*model.SlackAttachment) (string, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DMWithMessageAndAttachments", varargs...) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DMWithMessageAndAttachments indicates an expected call of DMWithMessageAndAttachments. +func (mr *MockPosterMockRecorder) DMWithMessageAndAttachments(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DMWithMessageAndAttachments", reflect.TypeOf((*MockPoster)(nil).DMWithMessageAndAttachments), varargs...) +} + // DeletePost mocks base method. func (m *MockPoster) DeletePost(arg0 string) error { m.ctrl.T.Helper() diff --git a/server/utils/bot/poster.go b/calendar/utils/bot/poster.go similarity index 79% rename from server/utils/bot/poster.go rename to calendar/utils/bot/poster.go index 7657eb2d..eccbb0ce 100644 --- a/server/utils/bot/poster.go +++ b/calendar/utils/bot/poster.go @@ -18,12 +18,18 @@ type Poster interface { // Often used to include post actions. DMWithAttachments(mattermostUserID string, attachments ...*model.SlackAttachment) (string, error) + // DMWithMessageAndAttachments posts a Direct Message that contains Slack attachments and a message. + DMWithMessageAndAttachments(mattermostUserID, message string, attachments ...*model.SlackAttachment) (string, error) + // Ephemeral sends an ephemeral message to a user Ephemeral(mattermostUserID, channelID, format string, args ...interface{}) // DMPUpdate updates the postID with the formatted message DMUpdate(postID, format string, args ...interface{}) error + // CreatePost creates a post + CreatePost(post *model.Post) error + // DeletePost deletes a single post DeletePost(postID string) error @@ -50,6 +56,13 @@ func (bot *bot) DMWithAttachments(mattermostUserID string, attachments ...*model return bot.dm(mattermostUserID, &post) } +// DMWithMessageAndAttachments posts a Direct Message that contains Slack attachments and a message. +func (bot *bot) DMWithMessageAndAttachments(mattermostUserID, message string, attachments ...*model.SlackAttachment) (string, error) { + post := model.Post{Message: message} + model.ParseSlackAttachment(&post, attachments) + return bot.dm(mattermostUserID, &post) +} + func (bot *bot) dm(mattermostUserID string, post *model.Post) (string, error) { channel, err := bot.pluginAPI.GetDirectChannel(mattermostUserID, bot.mattermostUserID) if err != nil { @@ -103,6 +116,16 @@ func (bot *bot) DMUpdate(postID, format string, args ...interface{}) error { return nil } +func (bot *bot) CreatePost(post *model.Post) error { + post.UserId = bot.mattermostUserID + _, appErr := bot.pluginAPI.CreatePost(post) + if appErr != nil { + return appErr + } + + return nil +} + func (bot *bot) DeletePost(postID string) error { appErr := bot.pluginAPI.DeletePost(postID) if appErr != nil { diff --git a/server/utils/byte_size.go b/calendar/utils/byte_size.go similarity index 100% rename from server/utils/byte_size.go rename to calendar/utils/byte_size.go diff --git a/server/utils/byte_size_test.go b/calendar/utils/byte_size_test.go similarity index 100% rename from server/utils/byte_size_test.go rename to calendar/utils/byte_size_test.go diff --git a/server/utils/fields/fields.go b/calendar/utils/fields/fields.go similarity index 100% rename from server/utils/fields/fields.go rename to calendar/utils/fields/fields.go diff --git a/server/utils/fields/value.go b/calendar/utils/fields/value.go similarity index 100% rename from server/utils/fields/value.go rename to calendar/utils/fields/value.go diff --git a/server/utils/flow/empty_step.go b/calendar/utils/flow/empty_step.go similarity index 66% rename from server/utils/flow/empty_step.go rename to calendar/utils/flow/empty_step.go index 3cdd1aac..f10bd19d 100644 --- a/server/utils/flow/empty_step.go +++ b/calendar/utils/flow/empty_step.go @@ -11,7 +11,7 @@ type EmptyStep struct { Message string } -func (s *EmptyStep) PostSlackAttachment(flowHandler string, i int) *model.SlackAttachment { +func (s *EmptyStep) PostSlackAttachment(_ string, _ int) *model.SlackAttachment { sa := model.SlackAttachment{ Title: s.Title, Text: s.Message, @@ -21,7 +21,7 @@ func (s *EmptyStep) PostSlackAttachment(flowHandler string, i int) *model.SlackA return &sa } -func (s *EmptyStep) ResponseSlackAttachment(value bool) *model.SlackAttachment { +func (s *EmptyStep) ResponseSlackAttachment(_ bool) *model.SlackAttachment { return nil } @@ -29,7 +29,7 @@ func (s *EmptyStep) GetPropertyName() string { return "" } -func (s *EmptyStep) ShouldSkip(value bool) int { +func (s *EmptyStep) ShouldSkip(_ bool) int { return 0 } diff --git a/server/utils/flow/flow.go b/calendar/utils/flow/flow.go similarity index 100% rename from server/utils/flow/flow.go rename to calendar/utils/flow/flow.go diff --git a/server/utils/flow/handler.go b/calendar/utils/flow/handler.go similarity index 85% rename from server/utils/flow/handler.go rename to calendar/utils/flow/handler.go index eaaae911..a1b7f07c 100644 --- a/server/utils/flow/handler.go +++ b/calendar/utils/flow/handler.go @@ -8,11 +8,12 @@ import ( "fmt" "net/http" "strconv" + "time" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" ) type fh struct { @@ -76,5 +77,9 @@ func (fh *fh) handleFlow(w http.ResponseWriter, r *http.Request) { } fh.store.RemovePostID(mattermostUserID, property) + + // TODO: Workaround for https://community.mattermost.com/core/pl/nphtmkowcjd8ic76tbqtapx6nc + // See: https://mattermost.atlassian.net/browse/MM-54032 + time.Sleep(2 * time.Second) fh.flow.StepDone(mattermostUserID, stepNumber, value) } diff --git a/server/utils/httputils/handler.go b/calendar/utils/httputils/handler.go similarity index 100% rename from server/utils/httputils/handler.go rename to calendar/utils/httputils/handler.go diff --git a/server/utils/httputils/limited_readcloser.go b/calendar/utils/httputils/limited_readcloser.go similarity index 91% rename from server/utils/httputils/limited_readcloser.go rename to calendar/utils/httputils/limited_readcloser.go index a1006d2e..542c44d2 100644 --- a/server/utils/httputils/limited_readcloser.go +++ b/calendar/utils/httputils/limited_readcloser.go @@ -6,7 +6,7 @@ package httputils import ( "io" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" ) type LimitReadCloser struct { diff --git a/server/utils/httputils/limited_readcloser_test.go b/calendar/utils/httputils/limited_readcloser_test.go similarity index 93% rename from server/utils/httputils/limited_readcloser_test.go rename to calendar/utils/httputils/limited_readcloser_test.go index 3703e708..c2590e5d 100644 --- a/server/utils/httputils/limited_readcloser_test.go +++ b/calendar/utils/httputils/limited_readcloser_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" ) func TestLimitReadCloser(t *testing.T) { diff --git a/server/utils/httputils/utils.go b/calendar/utils/httputils/utils.go similarity index 83% rename from server/utils/httputils/utils.go rename to calendar/utils/httputils/utils.go index 0ad18179..ce9682e6 100644 --- a/server/utils/httputils/utils.go +++ b/calendar/utils/httputils/utils.go @@ -10,6 +10,8 @@ import ( "net/url" "path" "strings" + + "github.com/pkg/errors" ) func NormalizeRemoteBaseURL(mattermostSiteURL, remoteURL string) (string, error) { @@ -71,3 +73,17 @@ func WriteNotFoundError(w http.ResponseWriter, err error) { func WriteUnauthorizedError(w http.ResponseWriter, err error) { WriteJSONError(w, http.StatusUnauthorized, "Unauthorized.", err) } + +func WriteJSONResponse(w http.ResponseWriter, data any, statusCode int) error { + jsonResponse, err := json.Marshal(data) + if err != nil { + return errors.Wrap(err, "couldn't parse response") + } + + w.WriteHeader(statusCode) + if _, err := w.Write(jsonResponse); err != nil { + return errors.Wrap(err, "couldn't send response to user") + } + + return nil +} diff --git a/server/utils/httputils/utils_test.go b/calendar/utils/httputils/utils_test.go similarity index 100% rename from server/utils/httputils/utils_test.go rename to calendar/utils/httputils/utils_test.go diff --git a/calendar/utils/kvstore/crypt.go b/calendar/utils/kvstore/crypt.go new file mode 100644 index 00000000..4725cd9e --- /dev/null +++ b/calendar/utils/kvstore/crypt.go @@ -0,0 +1,130 @@ +package kvstore + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "io" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/pkg/errors" +) + +func encode(encrypted []byte) []byte { + encoded := make([]byte, base64.URLEncoding.EncodedLen(len(encrypted))) + base64.URLEncoding.Encode(encoded, encrypted) + return encoded +} + +func decode(encoded []byte) ([]byte, error) { + decoded := make([]byte, base64.URLEncoding.DecodedLen(len(encoded))) + n, err := base64.URLEncoding.Decode(decoded, encoded) + if err != nil { + return nil, err + } + return decoded[:n], nil +} + +func encrypt(key []byte, data []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return []byte(""), errors.Wrap(err, "could not create a cipher block, check key") + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return []byte(""), err + } + + nonce := make([]byte, aesgcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return []byte(""), err + } + + sealed := aesgcm.Seal(nil, nonce, data, nil) + return encode(append(nonce, sealed...)), nil +} + +func decrypt(key []byte, data []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return []byte(""), errors.Wrap(err, "could not create a cipher block, check key") + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return []byte(""), err + } + + decoded, err := decode(data) + if err != nil { + return []byte(""), err + } + + nonceSize := aesgcm.NonceSize() + if len(decoded) < nonceSize { + return []byte(""), errors.New("token too short") + } + + nonce, encrypted := decoded[:nonceSize], decoded[nonceSize:] + plain, err := aesgcm.Open(nil, nonce, encrypted, nil) + if err != nil { + return []byte(""), err + } + + return plain, nil +} + +type encryptedKeyStore struct { + store KVStore + encryptionKey []byte +} + +var _ KVStore = (*encryptedKeyStore)(nil) + +func NewEncryptedKeyStore(s KVStore, encryptionKey []byte) KVStore { + return &encryptedKeyStore{ + store: s, + encryptionKey: encryptionKey, + } +} + +func (s encryptedKeyStore) Load(key string) ([]byte, error) { + value, err := s.store.Load(key) + if err != nil { + return value, err + } + + return decrypt(s.encryptionKey, value) +} + +func (s encryptedKeyStore) Store(key string, data []byte) error { + encryptedData, err := encrypt(s.encryptionKey, data) + if err != nil { + return errors.Wrap(err, "error encrypting data") + } + return s.store.Store(key, encryptedData) +} + +func (s encryptedKeyStore) StoreTTL(key string, data []byte, ttlSeconds int64) error { + encryptedData, err := encrypt(s.encryptionKey, data) + if err != nil { + return errors.Wrap(err, "error encrypting data") + } + + return s.store.StoreTTL(key, encryptedData, ttlSeconds) +} + +func (s encryptedKeyStore) StoreWithOptions(key string, data []byte, opts model.PluginKVSetOptions) (bool, error) { + encryptedData, err := encrypt(s.encryptionKey, data) + if err != nil { + return false, errors.Wrap(err, "error encrypting data") + } + + return s.store.StoreWithOptions(key, encryptedData, opts) +} + +func (s encryptedKeyStore) Delete(key string) error { + return s.store.Delete(key) +} diff --git a/calendar/utils/kvstore/crypt_test.go b/calendar/utils/kvstore/crypt_test.go new file mode 100644 index 00000000..fb8e26bc --- /dev/null +++ b/calendar/utils/kvstore/crypt_test.go @@ -0,0 +1,109 @@ +package kvstore + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncryptDecrypt(t *testing.T) { + for _, test := range []struct { + Name string + Key []byte + ExpectedError string + }{ + { + Name: "EncryptDecrypt: Invalid key", + Key: make([]byte, 1), + ExpectedError: "could not create a cipher block", + }, + { + Name: "EncryptDecrypt: Valid", + Key: make([]byte, 16), + }, + } { + t.Run(test.Name, func(t *testing.T) { + assert := assert.New(t) + encryptedKey, err := encrypt(test.Key, []byte("mockData")) + if test.ExpectedError != "" { + assert.Contains(err.Error(), test.ExpectedError) + assert.Equal([]byte(""), encryptedKey) + } else { + assert.Nil(err) + assert.NotEqual([]byte(""), encryptedKey) + } + + decryptedKey, err := decrypt(test.Key, encryptedKey) + if test.ExpectedError != "" { + assert.Contains(err.Error(), test.ExpectedError) + assert.Equal([]byte(""), decryptedKey) + } else { + assert.Nil(err) + assert.Equal([]byte("mockData"), decryptedKey) + } + }) + } +} + +func TestEncrypt(t *testing.T) { + for _, test := range []struct { + Name string + Key []byte + ExpectedError string + }{ + { + Name: "Encrypt: Invalid key", + Key: make([]byte, 1), + ExpectedError: "could not create a cipher block", + }, + { + Name: "Encrypt: Valid", + Key: make([]byte, 16), + }, + } { + t.Run(test.Name, func(t *testing.T) { + assert := assert.New(t) + resp, err := encrypt(test.Key, []byte("mockData")) + if test.ExpectedError != "" { + assert.Contains(err.Error(), test.ExpectedError) + assert.Equal([]byte(""), resp) + } else { + assert.Nil(err) + assert.NotEqual([]byte(""), resp) + } + }) + } +} + +func TestDecrypt(t *testing.T) { + for _, test := range []struct { + Name string + Key []byte + Text string + ExpectedError string + }{ + { + Name: "Decrypt: Invalid key", + Text: "8qhtxbdZSjFi4-YBVmJ8nWgW2iQEoLrt8sVRTsTxm3awzvG-", + Key: make([]byte, 1), + ExpectedError: "could not create a cipher block", + }, + { + Name: "Decrypt: Valid", + Text: "8qhtxbdZSjFi4-YBVmJ8nWgW2iQEoLrt8sVRTsTxm3awzvG-", + Key: make([]byte, 16), + }, + } { + t.Run(test.Name, func(t *testing.T) { + assert := assert.New(t) + resp, err := decrypt(test.Key, []byte(test.Text)) + if test.ExpectedError != "" { + assert.Contains(err.Error(), test.ExpectedError) + assert.Equal([]byte(""), resp) + } else { + assert.Nil(err) + assert.Equal([]byte("mockData"), resp) + } + }) + } +} diff --git a/server/utils/kvstore/hashed_key.go b/calendar/utils/kvstore/hashed_key.go similarity index 100% rename from server/utils/kvstore/hashed_key.go rename to calendar/utils/kvstore/hashed_key.go diff --git a/server/utils/kvstore/hashed_key_test.go b/calendar/utils/kvstore/hashed_key_test.go similarity index 100% rename from server/utils/kvstore/hashed_key_test.go rename to calendar/utils/kvstore/hashed_key_test.go diff --git a/server/utils/kvstore/keys.go b/calendar/utils/kvstore/keys.go similarity index 100% rename from server/utils/kvstore/keys.go rename to calendar/utils/kvstore/keys.go diff --git a/server/utils/kvstore/kvstore.go b/calendar/utils/kvstore/kvstore.go similarity index 100% rename from server/utils/kvstore/kvstore.go rename to calendar/utils/kvstore/kvstore.go diff --git a/server/utils/kvstore/ots.go b/calendar/utils/kvstore/ots.go similarity index 100% rename from server/utils/kvstore/ots.go rename to calendar/utils/kvstore/ots.go diff --git a/server/utils/kvstore/plugin_store.go b/calendar/utils/kvstore/plugin_store.go similarity index 100% rename from server/utils/kvstore/plugin_store.go rename to calendar/utils/kvstore/plugin_store.go diff --git a/server/utils/map.go b/calendar/utils/map.go similarity index 100% rename from server/utils/map.go rename to calendar/utils/map.go diff --git a/server/utils/markdown.go b/calendar/utils/markdown.go similarity index 100% rename from server/utils/markdown.go rename to calendar/utils/markdown.go diff --git a/server/utils/oauth2connect/oauth2.go b/calendar/utils/oauth2connect/oauth2.go similarity index 63% rename from server/utils/oauth2connect/oauth2.go rename to calendar/utils/oauth2connect/oauth2.go index 86b1c259..08904277 100644 --- a/server/utils/oauth2connect/oauth2.go +++ b/calendar/utils/oauth2connect/oauth2.go @@ -4,7 +4,8 @@ package oauth2connect import ( - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" ) type App interface { @@ -14,11 +15,14 @@ type App interface { type oa struct { app App + + provider config.ProviderConfig } -func Init(h *httputils.Handler, app App) { +func Init(h *httputils.Handler, app App, providerConfig config.ProviderConfig) { oa := &oa{ - app: app, + app: app, + provider: providerConfig, } oauth2Router := h.Router.PathPrefix("/oauth2").Subrouter() diff --git a/server/utils/oauth2connect/oauth2_complete.go b/calendar/utils/oauth2connect/oauth2_complete.go similarity index 80% rename from server/utils/oauth2connect/oauth2_complete.go rename to calendar/utils/oauth2connect/oauth2_complete.go index 9b30eb81..c7b04724 100644 --- a/server/utils/oauth2connect/oauth2_complete.go +++ b/calendar/utils/oauth2connect/oauth2_complete.go @@ -4,9 +4,10 @@ package oauth2connect import ( + "fmt" "net/http" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" ) func (oa *oa) oauth2Complete(w http.ResponseWriter, r *http.Request) { @@ -37,11 +38,11 @@ func (oa *oa) oauth2Complete(w http.ResponseWriter, r *http.Request) { -

Completed connecting to Microsoft Calendar. Please close this window.

+

Completed connecting to %s. Please close this window.

` w.Header().Set("Content-Type", "text/html") - w.Write([]byte(html)) + w.Write([]byte(fmt.Sprintf(html, oa.provider.DisplayName))) } diff --git a/server/utils/oauth2connect/oauth2_connect.go b/calendar/utils/oauth2connect/oauth2_connect.go similarity index 87% rename from server/utils/oauth2connect/oauth2_connect.go rename to calendar/utils/oauth2connect/oauth2_connect.go index b698ca30..80e91593 100644 --- a/server/utils/oauth2connect/oauth2_connect.go +++ b/calendar/utils/oauth2connect/oauth2_connect.go @@ -6,7 +6,7 @@ package oauth2connect import ( "net/http" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" ) func (oa *oa) oauth2Connect(w http.ResponseWriter, r *http.Request) { diff --git a/server/utils/pluginapi/api.go b/calendar/utils/pluginapi/api.go similarity index 70% rename from server/utils/pluginapi/api.go rename to calendar/utils/pluginapi/api.go index 35d20bdf..faf08654 100644 --- a/server/utils/pluginapi/api.go +++ b/calendar/utils/pluginapi/api.go @@ -9,7 +9,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" - "github.com/mattermost/mattermost-plugin-mscalendar/server/store" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/store" ) type API struct { @@ -22,6 +22,22 @@ func New(api plugin.API) *API { } } +func (a *API) SearchLinkableChannelForUser(teamID, mattemostUserID, search string) ([]*model.Channel, error) { + channels, err := a.api.SearchChannels(teamID, search) + if err != nil { + return nil, err + } + + var result []*model.Channel + for _, ch := range channels { + if a.CanLinkEventToChannel(ch.Id, mattemostUserID) { + result = append(result, ch) + } + } + + return result, nil +} + func (a *API) GetMattermostUserStatus(mattermostUserID string) (*model.Status, error) { st, err := a.api.GetUserStatus(mattermostUserID) if err != nil { @@ -82,6 +98,19 @@ func (a *API) GetMattermostUser(mattermostUserID string) (*model.User, error) { return mmuser, nil } +func (a *API) GetMattermostUserTeams(mattermostUserID string) ([]*model.Team, error) { + teams, err := a.api.GetTeamsForUser(mattermostUserID) + if err != nil { + return nil, err + } + + return teams, nil +} + +func (a *API) CanLinkEventToChannel(channelID, userID string) bool { + return a.api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) +} + func (a *API) CleanKVStore() error { appErr := a.api.KVDeleteAll() if appErr != nil { @@ -106,3 +135,7 @@ func (a *API) GetPost(postID string) (*model.Post, error) { } return p, nil } + +func (a *API) PublishWebsocketEvent(mattermostUserID, event string, payload map[string]any) { + a.api.PublishWebSocketEvent(event, payload, &model.WebsocketBroadcast{UserId: mattermostUserID}) +} diff --git a/server/utils/settingspanel/bool_setting.go b/calendar/utils/settingspanel/bool_setting.go similarity index 85% rename from server/utils/settingspanel/bool_setting.go rename to calendar/utils/settingspanel/bool_setting.go index 826d0276..af082c84 100644 --- a/server/utils/settingspanel/bool_setting.go +++ b/calendar/utils/settingspanel/bool_setting.go @@ -73,9 +73,15 @@ func (s *boolSetting) GetDependency() string { return s.dependsOn } +func (s *boolSetting) getActionStyle(actionValue, currentValue string) string { + if actionValue == currentValue { + return "primary" + } + return "default" +} + func (s *boolSetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) { title := fmt.Sprintf("Setting: %s", s.title) - currentValueMessage := "Disabled" actions := []*model.PostAction{} if !disabled { @@ -84,14 +90,9 @@ func (s *boolSetting) GetSlackAttachments(userID, settingHandler string, disable return nil, err } - currentTextValue := "No" - if currentValue == "true" { - currentTextValue = "Yes" - } - currentValueMessage = fmt.Sprintf("Current value: %s", currentTextValue) - actionTrue := model.PostAction{ - Name: "Yes", + Name: "Yes", + Style: s.getActionStyle("true", currentValue.(string)), Integration: &model.PostActionIntegration{ URL: settingHandler, Context: map[string]interface{}{ @@ -102,7 +103,8 @@ func (s *boolSetting) GetSlackAttachments(userID, settingHandler string, disable } actionFalse := model.PostAction{ - Name: "No", + Name: "No", + Style: s.getActionStyle("false", currentValue.(string)), Integration: &model.PostActionIntegration{ URL: settingHandler, Context: map[string]interface{}{ @@ -114,12 +116,11 @@ func (s *boolSetting) GetSlackAttachments(userID, settingHandler string, disable actions = []*model.PostAction{&actionTrue, &actionFalse} } - text := fmt.Sprintf("%s\n%s", s.description, currentValueMessage) sa := model.SlackAttachment{ Title: title, - Text: text, + Text: s.description, Actions: actions, - Fallback: fmt.Sprintf("%s: %s", title, text), + Fallback: fmt.Sprintf("%s: %s", title, s.description), } return &sa, nil diff --git a/server/utils/settingspanel/empty_setting.go b/calendar/utils/settingspanel/empty_setting.go similarity index 72% rename from server/utils/settingspanel/empty_setting.go rename to calendar/utils/settingspanel/empty_setting.go index 1a0f6cc3..4b8fa79f 100644 --- a/server/utils/settingspanel/empty_setting.go +++ b/calendar/utils/settingspanel/empty_setting.go @@ -20,10 +20,10 @@ func NewEmptySetting(id, title, description string) Setting { } } -func (s *emptySetting) Set(userID string, value interface{}) error { +func (s *emptySetting) Set(_ string, _ interface{}) error { return nil } -func (s *emptySetting) Get(userID string) (interface{}, error) { +func (s *emptySetting) Get(_ string) (interface{}, error) { return "", nil } func (s *emptySetting) GetID() string { @@ -32,7 +32,7 @@ func (s *emptySetting) GetID() string { func (s *emptySetting) GetDependency() string { return "" } -func (s *emptySetting) IsDisabled(foreignValue interface{}) bool { +func (s *emptySetting) IsDisabled(_ interface{}) bool { return false } func (s *emptySetting) GetTitle() string { @@ -41,7 +41,7 @@ func (s *emptySetting) GetTitle() string { func (s *emptySetting) GetDescription() string { return s.description } -func (s *emptySetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) { +func (s *emptySetting) GetSlackAttachments(_, _ string, _ bool) (*model.SlackAttachment, error) { title := fmt.Sprintf("Setting: %s", s.title) sa := model.SlackAttachment{ Title: title, diff --git a/server/utils/settingspanel/handler.go b/calendar/utils/settingspanel/handler.go similarity index 84% rename from server/utils/settingspanel/handler.go rename to calendar/utils/settingspanel/handler.go index 6924105d..56174ef0 100644 --- a/server/utils/settingspanel/handler.go +++ b/calendar/utils/settingspanel/handler.go @@ -3,11 +3,12 @@ package settingspanel import ( "encoding/json" "net/http" + "time" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/httputils" ) const ( @@ -63,6 +64,10 @@ func (sh *handler) handleAction(w http.ResponseWriter, r *http.Request) { return } + // TODO: Workaround for https://community.mattermost.com/core/pl/nphtmkowcjd8ic76tbqtapx6nc + // See: https://mattermost.atlassian.net/browse/MM-54032 + time.Sleep(2 * time.Second) + response := model.PostActionIntegrationResponse{} post, err := sh.panel.ToPost(mattermostUserID) if err == nil { diff --git a/server/utils/settingspanel/option_setting.go b/calendar/utils/settingspanel/option_setting.go similarity index 96% rename from server/utils/settingspanel/option_setting.go rename to calendar/utils/settingspanel/option_setting.go index f8bc7366..234b72be 100644 --- a/server/utils/settingspanel/option_setting.go +++ b/calendar/utils/settingspanel/option_setting.go @@ -75,7 +75,7 @@ func (s *optionSetting) GetSlackAttachments(userID, settingHandler string, disab if err != nil { return nil, err } - currentValueMessage = fmt.Sprintf("Current value: %s", currentTextValue) + currentValueMessage = fmt.Sprintf("**Current value:** %s", currentTextValue) actionOptions := model.PostAction{ Name: "Select an option:", diff --git a/server/utils/settingspanel/read_only_setting.go b/calendar/utils/settingspanel/read_only_setting.go similarity index 89% rename from server/utils/settingspanel/read_only_setting.go rename to calendar/utils/settingspanel/read_only_setting.go index 99aa1046..99bb559a 100644 --- a/server/utils/settingspanel/read_only_setting.go +++ b/calendar/utils/settingspanel/read_only_setting.go @@ -25,7 +25,7 @@ func NewReadOnlySetting(id string, title string, description string, dependsOn s } } -func (s *readOnlySetting) Set(userID string, value interface{}) error { +func (s *readOnlySetting) Set(_ string, _ interface{}) error { return nil } @@ -58,7 +58,7 @@ func (s *readOnlySetting) GetDependency() string { return s.dependsOn } -func (s *readOnlySetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) { +func (s *readOnlySetting) GetSlackAttachments(userID, _ string, disabled bool) (*model.SlackAttachment, error) { title := fmt.Sprintf("Setting: %s", s.title) currentValueMessage := "Disabled" diff --git a/server/utils/settingspanel/settings.go b/calendar/utils/settingspanel/settings.go similarity index 96% rename from server/utils/settingspanel/settings.go rename to calendar/utils/settingspanel/settings.go index f2e02182..a20399cb 100644 --- a/server/utils/settingspanel/settings.go +++ b/calendar/utils/settingspanel/settings.go @@ -5,8 +5,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/kvstore" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/kvstore" ) type Setting interface { diff --git a/server/utils/settingspanel/utils.go b/calendar/utils/settingspanel/utils.go similarity index 100% rename from server/utils/settingspanel/utils.go rename to calendar/utils/settingspanel/utils.go diff --git a/server/utils/slack_attachments.go b/calendar/utils/slack_attachments.go similarity index 100% rename from server/utils/slack_attachments.go rename to calendar/utils/slack_attachments.go diff --git a/calendar/utils/test/gomock.go b/calendar/utils/test/gomock.go new file mode 100644 index 00000000..e439ec80 --- /dev/null +++ b/calendar/utils/test/gomock.go @@ -0,0 +1,54 @@ +package test + +import ( + "fmt" + + "github.com/golang/mock/gomock" +) + +// Source: https://github.com/golang/mock/issues/43#issuecomment-1292042897 + +// doMatch keeps state of the custom lambda matcher. +// match is a lambda function that asserts actual value matching. +// x keeps actual value. +type doMatch[V any] struct { + match func(v V) bool + x any +} + +// DoMatch creates lambda matcher instance equipped with +// lambda function to detect if actual value matches +// some arbitrary criteria. +// Lambda matcher implements gomock customer matcher +// interface https://github.com/golang/mock/blob/5b455625bd2c8ffbcc0de6a0873f864ba3820904/gomock/matchers.go#L25. +// Sample of usage: +// +// mock.EXPECT().Foo(gomock.All( +// +// DoMatch(func(v Bar) bool { +// return v.Greeting == "Hello world" +// }), +// +// )) +func DoMatch[V any](m func(v V) bool) gomock.Matcher { + return &doMatch[V]{ + match: m, + } +} + +// Matches receives actual value x casts it to specific type defined as a type parameter V +// and calls labmda function 'match' to resolve if x matches or not. +func (o *doMatch[V]) Matches(x any) bool { + o.x = x + v, ok := x.(V) + if !ok { + return false + } + + return o.match(v) +} + +// String describes what matcher matches. +func (o *doMatch[V]) String() string { + return fmt.Sprintf("is matched to %v", o.x) +} diff --git a/server/utils/tz/conversion.go b/calendar/utils/tz/conversion.go similarity index 100% rename from server/utils/tz/conversion.go rename to calendar/utils/tz/conversion.go diff --git a/server/utils/tz/data.go b/calendar/utils/tz/data.go similarity index 100% rename from server/utils/tz/data.go rename to calendar/utils/tz/data.go diff --git a/server/utils/tz/tz_test.go b/calendar/utils/tz/tz_test.go similarity index 100% rename from server/utils/tz/tz_test.go rename to calendar/utils/tz/tz_test.go diff --git a/calendar/utils/url.go b/calendar/utils/url.go new file mode 100644 index 00000000..d1bd5b2b --- /dev/null +++ b/calendar/utils/url.go @@ -0,0 +1,8 @@ +package utils + +import "net/url" + +func IsURL(u string) bool { + _, err := url.ParseRequestURI(u) + return err == nil +} diff --git a/go.mod b/go.mod index 1c794c14..f85bf121 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2 golang.org/x/oauth2 v0.15.0 + google.golang.org/grpc v1.60.0 // indirect ) require ( @@ -29,6 +30,7 @@ require ( github.com/hashicorp/go-hclog v1.6.2 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect @@ -58,7 +60,6 @@ require ( golang.org/x/text v0.14.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect - google.golang.org/grpc v1.60.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 02149c98..7e133d74 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZ github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -89,8 +90,9 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= diff --git a/server/remote/msgraph/batch_request.go b/msgraph/batch_request.go similarity index 100% rename from server/remote/msgraph/batch_request.go rename to msgraph/batch_request.go diff --git a/server/remote/msgraph/call.go b/msgraph/call.go similarity index 100% rename from server/remote/msgraph/call.go rename to msgraph/call.go diff --git a/server/remote/msgraph/client.go b/msgraph/client.go similarity index 76% rename from server/remote/msgraph/client.go rename to msgraph/client.go index d4c5b508..564008ac 100644 --- a/server/remote/msgraph/client.go +++ b/msgraph/client.go @@ -9,8 +9,8 @@ import ( msgraph "github.com/yaegashi/msgraph.go/v1.0" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) type client struct { diff --git a/server/remote/msgraph/create_calendar.go b/msgraph/create_calendar.go similarity index 82% rename from server/remote/msgraph/create_calendar.go rename to msgraph/create_calendar.go index e7429295..2b43fa01 100644 --- a/server/remote/msgraph/create_calendar.go +++ b/msgraph/create_calendar.go @@ -8,8 +8,8 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) // CreateCalendar creates a calendar diff --git a/server/remote/msgraph/create_event.go b/msgraph/create_event.go similarity index 88% rename from server/remote/msgraph/create_event.go rename to msgraph/create_event.go index 715a98e3..f786830f 100644 --- a/server/remote/msgraph/create_event.go +++ b/msgraph/create_event.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) // CreateEvent creates a calendar event diff --git a/server/remote/msgraph/delete_calendar.go b/msgraph/delete_calendar.go similarity index 87% rename from server/remote/msgraph/delete_calendar.go rename to msgraph/delete_calendar.go index 2d8ffc50..fbefdccc 100644 --- a/server/remote/msgraph/delete_calendar.go +++ b/msgraph/delete_calendar.go @@ -6,7 +6,7 @@ package msgraph import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) func (c *client) DeleteCalendar(remoteUserID string, calID string) error { diff --git a/msgraph/event.go b/msgraph/event.go new file mode 100644 index 00000000..4502986f --- /dev/null +++ b/msgraph/event.go @@ -0,0 +1,90 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package msgraph + +import ( + "net/http" + "time" + + "github.com/pkg/errors" + msgraph "github.com/yaegashi/msgraph.go/v1.0" + + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" +) + +const ( + MicrosoftResponseStatusYes = "accepted" + MicrosoftResponseStatusMaybe = "tentativelyAccepted" + MicrosoftResponseStatusNo = "declined" + MicrosoftResponseStatusNone = "none" + MicrosoftResponseStatusOrganizer = "organizer" + MicrosoftResponseStatusNotResponsed = MicrosoftResponseStatusNone +) + +var responseStatusConversion = map[string]string{ + MicrosoftResponseStatusYes: remote.EventResponseStatusAccepted, + MicrosoftResponseStatusMaybe: remote.EventResponseStatusTentative, + MicrosoftResponseStatusNo: remote.EventResponseStatusDeclined, + MicrosoftResponseStatusNone: remote.EventResponseStatusNotAnswered, + // TODO: unused by us? Should we prefill event organizer response to this? + MicrosoftResponseStatusOrganizer: remote.EventResponseStatusNotAnswered, +} + +// converts microsoft calendar responses to our representation of fields +func normalizeEvents(events []*remote.Event) []*remote.Event { + for i := range events { + events[i].ResponseStatus.Response = responseStatusConversion[events[i].ResponseStatus.Response] + } + return events +} + +func (c *client) GetEvent(remoteUserID, eventID string) (*remote.Event, error) { + e := &remote.Event{} + + err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).Request().JSONRequest( + c.ctx, http.MethodGet, "", nil, &e) + if err != nil { + return nil, errors.Wrap(err, "msgraph GetEvent") + } + return e, nil +} + +func (c *client) AcceptEvent(remoteUserID, eventID string) error { + dummy := &msgraph.EventAcceptRequestParameter{} + err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).Accept(dummy).Request().Post(c.ctx) + if err != nil { + return errors.Wrap(err, "msgraph Accept Event") + } + return nil +} + +func (c *client) DeclineEvent(remoteUserID, eventID string) error { + dummy := &msgraph.EventDeclineRequestParameter{} + err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).Decline(dummy).Request().Post(c.ctx) + if err != nil { + return errors.Wrap(err, "msgraph DeclineEvent") + } + return nil +} + +func (c *client) TentativelyAcceptEvent(remoteUserID, eventID string) error { + dummy := &msgraph.EventTentativelyAcceptRequestParameter{} + err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).TentativelyAccept(dummy).Request().Post(c.ctx) + if err != nil { + return errors.Wrap(err, "msgraph TentativelyAcceptEvent") + } + return nil +} + +func (c *client) GetEventsBetweenDates(remoteUserID string, start, end time.Time) ([]*remote.Event, error) { + paramStr := getQueryParamStringForCalendarView(start, end) + res := &calendarViewResponse{} + err := c.rbuilder.Users().ID(remoteUserID).CalendarView().Request().JSONRequest( + c.ctx, http.MethodGet, paramStr, nil, res) + if err != nil { + return nil, errors.Wrap(err, "msgraph GetEventsBetweenDates") + } + + return normalizeEvents(res.Value), nil +} diff --git a/server/remote/msgraph/find_meeting_times.go b/msgraph/find_meeting_times.go similarity index 90% rename from server/remote/msgraph/find_meeting_times.go rename to msgraph/find_meeting_times.go index 30e1f21b..bf4b9644 100644 --- a/server/remote/msgraph/find_meeting_times.go +++ b/msgraph/find_meeting_times.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) // FindMeetingTimes finds meeting time suggestions for a calendar event diff --git a/server/remote/msgraph/get_calendars.go b/msgraph/get_calendars.go similarity index 83% rename from server/remote/msgraph/get_calendars.go rename to msgraph/get_calendars.go index c85ea3e8..ccf05af5 100644 --- a/server/remote/msgraph/get_calendars.go +++ b/msgraph/get_calendars.go @@ -8,8 +8,8 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) func (c *client) GetCalendars(remoteUserID string) ([]*remote.Calendar, error) { diff --git a/server/remote/msgraph/get_default_calendar_view.go b/msgraph/get_default_calendar_view.go similarity index 84% rename from server/remote/msgraph/get_default_calendar_view.go rename to msgraph/get_default_calendar_view.go index 3faa83a4..c426cb11 100644 --- a/server/remote/msgraph/get_default_calendar_view.go +++ b/msgraph/get_default_calendar_view.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) type calendarViewResponse struct { @@ -30,16 +30,7 @@ type calendarViewBatchResponse struct { } func (c *client) GetDefaultCalendarView(remoteUserID string, start, end time.Time) ([]*remote.Event, error) { - paramStr := getQueryParamStringForCalendarView(start, end) - - res := &calendarViewResponse{} - err := c.rbuilder.Users().ID(remoteUserID).CalendarView().Request().JSONRequest( - c.ctx, http.MethodGet, paramStr, nil, res) - if err != nil { - return nil, errors.Wrap(err, "msgraph GetDefaultCalendarView") - } - - return res.Value, nil + return c.GetEventsBetweenDates(remoteUserID, start, end) } func (c *client) DoBatchViewCalendarRequests(allParams []*remote.ViewCalendarParams) ([]*remote.ViewCalendarResponse, error) { @@ -72,7 +63,7 @@ func (c *client) DoBatchViewCalendarRequests(allParams []*remote.ViewCalendarPar for _, res := range batchRes.Responses { viewCalRes := &remote.ViewCalendarResponse{ RemoteUserID: res.ID, - Events: res.Body.Value, + Events: normalizeEvents(res.Body.Value), Error: res.Body.Error, } result = append(result, viewCalRes) diff --git a/server/remote/msgraph/get_mailbox_settings.go b/msgraph/get_mailbox_settings.go similarity index 88% rename from server/remote/msgraph/get_mailbox_settings.go rename to msgraph/get_mailbox_settings.go index e1231a7e..35363619 100644 --- a/server/remote/msgraph/get_mailbox_settings.go +++ b/msgraph/get_mailbox_settings.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) func (c *client) GetMailboxSettings(remoteUserID string) (*remote.MailboxSettings, error) { diff --git a/server/remote/msgraph/get_me.go b/msgraph/get_me.go similarity index 93% rename from server/remote/msgraph/get_me.go rename to msgraph/get_me.go index 7dde0629..a2403ef6 100644 --- a/server/remote/msgraph/get_me.go +++ b/msgraph/get_me.go @@ -6,7 +6,7 @@ package msgraph import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) func (c *client) GetMe() (*remote.User, error) { diff --git a/server/remote/msgraph/get_notification_data.go b/msgraph/get_notification_data.go similarity index 88% rename from server/remote/msgraph/get_notification_data.go rename to msgraph/get_notification_data.go index 8dabcdba..7901adb8 100644 --- a/server/remote/msgraph/get_notification_data.go +++ b/msgraph/get_notification_data.go @@ -8,8 +8,8 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) func (c *client) GetNotificationData(orig *remote.Notification) (*remote.Notification, error) { diff --git a/server/remote/msgraph/get_schedule_batched.go b/msgraph/get_schedule_batched.go similarity index 95% rename from server/remote/msgraph/get_schedule_batched.go rename to msgraph/get_schedule_batched.go index 3b0c9bf7..87bfc626 100644 --- a/server/remote/msgraph/get_schedule_batched.go +++ b/msgraph/get_schedule_batched.go @@ -5,7 +5,7 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) type getScheduleResponse struct { @@ -81,7 +81,7 @@ func (c *client) GetSchedule(requests []*remote.ScheduleUserInfo, startTime, end } func makeSingleRequestForGetSchedule(request *remote.ScheduleUserInfo, params *getScheduleRequestParams) *singleRequest { - u := "/Users/" + request.RemoteUserID + "/calendar/getSchedule" + u := "/Users/" + request.RemoteUserID + "/getSchedule" req := &singleRequest{ URL: u, Method: http.MethodPost, diff --git a/server/remote/msgraph/get_schedule_batched_test.go b/msgraph/get_schedule_batched_test.go similarity index 88% rename from server/remote/msgraph/get_schedule_batched_test.go rename to msgraph/get_schedule_batched_test.go index 863d4fe2..c9bb9579 100644 --- a/server/remote/msgraph/get_schedule_batched_test.go +++ b/msgraph/get_schedule_batched_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" ) func TestMakeSingleRequestForGetSchedule(t *testing.T) { @@ -26,7 +26,7 @@ func TestMakeSingleRequestForGetSchedule(t *testing.T) { } out := makeSingleRequestForGetSchedule(req, params) - require.Equal(t, "/Users/remote_user_id/calendar/getSchedule", out.URL) + require.Equal(t, "/Users/remote_user_id/getSchedule", out.URL) require.Equal(t, "POST", out.Method) require.Equal(t, 1, len(out.Headers)) require.Equal(t, "application/json", out.Headers["Content-Type"]) diff --git a/server/remote/msgraph/get_super_user_token.go b/msgraph/get_super_user_token.go similarity index 100% rename from server/remote/msgraph/get_super_user_token.go rename to msgraph/get_super_user_token.go diff --git a/server/remote/msgraph/handle_webhook.go b/msgraph/handle_webhook.go similarity index 94% rename from server/remote/msgraph/handle_webhook.go rename to msgraph/handle_webhook.go index a6882ef6..c663db94 100644 --- a/server/remote/msgraph/handle_webhook.go +++ b/msgraph/handle_webhook.go @@ -9,8 +9,8 @@ import ( "net/http" "time" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) const renewSubscriptionBeforeExpiration = 12 * time.Hour diff --git a/msgraph/msgraph.go b/msgraph/msgraph.go new file mode 100644 index 00000000..343f6f18 --- /dev/null +++ b/msgraph/msgraph.go @@ -0,0 +1,31 @@ +package msgraph + +import ( + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" +) + +const ( + ProviderMSCalendar = Kind + ProviderMSCalendarDisplayName = "Microsoft Calendar" + ProviderMSCalendarRepository = "mattermost-plugin-mscalendar" +) + +func GetMSCalendarProviderConfig() config.ProviderConfig { + return config.ProviderConfig{ + Name: ProviderMSCalendar, + DisplayName: ProviderMSCalendarDisplayName, + Repository: ProviderMSCalendarRepository, + + CommandTrigger: ProviderMSCalendar, + + TelemetryShortName: ProviderMSCalendar, + + BotUsername: ProviderMSCalendar, + BotDisplayName: ProviderMSCalendarDisplayName, + + Features: config.ProviderFeatures{ + EncryptedStore: false, + EventNotifications: true, + }, + } +} diff --git a/server/remote/msgraph/remote.go b/msgraph/remote.go similarity index 78% rename from server/remote/msgraph/remote.go rename to msgraph/remote.go index 72bd3975..6eeb7983 100644 --- a/server/remote/msgraph/remote.go +++ b/msgraph/remote.go @@ -5,6 +5,7 @@ package msgraph import ( "context" + "fmt" "net/http" "golang.org/x/oauth2" @@ -12,12 +13,12 @@ import ( msgraph "github.com/yaegashi/msgraph.go/v1.0" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) -const Kind = "msgraph" +const Kind = "mscalendar" type impl struct { conf *config.Config @@ -85,3 +86,11 @@ func (r *impl) NewOAuth2Config() *oauth2.Config { Endpoint: microsoft.AzureADEndpoint(r.conf.OAuth2Authority), } } + +func (r *impl) CheckConfiguration(cfg config.StoredConfig) error { + if cfg.OAuth2ClientID == "" || cfg.OAuth2ClientSecret == "" || cfg.OAuth2Authority == "" { + return fmt.Errorf("OAuth2 credentials to be set in the config") + } + + return nil +} diff --git a/server/remote/msgraph/subscription.go b/msgraph/subscription.go similarity index 75% rename from server/remote/msgraph/subscription.go rename to msgraph/subscription.go index 85e5848b..ad312aaf 100644 --- a/server/remote/msgraph/subscription.go +++ b/msgraph/subscription.go @@ -11,8 +11,8 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/utils/bot" ) const subscribeTTL = 48 * time.Hour @@ -23,7 +23,7 @@ func newRandomString() string { return base64.URLEncoding.EncodeToString(b) } -func (c *client) CreateMySubscription(notificationURL string) (*remote.Subscription, error) { +func (c *client) CreateMySubscription(notificationURL, _ string) (*remote.Subscription, error) { sub := &remote.Subscription{ Resource: "me/events", ChangeType: "created,updated,deleted", @@ -46,20 +46,20 @@ func (c *client) CreateMySubscription(notificationURL string) (*remote.Subscript return sub, nil } -func (c *client) DeleteSubscription(subscriptionID string) error { - err := c.rbuilder.Subscriptions().ID(subscriptionID).Request().Delete(c.ctx) +func (c *client) DeleteSubscription(sub *remote.Subscription) error { + err := c.rbuilder.Subscriptions().ID(sub.ID).Request().Delete(c.ctx) if err != nil { return errors.Wrap(err, "msgraph DeleteSubscription") } c.Logger.With(bot.LogContext{ - "subscriptionID": subscriptionID, + "subscriptionID": sub.ID, }).Debugf("msgraph: deleted subscription.") return nil } -func (c *client) RenewSubscription(subscriptionID string) (*remote.Subscription, error) { +func (c *client) RenewSubscription(_, _ string, oldSub *remote.Subscription) (*remote.Subscription, error) { expires := time.Now().Add(subscribeTTL) v := struct { ExpirationDateTime string `json:"expirationDateTime"` @@ -67,13 +67,13 @@ func (c *client) RenewSubscription(subscriptionID string) (*remote.Subscription, expires.Format(time.RFC3339), } sub := remote.Subscription{} - err := c.rbuilder.Subscriptions().ID(subscriptionID).Request().JSONRequest(c.ctx, http.MethodPatch, "", v, &sub) + err := c.rbuilder.Subscriptions().ID(oldSub.ID).Request().JSONRequest(c.ctx, http.MethodPatch, "", v, &sub) if err != nil { return nil, errors.Wrap(err, "msgraph RenewSubscription") } c.Logger.With(bot.LogContext{ - "subscriptionID": subscriptionID, + "subscriptionID": oldSub.ID, "expirationDateTime": expires.Format(time.RFC3339), }).Debugf("msgraph: renewed subscription.") diff --git a/plugin.json b/plugin.json index 03a78a24..7c2f75c5 100644 --- a/plugin.json +++ b/plugin.json @@ -5,7 +5,7 @@ "homepage_url": "https://mattermost.com/pl/mattermost-plugin-mscalendar", "support_url": "https://github.com/mattermost/mattermost-plugin-mscalendar/issues", "release_notes_url": "https://github.com/mattermost/mattermost-plugin-mscalendar/releases/tag/v1.2.1", - "icon_path": "assets/profile.svg", + "icon_path": "assets/profile-mscalendar.svg", "version": "1.2.1", "min_server_version": "6.3.0", "server": { diff --git a/server/api/notification.go b/server/api/notification.go deleted file mode 100644 index 905dc250..00000000 --- a/server/api/notification.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -package api - -import ( - "net/http" - - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" -) - -func (api *api) notification(w http.ResponseWriter, req *http.Request) { - err := api.NotificationProcessor.Enqueue( - api.Env.Remote.HandleWebhook(w, req)...) - if err != nil { - httputils.WriteInternalServerError(w, err) - return - } -} diff --git a/server/command/daily_summary.go b/server/command/daily_summary.go deleted file mode 100644 index 3c5ac33d..00000000 --- a/server/command/daily_summary.go +++ /dev/null @@ -1,77 +0,0 @@ -package command - -import ( - "fmt" - - "github.com/mattermost/mattermost-plugin-mscalendar/server/store" -) - -const dailySummaryHelp = "### Daily summary commands:\n" + - "`/mscalendar summary view` - View your daily summary\n" + - "`/mscalendar summary settings` - View your settings for the daily summary\n" + - "`/mscalendar summary time 8:00AM` - Set the time you would like to receive your daily summary\n" + - "`/mscalendar summary enable` - Enable your daily summary\n" + - "`/mscalendar summary disable` - Disable your daily summary" - -const dailySummarySetTimeErrorMessage = "Please enter a time, for example:\n`/mscalendar summary time 8:00AM`" - -func (c *Command) dailySummary(parameters ...string) (string, bool, error) { - if len(parameters) == 0 { - return dailySummaryHelp, false, nil - } - - switch parameters[0] { - case "view": - postStr, err := c.MSCalendar.GetDailySummaryForUser(c.user()) - if err != nil { - return err.Error(), false, err - } - return postStr, false, nil - case "time": - if len(parameters) != 2 { - return dailySummarySetTimeErrorMessage, false, nil - } - val := parameters[1] - - dsum, err := c.MSCalendar.SetDailySummaryPostTime(c.user(), val) - if err != nil { - return err.Error() + "\n" + dailySummarySetTimeErrorMessage, false, nil - } - - return dailySummaryResponse(dsum), false, nil - case "settings": - dsum, err := c.MSCalendar.GetDailySummarySettingsForUser(c.user()) - if err != nil { - return err.Error() + "\nYou may need to configure your daily summary using the commands below.\n" + dailySummaryHelp, false, nil - } - - return dailySummaryResponse(dsum), false, nil - case "enable": - dsum, err := c.MSCalendar.SetDailySummaryEnabled(c.user(), true) - if err != nil { - return err.Error(), false, err - } - - return dailySummaryResponse(dsum), false, nil - case "disable": - dsum, err := c.MSCalendar.SetDailySummaryEnabled(c.user(), false) - if err != nil { - return err.Error(), false, err - } - return dailySummaryResponse(dsum), false, nil - default: - return "Invalid command. Please try again\n\n" + dailySummaryHelp, false, nil - } -} - -func dailySummaryResponse(dsum *store.DailySummaryUserSettings) string { - if dsum.PostTime == "" { - return "Your daily summary time is not yet configured.\n" + dailySummarySetTimeErrorMessage - } - - enableStr := "" - if !dsum.Enable { - enableStr = ", but is disabled. Enable it with `/mscalendar summary enable`" - } - return fmt.Sprintf("Your daily summary is configured to show at %s %s%s.", dsum.PostTime, dsum.Timezone, enableStr) -} diff --git a/server/command/get_calendars.go b/server/command/get_calendars.go deleted file mode 100644 index 03764e0e..00000000 --- a/server/command/get_calendars.go +++ /dev/null @@ -1,13 +0,0 @@ -package command - -import ( - "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" -) - -func (c *Command) showCalendars(parameters ...string) (string, bool, error) { - resp, err := c.MSCalendar.GetCalendars(c.user()) - if err != nil { - return "", false, err - } - return utils.JSONBlock(resp), false, nil -} diff --git a/server/config/config.go b/server/config/config.go deleted file mode 100644 index 93d0e1b8..00000000 --- a/server/config/config.go +++ /dev/null @@ -1,29 +0,0 @@ -package config - -import "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" - -// StoredConfig represents the data stored in and managed with the Mattermost -// config. -type StoredConfig struct { - OAuth2Authority string - OAuth2ClientID string - OAuth2ClientSecret string - bot.Config - EnableStatusSync bool - EnableDailySummary bool -} - -// Config represents the the metadata handed to all request runners (command, -// http). -type Config struct { - PluginID string - BuildDate string - BuildHash string - BuildHashShort string - MattermostSiteHostname string - MattermostSiteURL string - PluginURL string - PluginURLPath string - PluginVersion string - StoredConfig -} diff --git a/server/config/const.go b/server/config/const.go deleted file mode 100644 index aea5812b..00000000 --- a/server/config/const.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -package config - -const ( - BotUserName = "mscalendar" - BotDisplayName = "Microsoft Calendar" - BotDescription = "Created by the Microsoft Calendar Plugin." - - ApplicationName = "Microsoft Calendar" - Repository = "mattermost-plugin-mscalendar" - CommandTrigger = "mscalendar" - TelemetryShortName = "mscalendar" - - PathOAuth2 = "/oauth2" - PathComplete = "/complete" - PathAPI = "/api/v1" - PathPostAction = "/action" - PathRespond = "/respond" - PathAccept = "/accept" - PathDecline = "/decline" - PathTentative = "/tentative" - PathConfirmStatusChange = "/confirm" - PathNotification = "/notification/v1" - PathEvent = "/event" - - FullPathEventNotification = PathNotification + PathEvent - FullPathOAuth2Redirect = PathOAuth2 + PathComplete - - EventIDKey = "EventID" -) diff --git a/server/main.go b/server/main.go index 9fdcc9e9..16595afe 100644 --- a/server/main.go +++ b/server/main.go @@ -3,26 +3,31 @@ package main import ( mattermostplugin "github.com/mattermost/mattermost/server/public/plugin" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" - "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" - "github.com/mattermost/mattermost-plugin-mscalendar/server/plugin" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/config" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/engine" + "github.com/mattermost/mattermost-plugin-mscalendar/calendar/plugin" + "github.com/mattermost/mattermost-plugin-mscalendar/msgraph" ) var BuildHash string var BuildHashShort string var BuildDate string +var CalendarProvider string func main() { + config.Provider = msgraph.GetMSCalendarProviderConfig() + mattermostplugin.ClientMain( plugin.NewWithEnv( - mscalendar.Env{ + engine.Env{ Config: &config.Config{ PluginID: manifest.ID, PluginVersion: manifest.Version, BuildHash: BuildHash, BuildHashShort: BuildHashShort, BuildDate: BuildDate, + Provider: config.Provider, }, - Dependencies: &mscalendar.Dependencies{}, + Dependencies: &engine.Dependencies{}, })) } diff --git a/server/mscalendar/views/calendar.go b/server/mscalendar/views/calendar.go deleted file mode 100644 index cdd19f44..00000000 --- a/server/mscalendar/views/calendar.go +++ /dev/null @@ -1,111 +0,0 @@ -package views - -import ( - "fmt" - "net/url" - "sort" - "time" - - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" -) - -func RenderCalendarView(events []*remote.Event, timeZone string) (string, error) { - if len(events) == 0 { - return "You have no upcoming events.", nil - } - - if timeZone != "" { - for _, e := range events { - e.Start = e.Start.In(timeZone) - e.End = e.End.In(timeZone) - } - } - - sort.Slice(events, func(i, j int) bool { - return events[i].Start.Time().Before(events[j].Start.Time()) - }) - - resp := "Times are shown in " + events[0].Start.TimeZone - for _, group := range groupEventsByDate(events) { - resp += "\n" + group[0].Start.Time().Format("Monday January 02") + "\n\n" - resp += renderTableHeader() - for _, e := range group { - eventString, err := renderEvent(e, true, timeZone) - if err != nil { - return "", err - } - resp += fmt.Sprintf("\n%s", eventString) - } - } - - return resp, nil -} - -func renderTableHeader() string { - return `| Time | Subject | -| :--: | :-- |` -} - -func renderEvent(event *remote.Event, asRow bool, timeZone string) (string, error) { - start := event.Start.In(timeZone).Time().Format(time.Kitchen) - end := event.End.In(timeZone).Time().Format(time.Kitchen) - - format := "(%s - %s) [%s](%s)" - if asRow { - format = "| %s - %s | [%s](%s) |" - } - - link, err := url.QueryUnescape(event.Weblink) - if err != nil { - return "", err - } - - subject := EnsureSubject(event.Subject) - - return fmt.Sprintf(format, start, end, subject, link), nil -} - -func groupEventsByDate(events []*remote.Event) [][]*remote.Event { - groups := map[string][]*remote.Event{} - - for _, event := range events { - date := event.Start.Time().Format("2006-01-02") - _, ok := groups[date] - if !ok { - groups[date] = []*remote.Event{} - } - - groups[date] = append(groups[date], event) - } - - days := []string{} - for k := range groups { - days = append(days, k) - } - sort.Strings(days) - - result := [][]*remote.Event{} - for _, day := range days { - group := groups[day] - result = append(result, group) - } - return result -} - -func RenderUpcomingEvent(event *remote.Event, timeZone string) (string, error) { - message := "You have an upcoming event:\n" - eventString, err := renderEvent(event, false, timeZone) - if err != nil { - return "", err - } - - return message + eventString, nil -} - -func EnsureSubject(s string) string { - if s == "" { - return "(No subject)" - } - - return s -} diff --git a/server/mscalendar/welcome_flow.go b/server/mscalendar/welcome_flow.go deleted file mode 100644 index bffc5e68..00000000 --- a/server/mscalendar/welcome_flow.go +++ /dev/null @@ -1,101 +0,0 @@ -package mscalendar - -import ( - "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/flow" -) - -type WelcomeFlow struct { - controller bot.FlowController - onFlowDone func(userID string) - url string - steps []flow.Step -} - -func NewWelcomeFlow(bot bot.FlowController, welcomer Welcomer) *WelcomeFlow { - wf := WelcomeFlow{ - url: "/welcome", - controller: bot, - onFlowDone: welcomer.WelcomeFlowEnd, - } - wf.makeSteps() - return &wf -} - -func (wf *WelcomeFlow) Step(i int) flow.Step { - if i < 0 { - return nil - } - if i >= len(wf.steps) { - return nil - } - return wf.steps[i] -} - -func (wf *WelcomeFlow) URL() string { - return wf.url -} - -func (wf *WelcomeFlow) Length() int { - return len(wf.steps) -} - -func (wf *WelcomeFlow) StepDone(userID string, step int, value bool) { - wf.controller.NextStep(userID, step, value) -} - -func (wf *WelcomeFlow) FlowDone(userID string) { - wf.onFlowDone(userID) -} - -func (wf *WelcomeFlow) makeSteps() { - steps := []flow.Step{} - steps = append(steps, &flow.SimpleStep{ - Title: "Update Status", - Message: "Would you like your Mattermost status to be automatically updated at the time of your Microsoft Calendar events?", - PropertyName: store.UpdateStatusPropertyName, - TrueButtonMessage: "Yes - Update my status", - FalseButtonMessage: "No - Don't update my status", - TrueResponseMessage: ":thumbsup: Got it! We'll automatically update your status in Mattermost.", - FalseResponseMessage: ":thumbsup: Got it! We won't update your status in Mattermost.", - FalseSkip: 2, - }, &flow.SimpleStep{ - Title: "Confirm status change", - Message: "Do you want to receive confirmations before we update your status for each event?", - PropertyName: store.GetConfirmationPropertyName, - TrueButtonMessage: "Yes - I would like to get confirmations", - FalseButtonMessage: "No - Update my status automatically", - TrueResponseMessage: "Cool, we'll also send you confirmations before updating your status.", - FalseResponseMessage: "Cool, we'll update your status automatically with no confirmation.", - }, &flow.SimpleStep{ - Title: "Status during meetings", - Message: "Do you want to set your status to `Away` or to `Do not Disturb` while you are on a meeting? Setting to `Do Not Disturb` will silence notifications.", - PropertyName: store.ReceiveNotificationsDuringMeetingName, - TrueButtonMessage: "Away", - FalseButtonMessage: "Do not Disturb", - TrueResponseMessage: "Great, your status will be set to Away.", - FalseResponseMessage: "Great, your status will be set to Do not Disturb.", - }, &flow.SimpleStep{ - Title: "Subscribe to events", - Message: "Do you want to receive notifications when you are invited to an event?", - PropertyName: store.SubscribePropertyName, - TrueButtonMessage: "Yes - I would like to receive notifications for new events", - FalseButtonMessage: "No - Do not notify me of new events", - TrueResponseMessage: "Great, you will receive a message any time you receive a new event.", - FalseResponseMessage: "Great, you will not receive any notification on new events.", - }, &flow.SimpleStep{ - Title: "Receive reminder", - Message: "Do you want to receive a reminder for upcoming events?", - PropertyName: store.ReceiveUpcomingEventReminderName, - TrueButtonMessage: "Yes - I would like to receive reminders for upcoming events", - FalseButtonMessage: "No - Do not notify me of upcoming events", - TrueResponseMessage: "Great, you will receive a message before your meetings.", - FalseResponseMessage: "Great, you will not receive any notification for upcoming events.", - }, &flow.EmptyStep{ - Title: "Daily Summary", - Message: "Remember that you can set-up a daily summary by typing `/mscalendar summary time 8:00AM`.", - }) - - wf.steps = steps -} diff --git a/server/remote/msgraph/event.go b/server/remote/msgraph/event.go deleted file mode 100644 index beab2469..00000000 --- a/server/remote/msgraph/event.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -package msgraph - -import ( - "net/http" - - "github.com/pkg/errors" - msgraph "github.com/yaegashi/msgraph.go/v1.0" - - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" -) - -func (c *client) GetEvent(remoteUserID, eventID string) (*remote.Event, error) { - e := &remote.Event{} - - err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).Request().JSONRequest( - c.ctx, http.MethodGet, "", nil, &e) - if err != nil { - return nil, errors.Wrap(err, "msgraph GetEvent") - } - return e, nil -} - -func (c *client) AcceptEvent(remoteUserID, eventID string) error { - dummy := &msgraph.EventAcceptRequestParameter{} - err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).Accept(dummy).Request().Post(c.ctx) - if err != nil { - return errors.Wrap(err, "msgraph Accept Event") - } - return nil -} - -func (c *client) DeclineEvent(remoteUserID, eventID string) error { - dummy := &msgraph.EventDeclineRequestParameter{} - err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).Decline(dummy).Request().Post(c.ctx) - if err != nil { - return errors.Wrap(err, "msgraph DeclineEvent") - } - return nil -} - -func (c *client) TentativelyAcceptEvent(remoteUserID, eventID string) error { - dummy := &msgraph.EventTentativelyAcceptRequestParameter{} - err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).TentativelyAccept(dummy).Request().Post(c.ctx) - if err != nil { - return errors.Wrap(err, "msgraph TentativelyAcceptEvent") - } - return nil -}