448 lines
14 KiB
Rust
448 lines
14 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 stonks;
|
|
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::event_listener::{Listener, TriggerEvent, TriggerType};
|
|
use crate::models::social_credit::SocialCreditPhrase;
|
|
use crate::models::task::Task;
|
|
use crate::user::User;
|
|
use log::{debug, 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;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
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 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)?;
|
|
}
|
|
|
|
if !new_message.content.starts_with("!") {
|
|
let trigger_event = TriggerEvent::new(
|
|
new_message.author.id,
|
|
TriggerType::OnMessage {
|
|
channel_id: Some(new_message.channel_id),
|
|
content: Some(new_message.content.clone()),
|
|
},
|
|
new_message.id,
|
|
new_message.channel_id,
|
|
);
|
|
|
|
match Listener::process_trigger(ctx, data, trigger_event).await {
|
|
Ok(_) => {
|
|
debug!("Processed message trigger successfully")
|
|
}
|
|
Err(err) => {
|
|
error!("Failed to process message trigger: {err}")
|
|
}
|
|
}
|
|
}
|
|
|
|
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(),
|
|
admin::reset_random_score(),
|
|
admin::remove_random_result(),
|
|
admin::op_give_money(),
|
|
admin::list_listeners(),
|
|
admin::remove_listener(),
|
|
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(),
|
|
joke::list_randoms(),
|
|
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::cancel_insurance(),
|
|
shop::the_concept_of_love(),
|
|
shop::good_fortune(),
|
|
shop::nft(),
|
|
shop::license_to_be_horny(),
|
|
shop::kill_gun(),
|
|
shop::cancel_ray(),
|
|
shop::helmet(),
|
|
shop::emp(),
|
|
shop::land_mine(),
|
|
shop::phrase_canceler(),
|
|
shop::nuke(),
|
|
stonks::contribute_to_gogurt_reserve(),
|
|
stonks::sell_from_gogurt_reserve(),
|
|
stonks::gogurt_reserve_stats(),
|
|
stonks::gogurt_reserve_contribution(),
|
|
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_MEMBERS
|
|
| serenity::GatewayIntents::GUILD_PRESENCES
|
|
| 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())
|
|
}
|