From 4ff8db7ee6aab73ad7d023d8de730b65f7ac9ca1 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Thu, 28 Mar 2024 20:23:31 +0100 Subject: [PATCH] Start adding a webmention.io client for retrieving comments. --- Cargo.lock | 409 +++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/commands/mod.rs | 1 + src/commands/sync_webmentions.rs | 18 ++ src/main.rs | 1 + src/webmentions.rs | 274 +++++++++++++++++++++ 6 files changed, 689 insertions(+), 15 deletions(-) create mode 100644 src/commands/sync_webmentions.rs create mode 100644 src/webmentions.rs diff --git a/Cargo.lock b/Cargo.lock index 0bc058d..270234e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "bumpalo" version = "3.15.4" @@ -356,12 +362,43 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -377,7 +414,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fuchsia-zircon-sys", ] @@ -438,6 +475,7 @@ dependencies = [ "germ", "once_cell", "regex", + "reqwest 0.12.2", "tokio 1.36.0", "url", "writefreely_client", @@ -517,7 +555,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap", + "slab", + "tokio 1.36.0", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +dependencies = [ + "bytes 1.5.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.1.0", "indexmap", "slab", "tokio 1.36.0", @@ -554,6 +611,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes 1.5.0", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -561,7 +629,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes 1.5.0", - "http", + "http 0.2.12", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes 1.5.0", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes 1.5.0", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite 0.2.13", ] @@ -587,9 +678,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.25", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -601,6 +692,26 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes 1.5.0", + "futures-channel", + "futures-util", + "h2 0.4.3", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "itoa", + "pin-project-lite 0.2.13", + "smallvec", + "tokio 1.36.0", + "want", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -608,13 +719,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", - "hyper", + "http 0.2.12", + "hyper 0.14.28", "rustls 0.21.10", "tokio 1.36.0", "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes 1.5.0", + "http-body-util", + "hyper 1.2.0", + "hyper-util", + "native-tls", + "tokio 1.36.0", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes 1.5.0", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.2.0", + "pin-project-lite 0.2.13", + "socket2", + "tokio 1.36.0", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -725,6 +872,12 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + [[package]] name = "lock_api" version = "0.4.11" @@ -804,6 +957,24 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "net2" version = "0.2.39" @@ -855,6 +1026,50 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -884,6 +1099,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + [[package]] name = "pin-project-lite" version = "0.1.12" @@ -902,6 +1137,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "proc-macro2" version = "1.0.79" @@ -936,7 +1177,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -979,10 +1220,10 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.25", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-rustls", "ipnet", "js-sys", @@ -1009,6 +1250,48 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338" +dependencies = [ + "base64 0.21.7", + "bytes 1.5.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.3", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite 0.2.13", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio 1.36.0", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -1045,6 +1328,19 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.18.1" @@ -1095,6 +1391,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1121,6 +1426,29 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.197" @@ -1267,7 +1595,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "system-configuration-sys", ] @@ -1282,6 +1610,18 @@ dependencies = [ "libc", ] +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "thiserror" version = "1.0.58" @@ -1376,6 +1716,16 @@ dependencies = [ "syn 2.0.53", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio 1.36.0", +] + [[package]] name = "tokio-rustls" version = "0.14.1" @@ -1412,6 +1762,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite 0.2.13", + "tokio 1.36.0", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -1424,6 +1796,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite 0.2.13", "tracing-core", ] @@ -1493,6 +1866,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "want" version = "0.3.1" @@ -1801,7 +2180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14cda99045977785c78822ba8cb1243e877105801328dc2171465dd878c1bf98" dependencies = [ "chrono", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "serde_repr", diff --git a/Cargo.toml b/Cargo.toml index bdb0a1a..1a07aee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ gemini-feed = "0.1.0" germ = {version = "0.4", features = ["blocking"] } once_cell = "1.19.0" regex = "1.10.3" +reqwest = { version = "0.12", features = ["json"] } tokio = {version = "1.36", features = [ "full" ] } url = "2.5.0" writefreely_client = "0.2.0" diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e7e55a9..457f243 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod sync; pub(crate) mod login; pub(crate) mod logout; +pub(crate) mod sync_webmentions; diff --git a/src/commands/sync_webmentions.rs b/src/commands/sync_webmentions.rs new file mode 100644 index 0000000..3e693e2 --- /dev/null +++ b/src/commands/sync_webmentions.rs @@ -0,0 +1,18 @@ +use anyhow::Result; + +pub(crate) struct SyncWebmentionsCommand<'a> { + webmention_io_url: &'a str, + webmention_io_token: &'a str, +} + +// How will this work? This tool is stateless. The easiest solution is +// to require last ID passed in, but that doesn't really make sense. +// We can have it operate on a directory of comment files, and store +// the state in the files themselves. Replicate the logic in the nu +// shell stuff. + +impl SyncWebmentionsCommand<'_> { + pub async fn execute(self) -> Result<()> { + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 19b0641..6bcf6df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use commands::{login::LoginCommand, logout::LogoutCommand}; use anyhow::Result; +mod webmentions; mod gemfeed; mod sanitization; mod wf; diff --git a/src/webmentions.rs b/src/webmentions.rs new file mode 100644 index 0000000..153882d --- /dev/null +++ b/src/webmentions.rs @@ -0,0 +1,274 @@ +use anyhow::{anyhow, Result}; +use chrono::{DateTime, FixedOffset}; +use url::Url; + +const WEBMENTIONS_IO_ENDPOINT: &'static str = "/api/mentions.jf2"; + +trait ToQueryPair { + fn to_query_pair(&self) -> T; +} + +pub(crate) enum WebmentionsSince { + SinceId(usize), + SinceDate(DateTime), +} + +impl ToQueryPair<(String, String)> for WebmentionsSince { + fn to_query_pair(&self) -> (String, String) { + match self { + Self::SinceId(id) => ("since_id".to_string(), id.to_string()), + Self::SinceDate(date) => ("since".to_string(), format!("{}", date.format("%FT%T%z"))), + } + } +} + +pub(crate) enum WebmentionType { + InReplyTo, + LikeOf, + RepostOf, + BookmarkOf, + MentionOf, + Rsvp, +} + +impl ToString for WebmentionType { + fn to_string(&self) -> String { + match self { + Self::InReplyTo => "in-reply-to".to_string(), + Self::LikeOf => "like-of".to_string(), + Self::RepostOf => "repost-of".to_string(), + Self::BookmarkOf => "bookmark-of".to_string(), + Self::MentionOf => "mention-of".to_string(), + Self::Rsvp => "rsvp".to_string(), + } + } +} + +impl ToQueryPair> for Vec { + fn to_query_pair(&self) -> Vec<(String, String)> { + self.iter() + .map(|mention_type| ("wm-property[]".to_string(), mention_type.to_string())) + .collect() + } +} + +impl<'a> ToQueryPair> for &'a [WebmentionType] { + fn to_query_pair(&self) -> Vec<(String, String)> { + self.iter() + .map(|mention_type| ("wm-property[]".to_string(), mention_type.to_string())) + .collect() + } +} + +impl ToQueryPair<(String, String)> for WebmentionType { + fn to_query_pair(&self) -> (String, String) { + ("wm-property".to_string(), self.to_string()) + } +} + +enum NumWebmentionTypes<'a> { + Single(&'a WebmentionType), + Multiple(&'a [WebmentionType]), + Zero, +} + +pub(crate) struct GetWebmentionsRequest { + /// If specified, retrieve webmentions since an ID or date/time. + /// If not specified, fetch all possible webmentions from the + /// server. + since: Option, + + /// If specified, fetch only these types of web mentions. An empty + /// vec will result in no webmentions fetched. If not specified, + /// fetch all kinds of webmentions. + types: Option>, +} + +impl GetWebmentionsRequest { + fn types(&self) -> Option { + self.types.as_ref().map(|types| { + if types.len() > 1 { + NumWebmentionTypes::Multiple(types.as_slice()) + } else if types.len() == 1 { + NumWebmentionTypes::Single(types.first().unwrap()) + } else { + NumWebmentionTypes::Zero + } + }) + } +} + +fn create_querystring(req: &GetWebmentionsRequest) -> Result { + let mut query_pairs: Vec = vec![]; + if let Some((key, value)) = req.since.as_ref().map(|s| s.to_query_pair()) { + query_pairs.push(format!("{}={}", &key, &value)); + } + + if let Some(num_types) = req.types() { + let pairs = match num_types { + NumWebmentionTypes::Multiple(types) => types.to_query_pair(), + NumWebmentionTypes::Single(wm_type) => vec![wm_type.to_query_pair()], + _ => { + return Err(anyhow!( + "Webmention types filter specified, but no types given" + )) + } + }; + + for (key, value) in pairs { + query_pairs.push(format!("{}={}", &key, &value)); + } + } + + if query_pairs.len() > 1 { + Ok(query_pairs.join("&")) + } else if query_pairs.len() == 1 { + Ok(query_pairs.swap_remove(0)) + } else { + Ok("".to_string()) + } +} + +fn create_request_url(base_url: &Url, req: &GetWebmentionsRequest) -> Result { + let mut url = base_url.join(WEBMENTIONS_IO_ENDPOINT)?; + let mut querystring = create_querystring(req)?; + url.set_query(Some(&querystring)); + + Ok(url) +} + +pub(crate) struct WebmentionIoClient { + url: Url, + domain: String, +} + +impl WebmentionIoClient { + pub async fn get_mentions(params: GetWebmentionsRequest) { + // + } +} + +#[cfg(test)] +mod create_querystring_tests { + use super::*; + + #[test] + fn create_querystring_with_since_date() -> Result<()> { + let date = DateTime::parse_from_str("2022-03-08T13:05:27-0100", "%FT%T%z")?; + + let req = GetWebmentionsRequest { + since: Some(WebmentionsSince::SinceDate(date)), + types: None, + }; + + let expected = "since=2022-03-08T13:05:27-0100"; + let querystring = create_querystring(&req)?; + assert_eq!(querystring.as_str(), expected); + + Ok(()) + } + + #[test] + fn create_querystring_with_since_id() -> Result<()> { + let req = GetWebmentionsRequest { + since: Some(WebmentionsSince::SinceId(12345)), + types: None, + }; + + let expected = "since_id=12345"; + let querystring = create_querystring(&req)?; + assert_eq!(querystring, expected); + + Ok(()) + } + + #[test] + fn create_querystring_with_one_type() -> Result<()> { + let req = GetWebmentionsRequest { + since: None, + types: Some(vec![WebmentionType::InReplyTo]), + }; + + let expected = "wm-property=in-reply-to"; + let querystring = create_querystring(&req)?; + assert_eq!(querystring, expected); + + Ok(()) + } + + #[test] + fn create_querystring_with_since_and_one_type() -> Result<()> { + let req = GetWebmentionsRequest { + since: Some(WebmentionsSince::SinceId(12345)), + types: Some(vec![WebmentionType::InReplyTo]), + }; + + let expected = "since_id=12345&wm-property=in-reply-to"; + let querystring = create_querystring(&req)?; + assert_eq!(querystring, expected); + + Ok(()) + } + + #[test] + fn create_querystring_with_mutiple_types() -> Result<()> { + let req = GetWebmentionsRequest { + since: None, + types: Some(vec![WebmentionType::InReplyTo, WebmentionType::BookmarkOf]), + }; + + let expected = "wm-property[]=in-reply-to&wm-property[]=bookmark-of"; + let querystring = create_querystring(&req)?; + assert_eq!(querystring, expected); + + Ok(()) + } + + #[test] + fn create_querystring_with_since_and_mutiple_types() -> Result<()> { + let req = GetWebmentionsRequest { + since: Some(WebmentionsSince::SinceId(12345)), + types: Some(vec![WebmentionType::InReplyTo, WebmentionType::BookmarkOf]), + }; + + let expected = "since_id=12345&wm-property[]=in-reply-to&wm-property[]=bookmark-of"; + let querystring = create_querystring(&req)?; + assert_eq!(querystring, expected); + + Ok(()) + } + + #[test] + fn create_querystring_with_no_types_in_filter() -> Result<()> { + let req = GetWebmentionsRequest { + since: None, + types: Some(vec![]), + }; + + let querystring = create_querystring(&req); + assert!(matches!(querystring, Err(_))); + + Ok(()) + } +} + +#[cfg(test)] +mod create_request_url_tests { + use super::*; + #[test] + fn test_create_url_with_since_date() -> Result<()> { + let base_url = Url::parse("https://webmention.io")?; + let date = DateTime::parse_from_str("2022-03-08T13:05:27-0100", "%FT%T%z")?; + + let req = GetWebmentionsRequest { + since: Some(WebmentionsSince::SinceDate(date)), + types: None, + }; + + let expected = "https://webmention.io/api/mentions.jf2?since=2022-03-08T13:05:27-0100"; + let api_url = create_request_url(&base_url, &req)?; + assert_eq!(api_url.as_str(), expected); + + Ok(()) + } +}