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-04 01:04:37 -08:00
|
|
|
use sqlx::Executor;
|
2025-02-04 23:57:02 -08:00
|
|
|
use tracing::error;
|
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
|
|
|
|
2025-02-02 15:24:33 -08:00
|
|
|
use crate::feed_utils::fetch_feed;
|
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 {
|
2025-02-03 15:46:28 -08:00
|
|
|
pub feed_id: Uuid,
|
|
|
|
pub name: String,
|
|
|
|
pub url: Url,
|
|
|
|
pub user_id: Uuid,
|
|
|
|
pub added_time: chrono::DateTime<chrono::Utc>,
|
|
|
|
pub last_checked_time: chrono::DateTime<chrono::Utc>,
|
|
|
|
pub 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,
|
2025-02-04 14:05:35 -08:00
|
|
|
last_checked_time: chrono::DateTime::UNIX_EPOCH,
|
2025-02-02 13:52:04 -08:00
|
|
|
categorization: Vec::new(),
|
2025-02-02 01:41:38 -08:00
|
|
|
}
|
|
|
|
}
|
2025-02-03 04:29:16 -08:00
|
|
|
|
2025-02-04 01:01:02 -08:00
|
|
|
pub async fn write_to_database<'a, E>(&self, executor: E) -> sqlx::Result<()>
|
|
|
|
where
|
2025-02-04 01:04:37 -08:00
|
|
|
E: Executor<'a, Database = sqlx::Sqlite>,
|
2025-02-04 01:01:02 -08:00
|
|
|
{
|
2025-02-03 04:29:16 -08:00
|
|
|
// Convert categorization to JSON value
|
2025-02-03 15:46:28 -08:00
|
|
|
let categorization_json = serde::json::to_value(&self.categorization).map_err(|e| {
|
2025-02-04 23:57:02 -08:00
|
|
|
error!("Failed to serialize categorization: {}", e);
|
2025-02-03 15:46:28 -08:00
|
|
|
sqlx::Error::Decode(Box::new(e))
|
|
|
|
})?;
|
2025-02-03 04:29:16 -08:00
|
|
|
|
2025-02-05 03:01:39 -08:00
|
|
|
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
|
2025-02-03 04:29:16 -08:00
|
|
|
)
|
2025-02-04 01:01:02 -08:00
|
|
|
.execute(executor)
|
2025-02-03 04:29:16 -08:00
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
2025-02-02 14:28:23 -08:00
|
|
|
#[post("/feeds", data = "<new_feed>")]
|
|
|
|
pub async fn create_feed(
|
2025-02-04 01:01:02 -08:00
|
|
|
mut db: Connection<Db>,
|
2025-02-02 14:28:23 -08:00
|
|
|
new_feed: Json<NewFeed>,
|
|
|
|
user: AuthenticatedUser,
|
|
|
|
) -> Result<Json<Feed>, 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)?;
|
|
|
|
|
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 21:07:22 -08:00
|
|
|
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(|| "<Unknown>".to_string())
|
|
|
|
});
|
2025-02-02 03:25:51 -08:00
|
|
|
|
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;
|
|
|
|
|
2025-02-04 01:01:02 -08:00
|
|
|
match feed.write_to_database(&mut **db).await {
|
2025-02-02 01:41:38 -08:00
|
|
|
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 = ?
|
2025-02-03 16:32:40 -08:00
|
|
|
ORDER BY name ASC
|
2025-02-02 01:41:38 -08:00
|
|
|
"#,
|
|
|
|
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
|
2025-02-02 14:28:23 -08:00
|
|
|
let categorization: Vec<String> =
|
|
|
|
serde::json::from_value(row.categorization).map_err(|e| {
|
|
|
|
eprintln!("Failed to parse categorization: {}", e);
|
|
|
|
Status::InternalServerError
|
|
|
|
})?;
|
2025-02-02 21:07:22 -08:00
|
|
|
let feed_id = Uuid::parse_str(&row.feed_id).map_err(|e| {
|
|
|
|
eprintln!("Failed to parse feed_id UUID: {}", e);
|
|
|
|
Status::InternalServerError
|
|
|
|
})?;
|
2025-02-02 03:28:08 -08:00
|
|
|
|
2025-02-02 21:07:22 -08:00
|
|
|
let user_id = Uuid::parse_str(&row.user_id).map_err(|e| {
|
|
|
|
eprintln!("Failed to parse user_id UUID: {}", e);
|
|
|
|
Status::InternalServerError
|
|
|
|
})?;
|
2025-02-02 02:01:51 -08:00
|
|
|
Ok(Feed {
|
|
|
|
name: row.name,
|
2025-02-02 21:07:22 -08:00
|
|
|
feed_id,
|
2025-02-02 02:01:51 -08:00
|
|
|
url,
|
2025-02-02 21:07:22 -08:00
|
|
|
user_id,
|
2025-02-02 02:01:51 -08:00
|
|
|
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-05 03:01:39 -08:00
|
|
|
let feed_id_str = feed_uuid.to_string();
|
|
|
|
let user_id_str = user.user_id.to_string();
|
2025-02-02 01:41:38 -08:00
|
|
|
|
2025-02-05 03:01:39 -08:00
|
|
|
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 {
|
2025-02-02 01:41:38 -08:00
|
|
|
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
|
|
|
}
|
2025-02-02 14:49:08 -08:00
|
|
|
|
|
|
|
#[get("/feeds/<feed_id>")]
|
|
|
|
pub async fn get_feed(
|
|
|
|
mut db: Connection<Db>,
|
|
|
|
feed_id: &str,
|
|
|
|
user: AuthenticatedUser,
|
|
|
|
) -> Result<Json<Feed>, 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<chrono::Utc>",
|
|
|
|
last_checked_time as "last_checked_time: chrono::DateTime<chrono::Utc>",
|
|
|
|
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<String> = serde::json::from_value(row.categorization).map_err(|e| {
|
|
|
|
eprintln!("Failed to parse categorization: {}", e);
|
|
|
|
Status::InternalServerError
|
|
|
|
})?;
|
2025-02-02 21:07:22 -08:00
|
|
|
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
|
|
|
|
})?;
|
2025-02-02 14:49:08 -08:00
|
|
|
|
|
|
|
Ok(Json(Feed {
|
2025-02-02 21:07:22 -08:00
|
|
|
feed_id,
|
2025-02-02 14:49:08 -08:00
|
|
|
name: row.name,
|
|
|
|
url,
|
2025-02-02 21:07:22 -08:00
|
|
|
user_id,
|
2025-02-02 14:49:08 -08:00
|
|
|
added_time: row.added_time,
|
|
|
|
last_checked_time: row.last_checked_time,
|
|
|
|
categorization,
|
|
|
|
}))
|
|
|
|
}
|