From f145ae987f7fbc1d0844a6be5e8e21a2c1adf0f8 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Mon, 30 Sep 2019 02:24:27 -0500 Subject: [PATCH 01/27] Add server name to msgs, split command into !sessions and !schedule --- discord.go | 118 ++++++++++++++++++++++++++++++++-------- notification_manager.go | 5 ++ 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/discord.go b/discord.go index c0d24ff64..a51883400 100644 --- a/discord.go +++ b/discord.go @@ -2,12 +2,13 @@ package servermanager import ( "errors" - "net/url" - "time" - + "fmt" "github.com/Clinet/discordgo-embed" "github.com/bwmarrin/discordgo" + "github.com/google/uuid" "github.com/sirupsen/logrus" + "net/url" + "time" ) type DiscordManager struct { @@ -96,35 +97,108 @@ 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 +func (dm *DiscordManager) CommandSessions() (string, error) { + serverOpts, err := dm.store.LoadServerOptions() + + start := time.Now() + end := start.AddDate(0, 0, 7) + + calendar, err := dm.scheduledRacesManager.buildCalendar(start, end) + + if err != nil { + return "", err } - if m.Content == "!schedule" { - start := time.Now() - end := start.AddDate(0, 0, 7) + var msg = fmt.Sprintf("Upcoming sessions on server %s\n", serverOpts.Name) - calendar, err := dm.scheduledRacesManager.buildCalendar(start, end) + 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 { - return - } + return msg, nil +} - var msg = "" +func (dm *DiscordManager) CommandSchedule() (string, error) { + serverOpts, err := dm.store.LoadServerOptions() + start := time.Now() + end := start.AddDate(0, 0, 7) + scheduled, err := dm.scheduledRacesManager.getScheduledRaces() - 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 { + return "", err + } - _, err = s.ChannelMessageSend(m.ChannelID, msg) + var recurring []ScheduledEvent - if err != nil { - logrus.Errorf("couldn't open discord session, err: %s", err) + for _, scheduledEvent := range scheduled { + if scheduledEvent.HasRecurrenceRule() { + customRace, ok := scheduledEvent.(*CustomRace) + + 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...) + + var msg = fmt.Sprintf("Upcoming events on server %s\n\n", serverOpts.Name) + + for _, scheduledEvent := range scheduled { + raceSetup := scheduledEvent.GetRaceSetup() + trackInfo := trackInfo(raceSetup.Track, raceSetup.TrackLayout) + cars := carList(scheduledEvent.GetRaceSetup().Cars) + msg += fmt.Sprintf("When: %s\n", scheduledEvent.GetScheduledTime().Format("Mon, 02 Jan 2006 15:04:05 MST")) + msg += fmt.Sprintf("Where: %s\n", trackInfo.Name) + msg += fmt.Sprintf("What: %s\n", cars) + msg += "\n\n" + } + + return msg, nil +} + +func (dm *DiscordManager) CommandHandler(s *discordgo.Session, m *discordgo.MessageCreate) { + if m.Author.ID == s.State.User.ID { + return + } + + var msg = "" + var err error + + switch m.Content { + case "!schedule": + msg, err = dm.CommandSchedule() + case "!sessions": + msg, err = dm.CommandSessions() + default: + return + } + + _, err = s.ChannelMessageSend(m.ChannelID, msg) + + if err != nil { + logrus.Errorf("couldn't open discord session, err: %s", err) + } } func (dm *DiscordManager) Stop() error { diff --git a/notification_manager.go b/notification_manager.go index 6522c9e39..27b50ba00 100644 --- a/notification_manager.go +++ b/notification_manager.go @@ -86,6 +86,8 @@ func (nm *NotificationManager) SendRaceStartMessage(config ServerConfig, event R msg = fmt.Sprintf("Race at %s is starting now", trackInfo.Name) } + msg += fmt.Sprintf("Server: %s\n", serverOpts.Name) + if serverOpts.ShowPasswordInNotifications == 1 { passwordString := "\nNo password" @@ -125,6 +127,8 @@ 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() + dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 MST") var aCarNames []string @@ -150,6 +154,7 @@ func (nm *NotificationManager) SendRaceScheduledMessage(event *CustomRace, date } var msg = "A new event has been scheduled\n" + msg += fmt.Sprintf("Server: %s\n", serverOpts.Name) eventName := event.EventName() if eventName != "" { From cd449ee857544e0eb97488609eaef5e8abb0a868 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Mon, 30 Sep 2019 17:44:53 -0500 Subject: [PATCH 02/27] Comments, tweaking msg formatting --- discord.go | 10 ++++++---- notification_manager.go | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/discord.go b/discord.go index a51883400..fdcf0a663 100644 --- a/discord.go +++ b/discord.go @@ -97,6 +97,7 @@ func (dm *DiscordManager) SaveServerOptions(oldServerOpts *GlobalServerConfig, n return nil } +// 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() @@ -120,6 +121,7 @@ func (dm *DiscordManager) CommandSessions() (string, error) { return msg, nil } +// CommandSchedule outputs an abbreviated list of all scheduled events func (dm *DiscordManager) CommandSchedule() (string, error) { serverOpts, err := dm.store.LoadServerOptions() start := time.Now() @@ -162,15 +164,15 @@ func (dm *DiscordManager) CommandSchedule() (string, error) { scheduled = append(scheduled, recurring...) - var msg = fmt.Sprintf("Upcoming events on server %s\n\n", serverOpts.Name) + var msg = fmt.Sprintf("\nUpcoming events on server %s\n\n", serverOpts.Name) for _, scheduledEvent := range scheduled { raceSetup := scheduledEvent.GetRaceSetup() trackInfo := trackInfo(raceSetup.Track, raceSetup.TrackLayout) cars := carList(scheduledEvent.GetRaceSetup().Cars) - msg += fmt.Sprintf("When: %s\n", scheduledEvent.GetScheduledTime().Format("Mon, 02 Jan 2006 15:04:05 MST")) - msg += fmt.Sprintf("Where: %s\n", trackInfo.Name) - msg += fmt.Sprintf("What: %s\n", cars) + msg += fmt.Sprintf("Date: %s\n", scheduledEvent.GetScheduledTime().Format("Mon, 02 Jan 2006 15:04:05 MST")) + msg += fmt.Sprintf("Track: %s\n", trackInfo.Name) + msg += fmt.Sprintf("Cars: %s\n", cars) msg += "\n\n" } diff --git a/notification_manager.go b/notification_manager.go index 27b50ba00..256267a64 100644 --- a/notification_manager.go +++ b/notification_manager.go @@ -86,7 +86,7 @@ func (nm *NotificationManager) SendRaceStartMessage(config ServerConfig, event R msg = fmt.Sprintf("Race at %s is starting now", trackInfo.Name) } - msg += fmt.Sprintf("Server: %s\n", serverOpts.Name) + msg += fmt.Sprintf("\nServer: %s", serverOpts.Name) if serverOpts.ShowPasswordInNotifications == 1 { passwordString := "\nNo password" From 1a8f7efda7a9d851527d262df4473d27a7d1772a Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Mon, 7 Oct 2019 03:08:52 -0500 Subject: [PATCH 03/27] Adding DiscordRoleID for tagging on notifications --- config_ini.go | 12 ++++--- discord.go | 79 ++++++++++++++++++++++++++++++++++++++--- notification_manager.go | 2 +- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/config_ini.go b/config_ini.go index bfef0aab5..5653b9adb 100644 --- a/config_ini.go +++ b/config_ini.go @@ -187,11 +187,13 @@ type GlobalServerConfig struct { FallBackResultsSorting int `ini:"-" input:"checkbox" help:"When on results will use a fallback method of sorting. Only enable this if you are experiencing results that are in the wrong order in the json file."` // 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"` + DiscordRoleID string `ini:"-" help:"If set, this role will be mentioned in all Discord notifications, and (if your bot has sufficient privileges) the !notify command will attempt to add this role to the user"` + + 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"` // Messages ContentManagerWelcomeMessage string `ini:"-" show:"-"` diff --git a/discord.go b/discord.go index fdcf0a663..968a89449 100644 --- a/discord.go +++ b/discord.go @@ -101,6 +101,10 @@ func (dm *DiscordManager) SaveServerOptions(oldServerOpts *GlobalServerConfig, n func (dm *DiscordManager) CommandSessions() (string, error) { serverOpts, err := dm.store.LoadServerOptions() + if err != nil { + return "", err + } + start := time.Now() end := start.AddDate(0, 0, 7) @@ -124,6 +128,11 @@ func (dm *DiscordManager) CommandSessions() (string, error) { // CommandSchedule outputs an abbreviated list of all scheduled events func (dm *DiscordManager) CommandSchedule() (string, error) { serverOpts, err := dm.store.LoadServerOptions() + + if err != nil { + return "", err + } + start := time.Now() end := start.AddDate(0, 0, 7) scheduled, err := dm.scheduledRacesManager.getScheduledRaces() @@ -179,6 +188,45 @@ func (dm *DiscordManager) CommandSchedule() (string, error) { return msg, nil } +// CommandSchedule outputs an abbreviated list of all scheduled events +func (dm *DiscordManager) CommandNotify(s *discordgo.Session, m *discordgo.MessageCreate) (string, error) { + serverOpts, err := dm.store.LoadServerOptions() + + if err != nil { + return "", err + } + + if serverOpts.DiscordRoleID == "" { + return "", nil + } + + member, err := s.State.Member(m.GuildID, m.Author.ID) + + if err != nil { + if err == discordgo.ErrStateNotFound { + member, err = s.GuildMember(m.GuildID, m.Author.ID) + } + + if err != nil { + return "", err + } + } + + for _, roleID := range member.Roles { + if roleID == serverOpts.DiscordRoleID { + return "You already have this role", nil + } + } + + err = s.GuildMemberRoleAdd(m.GuildID, m.Author.ID, serverOpts.DiscordRoleID) + + if err != nil { + return fmt.Sprintf("I'm sorry Dave, I can't do that (%s)", err.Error()), err + } + + return "Your role has been assigned", nil +} + func (dm *DiscordManager) CommandHandler(s *discordgo.Session, m *discordgo.MessageCreate) { if m.Author.ID == s.State.User.ID { return @@ -192,6 +240,8 @@ func (dm *DiscordManager) CommandHandler(s *discordgo.Session, m *discordgo.Mess msg, err = dm.CommandSchedule() case "!sessions": msg, err = dm.CommandSessions() + case "!spamificate": + msg, err = dm.CommandNotify(s, m) default: return } @@ -225,7 +275,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.Errorf("couldn't send discord message, err: %s", err) @@ -242,7 +302,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 } @@ -254,11 +314,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.Errorf("couldn't send discord message, err: %s", err) diff --git a/notification_manager.go b/notification_manager.go index f70d825d1..230713397 100644 --- a/notification_manager.go +++ b/notification_manager.go @@ -65,7 +65,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 From 232ae405cf1fbe58f3bae2be435cfd91a3866aa6 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Mon, 7 Oct 2019 03:14:04 -0500 Subject: [PATCH 04/27] Fixed comment --- discord.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discord.go b/discord.go index 968a89449..a9e335f97 100644 --- a/discord.go +++ b/discord.go @@ -188,7 +188,8 @@ func (dm *DiscordManager) CommandSchedule() (string, error) { return msg, nil } -// CommandSchedule outputs an abbreviated list of all scheduled events +// 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() @@ -208,7 +209,7 @@ func (dm *DiscordManager) CommandNotify(s *discordgo.Session, m *discordgo.Messa } if err != nil { - return "", err + return "You don't seem to exist, so I can't assign you that role", err } } From 7fcf9a1e3e061153bec207da5f6ed14bcb2fdddd Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Mon, 7 Oct 2019 04:42:06 -0500 Subject: [PATCH 05/27] Added configurable role command, and toggling on/off --- config_ini.go | 3 ++- discord.go | 50 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/config_ini.go b/config_ini.go index 5653b9adb..1fda6e843 100644 --- a/config_ini.go +++ b/config_ini.go @@ -190,7 +190,8 @@ type GlobalServerConfig struct { 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"` - DiscordRoleID string `ini:"-" help:"If set, this role will be mentioned in all Discord notifications, and (if your bot has sufficient privileges) the !notify command will attempt to add this role to the user"` + 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."` + DiscordRoleCommand string `ini:"-" help:"If the Discord Role ID is set, you can optionally specify a command string here, like \"notify\", which if run as a ! command by a user 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:"-" 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"` diff --git a/discord.go b/discord.go index a9e335f97..e7133cebd 100644 --- a/discord.go +++ b/discord.go @@ -31,7 +31,7 @@ func NewDiscordManager(store Store, scheduledRacesManager *ScheduledRacesManager opts, err := store.LoadServerOptions() if err != nil { - logrus.Errorf("couldn't load server options, err: %s", err) + logrus.WithError(err).Errorf("couldn't load server options") return discordManager, err } @@ -45,7 +45,7 @@ func NewDiscordManager(store Store, scheduledRacesManager *ScheduledRacesManager } if err != nil { - logrus.Errorf("couldn't open discord session, err: %s", err) + logrus.WithError(err).Errorf("couldn't open discord session") return discordManager, err } } else { @@ -78,7 +78,7 @@ func (dm *DiscordManager) SaveServerOptions(oldServerOpts *GlobalServerConfig, n } if err != nil { - logrus.Errorf("couldn't open discord session, err: %s", err) + logrus.WithError(err).Errorf("couldn't open discord session") return err } @@ -194,10 +194,10 @@ func (dm *DiscordManager) CommandNotify(s *discordgo.Session, m *discordgo.Messa serverOpts, err := dm.store.LoadServerOptions() if err != nil { - return "", err + return "A server error occurred, try again later", err } - if serverOpts.DiscordRoleID == "" { + if serverOpts.DiscordRoleID == "" || serverOpts.DiscordRoleCommand == "" { return "", nil } @@ -215,42 +215,58 @@ func (dm *DiscordManager) CommandNotify(s *discordgo.Session, m *discordgo.Messa for _, roleID := range member.Roles { if roleID == serverOpts.DiscordRoleID { - return "You already have this role", nil + err = s.GuildMemberRoleRemove(m.GuildID, m.Author.ID, serverOpts.DiscordRoleID) + + if err != nil { + return "You already have this role, and an error occurred trying to remove it", err + } + + return "You already had this role, it has now been removed. Type the command again to add it back.", nil } } err = s.GuildMemberRoleAdd(m.GuildID, m.Author.ID, serverOpts.DiscordRoleID) if err != nil { - return fmt.Sprintf("I'm sorry Dave, I can't do that (%s)", err.Error()), err + return fmt.Sprintf("A server error occurred, try again later"), err } - return "Your role has been assigned", nil + return "Your notification role has been assigned, you will now get pinged for notifications. Type the command again if you want to remove it.", 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 } var msg = "" - var err error switch m.Content { case "!schedule": msg, err = dm.CommandSchedule() case "!sessions": msg, err = dm.CommandSessions() - case "!spamificate": + case "!" + serverOpts.DiscordRoleCommand: msg, err = dm.CommandNotify(s, m) default: return } + if err != nil { + logrus.WithError(err).Errorf("Error during handling of Discord command") + } + _, err = s.ChannelMessageSend(m.ChannelID, msg) if err != nil { - logrus.Errorf("couldn't open discord session, err: %s", err) + logrus.WithError(err).Errorf("couldn't send Discord msg") } } @@ -269,7 +285,7 @@ func (dm *DiscordManager) SendMessage(msg string) error { opts, err := dm.store.LoadServerOptions() if err != nil { - logrus.Errorf("couldn't load server options, err: %s", err) + logrus.WithError(err).Errorf("couldn't load server options") return err } @@ -289,12 +305,12 @@ func (dm *DiscordManager) SendMessage(msg string) error { } if err != nil { - logrus.Errorf("couldn't send discord message, err: %s", err) + logrus.WithError(err).Errorf("couldn't send discord message") return err } } else { err = errors.New("no channel ID set in config") - logrus.Errorf("couldn't send discord message, err: %s", err) + logrus.WithError(err).Errorf("couldn't send discord message") return err } } @@ -311,7 +327,7 @@ func (dm *DiscordManager) SendMessageWithLink(msg string, linkText string, link opts, err := dm.store.LoadServerOptions() if err != nil { - logrus.Errorf("couldn't load server options, err: %s", err) + logrus.WithError(err).Errorf("couldn't load server options") return err } @@ -333,12 +349,12 @@ func (dm *DiscordManager) SendMessageWithLink(msg string, linkText string, link } if err != nil { - logrus.Errorf("couldn't send discord message, err: %s", err) + logrus.WithError(err).Errorf("couldn't send discord message") return err } } else { err = errors.New("no channel ID set in config") - logrus.Errorf("couldn't send discord message, err: %s", err) + logrus.WithError(err).Errorf("couldn't send discord message") return err } From 697c2227b0246fde4a91ebe471cc7bdd52d75653 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Mon, 7 Oct 2019 16:43:12 -0500 Subject: [PATCH 06/27] Improved comments and user feedback --- config_ini.go | 4 ++-- discord.go | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/config_ini.go b/config_ini.go index 1fda6e843..66488553f 100644 --- a/config_ini.go +++ b/config_ini.go @@ -189,8 +189,8 @@ type GlobalServerConfig struct { // 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"` - 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."` + 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\", which if run as a ! command by a user 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:"-" 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."` diff --git a/discord.go b/discord.go index e7133cebd..51e007854 100644 --- a/discord.go +++ b/discord.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" "net/url" + "strings" "time" ) @@ -201,37 +202,62 @@ func (dm *DiscordManager) CommandNotify(s *discordgo.Session, m *discordgo.Messa 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 "You don't seem to exist, so I can't assign you that role", err + return "You don't seem to exist, so I can't assign you that role. Try again later.", err + } + } + + // get the role name from ID, for use in user feedback + roleName := "notification" + roles, err := s.GuildRoles(m.GuildID) + + 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 { - return "You already have this role, and an error occurred trying to remove it", err + // 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 "You already had this role, it has now been removed. Type the command again to add it back.", 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 { - return fmt.Sprintf("A server error occurred, try again later"), err + // 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 } - return "Your notification role has been assigned, you will now get pinged for notifications. Type the command again if you want to remove it.", 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."), nil } func (dm *DiscordManager) CommandHandler(s *discordgo.Session, m *discordgo.MessageCreate) { From 1ccb8f8a5f9a97f43436eeaf59450ef8c1795ebb Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Mon, 7 Oct 2019 16:44:56 -0500 Subject: [PATCH 07/27] Improved tooltip --- config_ini.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_ini.go b/config_ini.go index 66488553f..a1e31da7d 100644 --- a/config_ini.go +++ b/config_ini.go @@ -191,7 +191,7 @@ type GlobalServerConfig struct { 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\", which if run as a ! command by a user 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."` + 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:"-" 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"` From 7f037487d4188d685b763c07d5a68b813f1bd9bd Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Wed, 9 Oct 2019 17:26:57 -0500 Subject: [PATCH 08/27] Migrating NotificationReminderTimer to string, with ability to have comma separated list of multiple reminder timers --- championship_manager.go | 40 +++++-------- championship_manager_test.go | 4 +- config_ini.go | 5 +- discord.go | 23 ++++---- form.go | 2 +- migrations.go | 20 +++++++ notification_manager.go | 110 +++++++++++++++++++---------------- race_manager.go | 66 ++++++++++----------- 8 files changed, 144 insertions(+), 126 deletions(-) diff --git a/championship_manager.go b/championship_manager.go index 5db8b4fdd..82e4936dd 100644 --- a/championship_manager.go +++ b/championship_manager.go @@ -584,12 +584,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 @@ -613,12 +607,14 @@ 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)) - 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, timer) + }) + } } } @@ -1357,12 +1353,6 @@ func (cm *ChampionshipManager) InitScheduledChampionships() error { return err } - serverOpts, err := cm.store.LoadServerOptions() - - if err != nil { - return err - } - for _, championship := range championships { championship := championship @@ -1381,14 +1371,16 @@ 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)) + 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)) - 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, timer) + }) + } } } diff --git a/championship_manager_test.go b/championship_manager_test.go index 11629f5d3..c55a24f46 100644 --- a/championship_manager_test.go +++ b/championship_manager_test.go @@ -128,11 +128,11 @@ func (d dummyNotificationManager) SendRaceScheduledMessage(event *CustomRace, da return nil } -func (d dummyNotificationManager) SendRaceReminderMessage(event *CustomRace) error { +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 aa0033aae..09754ecc6 100644 --- a/config_ini.go +++ b/config_ini.go @@ -194,8 +194,9 @@ type GlobalServerConfig struct { 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:"-" 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"` + NotificationReminderTimer int `ini:"" show:"hidden" 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:"-" show:"open" input:"checkbox" help:"Show the server password in race start notifications"` // Messages ContentManagerWelcomeMessage string `ini:"-" show:"-"` diff --git a/discord.go b/discord.go index 51e007854..1e47259c3 100644 --- a/discord.go +++ b/discord.go @@ -3,13 +3,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" - "net/url" - "strings" - "time" ) type DiscordManager struct { @@ -115,7 +116,7 @@ func (dm *DiscordManager) CommandSessions() (string, error) { return "", err } - var msg = fmt.Sprintf("Upcoming sessions on server %s\n", serverOpts.Name) + 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" @@ -178,10 +179,9 @@ func (dm *DiscordManager) CommandSchedule() (string, error) { for _, scheduledEvent := range scheduled { raceSetup := scheduledEvent.GetRaceSetup() - trackInfo := trackInfo(raceSetup.Track, raceSetup.TrackLayout) 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", trackInfo.Name) + msg += fmt.Sprintf("Track: %s\n", trackSummary(raceSetup.Track, raceSetup.TrackLayout)) msg += fmt.Sprintf("Cars: %s\n", cars) msg += "\n\n" } @@ -195,6 +195,7 @@ func (dm *DiscordManager) CommandNotify(s *discordgo.Session, m *discordgo.Messa 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 } @@ -257,7 +258,7 @@ func (dm *DiscordManager) CommandNotify(s *discordgo.Session, m *discordgo.Messa } // w00t! - return fmt.Sprintf("The %s role has been assigned, you will now get pinged with notifications. Type the command again to remove it."), nil + 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) { @@ -289,10 +290,12 @@ func (dm *DiscordManager) CommandHandler(s *discordgo.Session, m *discordgo.Mess logrus.WithError(err).Errorf("Error during handling of Discord command") } - _, err = s.ChannelMessageSend(m.ChannelID, msg) + if msg != "" { + _, err = s.ChannelMessageSend(m.ChannelID, msg) - if err != nil { - logrus.WithError(err).Errorf("couldn't send Discord msg") + if err != nil { + logrus.WithError(err).Errorf("couldn't send Discord msg") + } } } diff --git a/form.go b/form.go index 721e01a9c..9b1976b1c 100644 --- a/form.go +++ b/form.go @@ -144,7 +144,7 @@ func (f Form) buildOpts(val reflect.Value, t reflect.Type, parentName string) [] HelpText: template.HTML(typeField.Tag.Get("help")), Type: formType, Opts: make(map[string]bool), - Hidden: formShow == "open" && IsHosted && !f.forceShowAllOptions, + Hidden: formShow == "hidden" || (formShow == "open" && IsHosted && !f.forceShowAllOptions), } if formType == "dropdown" || formType == "multiSelect" { diff --git a/migrations.go b/migrations.go index c22b82575..a670bd95b 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" @@ -68,6 +69,7 @@ var ( addThemeChoiceToAccounts, addRaceWeekendExamples, addServerNameTemplate, + changeNotificationTimer, } ) @@ -445,3 +447,21 @@ 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) +} diff --git a/notification_manager.go b/notification_manager.go index 230713397..847253ac0 100644 --- a/notification_manager.go +++ b/notification_manager.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "os" + "strconv" "strings" "time" @@ -11,12 +12,14 @@ 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 + SendRaceReminderMessage(event *CustomRace, timer int) error + SendChampionshipReminderMessage(championship *Championship, event *ChampionshipEvent, timer int) error SaveServerOptions(oldServerOpts *GlobalServerConfig, newServerOpts *GlobalServerConfig) error } @@ -47,6 +50,47 @@ func (nm *NotificationManager) Stop() error { return nm.discordManager.Stop() } +func (nm *NotificationManager) HasNotificationReminders() bool { + reminders := nm.GetNotificationReminders() + + return len(reminders) > 0 +} + +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 @@ -73,13 +117,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 { @@ -91,9 +128,9 @@ func (nm *NotificationManager) SendRaceStartMessage(config ServerConfig, event R eventName := event.EventName() 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, trackSummary(config.CurrentRaceConfig.Track, config.CurrentRaceConfig.TrackLayout)) } else { - msg = fmt.Sprintf("Race at %s is starting now", trackInfo.Name) + msg = fmt.Sprintf("Race at %s is starting now", trackSummary(config.CurrentRaceConfig.Track, config.CurrentRaceConfig.TrackLayout)) } msg += fmt.Sprintf("\nServer: %s", serverOpts.Name) @@ -139,6 +176,11 @@ func (nm *NotificationManager) SendRaceStartMessage(config ServerConfig, event R 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 + } + dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 MST") var aCarNames []string @@ -156,13 +198,6 @@ 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 - } - var msg = "A new event has been scheduled\n" msg += fmt.Sprintf("Server: %s\n", serverOpts.Name) eventName := event.EventName() @@ -172,54 +207,27 @@ func (nm *NotificationManager) SendRaceScheduledMessage(event *CustomRace, date } msg += fmt.Sprintf("Date: %s\n", dateStr) - msg += fmt.Sprintf("Track: %s\n", trackInfo.Name) + msg += fmt.Sprintf("Track: %s\n", trackSummary(event.RaceConfig.Track, event.RaceConfig.TrackLayout)) 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 { - serverOpts, err := nm.store.LoadServerOptions() - - if err != nil { - logrus.WithError(err).Errorf("couldn't load server options, skipping notification") - return err - } - +func (nm *NotificationManager) SendRaceReminderMessage(event *CustomRace, timer int) error { 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 eventName != "" { - msg = fmt.Sprintf("%s race at %s starts in %d minutes", eventName, trackInfo.Name, serverOpts.NotificationReminderTimer) + msg = fmt.Sprintf("%s race at %s starts in %d minutes", eventName, trackSummary(event.RaceConfig.Track, event.RaceConfig.TrackLayout), timer) } else { - msg = fmt.Sprintf("Race at %s starts in %d minutes", trackInfo.Name, serverOpts.NotificationReminderTimer) + msg = fmt.Sprintf("Race at %s starts in %d minutes", trackSummary(event.RaceConfig.Track, event.RaceConfig.TrackLayout), timer) } 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) - - if err != nil { - logrus.WithError(err).Warnf("Could not load track details, skipping notification: %s, %s", event.RaceSetup.Track, event.RaceSetup.TrackLayout) - return err - } - - 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 race at %s starts in %d minutes", championship.Name, trackInfo.Name, serverOpts.NotificationReminderTimer)) +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)) } diff --git a/race_manager.go b/race_manager.go index 337564a75..f66fb37e6 100644 --- a/race_manager.go +++ b/race_manager.go @@ -980,12 +980,6 @@ func (rm *RaceManager) ScheduleRace(uuid string, date time.Time, action string, return err } - serverOpts, err := rm.store.LoadServerOptions() - - if err != nil { - return err - } - race.Scheduled = date // if there is an existing schedule timer for this event stop it @@ -1028,14 +1022,16 @@ 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)) - 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, timer) + }) + } } } else { @@ -1303,12 +1299,6 @@ func (rm *RaceManager) InitScheduledRaces() error { return err } - serverOpts, err := rm.store.LoadServerOptions() - - if err != nil { - return err - } - for _, race := range races { race := race @@ -1324,14 +1314,16 @@ 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)) - 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, timer) + }) + } } } } else { @@ -1371,7 +1363,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 } @@ -1383,26 +1375,28 @@ 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()) { - duration := time.Until(race.Scheduled) + if race.Scheduled.After(time.Now()) { + duration := time.Until(race.Scheduled) - 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.Add(time.Duration(0-timer) * time.Minute).After(time.Now()) { + // add reminder + duration = time.Until(race.Scheduled.Add(time.Duration(0-timer) * time.Minute)) - 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, timer) + }) + } } } } From 19129e7143738584cdee1a68338384f361705d80 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Wed, 9 Oct 2019 17:55:21 -0500 Subject: [PATCH 09/27] More work on multiple reminder timers --- championship_manager_test.go | 12 +++++++++++- notification_manager.go | 36 +++++++++++++++++------------------- race_weekend_manager.go | 30 +++++++++++++----------------- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/championship_manager_test.go b/championship_manager_test.go index ff9201466..b041cc487 100644 --- a/championship_manager_test.go +++ b/championship_manager_test.go @@ -112,7 +112,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 } diff --git a/notification_manager.go b/notification_manager.go index af522a762..eb9f2ffde 100644 --- a/notification_manager.go +++ b/notification_manager.go @@ -18,9 +18,9 @@ type NotificationDispatcher interface { 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 + 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 } @@ -51,12 +51,15 @@ 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 @@ -127,11 +130,12 @@ 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, trackSummary(config.CurrentRaceConfig.Track, config.CurrentRaceConfig.TrackLayout)) + msg = fmt.Sprintf("%s race at %s is starting now", eventName, trackInfo) } else { - msg = fmt.Sprintf("Race at %s is starting now", trackSummary(config.CurrentRaceConfig.Track, config.CurrentRaceConfig.TrackLayout)) + msg = fmt.Sprintf("Race at %s is starting now", trackInfo) } msg += fmt.Sprintf("\nServer: %s", serverOpts.Name) @@ -199,16 +203,17 @@ func (nm *NotificationManager) SendRaceScheduledMessage(event *CustomRace, date carNames := strings.Join(aCarNames, ", ") - var msg = "A new event has been scheduled\n" + 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", trackSummary(event.RaceConfig.Track, event.RaceConfig.TrackLayout)) + msg += fmt.Sprintf("Track: %s\n", trackInfo) msg += fmt.Sprintf("Car(s): %s\n", carNames) return nm.SendMessage(msg) @@ -217,12 +222,13 @@ func (nm *NotificationManager) SendRaceScheduledMessage(event *CustomRace, date // 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 eventName != "" { - msg = fmt.Sprintf("%s race at %s starts in %d minutes", eventName, trackSummary(event.RaceConfig.Track, event.RaceConfig.TrackLayout), timer) + 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", trackSummary(event.RaceConfig.Track, event.RaceConfig.TrackLayout), timer) + msg = fmt.Sprintf("Race at %s starts in %d minutes", trackInfo, timer) } return nm.SendMessage(msg) @@ -234,15 +240,7 @@ func (nm *NotificationManager) SendChampionshipReminderMessage(championship *Cha } // 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_weekend_manager.go b/race_weekend_manager.go index 81a6cb0e5..e025be027 100644 --- a/race_weekend_manager.go +++ b/race_weekend_manager.go @@ -933,26 +933,22 @@ 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) + rwm.scheduledSessionReminderTimers[session.ID.String()] = time.AfterFunc(duration, func() { + err := rwm.notificationManager.SendRaceWeekendReminderMessage(raceWeekend, session, timer) - rwm.scheduledSessionReminderTimers[session.ID.String()] = time.AfterFunc(duration, func() { - err := rwm.notificationManager.SendRaceWeekendReminderMessage(raceWeekend, session) - - 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") + } + }) + } } } From cb2d2fade5a4119669e289c98cafb2a69c651285 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Wed, 9 Oct 2019 18:04:43 -0500 Subject: [PATCH 10/27] Coding standards --- discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord.go b/discord.go index 1e47259c3..f504b1c28 100644 --- a/discord.go +++ b/discord.go @@ -175,7 +175,7 @@ func (dm *DiscordManager) CommandSchedule() (string, error) { scheduled = append(scheduled, recurring...) - var msg = fmt.Sprintf("\nUpcoming events on server %s\n\n", serverOpts.Name) + msg := fmt.Sprintf("\nUpcoming events on server %s\n\n", serverOpts.Name) for _, scheduledEvent := range scheduled { raceSetup := scheduledEvent.GetRaceSetup() From 6449f132469695b2ca85aef37d0622198906111d Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Wed, 9 Oct 2019 18:10:09 -0500 Subject: [PATCH 11/27] More user feedback in Discord command handling --- discord.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/discord.go b/discord.go index f504b1c28..f86c8b328 100644 --- a/discord.go +++ b/discord.go @@ -104,7 +104,7 @@ func (dm *DiscordManager) CommandSessions() (string, error) { serverOpts, err := dm.store.LoadServerOptions() if err != nil { - return "", err + return "A server error occurred, please try again later", err } start := time.Now() @@ -113,7 +113,7 @@ func (dm *DiscordManager) CommandSessions() (string, error) { calendar, err := dm.scheduledRacesManager.buildCalendar(start, end) if err != nil { - return "", err + return "A server error occurred, please try again later", err } msg := fmt.Sprintf("Upcoming sessions on server %s\n", serverOpts.Name) @@ -132,7 +132,7 @@ func (dm *DiscordManager) CommandSchedule() (string, error) { serverOpts, err := dm.store.LoadServerOptions() if err != nil { - return "", err + return "A server error occurred, please try again later", err } start := time.Now() @@ -140,7 +140,7 @@ func (dm *DiscordManager) CommandSchedule() (string, error) { scheduled, err := dm.scheduledRacesManager.getScheduledRaces() if err != nil { - return "", err + return "A server error occurred, please try again later", err } var recurring []ScheduledEvent @@ -286,6 +286,7 @@ func (dm *DiscordManager) CommandHandler(s *discordgo.Session, m *discordgo.Mess 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") } From 88bd0eb7e46f7784cad4ea91810ec009f6060c01 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Thu, 10 Oct 2019 01:39:59 -0500 Subject: [PATCH 12/27] Added NotifyWhenScheduled, and sending of schedule cancelations. --- config_ini.go | 5 +++-- notification_manager.go | 35 +++++++++++++++++++++++++++++++++++ race_manager.go | 2 ++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/config_ini.go b/config_ini.go index 09754ecc6..3fa3a11bb 100644 --- a/config_ini.go +++ b/config_ini.go @@ -194,9 +194,10 @@ type GlobalServerConfig struct { 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:"hidden" min:"0" max:"65535" help:"This setting has been deprecated and will be removed in the next release. Use Notification Reminder Timers instead."` + NotificationReminderTimer int `ini:"-" show:"hidden" 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:"-" show:"open" input:"checkbox" help:"Show the server password in race start notifications"` + 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/notification_manager.go b/notification_manager.go index eb9f2ffde..c650fa192 100644 --- a/notification_manager.go +++ b/notification_manager.go @@ -18,6 +18,7 @@ type NotificationDispatcher interface { SendMessageWithLink(msg string, linkText string, link *url.URL) error SendRaceStartMessage(config ServerConfig, event RaceEvent) error SendRaceScheduledMessage(event *CustomRace, date time.Time) 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 @@ -186,6 +187,10 @@ func (nm *NotificationManager) SendRaceScheduledMessage(event *CustomRace, date return err } + if serverOpts.NotifyWhenScheduled != 1 { + return nil + } + dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 MST") var aCarNames []string @@ -219,6 +224,36 @@ func (nm *NotificationManager) SendRaceScheduledMessage(event *CustomRace, date return nm.SendMessage(msg) } +// SendRaceScheduledMessage sends a notification when a race is scheduled +func (nm *NotificationManager) SendRaceCancelledMessage(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") + + 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("Event name: %s\n", eventName) + } + + msg += fmt.Sprintf("Date: %s\n", dateStr) + msg += fmt.Sprintf("Track: %s\n", trackInfo) + + return nm.SendMessage(msg) +} + // SendRaceReminderMessage sends a reminder a configurable number of minutes prior to a race starting func (nm *NotificationManager) SendRaceReminderMessage(event *CustomRace, timer int) error { msg := "" diff --git a/race_manager.go b/race_manager.go index f66fb37e6..bdf80997b 100644 --- a/race_manager.go +++ b/race_manager.go @@ -980,6 +980,7 @@ func (rm *RaceManager) ScheduleRace(uuid string, date time.Time, action string, return err } + originalDate := race.Scheduled race.Scheduled = date // if there is an existing schedule timer for this event stop it @@ -1035,6 +1036,7 @@ func (rm *RaceManager) ScheduleRace(uuid string, date time.Time, action string, } } else { + _ = rm.notificationManager.SendRaceCancelledMessage(race, originalDate) race.ClearRecurrenceRule() } From cd1e4461ca6e1647131619253ee5fde898b7c2f3 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Thu, 10 Oct 2019 01:49:53 -0500 Subject: [PATCH 13/27] Updated CM test suite, fixed doc-block comments --- championship_manager_test.go | 4 ++++ notification_manager.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/championship_manager_test.go b/championship_manager_test.go index b041cc487..114a6b81c 100644 --- a/championship_manager_test.go +++ b/championship_manager_test.go @@ -142,6 +142,10 @@ func (d dummyNotificationManager) SendRaceScheduledMessage(event *CustomRace, da return nil } +func (d dummyNotificationManager) SendRaceCancelledMessage(event *CustomRace, date time.Time) error { + return nil +} + func (d dummyNotificationManager) SendRaceReminderMessage(event *CustomRace, timer int) error { return nil } diff --git a/notification_manager.go b/notification_manager.go index c650fa192..287b43e58 100644 --- a/notification_manager.go +++ b/notification_manager.go @@ -224,7 +224,7 @@ func (nm *NotificationManager) SendRaceScheduledMessage(event *CustomRace, date return nm.SendMessage(msg) } -// SendRaceScheduledMessage sends a notification when a race is scheduled +// SendRaceCancelledMessage sends a notification when a race is cancelled func (nm *NotificationManager) SendRaceCancelledMessage(event *CustomRace, date time.Time) error { serverOpts, err := nm.store.LoadServerOptions() From 6fa749a779ffbae5331dfcbbd78fb52770734f8f Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Thu, 10 Oct 2019 07:49:56 -0500 Subject: [PATCH 14/27] Reverting show change, now I know about "-" --- config_ini.go | 2 +- form.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config_ini.go b/config_ini.go index 3fa3a11bb..8be6d0bc7 100644 --- a/config_ini.go +++ b/config_ini.go @@ -194,7 +194,7 @@ type GlobalServerConfig struct { 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:"hidden" min:"0" max:"65535" help:"This setting has been deprecated and will be removed in the next release. Use Notification Reminder Timers instead."` + 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)."` diff --git a/form.go b/form.go index 9b1976b1c..721e01a9c 100644 --- a/form.go +++ b/form.go @@ -144,7 +144,7 @@ func (f Form) buildOpts(val reflect.Value, t reflect.Type, parentName string) [] HelpText: template.HTML(typeField.Tag.Get("help")), Type: formType, Opts: make(map[string]bool), - Hidden: formShow == "hidden" || (formShow == "open" && IsHosted && !f.forceShowAllOptions), + Hidden: formShow == "open" && IsHosted && !f.forceShowAllOptions, } if formType == "dropdown" || formType == "multiSelect" { From 14639203b7590f17cc30f13527367dc53d4a7fc7 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Thu, 10 Oct 2019 17:28:31 -0500 Subject: [PATCH 15/27] Need to use locally scoped variable for timer loop --- championship_manager.go | 6 ++++-- race_manager.go | 9 ++++++--- race_weekend_manager.go | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/championship_manager.go b/championship_manager.go index 483034689..9be23d50d 100644 --- a/championship_manager.go +++ b/championship_manager.go @@ -610,9 +610,10 @@ func (cm *ChampionshipManager) ScheduleEvent(championshipID string, eventID stri 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, timer) + cm.notificationManager.SendChampionshipReminderMessage(championship, event, thisTimer) }) } } @@ -1400,9 +1401,10 @@ func (cm *ChampionshipManager) InitScheduledChampionships() error { 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, timer) + cm.notificationManager.SendChampionshipReminderMessage(championship, event, thisTimer) }) } } diff --git a/race_manager.go b/race_manager.go index bdf80997b..edb61e10a 100644 --- a/race_manager.go +++ b/race_manager.go @@ -1028,9 +1028,10 @@ func (rm *RaceManager) ScheduleRace(uuid string, date time.Time, action string, 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, timer) + _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) }) } } @@ -1321,9 +1322,10 @@ func (rm *RaceManager) InitScheduledRaces() error { 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, timer) + _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) }) } } @@ -1394,9 +1396,10 @@ func (rm *RaceManager) RescheduleNotifications(oldServerOpts *GlobalServerConfig 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, timer) + _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) }) } } diff --git a/race_weekend_manager.go b/race_weekend_manager.go index e025be027..b170df7e0 100644 --- a/race_weekend_manager.go +++ b/race_weekend_manager.go @@ -940,9 +940,10 @@ func (rwm *RaceWeekendManager) setupScheduledSessionTimer(raceWeekend *RaceWeeke 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, timer) + err := rwm.notificationManager.SendRaceWeekendReminderMessage(raceWeekend, session, thisTimer) if err != nil { logrus.WithError(err).Errorf("Could not send race weekend reminder message") From 9f682f22a44a8830dc1e222f2aa512c2c90e8e45 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Thu, 24 Oct 2019 13:19:50 -0500 Subject: [PATCH 16/27] Coding standards --- discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord.go b/discord.go index f86c8b328..a9512abeb 100644 --- a/discord.go +++ b/discord.go @@ -273,7 +273,7 @@ func (dm *DiscordManager) CommandHandler(s *discordgo.Session, m *discordgo.Mess return } - var msg = "" + msg := "" switch m.Content { case "!schedule": From e1f54f10895140401f14f33bb7274f4513d23304 Mon Sep 17 00:00:00 2001 From: Hugh Messenger Date: Fri, 25 Oct 2019 03:25:31 -0500 Subject: [PATCH 17/27] Move timer migration to end of list --- migrations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations.go b/migrations.go index 09e3d5833..b34ef3148 100644 --- a/migrations.go +++ b/migrations.go @@ -69,9 +69,9 @@ var ( addThemeChoiceToAccounts, addRaceWeekendExamples, addServerNameTemplate, - changeNotificationTimer, addAvailableCarsToChampionshipClass, addTyresForP13c, + changeNotificationTimer, } ) From bde6c1a06326912df200aef25f38e2f9ec659ea7 Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Fri, 25 Oct 2019 10:18:08 +0100 Subject: [PATCH 18/27] improve checking around championship class discovery in race weekends, fake class assignment when 'any car model' is specified (fixes #585) --- CHANGELOG.md | 7 +++++++ championships.go | 20 ++++++++++++++++++++ race_weekend.go | 4 ++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2da16c917..22a66f59e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +v1.5.2 +------ + +Fixes: + +* Fixes an issue where Championships with 'Any Car Model' specified would fail to find a class for a car. + v1.5.1 ------ diff --git a/championships.go b/championships.go index 45fb52966..16f30c909 100644 --- a/championships.go +++ b/championships.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "html/template" + "math/rand" "sort" "strings" "sync" @@ -402,6 +403,25 @@ func (c *Championship) FindClassForCarModel(model string) (*ChampionshipClass, e } } + if model == AnyCarModel { + // randomly assign a class based from whatever classes we have with AnyCarModel in their entrylist. + classes := make([]*ChampionshipClass, len(c.Classes)) + + copy(classes, c.Classes) + + rand.Shuffle(len(classes), func(i, j int) { + classes[i], classes[j] = classes[j], classes[i] + }) + + for _, class := range classes { + for _, entrant := range class.Entrants { + if entrant.Model == AnyCarModel { + return class, nil + } + } + } + } + return nil, ErrClassNotFound } diff --git a/race_weekend.go b/race_weekend.go index 5e0934414..9a75e2fa4 100644 --- a/race_weekend.go +++ b/race_weekend.go @@ -626,9 +626,9 @@ func (rws *RaceWeekendSession) FinishingGrid(raceWeekend *RaceWeekend) ([]*RaceW if err != nil { logrus.WithError(err).Warnf("Could not find class for car model: %s for entrant %s", entrant.Car.GetCar(), entrant.Car.GetGUID()) + } else { + entrant.EntrantResult.ClassID = class.ID } - - entrant.EntrantResult.ClassID = class.ID } e := NewRaceWeekendSessionEntrant(rws.ID, entrant.Car, entrant.EntrantResult, rws.Results) From 2f1d19a1faefe77f7efd6fd6fe1f41a7dd5d475f Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Fri, 25 Oct 2019 10:30:27 +0100 Subject: [PATCH 19/27] only call intn on skins if there are skins to choose from --- CHANGELOG.md | 2 ++ race_manager.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a66f59e..a55fa0c2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ v1.5.2 Fixes: * Fixes an issue where Championships with 'Any Car Model' specified would fail to find a class for a car. +* Fixes an issue where cars with no skins might prevent a race from starting. + v1.5.1 ------ diff --git a/race_manager.go b/race_manager.go index 16a027095..935f2e861 100644 --- a/race_manager.go +++ b/race_manager.go @@ -162,7 +162,7 @@ func (rm *RaceManager) applyConfigAndStart(raceConfig CurrentRaceConfig, entryLi // generate a random skin too car, err := rm.carManager.LoadCar(entrant.Model, nil) - if err != nil { + if err != nil || len(car.Skins) == 0 { logrus.WithError(err).Errorf("Could not load car %s. No skin will be specified", entrant.Model) entrant.Skin = "" } else { From 17fc65e2f2691ee83cc68c7605ac6b96db7b2a5e Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Fri, 25 Oct 2019 10:38:00 +0100 Subject: [PATCH 20/27] fix a scheduling issue for championship race weekend sessions --- CHANGELOG.md | 2 +- scheduled_races.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a55fa0c2d..77b7746c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ Fixes: * Fixes an issue where Championships with 'Any Car Model' specified would fail to find a class for a car. * Fixes an issue where cars with no skins might prevent a race from starting. - +* Fixes an issue where Scheduled Championship Race Weekend sessions caused the calendar to error on load. v1.5.1 ------ diff --git a/scheduled_races.go b/scheduled_races.go index 0c341de3d..885ed1291 100644 --- a/scheduled_races.go +++ b/scheduled_races.go @@ -195,6 +195,15 @@ func (srm *ScheduledRacesManager) getScheduledRaces() ([]ScheduledEvent, error) } for _, raceWeekend := range raceWeekends { + if raceWeekend.HasLinkedChampionship() { + raceWeekend.Championship, err = srm.store.LoadChampionship(raceWeekend.ChampionshipID.String()) + + if err != nil { + logrus.WithError(err).Warnf("Could not load linked Championship for Race Weekend") + continue + } + } + for _, session := range raceWeekend.Sessions { if session.ScheduledTime.IsZero() { continue From acf0d45d37b8dca7f8465f7eb8d4dc51ba55eec7 Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Fri, 25 Oct 2019 10:45:31 +0100 Subject: [PATCH 21/27] only send errors to sentry if they aren't net.OpErrors --- templates.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/templates.go b/templates.go index 827154e47..b98ae39b2 100644 --- a/templates.go +++ b/templates.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "html/template" + "net" "net/http" "net/url" "path" @@ -546,7 +547,10 @@ func (tr *Renderer) MustLoadTemplate(w http.ResponseWriter, r *http.Request, vie err := tr.LoadTemplate(w, r, view, vars) if err != nil { - raven.CaptureError(err, nil) + if _, ok := err.(*net.OpError); !ok { + // don't capture OpErrors, they flood sentry with non-errors + raven.CaptureError(err, nil) + } logrus.WithError(err).Errorf("Unable to load template: %s", view) http.Error(w, "unable to load template", http.StatusInternalServerError) return From 717d4507eea6b8a91ec8ec5ada127978b5e0daba Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Fri, 25 Oct 2019 13:07:12 +0100 Subject: [PATCH 22/27] filter out 'any car model' from final cars in server options --- race_manager.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/race_manager.go b/race_manager.go index 935f2e861..6245d89bd 100644 --- a/race_manager.go +++ b/race_manager.go @@ -117,6 +117,19 @@ func (rm *RaceManager) applyConfigAndStart(raceConfig CurrentRaceConfig, entryLi } } + // filter out "AnyCarModel" + finalCars := make([]string, 0) + + for _, car := range strings.Split(config.CurrentRaceConfig.Cars, ";") { + if car == AnyCarModel { + continue + } + + finalCars = append(finalCars, car) + } + + config.CurrentRaceConfig.Cars = strings.Join(finalCars, ";") + // if password override turn the password off if event.OverrideServerPassword() { config.GlobalServerConfig.Password = event.ReplacementServerPassword() From 4e509af394210fae0ea2664bc12d52c779cfa724 Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Fri, 25 Oct 2019 13:25:13 +0100 Subject: [PATCH 23/27] fix schedule after next race checkbox (fixes #586) --- CHANGELOG.md | 1 + cmd/server-manager/typescript/src/javascript/manager.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77b7746c7..6ea97d77f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Fixes: * Fixes an issue where Championships with 'Any Car Model' specified would fail to find a class for a car. * Fixes an issue where cars with no skins might prevent a race from starting. * Fixes an issue where Scheduled Championship Race Weekend sessions caused the calendar to error on load. +* Fixes the Race Weekend "Start after previous session" checkbox not displaying correctly. v1.5.1 ------ diff --git a/cmd/server-manager/typescript/src/javascript/manager.js b/cmd/server-manager/typescript/src/javascript/manager.js index 9c45b034a..6f4fca961 100644 --- a/cmd/server-manager/typescript/src/javascript/manager.js +++ b/cmd/server-manager/typescript/src/javascript/manager.js @@ -16,7 +16,7 @@ $(document).ready(function () { $.fn.bootstrapSwitch.defaults.size = 'small'; $.fn.bootstrapSwitch.defaults.animate = false; $.fn.bootstrapSwitch.defaults.onColor = "success"; - $document.find("input[type='checkbox']:not(input[name='EntryList.OverwriteAllEvents']:hidden)").bootstrapSwitch(); + $document.find("input[type='checkbox']:not(input[name='EntryList.OverwriteAllEvents']:hidden):not(input[name='session-start-after-parent']:hidden)").bootstrapSwitch(); championships.init(); $document.find(".race-setup").each(function (index, elem) { From d86779c3ad86a352ebd88c981f7f14415337aaa4 Mon Sep 17 00:00:00 2001 From: henry Date: Fri, 25 Oct 2019 15:40:39 +0100 Subject: [PATCH 24/27] changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2da16c917..a70ecba95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +v1.5.2 +------ + +Fixes: + +* Fixes track pages for users running Server Manager on Windows + +--- + v1.5.1 ------ From 646c17c16a64edff9dc8e52fbc8ac11c370b4efa Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Fri, 25 Oct 2019 16:21:52 +0100 Subject: [PATCH 25/27] add discord changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad3836308..2106290e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ v1.5.2 ------ +Added: + +* Discord Enhancements (Thanks @cheesegrits!): + - Splits the '!schedule' command into '!sessions' (full "wall of text" individual session calendar, restricted to one week ahead) and '!schedule' (abbreviated, one per race calendar). This still needs work, as can easily exceed Discord's max msg length. + + - Added role mentioning. If the optional DiscordRoleID is set, that role will be mentioned in all Discord notifications (meaning anyone with that role will get pinged). Additionally, if the optional 'DiscordRoleCommand' is also set, we will attempt to add/remove that role for users, when they issue the "!whatever" command verb - this requires that the bot has "Manage Roles" permission. + + - Changed NotificationReminderTimer to NotificationReminderTimers (plural), to support comma separated multiple timers (like "90,15" for two reminders at 90 and 15 minutes). + + - Added option to disable notifications when a race is scheduled. + + - Added notification for scheduled races being cancelled. + + - Improved formatting of Discord messages, everything is now an embed (except the role mention, which has to be part of the basic message). + Fixes: * Fixes track pages for users running Server Manager on Windows From 65c0c35cd6713789aa3719efc129b3a9a73a2c8a Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Fri, 25 Oct 2019 16:30:50 +0100 Subject: [PATCH 26/27] fix live timings disconnect on event loop --- CHANGELOG.md | 1 + race_control.go | 18 +++--------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ea97d77f..6826bb28a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Fixes: * Fixes an issue where cars with no skins might prevent a race from starting. * Fixes an issue where Scheduled Championship Race Weekend sessions caused the calendar to error on load. * Fixes the Race Weekend "Start after previous session" checkbox not displaying correctly. +* Fixes an issue where all drivers were incorrectly disconnected from the Live Timings page when an event with multiple sessions looped v1.5.1 ------ diff --git a/race_control.go b/race_control.go index 5d862f93f..23dd8be91 100644 --- a/race_control.go +++ b/race_control.go @@ -216,27 +216,15 @@ func (rc *RaceControl) OnNewSession(sessionInfo udp.SessionInfo) error { rc.driverGUIDUpdateCounter = make(map[udp.DriverGUID]int) rc.driverGUIDUpdateCounterMutex.Unlock() - deleteCars := true - emptyCarInfo := false - - if sessionInfo.CurrentSessionIndex != 0 && oldSessionInfo.Track == sessionInfo.Track && oldSessionInfo.TrackConfig == sessionInfo.TrackConfig { - // only remove cars on the first session (avoid deleting between practice/qualify/race) - deleteCars = false - emptyCarInfo = true - } + emptyCarInfo := true if (rc.ConnectedDrivers.Len() > 0 || rc.DisconnectedDrivers.Len() > 0) && sessionInfo.Type == udp.SessionTypePractice { if oldSessionInfo.Type == sessionInfo.Type && oldSessionInfo.Track == sessionInfo.Track && oldSessionInfo.TrackConfig == sessionInfo.TrackConfig && oldSessionInfo.Name == sessionInfo.Name { - // this is a looped practice event, keep the cars - deleteCars = false + // this is a looped event, keep the cars emptyCarInfo = false } } - if deleteCars { - rc.clearAllDrivers() - } - if emptyCarInfo { _ = rc.ConnectedDrivers.Each(func(driverGUID udp.DriverGUID, driver *RaceControlDriver) error { *driver = *NewRaceControlDriver(driver.CarInfo) @@ -274,7 +262,7 @@ func (rc *RaceControl) OnNewSession(sessionInfo udp.SessionInfo) error { rc.TrackMapData = *trackMapData } - logrus.Debugf("New session detected: %s at %s (%s) [deleteCars: %t, emptyCarInfo: %t]", sessionInfo.Type.String(), sessionInfo.Track, sessionInfo.TrackConfig, deleteCars, emptyCarInfo) + logrus.Debugf("New session detected: %s at %s (%s) [emptyCarInfo: %t]", sessionInfo.Type.String(), sessionInfo.Track, sessionInfo.TrackConfig, emptyCarInfo) go rc.requestSessionInfo() From 6936d04da1ed5f62195e8169fdbdc7f934986646 Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Fri, 25 Oct 2019 17:01:38 +0100 Subject: [PATCH 27/27] fix race control tests --- championship_manager_test.go | 13 +++++++++---- race_control_test.go | 7 +++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/championship_manager_test.go b/championship_manager_test.go index 2aa5a1b38..b1942c54a 100644 --- a/championship_manager_test.go +++ b/championship_manager_test.go @@ -59,7 +59,9 @@ var TestEntryList = EntryList{ }, } -type dummyServerProcess struct{} +type dummyServerProcess struct { + doneCh chan struct{} +} func (dummyServerProcess) Logs() string { return "" @@ -69,7 +71,10 @@ func (dummyServerProcess) Start(cfg ServerConfig, entryList EntryList, forwardin return nil } -func (dummyServerProcess) Stop() error { +func (d dummyServerProcess) Stop() error { + if d.doneCh != nil { + d.doneCh <- struct{}{} + } return nil } @@ -92,8 +97,8 @@ func (dummyServerProcess) SendUDPMessage(message udp.Message) error { return nil } -func (dummyServerProcess) Done() <-chan struct{} { - return nil +func (d dummyServerProcess) Done() <-chan struct{} { + return d.doneCh } func (dummyServerProcess) GetServerConfig() ServerConfig { diff --git a/race_control_test.go b/race_control_test.go index 077be0ad3..2593e2717 100644 --- a/race_control_test.go +++ b/race_control_test.go @@ -655,7 +655,8 @@ func TestRaceControl_OnNewSession(t *testing.T) { }) t.Run("Two separate event progressions", func(t *testing.T) { - raceControl := NewRaceControl(NilBroadcaster{}, nilTrackData{}, dummyServerProcess{}) + process := dummyServerProcess{doneCh: make(chan struct{})} + raceControl := NewRaceControl(NilBroadcaster{}, nilTrackData{}, process) if err := raceControl.OnVersion(udp.Version(4)); err != nil { t.Error(err) @@ -745,6 +746,8 @@ func TestRaceControl_OnNewSession(t *testing.T) { return } + process.Stop() + // now go to the next session, lap times should be removed from all drivers, but all should still be connected. err = raceControl.OnNewSession(udp.SessionInfo{ Version: 4, @@ -773,7 +776,7 @@ func TestRaceControl_OnNewSession(t *testing.T) { } if raceControl.ConnectedDrivers.Len() != 0 || raceControl.DisconnectedDrivers.Len() != 0 { - t.Error("Invalid driver list lengths. Expected 0 drivers to still be in driver lists.") + t.Errorf("Invalid driver list lengths. Expected 0 drivers to still be in driver lists. (%d conn, %d disconn)", raceControl.ConnectedDrivers.Len(), raceControl.DisconnectedDrivers.Len()) return } })