use rocket::http::Status; use rocket::serde::{self, json::Json, Deserialize, Serialize}; use rocket_db_pools::Connection; use sqlx::types::JsonValue; use url::Url; use uuid::Uuid; use crate::user::AuthenticatedUser; use crate::Db; #[derive(Debug, Serialize)] #[serde(crate = "rocket::serde")] pub struct Feed { feed_id: Uuid, name: String, url: Url, user_id: Uuid, added_time: chrono::DateTime, last_checked_time: chrono::DateTime, categorization: Vec, } impl Feed { pub fn new(name: String, url: Url, user_id: Uuid) -> Self { let now = chrono::Utc::now(); Feed { feed_id: Uuid::new_v4(), name, url, user_id, added_time: now, last_checked_time: now, categorization: Vec::new(), } } } #[derive(Debug, Deserialize)] #[serde(crate = "rocket::serde")] pub struct NewFeed { pub url: Url, #[serde(default)] pub categorization: Vec, } #[post("/feeds", data = "")] pub async fn create_feed( mut db: Connection, new_feed: Json, user: AuthenticatedUser, ) -> Result, Status> { let new_feed = new_feed.into_inner(); if !new_feed.url.scheme().eq("http") && !new_feed.url.scheme().eq("https") { eprintln!("Invalid URL scheme: {}", new_feed.url.scheme()); return Err(Status::UnprocessableEntity); } // Fetch the feed content let response = reqwest::get(new_feed.url.as_ref()).await.map_err(|e| { eprintln!("Failed to fetch feed: {}", e); Status::UnprocessableEntity })?; let content = response.text().await.map_err(|e| { eprintln!("Failed to read response body: {}", e); Status::UnprocessableEntity })?; // Parse the feed let feed_data = feed_rs::parser::parse(content.as_bytes()).map_err(|e| { eprintln!("Failed to parse feed content: {}", e); Status::UnprocessableEntity })?; // Use the feed title as the name, or URL if no title is available let name = feed_data .title .map(|t| t.content) .unwrap_or_else(|| new_feed.url.host_str().unwrap_or("Unknown").to_string()); let mut feed = Feed::new(name, new_feed.url, user.user_id); feed.categorization = new_feed.categorization; // Convert categorization to JSON value let categorization_json = serde::json::to_value(&feed.categorization).map_err(|e| { eprintln!("Failed to serialize categorization: {}", e); Status::InternalServerError })?; let query = sqlx::query( "INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time, categorization) VALUES (?1, ?2, ?3, ?4, ?5, ?6, json(?7))", ) .bind(feed.feed_id.to_string()) .bind(&feed.name) .bind(feed.url.as_str()) .bind(feed.user_id.to_string()) .bind(feed.added_time.to_rfc3339()) .bind(feed.last_checked_time.to_rfc3339()) .bind(&categorization_json.to_string()) .execute(&mut **db) .await; match query { Ok(_) => Ok(Json(feed)), Err(e) => { eprintln!("Database error: {}", e); match e { sqlx::Error::Database(db_err) if db_err.is_unique_violation() => { Err(Status::Conflict) } _ => Err(Status::InternalServerError), } } } } #[get("/feeds")] pub async fn list_feeds( mut db: Connection, user: AuthenticatedUser, ) -> Result>, Status> { let user_id = user.user_id.to_string(); let query = sqlx::query!( r#" SELECT feed_id as "feed_id: String", name, url, user_id as "user_id: String", added_time as "added_time: chrono::DateTime", last_checked_time as "last_checked_time: chrono::DateTime", categorization as "categorization: JsonValue" FROM feeds WHERE user_id = ? ORDER BY added_time DESC "#, user_id ) .fetch_all(&mut **db) .await .map_err(|e| { eprintln!("Database error: {}", e); Status::InternalServerError })?; let feeds = query .into_iter() .map(|row| { // Parse URL from string let url = Url::parse(&row.url).map_err(|e| { eprintln!("Failed to parse URL '{}': {}", row.url, e); Status::InternalServerError })?; // Parse categorization from JSON value let categorization: Vec = serde::json::from_value(row.categorization).map_err(|e| { eprintln!("Failed to parse categorization: {}", e); Status::InternalServerError })?; Ok(Feed { feed_id: Uuid::parse_str(&row.feed_id).unwrap(), name: row.name, url, user_id: Uuid::parse_str(&row.user_id).unwrap(), added_time: row.added_time, last_checked_time: row.last_checked_time, categorization, }) }) .collect::, Status>>()?; Ok(Json(feeds)) } #[delete("/feeds/")] pub async fn delete_feed(mut db: Connection, feed_id: &str, user: AuthenticatedUser) -> Status { // Validate UUID format let feed_uuid = match Uuid::parse_str(feed_id) { Ok(uuid) => uuid, Err(_) => return Status::BadRequest, }; let query = sqlx::query("DELETE FROM feeds WHERE feed_id = ? AND user_id = ?") .bind(feed_uuid.to_string()) .bind(user.user_id.to_string()) .execute(&mut **db) .await; match query { Ok(result) => { if result.rows_affected() > 0 { Status::NoContent } else { Status::NotFound } } Err(e) => { eprintln!("Database error: {}", e); Status::InternalServerError } } }