From 341a0a77aaa0bc2d26835cec2f4ecc1992b3f5ff Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 18 Oct 2023 00:37:52 +0200 Subject: [PATCH] chore: yanked nodeinfo because i need some changes will eventually PR them in, here's the source https://codeberg.org/thefederationinfo/nodeinfo-rs --- Cargo.toml | 1 + src/nodeinfo/fetcher.rs | 50 ++++++++++++++ src/nodeinfo/mod.rs | 2 + src/nodeinfo/model.rs | 147 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 src/nodeinfo/fetcher.rs create mode 100644 src/nodeinfo/mod.rs create mode 100644 src/nodeinfo/model.rs diff --git a/Cargo.toml b/Cargo.toml index bd89731..f35186c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ chrono = "0.4.31" clap = { version = "4.4.6", features = ["derive"] } derive_more = "0.99.17" lazy_static = "1.4.0" +# nodeinfo = { git = "https://codeberg.org/thefederationinfo/nodeinfo-rs.git" } reqwest = { version = "0.11.20", features = ["json"] } sea-orm = { version = "0.12.3", features = ["runtime-tokio-native-tls", "sqlx-sqlite", "sqlx-postgres"] } serde = { version = "1.0.188", features = ["derive"] } diff --git a/src/nodeinfo/fetcher.rs b/src/nodeinfo/fetcher.rs new file mode 100644 index 0000000..0adfe57 --- /dev/null +++ b/src/nodeinfo/fetcher.rs @@ -0,0 +1,50 @@ +use std::time::Duration; + +use serde::Deserialize; + +use crate::nodeinfo::model::NodeInfoOwned; + +#[derive(Debug, Clone, Deserialize)] +struct WellKnownNodeInfo { + links: Vec +} + +#[derive(Debug, Clone, Deserialize)] +struct WellKnownNodeInfoRef { + rel: String, + href: String, +} + +pub async fn node_info(domain: &str) -> Result { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build()?; + + let well_known : WellKnownNodeInfo = client + .get(format!("https://{}/.well-known/nodeinfo", domain)) + .send() + .await? + .json() + .await?; + + let (_, best_version) = well_known.links.iter() + .filter_map(|x| Some((parse_nodeinfo_spec_version(&x.rel)?, x))) + .max_by(|a, b| a.0.cmp(&b.0)) + .expect("no versions available"); + + client.get(&best_version.href) + .send() + .await? + .json() + .await +} + +fn parse_nodeinfo_spec_version(schema: &str) -> Option { + match schema { // TODO this silently filters out unsupported new versions + "http://nodeinfo.diaspora.software/ns/schema/1.0" => Some(1000), + "http://nodeinfo.diaspora.software/ns/schema/1.1" => Some(1010), + "http://nodeinfo.diaspora.software/ns/schema/2.0" => Some(2000), + "http://nodeinfo.diaspora.software/ns/schema/2.1" => Some(2010), + _ => None, + } +} diff --git a/src/nodeinfo/mod.rs b/src/nodeinfo/mod.rs new file mode 100644 index 0000000..e9fa803 --- /dev/null +++ b/src/nodeinfo/mod.rs @@ -0,0 +1,2 @@ +pub mod model; +pub mod fetcher; diff --git a/src/nodeinfo/model.rs b/src/nodeinfo/model.rs new file mode 100644 index 0000000..6911ffc --- /dev/null +++ b/src/nodeinfo/model.rs @@ -0,0 +1,147 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +/// Node metadata for version detection only used for deserialization. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, derive_more::Display)] +pub(crate) enum NodeVersion { + V1_0 = 1000, + V1_1 = 1001, + V2_0 = 2000, + V2_1 = 2001, +} + +pub type NodeInfo<'a> = NodeInfoInternal<&'a str>; +pub type NodeInfoOwned = NodeInfoInternal; + +/// Node metadata about a server running in the fediverse. +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct NodeInfoInternal { + pub version: NodeVersion, + pub software: Software, + #[serde(default)] + pub protocols: Vec, + #[serde(default)] + pub services: Services, + #[serde(rename = "openRegistrations", default)] + pub open_registrations: bool, + #[serde(default)] + pub usage: Usage, + #[serde(default)] + pub metadata: Map, +} + +/// Node legacy metadata about a server running in the federation, only used for deserialization. +#[derive(Debug, PartialEq, Deserialize)] +pub(crate) struct LegacyNodeInfo<'a> { + version: NodeVersion, + software: Software<&'a str>, + #[serde(default)] + services: Services<&'a str>, + #[serde(default)] + protocols: Services<&'a str>, + #[serde(rename = "openRegistrations", default)] + open_registrations: bool, + #[serde(default)] + usage: Usage, + #[serde(default)] + metadata: Map, +} + +/// Software contains infos about software running on the node. +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Software { + pub name: T, + pub version: T, + pub repository: Option, + pub homepage: Option, +} + +/// Services tell about third party sites the node can connect to or interact with. +#[derive(Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct Services { + pub inbound: Vec, + pub outbound: Vec, +} + +/// Usage statistics for the node +#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Usage { + pub users: Users, + pub local_posts: Option, + pub local_comments: Option, +} + +// UsersUsage are statistics about the users of the node. +#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Users { + pub total: Option, + pub active_halfyear: Option, + pub active_month: Option, +} + +impl<'a> Software<&'a str> { + fn to_owned(&self) -> Software { + Software { + name: self.name.to_string(), + version: self.version.to_string(), + repository: self.repository.map(ToString::to_string), + homepage: self.homepage.map(ToString::to_string), + } + } +} + +impl<'a> Services<&'a str> { + fn to_owned(&self) -> Services { + Services { + inbound: self.inbound.iter().map(ToString::to_string).collect(), + outbound: self.outbound.iter().map(ToString::to_string).collect(), + } + } +} + +impl NodeInfo<'_> { + /// Converts all internal `&str`s into `String`s. + pub fn to_owned(&self) -> NodeInfoOwned { + NodeInfoOwned { + version: self.version.to_string(), + software: self.software.to_owned(), + protocols: self.protocols.iter().map(ToString::to_string).collect(), + services: self.services.to_owned(), + open_registrations: self.open_registrations, + usage: self.usage.clone(), + metadata: self.metadata.clone(), + } + } +} + +impl<'a> From> for NodeInfo<'a> { + fn from(other: LegacyNodeInfo<'a>) -> Self { + let mut combined_protocols: Vec<&'a str> = other + .protocols + .inbound + .into_iter() + .chain(other.protocols.outbound) + .collect(); + combined_protocols.sort(); + combined_protocols.dedup(); + + NodeInfo { + version: other.version, + software: other.software, + services: other.services, + protocols: combined_protocols, + open_registrations: other.open_registrations, + usage: other.usage, + metadata: other.metadata, + } + } +} + +impl<'a> From> for NodeInfoOwned { + fn from(value: LegacyNodeInfo<'a>) -> Self { + NodeInfo::from(value).to_owned() + } +} +