use crate::config::Webhook; use anyhow::bail; use async_nats::Client; use ed25519_dalek::PublicKey; use hyper::{ body::{to_bytes, Bytes}, service::Service, Body, Method, Request, Response, }; use shared::payloads::{CachePayload, DispatchEventTagged}; use signature::validate; use std::{ future::Future, pin::Pin, task::{Context, Poll}, }; use tracing::{debug, error}; use twilight_model::gateway::event::DispatchEvent; use twilight_model::{ application::interaction::{Interaction, InteractionType}, gateway::payload::incoming::InteractionCreate, }; pub mod make_service; mod signature; #[cfg(test)] pub mod tests; /// Hyper service used to handle the discord webhooks #[derive(Clone)] pub struct WebhookService { pub config: Webhook, pub nats: Client, } impl WebhookService { async fn check_request(req: Request, pk: PublicKey) -> Result { if req.method() == Method::POST { let signature = if let Some(sig) = req.headers().get("X-Signature-Ed25519") { sig.clone() } else { bail!("Missing signature header"); }; let timestamp = if let Some(timestamp) = req.headers().get("X-Signature-Timestamp") { timestamp.clone() } else { bail!("Missing timestamp header"); }; let data = to_bytes(req.into_body()).await?; if validate( &pk, &[timestamp.as_bytes().to_vec(), data.to_vec()].concat(), signature.to_str()?, ) { Ok(data) } else { bail!("invalid signature"); } } else { bail!("not found"); } } async fn process_request( req: Request, nats: Client, pk: PublicKey, ) -> Result, anyhow::Error> { let data = Self::check_request(req, pk).await?; let interaction: Interaction = serde_json::from_slice(&data)?; if interaction.kind == InteractionType::Ping { Ok(Response::builder() .header("Content-Type", "application/json") .body(r#"{"type":1}"#.into()) .unwrap()) } else { debug!("calling nats"); // this should hopefully not fail ? let data = CachePayload { data: DispatchEventTagged(DispatchEvent::InteractionCreate(Box::new( InteractionCreate(interaction), ))), }; let payload = serde_json::to_string(&data).unwrap(); match nats .request( "nova.cache.dispatch.INTERACTION_CREATE".to_string(), Bytes::from(payload), ) .await { Ok(response) => Ok(Response::builder() .header("Content-Type", "application/json") .body(Body::from(response.payload)) .unwrap()), Err(error) => { error!("failed to request nats: {}", error); Err(anyhow::anyhow!("internal error")) } } } } } /// Implementation of the service impl Service> for WebhookService { type Response = hyper::Response; type Error = anyhow::Error; type Future = Pin> + Send>>; fn poll_ready(&mut self, _: &mut Context) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: Request) -> Self::Future { let future = Self::process_request(req, self.nats.clone(), self.config.discord.public_key); Box::pin(async move { let response = future.await; match response { Ok(r) => Ok(r), Err(e) => Err(e), } }) } }