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
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);
|
||||
match Self::load_file(&user_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);
|
||||
|
|
68
src/main.rs
68
src/main.rs
|
@ -1,17 +1,18 @@
|
|||
use anyhow::Result;
|
||||
use axum::{http::StatusCode, response::IntoResponse, Router};
|
||||
use anyhow::{Error, Result};
|
||||
use axum::{routing::post, Router};
|
||||
use clap::Parser;
|
||||
use diesel::{
|
||||
prelude::*,
|
||||
r2d2::{ConnectionManager, Pool, PooledConnection},
|
||||
r2d2::{ConnectionManager, Pool},
|
||||
};
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
use std::{env, net::SocketAddr};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use std::{env, net::SocketAddr, time::Duration};
|
||||
use time::OffsetDateTime;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{debug, info, trace};
|
||||
|
||||
mod api;
|
||||
mod args;
|
||||
mod config;
|
||||
mod models;
|
||||
|
@ -21,8 +22,22 @@ use args::Args;
|
|||
use config::Config;
|
||||
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(
|
||||
db: &mut PooledConnection<ConnectionManager<PgConnection>>,
|
||||
db_pool: &mut Pool<ConnectionManager<PgConnection>>,
|
||||
mailer: &SmtpTransport,
|
||||
) -> Result<()> {
|
||||
info!("checking for reminders");
|
||||
|
@ -30,7 +45,7 @@ fn remind(
|
|||
let result = schema::reminders::dsl::reminders
|
||||
.filter(schema::reminders::executed.is_null())
|
||||
.order(schema::reminders::planned.asc())
|
||||
.load::<Reminder>(db)?;
|
||||
.load::<Reminder>(&mut db_pool.get()?)?;
|
||||
|
||||
if result.is_empty() {
|
||||
info!("no reminders present, parking indefinitely");
|
||||
|
@ -49,14 +64,14 @@ fn remind(
|
|||
|
||||
diesel::update(&reminder)
|
||||
.set(schema::reminders::executed.eq(Some(OffsetDateTime::now_utc())))
|
||||
.execute(db)?;
|
||||
.execute(&mut db_pool.get()?)?;
|
||||
|
||||
debug!("email sent to {}", reminder.receiver);
|
||||
} else {
|
||||
let duration = reminder.planned - OffsetDateTime::now_utc();
|
||||
info!(?duration, "parking reminder");
|
||||
|
||||
std::thread::park_timeout(<std::time::Duration>::try_from(duration)?);
|
||||
std::thread::park_timeout(<Duration>::try_from(duration)?);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
@ -65,11 +80,6 @@ fn remind(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn not_found() -> impl IntoResponse {
|
||||
(StatusCode::NOT_FOUND, "Page not found")
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
@ -87,38 +97,24 @@ async fn main() -> Result<()> {
|
|||
);
|
||||
|
||||
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 {
|
||||
created: OffsetDateTime::now_utc(),
|
||||
planned: (OffsetDateTime::now_utc() + Duration::MINUTE),
|
||||
title: "Test title",
|
||||
message: "Test message",
|
||||
receiver: "finga@localhost",
|
||||
};
|
||||
let mut db_pool = get_connection_pool(&config)?;
|
||||
|
||||
diesel::insert_into(schema::reminders::table)
|
||||
.values(&test_reminder)
|
||||
.execute(&mut db_pool.get()?)?;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
std::thread::spawn(move || -> Result<(), Error> {
|
||||
let mailer = SmtpTransport::unencrypted_localhost();
|
||||
|
||||
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()
|
||||
.route("/v1/reminder", post(api::create_reminder))
|
||||
.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));
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue