From cb44b1471705de503e609fbd835fca2a77cce2db Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 24 Mar 2022 15:25:51 +0000 Subject: [PATCH] WIP: Email localization (fixes #500) (#2053) * Allow email localization (fixes #500) * add PersonAggregates::default() * add lemmy-translations submodule * fix gitmodules --- .drone.yml | 2 + .gitmodules | 4 + 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 | 19 +++- .../src/aggregates/person_aggregates.rs | 2 +- crates/utils/Cargo.toml | 4 + crates/utils/build.rs | 8 ++ crates/utils/src/email.rs | 4 + crates/utils/translations | 1 + crates/websocket/src/send.rs | 20 ++-- 14 files changed, 133 insertions(+), 78 deletions(-) create mode 100644 .gitmodules create mode 100644 crates/utils/build.rs create mode 160000 crates/utils/translations diff --git a/.drone.yml b/.drone.yml index 001714545..311a116cc 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 000000000..f673c7acb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "crates/utils/translations"] + path = crates/utils/translations + url = https://github.com/LemmyNet/lemmy-translations.git + branch = main diff --git a/Cargo.lock b/Cargo.lock index ad273ba18..1c9e75c3c 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 0b2a4c3aa..0819c98f2 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 0b4151913..7d239a64d 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 68ad36744..50919b3cb 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 ad7fd4adf..44999cf00 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 be746d2ae..00ef7db64 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,20 @@ impl PerformCrud for Register { ); } else { if email_verification { + let local_user_view = LocalUserView { + local_user: inserted_local_user, + person: inserted_person, + counts: PersonAggregates::default(), + }; + // 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/db_schema/src/aggregates/person_aggregates.rs b/crates/db_schema/src/aggregates/person_aggregates.rs index 344ec27d9..e0fc0734c 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 { diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 7391bcbad..3aa96d970 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 000000000..8fcef5c8f --- /dev/null +++ b/crates/utils/build.rs @@ -0,0 +1,8 @@ +fn main() -> Result<(), Box> { + rosetta_build::config() + .source("en", "translations/email/en.json") + .fallback("en") + .generate()?; + + Ok(()) +} diff --git a/crates/utils/src/email.rs b/crates/utils/src/email.rs index dfd66436b..b1d58c7ef 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 b/crates/utils/translations new file mode 160000 index 000000000..1314f10fb --- /dev/null +++ b/crates/utils/translations @@ -0,0 +1 @@ +Subproject commit 1314f10fbc0db9c16ff4209a2885431024a14ed8 diff --git a/crates/websocket/src/send.rs b/crates/websocket/src/send.rs index 36e93fb69..1f0677d7d 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(), ) }