diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index d2b1b96..b315638 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,14 +3,14 @@
"isRoot": true,
"tools": {
"csharpier": {
- "version": "0.30.6",
+ "version": "0.29.2",
"commands": [
"dotnet-csharpier"
],
"rollForward": false
},
"husky": {
- "version": "0.7.2",
+ "version": "0.7.1",
"commands": [
"husky"
],
diff --git a/.editorconfig b/.editorconfig
index 1e5c57b..d20e217 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -14,7 +14,4 @@ resharper_entity_framework_model_validation_unlimited_string_length_highlighting
# This is raised for every single property of records returned by endpoints
resharper_not_accessed_positional_property_local_highlighting = none
# ReSharper yells at us for the name "GuildCache", for some reason
-resharper_inconsistent_naming_highlighting = none
-# From the docs: "You might consider excluding [FirstOrDefault or LastOrDefault] if readability is a concern,
-# since the code you'd write to replace them is not easily readable."
-dotnet_code_quality.CA1826.exclude_ordefault_methods = true
\ No newline at end of file
+resharper_inconsistent_naming_highlighting = none
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 383eadd..7d908b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,3 @@ riderModule.iml
/_ReSharper.Caches/
config.ini
*.DotSettings.user
-.version
diff --git a/.idea/.idea.catalogger/.idea/sqldialects.xml b/.idea/.idea.catalogger/.idea/sqldialects.xml
index 4ea96ec..10eef95 100644
--- a/.idea/.idea.catalogger/.idea/sqldialects.xml
+++ b/.idea/.idea.catalogger/.idea/sqldialects.xml
@@ -2,7 +2,6 @@
-
\ No newline at end of file
diff --git a/Catalogger.Backend/Api/ApiCache.cs b/Catalogger.Backend/Api/ApiCache.cs
index c4161ad..b790643 100644
--- a/Catalogger.Backend/Api/ApiCache.cs
+++ b/Catalogger.Backend/Api/ApiCache.cs
@@ -14,11 +14,43 @@
// along with this program. If not, see .
using Catalogger.Backend.Database.Redis;
+using Remora.Discord.API;
+using Remora.Discord.API.Abstractions.Objects;
+using Remora.Discord.API.Abstractions.Rest;
namespace Catalogger.Backend.Api;
-public class ApiCache(RedisService redisService)
+public class ApiCache(RedisService redisService, IDiscordRestChannelAPI channelApi, Config config)
{
+ private List? _news;
+ private readonly SemaphoreSlim _newsSemaphore = new(1);
+
+ public async Task> GetNewsAsync()
+ {
+ await _newsSemaphore.WaitAsync();
+ try
+ {
+ if (_news != null)
+ return _news;
+
+ if (config.Web.NewsChannel == null)
+ return [];
+
+ var res = await channelApi.GetChannelMessagesAsync(
+ DiscordSnowflake.New(config.Web.NewsChannel.Value),
+ limit: 5
+ );
+ if (res.IsSuccess)
+ return _news = res.Entity.ToList();
+
+ return [];
+ }
+ finally
+ {
+ _newsSemaphore.Release();
+ }
+ }
+
private static string UserKey(string id) => $"api-user:{id}";
private static string GuildsKey(string userId) => $"api-user-guilds:{userId}";
diff --git a/Catalogger.Backend/Api/DiscordRequestService.cs b/Catalogger.Backend/Api/DiscordRequestService.cs
index c63e04e..abde930 100644
--- a/Catalogger.Backend/Api/DiscordRequestService.cs
+++ b/Catalogger.Backend/Api/DiscordRequestService.cs
@@ -30,10 +30,8 @@ public class DiscordRequestService
private readonly IClock _clock;
private readonly ApiTokenRepository _tokenRepository;
- private static readonly JsonSerializerOptions JsonOptions = new()
- {
- PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
- };
+ private static readonly JsonSerializerOptions JsonOptions =
+ new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
public DiscordRequestService(
ILogger logger,
@@ -84,9 +82,8 @@ public class DiscordRequestService
}
private static readonly Uri DiscordUserUri = new("https://discord.com/api/v10/users/@me");
- private static readonly Uri DiscordGuildsUri = new(
- "https://discord.com/api/v10/users/@me/guilds"
- );
+ private static readonly Uri DiscordGuildsUri =
+ new("https://discord.com/api/v10/users/@me/guilds");
private static readonly Uri DiscordTokenUri = new("https://discord.com/api/oauth2/token");
public async Task GetMeAsync(string token) => await GetAsync(DiscordUserUri, token);
diff --git a/Catalogger.Backend/Api/GuildsController.Backup.cs b/Catalogger.Backend/Api/GuildsController.Backup.cs
deleted file mode 100644
index f808b0c..0000000
--- a/Catalogger.Backend/Api/GuildsController.Backup.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-// 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 .
-
-using System.Net;
-using Catalogger.Backend.Api.Middleware;
-using Catalogger.Backend.Database.Models;
-using Microsoft.AspNetCore.Mvc;
-using NodaTime;
-using Remora.Discord.API;
-
-namespace Catalogger.Backend.Api;
-
-public partial class GuildsController
-{
- [Authorize]
- [HttpGet("config")]
- public async Task ExportConfigAsync(string id)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- return Ok(await ToExport(guildConfig));
- }
-
- [Authorize]
- [HttpPost("config")]
- public async Task ImportConfigAsync(string id, [FromBody] ConfigExport export)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- if (export.Id != guildId.Value)
- throw new ApiError(
- HttpStatusCode.BadRequest,
- ErrorCode.BadRequest,
- "This backup is not from this server."
- );
-
- // Filter invites to *only* those that exist for this guild.
- // Blame past me for not making (code, guild_id) a unique index >:|
- var cachedInvites = (await inviteCache.TryGetAsync(guildId)).ToList();
- var invites = export.Invites.Where(i => cachedInvites.Any(ci => i.Code == ci.Code));
-
- await guildRepository.ImportConfigAsync(
- guildId.Value,
- export.Channels.ToChannelConfig(),
- export.Channels.ToMessageConfig(),
- export.BannedSystems,
- export.KeyRoles
- );
-
- await inviteRepository.ImportInvitesAsync(
- guildId,
- invites.Select(i => new Invite
- {
- Code = i.Code,
- Name = i.Name,
- GuildId = guildId.Value,
- })
- );
-
- await watchlistRepository.ImportWatchlistAsync(
- guildId,
- export.Watchlist.Select(w => new Watchlist
- {
- GuildId = guildId.Value,
- UserId = w.UserId,
- ModeratorId = w.ModeratorId,
- AddedAt = w.AddedAt,
- Reason = w.Reason,
- })
- );
-
- return NoContent();
- }
-
- private async Task ToExport(Database.Models.Guild config)
- {
- var id = DiscordSnowflake.New(config.Id);
- var invites = await inviteRepository.GetGuildInvitesAsync(id);
- var watchlist = await watchlistRepository.GetGuildWatchlistAsync(id);
-
- return new ConfigExport(
- config.Id,
- ChannelsBackup.FromGuildConfig(config),
- config.BannedSystems,
- config.KeyRoles,
- invites.Select(i => new InviteExport(i.Code, i.Name)),
- watchlist.Select(w => new WatchlistExport(w.UserId, w.AddedAt, w.ModeratorId, w.Reason))
- );
- }
-}
diff --git a/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs b/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs
deleted file mode 100644
index 4114d5a..0000000
--- a/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-// 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 .
-
-using Microsoft.AspNetCore.Mvc;
-using Remora.Discord.API.Abstractions.Objects;
-
-namespace Catalogger.Backend.Api;
-
-public partial class GuildsController
-{
- [HttpPut("ignored-messages/channels/{channelId}")]
- public async Task AddIgnoredMessageChannelAsync(string id, ulong channelId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.Messages.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.Messages.IgnoredChannels.Add(channelId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return NoContent();
- }
-
- [HttpDelete("ignored-messages/channels/{channelId}")]
- public async Task RemoveIgnoredMessageChannelAsync(string id, ulong channelId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- guildConfig.Messages.IgnoredChannels.Remove(channelId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return NoContent();
- }
-
- [HttpPut("ignored-messages/roles/{roleId}")]
- public async Task AddIgnoredMessageRoleAsync(string id, ulong roleId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.Messages.IgnoredRoles.Contains(roleId))
- return NoContent();
-
- if (roleCache.GuildRoles(guildId).All(r => r.ID.Value != roleId))
- return NoContent();
-
- guildConfig.Messages.IgnoredRoles.Add(roleId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return NoContent();
- }
-
- [HttpDelete("ignored-messages/roles/{roleId}")]
- public async Task RemoveIgnoredMessageRoleAsync(string id, ulong roleId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- guildConfig.Messages.IgnoredRoles.Remove(roleId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return NoContent();
- }
-
- [HttpPut("ignored-entities/channels/{channelId}")]
- public async Task AddIgnoredEntityChannelAsync(string id, ulong channelId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.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.IgnoredChannels.Add(channelId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return NoContent();
- }
-
- [HttpDelete("ignored-entities/channels/{channelId}")]
- public async Task RemoveIgnoredEntityChannelAsync(string id, ulong channelId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- guildConfig.IgnoredChannels.Remove(channelId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return NoContent();
- }
-
- [HttpPut("ignored-entities/roles/{roleId}")]
- public async Task AddIgnoredEntityRoleAsync(string id, ulong roleId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.Messages.IgnoredRoles.Contains(roleId))
- return NoContent();
-
- if (roleCache.GuildRoles(guildId).All(r => r.ID.Value != roleId))
- return NoContent();
-
- guildConfig.IgnoredRoles.Add(roleId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return NoContent();
- }
-
- [HttpDelete("ignored-entities/roles/{roleId}")]
- public async Task RemoveIgnoredEntityRoleAsync(string id, ulong roleId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- guildConfig.IgnoredRoles.Remove(roleId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return NoContent();
- }
-}
diff --git a/Catalogger.Backend/Api/GuildsController.Users.cs b/Catalogger.Backend/Api/GuildsController.Ignores.cs
similarity index 50%
rename from Catalogger.Backend/Api/GuildsController.Users.cs
rename to Catalogger.Backend/Api/GuildsController.Ignores.cs
index e67cb55..ea9e647 100644
--- a/Catalogger.Backend/Api/GuildsController.Users.cs
+++ b/Catalogger.Backend/Api/GuildsController.Ignores.cs
@@ -13,9 +13,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-using System.Net;
-using Catalogger.Backend.Api.Middleware;
-using Catalogger.Backend.Extensions;
using Microsoft.AspNetCore.Mvc;
using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects;
@@ -24,71 +21,44 @@ namespace Catalogger.Backend.Api;
public partial class GuildsController
{
- [HttpGet("ignored-users")]
- public async Task GetIgnoredUsersAsync(string id, CancellationToken ct = default)
+ [HttpPut("ignored-channels/{channelId}")]
+ public async Task AddIgnoredChannelAsync(string id, ulong channelId)
{
- var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
- // not actually sure how long fetching members might take. timing it out after 10 seconds just in case
- // the underlying redis library doesn't support CancellationTokens so we don't pass it down
- // we just end the loop early if it expires
- cts.CancelAfter(TimeSpan.FromSeconds(10));
-
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await guildRepository.GetAsync(guildId);
- var output = new List();
- foreach (var userId in guildConfig.Messages.IgnoredUsers)
- {
- if (cts.Token.IsCancellationRequested)
- break;
+ if (guildConfig.Channels.IgnoredChannels.Contains(channelId))
+ return NoContent();
- var member = await memberCache.TryGetAsync(guildId, DiscordSnowflake.New(userId));
- output.Add(
- new IgnoredUser(
- Id: userId,
- Tag: member != null ? member.User.Value.Tag() : "unknown user"
- )
+ 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();
- return Ok(output.OrderBy(i => i.Id));
+ guildConfig.Channels.IgnoredChannels.Add(channelId);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
+
+ return NoContent();
}
- private record IgnoredUser(ulong Id, string Tag);
-
- [HttpPut("ignored-users/{userId}")]
- public async Task AddIgnoredUserAsync(string id, ulong userId)
+ [HttpDelete("ignored-channels/{channelId}")]
+ public async Task RemoveIgnoredChannelAsync(string id, ulong channelId)
{
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await guildRepository.GetAsync(guildId);
- IUser? user;
- var member = await memberCache.TryGetAsync(guildId, DiscordSnowflake.New(userId));
- if (member != null)
- user = member.User.Value;
- else
- user = await userCache.GetUserAsync(DiscordSnowflake.New(userId));
-
- if (user == null)
- throw new ApiError(HttpStatusCode.NotFound, ErrorCode.BadRequest, "User not found");
-
- if (guildConfig.Messages.IgnoredUsers.Contains(user.ID.Value))
- return Ok(new IgnoredUser(user.ID.Value, user.Tag()));
-
- guildConfig.Messages.IgnoredUsers.Add(user.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return Ok(new IgnoredUser(user.ID.Value, user.Tag()));
- }
-
- [HttpDelete("ignored-users/{userId}")]
- public async Task RemoveIgnoredUserAsync(string id, ulong userId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- guildConfig.Messages.IgnoredUsers.Remove(userId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ guildConfig.Channels.IgnoredChannels.Remove(channelId);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
return NoContent();
}
@@ -103,4 +73,35 @@ public partial class GuildsController
}
private record UserQueryResponse(string Name, string Id);
+
+ [HttpPut("ignored-users/{userId}")]
+ public async Task AddIgnoredUserAsync(string id, ulong userId)
+ {
+ var (guildId, _) = await ParseGuildAsync(id);
+ var guildConfig = await guildRepository.GetAsync(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);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
+
+ return NoContent();
+ }
+
+ [HttpDelete("ignored-users/{userId}")]
+ public async Task RemoveIgnoredUserAsync(string id, ulong userId)
+ {
+ var (guildId, _) = await ParseGuildAsync(id);
+ var guildConfig = await guildRepository.GetAsync(guildId);
+
+ guildConfig.Channels.IgnoredUsers.Remove(userId);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
+
+ return NoContent();
+ }
}
diff --git a/Catalogger.Backend/Api/GuildsController.KeyRoles.cs b/Catalogger.Backend/Api/GuildsController.KeyRoles.cs
deleted file mode 100644
index f1e786e..0000000
--- a/Catalogger.Backend/Api/GuildsController.KeyRoles.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-// 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 .
-
-using System.Net;
-using Catalogger.Backend.Api.Middleware;
-using Microsoft.AspNetCore.Mvc;
-using Remora.Discord.API;
-
-namespace Catalogger.Backend.Api;
-
-public partial class GuildsController
-{
- [HttpPut("key-roles/{roleId}")]
- public async Task AddKeyRoleAsync(string id, ulong roleId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (roleCache.GuildRoles(guildId).All(r => r.ID.Value != roleId))
- throw new ApiError(HttpStatusCode.BadRequest, ErrorCode.BadRequest, "Role not found");
-
- if (guildConfig.KeyRoles.Contains(roleId))
- throw new ApiError(
- HttpStatusCode.BadRequest,
- ErrorCode.BadRequest,
- "Role is already a key role"
- );
-
- guildConfig.KeyRoles.Add(roleId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
- return NoContent();
- }
-
- [HttpDelete("key-roles/{roleId}")]
- public async Task RemoveKeyRoleAsync(string id, ulong roleId)
- {
- var (guildId, _) = await ParseGuildAsync(id);
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (!guildConfig.KeyRoles.Contains(roleId))
- throw new ApiError(
- HttpStatusCode.BadRequest,
- ErrorCode.BadRequest,
- "Role is already not a key role"
- );
-
- guildConfig.KeyRoles.Remove(roleId);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
- return NoContent();
- }
-}
diff --git a/Catalogger.Backend/Api/GuildsController.Redirects.cs b/Catalogger.Backend/Api/GuildsController.Redirects.cs
index 4131343..9ce84de 100644
--- a/Catalogger.Backend/Api/GuildsController.Redirects.cs
+++ b/Catalogger.Backend/Api/GuildsController.Redirects.cs
@@ -61,7 +61,7 @@ public partial class GuildsController
);
guildConfig.Channels.Redirects[source.ID.Value] = target.ID.Value;
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
return NoContent();
}
@@ -80,7 +80,7 @@ public partial class GuildsController
);
guildConfig.Channels.Redirects.Remove(channelId, out _);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
return NoContent();
}
diff --git a/Catalogger.Backend/Api/GuildsController.Remove.cs b/Catalogger.Backend/Api/GuildsController.Remove.cs
index 1002efa..2d2ff5e 100644
--- a/Catalogger.Backend/Api/GuildsController.Remove.cs
+++ b/Catalogger.Backend/Api/GuildsController.Remove.cs
@@ -14,15 +14,12 @@
// along with this program. If not, see .
using System.Net;
-using System.Text;
-using System.Text.Json;
using Catalogger.Backend.Api.Middleware;
using Catalogger.Backend.Bot;
using Catalogger.Backend.Extensions;
using Catalogger.Backend.Services;
using Dapper;
using Microsoft.AspNetCore.Mvc;
-using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Extensions.Embeds;
namespace Catalogger.Backend.Api;
@@ -43,8 +40,6 @@ public partial class GuildsController
}
var guildConfig = await guildRepository.GetAsync(guildId);
- var export = await ToExport(guildConfig);
-
var logChannelId =
webhookExecutor.GetLogChannel(guildConfig, LogChannelType.GuildUpdate)
?? webhookExecutor.GetLogChannel(guildConfig, LogChannelType.GuildMemberRemove);
@@ -55,25 +50,15 @@ public partial class GuildsController
var embed = new EmbedBuilder()
.WithTitle("Catalogger is leaving this server")
.WithDescription(
- $"""
- A moderator in this server ({currentUser.Tag}, <@{currentUser.Id}>) requested that Catalogger leave.
- All data related to this server will be deleted.
-
- A backup of this server's configuration is attached to this message,
- in case you want to use the bot again later.
- """
+ $"A moderator in this server ({currentUser.Tag}, <@{currentUser.Id}>) requested that Catalogger leave. "
+ + "All data related to this server will be deleted."
)
.WithColour(DiscordUtils.Red)
.WithCurrentTimestamp()
.Build()
.GetOrThrow();
- var exportData = JsonSerializer.Serialize(export, JsonUtils.ApiJsonOptions);
- var file = new FileData(
- "config-backup.json",
- new MemoryStream(Encoding.UTF8.GetBytes(exportData))
- );
- await webhookExecutor.SendLogAsync(logChannelId.Value, [embed], [file]);
+ await webhookExecutor.SendLogAsync(logChannelId.Value, [embed], []);
}
else
{
@@ -130,9 +115,17 @@ public partial class GuildsController
guildId
);
+ // Clear out the caches for this guild
+ guildCache.Remove(guildId, out _);
+ emojiCache.Remove(guildId);
+ channelCache.RemoveGuild(guildId);
+ roleCache.RemoveGuild(guildId);
+ await memberCache.RemoveAllMembersAsync(guildId);
+ await inviteCache.RemoveAsync(guildId);
+
_logger.Information("Left guild {GuildId} and removed all data for it", guildId);
- return Ok(export);
+ return NoContent();
}
public record LeaveGuildRequest(string Name);
diff --git a/Catalogger.Backend/Api/GuildsController.cs b/Catalogger.Backend/Api/GuildsController.cs
index 73d81ef..560456f 100644
--- a/Catalogger.Backend/Api/GuildsController.cs
+++ b/Catalogger.Backend/Api/GuildsController.cs
@@ -19,7 +19,6 @@ using Catalogger.Backend.Cache;
using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Database;
using Catalogger.Backend.Database.Repositories;
-using Catalogger.Backend.Extensions;
using Catalogger.Backend.Services;
using Microsoft.AspNetCore.Mvc;
using Remora.Discord.API;
@@ -34,13 +33,12 @@ public partial class GuildsController(
ILogger logger,
DatabaseConnection dbConn,
GuildRepository guildRepository,
- InviteRepository inviteRepository,
- WatchlistRepository watchlistRepository,
+ GuildCache guildCache,
+ EmojiCache emojiCache,
ChannelCache channelCache,
RoleCache roleCache,
IMemberCache memberCache,
IInviteCache inviteCache,
- UserCache userCache,
DiscordRequestService discordRequestService,
IDiscordRestUserAPI userApi,
WebhookExecutorService webhookExecutor
@@ -93,16 +91,6 @@ public partial class GuildsController(
.Select(ToChannel)
));
- var roles = roleCache
- .GuildRoles(guildId)
- .OrderByDescending(r => r.Position)
- .Select(r => new GuildRole(
- r.ID.ToString(),
- r.Name,
- r.Position,
- r.Colour.ToPrettyString()
- ));
-
return Ok(
new GuildResponse(
guild.Id,
@@ -110,12 +98,7 @@ public partial class GuildsController(
guild.IconUrl,
categories,
channelsWithoutCategories,
- roles,
- guildConfig.IgnoredChannels,
- guildConfig.IgnoredRoles,
- guildConfig.Messages,
- guildConfig.Channels,
- guildConfig.KeyRoles
+ guildConfig.Channels
)
);
}
@@ -139,20 +122,13 @@ public partial class GuildsController(
string IconUrl,
IEnumerable Categories,
IEnumerable ChannelsWithoutCategory,
- IEnumerable Roles,
- List IgnoredChannels,
- List IgnoredRoles,
- Database.Models.Guild.MessageConfig Messages,
- Database.Models.Guild.ChannelConfig Channels,
- List KeyRoles
+ Database.Models.Guild.ChannelConfig Config
);
private record GuildCategory(string Id, string Name, IEnumerable Channels);
private record GuildChannel(string Id, string Name, bool CanLogTo, bool CanRedirectFrom);
- private record GuildRole(string Id, string Name, int Position, string Colour);
-
[Authorize]
[HttpPatch]
[ProducesResponseType(statusCode: StatusCodes.Status200OK)]
@@ -165,6 +141,28 @@ public partial class GuildsController(
.ToList();
var guildConfig = await guildRepository.GetAsync(guildId);
+ if (req.IgnoredChannels != null)
+ {
+ var categories = channelCache
+ .GuildChannels(guildId)
+ .Where(c => c.Type is ChannelType.GuildCategory)
+ .ToList();
+
+ if (
+ req.IgnoredChannels.Any(cId =>
+ guildChannels.All(c => c.ID.Value != cId)
+ && categories.All(c => c.ID.Value != cId)
+ )
+ )
+ throw new ApiError(
+ HttpStatusCode.BadRequest,
+ ErrorCode.BadRequest,
+ "One or more ignored channels are unknown"
+ );
+
+ guildConfig.Channels.IgnoredChannels = req.IgnoredChannels.ToList();
+ }
+
// i love repeating myself wheeeeee
if (
req.GuildUpdate == null
@@ -318,11 +316,12 @@ public partial class GuildsController(
)
guildConfig.Channels.MessageDeleteBulk = req.MessageDeleteBulk ?? 0;
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
return Ok(guildConfig.Channels);
}
public record ChannelRequest(
+ ulong[]? IgnoredChannels = null,
ulong? GuildUpdate = null,
ulong? GuildEmojisUpdate = null,
ulong? GuildRoleCreate = null,
diff --git a/Catalogger.Backend/Api/MetaController.cs b/Catalogger.Backend/Api/MetaController.cs
index 9d7a7b1..17d3c27 100644
--- a/Catalogger.Backend/Api/MetaController.cs
+++ b/Catalogger.Backend/Api/MetaController.cs
@@ -65,10 +65,6 @@ public class MetaController(
);
}
- [HttpGet("coffee")]
- public IActionResult BrewCoffee() =>
- Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot);
-
private record MetaResponse(
int Guilds,
string InviteUrl,
diff --git a/Catalogger.Backend/Api/Middleware/AuthenticationMiddleware.cs b/Catalogger.Backend/Api/Middleware/AuthenticationMiddleware.cs
index 5d2c888..47447dc 100644
--- a/Catalogger.Backend/Api/Middleware/AuthenticationMiddleware.cs
+++ b/Catalogger.Backend/Api/Middleware/AuthenticationMiddleware.cs
@@ -16,10 +16,12 @@
using System.Net;
using Catalogger.Backend.Database.Models;
using Catalogger.Backend.Database.Repositories;
+using NodaTime;
namespace Catalogger.Backend.Api.Middleware;
-public class AuthenticationMiddleware(ApiTokenRepository tokenRepository) : IMiddleware
+public class AuthenticationMiddleware(ApiTokenRepository tokenRepository, IClock clock)
+ : IMiddleware
{
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs
index 7f2ec0d..fcf54b9 100644
--- a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs
+++ b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs
@@ -43,7 +43,6 @@ public class ChannelCommands(
Config config,
GuildRepository guildRepository,
GuildCache guildCache,
- GuildFetchService guildFetchService,
ChannelCache channelCache,
IMemberCache memberCache,
IFeedbackService feedbackService,
@@ -69,11 +68,8 @@ public class ChannelCommands(
public async Task CheckPermissionsAsync()
{
var (userId, guildId) = contextInjection.GetUserAndGuild();
-
if (!guildCache.TryGet(guildId, out var guild))
- {
- return CataloggerError.Result($"Guild {guildId} not in cache");
- }
+ throw new CataloggerError("Guild not in cache");
var embed = new EmbedBuilder().WithTitle($"Permission check for {guild.Name}");
@@ -82,18 +78,8 @@ public class ChannelCommands(
DiscordSnowflake.New(config.Discord.ApplicationId)
);
var currentUser = await memberCache.TryGetAsync(guildId, userId);
-
if (botUser == null || currentUser == null)
- {
- // If this happens, something has gone wrong when fetching members. Refetch the guild's members.
- guildFetchService.EnqueueGuild(guildId);
- _logger.Error(
- "Either our own user {BotId} or the invoking user {UserId} is not in cache, aborting permission check",
- config.Discord.ApplicationId,
- userId
- );
- return CataloggerError.Result("Bot member or invoking member not found in cache");
- }
+ throw new CataloggerError("Bot member or invoking member not found in cache");
// We don't want to check categories or threads
var guildChannels = channelCache
@@ -218,7 +204,7 @@ public class ChannelCommands(
{
var (userId, guildId) = contextInjection.GetUserAndGuild();
if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
+ throw new CataloggerError("Guild not in cache");
var guildChannels = channelCache.GuildChannels(guildId).ToList();
var guildConfig = await guildRepository.GetAsync(guildId);
@@ -236,93 +222,6 @@ public class ChannelCommands(
return Result.Success;
}
- public static IStringSelectComponent LogTypeSelect =>
- new StringSelectComponent(
- CustomID: CustomIDHelpers.CreateSelectMenuID("select-log-type"),
- MinValues: 1,
- MaxValues: 1,
- Options:
- [
- new SelectOption(
- Label: "Server changes",
- Value: nameof(LogChannelType.GuildUpdate)
- ),
- new SelectOption(
- Label: "Emoji changes",
- Value: nameof(LogChannelType.GuildEmojisUpdate)
- ),
- new SelectOption(Label: "New roles", Value: nameof(LogChannelType.GuildRoleCreate)),
- new SelectOption(
- Label: "Edited roles",
- Value: nameof(LogChannelType.GuildRoleUpdate)
- ),
- new SelectOption(
- Label: "Deleted roles",
- Value: nameof(LogChannelType.GuildRoleDelete)
- ),
- new SelectOption(
- Label: "New channels",
- Value: nameof(LogChannelType.ChannelCreate)
- ),
- new SelectOption(
- Label: "Edited channels",
- Value: nameof(LogChannelType.ChannelUpdate)
- ),
- new SelectOption(
- Label: "Deleted channels",
- Value: nameof(LogChannelType.ChannelDelete)
- ),
- new SelectOption(
- Label: "Members joining",
- Value: nameof(LogChannelType.GuildMemberAdd)
- ),
- new SelectOption(
- Label: "Members leaving",
- Value: nameof(LogChannelType.GuildMemberRemove)
- ),
- new SelectOption(
- Label: "Member role changes",
- Value: nameof(LogChannelType.GuildMemberUpdate)
- ),
- new SelectOption(
- Label: "Key role changes",
- Value: nameof(LogChannelType.GuildKeyRoleUpdate)
- ),
- new SelectOption(
- Label: "Member name changes",
- Value: nameof(LogChannelType.GuildMemberNickUpdate)
- ),
- new SelectOption(
- Label: "Member avatar changes",
- Value: nameof(LogChannelType.GuildMemberAvatarUpdate)
- ),
- new SelectOption(
- Label: "Timeouts",
- Value: nameof(LogChannelType.GuildMemberTimeout)
- ),
- new SelectOption(Label: "Kicks", Value: nameof(LogChannelType.GuildMemberKick)),
- new SelectOption(Label: "Bans", Value: nameof(LogChannelType.GuildBanAdd)),
- new SelectOption(Label: "Unbans", Value: nameof(LogChannelType.GuildBanRemove)),
- new SelectOption(Label: "New invites", Value: nameof(LogChannelType.InviteCreate)),
- new SelectOption(
- Label: "Deleted invites",
- Value: nameof(LogChannelType.InviteDelete)
- ),
- new SelectOption(
- Label: "Edited messages",
- Value: nameof(LogChannelType.MessageUpdate)
- ),
- new SelectOption(
- Label: "Deleted messages",
- Value: nameof(LogChannelType.MessageDelete)
- ),
- new SelectOption(
- Label: "Bulk deleted messages",
- Value: nameof(LogChannelType.MessageDeleteBulk)
- ),
- ]
- );
-
public static (List, List) BuildRootMenu(
List guildChannels,
IGuild guild,
@@ -458,9 +357,208 @@ public class ChannelCommands(
List components =
[
- new ActionRowComponent([LogTypeSelect]),
new ActionRowComponent(
[
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Server changes",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildUpdate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Emoji changes",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildEmojisUpdate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "New roles",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildRoleCreate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Edited roles",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildRoleUpdate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Deleted roles",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildRoleDelete)
+ )
+ ),
+ ]
+ ),
+ new ActionRowComponent(
+ [
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "New channels",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.ChannelCreate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Edited channels",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.ChannelUpdate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Deleted channels",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.ChannelDelete)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Members joining",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildMemberAdd)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Members leaving",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildMemberRemove)
+ )
+ ),
+ ]
+ ),
+ new ActionRowComponent(
+ [
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Member role changes",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildMemberUpdate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Key role changes",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildKeyRoleUpdate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Member name changes",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildMemberNickUpdate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Member avatar changes",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildMemberAvatarUpdate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Timeouts",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildMemberTimeout)
+ )
+ ),
+ ]
+ ),
+ new ActionRowComponent(
+ [
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Kicks",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildMemberKick)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Bans",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildBanAdd)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Unbans",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.GuildBanRemove)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "New invites",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.InviteCreate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Deleted invites",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.InviteDelete)
+ )
+ ),
+ ]
+ ),
+ new ActionRowComponent(
+ [
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Edited messages",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.MessageUpdate)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Deleted messages",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.MessageDelete)
+ )
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Primary,
+ Label: "Bulk deleted messages",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ nameof(LogChannelType.MessageDeleteBulk)
+ )
+ ),
new ButtonComponent(
ButtonComponentStyle.Secondary,
Label: "Close",
diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs
index 1104c82..1bda457 100644
--- a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs
+++ b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs
@@ -45,117 +45,20 @@ public class ChannelCommandsComponents(
{
private readonly ILogger _logger = logger.ForContext();
- [SelectMenu("select-log-type")]
- [SuppressInteractionResponse(true)]
- public async Task OnMenuSelectionAsync(IReadOnlyList values)
- {
- if (contextInjection.Context is not IInteractionCommandContext ctx)
- return CataloggerError.Result("No context");
- if (!ctx.TryGetUserID(out var userId))
- return CataloggerError.Result("No user ID in context");
- if (!ctx.Interaction.Message.TryGet(out var msg))
- return CataloggerError.Result("No message ID in context");
- if (!ctx.TryGetGuildID(out var guildId))
- return CataloggerError.Result("No guild ID in context");
- if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
- var guildChannels = channelCache.GuildChannels(guildId).ToList();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- var result = await dataService.LeaseDataAsync(msg.ID);
- await using var lease = result.GetOrThrow();
- if (lease.Data.UserId != userId)
- {
- return (Result)
- await feedbackService.ReplyAsync(
- "This is not your configuration menu.",
- isEphemeral: true
- );
- }
-
- var state = values[0];
-
- if (!Enum.TryParse(state, out var logChannelType))
- return CataloggerError.Result($"Invalid config-channels state {state}");
-
- var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType);
- string? channelMention;
- if (channelId is 0)
- channelMention = null;
- else if (guildChannels.All(c => c.ID != channelId))
- channelMention = $"unknown channel {channelId}";
- else
- channelMention = $"<#{channelId}>";
-
- List embeds =
- [
- new Embed(
- Title: ChannelCommands.PrettyLogTypeName(logChannelType),
- Description: channelMention == null
- ? "This event is not currently logged.\nTo start logging it somewhere, select a channel below."
- : $"This event is currently set to log to {channelMention}."
- + "\nTo change where it is logged, select a channel below."
- + "\nTo disable logging this event entirely, select \"Stop logging\" below.",
- Colour: DiscordUtils.Purple
- ),
- ];
-
- List components =
- [
- new ActionRowComponent(
- new[]
- {
- new ChannelSelectComponent(
- CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"),
- ChannelTypes: new[] { ChannelType.GuildText }
- ),
- }
- ),
- new ActionRowComponent(
- new[]
- {
- new ButtonComponent(
- ButtonComponentStyle.Danger,
- Label: "Stop logging",
- CustomID: CustomIDHelpers.CreateButtonIDWithState(
- "config-channels",
- "reset"
- ),
- IsDisabled: channelMention == null
- ),
- new ButtonComponent(
- ButtonComponentStyle.Secondary,
- Label: "Return to menu",
- CustomID: CustomIDHelpers.CreateButtonIDWithState(
- "config-channels",
- "return"
- )
- ),
- }
- ),
- ];
-
- lease.Data = new ChannelCommandData(userId, CurrentPage: state);
- return await interactionApi.UpdateMessageAsync(
- ctx.Interaction,
- new InteractionMessageCallbackData(Embeds: embeds, Components: components)
- );
- }
-
[Button("config-channels")]
[SuppressInteractionResponse(true)]
public async Task OnButtonPressedAsync(string state)
{
if (contextInjection.Context is not IInteractionCommandContext ctx)
- return CataloggerError.Result("No context");
+ throw new CataloggerError("No context");
if (!ctx.TryGetUserID(out var userId))
- return CataloggerError.Result("No user ID in context");
+ throw new CataloggerError("No user ID in context");
if (!ctx.Interaction.Message.TryGet(out var msg))
- return CataloggerError.Result("No message ID in context");
+ throw new CataloggerError("No message ID in context");
if (!ctx.TryGetGuildID(out var guildId))
- return CataloggerError.Result("No guild ID in context");
+ throw new CataloggerError("No guild ID in context");
if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
+ throw new CataloggerError("Guild not in cache");
var guildChannels = channelCache.GuildChannels(guildId).ToList();
var guildConfig = await guildRepository.GetAsync(guildId);
@@ -179,9 +82,9 @@ public class ChannelCommandsComponents(
);
case "reset":
if (lease.Data.CurrentPage == null)
- return CataloggerError.Result("CurrentPage was null in reset button callback");
+ throw new CataloggerError("CurrentPage was null in reset button callback");
if (!Enum.TryParse(lease.Data.CurrentPage, out var channelType))
- return CataloggerError.Result(
+ throw new CataloggerError(
$"Invalid config-channels CurrentPage: '{lease.Data.CurrentPage}'"
);
@@ -261,7 +164,7 @@ public class ChannelCommandsComponents(
throw new ArgumentOutOfRangeException();
}
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
goto case "return";
case "return":
var (e, c) = ChannelCommands.BuildRootMenu(guildChannels, guild, guildConfig);
@@ -273,7 +176,71 @@ public class ChannelCommandsComponents(
return Result.Success;
}
- return Result.Success;
+ if (!Enum.TryParse(state, out var logChannelType))
+ throw new CataloggerError($"Invalid config-channels state {state}");
+
+ var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType);
+ string? channelMention;
+ if (channelId is 0)
+ channelMention = null;
+ else if (guildChannels.All(c => c.ID != channelId))
+ channelMention = $"unknown channel {channelId}";
+ else
+ channelMention = $"<#{channelId}>";
+
+ List embeds =
+ [
+ new Embed(
+ Title: ChannelCommands.PrettyLogTypeName(logChannelType),
+ Description: channelMention == null
+ ? "This event is not currently logged.\nTo start logging it somewhere, select a channel below."
+ : $"This event is currently set to log to {channelMention}."
+ + "\nTo change where it is logged, select a channel below."
+ + "\nTo disable logging this event entirely, select \"Stop logging\" below.",
+ Colour: DiscordUtils.Purple
+ ),
+ ];
+
+ List components =
+ [
+ new ActionRowComponent(
+ new[]
+ {
+ new ChannelSelectComponent(
+ CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"),
+ ChannelTypes: new[] { ChannelType.GuildText }
+ ),
+ }
+ ),
+ new ActionRowComponent(
+ new[]
+ {
+ new ButtonComponent(
+ ButtonComponentStyle.Danger,
+ Label: "Stop logging",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ "reset"
+ ),
+ IsDisabled: channelMention == null
+ ),
+ new ButtonComponent(
+ ButtonComponentStyle.Secondary,
+ Label: "Return to menu",
+ CustomID: CustomIDHelpers.CreateButtonIDWithState(
+ "config-channels",
+ "return"
+ )
+ ),
+ }
+ ),
+ ];
+
+ lease.Data = new ChannelCommandData(userId, CurrentPage: state);
+ return await interactionApi.UpdateMessageAsync(
+ ctx.Interaction,
+ new InteractionMessageCallbackData(Embeds: embeds, Components: components)
+ );
}
[SelectMenu("config-channels")]
@@ -281,15 +248,15 @@ public class ChannelCommandsComponents(
public async Task OnMenuSelectionAsync(IReadOnlyList channels)
{
if (contextInjection.Context is not IInteractionCommandContext ctx)
- return CataloggerError.Result("No context");
+ throw new CataloggerError("No context");
if (!ctx.TryGetUserID(out var userId))
- return CataloggerError.Result("No user ID in context");
+ throw new CataloggerError("No user ID in context");
if (!ctx.Interaction.Message.TryGet(out var msg))
- return CataloggerError.Result("No message ID in context");
+ throw new CataloggerError("No message ID in context");
if (!ctx.TryGetGuildID(out var guildId))
- return CataloggerError.Result("No guild ID in context");
+ throw new CataloggerError("No guild ID in context");
if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
+ throw new CataloggerError("Guild not in cache");
var guildConfig = await guildRepository.GetAsync(guildId);
var channelId = channels[0].ID.ToUlong();
@@ -305,7 +272,7 @@ public class ChannelCommandsComponents(
}
if (!Enum.TryParse(lease.Data.CurrentPage, out var channelType))
- return CataloggerError.Result(
+ throw new CataloggerError(
$"Invalid config-channels CurrentPage '{lease.Data.CurrentPage}'"
);
@@ -384,7 +351,7 @@ public class ChannelCommandsComponents(
throw new ArgumentOutOfRangeException();
}
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
List embeds =
[
diff --git a/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs b/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs
new file mode 100644
index 0000000..63dce5e
--- /dev/null
+++ b/Catalogger.Backend/Bot/Commands/IgnoreChannelCommands.cs
@@ -0,0 +1,210 @@
+// 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 .
+
+using System.ComponentModel;
+using Catalogger.Backend.Cache;
+using Catalogger.Backend.Cache.InMemoryCache;
+using Catalogger.Backend.Database.Repositories;
+using Catalogger.Backend.Extensions;
+using Catalogger.Backend.Services;
+using Remora.Commands.Attributes;
+using Remora.Commands.Groups;
+using Remora.Discord.API;
+using Remora.Discord.API.Abstractions.Objects;
+using Remora.Discord.Commands.Attributes;
+using Remora.Discord.Commands.Feedback.Services;
+using Remora.Discord.Commands.Services;
+using Remora.Discord.Extensions.Embeds;
+using Remora.Rest.Core;
+using IResult = Remora.Results.IResult;
+
+namespace Catalogger.Backend.Bot.Commands;
+
+[Group("ignored-channels")]
+[Description("Manage channels ignored for logging.")]
+[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
+public class IgnoreChannelCommands(
+ ILogger logger,
+ GuildRepository guildRepository,
+ IMemberCache memberCache,
+ GuildCache guildCache,
+ ChannelCache channelCache,
+ PermissionResolverService permissionResolver,
+ ContextInjectionService contextInjection,
+ FeedbackService feedbackService
+) : CommandGroup
+{
+ private readonly ILogger _logger = logger.ForContext();
+
+ [Command("add")]
+ [Description("Add a channel to the list of ignored channels.")]
+ public async Task AddIgnoredChannelAsync(
+ [ChannelTypes(
+ ChannelType.GuildCategory,
+ ChannelType.GuildText,
+ ChannelType.GuildAnnouncement,
+ ChannelType.GuildForum,
+ ChannelType.GuildMedia,
+ ChannelType.GuildVoice,
+ ChannelType.GuildStageVoice
+ )]
+ [Description("The channel to ignore")]
+ IChannel channel
+ )
+ {
+ var (_, guildId) = contextInjection.GetUserAndGuild();
+ var guildConfig = await guildRepository.GetAsync(guildId);
+
+ if (guildConfig.Channels.IgnoredChannels.Contains(channel.ID.Value))
+ return await feedbackService.ReplyAsync(
+ "That channel is already being ignored.",
+ isEphemeral: true
+ );
+
+ guildConfig.Channels.IgnoredChannels.Add(channel.ID.Value);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
+
+ return await feedbackService.ReplyAsync(
+ $"Successfully added {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} to the list of ignored channels."
+ );
+ }
+
+ [Command("remove")]
+ [Description("Remove a channel from the list of ignored channels.")]
+ public async Task RemoveIgnoredChannelAsync(
+ [Description("The channel to stop ignoring")] IChannel channel
+ )
+ {
+ var (_, guildId) = contextInjection.GetUserAndGuild();
+ var guildConfig = await guildRepository.GetAsync(guildId);
+
+ if (!guildConfig.Channels.IgnoredChannels.Contains(channel.ID.Value))
+ return await feedbackService.ReplyAsync(
+ "That channel is already not ignored.",
+ isEphemeral: true
+ );
+
+ guildConfig.Channels.IgnoredChannels.Remove(channel.ID.Value);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
+
+ return await feedbackService.ReplyAsync(
+ $"Successfully removed {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} from the list of ignored channels."
+ );
+ }
+
+ [Command("list")]
+ [Description("List channels ignored for logging.")]
+ public async Task ListIgnoredChannelsAsync()
+ {
+ var (userId, guildId) = contextInjection.GetUserAndGuild();
+ if (!guildCache.TryGet(guildId, out var guild))
+ throw new CataloggerError("Guild not in cache");
+
+ var guildChannels = channelCache.GuildChannels(guildId).ToList();
+ var guildConfig = await guildRepository.GetAsync(guildId);
+
+ var member = await memberCache.TryGetAsync(guildId, userId);
+ if (member == null)
+ throw new CataloggerError("Executing member not found");
+
+ var ignoredChannels = guildConfig
+ .Channels.IgnoredChannels.Select(id =>
+ {
+ var channel = guildChannels.FirstOrDefault(c => c.ID.Value == id);
+ if (channel == null)
+ return new IgnoredChannel(IgnoredChannelType.Unknown, DiscordSnowflake.New(id));
+
+ var type = channel.Type switch
+ {
+ ChannelType.GuildCategory => IgnoredChannelType.Category,
+ _ => IgnoredChannelType.Base,
+ };
+
+ return new IgnoredChannel(
+ type,
+ channel.ID,
+ permissionResolver
+ .GetChannelPermissions(guildId, member, channel)
+ .HasPermission(DiscordPermission.ViewChannel)
+ );
+ })
+ .ToList();
+
+ foreach (var ch in ignoredChannels)
+ {
+ _logger.Debug("Channel: {ChannelId}, type: {Type}", ch.Id, ch.Type);
+ }
+
+ var embed = new EmbedBuilder()
+ .WithTitle($"Ignored channels in {guild.Name}")
+ .WithColour(DiscordUtils.Purple);
+
+ var nonVisibleCategories = ignoredChannels.Count(c =>
+ c is { Type: IgnoredChannelType.Category, CanSee: false }
+ );
+ var visibleCategories = ignoredChannels
+ .Where(c => c is { Type: IgnoredChannelType.Category, CanSee: true })
+ .ToList();
+
+ if (nonVisibleCategories != 0 || visibleCategories.Count != 0)
+ {
+ var value = string.Join("\n", visibleCategories.Select(c => $"<#{c.Id}>"));
+ if (nonVisibleCategories != 0)
+ value +=
+ $"\n\n{nonVisibleCategories} channel(s) are not shown as you do not have access to them.";
+
+ embed.AddField("Categories", value);
+ }
+
+ var nonVisibleBase = ignoredChannels.Count(c =>
+ c is { Type: IgnoredChannelType.Base, CanSee: false }
+ );
+ var visibleBase = ignoredChannels
+ .Where(c => c is { Type: IgnoredChannelType.Base, CanSee: true })
+ .ToList();
+
+ if (nonVisibleBase != 0 || visibleBase.Count != 0)
+ {
+ var value = string.Join("\n", visibleBase.Select(c => $"<#{c.Id}>"));
+ if (nonVisibleBase != 0)
+ value +=
+ $"\n\n{nonVisibleBase} channel(s) are not shown as you do not have access to them.";
+
+ embed.AddField("Channels", value);
+ }
+
+ var unknownChannels = string.Join(
+ "\n",
+ ignoredChannels
+ .Where(c => c.Type == IgnoredChannelType.Unknown)
+ .Select(c => $"{c.Id} <#{c.Id}>")
+ );
+ if (!string.IsNullOrWhiteSpace(unknownChannels))
+ {
+ embed.AddField("Unknown", unknownChannels);
+ }
+
+ return await feedbackService.ReplyAsync(embeds: [embed.Build().GetOrThrow()]);
+ }
+
+ private record struct IgnoredChannel(IgnoredChannelType Type, Snowflake Id, bool CanSee = true);
+
+ private enum IgnoredChannelType
+ {
+ Unknown,
+ Base,
+ Category,
+ }
+}
diff --git a/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs b/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs
deleted file mode 100644
index 7e8987c..0000000
--- a/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs
+++ /dev/null
@@ -1,304 +0,0 @@
-// 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 .
-
-using System.ComponentModel;
-using Catalogger.Backend.Cache;
-using Catalogger.Backend.Cache.InMemoryCache;
-using Catalogger.Backend.Database.Repositories;
-using Catalogger.Backend.Extensions;
-using Catalogger.Backend.Services;
-using Remora.Commands.Attributes;
-using Remora.Commands.Groups;
-using Remora.Discord.API;
-using Remora.Discord.API.Abstractions.Objects;
-using Remora.Discord.Commands.Attributes;
-using Remora.Discord.Commands.Feedback.Services;
-using Remora.Discord.Commands.Services;
-using Remora.Discord.Extensions.Embeds;
-using Remora.Rest.Core;
-using IResult = Remora.Results.IResult;
-
-namespace Catalogger.Backend.Bot.Commands;
-
-[Group("ignore")]
-[Description("Manage the ignored channels and roles in this server.")]
-[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
-public class IgnoreEntitiesCommands : CommandGroup
-{
- [Group("role")]
- public class Roles(
- GuildRepository guildRepository,
- GuildCache guildCache,
- RoleCache roleCache,
- ContextInjectionService contextInjection,
- FeedbackService feedbackService
- ) : CommandGroup
- {
- [Command("add")]
- [Description("Add a role to the list of ignored roles.")]
- public async Task AddIgnoredRoleAsync(
- [Description("The role to ignore")] IRole role
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.IgnoredRoles.Contains(role.ID.Value))
- return await feedbackService.ReplyAsync(
- "That role is already being ignored.",
- isEphemeral: true
- );
-
- guildConfig.IgnoredRoles.Add(role.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully added {role.Name} to the list of ignored roles."
- );
- }
-
- [Command("remove")]
- [Description("Remove a role from the list of ignored roles.")]
- public async Task RemoveIgnoredRoleAsync(
- [Description("The role to stop ignoring")] IRole role
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (!guildConfig.IgnoredRoles.Contains(role.ID.Value))
- return await feedbackService.ReplyAsync(
- "That role is already not ignored.",
- isEphemeral: true
- );
-
- guildConfig.IgnoredRoles.Remove(role.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully removed {role.Name} from the list of ignored roles."
- );
- }
-
- [Command("list")]
- [Description("List roles ignored for logging.")]
- public async Task ListIgnoredRolesAsync()
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
-
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- var roles = roleCache
- .GuildRoles(guildId)
- .Where(r => guildConfig.IgnoredRoles.Contains(r.ID.Value))
- .OrderByDescending(r => r.Position)
- .Select(r => $"<@&{r.ID}>")
- .ToList();
- if (roles.Count == 0)
- return await feedbackService.ReplyAsync(
- "No roles are being ignored right now.",
- isEphemeral: true
- );
-
- return await feedbackService.ReplyAsync(
- embeds:
- [
- new EmbedBuilder()
- .WithTitle($"Ignored roles in {guild.Name}")
- .WithDescription(string.Join("\n", roles))
- .WithColour(DiscordUtils.Purple)
- .Build()
- .GetOrThrow(),
- ]
- );
- }
- }
-
- [Group("channel")]
- public class Channels(
- GuildRepository guildRepository,
- IMemberCache memberCache,
- GuildCache guildCache,
- ChannelCache channelCache,
- PermissionResolverService permissionResolver,
- ContextInjectionService contextInjection,
- FeedbackService feedbackService
- ) : CommandGroup
- {
- [Command("add")]
- [Description("Add a channel to the list of ignored channels.")]
- public async Task AddIgnoredChannelAsync(
- [ChannelTypes(
- ChannelType.GuildCategory,
- ChannelType.GuildText,
- ChannelType.GuildAnnouncement,
- ChannelType.GuildForum,
- ChannelType.GuildMedia,
- ChannelType.GuildVoice,
- ChannelType.GuildStageVoice
- )]
- [Description("The channel to ignore")]
- IChannel channel
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.IgnoredChannels.Contains(channel.ID.Value))
- return await feedbackService.ReplyAsync(
- "That channel is already being ignored.",
- isEphemeral: true
- );
-
- guildConfig.IgnoredChannels.Add(channel.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully added {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} to the list of ignored channels."
- );
- }
-
- [Command("remove")]
- [Description("Remove a channel from the list of ignored channels.")]
- public async Task RemoveIgnoredChannelAsync(
- [Description("The channel to stop ignoring")] IChannel channel
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (!guildConfig.IgnoredChannels.Contains(channel.ID.Value))
- return await feedbackService.ReplyAsync(
- "That channel is already not ignored.",
- isEphemeral: true
- );
-
- guildConfig.IgnoredChannels.Remove(channel.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully removed {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} from the list of ignored channels."
- );
- }
-
- [Command("list")]
- [Description("List channels ignored for logging.")]
- public async Task ListIgnoredChannelsAsync()
- {
- var (userId, guildId) = contextInjection.GetUserAndGuild();
- if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
-
- var guildChannels = channelCache.GuildChannels(guildId).ToList();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- var member = await memberCache.TryGetAsync(guildId, userId);
- if (member == null)
- return CataloggerError.Result("Executing member not found");
-
- var ignoredChannels = guildConfig
- .IgnoredChannels.Select(id =>
- {
- var channel = guildChannels.FirstOrDefault(c => c.ID.Value == id);
- if (channel == null)
- return new IgnoredChannel(
- IgnoredChannelType.Unknown,
- DiscordSnowflake.New(id)
- );
-
- var type = channel.Type switch
- {
- ChannelType.GuildCategory => IgnoredChannelType.Category,
- _ => IgnoredChannelType.Base,
- };
-
- return new IgnoredChannel(
- type,
- channel.ID,
- permissionResolver
- .GetChannelPermissions(guildId, member, channel)
- .HasPermission(DiscordPermission.ViewChannel)
- );
- })
- .ToList();
-
- var embed = new EmbedBuilder()
- .WithTitle($"Ignored channels in {guild.Name}")
- .WithColour(DiscordUtils.Purple);
-
- var nonVisibleCategories = ignoredChannels.Count(c =>
- c is { Type: IgnoredChannelType.Category, CanSee: false }
- );
- var visibleCategories = ignoredChannels
- .Where(c => c is { Type: IgnoredChannelType.Category, CanSee: true })
- .ToList();
-
- if (nonVisibleCategories != 0 || visibleCategories.Count != 0)
- {
- var value = string.Join("\n", visibleCategories.Select(c => $"<#{c.Id}>"));
- if (nonVisibleCategories != 0)
- value +=
- $"\n\n{nonVisibleCategories} channel(s) are not shown as you do not have access to them.";
-
- embed.AddField("Categories", value);
- }
-
- var nonVisibleBase = ignoredChannels.Count(c =>
- c is { Type: IgnoredChannelType.Base, CanSee: false }
- );
- var visibleBase = ignoredChannels
- .Where(c => c is { Type: IgnoredChannelType.Base, CanSee: true })
- .ToList();
-
- if (nonVisibleBase != 0 || visibleBase.Count != 0)
- {
- var value = string.Join("\n", visibleBase.Select(c => $"<#{c.Id}>"));
- if (nonVisibleBase != 0)
- value +=
- $"\n\n{nonVisibleBase} channel(s) are not shown as you do not have access to them.";
-
- embed.AddField("Channels", value);
- }
-
- var unknownChannels = string.Join(
- "\n",
- ignoredChannels
- .Where(c => c.Type == IgnoredChannelType.Unknown)
- .Select(c => $"{c.Id} <#{c.Id}>")
- );
- if (!string.IsNullOrWhiteSpace(unknownChannels))
- {
- embed.AddField("Unknown", unknownChannels);
- }
-
- return await feedbackService.ReplyAsync(embeds: [embed.Build().GetOrThrow()]);
- }
-
- private record struct IgnoredChannel(
- IgnoredChannelType Type,
- Snowflake Id,
- bool CanSee = true
- );
-
- private enum IgnoredChannelType
- {
- Unknown,
- Base,
- Category,
- }
- }
-}
diff --git a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs
deleted file mode 100644
index b61fabc..0000000
--- a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs
+++ /dev/null
@@ -1,213 +0,0 @@
-// 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 .
-
-using System.ComponentModel;
-using Catalogger.Backend.Cache;
-using Catalogger.Backend.Cache.InMemoryCache;
-using Catalogger.Backend.Database.Repositories;
-using Catalogger.Backend.Extensions;
-using Catalogger.Backend.Services;
-using Remora.Commands.Attributes;
-using Remora.Commands.Groups;
-using Remora.Discord.API;
-using Remora.Discord.API.Abstractions.Objects;
-using Remora.Discord.Commands.Attributes;
-using Remora.Discord.Commands.Feedback.Services;
-using Remora.Discord.Commands.Services;
-using Remora.Discord.Extensions.Embeds;
-using Remora.Rest.Core;
-using IResult = Remora.Results.IResult;
-
-namespace Catalogger.Backend.Bot.Commands;
-
-[Group("ignore-messages")]
-[Description("Manage users, roles, and channels whose messages are not logged.")]
-[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
-public partial class IgnoreMessageCommands : CommandGroup
-{
- [Group("channels")]
- public class Channels(
- GuildRepository guildRepository,
- IMemberCache memberCache,
- GuildCache guildCache,
- ChannelCache channelCache,
- PermissionResolverService permissionResolver,
- ContextInjectionService contextInjection,
- FeedbackService feedbackService
- ) : CommandGroup
- {
- [Command("add")]
- [Description("Add a channel to the list of ignored channels.")]
- public async Task AddIgnoredChannelAsync(
- [ChannelTypes(
- ChannelType.GuildCategory,
- ChannelType.GuildText,
- ChannelType.GuildAnnouncement,
- ChannelType.GuildForum,
- ChannelType.GuildMedia,
- ChannelType.GuildVoice,
- ChannelType.GuildStageVoice
- )]
- [Description("The channel to ignore")]
- IChannel channel
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.Messages.IgnoredChannels.Contains(channel.ID.Value))
- return await feedbackService.ReplyAsync(
- "That channel is already being ignored.",
- isEphemeral: true
- );
-
- guildConfig.Messages.IgnoredChannels.Add(channel.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully added {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} to the list of ignored channels."
- );
- }
-
- [Command("remove")]
- [Description("Remove a channel from the list of ignored channels.")]
- public async Task RemoveIgnoredChannelAsync(
- [Description("The channel to stop ignoring")] IChannel channel
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (!guildConfig.Messages.IgnoredChannels.Contains(channel.ID.Value))
- return await feedbackService.ReplyAsync(
- "That channel is already not ignored.",
- isEphemeral: true
- );
-
- guildConfig.Messages.IgnoredChannels.Remove(channel.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully removed {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} from the list of ignored channels."
- );
- }
-
- [Command("list")]
- [Description("List channels ignored for logging.")]
- public async Task ListIgnoredChannelsAsync()
- {
- var (userId, guildId) = contextInjection.GetUserAndGuild();
- if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
-
- var guildChannels = channelCache.GuildChannels(guildId).ToList();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- var member = await memberCache.TryGetAsync(guildId, userId);
- if (member == null)
- return CataloggerError.Result("Executing member not found");
-
- var ignoredChannels = guildConfig
- .Messages.IgnoredChannels.Select(id =>
- {
- var channel = guildChannels.FirstOrDefault(c => c.ID.Value == id);
- if (channel == null)
- return new IgnoredChannel(
- IgnoredChannelType.Unknown,
- DiscordSnowflake.New(id)
- );
-
- var type = channel.Type switch
- {
- ChannelType.GuildCategory => IgnoredChannelType.Category,
- _ => IgnoredChannelType.Base,
- };
-
- return new IgnoredChannel(
- type,
- channel.ID,
- permissionResolver
- .GetChannelPermissions(guildId, member, channel)
- .HasPermission(DiscordPermission.ViewChannel)
- );
- })
- .ToList();
-
- var embed = new EmbedBuilder()
- .WithTitle($"Ignored channels in {guild.Name}")
- .WithColour(DiscordUtils.Purple);
-
- var nonVisibleCategories = ignoredChannels.Count(c =>
- c is { Type: IgnoredChannelType.Category, CanSee: false }
- );
- var visibleCategories = ignoredChannels
- .Where(c => c is { Type: IgnoredChannelType.Category, CanSee: true })
- .ToList();
-
- if (nonVisibleCategories != 0 || visibleCategories.Count != 0)
- {
- var value = string.Join("\n", visibleCategories.Select(c => $"<#{c.Id}>"));
- if (nonVisibleCategories != 0)
- value +=
- $"\n\n{nonVisibleCategories} channel(s) are not shown as you do not have access to them.";
-
- embed.AddField("Categories", value);
- }
-
- var nonVisibleBase = ignoredChannels.Count(c =>
- c is { Type: IgnoredChannelType.Base, CanSee: false }
- );
- var visibleBase = ignoredChannels
- .Where(c => c is { Type: IgnoredChannelType.Base, CanSee: true })
- .ToList();
-
- if (nonVisibleBase != 0 || visibleBase.Count != 0)
- {
- var value = string.Join("\n", visibleBase.Select(c => $"<#{c.Id}>"));
- if (nonVisibleBase != 0)
- value +=
- $"\n\n{nonVisibleBase} channel(s) are not shown as you do not have access to them.";
-
- embed.AddField("Channels", value);
- }
-
- var unknownChannels = string.Join(
- "\n",
- ignoredChannels
- .Where(c => c.Type == IgnoredChannelType.Unknown)
- .Select(c => $"{c.Id} <#{c.Id}>")
- );
- if (!string.IsNullOrWhiteSpace(unknownChannels))
- {
- embed.AddField("Unknown", unknownChannels);
- }
-
- return await feedbackService.ReplyAsync(embeds: [embed.Build().GetOrThrow()]);
- }
-
- private record struct IgnoredChannel(
- IgnoredChannelType Type,
- Snowflake Id,
- bool CanSee = true
- );
-
- private enum IgnoredChannelType
- {
- Unknown,
- Base,
- Category,
- }
- }
-}
diff --git a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs
deleted file mode 100644
index 2cc46b7..0000000
--- a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-// 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 .
-
-using System.ComponentModel;
-using Catalogger.Backend.Cache.InMemoryCache;
-using Catalogger.Backend.Database.Repositories;
-using Catalogger.Backend.Extensions;
-using Remora.Commands.Attributes;
-using Remora.Commands.Groups;
-using Remora.Discord.API.Abstractions.Objects;
-using Remora.Discord.Commands.Feedback.Services;
-using Remora.Discord.Commands.Services;
-using Remora.Discord.Extensions.Embeds;
-using IResult = Remora.Results.IResult;
-
-namespace Catalogger.Backend.Bot.Commands;
-
-public partial class IgnoreMessageCommands
-{
- [Group("roles")]
- public class Roles(
- GuildRepository guildRepository,
- GuildCache guildCache,
- RoleCache roleCache,
- ContextInjectionService contextInjection,
- FeedbackService feedbackService
- ) : CommandGroup
- {
- [Command("add")]
- [Description("Add a role to the list of ignored roles.")]
- public async Task AddIgnoredRoleAsync(
- [Description("The role to ignore")] IRole role
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.Messages.IgnoredRoles.Contains(role.ID.Value))
- return await feedbackService.ReplyAsync(
- "That role is already being ignored.",
- isEphemeral: true
- );
-
- guildConfig.Messages.IgnoredRoles.Add(role.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully added {role.Name} to the list of ignored roles."
- );
- }
-
- [Command("remove")]
- [Description("Remove a role from the list of ignored roles.")]
- public async Task RemoveIgnoredRoleAsync(
- [Description("The role to stop ignoring")] IRole role
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (!guildConfig.Messages.IgnoredRoles.Contains(role.ID.Value))
- return await feedbackService.ReplyAsync(
- "That role is already not ignored.",
- isEphemeral: true
- );
-
- guildConfig.Messages.IgnoredRoles.Remove(role.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully removed {role.Name} from the list of ignored roles."
- );
- }
-
- [Command("list")]
- [Description("List roles ignored for logging.")]
- public async Task ListIgnoredRolesAsync()
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
-
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- var roles = roleCache
- .GuildRoles(guildId)
- .Where(r => guildConfig.Messages.IgnoredRoles.Contains(r.ID.Value))
- .OrderByDescending(r => r.Position)
- .Select(r => $"<@&{r.ID}>")
- .ToList();
- if (roles.Count == 0)
- return await feedbackService.ReplyAsync(
- "No roles are being ignored right now.",
- isEphemeral: true
- );
-
- return await feedbackService.ReplyAsync(
- embeds:
- [
- new EmbedBuilder()
- .WithTitle($"Ignored roles in {guild.Name}")
- .WithDescription(string.Join("\n", roles))
- .WithColour(DiscordUtils.Purple)
- .Build()
- .GetOrThrow(),
- ]
- );
- }
- }
-}
diff --git a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs
deleted file mode 100644
index 15ae280..0000000
--- a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs
+++ /dev/null
@@ -1,124 +0,0 @@
-// 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 .
-
-using System.ComponentModel;
-using Catalogger.Backend.Cache;
-using Catalogger.Backend.Cache.InMemoryCache;
-using Catalogger.Backend.Database.Repositories;
-using Catalogger.Backend.Extensions;
-using Remora.Commands.Attributes;
-using Remora.Commands.Groups;
-using Remora.Discord.API;
-using Remora.Discord.API.Abstractions.Objects;
-using Remora.Discord.Commands.Feedback.Services;
-using Remora.Discord.Commands.Services;
-using Remora.Discord.Pagination.Extensions;
-using Remora.Rest.Core;
-using IResult = Remora.Results.IResult;
-
-namespace Catalogger.Backend.Bot.Commands;
-
-public partial class IgnoreMessageCommands
-{
- [Group("users")]
- public class Users(
- GuildRepository guildRepository,
- IMemberCache memberCache,
- GuildCache guildCache,
- UserCache userCache,
- ContextInjectionService contextInjection,
- FeedbackService feedbackService
- ) : CommandGroup
- {
- [Command("add")]
- [Description("Add a user to the list of ignored users.")]
- public async Task AddIgnoredUserAsync(
- [Description("The user to ignore")] IUser user
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.Messages.IgnoredUsers.Contains(user.ID.Value))
- return await feedbackService.ReplyAsync(
- "That user is already being ignored.",
- isEphemeral: true
- );
-
- guildConfig.Messages.IgnoredUsers.Add(user.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully added {user.PrettyFormat()} to the list of ignored users."
- );
- }
-
- [Command("remove")]
- [Description("Remove a user from the list of ignored users.")]
- public async Task RemoveIgnoredUserAsync(
- [Description("The user to stop ignoring")] IUser user
- )
- {
- var (_, guildId) = contextInjection.GetUserAndGuild();
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (!guildConfig.Messages.IgnoredUsers.Contains(user.ID.Value))
- return await feedbackService.ReplyAsync(
- "That user is already not ignored.",
- isEphemeral: true
- );
-
- guildConfig.Messages.IgnoredUsers.Remove(user.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
-
- return await feedbackService.ReplyAsync(
- $"Successfully removed {user.PrettyFormat()} from the list of ignored users."
- );
- }
-
- [Command("list")]
- [Description("List currently ignored users.")]
- public async Task ListIgnoredUsersAsync()
- {
- var (userId, guildId) = contextInjection.GetUserAndGuild();
- if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
-
- var guildConfig = await guildRepository.GetAsync(guildId);
-
- if (guildConfig.Messages.IgnoredUsers.Count == 0)
- return await feedbackService.ReplyAsync("No users are being ignored right now.");
-
- var users = new List();
- foreach (var id in guildConfig.Messages.IgnoredUsers)
- {
- var user = await TryGetUserAsync(guildId, DiscordSnowflake.New(id));
- users.Add(user?.PrettyFormat() ?? $"*(unknown user {id})* <@{id}>");
- }
-
- return await feedbackService.SendContextualPaginatedMessageAsync(
- userId,
- DiscordUtils.PaginateStrings(
- users,
- $"Ignored users for {guild.Name} ({users.Count})"
- )
- );
- }
-
- private async Task TryGetUserAsync(Snowflake guildId, Snowflake userId) =>
- (await memberCache.TryGetAsync(guildId, userId))?.User.Value
- ?? await userCache.GetUserAsync(userId);
- }
-}
diff --git a/Catalogger.Backend/Bot/Commands/InviteCommands.cs b/Catalogger.Backend/Bot/Commands/InviteCommands.cs
index 6ec6991..77757f6 100644
--- a/Catalogger.Backend/Bot/Commands/InviteCommands.cs
+++ b/Catalogger.Backend/Bot/Commands/InviteCommands.cs
@@ -43,7 +43,6 @@ public class InviteCommands(
InviteRepository inviteRepository,
GuildCache guildCache,
IInviteCache inviteCache,
- UserCache userCache,
IDiscordRestChannelAPI channelApi,
IDiscordRestGuildAPI guildApi,
FeedbackService feedbackService,
@@ -59,7 +58,7 @@ public class InviteCommands(
var (userId, guildId) = contextInjection.GetUserAndGuild();
var guildInvites = await guildApi.GetGuildInvitesAsync(guildId).GetOrThrow();
if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
+ throw new CataloggerError("Guild not in cache");
var dbInvites = await inviteRepository.GetGuildInvitesAsync(guildId);
@@ -114,22 +113,21 @@ public class InviteCommands(
);
[Command("create")]
- [Description("Create a new invite.")]
+ [Description("Create a new invite.`")]
public async Task CreateInviteAsync(
[Description("The channel to create the invite in")]
[ChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)]
IChannel channel,
- [Description("What to name the new invite")] string? name = null,
- [Description("How long the invite should be valid for")] InviteDuration? duration = null
+ [Description("What to name the new invite")] string? name = null
)
{
var (userId, guildId) = contextInjection.GetUserAndGuild();
var inviteResult = await channelApi.CreateChannelInviteAsync(
channel.ID,
- maxAge: duration?.ToTimespan() ?? TimeSpan.Zero,
+ maxAge: TimeSpan.Zero,
isUnique: true,
- reason: $"Create invite command by {await userCache.TryFormatUserAsync(userId, addMention: false)}"
+ reason: $"Create invite command by {userId}"
);
if (inviteResult.Error != null)
{
@@ -146,20 +144,17 @@ public class InviteCommands(
);
}
- var durationText =
- duration != null ? $"\nThis invite {duration.ToHumanString()}." : string.Empty;
-
if (name == null)
return await feedbackService.ReplyAsync(
$"Created a new invite in <#{channel.ID}>!"
- + $"\nLink: https://discord.gg/{inviteResult.Entity.Code}{durationText}"
+ + $"\nLink: https://discord.gg/{inviteResult.Entity.Code}"
);
await inviteRepository.SetInviteNameAsync(guildId, inviteResult.Entity.Code, name);
return await feedbackService.ReplyAsync(
$"Created a new invite in <#{channel.ID}> with the name **{name}**!"
- + $"\nLink: https://discord.gg/{inviteResult.Entity.Code}{durationText}"
+ + $"\nLink: https://discord.gg/{inviteResult.Entity.Code}"
);
}
@@ -258,51 +253,3 @@ public class InviteAutocompleteProvider(
.ToList();
}
}
-
-public enum InviteDuration
-{
- [Description("30 minutes")]
- ThirtyMinutes,
-
- [Description("1 hour")]
- OneHour,
-
- [Description("6 hours")]
- SixHours,
-
- [Description("12 hours")]
- TwelveHours,
-
- [Description("1 day")]
- OneDay,
-
- [Description("1 week")]
- OneWeek,
-}
-
-internal static class InviteEnumExtensions
-{
- internal static TimeSpan ToTimespan(this InviteDuration dur) =>
- dur switch
- {
- InviteDuration.ThirtyMinutes => TimeSpan.FromMinutes(30),
- InviteDuration.OneHour => TimeSpan.FromHours(1),
- InviteDuration.SixHours => TimeSpan.FromHours(6),
- InviteDuration.TwelveHours => TimeSpan.FromHours(12),
- InviteDuration.OneDay => TimeSpan.FromDays(1),
- InviteDuration.OneWeek => TimeSpan.FromDays(7),
- _ => TimeSpan.Zero,
- };
-
- internal static string ToHumanString(this InviteDuration? dur) =>
- dur switch
- {
- InviteDuration.ThirtyMinutes => "expires after 30 minutes",
- InviteDuration.OneHour => "expires after 1 hour",
- InviteDuration.SixHours => "expires after 6 hours",
- InviteDuration.TwelveHours => "expires after 12 hours",
- InviteDuration.OneDay => "expires after 1 day",
- InviteDuration.OneWeek => "expires after 1 week",
- _ => "does not expire",
- };
-}
diff --git a/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs b/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs
index dd2ff90..67d3ab8 100644
--- a/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs
+++ b/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs
@@ -45,11 +45,11 @@ public class KeyRoleCommands(
{
var (_, guildId) = contextInjection.GetUserAndGuild();
if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
+ throw new CataloggerError("Guild not in cache");
var guildRoles = roleCache.GuildRoles(guildId).ToList();
var guildConfig = await guildRepository.GetAsync(guildId);
- if (guildConfig.KeyRoles.Count == 0)
+ if (guildConfig.KeyRoles.Length == 0)
return await feedbackService.ReplyAsync(
"There are no key roles to list. Add some with `/key-roles add`.",
isEphemeral: true
@@ -76,16 +76,13 @@ public class KeyRoleCommands(
[Command("add")]
[Description("Add a new key role.")]
public async Task AddKeyRoleAsync(
- [Option("role")]
- [Description("The role to add.")]
- [DiscordTypeHint(TypeHint.Role)]
- Snowflake roleId
+ [Description("The role to add.")] [DiscordTypeHint(TypeHint.Role)] Snowflake roleId
)
{
var (_, guildId) = contextInjection.GetUserAndGuild();
var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId);
if (role == null)
- return CataloggerError.Result("Role is not cached");
+ throw new CataloggerError("Role is not cached");
var guildConfig = await guildRepository.GetAsync(guildId);
if (guildConfig.KeyRoles.Any(id => role.ID.Value == id))
@@ -94,24 +91,20 @@ public class KeyRoleCommands(
isEphemeral: true
);
- guildConfig.KeyRoles.Add(role.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ await guildRepository.AddKeyRoleAsync(guildId, role.ID);
return await feedbackService.ReplyAsync($"Added {role.Name} to this server's key roles!");
}
[Command("remove")]
[Description("Remove a key role.")]
public async Task RemoveKeyRoleAsync(
- [Option("role")]
- [Description("The role to remove.")]
- [DiscordTypeHint(TypeHint.Role)]
- Snowflake roleId
+ [Description("The role to remove.")] [DiscordTypeHint(TypeHint.Role)] Snowflake roleId
)
{
var (_, guildId) = contextInjection.GetUserAndGuild();
var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId);
if (role == null)
- return CataloggerError.Result("Role is not cached");
+ throw new CataloggerError("Role is not cached");
var guildConfig = await guildRepository.GetAsync(guildId);
if (guildConfig.KeyRoles.All(id => role.ID != id))
@@ -120,8 +113,7 @@ public class KeyRoleCommands(
isEphemeral: true
);
- guildConfig.KeyRoles.Remove(role.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ await guildRepository.RemoveKeyRoleAsync(guildId, role.ID);
return await feedbackService.ReplyAsync(
$"Removed {role.Name} from this server's key roles!"
);
diff --git a/Catalogger.Backend/Bot/Commands/MetaCommands.cs b/Catalogger.Backend/Bot/Commands/MetaCommands.cs
index 8b543af..8f516f1 100644
--- a/Catalogger.Backend/Bot/Commands/MetaCommands.cs
+++ b/Catalogger.Backend/Bot/Commands/MetaCommands.cs
@@ -176,7 +176,7 @@ public class MetaCommands(
var embed = new EmbedBuilder()
.WithColour(DiscordUtils.Purple)
.WithFooter(
- $"{BuildInfo.Version}, {RuntimeInformation.FrameworkDescription} on {RuntimeInformation.RuntimeIdentifier}"
+ $"{RuntimeInformation.FrameworkDescription} on {RuntimeInformation.RuntimeIdentifier}"
)
.WithCurrentTimestamp();
embed.AddField(
@@ -209,7 +209,8 @@ public class MetaCommands(
"Numbers",
$"{CataloggerMetrics.MessagesStored.Value:N0} messages "
+ $"from {guildCache.Size:N0} servers\n"
- + $"Cached {channelCache.Size:N0} channels, {roleCache.Size:N0} roles, {emojiCache.Size:N0} emojis"
+ + $"Cached {channelCache.Size:N0} channels, {roleCache.Size:N0} roles, {emojiCache.Size:N0} emojis",
+ false
);
IEmbed[] embeds = [embed.Build().GetOrThrow()];
@@ -218,7 +219,7 @@ public class MetaCommands(
await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds);
}
- // TODO: add more checks around response format
+ // TODO: add more checks around response format, configurable prometheus endpoint
private async Task MessagesRate()
{
if (!config.Logging.EnableMetrics)
@@ -227,8 +228,7 @@ public class MetaCommands(
try
{
var query = HttpUtility.UrlEncode("increase(catalogger_received_messages[5m])");
- var prometheusUrl = config.Logging.PrometheusUrl ?? "http://localhost:9090";
- var resp = await _client.GetAsync($"{prometheusUrl}/api/v1/query?query={query}");
+ var resp = await _client.GetAsync($"http://localhost:9090/api/v1/query?query={query}");
resp.EnsureSuccessStatusCode();
var data = await resp.Content.ReadFromJsonAsync();
diff --git a/Catalogger.Backend/Bot/Commands/RedirectCommands.cs b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs
index 3864c54..c776675 100644
--- a/Catalogger.Backend/Bot/Commands/RedirectCommands.cs
+++ b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs
@@ -61,7 +61,7 @@ public class RedirectCommands(
var (_, guildId) = contextInjectionService.GetUserAndGuild();
var guildConfig = await guildRepository.GetAsync(guildId);
guildConfig.Channels.Redirects[source.ID.Value] = target.ID.Value;
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
var output =
$"Success! Edited and deleted messages from {FormatChannel(source)} will now be redirected to <#{target.ID}>.";
@@ -101,7 +101,7 @@ public class RedirectCommands(
var guildConfig = await guildRepository.GetAsync(guildId);
var wasSet = guildConfig.Channels.Redirects.Remove(source.ID.Value);
- await guildRepository.UpdateConfigAsync(guildId, guildConfig);
+ await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
var output = wasSet
? $"Removed the redirect for {FormatChannel(source)}! Message logs from"
@@ -141,7 +141,7 @@ public class RedirectCommands(
{
var (userId, guildId) = contextInjectionService.GetUserAndGuild();
if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
+ throw new CataloggerError("Guild was not cached");
var guildChannels = channelCache.GuildChannels(guildId).ToList();
var guildConfig = await guildRepository.GetAsync(guildId);
diff --git a/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs b/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs
deleted file mode 100644
index c92a886..0000000
--- a/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-// 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 .
-
-using System.ComponentModel;
-using Catalogger.Backend.Cache;
-using Catalogger.Backend.Cache.InMemoryCache;
-using Catalogger.Backend.Database.Models;
-using Catalogger.Backend.Database.Repositories;
-using Catalogger.Backend.Extensions;
-using Remora.Commands.Attributes;
-using Remora.Commands.Groups;
-using Remora.Discord.API;
-using Remora.Discord.API.Abstractions.Objects;
-using Remora.Discord.API.Objects;
-using Remora.Discord.Commands.Attributes;
-using Remora.Discord.Commands.Feedback.Services;
-using Remora.Discord.Commands.Services;
-using Remora.Discord.Pagination.Extensions;
-using Remora.Rest.Core;
-using IResult = Remora.Results.IResult;
-
-namespace Catalogger.Backend.Bot.Commands;
-
-[Group("watchlist")]
-[Description("Commands for managing the server's watchlist.")]
-[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
-public class WatchlistCommands(
- WatchlistRepository watchlistRepository,
- GuildCache guildCache,
- IMemberCache memberCache,
- UserCache userCache,
- ContextInjectionService contextInjectionService,
- FeedbackService feedbackService
-) : CommandGroup
-{
- [Command("add")]
- [Description("Add a user to the watchlist.")]
- public async Task AddAsync(
- [Description("The user to add")] IUser user,
- [Description("The reason for adding this user to the watchlist")] string reason
- )
- {
- var (userId, guildId) = contextInjectionService.GetUserAndGuild();
-
- var entry = await watchlistRepository.CreateEntryAsync(guildId, user.ID, userId, reason);
- return await feedbackService.ReplyAsync(
- $"Added {user.PrettyFormat()} to this server's watchlist, with the following reason:\n>>> {entry.Reason}"
- );
- }
-
- [Command("remove")]
- [Description("Remove a user from the watchlist.")]
- public async Task RemoveAsync([Description("The user to remove")] IUser user)
- {
- var (userId, guildId) = contextInjectionService.GetUserAndGuild();
- if (!await watchlistRepository.RemoveEntryAsync(guildId, user.ID))
- {
- return await feedbackService.ReplyAsync(
- $"{user.PrettyFormat()} is not on the watchlist, so you can't remove them from it."
- );
- }
-
- return await feedbackService.ReplyAsync(
- $"Removed {user.PrettyFormat()} from the watchlist!"
- );
- }
-
- [Command("show")]
- [Description("Show the current watchlist.")]
- public async Task ShowAsync()
- {
- var (userId, guildId) = contextInjectionService.GetUserAndGuild();
- if (!guildCache.TryGet(guildId, out var guild))
- return CataloggerError.Result("Guild not in cache");
-
- var watchlist = await watchlistRepository.GetGuildWatchlistAsync(guildId);
- if (watchlist.Count == 0)
- return await feedbackService.ReplyAsync(
- "There are no entries on the watchlist right now."
- );
-
- var fields = new List();
- foreach (var entry in watchlist)
- fields.Add(await GenerateWatchlistEntryFieldAsync(guildId, entry));
-
- return await feedbackService.SendContextualPaginatedMessageAsync(
- userId,
- DiscordUtils.PaginateFields(
- fields,
- title: $"Watchlist for {guild.Name} ({fields.Count})",
- fieldsPerPage: 5
- )
- );
- }
-
- private async Task GenerateWatchlistEntryFieldAsync(
- Snowflake guildId,
- Watchlist entry
- )
- {
- var user = await TryGetUserAsync(guildId, DiscordSnowflake.New(entry.UserId));
- var fieldName = user != null ? user.Tag() : $"unknown user {entry.UserId}";
-
- var moderator = await TryGetUserAsync(guildId, DiscordSnowflake.New(entry.ModeratorId));
- var modName =
- moderator != null
- ? moderator.PrettyFormat()
- : $"*(unknown user {entry.ModeratorId})* <@{entry.ModeratorId}>";
-
- return new EmbedField(
- Name: fieldName,
- Value: $"""
- **Moderator:** {modName}
- **Added:**
- **Reason:**
- >>> {entry.Reason}
- """
- );
- }
-
- private async Task TryGetUserAsync(Snowflake guildId, Snowflake userId) =>
- (await memberCache.TryGetAsync(guildId, userId))?.User.Value
- ?? await userCache.GetUserAsync(userId);
-}
diff --git a/Catalogger.Backend/Bot/DiscordUtils.cs b/Catalogger.Backend/Bot/DiscordUtils.cs
index b6512d1..8e0a867 100644
--- a/Catalogger.Backend/Bot/DiscordUtils.cs
+++ b/Catalogger.Backend/Bot/DiscordUtils.cs
@@ -44,28 +44,4 @@ public static class DiscordUtils
description,
new Embed(Title: title, Colour: Purple)
);
-
- public static List