rss-reader/src/feeds.rs

207 lines
6.0 KiB
Rust
Raw Normal View History

2025-02-02 01:41:38 -08:00
use rocket::http::Status;
2025-02-02 13:52:04 -08:00
use rocket::serde::{self, json::Json, Deserialize, Serialize};
2025-02-02 01:41:38 -08:00
use rocket_db_pools::Connection;
2025-02-02 13:52:04 -08:00
use sqlx::types::JsonValue;
2025-02-02 02:01:51 -08:00
use url::Url;
2025-02-02 03:28:08 -08:00
use uuid::Uuid;
2025-02-02 01:41:38 -08:00
use crate::user::AuthenticatedUser;
2025-02-02 03:28:08 -08:00
use crate::Db;
2025-02-02 01:41:38 -08:00
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Feed {
feed_id: Uuid,
name: String,
2025-02-02 02:01:51 -08:00
url: Url,
2025-02-02 01:41:38 -08:00
user_id: Uuid,
added_time: chrono::DateTime<chrono::Utc>,
last_checked_time: chrono::DateTime<chrono::Utc>,
2025-02-02 13:52:04 -08:00
categorization: Vec<String>,
2025-02-02 01:41:38 -08:00
}
impl Feed {
2025-02-02 02:01:51 -08:00
pub fn new(name: String, url: Url, user_id: Uuid) -> Self {
2025-02-02 01:41:38 -08:00
let now = chrono::Utc::now();
Feed {
feed_id: Uuid::new_v4(),
name,
url,
user_id,
added_time: now,
last_checked_time: now,
2025-02-02 13:52:04 -08:00
categorization: Vec::new(),
2025-02-02 01:41:38 -08:00
}
}
}
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct NewFeed {
2025-02-02 13:52:04 -08:00
pub url: Url,
#[serde(default)]
pub categorization: Vec<String>,
2025-02-02 01:41:38 -08:00
}
#[post("/feeds", data = "<new_feed>")]
pub async fn create_feed(
mut db: Connection<Db>,
new_feed: Json<NewFeed>,
user: AuthenticatedUser,
) -> Result<Json<Feed>, Status> {
let new_feed = new_feed.into_inner();
2025-02-02 03:25:51 -08:00
2025-02-02 02:01:51 -08:00
if !new_feed.url.scheme().eq("http") && !new_feed.url.scheme().eq("https") {
2025-02-02 03:25:51 -08:00
eprintln!("Invalid URL scheme: {}", new_feed.url.scheme());
2025-02-02 02:01:51 -08:00
return Err(Status::UnprocessableEntity);
}
2025-02-02 03:25:51 -08:00
// Fetch the feed content
2025-02-02 03:28:08 -08:00
let response = reqwest::get(new_feed.url.as_ref()).await.map_err(|e| {
eprintln!("Failed to fetch feed: {}", e);
Status::UnprocessableEntity
})?;
2025-02-02 03:25:51 -08:00
2025-02-02 03:28:08 -08:00
let content = response.text().await.map_err(|e| {
eprintln!("Failed to read response body: {}", e);
Status::UnprocessableEntity
})?;
2025-02-02 03:25:51 -08:00
// Parse the feed
2025-02-02 03:28:08 -08:00
let feed_data = feed_rs::parser::parse(content.as_bytes()).map_err(|e| {
eprintln!("Failed to parse feed content: {}", e);
Status::UnprocessableEntity
})?;
2025-02-02 03:25:51 -08:00
// Use the feed title as the name, or URL if no title is available
2025-02-02 03:28:08 -08:00
let name = feed_data
.title
2025-02-02 03:25:51 -08:00
.map(|t| t.content)
.unwrap_or_else(|| new_feed.url.host_str().unwrap_or("Unknown").to_string());
2025-02-02 13:52:04 -08:00
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
})?;
2025-02-02 01:41:38 -08:00
let query = sqlx::query(
2025-02-02 13:52:04 -08:00
"INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time, categorization)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, json(?7))",
2025-02-02 01:41:38 -08:00
)
2025-02-02 03:28:08 -08:00
.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())
2025-02-02 13:52:04 -08:00
.bind(&categorization_json.to_string())
2025-02-02 03:28:08 -08:00
.execute(&mut **db)
.await;
2025-02-02 01:41:38 -08:00
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<Db>,
user: AuthenticatedUser,
) -> Result<Json<Vec<Feed>>, 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<chrono::Utc>",
2025-02-02 13:52:04 -08:00
last_checked_time as "last_checked_time: chrono::DateTime<chrono::Utc>",
categorization as "categorization: JsonValue"
2025-02-02 01:41:38 -08:00
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()
2025-02-02 02:01:51 -08:00
.map(|row| {
// Parse URL from string
2025-02-02 13:52:04 -08:00
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<String> = serde::json::from_value(row.categorization).map_err(|e| {
eprintln!("Failed to parse categorization: {}", e);
Status::InternalServerError
})?;
2025-02-02 03:28:08 -08:00
2025-02-02 02:01:51 -08:00
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,
2025-02-02 13:52:04 -08:00
categorization,
2025-02-02 02:01:51 -08:00
})
2025-02-02 01:41:38 -08:00
})
2025-02-02 02:01:51 -08:00
.collect::<Result<Vec<_>, Status>>()?;
2025-02-02 01:41:38 -08:00
Ok(Json(feeds))
}
#[delete("/feeds/<feed_id>")]
2025-02-02 03:28:08 -08:00
pub async fn delete_feed(mut db: Connection<Db>, feed_id: &str, user: AuthenticatedUser) -> Status {
2025-02-02 01:41:38 -08:00
// Validate UUID format
let feed_uuid = match Uuid::parse_str(feed_id) {
Ok(uuid) => uuid,
Err(_) => return Status::BadRequest,
};
2025-02-02 03:28:08 -08:00
let query = sqlx::query("DELETE FROM feeds WHERE feed_id = ? AND user_id = ?")
2025-02-02 01:41:38 -08:00
.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
}
}
2025-02-02 03:28:08 -08:00
}