From eba8822825e11548fcbfa66e3ce192fc0d579341 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 12 Apr 2025 12:50:24 -0400 Subject: [PATCH] feat: implement a Discord bot using the Gateway API that listens for and responds to commands --- discord-bot/src/main.rs | 169 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 2 deletions(-) diff --git a/discord-bot/src/main.rs b/discord-bot/src/main.rs index e7a11a9..b5a016a 100644 --- a/discord-bot/src/main.rs +++ b/discord-bot/src/main.rs @@ -1,3 +1,168 @@ -fn main() { - println!("Hello, world!"); +use clap::Parser; +use secrecy::{ExposeSecret, SecretString}; +use snafu::{ResultExt, Snafu}; +use std::{error::Error, time::Duration}; +use tracing_subscriber::fmt::format::FmtSpan; +use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType}; +use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt}; +use twilight_model::{ + application::{command::CommandType, interaction::InteractionData}, + gateway::payload::incoming::InteractionCreate, + http::interaction::{InteractionResponse, InteractionResponseType}, +}; +use twilight_util::builder::command::CommandBuilder; + +#[derive(Debug, Parser)] +struct Args { + #[arg(long, env)] + discord_token: SecretString, +} + +#[derive(Debug, Snafu)] +enum AppError { + #[snafu(whatever, display("{message}"))] + Whatever { + message: String, + #[snafu(source(from(Box, Some)))] + source: Option>, + }, +} + +#[tokio::main] +#[snafu::report] +async fn main() -> Result<(), AppError> { + let Args { discord_token } = Args::parse(); + + tracing_subscriber::fmt() + .pretty() + .with_span_events(FmtSpan::ACTIVE) + .init(); + + let shard_id = ShardId::ONE; + let intents = Intents::empty(); + let mut shard = Shard::new(shard_id, discord_token.expose_secret().into(), intents); + + let cache = DefaultInMemoryCache::builder() + .resource_types(ResourceType::empty()) + .build(); + + tracing::info!("info"); + + let client = twilight_http::Client::new(discord_token.expose_secret().into()); + + let current_application = client + .current_user_application() + .await + .whatever_context("couldn't get current Discord application")?; + + let current_application = current_application + .model() + .await + .whatever_context("couldn't get current Discord application")?; + + let application_id = current_application.id; + + let interaction_client = client.interaction(application_id); + + let create_lobby = CommandBuilder::new( + "create", + "Create a lobby in this channel", + CommandType::ChatInput, + ) + .validate() + .whatever_context("command wasn't correct")? + .build(); + let commands = vec![create_lobby]; + + let returned_commands = interaction_client + .set_global_commands(&commands) + .await + .whatever_context("failed to set interaction commands")? + .models() + .await + .whatever_context("failed to deserialize set commands")?; + tracing::info!(?returned_commands); + + while let Some(event_res) = shard.next_event(EventTypeFlags::INTERACTION_CREATE).await { + let event = match event_res { + Ok(event) => event, + Err(receive_message_error) => { + tracing::error!(?receive_message_error); + continue; + } + }; + + tracing::info!(?event); + cache.update(&event); + + match event { + Event::GatewayClose(close_frame) => { + tracing::error!(?close_frame); + break; + } + + Event::InteractionCreate(interaction_create) => { + let InteractionCreate(interaction) = *interaction_create; + tracing::info!(?interaction); + + match interaction.data { + None => { + tracing::warn!("missing expected interaction data"); + continue; + } + Some(InteractionData::ApplicationCommand(command_data)) => { + let command_data = *command_data; + + match command_data.name.as_str() { + "create" => { + tracing::info!("hurray for creating a lobby! TODO"); + + let initial_response = InteractionResponse { + kind: InteractionResponseType::DeferredChannelMessageWithSource, + data: None, + }; + if let Err(error) = interaction_client + .create_response( + interaction.id, + &interaction.token, + &initial_response, + ) + .await + { + tracing::error!(?error); + } + + tokio::time::sleep(Duration::from_secs(5)).await; + + match interaction_client + .update_response(&interaction.token) + .content(Some("hello world!")) + .await + { + Ok(message) => match message.model().await { + Ok(message) => { + tracing::info!(?message); + } + Err(error) => { + tracing::error!(?error); + } + }, + Err(error) => { + tracing::error!(?error); + } + } + } + command_name => { + tracing::warn!(?command_name, "did not expect command"); + } + } + } + _ => {} + } + } + _ => {} + } + } + + Ok(()) }