Creation of a reminder via web api
Concurrently run the reminder service as well as an web endpoint to create a new reminder. Note that a new reminder does not notify the reminder service yet. Cargo update dependencies
This commit is contained in:
parent
6b6f5aa48a
commit
7ded3ef430
5 changed files with 123 additions and 56 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
@ -10,9 +10,9 @@ checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.63"
|
version = "0.1.64"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eff18d764974428cf3a9328e23fc5c986f5fbed46e6cd4cdf42544df5d297ec1"
|
checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -95,9 +95,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.3.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
|
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
|
@ -289,36 +289,36 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.25"
|
version = "0.3.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
|
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.25"
|
version = "0.3.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
|
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.25"
|
version = "0.3.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
|
checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.25"
|
version = "0.3.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
|
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.25"
|
version = "0.3.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
|
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
@ -835,9 +835,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quoted_printable"
|
name = "quoted_printable"
|
||||||
version = "0.4.6"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb"
|
checksum = "a24039f627d8285853cc90dcddf8c1ebfaa91f834566948872b225b9a28ed1b6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r2d2"
|
name = "r2d2"
|
||||||
|
@ -994,9 +994,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c68e921cef53841b8925c2abadd27c9b891d9613bdc43d6b823062866df38e8"
|
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,7 +13,7 @@ categories = ["utility"]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
time = "0.3"
|
time = { version = "0.3", features = ["serde-human-readable"] }
|
||||||
lettre = { version = "0.10", features = ["tracing"] }
|
lettre = { version = "0.10", features = ["tracing"] }
|
||||||
diesel = { version = "2", features = ["postgres", "r2d2", "time"] }
|
diesel = { version = "2", features = ["postgres", "r2d2", "time"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
69
src/api.rs
Normal file
69
src/api.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::{schema, NewReminder};
|
||||||
|
use anyhow::Error;
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response, Result},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use diesel::{
|
||||||
|
prelude::*,
|
||||||
|
r2d2::{ConnectionManager, Pool},
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use tracing::{error, trace};
|
||||||
|
|
||||||
|
pub struct ServerError(Error);
|
||||||
|
|
||||||
|
impl IntoResponse for ServerError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
error!("{}", self.0);
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong.").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<E> for ServerError
|
||||||
|
where
|
||||||
|
E: Into<Error>,
|
||||||
|
{
|
||||||
|
fn from(err: E) -> Self {
|
||||||
|
Self(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CreateReminder {
|
||||||
|
#[serde(with = "time::serde::iso8601")]
|
||||||
|
planned: OffsetDateTime,
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
receiver: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
pub async fn create_reminder(
|
||||||
|
State(db_pool): State<Pool<ConnectionManager<PgConnection>>>,
|
||||||
|
Json(data): Json<CreateReminder>,
|
||||||
|
) -> Result<impl IntoResponse, ServerError> {
|
||||||
|
let reminder = NewReminder {
|
||||||
|
created: OffsetDateTime::now_utc(),
|
||||||
|
planned: data.planned,
|
||||||
|
title: &data.title,
|
||||||
|
message: &data.message,
|
||||||
|
receiver: &data.receiver,
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(?data, "received data");
|
||||||
|
|
||||||
|
diesel::insert_into(schema::reminders::table)
|
||||||
|
.values(&reminder)
|
||||||
|
.execute(&mut db_pool.get()?)?;
|
||||||
|
|
||||||
|
Ok((StatusCode::CREATED, "Reminder created".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
pub async fn not_found() -> impl IntoResponse {
|
||||||
|
(StatusCode::NOT_FOUND, "Page not found")
|
||||||
|
}
|
|
@ -106,7 +106,9 @@ impl Config {
|
||||||
xdg::BaseDirectories::with_prefix(env!("CARGO_BIN_NAME"))?.get_config_file(file);
|
xdg::BaseDirectories::with_prefix(env!("CARGO_BIN_NAME"))?.get_config_file(file);
|
||||||
match Self::load_file(&user_config) {
|
match Self::load_file(&user_config) {
|
||||||
Ok(config) => return Ok(config),
|
Ok(config) => return Ok(config),
|
||||||
Err(error) => warn!(file = ?user_config, "cannot load configuration: {:#}", Error::msg(error)),
|
Err(error) => {
|
||||||
|
warn!(file = ?user_config, "cannot load configuration: {:#}", Error::msg(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let global_config = format!("/etc/{}/{}", env!("CARGO_BIN_NAME"), file);
|
let global_config = format!("/etc/{}/{}", env!("CARGO_BIN_NAME"), file);
|
||||||
|
|
68
src/main.rs
68
src/main.rs
|
@ -1,17 +1,18 @@
|
||||||
use anyhow::Result;
|
use anyhow::{Error, Result};
|
||||||
use axum::{http::StatusCode, response::IntoResponse, Router};
|
use axum::{routing::post, Router};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
r2d2::{ConnectionManager, Pool, PooledConnection},
|
r2d2::{ConnectionManager, Pool},
|
||||||
};
|
};
|
||||||
use lettre::{Message, SmtpTransport, Transport};
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
use std::{env, net::SocketAddr};
|
use std::{env, net::SocketAddr, time::Duration};
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::OffsetDateTime;
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::{debug, info, trace};
|
use tracing::{debug, info, trace};
|
||||||
|
|
||||||
|
mod api;
|
||||||
mod args;
|
mod args;
|
||||||
mod config;
|
mod config;
|
||||||
mod models;
|
mod models;
|
||||||
|
@ -21,8 +22,22 @@ use args::Args;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use models::{NewReminder, Reminder};
|
use models::{NewReminder, Reminder};
|
||||||
|
|
||||||
|
fn get_connection_pool(config: &Config) -> Result<Pool<ConnectionManager<PgConnection>>> {
|
||||||
|
Ok(
|
||||||
|
Pool::builder().build(ConnectionManager::<PgConnection>::new(format!(
|
||||||
|
"postgresql://{}:{}@{}:{}/{}",
|
||||||
|
config.database.user,
|
||||||
|
config.database.pass,
|
||||||
|
config.database.host,
|
||||||
|
config.database.port,
|
||||||
|
config.database.name
|
||||||
|
)))?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cognitive_complexity)]
|
||||||
fn remind(
|
fn remind(
|
||||||
db: &mut PooledConnection<ConnectionManager<PgConnection>>,
|
db_pool: &mut Pool<ConnectionManager<PgConnection>>,
|
||||||
mailer: &SmtpTransport,
|
mailer: &SmtpTransport,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("checking for reminders");
|
info!("checking for reminders");
|
||||||
|
@ -30,7 +45,7 @@ fn remind(
|
||||||
let result = schema::reminders::dsl::reminders
|
let result = schema::reminders::dsl::reminders
|
||||||
.filter(schema::reminders::executed.is_null())
|
.filter(schema::reminders::executed.is_null())
|
||||||
.order(schema::reminders::planned.asc())
|
.order(schema::reminders::planned.asc())
|
||||||
.load::<Reminder>(db)?;
|
.load::<Reminder>(&mut db_pool.get()?)?;
|
||||||
|
|
||||||
if result.is_empty() {
|
if result.is_empty() {
|
||||||
info!("no reminders present, parking indefinitely");
|
info!("no reminders present, parking indefinitely");
|
||||||
|
@ -49,14 +64,14 @@ fn remind(
|
||||||
|
|
||||||
diesel::update(&reminder)
|
diesel::update(&reminder)
|
||||||
.set(schema::reminders::executed.eq(Some(OffsetDateTime::now_utc())))
|
.set(schema::reminders::executed.eq(Some(OffsetDateTime::now_utc())))
|
||||||
.execute(db)?;
|
.execute(&mut db_pool.get()?)?;
|
||||||
|
|
||||||
debug!("email sent to {}", reminder.receiver);
|
debug!("email sent to {}", reminder.receiver);
|
||||||
} else {
|
} else {
|
||||||
let duration = reminder.planned - OffsetDateTime::now_utc();
|
let duration = reminder.planned - OffsetDateTime::now_utc();
|
||||||
info!(?duration, "parking reminder");
|
info!(?duration, "parking reminder");
|
||||||
|
|
||||||
std::thread::park_timeout(<std::time::Duration>::try_from(duration)?);
|
std::thread::park_timeout(<Duration>::try_from(duration)?);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,11 +80,6 @@ fn remind(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unused_async)]
|
|
||||||
async fn not_found() -> impl IntoResponse {
|
|
||||||
(StatusCode::NOT_FOUND, "Page not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
@ -87,38 +97,24 @@ async fn main() -> Result<()> {
|
||||||
);
|
);
|
||||||
|
|
||||||
let config = Config::load_config(args.config)?;
|
let config = Config::load_config(args.config)?;
|
||||||
let db_pool = Pool::builder().build(ConnectionManager::<PgConnection>::new(format!(
|
|
||||||
"postgresql://{}:{}@{}:{}/{}",
|
|
||||||
config.database.user,
|
|
||||||
config.database.pass,
|
|
||||||
config.database.host,
|
|
||||||
config.database.port,
|
|
||||||
config.database.name
|
|
||||||
)))?;
|
|
||||||
|
|
||||||
let test_reminder = NewReminder {
|
let mut db_pool = get_connection_pool(&config)?;
|
||||||
created: OffsetDateTime::now_utc(),
|
|
||||||
planned: (OffsetDateTime::now_utc() + Duration::MINUTE),
|
|
||||||
title: "Test title",
|
|
||||||
message: "Test message",
|
|
||||||
receiver: "finga@localhost",
|
|
||||||
};
|
|
||||||
|
|
||||||
diesel::insert_into(schema::reminders::table)
|
std::thread::spawn(move || -> Result<(), Error> {
|
||||||
.values(&test_reminder)
|
|
||||||
.execute(&mut db_pool.get()?)?;
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let mailer = SmtpTransport::unencrypted_localhost();
|
let mailer = SmtpTransport::unencrypted_localhost();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
remind(&mut db_pool.get().unwrap(), &mailer).unwrap();
|
remind(&mut db_pool, &mailer)?;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let db_pool = get_connection_pool(&config)?;
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
.route("/v1/reminder", post(api::create_reminder))
|
||||||
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
|
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
|
||||||
.fallback(not_found);
|
.fallback(api::not_found)
|
||||||
|
.with_state(db_pool);
|
||||||
|
|
||||||
let addr = SocketAddr::from((config.server.address, config.server.port));
|
let addr = SocketAddr::from((config.server.address, config.server.port));
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue