summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/guzzle.rs8
-rw-r--r--src/commands/ask.rs4
-rw-r--r--src/commands/bottom.rs22
-rw-r--r--src/commands/convert.rs22
-rw-r--r--src/commands/copypasta.rs2
-rw-r--r--src/commands/random_lore.rs4
-rw-r--r--src/consts.rs8
-rw-r--r--src/main.rs163
-rw-r--r--src/pinboard.rs109
-rw-r--r--src/utils.rs88
10 files changed, 293 insertions, 137 deletions
diff --git a/src/api/guzzle.rs b/src/api/guzzle.rs
index e94ebb6..0321127 100644
--- a/src/api/guzzle.rs
+++ b/src/api/guzzle.rs
@@ -9,10 +9,10 @@ struct GuzzleResponse {
const GUZZLE: &str = "https://api.mydadleft.me";
pub async fn get_random_teawie() -> String {
- let endpoint = "/get_random_teawie";
- let resp = reqwest::get(GUZZLE.to_owned() + endpoint).await.unwrap(); // why did i have to own
- // this constant? i have
- // no idea!
+ let endpoint = "get_random_teawie";
+ let resp = reqwest::get(format!("{GUZZLE}/{endpoint}")).await.unwrap(); // why did i have to own
+ // this constant? i have
+ // no idea!
let err_msg = "couldn't get a teawie";
match resp.status() {
diff --git a/src/commands/ask.rs b/src/commands/ask.rs
index 6188f43..b0b24f3 100644
--- a/src/commands/ask.rs
+++ b/src/commands/ask.rs
@@ -3,8 +3,8 @@ use serenity::builder::CreateApplicationCommand;
use serenity::model::prelude::command::CommandOptionType;
use serenity::model::prelude::interaction::application_command::CommandDataOption;
-pub async fn run(_: &[CommandDataOption]) -> String {
- utils::get_random_response().await
+pub fn run(_: &[CommandDataOption]) -> String {
+ utils::get_random_response()
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
diff --git a/src/commands/bottom.rs b/src/commands/bottom.rs
index d25eab1..dbe74b9 100644
--- a/src/commands/bottom.rs
+++ b/src/commands/bottom.rs
@@ -5,9 +5,8 @@ use serenity::model::prelude::interaction::application_command::{
CommandDataOption, CommandDataOptionValue,
};
-pub async fn run(options: &[CommandDataOption]) -> String {
+pub fn run(options: &[CommandDataOption]) -> String {
let err = "failed to get nested option in";
- let mut ret = "did you forget to enter a message?".to_string();
let data = options
.get(0)
@@ -16,6 +15,7 @@ pub async fn run(options: &[CommandDataOption]) -> String {
// get subcommand to decide whether to encode/decode
let subcommand = data.name.as_str();
+ // TODO: this is horrendous
// get message content
let option = data
.options
@@ -27,19 +27,13 @@ pub async fn run(options: &[CommandDataOption]) -> String {
if let CommandDataOptionValue::String(msg) = option {
match subcommand {
- "encode" => {
- ret = bottom_encode(msg).await;
- }
- "decode" => {
- ret = bottom_decode(msg).await;
- }
- _ => {
- ret = "something went wrong :(".to_string();
- }
- };
+ "encode" => bottom_encode(msg),
+ "decode" => bottom_decode(msg),
+ _ => "something went wrong :(".to_owned(),
+ }
+ } else {
+ "did you forget to enter a message?".to_owned()
}
-
- ret
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
diff --git a/src/commands/convert.rs b/src/commands/convert.rs
index 6fe141d..8f8c424 100644
--- a/src/commands/convert.rs
+++ b/src/commands/convert.rs
@@ -5,7 +5,7 @@ use serenity::model::prelude::interaction::application_command::{
CommandDataOption, CommandDataOptionValue,
};
-pub async fn run(options: &[CommandDataOption]) -> String {
+pub fn run(options: &[CommandDataOption]) -> String {
let err = "couldn't get convert subcommand!";
let data = options
.get(0)
@@ -20,19 +20,21 @@ pub async fn run(options: &[CommandDataOption]) -> String {
.as_ref()
.expect("failed to resolve string!");
- let mut ret = 0.0;
- if let CommandDataOptionValue::Number(number) = option {
+ let temp = if let &CommandDataOptionValue::Number(number) = option {
match subcommand {
- "fahrenheit" => ret = utils::celsius_to_fahrenheit(number).await,
- "celsius" => ret = utils::fahrenheit_to_celsius(number).await,
- _ => ret = 0.0,
- };
+ "fahrenheit" => Some(utils::celsius_to_fahrenheit(number)),
+ "celsius" => Some(utils::fahrenheit_to_celsius(number)),
+ _ => None,
+ }
+ } else {
+ None
};
- if ret == 0.0 {
- return "couldn't figure it out oops".to_string();
+ if let Some(temp) = temp {
+ format!("{temp:.2}")
+ } else {
+ "couldn't figure it out oops".to_owned()
}
- format!("{:.2}", ret)
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
diff --git a/src/commands/copypasta.rs b/src/commands/copypasta.rs
index ec3f3c6..670a5df 100644
--- a/src/commands/copypasta.rs
+++ b/src/commands/copypasta.rs
@@ -18,7 +18,7 @@ pub async fn run(options: &[CommandDataOption], channel_id: ChannelId, http: &Ar
.expect(err_msg);
if let CommandDataOptionValue::String(copypasta) = option {
- let replies = utils::get_copypasta(copypasta).await;
+ let replies = utils::get_copypasta(copypasta);
if replies.len() > 1 {
for reply in replies {
diff --git a/src/commands/random_lore.rs b/src/commands/random_lore.rs
index 345f753..b07660e 100644
--- a/src/commands/random_lore.rs
+++ b/src/commands/random_lore.rs
@@ -2,8 +2,8 @@ use crate::utils::get_random_lore;
use serenity::builder::CreateApplicationCommand;
use serenity::model::prelude::interaction::application_command::CommandDataOption;
-pub async fn run(_: &[CommandDataOption]) -> String {
- get_random_lore().await
+pub fn run(_: &[CommandDataOption]) -> String {
+ get_random_lore()
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
diff --git a/src/consts.rs b/src/consts.rs
index 5fce7c9..b108f34 100644
--- a/src/consts.rs
+++ b/src/consts.rs
@@ -43,22 +43,22 @@ pub const RESPONSES: [&str; 20] = [
pub const LORE: [&str; 22] = [
"Teawie is made of silly string and blood.",
"Teawie has the mentality of an eight year old, but no discernable age.",
- "Teawie can change size to fit into tight spaces. Although prefers to be small most of the time.",
+ "Teawie can change size to fit into tight spaces. Although he prefers to be small most of the time.",
"Teawie is blue flavored. The color specifically.",
"Teawie produces asexually via mitosis and in other cells.",
"Teawie cannot be physically damaged, and if put into a blender of some sort, will become stretchy.",
"Teawie has one tooth, but originally had a full set. These were lost due to him constantly eating stuff he probably shouldn't, such as computer hardware.",
"Teawie smells like cotton candy and fiberglass insulation.",
- "Teawie at a base \"knowledge level\", can only repeat his name. However this can be changed if Teawie inherets \"strings\" of information from a host.",
+ "Teawie at a base \"knowledge level\", can only repeat his name. However this can be changed if Teawie inherits \"strings\" of information from a host.",
"Teawie can \"infect\" other cells and turn them into Teawie.",
"Teawie has a gun, but the only bullets it has is smaller Teawies. They have the same attributes as Teawie, so they perform mitosis to keep the clip stocked.",
"The gun resembles the Ray Gun from the Call of Duty series. However this gun is blue rather than red.",
"Teawie \"bullets\" typically lodge into parts of a body and proceed to infect the victim. If a Teawie ends up lodged into the brain, Teawie will be inside the mind of the user, reigning pure terror until the victim passes on. Teawie will pilot the victim like a meat suit until decomposition fully occurs, and then find a new host.",
"A Teawie \"bullet\" will usually kill the host upon impact, However if the host survives, they will exhibit symptoms such as lightheadedness, fainting, fatigue, delusions, mental confusion and decline, disorientation, hallucinations and eventually death.",
"If a host possesses particular \"strings\" of information that a Teawie does not currently possess, Teawie will add those current strings to himself once the host expires. Essentially giving Teawie the host's memories and knowledge. This starts to debilitate overtime. Eventually ending right back where the Teawie started.",
- "Teawie is capable of infecting non-organic material such as a Personal Computer. If infecting something with quite a bit of power, Teawie will inheret \"strings\" from the object and become more powerful as a result.",
+ "Teawie is capable of infecting non-organic material such as a Personal Computer. If infecting something with quite a bit of power, Teawie will inherit \"strings\" from the object and become more powerful as a result.",
"Teawie himself isnt radioactive however some Teawies have a pretty bad habit of being in or around places with incredibly high radiation.",
- "Teawie has a Go-Kart that he will sometimes drive around in, however due to being mentally eight years old at a base \"knowledge level\", is not good at all with driving, however despite numerous crashes, ends up unscathed.",
+ "Teawie has a Go-Kart that he will sometimes drive around in, however due to being mentally eight years old at a base \"knowledge level\", he is not good at all with driving. Despite numerous crashes, he always ends up unscathed.",
"Teawie will \"insert\" himself into various media while trying to move from host to host.",
"Teawie is described as \"chewy\" in texture.",
"Teawie is friends with Blåhaj.",
diff --git a/src/main.rs b/src/main.rs
index 61a00c1..d9e1cd5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,30 +1,72 @@
-use lazy_static::lazy_static;
+use once_cell::sync::Lazy;
use regex::Regex;
use serenity::async_trait;
use serenity::framework::standard::macros::{command, group};
use serenity::framework::standard::{CommandResult, StandardFramework};
use serenity::model::application::command::Command;
-use serenity::model::application::interaction::{Interaction, InteractionResponseType};
-use serenity::model::channel::Message;
-use serenity::model::id::GuildId;
-use serenity::model::prelude::Ready;
+use serenity::model::prelude::*;
use serenity::prelude::*;
-use std::{env, vec};
+use utils::parse_snowflake_from_env;
+
+use crate::pinboard::PinBoard;
+use crate::utils::parse_snowflakes_from_env;
mod api;
mod commands;
mod consts;
+mod pinboard;
mod utils;
const TEAWIE_GUILD: GuildId = GuildId(1055663552679137310);
-const ALLOWED_GUILDS: [GuildId; 2] = [TEAWIE_GUILD, GuildId(1091969030694375444)];
-const BOT: u64 = 1056467120986271764;
+const BOT: UserId = UserId(1056467120986271764);
+
+fn is_guild_allowed(gid: GuildId) -> bool {
+ // Had to be global state because Serenity doesn't allow you to store
+ // extra state in frameworks
+ static ALLOWED_GUILDS: Lazy<Vec<GuildId>> = Lazy::new(|| {
+ parse_snowflakes_from_env("ALLOWED_GUILDS", GuildId)
+ .unwrap_or_else(|| vec![TEAWIE_GUILD, GuildId(1091969030694375444)])
+ });
+
+ ALLOWED_GUILDS.contains(&gid)
+}
#[group]
#[commands(bing, ask, random_lore, random_teawie, teawiespam)]
struct General;
-struct Handler;
+struct Handler {
+ bot: UserId,
+ pin_board: Option<PinBoard>,
+}
+
+impl Handler {
+ pub fn new() -> Self {
+ let bot = parse_snowflake_from_env("BOT", UserId).unwrap_or(BOT);
+ let pin_board = PinBoard::new();
+
+ Self { bot, pin_board }
+ }
+ fn should_echo(&self, msg: &Message) -> bool {
+ static MOYAI_REGEX: Lazy<Regex> =
+ Lazy::new(|| Regex::new(r"^<a?:\w*moy?ai\w*:\d+>$").unwrap());
+
+ // Don't echo to anything we posted ourselves, and don't echo at all unless on certain
+ // servers
+ if msg.author.id == self.bot || !is_guild_allowed(msg.guild_id.unwrap_or_default()) {
+ return false;
+ }
+
+ let content = &msg.content;
+
+ content == "🗿"
+ || consts::TEAMOJIS.contains(&content.as_str())
+ || MOYAI_REGEX.is_match(content)
+ || content
+ .to_ascii_lowercase()
+ .contains("twitter's recommendation algorithm")
+ }
+}
#[async_trait]
impl EventHandler for Handler {
@@ -32,55 +74,47 @@ impl EventHandler for Handler {
* echo some messages when they're sent
*/
async fn message(&self, ctx: Context, msg: Message) {
- let author = msg.author.id.as_u64();
-
- if author == &BOT
- || !ALLOWED_GUILDS.contains(&msg.guild_id.unwrap_or_else(|| GuildId::from(0)))
- {
- return;
- }
-
- let mut echo_msgs = vec!["🗿", "Twitter's Recommendation Algorithm"];
-
- for emoji in consts::TEAMOJIS {
- // i was also lazy here
- echo_msgs.push(emoji);
- }
-
- let mut should_echo = echo_msgs.contains(&msg.content.as_str());
-
- if !should_echo {
- lazy_static! {
- static ref EMOJI_RE: Regex = Regex::new(r"^<a?:(\w+):\d+>$").unwrap();
- }
- if let Some(cap) = EMOJI_RE.captures(msg.content.as_str()) {
- if let Some(emoji_name) = cap.get(1) {
- let emoji_name = emoji_name.as_str();
- should_echo = emoji_name.contains("moai") || emoji_name.contains("moyai");
- }
- }
- }
-
- if should_echo {
- let send = msg.reply(&ctx, msg.content.as_str());
+ if self.should_echo(&msg) {
+ let send = msg.reply(&ctx, &msg.content);
if let Err(why) = send.await {
println!("error when replying to {:?}: {:?}", msg.content, why);
}
}
}
+ async fn channel_pins_update(&self, ctx: Context, pin: ChannelPinsUpdateEvent) {
+ let Some(pin_board) = &self.pin_board else {
+ return;
+ };
+
+ println!(
+ "audit log: {:#?}",
+ pin.guild_id
+ .unwrap()
+ .audit_logs(
+ &ctx.http,
+ Some(Action::Message(MessageAction::Pin).num()),
+ None,
+ None,
+ Some(1),
+ )
+ .await
+ );
+ pin_board.handle_pin(&ctx, &pin).await;
+ }
+
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::ApplicationCommand(command) = interaction {
- println!("Received command interaction: {:#?}", command);
+ println!("Received command interaction: {command:#?}");
let content = match command.data.name.as_str() {
- "ask" => commands::ask::run(&command.data.options).await,
- "bottom" => commands::bottom::run(&command.data.options).await,
- "convertto" => commands::convert::run(&command.data.options).await,
+ "ask" => commands::ask::run(&command.data.options),
+ "bottom" => commands::bottom::run(&command.data.options),
+ "convertto" => commands::convert::run(&command.data.options),
"copypasta" => {
commands::copypasta::run(&command.data.options, command.channel_id, &ctx.http)
.await
}
- "random_lore" => commands::random_lore::run(&command.data.options).await,
+ "random_lore" => commands::random_lore::run(&command.data.options),
"random_teawie" => commands::random_teawie::run(&command.data.options).await,
_ => "not implemented :(".to_string(),
};
@@ -93,7 +127,7 @@ impl EventHandler for Handler {
})
.await
{
- println!("cannot respond to slash command: {}", why);
+ println!("cannot respond to slash command: {why}");
}
}
}
@@ -103,38 +137,41 @@ impl EventHandler for Handler {
let guild_commands =
GuildId::set_application_commands(&TEAWIE_GUILD, &ctx.http, |commands| {
- commands
- .create_application_command(|command| commands::copypasta::register(command))
+ commands.create_application_command(commands::copypasta::register)
})
.await;
- println!("registered guild commands: {:#?}", guild_commands);
+ println!("registered guild commands: {guild_commands:#?}");
let commands = Command::set_global_application_commands(&ctx.http, |commands| {
commands
- .create_application_command(|command| commands::ask::register(command))
- .create_application_command(|command| commands::bottom::register(command))
- .create_application_command(|command| commands::convert::register(command))
- .create_application_command(|command| commands::random_lore::register(command))
- .create_application_command(|command| commands::random_teawie::register(command))
+ .create_application_command(commands::ask::register)
+ .create_application_command(commands::bottom::register)
+ .create_application_command(commands::convert::register)
+ .create_application_command(commands::random_lore::register)
+ .create_application_command(commands::random_teawie::register)
})
.await;
- println!("registered global commands: {:#?}", commands);
+ println!("registered global commands: {commands:#?}");
}
}
#[tokio::main]
async fn main() {
+ dotenvy::dotenv().unwrap();
+
let framework = StandardFramework::new()
.configure(|c| c.prefix("!"))
.group(&GENERAL_GROUP);
- let token = env::var("TOKEN").expect("couldn't find token in environment.");
+ let token = std::env::var("TOKEN").expect("couldn't find token in environment.");
let intents = GatewayIntents::all();
+ let handler = Handler::new();
+
let mut client = Client::builder(token, intents)
- .event_handler(Handler)
+ .event_handler(handler)
.framework(framework)
.await
.expect("error creating client");
@@ -155,7 +192,7 @@ async fn bing(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
async fn ask(ctx: &Context, msg: &Message) -> CommandResult {
- let resp = utils::get_random_response().await;
+ let resp = utils::get_random_response();
msg.channel_id
.send_message(&ctx.http, |m| m.content(resp))
.await?;
@@ -165,7 +202,7 @@ async fn ask(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
async fn random_lore(ctx: &Context, msg: &Message) -> CommandResult {
- let resp = utils::get_random_lore().await;
+ let resp = utils::get_random_lore();
msg.channel_id
.send_message(&ctx.http, |m| m.content(resp))
.await?;
@@ -185,15 +222,11 @@ async fn random_teawie(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
async fn teawiespam(ctx: &Context, msg: &Message) -> CommandResult {
- if !ALLOWED_GUILDS.contains(&msg.guild_id.unwrap_or_else(|| GuildId::from(0))) {
+ if !is_guild_allowed(msg.guild_id.unwrap_or_default()) {
return Ok(());
}
- let mut resp = String::new();
-
- for _ in 0..50 {
- resp += "<:teawiesmile:1056438046440042546>";
- }
+ let resp = "<:teawiesmile:1056438046440042546>".repeat(50);
msg.channel_id
.send_message(&ctx.http, |m| m.content(resp))
diff --git a/src/pinboard.rs b/src/pinboard.rs
new file mode 100644
index 0000000..8f01bff
--- /dev/null
+++ b/src/pinboard.rs
@@ -0,0 +1,109 @@
+use crate::utils::{floor_char_boundary, parse_snowflake_from_env, parse_snowflakes_from_env};
+use serenity::model::prelude::*;
+use serenity::prelude::Context;
+
+pub struct PinBoard {
+ sources: Option<Vec<ChannelId>>,
+ target: ChannelId,
+}
+impl PinBoard {
+ pub fn new() -> Option<Self> {
+ let Some(target) = parse_snowflake_from_env("PIN_BOARD_TARGET", ChannelId) else {
+ return None;
+ };
+ let sources = parse_snowflakes_from_env("PIN_BOARD_SOURCES", ChannelId);
+
+ Some(Self { sources, target })
+ }
+
+ pub async fn handle_pin(&self, ctx: &Context, pin: &ChannelPinsUpdateEvent) {
+ if let Some(sources) = &self.sources {
+ if !sources.contains(&pin.channel_id) {
+ return; // Not on the list of permitted sources
+ }
+ }
+
+ let mut pinner = guess_pinner(ctx, pin).await;
+ let pins = pin
+ .channel_id
+ .pins(&ctx.http)
+ .await
+ .expect("couldn't get a list of pins!?");
+
+ for pin in pins {
+ // We call `take` because it's supposed to be just for the latest message.
+ self.redirect(ctx, &pin, pinner.take()).await;
+ pin.unpin(&ctx).await.expect("couldn't unpin message");
+ }
+ }
+
+ async fn redirect(&self, ctx: &Context, pin: &Message, pinner: Option<UserId>) {
+ let pinner = pinner.map_or("*someone*".to_owned(), |u| format!("<@{u}>"));
+
+ let truncation_point = floor_char_boundary(&pin.content, 700);
+ let truncated_content = if pin.content.len() <= truncation_point {
+ pin.content.to_string()
+ } else {
+ format!("{}...", &pin.content[..truncation_point])
+ };
+ let color = pin
+ .member(ctx)
+ .await
+ .ok()
+ .and_then(|m| m.highest_role_info(&ctx.cache))
+ .and_then(|(role, _)| role.to_role_cached(&ctx.cache))
+ .map(|role| role.colour);
+
+ self.target
+ .send_message(&ctx.http, |m| {
+ m.allowed_mentions(|am| am.empty_parse())
+ .content(format!("📌'd by {pinner} in {}", pin.link()))
+ .add_embed(|embed| {
+ embed.author(|author| {
+ author.name(&pin.author.name).icon_url(pin.author.face())
+ });
+
+ if let Some(color) = color {
+ embed.color(color);
+ }
+ embed.description(truncated_content)
+ })
+ })
+ .await
+ .expect("couldn't redirect message");
+ }
+}
+
+/// (Desperate, best-effort) attempt to get the user that pinned the last message
+///
+/// Now, since Discord is SUPER annoying, it doesn't actually tell you which bloody user
+/// that triggered the pins update event. So, you have to dig into the audit log.
+/// Unfortunately, while you do get a timestamp, the REST API does not return the time at
+/// which each action is logged, which, to me, means that it is not a freaking log *at all*.
+///
+/// I love Discord.
+///
+/// So, the plan is that only the first pinned message gets clear pinner information,
+/// since we can just get the latest pin, which should happen on the exact second.
+/// We can't reliably say the same for any existing pins, so we can only /shrug and say
+/// *somebody* did it. Ugh.
+async fn guess_pinner(ctx: &Context, pin: &ChannelPinsUpdateEvent) -> Option<UserId> {
+ if let Some(g) = pin.guild_id {
+ g.audit_logs(
+ &ctx.http,
+ // This `num` call shouldn't be necessary.
+ // See https://github.com/serenity-rs/serenity/issues/2488
+ Some(Action::Message(MessageAction::Pin).num()),
+ None, // user id
+ None, // before
+ Some(1), // limit
+ )
+ .await
+ .ok()
+ .and_then(|mut logs| logs.entries.pop())
+ .map(|first| first.user_id)
+ } else {
+ // TODO: mayyyyybe we can guess who pinned something in a DM...?
+ None
+ }
+}
diff --git a/src/utils.rs b/src/utils.rs
index 2713241..c2a2031 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,20 +1,30 @@
-use crate::consts::*;
+use crate::consts::{LORE, RESPONSES};
use bottomify::bottom::{decode_string, encode_string};
use include_dir::{include_dir, Dir};
use rand::seq::SliceRandom;
use std::collections::HashMap;
use std::vec;
-const CHAR_LIMIT: usize = 2000;
const FILES: Dir = include_dir!("src/copypastas");
+pub fn parse_snowflake_from_env<T, F: Fn(u64) -> T>(key: &str, f: F) -> Option<T> {
+ std::env::var(key).ok().and_then(|v| v.parse().map(&f).ok())
+}
+pub fn parse_snowflakes_from_env<T, F: Fn(u64) -> T>(key: &str, f: F) -> Option<Vec<T>> {
+ std::env::var(key).ok().and_then(|gs| {
+ gs.split(',')
+ .map(|g| g.parse().map(&f))
+ .collect::<Result<Vec<_>, _>>()
+ .ok()
+ })
+}
/*
* chooses a random element from an array
*/
-async fn random_choice<const N: usize>(arr: [&str; N]) -> String {
+fn random_choice<const N: usize>(arr: [&str; N]) -> String {
let mut rng = rand::thread_rng();
let resp = arr.choose(&mut rng).expect("couldn't choose random value!");
- resp.to_string()
+ (*resp).to_string()
}
/*
@@ -22,38 +32,51 @@ async fn random_choice<const N: usize>(arr: [&str; N]) -> String {
* from our consts
*/
-pub async fn get_random_response() -> String {
- random_choice(RESPONSES).await
+pub fn get_random_response() -> String {
+ random_choice(RESPONSES)
+}
+
+pub fn get_random_lore() -> String {
+ random_choice(LORE)
}
-pub async fn get_random_lore() -> String {
- random_choice(LORE).await
+// waiting for `round_char_boundary` to stabilize
+pub fn floor_char_boundary(s: &str, index: usize) -> usize {
+ if index >= s.len() {
+ s.len()
+ } else {
+ let lower_bound = index.saturating_sub(3);
+ let new_index = s.as_bytes()[lower_bound..=index]
+ .iter()
+ .rposition(|&b| (b as i8) >= -0x40); // b.is_utf8_char_boundary
+
+ // Can be made unsafe but whatever
+ lower_bound + new_index.unwrap()
+ }
+}
+// waiting for `int_roundings` to stabilize
+fn div_ceil(a: usize, b: usize) -> usize {
+ (a + b - 1) / b
}
/*
* splits a message into multiple parts so that
* it can fit discord's character limit
*/
-fn split_msg(msg: &String) -> Vec<String> {
- if msg.len() > CHAR_LIMIT {
- let split = msg[CHAR_LIMIT..].to_string();
+fn split_msg(mut msg: String) -> Vec<String> {
+ const CHAR_LIMIT: usize = 2000;
+ let mut msgs = Vec::with_capacity(div_ceil(msg.len(), CHAR_LIMIT));
- let add = split_msg(&split);
- let mut ret = vec![msg[..CHAR_LIMIT].to_string()];
-
- for v in add {
- ret.push(v);
- }
-
- return ret;
+ while msg.len() > CHAR_LIMIT {
+ msgs.push(msg.split_off(floor_char_boundary(&msg, CHAR_LIMIT)));
}
- vec![msg.to_string()]
+ msgs
}
/*
* gets a random copypasta from include/
*/
-pub async fn get_copypasta(name: &str) -> Vec<String> {
+pub fn get_copypasta(name: &str) -> Vec<String> {
let mut files: HashMap<&str, &str> = HashMap::new();
for file in FILES.files() {
@@ -66,34 +89,29 @@ pub async fn get_copypasta(name: &str) -> Vec<String> {
}
if files.contains_key(&name) {
- let reply = files[name];
- // split message if it's too big
- if reply.len() > CHAR_LIMIT {
- return split_msg(&reply.to_string());
- }
- return vec![reply.to_string()];
+ let reply = files[name].to_string();
+ split_msg(reply)
+ } else {
+ vec![format!("couldn't find {name:?} in files")]
}
-
- let err = format!("couldn't find {:?} in files", name);
- vec![err]
}
/*
* encodes a message into bottom
*/
-pub async fn bottom_encode(msg: &str) -> String {
+pub fn bottom_encode(msg: &str) -> String {
encode_string(&msg)
}
/*
* decodes a bottom string into english
*/
-pub async fn bottom_decode(msg: &str) -> String {
+pub fn bottom_decode(msg: &str) -> String {
let decoded = decode_string(&msg);
match decoded {
Ok(ret) => ret,
Err(why) => {
- println!("couldn't decode {:?}! ({:?})", msg, why);
+ println!("couldn't decode {msg:?}! ({why:?})");
"couldn't decode that! sowwy 🥺".to_string()
}
}
@@ -102,13 +120,13 @@ pub async fn bottom_decode(msg: &str) -> String {
/*
* converts celsius to fahrenheit
*/
-pub async fn celsius_to_fahrenheit(c: &f64) -> f64 {
+pub fn celsius_to_fahrenheit(c: f64) -> f64 {
(c * (9.0 / 5.0)) + 32.0
}
/*
* converts fahrenheit to celsius
*/
-pub async fn fahrenheit_to_celsius(f: &f64) -> f64 {
+pub fn fahrenheit_to_celsius(f: f64) -> f64 {
(f - 32.0) * (5.0 / 9.0)
}