diff --git a/championship_manager.go b/championship_manager.go index 07715a9db..5b62bb3e1 100644 --- a/championship_manager.go +++ b/championship_manager.go @@ -600,12 +600,6 @@ func (cm *ChampionshipManager) ScheduleEvent(championshipID string, eventID stri return err } - serverOpts, err := cm.store.LoadServerOptions() - - if err != nil { - return err - } - event.Scheduled = date // if there is an existing schedule timer for this event stop it @@ -648,12 +642,15 @@ func (cm *ChampionshipManager) ScheduleEvent(championshipID string, eventID stri } }) - if serverOpts.NotificationReminderTimer > 0 { - duration = time.Until(date.Add(time.Duration(0-serverOpts.NotificationReminderTimer) * time.Minute)) + if cm.notificationManager.HasNotificationReminders() { + for _, timer := range cm.notificationManager.GetNotificationReminders() { + duration = time.Until(date.Add(time.Duration(0-timer) * time.Minute)) + thisTimer := timer - cm.championshipEventReminderTimers[event.ID.String()] = time.AfterFunc(duration, func() { - cm.notificationManager.SendChampionshipReminderMessage(championship, event) - }) + cm.championshipEventReminderTimers[event.ID.String()] = time.AfterFunc(duration, func() { + cm.notificationManager.SendChampionshipReminderMessage(championship, event, thisTimer) + }) + } } } else { event.ClearRecurrenceRule() @@ -1472,12 +1469,6 @@ func (cm *ChampionshipManager) InitScheduledChampionships() error { return err } - serverOpts, err := cm.store.LoadServerOptions() - - if err != nil { - return err - } - for _, championship := range championships { championship := championship @@ -1496,14 +1487,17 @@ func (cm *ChampionshipManager) InitScheduledChampionships() error { } }) - if serverOpts.NotificationReminderTimer > 0 { - if event.Scheduled.Add(time.Duration(0-serverOpts.NotificationReminderTimer) * time.Minute).After(time.Now()) { - // add reminder - duration = time.Until(event.Scheduled.Add(time.Duration(0-serverOpts.NotificationReminderTimer) * time.Minute)) - - cm.championshipEventReminderTimers[event.ID.String()] = time.AfterFunc(duration, func() { - cm.notificationManager.SendChampionshipReminderMessage(championship, event) - }) + if cm.notificationManager.HasNotificationReminders() { + for _, timer := range cm.notificationManager.GetNotificationReminders() { + if event.Scheduled.Add(time.Duration(0-timer) * time.Minute).After(time.Now()) { + // add reminder + duration = time.Until(event.Scheduled.Add(time.Duration(0-timer) * time.Minute)) + thisTimer := timer + + cm.championshipEventReminderTimers[event.ID.String()] = time.AfterFunc(duration, func() { + cm.notificationManager.SendChampionshipReminderMessage(championship, event, thisTimer) + }) + } } } diff --git a/championship_manager_test.go b/championship_manager_test.go index 2aa5a1b38..1c5bbbb90 100644 --- a/championship_manager_test.go +++ b/championship_manager_test.go @@ -111,7 +111,17 @@ var championshipManager *ChampionshipManager type dummyNotificationManager struct{} -func (d dummyNotificationManager) SendRaceWeekendReminderMessage(raceWeekend *RaceWeekend, session *RaceWeekendSession) error { +func (d *dummyNotificationManager) HasNotificationReminders() bool { + return false +} + +func (d *dummyNotificationManager) GetNotificationReminders() []int { + var reminders []int + + return reminders +} + +func (d dummyNotificationManager) SendRaceWeekendReminderMessage(raceWeekend *RaceWeekend, session *RaceWeekendSession, timer int) error { return nil } @@ -131,11 +141,15 @@ func (d dummyNotificationManager) SendRaceScheduledMessage(event *CustomRace, da return nil } -func (d dummyNotificationManager) SendRaceReminderMessage(event *CustomRace) error { +func (d dummyNotificationManager) SendRaceCancelledMessage(event *CustomRace, date time.Time) error { + return nil +} + +func (d dummyNotificationManager) SendRaceReminderMessage(event *CustomRace, timer int) error { return nil } -func (d dummyNotificationManager) SendChampionshipReminderMessage(championship *Championship, event *ChampionshipEvent) error { +func (d dummyNotificationManager) SendChampionshipReminderMessage(championship *Championship, event *ChampionshipEvent, timer int) error { return nil } diff --git a/config_ini.go b/config_ini.go index b5cce0a86..0c3786916 100644 --- a/config_ini.go +++ b/config_ini.go @@ -188,11 +188,16 @@ type GlobalServerConfig struct { UseMPH int `ini:"-" input:"checkbox" help:"When on, this option will make Server Manager use MPH instead of Km/h for all speed values."` // Discord Integration - DiscordIntegration FormHeading `ini:"-" input:"heading"` - DiscordAPIToken string `ini:"-" help:"If set, will enable race start and scheduled reminder messages to the Discord channel ID specified below. Use your bot's user token, not the OAuth token."` - DiscordChannelID string `ini:"-" help:"If Discord is enabled, this is the channel ID it will send messages to"` - NotificationReminderTimer int `ini:"-" min:"0" max:"65535" help:"If Discord is enabled, a reminder will be sent this many minutes prior to race start. If 0, only race start messages will be sent."` - ShowPasswordInNotifications int `ini:"-" show:"open" input:"checkbox" help:"Show the server password in race start notifications"` + DiscordIntegration FormHeading `ini:"-" input:"heading"` + DiscordAPIToken string `ini:"-" help:"If set, will enable race start and scheduled reminder messages to the Discord channel ID specified below. Use your bot's user token, not the OAuth token."` + DiscordChannelID string `ini:"-" help:"If Discord is enabled, this is the channel ID it will send messages to. To find the channel ID, enable Developer mode in Discord (user settings, Appearance), then Server Settings, Roles, and right click on the channel and Copy ID."` + DiscordRoleID string `ini:"-" help:"If set, this role will be mentioned in all Discord notifications. Any users with this role and access to the channel will be pinged. To find the role ID, enable Developer mode (see above)), then Server Settings, Roles, right click on the role and Copy ID."` + DiscordRoleCommand string `ini:"-" help:"If the Discord Role ID is set, you can optionally specify a command string here, like \"notify\" (no ! prefix), which if run as a ! command by a user (on a line by itself) in Discord will cause this server to attempt to add the configured role to the user. If you run multiple servers with Discord enabled, only set this on one of them. In order for this to work your bot must have the \"Manage Roles\" permission."` + + NotificationReminderTimer int `ini:"-" show:"-" min:"0" max:"65535" help:"This setting has been deprecated and will be removed in the next release. Use Notification Reminder Timers instead."` + NotificationReminderTimers string `ini:"-" help:"If Discord is enabled, a reminder will be sent this many minutes prior to race start. If 0 or empty, only race start messages will be sent. You may schedule multiple reminders by using a comma separated list like 120,15."` + ShowPasswordInNotifications int `ini:"-" input:"checkbox" help:"Show the server password in race start notifications."` + NotifyWhenScheduled int `ini:"-" input:"checkbox" help:"Send a notification when a race is scheduled (or cancelled)."` // Messages ContentManagerWelcomeMessage string `ini:"-" show:"-"` diff --git a/discord.go b/discord.go index e26801d96..a9512abeb 100644 --- a/discord.go +++ b/discord.go @@ -2,11 +2,14 @@ package servermanager import ( "errors" + "fmt" "net/url" + "strings" "time" "github.com/Clinet/discordgo-embed" "github.com/bwmarrin/discordgo" + "github.com/google/uuid" "github.com/sirupsen/logrus" ) @@ -96,33 +99,203 @@ func (dm *DiscordManager) SaveServerOptions(oldServerOpts *GlobalServerConfig, n return nil } -func (dm *DiscordManager) CommandHandler(s *discordgo.Session, m *discordgo.MessageCreate) { - if m.Author.ID == s.State.User.ID { - return +// CommandSessions outputs a full list of all scheduled sessions (P, Q & R), using buildCalendar as a base +func (dm *DiscordManager) CommandSessions() (string, error) { + serverOpts, err := dm.store.LoadServerOptions() + + if err != nil { + return "A server error occurred, please try again later", err + } + + start := time.Now() + end := start.AddDate(0, 0, 7) + + calendar, err := dm.scheduledRacesManager.buildCalendar(start, end) + + if err != nil { + return "A server error occurred, please try again later", err + } + + msg := fmt.Sprintf("Upcoming sessions on server %s\n", serverOpts.Name) + + for _, event := range calendar { + msg += event.Start.Format("Mon, 02 Jan 2006 15:04:05 MST") + "\n" + msg += event.Title + "\n" + msg += event.Description + "\n\n" + } + + return msg, nil +} + +// CommandSchedule outputs an abbreviated list of all scheduled events +func (dm *DiscordManager) CommandSchedule() (string, error) { + serverOpts, err := dm.store.LoadServerOptions() + + if err != nil { + return "A server error occurred, please try again later", err + } + + start := time.Now() + end := start.AddDate(0, 0, 7) + scheduled, err := dm.scheduledRacesManager.getScheduledRaces() + + if err != nil { + return "A server error occurred, please try again later", err } - if m.Content == "!schedule" { - start := time.Now() - end := start.AddDate(0, 0, 7) + var recurring []ScheduledEvent + + for _, scheduledEvent := range scheduled { + if scheduledEvent.HasRecurrenceRule() { + customRace, ok := scheduledEvent.(*CustomRace) - calendar, err := dm.scheduledRacesManager.buildCalendar(start, end) + if !ok { + continue + } + + rule, err := customRace.GetRecurrenceRule() + + if err != nil { + continue + } + + for _, startTime := range rule.Between(start, end, true) { + newEvent := *customRace + newEvent.Scheduled = startTime + newEvent.UUID = uuid.New() + + if customRace.GetScheduledTime() == newEvent.GetScheduledTime() { + continue + } + + recurring = append(recurring, &newEvent) + } + } + } + + scheduled = append(scheduled, recurring...) + + msg := fmt.Sprintf("\nUpcoming events on server %s\n\n", serverOpts.Name) + + for _, scheduledEvent := range scheduled { + raceSetup := scheduledEvent.GetRaceSetup() + cars := carList(scheduledEvent.GetRaceSetup().Cars) + msg += fmt.Sprintf("Date: %s\n", scheduledEvent.GetScheduledTime().Format("Mon, 02 Jan 2006 15:04:05 MST")) + msg += fmt.Sprintf("Track: %s\n", trackSummary(raceSetup.Track, raceSetup.TrackLayout)) + msg += fmt.Sprintf("Cars: %s\n", cars) + msg += "\n\n" + } + + return msg, nil +} + +// CommandNotify attempts to add a role ID (if configured) to the user issuing the !notify command +// The role will be added as a mention on all Discord notifications +func (dm *DiscordManager) CommandNotify(s *discordgo.Session, m *discordgo.MessageCreate) (string, error) { + serverOpts, err := dm.store.LoadServerOptions() + + if err != nil { + logrus.WithError(err).Infof("couldn't get server options") + return "A server error occurred, try again later", err + } + + if serverOpts.DiscordRoleID == "" || serverOpts.DiscordRoleCommand == "" { + return "", nil + } + + // get the member + member, err := s.State.Member(m.GuildID, m.Author.ID) + + if err != nil { + // if it's not in the state, punt to asking the server + if err == discordgo.ErrStateNotFound { + member, err = s.GuildMember(m.GuildID, m.Author.ID) + } if err != nil { - return + return "You don't seem to exist, so I can't assign you that role. Try again later.", err } + } - msg := "" + // get the role name from ID, for use in user feedback + roleName := "notification" + roles, err := s.GuildRoles(m.GuildID) - for _, event := range calendar { - msg += event.Start.Format("Mon, 02 Jan 2006 15:04:05 MST") + "\n" - msg += event.Title + "\n" - msg += event.Description + "\n\n" + if err != nil { + // meh, just log it and carry on + logrus.WithError(err).Infof("failed to get Discord roles (make sure you have set the bot permissions to see roles)") + } else { + for _, role := range roles { + if strings.TrimSpace(role.ID) == strings.TrimSpace(serverOpts.DiscordRoleID) { + roleName = role.Name + break + } } + } + + for _, roleID := range member.Roles { + if roleID == serverOpts.DiscordRoleID { + // they have the role, so remove it + err = s.GuildMemberRoleRemove(m.GuildID, m.Author.ID, serverOpts.DiscordRoleID) + + if err != nil { + // meh, log the error here, and just return some feedback to the user + logrus.WithError(err).Infof("failed to remove Discord role (make sure you have set the bot permissions to manage roles)") + return fmt.Sprintf("You already have the %s role, and an error occurred trying to remove it. Try again later.", roleName), nil + } + + return fmt.Sprintf("You already had the %s role, it has now been removed. Type the command again to add it back.", roleName), nil + } + } + + // they didn't have the role, so add it + err = s.GuildMemberRoleAdd(m.GuildID, m.Author.ID, serverOpts.DiscordRoleID) + + if err != nil { + // meh, log the error here and return feedback to the user + logrus.WithError(err).Infof("failed to set Discord role (make sure you have set the bot permissions to manage roles)") + return fmt.Sprintf("A server error occurred trying to assign you the %s role, try again later", roleName), nil + } + + // w00t! + return fmt.Sprintf("The %s role has been assigned, you will now get pinged with notifications. Type the command again to remove it.", roleName), nil +} + +func (dm *DiscordManager) CommandHandler(s *discordgo.Session, m *discordgo.MessageCreate) { + serverOpts, err := dm.store.LoadServerOptions() + + if err != nil { + logrus.WithError(err).Errorf("couldn't load server opts") + return + } + + if m.Author.ID == s.State.User.ID { + return + } + msg := "" + + switch m.Content { + case "!schedule": + msg, err = dm.CommandSchedule() + case "!sessions": + msg, err = dm.CommandSessions() + case "!" + serverOpts.DiscordRoleCommand: + msg, err = dm.CommandNotify(s, m) + default: + return + } + + // if error, log it, but continue with message sending, handler may have put user feedback in msg + if err != nil { + logrus.WithError(err).Errorf("Error during handling of Discord command") + } + + if msg != "" { _, err = s.ChannelMessageSend(m.ChannelID, msg) if err != nil { - logrus.WithError(err).Errorf("couldn't open discord session") + logrus.WithError(err).Errorf("couldn't send Discord msg") } } } @@ -149,7 +322,17 @@ func (dm *DiscordManager) SendMessage(msg string) error { // could check DiscordChannelID in new, but plan is to allow per-championship channels, so will need to pass // it in as an arg and check it here anyway if opts.DiscordChannelID != "" { - _, err = dm.discord.ChannelMessageSend(opts.DiscordChannelID, msg) + if opts.DiscordRoleID != "" { + mention := fmt.Sprintf("Attention <@&%s>\n", opts.DiscordRoleID) + messageSend := &discordgo.MessageSend{ + Content: mention, + Embed: embed.NewGenericEmbed(msg, ""), + } + _, err = dm.discord.ChannelMessageSendComplex(opts.DiscordChannelID, messageSend) + } else { + + _, err = dm.discord.ChannelMessageSendEmbed(opts.DiscordChannelID, embed.NewGenericEmbed(msg, "")) + } if err != nil { logrus.WithError(err).Errorf("couldn't send discord message") @@ -166,7 +349,7 @@ func (dm *DiscordManager) SendMessage(msg string) error { } // SendMessage sends a message to the configured channel and logs any errors -func (dm *DiscordManager) SendEmbed(msg string, linkText string, link *url.URL) error { +func (dm *DiscordManager) SendMessageWithLink(msg string, linkText string, link *url.URL) error { if !dm.enabled { return nil } @@ -178,11 +361,22 @@ func (dm *DiscordManager) SendEmbed(msg string, linkText string, link *url.URL) return err } + linkMsg := "[" + linkText + "](" + link.String() + ")" + // could check DiscordChannelID in new, but plan is to allow per-championship channels, so will need to pass // it in as an arg and check it here anyway if opts.DiscordChannelID != "" { - linkMsg := "[" + linkText + "](" + link.String() + ")" - _, err = dm.discord.ChannelMessageSendEmbed(opts.DiscordChannelID, embed.NewGenericEmbed(msg, "%s", linkMsg)) + if opts.DiscordRoleID != "" { + mention := fmt.Sprintf("Attention <@&%s>\n", opts.DiscordRoleID) + messageSend := &discordgo.MessageSend{ + Content: mention, + Embed: embed.NewGenericEmbed(msg, "%s", linkMsg), + } + _, err = dm.discord.ChannelMessageSendComplex(opts.DiscordChannelID, messageSend) + } else { + + _, err = dm.discord.ChannelMessageSendEmbed(opts.DiscordChannelID, embed.NewGenericEmbed(msg, "%s", linkMsg)) + } if err != nil { logrus.WithError(err).Errorf("couldn't send discord message") diff --git a/migrations.go b/migrations.go index 18c778066..b34ef3148 100644 --- a/migrations.go +++ b/migrations.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "sort" + "strconv" "github.com/cj123/assetto-server-manager/fixtures/race-weekend-examples" @@ -70,6 +71,7 @@ var ( addServerNameTemplate, addAvailableCarsToChampionshipClass, addTyresForP13c, + changeNotificationTimer, } ) @@ -448,6 +450,24 @@ func addServerNameTemplate(s Store) error { return s.UpsertServerOptions(opts) } +func changeNotificationTimer(s Store) error { + logrus.Infof("Running migration: Change Notification Timer") + + opts, err := s.LoadServerOptions() + + if err != nil { + return err + } + + opts.NotificationReminderTimers = strconv.Itoa(opts.NotificationReminderTimer) + + if opts.NotificationReminderTimers == "0" { + opts.NotificationReminderTimers = "" + } + + return s.UpsertServerOptions(opts) +} + func addAvailableCarsToChampionshipClass(s Store) error { logrus.Infof("Running migration: Add Available Cars to Championship Class") diff --git a/notification_manager.go b/notification_manager.go index c712f678f..287b43e58 100644 --- a/notification_manager.go +++ b/notification_manager.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "os" + "strconv" "strings" "time" @@ -11,13 +12,16 @@ import ( ) type NotificationDispatcher interface { + HasNotificationReminders() bool + GetNotificationReminders() []int SendMessage(msg string) error SendMessageWithLink(msg string, linkText string, link *url.URL) error SendRaceStartMessage(config ServerConfig, event RaceEvent) error SendRaceScheduledMessage(event *CustomRace, date time.Time) error - SendRaceReminderMessage(event *CustomRace) error - SendChampionshipReminderMessage(championship *Championship, event *ChampionshipEvent) error - SendRaceWeekendReminderMessage(raceWeekend *RaceWeekend, session *RaceWeekendSession) error + SendRaceCancelledMessage(event *CustomRace, date time.Time) error + SendRaceReminderMessage(event *CustomRace, timer int) error + SendChampionshipReminderMessage(championship *Championship, event *ChampionshipEvent, timer int) error + SendRaceWeekendReminderMessage(raceWeekend *RaceWeekend, session *RaceWeekendSession, timer int) error SaveServerOptions(oldServerOpts *GlobalServerConfig, newServerOpts *GlobalServerConfig) error } @@ -48,6 +52,50 @@ func (nm *NotificationManager) Stop() error { return nm.discordManager.Stop() } +// HasNotificationReminders just tells us if we need to do any reminder scheduling +func (nm *NotificationManager) HasNotificationReminders() bool { + reminders := nm.GetNotificationReminders() + + return len(reminders) > 0 +} + +// GetNotificationReminders returns an array of int timers +// Doesn't return errors, just omits anything it doesn't like and logs errors +func (nm *NotificationManager) GetNotificationReminders() []int { + var reminders []int + + serverOpts, err := nm.store.LoadServerOptions() + + if err != nil { + logrus.WithError(err).Errorf("couldn't load server options") + return reminders + } + + timers := strings.Split(serverOpts.NotificationReminderTimers, ",") + + for _, a := range timers { + if strings.TrimSpace(a) == "" { + logrus.WithError(err).Infof("couldn't convert notification time to int") + continue + } + + i, err := strconv.Atoi(strings.TrimSpace(a)) + + if err != nil { + logrus.WithError(err).Errorf("couldn't convert notification time to int") + continue + } + + if i == 0 { + continue + } + + reminders = append(reminders, i) + } + + return reminders +} + // SendMessage sends a message (surprise surprise) func (nm *NotificationManager) SendMessage(msg string) error { var err error @@ -66,7 +114,7 @@ func (nm *NotificationManager) SendMessageWithLink(msg string, linkText string, // Call all message senders here ... atm just discord. The manager will know if it's enabled or not, so just call it if !nm.testing { - err = nm.discordManager.SendEmbed(msg, linkText, link) + err = nm.discordManager.SendMessageWithLink(msg, linkText, link) } return err @@ -74,13 +122,6 @@ func (nm *NotificationManager) SendMessageWithLink(msg string, linkText string, // SendRaceStartMessage sends a message as a race session is started func (nm *NotificationManager) SendRaceStartMessage(config ServerConfig, event RaceEvent) error { - trackInfo, err := GetTrackInfo(config.CurrentRaceConfig.Track, config.CurrentRaceConfig.TrackLayout) - - if err != nil { - logrus.WithError(err).Warnf("Could not load track details, skipping notification: %s, %s", config.CurrentRaceConfig.Track, config.CurrentRaceConfig.TrackLayout) - return err - } - serverOpts, err := nm.store.LoadServerOptions() if err != nil { @@ -90,13 +131,16 @@ func (nm *NotificationManager) SendRaceStartMessage(config ServerConfig, event R msg := "" eventName := event.EventName() + trackInfo := trackSummary(config.CurrentRaceConfig.Track, config.CurrentRaceConfig.TrackLayout) if eventName != "" { - msg = fmt.Sprintf("%s race at %s is starting now", eventName, trackInfo.Name) + msg = fmt.Sprintf("%s race at %s is starting now", eventName, trackInfo) } else { - msg = fmt.Sprintf("Race at %s is starting now", trackInfo.Name) + msg = fmt.Sprintf("Race at %s is starting now", trackInfo) } + msg += fmt.Sprintf("\nServer: %s", serverOpts.Name) + if serverOpts.ShowPasswordInNotifications == 1 { passwordString := "\nNo password" @@ -136,6 +180,17 @@ func (nm *NotificationManager) SendRaceStartMessage(config ServerConfig, event R // SendRaceScheduledMessage sends a notification when a race is scheduled func (nm *NotificationManager) SendRaceScheduledMessage(event *CustomRace, date time.Time) error { + serverOpts, err := nm.store.LoadServerOptions() + + if err != nil { + logrus.WithError(err).Errorf("couldn't load server options, skipping notification") + return err + } + + if serverOpts.NotifyWhenScheduled != 1 { + return nil + } + dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 MST") var aCarNames []string @@ -153,29 +208,24 @@ func (nm *NotificationManager) SendRaceScheduledMessage(event *CustomRace, date carNames := strings.Join(aCarNames, ", ") - trackInfo, err := GetTrackInfo(event.RaceConfig.Track, event.RaceConfig.TrackLayout) - - if err != nil { - logrus.WithError(err).Warnf("Could not load track details, skipping notification: %s, %s", event.RaceConfig.Track, event.RaceConfig.TrackLayout) - return err - } - msg := "A new event has been scheduled\n" + msg += fmt.Sprintf("Server: %s\n", serverOpts.Name) eventName := event.EventName() + trackInfo := trackSummary(event.RaceConfig.Track, event.RaceConfig.TrackLayout) if eventName != "" { msg += fmt.Sprintf("Event name: %s\n", eventName) } msg += fmt.Sprintf("Date: %s\n", dateStr) - msg += fmt.Sprintf("Track: %s\n", trackInfo.Name) + msg += fmt.Sprintf("Track: %s\n", trackInfo) msg += fmt.Sprintf("Car(s): %s\n", carNames) return nm.SendMessage(msg) } -// SendRaceReminderMessage sends a reminder a configurable number of minutes prior to a race starting -func (nm *NotificationManager) SendRaceReminderMessage(event *CustomRace) error { +// SendRaceCancelledMessage sends a notification when a race is cancelled +func (nm *NotificationManager) SendRaceCancelledMessage(event *CustomRace, date time.Time) error { serverOpts, err := nm.store.LoadServerOptions() if err != nil { @@ -183,53 +233,49 @@ func (nm *NotificationManager) SendRaceReminderMessage(event *CustomRace) error return err } - msg := "" - eventName := event.EventName() - trackInfo, err := GetTrackInfo(event.RaceConfig.Track, event.RaceConfig.TrackLayout) - - if err != nil { - logrus.WithError(err).Warnf("Could not load track details, skipping notification: %s, %s", event.RaceConfig.Track, event.RaceConfig.TrackLayout) - return err + if serverOpts.NotifyWhenScheduled != 1 { + return nil } + dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 MST") + + msg := "The following scheduled race has been cancelled\n" + msg += fmt.Sprintf("Server: %s\n", serverOpts.Name) + eventName := event.EventName() + trackInfo := trackSummary(event.RaceConfig.Track, event.RaceConfig.TrackLayout) + if eventName != "" { - msg = fmt.Sprintf("%s race at %s starts in %d minutes", eventName, trackInfo.Name, serverOpts.NotificationReminderTimer) - } else { - msg = fmt.Sprintf("Race at %s starts in %d minutes", trackInfo.Name, serverOpts.NotificationReminderTimer) + msg += fmt.Sprintf("Event name: %s\n", eventName) } + msg += fmt.Sprintf("Date: %s\n", dateStr) + msg += fmt.Sprintf("Track: %s\n", trackInfo) + return nm.SendMessage(msg) } -// SendChampionshipReminderMessage sends a reminder a configurable number of minutes prior to a championship race starting -func (nm *NotificationManager) SendChampionshipReminderMessage(championship *Championship, event *ChampionshipEvent) error { - trackInfo, err := GetTrackInfo(event.RaceSetup.Track, event.RaceSetup.TrackLayout) +// SendRaceReminderMessage sends a reminder a configurable number of minutes prior to a race starting +func (nm *NotificationManager) SendRaceReminderMessage(event *CustomRace, timer int) error { + msg := "" + trackInfo := trackSummary(event.RaceConfig.Track, event.RaceConfig.TrackLayout) + eventName := event.EventName() - if err != nil { - logrus.WithError(err).Warnf("Could not load track details, skipping notification: %s, %s", event.RaceSetup.Track, event.RaceSetup.TrackLayout) - return err + if eventName != "" { + msg = fmt.Sprintf("%s race at %s starts in %d minutes", eventName, trackInfo, timer) + } else { + msg = fmt.Sprintf("Race at %s starts in %d minutes", trackInfo, timer) } - serverOpts, err := nm.store.LoadServerOptions() - - if err != nil { - logrus.WithError(err).Errorf("couldn't load server options, skipping notification") - return err - } + return nm.SendMessage(msg) +} - return nm.SendMessage(fmt.Sprintf("%s race at %s starts in %d minutes", championship.Name, trackInfo.Name, serverOpts.NotificationReminderTimer)) +// SendChampionshipReminderMessage sends a reminder a configurable number of minutes prior to a championship race starting +func (nm *NotificationManager) SendChampionshipReminderMessage(championship *Championship, event *ChampionshipEvent, timer int) error { + return nm.SendMessage(fmt.Sprintf("%s race at %s starts in %d minutes", championship.Name, trackSummary(event.RaceSetup.Track, event.RaceSetup.TrackLayout), timer)) } // SendRaceWeekendReminderMessage sends a reminder a configurable number of minutes prior to a RaceWeekendSession starting -func (nm *NotificationManager) SendRaceWeekendReminderMessage(raceWeekend *RaceWeekend, session *RaceWeekendSession) error { +func (nm *NotificationManager) SendRaceWeekendReminderMessage(raceWeekend *RaceWeekend, session *RaceWeekendSession, timer int) error { trackInfo := trackSummary(session.RaceConfig.Track, session.RaceConfig.TrackLayout) - - serverOpts, err := nm.store.LoadServerOptions() - - if err != nil { - logrus.WithError(err).Errorf("couldn't load server options, skipping notification") - return err - } - - return nm.SendMessage(fmt.Sprintf("%s at %s (%s Race Weekend) starts in %d minutes", session.Name(), raceWeekend.Name, trackInfo, serverOpts.NotificationReminderTimer)) + return nm.SendMessage(fmt.Sprintf("%s at %s (%s Race Weekend) starts in %d minutes", session.Name(), raceWeekend.Name, trackInfo, timer)) } diff --git a/race_manager.go b/race_manager.go index 6245d89bd..e436cc080 100644 --- a/race_manager.go +++ b/race_manager.go @@ -1012,12 +1012,7 @@ func (rm *RaceManager) ScheduleRace(uuid string, date time.Time, action string, return err } - serverOpts, err := rm.store.LoadServerOptions() - - if err != nil { - return err - } - + originalDate := race.Scheduled race.Scheduled = date // if there is an existing schedule timer for this event stop it @@ -1060,17 +1055,21 @@ func (rm *RaceManager) ScheduleRace(uuid string, date time.Time, action string, } }) - if serverOpts.NotificationReminderTimer > 0 { + if rm.notificationManager.HasNotificationReminders() { _ = rm.notificationManager.SendRaceScheduledMessage(race, date) - duration = time.Until(date.Add(time.Duration(0-serverOpts.NotificationReminderTimer) * time.Minute)) + for _, timer := range rm.notificationManager.GetNotificationReminders() { + duration = time.Until(date.Add(time.Duration(0-timer) * time.Minute)) + thisTimer := timer - rm.customRaceReminderTimers[race.UUID.String()] = time.AfterFunc(duration, func() { - _ = rm.notificationManager.SendRaceReminderMessage(race) - }) + rm.customRaceReminderTimers[race.UUID.String()] = time.AfterFunc(duration, func() { + _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) + }) + } } } else { + _ = rm.notificationManager.SendRaceCancelledMessage(race, originalDate) race.ClearRecurrenceRule() } @@ -1333,12 +1332,6 @@ func (rm *RaceManager) InitScheduledRaces() error { return err } - serverOpts, err := rm.store.LoadServerOptions() - - if err != nil { - return err - } - for _, race := range races { race := race @@ -1354,14 +1347,17 @@ func (rm *RaceManager) InitScheduledRaces() error { } }) - if serverOpts.NotificationReminderTimer > 0 { - if race.Scheduled.Add(time.Duration(0-serverOpts.NotificationReminderTimer) * time.Minute).After(time.Now()) { - // add reminder - duration = time.Until(race.Scheduled.Add(time.Duration(0-serverOpts.NotificationReminderTimer) * time.Minute)) + if rm.notificationManager.HasNotificationReminders() { + for _, timer := range rm.notificationManager.GetNotificationReminders() { + if race.Scheduled.Add(time.Duration(0-timer) * time.Minute).After(time.Now()) { + // add reminder + duration = time.Until(race.Scheduled.Add(time.Duration(0-timer) * time.Minute)) + thisTimer := timer - rm.customRaceReminderTimers[race.UUID.String()] = time.AfterFunc(duration, func() { - _ = rm.notificationManager.SendRaceReminderMessage(race) - }) + rm.customRaceReminderTimers[race.UUID.String()] = time.AfterFunc(duration, func() { + _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) + }) + } } } } else { @@ -1401,7 +1397,7 @@ func (rm *RaceManager) InitScheduledRaces() error { // reschedule notifications if notification timer changed func (rm *RaceManager) RescheduleNotifications(oldServerOpts *GlobalServerConfig, newServerOpts *GlobalServerConfig) error { - if newServerOpts.NotificationReminderTimer == oldServerOpts.NotificationReminderTimer { + if newServerOpts.NotificationReminderTimers == oldServerOpts.NotificationReminderTimers { return nil } @@ -1413,24 +1409,29 @@ func (rm *RaceManager) RescheduleNotifications(oldServerOpts *GlobalServerConfig // rebuild the timers rm.customRaceReminderTimers = make(map[string]*time.Timer) - if newServerOpts.NotificationReminderTimer > 0 { + if rm.notificationManager.HasNotificationReminders() { races, err := rm.store.ListCustomRaces() if err != nil { return err } - for _, race := range races { - race := race + for _, timer := range rm.notificationManager.GetNotificationReminders() { + for _, race := range races { + race := race - if race.Scheduled.After(time.Now()) { - if race.Scheduled.Add(time.Duration(0-newServerOpts.NotificationReminderTimer) * time.Minute).After(time.Now()) { - // add reminder - duration := time.Until(race.Scheduled.Add(time.Duration(0-newServerOpts.NotificationReminderTimer) * time.Minute)) + if race.Scheduled.After(time.Now()) { + duration := time.Until(race.Scheduled) - rm.customRaceReminderTimers[race.UUID.String()] = time.AfterFunc(duration, func() { - _ = rm.notificationManager.SendRaceReminderMessage(race) - }) + if race.Scheduled.Add(time.Duration(0-timer) * time.Minute).After(time.Now()) { + // add reminder + duration = time.Until(race.Scheduled.Add(time.Duration(0-timer) * time.Minute)) + thisTimer := timer + + rm.customRaceReminderTimers[race.UUID.String()] = time.AfterFunc(duration, func() { + _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) + }) + } } } } diff --git a/race_weekend_manager.go b/race_weekend_manager.go index 1af2a2fd9..228a394de 100644 --- a/race_weekend_manager.go +++ b/race_weekend_manager.go @@ -1072,26 +1072,23 @@ func (rwm *RaceWeekendManager) setupScheduledSessionTimer(raceWeekend *RaceWeeke } }) - serverOpts, err := rwm.store.LoadServerOptions() + if rwm.notificationManager.HasNotificationReminders() { + for _, timer := range rwm.notificationManager.GetNotificationReminders() { + reminderTime := session.ScheduledTime.Add(time.Duration(-timer) * time.Minute) - if err != nil { - return err - } - - if serverOpts.NotificationReminderTimer > 0 { - reminderTime := session.ScheduledTime.Add(time.Duration(-serverOpts.NotificationReminderTimer) * time.Minute) - - if reminderTime.After(time.Now()) { - // add reminder - duration := time.Until(reminderTime) + if reminderTime.After(time.Now()) { + // add reminder + duration := time.Until(reminderTime) + thisTimer := timer - rwm.scheduledSessionReminderTimers[session.ID.String()] = time.AfterFunc(duration, func() { - err := rwm.notificationManager.SendRaceWeekendReminderMessage(raceWeekend, session) + rwm.scheduledSessionReminderTimers[session.ID.String()] = time.AfterFunc(duration, func() { + err := rwm.notificationManager.SendRaceWeekendReminderMessage(raceWeekend, session, thisTimer) - if err != nil { - logrus.WithError(err).Errorf("Could not send race weekend reminder message") - } - }) + if err != nil { + logrus.WithError(err).Errorf("Could not send race weekend reminder message") + } + }) + } } }