descriptions and pings, new yaml crate

This commit is contained in:
Benji Dial 2026-01-13 11:41:07 -05:00
parent bc9b9959c5
commit bebc021a37
6 changed files with 179 additions and 106 deletions

99
Cargo.lock generated
View file

@ -8,15 +8,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 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]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@ -26,6 +17,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@ -85,9 +82,9 @@ version = "4.0.0-snapshot"
dependencies = [ dependencies = [
"rand 0.9.2", "rand 0.9.2",
"rusqlite", "rusqlite",
"rust-yaml",
"serenity", "serenity",
"tokio", "tokio",
"yaml-rust2",
] ]
[[package]] [[package]]
@ -280,10 +277,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "equivalent" name = "encoding_rs"
version = "1.0.2" version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "errno" name = "errno"
@ -724,18 +724,6 @@ dependencies = [
"icu_properties", "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]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.11.0" version = "2.11.0"
@ -824,15 +812,6 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "memmap2"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -1135,35 +1114,6 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.28" version = "0.12.28"
@ -1235,18 +1185,6 @@ dependencies = [
"sqlite-wasm-rs", "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]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.1" version = "2.1.1"
@ -2362,6 +2300,17 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 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]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.1" version = "0.8.1"

View file

@ -4,7 +4,7 @@ version = "4.0.0-snapshot"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
rust-yaml = "0.0.5" yaml-rust2 = "0.11.0"
rand = "0.9.2" rand = "0.9.2"
rusqlite = { version = "0.38.0", features = [ "bundled" ] } rusqlite = { version = "0.38.0", features = [ "bundled" ] }
tokio = { version = "1.49.0", features = [ "rt-multi-thread" ] } tokio = { version = "1.49.0", features = [ "rt-multi-thread" ] }

View file

@ -2,24 +2,19 @@ use rand::seq::SliceRandom;
fn parse_yaml(yaml: &String) -> Option<bracket_bot_v4::CreateBracketInfo> { fn parse_yaml(yaml: &String) -> Option<bracket_bot_v4::CreateBracketInfo> {
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 bracket_name = String::from(content["name"].as_str()?);
let channel_id: u64 = content.get_str("channel_id")?.as_str()?.parse().ok()?; let channel_id: u64 = content["channel_id"].as_str()?.parse::<u64>().ok()?;
let role_id: u64 = content.get_str("role_id")?.as_str()?.parse().ok()?; let role_id: u64 = content["role_id"].as_str()?.parse::<u64>().ok()?;
let entries = content.get_str("entries")?.as_sequence()?; let entries = content["entries"].as_vec()?;
let mut entry_infos = Vec::new(); let mut entry_infos = Vec::new();
for entry in entries { for entry in entries {
let name = String::from(entry["name"].as_str()?);
let name = String::from(entry.get_str("name")?.as_str()?); let description = entry["description"].as_str().and_then(|s| Some(String::from(s)));
let description = match entry.get_str("description") { let emoji= entry["emoji"].as_str().and_then(|s| Some(String::from(s)));
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()?)) };
entry_infos.push(bracket_bot_v4::CreateBracketEntryInfo { name, description, emoji }); entry_infos.push(bracket_bot_v4::CreateBracketEntryInfo { name, description, emoji });
} }
entry_infos.shuffle(&mut rand::rng()); entry_infos.shuffle(&mut rand::rng());

4
src/bin/gateway.rs Normal file
View file

@ -0,0 +1,4 @@
#[tokio::main]
async fn main() {
bracket_bot_v4::run_gateway().await;
}

View file

@ -40,4 +40,6 @@ async fn main() {
bracket_bot_v4::post_poll(&mut dbc, &dc, bracket_id, channel_id, &poll).await; 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;
} }

View file

@ -21,7 +21,7 @@ pub struct CreateBracketInfo {
pub bracket_name: String, pub bracket_name: String,
pub channel_id: u64, pub channel_id: u64,
pub role_id: u64, pub role_id: u64,
pub entries: Vec<CreateBracketEntryInfo> pub entries: Vec<CreateBracketEntryInfo>
} }
pub struct UnprocessedPoll { pub struct UnprocessedPoll {
@ -127,19 +127,75 @@ async fn create_discord_conn_core(bot_token: &String)
Ok(DiscordConn { client }) Ok(DiscordConn { client })
} }
pub async fn create_discord_conn() -> DiscordConn { fn get_token() -> String {
let token = std::env::var("BOT_TOKEN"); let token = std::env::var("BOT_TOKEN");
match token { match token {
Err(e) => { Err(e) => {
eprintln!("Could not get BOT_TOKEN variable: {e}"); eprintln!("Could not get BOT_TOKEN variable: {e}");
std::process::exit(1); std::process::exit(1);
}, },
Ok(t) => { Ok(t) => t
match create_discord_conn_core(&t).await { }
}
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::<u64>().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<serenity::Client, serenity::Error> {
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) Ok(dc) => dc, Err(e) => discord_error(&e)
} }
}
}
} }
async fn create_bracket_core_discord(dc: &DiscordConn, info: &CreateBracketInfo) -> Result<(), serenity::Error> { async fn create_bracket_core_discord(dc: &DiscordConn, info: &CreateBracketInfo) -> Result<(), serenity::Error> {
@ -149,14 +205,14 @@ async fn create_bracket_core_discord(dc: &DiscordConn, info: &CreateBracketInfo)
let mut message = serenity::builder::CreateMessage::new(); let mut message = serenity::builder::CreateMessage::new();
message = message.content(format!("# Bracket: {}", info.bracket_name)); message = message.content(format!("# Bracket: {}", info.bracket_name));
let mut button = serenity::builder::CreateButton::new(info.role_id.to_string()); let button_id = String::from("toggle ") + &info.role_id.to_string();
button = button.label("toggle pings"); 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?; let sent = channel.send_message(&dc.client.http, message).await?;
//sent.pin(&dc.client.http).await sent.pin(&dc.client.http).await
Ok(())
} }
@ -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) fn go_to_next_round_core_db(dbc: &mut DbConn, bracket_id: i32)
-> Result<(i32, Vec<String>), rusqlite::Error> { -> Result<(i32, Vec<String>, Option<String>), rusqlite::Error> {
let trans = dbc.conn.transaction()?; 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)?); last_entry_id = Some(row.get(0)?);
} }
let winner_description: Option<String> =
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(rows);
drop(statement); drop(statement);
if all_entries.len() == 1 { if all_entries.len() == 1 {
trans.commit()?; trans.commit()?;
return Ok((next_round, all_entries)); return Ok((next_round, all_entries, winner_description));
} }
if all_entries.len() % 2 == 1 { 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()?; 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<String>) async fn go_to_next_round_core_discord(
dc: &DiscordConn, new_round: i32, channel_id: u64, names: &Vec<String>, winner_description: &Option<String>)
-> Result<(), serenity::Error> { -> Result<(), serenity::Error> {
if names.len() == 0 { if names.len() == 0 {
@ -470,6 +538,11 @@ async fn go_to_next_round_core_discord(dc: &DiscordConn, new_round: i32, channel
channel.say( channel.say(
&dc.client.http, &dc.client.http,
format!("# WINNER: {}", names[0])).await?; format!("# WINNER: {}", names[0])).await?;
if let Some(wd) = winner_description.as_ref() {
channel.say(
&dc.client.http,
wd).await?;
}
return Ok(()) 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) { 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) { match go_to_next_round_core_db(dbc, bracket_id) {
Ok(t) => t, Err(e) => db_error(&e) 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) 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<String>)
-> 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) dc: &DiscordConn, channel_id: u64, entry_1: &EntryForPoll, entry_2: &EntryForPoll)
-> Result<PostedPoll, serenity::Error> { -> Result<PostedPoll, serenity::Error> {
@ -560,6 +642,8 @@ pub async fn post_poll_core_discord(
message = message.poll(pollr); message = message.poll(pollr);
let posted = channel.send_message(&dc.client.http, message).await?; 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<i32> = None; let mut answer_id_1: Option<i32> = None;
let mut answer_id_2: Option<i32> = None; let mut answer_id_2: Option<i32> = 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<String, rusqlite::Error> {
Ok(
dbc.conn.query_one(
"SELECT role_id FROM brackets WHERE rowid = ?",
(bracket_id,),
|r| r.get::<usize, String>(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)
}
}