feat: role delete logging, used invite logging, also some random changes
This commit is contained in:
parent
4f54077c68
commit
c906a4d6b6
18 changed files with 386 additions and 76 deletions
|
|
@ -1,5 +1,4 @@
|
|||
using Catalogger.Backend.Cache.InMemoryCache;
|
||||
using Newtonsoft.Json;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Catalogger.Backend.Database.Queries;
|
|||
using Catalogger.Backend.Extensions;
|
||||
using Catalogger.Backend.Services;
|
||||
using Humanizer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Remora.Discord.API;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
|
|
@ -20,6 +21,7 @@ public class GuildMemberAddResponder(
|
|||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
IMemberCache memberCache,
|
||||
IInviteCache inviteCache,
|
||||
UserCache userCache,
|
||||
WebhookExecutorService webhookExecutor,
|
||||
IDiscordRestGuildAPI guildApi,
|
||||
|
|
@ -70,7 +72,62 @@ public class GuildMemberAddResponder(
|
|||
);
|
||||
}
|
||||
|
||||
// TODO: find used invite
|
||||
// Bots don't use invites, so the entire following block is useless
|
||||
if (user.IsBot.OrDefault())
|
||||
{
|
||||
goto afterInvite;
|
||||
}
|
||||
|
||||
var existingInvites = (await inviteCache.TryGetAsync(member.GuildID)).ToList();
|
||||
var newInvitesRes = await guildApi.GetGuildInvitesAsync(member.GuildID, ct);
|
||||
if (!newInvitesRes.IsSuccess)
|
||||
{
|
||||
_logger.Error(
|
||||
"Could not fetch invites for guild {GuildId}: {Error}",
|
||||
member.GuildID,
|
||||
newInvitesRes.Error
|
||||
);
|
||||
|
||||
goto afterInvite;
|
||||
}
|
||||
|
||||
// Update the invite cache immediately--we've already fetched a copy of the invites, after all
|
||||
await inviteCache.SetAsync(member.GuildID, newInvitesRes.Entity);
|
||||
|
||||
// If we can't find a used invite, and the guild has a vanity link, that invite was used.
|
||||
// Otherwise, we give up
|
||||
var usedInvite = FindUsedInvite(existingInvites, newInvitesRes.Entity);
|
||||
if (usedInvite == null)
|
||||
{
|
||||
builder.AddField(
|
||||
"Invite used",
|
||||
guildRes is { IsSuccess: true, Entity.VanityUrlCode: not null }
|
||||
? $"Vanity invite (`{guildRes.Entity.VanityUrlCode}`)"
|
||||
: "*Could not determine invite, sorry.*"
|
||||
);
|
||||
|
||||
goto afterInvite;
|
||||
}
|
||||
|
||||
var inviteName =
|
||||
await db
|
||||
.Invites.Where(i => i.Code == usedInvite.Code && i.GuildId == member.GuildID.Value)
|
||||
.Select(i => i.Name)
|
||||
.FirstOrDefaultAsync(ct) ?? "*(unnamed)*";
|
||||
|
||||
var inviteDescription = $"""
|
||||
**Code:** {usedInvite.Code}
|
||||
**Name:** {inviteName}
|
||||
**Uses:** {usedInvite.Uses}
|
||||
**Created at:** <t:{usedInvite.CreatedAt.ToUnixTimeSeconds()}>
|
||||
""";
|
||||
|
||||
if (usedInvite.Inviter.IsDefined(out var inviter))
|
||||
inviteDescription += $"\n**Created by:** {inviter.Tag()} <@{inviter.ID}>";
|
||||
|
||||
builder.AddField("Invite used", inviteDescription);
|
||||
|
||||
afterInvite:
|
||||
|
||||
List<Embed> embeds = [builder.Build().GetOrThrow()];
|
||||
|
||||
|
|
@ -145,4 +202,32 @@ public class GuildMemberAddResponder(
|
|||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static IInviteWithMetadata? FindUsedInvite(
|
||||
List<IInviteWithMetadata> existingInvites,
|
||||
IReadOnlyList<IInviteWithMetadata> newInvites
|
||||
)
|
||||
{
|
||||
// First, we check all invites in *both* lists, and look for one that has more uses now than before.
|
||||
// If one matches that, it's probably the used invite.
|
||||
var usedInvite = existingInvites.FirstOrDefault(e =>
|
||||
newInvites.Any(n => e.Code == n.Code && e.Uses < n.Uses)
|
||||
);
|
||||
if (usedInvite != null)
|
||||
return usedInvite;
|
||||
|
||||
// Then we check all new invites (i.e. ones that don't exist in the old list, but do in the new one)
|
||||
// and check for one that has one use.
|
||||
usedInvite = newInvites.FirstOrDefault(n =>
|
||||
existingInvites.All(e => e.Code != n.Code) && n.Uses == 1
|
||||
);
|
||||
if (usedInvite != null)
|
||||
return usedInvite;
|
||||
|
||||
// Finally, we check invites that exist in the old list but not the new one, and were one use away from expiry.
|
||||
// If one matches, we can safely say it was the used invite.
|
||||
return existingInvites.FirstOrDefault(e =>
|
||||
newInvites.All(n => n.Code != e.Code) && e.MaxUses == e.Uses - 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ public class GuildMemberRemoveResponder(
|
|||
DatabaseContext db,
|
||||
IMemberCache memberCache,
|
||||
RoleCache roleCache,
|
||||
WebhookExecutorService webhookExecutor,
|
||||
AuditLogEnrichedResponderService auditLogEnrichedResponderService
|
||||
WebhookExecutorService webhookExecutor
|
||||
) : IResponder<IGuildMemberRemove>
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<GuildMemberAddResponder>();
|
||||
|
|
@ -26,9 +25,6 @@ public class GuildMemberRemoveResponder(
|
|||
{
|
||||
try
|
||||
{
|
||||
// spin events that Discord doesn't send us all the data for off to another responder
|
||||
_ = auditLogEnrichedResponderService.RespondAsync(evt);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithTitle("Member left")
|
||||
.WithAuthor(evt.User.Tag(), iconUrl: evt.User.AvatarUrl())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
using Catalogger.Backend.Cache;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Catalogger.Backend.Bot.Responders.MemberUpdate;
|
||||
|
||||
public class GuildMemberUpdateResponder(ILogger logger, IMemberCache memberCache)
|
||||
: IResponder<IGuildMemberUpdate>
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<GuildMemberUpdateResponder>();
|
||||
|
||||
public async Task<Result> RespondAsync(
|
||||
IGuildMemberUpdate newMember,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var oldMember = await memberCache.TryGetAsync(newMember.GuildID, newMember.User.ID);
|
||||
if (oldMember == null)
|
||||
{
|
||||
_logger.Information(
|
||||
"Received member update event for {MemberId} in {GuildId} but they weren't cached, ignoring",
|
||||
newMember.User.ID,
|
||||
newMember.GuildID
|
||||
);
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await memberCache.UpdateAsync(newMember);
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
using Catalogger.Backend.Cache.InMemoryCache;
|
||||
using Catalogger.Backend.Database;
|
||||
using Catalogger.Backend.Database.Queries;
|
||||
using Catalogger.Backend.Extensions;
|
||||
using Catalogger.Backend.Services;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Catalogger.Backend.Bot.Responders.Roles;
|
||||
|
||||
public class RoleDeleteResponder(
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
RoleCache roleCache,
|
||||
WebhookExecutorService webhookExecutor
|
||||
) : IResponder<IGuildRoleDelete>
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<RoleDeleteResponder>();
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildRoleDelete evt, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!roleCache.TryGet(evt.RoleID, out var role))
|
||||
{
|
||||
_logger.Information(
|
||||
"Received role delete event for {RoleId} but it wasn't cached, ignoring",
|
||||
evt.RoleID
|
||||
);
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var guildConfig = await db.GetGuildAsync(evt.GuildID, ct);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithTitle($"Role \"{role.Name}\" deleted")
|
||||
.WithColour(DiscordUtils.Red)
|
||||
.WithFooter($"ID: {role.ID}")
|
||||
.WithCurrentTimestamp()
|
||||
.WithDescription(
|
||||
$"""
|
||||
**Name:** {role.Name}
|
||||
**Colour:** {role.Colour.ToPrettyString()}
|
||||
**Mentionable:** {role.IsMentionable}
|
||||
**Shown separately:** {role.IsHoisted}
|
||||
**Position:** {role.Position}
|
||||
Created <t:{role.ID.Timestamp.ToUnixTimeSeconds()}> ({role.ID.Timestamp.Prettify()} ago)
|
||||
"""
|
||||
);
|
||||
|
||||
if (!role.Permissions.Value.IsZero)
|
||||
{
|
||||
embed.AddField("Permissions", role.Permissions.ToPrettyString());
|
||||
}
|
||||
|
||||
webhookExecutor.QueueLog(
|
||||
guildConfig,
|
||||
LogChannelType.GuildRoleDelete,
|
||||
embed.Build().GetOrThrow()
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
roleCache.Remove(evt.RoleID, evt.GuildID, out _);
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
|
@ -78,10 +78,7 @@ public class RoleUpdateResponder(
|
|||
// All updates are shown in fields. If there are no fields, there were no updates we care about
|
||||
// (we don't care about position, for example, because it's not actually useful)
|
||||
if (embed.Fields.Count == 0)
|
||||
{
|
||||
_logger.Debug("We don't care about update of role {RoleId}, ignoring", evt.Role.ID);
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var guildConfig = await db.GetGuildAsync(evt.GuildID, ct);
|
||||
webhookExecutor.QueueLog(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue