diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs new file mode 100644 index 0000000..8fe9932 --- /dev/null +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs @@ -0,0 +1,94 @@ +using Catalogger.Backend.Cache; +using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Extensions; +using Catalogger.Backend.Services; +using Humanizer; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Catalogger.Backend.Bot.Responders.Guilds; + +public class GuildMemberAddResponder( + ILogger logger, + DatabaseContext db, + IMemberCache memberCache, + WebhookExecutorService webhookExecutor, + IDiscordRestGuildAPI guildApi, + PluralkitApiService pluralkitApi) : IResponder +{ + private readonly ILogger _logger = logger.ForContext(); + private static readonly TimeSpan NewAccountThreshold = 7.Days(); + + public async Task RespondAsync(IGuildMemberAdd member, CancellationToken ct = default) + { + await memberCache.SetAsync(member.GuildID, member); + + var user = member.User.GetOrThrow(); + + var builder = new EmbedBuilder() + .WithTitle("Member joined") + .WithColour(DiscordUtils.Green) + .WithAuthor(user.Tag(), null, user.AvatarUrl()) + .WithDescription($"<@{user.ID}>") + .WithCurrentTimestamp() + .WithFooter($"ID: {user.ID}"); + + var guildConfig = await db.GetGuildAsync(member.GuildID, ct); + var guildRes = await guildApi.GetGuildAsync(member.GuildID, withCounts: true, ct); + if (guildRes.IsSuccess) + builder.Description += $"\n{guildRes.Entity.ApproximateMemberCount.Value.Ordinalize()} to join"; + + builder.Description += $"\ncreated "; + + var pkSystem = await pluralkitApi.GetPluralKitSystemAsync(user.ID.Value, ct); + if (pkSystem != null) + { + var createdAt = pkSystem.Created != null + ? $"" + : "*(unknown)*"; + builder.AddField("PluralKit system", $""" + **ID:** {pkSystem.Id} (`{pkSystem.Uuid}`) + **Name:** {pkSystem.Name ?? "*(none)*"} + **Tag:** {pkSystem.Tag ?? "*(none)*"} + **Created:** {createdAt} + """); + } + + // TODO: find used invite + + List embeds = [builder.Build().GetOrThrow()]; + + if (user.ID.Timestamp > DateTimeOffset.Now - NewAccountThreshold) + { + embeds.Add(new EmbedBuilder().WithTitle("New account") + .WithDescription($"\u26a0\ufe0f Created ").Build() + .GetOrThrow()); + } + + if (pkSystem != null) + { + if (guildConfig.BannedSystems.Contains(pkSystem.Id) || + guildConfig.BannedSystems.Contains(pkSystem.Uuid.ToString())) + { + embeds.Add(new EmbedBuilder().WithTitle("Banned system") + .WithDescription( + "\u26a0\ufe0f The system associated with this account has been banned from the server.") + .WithColour(DiscordUtils.Red) + .WithFooter($"ID: {pkSystem.Id}") + .Build() + .GetOrThrow()); + } + } + + if (embeds.Count > 1) + await webhookExecutor.SendLogWithAttachmentsAsync(guildConfig.Channels.GuildMemberAdd, embeds, []); + else await webhookExecutor.QueueLogAsync(guildConfig.Channels.GuildMemberAdd, embeds[0]); + + return Result.Success; + } +} \ No newline at end of file diff --git a/Catalogger.Backend/Services/PluralkitApiService.cs b/Catalogger.Backend/Services/PluralkitApiService.cs index 79e342f..cc476d2 100644 --- a/Catalogger.Backend/Services/PluralkitApiService.cs +++ b/Catalogger.Backend/Services/PluralkitApiService.cs @@ -4,7 +4,6 @@ using System.Threading.RateLimiting; using Humanizer; using NodaTime; using Polly; -using Remora.Rest.Json.Policies; namespace Catalogger.Backend.Services; @@ -33,7 +32,7 @@ public class PluralkitApiService(ILogger logger) _logger.Debug("Requesting {Path} from PluralKit API", path); - var resp = await _client.SendAsync(req, ct); + var resp = await _pipeline.ExecuteAsync(async ct2 => await _client.SendAsync(req, ct2), ct); if (resp.StatusCode == HttpStatusCode.NotFound && allowNotFound) { _logger.Debug("PluralKit API path {Path} returned 404 but 404 response is valid", path); @@ -48,15 +47,15 @@ public class PluralkitApiService(ILogger logger) } return await resp.Content.ReadFromJsonAsync(new JsonSerializerOptions - { PropertyNamingPolicy = new SnakeCaseNamingPolicy() }, ct) ?? + { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }, ct) ?? throw new CataloggerError("JSON response from PluralKit API was null"); } public async Task GetPluralKitMessageAsync(ulong id, CancellationToken ct = default) => await DoRequestAsync($"/messages/{id}", allowNotFound: true, ct); - public async Task GetPluralKitSystemAsync(ulong id, CancellationToken ct = default) => - (await DoRequestAsync($"/systems/{id}", allowNotFound: false, ct))!; + public async Task GetPluralKitSystemAsync(ulong id, CancellationToken ct = default) => + await DoRequestAsync($"/systems/{id}", allowNotFound: true, ct); public record PkMessage( ulong Id, diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index f278e77..b4a58aa 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -5,6 +5,7 @@ using Catalogger.Backend.Extensions; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; using Remora.Rest.Core; using Guild = Catalogger.Backend.Database.Models.Guild; @@ -54,6 +55,8 @@ 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; @@ -88,15 +91,17 @@ public class WebhookExecutorService( embeds: embeds, username: _selfUser!.Username, avatarUrl: _selfUser.AvatarUrl()); } - public async Task SendLogWithAttachmentsAsync(ulong channelId, IEmbed embed, IEnumerable files) + public async Task SendLogWithAttachmentsAsync(ulong channelId, List embeds, IEnumerable files) { + if (channelId == 0) return; + var attachments = files .Select>(f => f) .ToList(); var webhook = await webhookCache.GetOrFetchWebhookAsync(channelId, id => FetchWebhookAsync(id)); await webhookApi.ExecuteWebhookAsync(DiscordSnowflake.New(webhook.Id), webhook.Token, shouldWait: false, - embeds: new List([embed]), attachments: attachments, username: _selfUser!.Username, + embeds: embeds, attachments: attachments, username: _selfUser!.Username, avatarUrl: _selfUser.AvatarUrl()); }