use rocket::http::Status; use rocket::serde::{self, json::Json, Deserialize, Serialize}; use rocket_db_pools::Connection; use sqlx::types::JsonValue; use sqlx::Executor; use tracing::error; use url::Url; use uuid::Uuid; use crate::feed_utils::fetch_feed; use crate::user::AuthenticatedUser; use crate::Db; #[derive(Debug, Serialize)] #[serde(crate = "rocket::serde")] pub struct Feed { pub feed_id: Uuid, pub name: String, pub url: Url, pub user_id: Uuid, pub added_time: chrono::DateTime, pub last_checked_time: chrono::DateTime, pub 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: chrono::DateTime::UNIX_EPOCH, categorization: Vec::new(), } } pub async fn write_to_database<'a, E>(&self, executor: E) -> sqlx::Result<()> where E: Executor<'a, Database = sqlx::Sqlite>, { // Convert categorization to JSON value let categorization_json = serde::json::to_value(&self.categorization).map_err(|e| { error!("Failed to serialize categorization: {}", e); sqlx::Error::Decode(Box::new(e)) })?; let feed_id_str = self.feed_id.to_string(); let user_id_str = self.user_id.to_string(); let added_time_str = self.added_time.to_rfc3339(); let last_checked_time_str = self.last_checked_time.to_rfc3339(); let url_str = self.url.as_str(); let categorization_str = categorization_json.to_string(); sqlx::query!( r#" INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time, categorization) VALUES (?, ?, ?, ?, ?, ?, json(?)) "#, feed_id_str, self.name, url_str, user_id_str, added_time_str, last_checked_time_str, categorization_str ) .execute(executor) .await?; Ok(()) } } #[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); } let feed_data = fetch_feed(&new_feed.url) .await .map_err(|_| 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() .map(|s| s.to_string()) .unwrap_or_else(|| "".to_string()) }); let mut feed = Feed::new(name, new_feed.url, user.user_id); feed.categorization = new_feed.categorization; match feed.write_to_database(&mut **db).await { 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 name ASC "#, 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 })?; let feed_id = Uuid::parse_str(&row.feed_id).map_err(|e| { eprintln!("Failed to parse feed_id UUID: {}", e); Status::InternalServerError })?; let user_id = Uuid::parse_str(&row.user_id).map_err(|e| { eprintln!("Failed to parse user_id UUID: {}", e); Status::InternalServerError })?; Ok(Feed { name: row.name, feed_id, url, user_id, 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 feed_id_str = feed_uuid.to_string(); let user_id_str = user.user_id.to_string(); let result = sqlx::query!( "DELETE FROM feeds WHERE feed_id = ? AND user_id = ?", feed_id_str, user_id_str ) .execute(&mut **db) .await; match result { Ok(result) => { if result.rows_affected() > 0 { Status::NoContent } else { Status::NotFound } } Err(e) => { eprintln!("Database error: {}", e); Status::InternalServerError } } } #[get("/feeds/")] pub async fn get_feed( mut db: Connection, feed_id: &str, user: AuthenticatedUser, ) -> Result, Status> { // Validate UUID format let feed_uuid = Uuid::parse_str(feed_id).map_err(|_| Status::BadRequest)?; let user_id = user.user_id.to_string(); let row = 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 feed_id = ? AND user_id = ? "#, feed_uuid, user_id ) .fetch_optional(&mut **db) .await .map_err(|e| { eprintln!("Database error: {}", e); Status::InternalServerError })?; let row = row.ok_or(Status::NotFound)?; // 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 })?; let user_id = Uuid::parse_str(&row.user_id).map_err(|e| { eprintln!("Failed to parse user_id UUID: {}", e); Status::InternalServerError })?; let feed_id = Uuid::parse_str(&row.feed_id).map_err(|e| { eprintln!("Failed to parse feed_id UUID: {}", e); Status::InternalServerError })?; Ok(Json(Feed { feed_id, name: row.name, url, user_id, added_time: row.added_time, last_checked_time: row.last_checked_time, categorization, })) }