FrenBot/src/discord/joke.rs

383 lines
11 KiB
Rust

use crate::BAD_APPLE;
use crate::album_manager::{Album, AlbumQuery, ImageQuery, ImageSort};
use crate::config::GlobalData;
use crate::discord::Context;
use crate::error::Error;
use crate::models::insult_compliment::{RandomResponseTemplate, ResponseType};
use crate::models::random::{Random, RandomConfig};
use chrono::Utc;
use emojis::Group;
use log::debug;
use poise::serenity_prelude::all::{CreateAttachment, CreateMessage};
use poise::serenity_prelude::constants::MESSAGE_CODE_LIMIT;
use poise::serenity_prelude::model::channel::Message;
use poise::serenity_prelude::utils::MessageBuilder;
use poise::{CreateReply, serenity_prelude};
use raas_types::raas::bot::roll::{Roll, RollCmd, roll_response};
use raas_types::raas::resp::response::Resp;
use raas_types::raas::service::raas_client::RaasClient;
use rand::prelude::IndexedRandom;
use rand::rng;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Clone, Serialize, Deserialize)]
struct DadJoke {
pub id: String,
pub joke: String,
pub status: i32,
}
/// Ask your dad
#[poise::command(prefix_command, category = "Joke")]
pub async fn dad_joke(ctx: Context<'_>) -> Result<(), Error> {
let client = Client::new();
let joke: DadJoke = client
.get("https://icanhazdadjoke.com/")
.header("Accept", "application/json")
.send()
.await?
.json()
.await?;
ctx.reply(joke.joke).await?;
Ok(())
}
#[derive(Serialize, Deserialize)]
struct RandomCtx {
user: String,
random_image: HashMap<String, String>,
}
impl RandomCtx {
pub async fn new(user_name: &str, global_data: &GlobalData) -> Result<Self, Error> {
let mut random_image: HashMap<String, String> = HashMap::new();
let albums: Vec<Album> = global_data
.picox
.query_album(AlbumQuery { album_name: None })
.await?;
for album in albums {
let images = global_data
.picox
.query_image(ImageQuery {
album: Some(album.album_name.clone()),
tags: vec![],
order: ImageSort::Random,
limit: 1,
})
.await?;
let image = images.first();
if let Some(image) = image {
random_image.insert(album.album_name.clone(), image.link.to_string());
}
}
Ok(Self {
user: user_name.to_string(),
random_image,
})
}
}
pub async fn render_random(
author_name: &str,
global_data: &GlobalData,
template: &str,
) -> Result<String, Error> {
let random_ctx = RandomCtx::new(author_name, global_data).await?;
Ok(tera::Tera::one_off(
template,
&tera::Context::from_serialize(random_ctx)?,
false,
)?)
}
pub async fn random(
ctx: &serenity_prelude::Context,
global_data: &GlobalData,
msg: &Message,
random_name: &str,
) -> Result<(), Error> {
let random = match RandomConfig::get_random(&global_data.db, random_name)? {
None => return Ok(()),
Some(r) => r,
};
let response_template_str = match random.get_response(&global_data.db)? {
None => {
msg.reply(&ctx, format!("I'm all out of material for {}", random_name))
.await?;
return Ok(());
}
Some(resp) => resp,
};
let guild_member = msg.guild_id.unwrap().member(&ctx, msg.author.id).await?;
let reply = render_random(
guild_member.display_name(),
global_data,
&response_template_str,
)
.await?;
msg.reply(&ctx.http, reply).await?;
Ok(())
}
/// Add a random command to the bot
#[poise::command(prefix_command, category = "Joke")]
pub async fn add_random(
ctx: Context<'_>,
#[description = "Random collection to add to"] random_name: String,
#[description = "Random message"] #[rest] random_response: String,
) -> Result<(), Error> {
if let Err(err) = render_random(ctx.author().display_name(), ctx.data(), &random_response).await
{
ctx.reply(format!("Template failed test render, try again: {}", err))
.await?;
} else {
RandomConfig::add_random(&ctx.data().db, &random_name, &random_response)?;
ctx.reply("New random added!").await?;
}
Ok(())
}
/// List all of the random sets
#[poise::command(prefix_command, category = "Joke")]
pub async fn list_random(
ctx: Context<'_>,
#[description = "Random collection to list"] random_name: String,
) -> Result<(), Error> {
let random = RandomConfig::get_random(&ctx.data().db, &random_name)?;
let dm_channel = ctx.author().id.create_dm_channel(&ctx).await?;
if let Some(random) = random {
let mut msg_builder = MessageBuilder::new();
msg_builder.push_line(format!(
"All possible responses for {} all {} of them..:",
random_name,
random.responses.len()
));
for resp in random.responses {
let random = ctx.data().db.get::<Random>(resp)?;
let line_msg = format!("* ({}) ({}) {}", resp, random.score, random.response);
if (msg_builder.0.len() + line_msg.len()) > MESSAGE_CODE_LIMIT {
dm_channel.say(&ctx, msg_builder.build()).await?;
msg_builder.0.clear();
}
msg_builder.push_line(line_msg);
}
if !msg_builder.0.is_empty() {
dm_channel.say(&ctx, msg_builder.build()).await?;
}
} else {
ctx.reply("*glances to the back room*, nope we ain't got that random")
.await?;
}
Ok(())
}
/// Roll a totally not loaded digital die!
#[poise::command(prefix_command, category = "Joke")]
pub async fn roll(
ctx: Context<'_>,
#[description = "die to roll"]
#[rest]
die_roll: String,
) -> Result<(), Error> {
let roll = die_roll.parse::<ndm::Dice>();
let reply = match roll {
Ok(roll) => format!("You rolled: **{}**", roll),
Err(err) => format!("Error parsing dice roll: {}", err),
};
ctx.reply(reply).await?;
Ok(())
}
/// Roll a totally not loaded, real actual die! (No joke)
#[poise::command(prefix_command, category = "Joke")]
pub async fn real_roll(ctx: Context<'_>) -> Result<(), Error> {
let addr = ctx.data().cfg.raas_server.clone();
let mut client = RaasClient::connect(addr)
.await
.map_err(|err| Error::RaasError(err.to_string()))?;
let rolls = 3;
let roll_request = raas_types::raas::cmd::Request {
timestamp: Utc::now().timestamp() as u64,
cmd: Some(raas_types::raas::cmd::request::Cmd::RollCmd(RollCmd {
cmd: Some(raas_types::raas::bot::roll::roll_cmd::Cmd::Roll(Roll {
rotations: rolls,
})),
})),
};
ctx.reply("Sent roll request, please hang on!").await?;
let response = client
.send_request(roll_request)
.await
.map_err(|err| Error::RaasError(err.to_string()))?;
let raas_response = response.into_inner();
debug!(
"Got resp from RaaS: {} @ {}",
raas_response.id, raas_response.timestamp
);
match raas_response.resp.unwrap() {
Resp::RollResp(roll_resp) => {
if let roll_response::Response::RollImage(img) = roll_resp.response.unwrap() {
ctx.channel_id()
.send_message(
&ctx,
CreateMessage::new()
.content("Your roll my friend, hope its good I can't read!")
.add_file(CreateAttachment::bytes(img.img, "roll.jpg")),
)
.await?;
}
}
Resp::Error(err) => {
ctx.reply(format!(
"My real flesh encountered an error. Get Dad to fix it. `{}`",
err.msg
))
.await?;
}
}
Ok(())
}
/// Play critically the acclaimed Bad Apple music video as god intended
#[poise::command(prefix_command, category = "Joke")]
pub async fn bad_apple(ctx: Context<'_>) -> Result<(), Error> {
let mut frames = BAD_APPLE.split('|');
let first_frame = frames.next().unwrap();
if first_frame.len() > 2000 {
ctx.reply(format!("Frame too big: {}", first_frame.len()))
.await?;
return Ok(());
}
let bad_apple_msg = ctx
.reply(
MessageBuilder::default()
.push_codeblock_safe(first_frame, None)
.build(),
)
.await?;
for (idx, frame) in frames.enumerate() {
tokio::time::sleep(Duration::from_millis(100)).await;
if (idx % 10) == 0 {
bad_apple_msg
.edit(
ctx,
CreateReply::default().content(
MessageBuilder::default()
.push_codeblock_safe(frame, None)
.build(),
),
)
.await?;
}
}
Ok(())
}
/// Insult or compliment your "friends"!
#[poise::command(prefix_command, category = "Joke", aliases("compliment"))]
pub async fn insult(
ctx: Context<'_>,
#[description = "person to compliment/insult"] target: String,
) -> Result<(), Error> {
let pool = if ctx.invoked_command_name() == "insult" {
ResponseType::Insult
} else if ctx.invoked_command_name() == "compliment" {
ResponseType::Compliment
} else {
ctx.reply("The h*ck did you just say to me??").await?;
return Ok(());
};
let output = RandomResponseTemplate::get_random_response(&ctx.data().db, pool, &target)?;
if let Some(output) = output {
ctx.reply(output).await?;
} else {
ctx.reply(format!("No {}s, mr freeman??", target)).await?;
}
Ok(())
}
fn get_unicode_emojis() -> Vec<String> {
let groups = [
Group::SmileysAndEmotion,
Group::Activities,
Group::Objects,
Group::AnimalsAndNature,
Group::PeopleAndBody,
Group::FoodAndDrink,
];
let mut emoji = Vec::new();
for group in groups {
emoji.extend(group.emojis())
}
emoji.iter().map(|e| e.to_string()).collect()
}
/// Stare into the digital black mirror and see what it reflects
#[poise::command(prefix_command, category = "Joke", aliases("🎱"))]
pub async fn emoji_8ball(
ctx: Context<'_>,
#[description = "prompt to dissect using magic"]
#[rest]
_prompt: String,
) -> Result<(), Error> {
let guild_emojis = ctx.guild_id().unwrap().emojis(&ctx).await?;
let mut emojis: Vec<String> = guild_emojis.iter().map(|e| e.to_string()).collect();
let mut unicode_emojis = get_unicode_emojis();
emojis.append(&mut unicode_emojis);
let emoji_msg: Vec<String> = emojis.choose_multiple(&mut rng(), 3).cloned().collect();
ctx.reply(emoji_msg.join("")).await?;
Ok(())
}