feat(bot): lookup command messages

This commit is contained in:
alyssa 2025-06-08 19:52:37 +00:00
parent 301bf25d44
commit 539fc82342
6 changed files with 120 additions and 37 deletions

View file

@ -33,7 +33,10 @@ public class ApplicationCommandProxiedMessage
var messageId = ctx.Event.Data!.TargetId!.Value; var messageId = ctx.Event.Data!.TargetId!.Value;
var msg = await ctx.Repository.GetFullMessage(messageId); var msg = await ctx.Repository.GetFullMessage(messageId);
if (msg == null) if (msg == null)
throw Errors.MessageNotFound(messageId); {
await QueryCommandMessage(ctx);
return;
}
var showContent = true; var showContent = true;
var channel = await _rest.GetChannelOrNull(msg.Message.Channel); var channel = await _rest.GetChannelOrNull(msg.Message.Channel);
@ -58,6 +61,20 @@ public class ApplicationCommandProxiedMessage
await ctx.Reply(embeds: embeds.ToArray()); await ctx.Reply(embeds: embeds.ToArray());
} }
private async Task QueryCommandMessage(InteractionContext ctx)
{
var messageId = ctx.Event.Data!.TargetId!.Value;
var msg = await ctx.Repository.GetCommandMessage(messageId);
if (msg == null)
throw Errors.MessageNotFound(messageId);
var embeds = new List<Embed>();
embeds.Add(await _embeds.CreateCommandMessageInfoEmbed(msg, true));
await ctx.Reply(embeds: embeds.ToArray());
}
public async Task DeleteMessage(InteractionContext ctx) public async Task DeleteMessage(InteractionContext ctx)
{ {
var messageId = ctx.Event.Data!.TargetId!.Value; var messageId = ctx.Event.Data!.TargetId!.Value;

View file

@ -101,12 +101,17 @@ public class Context
AllowedMentions = mentions ?? new AllowedMentions() AllowedMentions = mentions ?? new AllowedMentions()
}); });
// if (embed != null) // store log of sent message, so it can be queried or deleted later
// { // skip DMs as DM messages can always be deleted
// Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example) if (Guild != null)
// but since we can, we just store all sent messages for possible deletion await Repository.AddCommandMessage(new Core.CommandMessage
await _commandMessageService.RegisterMessage(msg.Id, Guild?.Id ?? 0, msg.ChannelId, Author.Id); {
// } Mid = msg.Id,
Guild = Guild!.Id,
Channel = Channel.Id,
Sender = Author.Id,
OriginalMid = Message.Id,
});
return msg; return msg;
} }

View file

@ -347,13 +347,8 @@ public class ProxiedMessage
var message = await ctx.Repository.GetFullMessage(messageId.Value); var message = await ctx.Repository.GetFullMessage(messageId.Value);
if (message == null) if (message == null)
{ {
if (isDelete) await GetCommandMessage(ctx, messageId.Value, isDelete);
{ return;
await DeleteCommandMessage(ctx, messageId.Value);
return;
}
throw Errors.MessageNotFound(messageId.Value);
} }
var showContent = true; var showContent = true;
@ -448,20 +443,35 @@ public class ProxiedMessage
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config)); await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config));
} }
private async Task DeleteCommandMessage(Context ctx, ulong messageId) private async Task GetCommandMessage(Context ctx, ulong messageId, bool isDelete)
{ {
var cmessage = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId); var msg = await _repo.GetCommandMessage(messageId);
if (cmessage == null) if (msg == null)
throw Errors.MessageNotFound(messageId); throw Errors.MessageNotFound(messageId);
if (cmessage!.AuthorId != ctx.Author.Id) if (isDelete)
throw new PKError("You can only delete command messages queried by this account."); {
if (msg.Sender != ctx.Author.Id)
throw new PKError("You can only delete command messages queried by this account.");
await ctx.Rest.DeleteMessage(cmessage.ChannelId, messageId); await ctx.Rest.DeleteMessage(msg.Channel, messageId);
if (ctx.Guild != null) if (ctx.Guild != null)
await ctx.Rest.DeleteMessage(ctx.Message); await ctx.Rest.DeleteMessage(ctx.Message);
else else
await ctx.Rest.CreateReaction(ctx.Message.ChannelId, ctx.Message.Id, new Emoji { Name = Emojis.Success }); await ctx.Rest.CreateReaction(ctx.Message.ChannelId, ctx.Message.Id, new Emoji { Name = Emojis.Success });
return;
}
var showContent = true;
var channel = await _rest.GetChannelOrNull(msg.Channel);
if (channel == null)
showContent = false;
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
showContent = false;
await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent));
} }
} }

View file

@ -9,29 +9,23 @@ namespace PluralKit.Bot;
public class CommandMessageService public class CommandMessageService
{ {
private readonly RedisService _redis; private readonly RedisService _redis;
private readonly ModelRepository _repo;
private readonly ILogger _logger; private readonly ILogger _logger;
private static readonly TimeSpan CommandMessageRetention = TimeSpan.FromHours(24); private static readonly TimeSpan CommandMessageRetention = TimeSpan.FromHours(24);
public CommandMessageService(RedisService redis, IClock clock, ILogger logger) public CommandMessageService(RedisService redis, ModelRepository repo, IClock clock, ILogger logger)
{ {
_redis = redis; _redis = redis;
_repo = repo;
_logger = logger.ForContext<CommandMessageService>(); _logger = logger.ForContext<CommandMessageService>();
} }
public async Task RegisterMessage(ulong messageId, ulong guildId, ulong channelId, ulong authorId)
{
if (_redis.Connection == null) return;
_logger.Debug(
"Registering command response {MessageId} from author {AuthorId} in {ChannelId}",
messageId, authorId, channelId
);
await _redis.Connection.GetDatabase().StringSetAsync("command_message:" + messageId.ToString(), $"{authorId}-{channelId}-{guildId}", expiry: CommandMessageRetention);
}
public async Task<CommandMessage?> GetCommandMessage(ulong messageId) public async Task<CommandMessage?> GetCommandMessage(ulong messageId)
{ {
var repoMsg = await _repo.GetCommandMessage(messageId);
if (repoMsg != null)
return new CommandMessage(repoMsg.Sender, repoMsg.Channel, repoMsg.Guild);
var str = await _redis.Connection.GetDatabase().StringGetAsync(messageId.ToString()); var str = await _redis.Connection.GetDatabase().StringGetAsync(messageId.ToString());
if (str.HasValue) if (str.HasValue)
{ {

View file

@ -420,6 +420,24 @@ public class EmbedService
return eb.Build(); return eb.Build();
} }
public async Task<Embed> CreateCommandMessageInfoEmbed(Core.CommandMessage msg, bool showContent)
{
var content = "*(command message deleted or inaccessible)*";
if (showContent)
{
var discordMessage = await _rest.GetMessageOrNull(msg.Channel, msg.OriginalMid);
if (discordMessage != null)
content = discordMessage.Content;
}
return new EmbedBuilder()
.Title("Command response message")
.Description(content)
.Field(new("Original message", $"https://discord.com/channels/{msg.Guild}/{msg.Channel}/{msg.OriginalMid}", true))
.Field(new("Sent by", $"<@{msg.Sender}>", true))
.Build();
}
public Task<Embed> CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group, public Task<Embed> CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group,
DateTimeZone tz, LookupContext ctx, string embedTitle, DateTimeZone tz, LookupContext ctx, string embedTitle,
bool ignoreNoFronters, bool showFlat) bool ignoreNoFronters, bool showFlat)

View file

@ -42,12 +42,37 @@ public partial class ModelRepository
}; };
} }
public async Task AddCommandMessage(CommandMessage msg)
{
var query = new Query("command_messages").AsInsert(new
{
mid = msg.Mid,
guild = msg.Guild,
channel = msg.Channel,
sender = msg.Sender,
original_mid = msg.OriginalMid
});
await _db.ExecuteQuery(query, messages: true);
_logger.Debug("Stored command message {@StoredMessage} in channel {Channel}", msg, msg.Channel);
}
public Task<CommandMessage?> GetCommandMessage(ulong id)
=> _db.QueryFirst<CommandMessage?>(new Query("command_messages").Where("mid", id), messages: true);
public async Task DeleteMessage(ulong id) public async Task DeleteMessage(ulong id)
{ {
var query = new Query("messages").AsDelete().Where("mid", id); var query = new Query("messages").AsDelete().Where("mid", id);
var rowCount = await _db.ExecuteQuery(query, messages: true); var rowCount = await _db.ExecuteQuery(query, messages: true);
if (rowCount > 0) if (rowCount > 0)
_logger.Information("Deleted message {MessageId} from database", id); _logger.Information("Deleted message {MessageId} from database", id);
else
{
var cquery = new Query("command_messages").AsDelete().Where("mid", id);
var crowCount = await _db.ExecuteQuery(query, messages: true);
if (crowCount > 0)
_logger.Information("Deleted command message {MessageId} from database", id);
}
} }
public async Task DeleteMessagesBulk(IReadOnlyCollection<ulong> ids) public async Task DeleteMessagesBulk(IReadOnlyCollection<ulong> ids)
@ -59,5 +84,19 @@ public partial class ModelRepository
if (rowCount > 0) if (rowCount > 0)
_logger.Information("Bulk deleted messages ({FoundCount} found) from database: {MessageIds}", rowCount, _logger.Information("Bulk deleted messages ({FoundCount} found) from database: {MessageIds}", rowCount,
ids); ids);
var cquery = new Query("command_messages").AsDelete().WhereIn("mid", ids.Select(id => (long)id).ToArray());
var crowCount = await _db.ExecuteQuery(query, messages: true);
if (crowCount > 0)
_logger.Information("Bulk deleted command messages ({FoundCount} found) from database: {MessageIds}", rowCount,
ids);
} }
}
public class CommandMessage
{
public ulong Mid { get; set; }
public ulong Guild { get; set; }
public ulong Channel { get; set; }
public ulong Sender { get; set; }
public ulong OriginalMid { get; set; }
} }