diff --git a/README.md b/README.md index d621686..023bf8b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ Reminder Bot can be built by running `cargo build --release` in the top level di #### Compilation environment variables These environment variables must be provided when compiling the bot +* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`) * `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size** +* `STRINGS_FILE` - accepts the name of a compiled strings file located in `$CARGO_MANIFEST_DIR/assets/` to be used for creating messages. Compiled string files can be generated with `compile.py` at https://github.com/reminder-bot/languages ### Setting up Python Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library diff --git a/src/commands/info_cmds.rs b/src/commands/info_cmds.rs index 8312350..143985d 100644 --- a/src/commands/info_cmds.rs +++ b/src/commands/info_cmds.rs @@ -41,7 +41,7 @@ async fn help(ctx: &Context, msg: &Message, _args: String) { let lm = data.get::().unwrap(); - let language = UserData::language_of(&msg.author, &ctx, &pool).await; + let language = UserData::language_of(&msg.author, &pool).await; let desc = lm.get(&language, "help"); @@ -75,7 +75,7 @@ async fn info(ctx: &Context, msg: &Message, _args: String) { let lm = data.get::().unwrap(); - let language = UserData::language_of(&msg.author, &ctx, &pool).await; + let language = UserData::language_of(&msg.author, &pool).await; let guild_data = GuildData::from_guild(msg.guild(&ctx).await.unwrap(), &pool) .await .unwrap(); @@ -116,7 +116,7 @@ async fn donate(ctx: &Context, msg: &Message, _args: String) { let lm = data.get::().unwrap(); - let language = UserData::language_of(&msg.author, &ctx, &pool).await; + let language = UserData::language_of(&msg.author, &pool).await; let desc = lm.get(&language, "donate"); let _ = msg @@ -161,28 +161,26 @@ async fn dashboard(ctx: &Context, msg: &Message, _args: String) { #[command] async fn clock(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await + let data = ctx.data.read().await; + + let pool = data .get::() .cloned() .expect("Could not get SQLPool from data"); + let lm = data.get::().unwrap(); let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); let now = Utc::now().with_timezone(&user_data.timezone()); + let clock_display = lm.get(&user_data.language, "clock/time"); + if args == "12" { let _ = msg .channel_id .say( &ctx, - user_data.response(&pool, "clock/time").await.replacen( - "{}", - &now.format("%I:%M:%S %p").to_string(), - 1, - ), + clock_display.replacen("{}", &now.format("%I:%M:%S %p").to_string(), 1), ) .await; } else { @@ -190,11 +188,7 @@ async fn clock(ctx: &Context, msg: &Message, args: String) { .channel_id .say( &ctx, - user_data.response(&pool, "clock/time").await.replacen( - "{}", - &now.format("%H:%M:%S").to_string(), - 1, - ), + clock_display.replacen("{}", &now.format("%H:%M:%S").to_string(), 1), ) .await; } diff --git a/src/commands/moderation_cmds.rs b/src/commands/moderation_cmds.rs index 1b2526d..fea6618 100644 --- a/src/commands/moderation_cmds.rs +++ b/src/commands/moderation_cmds.rs @@ -19,6 +19,7 @@ use crate::{ FrameworkCtx, SQLPool, }; +use crate::language_manager::LanguageManager; use serenity::model::id::ChannelId; use std::iter; @@ -27,13 +28,19 @@ use std::iter; #[permission_level(Restricted)] #[can_blacklist(false)] async fn blacklist(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); @@ -64,30 +71,24 @@ async fn blacklist(ctx: &Context, msg: &Message, args: String) { if local { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "blacklist/added").await) + .say(&ctx, lm.get(&user_data.language, "blacklist/added")) .await; } else { let _ = msg .channel_id - .say( - &ctx, - user_data.response(&pool, "blacklist/added_from").await, - ) + .say(&ctx, lm.get(&user_data.language, "blacklist/added_from")) .await; } } else { if local { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "blacklist/removed").await) + .say(&ctx, lm.get(&user_data.language, "blacklist/removed")) .await; } else { let _ = msg .channel_id - .say( - &ctx, - user_data.response(&pool, "blacklist/removed_from").await, - ) + .say(&ctx, lm.get(&user_data.language, "blacklist/removed_from")) .await; } } @@ -95,13 +96,19 @@ async fn blacklist(ctx: &Context, msg: &Message, args: String) { #[command] async fn timezone(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let mut user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); @@ -113,9 +120,8 @@ async fn timezone(ctx: &Context, msg: &Message, args: String) { let now = Utc::now().with_timezone(&user_data.timezone()); - let content = user_data - .response(&pool, "timezone/set_p") - .await + let content = lm + .get(&user_data.language, "timezone/set_p") .replacen("{timezone}", &user_data.timezone, 1) .replacen("{time}", &now.format("%H:%M").to_string(), 1); @@ -125,17 +131,13 @@ async fn timezone(ctx: &Context, msg: &Message, args: String) { Err(_) => { let _ = msg .channel_id - .say( - &ctx, - user_data.response(&pool, "timezone/no_timezone").await, - ) + .say(&ctx, lm.get(&user_data.language, "timezone/no_timezone")) .await; } } } else { - let content = user_data - .response(&pool, "timezone/no_argument") - .await + let content = lm + .get(&user_data.language, "timezone/no_argument") .replace( "{prefix}", &GuildData::prefix_from_id(msg.guild_id, &pool).await, @@ -148,57 +150,43 @@ async fn timezone(ctx: &Context, msg: &Message, args: String) { #[command] async fn language(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let mut user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); - match sqlx::query!( - " -SELECT code FROM languages WHERE code = ? OR name = ? - ", - args, - args - ) - .fetch_one(&pool) - .await - { - Ok(row) => { - user_data.language = row.code; + match lm.get_language(&args) { + Some(row) => { + user_data.language = row.to_string(); user_data.commit_changes(&pool).await; let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "lang/set_p").await) + .say(&ctx, lm.get(&user_data.language, "lang/set_p")) .await; } - Err(_) => { - let language_codes = sqlx::query!("SELECT name, code FROM languages") - .fetch_all(&pool) - .await - .unwrap() - .iter() - .map(|language| { - format!( - "{} ({})", - language.name.to_title_case(), - language.code.to_uppercase() - ) - }) + None => { + let language_codes = lm + .all_languages() + .map(|(k, v)| format!("{} ({})", v.to_title_case(), k.to_uppercase())) .collect::>() .join("\n"); let content = - user_data - .response(&pool, "lang/invalid") - .await + lm.get(&user_data.language, "lang/invalid") .replacen("{}", &language_codes, 1); let _ = msg.channel_id.say(&ctx, content).await; @@ -210,13 +198,19 @@ SELECT code FROM languages WHERE code = ? OR name = ? #[supports_dm(false)] #[permission_level(Restricted)] async fn prefix(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let mut guild_data = GuildData::from_guild(msg.guild(&ctx).await.unwrap(), &pool) .await @@ -226,18 +220,18 @@ async fn prefix(ctx: &Context, msg: &Message, args: String) { if args.len() > 5 { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "prefix/too_long").await) + .say(&ctx, lm.get(&user_data.language, "prefix/too_long")) .await; } else if args.is_empty() { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "prefix/no_argument").await) + .say(&ctx, lm.get(&user_data.language, "prefix/no_argument")) .await; } else { guild_data.prefix = args; guild_data.commit_changes(&pool).await; - let content = user_data.response(&pool, "prefix/success").await.replacen( + let content = lm.get(&user_data.language, "prefix/success").replacen( "{prefix}", &guild_data.prefix, 1, @@ -251,13 +245,19 @@ async fn prefix(ctx: &Context, msg: &Message, args: String) { #[supports_dm(false)] #[permission_level(Restricted)] async fn restrict(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); let guild_data = GuildData::from_guild(msg.guild(&ctx).await.unwrap(), &pool) @@ -292,7 +292,7 @@ DELETE FROM command_restrictions WHERE role_id = (SELECT id FROM roles WHERE rol if commands.is_empty() { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "restrict/disabled").await) + .say(&ctx, lm.get(&user_data.language, "restrict/disabled")) .await; } else { let _ = sqlx::query!( @@ -317,10 +317,11 @@ INSERT INTO command_restrictions (role_id, command) VALUES ((SELECT id FROM role if res.is_err() { println!("{:?}", res); - let content = user_data - .response(&pool, "restrict/failure") - .await - .replacen("{command}", &command, 1); + let content = lm.get(&user_data.language, "restrict/failure").replacen( + "{command}", + &command, + 1, + ); let _ = msg.channel_id.say(&ctx, content).await; } @@ -328,7 +329,7 @@ INSERT INTO command_restrictions (role_id, command) VALUES ((SELECT id FROM role let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "restrict/enabled").await) + .say(&ctx, lm.get(&user_data.language, "restrict/enabled")) .await; } } @@ -359,16 +360,15 @@ WHERE .map(|row| format!("<@&{}> can use {}", row.role, row.command)) .collect::>() .join("\n"); - let display = user_data - .response(&pool, "restrict/allowed") - .await - .replacen("{}", &display_inner, 1); + let display = + lm.get(&user_data.language, "restrict/allowed") + .replacen("{}", &display_inner, 1); let _ = msg.channel_id.say(&ctx, display).await; } else { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "restrict/help").await) + .say(&ctx, lm.get(&user_data.language, "restrict/help")) .await; } } @@ -377,13 +377,19 @@ WHERE #[supports_dm(false)] #[permission_level(Managed)] async fn alias(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); @@ -437,16 +443,15 @@ DELETE FROM command_aliases WHERE name = ? AND guild_id = (SELECT id FROM guilds .await .unwrap(); - let content = user_data - .response(&pool, "alias/removed") - .await + let content = lm + .get(&user_data.language, "alias/removed") .replace("{count}", &deleted_count.count.to_string()); let _ = msg.channel_id.say(&ctx, content).await; } else { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "alias/help").await) + .say(&ctx, lm.get(&user_data.language, "alias/help")) .await; } } @@ -470,9 +475,8 @@ UPDATE command_aliases SET command = ? WHERE guild_id = (SELECT id FROM guilds W .unwrap(); } - let content = user_data - .response(&pool, "alias/created") - .await + let content = lm + .get(&user_data.language, "alias/created") .replace("{name}", name); let _ = msg.channel_id.say(&ctx, content).await; @@ -495,7 +499,7 @@ SELECT command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHER }, Err(_) => { - let content = user_data.response(&pool, "alias/not_found").await.replace("{name}", name); + let content = lm.get(&user_data.language, "alias/not_found").replace("{name}", name); let _ = msg.channel_id.say(&ctx, content).await; }, @@ -505,9 +509,8 @@ SELECT command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHER } } else { let prefix = GuildData::prefix_from_id(msg.guild_id, &pool).await; - let content = user_data - .response(&pool, "alias/help") - .await + let content = lm + .get(&user_data.language, "alias/help") .replace("{prefix}", &prefix); let _ = msg.channel_id.say(&ctx, content).await; diff --git a/src/commands/reminder_cmds.rs b/src/commands/reminder_cmds.rs index e81ec3d..23d05de 100644 --- a/src/commands/reminder_cmds.rs +++ b/src/commands/reminder_cmds.rs @@ -50,6 +50,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use crate::language_manager::LanguageManager; use regex::RegexBuilder; fn shorthand_displacement(seconds: u64) -> String { @@ -113,13 +114,19 @@ async fn create_webhook( #[supports_dm(false)] #[permission_level(Restricted)] async fn pause(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); let mut channel = ChannelData::from_channel(msg.channel(&ctx).await.unwrap(), &pool) @@ -135,19 +142,16 @@ async fn pause(ctx: &Context, msg: &Message, args: String) { if channel.paused { let _ = msg .channel_id - .say( - &ctx, - user_data.response(&pool, "pause/paused_indefinite").await, - ) + .say(&ctx, lm.get(&user_data.language, "pause/paused_indefinite")) .await; } else { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "pause/unpaused").await) + .say(&ctx, lm.get(&user_data.language, "pause/unpaused")) .await; } } else { - let parser = TimeParser::new(args, user_data.timezone.parse().unwrap()); + let parser = TimeParser::new(args, user_data.timezone()); let pause_until = parser.timestamp(); match pause_until { @@ -159,17 +163,14 @@ async fn pause(ctx: &Context, msg: &Message, args: String) { channel.commit_changes(&pool).await; - let content = user_data - .response(&pool, "pause/paused_until") - .await - .replace( - "{}", - &user_data - .timezone() - .timestamp(timestamp, 0) - .format("%Y-%m-%d %H:%M:%S") - .to_string(), - ); + let content = lm.get(&user_data.language, "pause/paused_until").replace( + "{}", + &user_data + .timezone() + .timestamp(timestamp, 0) + .format("%Y-%m-%d %H:%M:%S") + .to_string(), + ); let _ = msg.channel_id.say(&ctx, content).await; } @@ -177,7 +178,7 @@ async fn pause(ctx: &Context, msg: &Message, args: String) { Err(_) => { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "pause/invalid_time").await) + .say(&ctx, lm.get(&user_data.language, "pause/invalid_time")) .await; } } @@ -187,20 +188,26 @@ async fn pause(ctx: &Context, msg: &Message, args: String) { #[command] #[permission_level(Restricted)] async fn offset(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); if args.is_empty() { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "offset/help").await) + .say(&ctx, lm.get(&user_data.language, "offset/help")) .await; } else { let parser = TimeParser::new(args, user_data.timezone()); @@ -237,7 +244,7 @@ UPDATE reminders SET `time` = `time` + ? WHERE reminders.channel_id = ? .unwrap(); } - let response = user_data.response(&pool, "offset/success").await.replacen( + let response = lm.get(&user_data.language, "offset/success").replacen( "{}", &displacement.to_string(), 1, @@ -247,7 +254,7 @@ UPDATE reminders SET `time` = `time` + ? WHERE reminders.channel_id = ? } else { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "offset/invalid_time").await) + .say(&ctx, lm.get(&user_data.language, "offset/invalid_time")) .await; } } @@ -256,13 +263,19 @@ UPDATE reminders SET `time` = `time` + ? WHERE reminders.channel_id = ? #[command] #[permission_level(Restricted)] async fn nudge(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); let mut channel = ChannelData::from_channel(msg.channel(&ctx).await.unwrap(), &pool) @@ -270,9 +283,8 @@ async fn nudge(ctx: &Context, msg: &Message, args: String) { .unwrap(); if args.is_empty() { - let content = user_data - .response(&pool, "nudge/no_argument") - .await + let content = lm + .get(&user_data.language, "nudge/no_argument") .replace("{nudge}", &format!("{}s", &channel.nudge.to_string())); let _ = msg.channel_id.say(&ctx, content).await; @@ -285,14 +297,14 @@ async fn nudge(ctx: &Context, msg: &Message, args: String) { if displacement < i16::MIN as i64 || displacement > i16::MAX as i64 { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "nudge/invalid_time").await) + .say(&ctx, lm.get(&user_data.language, "nudge/invalid_time")) .await; } else { channel.nudge = displacement as i16; channel.commit_changes(&pool).await; - let response = user_data.response(&pool, "nudge/success").await.replacen( + let response = lm.get(&user_data.language, "nudge/success").replacen( "{}", &displacement.to_string(), 1, @@ -305,7 +317,7 @@ async fn nudge(ctx: &Context, msg: &Message, args: String) { Err(_) => { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "nudge/invalid_time").await) + .say(&ctx, lm.get(&user_data.language, "nudge/invalid_time")) .await; } } @@ -399,13 +411,19 @@ impl LookReminder { #[command("look")] #[permission_level(Managed)] async fn look(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); @@ -531,10 +549,10 @@ LIMIT if reminders.is_empty() { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "look/no_reminders").await) + .say(&ctx, lm.get(&user_data.language, "look/no_reminders")) .await; } else { - let inter = user_data.response(&pool, "look/inter").await; + let inter = lm.get(&user_data.language, "look/inter"); let display = reminders.iter().map(|reminder| { let time_display = match flags.time_display { @@ -568,19 +586,25 @@ LIMIT #[command("del")] #[permission_level(Managed)] async fn delete(ctx: &Context, msg: &Message, _args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "del/listing").await) + .say(&ctx, lm.get(&user_data.language, "del/listing")) .await; let reminders = if let Some(guild_id) = msg.guild_id.map(|f| f.as_u64().to_owned()) { @@ -658,7 +682,7 @@ WHERE let _ = msg.channel_id.say_lines(&ctx, enumerated_reminders).await; let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "del/listed").await) + .say(&ctx, lm.get(&user_data.language, "del/listed")) .await; let reply = msg @@ -721,7 +745,7 @@ INSERT INTO events (event_name, bulk_count, guild_id, user_id) VALUES ('delete', .await; } - let content = user_data.response(&pool, "del/count").await.replacen( + let content = lm.get(&user_data.language, "del/count").replacen( "{}", &count_row.count.to_string(), 1, @@ -729,9 +753,8 @@ INSERT INTO events (event_name, bulk_count, guild_id, user_id) VALUES ('delete', let _ = msg.channel_id.say(&ctx, content).await; } else { - let content = user_data - .response(&pool, "del/count") - .await + let content = lm + .get(&user_data.language, "del/count") .replacen("{}", "0", 1); let _ = msg.channel_id.say(&ctx, content).await; @@ -757,15 +780,21 @@ async fn timer(ctx: &Context, msg: &Message, args: String) { format!("{:02}:{:02}:{:02}", hours, minutes, seconds) } - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; - let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } + + let language = UserData::language_of(&msg.author, &pool).await; let mut args_iter = args.splitn(2, ' '); @@ -796,7 +825,7 @@ async fn timer(ctx: &Context, msg: &Message, args: String) { if count >= 25 { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "timer/limit").await) + .say(&ctx, lm.get(&language, "timer/limit")) .await; } else { let name = args_iter @@ -808,7 +837,7 @@ async fn timer(ctx: &Context, msg: &Message, args: String) { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "timer/success").await) + .say(&ctx, lm.get(&language, "timer/success")) .await; } } @@ -839,18 +868,18 @@ DELETE FROM timers WHERE owner = ? AND name = ? let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "timer/deleted").await) + .say(&ctx, lm.get(&language, "timer/deleted")) .await; } else { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "timer/not_found").await) + .say(&ctx, lm.get(&language, "timer/not_found")) .await; } } else { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "timer/help").await) + .say(&ctx, lm.get(&language, "timer/help")) .await; } } @@ -858,7 +887,7 @@ DELETE FROM timers WHERE owner = ? AND name = ? _ => { let _ = msg .channel_id - .say(&ctx, user_data.response(&pool, "timer/help").await) + .say(&ctx, lm.get(&language, "timer/help")) .await; } } @@ -1026,13 +1055,19 @@ async fn remind_command(ctx: &Context, msg: &Message, args: String, command: Rem } } - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); @@ -1092,9 +1127,8 @@ async fn remind_command(ctx: &Context, msg: &Message, args: String, command: Rem .flatten() .unwrap_or(0) as u64; - let str_response = user_data - .response(&pool, &response.to_response()) - .await + let str_response = lm + .get(&user_data.language, &response.to_response()) .replace( "{prefix}", &GuildData::prefix_from_id(msg.guild_id, &pool).await, @@ -1110,13 +1144,19 @@ async fn remind_command(ctx: &Context, msg: &Message, args: String, command: Rem #[command("natural")] #[permission_level(Managed)] async fn natural(ctx: &Context, msg: &Message, args: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let now = SystemTime::now(); let since_epoch = now @@ -1125,9 +1165,9 @@ async fn natural(ctx: &Context, msg: &Message, args: String) { let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); - let send_str = user_data.response(&pool, "natural/send").await; - let to_str = user_data.response(&pool, "natural/to").await; - let every_str = user_data.response(&pool, "natural/every").await; + let send_str = lm.get(&user_data.language, "natural/send"); + let to_str = lm.get(&user_data.language, "natural/to"); + let every_str = lm.get(&user_data.language, "natural/every"); let mut args_iter = args.splitn(2, &send_str); @@ -1241,9 +1281,8 @@ async fn natural(ctx: &Context, msg: &Message, args: String) { let offset = timestamp as u64 - since_epoch.as_secs(); - let str_response = user_data - .response(&pool, &res.to_response_natural()) - .await + let str_response = lm + .get(&user_data.language, &res.to_response_natural()) .replace( "{prefix}", &GuildData::prefix_from_id(msg.guild_id, &pool).await, @@ -1275,9 +1314,8 @@ async fn natural(ctx: &Context, msg: &Message, args: String) { } } - let content = user_data - .response(&pool, "natural/bulk_set") - .await + let content = lm + .get(&user_data.language, "natural/bulk_set") .replace("{}", &ok_count.to_string()); let _ = msg.channel_id.say(&ctx, content).await; @@ -1291,9 +1329,8 @@ async fn natural(ctx: &Context, msg: &Message, args: String) { } else { let prefix = GuildData::prefix_from_id(msg.guild_id, &pool).await; - let resp = user_data - .response(&pool, "natural/no_argument") - .await + let resp = lm + .get(&user_data.language, "natural/no_argument") .replace("{prefix}", &prefix); let _ = msg diff --git a/src/commands/todo_cmds.rs b/src/commands/todo_cmds.rs index f8eadbb..d4918e3 100644 --- a/src/commands/todo_cmds.rs +++ b/src/commands/todo_cmds.rs @@ -18,6 +18,7 @@ use crate::{ use sqlx::MySqlPool; use std::convert::TryFrom; +use crate::language_manager::LanguageManager; use async_trait::async_trait; #[derive(Debug)] @@ -235,13 +236,19 @@ DELETE FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guil } async fn execute(&self, ctx: &Context, msg: &Message, subcommand: SubCommand, extra: String) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); let prefix = GuildData::prefix_from_id(msg.guild_id, &pool).await; @@ -279,9 +286,8 @@ DELETE FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guil } SubCommand::Add => { - let content = user_data - .response(&pool, "todo/added") - .await + let content = lm + .get(&user_data.language, "todo/added") .replacen("{name}", &extra, 1); self.add(extra, pool).await.unwrap(); @@ -290,35 +296,35 @@ DELETE FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guil } SubCommand::Remove => { - let _ = if let Ok(num) = extra.parse::() { + if let Ok(num) = extra.parse::() { if let Ok(todo) = self.remove(num - 1, &pool).await { - let content = user_data.response(&pool, "todo/removed").await.replacen( + let content = lm.get(&user_data.language, "todo/removed").replacen( "{}", &todo.value, 1, ); - msg.channel_id.say(&ctx, content) + let _ = msg.channel_id.say(&ctx, content).await; } else { - msg.channel_id - .say(&ctx, user_data.response(&pool, "todo/error_index").await) + let _ = msg + .channel_id + .say(&ctx, lm.get(&user_data.language, "todo/error_index")) + .await; } } else { - let content = user_data - .response(&pool, "todo/error_value") - .await + let content = lm + .get(&user_data.language, "todo/error_value") .replacen("{prefix}", &prefix, 1) .replacen("{command}", &self.command(Some(subcommand)), 1); - msg.channel_id.say(&ctx, content) + let _ = msg.channel_id.say(&ctx, content).await; } - .await; } SubCommand::Clear => { self.clear(&pool).await.unwrap(); - let content = user_data.response(&pool, "todo/cleared").await; + let content = lm.get(&user_data.language, "todo/cleared"); let _ = msg.channel_id.say(&ctx, content).await; } @@ -435,20 +441,25 @@ async fn todo_guild(ctx: &Context, msg: &Message, args: String) { } async fn show_help(ctx: &Context, msg: &Message, target: Option) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); + let pool; + let lm; + + { + let data = ctx.data.read().await; + + pool = data + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + lm = data.get::().cloned().unwrap(); + } let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); let prefix = GuildData::prefix_from_id(msg.guild_id, &pool).await; - let content = user_data - .response(&pool, "todo/help") - .await + let content = lm + .get(&user_data.language, "todo/help") .replace("{prefix}", &prefix) .replace( "{command}", diff --git a/src/framework.rs b/src/framework.rs index d3bd94c..3c722cf 100644 --- a/src/framework.rs +++ b/src/framework.rs @@ -20,6 +20,7 @@ use regex::{Match, Regex, RegexBuilder}; use std::{collections::HashMap, fmt}; +use crate::language_manager::LanguageManager; use crate::models::{GuildData, UserData}; use crate::{models::ChannelData, SQLPool}; @@ -354,16 +355,16 @@ impl Framework for RegexFramework { { if let Some(full_match) = self.command_matcher.captures(&msg.content) { if check_prefix(&ctx, &guild, full_match.name("prefix")).await { - let pool = ctx - .data - .read() - .await + let data = ctx.data.read().await; + + let pool = data .get::() .cloned() .expect("Could not get SQLPool from data"); - let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); - let guild_data = GuildData::from_guild(guild.clone(), &pool).await.unwrap(); + let lm = data.get::().unwrap(); + + let language = UserData::language_of(&msg.author, &pool).await; match check_self_permissions(&ctx, &guild, &channel).await { Ok(perms) => match perms { @@ -380,9 +381,6 @@ impl Framework for RegexFramework { .await .unwrap(); - // required due to a small bug resulting in some channels being detached from their guild ids - channel_data.update_guild_id(guild_data.id, &pool).await; - if !command.can_blacklist || !channel_data.blacklisted { let args = full_match .name("args") @@ -398,29 +396,18 @@ impl Framework for RegexFramework { { let _ = msg .channel_id - .say( - &ctx, - user_data - .response(&pool, "no_perms_restricted") - .await, - ) + .say(&ctx, lm.get(&language, "no_perms_restricted")) .await; } else if command.required_perms == PermissionLevel::Managed { let _ = msg .channel_id .say( &ctx, - user_data - .response(&pool, "no_perms_managed") - .await - .replace( - "{prefix}", - &GuildData::prefix_from_id( - msg.guild_id, - &pool, - ) + lm.get(&language, "no_perms_managed").replace( + "{prefix}", + &GuildData::prefix_from_id(msg.guild_id, &pool) .await, - ), + ), ) .await; } @@ -428,9 +415,8 @@ impl Framework for RegexFramework { } PermissionCheck::Basic(manage_webhooks, embed_links) => { - let response = user_data - .response(&pool, "no_perms_general") - .await + let response = lm + .get(&language, "no_perms_general") .replace( "{manage_webhooks}", if manage_webhooks { "✅" } else { "❌" }, diff --git a/src/language_manager.rs b/src/language_manager.rs index fa861f1..ce493cc 100644 --- a/src/language_manager.rs +++ b/src/language_manager.rs @@ -1,12 +1,10 @@ -use std::collections::HashMap; - use serde::Deserialize; -use serde_json::from_reader; +use serde_json::from_str; use serenity::prelude::TypeMapKey; -use std::error::Error; -use std::fs::File; -use std::io::BufReader; -use std::path::Path; + +use std::{collections::HashMap, error::Error, sync::Arc}; + +use crate::consts::LOCAL_LANGUAGE; #[derive(Deserialize)] pub struct LanguageManager { @@ -15,34 +13,41 @@ pub struct LanguageManager { } impl LanguageManager { - pub(crate) fn from_compiled

(path: P) -> Result> - where - P: AsRef, - { - let file = File::open(path)?; - let reader = BufReader::new(file); - - let new: Self = from_reader(reader)?; + pub fn from_compiled(content: &'static str) -> Result> { + let new: Self = from_str(content.as_ref())?; Ok(new) } - pub(crate) fn get(&self, language: &str, name: &'static str) -> &str { + pub fn get(&self, language: &str, name: &str) -> &str { self.strings .get(language) .map(|sm| sm.get(name)) .expect(&format!(r#"Language does not exist: "{}""#, language)) - .expect(&format!(r#"String does not exist: "{}""#, name)) + .unwrap_or_else(|| { + self.strings + .get(&*LOCAL_LANGUAGE) + .map(|sm| { + sm.get(name) + .expect(&format!(r#"String does not exist: "{}""#, name)) + }) + .expect("LOCAL_LANGUAGE is not available") + }) } - fn all_languages(&self) -> Vec<(&str, &str)> { + pub fn get_language(&self, language: &str) -> Option<&str> { self.languages .iter() - .map(|(k, v)| (k.as_str(), v.as_str())) - .collect() + .filter(|(k, v)| k.to_lowercase() == language || v.to_lowercase() == language) + .map(|(k, _)| k.as_str()) + .next() + } + + pub fn all_languages(&self) -> impl Iterator { + self.languages.iter().map(|(k, v)| (k.as_str(), v.as_str())) } } impl TypeMapKey for LanguageManager { - type Value = Self; + type Value = Arc; } diff --git a/src/main.rs b/src/main.rs index ffe4671..4495dba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -231,14 +231,18 @@ async fn main() -> Result<(), Box> { .await .unwrap(); - let language_manager = LanguageManager::from_compiled("out.json")?; + let language_manager = LanguageManager::from_compiled(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/", + env!("STRINGS_FILE") + )))?; let mut data = client.data.write().await; data.insert::(pool); data.insert::(Arc::new(reqwest::Client::new())); data.insert::(framework_arc); - data.insert::(language_manager) + data.insert::(Arc::new(language_manager)) } if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| { diff --git a/src/models.rs b/src/models.rs index f223ea4..df92c79 100644 --- a/src/models.rs +++ b/src/models.rs @@ -158,19 +158,6 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u } } - pub async fn update_guild_id(&self, id: u32, pool: &MySqlPool) { - sqlx::query!( - " -UPDATE channels SET guild_id = ? WHERE id = ? - ", - id, - self.id - ) - .execute(pool) - .await - .unwrap(); - } - pub async fn commit_changes(&self, pool: &MySqlPool) { sqlx::query!( " @@ -191,7 +178,7 @@ pub struct UserData { } impl UserData { - pub async fn language_of(user: &User, ctx: impl CacheHttp, pool: &MySqlPool) -> String { + pub async fn language_of(user: &User, pool: &MySqlPool) -> String { let user_id = user.id.as_u64().to_owned(); match sqlx::query!( @@ -285,10 +272,6 @@ UPDATE users SET name = ?, language = ?, timezone = ? WHERE id = ? .unwrap(); } - pub async fn response(&self, _pool: &MySqlPool, _name: &str) -> String { - unimplemented!() - } - pub fn timezone(&self) -> Tz { self.timezone.parse().unwrap() }