diff --git a/Commands/ApplicationStart/StartInstar.sh b/Commands/ApplicationStart/StartInstar.sh index b7086be..33b211a 100644 --- a/Commands/ApplicationStart/StartInstar.sh +++ b/Commands/ApplicationStart/StartInstar.sh @@ -12,7 +12,7 @@ startInstar() { local INSTAR_CONF="/Instar/bin/Config/$INSTAR_CONF_FILE" - /Instar/bin/InstarBot --config-path "$INSTAR_CONF" > /dev/null 2> /dev/null < /dev/null & + /Instar/bin/InstarBot --config-path "$INSTAR_CONF" --level Debug > /dev/null 2> /dev/null < /dev/null & } startInstar \ No newline at end of file diff --git a/InstarBot.Tests.Common/EmbedVerifier.cs b/InstarBot.Tests.Common/EmbedVerifier.cs index f22f65c..0ed5863 100644 --- a/InstarBot.Tests.Common/EmbedVerifier.cs +++ b/InstarBot.Tests.Common/EmbedVerifier.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using Discord; using Serilog; +using Xunit; namespace InstarBot.Tests; diff --git a/InstarBot.Tests.Common/InstarBot.Tests.Common.csproj b/InstarBot.Tests.Common/InstarBot.Tests.Common.csproj index 6e8fa45..fb541f3 100644 --- a/InstarBot.Tests.Common/InstarBot.Tests.Common.csproj +++ b/InstarBot.Tests.Common/InstarBot.Tests.Common.csproj @@ -7,20 +7,20 @@ InstarBot.Tests false false + Exe + - - - all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/InstarBot.Tests.Integration/Assembly.cs b/InstarBot.Tests.Integration/Assembly.cs deleted file mode 100644 index 4941ec7..0000000 --- a/InstarBot.Tests.Integration/Assembly.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: Parallelize] \ No newline at end of file diff --git a/InstarBot.Tests.Integration/InstarBot.Tests.Integration.csproj b/InstarBot.Tests.Integration/InstarBot.Tests.Integration.csproj index 3021c4c..7a51cf5 100644 --- a/InstarBot.Tests.Integration/InstarBot.Tests.Integration.csproj +++ b/InstarBot.Tests.Integration/InstarBot.Tests.Integration.csproj @@ -4,20 +4,20 @@ net10.0 enable enable + Exe + - - - all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/InstarBot.Tests.Integration/Interactions/AutoMemberSystemCommandTests.cs b/InstarBot.Tests.Integration/Interactions/AutoMemberSystemCommandTests.cs index 0c5ef3e..4cb0fd8 100644 --- a/InstarBot.Tests.Integration/Interactions/AutoMemberSystemCommandTests.cs +++ b/InstarBot.Tests.Integration/Interactions/AutoMemberSystemCommandTests.cs @@ -149,7 +149,7 @@ await tds.CreateNotificationAsync(new Notification // There is a potential asynchronous delay here, so let's keep waiting for this condition for 5 seconds. await Task.WhenAny( - Task.Delay(5000), + Task.Delay(5000, TestContext.Current.CancellationToken), Task.Factory.StartNew(async () => { while (true) @@ -160,7 +160,7 @@ await Task.WhenAny( // only poll once every 50ms await Task.Delay(50); } - })); + }, TestContext.Current.CancellationToken)); } [Fact] diff --git a/InstarBot.Tests.Integration/Interactions/PageCommandTests.cs b/InstarBot.Tests.Integration/Interactions/PageCommandTests.cs index 6550506..96319f0 100644 --- a/InstarBot.Tests.Integration/Interactions/PageCommandTests.cs +++ b/InstarBot.Tests.Integration/Interactions/PageCommandTests.cs @@ -14,7 +14,7 @@ public static class PageCommandTests { private const string TestReason = "Test reason for paging"; - private static async Task SetupOrchestrator(PageCommandTestContext context) + private static async Task SetupOrchestrator(PageCommandTestContext context) { var orchestrator = TestOrchestrator.Default; diff --git a/InstarBot.Tests.Integration/Interactions/PingCommandTests.cs b/InstarBot.Tests.Integration/Interactions/PingCommandTests.cs index de207af..542116b 100644 --- a/InstarBot.Tests.Integration/Interactions/PingCommandTests.cs +++ b/InstarBot.Tests.Integration/Interactions/PingCommandTests.cs @@ -6,13 +6,13 @@ namespace InstarBot.Tests.Integration.Interactions; public static class PingCommandTests { - /// - /// Tests that the ping command emits an ephemeral "Pong!" response. - /// - /// This test verifies that calling the ping command results in the - /// expected ephemeral "Pong!" response. - /// - [Fact(DisplayName = "User should be able to issue the Ping command.")] + /// + /// Tests that the ping command emits an ephemeral "Pong!" response. + /// + /// This test verifies that calling the ping command results in the + /// expected ephemeral "Pong!" response. + /// + [Fact(DisplayName = "User should be able to issue the Ping command.")] public static async Task PingCommand_Send_ShouldEmitEphemeralPong() { // Arrange diff --git a/InstarBot.Tests.Integration/Services/BirthdaySystemTests.cs b/InstarBot.Tests.Integration/Services/BirthdaySystemTests.cs index 6bb96e4..0ac5765 100644 --- a/InstarBot.Tests.Integration/Services/BirthdaySystemTests.cs +++ b/InstarBot.Tests.Integration/Services/BirthdaySystemTests.cs @@ -4,6 +4,7 @@ using Moq; using PaxAndromeda.Instar; using PaxAndromeda.Instar.Services; +using Serilog; using Xunit; using Metric = PaxAndromeda.Instar.Metrics.Metric; @@ -68,7 +69,7 @@ public static async Task BirthdaySystem_WhenUserBirthday_ShouldGrantRole() var channel = await orchestrator.Discord.GetChannel(orchestrator.Configuration.BirthdayConfig.BirthdayAnnounceChannel) as TestChannel; channel.Should().NotBeNull(); - var messages = await channel.GetMessagesAsync().SelectMany(n => n).ToListAsync(); + var messages = await channel.GetMessagesAsync().SelectMany(n => n).ToListAsync(TestContext.Current.CancellationToken); messages.Count.Should().BeGreaterThan(0); TestUtilities.MatchesFormat(Strings.Birthday_Announcement, messages[0].Content); @@ -153,7 +154,7 @@ public static async Task BirthdaySystem_WhenNoBirthdays_ShouldDoNothing() var channel = await orchestrator.Discord.GetChannel(orchestrator.Configuration.BirthdayConfig.BirthdayAnnounceChannel) as TestChannel; channel.Should().NotBeNull(); - var messages = await channel.GetMessagesAsync().SelectMany(n => n).ToListAsync(); + var messages = await channel.GetMessagesAsync().SelectMany(n => n).ToListAsync(TestContext.Current.CancellationToken); messages.Count.Should().Be(0); } @@ -210,8 +211,8 @@ public static async Task BirthdaySystem_WithBirthdayRoleButNoBirthdayRecord_Shou public static async Task BirthdaySystem_WithUserBirthdayStill_ShouldKeepBirthdayRoles() { // Arrange - var birthday = DateTime.Parse("2000-02-13T12:00:00Z"); - var currentTime = DateTime.Parse("2025-02-14T00:00:00Z"); + var birthday = DateTimeOffset.Parse("2000-02-13T00:00:00-08:00"); + var currentTime = DateTimeOffset.Parse("2025-02-14T00:00:00Z"); var orchestrator = await SetupOrchestrator(currentTime, birthday); var system = orchestrator.GetService(); diff --git a/InstarBot.Tests.Orchestrator/InstarBot.Test.Framework.csproj b/InstarBot.Tests.Orchestrator/InstarBot.Test.Framework.csproj index 76bb766..d61f75d 100644 --- a/InstarBot.Tests.Orchestrator/InstarBot.Test.Framework.csproj +++ b/InstarBot.Tests.Orchestrator/InstarBot.Test.Framework.csproj @@ -4,8 +4,15 @@ net10.0 enable enable + Exe + + + + + + diff --git a/InstarBot.Tests.Orchestrator/TestOrchestrator.cs b/InstarBot.Tests.Orchestrator/TestOrchestrator.cs index 5db5933..9e59670 100644 --- a/InstarBot.Tests.Orchestrator/TestOrchestrator.cs +++ b/InstarBot.Tests.Orchestrator/TestOrchestrator.cs @@ -12,6 +12,8 @@ using Serilog.Events; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +using Serilog.Sinks.XUnit3; +using Xunit; using InvalidOperationException = Amazon.CloudWatchLogs.Model.InvalidOperationException; namespace InstarBot.Test.Framework @@ -109,12 +111,13 @@ private void InitializeActor() tdbs.CreateUserAsync(InstarUserData.CreateFrom(user)).Wait(); } - private static void SetupLogging() + public static void SetupLogging() { Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .MinimumLevel.Is(LogEventLevel.Verbose) .WriteTo.Console() + .WriteTo.XUnit3TestOutput() .CreateLogger(); Log.Warning("Logging is enabled for this unit test."); } diff --git a/InstarBot.Tests.Unit/Assembly.cs b/InstarBot.Tests.Unit/Assembly.cs deleted file mode 100644 index 5d68f78..0000000 --- a/InstarBot.Tests.Unit/Assembly.cs +++ /dev/null @@ -1,3 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -[assembly: Parallelize] \ No newline at end of file diff --git a/InstarBot.Tests.Unit/AsyncAutoResetEventTests.cs b/InstarBot.Tests.Unit/AsyncAutoResetEventTests.cs index de0ac0c..933ee7d 100644 --- a/InstarBot.Tests.Unit/AsyncAutoResetEventTests.cs +++ b/InstarBot.Tests.Unit/AsyncAutoResetEventTests.cs @@ -15,7 +15,7 @@ public void WaitAsync_CompletesImmediately_WhenInitiallySignaled() var ev = new AsyncAutoResetEvent(true); // Act - var task = ev.WaitAsync(); + var task = ev.WaitAsync(TestContext.Current.CancellationToken); // Assert task.IsCompleted.Should().BeTrue(); @@ -26,8 +26,8 @@ public void Set_ReleasesSingleWaiter() { var ev = new AsyncAutoResetEvent(false); - var waiter1 = ev.WaitAsync(); - var waiter2 = ev.WaitAsync(); + var waiter1 = ev.WaitAsync(TestContext.Current.CancellationToken); + var waiter2 = ev.WaitAsync(TestContext.Current.CancellationToken); // First Set should release only one waiter. ev.Set(); @@ -49,13 +49,13 @@ public void Set_MarksEventSignaled_WhenNoWaiters() // No waiters now — Set should mark the event signaled so the next WaitAsync completes immediately. ev.Set(); - var immediate = ev.WaitAsync(); + var immediate = ev.WaitAsync(TestContext.Current.CancellationToken); // WaitAsync should complete immediately after Set() when there were no waiters immediate.IsCompleted.Should().BeTrue(); // That consumption should reset the event; a subsequent waiter should block. - var next = ev.WaitAsync(); + var next = ev.WaitAsync(TestContext.Current.CancellationToken); next.IsCompleted.Should().BeFalse(); } diff --git a/InstarBot.Tests.Unit/BirthdayTests.cs b/InstarBot.Tests.Unit/BirthdayTests.cs index b173e57..3b33abf 100644 --- a/InstarBot.Tests.Unit/BirthdayTests.cs +++ b/InstarBot.Tests.Unit/BirthdayTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using FluentAssertions; using Moq; using PaxAndromeda.Instar; diff --git a/InstarBot.Tests.Unit/InstarBot.Tests.Unit.csproj b/InstarBot.Tests.Unit/InstarBot.Tests.Unit.csproj index 4b438cd..92bf113 100644 --- a/InstarBot.Tests.Unit/InstarBot.Tests.Unit.csproj +++ b/InstarBot.Tests.Unit/InstarBot.Tests.Unit.csproj @@ -7,16 +7,15 @@ false InstarBot.Tests + Exe + - - - runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -25,6 +24,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/InstarBot/Birthday.cs b/InstarBot/Birthday.cs index fa86da9..163b7df 100644 --- a/InstarBot/Birthday.cs +++ b/InstarBot/Birthday.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using Serilog; namespace PaxAndromeda.Instar; @@ -94,7 +95,7 @@ public bool IsToday var currentLocalTime = dtNow.ToOffset(utcOffset); var localTimeToday = new DateTimeOffset(currentLocalTime.Date, currentLocalTime.Offset); - var localTimeTomorrow = localTimeToday.Date.AddDays(1); + var localTimeTomorrow = localTimeToday.AddDays(1); return Observed >= localTimeToday && Observed < localTimeTomorrow; } diff --git a/InstarBot/Commands/SetBirthdayCommand.cs b/InstarBot/Commands/SetBirthdayCommand.cs index 0d34ca5..072ceda 100644 --- a/InstarBot/Commands/SetBirthdayCommand.cs +++ b/InstarBot/Commands/SetBirthdayCommand.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Discord; using Discord.Interactions; using Discord.WebSocket; diff --git a/InstarBot/ConfigModels/AutoMemberConfig.cs b/InstarBot/ConfigModels/AutoMemberConfig.cs index fa94058..8be803f 100644 --- a/InstarBot/ConfigModels/AutoMemberConfig.cs +++ b/InstarBot/ConfigModels/AutoMemberConfig.cs @@ -14,7 +14,8 @@ public sealed class AutoMemberConfig public int MinimumMessages { get; init; } public int MinimumMessageTime { get; init; } public List RequiredRoles { get; init; } = null!; - public bool EnableGaiusCheck { get; init; } + public bool EnableGaiusCheck { get; init; } + public List AllowedPunishmentReasons { get; init; } = []; } [UsedImplicitly] diff --git a/InstarBot/InstarBot.csproj b/InstarBot/InstarBot.csproj index 19d97bf..603c8c3 100644 --- a/InstarBot/InstarBot.csproj +++ b/InstarBot/InstarBot.csproj @@ -22,6 +22,7 @@ + diff --git a/InstarBot/Modals/UserUpdatedEventArgs.cs b/InstarBot/Modals/UserUpdatedEventArgs.cs index 0c0a2e1..dd462f7 100644 --- a/InstarBot/Modals/UserUpdatedEventArgs.cs +++ b/InstarBot/Modals/UserUpdatedEventArgs.cs @@ -2,18 +2,18 @@ namespace PaxAndromeda.Instar.Modals; -public class UserUpdatedEventArgs(Snowflake id, IGuildUser before, IGuildUser after) +public class UserUpdatedEventArgs(Snowflake id, IUser before, IUser after) { public Snowflake ID { get; } = id; - public IGuildUser Before { get; } = before; + public IUser Before { get; } = before; - public IGuildUser After { get; } = after; + public IUser After { get; } = after; // TODO: add additional parts to this for the data we care about /// /// A flag indicating whether data we care about (e.g. nicknames, usernames) has changed. /// - public bool HasUpdated => Before.Username != After.Username || Before.Nickname != After.Nickname; + public bool HasUpdated => Before.Username != After.Username || (Before is IGuildUser gBefore && After is IGuildUser gAfter && gBefore.Nickname != gAfter.Nickname); } \ No newline at end of file diff --git a/InstarBot/Services/AutoMemberSystem.cs b/InstarBot/Services/AutoMemberSystem.cs index a367fa5..a094ce1 100644 --- a/InstarBot/Services/AutoMemberSystem.cs +++ b/InstarBot/Services/AutoMemberSystem.cs @@ -64,22 +64,23 @@ internal override async Task Initialize() await PreloadGaiusPunishments(); } - /// - /// Filters warnings and caselogs to only focus on the ones we care about. For example, we don't - /// want to withhold membership from someone who was kicked for having a too new Discord account. - /// - /// A collection of warnings retrieved from the Gaius API. - /// A collection of caselogs retrieved from the Gaius API. - /// A tuple containing the filtered warnings and caselogs, respectively. - /// If or is null. - private static (IEnumerable, IEnumerable) FilterPunishments(IEnumerable warnings, IEnumerable caselogs) + /// + /// Filters warnings and caselogs to only focus on the ones we care about. For example, we don't + /// want to withhold membership from someone who was kicked for having a too new Discord account. + /// + /// The current configuration version in use. + /// A collection of warnings retrieved from the Gaius API. + /// A collection of caselogs retrieved from the Gaius API. + /// A tuple containing the filtered warnings and caselogs, respectively. + /// If or is null. + private static (IEnumerable, IEnumerable) FilterPunishments(InstarDynamicConfiguration cfg, IEnumerable warnings, IEnumerable caselogs) { if (warnings is null) throw new ArgumentNullException(nameof(warnings)); if (caselogs is null) throw new ArgumentNullException(nameof(caselogs)); - - var filteredCaselogs = caselogs.Where(n => n is not { Type: CaselogType.Kick, Reason: "Join age punishment" }); + + var filteredCaselogs = caselogs.Where(n => n.Type != CaselogType.Kick && !cfg.AutoMemberConfig.AllowedPunishmentReasons.Contains(n.Reason)); return (warnings, filteredCaselogs); } @@ -92,9 +93,11 @@ private async Task UpdateGaiusPunishments() // bias for an hour and a half ago. var afterTime = _timeProvider.GetUtcNow().UtcDateTime - TimeSpan.FromHours(1.5); + var cfg = await _dynamicConfig.GetConfig(); + try { - var (warnings, caselogs) = FilterPunishments( + var (warnings, caselogs) = FilterPunishments(cfg, await _gaiusApiService.GetWarningsAfter(afterTime), await _gaiusApiService.GetCaselogsAfter(afterTime)); @@ -192,13 +195,17 @@ private async Task HandleUserUpdated(UserUpdatedEventArgs arg) if (!arg.HasUpdated) return; + if (arg.After is not IGuildUser gAfter) + return; + var user = await _ddbService.GetUserAsync(arg.ID); if (user is null) { + // new user for the database, create from the latest data and return try { - await _ddbService.CreateUserAsync(InstarUserData.CreateFrom(arg.After)); + await _ddbService.CreateUserAsync(InstarUserData.CreateFrom(gAfter)); Log.Information("Created new user {Username} (user ID {UserID})", arg.After.Username, arg.ID); } catch (Exception ex) @@ -217,10 +224,13 @@ private async Task HandleUserUpdated(UserUpdatedEventArgs arg) user.Data.Username = arg.After.Username; changed = true; } + + if (arg.Before is not IGuildUser gBefore) + return; - if (arg.Before.Nickname != arg.After.Nickname) + if (gBefore.Nickname != gAfter.Nickname) { - user.Data.Nicknames?.Add(new InstarUserDataHistoricalEntry(_timeProvider.GetUtcNow().UtcDateTime, arg.After.Nickname)); + user.Data.Nicknames?.Add(new InstarUserDataHistoricalEntry(_timeProvider.GetUtcNow().UtcDateTime, gAfter.Nickname)); changed = true; } @@ -230,7 +240,7 @@ private async Task HandleUserUpdated(UserUpdatedEventArgs arg) await user.CommitAsync(); } - await HandleAutoKickRoles(arg.After); + await HandleAutoKickRoles(gAfter); } private async Task HandleAutoKickRoles(IGuildUser user, InstarDynamicConfiguration? cfg = null) @@ -473,9 +483,11 @@ private Dictionary GetMessagesSent() private async Task PreloadGaiusPunishments() { + var cfg = await _dynamicConfig.GetConfig(); + try { - var (warnings, caselogs) = FilterPunishments( + var (warnings, caselogs) = FilterPunishments(cfg, await _gaiusApiService.GetAllWarnings(), await _gaiusApiService.GetAllCaselogs()); diff --git a/InstarBot/Services/BirthdaySystem.cs b/InstarBot/Services/BirthdaySystem.cs index 8895989..6d030ef 100644 --- a/InstarBot/Services/BirthdaySystem.cs +++ b/InstarBot/Services/BirthdaySystem.cs @@ -36,6 +36,8 @@ public override async Task RunAsync() var cfg = await dynamicConfig.GetConfig(); var currentTime = _timeProvider.GetUtcNow().UtcDateTime; + await discord.SyncUsers(); + await RemoveBirthdays(cfg, currentTime); var successfulAdds = await GrantBirthdays(cfg, currentTime); @@ -97,21 +99,6 @@ private async Task RemoveBirthdays(InstarDynamicConfiguration cfg, DateTime curr toRemove.Add(user.Data.UserID!); continue; } - - var birthDate = user.Data.Birthday.Birthdate; - - var thisYearBirthday = new DateTime( - currentTime.Year, - birthDate.Month, birthDate.Day, birthDate.Hour, birthDate.Minute, 0, DateTimeKind.Utc); - - if (thisYearBirthday > currentTime) - thisYearBirthday = thisYearBirthday.AddYears(-1); - - if (currentTime - thisYearBirthday >= TimeSpan.FromDays(1)) - { - Log.Information("Removing birthday role from {UserID} due to their birthday not being today. CurrentTime={CurrentTime}, ThisYearBirthday={ThisYearBirthday}, Diff={TimeDiff}", user.Data.UserID, currentTime, thisYearBirthday, currentTime - thisYearBirthday); - toRemove.Add(user.Data.UserID!); - } } foreach (var snowflake in toRemove) diff --git a/InstarBot/Services/DiscordService.cs b/InstarBot/Services/DiscordService.cs index 3854682..e3f19f0 100644 --- a/InstarBot/Services/DiscordService.cs +++ b/InstarBot/Services/DiscordService.cs @@ -87,6 +87,7 @@ public DiscordService(IServiceProvider provider, IConfiguration config, IDynamic _socketClient.MessageDeleted += async (msgCache, _) => await _messageDeletedEvent.Invoke(msgCache.Id); _socketClient.UserJoined += async user => await _userJoinedEvent.Invoke(user); _socketClient.UserLeft += async (_, user) => await _userLeftEvent.Invoke(user); + _socketClient.UserUpdated += HandleUserUpdate; _socketClient.GuildMemberUpdated += HandleUserUpdate; _interactionService.Log += HandleDiscordLog; @@ -97,19 +98,29 @@ public DiscordService(IServiceProvider provider, IConfiguration config, IDynamic // Validate if (_guild == 0) throw new ConfigurationException("TargetGuild is not set"); - } + } - private async Task HandleUserUpdate(Cacheable before, SocketGuildUser after) + private Task HandleUserUpdate(Cacheable before, SocketGuildUser after) { // Can't do anything if we don't have a before state. // In the case of a user join, that is handled in UserJoined event. if (!before.HasValue) - return; + return Task.CompletedTask; + + return HandleUserUpdate(before.Value, after); + } - await _userUpdatedEvent.Invoke(new UserUpdatedEventArgs(after.Id, before.Value, after)); + private async Task HandleUserUpdate(SocketGuildUser before, SocketGuildUser after) + { + await _userUpdatedEvent.Invoke(new UserUpdatedEventArgs(after.Id, before, after)); + } + + private async Task HandleUserUpdate(SocketUser before, SocketUser after) + { + await _userUpdatedEvent.Invoke(new UserUpdatedEventArgs(after.Id, before, after)); } - private async Task HandleMessageCommand(SocketMessageCommand arg) + private async Task HandleMessageCommand(SocketMessageCommand arg) { Log.Information("Message command: {CommandName}", arg.CommandName); diff --git a/InstarBot/Services/NotificationService.cs b/InstarBot/Services/NotificationService.cs index e438c6e..8a6a45a 100644 --- a/InstarBot/Services/NotificationService.cs +++ b/InstarBot/Services/NotificationService.cs @@ -64,6 +64,17 @@ private async Task ProcessNotification(InstarDatabaseEntry n var actor = discordService.GetUser(notification.Data.Actor); + // Don't post a notification if the reference user (if set) is not on the server. + if (notification.Data.ReferenceUser is not null) + { + var referenceUser = discordService.GetUser(notification.Data.ReferenceUser); + if (referenceUser is null) + { + Log.Warning("Reference user {ReferenceUserId} for notification dated {NotificationDate} not found on the server; skipping notification", notification.Data.ReferenceUser, notification.Data.Date); + return true; + } + } + Log.Debug("Actor ID {UserId} name: {Username}", notification.Data.Actor.ID, actor?.Username ?? ""); var embed = new NotificationEmbed(notification.Data, actor, cfg);