use std::sync::Arc;
use crate::outline::outlined;
use crate::trace::{Instrument, Span};
use crate::types::{
ApplicationCommandInteraction, Interaction, InteractionResponse, MessageComponentInteraction,
ModalSubmitInteraction,
};
use super::{__unshare, CommandSetDefinition, HandlerError, Route, ScopedDefinition};
pub trait InteractionHandler<S> {
fn render_error(&self, error: &HandlerError) -> InteractionResponse {
error.response()
}
fn command(
&self,
command: ApplicationCommandInteraction,
_state: &S,
) -> impl Future<Output = Result<InteractionResponse, HandlerError>> + Send {
async move { Err(HandlerError::unknown_command(&command.data.name)) }
}
fn component(
&self,
_component: MessageComponentInteraction,
_state: &S,
) -> impl Future<Output = Result<InteractionResponse, HandlerError>> + Send {
async { __unhandled_component() }
}
fn autocomplete(
&self,
_autocomplete: ApplicationCommandInteraction,
_state: &S,
) -> impl Future<Output = Result<InteractionResponse, HandlerError>> + Send {
async { __unhandled_autocomplete() }
}
fn modal_submit(
&self,
_modal: ModalSubmitInteraction,
_state: &S,
) -> impl Future<Output = Result<InteractionResponse, HandlerError>> + Send {
async { __unhandled_modal() }
}
}
#[doc(hidden)]
pub fn __unhandled_component() -> Result<InteractionResponse, HandlerError> {
Ok(not_handled("This bot does not handle components."))
}
#[doc(hidden)]
pub fn __unhandled_autocomplete() -> Result<InteractionResponse, HandlerError> {
Ok(InteractionResponse::respond_with_autocomplete_choices(
Vec::new(),
))
}
#[doc(hidden)]
pub fn __unhandled_modal() -> Result<InteractionResponse, HandlerError> {
Ok(not_handled("This bot does not handle modal submissions."))
}
fn not_handled(message: &str) -> InteractionResponse {
InteractionResponse::respond_with_ephemeral_message(message.to_string())
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoHandler;
impl<S> InteractionHandler<S> for NoHandler {}
pub async fn dispatch<S, H: InteractionHandler<S>>(
interaction: Interaction,
handler: &H,
state: &S,
) -> Result<InteractionResponse, HandlerError> {
let span = span(&interaction);
async move {
debug!("interaction received");
let routed = route(interaction, handler, state).await;
let checked = routed.and_then(|response| {
super::response::check_components_v2(&response)?;
Ok(response)
});
let response = match checked {
Ok(response) => response,
Err(error) => {
error.trace("interaction failed");
return Err(error);
}
};
debug!(response = response_kind(&response), "interaction answered");
Ok(response)
}
.instrument(span)
.await
}
#[cfg(feature = "tracing")]
fn response_kind(response: &InteractionResponse) -> &'static str {
match response {
InteractionResponse::Pong => "pong",
InteractionResponse::ChannelMessageWithSource(_) => "message",
InteractionResponse::DeferredChannelMessageWithSource(_) => "deferred_message",
InteractionResponse::DeferredUpdateMessage => "deferred_update",
InteractionResponse::UpdateMessage(_) => "update",
InteractionResponse::ApplicationCommandAutocompleteResult(_) => "autocomplete",
InteractionResponse::Modal(_) => "modal",
}
}
#[cfg(feature = "tracing")]
fn span(interaction: &Interaction) -> Span {
let (kind, common, name) = match interaction {
Interaction::Ping(ping) => ("ping", &ping.common, None),
Interaction::ApplicationCommand(command) => {
("command", &command.common, Some(command.data.name.as_str()))
}
Interaction::ApplicationCommandAutocomplete(autocomplete) => (
"autocomplete",
&autocomplete.common,
Some(autocomplete.data.name.as_str()),
),
Interaction::MessageComponent(component) => (
"component",
&component.common,
Some(super::custom_id::split(&component.data.custom_id).0),
),
Interaction::ModalSubmit(modal) => (
"modal_submit",
&modal.common,
Some(super::custom_id::split(&modal.data.custom_id).0),
),
};
info_span!(
"interaction",
interaction_id = %common.id,
interaction_type = kind,
name,
guild_id = common.guild_id.as_ref().map(tracing::field::display),
channel_id = common.channel_id.as_ref().map(tracing::field::display),
user_id = common.invoking_user().map(|user| tracing::field::display(&user.id)),
)
}
#[cfg(not(feature = "tracing"))]
fn span(_interaction: &Interaction) -> Span {
Span
}
async fn route<S, H: InteractionHandler<S>>(
interaction: Interaction,
handler: &H,
state: &S,
) -> Result<InteractionResponse, HandlerError> {
match interaction {
Interaction::Ping(_) => Ok(InteractionResponse::Pong),
Interaction::ApplicationCommand(command) => outlined(handler.command(command, state)).await,
Interaction::MessageComponent(component) => {
outlined(handler.component(component, state)).await
}
Interaction::ApplicationCommandAutocomplete(autocomplete) => {
outlined(handler.autocomplete(autocomplete, state)).await
}
Interaction::ModalSubmit(modal) => outlined(handler.modal_submit(modal, state)).await,
}
}
#[derive(Debug, Clone, Copy)]
pub struct WithFallback<C, F> {
commands: C,
fallback: F,
}
impl<C, F> WithFallback<C, F> {
pub fn new(commands: C, fallback: F) -> Self {
Self { commands, fallback }
}
pub fn commands(&self) -> &C {
&self.commands
}
}
impl<S: Sync, C: Route<S> + Sync, F: InteractionHandler<S> + Sync> InteractionHandler<S>
for WithFallback<C, F>
{
fn render_error(&self, error: &HandlerError) -> InteractionResponse {
super::route::__errors::<C>()(error)
}
async fn command(
&self,
command: ApplicationCommandInteraction,
state: &S,
) -> Result<InteractionResponse, HandlerError> {
let command = Arc::new(command);
match C::dispatch(&command, state, super::route::__errors::<C>()).await {
Some(result) => result,
None => self.fallback.command(__unshare(command), state).await,
}
}
async fn component(
&self,
component: MessageComponentInteraction,
state: &S,
) -> Result<InteractionResponse, HandlerError> {
let component = Arc::new(component);
match C::dispatch_component(&component, state, super::route::__errors::<C>()).await {
Some(result) => result,
None => self.fallback.component(__unshare(component), state).await,
}
}
async fn autocomplete(
&self,
autocomplete: ApplicationCommandInteraction,
state: &S,
) -> Result<InteractionResponse, HandlerError> {
let autocomplete = Arc::new(autocomplete);
match C::dispatch_autocomplete(&autocomplete, state, super::route::__errors::<C>()).await {
Some(result) => result,
None => {
self.fallback
.autocomplete(__unshare(autocomplete), state)
.await
}
}
}
async fn modal_submit(
&self,
modal: ModalSubmitInteraction,
state: &S,
) -> Result<InteractionResponse, HandlerError> {
let modal = Arc::new(modal);
match C::dispatch_modal(&modal, state, super::route::__errors::<C>()).await {
Some(result) => result,
None => self.fallback.modal_submit(__unshare(modal), state).await,
}
}
}
impl<C: CommandSetDefinition, F> CommandSetDefinition for WithFallback<C, F> {
fn definitions() -> Vec<ScopedDefinition> {
C::definitions()
}
}
#[cfg(test)]
mod tests {
use futures::executor::block_on;
use serde_json::json;
use super::*;
use crate::types::{Component, MessageCallbackData};
struct Answer(MessageCallbackData);
impl InteractionHandler<()> for Answer {
async fn command(
&self,
_command: ApplicationCommandInteraction,
_state: &(),
) -> Result<InteractionResponse, HandlerError> {
Ok(InteractionResponse::ChannelMessageWithSource(
self.0.clone(),
))
}
}
fn command() -> Interaction {
serde_json::from_value(json!({
"id": "2", "application_id": "1", "type": 2, "token": "tok", "version": 1,
"data": { "id": "4", "name": "x", "type": 1 },
}))
.unwrap()
}
fn v2() -> MessageCallbackData {
MessageCallbackData::components_v2(vec![Component::text("hi")])
}
#[test]
fn components_v2_with_content_embeds_or_poll_is_an_error() {
let cases = [
MessageCallbackData {
content: Some("hi".into()),
..v2()
},
MessageCallbackData {
embeds: Some(vec![crate::types::Embed::new()]),
..v2()
},
];
for data in cases {
let error = block_on(dispatch(command(), &Answer(data), &())).unwrap_err();
assert!(error.to_string().contains("Components V2"), "{error}");
}
}
#[test]
fn components_v2_with_empty_content_passes() {
let data = MessageCallbackData {
content: Some(String::new()),
embeds: Some(Vec::new()),
..v2()
};
assert!(block_on(dispatch(command(), &Answer(data), &())).is_ok());
let legacy = MessageCallbackData {
content: Some("hi".into()),
..Default::default()
};
assert!(block_on(dispatch(command(), &Answer(legacy), &())).is_ok());
}
}