Compare commits
18 commits
v2024.11.3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f3dfc74d6 | |||
| a4a6fb5d31 | |||
| 24f6aee57d | |||
| 8a4e3ff184 | |||
| 84c3b42874 | |||
| cb43ac1a50 | |||
| db3e6fa7b0 | |||
| 1a63540f89 | |||
| 0d7e809ef6 | |||
| 27e1903c4b | |||
| 5157105c35 | |||
| 7749c9d9e2 | |||
| 4047df8610 | |||
| 27e77eeaed | |||
| c06376dfda | |||
| 04d6bc958e | |||
| b3c541f743 | |||
| 48a11be7b7 |
61 changed files with 671 additions and 1067 deletions
|
|
@ -3,14 +3,14 @@
|
||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"csharpier": {
|
"csharpier": {
|
||||||
"version": "0.29.2",
|
"version": "0.30.6",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-csharpier"
|
"dotnet-csharpier"
|
||||||
],
|
],
|
||||||
"rollForward": false
|
"rollForward": false
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"commands": [
|
"commands": [
|
||||||
"husky"
|
"husky"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,10 @@ public class DiscordRequestService
|
||||||
private readonly IClock _clock;
|
private readonly IClock _clock;
|
||||||
private readonly ApiTokenRepository _tokenRepository;
|
private readonly ApiTokenRepository _tokenRepository;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions =
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
};
|
||||||
|
|
||||||
public DiscordRequestService(
|
public DiscordRequestService(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
|
@ -82,8 +84,9 @@ public class DiscordRequestService
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly Uri DiscordUserUri = new("https://discord.com/api/v10/users/@me");
|
private static readonly Uri DiscordUserUri = new("https://discord.com/api/v10/users/@me");
|
||||||
private static readonly Uri DiscordGuildsUri =
|
private static readonly Uri DiscordGuildsUri = new(
|
||||||
new("https://discord.com/api/v10/users/@me/guilds");
|
"https://discord.com/api/v10/users/@me/guilds"
|
||||||
|
);
|
||||||
private static readonly Uri DiscordTokenUri = new("https://discord.com/api/oauth2/token");
|
private static readonly Uri DiscordTokenUri = new("https://discord.com/api/oauth2/token");
|
||||||
|
|
||||||
public async Task<User> GetMeAsync(string token) => await GetAsync<User>(DiscordUserUri, token);
|
public async Task<User> GetMeAsync(string token) => await GetAsync<User>(DiscordUserUri, token);
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ public class ChannelCommands(
|
||||||
Config config,
|
Config config,
|
||||||
GuildRepository guildRepository,
|
GuildRepository guildRepository,
|
||||||
GuildCache guildCache,
|
GuildCache guildCache,
|
||||||
|
GuildFetchService guildFetchService,
|
||||||
ChannelCache channelCache,
|
ChannelCache channelCache,
|
||||||
IMemberCache memberCache,
|
IMemberCache memberCache,
|
||||||
IFeedbackService feedbackService,
|
IFeedbackService feedbackService,
|
||||||
|
|
@ -68,8 +69,11 @@ public class ChannelCommands(
|
||||||
public async Task<IResult> CheckPermissionsAsync()
|
public async Task<IResult> CheckPermissionsAsync()
|
||||||
{
|
{
|
||||||
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
||||||
|
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
{
|
||||||
|
return CataloggerError.Result($"Guild {guildId} not in cache");
|
||||||
|
}
|
||||||
|
|
||||||
var embed = new EmbedBuilder().WithTitle($"Permission check for {guild.Name}");
|
var embed = new EmbedBuilder().WithTitle($"Permission check for {guild.Name}");
|
||||||
|
|
||||||
|
|
@ -78,8 +82,18 @@ public class ChannelCommands(
|
||||||
DiscordSnowflake.New(config.Discord.ApplicationId)
|
DiscordSnowflake.New(config.Discord.ApplicationId)
|
||||||
);
|
);
|
||||||
var currentUser = await memberCache.TryGetAsync(guildId, userId);
|
var currentUser = await memberCache.TryGetAsync(guildId, userId);
|
||||||
|
|
||||||
if (botUser == null || currentUser == null)
|
if (botUser == null || currentUser == null)
|
||||||
throw new CataloggerError("Bot member or invoking member not found in cache");
|
{
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
// We don't want to check categories or threads
|
// We don't want to check categories or threads
|
||||||
var guildChannels = channelCache
|
var guildChannels = channelCache
|
||||||
|
|
@ -204,7 +218,7 @@ public class ChannelCommands(
|
||||||
{
|
{
|
||||||
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,15 +50,15 @@ public class ChannelCommandsComponents(
|
||||||
public async Task<Result> OnMenuSelectionAsync(IReadOnlyList<string> values)
|
public async Task<Result> OnMenuSelectionAsync(IReadOnlyList<string> values)
|
||||||
{
|
{
|
||||||
if (contextInjection.Context is not IInteractionCommandContext ctx)
|
if (contextInjection.Context is not IInteractionCommandContext ctx)
|
||||||
throw new CataloggerError("No context");
|
return CataloggerError.Result("No context");
|
||||||
if (!ctx.TryGetUserID(out var userId))
|
if (!ctx.TryGetUserID(out var userId))
|
||||||
throw new CataloggerError("No user ID in context");
|
return CataloggerError.Result("No user ID in context");
|
||||||
if (!ctx.Interaction.Message.TryGet(out var msg))
|
if (!ctx.Interaction.Message.TryGet(out var msg))
|
||||||
throw new CataloggerError("No message ID in context");
|
return CataloggerError.Result("No message ID in context");
|
||||||
if (!ctx.TryGetGuildID(out var guildId))
|
if (!ctx.TryGetGuildID(out var guildId))
|
||||||
throw new CataloggerError("No guild ID in context");
|
return CataloggerError.Result("No guild ID in context");
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ public class ChannelCommandsComponents(
|
||||||
var state = values[0];
|
var state = values[0];
|
||||||
|
|
||||||
if (!Enum.TryParse<LogChannelType>(state, out var logChannelType))
|
if (!Enum.TryParse<LogChannelType>(state, out var logChannelType))
|
||||||
throw new CataloggerError($"Invalid config-channels state {state}");
|
return CataloggerError.Result($"Invalid config-channels state {state}");
|
||||||
|
|
||||||
var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType);
|
var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType);
|
||||||
string? channelMention;
|
string? channelMention;
|
||||||
|
|
@ -147,15 +147,15 @@ public class ChannelCommandsComponents(
|
||||||
public async Task<Result> OnButtonPressedAsync(string state)
|
public async Task<Result> OnButtonPressedAsync(string state)
|
||||||
{
|
{
|
||||||
if (contextInjection.Context is not IInteractionCommandContext ctx)
|
if (contextInjection.Context is not IInteractionCommandContext ctx)
|
||||||
throw new CataloggerError("No context");
|
return CataloggerError.Result("No context");
|
||||||
if (!ctx.TryGetUserID(out var userId))
|
if (!ctx.TryGetUserID(out var userId))
|
||||||
throw new CataloggerError("No user ID in context");
|
return CataloggerError.Result("No user ID in context");
|
||||||
if (!ctx.Interaction.Message.TryGet(out var msg))
|
if (!ctx.Interaction.Message.TryGet(out var msg))
|
||||||
throw new CataloggerError("No message ID in context");
|
return CataloggerError.Result("No message ID in context");
|
||||||
if (!ctx.TryGetGuildID(out var guildId))
|
if (!ctx.TryGetGuildID(out var guildId))
|
||||||
throw new CataloggerError("No guild ID in context");
|
return CataloggerError.Result("No guild ID in context");
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
|
|
@ -179,9 +179,9 @@ public class ChannelCommandsComponents(
|
||||||
);
|
);
|
||||||
case "reset":
|
case "reset":
|
||||||
if (lease.Data.CurrentPage == null)
|
if (lease.Data.CurrentPage == null)
|
||||||
throw new CataloggerError("CurrentPage was null in reset button callback");
|
return CataloggerError.Result("CurrentPage was null in reset button callback");
|
||||||
if (!Enum.TryParse<LogChannelType>(lease.Data.CurrentPage, out var channelType))
|
if (!Enum.TryParse<LogChannelType>(lease.Data.CurrentPage, out var channelType))
|
||||||
throw new CataloggerError(
|
return CataloggerError.Result(
|
||||||
$"Invalid config-channels CurrentPage: '{lease.Data.CurrentPage}'"
|
$"Invalid config-channels CurrentPage: '{lease.Data.CurrentPage}'"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -281,15 +281,15 @@ public class ChannelCommandsComponents(
|
||||||
public async Task<Result> OnMenuSelectionAsync(IReadOnlyList<IPartialChannel> channels)
|
public async Task<Result> OnMenuSelectionAsync(IReadOnlyList<IPartialChannel> channels)
|
||||||
{
|
{
|
||||||
if (contextInjection.Context is not IInteractionCommandContext ctx)
|
if (contextInjection.Context is not IInteractionCommandContext ctx)
|
||||||
throw new CataloggerError("No context");
|
return CataloggerError.Result("No context");
|
||||||
if (!ctx.TryGetUserID(out var userId))
|
if (!ctx.TryGetUserID(out var userId))
|
||||||
throw new CataloggerError("No user ID in context");
|
return CataloggerError.Result("No user ID in context");
|
||||||
if (!ctx.Interaction.Message.TryGet(out var msg))
|
if (!ctx.Interaction.Message.TryGet(out var msg))
|
||||||
throw new CataloggerError("No message ID in context");
|
return CataloggerError.Result("No message ID in context");
|
||||||
if (!ctx.TryGetGuildID(out var guildId))
|
if (!ctx.TryGetGuildID(out var guildId))
|
||||||
throw new CataloggerError("No guild ID in context");
|
return CataloggerError.Result("No guild ID in context");
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
var channelId = channels[0].ID.ToUlong();
|
var channelId = channels[0].ID.ToUlong();
|
||||||
|
|
||||||
|
|
@ -305,7 +305,7 @@ public class ChannelCommandsComponents(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Enum.TryParse<LogChannelType>(lease.Data.CurrentPage, out var channelType))
|
if (!Enum.TryParse<LogChannelType>(lease.Data.CurrentPage, out var channelType))
|
||||||
throw new CataloggerError(
|
return CataloggerError.Result(
|
||||||
$"Invalid config-channels CurrentPage '{lease.Data.CurrentPage}'"
|
$"Invalid config-channels CurrentPage '{lease.Data.CurrentPage}'"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ public class IgnoreEntitiesCommands : CommandGroup
|
||||||
{
|
{
|
||||||
var (_, guildId) = contextInjection.GetUserAndGuild();
|
var (_, guildId) = contextInjection.GetUserAndGuild();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
|
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
|
|
@ -201,14 +201,14 @@ public class IgnoreEntitiesCommands : CommandGroup
|
||||||
{
|
{
|
||||||
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
|
|
||||||
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
var member = await memberCache.TryGetAsync(guildId, userId);
|
var member = await memberCache.TryGetAsync(guildId, userId);
|
||||||
if (member == null)
|
if (member == null)
|
||||||
throw new CataloggerError("Executing member not found");
|
return CataloggerError.Result("Executing member not found");
|
||||||
|
|
||||||
var ignoredChannels = guildConfig
|
var ignoredChannels = guildConfig
|
||||||
.IgnoredChannels.Select(id =>
|
.IgnoredChannels.Select(id =>
|
||||||
|
|
|
||||||
|
|
@ -110,14 +110,14 @@ public partial class IgnoreMessageCommands : CommandGroup
|
||||||
{
|
{
|
||||||
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
|
|
||||||
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
var member = await memberCache.TryGetAsync(guildId, userId);
|
var member = await memberCache.TryGetAsync(guildId, userId);
|
||||||
if (member == null)
|
if (member == null)
|
||||||
throw new CataloggerError("Executing member not found");
|
return CataloggerError.Result("Executing member not found");
|
||||||
|
|
||||||
var ignoredChannels = guildConfig
|
var ignoredChannels = guildConfig
|
||||||
.Messages.IgnoredChannels.Select(id =>
|
.Messages.IgnoredChannels.Select(id =>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ public partial class IgnoreMessageCommands
|
||||||
{
|
{
|
||||||
var (_, guildId) = contextInjection.GetUserAndGuild();
|
var (_, guildId) = contextInjection.GetUserAndGuild();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
|
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ public partial class IgnoreMessageCommands
|
||||||
{
|
{
|
||||||
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild was not cached");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
|
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ public class InviteCommands(
|
||||||
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
var (userId, guildId) = contextInjection.GetUserAndGuild();
|
||||||
var guildInvites = await guildApi.GetGuildInvitesAsync(guildId).GetOrThrow();
|
var guildInvites = await guildApi.GetGuildInvitesAsync(guildId).GetOrThrow();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
|
|
||||||
var dbInvites = await inviteRepository.GetGuildInvitesAsync(guildId);
|
var dbInvites = await inviteRepository.GetGuildInvitesAsync(guildId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ public class KeyRoleCommands(
|
||||||
{
|
{
|
||||||
var (_, guildId) = contextInjection.GetUserAndGuild();
|
var (_, guildId) = contextInjection.GetUserAndGuild();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
var guildRoles = roleCache.GuildRoles(guildId).ToList();
|
var guildRoles = roleCache.GuildRoles(guildId).ToList();
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ public class KeyRoleCommands(
|
||||||
var (_, guildId) = contextInjection.GetUserAndGuild();
|
var (_, guildId) = contextInjection.GetUserAndGuild();
|
||||||
var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId);
|
var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId);
|
||||||
if (role == null)
|
if (role == null)
|
||||||
throw new CataloggerError("Role is not cached");
|
return CataloggerError.Result("Role is not cached");
|
||||||
|
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
if (guildConfig.KeyRoles.Any(id => role.ID.Value == id))
|
if (guildConfig.KeyRoles.Any(id => role.ID.Value == id))
|
||||||
|
|
@ -111,7 +111,7 @@ public class KeyRoleCommands(
|
||||||
var (_, guildId) = contextInjection.GetUserAndGuild();
|
var (_, guildId) = contextInjection.GetUserAndGuild();
|
||||||
var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId);
|
var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId);
|
||||||
if (role == null)
|
if (role == null)
|
||||||
throw new CataloggerError("Role is not cached");
|
return CataloggerError.Result("Role is not cached");
|
||||||
|
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
if (guildConfig.KeyRoles.All(id => role.ID != id))
|
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);
|
await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add more checks around response format, configurable prometheus endpoint
|
// TODO: add more checks around response format
|
||||||
private async Task<double?> MessagesRate()
|
private async Task<double?> MessagesRate()
|
||||||
{
|
{
|
||||||
if (!config.Logging.EnableMetrics)
|
if (!config.Logging.EnableMetrics)
|
||||||
|
|
@ -227,7 +227,8 @@ public class MetaCommands(
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var query = HttpUtility.UrlEncode("increase(catalogger_received_messages[5m])");
|
var query = HttpUtility.UrlEncode("increase(catalogger_received_messages[5m])");
|
||||||
var resp = await _client.GetAsync($"http://localhost:9090/api/v1/query?query={query}");
|
var prometheusUrl = config.Logging.PrometheusUrl ?? "http://localhost:9090";
|
||||||
|
var resp = await _client.GetAsync($"{prometheusUrl}/api/v1/query?query={query}");
|
||||||
resp.EnsureSuccessStatusCode();
|
resp.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var data = await resp.Content.ReadFromJsonAsync<PrometheusResponse>();
|
var data = await resp.Content.ReadFromJsonAsync<PrometheusResponse>();
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ public class RedirectCommands(
|
||||||
{
|
{
|
||||||
var (userId, guildId) = contextInjectionService.GetUserAndGuild();
|
var (userId, guildId) = contextInjectionService.GetUserAndGuild();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild was not cached");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
var guildChannels = channelCache.GuildChannels(guildId).ToList();
|
||||||
var guildConfig = await guildRepository.GetAsync(guildId);
|
var guildConfig = await guildRepository.GetAsync(guildId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ public class WatchlistCommands(
|
||||||
{
|
{
|
||||||
var (userId, guildId) = contextInjectionService.GetUserAndGuild();
|
var (userId, guildId) = contextInjectionService.GetUserAndGuild();
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild was not cached");
|
return CataloggerError.Result("Guild not in cache");
|
||||||
|
|
||||||
var watchlist = await watchlistRepository.GetGuildWatchlistAsync(guildId);
|
var watchlist = await watchlistRepository.GetGuildWatchlistAsync(guildId);
|
||||||
if (watchlist.Count == 0)
|
if (watchlist.Count == 0)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ using Catalogger.Backend.Cache.InMemoryCache;
|
||||||
using Catalogger.Backend.Database.Repositories;
|
using Catalogger.Backend.Database.Repositories;
|
||||||
using Catalogger.Backend.Extensions;
|
using Catalogger.Backend.Extensions;
|
||||||
using Catalogger.Backend.Services;
|
using Catalogger.Backend.Services;
|
||||||
using Microsoft.Extensions.Logging.Configuration;
|
|
||||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
using Remora.Discord.Extensions.Embeds;
|
using Remora.Discord.Extensions.Embeds;
|
||||||
|
|
@ -36,6 +35,8 @@ public class ChannelCreateResponder(
|
||||||
{
|
{
|
||||||
public async Task<Result> RespondAsync(IChannelCreate ch, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IChannelCreate ch, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(ch);
|
||||||
|
|
||||||
if (!ch.GuildID.IsDefined())
|
if (!ch.GuildID.IsDefined())
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
channelCache.Set(ch);
|
channelCache.Set(ch);
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ public class ChannelDeleteResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IChannelDelete evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IChannelDelete evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var __ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
if (!evt.GuildID.IsDefined())
|
if (!evt.GuildID.IsDefined())
|
||||||
{
|
{
|
||||||
_logger.Debug("Deleted channel {ChannelId} is not in a guild", evt.ID);
|
_logger.Debug("Deleted channel {ChannelId} is not in a guild", evt.ID);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ public class ChannelUpdateResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IChannelUpdate evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IChannelUpdate evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!channelCache.TryGet(evt.ID, out var oldChannel))
|
if (!channelCache.TryGet(evt.ID, out var oldChannel))
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,27 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Catalogger.Backend.Extensions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Remora.Commands.Services;
|
using Remora.Commands.Services;
|
||||||
using Remora.Commands.Tokenization;
|
using Remora.Commands.Tokenization;
|
||||||
using Remora.Commands.Trees;
|
using Remora.Commands.Trees;
|
||||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||||
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
using Remora.Discord.API.Abstractions.Rest;
|
using Remora.Discord.API.Abstractions.Rest;
|
||||||
|
using Remora.Discord.API.Objects;
|
||||||
using Remora.Discord.Commands.Responders;
|
using Remora.Discord.Commands.Responders;
|
||||||
using Remora.Discord.Commands.Services;
|
using Remora.Discord.Commands.Services;
|
||||||
using Remora.Discord.Gateway.Responders;
|
using Remora.Discord.Gateway.Responders;
|
||||||
|
using Remora.Rest.Core;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
using Serilog.Context;
|
||||||
|
|
||||||
namespace Catalogger.Backend.Bot.Responders;
|
namespace Catalogger.Backend.Bot.Responders;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wrapper for Remora.Discord's default interaction responder, that ignores all events if test mode is enabled.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CustomInteractionResponder(
|
public class CustomInteractionResponder(
|
||||||
Config config,
|
Config config,
|
||||||
|
|
@ -45,34 +51,78 @@ public class CustomInteractionResponder(
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger = logger.ForContext<CustomInteractionResponder>();
|
private readonly ILogger _logger = logger.ForContext<CustomInteractionResponder>();
|
||||||
|
|
||||||
private readonly InteractionResponder _inner =
|
private readonly InteractionResponder _inner = new(
|
||||||
new(
|
commandService,
|
||||||
commandService,
|
options,
|
||||||
options,
|
interactionAPI,
|
||||||
interactionAPI,
|
eventCollector,
|
||||||
eventCollector,
|
services,
|
||||||
services,
|
contextInjection,
|
||||||
contextInjection,
|
tokenizerOptions,
|
||||||
tokenizerOptions,
|
treeSearchOptions,
|
||||||
treeSearchOptions,
|
treeNameResolver
|
||||||
treeNameResolver
|
);
|
||||||
);
|
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(
|
public async Task<Result> RespondAsync(IInteractionCreate evt, CancellationToken ct = default)
|
||||||
IInteractionCreate gatewayEvent,
|
|
||||||
CancellationToken ct = default
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
if (config.Discord.TestMode)
|
if (config.Discord.TestMode)
|
||||||
{
|
{
|
||||||
_logger.Information(
|
_logger.Information(
|
||||||
"Not responding to interaction create event {InteractionId} in {ChannelId} as test mode is enabled",
|
"Not responding to interaction create event {InteractionId} in {ChannelId} as test mode is enabled",
|
||||||
gatewayEvent.ID,
|
evt.ID,
|
||||||
gatewayEvent.Channel.Map(c => c.ID).OrDefault()
|
evt.Channel.Map(c => c.ID).OrDefault()
|
||||||
);
|
);
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _inner.RespondAsync(gatewayEvent, ct);
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using Catalogger.Backend.Cache.InMemoryCache;
|
using Catalogger.Backend.Cache.InMemoryCache;
|
||||||
|
using Catalogger.Backend.Extensions;
|
||||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
using Remora.Discord.Gateway.Responders;
|
using Remora.Discord.Gateway.Responders;
|
||||||
|
|
@ -28,6 +29,8 @@ public class AuditLogResponder(AuditLogCache auditLogCache, ILogger logger)
|
||||||
|
|
||||||
public Task<Result> RespondAsync(IGuildAuditLogEntryCreate evt, CancellationToken ct = default)
|
public Task<Result> RespondAsync(IGuildAuditLogEntryCreate evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
if (evt.TargetID == null || evt.UserID == null)
|
if (evt.TargetID == null || evt.UserID == null)
|
||||||
return Task.FromResult(Result.Success);
|
return Task.FromResult(Result.Success);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ public class GuildBanAddResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildBanAdd evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildBanAdd evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
var guildConfig = await guildRepository.GetAsync(evt.GuildID);
|
var guildConfig = await guildRepository.GetAsync(evt.GuildID);
|
||||||
|
|
||||||
// Delay 2 seconds for the audit log
|
// Delay 2 seconds for the audit log
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ public class GuildBanRemoveResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildBanRemove evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildBanRemove evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
var guildConfig = await guildRepository.GetAsync(evt.GuildID);
|
var guildConfig = await guildRepository.GetAsync(evt.GuildID);
|
||||||
|
|
||||||
// Delay 2 seconds for the audit log
|
// Delay 2 seconds for the audit log
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ public class GuildCreateResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildCreate evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildCreate evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
ulong guildId;
|
ulong guildId;
|
||||||
string? guildName = null;
|
string? guildName = null;
|
||||||
if (evt.Guild.TryPickT0(out var guild, out var unavailableGuild))
|
if (evt.Guild.TryPickT0(out var guild, out var unavailableGuild))
|
||||||
|
|
@ -101,6 +103,8 @@ public class GuildCreateResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildDelete evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildDelete evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
if (evt.IsUnavailable.OrDefault(false))
|
if (evt.IsUnavailable.OrDefault(false))
|
||||||
{
|
{
|
||||||
_logger.Debug("Guild {GuildId} became unavailable", evt.ID);
|
_logger.Debug("Guild {GuildId} became unavailable", evt.ID);
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ public class GuildEmojisUpdateResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildEmojisUpdate evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildEmojisUpdate evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!emojiCache.TryGet(evt.GuildID, out var oldEmoji))
|
if (!emojiCache.TryGet(evt.GuildID, out var oldEmoji))
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using Catalogger.Backend.Cache;
|
using Catalogger.Backend.Cache;
|
||||||
|
using Catalogger.Backend.Extensions;
|
||||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||||
using Remora.Discord.Gateway.Responders;
|
using Remora.Discord.Gateway.Responders;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
@ -27,6 +28,8 @@ public class GuildMembersChunkResponder(ILogger logger, IMemberCache memberCache
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildMembersChunk evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildMembersChunk evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
_logger.Debug(
|
_logger.Debug(
|
||||||
"Received chunk {ChunkIndex} / {ChunkCount} for guild {GuildId}",
|
"Received chunk {ChunkIndex} / {ChunkCount} for guild {GuildId}",
|
||||||
evt.ChunkIndex + 1,
|
evt.ChunkIndex + 1,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ public class GuildUpdateResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildUpdate evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildUpdate evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!guildCache.TryGet(evt.ID, out var oldGuild))
|
if (!guildCache.TryGet(evt.ID, out var oldGuild))
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ public class InviteCreateResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IInviteCreate evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IInviteCreate evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
var guildId = evt.GuildID.Value;
|
var guildId = evt.GuildID.Value;
|
||||||
|
|
||||||
var invitesResult = await guildApi.GetGuildInvitesAsync(guildId, ct);
|
var invitesResult = await guildApi.GetGuildInvitesAsync(guildId, ct);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ public class InviteDeleteResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IInviteDelete evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IInviteDelete evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
var guildId = evt.GuildID.Value;
|
var guildId = evt.GuildID.Value;
|
||||||
|
|
||||||
var dbDeleteCount = await inviteRepository.DeleteInviteAsync(guildId, evt.Code);
|
var dbDeleteCount = await inviteRepository.DeleteInviteAsync(guildId, evt.Code);
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ public class GuildMemberAddResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildMemberAdd member, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildMemberAdd member, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(member);
|
||||||
|
|
||||||
await memberCache.SetAsync(member.GuildID, member);
|
await memberCache.SetAsync(member.GuildID, member);
|
||||||
await memberCache.SetMemberNamesAsync(member.GuildID, [member]);
|
await memberCache.SetMemberNamesAsync(member.GuildID, [member]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ public class GuildMemberRemoveResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildMemberRemove evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildMemberRemove evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var embed = new EmbedBuilder()
|
var embed = new EmbedBuilder()
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ public class GuildMemberUpdateResponder(
|
||||||
CancellationToken ct = default
|
CancellationToken ct = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(newMember);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var oldMember = await memberCache.TryGetAsync(newMember.GuildID, newMember.User.ID);
|
var oldMember = await memberCache.TryGetAsync(newMember.GuildID, newMember.User.ID);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ public class MessageCreateResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IMessageCreate msg, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IMessageCreate msg, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var __ = LogUtils.Enrich(msg);
|
||||||
|
|
||||||
userCache.UpdateUser(msg.Author);
|
userCache.UpdateUser(msg.Author);
|
||||||
CataloggerMetrics.MessagesReceived.Inc();
|
CataloggerMetrics.MessagesReceived.Inc();
|
||||||
|
|
||||||
|
|
@ -75,7 +77,7 @@ public class MessageCreateResponder(
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
await messageRepository.SaveMessageAsync(msg, ct);
|
await messageRepository.SaveMessageAsync(msg, msg.GuildID, ct);
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,6 +144,19 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services)
|
||||||
await using var messageRepository =
|
await using var messageRepository =
|
||||||
scope.ServiceProvider.GetRequiredService<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(
|
_logger.Debug(
|
||||||
"Setting proxy data for {MessageId} and ignoring {OriginalId}",
|
"Setting proxy data for {MessageId} and ignoring {OriginalId}",
|
||||||
msgId,
|
msgId,
|
||||||
|
|
@ -193,6 +208,19 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services)
|
||||||
pkMessage.Original
|
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(
|
await messageRepository.SetProxiedMessageDataAsync(
|
||||||
msgId,
|
msgId,
|
||||||
pkMessage.Original,
|
pkMessage.Original,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ public class MessageDeleteBulkResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IMessageDeleteBulk evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IMessageDeleteBulk evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
var guild = await guildRepository.GetAsync(evt.GuildID);
|
var guild = await guildRepository.GetAsync(evt.GuildID);
|
||||||
if (guild.IsMessageIgnored(evt.ChannelID, null, null))
|
if (guild.IsMessageIgnored(evt.ChannelID, null, null))
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ using Catalogger.Backend.Database.Repositories;
|
||||||
using Catalogger.Backend.Extensions;
|
using Catalogger.Backend.Extensions;
|
||||||
using Catalogger.Backend.Services;
|
using Catalogger.Backend.Services;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using NodaTime;
|
|
||||||
using Remora.Discord.API;
|
using Remora.Discord.API;
|
||||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
|
@ -37,7 +36,6 @@ public class MessageDeleteResponder(
|
||||||
WebhookExecutorService webhookExecutor,
|
WebhookExecutorService webhookExecutor,
|
||||||
ChannelCache channelCache,
|
ChannelCache channelCache,
|
||||||
UserCache userCache,
|
UserCache userCache,
|
||||||
IClock clock,
|
|
||||||
PluralkitApiService pluralkitApi
|
PluralkitApiService pluralkitApi
|
||||||
) : IResponder<IMessageDelete>
|
) : IResponder<IMessageDelete>
|
||||||
{
|
{
|
||||||
|
|
@ -48,6 +46,8 @@ public class MessageDeleteResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IMessageDelete evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IMessageDelete evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
if (!evt.GuildID.IsDefined())
|
if (!evt.GuildID.IsDefined())
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
|
|
||||||
|
|
@ -64,20 +64,22 @@ public class MessageDeleteResponder(
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
|
|
||||||
var guild = await guildRepository.GetAsync(evt.GuildID);
|
var guild = await guildRepository.GetAsync(evt.GuildID);
|
||||||
if (guild.IsMessageIgnored(evt.ChannelID, null, null))
|
|
||||||
return Result.Success;
|
|
||||||
|
|
||||||
var msg = await messageRepository.GetMessageAsync(evt.ID.Value, ct);
|
var msg = await messageRepository.GetMessageAsync(evt.ID.Value, ct);
|
||||||
// Sometimes a message that *should* be logged isn't stored in the database, notify the user of that
|
// Sometimes a message that *should* be logged isn't stored in the database, notify the user of that
|
||||||
if (msg == null)
|
if (msg == null)
|
||||||
{
|
{
|
||||||
|
_logger.Debug(
|
||||||
|
"Deleted message {MessageId} should be logged but is not in the database",
|
||||||
|
evt.ID
|
||||||
|
);
|
||||||
|
|
||||||
webhookExecutor.QueueLog(
|
webhookExecutor.QueueLog(
|
||||||
webhookExecutor.GetLogChannel(guild, LogChannelType.MessageDelete, evt.ChannelID),
|
webhookExecutor.GetLogChannel(guild, LogChannelType.MessageDelete, evt.ChannelID),
|
||||||
new Embed(
|
new Embed(
|
||||||
Title: "Message deleted",
|
Title: "Message deleted",
|
||||||
Description: $"A message not found in the database was deleted in <#{evt.ChannelID}> ({evt.ChannelID}).",
|
Description: $"A message not found in the database was deleted in <#{evt.ChannelID}> ({evt.ChannelID}).",
|
||||||
Footer: new EmbedFooter(Text: $"ID: {evt.ID}"),
|
Footer: new EmbedFooter(Text: $"ID: {evt.ID} | Original sent at"),
|
||||||
Timestamp: clock.GetCurrentInstant().ToDateTimeOffset()
|
Timestamp: evt.ID.Timestamp
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -106,15 +108,20 @@ public class MessageDeleteResponder(
|
||||||
evt.ChannelID,
|
evt.ChannelID,
|
||||||
msg.UserId
|
msg.UserId
|
||||||
);
|
);
|
||||||
if (logChannel == null)
|
if (logChannel is null or 0)
|
||||||
return Result.Success;
|
{
|
||||||
|
_logger.Debug(
|
||||||
|
"Message {MessageId} should not be logged; either ignored or message delete logs are disabled",
|
||||||
|
evt.ID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var user = await userCache.GetUserAsync(DiscordSnowflake.New(msg.UserId));
|
var user = await userCache.GetUserAsync(DiscordSnowflake.New(msg.UserId));
|
||||||
var builder = new EmbedBuilder()
|
var builder = new EmbedBuilder()
|
||||||
.WithTitle("Message deleted")
|
.WithTitle("Message deleted")
|
||||||
.WithDescription(msg.Content)
|
.WithDescription(msg.Content)
|
||||||
.WithColour(DiscordUtils.Red)
|
.WithColour(DiscordUtils.Red)
|
||||||
.WithFooter($"ID: {msg.Id}")
|
.WithFooter($"ID: {msg.Id} | Original sent at")
|
||||||
.WithTimestamp(evt.ID);
|
.WithTimestamp(evt.ID);
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ using Catalogger.Backend.Services;
|
||||||
using Remora.Discord.API;
|
using Remora.Discord.API;
|
||||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
using Remora.Discord.API.Gateway.Events;
|
|
||||||
using Remora.Discord.API.Objects;
|
using Remora.Discord.API.Objects;
|
||||||
using Remora.Discord.Extensions.Embeds;
|
using Remora.Discord.Extensions.Embeds;
|
||||||
using Remora.Discord.Gateway.Responders;
|
using Remora.Discord.Gateway.Responders;
|
||||||
|
|
@ -40,11 +39,9 @@ public class MessageUpdateResponder(
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger = logger.ForContext<MessageUpdateResponder>();
|
private readonly ILogger _logger = logger.ForContext<MessageUpdateResponder>();
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IMessageUpdate evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IMessageUpdate msg, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// Discord only *very* recently changed message update events to have all fields,
|
using var _ = LogUtils.Enrich(msg);
|
||||||
// so we convert the event to a MessageCreate to avoid having to unwrap every single field
|
|
||||||
var msg = ConvertToMessageCreate(evt);
|
|
||||||
|
|
||||||
if (!msg.GuildID.IsDefined())
|
if (!msg.GuildID.IsDefined())
|
||||||
{
|
{
|
||||||
|
|
@ -132,7 +129,7 @@ public class MessageUpdateResponder(
|
||||||
if (oldMessage is { System: not null, Member: not null })
|
if (oldMessage is { System: not null, Member: not null })
|
||||||
{
|
{
|
||||||
embedBuilder.WithTitle($"Message by {msg.Author.Username} edited");
|
embedBuilder.WithTitle($"Message by {msg.Author.Username} edited");
|
||||||
embedBuilder.AddField("\u200b", "**PluralKit information**", false);
|
embedBuilder.AddField("\u200b", "**PluralKit information**");
|
||||||
embedBuilder.AddField("System ID", oldMessage.System, true);
|
embedBuilder.AddField("System ID", oldMessage.System, true);
|
||||||
embedBuilder.AddField("Member ID", oldMessage.Member, true);
|
embedBuilder.AddField("Member ID", oldMessage.Member, true);
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +169,7 @@ public class MessageUpdateResponder(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
!await messageRepository.SaveMessageAsync(msg, ct)
|
!await messageRepository.SaveMessageAsync(msg, msg.GuildID, ct)
|
||||||
&& msg.ApplicationID.Is(DiscordUtils.PkUserId)
|
&& msg.ApplicationID.Is(DiscordUtils.PkUserId)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
@ -194,44 +191,6 @@ 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)
|
private static IEnumerable<string> ChunksUpTo(string str, int maxChunkSize)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < str.Length; i += 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>();
|
private readonly ILogger _logger = logger.ForContext<ReadyResponder>();
|
||||||
|
|
||||||
public Task<Result> RespondAsync(IReady gatewayEvent, CancellationToken ct = default)
|
public Task<Result> RespondAsync(IReady evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var shardId = gatewayEvent.Shard.TryGet(out var shard)
|
using var _ = LogUtils.Enrich(evt);
|
||||||
? (shard.ShardID, shard.ShardCount)
|
|
||||||
: (0, 1);
|
var shardId = evt.Shard.TryGet(out var shard) ? (shard.ShardID, shard.ShardCount) : (0, 1);
|
||||||
_logger.Information(
|
_logger.Information(
|
||||||
"Ready as {User} on shard {ShardId}/{ShardCount}",
|
"Ready as {User} on shard {ShardId}/{ShardCount}",
|
||||||
gatewayEvent.User.Tag(),
|
evt.User.Tag(),
|
||||||
shardId.Item1,
|
shardId.Item1,
|
||||||
shardId.Item2
|
shardId.Item2
|
||||||
);
|
);
|
||||||
if (shardId.Item1 == 0)
|
if (shardId.Item1 == 0)
|
||||||
webhookExecutorService.SetSelfUser(gatewayEvent.User);
|
webhookExecutorService.SetSelfUser(evt.User);
|
||||||
|
|
||||||
return Task.FromResult(Result.Success);
|
return Task.FromResult(Result.Success);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ public class RoleCreateResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildRoleCreate evt, CancellationToken ct = default)
|
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);
|
_logger.Debug("Received new role {RoleId} in guild {GuildId}", evt.Role.ID, evt.GuildID);
|
||||||
roleCache.Set(evt.Role, evt.GuildID);
|
roleCache.Set(evt.Role, evt.GuildID);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ public class RoleDeleteResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildRoleDelete evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildRoleDelete evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var __ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!roleCache.TryGet(evt.RoleID, out var role))
|
if (!roleCache.TryGet(evt.RoleID, out var role))
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ public class RoleUpdateResponder(
|
||||||
|
|
||||||
public async Task<Result> RespondAsync(IGuildRoleUpdate evt, CancellationToken ct = default)
|
public async Task<Result> RespondAsync(IGuildRoleUpdate evt, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
using var _ = LogUtils.Enrich(evt);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var newRole = evt.Role;
|
var newRole = evt.Role;
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,7 @@ public static class BuildInfo
|
||||||
Version = versionData[0];
|
Version = versionData[0];
|
||||||
if (versionData[1] != "0" || dirty)
|
if (versionData[1] != "0" || dirty)
|
||||||
Version += $"+{versionData[2]}";
|
Version += $"+{versionData[2]}";
|
||||||
|
if (dirty)
|
||||||
|
Version += ".dirty";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,7 @@ internal record RedisMember(
|
||||||
User.ToRemoraUser(),
|
User.ToRemoraUser(),
|
||||||
Nickname,
|
Nickname,
|
||||||
Avatar != null ? new ImageHash(Avatar) : null,
|
Avatar != null ? new ImageHash(Avatar) : null,
|
||||||
|
Banner: null,
|
||||||
Roles.Select(DiscordSnowflake.New).ToList(),
|
Roles.Select(DiscordSnowflake.New).ToList(),
|
||||||
JoinedAt,
|
JoinedAt,
|
||||||
PremiumSince,
|
PremiumSince,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
|
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
|
||||||
|
|
@ -21,26 +20,25 @@
|
||||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
|
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
|
||||||
<PackageReference Include="LazyCache" Version="2.4.0"/>
|
<PackageReference Include="LazyCache" Version="2.4.0"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8"/>
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/>
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||||
<PackageReference Include="NodaTime" Version="3.1.12"/>
|
<PackageReference Include="NodaTime" Version="3.2.0" />
|
||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0"/>
|
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0"/>
|
||||||
<PackageReference Include="Npgsql" Version="8.0.5" />
|
<PackageReference Include="Npgsql" Version="9.0.0" />
|
||||||
<PackageReference Include="Npgsql.NodaTime" Version="8.0.5" />
|
<PackageReference Include="Npgsql.NodaTime" Version="9.0.0" />
|
||||||
<PackageReference Include="Polly.Core" Version="8.4.2"/>
|
<PackageReference Include="Polly.Core" Version="8.5.2" />
|
||||||
<PackageReference Include="Polly.RateLimiting" Version="8.4.2"/>
|
<PackageReference Include="Polly.RateLimiting" Version="8.5.0" />
|
||||||
<PackageReference Include="prometheus-net" Version="8.2.1"/>
|
<PackageReference Include="prometheus-net" Version="8.2.1"/>
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||||
<PackageReference Include="Remora.Sdk" Version="3.1.2"/>
|
<PackageReference Include="Remora.Sdk" Version="3.1.2"/>
|
||||||
<PackageReference Include="Remora.Discord" Version="2024.3.0-github11168366508"/>
|
<PackageReference Include="Remora.Discord" Version="2025.1.0" />
|
||||||
<PackageReference Include="Serilog" Version="4.0.2"/>
|
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2"/>
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0"/>
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0"/>
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
||||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/>
|
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/>
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16"/>
|
<PackageReference Include="StackExchange.Redis" Version="2.8.16"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.1"/>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,13 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Remora.Results;
|
||||||
|
using RemoraResult = Remora.Results.Result;
|
||||||
|
|
||||||
namespace Catalogger.Backend;
|
namespace Catalogger.Backend;
|
||||||
|
|
||||||
public class CataloggerError(string message) : Exception(message) { }
|
public class CataloggerError(string message) : Exception(message), IResultError
|
||||||
|
{
|
||||||
|
public static RemoraResult Result(string message) =>
|
||||||
|
RemoraResult.FromError(new CataloggerError(message));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ public static class CataloggerMetrics
|
||||||
|
|
||||||
public static long MessageRateMinute { get; set; }
|
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(
|
public static readonly Gauge GuildsCached = Metrics.CreateGauge(
|
||||||
"catalogger_cache_guilds",
|
"catalogger_cache_guilds",
|
||||||
"Number of guilds in the cache"
|
"Number of guilds in the cache"
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ public class Config
|
||||||
public bool EnableMetrics { get; init; } = true;
|
public bool EnableMetrics { get; init; } = true;
|
||||||
|
|
||||||
public string? SeqLogUrl { get; init; }
|
public string? SeqLogUrl { get; init; }
|
||||||
|
public string? PrometheusUrl { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DatabaseConfig
|
public class DatabaseConfig
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,13 @@ using System.Data;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Catalogger.Backend.Database;
|
namespace Catalogger.Backend.Database;
|
||||||
|
|
||||||
public class DatabaseConnection(Guid id, ILogger logger, NpgsqlConnection inner)
|
public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposable
|
||||||
: DbConnection,
|
|
||||||
IDisposable
|
|
||||||
{
|
{
|
||||||
public Guid ConnectionId => id;
|
|
||||||
public NpgsqlConnection Inner => inner;
|
public NpgsqlConnection Inner => inner;
|
||||||
private readonly ILogger _logger = logger.ForContext<DatabaseConnection>();
|
|
||||||
private readonly DateTimeOffset _openTime = DateTimeOffset.UtcNow;
|
|
||||||
|
|
||||||
private bool _hasClosed;
|
private bool _hasClosed;
|
||||||
|
|
||||||
|
|
@ -43,8 +39,6 @@ public class DatabaseConnection(Guid id, ILogger logger, NpgsqlConnection inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabasePool.DecrementConnections();
|
DatabasePool.DecrementConnections();
|
||||||
var openFor = DateTimeOffset.UtcNow - _openTime;
|
|
||||||
_logger.Verbose("Closing connection {ConnId}, open for {OpenFor}", ConnectionId, openFor);
|
|
||||||
_hasClosed = true;
|
_hasClosed = true;
|
||||||
await inner.CloseAsync();
|
await inner.CloseAsync();
|
||||||
}
|
}
|
||||||
|
|
@ -52,19 +46,22 @@ public class DatabaseConnection(Guid id, ILogger logger, NpgsqlConnection inner)
|
||||||
protected override async ValueTask<DbTransaction> BeginDbTransactionAsync(
|
protected override async ValueTask<DbTransaction> BeginDbTransactionAsync(
|
||||||
IsolationLevel isolationLevel,
|
IsolationLevel isolationLevel,
|
||||||
CancellationToken cancellationToken
|
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()
|
public new void Dispose()
|
||||||
{
|
{
|
||||||
Close();
|
Dispose(true);
|
||||||
inner.Dispose();
|
|
||||||
GC.SuppressFinalize(this);
|
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();
|
||||||
|
inner.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
public override async ValueTask DisposeAsync()
|
public override async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await CloseAsync();
|
await CloseAsync();
|
||||||
|
|
@ -73,13 +70,13 @@ public class DatabaseConnection(Guid id, ILogger logger, NpgsqlConnection inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) =>
|
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) =>
|
||||||
inner.BeginTransaction(isolationLevel);
|
throw new SyncException(nameof(BeginDbTransaction));
|
||||||
|
|
||||||
public override void ChangeDatabase(string databaseName) => inner.ChangeDatabase(databaseName);
|
public override void ChangeDatabase(string databaseName) => inner.ChangeDatabase(databaseName);
|
||||||
|
|
||||||
public override void Close() => inner.Close();
|
public override void Close() => throw new SyncException(nameof(Close));
|
||||||
|
|
||||||
public override void Open() => inner.Open();
|
public override void Open() => throw new SyncException(nameof(Open));
|
||||||
|
|
||||||
[AllowNull]
|
[AllowNull]
|
||||||
public override string ConnectionString
|
public override string ConnectionString
|
||||||
|
|
@ -94,4 +91,6 @@ public class DatabaseConnection(Guid id, ILogger logger, NpgsqlConnection inner)
|
||||||
public override string ServerVersion => inner.ServerVersion;
|
public override string ServerVersion => inner.ServerVersion;
|
||||||
|
|
||||||
protected override DbCommand CreateDbCommand() => inner.CreateCommand();
|
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)
|
if (hasMigrationTable)
|
||||||
{
|
{
|
||||||
return await conn.QuerySingleOrDefaultAsync<MigrationEntry>(
|
return await conn.QuerySingleOrDefaultAsync<MigrationEntry>(
|
||||||
"SELECT * FROM migrations ORDER BY applied_at DESC LIMIT 1"
|
"SELECT * FROM migrations ORDER BY applied_at DESC, migration_name DESC LIMIT 1"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +163,7 @@ public class DatabaseMigrator(ILogger logger, IClock clock, DatabaseConnection c
|
||||||
return await reader.ReadToEndAsync();
|
return await reader.ReadToEndAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<string> GetMigrationNames() =>
|
private static IEnumerable<string> GetMigrationNames() =>
|
||||||
typeof(DatabasePool)
|
typeof(DatabasePool)
|
||||||
.Assembly.GetManifestResourceNames()
|
.Assembly.GetManifestResourceNames()
|
||||||
.Where(s => s.StartsWith($"{RootPath}.Migrations"))
|
.Where(s => s.StartsWith($"{RootPath}.Migrations"))
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,13 @@ namespace Catalogger.Backend.Database;
|
||||||
|
|
||||||
public class DatabasePool
|
public class DatabasePool
|
||||||
{
|
{
|
||||||
private readonly ILogger _rootLogger;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly NpgsqlDataSource _dataSource;
|
private readonly NpgsqlDataSource _dataSource;
|
||||||
|
|
||||||
private static int _openConnections;
|
private static int _openConnections;
|
||||||
public static int OpenConnections => _openConnections;
|
public static int OpenConnections => _openConnections;
|
||||||
|
|
||||||
public DatabasePool(Config config, ILogger logger, ILoggerFactory? loggerFactory)
|
public DatabasePool(Config config, ILoggerFactory? loggerFactory)
|
||||||
{
|
{
|
||||||
_rootLogger = logger;
|
|
||||||
_logger = logger.ForContext<DatabasePool>();
|
|
||||||
|
|
||||||
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
||||||
{
|
{
|
||||||
Timeout = config.Database.Timeout ?? 5,
|
Timeout = config.Database.Timeout ?? 5,
|
||||||
|
|
@ -51,24 +46,14 @@ public class DatabasePool
|
||||||
|
|
||||||
public async Task<DatabaseConnection> AcquireAsync(CancellationToken ct = default)
|
public async Task<DatabaseConnection> AcquireAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return new DatabaseConnection(
|
IncrementConnections();
|
||||||
LogOpen(),
|
return new DatabaseConnection(await _dataSource.OpenConnectionAsync(ct));
|
||||||
_rootLogger,
|
|
||||||
await _dataSource.OpenConnectionAsync(ct)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DatabaseConnection Acquire()
|
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();
|
IncrementConnections();
|
||||||
return connId;
|
return new DatabaseConnection(_dataSource.OpenConnection());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteAsync(
|
public async Task ExecuteAsync(
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,10 @@ public class RedisService(Config config)
|
||||||
config.Database.Redis!
|
config.Database.Redis!
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly JsonSerializerOptions _options =
|
private readonly JsonSerializerOptions _options = new()
|
||||||
new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
};
|
||||||
|
|
||||||
public IDatabase GetDatabase(int db = -1) => _multiplexer.GetDatabase(db);
|
public IDatabase GetDatabase(int db = -1) => _multiplexer.GetDatabase(db);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ using Catalogger.Backend.Extensions;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Remora.Discord.API;
|
using Remora.Discord.API;
|
||||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||||
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
using Remora.Rest.Core;
|
using Remora.Rest.Core;
|
||||||
|
|
||||||
namespace Catalogger.Backend.Database.Repositories;
|
namespace Catalogger.Backend.Database.Repositories;
|
||||||
|
|
@ -63,7 +64,11 @@ public class MessageRepository(
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a new message. If the message is already in the database, updates the existing message instead.
|
/// Adds a new message. If the message is already in the database, updates the existing message instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<bool> SaveMessageAsync(IMessageCreate msg, CancellationToken ct = default)
|
public async Task<bool> SaveMessageAsync(
|
||||||
|
IMessage msg,
|
||||||
|
Optional<Snowflake> guildId,
|
||||||
|
CancellationToken ct = default
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var content = await Task.Run(
|
var content = await Task.Run(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -107,7 +112,9 @@ public class MessageRepository(
|
||||||
Id = msg.ID.Value,
|
Id = msg.ID.Value,
|
||||||
UserId = msg.Author.ID.Value,
|
UserId = msg.Author.ID.Value,
|
||||||
ChannelId = msg.ChannelID.Value,
|
ChannelId = msg.ChannelID.Value,
|
||||||
GuildId = msg.GuildID.Map(s => s.Value).OrDefault(),
|
GuildId = guildId.IsDefined(out var guildIdValue)
|
||||||
|
? guildIdValue.Value
|
||||||
|
: (ulong?)null,
|
||||||
Content = content,
|
Content = content,
|
||||||
Username = username,
|
Username = username,
|
||||||
Metadata = metadata,
|
Metadata = metadata,
|
||||||
|
|
|
||||||
152
Catalogger.Backend/Extensions/LogUtils.cs
Normal file
152
Catalogger.Backend/Extensions/LogUtils.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||||
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
using Remora.Rest.Core;
|
||||||
|
using Serilog.Context;
|
||||||
|
|
||||||
|
namespace Catalogger.Backend.Extensions;
|
||||||
|
|
||||||
|
public static class LogUtils
|
||||||
|
{
|
||||||
|
public static IDisposable Enrich<T>(T evt)
|
||||||
|
where T : IGatewayEvent
|
||||||
|
{
|
||||||
|
var type = ("Event", typeof(T).Name);
|
||||||
|
|
||||||
|
return evt switch
|
||||||
|
{
|
||||||
|
IMessageDelete md => PushProperties(
|
||||||
|
type,
|
||||||
|
("GuildId", md.GuildID),
|
||||||
|
("ChannelId", md.ChannelID),
|
||||||
|
("MessageId", md.ID)
|
||||||
|
),
|
||||||
|
IMessageUpdate mu => PushProperties(
|
||||||
|
type,
|
||||||
|
("GuildId", mu.GuildID),
|
||||||
|
("ChannelId", mu.ChannelID),
|
||||||
|
("MessageId", mu.ID)
|
||||||
|
),
|
||||||
|
IMessageCreate mc => PushProperties(
|
||||||
|
type,
|
||||||
|
("GuildId", mc.GuildID),
|
||||||
|
("ChannelId", mc.ChannelID),
|
||||||
|
("MessageId", mc.ID)
|
||||||
|
),
|
||||||
|
IMessageDeleteBulk mdb => PushProperties(
|
||||||
|
type,
|
||||||
|
("GuildId", mdb.GuildID),
|
||||||
|
("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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IDisposable PushProperties(params (string, object?)[] properties) =>
|
||||||
|
new MultiDisposable(
|
||||||
|
properties
|
||||||
|
.Select(p =>
|
||||||
|
{
|
||||||
|
if (p.Item2 is Optional<Snowflake> s)
|
||||||
|
return LogContext.PushProperty(p.Item1, s.IsDefined() ? s.Value : null);
|
||||||
|
return LogContext.PushProperty(p.Item1, p.Item2);
|
||||||
|
})
|
||||||
|
.ToArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
private record MultiDisposable(IDisposable[] Entries) : IDisposable
|
||||||
|
{
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var e in Entries)
|
||||||
|
e.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,19 +51,22 @@ public static class StartupExtensions
|
||||||
{
|
{
|
||||||
var logCfg = new LoggerConfiguration()
|
var logCfg = new LoggerConfiguration()
|
||||||
.Enrich.FromLogContext()
|
.Enrich.FromLogContext()
|
||||||
.MinimumLevel.Is(config.Logging.LogEventLevel)
|
.MinimumLevel.Verbose()
|
||||||
|
// Most Microsoft.* package logs are needlessly verbose, so we restrict them to INFO level and up
|
||||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||||
// ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead.
|
// 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.
|
// 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.Hosting", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", 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
|
// The default theme doesn't support light mode
|
||||||
.WriteTo.Console(theme: AnsiConsoleTheme.Sixteen, applyThemeToRedirectedOutput: true);
|
.WriteTo.Console(
|
||||||
|
theme: AnsiConsoleTheme.Sixteen,
|
||||||
|
applyThemeToRedirectedOutput: true,
|
||||||
|
restrictedToMinimumLevel: config.Logging.LogEventLevel
|
||||||
|
);
|
||||||
|
|
||||||
if (config.Logging.SeqLogUrl != null)
|
if (config.Logging.SeqLogUrl != null)
|
||||||
{
|
{
|
||||||
|
|
@ -274,8 +277,6 @@ public static class StartupExtensions
|
||||||
app.UseSerilogRequestLogging();
|
app.UseSerilogRequestLogging();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseHttpMetrics();
|
app.UseHttpMetrics();
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
app.UseMiddleware<ErrorMiddleware>();
|
app.UseMiddleware<ErrorMiddleware>();
|
||||||
app.UseMiddleware<AuthenticationMiddleware>();
|
app.UseMiddleware<AuthenticationMiddleware>();
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,7 @@ builder
|
||||||
| GatewayIntents.GuildMessages
|
| GatewayIntents.GuildMessages
|
||||||
| GatewayIntents.GuildWebhooks
|
| GatewayIntents.GuildWebhooks
|
||||||
| GatewayIntents.MessageContents
|
| GatewayIntents.MessageContents
|
||||||
// Actually GUILD_EXPRESSIONS
|
| GatewayIntents.GuildExpressions;
|
||||||
| GatewayIntents.GuildEmojisAndStickers;
|
|
||||||
|
|
||||||
// Set a default status for all shards. This is updated to a shard-specific one in StatusUpdateService.
|
// Set a default status for all shards. This is updated to a shard-specific one in StatusUpdateService.
|
||||||
g.Presence = new UpdatePresence(
|
g.Presence = new UpdatePresence(
|
||||||
|
|
@ -97,12 +96,7 @@ builder
|
||||||
.WithCommandGroup<KeyRoleCommands>()
|
.WithCommandGroup<KeyRoleCommands>()
|
||||||
.WithCommandGroup<InviteCommands>()
|
.WithCommandGroup<InviteCommands>()
|
||||||
.WithCommandGroup<IgnoreMessageCommands>()
|
.WithCommandGroup<IgnoreMessageCommands>()
|
||||||
.WithCommandGroup<IgnoreMessageCommands.Channels>()
|
|
||||||
.WithCommandGroup<IgnoreMessageCommands.Users>()
|
|
||||||
.WithCommandGroup<IgnoreMessageCommands.Roles>()
|
|
||||||
.WithCommandGroup<IgnoreEntitiesCommands>()
|
.WithCommandGroup<IgnoreEntitiesCommands>()
|
||||||
.WithCommandGroup<IgnoreEntitiesCommands.Channels>()
|
|
||||||
.WithCommandGroup<IgnoreEntitiesCommands.Roles>()
|
|
||||||
.WithCommandGroup<RedirectCommands>()
|
.WithCommandGroup<RedirectCommands>()
|
||||||
.WithCommandGroup<WatchlistCommands>()
|
.WithCommandGroup<WatchlistCommands>()
|
||||||
// End command tree
|
// End command tree
|
||||||
|
|
@ -120,12 +114,7 @@ builder.Services.AddMetricServer(o => o.Port = (ushort)config.Logging.MetricsPor
|
||||||
if (!config.Logging.EnableMetrics)
|
if (!config.Logging.EnableMetrics)
|
||||||
builder.Services.AddHostedService<BackgroundMetricsCollectionService>();
|
builder.Services.AddHostedService<BackgroundMetricsCollectionService>();
|
||||||
|
|
||||||
builder
|
builder.Services.MaybeAddDashboardServices(config).MaybeAddRedisCaches(config).AddCustomServices();
|
||||||
.Services.MaybeAddDashboardServices(config)
|
|
||||||
.MaybeAddRedisCaches(config)
|
|
||||||
.AddCustomServices()
|
|
||||||
.AddEndpointsApiExplorer()
|
|
||||||
.AddSwaggerGen();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ public class MetricsCollectionService(
|
||||||
|
|
||||||
var messageCount = await conn.ExecuteScalarAsync<int>("select count(id) from messages");
|
var messageCount = await conn.ExecuteScalarAsync<int>("select count(id) from messages");
|
||||||
|
|
||||||
|
CataloggerMetrics.DatabaseConnections.Set(DatabasePool.OpenConnections);
|
||||||
CataloggerMetrics.GuildsCached.Set(guildCache.Size);
|
CataloggerMetrics.GuildsCached.Set(guildCache.Size);
|
||||||
CataloggerMetrics.ChannelsCached.Set(channelCache.Size);
|
CataloggerMetrics.ChannelsCached.Set(channelCache.Size);
|
||||||
CataloggerMetrics.RolesCached.Set(roleCache.Size);
|
CataloggerMetrics.RolesCached.Set(roleCache.Size);
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,9 @@ public class NewsService(
|
||||||
|
|
||||||
private readonly ILogger _logger = logger.ForContext<NewsService>();
|
private readonly ILogger _logger = logger.ForContext<NewsService>();
|
||||||
private List<IMessage>? _messages;
|
private List<IMessage>? _messages;
|
||||||
|
private Instant _lastUpdated = Instant.MinValue;
|
||||||
private readonly SemaphoreSlim _lock = new(1);
|
private readonly SemaphoreSlim _lock = new(1);
|
||||||
private bool _isExpired => clock.GetCurrentInstant() > clock.GetCurrentInstant() + ExpiresAfter;
|
private bool _isExpired => clock.GetCurrentInstant() > _lastUpdated + ExpiresAfter;
|
||||||
|
|
||||||
public async Task<IEnumerable<NewsMessage>> GetNewsAsync()
|
public async Task<IEnumerable<NewsMessage>> GetNewsAsync()
|
||||||
{
|
{
|
||||||
|
|
@ -74,6 +75,7 @@ public class NewsService(
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
_lastUpdated = clock.GetCurrentInstant();
|
||||||
_lock.Release();
|
_lock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ public class TimeoutService(
|
||||||
_logger.Information("Populating timeout service with existing database timeouts");
|
_logger.Information("Populating timeout service with existing database timeouts");
|
||||||
|
|
||||||
await using var scope = serviceProvider.CreateAsyncScope();
|
await using var scope = serviceProvider.CreateAsyncScope();
|
||||||
var timeoutRepository = scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
|
await using var timeoutRepository =
|
||||||
|
scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
|
||||||
|
|
||||||
var timeouts = await timeoutRepository.GetAllAsync();
|
var timeouts = await timeoutRepository.GetAllAsync();
|
||||||
foreach (var timeout in timeouts)
|
foreach (var timeout in timeouts)
|
||||||
|
|
@ -53,8 +54,10 @@ public class TimeoutService(
|
||||||
_logger.Information("Sending timeout log for {TimeoutId}", timeoutId);
|
_logger.Information("Sending timeout log for {TimeoutId}", timeoutId);
|
||||||
|
|
||||||
await using var scope = serviceProvider.CreateAsyncScope();
|
await using var scope = serviceProvider.CreateAsyncScope();
|
||||||
var guildRepository = scope.ServiceProvider.GetRequiredService<GuildRepository>();
|
await using var guildRepository =
|
||||||
var timeoutRepository = scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
|
scope.ServiceProvider.GetRequiredService<GuildRepository>();
|
||||||
|
await using var timeoutRepository =
|
||||||
|
scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
|
||||||
|
|
||||||
var timeout = await timeoutRepository.RemoveAsync(timeoutId);
|
var timeout = await timeoutRepository.RemoveAsync(timeoutId);
|
||||||
if (timeout == null)
|
if (timeout == null)
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ public class WebhookExecutorService(
|
||||||
private readonly ILogger _logger = logger.ForContext<WebhookExecutorService>();
|
private readonly ILogger _logger = logger.ForContext<WebhookExecutorService>();
|
||||||
private readonly Snowflake _applicationId = DiscordSnowflake.New(config.Discord.ApplicationId);
|
private readonly Snowflake _applicationId = DiscordSnowflake.New(config.Discord.ApplicationId);
|
||||||
private readonly ConcurrentDictionary<ulong, ConcurrentQueue<IEmbed>> _cache = new();
|
private readonly ConcurrentDictionary<ulong, ConcurrentQueue<IEmbed>> _cache = new();
|
||||||
private readonly ConcurrentDictionary<ulong, object> _locks = new();
|
private readonly ConcurrentDictionary<ulong, Lock> _locks = new();
|
||||||
private readonly ConcurrentDictionary<ulong, Timer> _timers = new();
|
private readonly ConcurrentDictionary<ulong, Timer> _timers = new();
|
||||||
private IUser? _selfUser;
|
private IUser? _selfUser;
|
||||||
|
|
||||||
|
|
@ -189,7 +189,7 @@ public class WebhookExecutorService(
|
||||||
private List<IEmbed> TakeFromQueue(ulong channelId)
|
private List<IEmbed> TakeFromQueue(ulong channelId)
|
||||||
{
|
{
|
||||||
var queue = _cache.GetOrAdd(channelId, []);
|
var queue = _cache.GetOrAdd(channelId, []);
|
||||||
var channelLock = _locks.GetOrAdd(channelId, channelId);
|
var channelLock = _locks.GetOrAdd(channelId, new Lock());
|
||||||
lock (channelLock)
|
lock (channelLock)
|
||||||
{
|
{
|
||||||
var totalContentLength = 0;
|
var totalContentLength = 0;
|
||||||
|
|
@ -293,10 +293,10 @@ public class WebhookExecutorService(
|
||||||
roleIds != null && logChannelType is LogChannelType.GuildMemberUpdate;
|
roleIds != null && logChannelType is LogChannelType.GuildMemberUpdate;
|
||||||
|
|
||||||
if (isMessageLog)
|
if (isMessageLog)
|
||||||
return GetMessageLogChannel(guild, logChannelType, channelId, userId);
|
return GetLogChannelForMessageEvent(guild, logChannelType, channelId, userId);
|
||||||
|
|
||||||
if (isChannelLog)
|
if (isChannelLog)
|
||||||
return GetChannelLogChannel(guild, logChannelType, channelId!.Value);
|
return GetLogChannelForChannelEvent(guild, logChannelType, channelId!.Value);
|
||||||
|
|
||||||
if (isRoleLog && guild.IgnoredRoles.Contains(roleId!.Value.Value))
|
if (isRoleLog && guild.IgnoredRoles.Contains(roleId!.Value.Value))
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -305,20 +305,201 @@ public class WebhookExecutorService(
|
||||||
if (isMemberRoleUpdateLog && roleIds!.All(r => guild.IgnoredRoles.Contains(r.Value)))
|
if (isMemberRoleUpdateLog && roleIds!.All(r => guild.IgnoredRoles.Contains(r.Value)))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// If nothing is ignored, return the correct log channel!
|
// If nothing is ignored, and this isn't a message or channel event, return the default log channel.
|
||||||
return GetDefaultLogChannel(guild, logChannelType);
|
return GetDefaultLogChannel(guild, logChannelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ulong? GetChannelLogChannel(
|
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(
|
||||||
Guild guild,
|
Guild guild,
|
||||||
LogChannelType logChannelType,
|
LogChannelType logChannelType,
|
||||||
Snowflake channelId
|
Snowflake channelId
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!channelCache.TryGet(channelId, out var channel))
|
_logger.Verbose(
|
||||||
return GetDefaultLogChannel(guild, logChannelType);
|
"Getting log channel for event {Event} in guild {GuildId} and channel {ChannelId}",
|
||||||
|
logChannelType,
|
||||||
|
guild.Id,
|
||||||
|
channelId
|
||||||
|
);
|
||||||
|
|
||||||
Snowflake? categoryId;
|
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
|
||||||
|
)
|
||||||
|
{
|
||||||
if (
|
if (
|
||||||
channel.Type
|
channel.Type
|
||||||
is ChannelType.AnnouncementThread
|
is ChannelType.AnnouncementThread
|
||||||
|
|
@ -329,7 +510,16 @@ public class WebhookExecutorService(
|
||||||
// parent_id should always have a value for threads
|
// parent_id should always have a value for threads
|
||||||
channelId = channel.ParentID.Value!.Value;
|
channelId = channel.ParentID.Value!.Value;
|
||||||
if (!channelCache.TryGet(channelId, out var parentChannel))
|
if (!channelCache.TryGet(channelId, out var parentChannel))
|
||||||
return GetDefaultLogChannel(guild, logChannelType);
|
{
|
||||||
|
_logger.Verbose(
|
||||||
|
"Parent channel for thread {ChannelId} is not in cache, returning the default log channel",
|
||||||
|
channelId
|
||||||
|
);
|
||||||
|
|
||||||
|
channelId = Snowflake.CreateTimestampSnowflake();
|
||||||
|
categoryId = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
categoryId = parentChannel.ParentID.Value;
|
categoryId = parentChannel.ParentID.Value;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -338,94 +528,7 @@ public class WebhookExecutorService(
|
||||||
categoryId = channel.ParentID.Value;
|
categoryId = channel.ParentID.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the channel or its category is ignored
|
return true;
|
||||||
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) =>
|
public static ulong GetDefaultLogChannel(Guild guild, LogChannelType logChannelType) =>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ LogQueries = false
|
||||||
SeqLogUrl = http://localhost:5341
|
SeqLogUrl = http://localhost:5341
|
||||||
# Whether to enable Prometheus metrics. If disabled, Catalogger will update metrics manually every so often.
|
# Whether to enable Prometheus metrics. If disabled, Catalogger will update metrics manually every so often.
|
||||||
EnableMetrics = false
|
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]
|
[Database]
|
||||||
Url = Host=localhost;Database=postgres;Username=postgres;Password=postgres
|
Url = Host=localhost;Database=postgres;Username=postgres;Password=postgres
|
||||||
|
|
|
||||||
|
|
@ -1,751 +0,0 @@
|
||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"dependencies": {
|
|
||||||
"net8.0": {
|
|
||||||
"Dapper": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[2.1.35, )",
|
|
||||||
"resolved": "2.1.35",
|
|
||||||
"contentHash": "YKRwjVfrG7GYOovlGyQoMvr1/IJdn+7QzNXJxyMh0YfFF5yvDmTYaJOVYWsckreNjGsGSEtrMTpnzxTUq/tZQw=="
|
|
||||||
},
|
|
||||||
"Humanizer.Core": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[2.14.1, )",
|
|
||||||
"resolved": "2.14.1",
|
|
||||||
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
|
|
||||||
},
|
|
||||||
"LazyCache": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[2.4.0, )",
|
|
||||||
"resolved": "2.4.0",
|
|
||||||
"contentHash": "THig17vqe5PEs3wvTqFrNzorz2nD4Qz9F9C3YlAydU673CogAO8z1u8NNJD6x52I7oDCQ/N/HwJIZMBH8Y/Qiw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions": "2.1.0",
|
|
||||||
"Microsoft.Extensions.Caching.Memory": "2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.Mvc.NewtonsoftJson": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.0.8, )",
|
|
||||||
"resolved": "8.0.8",
|
|
||||||
"contentHash": "KL3lI8GmCnnROwDrbWbboVpHiXSNTyoLgYPdHus3hEjAwhSAm1JU5S+rmZk7w3Qt0rQfHVIFxKwCf6yapeZy+w==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.AspNetCore.JsonPatch": "8.0.8",
|
|
||||||
"Newtonsoft.Json": "13.0.3",
|
|
||||||
"Newtonsoft.Json.Bson": "1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.OpenApi": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.0.8, )",
|
|
||||||
"resolved": "8.0.8",
|
|
||||||
"contentHash": "wNHhohqP8rmsQ4UhKbd6jZMD6l+2Q/+DvRBT0Cgqeuglr13aF6sSJWicZKCIhZAUXzuhkdwtHVc95MlPlFk0dA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.OpenApi": "1.4.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Newtonsoft.Json": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[13.0.3, )",
|
|
||||||
"resolved": "13.0.3",
|
|
||||||
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
|
||||||
},
|
|
||||||
"NodaTime": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[3.1.12, )",
|
|
||||||
"resolved": "3.1.12",
|
|
||||||
"contentHash": "nDcUbG0jiEXmV8cOz7V8GnUKlmPJjqZm/R+E2JNnUSdlMoaQ19xSU8GXFLReGs/Nt8xdBfA8XfO77xVboWO1Vg==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Runtime.CompilerServices.Unsafe": "4.7.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NodaTime.Serialization.SystemTextJson": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[1.2.0, )",
|
|
||||||
"resolved": "1.2.0",
|
|
||||||
"contentHash": "HNMQdHw6xCrNaHEEvJlBek+uUNI4uySEQhU3t8FibZT9ASMz40y5qkLIwhrHsnXhxUzOPP4tmAGy8PfBwc3zMg==",
|
|
||||||
"dependencies": {
|
|
||||||
"NodaTime": "[3.0.0, 4.0.0)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Npgsql": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.0.5, )",
|
|
||||||
"resolved": "8.0.5",
|
|
||||||
"contentHash": "zRG5V8cyeZLpzJlKzFKjEwkRMYIYnHWJvEor2lWXeccS2E1G2nIWYYhnukB51iz5XsWSVEtqg3AxTWM0QJ6vfg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Npgsql.NodaTime": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.0.5, )",
|
|
||||||
"resolved": "8.0.5",
|
|
||||||
"contentHash": "oC7Ml5TDuQlcGECB5ML0XsPxFrYu3OdpG7c9cuqhB+xunLvqbZ0zXQoPJjvXK9KDNPDB/II61HNdsNas9f2J3A==",
|
|
||||||
"dependencies": {
|
|
||||||
"NodaTime": "3.1.9",
|
|
||||||
"Npgsql": "8.0.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Polly.Core": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.4.2, )",
|
|
||||||
"resolved": "8.4.2",
|
|
||||||
"contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g=="
|
|
||||||
},
|
|
||||||
"Polly.RateLimiting": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.4.2, )",
|
|
||||||
"resolved": "8.4.2",
|
|
||||||
"contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Polly.Core": "8.4.2",
|
|
||||||
"System.Threading.RateLimiting": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prometheus-net": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.2.1, )",
|
|
||||||
"resolved": "8.2.1",
|
|
||||||
"contentHash": "3wVgdEPOCBF752s2xps5T+VH+c9mJK8S8GKEDg49084P6JZMumTZI5Te6aJ9MQpX0sx7om6JOnBpIi7ZBmmiDQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Http": "3.1.0",
|
|
||||||
"Microsoft.Extensions.ObjectPool": "7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prometheus-net.AspNetCore": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.2.1, )",
|
|
||||||
"resolved": "8.2.1",
|
|
||||||
"contentHash": "/4TfTvbwIDqpaKTiWvEsjUywiHYF9zZvGZF5sK15avoDsUO/WPQbKsF8TiMaesuphdFQPK2z52P0zk6j26V0rQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"prometheus-net": "8.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[2024.3.0-github11168366508, )",
|
|
||||||
"resolved": "2024.3.0-github11168366508",
|
|
||||||
"contentHash": "tlqwVPeILmUmjEIsDgRQQChwCPnwAvpJTXSiYMruPDO+XVomfMjMUfS7EVIMUosHEC4bs4PS8m60lbTO2Lducw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Remora.Discord.Caching": "39.0.0-github11168366508",
|
|
||||||
"Remora.Discord.Commands": "28.1.0-github11168366508",
|
|
||||||
"Remora.Discord.Extensions": "5.3.6-github11168366508",
|
|
||||||
"Remora.Discord.Hosting": "6.0.10-github11168366508",
|
|
||||||
"Remora.Discord.Interactivity": "5.0.0-github11168366508",
|
|
||||||
"Remora.Discord.Pagination": "4.0.1-github11168366508"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Sdk": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[3.1.2, )",
|
|
||||||
"resolved": "3.1.2",
|
|
||||||
"contentHash": "IjHGwOH9XZJu4sMPA25M/gMLJktq4CdtSvekn8sAF85bE/3uhxU9pqmuzc4N39ktY7aTkLBRDa6/oQJnmiI6CQ=="
|
|
||||||
},
|
|
||||||
"Serilog": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[4.0.2, )",
|
|
||||||
"resolved": "4.0.2",
|
|
||||||
"contentHash": "Vehq4uNYtURe/OnHEpWGvMgrvr5Vou7oZLdn3BuEH5FSCeHXDpNJtpzWoqywXsSvCTuiv0I65mZDRnJSeUvisA=="
|
|
||||||
},
|
|
||||||
"Serilog.AspNetCore": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.0.2, )",
|
|
||||||
"resolved": "8.0.2",
|
|
||||||
"contentHash": "LNUd1bHsik2E7jSoCQFdeMGAWXjH7eUQ6c2pqm5vl+jGqvxdabYXxlrfaqApjtX5+BfAjW9jTA2EKmPwxknpIA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Logging": "8.0.0",
|
|
||||||
"Serilog": "3.1.1",
|
|
||||||
"Serilog.Extensions.Hosting": "8.0.0",
|
|
||||||
"Serilog.Formatting.Compact": "2.0.0",
|
|
||||||
"Serilog.Settings.Configuration": "8.0.2",
|
|
||||||
"Serilog.Sinks.Console": "5.0.0",
|
|
||||||
"Serilog.Sinks.Debug": "2.0.0",
|
|
||||||
"Serilog.Sinks.File": "5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Serilog.Extensions.Hosting": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.0.0, )",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "db0OcbWeSCvYQkHWu6n0v40N4kKaTAXNjlM3BKvcbwvNzYphQFcBR+36eQ/7hMMwOkJvAyLC2a9/jNdUL5NjtQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Hosting.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
|
||||||
"Serilog": "3.1.1",
|
|
||||||
"Serilog.Extensions.Logging": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Serilog.Sinks.Console": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[6.0.0, )",
|
|
||||||
"resolved": "6.0.0",
|
|
||||||
"contentHash": "fQGWqVMClCP2yEyTXPIinSr5c+CBGUvBybPxjAGcf7ctDhadFhrQw03Mv8rJ07/wR5PDfFjewf2LimvXCDzpbA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Serilog": "4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Serilog.Sinks.Seq": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[8.0.0, )",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "z5ig56/qzjkX6Fj4U/9m1g8HQaQiYPMZS4Uevtjg1I+WWzoGSf5t/E+6JbMP/jbZYhU63bA5NJN5y0x+qqx2Bw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Serilog": "4.0.0",
|
|
||||||
"Serilog.Sinks.File": "5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"StackExchange.Redis": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[2.8.16, )",
|
|
||||||
"resolved": "2.8.16",
|
|
||||||
"contentHash": "WaoulkOqOC9jHepca3JZKFTqndCWab5uYS7qCzmiQDlrTkFaDN7eLSlEfHycBxipRnQY9ppZM7QSsWAwUEGblw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
|
|
||||||
"Pipelines.Sockets.Unofficial": "2.2.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[6.8.1, )",
|
|
||||||
"resolved": "6.8.1",
|
|
||||||
"contentHash": "JN6ccH37QKtNOwBrvSxc+jBYIB+cw6RlZie2IKoJhjjf6HzBH+2kPJCpxmJ5EHIqmxvq6aQG+0A8XklGx9rAxA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.ApiDescription.Server": "6.0.5",
|
|
||||||
"Swashbuckle.AspNetCore.Swagger": "6.8.1",
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerGen": "6.8.1",
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerUI": "6.8.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"CommunityToolkit.HighPerformance": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.2.2",
|
|
||||||
"contentHash": "+zIp8d3sbtYaRbM6hqDs4Ui/z34j7DcUmleruZlYLE4CVxXq+MO8XJyIs42vzeTYFX+k0Iq1dEbBUnQ4z/Gnrw=="
|
|
||||||
},
|
|
||||||
"FuzzySharp": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.0.2",
|
|
||||||
"contentHash": "sBKqWxw3g//peYxDZ8JipRlyPbIyBtgzqBVA5GqwHVeqtIrw75maGXAllztf+1aJhchD+drcQIgf2mFho8ZV8A=="
|
|
||||||
},
|
|
||||||
"JsonDocumentPath": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.0.3",
|
|
||||||
"contentHash": "4mgdlioVvfq6ZjftvsoKANWgpr/AU+UySiW68EjcbPbTfvcrZOlgS+6JkouRAN4TwI8dN2DUAVME7bklThk3KQ=="
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.JsonPatch": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.8",
|
|
||||||
"contentHash": "IGhuO/SsjHIIvFP4O/5pn/WcPJor+A+BERBhIkMYrlYcRXnZmbBBNSyqoNI9wFq0oxtsrnYMnzXAIi+0MKVdSA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.CSharp": "4.7.0",
|
|
||||||
"Newtonsoft.Json": "13.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.CSharp": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.7.0",
|
|
||||||
"contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.ApiDescription.Server": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "6.0.5",
|
|
||||||
"contentHash": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw=="
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Memory": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Options": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.Binder": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyModel": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.1",
|
|
||||||
"contentHash": "5Ou6varcxLBzQ+Agfm0k0pnH7vrEITYlXMDuE6s7ZHlZHz6/G8XJ3iISZDr5rfwfge6RnXJ1+Wc479mMn52vjA==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Text.Encodings.Web": "8.0.0",
|
|
||||||
"System.Text.Json": "8.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Diagnostics": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Options": "8.0.0",
|
|
||||||
"System.Diagnostics.DiagnosticSource": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Hosting.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Http": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Diagnostics": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Logging": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Options": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Http.Polly": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.6",
|
|
||||||
"contentHash": "vehhL2uDlr2ovIFMuYcQwXgOCu7QECXnjcRD37luN40Fjqm0C4PDiN0t0dHoyfJp6OgJ+sOYDev5jVMGz4lJnQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Http": "8.0.0",
|
|
||||||
"Polly": "7.2.4",
|
|
||||||
"Polly.Extensions.Http": "3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Options": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.ObjectPool": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "7.0.0",
|
|
||||||
"contentHash": "udvKco0sAVgYGTBnHUb0tY9JQzJ/nPDiv/8PIyz69wl1AibeCDZOLVVI+6156dPfHmJH7ws5oUJRiW4ZmAvuuA=="
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Options": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.2",
|
|
||||||
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Options": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Primitives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
|
|
||||||
},
|
|
||||||
"Microsoft.OpenApi": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.6.14",
|
|
||||||
"contentHash": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw=="
|
|
||||||
},
|
|
||||||
"Newtonsoft.Json.Bson": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.0.2",
|
|
||||||
"contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Newtonsoft.Json": "12.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NGettext": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "0.6.7",
|
|
||||||
"contentHash": "gT6bf5PVayvTuEIuM2XSNqthrtn9W+LlCX4RD//Nb4hrT3agohHvPdjpROgNGgyXDkjwE74F+EwDwqUgJCJG8A=="
|
|
||||||
},
|
|
||||||
"OneOf": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "3.0.271",
|
|
||||||
"contentHash": "pqpqeK8xQGggExhr4tesVgJkjdn+9HQAO0QgrYV2hFjE3y90okzk1kQMntMiUOGfV7FrCUfKPaVvPBD4IANqKg=="
|
|
||||||
},
|
|
||||||
"Pipelines.Sockets.Unofficial": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.2.8",
|
|
||||||
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.IO.Pipelines": "5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Polly": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.4.0",
|
|
||||||
"contentHash": "z2EeUutuy49jBQyZ5s2FUuTCGx3GCzJ0cJ2HbjWwks94TsC6bKTtAHKBkMZOa/DyYRl5yIX7MshvMTWl1J6RNg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Polly.Core": "8.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Polly.Contrib.WaitAndRetry": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.1.1",
|
|
||||||
"contentHash": "1MUQLiSo4KDkQe6nzQRhIU05lm9jlexX5BVsbuw0SL82ynZ+GzAHQxJVDPVBboxV37Po3SG077aX8DuSy8TkaA=="
|
|
||||||
},
|
|
||||||
"Polly.Extensions.Http": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "3.0.0",
|
|
||||||
"contentHash": "drrG+hB3pYFY7w1c3BD+lSGYvH2oIclH8GRSehgfyP5kjnFnHKQuuBhuHLv+PWyFuaTDyk/vfRpnxOzd11+J8g==",
|
|
||||||
"dependencies": {
|
|
||||||
"Polly": "7.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Commands": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "10.0.5",
|
|
||||||
"contentHash": "uvZ34ywhK9WxBBqHZiLz7GXJDPZrt0N+IhRs5+V53TTCvLlgA0S8zBCPCANnVpcbVJ8Vl9l3EkcL+PY0VT0TYw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Options": "8.0.0",
|
|
||||||
"Remora.Results": "7.4.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.API": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "78.0.0-github11168366508",
|
|
||||||
"contentHash": "yDH7x0XLbe4GPhHeK5Ju4tGXCPpSAo0Jd20jikVZOlFHLJkynt0NVWYTT69ZJyniibopwpeANPyAnX8KhZmBbA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Options": "8.0.2",
|
|
||||||
"Remora.Discord.API.Abstractions": "82.0.0-github11168366508",
|
|
||||||
"Remora.Rest": "3.4.0",
|
|
||||||
"System.Text.Json": "8.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.API.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "82.0.0-github11168366508",
|
|
||||||
"contentHash": "vUsvcaM8bSqha9uBhye0mRvARaRHYQgQcIre+CcEloGO4n2JzalLdCFlYIUF3yzcBMGWQnnXymMSzvxjipPglw==",
|
|
||||||
"dependencies": {
|
|
||||||
"OneOf": "3.0.271",
|
|
||||||
"Remora.Rest.Core": "2.2.1",
|
|
||||||
"Remora.Results": "7.4.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.Caching": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "39.0.0-github11168366508",
|
|
||||||
"contentHash": "LY6fROu/g+lcfV60OAM+7KC29nsKtJNUuhiGPI1Mb1w6uR5LoTWGaM29/nQeY8DzixD60np7lF5ZwZUlgoTp0g==",
|
|
||||||
"dependencies": {
|
|
||||||
"Remora.Discord.Caching.Abstractions": "1.1.4-github11168366508",
|
|
||||||
"Remora.Discord.Gateway": "12.0.2-github11168366508"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.Caching.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.1.4-github11168366508",
|
|
||||||
"contentHash": "ZDh/C/d0lJ2rYY/8UyRDf57XYg2ZVnTjwuqVXNYrGI/kkQCMI3R4WCbPOppBrycji6iX5pp+fx1j1pSdZsc3eA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Caching.Abstractions": "8.0.0",
|
|
||||||
"Remora.Results": "7.4.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.Commands": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "28.1.0-github11168366508",
|
|
||||||
"contentHash": "SzYCnL4KEsnqvBaDLrXeAfkr45A3cHygJSO/VUSfQpTC6XoHDSMY181H7M2czgY+GiwSzrxYkeu/p89MFkzvxw==",
|
|
||||||
"dependencies": {
|
|
||||||
"FuzzySharp": "2.0.2",
|
|
||||||
"Humanizer.Core": "2.14.1",
|
|
||||||
"NGettext": "0.6.7",
|
|
||||||
"Remora.Commands": "10.0.5",
|
|
||||||
"Remora.Discord.Gateway": "12.0.2-github11168366508",
|
|
||||||
"Remora.Extensions.Options.Immutable": "1.0.8",
|
|
||||||
"System.ComponentModel.Annotations": "5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.Extensions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "5.3.6-github11168366508",
|
|
||||||
"contentHash": "xidy4VW5xS8m+crKKjZeN2p6H+TQOgl9Je79ykX1vckMrUOMGtSreKoCEzpVRMPyXotNr9K2xbj1dqNtr4afXw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Remora.Discord.API": "78.0.0-github11168366508",
|
|
||||||
"Remora.Discord.Commands": "28.1.0-github11168366508",
|
|
||||||
"Remora.Discord.Gateway": "12.0.2-github11168366508",
|
|
||||||
"Remora.Discord.Interactivity": "5.0.0-github11168366508"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.Gateway": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "12.0.2-github11168366508",
|
|
||||||
"contentHash": "yleE7MHFc8JC6QDhCf6O9Xn2mQA06mmZtwph4tiBnehBTf6GY0ST6op7szEHEE4BI6LuvSo7TuKaHqFzAbxLHQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"CommunityToolkit.HighPerformance": "8.2.2",
|
|
||||||
"Remora.Discord.Rest": "51.0.0-github11168366508",
|
|
||||||
"Remora.Extensions.Options.Immutable": "1.0.8",
|
|
||||||
"System.Threading.Channels": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.Hosting": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "6.0.10-github11168366508",
|
|
||||||
"contentHash": "BCTbNq/sYvUeiuFSNt8Y0aFi0+g4Fnz1vcHEwzFPxczGsW1QaHNOJst8GDpV9fEfcBrs5EHgE+Y4vo0ed8B9zQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Hosting.Abstractions": "8.0.0",
|
|
||||||
"Remora.Discord.Gateway": "12.0.2-github11168366508",
|
|
||||||
"Remora.Extensions.Options.Immutable": "1.0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.Interactivity": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "5.0.0-github11168366508",
|
|
||||||
"contentHash": "vJOy/8//5+UcTHx8TV4iilQrYJEVfqfmuPNISIShLlgbEzbp/UjmN7QBiOJtpgUAPifeaQbmBXLPlYR0nKEDxg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Remora.Discord.Commands": "28.1.0-github11168366508",
|
|
||||||
"Remora.Discord.Gateway": "12.0.2-github11168366508"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.Pagination": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.0.1-github11168366508",
|
|
||||||
"contentHash": "+JKA+GYTlAkX1MxElI+ICGGmZnteiODiVHN09+QeHsjHaWxSBkb7g3pk8OqWrLhyQlyGvI/37kHV+UjRT6Ua5A==",
|
|
||||||
"dependencies": {
|
|
||||||
"Remora.Discord.Interactivity": "5.0.0-github11168366508"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Discord.Rest": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "51.0.0-github11168366508",
|
|
||||||
"contentHash": "4NImnAdU27K2Wkbjvw1Dyyib+dZwpKvl39vwnYNnpcYRgQ9mSiKWXq6y2rw/bXXn/l7V/EO6qZsgN1+Q5Yo65A==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Caching.Memory": "8.0.0",
|
|
||||||
"Microsoft.Extensions.Http.Polly": "8.0.6",
|
|
||||||
"Polly": "8.4.0",
|
|
||||||
"Polly.Contrib.WaitAndRetry": "1.1.1",
|
|
||||||
"Remora.Discord.API": "78.0.0-github11168366508",
|
|
||||||
"Remora.Discord.Caching.Abstractions": "1.1.4-github11168366508"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Extensions.Options.Immutable": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.0.8",
|
|
||||||
"contentHash": "CCw7IlZnE7hCGsO7sb9w05qdYY7bTufdYe6hiXKTOE3IDwdl2xtV7vitMif1KXVAjSZi9QySk8UPA5OfJTC3bA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Options": "7.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Rest": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "3.4.0",
|
|
||||||
"contentHash": "uncX4dsj6sq52ZUAnUrUs/usl3YEO4KZ+939r1K6Ojlq2IAZuuJ/4WocicARAiUZp8xa4xeOk1xbAP0+54D3gg==",
|
|
||||||
"dependencies": {
|
|
||||||
"JsonDocumentPath": "1.0.3",
|
|
||||||
"Microsoft.Extensions.Http": "8.0.0",
|
|
||||||
"OneOf": "3.0.263",
|
|
||||||
"Remora.Rest.Core": "2.2.1",
|
|
||||||
"Remora.Results": "7.4.1",
|
|
||||||
"System.Text.Json": "8.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Remora.Rest.Core": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.2.1",
|
|
||||||
"contentHash": "XWhTyHiClwJHiZf0+Ci0+R8ZdeJOyFWvPYh05JNYwAE9327T57d7VIqInbZ8/NfRdgYZ3TSHEjUwITVhetQZZQ=="
|
|
||||||
},
|
|
||||||
"Remora.Results": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "7.4.1",
|
|
||||||
"contentHash": "XDO1jZBNpp3d0gApH0uG8BcOkjL4QxMJAEkmx3SlP202GDHev0BthuC4yOcENT5yApZvVT4IV5pJAwLYtSYIFg=="
|
|
||||||
},
|
|
||||||
"Serilog.Extensions.Logging": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "YEAMWu1UnWgf1c1KP85l1SgXGfiVo0Rz6x08pCiPOIBt2Qe18tcZLvdBUuV5o1QHvrs8FAry9wTIhgBRtjIlEg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Logging": "8.0.0",
|
|
||||||
"Serilog": "3.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Serilog.Formatting.Compact": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.0.0",
|
|
||||||
"contentHash": "ob6z3ikzFM3D1xalhFuBIK1IOWf+XrQq+H4KeH4VqBcPpNcmUgZlRQ2h3Q7wvthpdZBBoY86qZOI2LCXNaLlNA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Serilog": "3.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Serilog.Settings.Configuration": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.2",
|
|
||||||
"contentHash": "hn8HCAmupon7N0to20EwGeNJ+L3iRzjGzAHIl8+8CCFlEkVedHvS6NMYMb0VPNMsDgDwOj4cPBPV6Fc2hb0/7w==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
|
||||||
"Microsoft.Extensions.DependencyModel": "8.0.1",
|
|
||||||
"Serilog": "3.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Serilog.Sinks.Debug": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.0.0",
|
|
||||||
"contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==",
|
|
||||||
"dependencies": {
|
|
||||||
"Serilog": "2.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Serilog.Sinks.File": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "5.0.0",
|
|
||||||
"contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Serilog": "2.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore.Swagger": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "6.8.1",
|
|
||||||
"contentHash": "eOkdM4bsWBU5Ty3kWbyq5O9L+05kZT0vOdGh4a92vIb/LLQGQTPLRHXuJdnUBNIPNC8XfKWfSbtRfqzI6nnbqw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.OpenApi": "1.6.14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerGen": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "6.8.1",
|
|
||||||
"contentHash": "TjBPxsN0HeJzxEXZYeDXBNNMSyhg+TYXtkbwX+Cn8GH/y5ZeoB/chw0p71kRo5tR2sNshbKwL24T6f9pTF9PHg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Swashbuckle.AspNetCore.Swagger": "6.8.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Swashbuckle.AspNetCore.SwaggerUI": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "6.8.1",
|
|
||||||
"contentHash": "lpEszYJ7vZaTTE5Dp8MrsbSHrgDfjhDMjzW1qOA1Xs1Dnj3ZRBJAcPZUTsa5Bva+nLaw91JJ8OI8FkSg8hhIyA=="
|
|
||||||
},
|
|
||||||
"System.ComponentModel.Annotations": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "5.0.0",
|
|
||||||
"contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg=="
|
|
||||||
},
|
|
||||||
"System.Diagnostics.DiagnosticSource": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ=="
|
|
||||||
},
|
|
||||||
"System.IO.Pipelines": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "5.0.1",
|
|
||||||
"contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg=="
|
|
||||||
},
|
|
||||||
"System.Runtime.CompilerServices.Unsafe": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.7.1",
|
|
||||||
"contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ=="
|
|
||||||
},
|
|
||||||
"System.Text.Encodings.Web": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
|
|
||||||
},
|
|
||||||
"System.Text.Json": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.4",
|
|
||||||
"contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Text.Encodings.Web": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Threading.Channels": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA=="
|
|
||||||
},
|
|
||||||
"System.Threading.RateLimiting": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ internal class Program
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var db = new DatabasePool(config, Log.Logger, null);
|
var db = new DatabasePool(config, null);
|
||||||
DatabasePool.ConfigureDapper();
|
DatabasePool.ConfigureDapper();
|
||||||
if (Environment.GetEnvironmentVariable("MIGRATE") == "true")
|
if (Environment.GetEnvironmentVariable("MIGRATE") == "true")
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,6 @@ Command-line tools for this project can be installed with `dotnet tool restore`.
|
||||||
- We use [CSharpier][csharpier] for formatting .NET code.
|
- We use [CSharpier][csharpier] for formatting .NET code.
|
||||||
It can be called with `dotnet csharpier .`, but is automatically run by Husky pre-commit.
|
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
|
## Deploying Catalogger yourself
|
||||||
|
|
||||||
The bot itself should run on any server with .NET 8 and PostgreSQL 15 or later.
|
The bot itself should run on any server with .NET 8 and PostgreSQL 15 or later.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
<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">
|
<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/=pluralkit/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=remora/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<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