use std::{env, error::Error, sync::Arc}; use database::{BoardRow, Database, Error as DbError}; use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType}; use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt}; use twilight_http::{Client as HttpClient, client::InteractionClient}; use twilight_model::{ application::{ command::{Command, CommandType}, interaction::{ InteractionData, application_command::{CommandData, CommandOptionValue}, }, }, channel::message::MessageFlags, gateway::payload::incoming::InteractionCreate, http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}, id::{ Id, marker::{GuildMarker, InteractionMarker, UserMarker}, }, }; use twilight_util::builder::{ InteractionResponseDataBuilder, command::{CommandBuilder, IntegerBuilder, NumberBuilder, StringBuilder, UserBuilder}, }; mod database; const APP_ID: u64 = 1363055126264283136; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv::dotenv()?; let db = Arc::new(Database::new("kindbloot.db")); db.create_tables(); let mut shard = Shard::new( ShardId::ONE, env::var("DISCORD_TOKEN")?, Intents::GUILD_MESSAGES, ); let http = Arc::new(HttpClient::new(env::var("DISCORD_TOKEN")?)); let mut cache = DefaultInMemoryCache::builder() .resource_types(ResourceType::MESSAGE) .resource_types(ResourceType::MEMBER) .build(); let iclient = http.interaction(Id::new(APP_ID)); let commands_vec = commands(&iclient).await; iclient.set_global_commands(&commands_vec).await.unwrap(); println!("Set commands!"); let cmd_ap = commands_vec[1].clone(); let cmd_ap_id = cmd_ap.id; let cmd_ap_global = iclient .global_commands() .await .unwrap() .models() .await .unwrap(); for cmd in cmd_ap_global { println!("got appoints global? {}: {}", cmd.name, cmd.description); } while let Some(item) = shard.next_event(EventTypeFlags::all()).await { let Ok(event) = item else { eprintln!("error receiving event"); continue; }; cache.update(&event); tokio::spawn(handle_event(event, Arc::clone(&http), Arc::clone(&db))); } Ok(()) } async fn handle_event( event: Event, http: Arc, db: Arc, ) -> Result<(), Box> { match event { Event::InteractionCreate(create) => { let data = create.data.as_ref().unwrap(); if let InteractionData::ApplicationCommand(cmd) = data { let guild = create.guild.as_ref().unwrap().id.unwrap(); let command = match cmd.name.as_str() { "leaderboard" => Commands::Leaderboard, "addpoints" => Commands::AddPoints, _ => panic!("meow"), }; command_handler(&db, http, guild, &create, command, cmd).await; } } Event::GatewayClose(close) => { println!("GatewayClose - {close:?}") } _ => (), } Ok(()) } async fn commands(ic: &InteractionClient<'_>) -> Vec { let leaderboard = CommandBuilder::new( "leaderboard", "View the server leaderboard", CommandType::ChatInput, ) .build(); let addpoints = CommandBuilder::new( "addpoints", "Give someone, or multiple people, points!", CommandType::ChatInput, ) .option(IntegerBuilder::new("points", "number of points").required(true)) .option(StringBuilder::new("users", "person to give the points").required(true)) .validate() .unwrap() .build(); vec![leaderboard, addpoints] } async fn command_handler( db: &Database, http: Arc, guild: Id, create: &InteractionCreate, command: Commands, command_data: &CommandData, ) { match command { Commands::Leaderboard => { let data = get_leaderboard(&db, guild); let response = InteractionResponse { kind: InteractionResponseType::ChannelMessageWithSource, data: Some(data), }; let iclient = http.interaction(Id::new(APP_ID)); iclient .create_response(create.id, &create.token, &response) .await .unwrap(); } Commands::AddPoints => { let data = add_points(&db, &http, guild, command_data).await; let response = InteractionResponse { kind: InteractionResponseType::ChannelMessageWithSource, data: Some(data), }; let iclient = http.interaction(Id::new(APP_ID)); iclient .create_response(create.id, &create.token, &response) .await .unwrap(); } } } fn get_leaderboard(db: &Database, guild: Id) -> InteractionResponseData { let board = db.get_leaderboard(guild.get()); match board { Err(DbError::TableNotExist) => InteractionResponseDataBuilder::new() .content( "❌ No leaderboard exists for this server! Create a leaderboard by giving someone points!", ) .build(), Err(DbError::UserNotExist) => unreachable!(), Ok(data) => { let str = data .into_iter() .map(|br| format!("{}: {}", br.user_handle, br.points)) .collect::>() .join("\n"); InteractionResponseDataBuilder::new().content(str).build() } } } macro_rules! aumau { ($meow:expr) => { $meow.await.unwrap().model().await.unwrap() }; } async fn add_points( db: &Database, http: &HttpClient, guild: Id, data: &CommandData, ) -> InteractionResponseData { let mut points = None; let mut users: Vec> = vec![]; for opt in &data.options { match opt.name.as_str() { "points" => match opt.value { CommandOptionValue::Integer(num) => points = Some(num), _ => unreachable!(), }, "users" => match &opt.value { CommandOptionValue::String(raw) => { println!("raw: {raw}"); let mentions = extract_mentions(&raw); users = mentions; } _ => unreachable!(), }, _ => unreachable!(), } } if db.get_leaderboard(guild.get()).is_err() { db.create_leaderboard(guild.get()).unwrap(); } for user in &users { match db.give_user_points(guild.get(), user.get(), points.unwrap()) { Err(DbError::UserNotExist) => { let member = aumau!(http.guild_member(guild, *user)); let row = BoardRow { user_id: user.get(), user_handle: member.user.name, user_nickname: member.nick.or(member.user.global_name), points: points.unwrap(), }; db.add_user_to_leaderboard(guild.get(), row).unwrap(); } _ => (), } } let meow = users .into_iter() .map(|u| format!("<@{u}>")) .collect::>() .join(", "); InteractionResponseDataBuilder::new() .content(format!("added {} points to {meow}", points.unwrap())) .build() } enum Commands { Leaderboard, AddPoints, } fn extract_mentions(mut raw: &str) -> Vec> { let mut ret = vec![]; loop { let Some(start) = raw.find("<@") else { break; }; let Some(end) = raw.find('>') else { break; }; let id_str = &raw[start + 2..end]; raw = &raw[end + 1..]; let Ok(id) = u64::from_str_radix(id_str, 10) else { continue; }; ret.push(Id::new(id)); } ret }