diff --git a/.gitignore b/.gitignore
index 1469e2f..e3745a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,6 @@ Cargo.lock
# Environment
.env.local
+
+# IntelliJ IDEA thing, mostly broken versioning
+.idea/misc.xml
diff --git a/.idea/acrate.iml b/.idea/acrate.iml
index 983a32b..32d223d 100644
--- a/.idea/acrate.iml
+++ b/.idea/acrate.iml
@@ -9,6 +9,7 @@
+
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..6cfc387
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql:///acrate
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 30bab2a..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
new file mode 100644
index 0000000..4495f93
--- /dev/null
+++ b/.idea/sqldialects.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index aecded5..357abb7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,3 @@
[workspace]
resolver = "2"
-members = ["acrate-core", "acrate-hostmeta", "acrate-inbox", "acrate-nodeinfo"]
+members = ["acrate-core", "acrate-hostmeta", "acrate-inbox", "acrate-nodeinfo", "acrate-webfinger"]
diff --git a/README.md b/README.md
index 814f637..b524419 100644
--- a/README.md
+++ b/README.md
@@ -11,3 +11,15 @@ Federation database
> [!Caution]
>
> This software suite is in active development!
+
+## Crates
+
+### Libraries
+
+- `acrate-core`: Database stuff
+- `acrate-hostmeta`: RFC 6415 serde
+- `acrate-nodeinfo`: NodeInfo serde
+
+### Binaries
+
+- `acrate-webfinger`: WebFinger server
diff --git a/acrate-core/Cargo.toml b/acrate-core/Cargo.toml
index 6823787..d5b738a 100644
--- a/acrate-core/Cargo.toml
+++ b/acrate-core/Cargo.toml
@@ -4,10 +4,10 @@ version = "0.1.0"
edition = "2021"
[dependencies]
-diesel = "2.2.4"
+diesel = { version = "2.2.4", features = ["postgres", "uuid"] }
+diesel-async = { version = "0.5.1", features = ["postgres"] }
diesel_migrations = "2.2.0"
-acrate-hostmeta = { path = "../acrate-hostmeta" }
-acrate-nodeinfo = { path = "../acrate-nodeinfo" }
+uuid = "1.11.0"
[lints.clippy]
tabs-in-doc-comments = "allow"
diff --git a/acrate-core/migrations/2024-11-14-031744_meta/down.sql b/acrate-core/migrations/2024-11-14-031744_meta/down.sql
new file mode 100644
index 0000000..39d835e
--- /dev/null
+++ b/acrate-core/migrations/2024-11-14-031744_meta/down.sql
@@ -0,0 +1,6 @@
+DROP TABLE IF EXISTS meta_properties CASCADE;
+DROP TABLE IF EXISTS meta_link_properties CASCADE;
+DROP TABLE IF EXISTS meta_link_titles CASCADE;
+DROP TABLE IF EXISTS meta_links CASCADE;
+DROP TABLE IF EXISTS meta_aliases CASCADE;
+DROP TABLE IF EXISTS meta_subjects CASCADE;
diff --git a/acrate-core/migrations/2024-11-14-031744_meta/up.sql b/acrate-core/migrations/2024-11-14-031744_meta/up.sql
new file mode 100644
index 0000000..8387ac9
--- /dev/null
+++ b/acrate-core/migrations/2024-11-14-031744_meta/up.sql
@@ -0,0 +1,63 @@
+CREATE TABLE IF NOT EXISTS meta_subjects (
+ id UUID DEFAULT gen_random_uuid(),
+ document BPCHAR NOT NULL,
+ pattern BPCHAR NOT NULL,
+ redirect BPCHAR,
+
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS meta_aliases (
+ id UUID DEFAULT gen_random_uuid(),
+ document BPCHAR NOT NULL,
+ pattern BPCHAR NOT NULL,
+ alias BPCHAR NOT NULL,
+
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS meta_links (
+ id UUID DEFAULT gen_random_uuid(),
+ document BPCHAR NOT NULL,
+ pattern BPCHAR NOT NULL,
+ rel BPCHAR NOT NULL,
+ type BPCHAR,
+ href BPCHAR,
+ template BPCHAR,
+
+ CONSTRAINT either_href_or_template_not_null CHECK (
+ (href IS NOT NULL AND template IS NULL)
+ OR
+ (href IS NULL AND template IS NOT NULL)
+ ),
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS meta_link_properties (
+ id UUID DEFAULT gen_random_uuid(),
+ meta_link_id UUID REFERENCES meta_links (id) NOT NULL,
+ rel BPCHAR NOT NULL,
+ value BPCHAR,
+
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS meta_properties (
+ id UUID DEFAULT gen_random_uuid(),
+ document BPCHAR NOT NULL,
+ pattern BPCHAR NOT NULL,
+ rel BPCHAR NOT NULL,
+ value BPCHAR,
+
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS meta_link_titles (
+ id UUID DEFAULT gen_random_uuid(),
+ meta_link_id UUID REFERENCES meta_links (id) NOT NULL,
+ language BPCHAR NOT NULL DEFAULT 'und',
+ value BPCHAR NOT NULL,
+
+ CONSTRAINT unique_languages UNIQUE (meta_link_id, language),
+ PRIMARY KEY(id)
+);
diff --git a/acrate-core/src/lib.rs b/acrate-core/src/lib.rs
index dae5ee7..d340197 100644
--- a/acrate-core/src/lib.rs
+++ b/acrate-core/src/lib.rs
@@ -1,4 +1,8 @@
//! Core crate of the `acrate` project.
-pub use acrate_nodeinfo as nodeinfo;
-pub use acrate_hostmeta as hostmeta;
+mod schema;
+
+pub mod meta;
+
+pub use diesel;
+pub use diesel_async;
diff --git a/acrate-core/src/meta.rs b/acrate-core/src/meta.rs
new file mode 100644
index 0000000..4fe0343
--- /dev/null
+++ b/acrate-core/src/meta.rs
@@ -0,0 +1,165 @@
+use diesel::{Associations, Identifiable, Insertable, QueryResult, Queryable, QueryableByName, Selectable, pg::Pg};
+use diesel_async::AsyncPgConnection;
+use uuid::Uuid;
+use super::schema;
+
+
+#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
+#[diesel(table_name = schema::meta_subjects)]
+#[diesel(check_for_backend(Pg))]
+pub struct MetaSubject {
+ pub id: Uuid,
+ pub document: String,
+ pub pattern: String,
+ pub redirect: Option,
+}
+
+#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
+#[diesel(table_name = schema::meta_aliases)]
+#[diesel(check_for_backend(Pg))]
+pub struct MetaAlias {
+ pub id: Uuid,
+ pub document: String,
+ pub pattern: String,
+ pub alias: String,
+}
+
+#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
+#[diesel(table_name = schema::meta_links)]
+#[diesel(check_for_backend(Pg))]
+pub struct MetaLink {
+ pub id: Uuid,
+ pub document: String,
+ pub pattern: String,
+ pub rel: String,
+ pub type_: Option,
+ pub href: Option,
+ pub template: Option,
+}
+
+#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)]
+#[diesel(belongs_to(MetaLink))]
+#[diesel(table_name = schema::meta_link_properties)]
+#[diesel(check_for_backend(Pg))]
+pub struct MetaLinkProperty {
+ pub id: Uuid,
+ pub meta_link_id: Uuid,
+ pub rel: String,
+ pub value: Option,
+}
+
+#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable, Associations)]
+#[diesel(belongs_to(MetaLink))]
+#[diesel(table_name = schema::meta_link_titles)]
+#[diesel(check_for_backend(Pg))]
+pub struct MetaLinkTitle {
+ pub id: Uuid,
+ pub meta_link_id: Uuid,
+ pub language: String,
+ pub value: String,
+}
+
+#[derive(Debug, Queryable, QueryableByName, Identifiable, Selectable, Insertable)]
+#[diesel(table_name = schema::meta_properties)]
+#[diesel(check_for_backend(Pg))]
+pub struct MetaProperty {
+ pub id: Uuid,
+ pub document: String,
+ pub pattern: String,
+ pub rel: String,
+ pub value: Option,
+}
+
+impl MetaSubject {
+ pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> {
+ use diesel::prelude::*;
+ use diesel_async::RunQueryDsl;
+ use schema::meta_subjects::dsl::*;
+
+ let document_is_equal = document.eq(doc);
+ let subject_matches_pattern = subject.into_sql::().ilike(pattern);
+
+ meta_subjects
+ .filter(document_is_equal)
+ .filter(subject_matches_pattern)
+ .select(Self::as_select())
+ .load(conn)
+ .await
+ }
+}
+
+impl MetaAlias {
+ pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> {
+ use diesel::prelude::*;
+ use diesel_async::RunQueryDsl;
+ use schema::meta_aliases::dsl::*;
+
+ let document_is_equal = document.eq(doc);
+ let subject_matches_pattern = subject.into_sql::().ilike(pattern);
+
+ meta_aliases
+ .filter(document_is_equal)
+ .filter(subject_matches_pattern)
+ .select(Self::as_select())
+ .load(conn)
+ .await
+ }
+}
+
+impl MetaLink {
+ pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> {
+ use diesel::prelude::*;
+ use diesel_async::RunQueryDsl;
+ use schema::meta_links::dsl::*;
+
+ let document_is_equal = document.eq(doc);
+ let subject_matches_pattern = subject.into_sql::().ilike(pattern);
+
+ meta_links
+ .filter(document_is_equal)
+ .filter(subject_matches_pattern)
+ .select(Self::as_select())
+ .load(conn)
+ .await
+ }
+}
+
+impl MetaLinkProperty {
+ pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult> {
+ use diesel::prelude::*;
+ use diesel_async::RunQueryDsl;
+
+ Self::belonging_to(links)
+ .load(conn)
+ .await
+ }
+}
+
+impl MetaLinkTitle {
+ pub async fn query_by_link(conn: &mut AsyncPgConnection, links: &[MetaLink]) -> QueryResult> {
+ use diesel::prelude::*;
+ use diesel_async::RunQueryDsl;
+
+ Self::belonging_to(links)
+ .load(conn)
+ .await
+ }
+}
+
+impl MetaProperty {
+ pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult> {
+ use diesel::prelude::*;
+ use diesel_async::RunQueryDsl;
+ use schema::meta_properties::dsl::*;
+
+ let document_is_equal = document.eq(doc);
+ let subject_matches_pattern = subject.into_sql::().ilike(pattern);
+
+ meta_properties
+ .filter(document_is_equal)
+ .filter(subject_matches_pattern)
+ .select(Self::as_select())
+ .load(conn)
+ .await
+ }
+}
diff --git a/acrate-core/src/schema.rs b/acrate-core/src/schema.rs
new file mode 100644
index 0000000..94b2aa7
--- /dev/null
+++ b/acrate-core/src/schema.rs
@@ -0,0 +1,72 @@
+// @generated automatically by Diesel CLI.
+
+diesel::table! {
+ meta_aliases (id) {
+ id -> Uuid,
+ document -> Bpchar,
+ pattern -> Bpchar,
+ alias -> Bpchar,
+ }
+}
+
+diesel::table! {
+ meta_link_properties (id) {
+ id -> Uuid,
+ meta_link_id -> Uuid,
+ rel -> Bpchar,
+ value -> Nullable,
+ }
+}
+
+diesel::table! {
+ meta_link_titles (id) {
+ id -> Uuid,
+ meta_link_id -> Uuid,
+ language -> Bpchar,
+ value -> Bpchar,
+ }
+}
+
+diesel::table! {
+ meta_links (id) {
+ id -> Uuid,
+ document -> Bpchar,
+ pattern -> Bpchar,
+ rel -> Bpchar,
+ #[sql_name = "type"]
+ type_ -> Nullable,
+ href -> Nullable,
+ template -> Nullable,
+ }
+}
+
+diesel::table! {
+ meta_properties (id) {
+ id -> Uuid,
+ document -> Bpchar,
+ pattern -> Bpchar,
+ rel -> Bpchar,
+ value -> Nullable,
+ }
+}
+
+diesel::table! {
+ meta_subjects (id) {
+ id -> Uuid,
+ document -> Bpchar,
+ pattern -> Bpchar,
+ redirect -> Nullable,
+ }
+}
+
+diesel::joinable!(meta_link_properties -> meta_links (meta_link_id));
+diesel::joinable!(meta_link_titles -> meta_links (meta_link_id));
+
+diesel::allow_tables_to_appear_in_same_query!(
+ meta_aliases,
+ meta_link_properties,
+ meta_link_titles,
+ meta_links,
+ meta_properties,
+ meta_subjects,
+);
diff --git a/acrate-hostmeta/src/jrd.rs b/acrate-hostmeta/src/jrd.rs
index 7662b80..24c7f68 100644
--- a/acrate-hostmeta/src/jrd.rs
+++ b/acrate-hostmeta/src/jrd.rs
@@ -17,6 +17,7 @@ pub struct ResourceDescriptorJRD {
///
/// -
///
+ #[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option,
/// Other names the resource described by this document can be referred to.
@@ -70,6 +71,7 @@ pub struct ResourceDescriptorLinkJRD {
///
/// -
///
+ #[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option,
/// URI to the resource put in relation.
@@ -78,6 +80,7 @@ pub struct ResourceDescriptorLinkJRD {
///
/// -
///
+ #[serde(skip_serializing_if = "Option::is_none")]
pub href: Option,
/// Titles of the resource put in relation in various languages.
@@ -104,6 +107,7 @@ pub struct ResourceDescriptorLinkJRD {
///
/// -
///
+ #[serde(skip_serializing_if = "Option::is_none")]
pub template: Option,
}
@@ -219,7 +223,7 @@ impl From for ResourceDescriptorLinkJRD {
impl From for (String, Option) {
fn from(value: ResourceDescriptorPropertyXRD) -> Self {
- (value.r#type, value.value)
+ (value.rel, value.value)
}
}
diff --git a/acrate-hostmeta/src/xrd.rs b/acrate-hostmeta/src/xrd.rs
index 0e89c1d..96e3a73 100644
--- a/acrate-hostmeta/src/xrd.rs
+++ b/acrate-hostmeta/src/xrd.rs
@@ -9,6 +9,7 @@ use crate::jrd::{ResourceDescriptorJRD, ResourceDescriptorLinkJRD};
/// -
/// -
#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename = "XRD")]
pub struct ResourceDescriptorXRD {
/// The resource this document refers to.
///
@@ -17,6 +18,7 @@ pub struct ResourceDescriptorXRD {
/// -
///
#[serde(rename = "Subject")]
+ #[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option,
/// Other names the resource described by this document can be referred to.
@@ -58,6 +60,7 @@ pub struct ResourceDescriptorXRD {
/// -
///
#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename = "Link")]
pub struct ResourceDescriptorLinkXRD {
/// The kind of relation established by the subject with the attached resource.
///
@@ -75,6 +78,7 @@ pub struct ResourceDescriptorLinkXRD {
/// -
///
#[serde(rename = "@type")]
+ #[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option,
/// URI to the resource put in relation.
@@ -84,6 +88,7 @@ pub struct ResourceDescriptorLinkXRD {
/// -
///
#[serde(rename = "@href")]
+ #[serde(skip_serializing_if = "Option::is_none")]
pub href: Option,
/// Titles of the resource put in relation in various languages.
@@ -93,6 +98,7 @@ pub struct ResourceDescriptorLinkXRD {
/// -
///
#[serde(default)]
+ #[serde(rename = "Title")]
pub titles: Vec,
/// Additional information about the resource put in relation.
@@ -102,6 +108,7 @@ pub struct ResourceDescriptorLinkXRD {
/// -
///
#[serde(default)]
+ #[serde(rename = "Property")]
pub properties: Vec,
/// Template to fill to get the URL to resource-specific information.
@@ -111,6 +118,7 @@ pub struct ResourceDescriptorLinkXRD {
/// -
///
#[serde(rename = "@template")]
+ #[serde(skip_serializing_if = "Option::is_none")]
pub template: Option,
}
@@ -120,22 +128,26 @@ pub struct ResourceDescriptorLinkXRD {
///
/// -
#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename = "Title")]
pub struct ResourceDescriptorTitleXRD {
/// The language of the title.
#[serde(rename = "@lang")]
pub language: String,
/// The title itself.
+ #[serde(rename = "$text")]
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename = "Property")]
pub struct ResourceDescriptorPropertyXRD {
/// The property identifier, or type.
- #[serde(alias = "@type")]
- pub r#type: String,
+ #[serde(rename = "@rel")]
+ pub rel: String,
/// The property value.
+ #[serde(rename = "$text")]
pub value: Option,
}
@@ -234,7 +246,7 @@ impl From for ResourceDescriptorXRD {
impl From<(String, Option)> for ResourceDescriptorPropertyXRD {
fn from(value: (String, Option)) -> Self {
Self {
- r#type: value.0,
+ rel: value.0,
value: value.1,
}
}
diff --git a/acrate-webfinger/Cargo.toml b/acrate-webfinger/Cargo.toml
new file mode 100644
index 0000000..cfd73d0
--- /dev/null
+++ b/acrate-webfinger/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "acrate-webfinger"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+acrate-core = { path = "../acrate-core" }
+acrate-hostmeta = { path = "../acrate-hostmeta" }
+anyhow = "1.0.93"
+axum = { version = "0.7.7", features = ["macros"] }
+axum-extra = { version = "0.9.4", features = ["query"] }
+log = "0.4.22"
+micronfig = "0.3.0"
+minijinja = "2.5.0"
+pretty_env_logger = "0.5.0"
+quick-xml = { version = "0.37.0", features = ["serialize"] }
+serde = { version = "1.0.215", features = ["derive"] }
+serde_json = "1.0.132"
+tokio = { version = "1.41.1", features = ["macros", "net", "rt-multi-thread"] }
diff --git a/acrate-webfinger/src/config.rs b/acrate-webfinger/src/config.rs
new file mode 100644
index 0000000..feb7905
--- /dev/null
+++ b/acrate-webfinger/src/config.rs
@@ -0,0 +1,4 @@
+micronfig::config!(
+ ACRATE_WEBFINGER_DATABASE_URL: String,
+ ACRATE_WEBFINGER_BIND_ADDRESS: String,
+);
diff --git a/acrate-webfinger/src/main.rs b/acrate-webfinger/src/main.rs
new file mode 100644
index 0000000..7b248b0
--- /dev/null
+++ b/acrate-webfinger/src/main.rs
@@ -0,0 +1,41 @@
+use std::sync::Arc;
+use anyhow::Context;
+use axum::Extension;
+
+mod config;
+mod route;
+
+
+#[tokio::main]
+async fn main() -> anyhow::Result {
+ pretty_env_logger::init();
+ log::debug!("Logging initialized!");
+
+ log::trace!("Creating Minijinja environment...");
+ let mut mj = minijinja::Environment::<'static>::new();
+
+ log::trace!("Adding webfinger page to the Minijinja environment...");
+ mj.add_template("webfinger.html.j2", include_str!("webfinger.html.j2"))
+ .expect("webfinger.html.j2 to be a valid Minijinja template");
+
+ log::trace!("Creating Axum router...");
+ let app = axum::Router::new()
+ .route("/.well-known/webfinger", axum::routing::get(route::webfinger_handler))
+ .layer(Extension(Arc::new(mj)));
+ log::trace!("Axum router created successfully!");
+
+ log::trace!("Creating Tokio listener...");
+ let bind_address = config::ACRATE_WEBFINGER_BIND_ADDRESS();
+ let listener = tokio::net::TcpListener::bind(bind_address)
+ .await
+ .context("failed to bind listener to address")?;
+ log::trace!("Tokio listener bound to: {bind_address}");
+
+ log::debug!("Starting server...");
+ axum::serve(listener, app)
+ .await
+ .context("server exited with error")?;
+
+ log::error!("Server exited with no error, panicking.");
+ panic!("server exited with no error");
+}
diff --git a/acrate-webfinger/src/route.rs b/acrate-webfinger/src/route.rs
new file mode 100644
index 0000000..13a7d7d
--- /dev/null
+++ b/acrate-webfinger/src/route.rs
@@ -0,0 +1,290 @@
+use std::sync::Arc;
+use axum::Extension;
+use axum::http::{HeaderMap, Response, StatusCode};
+use axum_extra::extract::Query;
+use serde::Deserialize;
+use acrate_core::diesel::GroupedBy;
+use acrate_core::diesel_async::{AsyncConnection, AsyncPgConnection};
+use acrate_core::meta::{MetaAlias, MetaLink, MetaLinkProperty, MetaLinkTitle, MetaProperty, MetaSubject};
+use acrate_hostmeta::jrd::ResourceDescriptorLinkJRD;
+use acrate_hostmeta::xrd::{ResourceDescriptorLinkXRD, ResourceDescriptorPropertyXRD, ResourceDescriptorTitleXRD};
+use crate::config;
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct WebfingerQuery {
+ pub resource: Option,
+
+ #[serde(default)]
+ pub rel: Vec,
+}
+
+const WEBFINGER_DOC: &str = "/.well-known/webfinger";
+
+#[axum::debug_handler]
+pub async fn webfinger_handler(
+ Query(WebfingerQuery {resource, rel}): Query,
+ headers: HeaderMap,
+ Extension(mj): Extension>>,
+) -> Result, StatusCode> {
+ log::info!("Handling a WebFinger request!");
+
+ let resource = resource.unwrap_or_else(|| "".to_string());
+ log::debug!("Resource is: {resource:#?}");
+
+ log::debug!("Rel is: {rel:#?}");
+
+ let accept = headers.get("Accept")
+ .map(|v| v.to_str())
+ .filter(Result::is_ok)
+ .map(|v| v.unwrap())
+ .unwrap_or("application/json")
+ .to_string();
+ log::debug!("Accept is: {accept:#?}");
+
+ let mut response = Response::new("".to_string());
+
+ let mut conn = AsyncPgConnection::establish(config::ACRATE_WEBFINGER_DATABASE_URL())
+ .await
+ .map_err(|_| StatusCode::BAD_GATEWAY)?;
+
+ let subjects = MetaSubject::query_matching(&mut conn, WEBFINGER_DOC, &resource)
+ .await
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ let subject = subjects.first()
+ .ok_or(StatusCode::NOT_FOUND)?;
+
+ if subject.redirect.is_some() {
+ {
+ let headers = response.headers_mut();
+ headers.insert(
+ "Location",
+ subject.redirect
+ .as_ref()
+ .expect("redirect not to have become suddenly None")
+ .parse()
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
+ );
+ }
+ {
+ let status = response.status_mut();
+ *status = StatusCode::FOUND;
+ }
+
+ return Ok(response);
+ }
+
+ let aliases = MetaAlias::query_matching(&mut conn, WEBFINGER_DOC, &resource)
+ .await
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ let properties = MetaProperty::query_matching(&mut conn, WEBFINGER_DOC, &resource)
+ .await
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ let links = MetaLink::query_matching(&mut conn, WEBFINGER_DOC, &resource)
+ .await
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ let link_properties = MetaLinkProperty::query_by_link(&mut conn, &links)
+ .await
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
+ .grouped_by(&links);
+
+ let link_titles = MetaLinkTitle::query_by_link(&mut conn, &links)
+ .await
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
+ .grouped_by(&links);
+
+ let links_full: Vec<(MetaLink, Vec, Vec)> = links
+ .into_iter()
+ .zip(link_properties)
+ .zip(link_titles)
+ .map(|((link, properties), titles)| (link, properties, titles))
+ .collect();
+
+ {
+ let headers = response.headers_mut();
+ headers.insert(
+ "Access-Control-Allow-Origin",
+ "*".parse().unwrap()
+ );
+ }
+
+ for mime in accept.split(",") {
+ {
+ let headers = response.headers_mut();
+ headers.insert(
+ "Content-Type",
+ mime.parse().map_err(|_| StatusCode::BAD_REQUEST)?
+ );
+ }
+
+ let (mime, _params) = match mime.trim().split_once(";") {
+ Some((mime, params)) => (mime, Some(params)),
+ None => (mime, None),
+ };
+
+ match mime {
+ "*/*" | "application/json" | "application/jrd+json" => {
+ let subject = Some(resource);
+
+ let aliases = aliases
+ .into_iter()
+ .map(|alias| alias.alias)
+ .collect();
+
+ let properties = properties
+ .into_iter()
+ .map(|prop| (prop.rel, prop.value))
+ .collect();
+
+ let links = links_full
+ .into_iter()
+ .map(|(link, properties, titles)| ResourceDescriptorLinkJRD {
+ rel: link.rel,
+ r#type: link.type_,
+ href: link.href,
+ template: link.template,
+ properties: properties
+ .into_iter()
+ .map(|property| (property.rel, property.value))
+ .collect(),
+ titles: titles
+ .into_iter()
+ .map(|title| (title.language, title.value))
+ .collect(),
+ })
+ .collect::>();
+
+ let rd = acrate_hostmeta::jrd::ResourceDescriptorJRD {
+ subject,
+ aliases,
+ properties,
+ links,
+ };
+
+ let json = serde_json::to_string_pretty(&rd)
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ {
+ let body = response.body_mut();
+ body.push_str(&json);
+ }
+
+ return Ok(response);
+ },
+ "application/xml" | "application/xrd+xml" => {
+ let subject = Some(resource);
+
+ let aliases = aliases
+ .into_iter()
+ .map(|alias| alias.alias)
+ .collect();
+
+ let properties: Vec = properties
+ .into_iter()
+ .map(|prop| ResourceDescriptorPropertyXRD {
+ rel: prop.rel,
+ value: prop.value,
+ })
+ .collect();
+
+ let links = links_full
+ .into_iter()
+ .map(|(link, properties, titles)| ResourceDescriptorLinkXRD {
+ rel: link.rel,
+ r#type: link.type_,
+ href: link.href,
+ template: link.template,
+ properties: properties
+ .into_iter()
+ .map(|property| ResourceDescriptorPropertyXRD {
+ rel: property.rel,
+ value: property.value,
+ })
+ .collect(),
+ titles: titles
+ .into_iter()
+ .map(|title| ResourceDescriptorTitleXRD {
+ language: title.language,
+ value: title.value,
+ })
+ .collect(),
+ })
+ .collect::>();
+
+ let rd = acrate_hostmeta::xrd::ResourceDescriptorXRD {
+ subject,
+ aliases,
+ properties,
+ links,
+ };
+
+ let xml = quick_xml::se::to_string(&rd)
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ {
+ let body = response.body_mut();
+ body.push_str(r#""#);
+ body.push_str(&xml);
+ }
+
+ return Ok(response);
+ },
+ "text/html" => {
+ let aliases: Vec = aliases.into_iter()
+ .map(|alias| alias.alias)
+ .collect();
+
+ let properties: Vec<(String, Option)> = properties.into_iter()
+ .map(|prop| {
+ (prop.rel, prop.value)
+ })
+ .collect();
+
+ let links: Vec<(String, Option, Option, Option, Vec<(String, Option)>, Vec<(String, String)>)> = links_full
+ .into_iter()
+ .map(|(link, properties, titles)| {
+ (
+ link.rel,
+ link.type_,
+ link.href,
+ link.template,
+ properties.into_iter()
+ .map(|prop| (prop.rel, prop.value))
+ .collect::)>>(),
+ titles.into_iter()
+ .map(|title| (title.language, title.value))
+ .collect::>()
+ )
+ })
+ .collect();
+
+ let html = mj.get_template("webfinger.html.j2")
+ .expect("webfinger.html.j2 to exist")
+ .render(
+ minijinja::context!(
+ subject => resource,
+ aliases => aliases,
+ properties => properties,
+ links => links,
+ )
+ )
+ .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+
+ {
+ let body = response.body_mut();
+ body.push_str(&html);
+ }
+
+ return Ok(response);
+ },
+ _ => {
+ continue;
+ },
+ }
+ }
+
+ Err(StatusCode::NOT_ACCEPTABLE)
+}
diff --git a/acrate-webfinger/src/webfinger.html.j2 b/acrate-webfinger/src/webfinger.html.j2
new file mode 100644
index 0000000..3c4f191
--- /dev/null
+++ b/acrate-webfinger/src/webfinger.html.j2
@@ -0,0 +1,192 @@
+
+
+
+ {{ subject }} ยท Acrate Webfinger
+
+
+
+
+
+
+ {{ subject }}
+
+ {% if aliases %}
+
+
+
+ Aliases
+
+
+ {% for alias in aliases %}
+ - {{ alias }}
+ {% endfor %}
+
+
+ {% endif %}
+ {% if properties %}
+
+
+
+ Properties
+
+
+ {% for property in properties %}
+ -
+ {{ property[0] }}
+
+ -
+ {{ property[1] }}
+
+ {% endfor %}
+
+
+ {% endif %}
+ {% if links %}
+
+
+
+ Links
+
+
+ {% for link in links %}
+ -
+
+ {% if link[0] != "self" %}
+ {{ link[0] }}
+ {% else %}
+ {{ link[0] }}
+ {% endif %}
+ {% if link[1] is not none %}
+
+ ({{ link[1] }})
+
+ {% endif %}
+
+
+ -
+ {% if link[2] is not none %}
+
+ Link destination
+
+ {{ link[2] }}
+ {% endif %}
+ {% if link[3] is not none %}
+
+ Link template
+
+ {{ link[3] }}
+ {% endif %}
+ {% if link[4] %}
+
+ Link properties
+
+
+ {% for property in link[4] %}
+ -
+ {{ property[0] }}
+
+ -
+ {{ property[1] }}
+
+ {% endfor %}
+
+ {% endif %}
+ {% if link[5] %}
+
+ Link titles
+
+
+ {% for title in link[5] %}
+ -
+ {{ title[0] }}
+
+ -
+ {{ title[1] }}
+
+ {% endfor %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
+ {% endif %}
+
+