Skip to content

Commit

Permalink
Add weekly schedule (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
Twinki14 authored Nov 18, 2023
1 parent 97e7cf2 commit 5597876
Show file tree
Hide file tree
Showing 14 changed files with 360 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/Miha.Discord/Miha.Discord.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<PackageReference Include="Cronos" Version="0.7.1" />
<PackageReference Include="Discord.Addons.Hosting" Version="5.2.0" />
<PackageReference Include="Discord.Net" Version="3.12.0" />
<PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="SlimMessageBus" Version="2.0.1" />
<PackageReference Include="SlimMessageBus.Host.Memory" Version="2.1.10" />
</ItemGroup>
Expand Down
20 changes: 20 additions & 0 deletions src/Miha.Discord/Modules/Guild/ConfigureModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,24 @@ public async Task RolesAsync(
await FollowupMinimalAsync("Any users that have any roles in the list are allowed to have their birthdays announced", fields);
}
}

[SlashCommand("weekly-schedule", "Sets or updates the weekly event schedule channel")]
public async Task WeeklyScheduleAsync(
[Summary(description: "The channel a weekly schedule of events will be posted")] ITextChannel channel,
[Summary(description: "Setting this to true will disable the weekly event schedule")] bool disable = false)
{
var result = await _guildService.UpsertAsync(channel.GuildId, options => options.WeeklyScheduleChannel = disable ? null : channel.Id);

if (result.IsFailed)
{
await RespondErrorAsync(result.Errors);
return;
}

var fields = new EmbedFieldBuilder()
.WithName("Weekly event schedule channel")
.WithValue(disable ? "Disabled" : channel.Mention);

await RespondSuccessAsync("Updated guild options", fields);
}
}
13 changes: 12 additions & 1 deletion src/Miha.Discord/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
using Miha.Discord.Consumers;
using Miha.Discord.Consumers.GuildEvent;
using Miha.Discord.Services;
using Miha.Discord.Services.Hosted;
using Miha.Discord.Services.Interfaces;
using SlimMessageBus.Host;
using SlimMessageBus.Host.Memory;
using GuildEventMonitorService = Miha.Discord.Services.Hosted.GuildEventMonitorService;

namespace Miha.Discord;

Expand All @@ -19,10 +22,18 @@ public static IServiceCollection AddDiscordOptions(this IServiceCollection servi
return services;
}

public static IServiceCollection AddDiscordClientServices(this IServiceCollection services)
public static IServiceCollection AddDiscordServices(this IServiceCollection services)
{
services.AddSingleton<IGuildScheduledEventService, GuildScheduledEventService>();

return services;
}

public static IServiceCollection AddDiscordHostedServices(this IServiceCollection services)
{
services.AddHostedService<InteractionHandler>();
services.AddHostedService<GuildEventMonitorService>();
services.AddHostedService<GuildEventScheduleService>();
services.AddHostedService<SlimMessageBusService>();

return services;
Expand Down
50 changes: 50 additions & 0 deletions src/Miha.Discord/Services/GuildScheduledEventService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Discord;
using Discord.WebSocket;
using FluentResults;
using Microsoft.Extensions.Logging;
using Miha.Discord.Services.Interfaces;
using Miha.Shared;
using NodaTime;
using NodaTime.Calendars;
using NodaTime.Extensions;

namespace Miha.Discord.Services;

public class GuildScheduledEventService : IGuildScheduledEventService
{
private readonly DiscordSocketClient _discordClient;
private readonly ILogger<GuildScheduledEventService> _logger;

public GuildScheduledEventService(
DiscordSocketClient discordClient,
ILogger<GuildScheduledEventService> logger)
{
_discordClient = discordClient;
_logger = logger;
}

public async Task<Result<IEnumerable<IGuildScheduledEvent>>> GetScheduledWeeklyEventsAsync(ulong guildId, LocalDate dateOfTheWeek)
{
var weekNumberInYear = WeekYearRules.Iso.GetWeekOfWeekYear(dateOfTheWeek);

var guild = _discordClient.GetGuild(guildId);

if (guild is null)
{
return Result.Fail<IEnumerable<IGuildScheduledEvent>>("Failed to fetch discord guild");
}

var events = await guild.GetEventsAsync();

var eventsThisWeek = events.Where(guildEvent =>
{
var estDate = guildEvent.StartTime.ToZonedDateTime()
.WithZone(DateTimeZoneProviders.Tzdb[Timezones.IanaEasternTime]).Date;
var weekOfDate = WeekYearRules.Iso.GetWeekOfWeekYear(estDate);

return weekOfDate == weekNumberInYear;
}).Cast<IGuildScheduledEvent>();

return Result.Ok(eventsThisWeek);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using Miha.Logic.Services.Interfaces;
using Miha.Shared.ZonedClocks.Interfaces;

namespace Miha.Discord.Services;
namespace Miha.Discord.Services.Hosted;

public class BirthdayAnnouncementService : DiscordClientService
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Discord.Addons.Hosting;
using Discord.Addons.Hosting.Util;
using Discord.WebSocket;
using Humanizer;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -11,7 +12,7 @@
using Miha.Shared.ZonedClocks.Interfaces;
using Newtonsoft.Json;

namespace Miha.Discord.Services;
namespace Miha.Discord.Services.Hosted;

public partial class GuildEventMonitorService : DiscordClientService
{
Expand Down Expand Up @@ -49,6 +50,8 @@ public GuildEventMonitorService(

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Waiting for client to be ready...");

await Client.WaitForReadyAsync(stoppingToken);

while (!stoppingToken.IsCancellationRequested)
Expand All @@ -65,6 +68,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
continue;
}

var next = nextUtc.Value - utcNow;

_logger.LogDebug("Waiting {Time} until next operation", next.Humanize(3));

await Task.Delay(nextUtc.Value - utcNow, stoppingToken);
}
}
Expand Down
211 changes: 211 additions & 0 deletions src/Miha.Discord/Services/Hosted/GuildEventScheduleService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
using System.Text;
using Cronos;
using Discord;
using Discord.Addons.Hosting;
using Discord.Addons.Hosting.Util;
using Discord.WebSocket;
using Humanizer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Miha.Discord.Extensions;
using Miha.Discord.Services.Interfaces;
using Miha.Logic.Services.Interfaces;
using Miha.Shared.ZonedClocks.Interfaces;
using NodaTime.Extensions;

namespace Miha.Discord.Services.Hosted;

public partial class GuildEventScheduleService : DiscordClientService
{
private readonly DiscordSocketClient _client;
private readonly IEasternStandardZonedClock _easternStandardZonedClock;
private readonly IGuildService _guildService;
private readonly IGuildScheduledEventService _scheduledEventService;
private readonly DiscordOptions _discordOptions;
private readonly ILogger<GuildEventScheduleService> _logger;
private const string Schedule = "0,5,10,15,20,25,30,35,40,45,50,55 * * * *"; // https://crontab.cronhub.io/

private readonly CronExpression _cron;

public GuildEventScheduleService(
DiscordSocketClient client,
IEasternStandardZonedClock easternStandardZonedClock,
IGuildService guildService,
IGuildScheduledEventService scheduledEventService,
IOptions<DiscordOptions> discordOptions,
ILogger<GuildEventScheduleService> logger) : base(client, logger)
{
_client = client;
_easternStandardZonedClock = easternStandardZonedClock;
_guildService = guildService;
_scheduledEventService = scheduledEventService;
_discordOptions = discordOptions.Value;
_logger = logger;

_cron = CronExpression.Parse(Schedule, CronFormat.Standard);
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Waiting for client to be ready...");

await Client.WaitForReadyAsync(stoppingToken);

while (!stoppingToken.IsCancellationRequested)
{
await PostWeeklyScheduleAsync();

var utcNow = _easternStandardZonedClock.GetCurrentInstant().ToDateTimeUtc();
var nextUtc = _cron.GetNextOccurrence(DateTimeOffset.UtcNow, _easternStandardZonedClock.GetTimeZoneInfo());

if (nextUtc is null)
{
_logger.LogWarning("Next utc occurence is null");
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
continue;
}

var next = nextUtc.Value - utcNow;

_logger.LogDebug("Waiting {Time} until next operation", next.Humanize(3));

await Task.Delay(nextUtc.Value - utcNow, stoppingToken);
}

_logger.LogInformation("Hosted service ended");
}

private async Task PostWeeklyScheduleAsync()
{
if (_discordOptions.Guild is null)
{
_logger.LogWarning("Guild isn't configured");
return;
}

var guildResult = await _guildService.GetAsync(_discordOptions.Guild);
var guild = guildResult.Value;

if (guildResult.IsFailed || guild is null)
{
_logger.LogWarning("Guild doc failed, or the guild is null for some reason {Errors}", guildResult.Errors);
return;
}

if (guild.WeeklyScheduleChannel is null)
{
_logger.LogDebug("Guild doesn't have a configured weekly schedule channel");
return;
}

var eventsThisWeekResult = await _scheduledEventService.GetScheduledWeeklyEventsAsync(guild.Id, _easternStandardZonedClock.GetCurrentDate());
var eventsThisWeek = eventsThisWeekResult.Value;

if (eventsThisWeekResult.IsFailed || eventsThisWeek is null)
{
_logger.LogWarning("Fetching this weeks events failed, or is null {Errors}", eventsThisWeekResult.Errors);
return;
}

var weeklyScheduleChannelResult = await _guildService.GetWeeklyScheduleChannel(guild.Id);
var weeklyScheduleChannel = weeklyScheduleChannelResult.Value;

if (weeklyScheduleChannelResult.IsFailed || weeklyScheduleChannel is null)
{
_logger.LogWarning("Fetching the guilds weekly schedule channel failed, or is null {Errors}", weeklyScheduleChannelResult.Errors);
return;
}

var eventsByDay = new Dictionary<string, IList<IGuildScheduledEvent>>();
foreach (var guildScheduledEvent in eventsThisWeek.OrderBy(e => e.StartTime.Date))
{
var day = guildScheduledEvent
.StartTime
.ToZonedDateTime()
.WithZone(_easternStandardZonedClock.GetTzdbTimeZone())
.Date.AtMidnight().ToDateTimeUnspecified().ToString("dddd");

if (!eventsByDay.ContainsKey(day))
{
eventsByDay.Add(day, new List<IGuildScheduledEvent>());
}

eventsByDay[day].Add(guildScheduledEvent);
}

_logger.LogInformation("Wiping weekly schedule messages");

var deletedMessages = 0;
var messages = await weeklyScheduleChannel
.GetMessagesAsync(50)
.FlattenAsync();

foreach (var message in messages.Where(m => m.Author.Id == _client.CurrentUser.Id))
{
await message.DeleteAsync();
deletedMessages++;
}

_logger.LogInformation("Wiped {DeletedMessages} messages", deletedMessages);

_logger.LogInformation("Posting weekly schedule");

var postedHeader = false;
var postedFooter = false;

foreach (var (day, events) in eventsByDay)
{
var embed = new EmbedBuilder();
var description = new StringBuilder();

if (!postedHeader && day == eventsByDay.First().Key)
{
embed
.WithThumbnailUrl(_client.CurrentUser.GetAvatarUrl())
.WithAuthor("Weekly event schedule", _client.CurrentUser.GetAvatarUrl());
postedHeader = true;
}

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}";

if (location is "Unknown" && guildEvent.ChannelId is not null)
{
location = "Discord";
}

description.AppendLine($"- [{location} - {guildEvent.Name}]({url})");
description.AppendLine($" - {guildEvent.StartTime.ToDiscordTimestamp(TimestampTagStyles.ShortTime)} - {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
.WithVersionFooter()
.WithCurrentTimestamp();

postedFooter = true;
}

embed
.WithColor(new Color(255, 43, 241))
.WithDescription(description.ToString());

await weeklyScheduleChannel.SendMessageAsync(embed: embed.Build());

_logger.LogInformation("Finished posting weekly schedule");
}
}

[LoggerMessage(EventId = 1, Level = LogLevel.Error, Message = "Exception occurred")]
public partial void LogError(Exception e);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Miha.Discord.Services;
namespace Miha.Discord.Services.Hosted;

public class InteractionHandler : DiscordClientService
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using Miha.Shared;
using SlimMessageBus;

namespace Miha.Discord.Services;
namespace Miha.Discord.Services.Hosted;

public class SlimMessageBusService : DiscordClientService
{
Expand Down
Loading

0 comments on commit 5597876

Please sign in to comment.