WIP: Email localization (fixes #500) (#2053)

* Allow email localization (fixes #500)

* add PersonAggregates::default()

* add lemmy-translations submodule

* fix gitmodules
This commit is contained in:
Nutomic 2022-03-24 15:25:51 +00:00 committed by GitHub
parent ecd157d4a7
commit cb44b14717
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 133 additions and 78 deletions

View File

@ -14,6 +14,8 @@ steps:
commands: commands:
- chown 1000:1000 . -R - chown 1000:1000 . -R
- git fetch --tags - git fetch --tags
- git submodule init
- git submodule update --recursive --remote
- name: check formatting - name: check formatting
image: rustdocker/rust:nightly image: rustdocker/rust:nightly

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "crates/utils/translations"]
path = crates/utils/translations
url = https://github.com/LemmyNet/lemmy-translations.git
branch = main

29
Cargo.lock generated
View File

@ -1884,6 +1884,7 @@ dependencies = [
"lemmy_db_views_actor", "lemmy_db_views_actor",
"lemmy_db_views_moderator", "lemmy_db_views_moderator",
"lemmy_utils", "lemmy_utils",
"rosetta-i18n",
"serde", "serde",
"serde_json", "serde_json",
"tracing", "tracing",
@ -2170,6 +2171,8 @@ dependencies = [
"regex", "regex",
"reqwest", "reqwest",
"reqwest-middleware", "reqwest-middleware",
"rosetta-build",
"rosetta-i18n",
"serde", "serde",
"serde_json", "serde_json",
"smart-default", "smart-default",
@ -3368,6 +3371,26 @@ dependencies = [
"winapi", "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]] [[package]]
name = "rss" name = "rss"
version = "2.0.0" version = "2.0.0"
@ -3902,6 +3925,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6"
[[package]]
name = "tinyjson"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a8304da9f9370f6a6f9020b7903b044aa9ce3470f300a1fba5bc77c78145a16"
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.5.1" version = "1.5.1"

View File

@ -189,17 +189,11 @@ impl Perform for SaveUserSettings {
let email = diesel_option_overwrite(&email_deref); let email = diesel_option_overwrite(&email_deref);
if let Some(Some(email)) = &email { 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 // Only send the verification email if there was an email change
if previous_email.ne(email) { if previous_email.ne(email) {
send_verification_email( send_verification_email(&local_user_view, email, context.pool(), &context.settings())
local_user_view.local_user.id, .await?;
email,
&local_user_view.person.name,
context.pool(),
&context.settings(),
)
.await?;
} }
} }

View File

@ -26,3 +26,4 @@ serde_json = { version = "1.0.72", features = ["preserve_order"] }
tracing = "0.1.29" tracing = "0.1.29"
url = "2.2.2" url = "2.2.2"
itertools = "0.10.3" itertools = "0.10.3"
rosetta-i18n = "0.1"

View File

@ -33,12 +33,14 @@ use lemmy_db_views_actor::{
}; };
use lemmy_utils::{ use lemmy_utils::{
claims::Claims, claims::Claims,
email::send_email, email::{send_email, translations::Lang},
settings::structs::{FederationConfig, Settings}, settings::structs::{FederationConfig, Settings},
utils::generate_random_string, utils::generate_random_string,
LemmyError, LemmyError,
Sensitive, Sensitive,
}; };
use rosetta_i18n::{Language, LanguageId};
use tracing::warn;
use url::Url; use url::Url;
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError> pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
@ -363,9 +365,8 @@ pub fn honeypot_check(honeypot: &Option<String>) -> Result<(), LemmyError> {
pub fn send_email_to_user( pub fn send_email_to_user(
local_user_view: &LocalUserView, local_user_view: &LocalUserView,
subject_text: &str, subject: &str,
body_text: &str, body: &str,
comment_content: &str,
settings: &Settings, settings: &Settings,
) { ) {
if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email { 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 { 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!(
"<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
body_text,
local_user_view.person.name,
comment_content,
settings.get_protocol_and_hostname()
);
match send_email( match send_email(
subject, subject,
user_email, user_email,
&local_user_view.person.name, &local_user_view.person.name,
html, body,
settings, settings,
) { ) {
Ok(_o) => _o, Ok(_o) => _o,
Err(e) => tracing::error!("{}", e), Err(e) => warn!("{}", e),
}; };
} }
} }
pub async fn send_password_reset_email( pub async fn send_password_reset_email(
local_user_view: &LocalUserView, user: &LocalUserView,
pool: &DbPool, pool: &DbPool,
settings: &Settings, settings: &Settings,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
@ -407,29 +397,30 @@ pub async fn send_password_reset_email(
// Insert the row // Insert the row
let token2 = token.clone(); 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| { blocking(pool, move |conn| {
PasswordResetRequest::create_token(conn, local_user_id, &token2) PasswordResetRequest::create_token(conn, local_user_id, &token2)
}) })
.await??; .await??;
let email = &local_user_view.local_user.email.to_owned().expect("email"); let email = &user.local_user.email.to_owned().expect("email");
let subject = &format!("Password reset for {}", local_user_view.person.name); 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 protocol_and_hostname = settings.get_protocol_and_hostname();
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", local_user_view.person.name, protocol_and_hostname, &token); let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token);
send_email(subject, email, &local_user_view.person.name, html, settings) let body = &lang.password_reset_body(&user.person.name, reset_link);
send_email(subject, email, &user.person.name, body, settings)
} }
/// Send a verification email /// Send a verification email
pub async fn send_verification_email( pub async fn send_verification_email(
local_user_id: LocalUserId, user: &LocalUserView,
new_email: &str, new_email: &str,
username: &str,
pool: &DbPool, pool: &DbPool,
settings: &Settings, settings: &Settings,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let form = EmailVerificationForm { let form = EmailVerificationForm {
local_user_id, local_user_id: user.local_user.id,
email: new_email.to_string(), email: new_email.to_string(),
verification_token: generate_random_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??; blocking(pool, move |conn| EmailVerification::create(conn, &form)).await??;
let subject = format!("Verify your email address for {}", settings.hostname); let lang = get_user_lang(user);
let body = format!( let subject = lang.verify_email_subject(&settings.hostname);
concat!( let body = lang.verify_email_body(&user.person.name, &settings.hostname, verify_link);
"Please click the link below to verify your email address ", send_email(&subject, new_email, &user.person.name, &body, settings)?;
"for the account @{}@{}. Ignore this email if the account isn't yours.<br><br>",
"<a href=\"{}\">Verify your email</a>"
),
username, settings.hostname, verify_link
);
send_email(&subject, new_email, username, &body, settings)?;
Ok(()) Ok(())
} }
pub fn send_email_verification_success( pub fn send_email_verification_success(
local_user_view: &LocalUserView, user: &LocalUserView,
settings: &Settings, settings: &Settings,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let email = &local_user_view.local_user.email.to_owned().expect("email"); let email = &user.local_user.email.to_owned().expect("email");
let subject = &format!("Email verified for {}", local_user_view.person.actor_id); let lang = get_user_lang(user);
let html = "Your email has been verified."; let subject = &lang.email_verified_subject(&user.person.actor_id);
send_email(subject, email, &local_user_view.person.name, html, settings) 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( pub fn send_application_approved_email(
local_user_view: &LocalUserView, user: &LocalUserView,
settings: &Settings, settings: &Settings,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let email = &local_user_view.local_user.email.to_owned().expect("email"); let email = &user.local_user.email.to_owned().expect("email");
let subject = &format!( let lang = get_user_lang(user);
"Registration approved for {}", let subject = lang.registration_approved_subject(&user.person.actor_id);
local_user_view.person.actor_id let body = lang.registration_approved_body(&settings.hostname);
); send_email(&subject, email, &user.person.name, &body, settings)
let html = &format!(
"Your registration application has been approved. Welcome to {}!",
settings.hostname
);
send_email(subject, email, &local_user_view.person.name, html, settings)
} }
pub async fn check_registration_application( pub async fn check_registration_application(

View File

@ -4,6 +4,7 @@ use lemmy_api_common::{
blocking, blocking,
check_person_block, check_person_block,
get_local_user_view_from_jwt, get_local_user_view_from_jwt,
get_user_lang,
person::{CreatePrivateMessage, PrivateMessageResponse}, person::{CreatePrivateMessage, PrivateMessageResponse},
send_email_to_user, send_email_to_user,
}; };
@ -106,11 +107,16 @@ impl PerformCrud for CreatePrivateMessage {
LocalUserView::read_person(conn, recipient_id) LocalUserView::read_person(conn, recipient_id)
}) })
.await??; .await??;
let lang = get_user_lang(&local_recipient);
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
send_email_to_user( send_email_to_user(
&local_recipient, &local_recipient,
"Private Message from", &lang.notification_mentioned_by_subject(&local_recipient.person.name),
"Private Message", &lang.notification_mentioned_by_body(
&content_slurs_removed, &local_recipient.person.name,
&content_slurs_removed,
&inbox_link,
),
&context.settings(), &context.settings(),
); );
} }

View File

@ -15,6 +15,7 @@ use lemmy_apub::{
EndpointType, EndpointType,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::person_aggregates::PersonAggregates,
newtypes::CommunityId, newtypes::CommunityId,
source::{ source::{
community::{ community::{
@ -32,6 +33,7 @@ use lemmy_db_schema::{
}, },
traits::{Crud, Followable, Joinable}, traits::{Crud, Followable, Joinable},
}; };
use lemmy_db_views::local_user_view::LocalUserView;
use lemmy_db_views_actor::person_view::PersonViewSafe; use lemmy_db_views_actor::person_view::PersonViewSafe;
use lemmy_utils::{ use lemmy_utils::{
apub::generate_actor_keypair, apub::generate_actor_keypair,
@ -272,11 +274,20 @@ impl PerformCrud for Register {
); );
} else { } else {
if email_verification { 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( send_verification_email(
inserted_local_user.id, &local_user_view,
// we check at the beginning of this method that email is set &email,
&inserted_local_user.email.expect("email was provided"),
&inserted_person.name,
context.pool(), context.pool(),
&context.settings(), &context.settings(),
) )

View File

@ -3,7 +3,7 @@ use diesel::{result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive( #[derive(
Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone, Default,
)] )]
#[table_name = "person_aggregates"] #[table_name = "person_aggregates"]
pub struct PersonAggregates { pub struct PersonAggregates {

View File

@ -47,3 +47,7 @@ doku = "0.10.2"
uuid = { version = "0.8.2", features = ["serde", "v4"] } uuid = { version = "0.8.2", features = ["serde", "v4"] }
encoding = "0.2.33" encoding = "0.2.33"
html2text = "0.2.1" html2text = "0.2.1"
rosetta-i18n = "0.1"
[build-dependencies]
rosetta-build = "0.1"

8
crates/utils/build.rs Normal file
View File

@ -0,0 +1,8 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
rosetta_build::config()
.source("en", "translations/email/en.json")
.fallback("en")
.generate()?;
Ok(())
}

View File

@ -11,6 +11,10 @@ use lettre::{
use std::str::FromStr; use std::str::FromStr;
use uuid::Uuid; use uuid::Uuid;
pub mod translations {
rosetta_i18n::include_translations!();
}
pub fn send_email( pub fn send_email(
subject: &str, subject: &str,
to_email: &str, to_email: &str,

@ -0,0 +1 @@
Subproject commit 1314f10fbc0db9c16ff4209a2885431024a14ed8

View File

@ -8,6 +8,7 @@ use lemmy_api_common::{
check_person_block, check_person_block,
comment::CommentResponse, comment::CommentResponse,
community::CommunityResponse, community::CommunityResponse,
get_user_lang,
person::PrivateMessageResponse, person::PrivateMessageResponse,
post::PostResponse, post::PostResponse,
send_email_to_user, send_email_to_user,
@ -183,6 +184,7 @@ pub async fn send_local_notifs(
context: &LemmyContext, context: &LemmyContext,
) -> Result<Vec<LocalUserId>, LemmyError> { ) -> Result<Vec<LocalUserId>, LemmyError> {
let mut recipient_ids = Vec::new(); let mut recipient_ids = Vec::new();
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
// Send the local mentions // Send the local mentions
for mention in 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 // Send an email to those local users that have notifications on
if do_send_email { if do_send_email {
let lang = get_user_lang(&mention_user_view);
send_email_to_user( send_email_to_user(
&mention_user_view, &mention_user_view,
"Mentioned by", &lang.notification_mentioned_by_subject(&person.name),
"Person Mention", &lang.notification_mentioned_by_body(&person.name, &comment.content, &inbox_link),
&comment.content,
&context.settings(), &context.settings(),
) )
} }
@ -252,11 +254,11 @@ pub async fn send_local_notifs(
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);
if do_send_email { if do_send_email {
let lang = get_user_lang(&parent_user_view);
send_email_to_user( send_email_to_user(
&parent_user_view, &parent_user_view,
"Reply from", &lang.notification_post_reply_subject(&person.name),
"Comment Reply", &lang.notification_post_reply_body(&person.name, &comment.content, &inbox_link),
&comment.content,
&context.settings(), &context.settings(),
) )
} }
@ -282,11 +284,11 @@ pub async fn send_local_notifs(
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);
if do_send_email { if do_send_email {
let lang = get_user_lang(&parent_user_view);
send_email_to_user( send_email_to_user(
&parent_user_view, &parent_user_view,
"Reply from", &lang.notification_post_reply_subject(&person.name),
"Post Reply", &lang.notification_post_reply_body(&person.name, &comment.content, &inbox_link),
&comment.content,
&context.settings(), &context.settings(),
) )
} }