From ab5f97d05f1a7e4b10cb38eae4a5190a693322cb Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 14 Dec 2025 21:43:28 -0800 Subject: [PATCH] heavy start on add-feeds endpoint --- Cargo.lock | 1 + Cargo.toml | 1 + src/lib.rs | 12 +++++--- src/server.rs | 83 +++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 228e5c8..64b24f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,7 @@ dependencies = [ "rand 0.9.2", "reqwest", "serde", + "serde_json", "serde_urlencoded", "sqlx", "thiserror 2.0.17", diff --git a/Cargo.toml b/Cargo.toml index 8d626df..82ab152 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ html2md = "0.2.15" justerror = "1.1.0" reqwest = "0.12.24" serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" serde_urlencoded = "0.7.1" sqlx = { version = "0.8.6", default-features = false, features = ["chrono", "derive", "macros", "migrate", "runtime-tokio", "sqlite", "tls-none"] } thiserror = "2.0.17" diff --git a/src/lib.rs b/src/lib.rs index 11674a4..a9c16fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use std::time::Duration; use feed_rs::parser::parse; use reqwest::StatusCode; +use server::ServerState; use sqlx::{ SqlitePool, sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}, @@ -30,7 +31,8 @@ pub struct BlogdorTheAggregator { endpoint: String, channel_id: u32, email: String, - password: String, + password: String, // sent *to zulip* in POSTs *from us* + token: String, // checked against incoming POSTs *from zulip* } #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -72,9 +74,9 @@ impl BlogdorTheAggregator { .expect("ZULIP_CHANNEL must be set") .parse() .expect("ZULIP_CHANNEL must be an integer"); - - let email = std::env::var("BLOGDOR_EMAIL").expect("BLOGDOR_EMAIL must be set"); let password = std::env::var("ZULIP_TOKEN").expect("ZULIP_TOKEN must be set"); + let email = std::env::var("BLOGDOR_EMAIL").expect("BLOGDOR_EMAIL must be set"); + let token = std::env::var("BLOGDOR_TOKEN").expect("BLOGDOR_TOKEN must be set"); Self { db, @@ -84,6 +86,7 @@ impl BlogdorTheAggregator { channel_id, email, password, + token, } } @@ -92,7 +95,8 @@ impl BlogdorTheAggregator { } pub async fn spawn_http(&self) { - server::spawn_server(self.db.clone(), self.cancel.clone()).await; + let state = ServerState::new(self.db.clone(), &self.email, &self.token); + server::spawn_server(state, self.cancel.clone()).await; } pub async fn check_feeds(&self) -> Result>, String> { diff --git a/src/server.rs b/src/server.rs index e1f8323..a207b17 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,15 +1,42 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, sync::Arc}; -use axum::{Router, routing::post}; +use axum::{ + Router, + extract::{Json, State}, + http::StatusCode, + response::IntoResponse, + routing::post, +}; +use serde::Deserialize; +use serde_json::{Map, Value}; use sqlx::SqlitePool; use tokio_util::sync::CancellationToken; +type Payload = Map; + +#[derive(Debug, Clone)] +pub struct ServerState { + db: SqlitePool, + email: String, + token: String, +} + +impl ServerState { + pub fn new(db: SqlitePool, email: &str, token: &str) -> Self { + Self { + db, + email: email.to_string(), + token: token.to_string(), + } + } +} + pub(crate) async fn spawn_server( - pool: SqlitePool, + state: ServerState, cancel: CancellationToken, ) -> tokio::task::JoinHandle<()> { tokio::task::spawn(async move { - let server = make_router(pool); + let server = make_router(state); let addr: SocketAddr = ([127, 0, 0, 1], 3000).into(); tracing::debug!("binding to {addr:?}"); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); @@ -20,15 +47,28 @@ pub(crate) async fn spawn_server( }) } -fn make_router(db: SqlitePool) -> Router { +fn make_router(state: ServerState) -> Router { Router::new() - .route( - "/api/v1/add-feed", - post(async move || { - tracing::debug!("got a post to add a feed"); - }), - ) - .with_state(db) + .route("/api/v1/add-feed", post(handle_add_feed)) + .with_state(state.into()) +} + +async fn handle_add_feed( + State(state): State>, + Json(request): Json, +) -> impl IntoResponse { + let AddFeedRequest { + bot_email, + token, + message, + _rest: _, + } = request; + if state.email == bot_email && state.token == token { + tracing::debug!("gonna do a thing with {message:?}"); + } else { + tracing::debug!("psych"); + } + (StatusCode::IM_A_TEAPOT, "nee-ope") } async fn graceful_shutdown(cancel: CancellationToken) { @@ -56,3 +96,22 @@ async fn graceful_shutdown(cancel: CancellationToken) { } cancel.cancel(); } + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +struct AddFeedRequest { + bot_email: String, + token: String, + message: ZulipMessage, + #[serde(flatten)] + _rest: Payload, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +struct ZulipMessage { + content: String, + sender_email: String, + sender_id: u32, + sender_full_name: String, + #[serde(flatten)] + _rest: Payload, +}