From 012a4152ba21f52fdae68f1a2f240e2b0d5caebc Mon Sep 17 00:00:00 2001 From: Basique Evangelist Date: Wed, 25 Jan 2023 23:07:05 +0300 Subject: [PATCH 1/3] add atom feed endpoint --- Cargo.lock | 33 +++++++- Cargo.toml | 1 + src/routes/mod.rs | 1 + src/routes/updates.rs | 170 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 938439ad..af058b8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,6 +364,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atom_syndication" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91a85f2ee28cbd1ecf91288460f6dc74661fd99b4e9a559836a667ccf63aa38c" +dependencies = [ + "chrono", + "diligent-date-parser", + "quick-xml 0.27.1", +] + [[package]] name = "attohttpc" version = "0.19.1" @@ -987,6 +998,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "diligent-date-parser" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182" +dependencies = [ + "chrono", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1678,6 +1698,7 @@ dependencies = [ "actix-rt", "actix-web", "async-trait", + "atom_syndication", "base64 0.20.0", "bitflags", "bytes", @@ -2253,7 +2274,7 @@ dependencies = [ "itertools 0.9.0", "lazy_static", "nom 5.1.2", - "quick-xml", + "quick-xml 0.18.1", "regex", "regex-cache", "serde", @@ -2373,6 +2394,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" +dependencies = [ + "encoding_rs", + "memchr", +] + [[package]] name = "quote" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 1d20f4fb..da10d297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,3 +72,4 @@ sentry-actix = "0.29.1" image = "0.24.5" color-thief = "0.2.2" +atom_syndication = { version = "0.12.0", default-features = false } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 46e6c7e8..4cdb9a0a 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -89,6 +89,7 @@ pub fn maven_config(cfg: &mut web::ServiceConfig) { pub fn updates(cfg: &mut web::ServiceConfig) { cfg.service(updates::forge_updates); + cfg.service(updates::atom_feed); } pub fn versions_config(cfg: &mut web::ServiceConfig) { diff --git a/src/routes/updates.rs b/src/routes/updates.rs index c0a33c02..d066ae13 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; use actix_web::{get, web, HttpRequest, HttpResponse}; +use atom_syndication::{Feed, Text, Person, Category, Generator, Link, Entry, Content}; use serde::Serialize; use sqlx::PgPool; use crate::database; +use crate::database::models::TeamMember; +use crate::database::models::team_item::QueryTeamMember; use crate::models::projects::{Version, VersionType}; use crate::util::auth::{ get_user_from_headers, is_authorized, is_authorized_version, @@ -99,3 +102,170 @@ pub async fn forge_updates( Ok(HttpResponse::Ok().json(response)) } + +#[get("{id}/feed.atom")] +pub async fn atom_feed( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, +) -> Result { + let (id,) = info.into_inner(); + + let project = + if let Some(proj) = database::models::Project::get_full_from_slug_or_project_id(&id, &**pool) + .await? { + proj + } else { + return Ok(HttpResponse::NotFound().body("")); + }; + + let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + + if !is_authorized(&project.inner, &user_option, &pool).await? { + return Ok(HttpResponse::NotFound().body("")); + } + + // I would really like to get rid of this .clone() and making the method take a reference, but that + // necessitates a lot of refactoring. + let versions = + database::models::Version::get_many_full(project.versions.clone(), &**pool).await?; + + let mut versions = futures::stream::iter(versions) + .filter_map(|data| async { + if is_authorized_version(&data.inner, &user_option, &pool) + .await + .ok()? + { + Some(data) + } else { + None + } + }) + .collect::>() + .await; + + versions + .sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published)); + + let members_data = + TeamMember::get_from_team_full(project.inner.team_id, &**pool).await?; + + fn team_member_to_person(member: &QueryTeamMember) -> Person { + Person { + name: member.user.name.as_ref().unwrap_or(&member.user.username).clone(), + email: None, + uri: Some(format!("{}/user/{}", dotenvy::var("SITE_URL").unwrap_or_default(), member.user.username)) + } + } + + fn tag_to_category(tag: &String) -> Category { + Category { + term: tag.clone(), + scheme: None, + label: Some(tag.clone()) + } + } + + let project_id = crate::models::ids::ProjectId::from(project.inner.id); + + let feed = Feed { + title: Text::plain(project.inner.title.clone()), + id: project_id.to_string(), + updated: project.inner.updated.into(), + authors: members_data + .iter() + .filter(|x| x.role == crate::models::teams::OWNER_ROLE) + .map(team_member_to_person) + .collect::>(), + categories: project.categories + .iter() + .chain(project.additional_categories.iter()) + .map(tag_to_category) + .collect::>(), + contributors: members_data + .iter() + .filter(|x| x.role != crate::models::teams::OWNER_ROLE) + .map(team_member_to_person) + .collect::>(), + generator: Some(Generator { + value: "labrinth".to_string(), + uri: Some("https://github.com/modrinth/labrinth".to_string()), + version: Some(env!("CARGO_PKG_VERSION").to_string()) + }), + icon: project.inner.icon_url.clone(), + links: vec![ + Link { + href: format!( + "{}/{}/{}", + dotenvy::var("SITE_URL").unwrap_or_default(), + project.project_type, + project_id + ), + rel: "alternate".to_string(), + hreflang: None, + mime_type: Some("text/html".to_string()), + title: None, + length: None + } + ], + logo: None, + rights: None, + subtitle: Some(Text::plain(project.inner.description.clone())), + entries: versions + .iter() + .map(|v| { + let version_id = crate::models::ids::VersionId::from(v.inner.id); + let link = format!( + "{}/{}/{}/version/{}", + dotenvy::var("SITE_URL").unwrap_or_default(), project.project_type, project_id, version_id + ); + + Entry { + title: Text::plain(v.inner.name.clone()), + id: version_id.to_string(), + updated: v.inner.date_published.into(), + authors: members_data + .iter() + .find(|x| x.user.id == v.inner.author_id) + .map(team_member_to_person) + .into_iter() + .collect::>(), + categories: v.loaders + .iter() + .chain(v.game_versions.iter()) + .map(tag_to_category) + .collect::>(), + contributors: vec![], + links: vec![Link { + href: link.clone(), + rel: "alternate".to_string(), + hreflang: None, + mime_type: Some("text/html".to_string()), + title: None, + length: None + }], + published: Some(v.inner.date_published.into()), + rights: None, + source: None, + summary: None, + content: Some(Content { + base: None, + lang: Some("en_US".to_string()), + value: None, + src: Some(link), + content_type: None + }), + extensions: Default::default() + } + }) + .collect::>(), + extensions: Default::default(), + namespaces: Default::default(), + base: None, + lang: Some("en-US".to_string()) + }; + + Ok(HttpResponse::Ok() + .content_type("text/xml") + .body(feed.to_string())) +} \ No newline at end of file From b6f975b47656828628c9e936be9a8c1902e854c9 Mon Sep 17 00:00:00 2001 From: Basique Date: Tue, 3 Oct 2023 22:30:44 +0300 Subject: [PATCH 2/3] fix some validation errors --- src/routes/updates.rs | 59 ++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/routes/updates.rs b/src/routes/updates.rs index 3a49f5d5..91963e4a 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -148,15 +148,7 @@ pub async fn atom_feed( let versions = database::models::Version::get_many(&project.versions, &**pool, &redis).await?; - let mut versions = filter_authorized_versions( - versions - .into_iter() - .filter(|x| x.loaders.iter().any(|y| *y == "forge")) - .collect(), - &user_option, - &pool, - ) - .await?; + let mut versions = filter_authorized_versions(versions, &user_option, &pool).await?; versions.sort_by(|a, b| b.date_published.cmp(&a.date_published)); @@ -212,10 +204,16 @@ pub async fn atom_feed( } let project_id = crate::models::ids::ProjectId::from(project.inner.id); + let project_link = format!( + "{}/{}/{}", + dotenvy::var("SITE_URL").unwrap_or_default(), + project.project_type, + project_id + ); let feed = Feed { title: Text::plain(project.inner.title.clone()), - id: project_id.to_string(), + id: project_link.clone(), updated: project.inner.updated.into(), authors: members_data .iter() @@ -239,19 +237,28 @@ pub async fn atom_feed( version: Some(env!("CARGO_PKG_VERSION").to_string()), }), icon: project.inner.icon_url.clone(), - links: vec![Link { - href: format!( - "{}/{}/{}", - dotenvy::var("SITE_URL").unwrap_or_default(), - project.project_type, - project_id - ), - rel: "alternate".to_string(), - hreflang: None, - mime_type: Some("text/html".to_string()), - title: None, - length: None, - }], + links: vec![ + Link { + href: project_link, + rel: "alternate".to_string(), + hreflang: None, + mime_type: Some("text/html".to_string()), + title: None, + length: None, + }, + Link { + href: format!( + "{}/updates/{}/feed.atom", + dotenvy::var("SELF_ADDR").unwrap_or_default(), + project_id + ), + rel: "self".to_string(), + hreflang: None, + mime_type: Some("application/atom+xml".to_string()), + title: None, + length: None, + }, + ], logo: None, rights: None, subtitle: Some(Text::plain(project.inner.description.clone())), @@ -269,7 +276,7 @@ pub async fn atom_feed( Entry { title: Text::plain(v.name.clone()), - id: version_id.to_string(), + id: link.clone(), updated: v.date_published.into(), authors: members_data .iter() @@ -299,10 +306,10 @@ pub async fn atom_feed( summary: None, content: Some(Content { base: None, - lang: Some("en_US".to_string()), + lang: Some("en-us".to_string()), value: None, src: Some(link), - content_type: None, + content_type: Some("text/html".to_string()), }), extensions: Default::default(), } From c686bf1507fdaf853e1744a5c7db8238618ee2be Mon Sep 17 00:00:00 2001 From: Basique Date: Tue, 3 Oct 2023 23:00:55 +0300 Subject: [PATCH 3/3] Fix clippy warnings --- src/routes/updates.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/routes/updates.rs b/src/routes/updates.rs index 91963e4a..9adb4842 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -195,11 +195,11 @@ pub async fn atom_feed( } } - fn tag_to_category(tag: &String) -> Category { + fn tag_to_category(tag: &str) -> Category { Category { - term: tag.clone(), + term: tag.to_string(), scheme: None, - label: Some(tag.clone()), + label: Some(tag.to_string()), } } @@ -224,6 +224,7 @@ pub async fn atom_feed( .categories .iter() .chain(project.additional_categories.iter()) + .map(|x| x.as_str()) .map(tag_to_category) .collect::>(), contributors: members_data @@ -265,13 +266,12 @@ pub async fn atom_feed( entries: versions .iter() .map(|v| { - let version_id = crate::models::ids::VersionId::from(v.id); let link = format!( "{}/{}/{}/version/{}", dotenvy::var("SITE_URL").unwrap_or_default(), project.project_type, project_id, - version_id + v.id ); Entry { @@ -287,8 +287,8 @@ pub async fn atom_feed( categories: v .loaders .iter() - .map(|x| &x.0) - .chain(v.game_versions.iter().map(|x| &x.0)) + .map(|x| x.0.as_str()) + .chain(v.game_versions.iter().map(|x| x.0.as_str())) .map(tag_to_category) .collect::>(), contributors: vec![],