discord bot

This commit is contained in:
ゆめ 2023-06-20 16:39:53 -05:00
parent 4b6bfc892c
commit e666250fa9
18 changed files with 677 additions and 143 deletions

226
Cargo.lock generated
View file

@ -185,6 +185,22 @@ dependencies = [
"syn 2.0.18", "syn 2.0.18",
] ]
[[package]]
name = "async-tungstenite"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b71b31561643aa8e7df3effe284fa83ab1a840e52294c5f4bd7bfd8b2becbb"
dependencies = [
"futures-io",
"futures-util",
"log",
"pin-project-lite",
"tokio",
"tokio-rustls 0.23.4",
"tungstenite",
"webpki-roots",
]
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -328,6 +344,12 @@ version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.4.0" version = "1.4.0"
@ -362,6 +384,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "chrono-humanize"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32dce1ea1988dbdf9f9815ff11425828523bd2a134ec0805d2ac8af26ee6096e"
dependencies = [
"chrono",
]
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@ -496,6 +527,20 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "dashmap"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
dependencies = [
"cfg-if",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
"serde",
]
[[package]] [[package]]
name = "diesel" name = "diesel"
version = "2.1.0" version = "2.1.0"
@ -609,7 +654,7 @@ dependencies = [
"futures-util", "futures-util",
"http", "http",
"hyper", "hyper",
"hyper-rustls", "hyper-rustls 0.23.2",
"mime", "mime",
"serde", "serde",
"serde_json", "serde_json",
@ -942,6 +987,19 @@ dependencies = [
"tokio-rustls 0.23.4", "tokio-rustls 0.23.4",
] ]
[[package]]
name = "hyper-rustls"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7"
dependencies = [
"http",
"hyper",
"rustls 0.21.1",
"tokio",
"tokio-rustls 0.24.0",
]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.5.0" version = "0.5.0"
@ -1336,6 +1394,15 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "ordered-float"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -1426,6 +1493,12 @@ dependencies = [
"universal-hash", "universal-hash",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
version = "1.0.4" version = "1.0.4"
@ -1474,6 +1547,27 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49" checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.6.4" version = "0.6.4"
@ -1524,25 +1618,33 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
"hyper-rustls 0.24.0",
"hyper-tls", "hyper-tls",
"ipnet", "ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"mime_guess",
"native-tls", "native-tls",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls 0.21.1",
"rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls 0.24.0",
"tokio-util",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams",
"web-sys", "web-sys",
"webpki-roots",
"winreg", "winreg",
] ]
@ -1742,6 +1844,16 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.163" version = "1.0.163"
@ -1819,6 +1931,48 @@ dependencies = [
"unsafe-libyaml", "unsafe-libyaml",
] ]
[[package]]
name = "serenity"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82fd5e7b5858ad96e99d440138f34f5b98e1b959ebcd3a1036203b30e78eb788"
dependencies = [
"async-trait",
"async-tungstenite",
"base64 0.13.1",
"bitflags",
"bytes",
"cfg-if",
"chrono",
"dashmap",
"flate2",
"futures",
"mime",
"mime_guess",
"parking_lot",
"percent-encoding",
"reqwest",
"serde",
"serde-value",
"serde_json",
"time 0.3.21",
"tokio",
"tracing",
"typemap_rev",
"url",
]
[[package]]
name = "sha-1"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.6" version = "0.10.6"
@ -2214,9 +2368,21 @@ dependencies = [
"cfg-if", "cfg-if",
"log", "log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-attributes"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8803eee176538f94ae9a14b55b2804eb7e1441f8210b1c31290b3bccdccff73b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.31" version = "0.1.31"
@ -2232,6 +2398,33 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "tungstenite"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
dependencies = [
"base64 0.13.1",
"byteorder",
"bytes",
"http",
"httparse",
"log",
"rand",
"rustls 0.20.8",
"sha-1",
"thiserror",
"url",
"utf-8",
"webpki",
]
[[package]]
name = "typemap_rev"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5b74f0a24b5454580a79abb6994393b09adf0ab8070f15827cb666255de155"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.16.0" version = "1.16.0"
@ -2305,6 +2498,7 @@ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna 0.4.0", "idna 0.4.0",
"percent-encoding", "percent-encoding",
"serde",
] ]
[[package]] [[package]]
@ -2313,6 +2507,12 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517" checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.1" version = "0.2.1"
@ -2438,6 +2638,19 @@ version = "0.2.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
[[package]]
name = "wasm-streams"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.63" version = "0.3.63"
@ -2477,6 +2690,15 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "webpki-roots"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
dependencies = [
"webpki",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -2671,6 +2893,7 @@ dependencies = [
"axum-server", "axum-server",
"base64 0.21.2", "base64 0.21.2",
"chrono", "chrono",
"chrono-humanize",
"clap", "clap",
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
@ -2687,6 +2910,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"serenity",
"simple_logger", "simple_logger",
"tempfile", "tempfile",
"thirtyfour", "thirtyfour",

View file

@ -18,6 +18,7 @@ axum = { version = "0.6.18", features = ["http2"] }
axum-server = { version = "0.5.1", features = ["rustls", "rustls-pemfile", "tls-rustls"] } axum-server = { version = "0.5.1", features = ["rustls", "rustls-pemfile", "tls-rustls"] }
base64 = "0.21.2" base64 = "0.21.2"
chrono = { version = "0.4.26", features = ["serde"] } chrono = { version = "0.4.26", features = ["serde"] }
chrono-humanize = "0.2.2"
clap = { version = "4.3.2", features = ["derive"] } clap = { version = "4.3.2", features = ["derive"] }
diesel = { version = "2.1.0", features = ["sqlite", "chrono"] } diesel = { version = "2.1.0", features = ["sqlite", "chrono"] }
diesel_migrations = { version = "2.1.0", features = ["sqlite"] } diesel_migrations = { version = "2.1.0", features = ["sqlite"] }
@ -28,12 +29,13 @@ lettre = "0.10.4"
log = "0.4.18" log = "0.4.18"
rand_core = { version = "0.6.4", features = ["getrandom"] } rand_core = { version = "0.6.4", features = ["getrandom"] }
regex = "1.8.4" regex = "1.8.4"
reqwest = { version = "0.11.18", features = ["json", "blocking"] } reqwest = { version = "0.11.18", features = ["json"] }
rust-embed = "6.7.0" rust-embed = "6.7.0"
rustls-pemfile = "1.0.2" rustls-pemfile = "1.0.2"
serde = { version = "1.0.163", features = ["derive"] } serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96" serde_json = "1.0.96"
serde_yaml = "0.9.21" serde_yaml = "0.9.21"
serenity = { version = "0.11.5", default-features = false, features = ["builder", "cache", "client", "chrono","gateway", "model", "utils", "rustls_backend"] }
simple_logger = "4.1.0" simple_logger = "4.1.0"
tempfile = "3.6.0" tempfile = "3.6.0"
thirtyfour = "0.31.0" thirtyfour = "0.31.0"

View file

@ -11,6 +11,8 @@ canvas_lms:
refresh_interval: 300 refresh_interval: 300
comm: comm:
discord:
token: "xxx"
gotify: gotify:
url: https://gotify.yumechi.jp url: https://gotify.yumechi.jp
token: Axxxxxx token: Axxxxxx

View file

@ -1,9 +1,6 @@
use std::{ use std::sync::Arc;
future::Future,
pin::Pin,
sync::{Arc, Mutex},
};
use async_trait::async_trait;
use axum::{ use axum::{
body::HttpBody, body::HttpBody,
error_handling::HandleErrorLayer, error_handling::HandleErrorLayer,
@ -14,6 +11,7 @@ use axum::{
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tower::ServiceBuilder; use tower::ServiceBuilder;
use crate::{ use crate::{
@ -66,7 +64,7 @@ pub async fn route_login(
) -> Result<(Extension<SessionStore>, ApiResponse<()>), ApiResponse<()>> { ) -> Result<(Extension<SessionStore>, ApiResponse<()>), ApiResponse<()>> {
let failed_response = ApiResponse::<()>::error("Invalid credentials".to_string(), 401, None); let failed_response = ApiResponse::<()>::error("Invalid credentials".to_string(), 401, None);
let state = app.state.lock().unwrap(); let state = app.state.lock().await;
let state = state.as_ref().unwrap(); let state = state.as_ref().unwrap();
let Some(user) = state.config.auth.users.get(&form.username) else { let Some(user) = state.config.auth.users.get(&form.username) else {
@ -125,19 +123,14 @@ impl AuthApp {
} }
} }
#[async_trait]
impl App for AuthApp { impl App for AuthApp {
fn initialize( async fn initialize(self: Arc<Self>, config: &'static Config, app_state: Arc<Mutex<AppState>>) {
self: Arc<Self>, let mut state = self.state.lock().await;
config: &'static Config,
app_state: Arc<Mutex<AppState>>,
) -> Pin<Box<dyn Future<Output = ()>>> {
let mut state = self.state.lock().unwrap();
*state = Some(AuthAppState { *state = Some(AuthAppState {
config, config,
_app_state: app_state, _app_state: app_state,
}); });
Box::pin(async {})
} }
fn api_routes(self: Arc<Self>) -> Router { fn api_routes(self: Arc<Self>) -> Router {

View file

@ -1,14 +1,11 @@
use std::{ use std::sync::Arc;
future::Future,
pin::Pin,
sync::{Arc, Mutex},
};
use askama::Template; use askama::Template;
use async_trait::async_trait;
use axum::{body::HttpBody, extract::Query, http::Request, routing::get, Extension, Router}; use axum::{body::HttpBody, extract::Query, http::Request, routing::get, Extension, Router};
use log::{debug, info}; use log::{debug, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::Mutex as AsyncMutex; use tokio::sync::Mutex;
use crate::{ use crate::{
apps::{ apps::{
@ -62,7 +59,7 @@ pub async fn query_grades(
} }
pub struct CanvasLMSApp { pub struct CanvasLMSApp {
state: AsyncMutex<CanvasLMSAppState>, state: Mutex<CanvasLMSAppState>,
} }
struct CanvasLMSAppState { struct CanvasLMSAppState {
@ -114,7 +111,7 @@ where
impl CanvasLMSApp { impl CanvasLMSApp {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
state: AsyncMutex::new(CanvasLMSAppState { state: Mutex::new(CanvasLMSAppState {
config: None, config: None,
grade_cache: GradeCache { grade_cache: GradeCache {
last_updated: chrono::Local::now(), last_updated: chrono::Local::now(),
@ -149,13 +146,16 @@ impl CanvasLMSApp {
}; };
let template_rendered = template_ctx.render().unwrap(); let template_rendered = template_ctx.render().unwrap();
let global_app_state = let global_app_state =
state.global_app_state.as_ref().unwrap().lock().unwrap(); state.global_app_state.as_ref().unwrap().lock().await;
let email_result = global_app_state.comm.send_message(&Message { let email_result = global_app_state
.comm
.send_message(&Message {
subject: "New grades available".to_string(), subject: "New grades available".to_string(),
body: template_rendered, body: template_rendered,
mime: "text/html", mime: "text/html",
..Default::default() ..Default::default()
}); })
.await;
match email_result { match email_result {
Ok(_) => { Ok(_) => {
info!("Sent email notification for new grades"); info!("Sent email notification for new grades");
@ -176,12 +176,9 @@ impl CanvasLMSApp {
} }
} }
#[async_trait]
impl App for CanvasLMSApp { impl App for CanvasLMSApp {
fn initialize( async fn initialize(self: Arc<Self>, config: &'static Config, app_state: Arc<Mutex<AppState>>) {
self: Arc<Self>,
config: &'static Config,
app_state: Arc<Mutex<AppState>>,
) -> Pin<Box<dyn Future<Output = ()>>> {
let self_clone = self.clone(); let self_clone = self.clone();
let refresh_interval = config.canvas_lms.refresh_interval; let refresh_interval = config.canvas_lms.refresh_interval;
@ -189,7 +186,6 @@ impl App for CanvasLMSApp {
panic!("Canvas LMS refresh interval cannot be 0"); panic!("Canvas LMS refresh interval cannot be 0");
} }
let init_async = Box::pin(async move {
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
state.global_app_state = Some(app_state); state.global_app_state = Some(app_state);
state.config = Some(config); state.config = Some(config);
@ -207,9 +203,6 @@ impl App for CanvasLMSApp {
ticker.tick().await; ticker.tick().await;
} }
}); });
});
init_async
} }
fn api_routes(self: Arc<Self>) -> Router { fn api_routes(self: Arc<Self>) -> Router {

View file

@ -48,7 +48,7 @@ pub async fn route_get_directive(
let state = app.state.lock().await; let state = app.state.lock().await;
let state = state.as_ref().unwrap(); let state = state.as_ref().unwrap();
let mut global_app_state = state.global_app_state.lock().unwrap(); let mut global_app_state = state.global_app_state.lock().await;
let meds = { let meds = {
use crate::schema::medications::dsl::*; use crate::schema::medications::dsl::*;
@ -77,7 +77,7 @@ pub async fn route_post_directive(
let state = app.state.lock().await; let state = app.state.lock().await;
let state = state.as_ref().unwrap(); let state = state.as_ref().unwrap();
let mut global_app_state = state.global_app_state.lock().unwrap(); let mut global_app_state = state.global_app_state.lock().await;
form.uuid = uuid::Uuid::new_v4().to_string(); form.uuid = uuid::Uuid::new_v4().to_string();
form.created = chrono::Utc::now().naive_local(); form.created = chrono::Utc::now().naive_local();
@ -111,7 +111,7 @@ pub async fn route_patch_directive(
let state = app.state.lock().await; let state = app.state.lock().await;
let state = state.as_ref().unwrap(); let state = state.as_ref().unwrap();
let mut global_app_state = state.global_app_state.lock().unwrap(); let mut global_app_state = state.global_app_state.lock().await;
let res = { let res = {
use crate::schema::medications; use crate::schema::medications;
@ -155,7 +155,7 @@ pub async fn route_delete_directive(
let state = app.state.lock().await; let state = app.state.lock().await;
let state = state.as_ref().unwrap(); let state = state.as_ref().unwrap();
let mut global_app_state = state.global_app_state.lock().unwrap(); let mut global_app_state = state.global_app_state.lock().await;
{ {
use crate::schema::medications::dsl::medications; use crate::schema::medications::dsl::medications;

View file

@ -6,6 +6,7 @@ use crate::{
models::med::{Medication, MedicationLog}, models::med::{Medication, MedicationLog},
}; };
use anyhow::Result;
use axum::{ use axum::{
extract::{Path, Query}, extract::{Path, Query},
Extension, Extension,
@ -60,6 +61,55 @@ pub fn project_next_dose(med: &Medication, med_logs: Vec<MedicationLog>) -> Medi
} }
} }
pub async fn project_next_doses(
app: &MedManagementApp,
) -> Result<Vec<(Medication, MedicationLog)>> {
let state = app.state.lock().await;
let state = state.as_ref().unwrap();
let mut global_app_state = state.global_app_state.lock().await;
let meds = {
use crate::schema::medications::dsl::*;
use diesel::prelude::*;
medications
.load::<Medication>(&mut global_app_state.db)
.map_err(|e| {
error!("Failed to load meds: {:?}", e);
e
})?
};
let mut med_logs = Vec::with_capacity(meds.len());
for med in &meds {
let logs = {
use crate::schema::medication_logs::dsl::*;
use diesel::prelude::*;
medication_logs
.order(time_actual.desc())
.filter(med_uuid.eq(&med.uuid))
.limit(10)
.load::<MedicationLog>(&mut global_app_state.db)
.map_err(|e| {
error!("Failed to load logs: {:?}", e);
e
})?
};
med_logs.push(logs);
}
let mut res = Vec::with_capacity(meds.len());
for (med, logs) in meds.into_iter().zip(med_logs.into_iter()) {
let next_dose = project_next_dose(&med, logs);
res.push((med, next_dose));
}
Ok(res)
}
pub async fn route_project_next_dose( pub async fn route_project_next_dose(
auth: AuthInfo, auth: AuthInfo,
app: Extension<Arc<MedManagementApp>>, app: Extension<Arc<MedManagementApp>>,
@ -69,7 +119,7 @@ pub async fn route_project_next_dose(
let state = app.state.lock().await; let state = app.state.lock().await;
let state = state.as_ref().unwrap(); let state = state.as_ref().unwrap();
let mut global_app_state = state.global_app_state.lock().unwrap(); let mut global_app_state = state.global_app_state.lock().await;
let med = { let med = {
use crate::schema::medications::dsl::*; use crate::schema::medications::dsl::*;
@ -122,7 +172,7 @@ pub async fn route_get_log(
let state = app.state.lock().await; let state = app.state.lock().await;
let state = state.as_ref().unwrap(); let state = state.as_ref().unwrap();
let mut global_app_state = state.global_app_state.lock().unwrap(); let mut global_app_state = state.global_app_state.lock().await;
let med_logs = { let med_logs = {
use crate::schema::medication_logs::dsl::*; use crate::schema::medication_logs::dsl::*;
@ -169,7 +219,7 @@ pub async fn route_post_log(
let state = app.state.lock().await; let state = app.state.lock().await;
let state = state.as_ref().unwrap(); let state = state.as_ref().unwrap();
let mut global_app_state = state.global_app_state.lock().unwrap(); let mut global_app_state = state.global_app_state.lock().await;
let med = { let med = {
use crate::schema::medications::dsl::*; use crate::schema::medications::dsl::*;
@ -249,7 +299,7 @@ pub async fn route_delete_log(
let state = app.state.lock().await; let state = app.state.lock().await;
let state = state.as_ref().unwrap(); let state = state.as_ref().unwrap();
let mut global_app_state = state.global_app_state.lock().unwrap(); let mut global_app_state = state.global_app_state.lock().await;
{ {
use crate::schema::medication_logs::dsl::*; use crate::schema::medication_logs::dsl::*;

View file

@ -1,24 +1,29 @@
use std::{ use std::sync::Arc;
future::Future,
pin::Pin,
sync::{Arc, Mutex},
};
use crate::{config::Config, AppState}; use crate::{
comm::{Message, MessageDigestor},
config::Config,
AppState,
};
use super::App; use super::App;
use anyhow::Result;
use async_trait::async_trait;
use axum::{ use axum::{
routing::{delete, get, post}, routing::{delete, get, post},
Extension, Router, Extension, Router,
}; };
use tokio::sync::Mutex as AsyncMutex; use chrono::DateTime;
use lazy_static::lazy_static;
use serenity::utils::MessageBuilder;
use tokio::sync::Mutex;
mod directive; mod directive;
mod log; mod log;
pub struct MedManagementApp { pub struct MedManagementApp {
state: AsyncMutex<Option<MedManagementAppState>>, state: Mutex<Option<MedManagementAppState>>,
} }
struct MedManagementAppState { struct MedManagementAppState {
@ -28,23 +33,70 @@ struct MedManagementAppState {
impl MedManagementApp { impl MedManagementApp {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
state: AsyncMutex::new(None), state: Mutex::new(None),
} }
} }
} }
pub fn format_relative_time<Tz: chrono::TimeZone>(
time: chrono::DateTime<Tz>,
relative_to: chrono::DateTime<Tz>,
) -> String {
let duration = time.signed_duration_since(relative_to);
let duration = chrono_humanize::HumanTime::from(duration);
duration.to_string()
}
#[async_trait]
impl MessageDigestor for Arc<MedManagementApp> {
async fn digest(&self, message: &Message) -> Result<Option<Message>> {
lazy_static! {
static ref REGEX_GET_DIRECTIVE: regex::Regex =
regex::Regex::new(r"get med info$").unwrap();
}
if REGEX_GET_DIRECTIVE.is_match(&message.body) {
let next_doses = log::project_next_doses(self).await?;
let mut msg = MessageBuilder::new();
msg.push_line("");
for next_dose in next_doses.iter() {
msg.push_bold(next_dose.0.name.to_string());
msg.push_line(":");
msg.push_line(format!("Offset: {:.2}", next_dose.1.dose_offset,));
msg.push_line(format!(
"Next dose: {}",
format_relative_time(
DateTime::<chrono::Utc>::from_utc(next_dose.1.time_expected, chrono::Utc),
chrono::Utc::now()
)
));
msg.push_line("");
}
return Ok(Some(Message {
subject: "".to_string(),
priority: 0,
body: msg.build(),
mime: message.mime.clone(),
}));
}
Ok(None)
}
}
#[async_trait]
impl App for MedManagementApp { impl App for MedManagementApp {
fn initialize( async fn initialize(
self: Arc<Self>, self: Arc<Self>,
_config: &'static Config, _config: &'static Config,
app_state: Arc<Mutex<AppState>>, app_state: Arc<Mutex<AppState>>,
) -> Pin<Box<dyn Future<Output = ()>>> { ) {
Box::pin(async move {
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
*state = Some(MedManagementAppState { *state = Some(MedManagementAppState {
global_app_state: app_state, global_app_state: app_state,
}); });
})
} }
fn api_routes(self: Arc<Self>) -> Router { fn api_routes(self: Arc<Self>) -> Router {
@ -81,4 +133,7 @@ impl App for MedManagementApp {
) )
.layer(Extension(self.clone())) .layer(Extension(self.clone()))
} }
fn message_digestors(self: Arc<Self>) -> Vec<Box<dyn MessageDigestor + Send + Sync>> {
vec![Box::new(self)]
}
} }

View file

@ -1,23 +1,22 @@
use crate::{config::Config, AppState}; use crate::{comm::MessageDigestor, config::Config, AppState};
use async_trait::async_trait;
use axum::Router; use axum::Router;
use std::sync::Arc;
use tokio::sync::Mutex;
use std::{ #[async_trait]
future::Future,
pin::Pin,
sync::{Arc, Mutex},
};
pub trait App { pub trait App {
fn initialize( async fn initialize(
self: Arc<Self>, self: Arc<Self>,
_config: &'static Config, _config: &'static Config,
_app_state: Arc<Mutex<AppState>>, _app_state: Arc<Mutex<AppState>>,
) -> Pin<Box<dyn Future<Output = ()>>> { );
Box::pin(async {})
}
fn api_routes(self: Arc<Self>) -> Router { fn api_routes(self: Arc<Self>) -> Router {
Router::new() Router::new()
} }
fn message_digestors(self: Arc<Self>) -> Vec<Box<dyn MessageDigestor + Send + Sync>> {
vec![]
}
} }
pub mod auth; pub mod auth;

View file

@ -1,6 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use crate::{apps::App, http::ApiResponse}; use crate::{
apps::App,
comm::{discord::MIME_DISCORD, Message, MessageDigestor},
config::Config,
http::ApiResponse,
AppState,
};
use anyhow::Result;
use async_trait::async_trait;
use axum::{ use axum::{
body::HttpBody, body::HttpBody,
http::Request, http::Request,
@ -8,7 +16,9 @@ use axum::{
routing::get, routing::get,
Router, Router,
}; };
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
pub struct ServerInfoApp {} pub struct ServerInfoApp {}
@ -44,8 +54,46 @@ impl ServerInfoApp {
} }
} }
#[async_trait]
impl MessageDigestor for Arc<ServerInfoApp> {
async fn digest(&self, message: &Message) -> Result<Option<Message>> {
lazy_static! {
static ref REGEXP_ASK_SERVER_INFO: regex::Regex =
regex::Regex::new(r"server info$").unwrap();
}
if REGEXP_ASK_SERVER_INFO.is_match(&message.body) {
let server_info = ServerInfo {
version: env!("CARGO_PKG_VERSION").to_string(),
profile: PROFILE.to_string(),
};
let ret = format!(
"Server info:\nVersion: {}\nProfile: {}",
server_info.version, server_info.profile
);
return Ok(Some(Message {
body: ret,
subject: "".to_string(),
priority: 0,
mime: MIME_DISCORD,
}));
}
Ok(None)
}
}
#[async_trait]
impl App for ServerInfoApp { impl App for ServerInfoApp {
async fn initialize(
self: Arc<Self>,
_config: &'static Config,
_app_state: Arc<Mutex<AppState>>,
) {
}
fn api_routes(self: Arc<Self>) -> Router { fn api_routes(self: Arc<Self>) -> Router {
Router::new().route("/server_info", get(get_server_info)) Router::new().route("/server_info", get(get_server_info))
} }
fn message_digestors(self: Arc<Self>) -> Vec<Box<dyn MessageDigestor + Send + Sync>> {
vec![Box::new(self)]
}
} }

View file

@ -9,7 +9,7 @@ pub struct ChromeDriver {
impl ChromeDriver { impl ChromeDriver {
pub fn new() -> Self { pub fn new() -> Self {
let port = rand_core::OsRng.next_u32() as u16 + 10000; let port = (rand_core::OsRng.next_u32() % 1024) as u16 + 10000;
Self::new_port(port) Self::new_port(port)
} }
pub fn new_port(port: u16) -> Self { pub fn new_port(port: u16) -> Self {

View file

@ -1,9 +1,4 @@
use std::{ use std::{collections::HashMap, sync::Arc};
collections::HashMap,
future::Future,
pin::Pin,
sync::{Arc, Mutex},
};
use async_trait::async_trait; use async_trait::async_trait;
use axum::{routing::get, Extension, Router}; use axum::{routing::get, Extension, Router};
@ -11,7 +6,7 @@ use chrono::DateTime;
use log::info; use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thirtyfour::{DesiredCapabilities, WebDriver}; use thirtyfour::{DesiredCapabilities, WebDriver};
use tokio::sync::Mutex as AsyncMutex; use tokio::sync::Mutex;
use crate::{ use crate::{
comm::{Communicator, Message}, comm::{Communicator, Message},
@ -29,7 +24,7 @@ mod driver;
mod utd_app; mod utd_app;
pub struct WebcheckApp { pub struct WebcheckApp {
state: AsyncMutex<WebcheckAppState>, state: Mutex<WebcheckAppState>,
} }
struct WebcheckAppState { struct WebcheckAppState {
@ -69,7 +64,7 @@ pub trait WebDriverChecker {
impl WebcheckApp { impl WebcheckApp {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
state: AsyncMutex::new(WebcheckAppState { state: Mutex::new(WebcheckAppState {
config: None, config: None,
global_app_state: None, global_app_state: None,
last_response: HashMap::new(), last_response: HashMap::new(),
@ -116,14 +111,15 @@ impl WebcheckApp {
.as_ref() .as_ref()
.unwrap() .unwrap()
.lock() .lock()
.unwrap() .await
.comm .comm
.send_message(&Message { .send_message(&Message {
subject: format!("webcheck {} changed", key), subject: format!("webcheck {} changed", key),
body: format!("{} changed to {}", key, response), body: format!("{} changed to {}", key, response),
mime: "text/plain", mime: "text/plain",
priority: 0, priority: 0,
})?; })
.await?;
} }
} }
None => {} None => {}
@ -167,13 +163,9 @@ impl WebcheckApp {
} }
} }
#[async_trait]
impl App for WebcheckApp { impl App for WebcheckApp {
fn initialize( async fn initialize(self: Arc<Self>, config: &'static Config, app_state: Arc<Mutex<AppState>>) {
self: Arc<Self>,
config: &'static Config,
app_state: Arc<Mutex<AppState>>,
) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(async move {
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
state.config = Some(config); state.config = Some(config);
state.global_app_state = Some(app_state); state.global_app_state = Some(app_state);
@ -195,7 +187,6 @@ impl App for WebcheckApp {
let self_clone = self.clone(); let self_clone = self.clone();
tokio::spawn(self_clone.run_check_loops()); tokio::spawn(self_clone.run_check_loops());
})
} }
fn api_routes(self: Arc<Self>) -> Router { fn api_routes(self: Arc<Self>) -> Router {

128
src/comm/discord.rs Normal file
View file

@ -0,0 +1,128 @@
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use log::info;
use serenity::model::channel::Message;
use serenity::model::gateway::Ready;
use serenity::prelude::*;
use serenity::model::prelude::{ChannelId, UserId};
use crate::config::comm::DiscordConfig;
use super::{Communicator, MessageDigestor};
struct Handler {
state: Mutex<HandlerState>,
channel_id: ChannelId,
digestor: Arc<Vec<Box<dyn MessageDigestor + Send + Sync>>>,
}
impl Handler {
fn new(
channel_id: ChannelId,
digestor: Arc<Vec<Box<dyn MessageDigestor + Send + Sync>>>,
) -> Self {
Self {
state: Mutex::new(HandlerState { bot_id: UserId(0) }),
channel_id,
digestor,
}
}
}
struct HandlerState {
bot_id: UserId,
}
#[async_trait]
impl EventHandler for Handler {
async fn message(&self, ctx: Context, msg: Message) {
if msg.channel_id == self.channel_id {
info!("Received message: {:?}", msg.content);
let msg_generic = super::Message {
subject: "".to_string(),
body: msg.content.to_string(),
mime: MIME_DISCORD,
priority: 0,
};
for digestor in self.digestor.iter() {
match digestor.digest(&msg_generic).await {
Err(e) => {
info!("Error digesting message: {:?}", e);
}
Ok(Some(reply)) => {
if let Err(why) = msg.channel_id.say(&ctx.http, reply.body).await {
info!("Error sending message: {:?}", why);
}
}
Ok(None) => {}
}
}
}
}
async fn ready(&self, _: Context, ready: Ready) {
let mut state = self.state.lock().await;
state.bot_id = ready.user.id;
}
}
pub struct DiscordCommunicator {
client: Client,
config: &'static DiscordConfig,
}
impl DiscordCommunicator {
pub async fn new(
config: &'static DiscordConfig,
digestor: Arc<Vec<Box<dyn MessageDigestor + Send + Sync>>>,
) -> Self {
let channel_id = ChannelId(config.channel_id);
let intents = GatewayIntents::non_privileged()
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::DIRECT_MESSAGES
| GatewayIntents::MESSAGE_CONTENT;
let handler = Handler::new(channel_id, digestor);
let mut client = Client::builder(config.token.to_string(), intents)
.event_handler(handler)
.await
.expect("Error creating client");
tokio::spawn(async move {
if let Err(why) = client.start().await {
info!("Client error: {:?}", why);
}
});
let client = Client::builder(config.token.to_string(), intents)
.await
.expect("Error creating client");
Self { client, config }
}
}
pub const MIME_DISCORD: &'static str = "application/vnd.discord";
#[async_trait]
impl Communicator for DiscordCommunicator {
fn name(&self) -> &'static str {
"Discord"
}
fn supported_mimes(&self) -> Vec<&'static str> {
vec![MIME_DISCORD]
}
async fn send_message(&self, message: &super::Message) -> Result<()> {
let channel = ChannelId(self.config.channel_id);
channel
.say(&self.client.cache_and_http.http, message.body.to_string())
.await?;
Ok(())
}
}

View file

@ -1,3 +1,4 @@
use async_trait::async_trait;
use lettre::{ use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Transport, message::header::ContentType, transport::smtp::authentication::Credentials, Transport,
}; };
@ -12,11 +13,16 @@ pub struct EmailCommunicator {
impl EmailCommunicator { impl EmailCommunicator {
pub fn new(config: &'static Config) -> Self { pub fn new(config: &'static Config) -> Self {
Self { Self {
config: &config.comm.email, config: &config
.comm
.email
.as_ref()
.expect("Email communicator not configured"),
} }
} }
} }
#[async_trait]
impl Communicator for EmailCommunicator { impl Communicator for EmailCommunicator {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"email" "email"
@ -24,7 +30,7 @@ impl Communicator for EmailCommunicator {
fn supported_mimes(&self) -> Vec<&'static str> { fn supported_mimes(&self) -> Vec<&'static str> {
vec!["text/plain", "text/html"] vec!["text/plain", "text/html"]
} }
fn send_message(&self, message: &super::Message) -> anyhow::Result<()> { async fn send_message(&self, message: &super::Message) -> anyhow::Result<()> {
let mailer = lettre::SmtpTransport::relay(&self.config.host)? let mailer = lettre::SmtpTransport::relay(&self.config.host)?
.credentials(Credentials::new( .credentials(Credentials::new(
self.config.username.clone(), self.config.username.clone(),

View file

@ -1,7 +1,7 @@
use super::Communicator; use super::Communicator;
use crate::config::{comm::GotifyConfig, Config}; use crate::config::{comm::GotifyConfig, Config};
use async_trait::async_trait;
use serde::Serialize; use serde::Serialize;
pub struct GotifyCommunicator { pub struct GotifyCommunicator {
config: &'static GotifyConfig, config: &'static GotifyConfig,
} }
@ -9,7 +9,11 @@ pub struct GotifyCommunicator {
impl GotifyCommunicator { impl GotifyCommunicator {
pub fn new(config: &'static Config) -> Self { pub fn new(config: &'static Config) -> Self {
Self { Self {
config: &config.comm.gotify, config: &config
.comm
.gotify
.as_ref()
.expect("Gotify communicator not configured"),
} }
} }
} }
@ -49,6 +53,7 @@ struct GotifyMessageExtrasClientDisplay {
content_type: String, content_type: String,
} }
#[async_trait]
impl Communicator for GotifyCommunicator { impl Communicator for GotifyCommunicator {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"gotify" "gotify"
@ -56,13 +61,14 @@ impl Communicator for GotifyCommunicator {
fn supported_mimes(&self) -> Vec<&'static str> { fn supported_mimes(&self) -> Vec<&'static str> {
vec!["text/plain", "text/markdown"] vec!["text/plain", "text/markdown"]
} }
fn send_message(&self, message: &super::Message) -> anyhow::Result<()> { async fn send_message(&self, message: &super::Message) -> anyhow::Result<()> {
let client = reqwest::blocking::Client::new(); let client = reqwest::Client::new();
let response = client let response = client
.post(&format!("{}/message", self.config.url)) .post(&format!("{}/message", self.config.url))
.header("X-Gotify-Key", &self.config.token) .header("X-Gotify-Key", &self.config.token)
.json::<GotifyMessage>(&message.into()) .json::<GotifyMessage>(&message.into())
.send()?; .send()
.await?;
if !response.status().is_success() { if !response.status().is_success() {
anyhow::bail!("Gotify returned an error: {:?}", response); anyhow::bail!("Gotify returned an error: {:?}", response);

View file

@ -1,8 +1,10 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait;
use log::{error, warn}; use log::{error, warn};
pub mod discord;
pub mod email; pub mod email;
pub mod gotify; pub mod gotify;
@ -28,10 +30,16 @@ impl Default for Message {
pub const MIME_PLAIN: &'static str = "text/plain"; pub const MIME_PLAIN: &'static str = "text/plain";
pub const MIME_HTML: &'static str = "text/html"; pub const MIME_HTML: &'static str = "text/html";
#[async_trait]
pub trait Communicator { pub trait Communicator {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
fn supported_mimes(&self) -> Vec<&'static str>; fn supported_mimes(&self) -> Vec<&'static str>;
fn send_message(&self, message: &Message) -> Result<()>; async fn send_message(&self, message: &Message) -> Result<()>;
}
#[async_trait]
pub trait MessageDigestor {
async fn digest(&self, message: &Message) -> Result<Option<Message>>;
} }
pub struct GlobalCommunicator { pub struct GlobalCommunicator {
@ -70,6 +78,7 @@ impl GlobalCommunicator {
} }
} }
#[async_trait]
impl Communicator for GlobalCommunicator { impl Communicator for GlobalCommunicator {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"global" "global"
@ -79,11 +88,11 @@ impl Communicator for GlobalCommunicator {
self.communicators.keys().map(|k| *k).collect() self.communicators.keys().map(|k| *k).collect()
} }
fn send_message(&self, message: &Message) -> Result<()> { async fn send_message(&self, message: &Message) -> Result<()> {
let mime = message.mime; let mime = message.mime;
if let Some(communicators) = self.communicators.get(mime) { if let Some(communicators) = self.communicators.get(mime) {
for communicator in communicators { for communicator in communicators {
if let Err(e) = communicator.send_message(message) { if let Err(e) = communicator.send_message(message).await {
warn!("Failed to send message with {}: {}", communicator.name(), e); warn!("Failed to send message with {}: {}", communicator.name(), e);
continue; continue;
} }

View file

@ -2,8 +2,9 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
pub email: EmailConfig, pub email: Option<EmailConfig>,
pub gotify: GotifyConfig, pub gotify: Option<GotifyConfig>,
pub discord: Option<DiscordConfig>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -22,3 +23,9 @@ pub struct GotifyConfig {
pub url: String, pub url: String,
pub token: String, pub token: String,
} }
#[derive(Debug, Deserialize)]
pub struct DiscordConfig {
pub token: String,
pub channel_id: u64,
}

View file

@ -1,4 +1,4 @@
use std::sync::{Arc, Mutex}; use std::sync::Arc;
use apps::{med, webcheck}; use apps::{med, webcheck};
use axum::{http::Request, middleware::Next, response::Response, routing::get, Extension, Router}; use axum::{http::Request, middleware::Next, response::Response, routing::get, Extension, Router};
@ -7,6 +7,7 @@ use diesel::{sqlite, Connection};
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use hyper::Method; use hyper::Method;
use log::info; use log::info;
use tokio::sync::Mutex;
use tower_http::cors::{self, CorsLayer}; use tower_http::cors::{self, CorsLayer};
use crate::{ use crate::{
@ -85,7 +86,7 @@ pub async fn server_listen(router: Router) {
pub async fn main_server(dev: bool) { pub async fn main_server(dev: bool) {
let config = config::get_config(); let config = config::get_config();
let apps: &mut [Arc<dyn App>] = &mut [ let apps: &mut [Arc<dyn App + Send + Sync>] = &mut [
Arc::new(server_info::ServerInfoApp::new()), Arc::new(server_info::ServerInfoApp::new()),
Arc::new(auth::AuthApp::new()), Arc::new(auth::AuthApp::new()),
Arc::new(canvas_lms::CanvasLMSApp::new()), Arc::new(canvas_lms::CanvasLMSApp::new()),
@ -93,9 +94,29 @@ pub async fn main_server(dev: bool) {
Arc::new(webcheck::WebcheckApp::new()), Arc::new(webcheck::WebcheckApp::new()),
]; ];
let mut message_digestor = Vec::new();
for app in &mut *apps {
message_digestor.extend(app.clone().message_digestors());
}
let message_digestor = Arc::new(message_digestor);
let mut comm = GlobalCommunicator::new(); let mut comm = GlobalCommunicator::new();
if config.comm.discord.is_some() {
let discord_comm = Arc::new(
comm::discord::DiscordCommunicator::new(
config.comm.discord.as_ref().unwrap(),
message_digestor,
)
.await,
);
comm.add_communicator(discord_comm);
}
if config.comm.gotify.is_some() {
comm.add_communicator(Arc::new(comm::gotify::GotifyCommunicator::new(config))); comm.add_communicator(Arc::new(comm::gotify::GotifyCommunicator::new(config)));
}
if config.comm.email.is_some() {
comm.add_communicator(Arc::new(comm::email::EmailCommunicator::new(config))); comm.add_communicator(Arc::new(comm::email::EmailCommunicator::new(config)));
}
let app_state = Arc::new(Mutex::new(AppState { let app_state = Arc::new(Mutex::new(AppState {
db: establish_db_connection(), db: establish_db_connection(),
comm: comm, comm: comm,