feat(dashboard): working ignored channels page

This commit is contained in:
sam 2024-10-20 15:20:22 +02:00
parent 1c43beb82f
commit bccf7caf34
Signed by: sam
GPG key ID: 5F3C3C1B3166639D
13 changed files with 304 additions and 8 deletions

View file

@ -0,0 +1,112 @@
// 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/>.
using Catalogger.Backend.Database.Queries;
using Microsoft.AspNetCore.Mvc;
using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects;
namespace Catalogger.Backend.Api;
public partial class GuildsController
{
[HttpPut("ignored-channels/{channelId}")]
public async Task<IActionResult> AddIgnoredChannelAsync(string id, ulong channelId)
{
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await db.GetGuildAsync(guildId);
if (guildConfig.Channels.IgnoredChannels.Contains(channelId))
return NoContent();
var channel = channelCache
.GuildChannels(guildId)
.FirstOrDefault(c =>
c.ID.Value == channelId
&& c.Type
is ChannelType.GuildText
or ChannelType.GuildCategory
or ChannelType.GuildAnnouncement
or ChannelType.GuildForum
or ChannelType.GuildMedia
or ChannelType.GuildVoice
);
if (channel == null)
return NoContent();
guildConfig.Channels.IgnoredChannels.Add(channelId);
db.Update(guildConfig);
await db.SaveChangesAsync();
return NoContent();
}
[HttpDelete("ignored-channels/{channelId}")]
public async Task<IActionResult> RemoveIgnoredChannelAsync(string id, ulong channelId)
{
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await db.GetGuildAsync(guildId);
guildConfig.Channels.IgnoredChannels.Remove(channelId);
db.Update(guildConfig);
await db.SaveChangesAsync();
return NoContent();
}
[HttpGet("users")]
public async Task<IActionResult> ListUsersAsync(string id, [FromQuery] string query)
{
var (guildId, _) = await ParseGuildAsync(id);
var members = await memberCache.GetMemberNamesAsync(guildId, query);
return Ok(members.OrderBy(m => m.Name).Select(m => new UserQueryResponse(m.Name, m.Id)));
}
private record UserQueryResponse(string Name, string Id);
[HttpPut("ignored-users/{userId}")]
public async Task<IActionResult> AddIgnoredUserAsync(string id, ulong userId)
{
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await db.GetGuildAsync(guildId);
if (guildConfig.Channels.IgnoredUsers.Contains(userId))
return NoContent();
var user = await memberCache.TryGetAsync(guildId, DiscordSnowflake.New(userId));
if (user == null)
return NoContent();
guildConfig.Channels.IgnoredUsers.Add(userId);
db.Update(guildConfig);
await db.SaveChangesAsync();
return NoContent();
}
[HttpDelete("ignored-users/{userId}")]
public async Task<IActionResult> RemoveIgnoredUserAsync(string id, ulong userId)
{
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await db.GetGuildAsync(guildId);
guildConfig.Channels.IgnoredUsers.Remove(userId);
db.Update(guildConfig);
await db.SaveChangesAsync();
return NoContent();
}
}

View file

@ -15,6 +15,7 @@
using System.Net;
using Catalogger.Backend.Api.Middleware;
using Catalogger.Backend.Cache;
using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Database;
using Catalogger.Backend.Database.Queries;
@ -32,6 +33,7 @@ public partial class GuildsController(
DatabaseContext db,
ChannelCache channelCache,
RedisService redisService,
IMemberCache memberCache,
DiscordRequestService discordRequestService
) : ApiControllerBase
{

View file

@ -35,6 +35,7 @@ public class GuildMembersChunkResponder(ILogger logger, IMemberCache memberCache
);
await memberCache.SetManyAsync(evt.GuildID, evt.Members);
await memberCache.SetMemberNamesAsync(evt.GuildID, evt.Members);
if (evt.ChunkIndex == evt.ChunkCount - 1)
{

View file

@ -49,6 +49,7 @@ public class GuildMemberAddResponder(
public async Task<Result> RespondAsync(IGuildMemberAdd member, CancellationToken ct = default)
{
await memberCache.SetAsync(member.GuildID, member);
await memberCache.SetMemberNamesAsync(member.GuildID, [member]);
var user = member.User.GetOrThrow();
userCache.UpdateUser(user);

View file

@ -128,6 +128,7 @@ public class GuildMemberRemoveResponder(
finally
{
await memberCache.RemoveAsync(evt.GuildID, evt.User.ID);
await memberCache.TryRemoveMemberNameAsync(evt.GuildID, evt.User.Username);
}
}
}

View file

@ -96,6 +96,7 @@ public class GuildMemberUpdateResponder(
finally
{
await memberCache.UpdateAsync(newMember);
userCache.UpdateUser(newMember.User);
}
@ -194,6 +195,13 @@ public class GuildMemberUpdateResponder(
**After:** {newMember.User.Tag()}
"""
);
await memberCache.UpdateMemberNameAsync(
newMember.GuildID,
newMember.User.ID,
oldUser.Tag(),
newMember.User.Tag()
);
}
var guildConfig = await db.GetGuildAsync(newMember.GuildID, ct);

View file

@ -29,4 +29,21 @@ public interface IMemberCache
public Task MarkAsCachedAsync(Snowflake guildId);
public Task MarkAsUncachedAsync(Snowflake guildId);
public Task UpdateAsync(IGuildMemberUpdate newMember);
// These methods can be stubbed out for any implementation that isn't intended for use with the dashboard.
public Task SetMemberNamesAsync(Snowflake guildId, IEnumerable<IGuildMember> members);
public Task UpdateMemberNameAsync(
Snowflake guildId,
Snowflake userId,
string prevName,
string newName
);
public Task<IEnumerable<(string Name, string Id)>> GetMemberNamesAsync(
Snowflake guildId,
string prefix
);
public Task TryRemoveMemberNameAsync(Snowflake guildId, string username);
}

View file

@ -128,4 +128,21 @@ public class InMemoryMemberCache(IDiscordRestGuildAPI guildApi, ILogger logger)
_members[(newMember.GuildID, newMember.User.ID)] = member;
}
public Task SetMemberNamesAsync(Snowflake guildId, IEnumerable<IGuildMember> members) =>
Task.CompletedTask;
public Task UpdateMemberNameAsync(
Snowflake guildId,
Snowflake userId,
string prevName,
string newName
) => Task.CompletedTask;
public Task<IEnumerable<(string Name, string Id)>> GetMemberNamesAsync(
Snowflake guildId,
string prefix
) => Task.FromResult<IEnumerable<(string, string)>>([]);
public Task TryRemoveMemberNameAsync(Snowflake guildId, string username) => Task.CompletedTask;
}

View file

@ -14,12 +14,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Catalogger.Backend.Database.Redis;
using Catalogger.Backend.Extensions;
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;
using StackExchange.Redis;
namespace Catalogger.Backend.Cache.RedisCache;
@ -146,9 +148,55 @@ public class RedisMemberCache(
await SetInnerAsync(newMember.GuildID, member);
}
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);
private const string GuildCacheKey = "cached-guilds";
private static string GuildMembersKey(Snowflake guildId) => $"guild-members:{guildId}";
private static string MemberNamesKey(Snowflake guildId) => $"guild-member-names:{guildId}";
}
internal record RedisMember(