2024-10-14 14:56:40 +02:00
|
|
|
// Copyright (C) 2021-present sam (starshines.gay)
|
|
|
|
|
//
|
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
|
|
|
// by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
//
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
|
//
|
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
2024-08-13 13:08:50 +02:00
|
|
|
using System.ComponentModel;
|
2024-08-13 16:48:54 +02:00
|
|
|
using System.Diagnostics;
|
2024-08-13 17:02:11 +02:00
|
|
|
using System.Runtime.InteropServices;
|
2024-08-20 20:19:24 +02:00
|
|
|
using System.Text.Json;
|
|
|
|
|
using System.Web;
|
2024-08-19 16:12:28 +02:00
|
|
|
using Catalogger.Backend.Cache.InMemoryCache;
|
2024-08-13 13:08:50 +02:00
|
|
|
using Catalogger.Backend.Extensions;
|
2024-10-24 22:16:19 +02:00
|
|
|
using Catalogger.Backend.Services;
|
2024-08-13 16:48:54 +02:00
|
|
|
using Humanizer;
|
2024-08-21 17:31:39 +02:00
|
|
|
using Humanizer.Localisation;
|
2024-08-13 13:08:50 +02:00
|
|
|
using Remora.Commands.Attributes;
|
|
|
|
|
using Remora.Commands.Groups;
|
2024-08-13 17:02:11 +02:00
|
|
|
using Remora.Discord.API.Abstractions.Objects;
|
2024-08-13 13:08:50 +02:00
|
|
|
using Remora.Discord.API.Abstractions.Rest;
|
2024-08-24 19:02:19 +02:00
|
|
|
using Remora.Discord.Commands.Extensions;
|
2024-08-13 13:08:50 +02:00
|
|
|
using Remora.Discord.Commands.Feedback.Services;
|
2024-08-24 19:02:19 +02:00
|
|
|
using Remora.Discord.Commands.Services;
|
2024-08-13 16:48:54 +02:00
|
|
|
using Remora.Discord.Extensions.Embeds;
|
2024-08-13 13:08:50 +02:00
|
|
|
using Remora.Results;
|
2024-08-15 17:23:56 +02:00
|
|
|
using IClock = NodaTime.IClock;
|
2024-08-13 13:08:50 +02:00
|
|
|
using IResult = Remora.Results.IResult;
|
|
|
|
|
|
|
|
|
|
namespace Catalogger.Backend.Bot.Commands;
|
|
|
|
|
|
|
|
|
|
[Group("catalogger")]
|
2024-10-14 21:28:34 +02:00
|
|
|
[Description("Commands for information about the bot itself.")]
|
2024-08-13 13:08:50 +02:00
|
|
|
public class MetaCommands(
|
2024-08-20 20:19:24 +02:00
|
|
|
ILogger logger,
|
2024-08-13 13:08:50 +02:00
|
|
|
IClock clock,
|
2024-08-20 20:19:24 +02:00
|
|
|
Config config,
|
2024-08-24 19:02:19 +02:00
|
|
|
ShardedGatewayClient client,
|
2024-08-15 17:23:56 +02:00
|
|
|
IFeedbackService feedbackService,
|
2024-08-24 19:02:19 +02:00
|
|
|
ContextInjectionService contextInjection,
|
2024-08-19 16:12:28 +02:00
|
|
|
GuildCache guildCache,
|
2024-10-16 14:53:30 +02:00
|
|
|
RoleCache roleCache,
|
2024-08-19 16:12:28 +02:00
|
|
|
ChannelCache channelCache,
|
2024-10-14 17:09:12 +02:00
|
|
|
EmojiCache emojiCache,
|
2024-10-24 22:16:19 +02:00
|
|
|
IDiscordRestChannelAPI channelApi,
|
|
|
|
|
PermissionResolverService permissionResolver
|
2024-10-09 17:35:11 +02:00
|
|
|
) : CommandGroup
|
2024-08-13 13:08:50 +02:00
|
|
|
{
|
2024-08-20 20:19:24 +02:00
|
|
|
private readonly ILogger _logger = logger.ForContext<MetaCommands>();
|
|
|
|
|
private readonly HttpClient _client = new();
|
|
|
|
|
|
2024-10-24 22:16:19 +02:00
|
|
|
[Command("help")]
|
|
|
|
|
[Description("Learn more about Catalogger.")]
|
|
|
|
|
public async Task<IResult> HelpAsync()
|
|
|
|
|
{
|
|
|
|
|
var embed = new EmbedBuilder()
|
|
|
|
|
.WithColour(DiscordUtils.Purple)
|
|
|
|
|
.WithTitle("Catalogger")
|
|
|
|
|
.WithDescription(
|
|
|
|
|
"""
|
|
|
|
|
A logging bot that integrates with PluralKit's message proxying.
|
|
|
|
|
Use `/configure-channels` to get started!
|
|
|
|
|
"""
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (config.Discord.EnableDash)
|
|
|
|
|
embed.Description +=
|
|
|
|
|
$"\n\nYou can also use the dashboard for configuration: {config.Web.BaseUrl}";
|
|
|
|
|
|
|
|
|
|
embed.AddField(
|
|
|
|
|
"Configuration",
|
|
|
|
|
"""
|
|
|
|
|
`/configure-channels`: Set which events will be logged to which channels
|
|
|
|
|
`/ignored-channels`: Set which channels will be ignored entirely
|
|
|
|
|
`/redirects`: Override where a channel's messages will be logged
|
|
|
|
|
`/key-roles`: Set which roles are treated as key roles and are logged with more detail than others
|
|
|
|
|
`/invites`: Manage invites and create new ones
|
|
|
|
|
`/check-permissions`: Check for any issues with logging
|
|
|
|
|
"""
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
embed.AddField("Creator", "<@694563574386786314> / starshines.gay");
|
|
|
|
|
embed.AddField(
|
|
|
|
|
"Source code",
|
|
|
|
|
"https://codeberg.org/starshine/catalogger / Licensed under the GNU AGPL v3"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (config.Discord.SupportGuild != null)
|
|
|
|
|
embed.AddField(
|
|
|
|
|
"Support",
|
|
|
|
|
$"Use this link to join the support server: {config.Discord.SupportGuild}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return await feedbackService.ReplyAsync(
|
|
|
|
|
embeds: [embed.Build().GetOrThrow()],
|
|
|
|
|
isEphemeral: true
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Command("invite")]
|
|
|
|
|
[Description("Get a link to invite Catalogger to your server.")]
|
|
|
|
|
public async Task<IResult> InviteAsync()
|
|
|
|
|
{
|
|
|
|
|
var inviteUrl =
|
|
|
|
|
$"https://discord.com/oauth2/authorize?client_id={config.Discord.ApplicationId}"
|
|
|
|
|
+ "&permissions=537250993&scope=bot+applications.commands";
|
|
|
|
|
|
|
|
|
|
return await feedbackService.ReplyAsync(
|
|
|
|
|
$"Use this link to invite Catalogger to your server: {inviteUrl}",
|
|
|
|
|
isEphemeral: true
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Command("dashboard")]
|
|
|
|
|
[Description("Get a link to the dashboard.")]
|
|
|
|
|
public async Task<IResult> DashboardLinkAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!config.Discord.EnableDash)
|
|
|
|
|
return await feedbackService.ReplyAsync(
|
|
|
|
|
"The dashboard is not enabled for this version of Catalogger.",
|
|
|
|
|
isEphemeral: true
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
contextInjection.Context?.TryGetGuildID(out var guildId) != true
|
|
|
|
|
|| contextInjection.Context?.TryGetUserID(out var userId) != true
|
|
|
|
|
)
|
|
|
|
|
return await feedbackService.ReplyAsync(
|
|
|
|
|
$"The dashboard is available here: {config.Web.BaseUrl}",
|
|
|
|
|
isEphemeral: true
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
var perms = await permissionResolver.GetGuildPermissionsAsync(guildId, userId);
|
|
|
|
|
if (
|
|
|
|
|
perms.HasPermission(DiscordPermission.ManageGuild)
|
|
|
|
|
|| perms.HasPermission(DiscordPermission.Administrator)
|
|
|
|
|
)
|
|
|
|
|
return await feedbackService.ReplyAsync(
|
|
|
|
|
$"The dashboard for this server is available here: {config.Web.BaseUrl}/dash/{guildId}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return await feedbackService.ReplyAsync(
|
|
|
|
|
$"The dashboard is available here: {config.Web.BaseUrl}",
|
|
|
|
|
isEphemeral: true
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-13 13:08:50 +02:00
|
|
|
[Command("ping")]
|
|
|
|
|
[Description("Ping pong! See the bot's latency")]
|
|
|
|
|
public async Task<IResult> PingAsync()
|
|
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
var shardId =
|
|
|
|
|
contextInjection.Context?.TryGetGuildID(out var guildId) == true
|
|
|
|
|
? client.ShardIdFor(guildId.Value)
|
|
|
|
|
: 0;
|
2024-08-24 19:02:19 +02:00
|
|
|
|
2024-10-09 17:35:11 +02:00
|
|
|
var averageLatency =
|
|
|
|
|
client.Shards.Values.Select(x => x.Latency.TotalMilliseconds).Sum()
|
|
|
|
|
/ client.Shards.Count;
|
2024-08-24 19:02:19 +02:00
|
|
|
|
2024-08-13 13:08:50 +02:00
|
|
|
var t1 = clock.GetCurrentInstant();
|
|
|
|
|
var msg = await feedbackService.SendContextualAsync("...").GetOrThrow();
|
|
|
|
|
var elapsed = clock.GetCurrentInstant() - t1;
|
|
|
|
|
|
2024-08-13 16:48:54 +02:00
|
|
|
var process = Process.GetCurrentProcess();
|
|
|
|
|
var memoryUsage = process.WorkingSet64;
|
|
|
|
|
|
|
|
|
|
var embed = new EmbedBuilder()
|
|
|
|
|
.WithColour(DiscordUtils.Purple)
|
2024-10-09 17:35:11 +02:00
|
|
|
.WithFooter(
|
2024-10-31 01:26:50 +01:00
|
|
|
$"{BuildInfo.Version}, {RuntimeInformation.FrameworkDescription} on {RuntimeInformation.RuntimeIdentifier}"
|
2024-10-09 17:35:11 +02:00
|
|
|
)
|
2024-08-13 16:48:54 +02:00
|
|
|
.WithCurrentTimestamp();
|
2024-10-09 17:35:11 +02:00
|
|
|
embed.AddField(
|
|
|
|
|
"Ping",
|
|
|
|
|
$"Gateway: {client.Shards[shardId].Latency.TotalMilliseconds:N0}ms (average: {averageLatency:N0}ms)\n"
|
|
|
|
|
+ $"API: {elapsed.TotalMilliseconds:N0}ms",
|
|
|
|
|
inline: true
|
|
|
|
|
);
|
2024-08-13 17:02:11 +02:00
|
|
|
embed.AddField("Memory usage", memoryUsage.Bytes().Humanize(), inline: true);
|
2024-08-13 16:48:54 +02:00
|
|
|
|
2024-08-20 20:19:24 +02:00
|
|
|
var messageRate = await MessagesRate();
|
2024-10-09 17:35:11 +02:00
|
|
|
embed.AddField(
|
|
|
|
|
"Messages received",
|
2024-08-20 20:19:24 +02:00
|
|
|
messageRate != null
|
|
|
|
|
? $"{messageRate / 5:F1}/m\n({CataloggerMetrics.MessagesReceived.Value:N0} since last restart)"
|
|
|
|
|
: $"{CataloggerMetrics.MessagesReceived.Value:N0} since last restart",
|
2024-10-09 17:35:11 +02:00
|
|
|
true
|
|
|
|
|
);
|
2024-08-15 17:23:56 +02:00
|
|
|
|
2024-08-24 19:02:19 +02:00
|
|
|
embed.AddField("Shard", $"{shardId + 1} of {client.Shards.Count}", true);
|
2024-08-21 17:31:39 +02:00
|
|
|
|
2024-10-09 17:35:11 +02:00
|
|
|
embed.AddField(
|
|
|
|
|
"Uptime",
|
|
|
|
|
$"{(CataloggerMetrics.Startup - clock.GetCurrentInstant()).Prettify(TimeUnit.Second)}\n"
|
|
|
|
|
+ $"since <t:{CataloggerMetrics.Startup.ToUnixTimeSeconds()}:F>",
|
|
|
|
|
true
|
|
|
|
|
);
|
2024-08-13 16:48:54 +02:00
|
|
|
|
2024-10-09 17:35:11 +02:00
|
|
|
embed.AddField(
|
|
|
|
|
"Numbers",
|
|
|
|
|
$"{CataloggerMetrics.MessagesStored.Value:N0} messages "
|
2024-10-16 14:53:30 +02:00
|
|
|
+ $"from {guildCache.Size:N0} servers\n"
|
2024-10-31 01:26:50 +01:00
|
|
|
+ $"Cached {channelCache.Size:N0} channels, {roleCache.Size:N0} roles, {emojiCache.Size:N0} emojis"
|
2024-10-09 17:35:11 +02:00
|
|
|
);
|
2024-08-24 19:02:19 +02:00
|
|
|
|
2024-08-15 17:23:56 +02:00
|
|
|
IEmbed[] embeds = [embed.Build().GetOrThrow()];
|
2024-08-13 17:02:11 +02:00
|
|
|
|
2024-10-09 17:35:11 +02:00
|
|
|
return (Result)
|
|
|
|
|
await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds);
|
2024-08-13 13:08:50 +02:00
|
|
|
}
|
2024-08-19 16:12:28 +02:00
|
|
|
|
2025-04-11 14:58:21 +02:00
|
|
|
// TODO: add more checks around response format
|
2024-08-20 20:19:24 +02:00
|
|
|
private async Task<double?> MessagesRate()
|
2024-08-19 16:12:28 +02:00
|
|
|
{
|
2024-10-09 17:35:11 +02:00
|
|
|
if (!config.Logging.EnableMetrics)
|
|
|
|
|
return null;
|
2024-08-19 16:12:28 +02:00
|
|
|
|
2024-08-20 20:19:24 +02:00
|
|
|
try
|
|
|
|
|
{
|
2024-10-25 16:04:01 +02:00
|
|
|
var query = HttpUtility.UrlEncode("increase(catalogger_received_messages[5m])");
|
2025-04-11 14:58:21 +02:00
|
|
|
var prometheusUrl = config.Logging.PrometheusUrl ?? "http://localhost:9090";
|
|
|
|
|
var resp = await _client.GetAsync($"{prometheusUrl}/api/v1/query?query={query}");
|
2024-08-20 20:19:24 +02:00
|
|
|
resp.EnsureSuccessStatusCode();
|
2024-08-19 16:12:28 +02:00
|
|
|
|
2024-08-20 20:19:24 +02:00
|
|
|
var data = await resp.Content.ReadFromJsonAsync<PrometheusResponse>();
|
|
|
|
|
var rawNumber = (data?.data.result[0].value[1] as JsonElement?)?.GetString();
|
|
|
|
|
return double.TryParse(rawNumber, out var rate) ? rate : null;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
_logger.Warning(e, "Failed querying Prometheus for message rate");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-08-19 16:12:28 +02:00
|
|
|
}
|
2024-08-20 20:19:24 +02:00
|
|
|
|
|
|
|
|
// ReSharper disable InconsistentNaming, ClassNeverInstantiated.Local
|
|
|
|
|
private record PrometheusResponse(PrometheusData data);
|
|
|
|
|
|
|
|
|
|
private record PrometheusData(PrometheusResult[] result);
|
|
|
|
|
|
|
|
|
|
private record PrometheusResult(object[] value);
|
|
|
|
|
// ReSharper restore InconsistentNaming, ClassNeverInstantiated.Local
|
2024-10-09 17:35:11 +02:00
|
|
|
}
|