diff --git a/Makefile b/Makefile index 5002ea4d..b370edd9 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,7 @@ ifneq ($(HAS_SERVER),) go install github.com/golang/mock/mockgen mockgen -destination server/jobs/mock_cluster/mock_cluster.go github.com/mattermost/mattermost-plugin-api/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 @@ -112,6 +113,7 @@ 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 diff --git a/server/command/availability.go b/server/command/availability.go index dbc3fe2f..7c5113d9 100644 --- a/server/command/availability.go +++ b/server/command/availability.go @@ -3,23 +3,23 @@ package command -func (c *Command) availability(parameters ...string) (string, error) { +func (c *Command) availability(parameters ...string) (string, bool, error) { switch { case len(parameters) == 0: resString, err := c.MSCalendar.SyncStatus(c.Args.UserId) if err != nil { - return "", err + return "", false, err } - return resString, nil + return resString, false, nil case len(parameters) == 1 && parameters[0] == "all": resString, err := c.MSCalendar.SyncStatusAll() if err != nil { - return "", err + return "", false, err } - return resString, nil + return resString, false, nil } - return "bad syntax", nil + return "bad syntax", false, nil } diff --git a/server/command/command.go b/server/command/command.go index 83e756d5..a2473af7 100644 --- a/server/command/command.go +++ b/server/command/command.go @@ -51,10 +51,10 @@ func Register(registerFunc RegisterFunc) { } // Handle should be called by the plugin when a command invocation is received from the Mattermost server. -func (c *Command) Handle() (string, error) { +func (c *Command) Handle() (string, bool, error) { cmd, parameters, err := c.isValid() if err != nil { - return "", err + return "", false, err } handler := c.help @@ -86,12 +86,12 @@ func (c *Command) Handle() (string, error) { case "settings": handler = c.settings } - out, err := handler(parameters...) + out, mustRedirectToDM, err := handler(parameters...) if err != nil { - return out, errors.WithMessagef(err, "Command /%s %s failed", config.CommandTrigger, cmd) + return out, false, errors.WithMessagef(err, "Command /%s %s failed", config.CommandTrigger, cmd) } - return out, nil + return out, mustRedirectToDM, nil } func (c *Command) isValid() (subcommand string, parameters []string, err error) { diff --git a/server/command/connect.go b/server/command/connect.go index c3e7ee26..6f871510 100644 --- a/server/command/connect.go +++ b/server/command/connect.go @@ -9,14 +9,25 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/config" ) -func (c *Command) connect(parameters ...string) (string, error) { +const ( + ConnectBotAlreadyConnectedTemplate = "The bot account is already connected to %s account `%s`. To connect to a different account, first run `/%s disconnect_bot`." + ConnectBotSuccessTemplate = "[Click here to link the bot's %s account.](%s/oauth2/connect_bot)" + ConnectAlreadyConnectedTemplate = "Your Mattermost account is already connected to %s account `%s`. To connect to a different account, first run `/%s disconnect`." + 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) if err == nil { - return fmt.Sprintf("Your Mattermost account is already connected to %s account `%s`. To connect to a different account, first run `/%s disconnect`.", config.ApplicationName, ru.Mail, config.CommandTrigger), nil + return fmt.Sprintf(ConnectAlreadyConnectedTemplate, config.ApplicationName, ru.Mail, config.CommandTrigger), false, nil + } + + out := "" //fmt.Sprintf(mscalendar.WelcomeMessage, c.Config.PluginURL) + + err = c.MSCalendar.Welcome(c.Args.UserId) + if err != nil { + out = ConnectErrorMessage + err.Error() } - out := fmt.Sprintf("[Click here to link your %s account.](%s/oauth2/connect)", - config.ApplicationName, - c.Config.PluginURL) - return out, nil + return out, true, nil } diff --git a/server/command/connect_test.go b/server/command/connect_test.go index 6e0b85e8..962e0b47 100644 --- a/server/command/connect_test.go +++ b/server/command/connect_test.go @@ -38,8 +38,9 @@ func TestConnect(t *testing.T) { setup: func(m mscalendar.MSCalendar) { mscal := m.(*mock_mscalendar.MockMSCalendar) mscal.EXPECT().GetRemoteUser("user_id").Return(nil, errors.New("remote user not found")).Times(1) + mscal.EXPECT().Welcome("user_id").Return(nil) }, - expectedOutput: "[Click here to link your Microsoft Calendar account.](http://localhost/oauth2/connect)", + expectedOutput: "", expectedError: "", }, } @@ -70,7 +71,7 @@ func TestConnect(t *testing.T) { tc.setup(mscal) } - out, err := command.Handle() + out, _, err := command.Handle() if tc.expectedOutput != "" { require.Equal(t, tc.expectedOutput, out) } diff --git a/server/command/create_calendar.go b/server/command/create_calendar.go index 66ebd8a0..e4b7be61 100644 --- a/server/command/create_calendar.go +++ b/server/command/create_calendar.go @@ -4,9 +4,9 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" ) -func (c *Command) createCalendar(parameters ...string) (string, error) { +func (c *Command) createCalendar(parameters ...string) (string, bool, error) { if len(parameters) != 1 { - return "Please provide the name of one calendar to create", nil + return "Please provide the name of one calendar to create", false, nil } calIn := &remote.Calendar{ @@ -15,7 +15,7 @@ func (c *Command) createCalendar(parameters ...string) (string, error) { _, err := c.MSCalendar.CreateCalendar(c.user(), calIn) if err != nil { - return "", err + return "", false, err } - return "", nil + return "", false, nil } diff --git a/server/command/create_event.go b/server/command/create_event.go index 0a4a470c..405c1e48 100644 --- a/server/command/create_event.go +++ b/server/command/create_event.go @@ -27,40 +27,40 @@ func getCreateEventFlagSet() *flag.FlagSet { return flagSet } -func (c *Command) createEvent(parameters ...string) (string, error) { +func (c *Command) createEvent(parameters ...string) (string, bool, error) { if len(parameters) == 0 { - return fmt.Sprintf(getCreateEventFlagSet().FlagUsages()), nil + return fmt.Sprintf(getCreateEventFlagSet().FlagUsages()), false, nil } tz, err := c.MSCalendar.GetTimezone(c.user()) if err != nil { - return "", nil + return "", false, nil } event, err := parseCreateArgs(parameters, tz) if err != nil { - return err.Error(), nil + return err.Error(), false, nil } createFlagSet := getCreateEventFlagSet() err = createFlagSet.Parse(parameters) if err != nil { - return "", err + return "", false, err } mattermostUserIDs, err := createFlagSet.GetStringSlice("attendees") if err != nil { - return "", err + return "", false, err } calEvent, err := c.MSCalendar.CreateEvent(c.user(), event, mattermostUserIDs) if err != nil { - return "", err + return "", false, err } resp := "Event Created\n" + utils.JSONBlock(&calEvent) - return resp, nil + return resp, false, nil } func parseCreateArgs(args []string, timeZone string) (*remote.Event, error) { diff --git a/server/command/daily_summary.go b/server/command/daily_summary.go index c3505221..3c5ac33d 100644 --- a/server/command/daily_summary.go +++ b/server/command/daily_summary.go @@ -15,52 +15,52 @@ const dailySummaryHelp = "### Daily summary commands:\n" + const dailySummarySetTimeErrorMessage = "Please enter a time, for example:\n`/mscalendar summary time 8:00AM`" -func (c *Command) dailySummary(parameters ...string) (string, error) { +func (c *Command) dailySummary(parameters ...string) (string, bool, error) { if len(parameters) == 0 { - return dailySummaryHelp, nil + return dailySummaryHelp, false, nil } switch parameters[0] { case "view": postStr, err := c.MSCalendar.GetDailySummaryForUser(c.user()) if err != nil { - return err.Error(), err + return err.Error(), false, err } - return postStr, nil + return postStr, false, nil case "time": if len(parameters) != 2 { - return dailySummarySetTimeErrorMessage, nil + return dailySummarySetTimeErrorMessage, false, nil } val := parameters[1] dsum, err := c.MSCalendar.SetDailySummaryPostTime(c.user(), val) if err != nil { - return err.Error() + "\n" + dailySummarySetTimeErrorMessage, nil + return err.Error() + "\n" + dailySummarySetTimeErrorMessage, false, nil } - return dailySummaryResponse(dsum), 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, nil + return err.Error() + "\nYou may need to configure your daily summary using the commands below.\n" + dailySummaryHelp, false, nil } - return dailySummaryResponse(dsum), nil + return dailySummaryResponse(dsum), false, nil case "enable": dsum, err := c.MSCalendar.SetDailySummaryEnabled(c.user(), true) if err != nil { - return err.Error(), err + return err.Error(), false, err } - return dailySummaryResponse(dsum), nil + return dailySummaryResponse(dsum), false, nil case "disable": dsum, err := c.MSCalendar.SetDailySummaryEnabled(c.user(), false) if err != nil { - return err.Error(), err + return err.Error(), false, err } - return dailySummaryResponse(dsum), nil + return dailySummaryResponse(dsum), false, nil default: - return "Invalid command. Please try again\n\n" + dailySummaryHelp, nil + return "Invalid command. Please try again\n\n" + dailySummaryHelp, false, nil } } diff --git a/server/command/delete_calendar.go b/server/command/delete_calendar.go index a0dbfddf..ad532898 100644 --- a/server/command/delete_calendar.go +++ b/server/command/delete_calendar.go @@ -1,13 +1,13 @@ package command -func (c *Command) deleteCalendar(parameters ...string) (string, error) { +func (c *Command) deleteCalendar(parameters ...string) (string, bool, error) { if len(parameters) != 1 { - return "Please provide the ID of only one calendar ", nil + return "Please provide the ID of only one calendar ", false, nil } err := c.MSCalendar.DeleteCalendar(c.user(), parameters[0]) if err != nil { - return "", err + return "", false, err } - return "", nil + return "", false, nil } diff --git a/server/command/disconnect.go b/server/command/disconnect.go index 189dc60a..b350d8f3 100644 --- a/server/command/disconnect.go +++ b/server/command/disconnect.go @@ -3,12 +3,12 @@ package command -func (c *Command) disconnect(parameters ...string) (string, error) { +func (c *Command) disconnect(parameters ...string) (string, bool, error) { err := c.MSCalendar.DisconnectUser(c.Args.UserId) if err != nil { - return "", err + return "", false, err } c.MSCalendar.ClearSettingsPosts(c.Args.UserId) - return "Successfully disconnected your account", nil + return "Successfully disconnected your account", false, nil } diff --git a/server/command/disconnect_test.go b/server/command/disconnect_test.go index d903757c..620fc7fb 100644 --- a/server/command/disconnect_test.go +++ b/server/command/disconnect_test.go @@ -70,7 +70,7 @@ func TestDisconnect(t *testing.T) { tc.setup(mscal) } - out, err := command.Handle() + out, _, err := command.Handle() if tc.expectedOutput != "" { require.Equal(t, tc.expectedOutput, out) } diff --git a/server/command/find_meeting_times.go b/server/command/find_meeting_times.go index 97eb203a..292d4a82 100644 --- a/server/command/find_meeting_times.go +++ b/server/command/find_meeting_times.go @@ -11,7 +11,7 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" ) -func (c *Command) findMeetings(parameters ...string) (string, error) { +func (c *Command) findMeetings(parameters ...string) (string, bool, error) { meetingParams := &remote.FindMeetingTimesParameters{} var attendees []remote.Attendee @@ -30,7 +30,7 @@ func (c *Command) findMeetings(parameters ...string) (string, error) { meetings, err := c.MSCalendar.FindMeetingTimes(c.user(), meetingParams) if err != nil { - return "", err + return "", false, err } timeZone, _ := c.MSCalendar.GetTimezone(c.user()) @@ -43,7 +43,7 @@ func (c *Command) findMeetings(parameters ...string) (string, error) { resp += utils.JSONBlock(renderMeetingTime(m)) } - return resp, nil + return resp, false, nil } func renderMeetingTime(m *remote.MeetingTimeSuggestion) string { diff --git a/server/command/get_calendars.go b/server/command/get_calendars.go index 81bf517f..03764e0e 100644 --- a/server/command/get_calendars.go +++ b/server/command/get_calendars.go @@ -4,10 +4,10 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" ) -func (c *Command) showCalendars(parameters ...string) (string, error) { +func (c *Command) showCalendars(parameters ...string) (string, bool, error) { resp, err := c.MSCalendar.GetCalendars(c.user()) if err != nil { - return "", err + return "", false, err } - return utils.JSONBlock(resp), nil + return utils.JSONBlock(resp), false, nil } diff --git a/server/command/help.go b/server/command/help.go index e5e53df3..75501f0f 100644 --- a/server/command/help.go +++ b/server/command/help.go @@ -9,7 +9,7 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/config" ) -func (c *Command) help(parameters ...string) (string, error) { +func (c *Command) help(parameters ...string) (string, bool, error) { resp := fmt.Sprintf("Mattermost Microsoft Calendar plugin version: %s, "+ "[%s](/~https://github.com/mattermost/%s/commit/%s), built %s\n", c.Config.PluginVersion, @@ -31,5 +31,5 @@ func (c *Command) help(parameters ...string) (string, error) { resp += "* /mscalendar findmeetings (Optional: )\n" resp += " * - space delimited : combinations \n" resp += " * options - required, optional \n" - return resp, nil + return resp, false, nil } diff --git a/server/command/info.go b/server/command/info.go index e6991ad2..c0fde2b2 100644 --- a/server/command/info.go +++ b/server/command/info.go @@ -9,7 +9,7 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/config" ) -func (c *Command) info(parameters ...string) (string, error) { +func (c *Command) info(parameters ...string) (string, bool, error) { resp := fmt.Sprintf("Mattermost Microsoft Calendar plugin version: %s, "+ "[%s](/~https://github.com/mattermost/%s/commit/%s), built %s\n", c.Config.PluginVersion, @@ -17,5 +17,5 @@ func (c *Command) info(parameters ...string) (string, error) { config.Repository, c.Config.BuildHash, c.Config.BuildDate) - return resp, nil + return resp, false, nil } diff --git a/server/command/settings.go b/server/command/settings.go index bf5fa44d..7ee56d15 100644 --- a/server/command/settings.go +++ b/server/command/settings.go @@ -3,8 +3,7 @@ package command -func (c *Command) settings(parameters ...string) (string, error) { +func (c *Command) settings(parameters ...string) (string, bool, error) { c.MSCalendar.PrintSettings(c.Args.UserId) - out := "The bot will show you the settings." - return out, nil + return "", true, nil } diff --git a/server/command/subscribe.go b/server/command/subscribe.go index c6f5626e..55b34e3b 100644 --- a/server/command/subscribe.go +++ b/server/command/subscribe.go @@ -9,53 +9,53 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" ) -func (c *Command) subscribe(parameters ...string) (string, error) { +func (c *Command) subscribe(parameters ...string) (string, bool, error) { switch { case len(parameters) == 0: storedSub, err := c.MSCalendar.CreateMyEventSubscription() if err != nil { - return "", err + return "", false, err } - return fmt.Sprintf("Subscription %s created.", storedSub.Remote.ID), nil + return fmt.Sprintf("Subscription %s created.", storedSub.Remote.ID), false, nil case len(parameters) == 1 && parameters[0] == "list": subs, err := c.MSCalendar.ListRemoteSubscriptions() if err != nil { - return "", err + return "", false, err } - return fmt.Sprintf("Subscriptions:%s", utils.JSONBlock(subs)), nil + return fmt.Sprintf("Subscriptions:%s", utils.JSONBlock(subs)), false, nil case len(parameters) == 1 && parameters[0] == "show": storedSub, err := c.MSCalendar.LoadMyEventSubscription() if err != nil { - return "", err + return "", false, err } - return fmt.Sprintf("Subscription:%s", utils.JSONBlock(storedSub)), nil + return fmt.Sprintf("Subscription:%s", utils.JSONBlock(storedSub)), false, nil case len(parameters) == 1 && parameters[0] == "renew": storedSub, err := c.MSCalendar.RenewMyEventSubscription() if err != nil { - return "", err + return "", false, err } if storedSub == nil { - return fmt.Sprintf("Not subscribed. Use `/mscalendar subscribe` to subscribe."), nil + return fmt.Sprintf("Not subscribed. Use `/mscalendar subscribe` to subscribe."), false, nil } - return fmt.Sprintf("Subscription %s renewed until %s", storedSub.Remote.ID, storedSub.Remote.ExpirationDateTime), nil + return fmt.Sprintf("Subscription %s renewed until %s", storedSub.Remote.ID, storedSub.Remote.ExpirationDateTime), false, nil case len(parameters) == 1 && parameters[0] == "delete": err := c.MSCalendar.DeleteMyEventSubscription() if err != nil { - return "", err + return "", false, err } - return fmt.Sprintf("User's subscription deleted"), nil + return fmt.Sprintf("User's subscription deleted"), false, nil case len(parameters) == 2 && parameters[0] == "delete": err := c.MSCalendar.DeleteOrphanedSubscription(parameters[1]) if err != nil { - return "", err + return "", false, err } - return fmt.Sprintf("Subscription %s deleted", parameters[1]), nil + return fmt.Sprintf("Subscription %s deleted", parameters[1]), false, nil } - return "bad syntax", nil + return "bad syntax", false, nil } diff --git a/server/command/view_calendar.go b/server/command/view_calendar.go index 6acb052c..d6d88ab7 100644 --- a/server/command/view_calendar.go +++ b/server/command/view_calendar.go @@ -9,16 +9,17 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/views" ) -func (c *Command) viewCalendar(parameters ...string) (string, error) { +func (c *Command) viewCalendar(parameters ...string) (string, bool, error) { tz, err := c.MSCalendar.GetTimezone(c.user()) if err != nil { - return "Error: No timezone found", err + 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)) if err != nil { - return "", err + return "", false, err } - return views.RenderCalendarView(events, tz) + out, err := views.RenderCalendarView(events, tz) + return out, false, err } diff --git a/server/mscalendar/availability.go b/server/mscalendar/availability.go index 4ad0dd11..4d64fe9d 100644 --- a/server/mscalendar/availability.go +++ b/server/mscalendar/availability.go @@ -134,7 +134,7 @@ func (m *mscalendar) notifyUpcomingEvent(mattermostUserID string, items []remote m.Logger.Errorf("notifyUpcomingEvent error rendering schedule item, err=", err.Error()) continue } - err = m.Poster.DM(mattermostUserID, message) + _, err = m.Poster.DM(mattermostUserID, message) if err != nil { m.Logger.Errorf("notifyUpcomingEvent error creating DM, err=", err.Error()) continue diff --git a/server/mscalendar/availability_test.go b/server/mscalendar/availability_test.go index 954835b5..e2b7c9f2 100644 --- a/server/mscalendar/availability_test.go +++ b/server/mscalendar/availability_test.go @@ -112,7 +112,7 @@ func TestSyncStatusAll(t *testing.T) { return []*remote.ScheduleInformation{tc.sched}, nil }) - mockPluginAPI.EXPECT().GetMattermostUserStatusesByIds([]string{"user_mm_id"}).Return([]*model.Status{&model.Status{Status: tc.currentStatus, UserId: "user_mm_id"}}, nil) + mockPluginAPI.EXPECT().GetMattermostUserStatusesByIds([]string{"user_mm_id"}).Return([]*model.Status{{Status: tc.currentStatus, UserId: "user_mm_id"}}, nil) if tc.newStatus == "" { mockPluginAPI.EXPECT().UpdateMattermostUserStatus("user_mm_id", gomock.Any()).Times(0) diff --git a/server/mscalendar/calendar.go b/server/mscalendar/calendar.go index 3636e034..199c8ecf 100644 --- a/server/mscalendar/calendar.go +++ b/server/mscalendar/calendar.go @@ -72,7 +72,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 MS office calendar event but have not linked your account. Feel free to join us by connecting your www.office.com using `/mscalendar connect`") + _, 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`") } } } diff --git a/server/mscalendar/daily_summary_test.go b/server/mscalendar/daily_summary_test.go index f4e0a0e4..389564c0 100644 --- a/server/mscalendar/daily_summary_test.go +++ b/server/mscalendar/daily_summary_test.go @@ -132,10 +132,10 @@ func TestProcessAllDailySummary(t *testing.T) { mockPoster := deps.Poster.(*mock_bot.MockPoster) gomock.InOrder( - mockPoster.EXPECT().DM("user1_mm_id", "You have no upcoming events.").Return(nil).Times(1), + mockPoster.EXPECT().DM("user1_mm_id", "You have no upcoming events.").Return("postID", nil).Times(1), mockPoster.EXPECT().DM("user2_mm_id", `Times are shown in Pacific Standard Time Wednesday February 12 -* 9:00AM - 11:00AM `+"`The subject`\n").Return(nil).Times(1), +* 9:00AM - 11:00AM `+"`The subject`\n").Return("postID", nil).Times(1), ) s.EXPECT().ModifyDailySummaryIndex(gomock.Any()).Return(nil) diff --git a/server/mscalendar/mock_mscalendar/mock_mscalendar.go b/server/mscalendar/mock_mscalendar/mock_mscalendar.go index b0b6e891..7481bbb7 100644 --- a/server/mscalendar/mock_mscalendar/mock_mscalendar.go +++ b/server/mscalendar/mock_mscalendar/mock_mscalendar.go @@ -50,6 +50,34 @@ func (mr *MockMSCalendarMockRecorder) AcceptEvent(arg0, arg1 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptEvent", reflect.TypeOf((*MockMSCalendar)(nil).AcceptEvent), arg0, arg1) } +// AfterDisconnect mocks base method +func (m *MockMSCalendar) AfterDisconnect(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AfterDisconnect", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// AfterDisconnect indicates an expected call of AfterDisconnect +func (mr *MockMSCalendarMockRecorder) AfterDisconnect(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterDisconnect", reflect.TypeOf((*MockMSCalendar)(nil).AfterDisconnect), arg0) +} + +// AfterSuccessfullyConnect mocks base method +func (m *MockMSCalendar) AfterSuccessfullyConnect(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AfterSuccessfullyConnect", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AfterSuccessfullyConnect indicates an expected call of AfterSuccessfullyConnect +func (mr *MockMSCalendarMockRecorder) 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) +} + // ClearSettingsPosts mocks base method func (m *MockMSCalendar) ClearSettingsPosts(arg0 string) { m.ctrl.T.Helper() @@ -499,3 +527,29 @@ func (mr *MockMSCalendarMockRecorder) ViewCalendar(arg0, arg1, arg2 interface{}) mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ViewCalendar", reflect.TypeOf((*MockMSCalendar)(nil).ViewCalendar), arg0, arg1, arg2) } + +// Welcome mocks base method +func (m *MockMSCalendar) Welcome(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Welcome", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Welcome indicates an expected call of Welcome +func (mr *MockMSCalendarMockRecorder) Welcome(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Welcome", reflect.TypeOf((*MockMSCalendar)(nil).Welcome), arg0) +} + +// WelcomeFlowEnd mocks base method +func (m *MockMSCalendar) 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 { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WelcomeFlowEnd", reflect.TypeOf((*MockMSCalendar)(nil).WelcomeFlowEnd), arg0) +} diff --git a/server/mscalendar/mock_welcomer/mock_welcomer.go b/server/mscalendar/mock_welcomer/mock_welcomer.go new file mode 100644 index 00000000..f300f195 --- /dev/null +++ b/server/mscalendar/mock_welcomer/mock_welcomer.go @@ -0,0 +1,87 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar (interfaces: Welcomer) + +// Package mock_welcomer is a generated GoMock package. +package mock_welcomer + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockWelcomer is a mock of Welcomer interface +type MockWelcomer struct { + ctrl *gomock.Controller + recorder *MockWelcomerMockRecorder +} + +// MockWelcomerMockRecorder is the mock recorder for MockWelcomer +type MockWelcomerMockRecorder struct { + mock *MockWelcomer +} + +// NewMockWelcomer creates a new mock instance +func NewMockWelcomer(ctrl *gomock.Controller) *MockWelcomer { + mock := &MockWelcomer{ctrl: ctrl} + mock.recorder = &MockWelcomerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockWelcomer) EXPECT() *MockWelcomerMockRecorder { + return m.recorder +} + +// AfterDisconnect mocks base method +func (m *MockWelcomer) AfterDisconnect(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AfterDisconnect", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// AfterDisconnect indicates an expected call of AfterDisconnect +func (mr *MockWelcomerMockRecorder) AfterDisconnect(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterDisconnect", reflect.TypeOf((*MockWelcomer)(nil).AfterDisconnect), arg0) +} + +// AfterSuccessfullyConnect mocks base method +func (m *MockWelcomer) AfterSuccessfullyConnect(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AfterSuccessfullyConnect", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AfterSuccessfullyConnect indicates an expected call of AfterSuccessfullyConnect +func (mr *MockWelcomerMockRecorder) AfterSuccessfullyConnect(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterSuccessfullyConnect", reflect.TypeOf((*MockWelcomer)(nil).AfterSuccessfullyConnect), arg0, arg1) +} + +// Welcome mocks base method +func (m *MockWelcomer) Welcome(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Welcome", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Welcome indicates an expected call of Welcome +func (mr *MockWelcomerMockRecorder) Welcome(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Welcome", reflect.TypeOf((*MockWelcomer)(nil).Welcome), arg0) +} + +// WelcomeFlowEnd mocks base method +func (m *MockWelcomer) WelcomeFlowEnd(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "WelcomeFlowEnd", arg0) +} + +// WelcomeFlowEnd indicates an expected call of WelcomeFlowEnd +func (mr *MockWelcomerMockRecorder) WelcomeFlowEnd(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WelcomeFlowEnd", reflect.TypeOf((*MockWelcomer)(nil).WelcomeFlowEnd), arg0) +} diff --git a/server/mscalendar/mscalendar.go b/server/mscalendar/mscalendar.go index 2d9ebc65..27f12f0e 100644 --- a/server/mscalendar/mscalendar.go +++ b/server/mscalendar/mscalendar.go @@ -19,6 +19,7 @@ type MSCalendar interface { EventResponder Subscriptions Users + Welcomer Settings DailySummary } @@ -32,6 +33,7 @@ type Dependencies struct { Store store.Store SettingsPanel settingspanel.Panel IsAuthorizedAdmin func(string) (bool, error) + Welcomer Welcomer } type PluginAPI interface { diff --git a/server/mscalendar/oauth2.go b/server/mscalendar/oauth2.go index 912b4994..508d1b55 100644 --- a/server/mscalendar/oauth2.go +++ b/server/mscalendar/oauth2.go @@ -18,10 +18,6 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/oauth2connect" ) -const WelcomeMessage = `### Welcome to the Microsoft Calendar plugin! -Here is some info to prove we got you logged in -- Name: %s -` const BotWelcomeMessage = "Bot user connected to account %s." const RemoteUserAlreadyConnected = "%s account `%s` is already mapped to Mattermost account `%s`. Please run `/%s disconnect`, while logged in as the Mattermost account." @@ -112,6 +108,7 @@ func (app *oauth2App) CompleteOAuth2(authedUserID, code, state string) error { return err } - app.Poster.DM(mattermostUserID, WelcomeMessage, me.Mail) + app.Welcomer.AfterSuccessfullyConnect(mattermostUserID, me.Mail) + return nil } diff --git a/server/mscalendar/oauth2_test.go b/server/mscalendar/oauth2_test.go index d0b59db8..88a23c16 100644 --- a/server/mscalendar/oauth2_test.go +++ b/server/mscalendar/oauth2_test.go @@ -14,6 +14,7 @@ import ( "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" @@ -42,7 +43,7 @@ func TestCompleteOAuth2Happy(t *testing.T) { app, env := newOAuth2TestApp(ctrl) ss := env.Dependencies.Store.(*mock_store.MockStore) - poster := env.Dependencies.Poster.(*mock_bot.MockPoster) + welcomer := env.Dependencies.Welcomer.(*mock_welcomer.MockWelcomer) state := "" gomock.InOrder( @@ -68,11 +69,7 @@ func TestCompleteOAuth2Happy(t *testing.T) { ss.EXPECT().LoadMattermostUserID(fakeRemoteID).Return("", errors.New("Connected user not found")).Times(1), ss.EXPECT().StoreUser(gomock.Any()).Return(nil).Times(1), ss.EXPECT().StoreUserInIndex(gomock.Any()).Return(nil).Times(1), - poster.EXPECT().DM( - gomock.Eq(fakeID), - gomock.Eq(WelcomeMessage), - gomock.Eq("mail-value"), - ).Return(nil).Times(1), + welcomer.EXPECT().AfterSuccessfullyConnect(fakeID, "mail-value").Return(nil).Times(1), ) err = app.CompleteOAuth2(fakeID, fakeCode, state) @@ -216,7 +213,7 @@ func TestCompleteOAuth2Errors(t *testing.T) { gomock.Eq("mail-value"), gomock.Eq("mscalendar"), gomock.Eq("sample-username"), - ).Return(nil).Times(1) + ).Return("post_id", nil).Times(1) }, }, { @@ -374,6 +371,7 @@ func newOAuth2TestApp(ctrl *gomock.Controller) (oauth2connect.App, Env) { Poster: mock_bot.NewMockPoster(ctrl), Remote: remote.Makers[msgraph.Kind](conf, &bot.NilLogger{}), PluginAPI: mock_plugin_api.NewMockPluginAPI(ctrl), + Welcomer: mock_welcomer.NewMockWelcomer(ctrl), IsAuthorizedAdmin: func(mattermostUserID string) (bool, error) { return false, nil }, }, } diff --git a/server/mscalendar/user.go b/server/mscalendar/user.go index 2f1c3f79..c02a433a 100644 --- a/server/mscalendar/user.go +++ b/server/mscalendar/user.go @@ -115,6 +115,7 @@ func (user *User) Markdown() string { } func (m *mscalendar) DisconnectUser(mattermostUserID string) error { + m.AfterDisconnect(mattermostUserID) err := m.Filter( withClient, ) diff --git a/server/mscalendar/welcome_flow.go b/server/mscalendar/welcome_flow.go new file mode 100644 index 00000000..9fca9d4d --- /dev/null +++ b/server/mscalendar/welcome_flow.go @@ -0,0 +1,85 @@ +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 { + steps []flow.Step + url string + controller bot.FlowController + onFlowDone func(userID string) +} + +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: "Do you wish your Mattermost status to be automatically updated to be *Do not disturb* 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: 1, + }, &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: "Subscribe to events", + Message: "Do you want to receive notifications when you receive a new 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.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/mscalendar/welcomer.go b/server/mscalendar/welcomer.go new file mode 100644 index 00000000..5926d284 --- /dev/null +++ b/server/mscalendar/welcomer.go @@ -0,0 +1,189 @@ +package mscalendar + +import ( + "fmt" + + "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-server/v5/model" +) + +type Welcomer interface { + Welcome(userID string) error + AfterSuccessfullyConnect(userID, userLogin string) error + AfterDisconnect(userID string) error + WelcomeFlowEnd(userID string) +} + +type Bot interface { + bot.Bot + Welcomer + flow.FlowStore +} + +type mscBot struct { + bot.Bot + Env + pluginURL string +} + +const ( + WelcomeMessage = `Welcome to the Microsoft Calendar plugin. + [Click here to link your account.](%s/oauth2/connect)` +) + +func (m *mscalendar) Welcome(userID string) error { + return m.Welcomer.Welcome(userID) +} + +func (m *mscalendar) AfterSuccessfullyConnect(userID, userLogin string) error { + return m.Welcomer.AfterSuccessfullyConnect(userID, userLogin) +} + +func (m *mscalendar) AfterDisconnect(userID string) error { + return m.Welcomer.AfterDisconnect(userID) +} + +func (m *mscalendar) WelcomeFlowEnd(userID string) { + m.Welcomer.WelcomeFlowEnd(userID) +} + +func NewMSCalendarBot(bot bot.Bot, env Env, pluginURL string) Bot { + return &mscBot{ + Bot: bot, + Env: env, + pluginURL: pluginURL, + } +} + +func (bot *mscBot) Welcome(userID string) error { + bot.cleanWelcomePost(userID) + + postID, err := bot.DMWithAttachments(userID, bot.newConnectAttachment()) + if err != nil { + return err + } + + bot.Store.StoreUserWelcomePost(userID, postID) + + return nil +} + +func (bot *mscBot) AfterSuccessfullyConnect(userID, userLogin string) error { + postID, err := bot.Store.DeleteUserWelcomePost(userID) + if err != nil { + bot.Errorf("error deleting user welcom post id, err=" + err.Error()) + } + if postID != "" { + post := &model.Post{ + Id: postID, + } + model.ParseSlackAttachment(post, []*model.SlackAttachment{bot.newConnectedAttachment(userLogin)}) + bot.UpdatePost(post) + } + + return bot.Start(userID) +} + +func (bot *mscBot) AfterDisconnect(userID string) error { + errCancel := bot.Cancel(userID) + errClean := bot.cleanWelcomePost(userID) + if errCancel != nil { + return errCancel + } + + if errClean != nil { + return errClean + } + return nil +} + +func (bot *mscBot) WelcomeFlowEnd(userID string) { + bot.notifySettings(userID) +} + +func (bot *mscBot) newConnectAttachment() *model.SlackAttachment { + sa := model.SlackAttachment{ + Title: "Connect", + Text: fmt.Sprintf(WelcomeMessage, bot.pluginURL), + } + + return &sa +} + +func (bot *mscBot) newConnectedAttachment(userLogin string) *model.SlackAttachment { + return &model.SlackAttachment{ + Title: "Connect", + Text: ":tada: Congratulations! Your microsoft account (*" + userLogin + "*) has been connected to Mattermost.", + } +} + +func (bot *mscBot) notifySettings(userID string) error { + _, err := bot.DM(userID, "Feel free to change these settings anytime by typing `/%s settings`", config.CommandTrigger) + if err != nil { + return err + } + return nil +} + +func (bot *mscBot) cleanWelcomePost(mattermostUserID string) error { + postID, err := bot.Store.DeleteUserWelcomePost(mattermostUserID) + if err != nil { + return err + } + + if postID != "" { + err = bot.DeletePost(postID) + if err != nil { + bot.Errorf(err.Error()) + } + } + return nil +} + +func (bot *mscBot) SetProperty(userID, propertyName string, value bool) error { + if propertyName == store.SubscribePropertyName { + if value { + m := New(bot.Env, userID) + l, err := m.ListRemoteSubscriptions() + if err != nil { + return err + } + if len(l) >= 1 { + return nil + } + + _, err = m.CreateMyEventSubscription() + if err != nil { + return err + } + } + return nil + } + + return bot.Dependencies.Store.SetProperty(userID, propertyName, value) +} + +func (bot *mscBot) SetPostID(userID, propertyName, postID string) error { + return bot.Dependencies.Store.SetPostID(userID, propertyName, postID) +} + +func (bot *mscBot) GetPostID(userID, propertyName string) (string, error) { + return bot.Dependencies.Store.GetPostID(userID, propertyName) +} + +func (bot *mscBot) RemovePostID(userID, propertyName string) error { + return bot.Dependencies.Store.RemovePostID(userID, propertyName) +} + +func (bot *mscBot) GetCurrentStep(userID string) (int, error) { + return bot.Dependencies.Store.GetCurrentStep(userID) +} +func (bot *mscBot) SetCurrentStep(userID string, step int) error { + return bot.Dependencies.Store.SetCurrentStep(userID, step) +} +func (bot *mscBot) DeleteCurrentStep(userID string) error { + return bot.Dependencies.Store.DeleteCurrentStep(userID) +} diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 7d309669..9db73d59 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -4,6 +4,7 @@ package plugin import ( + "fmt" "net/http" "net/url" "os" @@ -26,6 +27,7 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/remote/msgraph" "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/server/utils/httputils" "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/oauth2connect" "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/pluginapi" @@ -59,7 +61,7 @@ func NewWithEnv(env mscalendar.Env) *Plugin { } func (p *Plugin) OnActivate() error { - p.initEnv(&p.env) + p.initEnv(&p.env, "") bundlePath, err := p.API.GetBundlePath() if err != nil { return errors.Wrap(err, "couldn't get bundle path") @@ -117,7 +119,7 @@ func (p *Plugin) OnConfigurationChange() (err error) { pluginURL := strings.TrimRight(*mattermostSiteURL, "/") + pluginURLPath p.updateEnv(func(e *Env) { - p.initEnv(e) + p.initEnv(e, pluginURL) e.StoredConfig = stored e.Config.MattermostSiteURL = *mattermostSiteURL @@ -127,9 +129,13 @@ func (p *Plugin) OnConfigurationChange() (err error) { e.Dependencies.Remote = remote.Makers[msgraph.Kind](e.Config, e.Logger) e.bot = e.bot.WithConfig(stored.BotConfig) + + mscalendarBot := mscalendar.NewMSCalendarBot(e.bot, e.Env, pluginURL) + e.Config.BotUserID = e.bot.MattermostUserID() e.Dependencies.Logger = e.bot e.Dependencies.Poster = e.bot + e.Dependencies.Welcomer = mscalendarBot e.Dependencies.Store = store.NewPluginStore(p.API, e.bot) e.Dependencies.IsAuthorizedAdmin = p.IsAuthorizedAdmin e.Dependencies.SettingsPanel = mscalendar.NewSettingsPanel( @@ -143,6 +149,9 @@ func (p *Plugin) OnConfigurationChange() (err error) { }, ) + welcomeFlow := mscalendar.NewWelcomeFlow(e.bot, e.Dependencies.Welcomer) + e.bot.RegisterFlow(welcomeFlow, mscalendarBot) + if e.notificationProcessor == nil { e.notificationProcessor = mscalendar.NewNotificationProcessor(e.Env) } else { @@ -151,6 +160,7 @@ func (p *Plugin) OnConfigurationChange() (err error) { e.httpHandler = httputils.NewHandler() oauth2connect.Init(e.httpHandler, mscalendar.NewOAuth2App(e.Env)) + flow.Init(e.httpHandler, welcomeFlow, mscalendarBot) settingspanel.Init(e.httpHandler, e.Dependencies.SettingsPanel) api.Init(e.httpHandler, e.Env, e.notificationProcessor) @@ -193,7 +203,7 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo Config: env.Config, MSCalendar: mscalendar.New(env.Env, args.UserId), } - out, err := command.Handle() + out, mustRedirectToDM, err := command.Handle() if err != nil { p.API.LogError(err.Error()) return nil, model.NewAppError("mscalendarplugin.ExecuteCommand", "Unable to execute command.", nil, err.Error(), http.StatusInternalServerError) @@ -202,7 +212,18 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo if out != "" { env.Poster.Ephemeral(args.UserId, args.ChannelId, out) } - return &model.CommandResponse{}, nil + + response := &model.CommandResponse{} + if mustRedirectToDM { + t, appErr := p.API.GetTeam(args.TeamId) + 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) + response.GotoLocation = dmURL + } + + return response, nil } func (p *Plugin) ServeHTTP(pc *plugin.Context, w http.ResponseWriter, req *http.Request) { @@ -271,11 +292,11 @@ func (p *Plugin) loadTemplates(bundlePath string) error { return nil } -func (p *Plugin) initEnv(e *Env) error { +func (p *Plugin) initEnv(e *Env, pluginURL string) error { e.Dependencies.PluginAPI = pluginapi.New(p.API) if e.bot == nil { - e.bot = bot.New(p.API, p.Helpers) + e.bot = bot.New(p.API, p.Helpers, pluginURL) err := e.bot.Ensure( &model.Bot{ Username: config.BotUserName, diff --git a/server/store/flow_store.go b/server/store/flow_store.go new file mode 100644 index 00000000..fd343fe9 --- /dev/null +++ b/server/store/flow_store.go @@ -0,0 +1,140 @@ +package store + +import "fmt" + +const ( + UpdateStatusPropertyName = "update_status" + GetConfirmationPropertyName = "get_confirmation" + SubscribePropertyName = "subscribe" +) + +func (s *pluginStore) SetProperty(userID, propertyName string, value bool) error { + user, err := s.LoadUser(userID) + if err != nil { + return err + } + + switch propertyName { + case UpdateStatusPropertyName: + user.Settings.UpdateStatus = value + case GetConfirmationPropertyName: + user.Settings.GetConfirmation = value + default: + return fmt.Errorf("property %s not found", propertyName) + } + + err = s.StoreUser(user) + if err != nil { + return err + } + + return nil +} + +func (s *pluginStore) SetPostID(userID, propertyName, postID string) error { + user, err := s.LoadUser(userID) + if err != nil { + return err + } + + switch propertyName { + case UpdateStatusPropertyName: + user.WelcomeFlowStatus.UpdateStatusPostID = postID + case GetConfirmationPropertyName: + user.WelcomeFlowStatus.GetConfirmationPostID = postID + case SubscribePropertyName: + user.WelcomeFlowStatus.SubscribePostID = postID + default: + return fmt.Errorf("property %s not found", propertyName) + } + + err = s.StoreUser(user) + if err != nil { + return err + } + + return nil +} + +func (s *pluginStore) GetPostID(userID, propertyName string) (string, error) { + user, err := s.LoadUser(userID) + if err != nil { + return "", err + } + + switch propertyName { + case UpdateStatusPropertyName: + return user.WelcomeFlowStatus.UpdateStatusPostID, nil + case GetConfirmationPropertyName: + return user.WelcomeFlowStatus.GetConfirmationPostID, nil + case SubscribePropertyName: + return user.WelcomeFlowStatus.SubscribePostID, nil + default: + return "", fmt.Errorf("property %s not found", propertyName) + } +} + +func (s *pluginStore) RemovePostID(userID, propertyName string) error { + user, err := s.LoadUser(userID) + if err != nil { + return err + } + + switch propertyName { + case UpdateStatusPropertyName: + user.WelcomeFlowStatus.UpdateStatusPostID = "" + case GetConfirmationPropertyName: + user.WelcomeFlowStatus.GetConfirmationPostID = "" + case SubscribePropertyName: + user.WelcomeFlowStatus.SubscribePostID = "" + default: + return fmt.Errorf("property %s not found", propertyName) + } + + err = s.StoreUser(user) + if err != nil { + return err + } + + return nil +} + +func (s *pluginStore) GetCurrentStep(userID string) (int, error) { + user, err := s.LoadUser(userID) + if err != nil { + return 0, err + } + + return user.WelcomeFlowStatus.Step, nil +} +func (s *pluginStore) SetCurrentStep(userID string, step int) error { + user, err := s.LoadUser(userID) + if err != nil { + return err + } + + user.WelcomeFlowStatus.Step = step + + err = s.StoreUser(user) + if err != nil { + return err + } + + return nil +} + +func (s *pluginStore) DeleteCurrentStep(userID string) error { + user, err := s.LoadUser(userID) + if err != nil { + return err + } + + user.WelcomeFlowStatus.Step = 0 + + err = s.StoreUser(user) + if err != nil { + return err + } + + return nil +} diff --git a/server/store/mock_store/mock_store.go b/server/store/mock_store/mock_store.go index 0d97fd57..3105104d 100644 --- a/server/store/mock_store/mock_store.go +++ b/server/store/mock_store/mock_store.go @@ -33,6 +33,20 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder { return m.recorder } +// DeleteCurrentStep mocks base method +func (m *MockStore) DeleteCurrentStep(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCurrentStep", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCurrentStep indicates an expected call of DeleteCurrentStep +func (mr *MockStoreMockRecorder) DeleteCurrentStep(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCurrentStep", reflect.TypeOf((*MockStore)(nil).DeleteCurrentStep), arg0) +} + // DeleteDailySummaryUserSettings mocks base method func (m *MockStore) DeleteDailySummaryUserSettings(arg0 string) error { m.ctrl.T.Helper() @@ -117,6 +131,36 @@ func (mr *MockStoreMockRecorder) DeleteUserSubscription(arg0, arg1 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserSubscription", reflect.TypeOf((*MockStore)(nil).DeleteUserSubscription), arg0, arg1) } +// DeleteUserWelcomePost mocks base method +func (m *MockStore) DeleteUserWelcomePost(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUserWelcomePost", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteUserWelcomePost indicates an expected call of DeleteUserWelcomePost +func (mr *MockStoreMockRecorder) DeleteUserWelcomePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserWelcomePost", reflect.TypeOf((*MockStore)(nil).DeleteUserWelcomePost), arg0) +} + +// GetCurrentStep mocks base method +func (m *MockStore) GetCurrentStep(arg0 string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCurrentStep", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCurrentStep indicates an expected call of GetCurrentStep +func (mr *MockStoreMockRecorder) GetCurrentStep(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentStep", reflect.TypeOf((*MockStore)(nil).GetCurrentStep), arg0) +} + // GetPanelPostID mocks base method func (m *MockStore) GetPanelPostID(arg0 string) (string, error) { m.ctrl.T.Helper() @@ -132,6 +176,21 @@ func (mr *MockStoreMockRecorder) GetPanelPostID(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPanelPostID", reflect.TypeOf((*MockStore)(nil).GetPanelPostID), arg0) } +// GetPostID mocks base method +func (m *MockStore) GetPostID(arg0, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPostID", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPostID indicates an expected call of GetPostID +func (mr *MockStoreMockRecorder) GetPostID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostID", reflect.TypeOf((*MockStore)(nil).GetPostID), arg0, arg1) +} + // GetSetting mocks base method func (m *MockStore) GetSetting(arg0, arg1 string) (interface{}, error) { m.ctrl.T.Helper() @@ -267,6 +326,21 @@ func (mr *MockStoreMockRecorder) LoadUserIndex() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserIndex", reflect.TypeOf((*MockStore)(nil).LoadUserIndex)) } +// LoadUserWelcomePost mocks base method +func (m *MockStore) LoadUserWelcomePost(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadUserWelcomePost", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadUserWelcomePost indicates an expected call of LoadUserWelcomePost +func (mr *MockStoreMockRecorder) LoadUserWelcomePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserWelcomePost", reflect.TypeOf((*MockStore)(nil).LoadUserWelcomePost), arg0) +} + // ModifyDailySummaryIndex mocks base method func (m *MockStore) ModifyDailySummaryIndex(arg0 func(store.DailySummaryIndex) (store.DailySummaryIndex, error)) error { m.ctrl.T.Helper() @@ -295,18 +369,32 @@ func (mr *MockStoreMockRecorder) ModifyUserIndex(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifyUserIndex", reflect.TypeOf((*MockStore)(nil).ModifyUserIndex), arg0) } -// StoreDailySummaryUserSettings mocks base method -func (m *MockStore) StoreDailySummaryUserSettings(arg0 *store.DailySummaryUserSettings) error { +// RemovePostID mocks base method +func (m *MockStore) RemovePostID(arg0, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StoreDailySummaryUserSettings", arg0) + ret := m.ctrl.Call(m, "RemovePostID", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// StoreDailySummaryUserSettings indicates an expected call of StoreDailySummaryUserSettings -func (mr *MockStoreMockRecorder) StoreDailySummaryUserSettings(arg0 interface{}) *gomock.Call { +// RemovePostID indicates an expected call of RemovePostID +func (mr *MockStoreMockRecorder) RemovePostID(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreDailySummaryUserSettings", reflect.TypeOf((*MockStore)(nil).StoreDailySummaryUserSettings), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePostID", reflect.TypeOf((*MockStore)(nil).RemovePostID), arg0, arg1) +} + +// SetCurrentStep mocks base method +func (m *MockStore) SetCurrentStep(arg0 string, arg1 int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetCurrentStep", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetCurrentStep indicates an expected call of SetCurrentStep +func (mr *MockStoreMockRecorder) SetCurrentStep(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCurrentStep", reflect.TypeOf((*MockStore)(nil).SetCurrentStep), arg0, arg1) } // SetPanelPostID mocks base method @@ -323,6 +411,34 @@ func (mr *MockStoreMockRecorder) SetPanelPostID(arg0, arg1 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPanelPostID", reflect.TypeOf((*MockStore)(nil).SetPanelPostID), arg0, arg1) } +// SetPostID mocks base method +func (m *MockStore) SetPostID(arg0, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetPostID", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetPostID indicates an expected call of SetPostID +func (mr *MockStoreMockRecorder) SetPostID(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPostID", reflect.TypeOf((*MockStore)(nil).SetPostID), arg0, arg1, arg2) +} + +// SetProperty mocks base method +func (m *MockStore) SetProperty(arg0, arg1 string, arg2 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetProperty", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetProperty indicates an expected call of SetProperty +func (mr *MockStoreMockRecorder) SetProperty(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProperty", reflect.TypeOf((*MockStore)(nil).SetProperty), arg0, arg1, arg2) +} + // SetSetting mocks base method func (m *MockStore) SetSetting(arg0, arg1 string, arg2 interface{}) error { m.ctrl.T.Helper() @@ -337,6 +453,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) } +// StoreDailySummaryUserSettings mocks base method +func (m *MockStore) StoreDailySummaryUserSettings(arg0 *store.DailySummaryUserSettings) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StoreDailySummaryUserSettings", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// StoreDailySummaryUserSettings indicates an expected call of StoreDailySummaryUserSettings +func (mr *MockStoreMockRecorder) StoreDailySummaryUserSettings(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreDailySummaryUserSettings", reflect.TypeOf((*MockStore)(nil).StoreDailySummaryUserSettings), arg0) +} + // StoreOAuth2State mocks base method func (m *MockStore) StoreOAuth2State(arg0 string) error { m.ctrl.T.Helper() @@ -407,6 +537,20 @@ func (mr *MockStoreMockRecorder) StoreUserSubscription(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreUserSubscription", reflect.TypeOf((*MockStore)(nil).StoreUserSubscription), arg0, arg1) } +// StoreUserWelcomePost mocks base method +func (m *MockStore) StoreUserWelcomePost(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StoreUserWelcomePost", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// StoreUserWelcomePost indicates an expected call of StoreUserWelcomePost +func (mr *MockStoreMockRecorder) StoreUserWelcomePost(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreUserWelcomePost", reflect.TypeOf((*MockStore)(nil).StoreUserWelcomePost), arg0, arg1) +} + // VerifyOAuth2State mocks base method func (m *MockStore) VerifyOAuth2State(arg0 string) error { m.ctrl.T.Helper() diff --git a/server/store/store.go b/server/store/store.go index 5a3afc48..4a474645 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -9,6 +9,7 @@ import ( "github.com/mattermost/mattermost-server/v5/plugin" "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" ) @@ -20,6 +21,7 @@ const ( OAuth2KeyPrefix = "oauth2_" SubscriptionKeyPrefix = "sub_" EventKeyPrefix = "ev_" + WelcomeKeyPrefix = "welcome_" SettingsPanelPrefix = "settings_panel_" DailySummaryKeyPrefix = "dsum_" ) @@ -33,6 +35,8 @@ type Store interface { OAuth2StateStore SubscriptionStore EventStore + WelcomeStore + flow.FlowStore settingspanel.SettingStore settingspanel.PanelStore DailySummaryStore @@ -46,6 +50,7 @@ type pluginStore struct { userIndexKV kvstore.KVStore subscriptionKV kvstore.KVStore eventKV kvstore.KVStore + welcomeIndexKV kvstore.KVStore settingsPanelKV kvstore.KVStore dailySummaryKV kvstore.KVStore Logger bot.Logger @@ -62,6 +67,7 @@ func NewPluginStore(api plugin.API, logger bot.Logger) Store { eventKV: kvstore.NewHashedKeyStore(basicKV, EventKeyPrefix), dailySummaryKV: kvstore.NewHashedKeyStore(basicKV, DailySummaryKeyPrefix), oauth2KV: kvstore.NewHashedKeyStore(kvstore.NewOneTimePluginStore(api, OAuth2KeyExpiration), OAuth2KeyPrefix), + welcomeIndexKV: kvstore.NewHashedKeyStore(basicKV, WelcomeKeyPrefix), settingsPanelKV: kvstore.NewHashedKeyStore(basicKV, SettingsPanelPrefix), Logger: logger, } diff --git a/server/store/user_store.go b/server/store/user_store.go index 63da6266..7836f932 100644 --- a/server/store/user_store.go +++ b/server/store/user_store.go @@ -33,11 +33,12 @@ type UserShort struct { } type User struct { - PluginVersion string - Remote *remote.User - MattermostUserID string - OAuth2Token *oauth2.Token - Settings Settings `json:"mattermostSettings,omitempty"` + PluginVersion string + Remote *remote.User + MattermostUserID string + OAuth2Token *oauth2.Token + Settings Settings `json:"mattermostSettings,omitempty"` + WelcomeFlowStatus WelcomeFlowStatus `json:"mattermostFlags,omitempty"` } type Settings struct { @@ -46,6 +47,13 @@ type Settings struct { GetConfirmation bool } +type WelcomeFlowStatus struct { + UpdateStatusPostID string + GetConfirmationPostID string + SubscribePostID string + Step int +} + func (settings Settings) String() string { sub := "no subscription" if settings.EventSubscriptionID != "" { diff --git a/server/store/welcome_store.go b/server/store/welcome_store.go new file mode 100644 index 00000000..e7c3d08e --- /dev/null +++ b/server/store/welcome_store.go @@ -0,0 +1,36 @@ +package store + +import "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/kvstore" + +type WelcomeStore interface { + LoadUserWelcomePost(mattermostID string) (string, error) + StoreUserWelcomePost(mattermostID, postID string) error + DeleteUserWelcomePost(mattermostID string) (string, error) +} + +func (s *pluginStore) LoadUserWelcomePost(mattermostID string) (string, error) { + var postID string + err := kvstore.LoadJSON(s.welcomeIndexKV, mattermostID, &postID) + if err != nil { + return "", err + } + return postID, nil +} + +func (s *pluginStore) StoreUserWelcomePost(mattermostID, postID string) error { + err := kvstore.StoreJSON(s.welcomeIndexKV, mattermostID, postID) + if err != nil { + return err + } + return nil +} + +func (s *pluginStore) DeleteUserWelcomePost(mattermostID string) (string, error) { + var postID string + kvstore.LoadJSON(s.welcomeIndexKV, mattermostID, &postID) + err := s.welcomeIndexKV.Delete(mattermostID) + if err != nil { + return "", err + } + return postID, nil +} diff --git a/server/utils/bot/bot.go b/server/utils/bot/bot.go index f7e7e9f9..3928ea79 100644 --- a/server/utils/bot/bot.go +++ b/server/utils/bot/bot.go @@ -6,6 +6,7 @@ package bot import ( "github.com/pkg/errors" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/flow" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/plugin" ) @@ -14,10 +15,12 @@ type Bot interface { Poster Logger Admin + FlowController Ensure(stored *model.Bot, iconPath string) error WithConfig(BotConfig) Bot MattermostUserID() string + RegisterFlow(flow.Flow, flow.FlowStore) } type bot struct { @@ -27,15 +30,25 @@ type bot struct { mattermostUserID string displayName string logContext LogContext + pluginURL string + + flow flow.Flow + flowStore flow.FlowStore } -func New(api plugin.API, helpers plugin.Helpers) Bot { +func New(api plugin.API, helpers plugin.Helpers, pluginURL string) Bot { return &bot{ pluginAPI: api, pluginHelpers: helpers, + pluginURL: pluginURL, } } +func (bot *bot) RegisterFlow(flow flow.Flow, flowStore flow.FlowStore) { + bot.flow = flow + bot.flowStore = flowStore +} + func (bot *bot) Ensure(stored *model.Bot, iconPath string) error { if bot.mattermostUserID != "" { // Already done diff --git a/server/utils/bot/flow_controller.go b/server/utils/bot/flow_controller.go new file mode 100644 index 00000000..c33d126e --- /dev/null +++ b/server/utils/bot/flow_controller.go @@ -0,0 +1,108 @@ +package bot + +import "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/flow" + +type FlowController interface { + Start(userID string) error + NextStep(userID string, from int, value bool) error + Cancel(userID string) error +} + +func (bot *bot) Start(userID string) error { + err := bot.setFlowStep(userID, 0) + if err != nil { + return err + } + return bot.processStep(userID, bot.flow.Step(0), 0) +} + +func (bot *bot) NextStep(userID string, from int, value bool) error { + step, err := bot.getFlowStep(userID) + if err != nil { + return err + } + + if step != from { + return nil + } + + skip := bot.flow.Step(step).ShouldSkip(value) + step += 1 + skip + if step >= bot.flow.Length() { + bot.removeFlowStep(userID) + bot.flow.FlowDone(userID) + return nil + } + + err = bot.setFlowStep(userID, step) + if err != nil { + return err + } + + return bot.processStep(userID, bot.flow.Step(step), step) +} + +func (bot *bot) Cancel(userID string) error { + stepIndex, err := bot.getFlowStep(userID) + if err != nil { + return err + } + + step := bot.flow.Step(stepIndex) + if step == nil { + return nil + } + + postID, err := bot.flowStore.GetPostID(userID, step.GetPropertyName()) + if err != nil { + return err + } + + err = bot.DeletePost(postID) + if err != nil { + return err + } + + return nil +} + +func (bot *bot) setFlowStep(userID string, step int) error { + return bot.flowStore.SetCurrentStep(userID, step) +} + +func (bot *bot) getFlowStep(userID string) (int, error) { + return bot.flowStore.GetCurrentStep(userID) +} + +func (bot *bot) removeFlowStep(userID string) error { + return bot.flowStore.DeleteCurrentStep(userID) +} + +func (bot *bot) processStep(userID string, step flow.Step, i int) error { + if step == nil { + bot.Errorf("Step nil") + } + + if bot.flow == nil { + bot.Errorf("Bot nil") + } + + if bot.flowStore == nil { + bot.Errorf("Store nil") + } + postID, err := bot.DMWithAttachments(userID, step.PostSlackAttachment(bot.pluginURL+bot.flow.URL(), i)) + if err != nil { + return err + } + + if step.IsEmpty() { + return bot.NextStep(userID, i, false) + } + + err = bot.flowStore.SetPostID(userID, step.GetPropertyName(), postID) + if err != nil { + return err + } + + return nil +} diff --git a/server/utils/bot/mock_bot/mock_poster.go b/server/utils/bot/mock_bot/mock_poster.go index 0db53f85..2971b383 100644 --- a/server/utils/bot/mock_bot/mock_poster.go +++ b/server/utils/bot/mock_bot/mock_poster.go @@ -34,15 +34,16 @@ func (m *MockPoster) EXPECT() *MockPosterMockRecorder { } // DM mocks base method -func (m *MockPoster) DM(arg0, arg1 string, arg2 ...interface{}) error { +func (m *MockPoster) DM(arg0, arg1 string, arg2 ...interface{}) (string, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "DM", varargs...) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DM indicates an expected call of DM @@ -52,6 +53,25 @@ func (mr *MockPosterMockRecorder) DM(arg0, arg1 interface{}, arg2 ...interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DM", reflect.TypeOf((*MockPoster)(nil).DM), varargs...) } +// DMUpdate mocks base method +func (m *MockPoster) DMUpdate(arg0, arg1 string, arg2 ...interface{}) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DMUpdate", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// DMUpdate indicates an expected call of DMUpdate +func (mr *MockPosterMockRecorder) DMUpdate(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, "DMUpdate", reflect.TypeOf((*MockPoster)(nil).DMUpdate), varargs...) +} + // DMWithAttachments mocks base method func (m *MockPoster) DMWithAttachments(arg0 string, arg1 ...*model.SlackAttachment) (string, error) { m.ctrl.T.Helper() diff --git a/server/utils/bot/poster.go b/server/utils/bot/poster.go index 6024a76a..12f702bd 100644 --- a/server/utils/bot/poster.go +++ b/server/utils/bot/poster.go @@ -12,7 +12,7 @@ import ( type Poster interface { // DM posts a simple Direct Message to the specified user - DM(mattermostUserID, format string, args ...interface{}) error + DM(mattermostUserID, format string, args ...interface{}) (string, error) // DMWithAttachments posts a Direct Message that contains Slack attachments. // Often used to include post actions. @@ -21,6 +21,9 @@ type Poster interface { // 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 + // DeletePost deletes a single post DeletePost(postID string) error @@ -29,14 +32,14 @@ type Poster interface { } // DM posts a simple Direct Message to the specified user -func (bot *bot) DM(mattermostUserID, format string, args ...interface{}) error { - _, err := bot.dm(mattermostUserID, &model.Post{ +func (bot *bot) DM(mattermostUserID, format string, args ...interface{}) (string, error) { + postID, err := bot.dm(mattermostUserID, &model.Post{ Message: fmt.Sprintf(format, args...), }) if err != nil { - return err + return "", err } - return nil + return postID, nil } // DMWithAttachments posts a Direct Message that contains Slack attachments. @@ -85,6 +88,21 @@ func (bot *bot) Ephemeral(userId, channelId, format string, args ...interface{}) _ = bot.pluginAPI.SendEphemeralPost(userId, post) } +func (bot *bot) DMUpdate(postID, format string, args ...interface{}) error { + post, appErr := bot.pluginAPI.GetPost(postID) + if appErr != nil { + return appErr + } + + post.Message = fmt.Sprintf(format, args...) + _, appErr = bot.pluginAPI.UpdatePost(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/flow/empty_step.go b/server/utils/flow/empty_step.go new file mode 100644 index 00000000..75618efc --- /dev/null +++ b/server/utils/flow/empty_step.go @@ -0,0 +1,33 @@ +package flow + +import "github.com/mattermost/mattermost-server/v5/model" + +type EmptyStep struct { + Title string + Message string +} + +func (s *EmptyStep) PostSlackAttachment(flowHandler string, i int) *model.SlackAttachment { + sa := model.SlackAttachment{ + Title: s.Title, + Text: s.Message, + } + + return &sa +} + +func (s *EmptyStep) ResponseSlackAttachment(value bool) *model.SlackAttachment { + return nil +} + +func (s *EmptyStep) GetPropertyName() string { + return "" +} + +func (s *EmptyStep) ShouldSkip(value bool) int { + return 0 +} + +func (s *EmptyStep) IsEmpty() bool { + return true +} diff --git a/server/utils/flow/flow.go b/server/utils/flow/flow.go new file mode 100644 index 00000000..d7d26ba3 --- /dev/null +++ b/server/utils/flow/flow.go @@ -0,0 +1,100 @@ +package flow + +import ( + "strconv" + + "github.com/mattermost/mattermost-server/v5/model" +) + +type Flow interface { + Step(i int) Step + URL() string + Length() int + StepDone(userID string, step int, value bool) + FlowDone(userID string) +} + +type FlowStore interface { + SetProperty(userID, propertyName string, value bool) error + SetPostID(userID, propertyName, postID string) error + GetPostID(userID, propertyName string) (string, error) + RemovePostID(userID, propertyName string) error + GetCurrentStep(userID string) (int, error) + SetCurrentStep(userID string, step int) error + DeleteCurrentStep(userID string) error +} + +type Step interface { + PostSlackAttachment(flowHandler string, i int) *model.SlackAttachment + ResponseSlackAttachment(value bool) *model.SlackAttachment + GetPropertyName() string + ShouldSkip(value bool) int + IsEmpty() bool +} + +type SimpleStep struct { + Title string + Message string + PropertyName string + TrueButtonMessage string + FalseButtonMessage string + TrueResponseMessage string + FalseResponseMessage string + TrueSkip int + FalseSkip int +} + +func (s *SimpleStep) PostSlackAttachment(flowHandler string, i int) *model.SlackAttachment { + actionTrue := model.PostAction{ + Name: s.TrueButtonMessage, + Integration: &model.PostActionIntegration{ + URL: flowHandler + "?" + s.PropertyName + "=true&step=" + strconv.Itoa(i), + }, + } + + actionFalse := model.PostAction{ + Name: s.FalseButtonMessage, + Integration: &model.PostActionIntegration{ + URL: flowHandler + "?" + s.PropertyName + "=false&step=" + strconv.Itoa(i), + }, + } + + sa := model.SlackAttachment{ + Title: s.Title, + Text: s.Message, + Actions: []*model.PostAction{&actionTrue, &actionFalse}, + } + + return &sa +} + +func (s *SimpleStep) ResponseSlackAttachment(value bool) *model.SlackAttachment { + message := s.FalseResponseMessage + if value { + message = s.TrueResponseMessage + } + + sa := model.SlackAttachment{ + Title: s.Title, + Text: message, + Actions: []*model.PostAction{}, + } + + return &sa +} + +func (s *SimpleStep) GetPropertyName() string { + return s.PropertyName +} + +func (s *SimpleStep) ShouldSkip(value bool) int { + if value { + return s.TrueSkip + } + + return s.FalseSkip +} + +func (s *SimpleStep) IsEmpty() bool { + return false +} diff --git a/server/utils/flow/handler.go b/server/utils/flow/handler.go new file mode 100644 index 00000000..9e067474 --- /dev/null +++ b/server/utils/flow/handler.go @@ -0,0 +1,75 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package flow + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/mattermost/mattermost-server/v5/model" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" +) + +type fh struct { + flow Flow + store FlowStore +} + +func Init(h *httputils.Handler, flow Flow, store FlowStore) { + fh := &fh{ + flow: flow, + store: store, + } + + flowRouter := h.Router.PathPrefix("/").Subrouter() + flowRouter.HandleFunc(flow.URL(), fh.handleFlow).Methods(http.MethodPost) +} + +func (fh *fh) handleFlow(w http.ResponseWriter, r *http.Request) { + mattermostUserID := r.Header.Get("Mattermost-User-ID") + if mattermostUserID == "" { + utils.SlackAttachmentError(w, "Error: Not authorized") + return + } + + stepNumber, err := strconv.Atoi(r.URL.Query().Get("step")) + if err != nil { + utils.SlackAttachmentError(w, fmt.Sprintf("Error: Step provided is not an int, err=%s", err.Error())) + return + } + + step := fh.flow.Step(stepNumber) + if step == nil { + utils.SlackAttachmentError(w, fmt.Sprintf("Error: There is no step %d.", step)) + return + } + + property := step.GetPropertyName() + valueString := r.URL.Query().Get(property) + if valueString == "" { + utils.SlackAttachmentError(w, "Correct property not set") + return + } + + value := valueString == "true" + err = fh.store.SetProperty(mattermostUserID, property, value) + if err != nil { + utils.SlackAttachmentError(w, "There has been a problem setting the property, err="+err.Error()) + return + } + + response := model.PostActionIntegrationResponse{} + post := model.Post{} + model.ParseSlackAttachment(&post, []*model.SlackAttachment{step.ResponseSlackAttachment(value)}) + response.Update = &post + + w.Header().Set("Content-Type", "application/json") + w.Write(response.ToJson()) + + fh.store.RemovePostID(mattermostUserID, property) + fh.flow.StepDone(mattermostUserID, stepNumber, value) +} diff --git a/server/utils/settingspanel/handler.go b/server/utils/settingspanel/handler.go index 3f421c07..ff6f97ef 100644 --- a/server/utils/settingspanel/handler.go +++ b/server/utils/settingspanel/handler.go @@ -3,6 +3,7 @@ package settingspanel import ( "net/http" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils" "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" "github.com/mattermost/mattermost-server/v5/model" ) @@ -23,25 +24,25 @@ func Init(h *httputils.Handler, panel Panel) { } panelRouter := h.Router.PathPrefix("/").Subrouter() - panelRouter.HandleFunc(panel.URL(), sh.handleAction).Methods("POST") + panelRouter.HandleFunc(panel.URL(), sh.handleAction).Methods(http.MethodPost) } func (sh *handler) handleAction(w http.ResponseWriter, r *http.Request) { mattermostUserID := r.Header.Get("Mattermost-User-ID") if mattermostUserID == "" { - http.Error(w, "Not authorized", http.StatusUnauthorized) + utils.SlackAttachmentError(w, "Error: Not authorized") return } request := model.PostActionIntegrationRequestFromJson(r.Body) if request == nil { - http.Error(w, "invalid request", http.StatusBadRequest) + utils.SlackAttachmentError(w, "Error: invalid request") return } id, ok := request.Context[ContextIDKey] if !ok { - http.Error(w, "invalid request", http.StatusBadRequest) + utils.SlackAttachmentError(w, "Error: no id in context") return } @@ -49,13 +50,17 @@ func (sh *handler) handleAction(w http.ResponseWriter, r *http.Request) { if !ok { value, ok = request.Context[ContextOptionValueKey] if !ok { - http.Error(w, "valid key not found", http.StatusBadRequest) + utils.SlackAttachmentError(w, "Error: valid key not found") return } } idString := id.(string) - sh.panel.Set(mattermostUserID, idString, value) + err := sh.panel.Set(mattermostUserID, idString, value) + if err != nil { + utils.SlackAttachmentError(w, "Error: cannot set the property, "+err.Error()) + return + } response := model.PostActionIntegrationResponse{} post, err := sh.panel.ToPost(mattermostUserID)