diff --git a/geoffrey_bot/src/bot/commands/add_item.rs b/geoffrey_bot/src/bot/commands/add_item.rs index cacea95..614d861 100644 --- a/geoffrey_bot/src/bot/commands/add_item.rs +++ b/geoffrey_bot/src/bot/commands/add_item.rs @@ -11,10 +11,11 @@ use geoffrey_models::models::response::api_error::GeoffreyAPIError; use crate::bot::arg_parse::{option_to_i64, option_to_string}; use crate::bot::commands::BotCommand; -use crate::bot::formatters::display_loc_full; +use crate::bot::formatters::GeoffreyFormatter; use crate::bot::lang::{PLAYER_ALREADY_SELLS_ITEM, PLAYER_DOES_NOT_HAVE_MATCHING_SHOP}; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; pub struct AddItemCommand; @@ -93,11 +94,15 @@ impl BotCommand for AddItemCommand { } fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, req: Self::ApiParams) -> String { - format!( - "**{}** has been added to {} :\n{}", - req.item_name, - resp.name, - display_loc_full(&resp, &ctx.settings) - ) + GeoffreyFormatter::new(ctx.settings.clone()) + .push( + &MessageBuilder::new() + .push_bold_safe(req.item_name) + .push(" has been added to ") + .push_line(&resp.name) + .build(), + ) + .push_loc_full(&resp) + .build() } } diff --git a/geoffrey_bot/src/bot/commands/add_location.rs b/geoffrey_bot/src/bot/commands/add_location.rs index 106273e..46e3cfd 100644 --- a/geoffrey_bot/src/bot/commands/add_location.rs +++ b/geoffrey_bot/src/bot/commands/add_location.rs @@ -14,9 +14,10 @@ use crate::bot::arg_parse::{ add_z_position_argument, option_to_dim, option_to_i64, option_to_loc_type, option_to_string, }; use crate::bot::commands::BotCommand; -use crate::bot::formatters::display_loc_full; +use crate::bot::formatters::GeoffreyFormatter; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; pub struct AddLocationCommand; @@ -80,10 +81,14 @@ impl BotCommand for AddLocationCommand { } fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { - format!( - "**{}** has been added to Geoffrey:\n{}", - resp.name, - display_loc_full(&resp, &ctx.settings) - ) + GeoffreyFormatter::new(ctx.settings.clone()) + .push( + &MessageBuilder::new() + .push_bold_safe(&resp.name) + .push_line(" has been added to geoffrey:") + .build(), + ) + .push_loc_full(&resp) + .build() } } diff --git a/geoffrey_bot/src/bot/commands/delete.rs b/geoffrey_bot/src/bot/commands/delete.rs index e812618..958c51b 100644 --- a/geoffrey_bot/src/bot/commands/delete.rs +++ b/geoffrey_bot/src/bot/commands/delete.rs @@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand; use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_LOC; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; pub struct DeleteCommand; @@ -62,9 +63,9 @@ impl BotCommand for DeleteCommand { } fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { - format!( - "**{}** has been been removed from Geoffrey, good riddance!", - resp.name - ) + MessageBuilder::new() + .push_bold_safe(resp.name) + .push(" has been removed from Geoffrey, good riddance!") + .build() } } diff --git a/geoffrey_bot/src/bot/commands/edit_name.rs b/geoffrey_bot/src/bot/commands/edit_name.rs index e1cae7d..a32f515 100644 --- a/geoffrey_bot/src/bot/commands/edit_name.rs +++ b/geoffrey_bot/src/bot/commands/edit_name.rs @@ -12,6 +12,7 @@ use crate::bot::arg_parse::option_to_string; use crate::bot::commands::BotCommand; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; //TODO: Combine edit commands into one class once I figure out why subcommand are not working pub struct EditNameCommand; @@ -63,6 +64,11 @@ impl BotCommand for EditNameCommand { } fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { - format!("**{}** has been renamed to {}", args.loc_name, resp.name,) + MessageBuilder::new() + .push_bold_safe(&args.loc_name) + .push(" has been renamed to ") + .push_bold_safe(&resp.name) + .push(".") + .build() } } diff --git a/geoffrey_bot/src/bot/commands/edit_pos.rs b/geoffrey_bot/src/bot/commands/edit_pos.rs index 70357f1..dbf8878 100644 --- a/geoffrey_bot/src/bot/commands/edit_pos.rs +++ b/geoffrey_bot/src/bot/commands/edit_pos.rs @@ -16,6 +16,7 @@ use crate::bot::arg_parse::{ use crate::bot::commands::BotCommand; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; //TODO: Combine edit commands into one class once I figure out why subcommand are not working pub struct EditPosCommand; @@ -70,6 +71,11 @@ impl BotCommand for EditPosCommand { } fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { - format!("**{}** has been moved to {}", resp.name, resp.position) + MessageBuilder::new() + .push_bold_safe(resp.name) + .push(" has been moved to ") + .push_bold_safe(&resp.position) + .push(".") + .build() } } diff --git a/geoffrey_bot/src/bot/commands/find.rs b/geoffrey_bot/src/bot/commands/find.rs index 95dbc8b..1957fdc 100644 --- a/geoffrey_bot/src/bot/commands/find.rs +++ b/geoffrey_bot/src/bot/commands/find.rs @@ -1,5 +1,3 @@ -use std::fmt::Write; - use async_trait::async_trait; use reqwest::Method; use serenity::builder::CreateApplicationCommand; @@ -12,7 +10,7 @@ use geoffrey_models::models::parameters::find_params::FindParams; use crate::bot::arg_parse::option_to_string; use crate::bot::commands::BotCommand; -use crate::bot::formatters::display_loc; +use crate::bot::formatters::GeoffreyFormatter; use crate::context::GeoffreyContext; use crate::error::BotError; @@ -53,16 +51,26 @@ impl BotCommand for FindCommand { Ok(FindParams::new(query)) } - fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { + fn build_response( + ctx: &GeoffreyContext, + resp: Self::ApiResp, + params: Self::ApiParams, + ) -> String { if resp.is_empty() { - "No locations match that query, try better next time ding dong".to_string() + "No locations match that query. Try better next time, ding dong".to_string() } else { - let mut resp_str = String::new(); - writeln!(resp_str, "The following locations match:").unwrap(); - for loc in resp { - writeln!(resp_str, "{}", display_loc(&loc, &ctx.settings)).unwrap(); + let mut formatter = GeoffreyFormatter::new(ctx.settings.clone()); + + formatter + .push("The following locations match '") + .push(¶ms.query) + .push("'"); + + for loc in &resp { + formatter.push_loc(loc).push_new_line().push_new_line(); } - resp_str + + formatter.build() } } } diff --git a/geoffrey_bot/src/bot/commands/info.rs b/geoffrey_bot/src/bot/commands/info.rs index 89e322e..35e5937 100644 --- a/geoffrey_bot/src/bot/commands/info.rs +++ b/geoffrey_bot/src/bot/commands/info.rs @@ -11,7 +11,7 @@ use geoffrey_models::models::response::api_error::GeoffreyAPIError; use crate::bot::arg_parse::option_to_string; use crate::bot::commands::BotCommand; -use crate::bot::formatters::display_loc_full; +use crate::bot::formatters::GeoffreyFormatter; use crate::bot::lang::NO_LOCATION_FOUND; use crate::context::GeoffreyContext; use crate::error::BotError; @@ -63,6 +63,8 @@ impl BotCommand for InfoCommand { } fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { - display_loc_full(&resp, &ctx.settings) + GeoffreyFormatter::new(ctx.settings.clone()) + .push_loc_full(&resp) + .build() } } diff --git a/geoffrey_bot/src/bot/commands/register.rs b/geoffrey_bot/src/bot/commands/register.rs index ba80bd9..d6c1b19 100644 --- a/geoffrey_bot/src/bot/commands/register.rs +++ b/geoffrey_bot/src/bot/commands/register.rs @@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand; use crate::bot::lang::ACCOUNT_LINK_INVALID; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; pub struct RegisterCommand; @@ -72,9 +73,9 @@ impl BotCommand for RegisterCommand { } fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { - format!( - "**{}**, you have been registered for the Geoffrey bot!", - resp.name - ) + MessageBuilder::new() + .push_bold_safe(resp.name) + .push_line(", you have been registered for the Geoffrey bot!") + .build() } } diff --git a/geoffrey_bot/src/bot/commands/remove_item.rs b/geoffrey_bot/src/bot/commands/remove_item.rs index a88d6ae..37e12be 100644 --- a/geoffrey_bot/src/bot/commands/remove_item.rs +++ b/geoffrey_bot/src/bot/commands/remove_item.rs @@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand; use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_SHOP; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; pub struct RemoveItemCommand; @@ -71,9 +72,11 @@ impl BotCommand for RemoveItemCommand { } fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { - format!( - "**{}** has been removed from **{}**", - args.item_name, resp.name - ) + MessageBuilder::new() + .push_bold_safe(&args.item_name) + .push(" has been removed from ") + .push_bold_safe(resp.name) + .push_line("") + .build() } } diff --git a/geoffrey_bot/src/bot/commands/report_out_of_stock.rs b/geoffrey_bot/src/bot/commands/report_out_of_stock.rs index 12fc68f..e5e33a8 100644 --- a/geoffrey_bot/src/bot/commands/report_out_of_stock.rs +++ b/geoffrey_bot/src/bot/commands/report_out_of_stock.rs @@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand; use crate::bot::lang::NO_LOCATION_FOUND; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; pub struct ReportOutOfStockCommand; @@ -71,9 +72,11 @@ impl BotCommand for ReportOutOfStockCommand { } fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { - format!( - "**{}** has been reported out of stock at {}", - args.item_name, resp.name - ) + MessageBuilder::new() + .push_bold_safe(&args.item_name) + .push(" has been reported out of stock at ") + .push_bold_safe(resp.name) + .push_line("") + .build() } } diff --git a/geoffrey_bot/src/bot/commands/restock.rs b/geoffrey_bot/src/bot/commands/restock.rs index 3459b0d..fa45721 100644 --- a/geoffrey_bot/src/bot/commands/restock.rs +++ b/geoffrey_bot/src/bot/commands/restock.rs @@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand; use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_SHOP; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; pub struct RestockCommand; @@ -71,9 +72,11 @@ impl BotCommand for RestockCommand { } fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { - format!( - "**{}** has been restocked at **{}**", - args.item_name, resp.name - ) + MessageBuilder::new() + .push_bold_safe(&args.item_name) + .push(" has been restocked at ") + .push_bold_safe(resp.name) + .push_line("") + .build() } } diff --git a/geoffrey_bot/src/bot/commands/selling.rs b/geoffrey_bot/src/bot/commands/selling.rs index 1e36278..b76167d 100644 --- a/geoffrey_bot/src/bot/commands/selling.rs +++ b/geoffrey_bot/src/bot/commands/selling.rs @@ -1,5 +1,3 @@ -use std::fmt::Write; - use async_trait::async_trait; use reqwest::Method; use serenity::builder::CreateApplicationCommand; @@ -12,9 +10,10 @@ use geoffrey_models::models::response::selling_listing::SellingListing; use crate::bot::arg_parse::{option_to_order, option_to_sort, option_to_string}; use crate::bot::commands::BotCommand; -use crate::bot::formatters::display_item_listing; +use crate::bot::formatters::GeoffreyFormatter; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; pub struct SellingCommand; @@ -73,23 +72,31 @@ impl BotCommand for SellingCommand { Ok(SellingParams::new(query, sort, order)) } - fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { + fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { if resp.is_empty() { "No shops were found selling that, maybe I should start selling it...".to_string() } else { - let mut resp_str = String::new(); - writeln!(resp_str, "The following items match \"{}\":", args.query).unwrap(); + let mut formatter = GeoffreyFormatter::new(ctx.settings.clone()); + + formatter.push( + &MessageBuilder::new() + .push("The following items match '") + .push_bold_safe(args.query) + .push_line("':") + .build(), + ); + for item in resp { - writeln!( - resp_str, - "{} @ {} {}", - display_item_listing(&item.listing), - item.shop_name, - item.shop_loc - ) - .unwrap(); + formatter + .push_item_listing(&item.listing) + .push(" @ ") + .push(&item.shop_name) + .push(" ") + .push(&item.shop_loc.to_string()) + .push_new_line(); } - resp_str + + formatter.build() } } } diff --git a/geoffrey_bot/src/bot/commands/set_portal.rs b/geoffrey_bot/src/bot/commands/set_portal.rs index 6d8faad..22eafe1 100644 --- a/geoffrey_bot/src/bot/commands/set_portal.rs +++ b/geoffrey_bot/src/bot/commands/set_portal.rs @@ -15,6 +15,7 @@ use crate::bot::commands::BotCommand; use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_LOC; use crate::context::GeoffreyContext; use crate::error::BotError; +use serenity::utils::MessageBuilder; pub struct SetPortalCommand; @@ -90,13 +91,17 @@ impl BotCommand for SetPortalCommand { Some(p) => p, }; - format!( - "**{}** has had its portal set to {} {} (x={}, z={})", - resp.name, - portal.direction(), - portal.tunnel_addr(), - portal.x, - portal.z - ) + MessageBuilder::new() + .push_bold_safe(&resp.name) + .push(format!( + " has had its portal set to {} {} (x={}, z={})", + portal.direction(), + portal.tunnel_addr(), + portal.x, + portal.z + )) + .push_bold_safe(resp.name) + .push_line("") + .build() } } diff --git a/geoffrey_bot/src/bot/formatters.rs b/geoffrey_bot/src/bot/formatters.rs index 2463855..332dc23 100644 --- a/geoffrey_bot/src/bot/formatters.rs +++ b/geoffrey_bot/src/bot/formatters.rs @@ -4,100 +4,163 @@ use geoffrey_models::models::locations::{Location, LocationData}; use geoffrey_models::models::player::Player; use geoffrey_models::models::settings::GeoffreySettings; use geoffrey_models::models::Portal; +use serenity::client::Cache; +use serenity::utils::{content_safe, ContentSafeOptions, MessageBuilder}; -pub fn display_owners(owners: &[Player], settings: &GeoffreySettings) -> String { - let mut plural = ""; - let mut ellipses = ""; - - if owners.len() > 1 { - plural = "s" - } - - let range = if owners.len() > settings.max_owners_to_display as usize { - ellipses = "..."; - settings.max_owners_to_display as usize - } else { - owners.len() - }; - - format!( - "Owner{}: {}{}", - plural, - owners[0..range] - .iter() - .map(|owner| owner.name.clone()) - .collect::>() - .join(", "), - ellipses - ) +pub struct GeoffreyFormatter { + msg: String, + settings: GeoffreySettings, } -pub fn display_portal(portal: &Portal) -> String { - format!( - "Portal: {} {} (x={}, z={})", - portal.direction(), - portal.tunnel_addr(), - portal.x, - portal.z - ) -} - -pub fn display_loc(loc: &Location, settings: &GeoffreySettings) -> String { - let portal_str = match &loc.portal { - None => "".to_string(), - Some(p) => format!("{}, ", display_portal(p)), - }; - - format!( - "**{}**, {}, {}{}", - loc.name, - loc.position, - portal_str, - display_owners(&loc.owners, settings), - ) -} - -pub fn display_item_listing(listing: &ItemListing) -> String { - let stocked_diff = Utc::now() - listing.restocked_time; - - let time_str = if stocked_diff < Duration::days(1) { - "today".to_string() - } else if stocked_diff < Duration::days(2) { - "1 day ago".to_string() - } else { - format!("{} days ago", stocked_diff.num_days()) - }; - - let item_listing = format!( - "**{}**, {} for {}D. Restocked {}.", - listing.item.name, listing.count_per_price, listing.price, time_str - ); - - if listing.is_out_of_stock(1) { - format!("~~{}~~", item_listing) - } else { - item_listing - } -} - -pub fn display_loc_full(loc: &Location, settings: &GeoffreySettings) -> String { - let info = match &loc.loc_data { - LocationData::Shop(shop) => { - if !shop.item_listings.is_empty() { - format!( - "\n**Inventory**:\n{}", - shop.item_listings - .iter() - .map(display_item_listing) - .collect::>() - .join("\n") - ) - } else { - "".to_string() - } +impl GeoffreyFormatter { + pub fn new(settings: GeoffreySettings) -> Self { + Self { + msg: String::new(), + settings, } - _ => "".to_string(), - }; + } - format!("{}\n{}", display_loc(loc, settings), info) + pub fn build(&mut self) -> String { + self.msg.clone() + } + + pub fn push(&mut self, string: &str) -> &mut Self { + self.msg.push_str(string); + + self + } + + pub fn push_new_line(&mut self) -> &mut Self { + self.push("\n") + } + + pub fn push_owners(&mut self, owners: &[Player]) -> &mut Self { + let mut plural = ""; + let mut ellipses = ""; + + if owners.len() > 1 { + plural = "s" + } + + let range = if owners.len() > self.settings.max_owners_to_display as usize { + ellipses = "..."; + self.settings.max_owners_to_display as usize + } else { + owners.len() + }; + + self.msg = MessageBuilder::new() + .push(self.msg.clone()) + .push("Owner") + .push(plural) + .push(": ") + .push( + owners[0..range] + .iter() + .map(|owner| owner.name.clone()) + .collect::>() + .join(", "), + ) + .push(ellipses) + .build(); + + self + } + + pub fn push_portal(&mut self, portal: &Portal) -> &mut Self { + self.push(&format!( + "Portal: {} {} (x={}, z={})", + portal.direction(), + portal.tunnel_addr(), + portal.x, + portal.z + )) + } + + pub fn push_loc(&mut self, loc: &Location) -> &mut Self { + self.push( + &MessageBuilder::new() + .push_bold_safe(&loc.name) + .push(", ") + .push(&loc.position) + .push(", ") + .build(), + ); + + match &loc.portal { + None => {} + Some(p) => { + self.push_portal(p).push(", "); + } + }; + + self.push_owners(&loc.owners) + } + + pub fn push_item_listing(&mut self, listing: &ItemListing) -> &mut Self { + let stocked_diff = Utc::now() - listing.restocked_time; + + let time_str = if stocked_diff < Duration::days(1) { + "today".to_string() + } else if stocked_diff < Duration::days(2) { + "1 day ago".to_string() + } else { + format!("{} days ago", stocked_diff.num_days()) + }; + + let msg = MessageBuilder::new() + .push_bold_safe(&listing.item.name) + .push(", ") + .push(&listing.count_per_price) + .push(" for ") + .push(listing.price) + .push("D. Restocked ") + .push(time_str) + .build(); + + let msg = if listing.is_out_of_stock(self.settings.min_out_of_stock_votes) { + MessageBuilder::new().push_strike_safe(msg).to_string() + } else { + msg + }; + + self.push(&msg) + } + + pub fn push_loc_full(&mut self, loc: &Location) -> &mut Self { + self.push_loc(loc); + + match &loc.loc_data { + LocationData::Shop(shop) => { + if !shop.item_listings.is_empty() { + self.push_new_line() + .push(&MessageBuilder::new().push_bold("Inventory:").to_string()) + .push_new_line(); + + for listing in &shop.item_listings { + self.push_item_listing(listing).push_new_line(); + } + + self + } else { + self + } + } + _ => self, + } + } +} + +pub async fn clean_message(cache: impl AsRef, msg: &str) -> String { + content_safe( + cache, + msg, + &ContentSafeOptions::new() + .clean_channel(true) + .clean_user(true) + .clean_everyone(true) + .clean_here(true) + .clean_role(true), + ) + .await } diff --git a/geoffrey_bot/src/main.rs b/geoffrey_bot/src/main.rs index ca61e74..539052e 100644 --- a/geoffrey_bot/src/main.rs +++ b/geoffrey_bot/src/main.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use reqwest::Method; -use serenity::utils::{content_safe, ContentSafeOptions}; use serenity::{ async_trait, model::{ @@ -18,6 +17,7 @@ use geoffrey_models::models::player::UserID; use geoffrey_models::models::settings::GeoffreySettings; use crate::api::run_api_query; +use crate::bot::formatters::clean_message; use crate::bot::{build_commands, CommandRunner}; use crate::configs::GeoffreyBotConfig; use crate::context::GeoffreyContext; @@ -128,15 +128,13 @@ impl EventHandler for Handler { ) .await { - Ok(msg) => msg, + Ok(msg) => clean_message(ctx.cache, &msg).await, Err(e) => { log::warn!("Error running command '{}': {:?}", command_name, e); return; } }; - let msg = content_safe(&ctx.cache, &msg, &ContentSafeOptions::default()).await; - command .create_interaction_response(&ctx.http, |resp| { resp.kind(InteractionResponseType::ChannelMessageWithSource) diff --git a/geoffrey_db/src/query/location_query.rs b/geoffrey_db/src/query/location_query.rs index 338d41c..1a93d42 100644 --- a/geoffrey_db/src/query/location_query.rs +++ b/geoffrey_db/src/query/location_query.rs @@ -9,6 +9,10 @@ impl QueryBuilder { })) } + pub fn with_name_regex_case_insensitive(self, exp: &str) -> Result { + self.with_name_regex(&format!(r"(?i){}", exp)) + } + pub fn with_name_regex(self, exp: &str) -> Result { let filter = regex::Regex::new(exp)?;