diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e590460 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,293 @@ +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<HttpClient>, + db: Arc<Database>, +) -> Result<(), Box<dyn Error + Send + Sync>> { + 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<Command> { + 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<HttpClient>, + guild: Id<GuildMarker>, + 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<GuildMarker>) -> 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::<Vec<String>>() + .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<GuildMarker>, + data: &CommandData, +) -> InteractionResponseData { + let mut points = None; + let mut users: Vec<Id<UserMarker>> = 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::<Vec<String>>() + .join(", "); + + InteractionResponseDataBuilder::new() + .content(format!("added {} points to {meow}", points.unwrap())) + .build() +} + +enum Commands { + Leaderboard, + AddPoints, +} + +fn extract_mentions(mut raw: &str) -> Vec<Id<UserMarker>> { + 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 +} |