feat: implement a Discord bot using the Gateway API that listens for and responds to commands

This commit is contained in:
2025-04-12 12:50:24 -04:00
parent 492fe68c09
commit eba8822825

View File

@@ -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<dyn Error>, Some)))]
source: Option<Box<dyn Error>>,
},
}
#[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(())
}