mod admin; mod album; mod birthday; mod celeryman; mod color; mod emoji_race; mod fren_coin; mod joke; mod little_fren; mod motivate; mod role; pub(crate) mod shop; mod transit; pub(crate) mod voices; use crate::config::GlobalData; use crate::discord::fren_coin::give_coin; use crate::discord::joke::random; use crate::error::Error; use crate::models::lil_fren::lil_fren_task; use crate::models::social_credit::SocialCreditPhrase; use crate::models::task::Task; use crate::user::User; use log::{error, info}; use poise::serenity_prelude::{GuildId, Http, Message, MessageBuilder, ReactionType, RoleId}; use poise::{FrameworkOptions, find_command, serenity_prelude as serenity}; use rand::prelude::IteratorRandom; use rand::{Rng, rng}; use songbird::SerenityInit; use std::sync::Arc; use std::time::Duration; pub type Context<'a> = poise::Context<'a, Arc, Error>; async fn event_handler( ctx: &serenity::Context, event: &serenity::FullEvent, framework: poise::FrameworkContext<'_, Arc, Error>, data: &Arc, ) -> Result<(), Error> { match event { serenity::FullEvent::Ready { data_about_bot, .. } => { info!("Bot ready, and logged in as {}", data_about_bot.user.name); { info!("Starting tasks handler..."); let data = data.clone(); let ctx = ctx.clone(); tokio::spawn(async move { let _ = Task::create_reoccurring_tasks(&data).await.is_ok(); loop { let _ = Task::run_tasks(&ctx, &data).await.is_ok(); tokio::time::sleep(Duration::from_secs(5)).await; } }); } { info!("Starting lil buddy task..."); let data = data.clone(); tokio::spawn(async move { lil_fren_task(data).await }); } } serenity::FullEvent::Message { new_message } => { if new_message.content.starts_with("!") { let command = new_message.content.replace("!", ""); if find_command(&framework.options.commands, &command, true, &mut Vec::new()) .is_none() { handle_unrecognised_commands(ctx, new_message, data).await?; } } handle_message(ctx, data, new_message).await?; } _ => {} } Ok(()) } async fn handle_message( ctx: &serenity::Context, data: &Arc, new_message: &Message, ) -> Result<(), Error> { if new_message.content.eq_ignore_ascii_case("yes") || new_message.content.eq_ignore_ascii_case("mhmm") { let mut bot_state = data.bot_state.lock().await; if let Some(u) = bot_state.accepted_nsfw { if new_message.author.id == u { new_message.reply(&ctx.http, "||https://cdn.discordapp.com/attachments/614891432079130625/1041545254362423368/unknown.png||").await.unwrap(); bot_state.accepted_nsfw = None; } } } if new_message.content.to_lowercase().contains("good bot") { let recv_coin = give_coin(&data.db, new_message.author.id, 0.50, 25).await?; if recv_coin { let emojis = &new_message.guild_id.unwrap().emojis(&ctx.http).await?; let emoji = { let mut rng = rng(); emojis.iter().choose(&mut rng) }; if let Some(emoji) = emoji { new_message .react( &ctx.http, ReactionType::Custom { animated: emoji.animated, id: emoji.id, name: Some(emoji.name.clone()), }, ) .await?; }; } }; if new_message.content.to_lowercase().contains("bad bot") { new_message.react(&ctx.http, '😭').await?; } give_coin(&data.db, new_message.author.id, 0.05, 10).await?; if let Some(phrase) = SocialCreditPhrase::check_if_match(&data.db, &new_message.content)? { info!( "{} matched phrase '{}' for social credit checking", new_message.author.name, phrase.phrase ); let social_credit_change = rand::rng().random_range(phrase.lower_bound..=phrase.upper_bound); User::update_social_credit(&data.db, new_message.author.id, social_credit_change)?; } Ok(()) } async fn handle_unrecognised_commands( ctx: &serenity::Context, message: &Message, data: &GlobalData, ) -> Result<(), Error> { let command_parts: Vec<&str> = message.content.split(" ").collect(); let command = command_parts[0].replace("!", ""); let tags = if command_parts.len() > 1 { command_parts[1..].to_vec() } else { Vec::new() }; let parsed_album = album::parse_album(ctx, message, data, &command, tags) .await .unwrap_or_else(|e| { error!("Error processing album command: {}", e); true }); if !parsed_album { match random(ctx, data, message, &command).await { Ok(_) => {} Err(e) => error!("Error processing random command: {}", e), } } Ok(()) } async fn pre_command(ctx: Context<'_>) { info!( "User '{}' is running command '{}' with invocation '{}' in channel '{}'", ctx.author().name, ctx.invoked_command_name(), ctx.invocation_string(), ctx.channel_id() ); } #[poise::command(prefix_command, category = "Help")] pub async fn help(ctx: Context<'_>, command: Option) -> Result<(), Error> { if let Some(command) = command { let command = find_command( &ctx.framework().options.commands, &command, true, &mut Vec::new(), ); if let Some((command, _, _)) = command { let mut msg_builder = MessageBuilder::new(); msg_builder.push_line(format!("# {}", &command.name)); msg_builder.push_line(command.description.as_ref().unwrap_or(&"".to_string())); let parameters = if !command.parameters.is_empty() { msg_builder.push_line("### Arguments:"); for parameter in &command.parameters { msg_builder.push_line(format!( "* `{}`: {}", parameter.name, parameter.description.as_ref().unwrap_or(&"".to_string()) )); } let parameters: Vec = command .parameters .iter() .map(|parameter| parameter.name.clone()) .collect(); let mut parameters_str = parameters.join(" "); parameters_str.insert(0, ' '); parameters_str } else { "".to_string() }; msg_builder.push_line("### Usage:"); msg_builder.push_line(format!("`!{}{}`", &command.name, parameters)); ctx.reply(msg_builder.build()).await?; } else { ctx.reply("tbh, no idea what that is. Cringe TBH").await?; } } else { let mut commands: Vec<(String, String)> = ctx .framework() .options .commands .iter() .map(|c| { ( c.category.as_ref().unwrap_or(&"None".to_string()).clone(), c.name.clone(), ) }) .collect(); if !ctx.data().cfg.admins.contains(&ctx.author().id) { // Remove admin commands for lame normal users commands.retain_mut(|(category, _)| !category.eq_ignore_ascii_case("Admin")) } commands.sort_by(|(category1, _), (category2, _)| category1.cmp(category2)); let mut msg_builder = MessageBuilder::new(); msg_builder.push_line("# Fren Bot"); msg_builder.push_line_safe("Your best friend in this Discord!"); msg_builder.push_line(""); msg_builder.push_italic_line( "Do `!help ` to get more information on any of the commands below.", ); let mut last_category = "".to_string(); for (category, command) in &commands { if !last_category.eq_ignore_ascii_case(category) { last_category = category.clone(); msg_builder.push_line(format!("### {}", last_category)); } msg_builder.push_line(format!("* `!{}`", command)); } msg_builder.push_line("-# Made with :sparkling_heart: by Joey"); ctx.reply(msg_builder.build()).await?; } Ok(()) } pub async fn run_bot(global_data: GlobalData) { let framework_options: FrameworkOptions, Error> = poise::FrameworkOptions { prefix_options: poise::PrefixFrameworkOptions { prefix: Some("!".into()), ignore_bots: true, ignore_thread_creation: false, case_insensitive_commands: true, ..Default::default() }, commands: vec![ help(), admin::dump_db(), admin::load_db(), admin::add_key(), admin::debug_buddy(), admin::draw_buddy_states(), admin::debug_buddy(), admin::op_give(), admin::list_tasks(), admin::add_role(), admin::remove_role(), admin::add_social_credit_phrase(), admin::remove_social_credit_phrase(), admin::list_social_credit_phrases(), album::add_image(), album::list_albums(), birthday::add_birthday(), birthday::list_birthdays(), celeryman::nudetayne(), celeryman::celeryman(), celeryman::tayne(), color::set_color(), color::remove_color(), emoji_race::bet(), emoji_race::race(), emoji_race::start_race(), fren_coin::balance(), fren_coin::gift(), fren_coin::social_credit(), joke::add_random(), joke::dad_joke(), joke::bad_apple(), joke::emoji_8ball(), joke::insult(), joke::list_random(), joke::real_roll(), joke::roll(), little_fren::adopt(), little_fren::checkup(), little_fren::feed(), little_fren::give_medicine(), little_fren::give_water(), little_fren::play(), motivate::motivation(), motivate::motivation_add_album(), motivate::green_screen(), role::list_roles(), role::join_role(), role::leave_role(), shop::buy(), shop::inventory(), shop::item_help(), shop::sell_item(), shop::shop(), shop::use_item(), transit::cta_bets(), voices::list_voices(), voices::list_words(), voices::say(), ], event_handler: |ctx, event, framework, data| { Box::pin(event_handler(ctx, event, framework, data)) }, pre_command: |ctx| Box::pin(pre_command(ctx)), on_error: |err| { Box::pin(async move { if let Err(e) = poise::builtins::on_error(err).await { error!("Failed to handle error: {}", e) } }) }, ..Default::default() }; let token = global_data.cfg.bot_token.clone(); let framework = poise::framework::Framework::builder() .options(framework_options) .setup(move |ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Arc::new(global_data)) }) }) .build(); let intents = serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::DIRECT_MESSAGES | serenity::GatewayIntents::GUILDS | serenity::GatewayIntents::MESSAGE_CONTENT | serenity::GatewayIntents::GUILD_MESSAGE_REACTIONS; let mut client = serenity::ClientBuilder::new(token, intents) .framework(framework) .register_songbird() .await .unwrap(); client.start().await.unwrap(); } pub async fn get_role( http: &Http, guild_id: GuildId, role_name: &str, ) -> Result, Error> { Ok(guild_id .roles(http) .await? .iter() .find(|(_, role)| role.name == role_name) .map(|(role_id, _)| role_id) .copied()) }