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] today/tomorrow commands with styling #273

Merged
merged 11 commits into from
Aug 1, 2023
9 changes: 9 additions & 0 deletions server/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ var cmds = []*model.AutocompleteData{
model.NewAutocompleteData("disconnect", "", fmt.Sprintf("Disconnect from your %s account", config.Provider.DisplayName)),
model.NewAutocompleteData("summary", "", "View your events for today, or edit the settings for your daily summary."),
model.NewAutocompleteData("viewcal", "", "View your events for the upcoming week."),
model.NewAutocompleteData("today", "", "Display today's events."),
model.NewAutocompleteData("tomorrow", "", "Display tomorrow's events."),
model.NewAutocompleteData("settings", "", "Edit your user personal settings."),
model.NewAutocompleteData("subscribe", "", "Enable notifications for event invitations and updates."),
model.NewAutocompleteData("unsubscribe", "", "Disable notifications for event invitations and updates."),
Expand Down Expand Up @@ -111,6 +113,13 @@ func (c *Command) Handle() (string, bool, error) {
handler = c.requireConnectedUser(c.requireAdminUser(c.debugAvailability))
case "settings":
handler = c.requireConnectedUser(c.settings)
// Aliases
case "today":
parameters = []string{"today"}
handler = c.requireConnectedUser(c.dailySummary)
case "tomorrow":
parameters = []string{"tomorrow"}
handler = c.requireConnectedUser(c.dailySummary)
}
out, mustRedirectToDM, err := handler(parameters...)
if err != nil {
Expand Down
14 changes: 10 additions & 4 deletions server/command/daily_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command

import (
"fmt"
"time"

"github.com/mattermost/mattermost-plugin-mscalendar/server/store"
)
Expand All @@ -21,8 +22,14 @@ func (c *Command) dailySummary(parameters ...string) (string, bool, error) {
}

switch parameters[0] {
case "view":
postStr, err := c.MSCalendar.GetDailySummaryForUser(c.user())
case "view", "today":
postStr, err := c.MSCalendar.GetDaySummaryForUser(time.Now(), c.user())
if err != nil {
return err.Error(), false, err
}
return postStr, false, nil
case "tomorrow":
postStr, err := c.MSCalendar.GetDaySummaryForUser(time.Now().Add((time.Hour*24)*4), c.user())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we add (time.Hour*24)*4 here? Also do we need to take into account weekends (maybe that's the intention here)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh no no no, I accidentaly commited that. Used to get the output of a specific day for a screenshot.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though this begs the question, should we have a special case for weekends? Currently, the daily summary job avoids sending the summary on the weekend, so there is already some convention in the plugin to avoid weekends. Maybe the tomorrow command should do something similar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't check that code, is it configurable? Does it take into consideration countries were weekends are longer/shorter/on different days?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the line that does this

if now.Weekday() == time.Saturday || now.Weekday() == time.Sunday {

It's currently not configurable, but we can use WorkingHours to figure out the correct days if available for gcal:

type WorkingHours struct {
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
TimeZone struct {
Name string `json:"name"`
}
DaysOfWeek []string `json:"daysOfWeek"`
}
type MailboxSettings struct {
TimeZone string `json:"timeZone"`
WorkingHours WorkingHours `json:"workingHours"`
}

if err != nil {
return err.Error(), false, err
}
Expand Down Expand Up @@ -59,9 +66,8 @@ func (c *Command) dailySummary(parameters ...string) (string, bool, error) {
return err.Error(), false, err
}
return dailySummaryResponse(dsum), false, nil
default:
return "Invalid command. Please try again\n\n" + dailySummaryHelp, false, nil
}
return "Invalid command. Please try again\n\n" + dailySummaryHelp, false, nil
}

func dailySummaryResponse(dsum *store.DailySummaryUserSettings) string {
Expand Down
13 changes: 9 additions & 4 deletions server/mscalendar/daily_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const dailySummaryTimeWindow = time.Minute * 2
const DailySummaryJobInterval = 15 * time.Minute

type DailySummary interface {
GetDailySummaryForUser(user *User) (string, error)
GetDaySummaryForUser(now time.Time, user *User) (string, error)
GetDailySummarySettingsForUser(user *User) (*store.DailySummaryUserSettings, error)
SetDailySummaryPostTime(user *User, timeStr string) (*store.DailySummaryUserSettings, error)
SetDailySummaryEnabled(user *User, enable bool) (*store.DailySummaryUserSettings, error)
Expand Down Expand Up @@ -174,18 +174,23 @@ func (m *mscalendar) ProcessAllDailySummary(now time.Time) error {
return nil
}

func (m *mscalendar) GetDailySummaryForUser(user *User) (string, error) {
func (m *mscalendar) GetDaySummaryForUser(day time.Time, user *User) (string, error) {
tz, err := m.GetTimezone(user)
if err != nil {
return "", err
}

calendarData, err := m.getTodayCalendarEvents(user, time.Now(), tz)
calendarData, err := m.getTodayCalendarEvents(user, day, tz)
if err != nil {
return "Failed to get calendar events", err
}

return views.RenderCalendarView(calendarData, tz)
messageString, err := views.RenderCalendarView(calendarData, tz)
if err != nil {
return "", errors.Wrap(err, "failed to render daily summary")
}

return messageString, nil
}

func shouldPostDailySummary(dsum *store.DailySummaryUserSettings, now time.Time) (bool, error) {
Expand Down
28 changes: 14 additions & 14 deletions server/mscalendar/mock_mscalendar/mock_mscalendar.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 55 additions & 4 deletions server/mscalendar/views/calendar.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"fmt"
"net/url"
"sort"
"strings"
"time"

"github.com/mattermost/mattermost-server/v6/model"

"github.com/mattermost/mattermost-plugin-mscalendar/server/remote"
)

Expand Down Expand Up @@ -41,9 +44,48 @@ func RenderCalendarView(events []*remote.Event, timeZone string) (string, error)
return resp, nil
}

func RenderDaySummary(events []*remote.Event, timezone string) (string, []*model.SlackAttachment, error) {
if len(events) == 0 {
return "You have no events for that day", nil, nil
}

if timezone != "" {
for _, e := range events {
e.Start = e.Start.In(timezone)
e.End = e.End.In(timezone)
}
}

message := fmt.Sprintf("Agenda for %s.\nTimes are shown in %s", events[0].Start.Time().Format("Monday, 02 January"), events[0].Start.TimeZone)

var attachments []*model.SlackAttachment
for _, event := range events {
var actions []*model.PostAction

fields := []*model.SlackAttachmentField{}
if event.Location != nil && event.Location.DisplayName != "" {
fields = append(fields, &model.SlackAttachmentField{
Title: "Location",
Value: event.Location.DisplayName,
Short: true,
})
}

attachments = append(attachments, &model.SlackAttachment{
Title: event.Subject,
// Text: event.BodyPreview,
Text: fmt.Sprintf("(%s - %s)", event.Start.In(timezone).Time().Format(time.Kitchen), event.End.In(timezone).Time().Format(time.Kitchen)),
Fields: fields,
Actions: actions,
})
}

return message, attachments, nil
}

func renderTableHeader() string {
return `| Time | Subject |
| :--: | :-- |`
return `| Time | Subject | |
| :-- | :-- | :--`
}

func renderEvent(event *remote.Event, asRow bool, timeZone string) (string, error) {
Expand All @@ -52,17 +94,26 @@ func renderEvent(event *remote.Event, asRow bool, timeZone string) (string, erro

format := "(%s - %s) [%s](%s)"
if asRow {
format = "| %s - %s | [%s](%s) |"
format = "| %s - %s | [%s](%s) | %s |"
}

link, err := url.QueryUnescape(event.Weblink)
if err != nil {
return "", err
}

var other string
if event.Location != nil && isKnownMeetingURL(event.Location.DisplayName) {
other = "[Join meeting](" + event.Location.DisplayName + ")"
}

subject := EnsureSubject(event.Subject)

return fmt.Sprintf(format, start, end, subject, link), nil
return fmt.Sprintf(format, start, end, subject, link, other), nil
}

func isKnownMeetingURL(location string) bool {
return strings.Contains(location, "zoom.us/j/") || strings.Contains(location, "discord.gg") || strings.Contains(location, "meet.google.com")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat 👍 Something comes to mind, that a customer may have other call-related domains, including Mattermost Calls. Maybe have this configurable in some way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I want to integrate calls too, but need to send the SITE_URL over to check the URL to do it properly, so it needs a bit more work. Also, Google sends conferenceData for some events instead of location (I'm assuming they have a specific integration), so it wont work every time.

}

func groupEventsByDate(events []*remote.Event) [][]*remote.Event {
Expand Down
14 changes: 7 additions & 7 deletions server/remote/gcal/get_default_calendar_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,23 @@ func (c *client) GetDefaultCalendarView(_ string, start, end time.Time) ([]*remo
req.SingleEvents(true)
req.OrderBy("startTime")

events, err := req.Do()
result, err := req.Do()
if err != nil {
return nil, errors.Wrap(err, "gcal GetDefaultCalendarView, error performing request")
}

result := []*remote.Event{}
if len(events.Items) == 0 {
return result, nil
if len(result.Items) == 0 {
return []*remote.Event{}, nil
}

for _, event := range events.Items {
events := []*remote.Event{}
for _, event := range result.Items {
if event.ICalUID != "" {
result = append(result, convertGCalEventToRemoteEvent(event))
events = append(events, convertGCalEventToRemoteEvent(event))
}
}

return result, nil
return events, nil
}

func convertGCalEventDateTimeToRemoteDateTime(dt *calendar.EventDateTime) *remote.DateTime {
Expand Down
20 changes: 20 additions & 0 deletions server/utils/bot/mock_bot/mock_poster.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions server/utils/bot/poster.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ type Poster interface {
// Often used to include post actions.
DMWithAttachments(mattermostUserID string, attachments ...*model.SlackAttachment) (string, error)

// DMWithMessageAndAttachments posts a Direct Message that contains Slack attachments and a message.
DMWithMessageAndAttachments(mattermostUserID, message string, attachments ...*model.SlackAttachment) (string, error)

// Ephemeral sends an ephemeral message to a user
Ephemeral(mattermostUserID, channelID, format string, args ...interface{})

Expand Down Expand Up @@ -50,6 +53,13 @@ func (bot *bot) DMWithAttachments(mattermostUserID string, attachments ...*model
return bot.dm(mattermostUserID, &post)
}

// DMWithMessageAndAttachments posts a Direct Message that contains Slack attachments and a message.
func (bot *bot) DMWithMessageAndAttachments(mattermostUserID, message string, attachments ...*model.SlackAttachment) (string, error) {
post := model.Post{Message: message}
model.ParseSlackAttachment(&post, attachments)
return bot.dm(mattermostUserID, &post)
}

func (bot *bot) dm(mattermostUserID string, post *model.Post) (string, error) {
channel, err := bot.pluginAPI.GetDirectChannel(mattermostUserID, bot.mattermostUserID)
if err != nil {
Expand Down
Loading