feat: role delete logging, used invite logging, also some random changes

This commit is contained in:
sam 2024-10-09 22:31:58 +02:00
parent 4f54077c68
commit c906a4d6b6
18 changed files with 386 additions and 76 deletions

View file

@ -5,6 +5,6 @@ namespace Catalogger.Backend.Cache;
public interface IInviteCache
{
public Task<IEnumerable<IInvite>> TryGetAsync(Snowflake guildId);
public Task SetAsync(Snowflake guildId, IEnumerable<IInvite> invites);
public Task<IEnumerable<IInviteWithMetadata>> TryGetAsync(Snowflake guildId);
public Task SetAsync(Snowflake guildId, IEnumerable<IInviteWithMetadata> invites);
}

View file

@ -1,3 +1,4 @@
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Rest.Core;
@ -12,4 +13,5 @@ public interface IMemberCache
public Task<bool> IsGuildCachedAsync(Snowflake guildId);
public Task MarkAsCachedAsync(Snowflake guildId);
public Task MarkAsUncachedAsync(Snowflake guildId);
public Task UpdateAsync(IGuildMemberUpdate newMember);
}

View file

@ -6,14 +6,15 @@ namespace Catalogger.Backend.Cache.InMemoryCache;
public class InMemoryInviteCache : IInviteCache
{
private readonly ConcurrentDictionary<Snowflake, IEnumerable<IInvite>> _invites = new();
private readonly ConcurrentDictionary<Snowflake, IEnumerable<IInviteWithMetadata>> _invites =
new();
public Task<IEnumerable<IInvite>> TryGetAsync(Snowflake guildId) =>
public Task<IEnumerable<IInviteWithMetadata>> TryGetAsync(Snowflake guildId) =>
_invites.TryGetValue(guildId, out var invites)
? Task.FromResult(invites)
: Task.FromResult<IEnumerable<IInvite>>([]);
: Task.FromResult<IEnumerable<IInviteWithMetadata>>([]);
public Task SetAsync(Snowflake guildId, IEnumerable<IInvite> invites)
public Task SetAsync(Snowflake guildId, IEnumerable<IInviteWithMetadata> invites)
{
_invites[guildId] = invites;
return Task.CompletedTask;

View file

@ -1,12 +1,19 @@
using System.Collections.Concurrent;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Rest.Core;
namespace Catalogger.Backend.Cache.InMemoryCache;
public class InMemoryMemberCache : IMemberCache
public class InMemoryMemberCache(IDiscordRestGuildAPI guildApi, ILogger logger) : IMemberCache
{
private readonly ConcurrentDictionary<(Snowflake, Snowflake), IGuildMember> _members = new();
private readonly ILogger _logger = logger.ForContext<InMemoryMemberCache>();
private readonly ConcurrentDictionary<
(Snowflake GuildId, Snowflake UserId),
IGuildMember
> _members = new();
private readonly ConcurrentDictionary<Snowflake, byte> _guilds = new();
#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type.
@ -52,4 +59,56 @@ public class InMemoryMemberCache : IMemberCache
_guilds.Remove(guildId, out _);
return Task.CompletedTask;
}
public async Task UpdateAsync(IGuildMemberUpdate newMember)
{
if (!_members.TryGetValue((newMember.GuildID, newMember.User.ID), out var oldMember))
{
_logger.Warning(
"Received member update event for {MemberId} in {GuildId}, but member wasn't found in cache. Fetching them and storing.",
newMember.User.ID,
newMember.GuildID
);
var memberResult = await guildApi.GetGuildMemberAsync(
newMember.GuildID,
newMember.User.ID
);
if (!memberResult.IsSuccess)
{
_logger.Error(
"Could not get uncached member {MemberId} in {GuildId} via REST: {Error}",
newMember.User.ID,
newMember.GuildID,
memberResult.Error
);
return;
}
_members[(newMember.GuildID, newMember.User.ID)] = memberResult.Entity;
return;
}
// Unwrapping the interface because fuck it, we ball
var member = (GuildMember)oldMember with
{
User = new Optional<IUser>(newMember.User),
Nickname = newMember.Nickname.HasValue ? newMember.Nickname.Value : oldMember.Nickname,
Avatar = newMember.Avatar.HasValue ? newMember.Avatar : oldMember.Avatar,
Roles = newMember.Roles.ToArray(),
JoinedAt = newMember.JoinedAt ?? oldMember.JoinedAt,
PremiumSince = newMember.PremiumSince.HasValue
? newMember.PremiumSince.Value
: oldMember.PremiumSince,
IsPending = newMember.IsPending.HasValue
? newMember.IsPending.Value
: oldMember.IsPending,
CommunicationDisabledUntil = newMember.CommunicationDisabledUntil.HasValue
? newMember.CommunicationDisabledUntil.Value
: oldMember.CommunicationDisabledUntil,
};
_members[(newMember.GuildID, newMember.User.ID)] = member;
}
}

View file

@ -8,14 +8,14 @@ namespace Catalogger.Backend.Cache.RedisCache;
public class RedisInviteCache(RedisService redisService) : IInviteCache
{
public async Task<IEnumerable<IInvite>> TryGetAsync(Snowflake guildId)
public async Task<IEnumerable<IInviteWithMetadata>> TryGetAsync(Snowflake guildId)
{
var redisInvites =
await redisService.GetAsync<List<RedisInvite>>(InvitesKey(guildId)) ?? [];
return redisInvites.Select(r => r.ToRemoraInvite());
}
public async Task SetAsync(Snowflake guildId, IEnumerable<IInvite> invites) =>
public async Task SetAsync(Snowflake guildId, IEnumerable<IInviteWithMetadata> invites) =>
await redisService.SetAsync(InvitesKey(guildId), invites.Select(RedisInvite.FromIInvite));
private static string InvitesKey(Snowflake guildId) => $"guild-invites:{guildId}";
@ -25,24 +25,39 @@ internal record RedisInvite(
string Code,
RedisPartialGuild? Guild,
RedisPartialChannel? Channel,
int Uses,
int MaxUses,
TimeSpan MaxAge,
bool IsTemporary,
DateTimeOffset CreatedAt,
RedisUser? Inviter,
DateTimeOffset? ExpiresAt
)
{
public static RedisInvite FromIInvite(IInvite invite) =>
public static RedisInvite FromIInvite(IInviteWithMetadata invite) =>
new(
invite.Code,
invite.Guild.Map(RedisPartialGuild.FromIPartialGuild).OrDefault(),
invite.Channel != null ? RedisPartialChannel.FromIPartialChannel(invite.Channel) : null,
invite.Uses,
invite.MaxUses,
invite.MaxAge,
invite.IsTemporary,
invite.CreatedAt,
invite.Inviter.Map(RedisUser.FromIUser).OrDefault(),
invite.ExpiresAt.OrDefault()
);
public Invite ToRemoraInvite() =>
public InviteWithMetadata ToRemoraInvite() =>
new(
Code,
Guild?.ToRemoraPartialGuild() ?? new Optional<IPartialGuild>(),
Channel?.ToRemoraPartialChannel(),
Uses,
MaxUses,
MaxAge,
IsTemporary,
CreatedAt,
Inviter?.ToRemoraUser() ?? new Optional<IUser>(),
ExpiresAt: ExpiresAt
);

View file

@ -1,32 +1,46 @@
using Catalogger.Backend.Database.Redis;
using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Rest.Core;
namespace Catalogger.Backend.Cache.RedisCache;
public class RedisMemberCache(RedisService redisService) : IMemberCache
public class RedisMemberCache(
RedisService redisService,
IDiscordRestGuildAPI guildApi,
ILogger logger
) : IMemberCache
{
private readonly ILogger _logger = logger.ForContext<RedisMemberCache>();
public async Task<IGuildMember?> TryGetAsync(Snowflake guildId, Snowflake userId)
{
var redisMember = await redisService.GetHashAsync<RedisMember>(
GuildMembersKey(guildId),
userId.ToString()
);
var redisMember = await TryGetInnerAsync(guildId, userId);
return redisMember?.ToRemoraMember();
}
private async Task<RedisMember?> TryGetInnerAsync(Snowflake guildId, Snowflake userId) =>
await redisService.GetHashAsync<RedisMember>(GuildMembersKey(guildId), userId.ToString());
public async Task SetAsync(Snowflake guildId, IGuildMember member)
{
if (!member.User.IsDefined())
throw new CataloggerError(
"Member with undefined User passed to RedisMemberCache.SetAsync"
);
await SetInnerAsync(guildId, RedisMember.FromIGuildMember(member));
}
private async Task SetInnerAsync(Snowflake guildId, RedisMember member)
{
await redisService.SetHashAsync(
GuildMembersKey(guildId),
member.User.Value.ID.ToString(),
RedisMember.FromIGuildMember(member)
member.User.Id.ToString(),
member
);
}
@ -59,6 +73,58 @@ public class RedisMemberCache(RedisService redisService) : IMemberCache
public async Task MarkAsUncachedAsync(Snowflake guildId) =>
await redisService.GetDatabase().SetRemoveAsync(GuildCacheKey, guildId.ToString());
public async Task UpdateAsync(IGuildMemberUpdate newMember)
{
var oldMember = await TryGetInnerAsync(newMember.GuildID, newMember.User.ID);
if (oldMember == null)
{
_logger.Warning(
"Received member update event for {MemberId} in {GuildId}, but member wasn't found in cache. Fetching them and storing.",
newMember.User.ID,
newMember.GuildID
);
var memberResult = await guildApi.GetGuildMemberAsync(
newMember.GuildID,
newMember.User.ID
);
if (!memberResult.IsSuccess)
{
_logger.Error(
"Could not get uncached member {MemberId} in {GuildId} via REST: {Error}",
newMember.User.ID,
newMember.GuildID,
memberResult.Error
);
return;
}
await SetAsync(newMember.GuildID, memberResult.Entity);
return;
}
var member = oldMember with
{
User = RedisUser.FromIUser(newMember.User),
Nickname = newMember.Nickname.HasValue ? newMember.Nickname.Value : oldMember.Nickname,
Avatar = newMember.Avatar.HasValue ? newMember.Avatar.Value?.Value : oldMember.Avatar,
Roles = newMember.Roles.ToArray(),
JoinedAt = newMember.JoinedAt ?? oldMember.JoinedAt,
PremiumSince = newMember.PremiumSince.HasValue
? newMember.PremiumSince.Value
: oldMember.PremiumSince,
IsPending = newMember.IsPending.HasValue
? newMember.IsPending.Value
: oldMember.IsPending,
CommunicationDisabledUntil = newMember.CommunicationDisabledUntil.HasValue
? newMember.CommunicationDisabledUntil.Value
: oldMember.CommunicationDisabledUntil,
};
await SetInnerAsync(newMember.GuildID, member);
}
private const string GuildCacheKey = "cached-guilds";
private static string GuildMembersKey(Snowflake guildId) => $"guild-members:{guildId}";