Implement basic Corobel
This commit is contained in:
parent
e07c0e9e11
commit
25c6dffc85
|
@ -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"
|
||||
|
|
|
@ -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
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.
|
||||
Corobel provides feeds at `/<project>/feed.rss` and Webfinger objects at `.well-known/webfinger?<project>`.
|
||||
|
||||
Place it at `.well-known/webfinger` and set your domain name with `--domain`.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
[debug]
|
||||
address = "127.0.0.1"
|
||||
port = 8000
|
||||
|
||||
[release]
|
||||
address = "127.0.0.1"
|
||||
port = 6587
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
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()
|
||||
});
|
||||
static ARGS: once_cell::sync::Lazy<Args> = once_cell::sync::Lazy::new(|| Args::parse());
|
||||
|
||||
#[get("/?<params..>")]
|
||||
#[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);
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue