diff --git a/src/Miha.Discord/Extensions/DiscordTimestampExtensions.cs b/src/Miha.Discord/Extensions/DiscordTimestampExtensions.cs index b96ad7e..3cc9098 100644 --- a/src/Miha.Discord/Extensions/DiscordTimestampExtensions.cs +++ b/src/Miha.Discord/Extensions/DiscordTimestampExtensions.cs @@ -1,4 +1,5 @@ using Discord; +using NodaTime; namespace Miha.Discord.Extensions; @@ -7,6 +8,9 @@ public static class DiscordTimestampExtensions public static string ToDiscordTimestamp(this DateTimeOffset offset) => TimestampTag.FromDateTimeOffset(offset).ToString(); public static string ToDiscordTimestamp(this DateTimeOffset offset, TimestampTagStyles style) => TimestampTag.FromDateTimeOffset(offset, style).ToString(); + public static string ToDiscordTimestamp(this Instant instant) => TimestampTag.FromDateTimeOffset(instant.ToDateTimeOffset()).ToString(); + public static string ToDiscordTimestamp(this Instant instant, TimestampTagStyles style) => TimestampTag.FromDateTimeOffset(instant.ToDateTimeOffset(), style).ToString(); + public static string? ToDiscordTimestamp(this DateTimeOffset? offset) => offset?.ToDiscordTimestamp(); public static string? ToDiscordTimestamp(this DateTimeOffset? offset, TimestampTagStyles style) => offset?.ToDiscordTimestamp(style); } diff --git a/src/Miha.Discord/Services/GuildScheduledEventService.cs b/src/Miha.Discord/Services/GuildScheduledEventService.cs index 6e8d311..809b43e 100644 --- a/src/Miha.Discord/Services/GuildScheduledEventService.cs +++ b/src/Miha.Discord/Services/GuildScheduledEventService.cs @@ -3,23 +3,25 @@ using FluentResults; using Microsoft.Extensions.Logging; using Miha.Discord.Services.Interfaces; -using Miha.Shared; +using Miha.Shared.ZonedClocks.Interfaces; using NodaTime; using NodaTime.Calendars; -using NodaTime.Extensions; namespace Miha.Discord.Services; public class GuildScheduledEventService : IGuildScheduledEventService { private readonly DiscordSocketClient _discordClient; + private readonly IEasternStandardZonedClock _easternStandardZonedClock; private readonly ILogger _logger; public GuildScheduledEventService( DiscordSocketClient discordClient, + IEasternStandardZonedClock easternStandardZonedClock, ILogger logger) { _discordClient = discordClient; + _easternStandardZonedClock = easternStandardZonedClock; _logger = logger; } @@ -38,8 +40,8 @@ public async Task>> GetScheduledWeeklyE var eventsThisWeek = events.Where(guildEvent => { - var estDate = guildEvent.StartTime.ToZonedDateTime() - .WithZone(DateTimeZoneProviders.Tzdb[Timezones.IanaEasternTime]).Date; + var estDate = _easternStandardZonedClock.ToZonedDateTime(guildEvent.StartTime).Date; + var weekOfDate = WeekYearRules.Iso.GetWeekOfWeekYear(estDate); return weekOfDate == weekNumberInYear; diff --git a/src/Miha.Discord/Services/Hosted/GuildEventScheduleService.cs b/src/Miha.Discord/Services/Hosted/GuildEventScheduleService.cs index 15fb206..b68dff9 100644 --- a/src/Miha.Discord/Services/Hosted/GuildEventScheduleService.cs +++ b/src/Miha.Discord/Services/Hosted/GuildEventScheduleService.cs @@ -116,43 +116,78 @@ private async Task PostWeeklyScheduleAsync() return; } - var eventsByDay = new Dictionary>(); - foreach (var guildScheduledEvent in eventsThisWeek.OrderBy(e => e.StartTime.Date)) + var daysThisWeek = _easternStandardZonedClock.GetCurrentWeekAsDates(); + + var eventsByDay = new Dictionary>(); + var eventsThisWeekList = eventsThisWeek.ToList(); + foreach (var dayOfWeek in daysThisWeek.OrderBy(d => d)) { - var day = guildScheduledEvent - .StartTime - .ToZonedDateTime() - .WithZone(_easternStandardZonedClock.GetTzdbTimeZone()) - .Date.AtMidnight().ToDateTimeUnspecified().ToString("dddd"); + eventsByDay.Add(dayOfWeek, new List()); - if (!eventsByDay.ContainsKey(day)) + foreach (var guildScheduledEvent in eventsThisWeekList.Where(e => _easternStandardZonedClock.ToZonedDateTime(e.StartTime).Date.ToDateOnly() == dayOfWeek)) { - eventsByDay.Add(day, new List()); + eventsByDay[dayOfWeek].Add(guildScheduledEvent); } - - eventsByDay[day].Add(guildScheduledEvent); } - - _logger.LogInformation("Wiping weekly schedule messages"); - - var deletedMessages = 0; - var messages = await weeklyScheduleChannel + + _logger.LogInformation("Updating weekly schedule"); + + var postedHeader = false; + var postedFooter = false; + + var messages = (await weeklyScheduleChannel .GetMessagesAsync(50) - .FlattenAsync(); + .FlattenAsync()) + .ToList(); - foreach (var message in messages.Where(m => m.Author.Id == _client.CurrentUser.Id)) + // Wipe any existing messages if a message by day doesn't already exist + foreach (var (day, _) in eventsByDay) { - await message.DeleteAsync(); - deletedMessages++; - } + var lastPostedMessage = messages + .FirstOrDefault(m => + m.Author.Id == _client.CurrentUser.Id && + m.Embeds.Any(e => e.Description.Contains(day.ToString("dddd")))); - _logger.LogInformation("Wiped {DeletedMessages} messages", deletedMessages); - - _logger.LogInformation("Posting weekly schedule"); + if (lastPostedMessage is not null) + { + continue; + } + + var messagesToDelete = messages + .Where(m => m.Author.Id == _client.CurrentUser.Id) + .ToList(); + + if (messagesToDelete.Any()) + { + var deletedMessages = 0; + + _logger.LogInformation("Wiping posted messages"); + + foreach (var message in messagesToDelete) + { + await message.DeleteAsync(); + deletedMessages++; + } + + _logger.LogInformation("Deleted {DeletedMessages} messages", deletedMessages); + + // Update the messages list + messages = (await weeklyScheduleChannel + .GetMessagesAsync(50) + .FlattenAsync()) + .ToList(); + } + + break; + } - var postedHeader = false; - var postedFooter = false; + // TODO - Future me + // If the ordering becomes a problem, a potential solution could be to use an index + // to update the message at [1] (Tuesday), [6] (Sunday), [0] Monday for example + // this would ensure the order of messages align with the days of the week + // and to delete all messages from the bot if there's any more than 7 messages total + // Update (or post) a message with an embed of events for that day, for each day of the week foreach (var (day, events) in eventsByDay) { var embed = new EmbedBuilder(); @@ -165,28 +200,50 @@ private async Task PostWeeklyScheduleAsync() .WithAuthor("Weekly event schedule", _client.CurrentUser.GetAvatarUrl()); postedHeader = true; } + + var timeStamp = day + .ToLocalDate() + .AtStartOfDayInZone(_easternStandardZonedClock.GetTzdbTimeZone()) + .ToInstant() + .ToDiscordTimestamp(TimestampTagStyles.ShortDate); - description.AppendLine("### " + day + " - " + DiscordTimestampExtensions.ToDiscordTimestamp(events.First().StartTime.Date, TimestampTagStyles.ShortDate)); - - foreach (var guildEvent in events.OrderBy(e => e.StartTime)) - { - var location = guildEvent.Location ?? "Unknown"; - var url = $"https://discord.com/events/{guildEvent.Guild.Id}/{guildEvent.Id}"; + description.AppendLine($"### {day.ToString("dddd")} - {timeStamp}"); - if (location is "Unknown" && guildEvent.ChannelId is not null) + if (!events.Any()) + { + description.AppendLine("*No events scheduled*"); + } + else + { + foreach (var guildEvent in events.OrderBy(e => e.StartTime)) { - location = "Discord"; - } + var location = guildEvent.Location ?? "Unknown"; + var url = $"https://discord.com/events/{guildEvent.Guild.Id}/{guildEvent.Id}"; - description.AppendLine($"- [{location} - {guildEvent.Name}]({url})"); - description.AppendLine($" - {guildEvent.StartTime.ToDiscordTimestamp(TimestampTagStyles.ShortTime)} - {guildEvent.StartTime.ToDiscordTimestamp(TimestampTagStyles.Relative)}"); + if (location is "Unknown" && guildEvent.ChannelId is not null) + { + location = "Discord"; + } - if (guildEvent.Creator is not null) - { - description.AppendLine($" - Hosted by {guildEvent.Creator.Mention}"); + description.AppendLine($"- [{location} - {guildEvent.Name}]({url})"); + + description.Append($" - {guildEvent.StartTime.ToDiscordTimestamp(TimestampTagStyles.ShortTime)}"); + if (guildEvent.Status is GuildScheduledEventStatus.Active) + { + description.AppendLine(" - Happening now!"); + } + else + { + description.AppendLine($" - {guildEvent.StartTime.ToDiscordTimestamp(TimestampTagStyles.Relative)}"); + } + + if (guildEvent.Creator is not null) + { + description.AppendLine($" - Hosted by {guildEvent.Creator.Mention}"); + } } } - + if (!postedFooter && day == eventsByDay.Last().Key) { embed @@ -199,10 +256,26 @@ private async Task PostWeeklyScheduleAsync() embed .WithColor(new Color(255, 43, 241)) .WithDescription(description.ToString()); + + var lastPostedMessage = messages + .FirstOrDefault(m => + m.Author.Id == _client.CurrentUser.Id && + m.Embeds.Any(e => e.Description.Contains(day.ToString("dddd")))); - await weeklyScheduleChannel.SendMessageAsync(embed: embed.Build()); + if (lastPostedMessage is null) + { + _logger.LogInformation("Posting new message"); + await weeklyScheduleChannel.SendMessageAsync(embed: embed.Build()); + } + else + { + await weeklyScheduleChannel.ModifyMessageAsync(lastPostedMessage.Id, props => + { + props.Embed = embed.Build(); + }); + } - _logger.LogInformation("Finished posting weekly schedule"); + _logger.LogInformation("Finished updating weekly schedule"); } } diff --git a/src/Miha.Shared/ZonedClocks/Interfaces/IZonedClock.cs b/src/Miha.Shared/ZonedClocks/Interfaces/IZonedClock.cs index 5b7cdfd..725e956 100644 --- a/src/Miha.Shared/ZonedClocks/Interfaces/IZonedClock.cs +++ b/src/Miha.Shared/ZonedClocks/Interfaces/IZonedClock.cs @@ -9,38 +9,59 @@ public interface IZonedClock : IClock /// to the time zone of this object. /// /// The current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// time zone of this . public ZonedDateTime GetCurrentZonedDateTime(); /// /// Returns the local date/time of the current instant provided by the underlying clock, adjusted - /// to the time zone of this object. + /// to the time zone of this . /// /// The local date/time of the current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// time zone of this . public LocalDateTime GetCurrentLocalDateTime(); /// /// Returns the offset date/time of the current instant provided by the underlying clock, adjusted - /// to the time zone of this object. + /// to the time zone of this . /// /// The offset date/time of the current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// time zone of this . public OffsetDateTime GetCurrentOffsetDateTime(); /// /// Returns the local date of the current instant provided by the underlying clock, adjusted - /// to the time zone of this object. + /// to the time zone of this . /// /// The local date of the current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// time zone of this . public LocalDate GetCurrentDate(); /// /// Returns the local time of the current instant provided by the underlying clock, adjusted - /// to the time zone of this object. + /// to the time zone of this . /// /// The local time of the current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// time zone of this . public LocalTime GetCurrentTimeOfDay(); + + /// + /// Returns the current week of the year, adjusted to the time zone of this , and abides + /// by the ISO-8601 standard. + /// + /// The current week of the year, adjusted to the time zone of this . + public int GetCurrentWeek(); + + /// + /// Returns an enumerable containing all the days of the current week, adjusted + /// to the time zone of this and abides by the ISO-8601 standard. + /// + /// A list of dates of the current week, adjusted to the time zone of this . + public IEnumerable GetCurrentWeekAsDates(IsoDayOfWeek isoDayOfWeek = IsoDayOfWeek.Monday); + + /// + /// Converts an offset to a using the time zone of this . + /// + /// + /// + public ZonedDateTime ToZonedDateTime(DateTimeOffset offset); } diff --git a/src/Miha.Shared/ZonedClocks/ZonedClock.cs b/src/Miha.Shared/ZonedClocks/ZonedClock.cs index c922a69..c255b3e 100644 --- a/src/Miha.Shared/ZonedClocks/ZonedClock.cs +++ b/src/Miha.Shared/ZonedClocks/ZonedClock.cs @@ -1,5 +1,7 @@ using Miha.Shared.ZonedClocks.Interfaces; using NodaTime; +using NodaTime.Calendars; +using NodaTime.Extensions; namespace Miha.Shared.ZonedClocks; @@ -39,49 +41,49 @@ public ZonedClock(IClock clock, DateTimeZone zone, CalendarSystem calendar) Calendar = calendar; } - /// - /// Returns the current instant provided by the underlying clock. - /// - /// The current instant provided by the underlying clock. + /// public Instant GetCurrentInstant() => Clock.GetCurrentInstant(); - /// - /// Returns the current instant provided by the underlying clock, adjusted - /// to the time zone of this object. - /// - /// The current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// public ZonedDateTime GetCurrentZonedDateTime() => GetCurrentInstant().InZone(Zone, Calendar); - /// - /// Returns the local date/time of the current instant provided by the underlying clock, adjusted - /// to the time zone of this object. - /// - /// The local date/time of the current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// public LocalDateTime GetCurrentLocalDateTime() => GetCurrentZonedDateTime().LocalDateTime; - /// - /// Returns the offset date/time of the current instant provided by the underlying clock, adjusted - /// to the time zone of this object. - /// - /// The offset date/time of the current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// public OffsetDateTime GetCurrentOffsetDateTime() => GetCurrentZonedDateTime().ToOffsetDateTime(); - /// - /// Returns the local date of the current instant provided by the underlying clock, adjusted - /// to the time zone of this object. - /// - /// The local date of the current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// public LocalDate GetCurrentDate() => GetCurrentZonedDateTime().Date; - /// - /// Returns the local time of the current instant provided by the underlying clock, adjusted - /// to the time zone of this object. - /// - /// The local time of the current instant provided by the underlying clock, adjusted to the - /// time zone of this object. + /// public LocalTime GetCurrentTimeOfDay() => GetCurrentZonedDateTime().TimeOfDay; + + /// + public int GetCurrentWeek() => WeekYearRules.Iso.GetWeekOfWeekYear(GetCurrentDate()); + + /// + public IEnumerable GetCurrentWeekAsDates(IsoDayOfWeek isoDayOfWeek = IsoDayOfWeek.Monday) + { + // Get the current date + var currentDate = GetCurrentDate(); + + // Get the current week in year + var currentWeek = WeekYearRules.Iso.GetWeekOfWeekYear(currentDate); + + // Get the first day of the week (Monday) for the current week + var firstDayOfWeek = LocalDate.FromWeekYearWeekAndDay(currentDate.Year, currentWeek, isoDayOfWeek); + + var daysOfWeek = new List(); + for (var i = 0; i < 7; i++) + { + var day = firstDayOfWeek.PlusDays(i).ToDateOnly(); + daysOfWeek.Add(day); + } + + return daysOfWeek; + } + + /// + public ZonedDateTime ToZonedDateTime(DateTimeOffset offset) => offset.ToZonedDateTime().WithZone(Zone); }