Skip to content

Commit c694e7a

Browse files
committed
Look up original message *before* checking perms
Previously we were checking whether a user was the author based on the referenced_message. However, that field isn't always populated. Instead, we need to look up the original referenced message (in the cache or via the HTTP API) and find that author instead. We already did this for finding the message code, so we just move some of this logic around. There's some awkward cloning we have to do for cached values. I tried adding a fancy enum MessageRef { Owned(..), Borrowed(..), Cached(...) } type, but the cache reference isn't Send, so can't be used in our async code.
1 parent 16dd713 commit c694e7a

3 files changed

Lines changed: 109 additions & 86 deletions

File tree

src/commands/eval.rs

Lines changed: 70 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1+
use std::borrow::Cow;
12
use std::time::Duration;
23

34
use bytes::Bytes;
4-
use serenity::all::prelude::Context;
5-
use serenity::builder::{
6-
CreateAllowedMentions, CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup,
7-
CreateMessage, EditInteractionResponse,
8-
};
9-
use serenity::model::Permissions;
10-
use serenity::model::application::{
11-
CommandInteraction, CommandOptionType, CommandType, MessageInteractionMetadata, ResolvedOption,
12-
ResolvedTarget, ResolvedValue,
13-
};
14-
use serenity::model::channel::{Message, MessageReference};
155
use serenity::{
6+
all::Context,
167
builder::{
17-
CreateActionRow, CreateAttachment, CreateButton, CreateInteractionResponse,
18-
CreateInteractionResponseMessage,
8+
CreateActionRow, CreateAllowedMentions, CreateAttachment, CreateButton, CreateCommand,
9+
CreateCommandOption, CreateInteractionResponse, CreateInteractionResponseFollowup,
10+
CreateInteractionResponseMessage, CreateMessage, EditInteractionResponse,
11+
},
12+
model::{
13+
Permissions,
14+
application::{
15+
ButtonStyle, CommandInteraction, CommandOptionType, CommandType, ComponentInteraction,
16+
MessageInteractionMetadata, ResolvedOption, ResolvedTarget, ResolvedValue,
17+
},
18+
channel::Message,
1919
},
20-
model::application::{ButtonStyle, ComponentInteraction},
2120
};
2221

2322
use crate::commands::eval::code_block::{CodeBlockResult, get_code_block};
2423
use crate::commands::strip_bot_mention;
24+
use crate::discord::MessageExt;
2525
use crate::{InteractionError, State};
2626

2727
pub const ON_RERUN: &str = "on_rerun";
@@ -188,9 +188,11 @@ pub async fn run_slash(
188188

189189
interaction.defer(&ctx).await?;
190190
let response = match submit_code(state, CodeBlockResult::One(code.to_owned()), false).await {
191-
EvalResult::Failure(err) => CreateInteractionResponseFollowup::new()
192-
.content(format!(":bangbang: {err}"))
193-
.ephemeral(true),
191+
EvalResult::Failure(err) => {
192+
// Ideally we'd use ephemeral() here, but that doesn't work. See
193+
// https://github.com/SquidDev-CC/FAQBot-CC/issues/66
194+
CreateInteractionResponseFollowup::new().content(format!(":bangbang: {err}"))
195+
}
194196
EvalResult::Success(text, screenshot) => {
195197
// It's not possible to Rerun this code, as (AFAICT) there's no way to look
196198
// up the original code, so we just drop the buttons.
@@ -223,9 +225,11 @@ pub async fn run_message(
223225

224226
let code = code_block::get_code_block(&msg.content).owned();
225227
let response = match submit_code(state, code, false).await {
226-
EvalResult::Failure(err) => CreateInteractionResponseFollowup::new()
227-
.content(format!(":bangbang: {err}"))
228-
.ephemeral(true),
228+
EvalResult::Failure(err) => {
229+
// Ideally we'd use ephemeral() here, but that doesn't work. See
230+
// https://github.com/SquidDev-CC/FAQBot-CC/issues/66
231+
CreateInteractionResponseFollowup::new().content(format!(":bangbang: {err}"))
232+
}
229233
EvalResult::Success(text, screenshot) => CreateInteractionResponseFollowup::new()
230234
.content(text)
231235
.add_file(CreateAttachment::bytes(screenshot, "image.png"))
@@ -287,7 +291,7 @@ pub async fn eval_from_ping(
287291
///
288292
/// Moderators and the original author (either of the interaction or the target
289293
/// message) can use these controls.
290-
fn can_interact(interaction: &ComponentInteraction) -> bool {
294+
fn can_interact(interaction: &ComponentInteraction, original_message: Option<&Message>) -> bool {
291295
if interaction
292296
.member
293297
.as_ref()
@@ -311,23 +315,34 @@ fn can_interact(interaction: &ComponentInteraction) -> bool {
311315

312316
// If this is a message command, and then the author of the original message
313317
// can also run this.
314-
if let Some(message) = &interaction.message.referenced_message
315-
&& message.author == interaction.user
316-
{
317-
tracing::info!("User wrote the original message ");
318+
if original_message.is_some_and(|x| x.author == interaction.user) {
319+
tracing::debug!("User wrote the original message ");
318320
return true;
319321
}
320322

321-
tracing::warn!("User does not have permission");
323+
tracing::warn!(
324+
author = original_message.map(|x| &x.author.name),
325+
"User does not have permission"
326+
);
322327
false
323328
}
324329

325-
async fn check_interact(
326-
ctx: &Context,
327-
interaction: &ComponentInteraction,
328-
) -> Option<Result<(), InteractionError>> {
329-
if !can_interact(interaction) {
330-
Some(
330+
/// Get the original message containing the code for this component interaction,
331+
/// then check the user can actually perform the interaction.
332+
async fn check_interaction<'a>(
333+
ctx: &'a Context,
334+
interaction: &'a ComponentInteraction,
335+
) -> Result<Option<Cow<'a, Message>>, Result<(), InteractionError>> {
336+
let original_message = match interaction.message.find_reply(ctx).await {
337+
Ok(x) => x,
338+
Err(err) => {
339+
tracing::error!(?err, "Failed to find original message");
340+
None
341+
}
342+
};
343+
344+
if !can_interact(interaction, original_message.as_deref()) {
345+
Err(
331346
interaction
332347
.create_response(
333348
&ctx,
@@ -341,40 +356,7 @@ async fn check_interact(
341356
.map_err(Into::into),
342357
)
343358
} else {
344-
None
345-
}
346-
}
347-
348-
/// Find the original code for a "Rerun" command.
349-
async fn rerun_find_original_code(
350-
ctx: &Context,
351-
interaction: &ComponentInteraction,
352-
) -> Option<CodeBlockResult<String>> {
353-
fn get_code(ctx: &Context, message: &str) -> CodeBlockResult<String> {
354-
let message = strip_bot_mention(ctx, message).unwrap_or(message);
355-
get_code_block(message).owned()
356-
}
357-
358-
if let Some(reply_to) = &interaction.message.referenced_message {
359-
// If we're a reply and we have a referenced_message, use that.
360-
Some(get_code(ctx, &reply_to.content))
361-
} else if let Some(MessageReference {
362-
channel_id,
363-
message_id: Some(message_id),
364-
..
365-
}) = &interaction.message.message_reference
366-
{
367-
// If we're a reply (and don't have a referenced message), look the message up.
368-
if let Some(message) = ctx.cache.message(channel_id, message_id) {
369-
Some(get_code(ctx, &message.content))
370-
} else if let Ok(message) = ctx.http.get_message(*channel_id, *message_id).await {
371-
Some(get_code(ctx, &message.content))
372-
} else {
373-
None
374-
}
375-
} else {
376-
// Otherwise we can't find the message content.
377-
None
359+
Ok(original_message)
378360
}
379361
}
380362

@@ -384,29 +366,31 @@ pub async fn rerun(
384366
ctx: &Context,
385367
interaction: &ComponentInteraction,
386368
) -> Result<(), InteractionError> {
387-
if let Some(res) = check_interact(ctx, interaction).await {
388-
return res;
389-
}
390-
391-
let Some(code) = rerun_find_original_code(ctx, interaction).await else {
392-
interaction
393-
.create_response(
394-
&ctx,
395-
CreateInteractionResponse::Message(
396-
CreateInteractionResponseMessage::new()
397-
.content("Cannot find the original message!")
398-
.ephemeral(true),
399-
),
400-
)
401-
.await?;
402-
403-
return Ok(());
369+
let message = match check_interaction(ctx, interaction).await {
370+
Ok(Some(x)) => x,
371+
Ok(None) => {
372+
return interaction
373+
.create_response(
374+
&ctx,
375+
CreateInteractionResponse::Message(
376+
CreateInteractionResponseMessage::new()
377+
.content(":bangbang: Cannot find the original message!")
378+
.ephemeral(true),
379+
),
380+
)
381+
.await
382+
.map_err(Into::into);
383+
}
384+
Err(err) => return err,
404385
};
405386

406387
interaction
407388
.create_response(&ctx, CreateInteractionResponse::Acknowledge)
408389
.await?;
409390

391+
let message = strip_bot_mention(ctx, &message.content).unwrap_or(&message.content);
392+
let code = get_code_block(message).owned();
393+
410394
match submit_code(state, code, false).await {
411395
EvalResult::Failure(err) => {
412396
interaction
@@ -433,9 +417,9 @@ pub async fn delete(
433417
ctx: &Context,
434418
interaction: &ComponentInteraction,
435419
) -> Result<(), InteractionError> {
436-
if let Some(res) = check_interact(ctx, interaction).await {
437-
return res;
438-
}
420+
if let Err(err) = check_interaction(ctx, interaction).await {
421+
return err;
422+
};
439423

440424
interaction.message.delete(&ctx).await?;
441425
interaction

src/discord.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//! Additional helpers for working with Discord.
2+
3+
use std::borrow::Cow;
4+
5+
use serenity::all::{Context, Error, Message, MessageReference};
6+
7+
pub trait MessageExt {
8+
/// Get the reply of this message, looking it up in the cache or via the API if required.
9+
async fn find_reply<'a>(&'a self, ctx: &'a Context) -> Result<Option<Cow<'a, Message>>, Error>;
10+
}
11+
12+
impl MessageExt for Message {
13+
async fn find_reply<'a>(&'a self, ctx: &'a Context) -> Result<Option<Cow<'a, Message>>, Error> {
14+
if let Some(reply_to) = &self.referenced_message {
15+
// If we're a reply and we have a referenced_message, use that.
16+
Ok(Some(Cow::Borrowed(reply_to.as_ref())))
17+
} else if let Some(MessageReference {
18+
channel_id,
19+
message_id: Some(message_id),
20+
..
21+
}) = &self.message_reference
22+
{
23+
// If we're a reply (and don't have a referenced message), look the message up.
24+
if let Some(message) = ctx.cache.message(channel_id, message_id) {
25+
// It would be nice to avoid the the clone() here. However, we can't return the CacheRef, as that's not Send
26+
// (and we unfortunately need it to be in our async code).
27+
Ok(Some(Cow::Owned(message.clone())))
28+
} else {
29+
Ok(Some(Cow::Owned(
30+
ctx.http.get_message(*channel_id, *message_id).await?,
31+
)))
32+
}
33+
} else {
34+
// Otherwise we can't find the message content.
35+
Ok(None)
36+
}
37+
}
38+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub use crate::state::State;
99
mod cached_request;
1010
mod commands;
1111
mod config;
12+
mod discord;
1213
mod handler;
1314
mod lua_definitions;
1415
mod state;

0 commit comments

Comments
 (0)