Tweak discord formatting
ci/woodpecker/push/woodpecker Pipeline was successful Details

+ Created Formatter to handle geoffrey formatting
+ using Serenity's discord message builder to help with formatting
+ Added message sanitation
+ Clippy + Fmt
main
Joey Hines 2022-01-08 15:20:42 -07:00
parent a8f5f5d87b
commit 5fcb021fe0
No known key found for this signature in database
GPG Key ID: 80F567B5C968F91B
16 changed files with 285 additions and 165 deletions

View File

@ -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::arg_parse::{option_to_i64, option_to_string};
use crate::bot::commands::BotCommand; 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::bot::lang::{PLAYER_ALREADY_SELLS_ITEM, PLAYER_DOES_NOT_HAVE_MATCHING_SHOP};
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
pub struct AddItemCommand; pub struct AddItemCommand;
@ -93,11 +94,15 @@ impl BotCommand for AddItemCommand {
} }
fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, req: Self::ApiParams) -> String { fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, req: Self::ApiParams) -> String {
format!( GeoffreyFormatter::new(ctx.settings.clone())
"**{}** has been added to {} :\n{}", .push(
req.item_name, &MessageBuilder::new()
resp.name, .push_bold_safe(req.item_name)
display_loc_full(&resp, &ctx.settings) .push(" has been added to ")
.push_line(&resp.name)
.build(),
) )
.push_loc_full(&resp)
.build()
} }
} }

View File

@ -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, 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::commands::BotCommand;
use crate::bot::formatters::display_loc_full; use crate::bot::formatters::GeoffreyFormatter;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
pub struct AddLocationCommand; pub struct AddLocationCommand;
@ -80,10 +81,14 @@ impl BotCommand for AddLocationCommand {
} }
fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String {
format!( GeoffreyFormatter::new(ctx.settings.clone())
"**{}** has been added to Geoffrey:\n{}", .push(
resp.name, &MessageBuilder::new()
display_loc_full(&resp, &ctx.settings) .push_bold_safe(&resp.name)
.push_line(" has been added to geoffrey:")
.build(),
) )
.push_loc_full(&resp)
.build()
} }
} }

View File

@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand;
use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_LOC; use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_LOC;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
pub struct DeleteCommand; pub struct DeleteCommand;
@ -62,9 +63,9 @@ impl BotCommand for DeleteCommand {
} }
fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String {
format!( MessageBuilder::new()
"**{}** has been been removed from Geoffrey, good riddance!", .push_bold_safe(resp.name)
resp.name .push(" has been removed from Geoffrey, good riddance!")
) .build()
} }
} }

View File

@ -12,6 +12,7 @@ use crate::bot::arg_parse::option_to_string;
use crate::bot::commands::BotCommand; use crate::bot::commands::BotCommand;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
//TODO: Combine edit commands into one class once I figure out why subcommand are not working //TODO: Combine edit commands into one class once I figure out why subcommand are not working
pub struct EditNameCommand; pub struct EditNameCommand;
@ -63,6 +64,11 @@ impl BotCommand for EditNameCommand {
} }
fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { 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()
} }
} }

View File

@ -16,6 +16,7 @@ use crate::bot::arg_parse::{
use crate::bot::commands::BotCommand; use crate::bot::commands::BotCommand;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
//TODO: Combine edit commands into one class once I figure out why subcommand are not working //TODO: Combine edit commands into one class once I figure out why subcommand are not working
pub struct EditPosCommand; pub struct EditPosCommand;
@ -70,6 +71,11 @@ impl BotCommand for EditPosCommand {
} }
fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { 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()
} }
} }

View File

@ -1,5 +1,3 @@
use std::fmt::Write;
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::Method; use reqwest::Method;
use serenity::builder::CreateApplicationCommand; 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::arg_parse::option_to_string;
use crate::bot::commands::BotCommand; use crate::bot::commands::BotCommand;
use crate::bot::formatters::display_loc; use crate::bot::formatters::GeoffreyFormatter;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
@ -53,16 +51,26 @@ impl BotCommand for FindCommand {
Ok(FindParams::new(query)) 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() { 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 { } else {
let mut resp_str = String::new(); let mut formatter = GeoffreyFormatter::new(ctx.settings.clone());
writeln!(resp_str, "The following locations match:").unwrap();
for loc in resp { formatter
writeln!(resp_str, "{}", display_loc(&loc, &ctx.settings)).unwrap(); .push("The following locations match '")
.push(&params.query)
.push("'");
for loc in &resp {
formatter.push_loc(loc).push_new_line().push_new_line();
} }
resp_str
formatter.build()
} }
} }
} }

View File

@ -11,7 +11,7 @@ use geoffrey_models::models::response::api_error::GeoffreyAPIError;
use crate::bot::arg_parse::option_to_string; use crate::bot::arg_parse::option_to_string;
use crate::bot::commands::BotCommand; 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::bot::lang::NO_LOCATION_FOUND;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
@ -63,6 +63,8 @@ impl BotCommand for InfoCommand {
} }
fn build_response(ctx: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { 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()
} }
} }

View File

@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand;
use crate::bot::lang::ACCOUNT_LINK_INVALID; use crate::bot::lang::ACCOUNT_LINK_INVALID;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
pub struct RegisterCommand; pub struct RegisterCommand;
@ -72,9 +73,9 @@ impl BotCommand for RegisterCommand {
} }
fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String { fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, _: Self::ApiParams) -> String {
format!( MessageBuilder::new()
"**{}**, you have been registered for the Geoffrey bot!", .push_bold_safe(resp.name)
resp.name .push_line(", you have been registered for the Geoffrey bot!")
) .build()
} }
} }

View File

@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand;
use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_SHOP; use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_SHOP;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
pub struct RemoveItemCommand; pub struct RemoveItemCommand;
@ -71,9 +72,11 @@ impl BotCommand for RemoveItemCommand {
} }
fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String {
format!( MessageBuilder::new()
"**{}** has been removed from **{}**", .push_bold_safe(&args.item_name)
args.item_name, resp.name .push(" has been removed from ")
) .push_bold_safe(resp.name)
.push_line("")
.build()
} }
} }

View File

@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand;
use crate::bot::lang::NO_LOCATION_FOUND; use crate::bot::lang::NO_LOCATION_FOUND;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
pub struct ReportOutOfStockCommand; pub struct ReportOutOfStockCommand;
@ -71,9 +72,11 @@ impl BotCommand for ReportOutOfStockCommand {
} }
fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String {
format!( MessageBuilder::new()
"**{}** has been reported out of stock at {}", .push_bold_safe(&args.item_name)
args.item_name, resp.name .push(" has been reported out of stock at ")
) .push_bold_safe(resp.name)
.push_line("")
.build()
} }
} }

View File

@ -14,6 +14,7 @@ use crate::bot::commands::BotCommand;
use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_SHOP; use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_SHOP;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
pub struct RestockCommand; pub struct RestockCommand;
@ -71,9 +72,11 @@ impl BotCommand for RestockCommand {
} }
fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String { fn build_response(_: &GeoffreyContext, resp: Self::ApiResp, args: Self::ApiParams) -> String {
format!( MessageBuilder::new()
"**{}** has been restocked at **{}**", .push_bold_safe(&args.item_name)
args.item_name, resp.name .push(" has been restocked at ")
) .push_bold_safe(resp.name)
.push_line("")
.build()
} }
} }

View File

@ -1,5 +1,3 @@
use std::fmt::Write;
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::Method; use reqwest::Method;
use serenity::builder::CreateApplicationCommand; 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::arg_parse::{option_to_order, option_to_sort, option_to_string};
use crate::bot::commands::BotCommand; use crate::bot::commands::BotCommand;
use crate::bot::formatters::display_item_listing; use crate::bot::formatters::GeoffreyFormatter;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
pub struct SellingCommand; pub struct SellingCommand;
@ -73,23 +72,31 @@ impl BotCommand for SellingCommand {
Ok(SellingParams::new(query, sort, order)) 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() { if resp.is_empty() {
"No shops were found selling that, maybe I should start selling it...".to_string() "No shops were found selling that, maybe I should start selling it...".to_string()
} else { } else {
let mut resp_str = String::new(); let mut formatter = GeoffreyFormatter::new(ctx.settings.clone());
writeln!(resp_str, "The following items match \"{}\":", args.query).unwrap();
formatter.push(
&MessageBuilder::new()
.push("The following items match '")
.push_bold_safe(args.query)
.push_line("':")
.build(),
);
for item in resp { for item in resp {
writeln!( formatter
resp_str, .push_item_listing(&item.listing)
"{} @ {} {}", .push(" @ ")
display_item_listing(&item.listing), .push(&item.shop_name)
item.shop_name, .push(" ")
item.shop_loc .push(&item.shop_loc.to_string())
) .push_new_line();
.unwrap();
} }
resp_str
formatter.build()
} }
} }
} }

View File

@ -15,6 +15,7 @@ use crate::bot::commands::BotCommand;
use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_LOC; use crate::bot::lang::PLAYER_DOES_NOT_HAVE_MATCHING_LOC;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
use crate::error::BotError; use crate::error::BotError;
use serenity::utils::MessageBuilder;
pub struct SetPortalCommand; pub struct SetPortalCommand;
@ -90,13 +91,17 @@ impl BotCommand for SetPortalCommand {
Some(p) => p, Some(p) => p,
}; };
format!( MessageBuilder::new()
"**{}** has had its portal set to {} {} (x={}, z={})", .push_bold_safe(&resp.name)
resp.name, .push(format!(
" has had its portal set to {} {} (x={}, z={})",
portal.direction(), portal.direction(),
portal.tunnel_addr(), portal.tunnel_addr(),
portal.x, portal.x,
portal.z portal.z
) ))
.push_bold_safe(resp.name)
.push_line("")
.build()
} }
} }

View File

@ -4,8 +4,37 @@ use geoffrey_models::models::locations::{Location, LocationData};
use geoffrey_models::models::player::Player; use geoffrey_models::models::player::Player;
use geoffrey_models::models::settings::GeoffreySettings; use geoffrey_models::models::settings::GeoffreySettings;
use geoffrey_models::models::Portal; 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 { pub struct GeoffreyFormatter {
msg: String,
settings: GeoffreySettings,
}
impl GeoffreyFormatter {
pub fn new(settings: GeoffreySettings) -> Self {
Self {
msg: String::new(),
settings,
}
}
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 plural = "";
let mut ellipses = ""; let mut ellipses = "";
@ -13,51 +42,62 @@ pub fn display_owners(owners: &[Player], settings: &GeoffreySettings) -> String
plural = "s" plural = "s"
} }
let range = if owners.len() > settings.max_owners_to_display as usize { let range = if owners.len() > self.settings.max_owners_to_display as usize {
ellipses = "..."; ellipses = "...";
settings.max_owners_to_display as usize self.settings.max_owners_to_display as usize
} else { } else {
owners.len() owners.len()
}; };
format!( self.msg = MessageBuilder::new()
"Owner{}: {}{}", .push(self.msg.clone())
plural, .push("Owner")
.push(plural)
.push(": ")
.push(
owners[0..range] owners[0..range]
.iter() .iter()
.map(|owner| owner.name.clone()) .map(|owner| owner.name.clone())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(", "), .join(", "),
ellipses
) )
.push(ellipses)
.build();
self
} }
pub fn display_portal(portal: &Portal) -> String { pub fn push_portal(&mut self, portal: &Portal) -> &mut Self {
format!( self.push(&format!(
"Portal: {} {} (x={}, z={})", "Portal: {} {} (x={}, z={})",
portal.direction(), portal.direction(),
portal.tunnel_addr(), portal.tunnel_addr(),
portal.x, portal.x,
portal.z portal.z
) ))
} }
pub fn display_loc(loc: &Location, settings: &GeoffreySettings) -> String { pub fn push_loc(&mut self, loc: &Location) -> &mut Self {
let portal_str = match &loc.portal { self.push(
None => "".to_string(), &MessageBuilder::new()
Some(p) => format!("{}, ", display_portal(p)), .push_bold_safe(&loc.name)
.push(", ")
.push(&loc.position)
.push(", ")
.build(),
);
match &loc.portal {
None => {}
Some(p) => {
self.push_portal(p).push(", ");
}
}; };
format!( self.push_owners(&loc.owners)
"**{}**, {}, {}{}",
loc.name,
loc.position,
portal_str,
display_owners(&loc.owners, settings),
)
} }
pub fn display_item_listing(listing: &ItemListing) -> String { pub fn push_item_listing(&mut self, listing: &ItemListing) -> &mut Self {
let stocked_diff = Utc::now() - listing.restocked_time; let stocked_diff = Utc::now() - listing.restocked_time;
let time_str = if stocked_diff < Duration::days(1) { let time_str = if stocked_diff < Duration::days(1) {
@ -68,36 +108,59 @@ pub fn display_item_listing(listing: &ItemListing) -> String {
format!("{} days ago", stocked_diff.num_days()) format!("{} days ago", stocked_diff.num_days())
}; };
let item_listing = format!( let msg = MessageBuilder::new()
"**{}**, {} for {}D. Restocked {}.", .push_bold_safe(&listing.item.name)
listing.item.name, listing.count_per_price, listing.price, time_str .push(", ")
); .push(&listing.count_per_price)
.push(" for ")
.push(listing.price)
.push("D. Restocked ")
.push(time_str)
.build();
if listing.is_out_of_stock(1) { let msg = if listing.is_out_of_stock(self.settings.min_out_of_stock_votes) {
format!("~~{}~~", item_listing) MessageBuilder::new().push_strike_safe(msg).to_string()
} else { } else {
item_listing msg
}
}
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::<Vec<String>>()
.join("\n")
)
} else {
"".to_string()
}
}
_ => "".to_string(),
}; };
format!("{}\n{}", display_loc(loc, settings), info) 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<Cache>, 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
} }

View File

@ -1,7 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use reqwest::Method; use reqwest::Method;
use serenity::utils::{content_safe, ContentSafeOptions};
use serenity::{ use serenity::{
async_trait, async_trait,
model::{ model::{
@ -18,6 +17,7 @@ use geoffrey_models::models::player::UserID;
use geoffrey_models::models::settings::GeoffreySettings; use geoffrey_models::models::settings::GeoffreySettings;
use crate::api::run_api_query; use crate::api::run_api_query;
use crate::bot::formatters::clean_message;
use crate::bot::{build_commands, CommandRunner}; use crate::bot::{build_commands, CommandRunner};
use crate::configs::GeoffreyBotConfig; use crate::configs::GeoffreyBotConfig;
use crate::context::GeoffreyContext; use crate::context::GeoffreyContext;
@ -128,15 +128,13 @@ impl EventHandler for Handler {
) )
.await .await
{ {
Ok(msg) => msg, Ok(msg) => clean_message(ctx.cache, &msg).await,
Err(e) => { Err(e) => {
log::warn!("Error running command '{}': {:?}", command_name, e); log::warn!("Error running command '{}': {:?}", command_name, e);
return; return;
} }
}; };
let msg = content_safe(&ctx.cache, &msg, &ContentSafeOptions::default()).await;
command command
.create_interaction_response(&ctx.http, |resp| { .create_interaction_response(&ctx.http, |resp| {
resp.kind(InteractionResponseType::ChannelMessageWithSource) resp.kind(InteractionResponseType::ChannelMessageWithSource)

View File

@ -9,6 +9,10 @@ impl QueryBuilder<LocationDb> {
})) }))
} }
pub fn with_name_regex_case_insensitive(self, exp: &str) -> Result<Self> {
self.with_name_regex(&format!(r"(?i){}", exp))
}
pub fn with_name_regex(self, exp: &str) -> Result<Self> { pub fn with_name_regex(self, exp: &str) -> Result<Self> {
let filter = regex::Regex::new(exp)?; let filter = regex::Regex::new(exp)?;