diff --git a/Cargo.lock b/Cargo.lock index 6bf170e..c58a9b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,15 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -26,6 +17,12 @@ dependencies = [ "libc", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.6" @@ -85,9 +82,9 @@ version = "4.0.0-snapshot" dependencies = [ "rand 0.9.2", "rusqlite", - "rust-yaml", "serenity", "tokio", + "yaml-rust2", ] [[package]] @@ -280,10 +277,13 @@ dependencies = [ ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] [[package]] name = "errno" @@ -724,18 +724,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -824,15 +812,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memmap2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" -dependencies = [ - "libc", -] - [[package]] name = "mime" version = "0.3.17" @@ -1135,35 +1114,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - [[package]] name = "reqwest" version = "0.12.28" @@ -1235,18 +1185,6 @@ dependencies = [ "sqlite-wasm-rs", ] -[[package]] -name = "rust-yaml" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22075c416a33b2fc18e6502b3836023436ca39b67cff4ae8300dffa12bc05dfc" -dependencies = [ - "base64", - "indexmap", - "memmap2", - "regex", -] - [[package]] name = "rustc-hash" version = "2.1.1" @@ -2362,6 +2300,17 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "yaml-rust2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index d489835..08d019a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "4.0.0-snapshot" edition = "2024" [dependencies] -rust-yaml = "0.0.5" +yaml-rust2 = "0.11.0" rand = "0.9.2" rusqlite = { version = "0.38.0", features = [ "bundled" ] } tokio = { version = "1.49.0", features = [ "rt-multi-thread" ] } diff --git a/src/bin/create-bracket.rs b/src/bin/create-bracket.rs index 128fc1a..a7818e8 100644 --- a/src/bin/create-bracket.rs +++ b/src/bin/create-bracket.rs @@ -2,24 +2,19 @@ use rand::seq::SliceRandom; fn parse_yaml(yaml: &String) -> Option { - let content = rust_yaml::Yaml::new().load_str(&yaml).ok()?; + let content = &yaml_rust2::YamlLoader::load_from_str(yaml).ok()?[0]; - let bracket_name = String::from(content.get_str("name")?.as_str()?); - let channel_id: u64 = content.get_str("channel_id")?.as_str()?.parse().ok()?; - let role_id: u64 = content.get_str("role_id")?.as_str()?.parse().ok()?; - let entries = content.get_str("entries")?.as_sequence()?; + let bracket_name = String::from(content["name"].as_str()?); + let channel_id: u64 = content["channel_id"].as_str()?.parse::().ok()?; + let role_id: u64 = content["role_id"].as_str()?.parse::().ok()?; + let entries = content["entries"].as_vec()?; let mut entry_infos = Vec::new(); for entry in entries { - - let name = String::from(entry.get_str("name")?.as_str()?); - let description = match entry.get_str("description") { - None => None, Some(v) => Some(String::from(v.as_str()?)) }; - let emoji = match entry.get_str("emoji") { - None => None, Some(v) => Some(String::from(v.as_str()?)) }; - + let name = String::from(entry["name"].as_str()?); + let description = entry["description"].as_str().and_then(|s| Some(String::from(s))); + let emoji= entry["emoji"].as_str().and_then(|s| Some(String::from(s))); entry_infos.push(bracket_bot_v4::CreateBracketEntryInfo { name, description, emoji }); - } entry_infos.shuffle(&mut rand::rng()); diff --git a/src/bin/gateway.rs b/src/bin/gateway.rs new file mode 100644 index 0000000..f6209c8 --- /dev/null +++ b/src/bin/gateway.rs @@ -0,0 +1,4 @@ +#[tokio::main] +async fn main() { + bracket_bot_v4::run_gateway().await; +} diff --git a/src/bin/post-polls.rs b/src/bin/post-polls.rs index 4d3fc59..c209a3b 100644 --- a/src/bin/post-polls.rs +++ b/src/bin/post-polls.rs @@ -40,4 +40,6 @@ async fn main() { bracket_bot_v4::post_poll(&mut dbc, &dc, bracket_id, channel_id, &poll).await; } + bracket_bot_v4::ping_bracket_role(&dbc, &dc, bracket_id, channel_id).await; + } diff --git a/src/lib.rs b/src/lib.rs index c9de53e..46d5a0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ pub struct CreateBracketInfo { pub bracket_name: String, pub channel_id: u64, pub role_id: u64, -pub entries: Vec + pub entries: Vec } pub struct UnprocessedPoll { @@ -127,18 +127,74 @@ async fn create_discord_conn_core(bot_token: &String) Ok(DiscordConn { client }) } -pub async fn create_discord_conn() -> DiscordConn { +fn get_token() -> String { let token = std::env::var("BOT_TOKEN"); match token { Err(e) => { eprintln!("Could not get BOT_TOKEN variable: {e}"); std::process::exit(1); }, - Ok(t) => { - match create_discord_conn_core(&t).await { - Ok(dc) => dc, Err(e) => discord_error(&e) + Ok(t) => t + } +} + +struct GatewayHandler; + +#[serenity::async_trait] +impl serenity::client::EventHandler for GatewayHandler { + + async fn interaction_create( + &self, ctx: serenity::client::Context, interaction: serenity::model::application::Interaction) { + + if let serenity::model::application::Interaction::Component(cpt) = interaction { + let id = &cpt.data.custom_id; + + if id.starts_with("toggle ") { + + let role_id = serenity::model::id::RoleId::new(id[7..].parse::().unwrap()); + let guild_id = cpt.guild_id.unwrap(); + let member = guild_id.member(&ctx, cpt.user.id).await.unwrap(); + let removing = cpt.user.has_role(&ctx, guild_id, role_id).await.unwrap(); + + let result = + if removing { + member.remove_role(&ctx, role_id).await + } + else { + member.add_role(&ctx, role_id).await + }; + match result { + Ok(()) => {}, Err(e) => { + eprintln!("error toggling role {}: {}", role_id, e); + return; + } + } + + let mut msg = + serenity::builder::CreateInteractionResponseMessage::default(); + msg = msg.content(if removing { "Removed role." } else { "Added role." }); + msg = msg.ephemeral(true); + let resp= serenity::builder::CreateInteractionResponse::Message(msg); + cpt.create_response(&ctx, resp).await.unwrap(); + } + } + + } + +} + +async fn create_gateway_client() -> Result { + let intents = serenity::model::gateway::GatewayIntents::empty(); + let mut builder = serenity::Client::builder(get_token(), intents); + builder = builder.event_handler(GatewayHandler); + builder.await +} + +pub async fn create_discord_conn() -> DiscordConn { + match create_discord_conn_core(&get_token()).await { + Ok(dc) => dc, Err(e) => discord_error(&e) } } @@ -149,14 +205,14 @@ async fn create_bracket_core_discord(dc: &DiscordConn, info: &CreateBracketInfo) let mut message = serenity::builder::CreateMessage::new(); message = message.content(format!("# Bracket: {}", info.bracket_name)); - let mut button = serenity::builder::CreateButton::new(info.role_id.to_string()); - button = button.label("toggle pings"); + let button_id = String::from("toggle ") + &info.role_id.to_string(); + let mut button = serenity::builder::CreateButton::new(button_id); + button = button.label("Toggle pings"); - //message = message.button(button); + message = message.button(button); let sent = channel.send_message(&dc.client.http, message).await?; - //sent.pin(&dc.client.http).await - Ok(()) + sent.pin(&dc.client.http).await } @@ -405,7 +461,7 @@ pub fn get_channel_id(dbc: &mut DbConn, bracket_id: i32) -> u64 { } fn go_to_next_round_core_db(dbc: &mut DbConn, bracket_id: i32) - -> Result<(i32, Vec), rusqlite::Error> { + -> Result<(i32, Vec, Option), rusqlite::Error> { let trans = dbc.conn.transaction()?; @@ -436,12 +492,23 @@ fn go_to_next_round_core_db(dbc: &mut DbConn, bracket_id: i32) last_entry_id = Some(row.get(0)?); } + let winner_description: Option = + if all_entries.len() == 1 { + trans.query_one( + "SELECT description FROM entries WHERE rowid = ?", + (last_entry_id,), + |r| r.get(0))? + } + else { + None + }; + drop(rows); drop(statement); if all_entries.len() == 1 { trans.commit()?; - return Ok((next_round, all_entries)); + return Ok((next_round, all_entries, winner_description)); } if all_entries.len() % 2 == 1 { @@ -451,13 +518,14 @@ fn go_to_next_round_core_db(dbc: &mut DbConn, bracket_id: i32) } trans.commit()?; - Ok((next_round, all_entries)) + Ok((next_round, all_entries, winner_description)) } -async fn go_to_next_round_core_discord(dc: &DiscordConn, new_round: i32, channel_id: u64, names: &Vec) +async fn go_to_next_round_core_discord( + dc: &DiscordConn, new_round: i32, channel_id: u64, names: &Vec, winner_description: &Option) -> Result<(), serenity::Error> { if names.len() == 0 { @@ -470,6 +538,11 @@ async fn go_to_next_round_core_discord(dc: &DiscordConn, new_round: i32, channel channel.say( &dc.client.http, format!("# WINNER: {}", names[0])).await?; + if let Some(wd) = winner_description.as_ref() { + channel.say( + &dc.client.http, + wd).await?; + } return Ok(()) } @@ -489,11 +562,11 @@ async fn go_to_next_round_core_discord(dc: &DiscordConn, new_round: i32, channel } pub async fn go_to_next_round(dbc: &mut DbConn, dc: &DiscordConn, bracket_id: i32, channel_id: u64) { - let (new_round, names) = + let (new_round, names, winner_description) = match go_to_next_round_core_db(dbc, bracket_id) { Ok(t) => t, Err(e) => db_error(&e) }; - match go_to_next_round_core_discord(&dc, new_round, channel_id, &names).await { + match go_to_next_round_core_discord(&dc, new_round, channel_id, &names, &winner_description).await { Ok(()) => {}, Err(e) => discord_error(&e) } } @@ -539,7 +612,16 @@ fn create_poll_answer(entry: &EntryForPoll) -> serenity::builder::CreatePollAnsw } -pub async fn post_poll_core_discord( +async fn maybe_post_description(dc: &DiscordConn, channel_id: u64, description: &Option) + -> Result<(), serenity::Error> { + if let Some(s) = description { + let channel = serenity::model::id::ChannelId::new(channel_id); + channel.say(&dc.client.http, s).await?; + } + Ok(()) +} + +async fn post_poll_core_discord( dc: &DiscordConn, channel_id: u64, entry_1: &EntryForPoll, entry_2: &EntryForPoll) -> Result { @@ -560,6 +642,8 @@ pub async fn post_poll_core_discord( message = message.poll(pollr); let posted = channel.send_message(&dc.client.http, message).await?; + maybe_post_description(dc, channel_id, &entry_1.description).await?; + maybe_post_description(dc, channel_id, &entry_2.description).await?; let mut answer_id_1: Option = None; let mut answer_id_2: Option = None; @@ -617,3 +701,42 @@ pub async fn post_poll(dbc: &mut DbConn, dc: &DiscordConn, bracket_id: i32, chan } } + +fn get_bracket_role(dbc: &DbConn, bracket_id: i32) -> Result { + Ok( + dbc.conn.query_one( + "SELECT role_id FROM brackets WHERE rowid = ?", + (bracket_id,), + |r| r.get::(0))?) +} + +pub async fn ping_bracket_role(dbc: &DbConn, dc: &DiscordConn, bracket_id: i32, channel_id: u64) { + + let role_id = + match get_bracket_role(&dbc, bracket_id) { + Ok(r) => r, Err(e) => db_error(&e) + }; + + let mut m = serenity::builder::CreateAllowedMentions::new(); + m = m.all_roles(true); + let mut msg = serenity::builder::CreateMessage::new(); + msg = msg.allowed_mentions(m); + msg = msg.content(format!("<@&{}>", role_id)); + + let channel = serenity::model::id::ChannelId::new(channel_id); + match channel.send_message(&dc.client.http, msg).await { + Ok(_) => {}, Err(e) => discord_error(&e) + } + +} + +async fn run_gateway_core() -> Result<(), serenity::Error> { + let mut client = create_gateway_client().await?; + client.start().await +} + +pub async fn run_gateway() { + match run_gateway_core().await { + Ok(()) => {}, Err(e) => discord_error(&e) + } +}