first commit
This commit is contained in:
Generated
+2516
File diff suppressed because it is too large
Load Diff
+11
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "messaging"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
dotenvy = "0.15.7"
|
||||
fcm-service = "0.2.3"
|
||||
lettre = { version = "0.11.22", default-features = false, features = ["builder", "smtp-transport", "tokio1-rustls-tls"] }
|
||||
mysql_async = "0.37.0"
|
||||
tokio = { version = "1.52.3" , features = ["rt", "rt-multi-thread", "macros"] }
|
||||
@@ -0,0 +1,132 @@
|
||||
use mysql_async::prelude::*;
|
||||
use mysql_async::{Opts, OptsBuilder, Pool};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Device {
|
||||
pub idtoken: i32,
|
||||
pub device: Option<String>,
|
||||
pub token: Option<String>,
|
||||
pub active: Option<i8>,
|
||||
pub pausefrom: Option<i32>,
|
||||
pub pauseto: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Db {
|
||||
pool: Pool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Db {
|
||||
/// Creates a new `Db` instance with a given database URL string.
|
||||
pub fn new(database_url: &str) -> Self {
|
||||
let opts = Opts::from_url(database_url).expect("Invalid database URL");
|
||||
let pool = Pool::new(opts);
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
/// Creates a `Db` instance by reading database configuration from environment variables:
|
||||
/// - `DATABASE_URL` (if set)
|
||||
/// - `DB_USER` (e.g. from .env)
|
||||
/// - `DB_PASSWORD` (e.g. from .env)
|
||||
/// - `DB_HOST` (e.g. "localhost:3306" from .env)
|
||||
/// - `DB_NAME` (defaults to "places")
|
||||
pub fn from_env() -> Self {
|
||||
if let Ok(url) = std::env::var("DATABASE_URL") {
|
||||
return Self::new(&url);
|
||||
}
|
||||
|
||||
let user = std::env::var("DB_USER").unwrap_or_else(|_| "root".to_string());
|
||||
let password = std::env::var("DB_PASSWORD").unwrap_or_else(|_| "delta32".to_string());
|
||||
let host_str = std::env::var("DB_HOST").unwrap_or_else(|_| "127.0.0.1:3306".to_string());
|
||||
let db_name = std::env::var("DB_NAME").unwrap_or_else(|_| "places".to_string());
|
||||
|
||||
let (host, port) = if let Some((h, p)) = host_str.split_once(':') {
|
||||
(h.to_string(), p.parse::<u16>().unwrap_or(3306))
|
||||
} else {
|
||||
(host_str, 3306)
|
||||
};
|
||||
|
||||
// If host is "localhost", use "127.0.0.1" for TCP socket connection
|
||||
let host = if host == "localhost" {
|
||||
"127.0.0.1".to_string()
|
||||
} else {
|
||||
host
|
||||
};
|
||||
|
||||
let opts = Opts::from(
|
||||
OptsBuilder::default()
|
||||
.ip_or_hostname(host)
|
||||
.tcp_port(port)
|
||||
.user(Some(user))
|
||||
.pass(Some(password))
|
||||
.db_name(Some(db_name)),
|
||||
);
|
||||
|
||||
let pool = Pool::new(opts);
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub fn from_env_or_default() -> Self {
|
||||
Self::from_env()
|
||||
}
|
||||
|
||||
/// Retrieves the FCM token for a given device name (e.g. "Pixel 10").
|
||||
pub async fn get_token_by_device(&self, device_name: &str) -> Result<Option<String>, mysql_async::Error> {
|
||||
let mut conn = self.pool.get_conn().await?;
|
||||
let token: Option<String> = conn
|
||||
.exec_first(
|
||||
"SELECT token FROM device WHERE device = :device_name LIMIT 1",
|
||||
params! {
|
||||
"device_name" => device_name,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Retrieves full device details by device name.
|
||||
pub async fn get_device_by_name(&self, device_name: &str) -> Result<Option<Device>, mysql_async::Error> {
|
||||
let mut conn = self.pool.get_conn().await?;
|
||||
let row: Option<(i32, Option<String>, Option<String>, Option<i8>, Option<i32>, Option<i32>)> = conn
|
||||
.exec_first(
|
||||
"SELECT idtoken, device, token, active, pausefrom, pauseto FROM device WHERE device = :device_name LIMIT 1",
|
||||
params! {
|
||||
"device_name" => device_name,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(row.map(|(idtoken, device, token, active, pausefrom, pauseto)| Device {
|
||||
idtoken,
|
||||
device,
|
||||
token,
|
||||
active,
|
||||
pausefrom,
|
||||
pauseto,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Retrieves all devices from the `device` table.
|
||||
pub async fn get_all_devices(&self) -> Result<Vec<Device>, mysql_async::Error> {
|
||||
let mut conn = self.pool.get_conn().await?;
|
||||
let devices = conn
|
||||
.exec_map(
|
||||
"SELECT idtoken, device, token, active, pausefrom, pauseto FROM device",
|
||||
(),
|
||||
|(idtoken, device, token, active, pausefrom, pauseto)| Device {
|
||||
idtoken,
|
||||
device,
|
||||
token,
|
||||
active,
|
||||
pausefrom,
|
||||
pauseto,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(devices)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EmailService {
|
||||
pub smtp_host: String,
|
||||
pub smtp_port: u16,
|
||||
pub email_address: String,
|
||||
pub email_password: String,
|
||||
pub recipient: String,
|
||||
}
|
||||
|
||||
impl EmailService {
|
||||
/// Reads email configuration from environment variables (.env):
|
||||
/// - `EMAIL_ADDRESS` (required)
|
||||
/// - `EMAIL_PASSWORD` (required)
|
||||
/// - `SMTP_HOST` (defaults to mail.<domain> or localhost)
|
||||
/// - `SMTP_PORT` (defaults to 587)
|
||||
/// - `EMAIL_TO` (defaults to `EMAIL_ADDRESS`)
|
||||
pub fn from_env() -> Option<Self> {
|
||||
let email_address = std::env::var("EMAIL_ADDRESS").ok()?;
|
||||
let email_password = std::env::var("EMAIL_PASSWORD").ok()?;
|
||||
|
||||
let domain = email_address.split('@').nth(1).unwrap_or("localhost");
|
||||
let default_smtp_host = format!("mail.{}", domain);
|
||||
let smtp_host = std::env::var("SMTP_HOST").unwrap_or(default_smtp_host);
|
||||
let smtp_port = std::env::var("SMTP_PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse::<u16>().ok())
|
||||
.unwrap_or(587);
|
||||
let recipient = std::env::var("EMAIL_TO").unwrap_or_else(|_| email_address.clone());
|
||||
|
||||
Some(Self {
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
email_address,
|
||||
email_password,
|
||||
recipient,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sends an alert email when FCM notification delivery fails.
|
||||
pub async fn send_fcm_failure_alert(
|
||||
&self,
|
||||
device_name: &str,
|
||||
error_details: &str,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let email = Message::builder()
|
||||
.from(self.email_address.parse()?)
|
||||
.to(self.recipient.parse()?)
|
||||
.subject(format!("[ALERT] FCM Notification Failed for {}", device_name))
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(format!(
|
||||
"FCM notification delivery failed.\n\nDevice: {}\nError: {}\n",
|
||||
device_name, error_details
|
||||
))?;
|
||||
|
||||
let creds = Credentials::new(self.email_address.clone(), self.email_password.clone());
|
||||
|
||||
let mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&self.smtp_host)?
|
||||
.port(self.smtp_port)
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
mailer.send(email).await?;
|
||||
println!("Alert email sent successfully to {}", self.recipient);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
mod db;
|
||||
mod email;
|
||||
|
||||
use db::Db;
|
||||
use email::EmailService;
|
||||
use fcm_service::{FcmMessage, FcmNotification, FcmService, Target};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let db = Db::from_env();
|
||||
let email_service = EmailService::from_env();
|
||||
|
||||
let device_name = "Pixel 10";
|
||||
|
||||
let token = match db.get_token_by_device(device_name).await? {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
let err_msg = format!("Device '{}' not found in database", device_name);
|
||||
eprintln!("{}", err_msg);
|
||||
if let Some(es) = &email_service {
|
||||
if let Err(e) = es.send_fcm_failure_alert(device_name, &err_msg).await {
|
||||
eprintln!("Failed to send alert email: {}", e);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
println!("Fetched token for '{}': {}", device_name, token);
|
||||
|
||||
let service = FcmService::new("maps-32aa0-firebase-adminsdk-4qso4-74d42c4730.json");
|
||||
|
||||
let mut message = FcmMessage::new();
|
||||
let mut notification = FcmNotification::new();
|
||||
notification.set_title("Hello".to_string());
|
||||
notification.set_body("World".to_string());
|
||||
notification.set_image(None);
|
||||
message.set_notification(Some(notification));
|
||||
message.set_target(Target::Token(token));
|
||||
|
||||
if let Err(err) = service.send_notification(message).await {
|
||||
eprintln!("FCM notification failed: {}", err);
|
||||
if let Some(es) = &email_service {
|
||||
if let Err(e) = es.send_fcm_failure_alert(device_name, &err.to_string()).await {
|
||||
eprintln!("Failed to send alert email: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("FCM notification sent successfully to '{}'", device_name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user