2024-10-14 14:56:40 +02:00
|
|
|
// Copyright (C) 2021-present sam (starshines.gay)
|
|
|
|
|
//
|
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
|
|
|
// by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
//
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
|
//
|
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
2024-08-19 16:12:28 +02:00
|
|
|
using Catalogger.Backend.Database.Redis;
|
2024-10-20 15:20:22 +02:00
|
|
|
using Catalogger.Backend.Extensions;
|
2024-08-19 16:12:28 +02:00
|
|
|
using Remora.Discord.API;
|
2024-10-09 22:31:58 +02:00
|
|
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
2024-08-19 16:12:28 +02:00
|
|
|
using Remora.Discord.API.Abstractions.Objects;
|
2024-10-09 22:31:58 +02:00
|
|
|
using Remora.Discord.API.Abstractions.Rest;
|
2024-08-19 16:12:28 +02:00
|
|
|
using Remora.Discord.API.Objects;
|
|
|
|
|
using Remora.Rest.Core;
|
2024-10-20 15:20:22 +02:00
|
|
|
using StackExchange.Redis;
|
2024-08-19 16:12:28 +02:00
|
|
|
|
|
|
|
|
namespace Catalogger.Backend.Cache.RedisCache;
|
|
|
|
|
|
2024-10-09 22:31:58 +02:00
|
|
|
public class RedisMemberCache(
|
|
|
|
|
RedisService redisService,
|
|
|
|
|
IDiscordRestGuildAPI guildApi,
|
|
|
|
|
ILogger logger
|
|
|
|
|
) : IMemberCache
|
2024-08-19 16:12:28 +02:00
|
|
|
{
|
2024-10-09 22:31:58 +02:00
|
|
|
private readonly ILogger _logger = logger.ForContext<RedisMemberCache>();
|
|
|
|
|
|
2024-08-19 16:12:28 +02:00
|
|
|
public async Task<IGuildMember?> TryGetAsync(Snowflake guildId, Snowflake userId)
|
|
|
|
|
{
|
2024-10-09 22:31:58 +02:00
|
|
|
var redisMember = await TryGetInnerAsync(guildId, userId);
|
2024-08-19 16:12:28 +02:00
|
|
|
return redisMember?.ToRemoraMember();
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 22:31:58 +02:00
|
|
|
private async Task<RedisMember?> TryGetInnerAsync(Snowflake guildId, Snowflake userId) =>
|
|
|
|
|
await redisService.GetHashAsync<RedisMember>(GuildMembersKey(guildId), userId.ToString());
|
|
|
|
|
|
2024-08-19 16:12:28 +02:00
|
|
|
public async Task SetAsync(Snowflake guildId, IGuildMember member)
|
|
|
|
|
{
|
|
|
|
|
if (!member.User.IsDefined())
|
2024-10-09 17:35:11 +02:00
|
|
|
throw new CataloggerError(
|
|
|
|
|
"Member with undefined User passed to RedisMemberCache.SetAsync"
|
|
|
|
|
);
|
2024-10-09 22:31:58 +02:00
|
|
|
|
|
|
|
|
await SetInnerAsync(guildId, RedisMember.FromIGuildMember(member));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task SetInnerAsync(Snowflake guildId, RedisMember member)
|
|
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
await redisService.SetHashAsync(
|
|
|
|
|
GuildMembersKey(guildId),
|
2024-10-09 22:31:58 +02:00
|
|
|
member.User.Id.ToString(),
|
|
|
|
|
member
|
2024-10-09 17:35:11 +02:00
|
|
|
);
|
2024-08-19 16:12:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task SetManyAsync(Snowflake guildId, IReadOnlyList<IGuildMember> members)
|
|
|
|
|
{
|
|
|
|
|
if (members.Any(m => !m.User.IsDefined()))
|
2024-10-09 17:35:11 +02:00
|
|
|
throw new CataloggerError(
|
|
|
|
|
"Member with undefined User passed to RedisMemberCache.SetAsync"
|
|
|
|
|
);
|
2024-08-19 16:12:28 +02:00
|
|
|
var redisMembers = members.Select(RedisMember.FromIGuildMember).ToList();
|
|
|
|
|
|
2024-10-09 17:35:11 +02:00
|
|
|
await redisService.SetHashAsync(
|
|
|
|
|
GuildMembersKey(guildId),
|
|
|
|
|
redisMembers,
|
|
|
|
|
m => m.User.Id.ToString()
|
|
|
|
|
);
|
2024-08-19 16:12:28 +02:00
|
|
|
}
|
|
|
|
|
|
2024-09-02 17:00:33 +02:00
|
|
|
public async Task RemoveAsync(Snowflake guildId, Snowflake userId) =>
|
2024-10-09 17:35:11 +02:00
|
|
|
await redisService
|
|
|
|
|
.GetDatabase()
|
|
|
|
|
.HashDeleteAsync(GuildMembersKey(guildId), userId.ToString());
|
2024-09-02 17:00:33 +02:00
|
|
|
|
2024-08-19 16:12:28 +02:00
|
|
|
public async Task<bool> IsGuildCachedAsync(Snowflake guildId) =>
|
|
|
|
|
await redisService.GetDatabase().SetContainsAsync(GuildCacheKey, guildId.ToString());
|
|
|
|
|
|
|
|
|
|
public async Task MarkAsCachedAsync(Snowflake guildId) =>
|
|
|
|
|
await redisService.GetDatabase().SetAddAsync(GuildCacheKey, guildId.ToString());
|
|
|
|
|
|
|
|
|
|
public async Task MarkAsUncachedAsync(Snowflake guildId) =>
|
|
|
|
|
await redisService.GetDatabase().SetRemoveAsync(GuildCacheKey, guildId.ToString());
|
|
|
|
|
|
2024-10-09 22:31:58 +02:00
|
|
|
public async Task UpdateAsync(IGuildMemberUpdate newMember)
|
|
|
|
|
{
|
2024-10-11 21:29:02 +02:00
|
|
|
_logger.Debug(
|
|
|
|
|
"Updating member {MemberId} in {GuildId}",
|
|
|
|
|
newMember.User.ID,
|
|
|
|
|
newMember.GuildID
|
|
|
|
|
);
|
|
|
|
|
|
2024-10-09 22:31:58 +02:00
|
|
|
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,
|
2024-10-11 21:29:02 +02:00
|
|
|
Roles = newMember.Roles.Select(id => id.Value).ToArray(),
|
2024-10-09 22:31:58 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 15:20:22 +02:00
|
|
|
public async Task SetMemberNamesAsync(Snowflake guildId, IEnumerable<IGuildMember> members)
|
|
|
|
|
{
|
|
|
|
|
await redisService
|
|
|
|
|
.GetDatabase()
|
|
|
|
|
.HashSetAsync(
|
|
|
|
|
MemberNamesKey(guildId),
|
|
|
|
|
members
|
|
|
|
|
.Select(m => new HashEntry(m.User.Value.Tag(), m.User.Value.ID.ToString()))
|
|
|
|
|
.ToArray()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task UpdateMemberNameAsync(
|
|
|
|
|
Snowflake guildId,
|
|
|
|
|
Snowflake userId,
|
|
|
|
|
string prevName,
|
|
|
|
|
string newName
|
|
|
|
|
) =>
|
|
|
|
|
await Task.WhenAll(
|
|
|
|
|
redisService.GetDatabase().HashDeleteAsync(MemberNamesKey(guildId), prevName),
|
|
|
|
|
redisService
|
|
|
|
|
.GetDatabase()
|
|
|
|
|
.HashSetAsync(MemberNamesKey(guildId), newName, userId.ToString())
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
public async Task<IEnumerable<(string Name, string Id)>> GetMemberNamesAsync(
|
|
|
|
|
Snowflake guildId,
|
|
|
|
|
string prefix
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
var entries = redisService
|
|
|
|
|
.GetDatabase()
|
|
|
|
|
.HashScanAsync(MemberNamesKey(guildId), $"{prefix}*", 50);
|
|
|
|
|
|
|
|
|
|
var names = new List<(string Name, string Id)>();
|
|
|
|
|
await foreach (var entry in entries)
|
|
|
|
|
names.Add((entry.Name, entry.Value)!);
|
|
|
|
|
|
|
|
|
|
return names;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task TryRemoveMemberNameAsync(Snowflake guildId, string username) =>
|
|
|
|
|
await redisService.GetDatabase().HashDeleteAsync(MemberNamesKey(guildId), username);
|
|
|
|
|
|
2024-10-24 21:10:54 +02:00
|
|
|
public async Task RemoveAllMembersAsync(Snowflake guildId) =>
|
|
|
|
|
await redisService
|
|
|
|
|
.GetDatabase()
|
|
|
|
|
.KeyDeleteAsync([GuildMembersKey(guildId), MemberNamesKey(guildId)]);
|
|
|
|
|
|
2024-08-19 16:12:28 +02:00
|
|
|
private const string GuildCacheKey = "cached-guilds";
|
2024-10-09 17:35:11 +02:00
|
|
|
|
2024-08-19 16:12:28 +02:00
|
|
|
private static string GuildMembersKey(Snowflake guildId) => $"guild-members:{guildId}";
|
2024-10-20 15:20:22 +02:00
|
|
|
|
|
|
|
|
private static string MemberNamesKey(Snowflake guildId) => $"guild-member-names:{guildId}";
|
2024-08-19 16:12:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal record RedisMember(
|
|
|
|
|
RedisUser User,
|
|
|
|
|
string? Nickname,
|
|
|
|
|
string? Avatar,
|
2024-10-11 21:29:02 +02:00
|
|
|
ulong[] Roles,
|
2024-08-19 16:12:28 +02:00
|
|
|
DateTimeOffset JoinedAt,
|
|
|
|
|
DateTimeOffset? PremiumSince,
|
|
|
|
|
GuildMemberFlags Flags,
|
|
|
|
|
bool? IsPending,
|
2024-10-09 17:35:11 +02:00
|
|
|
DateTimeOffset? CommunicationDisabledUntil
|
|
|
|
|
)
|
2024-08-19 16:12:28 +02:00
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
public static RedisMember FromIGuildMember(IGuildMember member) =>
|
|
|
|
|
new(
|
|
|
|
|
RedisUser.FromIUser(member.User.Value),
|
|
|
|
|
member.Nickname.OrDefault(null),
|
|
|
|
|
member.Avatar.OrDefault(null)?.Value,
|
2024-10-11 21:29:02 +02:00
|
|
|
member.Roles.Select(id => id.Value).ToArray(),
|
2024-10-09 17:35:11 +02:00
|
|
|
member.JoinedAt,
|
|
|
|
|
member.PremiumSince.OrDefault(null),
|
|
|
|
|
member.Flags,
|
|
|
|
|
member.IsPending.OrDefault(null),
|
|
|
|
|
member.CommunicationDisabledUntil.OrDefault(null)
|
|
|
|
|
);
|
2024-08-19 16:12:28 +02:00
|
|
|
|
2024-10-09 17:35:11 +02:00
|
|
|
public GuildMember ToRemoraMember() =>
|
|
|
|
|
new(
|
|
|
|
|
User.ToRemoraUser(),
|
|
|
|
|
Nickname,
|
|
|
|
|
Avatar != null ? new ImageHash(Avatar) : null,
|
2025-03-14 20:11:00 +01:00
|
|
|
Banner: null,
|
2024-10-11 21:29:02 +02:00
|
|
|
Roles.Select(DiscordSnowflake.New).ToList(),
|
2024-10-09 17:35:11 +02:00
|
|
|
JoinedAt,
|
|
|
|
|
PremiumSince,
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
Flags,
|
|
|
|
|
IsPending,
|
|
|
|
|
default,
|
|
|
|
|
CommunicationDisabledUntil
|
|
|
|
|
);
|
2024-08-19 16:12:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal record RedisUser(
|
|
|
|
|
ulong Id,
|
|
|
|
|
string Username,
|
|
|
|
|
ushort Discriminator,
|
|
|
|
|
string? GlobalName,
|
|
|
|
|
string? Avatar,
|
|
|
|
|
bool IsBot,
|
|
|
|
|
bool IsSystem,
|
2024-10-09 17:35:11 +02:00
|
|
|
string? Banner
|
|
|
|
|
)
|
2024-08-19 16:12:28 +02:00
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
public static RedisUser FromIUser(IUser user) =>
|
|
|
|
|
new(
|
|
|
|
|
user.ID.Value,
|
|
|
|
|
user.Username,
|
|
|
|
|
user.Discriminator,
|
|
|
|
|
user.GlobalName.OrDefault(null),
|
|
|
|
|
user.Avatar?.Value,
|
|
|
|
|
user.IsBot.OrDefault(false),
|
|
|
|
|
user.IsSystem.OrDefault(false),
|
|
|
|
|
user.Banner.OrDefault(null)?.Value
|
|
|
|
|
);
|
2024-08-19 16:12:28 +02:00
|
|
|
|
2024-10-09 17:35:11 +02:00
|
|
|
public User ToRemoraUser() =>
|
|
|
|
|
new(
|
|
|
|
|
DiscordSnowflake.New(Id),
|
|
|
|
|
Username,
|
|
|
|
|
Discriminator,
|
|
|
|
|
GlobalName,
|
|
|
|
|
Avatar != null ? new ImageHash(Avatar) : null,
|
|
|
|
|
IsBot,
|
|
|
|
|
IsSystem,
|
|
|
|
|
Banner: Banner != null ? new ImageHash(Banner) : null
|
|
|
|
|
);
|
|
|
|
|
}
|