Joey Hines dfffe202df
Added edit_img command
+ Refactored image editing so it's all handled in the same way
+ Added pipeline concept for more advanced edits
2025-07-06 20:38:09 -06:00

415 lines
13 KiB
Rust

mod admin;
mod album;
mod birthday;
mod celeryman;
mod color;
mod emoji_race;
mod fren_coin;
mod image;
mod joke;
mod little_fren;
mod movie;
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<GlobalData>, Error>;
async fn event_handler(
ctx: &serenity::Context,
event: &serenity::FullEvent,
framework: poise::FrameworkContext<'_, Arc<GlobalData>, Error>,
data: &Arc<GlobalData>,
) -> 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<GlobalData>,
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<String>) -> 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<String> = 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 <command>` 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<Arc<GlobalData>, 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(),
admin::remove_movie(),
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(),
image::motivation(),
image::motivation_add_album(),
image::green_screen(),
image::overlay(),
image::edit_img(),
movie::add_movie(),
movie::list_movies(),
movie::rate_movie(),
movie::movie(),
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<Option<RoleId>, Error> {
Ok(guild_id
.roles(http)
.await?
.iter()
.find(|(_, role)| role.name == role_name)
.map(|(role_id, _)| role_id)
.copied())
}