Create Atom feeds for local users

This commit is contained in:
silverpill 2022-04-13 17:45:47 +00:00
parent aa997e3a82
commit c0837bbf77
9 changed files with 168 additions and 53 deletions

77
Cargo.lock generated
View file

@ -278,15 +278,13 @@ dependencies = [
[[package]]
name = "ammonia"
version = "3.1.2"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e445c26125ff80316eaea16e812d717b147b82a68682bd4730f74d4845c8b35"
checksum = "d5ed2509ee88cc023cccee37a6fab35826830fe8b748b3869790e7720c2c4a74"
dependencies = [
"html5ever",
"lazy_static",
"maplit",
"markup5ever_rcdom",
"matches",
"once_cell",
"tendril",
"url 2.2.2",
]
@ -1158,9 +1156,9 @@ dependencies = [
[[package]]
name = "html5ever"
version = "0.25.1"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b"
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
@ -1488,30 +1486,18 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "markup5ever"
version = "0.10.1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [
"log",
"phf 0.8.0",
"phf",
"phf_codegen",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b"
dependencies = [
"html5ever",
"markup5ever",
"tendril",
"xml5ever",
]
[[package]]
name = "matches"
version = "0.1.8"
@ -1762,9 +1748,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.8.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
[[package]]
name = "opaque-debug"
@ -1924,15 +1910,6 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_shared 0.8.0",
]
[[package]]
name = "phf"
version = "0.10.1"
@ -1944,12 +1921,12 @@ dependencies = [
[[package]]
name = "phf_codegen"
version = "0.8.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator",
"phf_shared 0.8.0",
"phf_generator 0.10.0",
"phf_shared 0.10.0",
]
[[package]]
@ -1962,6 +1939,16 @@ dependencies = [
"rand 0.7.3",
]
[[package]]
name = "phf_generator"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand 0.8.4",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -2879,7 +2866,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
dependencies = [
"phf_generator",
"phf_generator 0.8.0",
"phf_shared 0.8.0",
"proc-macro2",
"quote",
@ -3089,7 +3076,7 @@ dependencies = [
"log",
"parking_lot 0.11.1",
"percent-encoding 2.1.0",
"phf 0.10.1",
"phf",
"pin-project-lite",
"postgres-protocol",
"postgres-types",
@ -3527,18 +3514,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]]
name = "xml5ever"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59"
dependencies = [
"log",
"mac",
"markup5ever",
"time 0.1.44",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"

View file

@ -18,7 +18,7 @@ actix-web-httpauth = "0.6.0"
# Used for managing async tasks
actix-rt = "2.7.0"
# Used for HTML sanitization
ammonia = "3.1.2"
ammonia = "3.2.0"
# Used for working with RSA keys, HTTP signatures and file uploads
base64 = "0.13.0"
# Used for working with dates

79
src/atom/feeds.rs Normal file
View file

@ -0,0 +1,79 @@
use ammonia::clean_text;
use chrono::{DateTime, NaiveDateTime, Utc};
use crate::activitypub::views::{get_actor_url, get_object_url};
use crate::config::Instance;
use crate::models::posts::types::Post;
use crate::models::profiles::types::DbActorProfile;
use crate::utils::html::clean_html_all;
const ENTRY_TITLE_MAX_LENGTH: usize = 75;
fn get_min_datetime() -> DateTime<Utc> {
DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc)
}
fn make_entry(
instance_url: &str,
post: &Post,
) -> String {
let object_id = get_object_url(instance_url, &post.id);
let content_escaped = clean_text(&post.content);
let content_cleaned = clean_html_all(&post.content);
// Use trimmed content for title
let mut title: String = content_cleaned.chars()
.take(ENTRY_TITLE_MAX_LENGTH)
.collect();
if title.len() == ENTRY_TITLE_MAX_LENGTH &&
content_cleaned.len() != ENTRY_TITLE_MAX_LENGTH {
title += "...";
};
format!(
"<entry>\
<id>{url}</id>\
<title>{title}</title>\
<updated>{updated_at}</updated>\
<author><name>{author}</name></author>\
<content type=\"html\">{content}</content>\
</entry>",
url=object_id,
title=title,
updated_at=post.created_at.to_rfc3339(),
author=post.author.username,
content=content_escaped,
)
}
pub fn make_feed(
instance: &Instance,
profile: &DbActorProfile,
posts: Vec<Post>,
) -> String {
let actor_url = get_actor_url(&instance.url(), &profile.username);
let actor_name = profile.display_name.as_ref()
.unwrap_or(&profile.username);
let actor_address = profile.actor_address(&instance.host());
let feed_title = format!("{} (@{})", actor_name, actor_address);
let mut entries = vec![];
let mut feed_updated_at = get_min_datetime();
for post in posts {
let entry = make_entry(&instance.url(), &post);
entries.push(entry);
if post.created_at > feed_updated_at {
feed_updated_at = post.created_at;
};
};
format!(
r#"<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>{url}</id>
<title>{title}</title>
<updated>{updated_at}</updated>
{entries}
</feed>"#,
url=actor_url,
title=feed_title,
updated_at=feed_updated_at.to_rfc3339(),
entries=entries.join(""),
)
}

2
src/atom/mod.rs Normal file
View file

@ -0,0 +1,2 @@
mod feeds;
pub mod views;

39
src/atom/views.rs Normal file
View file

@ -0,0 +1,39 @@
use actix_web::{get, web, HttpResponse};
use crate::config::Config;
use crate::database::{Pool, get_database_client};
use crate::errors::HttpError;
use crate::models::posts::queries::get_posts_by_author;
use crate::models::users::queries::get_user_by_name;
use super::feeds::make_feed;
const FEED_SIZE: i64 = 10;
#[get("/feeds/{username}")]
pub async fn get_atom_feed(
config: web::Data<Config>,
db_pool: web::Data<Pool>,
username: web::Path<String>,
) -> Result<HttpResponse, HttpError> {
let db_client = &**get_database_client(&db_pool).await?;
let user = get_user_by_name(db_client, &username).await?;
// Posts are ordered by creation date
let posts = get_posts_by_author(
db_client,
&user.id,
None, // include only public posts
false, // exclude replies
false, // exclude reposts
None,
FEED_SIZE,
).await?;
let feed = make_feed(
&config.instance(),
&user.profile,
posts,
);
let response = HttpResponse::Ok()
.content_type("application/atom+xml")
.body(feed);
Ok(response)
}

View file

@ -1,4 +1,5 @@
pub mod activitypub;
pub mod atom;
pub mod config;
pub mod database;
mod errors;

View file

@ -6,6 +6,7 @@ use actix_web::{
};
use mitra::activitypub::views as activitypub;
use mitra::atom::views as atom;
use mitra::config::{Environment, parse_config};
use mitra::database::{get_database_client, create_pool};
use mitra::database::migrate::apply_migrations;
@ -99,6 +100,7 @@ async fn main() -> std::io::Result<()> {
.service(activitypub::actor_scope())
.service(activitypub::instance_actor_scope())
.service(activitypub::object_view)
.service(atom::get_atom_feed)
.service(nodeinfo::get_nodeinfo)
.service(nodeinfo::get_nodeinfo_2_0);
if let Some(blockchain_config) = &config.blockchain {

View file

@ -50,7 +50,10 @@ impl NodeInfo20 {
name: "mitra".to_string(),
version: config.version.clone(),
};
let services = Services { inbound: vec![], outbound: vec![] };
let services = Services {
inbound: vec![],
outbound: vec!["atom1.0".to_string()],
};
let metadata = Metadata {
node_name: config.instance_title.clone(),
node_description: config.instance_short_description.clone(),

View file

@ -26,6 +26,13 @@ pub fn clean_html_strict(unsafe_html: &str) -> String {
safe_html
}
pub fn clean_html_all(html: &str) -> String {
let text = Builder::empty()
.clean(html)
.to_string();
text
}
#[cfg(test)]
mod tests {
use super::*;
@ -43,4 +50,11 @@ mod tests {
let safe_html = clean_html_strict(unsafe_html);
assert_eq!(safe_html, r#"test bold with <a href="https://example.com" rel="noopener noreferrer">link</a> and <code>code</code>"#);
}
#[test]
fn test_clean_html_all() {
let html = r#"<p>test <b>bold</b><script>dangerous</script> with <a href="https://example.com">link</a> and <code>code</code></p>"#;
let text = clean_html_all(html);
assert_eq!(text, "test bold with link and code");
}
}