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:
- chown 1000:1000 . -R
- git fetch --tags
- git submodule init
- git submodule update --recursive --remote
- name: check formatting
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_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"

View file

@ -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?;
}
}

View file

@ -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"

View file

@ -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<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(
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!(
"<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(
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!("<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);
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.<br><br>",
"<a href=\"{}\">Verify your email</a>"
),
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(

View file

@ -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(),
);
}

View file

@ -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(),
)

View file

@ -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 {

View file

@ -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"

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 uuid::Uuid;
pub mod translations {
rosetta_i18n::include_translations!();
}
pub fn send_email(
subject: &str,
to_email: &str,

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

View file

@ -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<Vec<LocalUserId>, 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(),
)
}