this repo has no description
1
fork

Configure Feed

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

stuff from discord idk (#22)

* feat: upload idk.webp everyday at 13:00 utc+2

kaya wanted it soooooo

* feat: incremental migrator for color_me, rename functionality

* fix(color_me) `COLOR_ROLES_DB` envvar for db conn

+222 -33
+1
.gitignore
··· 5 5 /.idea 6 6 nixpkgs.db 7 7 nixpkgs.hash 8 + color_me.db
+1
Cargo.lock
··· 209 209 dependencies = [ 210 210 "bottomify", 211 211 "brotli", 212 + "chrono", 212 213 "color-eyre", 213 214 "dotenv", 214 215 "humantime",
+1
Cargo.toml
··· 12 12 13 13 [dependencies] 14 14 bottomify = "1.2.0" 15 + chrono = "0.4" 15 16 color-eyre = "0.6.5" 16 17 dotenv = "0.15.0" 17 18 humantime = "2.3.0"
assets/idk.webp

This is a binary file and will not be displayed.

+1 -1
src/commands/bot/bot.rs
··· 1 1 use crate::types::Context; 2 2 use color_eyre::eyre::Result; 3 3 use poise::{ 4 - serenity_prelude::{CreateEmbed, CreateEmbedAuthor}, 5 4 CreateReply, 5 + serenity_prelude::{CreateEmbed, CreateEmbedAuthor}, 6 6 }; 7 7 8 8 /// Displays information about the bot
+1 -3
src/commands/nix/nixpkg.rs
··· 94 94 name: row 95 95 .get::<_, Option<String>>(0)? 96 96 .unwrap_or_else(|| "Unknown".to_string()), 97 - github: row 98 - .get::<_, Option<String>>(1)? 99 - .unwrap_or_else(String::new), 97 + github: row.get::<_, Option<String>>(1)?.unwrap_or_else(String::new), 100 98 }) 101 99 })? 102 100 .filter_map(Result::ok)
+169 -27
src/commands/user/color_me.rs
··· 1 1 use crate::types::Context; 2 2 use color_eyre::eyre::Result; 3 - use poise::serenity_prelude::{Colour, EditRole}; 4 3 use poise::CreateReply; 4 + use poise::serenity_prelude::{Colour, EditRole, RoleId, UserId}; 5 + use rusqlite::{Connection, params}; 6 + use std::sync::{LazyLock, Mutex}; 7 + 8 + static COLOR_DB: LazyLock<Mutex<Connection>> = LazyLock::new(|| { 9 + let db_path = std::env::var("COLOR_ROLES_DB").unwrap_or_else(|_| "color_roles.db".to_string()); 10 + let conn = Connection::open(db_path).expect("Failed to open color roles database"); 11 + 12 + conn.execute( 13 + "CREATE TABLE IF NOT EXISTS color_roles ( 14 + user_id INTEGER PRIMARY KEY, 15 + guild_id INTEGER NOT NULL, 16 + role_id INTEGER NOT NULL, 17 + role_name TEXT NOT NULL 18 + )", 19 + [], 20 + ) 21 + .expect("Failed to create color_roles table"); 22 + 23 + Mutex::new(conn) 24 + }); 25 + 26 + fn get_user_role(user_id: UserId, guild_id: u64) -> Option<(RoleId, String)> { 27 + let conn = COLOR_DB.lock().ok()?; 28 + let mut stmt = conn 29 + .prepare("SELECT role_id, role_name FROM color_roles WHERE user_id = ? AND guild_id = ?") 30 + .ok()?; 31 + 32 + stmt.query_row(params![user_id.get() as i64, guild_id as i64], |row| { 33 + let role_id: i64 = row.get(0)?; 34 + let role_name: String = row.get(1)?; 35 + Ok((RoleId::new(role_id as u64), role_name)) 36 + }) 37 + .ok() 38 + } 39 + 40 + fn save_user_role(user_id: UserId, guild_id: u64, role_id: RoleId, role_name: &str) -> Result<()> { 41 + let conn = COLOR_DB.lock().unwrap(); 42 + conn.execute( 43 + "INSERT OR REPLACE INTO color_roles (user_id, guild_id, role_id, role_name) VALUES (?, ?, ?, ?)", 44 + params![user_id.get() as i64, guild_id as i64, role_id.get() as i64, role_name], 45 + )?; 46 + Ok(()) 47 + } 48 + 49 + fn delete_user_role(user_id: UserId, guild_id: u64) -> Result<()> { 50 + let conn = COLOR_DB.lock().unwrap(); 51 + conn.execute( 52 + "DELETE FROM color_roles WHERE user_id = ? AND guild_id = ?", 53 + params![user_id.get() as i64, guild_id as i64], 54 + )?; 55 + Ok(()) 56 + } 57 + 58 + fn update_role_name(user_id: UserId, guild_id: u64, new_name: &str) -> Result<()> { 59 + let conn = COLOR_DB.lock().unwrap(); 60 + conn.execute( 61 + "UPDATE color_roles SET role_name = ? WHERE user_id = ? AND guild_id = ?", 62 + params![new_name, user_id.get() as i64, guild_id as i64], 63 + )?; 64 + Ok(()) 65 + } 5 66 6 67 /// Change your display color or remove your color role. 7 68 #[poise::command(slash_command)] 8 69 pub async fn color_me( 9 70 ctx: Context<'_>, 10 71 #[description = "Hex color code (e.g., #FF5733 or FF5733)"] color: Option<String>, 72 + #[description = "Custom name for your role (optional)"] role_name: Option<String>, 11 73 ) -> Result<()> { 12 74 let Some(guild_id) = ctx.guild_id() else { 13 75 ctx.send( ··· 22 84 let member = guild_id.member(ctx.http(), ctx.author().id).await?; 23 85 let username = &ctx.author().name; 24 86 25 - // lookup if user already has a role with their username 26 - let existing_role = guild_id 87 + // check for existing role 88 + let db_role = get_user_role(ctx.author().id, guild_id.get()); 89 + 90 + // they might not be in the db so lookup if there's a role with their username (previous behavior) 91 + let username_role = guild_id 27 92 .roles(ctx.http()) 28 93 .await? 29 94 .into_iter() 30 95 .find(|(_, role)| role.name == *username); 31 96 97 + let existing_role = if let Some((role_id, stored_name)) = db_role { 98 + if guild_id.roles(ctx.http()).await?.contains_key(&role_id) { 99 + Some((role_id, stored_name)) 100 + } else { 101 + // role was deleted from discord, clean up database 102 + delete_user_role(ctx.author().id, guild_id.get())?; 103 + None 104 + } 105 + } else if let Some((role_id, role)) = username_role { 106 + // migrate username-based role to database 107 + save_user_role(ctx.author().id, guild_id.get(), role_id, &role.name)?; 108 + Some((role_id, role.name.clone())) 109 + } else { 110 + None 111 + }; 112 + 32 113 match color { 33 114 Some(color_str) => { 34 115 // parse the hex color ··· 44 125 }; 45 126 46 127 let colour_picked = Colour::new(color_value); 128 + let desired_role_name = role_name.as_deref().unwrap_or(username); 47 129 48 - if let Some((role_id, _)) = existing_role { 49 - // update existing role 130 + if let Some((role_id, current_name)) = existing_role { 50 131 guild_id 51 132 .edit_role(ctx.http(), role_id, EditRole::new().colour(colour_picked.0)) 52 133 .await?; 53 - ctx.send( 54 - CreateReply::default() 55 - .content(format!("Updated your color to `#{color_str}`! <3")) 56 - .ephemeral(true), 57 - ) 58 - .await?; 134 + 135 + if let Some(new_name) = &role_name { 136 + if new_name != &current_name { 137 + guild_id 138 + .edit_role(ctx.http(), role_id, EditRole::new().name(new_name)) 139 + .await?; 140 + update_role_name(ctx.author().id, guild_id.get(), new_name)?; 141 + 142 + ctx.send( 143 + CreateReply::default() 144 + .content(format!("Updated your color to `#{color_str}` and renamed your role to `{new_name}`! <3")) 145 + .ephemeral(true), 146 + ) 147 + .await?; 148 + } else { 149 + ctx.send( 150 + CreateReply::default() 151 + .content(format!("Updated your color to `#{color_str}`! <3")) 152 + .ephemeral(true), 153 + ) 154 + .await?; 155 + } 156 + } else { 157 + ctx.send( 158 + CreateReply::default() 159 + .content(format!("Updated your color to `#{color_str}`! <3")) 160 + .ephemeral(true), 161 + ) 162 + .await?; 163 + } 59 164 } else { 60 - // or else create new role 61 165 let new_role = guild_id 62 166 .create_role( 63 167 ctx.http(), 64 168 EditRole::new() 65 - .name(username) 169 + .name(desired_role_name) 66 170 .colour(colour_picked.0) 67 171 .hoist(false) 68 172 .mentionable(false), ··· 70 174 .await?; 71 175 72 176 member.add_role(ctx.http(), new_role.id).await?; 177 + save_user_role( 178 + ctx.author().id, 179 + guild_id.get(), 180 + new_role.id, 181 + desired_role_name, 182 + )?; 73 183 74 184 ctx.send( 75 185 CreateReply::default() 76 186 .content(format!( 77 - "Created a new role and set your color to `#{color_str}`! <3" 187 + "Created a new role `{desired_role_name}` and set your color to `#{color_str}`! <3" 78 188 )) 79 189 .ephemeral(true), 80 190 ) ··· 82 192 } 83 193 } 84 194 None => { 85 - // remove the role if it exists 86 - match existing_role { 87 - Some((role_id, _)) => { 88 - member.remove_role(ctx.http(), role_id).await?; 89 - guild_id.delete_role(ctx.http(), role_id).await?; 195 + if let Some(new_name) = role_name { 196 + if let Some((role_id, current_name)) = existing_role { 197 + if new_name != current_name { 198 + guild_id 199 + .edit_role(ctx.http(), role_id, EditRole::new().name(&new_name)) 200 + .await?; 201 + update_role_name(ctx.author().id, guild_id.get(), &new_name)?; 90 202 203 + ctx.send( 204 + CreateReply::default() 205 + .content(format!("Renamed your role to `{new_name}`! <3")) 206 + .ephemeral(true), 207 + ) 208 + .await?; 209 + } else { 210 + ctx.send( 211 + CreateReply::default() 212 + .content("Your role already has that name!") 213 + .ephemeral(true), 214 + ) 215 + .await?; 216 + } 217 + } else { 91 218 ctx.send( 92 219 CreateReply::default() 93 - .content("Removed your color role.") 220 + .content("You don't have a color role yet! Use `/color_me` with a color to create one.") 94 221 .ephemeral(true), 95 222 ) 96 223 .await?; 97 224 } 98 - None => { 99 - ctx.send( 100 - CreateReply::default() 101 - .content("You don't have a color role to remove.") 102 - .ephemeral(true), 103 - ) 104 - .await?; 225 + } else { 226 + match existing_role { 227 + Some((role_id, _)) => { 228 + member.remove_role(ctx.http(), role_id).await?; 229 + guild_id.delete_role(ctx.http(), role_id).await?; 230 + delete_user_role(ctx.author().id, guild_id.get())?; 231 + 232 + ctx.send( 233 + CreateReply::default() 234 + .content("Removed your color role.") 235 + .ephemeral(true), 236 + ) 237 + .await?; 238 + } 239 + None => { 240 + ctx.send( 241 + CreateReply::default() 242 + .content("You don't have a color role to remove.") 243 + .ephemeral(true), 244 + ) 245 + .await?; 246 + } 105 247 } 106 248 } 107 249 }
+1 -1
src/commands/user/whois.rs
··· 1 1 use crate::types::Context; 2 2 use color_eyre::eyre::Result; 3 3 use poise::{ 4 - serenity_prelude::{CreateEmbed, User}, 5 4 CreateReply, 5 + serenity_prelude::{CreateEmbed, User}, 6 6 }; 7 7 8 8 /// Displays your or another user's info
+47 -1
src/main.rs
··· 6 6 use std::{env, path::Path}; 7 7 8 8 use color_eyre::eyre::Result; 9 - use poise::serenity_prelude::{ActivityData, ClientBuilder, GatewayIntents}; 9 + use poise::serenity_prelude::{ 10 + ActivityData, ChannelId, ClientBuilder, CreateAttachment, CreateMessage, GatewayIntents, 11 + }; 10 12 use sha2::{Digest, Sha256}; 11 13 12 14 #[derive(Debug)] ··· 418 420 if let Err(e) = ensure_nixpkgs_database().await { 419 421 eprintln!("Failed to update nixpkgs database: {e}"); 420 422 } 423 + } 424 + }); 425 + 426 + let ctx_clone = ctx.clone(); 427 + tokio::spawn(async move { 428 + loop { 429 + let now = chrono::Utc::now(); 430 + let target_time = now 431 + .date_naive() 432 + .and_hms_opt(11, 0, 0) 433 + .expect("Invalid time"); 434 + let mut target_datetime = 435 + chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset( 436 + target_time, 437 + chrono::Utc, 438 + ); 439 + 440 + if now.time() 441 + >= chrono::NaiveTime::from_hms_opt(11, 0, 0).expect("Invalid time") 442 + { 443 + target_datetime += chrono::Duration::days(1); 444 + } 445 + 446 + let duration_until_target = (target_datetime - now) 447 + .to_std() 448 + .unwrap_or(std::time::Duration::from_secs(0)); 449 + tokio::time::sleep(duration_until_target).await; 450 + 451 + let channel_id = ChannelId::new(1095083877380395202); 452 + let attachment = CreateAttachment::path("assets/idk.webp").await; 453 + 454 + match attachment { 455 + Ok(file) => { 456 + let builder = CreateMessage::new().add_file(file); 457 + if let Err(e) = channel_id.send_message(&ctx_clone, builder).await { 458 + eprintln!("Failed to send daily idk.webp: {e}"); 459 + } 460 + } 461 + Err(e) => { 462 + eprintln!("Failed to load assets/idk.webp: {e}"); 463 + } 464 + } 465 + 466 + tokio::time::sleep(std::time::Duration::from_secs(60)).await; 421 467 } 422 468 }); 423 469