this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: starboard race conditions, switch to toml config (#23)

+315 -109
+4
.gitignore
··· 5 5 /.idea 6 6 # Database files (stored in /var/lib/blahaj for systemd services) 7 7 /var/lib/blahaj/ 8 + color_me.db 9 + blahaj.toml 10 + packages.db 11 + nixpkgs.hash
+78
Cargo.lock
··· 205 205 "brotli", 206 206 "chrono", 207 207 "color-eyre", 208 + "confique", 208 209 "dotenv", 209 210 "humantime", 210 211 "kittysay", ··· 462 463 "proc-macro2", 463 464 "quote", 464 465 "syn 1.0.109", 466 + ] 467 + 468 + [[package]] 469 + name = "confique" 470 + version = "0.4.0" 471 + source = "registry+https://github.com/rust-lang/crates.io-index" 472 + checksum = "06b4f5ec222421e22bb0a8cbaa36b1d2b50fd45cdd30c915ded34108da78b29f" 473 + dependencies = [ 474 + "confique-macro", 475 + "serde", 476 + "toml", 477 + ] 478 + 479 + [[package]] 480 + name = "confique-macro" 481 + version = "0.0.13" 482 + source = "registry+https://github.com/rust-lang/crates.io-index" 483 + checksum = "e4d1754680cd218e7bcb4c960cc9bae3444b5197d64563dccccfdf83cab9e1a7" 484 + dependencies = [ 485 + "heck", 486 + "proc-macro2", 487 + "quote", 488 + "syn 2.0.111", 465 489 ] 466 490 467 491 [[package]] ··· 2276 2300 ] 2277 2301 2278 2302 [[package]] 2303 + name = "serde_spanned" 2304 + version = "1.0.4" 2305 + source = "registry+https://github.com/rust-lang/crates.io-index" 2306 + checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" 2307 + dependencies = [ 2308 + "serde_core", 2309 + ] 2310 + 2311 + [[package]] 2279 2312 name = "serde_urlencoded" 2280 2313 version = "0.7.1" 2281 2314 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2800 2833 "pin-project-lite", 2801 2834 "tokio", 2802 2835 ] 2836 + 2837 + [[package]] 2838 + name = "toml" 2839 + version = "0.9.11+spec-1.1.0" 2840 + source = "registry+https://github.com/rust-lang/crates.io-index" 2841 + checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" 2842 + dependencies = [ 2843 + "indexmap", 2844 + "serde_core", 2845 + "serde_spanned", 2846 + "toml_datetime", 2847 + "toml_parser", 2848 + "toml_writer", 2849 + "winnow", 2850 + ] 2851 + 2852 + [[package]] 2853 + name = "toml_datetime" 2854 + version = "0.7.5+spec-1.1.0" 2855 + source = "registry+https://github.com/rust-lang/crates.io-index" 2856 + checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" 2857 + dependencies = [ 2858 + "serde_core", 2859 + ] 2860 + 2861 + [[package]] 2862 + name = "toml_parser" 2863 + version = "1.0.6+spec-1.1.0" 2864 + source = "registry+https://github.com/rust-lang/crates.io-index" 2865 + checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" 2866 + dependencies = [ 2867 + "winnow", 2868 + ] 2869 + 2870 + [[package]] 2871 + name = "toml_writer" 2872 + version = "1.0.6+spec-1.1.0" 2873 + source = "registry+https://github.com/rust-lang/crates.io-index" 2874 + checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" 2803 2875 2804 2876 [[package]] 2805 2877 name = "tower" ··· 3568 3640 version = "0.53.1" 3569 3641 source = "registry+https://github.com/rust-lang/crates.io-index" 3570 3642 checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 3643 + 3644 + [[package]] 3645 + name = "winnow" 3646 + version = "0.7.14" 3647 + source = "registry+https://github.com/rust-lang/crates.io-index" 3648 + checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" 3571 3649 3572 3650 [[package]] 3573 3651 name = "wit-bindgen"
+11 -10
Cargo.toml
··· 7 7 readme = "README.md" 8 8 homepage = "https://github.com/isabelroses/blahaj" 9 9 repository = "https://github.com/isabelroses/blahaj" 10 - keywords = ["discord", "bot", "serenity"] 10 + keywords = ["bot", "discord", "serenity"] 11 11 edition = "2024" 12 12 13 13 [dependencies] 14 14 bottomify = "1.2.0" 15 + brotli = "8.0.2" 15 16 chrono = "0.4.43" 16 17 color-eyre = "0.6.5" 18 + confique = { version = "0.4.0", features = ["toml"] } 17 19 dotenv = "0.15.0" 18 20 humantime = "2.3.0" 19 21 kittysay = "0.8.0" 20 22 nixpkgs-track_lib = "0.5.0" 23 + once_cell = "1.21.3" 24 + poise = { git = "https://github.com/serenity-rs/poise", branch = "current" } 21 25 rand = "0.9.2" 22 26 regex = "1.12.2" 27 + rusqlite = { version = "0.38.0", features = ["bundled"] } 23 28 serde_json = "1.0.149" 24 29 serenity = "0.12.5" 25 - poise = { git = "https://github.com/serenity-rs/poise", branch = "current" } 30 + sha2 = "0.10.9" 26 31 simd-json = { version = "0.17.0", features = ["serde"] } 27 - rusqlite = { version = "0.38.0", features = ["bundled"] } 28 - once_cell = "1.21.3" 29 - brotli = "8.0.2" 30 - sha2 = "0.10.9" 32 + 33 + [dependencies.reqwest] 34 + version = "0.12.12" 35 + features = ["json"] 31 36 32 37 [dependencies.serde] 33 38 version = "1.0.228" 34 39 features = ["derive"] 35 - 36 - [dependencies.reqwest] 37 - version = "0.12.12" 38 - features = ["json"] 39 40 40 41 [dependencies.tokio] 41 42 version = "1.49.0"
+12
blahaj.example.toml
··· 1 + # Discord bot token (required) 2 + discord_token = "YOUR_DISCORD_TOKEN" 3 + 4 + # GitHub token for nixpkgs commands (required) 5 + github_token = "YOUR_GITHUB_TOKEN" 6 + 7 + # Directory for SQLite DBs and nixpkgs hash file 8 + # Can also be set via BLAHAJ_DATA_DIR 9 + #data_dir = "/var/lib/blahaj" 10 + 11 + # Nixpkgs channel base URL 12 + #nixpkgs_channel = "https://channels.nixos.org/nixpkgs-unstable"
+47
src/config.rs
··· 1 + use color_eyre::eyre::{eyre, Result}; 2 + use confique::Config; 3 + use std::path::PathBuf; 4 + use std::sync::OnceLock; 5 + 6 + #[derive(Config, Debug, Clone)] 7 + pub struct AppConfig { 8 + #[config(env = "DISCORD_TOKEN")] 9 + pub discord_token: String, 10 + 11 + #[config(env = "GITHUB_TOKEN")] 12 + pub github_token: String, 13 + 14 + #[config(env = "BLAHAJ_DATA_DIR", default = "/var/lib/blahaj")] 15 + pub data_dir: PathBuf, 16 + 17 + #[config( 18 + env = "NIXPKGS_CHANNEL", 19 + default = "https://channels.nixos.org/nixpkgs-unstable" 20 + )] 21 + pub nixpkgs_channel: String, 22 + } 23 + 24 + static CONFIG: OnceLock<AppConfig> = OnceLock::new(); 25 + 26 + pub fn init() -> Result<&'static AppConfig> { 27 + if let Some(config) = CONFIG.get() { 28 + return Ok(config); 29 + } 30 + 31 + let mut builder = AppConfig::builder().env(); 32 + if let Ok(path) = std::env::var("BLAHAJ_CONFIG") { 33 + builder = builder.file(path); 34 + } else { 35 + builder = builder.file("blahaj.toml"); 36 + } 37 + 38 + let config = builder.load().map_err(|err| eyre!("{err}"))?; 39 + CONFIG 40 + .set(config) 41 + .map_err(|_| eyre!("config already initialized"))?; 42 + Ok(CONFIG.get().expect("config initialized")) 43 + } 44 + 45 + pub fn get() -> &'static AppConfig { 46 + CONFIG.get().expect("config not initialized") 47 + }
+137 -91
src/event_handler/starboard.rs
··· 73 73 return Ok(()); 74 74 } 75 75 76 - // Check if already starred 77 - let already_starred: bool = { 76 + let mut should_send = false; 77 + let mut edit_starboard_msg_id: Option<i64> = None; 78 + 79 + { 78 80 let conn = STARBOARD_DB.lock().unwrap(); 79 - conn.query_row( 80 - "SELECT COUNT(*) FROM starred_messages WHERE message_id = ?", 81 - [reaction.message_id.get().cast_signed()], 82 - |row| { 83 - let count: i32 = row.get(0)?; 84 - Ok(count > 0) 85 - }, 86 - ) 87 - .unwrap_or(false) 88 - }; 81 + let existing: Option<(Option<i64>, i64)> = conn 82 + .query_row( 83 + "SELECT starboard_message_id, posting FROM starred_messages WHERE message_id = ?", 84 + [reaction.message_id.get().cast_signed()], 85 + |row| Ok((row.get(0)?, row.get(1)?)), 86 + ) 87 + .ok(); 89 88 90 - if already_starred { 91 - // Update the star count 92 - { 93 - let conn = STARBOARD_DB.lock().unwrap(); 89 + if let Some((starboard_msg_id, posting)) = existing { 94 90 conn.execute( 95 91 "UPDATE starred_messages SET star_count = ? WHERE message_id = ?", 96 92 [ ··· 99 95 ], 100 96 ) 101 97 .ok(); 102 - } 103 98 104 - // Update the starboard message if it exists 105 - let starboard_msg_id: Option<i64> = { 106 - let conn = STARBOARD_DB.lock().unwrap(); 107 - conn.query_row( 108 - "SELECT starboard_message_id FROM starred_messages WHERE message_id = ?", 109 - [reaction.message_id.get().cast_signed()], 110 - |row| row.get::<_, i64>(0), 111 - ) 112 - .ok() 113 - }; 114 - 115 - if let Some(starboard_msg_id) = starboard_msg_id 116 - && let Ok(mut starboard_msg) = 117 - poise::serenity_prelude::ChannelId::new(starboard_channel_id) 118 - .message( 119 - ctx, 120 - poise::serenity_prelude::MessageId::new(starboard_msg_id.cast_unsigned()), 99 + if let Some(starboard_msg_id) = starboard_msg_id { 100 + edit_starboard_msg_id = Some(starboard_msg_id); 101 + } else if posting == 0 { 102 + let updated = conn 103 + .execute( 104 + "UPDATE starred_messages SET posting = 1 WHERE message_id = ?", 105 + [reaction.message_id.get().cast_signed()], 121 106 ) 122 - .await 123 - { 124 - let embed = create_star_embed(&message, star_count); 125 - starboard_msg 126 - .edit(ctx, EditMessage::new().embed(embed)) 127 - .await 107 + .ok(); 108 + should_send = matches!(updated, Some(rows) if rows > 0); 109 + } 110 + } else { 111 + let inserted = conn 112 + .execute( 113 + "INSERT INTO starred_messages (message_id, guild_id, channel_id, starboard_message_id, star_count, posting) VALUES (?, ?, ?, NULL, ?, 1)", 114 + [ 115 + reaction.message_id.get().cast_signed(), 116 + guild_id.get().cast_signed(), 117 + reaction.channel_id.get().cast_signed(), 118 + star_count.cast_signed(), 119 + ], 120 + ) 128 121 .ok(); 122 + should_send = matches!(inserted, Some(rows) if rows > 0); 129 123 } 124 + } 130 125 131 - return Ok(()); 126 + if let Some(starboard_msg_id) = edit_starboard_msg_id 127 + && let Ok(mut starboard_msg) = poise::serenity_prelude::ChannelId::new(starboard_channel_id) 128 + .message( 129 + ctx, 130 + poise::serenity_prelude::MessageId::new(starboard_msg_id.cast_unsigned()), 131 + ) 132 + .await 133 + { 134 + let embed = create_star_embed(&message, star_count); 135 + starboard_msg 136 + .edit(ctx, EditMessage::new().embed(embed)) 137 + .await 138 + .ok(); 132 139 } 133 140 134 - // Create starboard message 135 - let starboard_channel = poise::serenity_prelude::ChannelId::new(starboard_channel_id); 136 - let embed = create_star_embed(&message, star_count); 141 + if should_send { 142 + // Create starboard message 143 + let starboard_channel = poise::serenity_prelude::ChannelId::new(starboard_channel_id); 144 + let embed = create_star_embed(&message, star_count); 137 145 138 - if let Ok(starboard_msg) = starboard_channel 139 - .send_message( 140 - ctx, 141 - poise::serenity_prelude::CreateMessage::new() 142 - .embed(embed) 143 - .content(format!( 144 - "https://discord.com/channels/{}/{}/{}", 145 - guild_id, reaction.channel_id, reaction.message_id 146 - )), 147 - ) 148 - .await 149 - { 150 - // Save to database 151 - let conn = STARBOARD_DB.lock().unwrap(); 152 - conn.execute( 153 - "INSERT INTO starred_messages (message_id, guild_id, channel_id, starboard_message_id, star_count) VALUES (?, ?, ?, ?, ?)", 154 - [ 155 - reaction.message_id.get().cast_signed(), 156 - guild_id.get().cast_signed(), 157 - reaction.channel_id.get().cast_signed(), 158 - starboard_msg.id.get().cast_signed(), 159 - star_count.cast_signed(), 160 - ], 161 - ).ok(); 146 + if let Ok(starboard_msg) = starboard_channel 147 + .send_message( 148 + ctx, 149 + poise::serenity_prelude::CreateMessage::new() 150 + .embed(embed) 151 + .content(format!( 152 + "https://discord.com/channels/{}/{}/{}", 153 + guild_id, reaction.channel_id, reaction.message_id 154 + )), 155 + ) 156 + .await 157 + { 158 + let conn = STARBOARD_DB.lock().unwrap(); 159 + conn.execute( 160 + "UPDATE starred_messages SET starboard_message_id = ?, posting = 0, star_count = ? WHERE message_id = ?", 161 + [ 162 + starboard_msg.id.get().cast_signed(), 163 + star_count.cast_signed(), 164 + reaction.message_id.get().cast_signed(), 165 + ], 166 + ) 167 + .ok(); 168 + } else { 169 + let conn = STARBOARD_DB.lock().unwrap(); 170 + conn.execute( 171 + "UPDATE starred_messages SET posting = 0 WHERE message_id = ?", 172 + [reaction.message_id.get().cast_signed()], 173 + ) 174 + .ok(); 175 + } 162 176 } 163 177 164 178 Ok(()) ··· 210 224 .map_or(0, |r| r.count); 211 225 212 226 // Check if in starboard 213 - let starboard_msg_id: Option<i64> = { 227 + let starboard_entry: Option<(Option<i64>, i64)> = { 214 228 let conn = STARBOARD_DB.lock().unwrap(); 215 229 conn.query_row( 216 - "SELECT starboard_message_id FROM starred_messages WHERE message_id = ?", 230 + "SELECT starboard_message_id, posting FROM starred_messages WHERE message_id = ?", 217 231 [reaction.message_id.get().cast_signed()], 218 - |row| row.get(0), 232 + |row| Ok((row.get(0)?, row.get(1)?)), 219 233 ) 220 234 .ok() 221 235 }; 222 236 223 - let Some(starboard_msg_id) = starboard_msg_id else { 237 + let Some((starboard_msg_id, posting)) = starboard_entry else { 224 238 return Ok(()); 225 239 }; 226 240 227 241 if star_count < u64::from(threshold) { 228 - // Remove from starboard 229 - poise::serenity_prelude::ChannelId::new(starboard_channel_id) 230 - .delete_message( 231 - ctx, 232 - poise::serenity_prelude::MessageId::new(starboard_msg_id.cast_unsigned()), 242 + if let Some(starboard_msg_id) = starboard_msg_id { 243 + let deleted = poise::serenity_prelude::ChannelId::new(starboard_channel_id) 244 + .delete_message( 245 + ctx, 246 + poise::serenity_prelude::MessageId::new(starboard_msg_id.cast_unsigned()), 247 + ) 248 + .await 249 + .is_ok(); 250 + 251 + if deleted { 252 + let conn = STARBOARD_DB.lock().unwrap(); 253 + conn.execute( 254 + "DELETE FROM starred_messages WHERE message_id = ?", 255 + [reaction.message_id.get().cast_signed()], 256 + ) 257 + .ok(); 258 + } else { 259 + let conn = STARBOARD_DB.lock().unwrap(); 260 + conn.execute( 261 + "UPDATE starred_messages SET star_count = ? WHERE message_id = ?", 262 + [ 263 + star_count.cast_signed(), 264 + reaction.message_id.get().cast_signed(), 265 + ], 266 + ) 267 + .ok(); 268 + } 269 + } else if posting == 0 { 270 + let conn = STARBOARD_DB.lock().unwrap(); 271 + conn.execute( 272 + "DELETE FROM starred_messages WHERE message_id = ?", 273 + [reaction.message_id.get().cast_signed()], 233 274 ) 234 - .await 275 + .ok(); 276 + } else { 277 + let conn = STARBOARD_DB.lock().unwrap(); 278 + conn.execute( 279 + "UPDATE starred_messages SET star_count = ? WHERE message_id = ?", 280 + [ 281 + star_count.cast_signed(), 282 + reaction.message_id.get().cast_signed(), 283 + ], 284 + ) 235 285 .ok(); 236 - 237 - let conn = STARBOARD_DB.lock().unwrap(); 238 - conn.execute( 239 - "DELETE FROM starred_messages WHERE message_id = ?", 240 - [reaction.message_id.get().cast_signed()], 241 - ) 242 - .ok(); 286 + } 243 287 } else { 244 288 // Update star count 245 289 { ··· 255 299 } 256 300 257 301 // Update the starboard message 258 - if let Ok(mut starboard_msg) = poise::serenity_prelude::ChannelId::new(starboard_channel_id) 259 - .message( 260 - ctx, 261 - poise::serenity_prelude::MessageId::new(starboard_msg_id.cast_unsigned()), 262 - ) 263 - .await 302 + if let Some(starboard_msg_id) = starboard_msg_id 303 + && let Ok(mut starboard_msg) = 304 + poise::serenity_prelude::ChannelId::new(starboard_channel_id) 305 + .message( 306 + ctx, 307 + poise::serenity_prelude::MessageId::new(starboard_msg_id.cast_unsigned()), 308 + ) 309 + .await 264 310 { 265 311 let embed = create_star_embed(&message, star_count); 266 312 starboard_msg
+3 -2
src/main.rs
··· 1 1 mod commands; 2 + mod config; 2 3 mod event_handler; 3 4 mod nixpkgs_db; 4 5 mod types; 5 6 mod utils; 6 7 7 8 use dotenv::dotenv; 8 - use std::env; 9 9 10 10 use color_eyre::eyre::Result; 11 11 use poise::serenity_prelude::{ ··· 20 20 21 21 // Enable color_eyre beacuse error handling ig 22 22 color_eyre::install()?; 23 + let config = config::init()?; 23 24 nixpkgs_db::ensure_nixpkgs_database().await?; 24 25 25 26 // Configure the client with your Discord bot token in the environment. 26 - let token = env::var("DISCORD_TOKEN").expect("Expected DISCORD_TOKEN to be set"); 27 + let token = config.discord_token.clone(); 27 28 28 29 let intents = GatewayIntents::non_privileged() 29 30 | GatewayIntents::MESSAGE_CONTENT
+1 -2
src/nixpkgs_db.rs
··· 43 43 } 44 44 45 45 pub async fn get_latest_nixpkgs_release() -> Result<NixpkgsRelease> { 46 - let base_url = std::env::var("NIXPKGS_CHANNEL") 47 - .unwrap_or_else(|_| "https://channels.nixos.org/nixpkgs-unstable".to_string()); 46 + let base_url = crate::config::get().nixpkgs_channel.clone(); 48 47 49 48 let response = reqwest::get(&base_url).await?; 50 49 let html = response.text().await?;
+2 -2
src/types.rs
··· 1 1 use reqwest::Client; 2 - use std::{convert::AsRef, env}; 2 + use std::convert::AsRef; 3 3 4 4 #[derive(Debug)] 5 5 // User data, which is stored and accessible in all command invocations ··· 15 15 .user_agent("isabelroses/blahaj") 16 16 .build() 17 17 .unwrap(), 18 - github_token: env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN not set"), 18 + github_token: crate::config::get().github_token.clone(), 19 19 } 20 20 } 21 21 }
+20 -2
src/utils.rs
··· 2 2 use std::path::PathBuf; 3 3 use std::sync::{LazyLock, Mutex}; 4 4 5 - // TODO: figure this out lol 6 5 pub fn get_data_dir() -> PathBuf { 7 - PathBuf::from("/var/lib/blahaj") 6 + crate::config::get().data_dir.clone() 8 7 } 9 8 10 9 pub static STARBOARD_DB: LazyLock<Mutex<Connection>> = LazyLock::new(|| { ··· 18 17 channel_id INTEGER NOT NULL, 19 18 starboard_message_id INTEGER, 20 19 star_count INTEGER NOT NULL DEFAULT 1, 20 + posting INTEGER NOT NULL DEFAULT 0, 21 21 UNIQUE(message_id) 22 22 )", 23 23 [], ··· 33 33 [], 34 34 ) 35 35 .expect("Failed to create starboard_config table"); 36 + 37 + ensure_starboard_schema(&conn).expect("Failed to migrate starboard schema"); 36 38 37 39 Mutex::new(conn) 38 40 }); 41 + 42 + fn ensure_starboard_schema(conn: &Connection) -> rusqlite::Result<()> { 43 + let mut stmt = conn.prepare("PRAGMA table_info(starred_messages)")?; 44 + let column_names = stmt 45 + .query_map([], |row| row.get::<_, String>(1))? 46 + .collect::<Result<Vec<_>, _>>()?; 47 + 48 + if !column_names.iter().any(|name| name == "posting") { 49 + conn.execute( 50 + "ALTER TABLE starred_messages ADD COLUMN posting INTEGER NOT NULL DEFAULT 0", 51 + [], 52 + )?; 53 + } 54 + 55 + Ok(()) 56 + }