1use super::{Context, empty, response};
4use http::{HeaderValue, StatusCode, header};
5use serde_json::{Value, json};
6
7pub(super) const LINKS_PATH: &str = "/.well-known/nodeinfo";
8pub(super) const PATH_2_0: &str = "/nodeinfo/2.0";
9pub(super) const PATH_2_1: &str = "/nodeinfo/2.1";
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub(super) enum Version {
13 V2_0,
14 V2_1,
15}
16
17impl Version {
18 fn number(self) -> &'static str {
19 match self {
20 Self::V2_0 => "2.0",
21 Self::V2_1 => "2.1",
22 }
23 }
24
25 fn schema(self) -> String {
26 format!(
27 "http://nodeinfo.diaspora.software/ns/schema/{}",
28 self.number()
29 )
30 }
31
32 fn path(self) -> &'static str {
33 match self {
34 Self::V2_0 => PATH_2_0,
35 Self::V2_1 => PATH_2_1,
36 }
37 }
38}
39
40#[derive(Clone, Debug, Default, Eq, PartialEq)]
42pub struct Software {
43 pub name: String,
45 pub version: String,
46 pub repository: Option<String>,
48 pub homepage: Option<String>,
50}
51
52#[derive(Clone, Debug, Default, Eq, PartialEq)]
54pub struct Usage {
55 pub users_total: Option<u64>,
56 pub users_active_month: Option<u64>,
57 pub users_active_halfyear: Option<u64>,
58 pub local_posts: Option<u64>,
59 pub local_comments: Option<u64>,
60}
61
62#[derive(Clone, Debug, PartialEq)]
65pub struct NodeInfo {
66 pub software: Software,
67 pub protocols: Vec<String>,
69 pub inbound_services: Vec<String>,
70 pub outbound_services: Vec<String>,
71 pub open_registrations: bool,
72 pub usage: Usage,
73 pub metadata: Value,
75}
76
77impl NodeInfo {
78 #[must_use]
81 pub fn new(software: Software) -> Self {
82 Self {
83 software,
84 protocols: vec!["activitypub".to_owned()],
85 inbound_services: Vec::new(),
86 outbound_services: Vec::new(),
87 open_registrations: false,
88 usage: Usage::default(),
89 metadata: json!({}),
90 }
91 }
92
93 #[must_use]
98 pub fn from_document(document: &Value) -> Option<Self> {
99 let text = |value: Option<&Value>| value.and_then(Value::as_str).map(str::to_owned);
100 let strings = |value: Option<&Value>| -> Vec<String> {
101 match value {
102 Some(Value::Array(items)) => items
103 .iter()
104 .filter_map(Value::as_str)
105 .map(str::to_owned)
106 .collect(),
107 _ => Vec::new(),
108 }
109 };
110 let count = |value: Option<&Value>| value.and_then(Value::as_u64);
111 let software = document.get("software")?;
112 let name = text(software.get("name"))?.trim().to_ascii_lowercase();
113 if name.is_empty() {
114 return None;
115 }
116 let usage = document.get("usage");
117 let users = usage.and_then(|usage| usage.get("users"));
118 let services = document.get("services");
119 Some(Self {
120 software: Software {
121 name,
122 version: text(software.get("version")).unwrap_or_default(),
123 repository: text(software.get("repository")),
124 homepage: text(software.get("homepage")),
125 },
126 protocols: strings(document.get("protocols")),
127 inbound_services: strings(services.and_then(|services| services.get("inbound"))),
128 outbound_services: strings(services.and_then(|services| services.get("outbound"))),
129 open_registrations: document
130 .get("openRegistrations")
131 .and_then(Value::as_bool)
132 .unwrap_or(false),
133 usage: Usage {
134 users_total: count(users.and_then(|users| users.get("total"))),
135 users_active_month: count(users.and_then(|users| users.get("activeMonth"))),
136 users_active_halfyear: count(users.and_then(|users| users.get("activeHalfyear"))),
137 local_posts: count(usage.and_then(|usage| usage.get("localPosts"))),
138 local_comments: count(usage.and_then(|usage| usage.get("localComments"))),
139 },
140 metadata: document
141 .get("metadata")
142 .cloned()
143 .unwrap_or_else(|| json!({})),
144 })
145 }
146
147 fn document(&self, version: Version) -> Value {
148 let mut software = json!({
149 "name": self.software.name,
150 "version": self.software.version,
151 });
152 if version == Version::V2_1 {
153 if let Some(repository) = &self.software.repository {
154 software["repository"] = repository.as_str().into();
155 }
156 if let Some(homepage) = &self.software.homepage {
157 software["homepage"] = homepage.as_str().into();
158 }
159 }
160 let mut users = serde_json::Map::new();
161 for (name, value) in [
162 ("total", self.usage.users_total),
163 ("activeMonth", self.usage.users_active_month),
164 ("activeHalfyear", self.usage.users_active_halfyear),
165 ] {
166 if let Some(value) = value {
167 users.insert(name.into(), value.into());
168 }
169 }
170 let mut usage = json!({"users": users});
171 if let Some(posts) = self.usage.local_posts {
172 usage["localPosts"] = posts.into();
173 }
174 if let Some(comments) = self.usage.local_comments {
175 usage["localComments"] = comments.into();
176 }
177 json!({
178 "version": version.number(),
179 "software": software,
180 "protocols": self.protocols,
181 "services": {"inbound": self.inbound_services, "outbound": self.outbound_services},
182 "openRegistrations": self.open_registrations,
183 "usage": usage,
184 "metadata": self.metadata,
185 })
186 }
187}
188
189fn cors(mut response: http::Response<Vec<u8>>) -> http::Response<Vec<u8>> {
190 response.headers_mut().insert(
191 header::ACCESS_CONTROL_ALLOW_ORIGIN,
192 HeaderValue::from_static("*"),
193 );
194 response
195}
196
197pub(super) fn links<D: Clone + Send + Sync + 'static>(
198 context: &Context<D>,
199) -> http::Response<Vec<u8>> {
200 let links: Vec<Value> = [Version::V2_0, Version::V2_1]
201 .into_iter()
202 .map(|version| {
203 let mut href = context.origin().clone();
204 href.set_path(version.path());
205 href.set_query(None);
206 json!({"rel": version.schema(), "href": href.as_str()})
207 })
208 .collect();
209 cors(response(
210 StatusCode::OK,
211 "application/json",
212 serde_json::to_vec(&json!({"links": links})).unwrap_or_default(),
213 ))
214}
215
216pub(super) async fn document<D: Clone + Send + Sync + 'static>(
217 context: &Context<D>,
218 version: Version,
219) -> http::Response<Vec<u8>> {
220 let Some(nodeinfo) = &context.inner.federation.nodeinfo else {
221 return empty(StatusCode::NOT_FOUND);
222 };
223 match nodeinfo(context.clone()).await {
224 Ok(nodeinfo) => cors(response(
225 StatusCode::OK,
226 &format!("application/json; profile=\"{}#\"", version.schema()),
227 serde_json::to_vec(&nodeinfo.document(version)).unwrap_or_default(),
228 )),
229 Err(error) => {
230 context.report(&error);
231 empty(StatusCode::INTERNAL_SERVER_ERROR)
232 }
233 }
234}