From 3deaf0c9fe8471e3600cf5381a3b697785e5a202 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 20 Jan 2022 23:34:43 +0100 Subject: [PATCH 1/4] Allow email localization (fixes #500) --- Cargo.lock | 29 ++++++ crates/api/src/local_user.rs | 12 +-- crates/api_common/Cargo.toml | 1 + crates/api_common/src/lib.rs | 93 ++++++++----------- crates/api_crud/src/private_message/create.rs | 12 ++- crates/api_crud/src/user/create.rs | 26 +++++- crates/utils/Cargo.toml | 4 + crates/utils/build.rs | 8 ++ crates/utils/src/email.rs | 4 + crates/utils/translations/en.json | 18 ++++ crates/websocket/src/send.rs | 20 ++-- 11 files changed, 150 insertions(+), 77 deletions(-) create mode 100644 crates/utils/build.rs create mode 100644 crates/utils/translations/en.json diff --git a/Cargo.lock b/Cargo.lock index ad273ba184..1c9e75c3ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1884,6 +1884,7 @@ dependencies = [ "lemmy_db_views_actor", "lemmy_db_views_moderator", "lemmy_utils", + "rosetta-i18n", "serde", "serde_json", "tracing", @@ -2170,6 +2171,8 @@ dependencies = [ "regex", "reqwest", "reqwest-middleware", + "rosetta-build", + "rosetta-i18n", "serde", "serde_json", "smart-default", @@ -3368,6 +3371,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "rosetta-build" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f697b8b3f19bee20f30dc87213d05ce091c43bc733ab1bfc98b0e5cdd9943f3" +dependencies = [ + "convert_case", + "lazy_static", + "proc-macro2 1.0.33", + "quote 1.0.10", + "regex", + "tinyjson", +] + +[[package]] +name = "rosetta-i18n" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5298de832602aecc9458398f435d9bff0be57da7aac11221b6ff3d4ef9503de" + [[package]] name = "rss" version = "2.0.0" @@ -3902,6 +3925,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" +[[package]] +name = "tinyjson" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a8304da9f9370f6a6f9020b7903b044aa9ce3470f300a1fba5bc77c78145a16" + [[package]] name = "tinyvec" version = "1.5.1" diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index 0b2a4c3aa1..0819c98f26 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -189,17 +189,11 @@ impl Perform for SaveUserSettings { let email = diesel_option_overwrite(&email_deref); if let Some(Some(email)) = &email { - let previous_email = local_user_view.local_user.email.unwrap_or_default(); + let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); // Only send the verification email if there was an email change if previous_email.ne(email) { - send_verification_email( - local_user_view.local_user.id, - email, - &local_user_view.person.name, - context.pool(), - &context.settings(), - ) - .await?; + send_verification_email(&local_user_view, email, context.pool(), &context.settings()) + .await?; } } diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index 0b41519131..7d239a64da 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -26,3 +26,4 @@ serde_json = { version = "1.0.72", features = ["preserve_order"] } tracing = "0.1.29" url = "2.2.2" itertools = "0.10.3" +rosetta-i18n = "0.1" diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 68ad367448..50919b3cb6 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -33,12 +33,14 @@ use lemmy_db_views_actor::{ }; use lemmy_utils::{ claims::Claims, - email::send_email, + email::{send_email, translations::Lang}, settings::structs::{FederationConfig, Settings}, utils::generate_random_string, LemmyError, Sensitive, }; +use rosetta_i18n::{Language, LanguageId}; +use tracing::warn; use url::Url; pub async fn blocking(pool: &DbPool, f: F) -> Result @@ -363,9 +365,8 @@ pub fn honeypot_check(honeypot: &Option) -> Result<(), LemmyError> { pub fn send_email_to_user( local_user_view: &LocalUserView, - subject_text: &str, - body_text: &str, - comment_content: &str, + subject: &str, + body: &str, settings: &Settings, ) { if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email { @@ -373,32 +374,21 @@ pub fn send_email_to_user( } if let Some(user_email) = &local_user_view.local_user.email { - let subject = &format!( - "{} - {} {}", - subject_text, settings.hostname, local_user_view.person.name, - ); - let html = &format!( - "

{}


{} - {}

inbox", - body_text, - local_user_view.person.name, - comment_content, - settings.get_protocol_and_hostname() - ); match send_email( subject, user_email, &local_user_view.person.name, - html, + body, settings, ) { Ok(_o) => _o, - Err(e) => tracing::error!("{}", e), + Err(e) => warn!("{}", e), }; } } pub async fn send_password_reset_email( - local_user_view: &LocalUserView, + user: &LocalUserView, pool: &DbPool, settings: &Settings, ) -> Result<(), LemmyError> { @@ -407,29 +397,30 @@ pub async fn send_password_reset_email( // Insert the row let token2 = token.clone(); - let local_user_id = local_user_view.local_user.id; + let local_user_id = user.local_user.id; blocking(pool, move |conn| { PasswordResetRequest::create_token(conn, local_user_id, &token2) }) .await??; - let email = &local_user_view.local_user.email.to_owned().expect("email"); - let subject = &format!("Password reset for {}", local_user_view.person.name); + let email = &user.local_user.email.to_owned().expect("email"); + let lang = get_user_lang(user); + let subject = &lang.password_reset_subject(&user.person.name); let protocol_and_hostname = settings.get_protocol_and_hostname(); - let html = &format!("

Password Reset Request for {}


Click here to reset your password", local_user_view.person.name, protocol_and_hostname, &token); - send_email(subject, email, &local_user_view.person.name, html, settings) + let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token); + let body = &lang.password_reset_body(&user.person.name, reset_link); + send_email(subject, email, &user.person.name, body, settings) } /// Send a verification email pub async fn send_verification_email( - local_user_id: LocalUserId, + user: &LocalUserView, new_email: &str, - username: &str, pool: &DbPool, settings: &Settings, ) -> Result<(), LemmyError> { let form = EmailVerificationForm { - local_user_id, + local_user_id: user.local_user.id, email: new_email.to_string(), verification_token: generate_random_string(), }; @@ -440,44 +431,42 @@ pub async fn send_verification_email( ); blocking(pool, move |conn| EmailVerification::create(conn, &form)).await??; - let subject = format!("Verify your email address for {}", settings.hostname); - let body = format!( - concat!( - "Please click the link below to verify your email address ", - "for the account @{}@{}. Ignore this email if the account isn't yours.

", - "Verify your email" - ), - username, settings.hostname, verify_link - ); - send_email(&subject, new_email, username, &body, settings)?; + let lang = get_user_lang(user); + let subject = lang.verify_email_subject(&settings.hostname); + let body = lang.verify_email_body(&user.person.name, &settings.hostname, verify_link); + send_email(&subject, new_email, &user.person.name, &body, settings)?; Ok(()) } pub fn send_email_verification_success( - local_user_view: &LocalUserView, + user: &LocalUserView, settings: &Settings, ) -> Result<(), LemmyError> { - let email = &local_user_view.local_user.email.to_owned().expect("email"); - let subject = &format!("Email verified for {}", local_user_view.person.actor_id); - let html = "Your email has been verified."; - send_email(subject, email, &local_user_view.person.name, html, settings) + let email = &user.local_user.email.to_owned().expect("email"); + let lang = get_user_lang(user); + let subject = &lang.email_verified_subject(&user.person.actor_id); + let body = &lang.email_verified_body(); + send_email(subject, email, &user.person.name, body, settings) +} + +pub fn get_user_lang(user: &LocalUserView) -> Lang { + let user_lang = LanguageId::new(user.local_user.lang.clone()); + Lang::from_language_id(&user_lang).unwrap_or_else(|| { + let en = LanguageId::new("en"); + Lang::from_language_id(&en).expect("default language") + }) } pub fn send_application_approved_email( - local_user_view: &LocalUserView, + user: &LocalUserView, settings: &Settings, ) -> Result<(), LemmyError> { - let email = &local_user_view.local_user.email.to_owned().expect("email"); - let subject = &format!( - "Registration approved for {}", - local_user_view.person.actor_id - ); - let html = &format!( - "Your registration application has been approved. Welcome to {}!", - settings.hostname - ); - send_email(subject, email, &local_user_view.person.name, html, settings) + let email = &user.local_user.email.to_owned().expect("email"); + let lang = get_user_lang(user); + let subject = lang.registration_approved_subject(&user.person.actor_id); + let body = lang.registration_approved_body(&settings.hostname); + send_email(&subject, email, &user.person.name, &body, settings) } pub async fn check_registration_application( diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index ad7fd4adf9..44999cf005 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -4,6 +4,7 @@ use lemmy_api_common::{ blocking, check_person_block, get_local_user_view_from_jwt, + get_user_lang, person::{CreatePrivateMessage, PrivateMessageResponse}, send_email_to_user, }; @@ -106,11 +107,16 @@ impl PerformCrud for CreatePrivateMessage { LocalUserView::read_person(conn, recipient_id) }) .await??; + let lang = get_user_lang(&local_recipient); + let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); send_email_to_user( &local_recipient, - "Private Message from", - "Private Message", - &content_slurs_removed, + &lang.notification_mentioned_by_subject(&local_recipient.person.name), + &lang.notification_mentioned_by_body( + &local_recipient.person.name, + &content_slurs_removed, + &inbox_link, + ), &context.settings(), ); } diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index be746d2ae3..a74e87bfc2 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -15,6 +15,7 @@ use lemmy_apub::{ EndpointType, }; use lemmy_db_schema::{ + aggregates::person_aggregates::PersonAggregates, newtypes::CommunityId, source::{ community::{ @@ -32,6 +33,7 @@ use lemmy_db_schema::{ }, traits::{Crud, Followable, Joinable}, }; +use lemmy_db_views::local_user_view::LocalUserView; use lemmy_db_views_actor::person_view::PersonViewSafe; use lemmy_utils::{ apub::generate_actor_keypair, @@ -272,11 +274,27 @@ impl PerformCrud for Register { ); } else { if email_verification { + let local_user_view = LocalUserView { + local_user: inserted_local_user, + person: inserted_person, + counts: PersonAggregates { + id: 0, + person_id: Default::default(), + post_count: 0, + post_score: 0, + comment_count: 0, + comment_score: 0, + }, + }; + // we check at the beginning of this method that email is set + let email = local_user_view + .local_user + .email + .clone() + .expect("email was provided"); send_verification_email( - inserted_local_user.id, - // we check at the beginning of this method that email is set - &inserted_local_user.email.expect("email was provided"), - &inserted_person.name, + &local_user_view, + &email, context.pool(), &context.settings(), ) diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 7391bcbadb..3aa96d970d 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -47,3 +47,7 @@ doku = "0.10.2" uuid = { version = "0.8.2", features = ["serde", "v4"] } encoding = "0.2.33" html2text = "0.2.1" +rosetta-i18n = "0.1" + +[build-dependencies] +rosetta-build = "0.1" diff --git a/crates/utils/build.rs b/crates/utils/build.rs new file mode 100644 index 0000000000..8a6749d8be --- /dev/null +++ b/crates/utils/build.rs @@ -0,0 +1,8 @@ +fn main() -> Result<(), Box> { + rosetta_build::config() + .source("en", "translations/en.json") + .fallback("en") + .generate()?; + + Ok(()) +} diff --git a/crates/utils/src/email.rs b/crates/utils/src/email.rs index dfd66436b6..b1d58c7ef7 100644 --- a/crates/utils/src/email.rs +++ b/crates/utils/src/email.rs @@ -11,6 +11,10 @@ use lettre::{ use std::str::FromStr; use uuid::Uuid; +pub mod translations { + rosetta_i18n::include_translations!(); +} + pub fn send_email( subject: &str, to_email: &str, diff --git a/crates/utils/translations/en.json b/crates/utils/translations/en.json new file mode 100644 index 0000000000..1ce2853a76 --- /dev/null +++ b/crates/utils/translations/en.json @@ -0,0 +1,18 @@ +{ + "registration_approved_subject": "Registration approved for {username}", + "registration_approved_body": "Your registration application has been approved. Welcome to {hostname}!", + "password_reset_subject": "Password reset for {username}", + "password_reset_body": "

Password Reset Request for {username}


Click here to reset your password", + "verify_email_subject": "Verify your email address for {hostname}", + "verify_email_body": "Please click the link below to verify your email address for the account @{username}@{hostname}. Ignore this email if the account isn't yours.

, Verify your email", + "email_verified_subject": "Email verified for {username}", + "email_verified_body": "Your email has been verified.", + "notification_post_reply_subject": "Reply from {username}", + "notification_post_reply_body": "

Post reply


{username} - {comment_text}

inbox", + "notification_comment_reply_subject": "Reply from {username}", + "notification_comment_reply_body": "

Comment reply


{username} - {comment_text}

inbox", + "notification_mentioned_by_subject": "Mentioned by {username}", + "notification_mentioned_by_body": "

Person Mention


{username} - {comment_text}

inbox", + "notification_private_message_subject": "Private message from {username}", + "notification_private_message_body": "

Private message


{username} - {message_text}

inbox" +} diff --git a/crates/websocket/src/send.rs b/crates/websocket/src/send.rs index 36e93fb695..1f0677d7d6 100644 --- a/crates/websocket/src/send.rs +++ b/crates/websocket/src/send.rs @@ -8,6 +8,7 @@ use lemmy_api_common::{ check_person_block, comment::CommentResponse, community::CommunityResponse, + get_user_lang, person::PrivateMessageResponse, post::PostResponse, send_email_to_user, @@ -183,6 +184,7 @@ pub async fn send_local_notifs( context: &LemmyContext, ) -> Result, LemmyError> { let mut recipient_ids = Vec::new(); + let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); // Send the local mentions for mention in mentions @@ -217,11 +219,11 @@ pub async fn send_local_notifs( // Send an email to those local users that have notifications on if do_send_email { + let lang = get_user_lang(&mention_user_view); send_email_to_user( &mention_user_view, - "Mentioned by", - "Person Mention", - &comment.content, + &lang.notification_mentioned_by_subject(&person.name), + &lang.notification_mentioned_by_body(&person.name, &comment.content, &inbox_link), &context.settings(), ) } @@ -252,11 +254,11 @@ pub async fn send_local_notifs( recipient_ids.push(parent_user_view.local_user.id); if do_send_email { + let lang = get_user_lang(&parent_user_view); send_email_to_user( &parent_user_view, - "Reply from", - "Comment Reply", - &comment.content, + &lang.notification_post_reply_subject(&person.name), + &lang.notification_post_reply_body(&person.name, &comment.content, &inbox_link), &context.settings(), ) } @@ -282,11 +284,11 @@ pub async fn send_local_notifs( recipient_ids.push(parent_user_view.local_user.id); if do_send_email { + let lang = get_user_lang(&parent_user_view); send_email_to_user( &parent_user_view, - "Reply from", - "Post Reply", - &comment.content, + &lang.notification_post_reply_subject(&person.name), + &lang.notification_post_reply_body(&person.name, &comment.content, &inbox_link), &context.settings(), ) } From 3723428c69b2162d667bbddec50f2e6fc63b5343 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 21 Mar 2022 13:09:04 +0100 Subject: [PATCH 2/4] add PersonAggregates::default() --- crates/api_crud/src/user/create.rs | 9 +-------- .../db_schema/src/aggregates/person_aggregates.rs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index a74e87bfc2..00ef7db64f 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -277,14 +277,7 @@ impl PerformCrud for Register { let local_user_view = LocalUserView { local_user: inserted_local_user, person: inserted_person, - counts: PersonAggregates { - id: 0, - person_id: Default::default(), - post_count: 0, - post_score: 0, - comment_count: 0, - comment_score: 0, - }, + counts: PersonAggregates::default(), }; // we check at the beginning of this method that email is set let email = local_user_view diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index 344ec27d9a..1992875762 100644 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_aggregates.rs @@ -23,6 +23,19 @@ impl PersonAggregates { } } +impl Default for PersonAggregates { + fn default() -> Self { + PersonAggregates { + id: 0, + person_id: Default::default(), + post_count: 0, + post_score: 0, + comment_count: 0, + comment_score: 0 + } + } +} + #[cfg(test)] mod tests { use crate::{ From 46901fde593bfc22a5dea75852babdadf4482e0f Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 21 Mar 2022 13:14:23 +0100 Subject: [PATCH 3/4] add lemmy-translations submodule --- .drone.yml | 2 ++ .gitmodules | 3 +++ .../src/aggregates/person_aggregates.rs | 15 +-------------- crates/utils/build.rs | 2 +- crates/utils/translations | 1 + crates/utils/translations/en.json | 18 ------------------ 6 files changed, 8 insertions(+), 33 deletions(-) create mode 100644 .gitmodules create mode 160000 crates/utils/translations delete mode 100644 crates/utils/translations/en.json diff --git a/.drone.yml b/.drone.yml index 001714545f..311a116ccb 100644 --- a/.drone.yml +++ b/.drone.yml @@ -14,6 +14,8 @@ steps: commands: - chown 1000:1000 . -R - git fetch --tags + - git submodule init + - git submodule update --recursive --remote - name: check formatting image: rustdocker/rust:nightly diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..4479b0734c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crates/utils/translations"] + path = crates/utils/translations + url = https://github.com/LemmyNet/lemmy-translations.git diff --git a/crates/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index 1992875762..e0fc0734c3 100644 --- a/crates/db_schema/src/aggregates/person_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_aggregates.rs @@ -3,7 +3,7 @@ use diesel::{result::Error, *}; use serde::{Deserialize, Serialize}; #[derive( - Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone, + Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone, Default, )] #[table_name = "person_aggregates"] pub struct PersonAggregates { @@ -23,19 +23,6 @@ impl PersonAggregates { } } -impl Default for PersonAggregates { - fn default() -> Self { - PersonAggregates { - id: 0, - person_id: Default::default(), - post_count: 0, - post_score: 0, - comment_count: 0, - comment_score: 0 - } - } -} - #[cfg(test)] mod tests { use crate::{ diff --git a/crates/utils/build.rs b/crates/utils/build.rs index 8a6749d8be..8fcef5c8fd 100644 --- a/crates/utils/build.rs +++ b/crates/utils/build.rs @@ -1,6 +1,6 @@ fn main() -> Result<(), Box> { rosetta_build::config() - .source("en", "translations/en.json") + .source("en", "translations/email/en.json") .fallback("en") .generate()?; diff --git a/crates/utils/translations b/crates/utils/translations new file mode 160000 index 0000000000..1314f10fbc --- /dev/null +++ b/crates/utils/translations @@ -0,0 +1 @@ +Subproject commit 1314f10fbc0db9c16ff4209a2885431024a14ed8 diff --git a/crates/utils/translations/en.json b/crates/utils/translations/en.json deleted file mode 100644 index 1ce2853a76..0000000000 --- a/crates/utils/translations/en.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "registration_approved_subject": "Registration approved for {username}", - "registration_approved_body": "Your registration application has been approved. Welcome to {hostname}!", - "password_reset_subject": "Password reset for {username}", - "password_reset_body": "

Password Reset Request for {username}


Click here to reset your password", - "verify_email_subject": "Verify your email address for {hostname}", - "verify_email_body": "Please click the link below to verify your email address for the account @{username}@{hostname}. Ignore this email if the account isn't yours.

, Verify your email", - "email_verified_subject": "Email verified for {username}", - "email_verified_body": "Your email has been verified.", - "notification_post_reply_subject": "Reply from {username}", - "notification_post_reply_body": "

Post reply


{username} - {comment_text}

inbox", - "notification_comment_reply_subject": "Reply from {username}", - "notification_comment_reply_body": "

Comment reply


{username} - {comment_text}

inbox", - "notification_mentioned_by_subject": "Mentioned by {username}", - "notification_mentioned_by_body": "

Person Mention


{username} - {comment_text}

inbox", - "notification_private_message_subject": "Private message from {username}", - "notification_private_message_body": "

Private message


{username} - {message_text}

inbox" -} From 53f2399e21bafa873b804a345342a7c1b1f34ca1 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Wed, 23 Mar 2022 22:12:47 +0100 Subject: [PATCH 4/4] fix gitmodules --- .gitmodules | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitmodules b/.gitmodules index 4479b0734c..f673c7acb9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "crates/utils/translations"] path = crates/utils/translations url = https://github.com/LemmyNet/lemmy-translations.git + branch = main