Skip to content

Commit

Permalink
Merge pull request #17 from vklachkov/send-emails
Browse files Browse the repository at this point in the history
Рассылка писем новым участникам
  • Loading branch information
vklachkov authored Sep 25, 2024
2 parents 70d22c2 + dc1760f commit 85b324e
Show file tree
Hide file tree
Showing 8 changed files with 997 additions and 19 deletions.
643 changes: 640 additions & 3 deletions backend/Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.89"
argh = "0.1.12"
async-trait = "0.1.82"
axum = "0.7.5"
Expand All @@ -12,6 +13,7 @@ derive_more = { version = "1.0.0", features = ["full"] }
diesel = { version = "2.2.4", features = ["postgres", "serde_json"] }
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
rand = "0.8.5"
reqwest = { version = "0.12.7", features = ["json"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
thiserror = "1.0.63"
Expand Down
152 changes: 152 additions & 0 deletions backend/mail/code.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<!doctype html>
<html lang="ru">
<body>
<div
style='background-color:#E5E5E5;color:#242424;font-family:"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif;font-size:16px;font-weight:400;letter-spacing:0.15008px;line-height:1.5;margin:0;padding:32px 0;min-height:100%;width:100%'
>
<table
align="center"
width="100%"
style="margin:0 auto;max-width:600px;background-color:#FFFFFF;border-radius:16px"
role="presentation"
cellspacing="0"
cellpadding="0"
border="0"
>
<tbody>
<tr style="width:100%">
<td>
<div style="padding:16px 24px 16px 24px;text-align:center">
<img
alt="Sample product"
src="https://static.tildacdn.com/tild3930-3165-4663-a230-376435616636/noroot.png"
width="175"
style="width:175px;outline:none;border:none;text-decoration:none;vertical-align:middle;display:inline-block;max-width:100%"
/>
</div>
<div
style="font-size:24px;text-align:center;padding:0px 48px 0px 48px"
>
<b>Привет, <span style="color:#e81f29">NAME</span>!</b>
</div>
<div
style="font-weight:normal;text-align:center;padding:0px 48px 4px 48px"
>
<p>
Твоя заявка на участие в Космическом Чемпионате принята и
скоро будет рассмотрена кураторами конструкторских бюро
</p>
</div>
<div
style="font-weight:normal;text-align:center;padding:0px 48px 0px 48px"
>
Твой шифр:
</div>
<div style="padding:8px 0px 8px 0px">
<table
align="center"
width="100%"
cellpadding="0"
border="0"
style="table-layout:fixed;border-collapse:collapse"
>
<tbody style="width:100%">
<tr style="width:100%">
<td
style="box-sizing:content-box;vertical-align:middle;padding-left:0;padding-right:10.666666666666666px"
></td>
<td
style="box-sizing:content-box;vertical-align:middle;padding-left:5.333333333333333px;padding-right:5.333333333333333px"
>
<div
style="background-color:#F5F5F5;font-size:18px;font-weight:normal;text-align:center;padding:4px 0px 4px 0px"
>
<p><span style="color:#6b5693"><b>ЯЯ-0000</b></span></p>
</div>
</td>
<td
style="box-sizing:content-box;vertical-align:middle;padding-left:10.666666666666666px;padding-right:0"
></td>
</tr>
</tbody>
</table>
</div>
<div
style="font-size:16px;text-align:center;padding:16px 48px 8px 48px"
>
Он появится в документе с оценками в ближайшие несколько дней
</div>
<div
style="font-size:16px;text-align:center;padding:8px 48px 8px 48px"
>
Следи за статусом заявки
<a
href="https://drive.google.com/file/d/13ZYHIFoTiKnus_Lw4PJJe98pyjU5gLQM/view?usp=sharing"
>на сайте</a
>
</div>
<div style="padding:0px 24px 0px 24px">
<div style="padding:0px 0px 0px 0px">
<table
align="center"
width="100%"
cellpadding="0"
border="0"
style="table-layout:fixed;border-collapse:collapse"
>
<tbody style="width:100%">
<tr style="width:100%">
<td
style="box-sizing:content-box;vertical-align:middle;padding-left:0;padding-right:8px"
>
<div
style="padding:16px 8px 16px 8px;text-align:right"
>
<a
href="https://t.me/spacechampionship"
style="text-decoration:none"
target="_blank"
><img
alt="Vk"
src="https://i.ibb.co/Wn8V8Vc/vk-small.png"
width="50"
style="width:50px;outline:none;border:none;text-decoration:none;vertical-align:middle;display:inline-block;max-width:100%"
/></a>
</div>
</td>
<td
style="box-sizing:content-box;vertical-align:middle;padding-left:8px;padding-right:0"
>
<div style="padding:16px 8px 16px 8px">
<a
href="https://t.me/spacechampionship"
style="text-decoration:none"
target="_blank"
><img
alt="Telegram"
src="https://i.ibb.co/q0Dys67/tg-small.png"
width="50"
style="width:50px;outline:none;border:none;text-decoration:none;vertical-align:middle;display:inline-block;max-width:100%"
/></a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
style="color:#737373;font-weight:normal;text-align:center;padding:0px 48px 0px 48px"
>
<p>
Если письмо пришло вам по ошибке, напишите на почту
<a href="mailto:spacechamp@fedcdo.ru">spacechamp@fedcdo.ru</a>
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
75 changes: 75 additions & 0 deletions backend/scripts/send_participants_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import json
import sys
import requests

host = sys.argv[1]
login = sys.argv[2]
password = sys.argv[3]
notisend_token = sys.argv[4]

login_request = requests.post(f"{host}/api/v1/login", json={"name": login, "password": password})
if login_request.status_code != 200:
print(f"Failed to login. Status code: {login_request.status_code}")
sys.exit(1)

participants_request = requests.get(f"{host}/api/v1/org/participants", cookies=login_request.cookies)
if participants_request.status_code != 200:
print(f"Failed to fetch participants. Status code: {participants_request.status_code}")
sys.exit(1)

participants = json.loads(participants_request.content)

emails = {}

for participant in participants:
email = participant["info"]["email"]

if email in emails:
print(f"Дубликат: {email}")
continue

id = participant["id"]
code = participant["code"]

if id <= 60:
print(f"Письмо на почту {email} уже отправлено")
continue

name = participant["info"]["name"]
name = ' '.join(name.split(' ')[:-1])

emails[email] = {
"id": id,
"name": name,
"code": code,
}

for (email, info) in emails.items():
print(f"Отправка. Id {info["id"]}, Name {info["name"]}, Code {info["code"]}")

with open("../mail/code.html") as mail:
html = mail.read().replace("NAME", info["name"]).replace("ЯЯ-0000", info["code"])

body = {
"from_email": "info@spacechamp-org.ru",
"from_name": "Космический Чемпионат 2024",
"to": email,
"subject": "Заявка на Космический Чемпионат 2024",
"text": f"Твоя заявка на участие в Космическом Чемпионате принята! Твой шифр: {info["code"]}",
"html": html
}

send_response = requests.post(
"https://api.notisend.ru/v1/email/messages",
json=body,
headers={
'Authorization': f'Bearer {notisend_token}',
})

service_response = json.loads(send_response.content)

if "errors" in service_response:
for error in service_response["errors"]:
print(f"Ошибка при отправке. Код {error["code"]}: {error["detail"]}")
else:
print(f"Запрос на отправку доставлен. Статус сообщения: {service_response["status"]}")
103 changes: 98 additions & 5 deletions backend/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
datasource::{DataSource, DataSourceError},
domain::*,
};
use anyhow::{bail, Context};
use axum::{
extract::{Path, State},
http::StatusCode,
Expand All @@ -19,10 +20,15 @@ use std::{collections::HashMap, sync::Arc};

struct BackendState {
datasource: Arc<DataSource>,
tokens: Arc<BackendTokens>,
}

pub fn v1(datasource: Arc<DataSource>) -> Router {
let state = BackendState { datasource };
pub struct BackendTokens {
pub notisend: String,
}

pub fn v1(datasource: Arc<DataSource>, tokens: Arc<BackendTokens>) -> Router {
let state = BackendState { datasource, tokens };

Router::new()
.route("/login", post(login))
Expand Down Expand Up @@ -132,15 +138,101 @@ async fn new_application_webhook(
),
]);

match state.datasource.create_participant(info, answers).await {
Ok(()) => StatusCode::OK,
let id = match state.datasource.create_participant(info, answers).await {
Ok((id, _)) => id,
Err(err) => {
tracing::error!("Failed to create participant from webhook: {err}");
StatusCode::INTERNAL_SERVER_ERROR
return StatusCode::INTERNAL_SERVER_ERROR;
}
};

if let Ok(Some(participant)) = state.datasource.get_participant(id).await {
send_email_with_code(state, participant).await;
}

StatusCode::OK
}

async fn send_email_with_code(state: Arc<BackendState>, participant: Participant) {
match send_email(&state.tokens.notisend, &participant).await {
Ok(()) => {
tracing::info!(
"Successfully send code to participant '{name}' (code '{code}', email '{email}')",
name = participant.info.name,
code = participant.code,
email = participant.info.email,
);
}
Err(err) => {
tracing::info!(
"Failed to send code to participant '{name}' (code '{code}', email '{email}'): {err:#}",
name = participant.info.name,
code = participant.code,
email = participant.info.email,
);
}
}
}

async fn send_email(token: &str, participant: &Participant) -> anyhow::Result<()> {
const EMAIL: &str = include_str!("../../mail/code.html");

let email = &participant.info.email;
let name = get_name(&participant.info.name);
let code = &participant.code;

let prepared_email_content = EMAIL.replace("NAME", &name).replace("ЯЯ-0000", code);

let request_body = serde_json::json!({
"from_email": "info@spacechamp-org.ru",
"from_name": "Космический Чемпионат 2024",
"to": email,
"subject": "Заявка на Космический Чемпионат 2024",
"text": format!("Твоя заявка на участие в Космическом Чемпионате принята! Твой шифр: {code}"),
"html": prepared_email_content
});

let send_result = reqwest::Client::new()
.post("https://api.notisend.ru/v1/email/messages")
.json(&request_body)
.bearer_auth(token)
.send()
.await
.context("sending 'email/messages' request")?;

let response = send_result
.json::<serde_json::Value>()
.await
.context("parsing notisend response")?;

if let Some(errors) = response.get("errors") {
let errors = errors.as_array().context("invalid notisend response")?;
let code = errors[0]
.get("code")
.and_then(|v| v.as_str())
.unwrap_or_default();
let detail = errors[0]
.get("detail")
.and_then(|v| v.as_str())
.unwrap_or_default();
bail!("notisend error {code}: {detail}");
}

let status = response
.get("status")
.and_then(|v| v.as_str())
.unwrap_or_default();
if status != "queued" {
bail!("notisend strange status: {status}")
}

Ok(())
}

fn get_name(name: &str) -> String {
name.split(' ').take(2).collect::<Vec<_>>().join(" ")
}

async fn all_participants(
State(state): State<Arc<BackendState>>,
) -> Result<Json<Vec<Participant>>> {
Expand All @@ -155,6 +247,7 @@ async fn create_participant(
.datasource
.create_participant(info, answers)
.await
.map(|_| ())
.map_err(Into::into)
}

Expand Down
2 changes: 1 addition & 1 deletion backend/src/datasource/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl DataSource {
&self,
info: ParticipantInfo,
answers: HashMap<String, String>,
) -> Result<()> {
) -> Result<(ParticipantId, String)> {
self.participants.create(info, answers).await
}

Expand Down
Loading

0 comments on commit 85b324e

Please sign in to comment.