diff --git a/Catalogger.Backend/Bot/Commands/MetaCommands.cs b/Catalogger.Backend/Bot/Commands/MetaCommands.cs index 91bf29e..bfd32a4 100644 --- a/Catalogger.Backend/Bot/Commands/MetaCommands.cs +++ b/Catalogger.Backend/Bot/Commands/MetaCommands.cs @@ -6,6 +6,7 @@ using System.Web; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Extensions; using Humanizer; +using Humanizer.Localisation; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -60,8 +61,14 @@ public class MetaCommands( true); embed.AddField("Numbers", - $"{CataloggerMetrics.MessagesStored.Value:N0} messages from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels", - inline: false); + $"{CataloggerMetrics.MessagesStored.Value:N0} messages " + + $"from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels", + true); + + embed.AddField("Uptime", + $"{(CataloggerMetrics.Startup - clock.GetCurrentInstant()).Prettify(TimeUnit.Second)}\n" + + $"since ", + true); IEmbed[] embeds = [embed.Build().GetOrThrow()]; @@ -80,9 +87,7 @@ public class MetaCommands( resp.EnsureSuccessStatusCode(); var data = await resp.Content.ReadFromJsonAsync(); - _logger.Debug("Raw json: {Data}", JsonSerializer.Serialize(data)); var rawNumber = (data?.data.result[0].value[1] as JsonElement?)?.GetString(); - _logger.Debug("Raw data: {Raw}", rawNumber); return double.TryParse(rawNumber, out var rate) ? rate : null; } catch (Exception e) diff --git a/Catalogger.Backend/Bot/DiscordUtils.cs b/Catalogger.Backend/Bot/DiscordUtils.cs index 2bc59a7..5120bfb 100644 --- a/Catalogger.Backend/Bot/DiscordUtils.cs +++ b/Catalogger.Backend/Bot/DiscordUtils.cs @@ -12,4 +12,5 @@ public static class DiscordUtils public static readonly Color Purple = Color.FromArgb(155, 89, 182); public static readonly Color Green = Color.FromArgb(46, 204, 113); public static readonly Color Blue = Color.FromArgb(52, 152, 219); + public static readonly Color Orange = Color.FromArgb(230, 126, 34); } \ No newline at end of file diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs index 8fe9932..604e1b7 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs @@ -1,9 +1,11 @@ using Catalogger.Backend.Cache; +using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; +using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; @@ -17,6 +19,7 @@ public class GuildMemberAddResponder( ILogger logger, DatabaseContext db, IMemberCache memberCache, + UserCache userCache, WebhookExecutorService webhookExecutor, IDiscordRestGuildAPI guildApi, PluralkitApiService pluralkitApi) : IResponder @@ -43,13 +46,14 @@ public class GuildMemberAddResponder( if (guildRes.IsSuccess) builder.Description += $"\n{guildRes.Entity.ApproximateMemberCount.Value.Ordinalize()} to join"; - builder.Description += $"\ncreated "; + builder.Description += + $"\ncreated {user.ID.Timestamp.Prettify()} ago\n"; var pkSystem = await pluralkitApi.GetPluralKitSystemAsync(user.ID.Value, ct); if (pkSystem != null) { var createdAt = pkSystem.Created != null - ? $"" + ? $"{pkSystem.Created.Value.Prettify()} ago ()" : "*(unknown)*"; builder.AddField("PluralKit system", $""" **ID:** {pkSystem.Id} (`{pkSystem.Uuid}`) @@ -65,8 +69,28 @@ public class GuildMemberAddResponder( if (user.ID.Timestamp > DateTimeOffset.Now - NewAccountThreshold) { - embeds.Add(new EmbedBuilder().WithTitle("New account") - .WithDescription($"\u26a0\ufe0f Created ").Build() + embeds.Add(new EmbedBuilder() + .WithTitle("New account") + .WithColour(DiscordUtils.Orange) + .WithDescription($"\u26a0\ufe0f Created {user.ID.Timestamp.Prettify()} ago") + .Build() + .GetOrThrow()); + } + + var watchlist = await db.GetWatchlistEntryAsync(member.GuildID, user.ID, ct); + if (watchlist != null) + { + var moderator = await userCache.GetUserAsync(DiscordSnowflake.New(watchlist.ModeratorId)); + var mod = moderator != null ? $"{moderator.Tag()} (<@{moderator.ID}>)" : $"<@{watchlist.ModeratorId}>"; + + embeds.Add(new EmbedBuilder() + .WithTitle("⚠️ User on watchlist") + .WithColour(DiscordUtils.Red) + .WithDescription($"**{user.Tag()}** is on this server's watch list.\n\n{watchlist.Reason}") + .WithFooter($"ID: {user.ID} | Added") + .WithTimestamp(watchlist.AddedAt.ToDateTimeOffset()) + .AddField("Moderator", mod).GetOrThrow() + .Build() .GetOrThrow()); } diff --git a/Catalogger.Backend/CataloggerMetrics.cs b/Catalogger.Backend/CataloggerMetrics.cs index 2d36c65..3c8edcc 100644 --- a/Catalogger.Backend/CataloggerMetrics.cs +++ b/Catalogger.Backend/CataloggerMetrics.cs @@ -1,9 +1,12 @@ +using NodaTime; using Prometheus; namespace Catalogger.Backend; public static class CataloggerMetrics { + public static Instant Startup { get; set; } + public static readonly Gauge MessagesReceived = Metrics.CreateGauge("catalogger_received_messages", "Number of messages Catalogger has received"); diff --git a/Catalogger.Backend/Config.cs b/Catalogger.Backend/Config.cs index e97e999..6ebd9aa 100644 --- a/Catalogger.Backend/Config.cs +++ b/Catalogger.Backend/Config.cs @@ -13,7 +13,7 @@ public class Config { public LogEventLevel LogEventLevel { get; init; } = LogEventLevel.Debug; public bool LogQueries { get; init; } = false; - + public int MetricsPort { get; init; } = 5001; public bool EnableMetrics { get; init; } = true; } diff --git a/Catalogger.Backend/Database/Queries/QueryExtensions.cs b/Catalogger.Backend/Database/Queries/QueryExtensions.cs index 2e907cb..a59764d 100644 --- a/Catalogger.Backend/Database/Queries/QueryExtensions.cs +++ b/Catalogger.Backend/Database/Queries/QueryExtensions.cs @@ -15,8 +15,12 @@ public static class QueryExtensions public static async ValueTask GetGuildAsync(this DatabaseContext db, ulong id, CancellationToken ct = default) { - var guild = await db.Guilds.FindAsync(id); + var guild = await db.Guilds.FindAsync([id], ct); if (guild == null) throw new CataloggerError("Guild not found, was not initialized during guild create"); return guild; } + + public static async Task GetWatchlistEntryAsync(this DatabaseContext db, Snowflake guildId, + Snowflake userId, CancellationToken ct = default) => + await db.Watchlists.FindAsync([guildId.Value, userId.Value], ct); } \ No newline at end of file diff --git a/Catalogger.Backend/Database/QueryUtils.cs b/Catalogger.Backend/Database/QueryUtils.cs deleted file mode 100644 index 1969d41..0000000 --- a/Catalogger.Backend/Database/QueryUtils.cs +++ /dev/null @@ -1 +0,0 @@ -namespace Catalogger.Backend.Database; \ No newline at end of file diff --git a/Catalogger.Backend/Extensions/StartupExtensions.cs b/Catalogger.Backend/Extensions/StartupExtensions.cs index ed893d2..483bee9 100644 --- a/Catalogger.Backend/Extensions/StartupExtensions.cs +++ b/Catalogger.Backend/Extensions/StartupExtensions.cs @@ -103,6 +103,8 @@ public static class StartupExtensions var logger = scope.ServiceProvider.GetRequiredService().ForContext(); logger.Information("Starting Catalogger.NET"); + CataloggerMetrics.Startup = scope.ServiceProvider.GetRequiredService().GetCurrentInstant(); + await using (var db = scope.ServiceProvider.GetRequiredService()) { var migrationCount = (await db.Database.GetPendingMigrationsAsync()).Count(); diff --git a/Catalogger.Backend/Extensions/TimeExtensions.cs b/Catalogger.Backend/Extensions/TimeExtensions.cs new file mode 100644 index 0000000..5721b88 --- /dev/null +++ b/Catalogger.Backend/Extensions/TimeExtensions.cs @@ -0,0 +1,20 @@ +using Humanizer; +using Humanizer.Localisation; +using NodaTime; + +namespace Catalogger.Backend.Extensions; + +public static class TimeExtensions +{ + public static string Prettify(this TimeSpan timespan, TimeUnit minUnit = TimeUnit.Minute) => + timespan.Humanize(minUnit: minUnit, precision: 5, collectionSeparator: null); + + public static string Prettify(this Duration duration, TimeUnit minUnit = TimeUnit.Minute) => + duration.ToTimeSpan().Prettify(minUnit); + + public static string Prettify(this DateTimeOffset datetime, TimeUnit minUnit = TimeUnit.Minute) => + (datetime - DateTimeOffset.Now).Prettify(minUnit); + + public static string Prettify(this Instant instant, TimeUnit minUnit = TimeUnit.Minute) => + (instant - SystemClock.Instance.GetCurrentInstant()).Prettify(minUnit); +} \ No newline at end of file diff --git a/Catalogger.Backend/Services/MetricsCollectionService.cs b/Catalogger.Backend/Services/MetricsCollectionService.cs index c7bc61f..fb42016 100644 --- a/Catalogger.Backend/Services/MetricsCollectionService.cs +++ b/Catalogger.Backend/Services/MetricsCollectionService.cs @@ -45,11 +45,11 @@ public class MetricsCollectionService( public class BackgroundMetricsCollectionService(ILogger logger, MetricsCollectionService innerService) : BackgroundService { private readonly ILogger _logger = logger.ForContext(); - + protected override async Task ExecuteAsync(CancellationToken ct) { _logger.Information("Metrics are disabled, periodically collecting metrics manually"); - + using var timer = new PeriodicTimer(1.Minutes()); while (await timer.WaitForNextTickAsync(ct)) { diff --git a/Catalogger.Backend/Services/PluralkitApiService.cs b/Catalogger.Backend/Services/PluralkitApiService.cs index cc476d2..4569856 100644 --- a/Catalogger.Backend/Services/PluralkitApiService.cs +++ b/Catalogger.Backend/Services/PluralkitApiService.cs @@ -47,7 +47,7 @@ public class PluralkitApiService(ILogger logger) } return await resp.Content.ReadFromJsonAsync(new JsonSerializerOptions - { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }, ct) ?? + { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }, ct) ?? throw new CataloggerError("JSON response from PluralKit API was null"); } diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index b4a58aa..0929c61 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -56,7 +56,7 @@ public class WebhookExecutorService( public async Task QueueLogAsync(ulong channelId, IEmbed embed) { if (channelId == 0) return; - + var queue = _cache.GetOrAdd(channelId, []); queue.Enqueue(embed); _cache[channelId] = queue;