Browse Source

Implement basic Corobel

main
Leonora Tindall 1 month ago
parent
commit
25c6dffc85
Signed by: nora GPG Key ID: 7A8B52EC67E09AAF
  1. 341
      Cargo.lock
  2. 5
      Cargo.toml
  3. 33
      README.md
  4. 7
      Rocket.toml
  5. 11
      config/corobel.service
  6. 1898
      samples/cohost/api/v1/project_posts.json
  7. 0
      samples/corobel/.well-known/webfinger
  8. 10
      src/cohost_account.rs
  9. 73
      src/cohost_posts.rs
  10. 147
      src/main.rs
  11. 100
      src/syndication.rs
  12. 9
      src/webfinger.rs
  13. 37
      static/index.html

341
Cargo.lock

@ -37,6 +37,15 @@ dependencies = [
"subtle",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "async-stream"
version = "0.3.3"
@ -69,6 +78,19 @@ dependencies = [
"syn",
]
[[package]]
name = "atom_syndication"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21fb6a0b39c6517edafe46f8137e53c51742425a4dae1c73ee12264a37ad7541"
dependencies = [
"chrono",
"derive_builder",
"diligent-date-parser",
"never",
"quick-xml",
]
[[package]]
name = "atomic"
version = "0.5.1"
@ -146,6 +168,22 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"serde",
"time 0.1.44",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "cipher"
version = "0.4.3"
@ -193,6 +231,16 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "cookie"
version = "0.16.1"
@ -207,7 +255,7 @@ dependencies = [
"rand",
"sha2",
"subtle",
"time",
"time 0.3.16",
"version_check",
]
@ -223,7 +271,7 @@ dependencies = [
"publicsuffix",
"serde",
"serde_json",
"time",
"time 0.3.16",
"url",
]
@ -243,6 +291,23 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "corobel"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"eggbug",
"once_cell",
"pulldown-cmark",
"reqwest",
"rocket",
"rss",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "cpufeatures"
version = "0.2.5"
@ -272,6 +337,116 @@ dependencies = [
"cipher",
]
[[package]]
name = "cxx"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a"
[[package]]
name = "cxxbridge-macro"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "darling"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "derive_builder"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@ -327,6 +502,15 @@ dependencies = [
"subtle",
]
[[package]]
name = "diligent-date-parser"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d0fd95c7c02e2d6c588c6c5628466fff9bdde4b8c6196465e087b08e792720"
dependencies = [
"chrono",
]
[[package]]
name = "eggbug"
version = "0.1.2"
@ -506,6 +690,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.8"
@ -514,7 +707,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@ -662,6 +855,36 @@ dependencies = [
"tokio-native-tls",
]
[[package]]
name = "iana-time-zone"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
"cxx",
"cxx-build",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.2.3"
@ -751,6 +974,15 @@ version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "link-cplusplus"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
dependencies = [
"cc",
]
[[package]]
name = "lock_api"
version = "0.4.9"
@ -830,7 +1062,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.42.0",
]
@ -873,18 +1105,10 @@ dependencies = [
]
[[package]]
name = "northbound-train"
name = "never"
version = "0.1.0"
dependencies = [
"clap",
"eggbug",
"once_cell",
"reqwest",
"rocket",
"serde",
"serde_json",
"tokio",
]
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
[[package]]
name = "nu-ansi-term"
@ -896,6 +1120,25 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.13.1"
@ -1149,6 +1392,28 @@ dependencies = [
"psl-types",
]
[[package]]
name = "pulldown-cmark"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63"
dependencies = [
"bitflags",
"getopts",
"memchr",
"unicase",
]
[[package]]
name = "quick-xml"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]]
name = "quote"
version = "1.0.21"
@ -1322,7 +1587,7 @@ dependencies = [
"serde_json",
"state",
"tempfile",
"time",
"time 0.3.16",
"tokio",
"tokio-stream",
"tokio-util",
@ -1369,11 +1634,24 @@ dependencies = [
"smallvec",
"stable-pattern",
"state",
"time",
"time 0.3.16",
"tokio",
"uncased",
]
[[package]]
name = "rss"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acaf1331b7fc4edc3c2920819fee1766c27e8d40da593155832db3d6dea64e92"
dependencies = [
"atom_syndication",
"chrono",
"derive_builder",
"never",
"quick-xml",
]
[[package]]
name = "rustversion"
version = "1.0.9"
@ -1408,6 +1686,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scratch"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "security-framework"
version = "2.7.0"
@ -1627,6 +1911,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "time"
version = "0.3.16"
@ -1875,6 +2170,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-xid"
version = "0.2.4"
@ -1939,6 +2240,12 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

5
Cargo.toml

@ -1,5 +1,5 @@
[package]
name = "northbound-train"
name = "corobel"
version = "0.1.0"
edition = "2021"
@ -14,3 +14,6 @@ serde = { version = "1.0.147", features = [ "derive" ] }
tokio = { version = "1.21.2", features = [ "full" ] }
serde_json = "1.0.87"
once_cell = "1.16.0"
chrono = { version = "0.4.22", features = [ "serde" ] }
rss = { version = "2.0.1", features = [ "builders", "atom", "chrono" ] }
pulldown-cmark = "0.9.2"

33
README.md

@ -1,26 +1,23 @@
# Northbound Train
# Corobel
Translate Cohost users and posts into ActivityPub actors and activities.
> Till we feel the far track humming,
> and we see her headlight plain,
> and we gather and wait her coming --
> the wonderful north-bound train.
>
> - *Bridge-Guard in the Karroo*, Rudyard Kipling
Translate Cohost projects and posts into RSS 2.0 feeds so you can follow them in your RSS reader.
## What it currently does
Right now northbound-train just proxies Webfinger requests to Cohost project lookups using the v1 API.
Place it at `.well-known/webfinger` and set your domain name with `--domain`.
Corobel provides feeds at `/<project>/feed.rss` and Webfinger objects at `.well-known/webfinger?<project>`.
If you must, you can configure the base URL with `--base-url`.
Set your domain name with `--domain`. If you must, you can configure the base URL with `--base-url`.
A template SystemD unit file is provided at `config/corobel.service`. See `Rocket.toml` for the appropriate
ports to use for development and deployment.
## Todo
- [x] Webfinger for Cohost users
- [ ] Handle redirects if they exist (?)
- [ ] Expose ActivityPub actors for Cohost users
- [ ] Permit following ActivityPub actors for Cohost users
- [ ] Deliver posts from Cohost users to ActivityPub followers
- [x] Webfinger for Cohost projects
- [ ] Handle redirects
- [ ] RSS feeds for projects
- [ ] RSS feeds for tags
- [ ] Atom Extension pagination support
- [ ] Read More support
- [ ] Dublin Core support
- [ ] Media Envelope support

7
Rocket.toml

@ -0,0 +1,7 @@
[debug]
address = "127.0.0.1"
port = 8000
[release]
address = "127.0.0.1"
port = 6587

11
config/corobel.service

@ -0,0 +1,11 @@
[Unit]
Description=Corobel RSS Gateway
[Service]
Type=simple
User=nora
WorkingDirectory=/home/nora/corobel
ExecStart=/home/nora/corobel/corobel --domain corobel.nora.codes
[Install]
WantedBy=multi-user.target

1898
samples/cohost/api/v1/project_posts.json

File diff suppressed because it is too large

0
samples/northbound-train/.well-known/webfinger → samples/corobel/.well-known/webfinger

10
src/cohost_account.rs

@ -6,11 +6,11 @@ pub const COHOST_ACCOUNT_API_URL: &str = "https://cohost.org/api/v1/project/";
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct CohostAccount {
#[serde(rename = "projectId")]
project_id: u64,
pub project_id: u64,
#[serde(rename = "displayName")]
display_name: String,
dek: String,
description: String,
pub display_name: String,
pub dek: String,
pub description: String,
}
#[test]
@ -25,4 +25,4 @@ fn deserialize_account() -> Result<(), Box<dyn std::error::Error>> {
let actual_account: CohostAccount = serde_json::from_str(example)?;
assert_eq!(expected_account, actual_account);
Ok(())
}
}

73
src/cohost_posts.rs

@ -0,0 +1,73 @@
use chrono::{DateTime, Utc};
use serde::Deserialize;
pub fn cohost_posts_api_url(project: impl AsRef<str>, page: u64) -> String {
format!(
"https://cohost.org/api/v1/project/{}/posts?page={}",
project.as_ref(),
page
)
}
// Cohost doesn't give us Next links ("rel: next") for further pages, so we'll have to ALWAYS populate the rel=next field
#[derive(Deserialize)]
pub struct CohostPostsPage {
#[serde(rename = "nItems")]
pub number_items: usize,
#[serde(rename = "nPages")]
pub number_pages: u64,
pub items: Vec<CohostPost>,
#[serde(rename = "_links")]
pub links: Vec<CohostPostLink>,
}
#[derive(Deserialize)]
pub struct CohostPost {
#[serde(rename = "postId")]
pub id: u64,
pub headline: String,
#[serde(rename = "publishedAt")]
pub published_at: DateTime<Utc>,
pub cws: Vec<String>,
pub tags: Vec<String>,
#[serde(rename = "plainTextBody")]
pub plain_body: String,
#[serde(rename = "singlePostPageUrl")]
pub url: String,
#[serde(rename = "postingProject")]
pub poster: CohostPostingProject,
#[serde(rename = "shareTree")]
pub share_tree: Vec<CohostPost>,
}
#[derive(Deserialize)]
pub struct CohostPostingProject {
#[serde(rename = "projectId")]
pub id: u64,
pub handle: String,
#[serde(rename = "displayName")]
pub display_name: String,
pub dek: String,
pub description: String,
pub pronouns: String,
}
#[derive(Deserialize)]
pub struct CohostPostLink {
pub href: String,
pub rel: String,
#[serde(rename = "type")]
pub t_type: String,
}
#[test]
fn test_deserialize() -> Result<(), Box<dyn std::error::Error>> {
let post_page_json = include_str!("../samples/cohost/api/v1/project_posts.json");
let post_page_actual: CohostPostsPage = serde_json::from_str(post_page_json)?;
assert_eq!(post_page_actual.number_items, post_page_actual.items.len());
let post = &post_page_actual.items[0];
assert_eq!(post.id, 149268);
assert_eq!(post.poster.id, 32693);
Ok(())
}

147
src/main.rs

@ -1,55 +1,148 @@
use clap::Parser;
use std::collections::HashMap;
use std::error::Error;
use clap::Parser;
#[macro_use] extern crate rocket;
#[macro_use]
extern crate rocket;
use reqwest::StatusCode;
use rocket::{Request, Response};
use rocket::response::content::RawHtml;
use rocket::serde::json::Json;
mod cohost_account;
mod cohost_posts;
mod syndication;
mod webfinger;
use cohost_account::{CohostAccount, COHOST_ACCOUNT_API_URL};
use cohost_posts::{cohost_posts_api_url, CohostPostsPage};
use webfinger::CohostWebfingerResource;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// The base URL for the northbound instance
/// The base URL for the corobel instance
#[clap(short, long, required = true)]
domain: String,
/// The base URL for the northbound instance
/// The base URL for the corobel instance
#[clap(short, long, default_value_t = default_base_url() )]
base_url: String,
}
fn default_base_url() -> String { "/.well-known/webfinger".into() }
fn default_base_url() -> String {
"/".into()
}
static ARGS: once_cell::sync::Lazy<Args> = once_cell::sync::Lazy::new(|| Args::parse());
#[get("/")]
fn index() -> RawHtml<&'static str> {
RawHtml(include_str!("../static/index.html"))
}
#[get("/<project>/feed.rss?<page>")]
async fn syndication_rss_route(project: &str, page: Option<u64>) -> Option<String> {
let page = page.unwrap_or(0);
let project_url = format!("{}{}", COHOST_ACCOUNT_API_URL, project);
let posts_url = cohost_posts_api_url(project, page);
static ARGS: once_cell::sync::Lazy<Args> = once_cell::sync::Lazy::new(|| {
Args::parse()
});
eprintln!("making request to {}", project_url);
let project_data: CohostAccount = match reqwest::get(project_url).await {
Ok(v) => match v.status() {
StatusCode::OK => match v.json::<CohostAccount>().await {
Ok(a) => a,
Err(e) => {
eprintln!("Couldn't deserialize Cohost project '{}': {:?}", project, e);
return None;
}
},
// TODO NORA: Handle possible redirects
s => {
eprintln!(
"Didn't receive status code 200 for Cohost project '{}'; got {:?} instead.",
project, s
);
return None;
}
},
Err(e) => {
eprintln!(
"Error making request to Cohost for project '{}': {:?}",
project, e
);
return None;
}
};
#[get("/?<params..>")]
eprintln!("making request to {}", posts_url);
match reqwest::get(posts_url).await {
Ok(v) => match v.status() {
StatusCode::OK => match v.json::<CohostPostsPage>().await {
Ok(page_data) => {
return Some(
syndication::channel_for_posts_page(project, page, project_data, page_data)
.to_string(),
);
}
Err(e) => {
eprintln!(
"Couldn't deserialize Cohost posts page for '{}': {:?}",
project, e
);
}
},
// TODO NORA: Handle possible redirects
s => {
eprintln!("Didn't receive status code 200 for posts for Cohost project '{}'; got {:?} instead.", page, s);
return None;
}
},
Err(e) => {
eprintln!(
"Error making request to Cohost for posts for project '{}': {:?}",
project, e
);
return None;
}
};
None
}
#[get("/.well-known/webfinger?<params..>")]
async fn webfinger_route(params: HashMap<String, String>) -> Option<Json<CohostWebfingerResource>> {
if params.len() != 1 {
eprintln!("Too may or too few parameters. Expected 1, got {}", params.len());
eprintln!(
"Too may or too few parameters. Expected 1, got {}",
params.len()
);
return None;
}
if let Some(param) = params.iter().next() {
let url = format!("{}{}", COHOST_ACCOUNT_API_URL, param.0);
eprintln!("making request to {}", url);
match reqwest::get(url).await {
Ok(v) => match v.status() {
StatusCode::OK => {
return Some(Json(CohostWebfingerResource::new(param.0.as_str(), &ARGS.domain)));
},
// TODO NORA: Handle possible redirects
s => {
eprintln!("Didn't receive status code 200 for Cohost project '{}'; got {:?} instead.", param.0, s);
return None;
Ok(v) => {
match v.status() {
StatusCode::OK => match v.json::<CohostAccount>().await {
Ok(_v) => {
return Some(Json(CohostWebfingerResource::new(
param.0.as_str(),
&ARGS.domain,
)));
}
Err(e) => {
eprintln!("Couldn't deserialize Cohost project '{}': {:?}", param.0, e);
}
},
// TODO NORA: Handle possible redirects
s => {
eprintln!("Didn't receive status code 200 for Cohost project '{}'; got {:?} instead.", param.0, s);
return None;
}
}
},
Err(e) => {
eprintln!("Error making request to Cohost for project '{}': {:?}", param.0, e);
}
Err(e) => {
eprintln!(
"Error making request to Cohost for project '{}': {:?}",
param.0, e
);
return None;
}
};
@ -62,7 +155,13 @@ async fn main() -> Result<(), Box<dyn Error>> {
// Set up the global config
once_cell::sync::Lazy::force(&ARGS);
let _rocket = rocket::build()
.mount(&ARGS.base_url, routes![webfinger_route])
.ignite().await?.launch().await?;
.mount(
&ARGS.base_url,
routes![index, webfinger_route, syndication_rss_route],
)
.ignite()
.await?
.launch()
.await?;
Ok(())
}

100
src/syndication.rs

@ -0,0 +1,100 @@
use crate::cohost_account::CohostAccount;
use crate::cohost_posts::*;
use crate::ARGS;
use rss::Channel;
pub fn channel_for_posts_page(
project_name: impl AsRef<str>,
page_number: u64,
project: CohostAccount,
page: CohostPostsPage,
) -> Channel {
let mut builder = rss::ChannelBuilder::default();
builder
.title(format!("{} Cohost Posts", project.display_name))
.description(project.description)
.generator(Some(format!(
"{} {}",
env!("CARGO_CRATE_NAME"),
env!("CARGO_PKG_VERSION")
)))
.link(format!(
"https://cohost.org/{}?page={}",
project_name.as_ref(),
page_number
));
let mut items = Vec::with_capacity(page.number_items);
for item in page.items {
let mut item_builder = rss::ItemBuilder::default();
let mut categories: Vec<rss::Category> = Vec::with_capacity(item.tags.len());
for tag in item.tags.iter().cloned() {
categories.push(rss::Category {
name: tag,
domain: Some("https://cohost.org".into()),
});
}
item_builder
.link(item.url.clone())
.title(item.headline)
.author(item.poster.handle)
.guid(Some(rss::Guid {
value: item.url.clone(),
permalink: true,
}))
.categories(categories)
.pub_date(item.published_at.to_rfc2822())
.source(Some(rss::Source {
title: Some(format!("{} Cohost Posts", project.display_name)),
url: format!("https://{}/feed/{}.rss", ARGS.domain, project_name.as_ref()),
}));
let mut body_text = String::new();
if item.share_tree.len() == 1 {
body_text.push_str("(in reply to another post)\n---\n")
} else if item.share_tree.len() > 1 {
body_text.push_str(&format!(
"(in reply to {} other posts)\n---\n",
item.share_tree.len()
));
}
if item.cws.is_empty() {
body_text.push_str(&item.plain_body);
} else {
body_text.push_str("Sensitive post, body text omitted. Content warnings:{}");
for cw in item.cws {
body_text.push_str(&format!(" {},", cw));
}
body_text.pop(); // Remove trailing comma
body_text.push_str("\n---\n")
};
if !item.tags.is_empty() {
body_text.push_str("\n\n Post tagged:");
for tag in item.tags {
body_text.push_str(&format!(" #{},", tag));
}
body_text.pop(); // Remove trailing comma
}
use pulldown_cmark::Options;
let options = Options::ENABLE_FOOTNOTES
& Options::ENABLE_STRIKETHROUGH
& Options::ENABLE_TABLES
& Options::ENABLE_TASKLISTS;
let parser = pulldown_cmark::Parser::new_ext(&body_text, options);
let mut html_output = String::new();
pulldown_cmark::html::push_html(&mut html_output, parser);
item_builder.content(html_output);
items.push(item_builder.build());
}
builder.items(items);
builder.build()
}

9
src/webfinger.rs

@ -30,17 +30,18 @@ impl CohostWebfingerProfileLink {
Self {
rel: "http://webfinger.net/rel/profile-page".into(),
t_type: "text/html".into(),
href: format!("https://cohost.org/{}", project_id.as_ref())
href: format!("https://cohost.org/{}", project_id.as_ref()),
}
}
}
#[test]
fn serialize_webfinger_resource() -> Result<(), Box<dyn std::error::Error>> {
let expected_json = include_str!("../samples/northbound-train/.well-known/webfinger");
let resource: CohostWebfingerResource = CohostWebfingerResource::new("noracodes", "example.com");
let expected_json = include_str!("../samples/corobel/.well-known/webfinger");
let resource: CohostWebfingerResource =
CohostWebfingerResource::new("noracodes", "example.com");
let actual_json = serde_json::to_string_pretty(&resource)?;
println!("{}", actual_json);
assert_eq!(&expected_json, &actual_json);
Ok(())
}
}

37
static/index.html

@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>corobel: RSS for Cohost</title>
<meta name="description" content="A simple stateless tool to translate Cohost to RSS feeds.">
<meta name="author" content="Leonora Tindall">
<meta property="og:title" content="corobel: RSS for Cohost">
<meta property="og:type" content="website">
<meta property="og:description" content="A simple stateless tool to translate Cohost to RSS feeds.">
<style type="text/css">
html {
max-width: 70ch;
padding: 3em 1em;
margin: auto;
line-height: 1.75;
font-size: 1.25em;
}
</style>
</head>
<body>
<h1>corobel</h1>
<h2>RSS feeds from Cohost pages</h2>
<p>
Go to <code>/project_name/feed.rss</code> to get a feed for a project.
For example, <a href="/noracodes/feed.rss"><code>/noracodes/feed.rss</code></a> will give you the feed for my page.
</p>
<p>
Brought to you by Leonora Tindall, written in Rust with Rocket.
</p>
</body>
</html>
Loading…
Cancel
Save