Commit
+558 -161 +/-16 browse
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index 595cb35..0d4e9d6 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -151,6 +151,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
6 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" |
7 | |
8 | [[package]] |
9 | + name = "async-convert" |
10 | + version = "1.0.0" |
11 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
12 | + checksum = "6d416feee97712e43152cd42874de162b8f9b77295b1c85e5d92725cc8310bae" |
13 | + dependencies = [ |
14 | + "async-trait", |
15 | + ] |
16 | + |
17 | + [[package]] |
18 | name = "async-trait" |
19 | version = "0.1.83" |
20 | source = "registry+https://github.com/rust-lang/crates.io-index" |
21 | @@ -280,6 +289,7 @@ dependencies = [ |
22 | "mime", |
23 | "pin-project-lite", |
24 | "serde", |
25 | + "serde_html_form", |
26 | "tower", |
27 | "tower-layer", |
28 | "tower-service", |
29 | @@ -353,7 +363,7 @@ dependencies = [ |
30 | "tree-sitter", |
31 | "tree-sitter-highlight", |
32 | "url", |
33 | - "webfinger", |
34 | + "webfinger-rs", |
35 | ] |
36 | |
37 | [[package]] |
38 | @@ -673,6 +683,7 @@ dependencies = [ |
39 | "iana-time-zone", |
40 | "js-sys", |
41 | "num-traits", |
42 | + "serde", |
43 | "wasm-bindgen", |
44 | "windows-targets 0.52.6", |
45 | ] |
46 | @@ -1114,6 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
47 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" |
48 | dependencies = [ |
49 | "powerfmt", |
50 | + "serde", |
51 | ] |
52 | |
53 | [[package]] |
54 | @@ -1225,6 +1237,15 @@ dependencies = [ |
55 | ] |
56 | |
57 | [[package]] |
58 | + name = "document-features" |
59 | + version = "0.2.10" |
60 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
61 | + checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" |
62 | + dependencies = [ |
63 | + "litrs", |
64 | + ] |
65 | + |
66 | + [[package]] |
67 | name = "dotenvy" |
68 | version = "0.15.7" |
69 | source = "registry+https://github.com/rust-lang/crates.io-index" |
70 | @@ -2204,6 +2225,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" |
71 | dependencies = [ |
72 | "autocfg", |
73 | "hashbrown 0.12.3", |
74 | + "serde", |
75 | ] |
76 | |
77 | [[package]] |
78 | @@ -2214,6 +2236,7 @@ checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" |
79 | dependencies = [ |
80 | "equivalent", |
81 | "hashbrown 0.15.0", |
82 | + "serde", |
83 | ] |
84 | |
85 | [[package]] |
86 | @@ -2277,6 +2300,27 @@ dependencies = [ |
87 | ] |
88 | |
89 | [[package]] |
90 | + name = "kinded" |
91 | + version = "0.3.0" |
92 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
93 | + checksum = "ce4bdbb2f423660b19f0e9f7115182214732d8dd5f840cd0a3aee3e22562f34c" |
94 | + dependencies = [ |
95 | + "kinded_macros", |
96 | + ] |
97 | + |
98 | + [[package]] |
99 | + name = "kinded_macros" |
100 | + version = "0.3.0" |
101 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
102 | + checksum = "a13b4ddc5dcb32f45dac3d6f606da2a52fdb9964a18427e63cd5ef6c0d13288d" |
103 | + dependencies = [ |
104 | + "convert_case", |
105 | + "proc-macro2", |
106 | + "quote", |
107 | + "syn 2.0.79", |
108 | + ] |
109 | + |
110 | + [[package]] |
111 | name = "lazy_static" |
112 | version = "1.5.0" |
113 | source = "registry+https://github.com/rust-lang/crates.io-index" |
114 | @@ -2419,6 +2463,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
115 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" |
116 | |
117 | [[package]] |
118 | + name = "litrs" |
119 | + version = "0.4.1" |
120 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
121 | + checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" |
122 | + |
123 | + [[package]] |
124 | name = "lock_api" |
125 | version = "0.4.12" |
126 | source = "registry+https://github.com/rust-lang/crates.io-index" |
127 | @@ -2651,6 +2701,29 @@ dependencies = [ |
128 | ] |
129 | |
130 | [[package]] |
131 | + name = "nutype" |
132 | + version = "0.5.0" |
133 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
134 | + checksum = "d8789358e2d6cdffb0cb170c7802ee7548beb8067ed643f3122fa36c335f3c64" |
135 | + dependencies = [ |
136 | + "nutype_macros", |
137 | + ] |
138 | + |
139 | + [[package]] |
140 | + name = "nutype_macros" |
141 | + version = "0.5.0" |
142 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
143 | + checksum = "93a3e222ba1f06a03552910fe89a232a1661dcf8ad4c837531fb199828d0916b" |
144 | + dependencies = [ |
145 | + "cfg-if", |
146 | + "kinded", |
147 | + "proc-macro2", |
148 | + "quote", |
149 | + "syn 2.0.79", |
150 | + "urlencoding", |
151 | + ] |
152 | + |
153 | + [[package]] |
154 | name = "object" |
155 | version = "0.36.5" |
156 | source = "registry+https://github.com/rust-lang/crates.io-index" |
157 | @@ -3283,6 +3356,7 @@ dependencies = [ |
158 | "tracing", |
159 | "tracing-subscriber", |
160 | "url", |
161 | + "webfinger-rs", |
162 | ] |
163 | |
164 | [[package]] |
165 | @@ -3760,6 +3834,19 @@ dependencies = [ |
166 | ] |
167 | |
168 | [[package]] |
169 | + name = "serde_html_form" |
170 | + version = "0.2.6" |
171 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
172 | + checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" |
173 | + dependencies = [ |
174 | + "form_urlencoded", |
175 | + "indexmap 2.6.0", |
176 | + "itoa", |
177 | + "ryu", |
178 | + "serde", |
179 | + ] |
180 | + |
181 | + [[package]] |
182 | name = "serde_json" |
183 | version = "1.0.128" |
184 | source = "registry+https://github.com/rust-lang/crates.io-index" |
185 | @@ -3803,6 +3890,36 @@ dependencies = [ |
186 | ] |
187 | |
188 | [[package]] |
189 | + name = "serde_with" |
190 | + version = "3.11.0" |
191 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
192 | + checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" |
193 | + dependencies = [ |
194 | + "base64 0.22.1", |
195 | + "chrono", |
196 | + "hex", |
197 | + "indexmap 1.9.3", |
198 | + "indexmap 2.6.0", |
199 | + "serde", |
200 | + "serde_derive", |
201 | + "serde_json", |
202 | + "serde_with_macros", |
203 | + "time", |
204 | + ] |
205 | + |
206 | + [[package]] |
207 | + name = "serde_with_macros" |
208 | + version = "3.11.0" |
209 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
210 | + checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" |
211 | + dependencies = [ |
212 | + "darling", |
213 | + "proc-macro2", |
214 | + "quote", |
215 | + "syn 2.0.79", |
216 | + ] |
217 | + |
218 | + [[package]] |
219 | name = "sha1" |
220 | version = "0.10.6" |
221 | source = "registry+https://github.com/rust-lang/crates.io-index" |
222 | @@ -4972,6 +5089,12 @@ dependencies = [ |
223 | ] |
224 | |
225 | [[package]] |
226 | + name = "urlencoding" |
227 | + version = "2.1.3" |
228 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
229 | + checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" |
230 | + |
231 | + [[package]] |
232 | name = "utf8parse" |
233 | version = "0.2.2" |
234 | source = "registry+https://github.com/rust-lang/crates.io-index" |
235 | @@ -5132,6 +5255,27 @@ dependencies = [ |
236 | ] |
237 | |
238 | [[package]] |
239 | + name = "webfinger-rs" |
240 | + version = "0.0.12" |
241 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
242 | + checksum = "a824b603913432754acb5eb98dc33ba96e35a2889a4efe72e0c8b6d6ec815cef" |
243 | + dependencies = [ |
244 | + "async-convert", |
245 | + "axum", |
246 | + "axum-extra", |
247 | + "document-features", |
248 | + "http 1.1.0", |
249 | + "nutype", |
250 | + "percent-encoding", |
251 | + "reqwest 0.12.8", |
252 | + "serde", |
253 | + "serde_json", |
254 | + "serde_with", |
255 | + "thiserror", |
256 | + "tracing", |
257 | + ] |
258 | + |
259 | + [[package]] |
260 | name = "webpki-roots" |
261 | version = "0.26.6" |
262 | source = "registry+https://github.com/rust-lang/crates.io-index" |
263 | diff --git a/ayllu/Cargo.toml b/ayllu/Cargo.toml |
264 | index e55335b..9565dcf 100644 |
265 | --- a/ayllu/Cargo.toml |
266 | +++ b/ayllu/Cargo.toml |
267 | @@ -52,7 +52,6 @@ tracing = "0.1.40" |
268 | tower = { version = "0.5.1", features = ["util", "timeout", "tracing"] } |
269 | mime = "0.3.17" |
270 | async-trait = "0.1.83" |
271 | - webfinger = "0.5.1" |
272 | tarpc = { version = "0.34.0", features = ["full"] } |
273 | bytes = "1.7.2" |
274 | tokio-stream = "0.1.16" |
275 | @@ -60,6 +59,7 @@ httparse = "1.9.5" |
276 | thiserror = "1.0.64" |
277 | headers = "0.4.0" |
278 | include_dir = { version = "0.7.4", features = ["glob"] } |
279 | + webfinger-rs = { version = "0.0.12", features = ["axum"] } |
280 | |
281 | # NOTE: this must be cautiously updated along with sqlx and rusqlite. |
282 | [dependencies.libsqlite3-sys] |
283 | diff --git a/ayllu/src/config.rs b/ayllu/src/config.rs |
284 | index e3f2cd5..78ed315 100644 |
285 | --- a/ayllu/src/config.rs |
286 | +++ b/ayllu/src/config.rs |
287 | @@ -176,9 +176,21 @@ impl Xmpp { |
288 | } |
289 | |
290 | #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
291 | + pub struct MailingList { |
292 | + pub id: String, |
293 | + pub name: Option<String>, |
294 | + pub address: String, |
295 | + pub description: Option<String>, |
296 | + pub topics: Vec<String>, |
297 | + // pub post_policy: PostPolicy, |
298 | + // pub subscription_policy: SubscriptionPolicy, |
299 | + } |
300 | + |
301 | + #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
302 | pub struct Mail { |
303 | #[serde(default = "Mail::default_socket_path")] |
304 | pub socket_path: String, |
305 | + pub lists: Vec<MailingList>, |
306 | } |
307 | |
308 | impl Mail { |
309 | @@ -196,9 +208,19 @@ pub struct Link { |
310 | } |
311 | |
312 | #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
313 | + pub struct UrlLink { |
314 | + pub url: String, |
315 | + pub mime_type: Option<String> |
316 | + } |
317 | + |
318 | + |
319 | + #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
320 | pub struct Author { |
321 | pub email: String, |
322 | - pub links: Vec<Link>, |
323 | + pub tagline: Option<String>, |
324 | + pub avatar: Option<UrlLink>, |
325 | + pub profiles: Option<Vec<UrlLink>>, |
326 | + // TODO: PGP Keys, SSH Keys |
327 | } |
328 | |
329 | #[derive(Deserialize, Serialize, Clone, Debug)] |
330 | @@ -255,6 +277,7 @@ pub struct Config { |
331 | #[serde(default = "Config::default_site_name")] |
332 | pub site_name: String, |
333 | pub origin: String, |
334 | + // TODO: Deprecate this |
335 | pub domain: Option<String>, |
336 | pub git: Git, |
337 | #[serde(default = "Config::default_worker_threads")] |
338 | diff --git a/ayllu/src/web2/error.rs b/ayllu/src/web2/error.rs |
339 | index 14b3954..4f20162 100644 |
340 | --- a/ayllu/src/web2/error.rs |
341 | +++ b/ayllu/src/web2/error.rs |
342 | @@ -1,6 +1,10 @@ |
343 | use std::io::Error as IoError; |
344 | |
345 | - use axum::{body::Body, response::IntoResponse, response::Response}; |
346 | + use axum::{ |
347 | + body::Body, |
348 | + response::{IntoResponse, Response}, |
349 | + }; |
350 | + use serde::Serialize; |
351 | use tera::Error as TeraError; |
352 | |
353 | use ayllu_api::error::ApiError; |
354 | @@ -10,7 +14,7 @@ use ayllu_rpc::tarpc::client::RpcError; |
355 | |
356 | /// Error maps known error types into errors that can be translated into HTTP |
357 | /// status codes, e.g. Io::NotFound -> 404 |
358 | - #[derive(thiserror::Error, Debug, Clone)] |
359 | + #[derive(thiserror::Error, Debug, Clone, Serialize)] |
360 | pub enum Error { |
361 | #[error("Generic failure: {0}")] |
362 | Message(String), |
363 | diff --git a/ayllu/src/web2/middleware/sites.rs b/ayllu/src/web2/middleware/sites.rs |
364 | index c711298..41e5660 100644 |
365 | --- a/ayllu/src/web2/middleware/sites.rs |
366 | +++ b/ayllu/src/web2/middleware/sites.rs |
367 | @@ -133,6 +133,7 @@ pub async fn middleware( |
368 | |
369 | // special case if running a static site in-front of the forge |
370 | // where requests can fall through to the backend. |
371 | + // TODO: Change this to use the origin URL |
372 | if hostname.is_some_and(|name| name == cfg.domain.unwrap()) { |
373 | let response = next.run(req).await; |
374 | Ok(response) |
375 | diff --git a/ayllu/src/web2/routes/finger.rs b/ayllu/src/web2/routes/finger.rs |
376 | index ab8c4d1..2c3eb33 100644 |
377 | --- a/ayllu/src/web2/routes/finger.rs |
378 | +++ b/ayllu/src/web2/routes/finger.rs |
379 | @@ -1,93 +1,313 @@ |
380 | - use axum::{ |
381 | - extract::{Extension, Query}, |
382 | - http::StatusCode, |
383 | - response::IntoResponse, |
384 | - response::Json, |
385 | - response::Response, |
386 | + use std::collections::HashMap; |
387 | + use std::path::PathBuf; |
388 | + |
389 | + use axum::{extract::Extension, http::Uri, response::Json}; |
390 | + use url::Url; |
391 | + use webfinger_rs::{Link, Rel, WebFingerRequest, WebFingerResponse}; |
392 | + |
393 | + use crate::{ |
394 | + config::{Author, Collection, Config, MailingList}, |
395 | + web2::error::Error, |
396 | }; |
397 | - use serde::Deserialize; |
398 | - use webfinger::{Link, Prefix, Resolver, ResolverError, Webfinger}; |
399 | + use ayllu_git::{name, Error as GitError, Wrapper as Repository}; |
400 | |
401 | - use std::sync::Arc; |
402 | + const AVATAR: &str = "http://webfinger.net/rel/avatar"; |
403 | + const PROFILE_PAGE: &str = "http://webfinger.net/rel/profile-page"; |
404 | |
405 | - use crate::config::Config; |
406 | - // use crate::web2::error::Error; |
407 | + const COLLECTION: &str = "http://ayllu-forge.org/rel/collection"; |
408 | + const DESCRIPTION: &str = "http://ayllu-forge.org/rel/description"; |
409 | + const MAILING_LIST: &str = "http://ayllu-forge.org/rel/mailing-list"; |
410 | + const REPOSITORY: &str = "http://ayllu-forge.org/rel/repository"; |
411 | |
412 | - #[derive(Clone)] |
413 | - pub struct CResolver { |
414 | - pub domain: &'static str, |
415 | - pub config: Arc<Config>, |
416 | + struct Pair(Collection, Vec<(String, ayllu_git::Config)>); |
417 | + |
418 | + fn get_all(collections: &[Collection]) -> Result<Vec<Pair>, ayllu_git::Error> { |
419 | + collections |
420 | + .iter() |
421 | + .filter(|collection| { |
422 | + collection.hidden.is_none() || collection.hidden.is_some_and(|hidden| !hidden) |
423 | + }) |
424 | + .try_fold(Vec::new(), |mut accm, collection| { |
425 | + let scanner = ayllu_git::Scanner::from_path(&collection.path)?; |
426 | + let reposoitories: Result<Vec<(String, ayllu_git::Config)>, GitError> = scanner |
427 | + .map(|repo_path| { |
428 | + let repository = Repository::new(repo_path.as_path())?; |
429 | + let repository_config = repository.config()?; |
430 | + let repository_name = name(&repo_path); |
431 | + Ok::<(String, ayllu_git::Config), GitError>(( |
432 | + repository_name, |
433 | + repository_config, |
434 | + )) |
435 | + }) |
436 | + .collect(); |
437 | + accm.push(Pair(collection.clone(), reposoitories?)); |
438 | + Ok::<Vec<Pair>, GitError>(accm) |
439 | + }) |
440 | } |
441 | |
442 | - #[derive(Deserialize, Clone)] |
443 | - pub struct FingerParams { |
444 | - pub resource: String, |
445 | + #[derive(Clone, Debug)] |
446 | + enum Resource { |
447 | + Acct(Author), |
448 | + MailingList(MailingList), |
449 | + Collection((Collection, Vec<String>)), |
450 | + Repository((Collection, PathBuf)), |
451 | + Index, |
452 | } |
453 | |
454 | - pub struct Error(ResolverError); |
455 | + /// In-memory resolver for handling finger queries. Uses a combination of static |
456 | + /// configuration in the Ayllu config file and repositories on the file system. |
457 | + #[derive(Clone)] |
458 | + pub struct Resolver { |
459 | + pub collections: Vec<Collection>, |
460 | + pub authors: HashMap<String, Author>, |
461 | + pub mailing_lists: HashMap<String, MailingList>, |
462 | + origin: Url, |
463 | + } |
464 | |
465 | - impl IntoResponse for Error { |
466 | - fn into_response(self) -> Response { |
467 | - let status_code = match self.0 { |
468 | - ResolverError::InvalidResource => StatusCode::INTERNAL_SERVER_ERROR, |
469 | - ResolverError::WrongDomain => StatusCode::INTERNAL_SERVER_ERROR, |
470 | - ResolverError::NotFound => StatusCode::NOT_FOUND, |
471 | - }; |
472 | - (status_code, format!("resolver: {:?}", self.0)).into_response() |
473 | + impl Resolver { |
474 | + pub fn new(config: &Config) -> Self { |
475 | + Resolver { |
476 | + origin: Url::parse(&config.origin).expect("Origin URL is invalid"), |
477 | + collections: config.collections.clone(), |
478 | + authors: HashMap::from_iter( |
479 | + config |
480 | + .authors |
481 | + .iter() |
482 | + .map(|author| (author.email.clone(), author.clone())), |
483 | + ), |
484 | + mailing_lists: HashMap::from_iter(config.mail.as_ref().map_or( |
485 | + Vec::new(), |
486 | + |mail_config| { |
487 | + mail_config |
488 | + .lists |
489 | + .iter() |
490 | + .map(|mailing_list| (mailing_list.address.clone(), mailing_list.clone())) |
491 | + .collect() |
492 | + }, |
493 | + )), |
494 | + } |
495 | } |
496 | - } |
497 | |
498 | - impl Resolver<()> for CResolver { |
499 | - fn instance_domain<'a>(&self) -> &'a str { |
500 | - self.domain |
501 | + /// Determine the type of resource the caller wants to resolve but don't |
502 | + /// resolve it yet |
503 | + fn hint(&self, resource: &Uri) -> Result<Resource, Error> { |
504 | + let authority_str = resource.authority().map(|authority| authority.as_str()); |
505 | + let account = match authority_str { |
506 | + Some(authority) => match authority.split_once(':') { |
507 | + Some(("acct", address)) => Some(address), |
508 | + _ => None, |
509 | + }, |
510 | + None => None, |
511 | + }; |
512 | + if let Some(account) = account { |
513 | + if let Some(author) = self.authors.get(account) { |
514 | + return Ok(Resource::Acct(author.clone())); |
515 | + } else { |
516 | + return Err::<Resource, Error>(Error::NotFound(resource.to_string())); |
517 | + }; |
518 | + }; |
519 | + |
520 | + let mut collections = self.collections.iter().filter(|collection| { |
521 | + collection.hidden.is_none() || collection.hidden.is_some_and(|hidden| !hidden) |
522 | + }); |
523 | + |
524 | + if resource.to_string().eq(&self.origin.to_string()) { |
525 | + return Ok(Resource::Index); |
526 | + } |
527 | + |
528 | + if let Some(mailing_list) = self.mailing_lists.get(&resource.to_string()) { |
529 | + return Ok(Resource::MailingList(mailing_list.clone())); |
530 | + } |
531 | + |
532 | + if let Some(resource) = collections.find_map(|collection| { |
533 | + if collection.name.eq(resource.path().trim_start_matches("/")) { |
534 | + let scanner = ayllu_git::Scanner::from_path(&collection.path).ok()?; |
535 | + Some(Resource::Collection(( |
536 | + collection.clone(), |
537 | + scanner |
538 | + .into_iter() |
539 | + .map(|repo_path| ayllu_git::name(repo_path.as_path())) |
540 | + .collect(), |
541 | + ))) |
542 | + } else { |
543 | + None |
544 | + } |
545 | + }) { |
546 | + return Ok(resource); |
547 | + } |
548 | + |
549 | + if let Some(resource) = collections.find_map(|collection| { |
550 | + let mut scanner = ayllu_git::Scanner::from_path(&collection.path).ok()?; |
551 | + scanner.find_map(|repo_path| { |
552 | + let (collection_str, name) = ayllu_git::collection_and_name(repo_path.as_path()); |
553 | + if resource |
554 | + .path() |
555 | + .trim_start_matches("/") |
556 | + .eq(&format!("{}/{}", collection_str, name)) |
557 | + && resource.host().is_some_and(|resource_host| { |
558 | + resource_host.eq(self.origin.host_str().unwrap_or_default()) |
559 | + }) |
560 | + { |
561 | + Some(Ok::<Resource, ayllu_git::Error>(Resource::Repository(( |
562 | + collection.clone(), |
563 | + repo_path.clone(), |
564 | + )))) |
565 | + } else { |
566 | + None |
567 | + } |
568 | + }) |
569 | + }) { |
570 | + return Ok(resource?); |
571 | + } |
572 | + |
573 | + Err(Error::NotFound(resource.to_string())) |
574 | } |
575 | |
576 | - fn find( |
577 | - &self, |
578 | - prefix: Prefix, |
579 | - acct: String, |
580 | - _resource_repo: (), |
581 | - ) -> Result<Webfinger, ResolverError> { |
582 | - match prefix { |
583 | - Prefix::Acct => { |
584 | - // TODO: expand this to use in addition to the static config |
585 | - // objects also "author repositories" where each author has a |
586 | - // repo that contains a static json response for this. |
587 | - let author = self |
588 | - .config |
589 | - .authors |
590 | + pub async fn resolve(&self, resource: &Uri) -> Result<WebFingerResponse, Error> { |
591 | + match self.hint(resource)? { |
592 | + Resource::Acct(author) => { |
593 | + let mut links: Vec<Link> = Vec::new(); |
594 | + if let Some(profiles) = author.profiles.as_ref() { |
595 | + profiles.iter().for_each(|profile| { |
596 | + let mut link = Link::new(PROFILE_PAGE.into()); |
597 | + link.href = Some(profile.url.clone()); |
598 | + link.r#type = profile.mime_type.clone(); |
599 | + links.push(link); |
600 | + }); |
601 | + } |
602 | + if let Some(avatar) = author.avatar.as_ref() { |
603 | + links.push(Link::builder(AVATAR).href(avatar.url.to_string()).build()); |
604 | + } |
605 | + links.push( |
606 | + Link::builder(DESCRIPTION) |
607 | + .properties(HashMap::from_iter(vec![( |
608 | + "text".to_string(), |
609 | + author.tagline.clone(), |
610 | + )])) |
611 | + .build(), |
612 | + ); |
613 | + Ok(WebFingerResponse { |
614 | + subject: author.email.clone(), |
615 | + aliases: None, |
616 | + properties: None, |
617 | + links, |
618 | + }) |
619 | + } |
620 | + Resource::MailingList(mailing_list) => { |
621 | + // TODO: Subscribe, unsubscribe, etc. |
622 | + Ok(WebFingerResponse { |
623 | + subject: resource.to_string(), |
624 | + aliases: None, |
625 | + properties: None, |
626 | + links: vec![Link::builder(DESCRIPTION) |
627 | + .href(format!("mailto://{}", mailing_list.address)) |
628 | + .properties(HashMap::from_iter(vec![( |
629 | + "description".to_string(), |
630 | + mailing_list.description.clone(), |
631 | + )])) |
632 | + .build()], |
633 | + }) |
634 | + } |
635 | + Resource::Collection((collection, repositories)) => { |
636 | + let mut links: Vec<Link> = repositories |
637 | .iter() |
638 | - .find(|author| author.email == acct); |
639 | - match author { |
640 | - Some(author) => Ok(Webfinger { |
641 | - subject: acct.clone(), |
642 | - aliases: vec![acct.clone()], |
643 | - links: author |
644 | - .links |
645 | - .iter() |
646 | - .map(|link| Link { |
647 | - rel: link.rel.clone(), |
648 | - href: link.href.clone(), |
649 | - template: link.template.clone(), |
650 | - mime_type: link.mime_template.clone(), |
651 | - }) |
652 | - .collect(), |
653 | - }), |
654 | - None => Err(ResolverError::NotFound), |
655 | + .map(|repo_name| { |
656 | + Link::builder(REPOSITORY) |
657 | + .href( |
658 | + self.origin |
659 | + .join(&format!("{}/{}", collection.name, repo_name)) |
660 | + .unwrap(), |
661 | + ) |
662 | + .build() |
663 | + }) |
664 | + .collect(); |
665 | + links.push( |
666 | + Link::builder(DESCRIPTION) |
667 | + .properties(HashMap::from_iter(vec![( |
668 | + "description".to_string(), |
669 | + collection.description.clone(), |
670 | + )])) |
671 | + .build(), |
672 | + ); |
673 | + Ok(WebFingerResponse { |
674 | + subject: resource.to_string(), |
675 | + aliases: None, |
676 | + properties: None, |
677 | + links, |
678 | + }) |
679 | + } |
680 | + Resource::Repository((collection, repo_path)) => { |
681 | + let collection_link = self.origin.join(&collection.name).unwrap(); |
682 | + let repository = ayllu_git::Wrapper::new(repo_path.as_path())?; |
683 | + let config = repository.config()?; |
684 | + let mut links: Vec<Link> = vec![ |
685 | + Link::builder(COLLECTION) |
686 | + .href(collection_link.to_string()) |
687 | + .build(), |
688 | + Link::builder(DESCRIPTION) |
689 | + .properties(HashMap::from_iter(vec![( |
690 | + "description".to_string(), |
691 | + config.description.clone(), |
692 | + )])) |
693 | + .build(), |
694 | + ]; |
695 | + if let Some(mailing_lists) = config.mail { |
696 | + mailing_lists.iter().for_each(|mailing_list| { |
697 | + links.push( |
698 | + Link::builder(MAILING_LIST) |
699 | + .href(format!("mailto://{}", mailing_list.0)) |
700 | + .build(), |
701 | + ); |
702 | + }); |
703 | } |
704 | + Ok(WebFingerResponse { |
705 | + subject: resource.to_string(), |
706 | + aliases: None, |
707 | + properties: None, |
708 | + links, |
709 | + }) |
710 | + } |
711 | + Resource::Index => { |
712 | + let index = get_all(&self.collections)?; |
713 | + let links = index.iter().fold(Vec::new(), |mut accm, pair| { |
714 | + let collection_link = self.origin.join(&pair.0.name).unwrap(); |
715 | + accm.push( |
716 | + Link::builder(COLLECTION) |
717 | + .href(collection_link.clone()) |
718 | + .properties(HashMap::from_iter(vec![( |
719 | + "description".to_string(), |
720 | + pair.0.description.clone(), |
721 | + )])) |
722 | + .build(), |
723 | + ); |
724 | + accm.extend(pair.1.iter().map(|(name, config)| { |
725 | + let repository_link = collection_link.join(name).unwrap(); |
726 | + Link::builder(REPOSITORY) |
727 | + .href(repository_link) |
728 | + .properties(HashMap::from_iter(vec![( |
729 | + "description".to_string(), |
730 | + config.description.clone(), |
731 | + )])) |
732 | + .build() |
733 | + })); |
734 | + accm |
735 | + }); |
736 | + Ok(WebFingerResponse::builder(resource.to_string()) |
737 | + .links(links) |
738 | + .build()) |
739 | } |
740 | - Prefix::Group => Err(ResolverError::InvalidResource), |
741 | - Prefix::Custom(_) => Err(ResolverError::InvalidResource), |
742 | } |
743 | } |
744 | } |
745 | |
746 | pub async fn serve( |
747 | - Extension(resolver): Extension<CResolver>, |
748 | - Query(params): Query<FingerParams>, |
749 | - ) -> Result<Json<Webfinger>, Error> { |
750 | - match resolver.find(webfinger::Prefix::Acct, params.resource, ()) { |
751 | - Ok(result) => Ok(Json(result)), |
752 | - Err(e) => Err(Error(e)), |
753 | - } |
754 | + Extension(resolver): Extension<Resolver>, |
755 | + request: WebFingerRequest, |
756 | + ) -> Result<WebFingerResponse, Json<Error>> { |
757 | + let mut response = resolver.resolve(&request.resource).await?; |
758 | + // filter response if rels is provided in query |
759 | + response |
760 | + .links |
761 | + .retain(|link| request.rels.is_empty() || request.rels.contains(&link.rel)); |
762 | + Ok(response) |
763 | } |
764 | diff --git a/ayllu/src/web2/server.rs b/ayllu/src/web2/server.rs |
765 | index eda1e10..f03b1f1 100644 |
766 | --- a/ayllu/src/web2/server.rs |
767 | +++ b/ayllu/src/web2/server.rs |
768 | @@ -230,10 +230,7 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
769 | ) |
770 | .route( |
771 | "/.well-known/webfinger", |
772 | - routing::get(finger::serve).layer(Extension(finger::CResolver { |
773 | - domain: "todo", |
774 | - config: Arc::new(cfg.clone()), |
775 | - })), |
776 | + routing::get(finger::serve).layer(Extension(finger::Resolver::new(cfg))), |
777 | ) |
778 | .nest( |
779 | "/static", |
780 | diff --git a/config.example.toml b/config.example.toml |
781 | index 7066083..2607305 100644 |
782 | --- a/config.example.toml |
783 | +++ b/config.example.toml |
784 | @@ -1,14 +1,8 @@ |
785 | # site name used in various places across the instance |
786 | site_name = "🌄 Ayllu" |
787 | |
788 | - # string that is used in constructing URLs on the frontend, often different |
789 | - # from a listen address when using a reverse proxy. |
790 | - origin = "http://localhost:8080" |
791 | - |
792 | - # optional domain name to use for site matching when the code forge is being |
793 | - # served behind a static website. This value will be loaded from the origin |
794 | - # url by default |
795 | - # domain = "fuubar.com" |
796 | + # A valid URI that identifies this server on the global internet |
797 | + origin = "localhost" |
798 | |
799 | # sysadmin contact address |
800 | sysadmin = "admin@ayllu-forge.org" |
801 | diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml |
802 | index b717f8f..fc90767 100644 |
803 | --- a/crates/git/Cargo.toml |
804 | +++ b/crates/git/Cargo.toml |
805 | @@ -2,6 +2,7 @@ |
806 | name = "ayllu_git" |
807 | version = "0.2.1" |
808 | edition = "2021" |
809 | + rust-version = "1.70.0" |
810 | |
811 | [dependencies] |
812 | git2 = "0.19.0" |
813 | diff --git a/crates/git/src/lib.rs b/crates/git/src/lib.rs |
814 | index 0d7a151..01d47ac 100644 |
815 | --- a/crates/git/src/lib.rs |
816 | +++ b/crates/git/src/lib.rs |
817 | @@ -1,7 +1,7 @@ |
818 | pub use config::{ChatKind, ChatLink, Config}; |
819 | pub use error::{Error, ErrorKind}; |
820 | pub use lite::{Blob, Branch, Commit, Kind, Stats, Tag, TreeEntry}; |
821 | - pub use scanner::{contains, git_dir, Scanner}; |
822 | + pub use scanner::{collection, collection_and_name, contains, git_dir, name, Scanner}; |
823 | pub use wrapper::{Selector, Wrapper}; |
824 | |
825 | mod clone; |
826 | diff --git a/crates/git/src/scanner.rs b/crates/git/src/scanner.rs |
827 | index e7c961a..ba94cf3 100644 |
828 | --- a/crates/git/src/scanner.rs |
829 | +++ b/crates/git/src/scanner.rs |
830 | @@ -41,6 +41,26 @@ pub fn contains(collections: Vec<&Path>, path: &Path) -> bool { |
831 | false |
832 | } |
833 | |
834 | + /// Return the name of the repository path |
835 | + pub fn name(path: &Path) -> String { |
836 | + path.parent() |
837 | + .expect("path has no parent") |
838 | + .to_string_lossy() |
839 | + .to_string() |
840 | + } |
841 | + |
842 | + /// Return the collection of the repository path |
843 | + pub fn collection(path: &Path) -> String { |
844 | + let parent = path.parent().expect("path has no parent"); |
845 | + let file_name = parent.file_name().expect("parent has no file name"); |
846 | + file_name.to_string_lossy().to_string() |
847 | + } |
848 | + |
849 | + /// Return the collection and name of the repository path |
850 | + pub fn collection_and_name(path: &Path) -> (String, String) { |
851 | + (collection(path), name(path)) |
852 | + } |
853 | + |
854 | // wrap around a directory listing returning each result that looks like a |
855 | // git repository. |
856 | pub struct Scanner { |
857 | @@ -76,7 +96,6 @@ impl Iterator for Scanner { |
858 | } |
859 | |
860 | #[cfg(test)] |
861 | - |
862 | mod tests { |
863 | |
864 | use crate::testing; |
865 | diff --git a/quipu/Cargo.toml b/quipu/Cargo.toml |
866 | index b1d9f2e..cb3cd62 100644 |
867 | --- a/quipu/Cargo.toml |
868 | +++ b/quipu/Cargo.toml |
869 | @@ -21,3 +21,4 @@ clap_complete = "4.5.33" |
870 | thiserror = "1.0.64" |
871 | serde = { version = "1.0.210", features = ["derive"] } |
872 | url = { version = "2.5.2", features = ["serde"] } |
873 | + webfinger-rs = { version = "0.0.12", features = ["reqwest"] } |
874 | diff --git a/quipu/src/client.rs b/quipu/src/client.rs |
875 | new file mode 100644 |
876 | index 0000000..300e24e |
877 | --- /dev/null |
878 | +++ b/quipu/src/client.rs |
879 | @@ -0,0 +1,41 @@ |
880 | + use reqwest::{Client, ClientBuilder}; |
881 | + use url::Url; |
882 | + |
883 | + use ayllu_api::ping::Status; |
884 | + |
885 | + use crate::error::QuipuError; |
886 | + |
887 | + const QUIPU_USER_AGENT: &str = "Quipu 0.0.0"; |
888 | + |
889 | + pub struct Quipu { |
890 | + endpoint: Url, |
891 | + client: Client, |
892 | + } |
893 | + |
894 | + impl Quipu { |
895 | + pub fn new(endpoint: Url) -> Self { |
896 | + let client = ClientBuilder::new() |
897 | + .user_agent(QUIPU_USER_AGENT) |
898 | + .build() |
899 | + .unwrap(); |
900 | + Self { endpoint, client } |
901 | + } |
902 | + |
903 | + pub async fn ping(&self) -> Result<Status, QuipuError> { |
904 | + let response = self |
905 | + .client |
906 | + .get(self.endpoint.join("/0/ping")?) |
907 | + .send() |
908 | + .await?; |
909 | + Ok(response.json().await?) |
910 | + } |
911 | + |
912 | + pub async fn finger(&self, resource: &str) -> Result<(), QuipuError> { |
913 | + let request = webfinger_rs::RequestBuilder::new(resource)? |
914 | + .host(self.endpoint.authority()) |
915 | + .build(); |
916 | + let result = request.execute_reqwest_with_client(&self.client).await?; |
917 | + println!("{}", result); |
918 | + Ok(()) |
919 | + } |
920 | + } |
921 | diff --git a/quipu/src/client_rest.rs b/quipu/src/client_rest.rs |
922 | deleted file mode 100644 |
923 | index aa49e95..0000000 |
924 | --- a/quipu/src/client_rest.rs |
925 | +++ /dev/null |
926 | @@ -1,41 +0,0 @@ |
927 | - use reqwest::{Client, ClientBuilder}; |
928 | - use url::Url; |
929 | - |
930 | - use ayllu_api::{discovery::Collection, ping::Status}; |
931 | - |
932 | - use crate::error::QuipuError; |
933 | - |
934 | - const QUIPU_USER_AGENT: &str = "Quipu 0.0.0"; |
935 | - |
936 | - pub struct Quipu { |
937 | - endpoint: Url, |
938 | - client: Client, |
939 | - } |
940 | - |
941 | - impl Quipu { |
942 | - pub fn new(endpoint: Url) -> Self { |
943 | - let client = ClientBuilder::new() |
944 | - .user_agent(QUIPU_USER_AGENT) |
945 | - .build() |
946 | - .unwrap(); |
947 | - Self { endpoint, client } |
948 | - } |
949 | - |
950 | - pub async fn get_index(&self) -> Result<Vec<Collection>, QuipuError> { |
951 | - let response = self |
952 | - .client |
953 | - .get(self.endpoint.join("/0/index")?) |
954 | - .send() |
955 | - .await?; |
956 | - Ok(response.json().await?) |
957 | - } |
958 | - |
959 | - pub async fn ping(&self) -> Result<Status, QuipuError> { |
960 | - let response = self |
961 | - .client |
962 | - .get(self.endpoint.join("/0/ping")?) |
963 | - .send() |
964 | - .await?; |
965 | - Ok(response.json().await?) |
966 | - } |
967 | - } |
968 | diff --git a/quipu/src/error.rs b/quipu/src/error.rs |
969 | index 6d1ef8a..95c986f 100644 |
970 | --- a/quipu/src/error.rs |
971 | +++ b/quipu/src/error.rs |
972 | @@ -7,16 +7,18 @@ use ayllu_config::Error as ConfigError; |
973 | |
974 | #[derive(Error, Debug)] |
975 | pub enum QuipuError { |
976 | - #[error("IO Error")] |
977 | + #[error("IO Error: {0}")] |
978 | Disconnect(#[from] std::io::Error), |
979 | - #[error("Configuration Error")] |
980 | + #[error("Configuration Error: {0}")] |
981 | Config(#[from] ConfigError), |
982 | - #[error("Invalid Log Level")] |
983 | + #[error("Invalid Log Level: {0}")] |
984 | LogLevel(#[from] ParseLevelError), |
985 | - #[error("Invalid Url")] |
986 | + #[error("Invalid Url: {0}")] |
987 | URLParsing(#[from] ParseUrlError), |
988 | - #[error("Request Error")] |
989 | + #[error("Request Error: {0}")] |
990 | Request(#[from] ReqwestError), |
991 | - #[error("Internal Quipu Error")] |
992 | - Message(String) |
993 | + #[error("Internal Quipu Error: {0}")] |
994 | + Message(String), |
995 | + #[error("Finger Error: {0}")] |
996 | + Finger(#[from] webfinger_rs::Error), |
997 | } |
998 | diff --git a/quipu/src/main.rs b/quipu/src/main.rs |
999 | index 6da65c6..c4bc76e 100644 |
1000 | --- a/quipu/src/main.rs |
1001 | +++ b/quipu/src/main.rs |
1002 | @@ -2,14 +2,14 @@ use std::io::stdout; |
1003 | use std::path::PathBuf; |
1004 | use std::str::FromStr; |
1005 | |
1006 | - use clap::{arg, Command, CommandFactory, Parser, Subcommand, ValueEnum}; |
1007 | + use clap::{arg, Command, CommandFactory, Parser, Subcommand}; |
1008 | use clap_complete::{generate, Generator, Shell}; |
1009 | use tracing::Level; |
1010 | use url::Url; |
1011 | |
1012 | use ayllu_config::Reader; |
1013 | |
1014 | - mod client_rest; |
1015 | + mod client; |
1016 | mod config; |
1017 | mod error; |
1018 | mod output; |
1019 | @@ -17,7 +17,7 @@ mod output; |
1020 | #[derive(Parser)] |
1021 | #[command(author, version, about, long_about = None)] |
1022 | #[command(name = "quipu")] |
1023 | - #[command(about = "Ayllu RPC Client")] |
1024 | + #[command(about = "Ayllu Client")] |
1025 | struct Cli { |
1026 | /// Path to your configuration file |
1027 | #[arg(short, long, value_name = "FILE")] |
1028 | @@ -38,27 +38,17 @@ struct Cli { |
1029 | command: Commands, |
1030 | } |
1031 | |
1032 | - #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] |
1033 | - /// a resource that exists on the remote server |
1034 | - enum Resource { |
1035 | - /// an amalgamation of collections and repositories |
1036 | - Index, |
1037 | - } |
1038 | - |
1039 | #[derive(Subcommand, Debug, PartialEq)] |
1040 | enum Commands { |
1041 | - /// generate autocomplete commands for common shells |
1042 | + /// Generate autocomplete commands for common shells |
1043 | Complete { |
1044 | #[arg(long)] |
1045 | shell: Shell, |
1046 | }, |
1047 | - /// get resources from a remote Ayllu server |
1048 | - Get { |
1049 | - /// resource to request from remote server |
1050 | - resource: Resource, |
1051 | - }, |
1052 | - /// verify the remote server is functional |
1053 | + /// Verify the remote server is functional |
1054 | Ping, |
1055 | + /// Perform a webfinger query against the Ayllu instance |
1056 | + Finger { resource: String }, |
1057 | } |
1058 | |
1059 | fn print_completions<G: Generator>(gen: G, cmd: &mut Command) { |
1060 | @@ -111,19 +101,20 @@ async fn main() -> Result<(), error::QuipuError> { |
1061 | print_completions(shell, &mut cmd); |
1062 | Ok(()) |
1063 | } |
1064 | - Commands::Get { resource: _ } => { |
1065 | + Commands::Finger { resource } => { |
1066 | let instance = get_instance(&cfg, cli.url, cli.instance)?; |
1067 | - let client = client_rest::Quipu::new(instance.url); |
1068 | - let collections = client.get_index().await?; |
1069 | - output::pretty(output::Resource::Collections(collections))?; |
1070 | + let client = client::Quipu::new(instance.url); |
1071 | + client.finger(&resource).await?; |
1072 | Ok(()) |
1073 | } |
1074 | Commands::Ping => { |
1075 | let instance = get_instance(&cfg, cli.url, cli.instance)?; |
1076 | - let client = client_rest::Quipu::new(instance.url); |
1077 | + let client = client::Quipu::new(instance.url); |
1078 | let status = client.ping().await?; |
1079 | if !status.ok { |
1080 | - return Err(error::QuipuError::Message("server is not ready".to_string())); |
1081 | + return Err(error::QuipuError::Message( |
1082 | + "server is not ready".to_string(), |
1083 | + )); |
1084 | } |
1085 | Ok(()) |
1086 | } |