use std::sync::Arc;
use autumn_web::reexports::axum::body::to_bytes;
use autumn_web::reexports::axum::extract::{FromRequest, Request};
use autumn_web::reexports::axum::http::request::Parts;
use autumn_web::{AppState, AutumnError, AutumnResult, Json, State, post, routes};
use crate::error::Error;
use crate::plugin::DiscordRuntime;
use crate::types::{Interaction, InteractionResponse, InteractionType};
use crate::verify::{SIGNATURE_HEADER, TIMESTAMP_HEADER, verify_interaction};
const MAX_BODY_BYTES: usize = 1024 * 1024;
pub struct VerifiedInteraction {
pub interaction: Interaction,
pub runtime: Arc<DiscordRuntime>,
}
impl std::fmt::Debug for VerifiedInteraction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VerifiedInteraction")
.field("kind", &self.interaction.kind)
.finish_non_exhaustive()
}
}
impl FromRequest<AppState> for VerifiedInteraction {
type Rejection = AutumnError;
async fn from_request(req: Request, state: &AppState) -> Result<Self, Self::Rejection> {
let runtime = state
.extension::<DiscordRuntime>()
.ok_or_else(|| Error::MissingRuntime.to_autumn_error())?;
let (parts, body) = req.into_parts();
let signature = header_value(&parts, SIGNATURE_HEADER);
let timestamp = header_value(&parts, TIMESTAMP_HEADER);
let bytes = to_bytes(body, MAX_BODY_BYTES)
.await
.map_err(|_| AutumnError::bad_request_msg("interaction body too large"))?;
verify_interaction(&runtime.public_key, signature, timestamp, &bytes)
.map_err(|err| err.to_autumn_error())?;
let interaction: Interaction =
serde_json::from_slice(&bytes).map_err(|err| Error::Json(err).to_autumn_error())?;
Ok(Self {
interaction,
runtime,
})
}
}
fn header_value<'a>(parts: &'a Parts, name: &str) -> Option<&'a str> {
parts.headers.get(name)?.to_str().ok()
}
#[post("/interactions")]
async fn interactions_webhook(
State(_state): State<AppState>,
VerifiedInteraction {
interaction,
runtime,
}: VerifiedInteraction,
) -> AutumnResult<Json<InteractionResponse>> {
if interaction.kind == InteractionType::Ping {
return Ok(Json(InteractionResponse::pong()));
}
let ctx = crate::commands::CommandContext {
interaction,
client: runtime.client.clone(),
};
let response = runtime.registry.dispatch(ctx).await?;
Ok(Json(response))
}
pub(crate) fn webhook_routes() -> Vec<autumn_web::Route> {
routes![interactions_webhook]
}
#[cfg(test)]
mod tests;