/*
* Copyright (C) 2025 Jonni Liljamo <jonni@liljamo.com>
*
* This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for
* more information.
*/
use std::sync::Arc;
use axum::{
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use clap::Parser;
use reqwest::StatusCode;
use serde::Deserialize;
use tokio::net::TcpListener;
use tower_http::trace::TraceLayer;
use tracing::{error, info};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod config;
use config::Config;
mod state;
use state::State;
mod service;
#[derive(Parser)]
#[command(version)]
struct Args {
/// Config file location
#[arg(short, long, default_value = "./canwa.toml")]
config: String,
}
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
format!(
"{}=debug,tower_http=debug,axum::rejection=trace",
env!("CARGO_CRATE_NAME")
)
.into()
}),
)
.with(tracing_subscriber::fmt::layer())
.init();
let args: Args = Args::parse();
let config: Config = Config::from_path(&args.config).await.unwrap();
let state: Arc<State> = Arc::new(State::from_config(&config).unwrap());
let router = Router::new()
.route("/", get(|| async { "canwa" }))
.route(
"/message",
post({
let shared_state = Arc::clone(&state);
move |body| message(shared_state, body)
}),
)
.layer(TraceLayer::new_for_http())
.with_state(state);
let addr = format!("{}:{}", config.interface, config.port);
info!(msg="serving http", %addr);
let listener = TcpListener::bind(addr).await.unwrap();
axum::serve(listener, router).await.unwrap();
}
#[derive(Deserialize)]
struct MessageForm {
token: String,
title: String,
message: String,
}
async fn message(state: Arc<State>, Json(message): Json<MessageForm>) -> impl IntoResponse {
let notifier = match state
.notifiers
.iter()
.find(|(_k, v)| v.token == message.token)
{
Some(n) => n,
None => return (StatusCode::UNAUTHORIZED, "unauthorized"),
};
info!(msg = "message", notifier = notifier.0);
for (_k, v) in state
.services
.iter()
.filter(|(k, _v)| notifier.1.services.contains(k))
{
match v.send(&message.title, &message.message).await {
Ok(_) => {}
Err(err) => {
error!(msg = "message sending failed", ?err);
return (StatusCode::INTERNAL_SERVER_ERROR, "failed to send message");
}
}
}
(StatusCode::OK, "")
}