Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GCAL] Channels reminder underlying logic #274

Merged
merged 53 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4d3a2bb
nil control
fmartingr Jul 25, 2023
fe6fe75
use subscriptions default ttl
fmartingr Jul 25, 2023
7634f2c
return specific error when no superuser token can be used
fmartingr Jul 25, 2023
9dc21a2
notification formatting logic to other file
fmartingr Jul 25, 2023
a436769
Client.GetEventsBetweenDates
fmartingr Jul 25, 2023
18767a3
typo: import
fmartingr Jul 25, 2023
8de4223
shorter command name :)
fmartingr Jul 25, 2023
af611a9
availability without super user token
fmartingr Jul 25, 2023
24c2c64
delete store subs only if we successfuly do on remote
fmartingr Jul 25, 2023
0ecad3f
comment
fmartingr Jul 25, 2023
165948c
ignore missing subs
fmartingr Jul 25, 2023
2e6c571
update sync method
fmartingr Jul 25, 2023
cc88b2e
allow switching acting user on the fly
fmartingr Jul 25, 2023
0def44a
daily summary without super user token
fmartingr Jul 25, 2023
c5f0e1c
formatted imports
fmartingr Jul 25, 2023
0b7bce2
calendar -> calendarEvents
fmartingr Jul 25, 2023
100497a
processAllDailySummaryByUser
fmartingr Jul 25, 2023
d90fb5e
revert silenced error under notifications
fmartingr Jul 25, 2023
4345953
msgraph GetEventsBetweenDates
fmartingr Jul 25, 2023
57b30f9
Update server/mscalendar/daily_summary.go
fmartingr Jul 26, 2023
92de736
[GCAL] Create event logic (#269)
fmartingr Jul 25, 2023
3e3f43d
Client.GetEventsBetweenDates
fmartingr Jul 25, 2023
5c05a74
fix: store last post time
fmartingr Jul 26, 2023
5dbc701
refactored availability logic
fmartingr Jul 26, 2023
19abc70
availability test
fmartingr Jul 26, 2023
c3336e9
refactored daily summary logic
fmartingr Jul 26, 2023
e4f73fd
test: availability
fmartingr Jul 26, 2023
9951416
daily summary tests (wip)
fmartingr Jul 26, 2023
a92d545
daily summary test
fmartingr Jul 27, 2023
c090410
fix: merge duplications
fmartingr Jul 27, 2023
ecca739
slack attachment for notifications
fmartingr Jul 27, 2023
6f2b1d7
lint and test
fmartingr Jul 27, 2023
351730c
goimports
fmartingr Jul 27, 2023
cb62ef8
Allow poster to create posts
fmartingr Jul 27, 2023
076eac4
Allow store event metadata
fmartingr Jul 27, 2023
61239b2
styling
fmartingr Jul 27, 2023
edb1105
store recurring event id
fmartingr Jul 27, 2023
22449e8
deliver channel notifications
fmartingr Jul 27, 2023
bdcc758
test: fixed for base case
fmartingr Jul 28, 2023
7d3d03f
test: channel reminders
fmartingr Jul 28, 2023
d3a4628
test: recurring events
fmartingr Jul 28, 2023
faaf79c
move from slice to map
fmartingr Jul 28, 2023
84cea59
Added store methods to interact with event metadata
fmartingr Jul 28, 2023
742609b
lint
fmartingr Jul 28, 2023
71ff7d2
lint imports
fmartingr Jul 28, 2023
f1b3d97
Merge remote-tracking branch 'origin/migrate-to-gcal' into fmartingr/…
fmartingr Aug 1, 2023
fced166
restore logic lost in merge
fmartingr Aug 1, 2023
a83217b
Merge remote-tracking branch 'origin/migrate-to-gcal' into fmartingr/…
fmartingr Aug 1, 2023
86127c6
duplicated test
fmartingr Aug 1, 2023
65e6cf4
Update server/mscalendar/availability_test.go
fmartingr Aug 1, 2023
288590f
remove recurrent event id field
fmartingr Aug 1, 2023
1673918
Merge remote-tracking branch 'origin/migrate-to-gcal' into fmartingr/…
fmartingr Aug 1, 2023
2d7fa0a
Merge remote-tracking branch 'origin/migrate-to-gcal' into fmartingr/…
fmartingr Aug 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion server/mscalendar/availability.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ func (m *mscalendar) notifyUpcomingEvents(mattermostUserID string, events []*rem
}
}

_, attachment, err := views.RenderUpcomingEventAsAttachment(event, timezone)
message, attachment, err := views.RenderUpcomingEventAsAttachment(event, timezone)
if err != nil {
m.Logger.Warnf("notifyUpcomingEvent error rendering schedule item. err=%v", err)
continue
Expand All @@ -516,6 +516,31 @@ func (m *mscalendar) notifyUpcomingEvents(mattermostUserID string, events []*rem
m.Logger.Warnf("notifyUpcomingEvents error creating DM. err=%v", err)
continue
}

// Process channel reminders
eventMetadata, errMetadata := m.Store.LoadEventMetadata(event.GetMainID())
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.LinkedChannels {
post := &model.Post{
ChannelId: channelID,
Message: message,
}
model.ParseSlackAttachment(post, []*model.SlackAttachment{attachment})
err = m.Poster.CreatePost(post)
if err != nil {
m.Logger.Warnf("notifyUpcomingEvents error creating post in channel. err=%v", err)
continue
}
}
}
}
}
}
Expand Down
37 changes: 37 additions & 0 deletions server/mscalendar/availability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/mattermost/mattermost-plugin-mscalendar/server/store"
"github.com/mattermost/mattermost-plugin-mscalendar/server/store/mock_store"
"github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot/mock_bot"
"github.com/mattermost/mattermost-plugin-mscalendar/server/utils/test"
)

func TestSyncStatusAll(t *testing.T) {
Expand Down Expand Up @@ -249,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
}{
Expand Down Expand Up @@ -293,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", ICalUID: "event_id", 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": {
LinkedChannels: map[string]struct{}{"channel_id": {}},
fmartingr marked this conversation as resolved.
Show resolved Hide resolved
},
},
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_20230728", RecurringEventID: "event_id", ICalUID: "event_id", Start: remote.NewDateTime(time.Now().Add(7*time.Minute).UTC(), "UTC"), End: remote.NewDateTime(time.Now().Add(45*time.Minute).UTC(), "UTC")},
fmartingr marked this conversation as resolved.
Show resolved Hide resolved
},
eventMetadata: map[string]*store.EventMetadata{
"event_id": {
LinkedChannels: map[string]struct{}{"channel_id": {}},
},
},
numReminders: 1,
shouldLogError: false,
},
"Remote API Error. Error should be logged.": {
remoteEvents: []*remote.Event{},
numReminders: 0,
Expand Down Expand Up @@ -333,6 +359,17 @@ func TestReminders(t *testing.T) {
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.LinkedChannels {
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)
Expand Down
107 changes: 107 additions & 0 deletions server/mscalendar/daily_summary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,113 @@ Wednesday February 12, 2020
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),
)

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(2)

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(1)

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.TODO(), gomock.Any()).Return(mockClient)

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),
Expand Down
26 changes: 13 additions & 13 deletions server/mscalendar/views/calendar.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"net/url"
"sort"
"strings"
"time"

"github.com/mattermost/mattermost-server/v6/model"
Expand Down Expand Up @@ -113,12 +112,14 @@ func renderEvent(event *remote.Event, asRow bool, timeZone string) (string, erro
}

func isKnownMeetingURL(location string) bool {
return strings.Contains(location, "zoom.us/j/") || strings.Contains(location, "discord.gg") || strings.Contains(location, "meet.google.com")
_, err := url.ParseRequestURI(location)
return err == nil
}

func renderEventAsAttachment(event *remote.Event, timezone string) (*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{
Expand All @@ -127,19 +128,18 @@ func renderEventAsAttachment(event *remote.Event, timezone string) (*model.Slack
Short: true,
})

// Add actions for known links
// Disable join meeting button for now, since we don't have a handler and
// the location url is shown parsed and clickable anyway.
// if joinMeetingAction := getActionForLocation(event.Location); joinMeetingAction != nil {
// actions = append(actions, joinMeetingAction)
// }
// Use location display name as link if can be parsed as an URL
if isKnownMeetingURL(event.Location.DisplayName) {
titleLink = event.Location.DisplayName
}
}

return &model.SlackAttachment{
Title: event.Subject,
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,
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,
}, nil
}

Expand Down Expand Up @@ -189,7 +189,7 @@ func EnsureSubject(s string) string {
}

func RenderUpcomingEventAsAttachment(event *remote.Event, timeZone string) (message string, attachment *model.SlackAttachment, err error) {
message = "You have an upcoming event:\n"
message = "Upcoming event:\n"
attachment, err = renderEventAsAttachment(event, timeZone)
return message, attachment, err
}
8 changes: 8 additions & 0 deletions server/remote/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Event struct {
ShowAs string `json:"showAs,omitempty"`
Weblink string `json:"weblink,omitempty"`
ID string `json:"id,omitempty"`
RecurringEventID string `json:"recurringEventId,omitempty"`
Attendees []*Attendee `json:"attendees,omitempty"`
ReminderMinutesBeforeStart int `json:"reminderMinutesBeforeStart,omitempty"`
IsOrganizer bool `json:"isOrganizer,omitempty"`
Expand All @@ -26,6 +27,13 @@ type Event struct {
ResponseRequested bool `json:"responseRequested,omitempty"`
}

func (e Event) GetMainID() string {
if e.RecurringEventID != "" {
return e.RecurringEventID
}
return e.ID
fmartingr marked this conversation as resolved.
Show resolved Hide resolved
}

type ItemBody struct {
Content string `json:"content,omitempty"`
ContentType string `json:"contentType,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions server/remote/gcal/get_default_calendar_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func convertGCalEventToRemoteEvent(event *calendar.Event) *remote.Event {

return &remote.Event{
ID: event.Id,
RecurringEventID: event.RecurringEventId,
ICalUID: event.ICalUID,
Subject: event.Summary,
Body: &remote.ItemBody{Content: event.Description},
Expand Down
52 changes: 52 additions & 0 deletions server/store/event_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"encoding/json"
"time"

"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/server/utils/kvstore"
Expand All @@ -18,18 +20,29 @@ import (
const ttlAfterEventEnd = 30 * 24 * time.Hour // 30 days
const defaultEventTTL = 30 * 24 * time.Hour // 30 days

type EventMetadata struct {
LinkedChannels map[string]struct{}
fmartingr marked this conversation as resolved.
Show resolved Hide resolved
}

type Event struct {
Remote *remote.Event
PluginVersion string
}

type EventStore interface {
LoadEventMetadata(eventID string) (*EventMetadata, error)
StoreEventMetadata(eventID string, eventMeta *EventMetadata) 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{}
Expand All @@ -40,6 +53,45 @@ 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
}

eventMeta.LinkedChannels[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.LinkedChannels, 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) StoreUserEvent(mattermostUserID string, event *Event) error {
now := time.Now()
end := now.Add(defaultEventTTL)
Expand Down
Loading
Loading