first commit

This commit is contained in:
Dimitrios Kouros
2026-08-03 07:23:58 +03:00
commit 4094ece803
5 changed files with 2785 additions and 0 deletions
+132
View File
@@ -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)
}
}
+71
View File
@@ -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
View File
@@ -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(())
}