Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Email localization (fixes #500) #2053

Merged
merged 4 commits into from
Mar 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "crates/utils/translations"]
path = crates/utils/translations
url = https://github.com/LemmyNet/lemmy-translations.git
dessalines marked this conversation as resolved.
Show resolved Hide resolved
branch = main
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 3 additions & 9 deletions crates/api/src/local_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/api_common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
93 changes: 41 additions & 52 deletions crates/api_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -363,42 +365,30 @@ 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 {
return;
}

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> {
Expand All @@ -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(),
};
Expand All @@ -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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this doesn't work correctly for things like pt-br, we might have to unfortunately create mappings. But lets not worry about that yet.

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(
Expand Down
12 changes: 9 additions & 3 deletions crates/api_crud/src/private_message/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was confused at first, but yes this does make sense, its their lemmy-ui inbox page.

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(),
);
}
Expand Down
19 changes: 15 additions & 4 deletions crates/api_crud/src/user/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use lemmy_apub::{
EndpointType,
};
use lemmy_db_schema::{
aggregates::person_aggregates::PersonAggregates,
newtypes::CommunityId,
source::{
community::{
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, iirc email is only in LocalUser, not the LocalUserSettings or Safe.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean, local_user_view contains LocalUser. And I dont think i changed any of the logic.

context.pool(),
&context.settings(),
)
Expand Down
2 changes: 1 addition & 1 deletion crates/db_schema/src/aggregates/person_aggregates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions crates/utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 8 additions & 0 deletions crates/utils/build.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
4 changes: 4 additions & 0 deletions crates/utils/src/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/utils/translations
Submodule translations added at 1314f1
Loading