Create Atom feeds for local users
This commit is contained in:
parent
aa997e3a82
commit
c0837bbf77
9 changed files with 168 additions and 53 deletions
77
Cargo.lock
generated
77
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
79
src/atom/feeds.rs
Normal 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
2
src/atom/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod feeds;
|
||||
pub mod views;
|
39
src/atom/views.rs
Normal file
39
src/atom/views.rs
Normal 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)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub mod activitypub;
|
||||
pub mod atom;
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
mod errors;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue