Compare commits
No commits in common. "main" and "v2024.11.5" have entirely different histories.
main
...
v2024.11.5
54 changed files with 275 additions and 557 deletions
|
|
@ -3,14 +3,14 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "0.30.6",
|
||||
"version": "0.30.1",
|
||||
"commands": [
|
||||
"dotnet-csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
},
|
||||
"husky": {
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.1",
|
||||
"commands": [
|
||||
"husky"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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<IResult> 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,15 +50,15 @@ public class ChannelCommandsComponents(
|
|||
public async Task<Result> OnMenuSelectionAsync(IReadOnlyList<string> values)
|
||||
{
|
||||
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);
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ public class ChannelCommandsComponents(
|
|||
var state = values[0];
|
||||
|
||||
if (!Enum.TryParse<LogChannelType>(state, out var logChannelType))
|
||||
return CataloggerError.Result($"Invalid config-channels state {state}");
|
||||
throw new CataloggerError($"Invalid config-channels state {state}");
|
||||
|
||||
var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType);
|
||||
string? channelMention;
|
||||
|
|
@ -147,15 +147,15 @@ public class ChannelCommandsComponents(
|
|||
public async Task<Result> 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 +179,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<LogChannelType>(lease.Data.CurrentPage, out var channelType))
|
||||
return CataloggerError.Result(
|
||||
throw new CataloggerError(
|
||||
$"Invalid config-channels CurrentPage: '{lease.Data.CurrentPage}'"
|
||||
);
|
||||
|
||||
|
|
@ -281,15 +281,15 @@ public class ChannelCommandsComponents(
|
|||
public async Task<Result> OnMenuSelectionAsync(IReadOnlyList<IPartialChannel> 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 +305,7 @@ public class ChannelCommandsComponents(
|
|||
}
|
||||
|
||||
if (!Enum.TryParse<LogChannelType>(lease.Data.CurrentPage, out var channelType))
|
||||
return CataloggerError.Result(
|
||||
throw new CataloggerError(
|
||||
$"Invalid config-channels CurrentPage '{lease.Data.CurrentPage}'"
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ public class IgnoreEntitiesCommands : CommandGroup
|
|||
{
|
||||
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 guildConfig = await guildRepository.GetAsync(guildId);
|
||||
|
||||
|
|
@ -201,14 +201,14 @@ public class IgnoreEntitiesCommands : CommandGroup
|
|||
{
|
||||
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);
|
||||
|
||||
var member = await memberCache.TryGetAsync(guildId, userId);
|
||||
if (member == null)
|
||||
return CataloggerError.Result("Executing member not found");
|
||||
throw new CataloggerError("Executing member not found");
|
||||
|
||||
var ignoredChannels = guildConfig
|
||||
.IgnoredChannels.Select(id =>
|
||||
|
|
|
|||
|
|
@ -110,14 +110,14 @@ public partial class IgnoreMessageCommands : CommandGroup
|
|||
{
|
||||
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);
|
||||
|
||||
var member = await memberCache.TryGetAsync(guildId, userId);
|
||||
if (member == null)
|
||||
return CataloggerError.Result("Executing member not found");
|
||||
throw new CataloggerError("Executing member not found");
|
||||
|
||||
var ignoredChannels = guildConfig
|
||||
.Messages.IgnoredChannels.Select(id =>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public partial class IgnoreMessageCommands
|
|||
{
|
||||
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 guildConfig = await guildRepository.GetAsync(guildId);
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ public partial class IgnoreMessageCommands
|
|||
{
|
||||
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
||||
if (!guildCache.TryGet(guildId, out var guild))
|
||||
return CataloggerError.Result("Guild not in cache");
|
||||
throw new CataloggerError("Guild was not cached");
|
||||
|
||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,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);
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ 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);
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ public class KeyRoleCommands(
|
|||
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))
|
||||
|
|
@ -111,7 +111,7 @@ public class KeyRoleCommands(
|
|||
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))
|
||||
|
|
|
|||
|
|
@ -218,7 +218,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<double?> MessagesRate()
|
||||
{
|
||||
if (!config.Logging.EnableMetrics)
|
||||
|
|
@ -227,8 +227,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<PrometheusResponse>();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ public class WatchlistCommands(
|
|||
{
|
||||
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 watchlist = await watchlistRepository.GetGuildWatchlistAsync(guildId);
|
||||
if (watchlist.Count == 0)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ using Catalogger.Backend.Cache.InMemoryCache;
|
|||
using Catalogger.Backend.Database.Repositories;
|
||||
using Catalogger.Backend.Extensions;
|
||||
using Catalogger.Backend.Services;
|
||||
using Microsoft.Extensions.Logging.Configuration;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
|
|
@ -35,8 +36,6 @@ public class ChannelCreateResponder(
|
|||
{
|
||||
public async Task<Result> RespondAsync(IChannelCreate ch, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(ch);
|
||||
|
||||
if (!ch.GuildID.IsDefined())
|
||||
return Result.Success;
|
||||
channelCache.Set(ch);
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ public class ChannelDeleteResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IChannelDelete evt, CancellationToken ct = default)
|
||||
{
|
||||
using var __ = LogUtils.Enrich(evt);
|
||||
|
||||
if (!evt.GuildID.IsDefined())
|
||||
{
|
||||
_logger.Debug("Deleted channel {ChannelId} is not in a guild", evt.ID);
|
||||
|
|
|
|||
|
|
@ -40,8 +40,6 @@ public class ChannelUpdateResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IChannelUpdate evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
try
|
||||
{
|
||||
if (!channelCache.TryGet(evt.ID, out var oldChannel))
|
||||
|
|
|
|||
|
|
@ -13,27 +13,21 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using Catalogger.Backend.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Remora.Commands.Services;
|
||||
using Remora.Commands.Tokenization;
|
||||
using Remora.Commands.Trees;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.API.Objects;
|
||||
using Remora.Discord.Commands.Responders;
|
||||
using Remora.Discord.Commands.Services;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
using Serilog.Context;
|
||||
|
||||
namespace Catalogger.Backend.Bot.Responders;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for Remora.Discord's default interaction responder, that ignores all events if test mode is enabled,
|
||||
/// and handles <see cref="CataloggerError" /> results returned by commands.
|
||||
/// Wrapper for Remora.Discord's default interaction responder, that ignores all events if test mode is enabled.
|
||||
/// </summary>
|
||||
public class CustomInteractionResponder(
|
||||
Config config,
|
||||
|
|
@ -63,66 +57,21 @@ public class CustomInteractionResponder(
|
|||
treeNameResolver
|
||||
);
|
||||
|
||||
public async Task<Result> RespondAsync(IInteractionCreate evt, CancellationToken ct = default)
|
||||
public async Task<Result> RespondAsync(
|
||||
IInteractionCreate gatewayEvent,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
if (config.Discord.TestMode)
|
||||
{
|
||||
_logger.Information(
|
||||
"Not responding to interaction create event {InteractionId} in {ChannelId} as test mode is enabled",
|
||||
evt.ID,
|
||||
evt.Channel.Map(c => c.ID).OrDefault()
|
||||
gatewayEvent.ID,
|
||||
gatewayEvent.Channel.Map(c => c.ID).OrDefault()
|
||||
);
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
using var _ = LogUtils.PushProperties(
|
||||
("Event", nameof(IInteractionCreate)),
|
||||
("InteractionId", evt.ID),
|
||||
("GuildId", evt.GuildID),
|
||||
("UserId", evt.User.Map(u => u.ID)),
|
||||
("MemberId", evt.Member.Map(m => m.User.Map(u => u.ID).OrDefault())),
|
||||
("ChannelId", evt.Channel.Map(c => c.ID)),
|
||||
("InteractionType", evt.Type)
|
||||
);
|
||||
|
||||
using var __ = LogContext.PushProperty(
|
||||
"InteractionData",
|
||||
evt.Data.HasValue ? (object?)evt.Data.Value : null,
|
||||
true
|
||||
);
|
||||
|
||||
var result = await _inner.RespondAsync(evt, ct);
|
||||
if (result.Error is not CataloggerError cataloggerError)
|
||||
return result;
|
||||
|
||||
return await interactionAPI.CreateInteractionResponseAsync(
|
||||
evt.ID,
|
||||
evt.Token,
|
||||
new InteractionResponse(
|
||||
Type: InteractionCallbackType.ChannelMessageWithSource,
|
||||
Data: new Optional<OneOf.OneOf<
|
||||
IInteractionMessageCallbackData,
|
||||
IInteractionAutocompleteCallbackData,
|
||||
IInteractionModalCallbackData
|
||||
>>(
|
||||
new InteractionMessageCallbackData(
|
||||
Embeds: new Optional<IReadOnlyList<IEmbed>>(
|
||||
[
|
||||
new Embed(
|
||||
Colour: DiscordUtils.Red,
|
||||
Title: "Something went wrong",
|
||||
Description: $"""
|
||||
Something went wrong while running this command.
|
||||
> {cataloggerError.Message}
|
||||
Please try again later.
|
||||
"""
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
ct: ct
|
||||
);
|
||||
return await _inner.RespondAsync(gatewayEvent, ct);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using Catalogger.Backend.Cache.InMemoryCache;
|
||||
using Catalogger.Backend.Extensions;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
|
|
@ -29,8 +28,6 @@ public class AuditLogResponder(AuditLogCache auditLogCache, ILogger logger)
|
|||
|
||||
public Task<Result> RespondAsync(IGuildAuditLogEntryCreate evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
if (evt.TargetID == null || evt.UserID == null)
|
||||
return Task.FromResult(Result.Success);
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ public class GuildBanAddResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildBanAdd evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
var guildConfig = await guildRepository.GetAsync(evt.GuildID);
|
||||
|
||||
// Delay 2 seconds for the audit log
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ public class GuildBanRemoveResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildBanRemove evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
var guildConfig = await guildRepository.GetAsync(evt.GuildID);
|
||||
|
||||
// Delay 2 seconds for the audit log
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@ public class GuildCreateResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildCreate evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
ulong guildId;
|
||||
string? guildName = null;
|
||||
if (evt.Guild.TryPickT0(out var guild, out var unavailableGuild))
|
||||
|
|
@ -103,8 +101,6 @@ public class GuildCreateResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildDelete evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
if (evt.IsUnavailable.OrDefault(false))
|
||||
{
|
||||
_logger.Debug("Guild {GuildId} became unavailable", evt.ID);
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ public class GuildEmojisUpdateResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildEmojisUpdate evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
try
|
||||
{
|
||||
if (!emojiCache.TryGet(evt.GuildID, out var oldEmoji))
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using Catalogger.Backend.Cache;
|
||||
using Catalogger.Backend.Extensions;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
|
@ -28,8 +27,6 @@ public class GuildMembersChunkResponder(ILogger logger, IMemberCache memberCache
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildMembersChunk evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
_logger.Debug(
|
||||
"Received chunk {ChunkIndex} / {ChunkCount} for guild {GuildId}",
|
||||
evt.ChunkIndex + 1,
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ public class GuildUpdateResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildUpdate evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
try
|
||||
{
|
||||
if (!guildCache.TryGet(evt.ID, out var oldGuild))
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ public class InviteCreateResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IInviteCreate evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
var guildId = evt.GuildID.Value;
|
||||
|
||||
var invitesResult = await guildApi.GetGuildInvitesAsync(guildId, ct);
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ public class InviteDeleteResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IInviteDelete evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
var guildId = evt.GuildID.Value;
|
||||
|
||||
var dbDeleteCount = await inviteRepository.DeleteInviteAsync(guildId, evt.Code);
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ public class GuildMemberAddResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildMemberAdd member, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(member);
|
||||
|
||||
await memberCache.SetAsync(member.GuildID, member);
|
||||
await memberCache.SetMemberNamesAsync(member.GuildID, [member]);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,6 @@ public class GuildMemberRemoveResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildMemberRemove evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
try
|
||||
{
|
||||
var embed = new EmbedBuilder()
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ public class GuildMemberUpdateResponder(
|
|||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(newMember);
|
||||
|
||||
try
|
||||
{
|
||||
var oldMember = await memberCache.TryGetAsync(newMember.GuildID, newMember.User.ID);
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public class MessageCreateResponder(
|
|||
return Result.Success;
|
||||
}
|
||||
|
||||
await messageRepository.SaveMessageAsync(msg, msg.GuildID, ct);
|
||||
await messageRepository.SaveMessageAsync(msg, ct);
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
|
@ -144,19 +144,6 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services)
|
|||
await using var messageRepository =
|
||||
scope.ServiceProvider.GetRequiredService<MessageRepository>();
|
||||
|
||||
if (await messageRepository.IsMessageIgnoredAsync(originalId))
|
||||
{
|
||||
_logger.Debug(
|
||||
"Proxied message {MessageId} should be ignored as trigger {OriginalId} is already ignored",
|
||||
msgId,
|
||||
originalId
|
||||
);
|
||||
|
||||
await messageRepository.IgnoreMessageAsync(originalId);
|
||||
await messageRepository.IgnoreMessageAsync(msgId);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug(
|
||||
"Setting proxy data for {MessageId} and ignoring {OriginalId}",
|
||||
msgId,
|
||||
|
|
@ -208,19 +195,6 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services)
|
|||
pkMessage.Original
|
||||
);
|
||||
|
||||
if (await messageRepository.IsMessageIgnoredAsync(pkMessage.Original))
|
||||
{
|
||||
_logger.Debug(
|
||||
"Proxied message {MessageId} should be ignored as trigger {OriginalId} is already ignored",
|
||||
pkMessage.Id,
|
||||
pkMessage.Original
|
||||
);
|
||||
|
||||
await messageRepository.IgnoreMessageAsync(pkMessage.Original);
|
||||
await messageRepository.IgnoreMessageAsync(msgId);
|
||||
return;
|
||||
}
|
||||
|
||||
await messageRepository.SetProxiedMessageDataAsync(
|
||||
msgId,
|
||||
pkMessage.Original,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ using Catalogger.Backend.Database.Repositories;
|
|||
using Catalogger.Backend.Extensions;
|
||||
using Catalogger.Backend.Services;
|
||||
using Humanizer;
|
||||
using NodaTime;
|
||||
using Remora.Discord.API;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
|
|
@ -26,6 +27,7 @@ using Remora.Discord.Extensions.Embeds;
|
|||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
using Serilog.Context;
|
||||
|
||||
namespace Catalogger.Backend.Bot.Responders.Messages;
|
||||
|
||||
|
|
@ -36,6 +38,7 @@ public class MessageDeleteResponder(
|
|||
WebhookExecutorService webhookExecutor,
|
||||
ChannelCache channelCache,
|
||||
UserCache userCache,
|
||||
IClock clock,
|
||||
PluralkitApiService pluralkitApi
|
||||
) : IResponder<IMessageDelete>
|
||||
{
|
||||
|
|
@ -78,8 +81,8 @@ public class MessageDeleteResponder(
|
|||
new Embed(
|
||||
Title: "Message deleted",
|
||||
Description: $"A message not found in the database was deleted in <#{evt.ChannelID}> ({evt.ChannelID}).",
|
||||
Footer: new EmbedFooter(Text: $"ID: {evt.ID} | Original sent at"),
|
||||
Timestamp: evt.ID.Timestamp
|
||||
Footer: new EmbedFooter(Text: $"ID: {evt.ID}"),
|
||||
Timestamp: clock.GetCurrentInstant().ToDateTimeOffset()
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -121,7 +124,7 @@ public class MessageDeleteResponder(
|
|||
.WithTitle("Message deleted")
|
||||
.WithDescription(msg.Content)
|
||||
.WithColour(DiscordUtils.Red)
|
||||
.WithFooter($"ID: {msg.Id} | Original sent at")
|
||||
.WithFooter($"ID: {msg.Id}")
|
||||
.WithTimestamp(evt.ID);
|
||||
|
||||
if (user != null)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ using Catalogger.Backend.Services;
|
|||
using Remora.Discord.API;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.API.Gateway.Events;
|
||||
using Remora.Discord.API.Objects;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
|
|
@ -39,9 +40,13 @@ public class MessageUpdateResponder(
|
|||
{
|
||||
private readonly ILogger _logger = logger.ForContext<MessageUpdateResponder>();
|
||||
|
||||
public async Task<Result> RespondAsync(IMessageUpdate msg, CancellationToken ct = default)
|
||||
public async Task<Result> RespondAsync(IMessageUpdate evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(msg);
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
// Discord only *very* recently changed message update events to have all fields,
|
||||
// so we convert the event to a MessageCreate to avoid having to unwrap every single field
|
||||
var msg = ConvertToMessageCreate(evt);
|
||||
|
||||
if (!msg.GuildID.IsDefined())
|
||||
{
|
||||
|
|
@ -129,7 +134,7 @@ public class MessageUpdateResponder(
|
|||
if (oldMessage is { System: not null, Member: not null })
|
||||
{
|
||||
embedBuilder.WithTitle($"Message by {msg.Author.Username} edited");
|
||||
embedBuilder.AddField("\u200b", "**PluralKit information**");
|
||||
embedBuilder.AddField("\u200b", "**PluralKit information**", false);
|
||||
embedBuilder.AddField("System ID", oldMessage.System, true);
|
||||
embedBuilder.AddField("Member ID", oldMessage.Member, true);
|
||||
}
|
||||
|
|
@ -169,7 +174,7 @@ public class MessageUpdateResponder(
|
|||
)
|
||||
{
|
||||
if (
|
||||
!await messageRepository.SaveMessageAsync(msg, msg.GuildID, ct)
|
||||
!await messageRepository.SaveMessageAsync(msg, ct)
|
||||
&& msg.ApplicationID.Is(DiscordUtils.PkUserId)
|
||||
)
|
||||
{
|
||||
|
|
@ -191,6 +196,44 @@ public class MessageUpdateResponder(
|
|||
}
|
||||
}
|
||||
|
||||
private static MessageCreate ConvertToMessageCreate(IMessageUpdate evt) =>
|
||||
new(
|
||||
evt.GuildID,
|
||||
evt.Member,
|
||||
evt.Mentions.GetOrThrow(),
|
||||
evt.ID.GetOrThrow(),
|
||||
evt.ChannelID.GetOrThrow(),
|
||||
evt.Author.GetOrThrow(),
|
||||
evt.Content.GetOrThrow(),
|
||||
evt.Timestamp.GetOrThrow(),
|
||||
evt.EditedTimestamp.GetOrThrow(),
|
||||
IsTTS: false,
|
||||
evt.MentionsEveryone.GetOrThrow(),
|
||||
evt.MentionedRoles.GetOrThrow(),
|
||||
evt.MentionedChannels,
|
||||
evt.Attachments.GetOrThrow(),
|
||||
evt.Embeds.GetOrThrow(),
|
||||
evt.Reactions,
|
||||
evt.Nonce,
|
||||
evt.IsPinned.GetOrThrow(),
|
||||
evt.WebhookID,
|
||||
evt.Type.GetOrThrow(),
|
||||
evt.Activity,
|
||||
evt.Application,
|
||||
evt.ApplicationID,
|
||||
evt.MessageReference,
|
||||
evt.Flags,
|
||||
evt.ReferencedMessage,
|
||||
evt.Interaction,
|
||||
evt.Thread,
|
||||
evt.Components,
|
||||
evt.StickerItems,
|
||||
evt.Position,
|
||||
evt.Resolved,
|
||||
evt.InteractionMetadata,
|
||||
evt.Poll
|
||||
);
|
||||
|
||||
private static IEnumerable<string> ChunksUpTo(string str, int maxChunkSize)
|
||||
{
|
||||
for (var i = 0; i < str.Length; i += maxChunkSize)
|
||||
|
|
|
|||
|
|
@ -26,19 +26,19 @@ public class ReadyResponder(ILogger logger, WebhookExecutorService webhookExecut
|
|||
{
|
||||
private readonly ILogger _logger = logger.ForContext<ReadyResponder>();
|
||||
|
||||
public Task<Result> RespondAsync(IReady evt, CancellationToken ct = default)
|
||||
public Task<Result> RespondAsync(IReady gatewayEvent, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
var shardId = evt.Shard.TryGet(out var shard) ? (shard.ShardID, shard.ShardCount) : (0, 1);
|
||||
var shardId = gatewayEvent.Shard.TryGet(out var shard)
|
||||
? (shard.ShardID, shard.ShardCount)
|
||||
: (0, 1);
|
||||
_logger.Information(
|
||||
"Ready as {User} on shard {ShardId}/{ShardCount}",
|
||||
evt.User.Tag(),
|
||||
gatewayEvent.User.Tag(),
|
||||
shardId.Item1,
|
||||
shardId.Item2
|
||||
);
|
||||
if (shardId.Item1 == 0)
|
||||
webhookExecutorService.SetSelfUser(evt.User);
|
||||
webhookExecutorService.SetSelfUser(gatewayEvent.User);
|
||||
|
||||
return Task.FromResult(Result.Success);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ public class RoleCreateResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildRoleCreate evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
_logger.Debug("Received new role {RoleId} in guild {GuildId}", evt.Role.ID, evt.GuildID);
|
||||
roleCache.Set(evt.Role, evt.GuildID);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ public class RoleDeleteResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildRoleDelete evt, CancellationToken ct = default)
|
||||
{
|
||||
using var __ = LogUtils.Enrich(evt);
|
||||
|
||||
try
|
||||
{
|
||||
if (!roleCache.TryGet(evt.RoleID, out var role))
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ public class RoleUpdateResponder(
|
|||
|
||||
public async Task<Result> RespondAsync(IGuildRoleUpdate evt, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = LogUtils.Enrich(evt);
|
||||
|
||||
try
|
||||
{
|
||||
var newRole = evt.Role;
|
||||
|
|
|
|||
|
|
@ -234,7 +234,6 @@ internal record RedisMember(
|
|||
User.ToRemoraUser(),
|
||||
Nickname,
|
||||
Avatar != null ? new ImageHash(Avatar) : null,
|
||||
Banner: null,
|
||||
Roles.Select(DiscordSnowflake.New).ToList(),
|
||||
JoinedAt,
|
||||
PremiumSince,
|
||||
|
|
|
|||
|
|
@ -21,18 +21,18 @@
|
|||
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
|
||||
<PackageReference Include="LazyCache" Version="2.4.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="NodaTime" Version="3.2.0" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0"/>
|
||||
<PackageReference Include="Npgsql" Version="9.0.0" />
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="9.0.0" />
|
||||
<PackageReference Include="Polly.Core" Version="8.5.2" />
|
||||
<PackageReference Include="Polly.Core" Version="8.5.0" />
|
||||
<PackageReference Include="Polly.RateLimiting" Version="8.5.0" />
|
||||
<PackageReference Include="prometheus-net" Version="8.2.1"/>
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||
<PackageReference Include="Remora.Sdk" Version="3.1.2"/>
|
||||
<PackageReference Include="Remora.Discord" Version="2025.1.0" />
|
||||
<PackageReference Include="Remora.Discord" Version="2024.3.0-github11168366508"/>
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0"/>
|
||||
|
|
|
|||
|
|
@ -13,13 +13,6 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using Remora.Results;
|
||||
using RemoraResult = Remora.Results.Result;
|
||||
|
||||
namespace Catalogger.Backend;
|
||||
|
||||
public class CataloggerError(string message) : Exception(message), IResultError
|
||||
{
|
||||
public static RemoraResult Result(string message) =>
|
||||
RemoraResult.FromError(new CataloggerError(message));
|
||||
}
|
||||
public class CataloggerError(string message) : Exception(message) { }
|
||||
|
|
|
|||
|
|
@ -29,11 +29,6 @@ public static class CataloggerMetrics
|
|||
|
||||
public static long MessageRateMinute { get; set; }
|
||||
|
||||
public static readonly Gauge DatabaseConnections = Metrics.CreateGauge(
|
||||
"catalogger_open_database_connections",
|
||||
"Number of open database connections"
|
||||
);
|
||||
|
||||
public static readonly Gauge GuildsCached = Metrics.CreateGauge(
|
||||
"catalogger_cache_guilds",
|
||||
"Number of guilds in the cache"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ public class Config
|
|||
public bool EnableMetrics { get; init; } = true;
|
||||
|
||||
public string? SeqLogUrl { get; init; }
|
||||
public string? PrometheusUrl { get; init; }
|
||||
}
|
||||
|
||||
public class DatabaseConfig
|
||||
|
|
|
|||
|
|
@ -17,13 +17,17 @@ using System.Data;
|
|||
using System.Data.Common;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Npgsql;
|
||||
using Serilog;
|
||||
|
||||
namespace Catalogger.Backend.Database;
|
||||
|
||||
public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposable
|
||||
public class DatabaseConnection(Guid id, ILogger logger, NpgsqlConnection inner)
|
||||
: DbConnection,
|
||||
IDisposable
|
||||
{
|
||||
public Guid ConnectionId => id;
|
||||
public NpgsqlConnection Inner => inner;
|
||||
private readonly ILogger _logger = logger.ForContext<DatabaseConnection>();
|
||||
private readonly DateTimeOffset _openTime = DateTimeOffset.UtcNow;
|
||||
|
||||
private bool _hasClosed;
|
||||
|
||||
|
|
@ -39,6 +43,8 @@ public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposa
|
|||
}
|
||||
|
||||
DatabasePool.DecrementConnections();
|
||||
var openFor = DateTimeOffset.UtcNow - _openTime;
|
||||
_logger.Verbose("Closing connection {ConnId}, open for {OpenFor}", ConnectionId, openFor);
|
||||
_hasClosed = true;
|
||||
await inner.CloseAsync();
|
||||
}
|
||||
|
|
@ -46,20 +52,17 @@ public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposa
|
|||
protected override async ValueTask<DbTransaction> BeginDbTransactionAsync(
|
||||
IsolationLevel isolationLevel,
|
||||
CancellationToken cancellationToken
|
||||
) => await inner.BeginTransactionAsync(isolationLevel, cancellationToken);
|
||||
)
|
||||
{
|
||||
_logger.Verbose("Beginning transaction on connection {ConnId}", ConnectionId);
|
||||
return await inner.BeginTransactionAsync(isolationLevel, cancellationToken);
|
||||
}
|
||||
|
||||
public new void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
Log.Error("Called Dispose method on DbConnection, should call DisposeAsync!");
|
||||
Log.Warning("CloseAsync will be called synchronously.");
|
||||
CloseAsync().Wait();
|
||||
Close();
|
||||
inner.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
|
|
@ -70,13 +73,13 @@ public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposa
|
|||
}
|
||||
|
||||
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) =>
|
||||
throw new SyncException(nameof(BeginDbTransaction));
|
||||
inner.BeginTransaction(isolationLevel);
|
||||
|
||||
public override void ChangeDatabase(string databaseName) => inner.ChangeDatabase(databaseName);
|
||||
|
||||
public override void Close() => throw new SyncException(nameof(Close));
|
||||
public override void Close() => inner.Close();
|
||||
|
||||
public override void Open() => throw new SyncException(nameof(Open));
|
||||
public override void Open() => inner.Open();
|
||||
|
||||
[AllowNull]
|
||||
public override string ConnectionString
|
||||
|
|
@ -91,6 +94,4 @@ public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposa
|
|||
public override string ServerVersion => inner.ServerVersion;
|
||||
|
||||
protected override DbCommand CreateDbCommand() => inner.CreateCommand();
|
||||
|
||||
public class SyncException(string method) : Exception($"Tried to use sync method {method}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ public class DatabaseMigrator(ILogger logger, IClock clock, DatabaseConnection c
|
|||
if (hasMigrationTable)
|
||||
{
|
||||
return await conn.QuerySingleOrDefaultAsync<MigrationEntry>(
|
||||
"SELECT * FROM migrations ORDER BY applied_at DESC, migration_name DESC LIMIT 1"
|
||||
"SELECT * FROM migrations ORDER BY applied_at DESC LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ public class DatabaseMigrator(ILogger logger, IClock clock, DatabaseConnection c
|
|||
return await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetMigrationNames() =>
|
||||
public static IEnumerable<string> GetMigrationNames() =>
|
||||
typeof(DatabasePool)
|
||||
.Assembly.GetManifestResourceNames()
|
||||
.Where(s => s.StartsWith($"{RootPath}.Migrations"))
|
||||
|
|
|
|||
|
|
@ -24,13 +24,18 @@ namespace Catalogger.Backend.Database;
|
|||
|
||||
public class DatabasePool
|
||||
{
|
||||
private readonly ILogger _rootLogger;
|
||||
private readonly ILogger _logger;
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
|
||||
private static int _openConnections;
|
||||
public static int OpenConnections => _openConnections;
|
||||
|
||||
public DatabasePool(Config config, ILoggerFactory? loggerFactory)
|
||||
public DatabasePool(Config config, ILogger logger, ILoggerFactory? loggerFactory)
|
||||
{
|
||||
_rootLogger = logger;
|
||||
_logger = logger.ForContext<DatabasePool>();
|
||||
|
||||
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
||||
{
|
||||
Timeout = config.Database.Timeout ?? 5,
|
||||
|
|
@ -46,14 +51,24 @@ public class DatabasePool
|
|||
|
||||
public async Task<DatabaseConnection> AcquireAsync(CancellationToken ct = default)
|
||||
{
|
||||
IncrementConnections();
|
||||
return new DatabaseConnection(await _dataSource.OpenConnectionAsync(ct));
|
||||
return new DatabaseConnection(
|
||||
LogOpen(),
|
||||
_rootLogger,
|
||||
await _dataSource.OpenConnectionAsync(ct)
|
||||
);
|
||||
}
|
||||
|
||||
public DatabaseConnection Acquire()
|
||||
{
|
||||
return new DatabaseConnection(LogOpen(), _rootLogger, _dataSource.OpenConnection());
|
||||
}
|
||||
|
||||
private Guid LogOpen()
|
||||
{
|
||||
var connId = Guid.NewGuid();
|
||||
_logger.Verbose("Opening database connection {ConnId}", connId);
|
||||
IncrementConnections();
|
||||
return new DatabaseConnection(_dataSource.OpenConnection());
|
||||
return connId;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ using Catalogger.Backend.Extensions;
|
|||
using Dapper;
|
||||
using Remora.Discord.API;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Rest.Core;
|
||||
|
||||
namespace Catalogger.Backend.Database.Repositories;
|
||||
|
|
@ -64,11 +63,7 @@ public class MessageRepository(
|
|||
/// <summary>
|
||||
/// Adds a new message. If the message is already in the database, updates the existing message instead.
|
||||
/// </summary>
|
||||
public async Task<bool> SaveMessageAsync(
|
||||
IMessage msg,
|
||||
Optional<Snowflake> guildId,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
public async Task<bool> SaveMessageAsync(IMessageCreate msg, CancellationToken ct = default)
|
||||
{
|
||||
var content = await Task.Run(
|
||||
() =>
|
||||
|
|
@ -112,9 +107,7 @@ public class MessageRepository(
|
|||
Id = msg.ID.Value,
|
||||
UserId = msg.Author.ID.Value,
|
||||
ChannelId = msg.ChannelID.Value,
|
||||
GuildId = guildId.IsDefined(out var guildIdValue)
|
||||
? guildIdValue.Value
|
||||
: (ulong?)null,
|
||||
GuildId = msg.GuildID.Map(s => s.Value).OrDefault(),
|
||||
Content = content,
|
||||
Username = username,
|
||||
Metadata = metadata,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Rest.Core;
|
||||
using Serilog.Context;
|
||||
|
||||
|
|
@ -20,13 +19,7 @@ public static class LogUtils
|
|||
("ChannelId", md.ChannelID),
|
||||
("MessageId", md.ID)
|
||||
),
|
||||
IMessageUpdate mu => PushProperties(
|
||||
type,
|
||||
("GuildId", mu.GuildID),
|
||||
("ChannelId", mu.ChannelID),
|
||||
("MessageId", mu.ID)
|
||||
),
|
||||
IMessageCreate mc => PushProperties(
|
||||
IMessageUpdate mc => PushProperties(
|
||||
type,
|
||||
("GuildId", mc.GuildID),
|
||||
("ChannelId", mc.ChannelID),
|
||||
|
|
@ -38,93 +31,6 @@ public static class LogUtils
|
|||
("ChannelId", mdb.ChannelID),
|
||||
("MessageIds", mdb.IDs)
|
||||
),
|
||||
IGuildRoleCreate grc => PushProperties(
|
||||
type,
|
||||
("GuildId", grc.GuildID),
|
||||
("RoleId", grc.Role.ID)
|
||||
),
|
||||
IGuildRoleUpdate gru => PushProperties(
|
||||
type,
|
||||
("GuildId", gru.GuildID),
|
||||
("RoleId", gru.Role.ID)
|
||||
),
|
||||
IGuildRoleDelete grd => PushProperties(
|
||||
type,
|
||||
("GuildId", grd.GuildID),
|
||||
("RoleId", grd.RoleID)
|
||||
),
|
||||
IGuildMemberAdd gma => PushProperties(
|
||||
type,
|
||||
("GuildId", gma.GuildID),
|
||||
("UserId", gma.User.Map(u => u.ID))
|
||||
),
|
||||
IGuildMemberUpdate gmu => PushProperties(
|
||||
type,
|
||||
("GuildId", gmu.GuildID),
|
||||
("UserId", gmu.User.ID)
|
||||
),
|
||||
IGuildMemberRemove gmr => PushProperties(
|
||||
type,
|
||||
("GuildId", gmr.GuildID),
|
||||
("UserId", gmr.User.ID)
|
||||
),
|
||||
IInviteCreate ic => PushProperties(
|
||||
type,
|
||||
("GuildId", ic.GuildID),
|
||||
("ChannelId", ic.ChannelID),
|
||||
("InviteCode", ic.Code)
|
||||
),
|
||||
IInviteDelete id => PushProperties(
|
||||
type,
|
||||
("GuildId", id.GuildID),
|
||||
("ChannelId", id.ChannelID),
|
||||
("Code", id.Code)
|
||||
),
|
||||
IChannelCreate cc => PushProperties(
|
||||
type,
|
||||
("GuildId", cc.GuildID),
|
||||
("ChannelId", cc.ID)
|
||||
),
|
||||
IChannelUpdate cu => PushProperties(
|
||||
type,
|
||||
("GuildId", cu.GuildID),
|
||||
("ChannelId", cu.ID)
|
||||
),
|
||||
IChannelDelete cd => PushProperties(
|
||||
type,
|
||||
("GuildId", cd.GuildID),
|
||||
("ChannelId", cd.ID)
|
||||
),
|
||||
IGuildAuditLogEntryCreate ale => PushProperties(
|
||||
type,
|
||||
("GuildId", ale.GuildID),
|
||||
("AuditLogEntryId", ale.ID),
|
||||
("ActionType", ale.ActionType)
|
||||
),
|
||||
IGuildBanAdd gba => PushProperties(
|
||||
type,
|
||||
("GuildId", gba.GuildID),
|
||||
("UserId", gba.User.ID)
|
||||
),
|
||||
IGuildBanRemove gbr => PushProperties(
|
||||
type,
|
||||
("GuildId", gbr.GuildID),
|
||||
("UserId", gbr.User.ID)
|
||||
),
|
||||
IGuildCreate gc => PushProperties(
|
||||
type,
|
||||
("GuildId", gc.Guild.Match(g => g.ID, g => g.ID))
|
||||
),
|
||||
IGuildDelete gd => PushProperties(type, ("GuildId", gd.ID)),
|
||||
IGuildEmojisUpdate geu => PushProperties(type, ("GuildId", geu.GuildID)),
|
||||
IGuildMembersChunk gmc => PushProperties(
|
||||
type,
|
||||
("GuildId", gmc.GuildID),
|
||||
("MemberCount", gmc.Members.Count),
|
||||
("ChunkIndex", gmc.ChunkIndex),
|
||||
("ChunkCount", gmc.ChunkCount)
|
||||
),
|
||||
IGuildUpdate gu => PushProperties(type, ("GuildId", gu.ID)),
|
||||
_ => PushProperties(type),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,22 +51,19 @@ public static class StartupExtensions
|
|||
{
|
||||
var logCfg = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.MinimumLevel.Verbose()
|
||||
// Most Microsoft.* package logs are needlessly verbose, so we restrict them to INFO level and up
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.MinimumLevel.Is(config.Logging.LogEventLevel)
|
||||
// ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead.
|
||||
// Serilog doesn't disable the built-in logs, so we do it here.
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.MinimumLevel.Override(
|
||||
"Microsoft.EntityFrameworkCore.Database.Command",
|
||||
config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Fatal
|
||||
)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
|
||||
// Let's not put webhook tokens and even *full bot tokens* in the logs, thank you
|
||||
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning)
|
||||
// The default theme doesn't support light mode
|
||||
.WriteTo.Console(
|
||||
theme: AnsiConsoleTheme.Sixteen,
|
||||
applyThemeToRedirectedOutput: true,
|
||||
restrictedToMinimumLevel: config.Logging.LogEventLevel
|
||||
);
|
||||
.WriteTo.Console(theme: AnsiConsoleTheme.Sixteen, applyThemeToRedirectedOutput: true);
|
||||
|
||||
if (config.Logging.SeqLogUrl != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ builder
|
|||
| GatewayIntents.GuildMessages
|
||||
| GatewayIntents.GuildWebhooks
|
||||
| GatewayIntents.MessageContents
|
||||
| GatewayIntents.GuildExpressions;
|
||||
// Actually GUILD_EXPRESSIONS
|
||||
| GatewayIntents.GuildEmojisAndStickers;
|
||||
|
||||
// Set a default status for all shards. This is updated to a shard-specific one in StatusUpdateService.
|
||||
g.Presence = new UpdatePresence(
|
||||
|
|
@ -96,7 +97,12 @@ builder
|
|||
.WithCommandGroup<KeyRoleCommands>()
|
||||
.WithCommandGroup<InviteCommands>()
|
||||
.WithCommandGroup<IgnoreMessageCommands>()
|
||||
.WithCommandGroup<IgnoreMessageCommands.Channels>()
|
||||
.WithCommandGroup<IgnoreMessageCommands.Users>()
|
||||
.WithCommandGroup<IgnoreMessageCommands.Roles>()
|
||||
.WithCommandGroup<IgnoreEntitiesCommands>()
|
||||
.WithCommandGroup<IgnoreEntitiesCommands.Channels>()
|
||||
.WithCommandGroup<IgnoreEntitiesCommands.Roles>()
|
||||
.WithCommandGroup<RedirectCommands>()
|
||||
.WithCommandGroup<WatchlistCommands>()
|
||||
// End command tree
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ public class MetricsCollectionService(
|
|||
|
||||
var messageCount = await conn.ExecuteScalarAsync<int>("select count(id) from messages");
|
||||
|
||||
CataloggerMetrics.DatabaseConnections.Set(DatabasePool.OpenConnections);
|
||||
CataloggerMetrics.GuildsCached.Set(guildCache.Size);
|
||||
CataloggerMetrics.ChannelsCached.Set(channelCache.Size);
|
||||
CataloggerMetrics.RolesCached.Set(roleCache.Size);
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ public class TimeoutService(
|
|||
_logger.Information("Populating timeout service with existing database timeouts");
|
||||
|
||||
await using var scope = serviceProvider.CreateAsyncScope();
|
||||
await using var timeoutRepository =
|
||||
scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
|
||||
var timeoutRepository = scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
|
||||
|
||||
var timeouts = await timeoutRepository.GetAllAsync();
|
||||
foreach (var timeout in timeouts)
|
||||
|
|
@ -54,10 +53,8 @@ public class TimeoutService(
|
|||
_logger.Information("Sending timeout log for {TimeoutId}", timeoutId);
|
||||
|
||||
await using var scope = serviceProvider.CreateAsyncScope();
|
||||
await using var guildRepository =
|
||||
scope.ServiceProvider.GetRequiredService<GuildRepository>();
|
||||
await using var timeoutRepository =
|
||||
scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
|
||||
var guildRepository = scope.ServiceProvider.GetRequiredService<GuildRepository>();
|
||||
var timeoutRepository = scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
|
||||
|
||||
var timeout = await timeoutRepository.RemoveAsync(timeoutId);
|
||||
if (timeout == null)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ public class WebhookExecutorService(
|
|||
private readonly ILogger _logger = logger.ForContext<WebhookExecutorService>();
|
||||
private readonly Snowflake _applicationId = DiscordSnowflake.New(config.Discord.ApplicationId);
|
||||
private readonly ConcurrentDictionary<ulong, ConcurrentQueue<IEmbed>> _cache = new();
|
||||
private readonly ConcurrentDictionary<ulong, Lock> _locks = new();
|
||||
private readonly ConcurrentDictionary<ulong, object> _locks = new();
|
||||
private readonly ConcurrentDictionary<ulong, Timer> _timers = new();
|
||||
private IUser? _selfUser;
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ public class WebhookExecutorService(
|
|||
private List<IEmbed> TakeFromQueue(ulong channelId)
|
||||
{
|
||||
var queue = _cache.GetOrAdd(channelId, []);
|
||||
var channelLock = _locks.GetOrAdd(channelId, new Lock());
|
||||
var channelLock = _locks.GetOrAdd(channelId, channelId);
|
||||
lock (channelLock)
|
||||
{
|
||||
var totalContentLength = 0;
|
||||
|
|
@ -293,10 +293,10 @@ public class WebhookExecutorService(
|
|||
roleIds != null && logChannelType is LogChannelType.GuildMemberUpdate;
|
||||
|
||||
if (isMessageLog)
|
||||
return GetLogChannelForMessageEvent(guild, logChannelType, channelId, userId);
|
||||
return GetMessageLogChannel(guild, logChannelType, channelId, userId);
|
||||
|
||||
if (isChannelLog)
|
||||
return GetLogChannelForChannelEvent(guild, logChannelType, channelId!.Value);
|
||||
return GetChannelLogChannel(guild, logChannelType, channelId!.Value);
|
||||
|
||||
if (isRoleLog && guild.IgnoredRoles.Contains(roleId!.Value.Value))
|
||||
return null;
|
||||
|
|
@ -305,201 +305,20 @@ public class WebhookExecutorService(
|
|||
if (isMemberRoleUpdateLog && roleIds!.All(r => guild.IgnoredRoles.Contains(r.Value)))
|
||||
return null;
|
||||
|
||||
// If nothing is ignored, and this isn't a message or channel event, return the default log channel.
|
||||
// If nothing is ignored, return the correct log channel!
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
private ulong? GetLogChannelForMessageEvent(
|
||||
Guild guild,
|
||||
LogChannelType logChannelType,
|
||||
Snowflake? channelId = null,
|
||||
ulong? userId = null
|
||||
)
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Getting log channel for event {Event}. Channel ID: {ChannelId}, user ID: {UserId}",
|
||||
logChannelType,
|
||||
channelId,
|
||||
userId
|
||||
);
|
||||
|
||||
// Check if the user is ignored globally
|
||||
if (userId != null && guild.Messages.IgnoredUsers.Contains(userId.Value))
|
||||
{
|
||||
_logger.Verbose("User {UserId} is ignored globally", userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the user isn't ignored and we didn't get a channel ID, return the default log channel
|
||||
if (channelId == null)
|
||||
{
|
||||
_logger.Verbose(
|
||||
"No channel ID given so returning default channel for {Event}",
|
||||
logChannelType
|
||||
);
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
if (!channelCache.TryGet(channelId.Value, out var channel))
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Channel with ID {ChannelId} is not cached, returning default log channel",
|
||||
channelId
|
||||
);
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
if (!GetChannelAndParentId(channel, out var actualChannelId, out var categoryId))
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Could not get root channel and category ID for channel {ChannelId}, returning default log channel",
|
||||
channelId
|
||||
);
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
// Check if the channel or its category is ignored
|
||||
if (
|
||||
guild.Messages.IgnoredChannels.Contains(actualChannelId.Value)
|
||||
|| categoryId != null && guild.Messages.IgnoredChannels.Contains(categoryId.Value.Value)
|
||||
)
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Channel {ChannelId} or its parent {CategoryId} is ignored",
|
||||
actualChannelId,
|
||||
categoryId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (userId != null)
|
||||
{
|
||||
// Check the channel-local and category-local ignored users
|
||||
var channelIgnoredUsers =
|
||||
guild.Messages.IgnoredUsersPerChannel.GetValueOrDefault(actualChannelId.Value)
|
||||
?? [];
|
||||
|
||||
// Obviously, we can only check for category-level ignored users if we actually got a category ID.
|
||||
var categoryIgnoredUsers =
|
||||
(
|
||||
categoryId != null
|
||||
? guild.Messages.IgnoredUsersPerChannel.GetValueOrDefault(
|
||||
categoryId.Value.Value
|
||||
)
|
||||
: []
|
||||
) ?? [];
|
||||
|
||||
// Combine the ignored users in the channel and category, then check if the user is in there.
|
||||
if (channelIgnoredUsers.Concat(categoryIgnoredUsers).Contains(userId.Value))
|
||||
{
|
||||
_logger.Verbose(
|
||||
"User {UserId} is ignored in {ChannelId} or its category {CategoryId}",
|
||||
userId,
|
||||
channelId,
|
||||
categoryId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// These three events can be redirected to other channels. Redirects can be on a channel or category level.
|
||||
// The events are only redirected if they're supposed to be logged in the first place (i.e. GetDefaultLogChannel doesn't return 0)
|
||||
if (GetDefaultLogChannel(guild, logChannelType) == 0)
|
||||
{
|
||||
_logger.Verbose(
|
||||
"No default log channel for event {EventType}, ignoring event",
|
||||
logChannelType
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (guild.Channels.Redirects.TryGetValue(actualChannelId.Value, out var channelRedirect))
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Messages from channel {ChannelId} should be redirected to {RedirectId}",
|
||||
actualChannelId,
|
||||
channelRedirect
|
||||
);
|
||||
return channelRedirect;
|
||||
}
|
||||
|
||||
var categoryRedirect =
|
||||
categoryId != null
|
||||
? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value)
|
||||
: 0;
|
||||
|
||||
if (categoryRedirect != 0)
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Messages from categoryId {CategoryId} should be redirected to {RedirectId}",
|
||||
categoryId,
|
||||
categoryRedirect
|
||||
);
|
||||
return categoryRedirect;
|
||||
}
|
||||
|
||||
_logger.Verbose(
|
||||
"No redirects or ignores for event {EventType}, returning default log channel",
|
||||
logChannelType
|
||||
);
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
private ulong? GetLogChannelForChannelEvent(
|
||||
private ulong? GetChannelLogChannel(
|
||||
Guild guild,
|
||||
LogChannelType logChannelType,
|
||||
Snowflake channelId
|
||||
)
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Getting log channel for event {Event} in guild {GuildId} and channel {ChannelId}",
|
||||
logChannelType,
|
||||
guild.Id,
|
||||
channelId
|
||||
);
|
||||
|
||||
if (!channelCache.TryGet(channelId, out var channel))
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Channel with ID {ChannelId} is not cached, returning default log channel",
|
||||
channelId
|
||||
);
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
if (!GetChannelAndParentId(channel, out channelId, out var categoryId))
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Could not get root channel and category ID for channel {ChannelId}, returning default log channel",
|
||||
channelId
|
||||
);
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
// Check if the channel or its category is ignored
|
||||
if (
|
||||
guild.IgnoredChannels.Contains(channelId.Value)
|
||||
|| (categoryId != null && guild.IgnoredChannels.Contains(categoryId.Value.Value))
|
||||
)
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Channel {ChannelId} or its parent {CategoryId} is ignored",
|
||||
channelId,
|
||||
categoryId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.Verbose("Returning default log channel for {EventType}", logChannelType);
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
private bool GetChannelAndParentId(
|
||||
IChannel channel,
|
||||
out Snowflake channelId,
|
||||
out Snowflake? categoryId
|
||||
)
|
||||
{
|
||||
Snowflake? categoryId;
|
||||
if (
|
||||
channel.Type
|
||||
is ChannelType.AnnouncementThread
|
||||
|
|
@ -510,16 +329,7 @@ public class WebhookExecutorService(
|
|||
// parent_id should always have a value for threads
|
||||
channelId = channel.ParentID.Value!.Value;
|
||||
if (!channelCache.TryGet(channelId, out var parentChannel))
|
||||
{
|
||||
_logger.Verbose(
|
||||
"Parent channel for thread {ChannelId} is not in cache, returning the default log channel",
|
||||
channelId
|
||||
);
|
||||
|
||||
channelId = Snowflake.CreateTimestampSnowflake();
|
||||
categoryId = null;
|
||||
return false;
|
||||
}
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
categoryId = parentChannel.ParentID.Value;
|
||||
}
|
||||
else
|
||||
|
|
@ -528,7 +338,94 @@ public class WebhookExecutorService(
|
|||
categoryId = channel.ParentID.Value;
|
||||
}
|
||||
|
||||
return true;
|
||||
// Check if the channel or its category is ignored
|
||||
if (
|
||||
guild.IgnoredChannels.Contains(channelId.Value)
|
||||
|| (categoryId != null && guild.IgnoredChannels.Contains(categoryId.Value.Value))
|
||||
)
|
||||
return null;
|
||||
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
private ulong? GetMessageLogChannel(
|
||||
Guild guild,
|
||||
LogChannelType logChannelType,
|
||||
Snowflake? channelId = null,
|
||||
ulong? userId = null
|
||||
)
|
||||
{
|
||||
// Check if the user is ignored globally
|
||||
if (userId != null && guild.Messages.IgnoredUsers.Contains(userId.Value))
|
||||
return null;
|
||||
|
||||
// If the user isn't ignored and we didn't get a channel ID, return the default log channel
|
||||
if (channelId == null)
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
|
||||
if (!channelCache.TryGet(channelId.Value, out var channel))
|
||||
return null;
|
||||
|
||||
Snowflake? categoryId;
|
||||
if (
|
||||
channel.Type
|
||||
is ChannelType.AnnouncementThread
|
||||
or ChannelType.PrivateThread
|
||||
or ChannelType.PublicThread
|
||||
)
|
||||
{
|
||||
// parent_id should always have a value for threads
|
||||
channelId = channel.ParentID.Value!.Value;
|
||||
if (!channelCache.TryGet(channelId.Value, out var parentChannel))
|
||||
return GetDefaultLogChannel(guild, logChannelType);
|
||||
categoryId = parentChannel.ParentID.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
channelId = channel.ID;
|
||||
categoryId = channel.ParentID.Value;
|
||||
}
|
||||
|
||||
// Check if the channel or its category is ignored
|
||||
if (
|
||||
guild.Messages.IgnoredChannels.Contains(channelId.Value.Value)
|
||||
|| categoryId != null && guild.Messages.IgnoredChannels.Contains(categoryId.Value.Value)
|
||||
)
|
||||
return null;
|
||||
|
||||
if (userId != null)
|
||||
{
|
||||
// Check the channel-local and category-local ignored users
|
||||
var channelIgnoredUsers =
|
||||
guild.Messages.IgnoredUsersPerChannel.GetValueOrDefault(channelId.Value.Value)
|
||||
?? [];
|
||||
var categoryIgnoredUsers =
|
||||
(
|
||||
categoryId != null
|
||||
? guild.Messages.IgnoredUsersPerChannel.GetValueOrDefault(
|
||||
categoryId.Value.Value
|
||||
)
|
||||
: []
|
||||
) ?? [];
|
||||
if (channelIgnoredUsers.Concat(categoryIgnoredUsers).Contains(userId.Value))
|
||||
return null;
|
||||
}
|
||||
|
||||
// These three events can be redirected to other channels. Redirects can be on a channel or category level.
|
||||
// The events are only redirected if they're supposed to be logged in the first place.
|
||||
if (GetDefaultLogChannel(guild, logChannelType) == 0)
|
||||
return null;
|
||||
|
||||
var categoryRedirect =
|
||||
categoryId != null
|
||||
? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value)
|
||||
: 0;
|
||||
|
||||
if (guild.Channels.Redirects.TryGetValue(channelId.Value.Value, out var channelRedirect))
|
||||
return channelRedirect;
|
||||
return categoryRedirect != 0
|
||||
? categoryRedirect
|
||||
: GetDefaultLogChannel(guild, logChannelType);
|
||||
}
|
||||
|
||||
public static ulong GetDefaultLogChannel(Guild guild, LogChannelType logChannelType) =>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ LogQueries = false
|
|||
SeqLogUrl = http://localhost:5341
|
||||
# Whether to enable Prometheus metrics. If disabled, Catalogger will update metrics manually every so often.
|
||||
EnableMetrics = false
|
||||
# The URL for the Prometheus server. Used for message rate if metrics are enabled.
|
||||
# Defaults to http://localhost:9090, should be changed if Prometheus is on another server.
|
||||
PrometheusUrl = http://localhost:9090
|
||||
|
||||
[Database]
|
||||
Url = Host=localhost;Database=postgres;Username=postgres;Password=postgres
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ internal class Program
|
|||
return;
|
||||
}
|
||||
|
||||
var db = new DatabasePool(config, null);
|
||||
var db = new DatabasePool(config, Log.Logger, null);
|
||||
DatabasePool.ConfigureDapper();
|
||||
if (Environment.GetEnvironmentVariable("MIGRATE") == "true")
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ Command-line tools for this project can be installed with `dotnet tool restore`.
|
|||
- We use [CSharpier][csharpier] for formatting .NET code.
|
||||
It can be called with `dotnet csharpier .`, but is automatically run by Husky pre-commit.
|
||||
|
||||
### Nuget
|
||||
|
||||
We currently use Remora's GitHub packages as the releases on nuget.org are missing some key features.
|
||||
Add these with `dotnet nuget add source --username <githubUsername> --password <githubToken> --store-password-in-clear-text --name Remora "https://nuget.pkg.github.com/Remora/index.json"`
|
||||
|
||||
You must generate a personal access token (classic) [here](personal-access-token). Only give it the `read:packages` permission.
|
||||
|
||||
## Deploying Catalogger yourself
|
||||
|
||||
The bot itself should run on any server with .NET 8 and PostgreSQL 15 or later.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseVar</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseVar</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=pluralkit/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=remora/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
Loading…
Add table
Add a link
Reference in a new issue