From 7db6ebf213103c5be278949018c4ea06927dd46b Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 24 Feb 2025 16:00:49 +0100 Subject: [PATCH] feat: implement simple api subscription routes --- .gitignore | 1 + src/db/mod.rs | 1 + src/db/models/subscription.rs | 19 ++++- src/server/gpodder/format.rs | 4 +- src/server/gpodder/mod.rs | 13 ++- src/server/gpodder/simple/mod.rs | 4 +- src/server/gpodder/simple/subscriptions.rs | 96 ++++++++++++++++++++++ 7 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 src/server/gpodder/simple/subscriptions.rs diff --git a/.gitignore b/.gitignore index 8b77cd3..5fac962 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target data +venv diff --git a/src/db/mod.rs b/src/db/mod.rs index 68c1607..8aa2823 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -3,6 +3,7 @@ mod schema; pub use models::device::{Device, DeviceType, NewDevice}; pub use models::session::Session; +pub use models::subscription::{NewSubscription, Subscription}; pub use models::user::{NewUser, User}; use diesel::{ diff --git a/src/db/models/subscription.rs b/src/db/models/subscription.rs index ce1d6bd..16f3530 100644 --- a/src/db/models/subscription.rs +++ b/src/db/models/subscription.rs @@ -22,7 +22,7 @@ pub struct NewSubscription { impl Subscription { pub fn for_device(pool: &DbPool, device_id: i64) -> DbResult> { - Ok(subscriptions::dsl::subscriptions + Ok(subscriptions::table .select(subscriptions::url) .filter(subscriptions::device_id.eq(device_id)) .get_results(&mut pool.get()?)?) @@ -36,4 +36,21 @@ impl Subscription { .distinct() .get_results(&mut pool.get()?)?) } + + pub fn update_for_device(pool: &DbPool, device_id: i64, urls: Vec) -> DbResult<()> { + pool.get()?.transaction(|conn| { + diesel::delete(subscriptions::table.filter(subscriptions::device_id.eq(device_id))) + .execute(conn)?; + + diesel::insert_into(subscriptions::table) + .values( + urls.into_iter() + .map(|url| NewSubscription { device_id, url }) + .collect::>(), + ) + .execute(conn)?; + + Ok(()) + }) + } } diff --git a/src/server/gpodder/format.rs b/src/server/gpodder/format.rs index 4c2a5de..286c61f 100644 --- a/src/server/gpodder/format.rs +++ b/src/server/gpodder/format.rs @@ -9,8 +9,8 @@ use serde::{ #[serde(rename_all = "lowercase")] pub enum Format { Json, - OPML, - Plaintext, + // OPML, + // Plaintext, } #[derive(Debug)] diff --git a/src/server/gpodder/mod.rs b/src/server/gpodder/mod.rs index 00f1d6b..1c8dd50 100644 --- a/src/server/gpodder/mod.rs +++ b/src/server/gpodder/mod.rs @@ -5,7 +5,7 @@ mod simple; use axum::{ extract::{Request, State}, - http::{HeaderName, HeaderValue, StatusCode}, + http::{header::WWW_AUTHENTICATE, HeaderName, HeaderValue, StatusCode}, middleware::Next, response::{IntoResponse, Response}, RequestExt, Router, @@ -43,7 +43,6 @@ pub fn router(ctx: Context) -> Router { pub async fn auth_middleware(State(ctx): State, mut req: Request, next: Next) -> Response { // SAFETY: this extractor's error type is Infallible let jar: CookieJar = req.extract_parts().await.unwrap(); - tracing::debug!("{:?}", jar); let mut auth_user = None; let mut new_session_id = None; @@ -107,6 +106,14 @@ pub async fn auth_middleware(State(ctx): State, mut req: Request, next: res } } else { - StatusCode::UNAUTHORIZED.into_response() + let mut res = StatusCode::UNAUTHORIZED.into_response(); + + // This is what the gpodder.net service returns, and some clients seem to depend on it + res.headers_mut().insert( + WWW_AUTHENTICATE, + HeaderValue::from_static("Basic realm=\"\""), + ); + + res } } diff --git a/src/server/gpodder/simple/mod.rs b/src/server/gpodder/simple/mod.rs index 299d671..1ae1702 100644 --- a/src/server/gpodder/simple/mod.rs +++ b/src/server/gpodder/simple/mod.rs @@ -1,7 +1,9 @@ +mod subscriptions; + use axum::Router; use crate::server::Context; pub fn router(ctx: Context) -> Router { - Router::new() + Router::new().nest("/subscriptions", subscriptions::router(ctx)) } diff --git a/src/server/gpodder/simple/subscriptions.rs b/src/server/gpodder/simple/subscriptions.rs new file mode 100644 index 0000000..dbbc861 --- /dev/null +++ b/src/server/gpodder/simple/subscriptions.rs @@ -0,0 +1,96 @@ +use axum::{ + extract::{Path, State}, + middleware, + routing::get, + Extension, Form, Json, Router, +}; + +use crate::{ + db, + server::{ + error::{AppError, AppResult}, + gpodder::{auth_middleware, format::StringWithFormat, models::DeviceType}, + Context, + }, +}; + +pub fn router(ctx: Context) -> Router { + Router::new() + .route( + "/{username}/{id}", + get(get_device_subscriptions).put(put_device_subscriptions), + ) + .route("/{username}", get(get_user_subscriptions)) + .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware)) +} + +pub async fn get_device_subscriptions( + State(ctx): State, + Path((username, id)): Path<(String, StringWithFormat)>, + Extension(user): Extension, +) -> AppResult>> { + if username != user.username { + return Err(AppError::BadRequest); + } + + let subscriptions = tokio::task::spawn_blocking(move || { + let device = + db::Device::by_device_id(&ctx.pool, user.id, &id)?.ok_or(AppError::NotFound)?; + + Ok::<_, AppError>(db::Subscription::for_device(&ctx.pool, device.id)?) + }) + .await + .unwrap()?; + + Ok(Json(subscriptions)) +} + +pub async fn get_user_subscriptions( + State(ctx): State, + Path(username): Path, + Extension(user): Extension, +) -> AppResult>> { + if *username != user.username { + return Err(AppError::BadRequest); + } + + let subscriptions = + tokio::task::spawn_blocking(move || db::Subscription::for_user(&ctx.pool, user.id)) + .await + .unwrap()?; + + Ok(Json(subscriptions)) +} + +pub async fn put_device_subscriptions( + State(ctx): State, + Path((username, id)): Path<(String, StringWithFormat)>, + Extension(user): Extension, + Json(urls): Json>, +) -> AppResult<()> { + if *username != user.username { + return Err(AppError::BadRequest); + } + + tokio::task::spawn_blocking(move || { + let device = if let Some(device) = db::Device::by_device_id(&ctx.pool, user.id, &id)? { + device + } else { + db::NewDevice::new( + user.id, + id.to_string(), + String::new(), + DeviceType::Other.into(), + ) + .insert(&ctx.pool)? + }; + + Ok::<_, AppError>(db::Subscription::update_for_device( + &ctx.pool, device.id, urls, + )?) + }) + .await + .unwrap()?; + + Ok(()) +}