commit
0e7aacdd0e
20 changed files with 916 additions and 17 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -15,3 +15,6 @@ Cargo.lock
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# IntelliJ IDEA thing, mostly broken versioning
|
||||||
|
.idea/misc.xml
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
<sourceFolder url="file://$MODULE_DIR$/acrate-inbox/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/acrate-inbox/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/tests" isTestSource="true" />
|
<sourceFolder url="file://$MODULE_DIR$/acrate-nodeinfo/tests" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/acrate-webfinger/src" isTestSource="false" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
|
12
.idea/dataSources.xml
Normal file
12
.idea/dataSources.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="acrate" uuid="8258414e-095d-430b-a0a5-b48e72af23a9">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql:///acrate</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DiscordProjectSettings">
|
|
||||||
<option name="show" value="ASK" />
|
|
||||||
<option name="description" value="" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
6
.idea/sqldialects.xml
Normal file
6
.idea/sqldialects.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/acrate-core/migrations/2024-11-14-031744_meta/up.sql" dialect="GenericSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -1,3 +1,3 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["acrate-core", "acrate-hostmeta", "acrate-inbox", "acrate-nodeinfo"]
|
members = ["acrate-core", "acrate-hostmeta", "acrate-inbox", "acrate-nodeinfo", "acrate-webfinger"]
|
||||||
|
|
12
README.md
12
README.md
|
@ -11,3 +11,15 @@ Federation database
|
||||||
> [!Caution]
|
> [!Caution]
|
||||||
>
|
>
|
||||||
> This software suite is in active development!
|
> 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
|
||||||
|
|
|
@ -4,10 +4,10 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
diesel_migrations = "2.2.0"
|
||||||
acrate-hostmeta = { path = "../acrate-hostmeta" }
|
uuid = "1.11.0"
|
||||||
acrate-nodeinfo = { path = "../acrate-nodeinfo" }
|
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
tabs-in-doc-comments = "allow"
|
tabs-in-doc-comments = "allow"
|
||||||
|
|
6
acrate-core/migrations/2024-11-14-031744_meta/down.sql
Normal file
6
acrate-core/migrations/2024-11-14-031744_meta/down.sql
Normal file
|
@ -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;
|
63
acrate-core/migrations/2024-11-14-031744_meta/up.sql
Normal file
63
acrate-core/migrations/2024-11-14-031744_meta/up.sql
Normal file
|
@ -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)
|
||||||
|
);
|
|
@ -1,4 +1,8 @@
|
||||||
//! Core crate of the `acrate` project.
|
//! Core crate of the `acrate` project.
|
||||||
|
|
||||||
pub use acrate_nodeinfo as nodeinfo;
|
mod schema;
|
||||||
pub use acrate_hostmeta as hostmeta;
|
|
||||||
|
pub mod meta;
|
||||||
|
|
||||||
|
pub use diesel;
|
||||||
|
pub use diesel_async;
|
||||||
|
|
165
acrate-core/src/meta.rs
Normal file
165
acrate-core/src/meta.rs
Normal file
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
pub href: Option<String>,
|
||||||
|
pub template: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetaSubject {
|
||||||
|
pub async fn query_matching(conn: &mut AsyncPgConnection, doc: &str, subject: &str) -> QueryResult<Vec<Self>> {
|
||||||
|
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::<diesel::sql_types::Text>().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<Vec<Self>> {
|
||||||
|
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::<diesel::sql_types::Text>().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<Vec<Self>> {
|
||||||
|
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::<diesel::sql_types::Text>().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<Vec<Self>> {
|
||||||
|
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<Vec<Self>> {
|
||||||
|
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<Vec<Self>> {
|
||||||
|
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::<diesel::sql_types::Text>().ilike(pattern);
|
||||||
|
|
||||||
|
meta_properties
|
||||||
|
.filter(document_is_equal)
|
||||||
|
.filter(subject_matches_pattern)
|
||||||
|
.select(Self::as_select())
|
||||||
|
.load(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
72
acrate-core/src/schema.rs
Normal file
72
acrate-core/src/schema.rs
Normal file
|
@ -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<Bpchar>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Bpchar>,
|
||||||
|
href -> Nullable<Bpchar>,
|
||||||
|
template -> Nullable<Bpchar>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
meta_properties (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
document -> Bpchar,
|
||||||
|
pattern -> Bpchar,
|
||||||
|
rel -> Bpchar,
|
||||||
|
value -> Nullable<Bpchar>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
meta_subjects (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
document -> Bpchar,
|
||||||
|
pattern -> Bpchar,
|
||||||
|
redirect -> Nullable<Bpchar>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
|
@ -17,6 +17,7 @@ pub struct ResourceDescriptorJRD {
|
||||||
///
|
///
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1>
|
||||||
///
|
///
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub subject: Option<String>,
|
pub subject: Option<String>,
|
||||||
|
|
||||||
/// Other names the resource described by this document can be referred to.
|
/// Other names the resource described by this document can be referred to.
|
||||||
|
@ -70,6 +71,7 @@ pub struct ResourceDescriptorLinkJRD {
|
||||||
///
|
///
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2>
|
||||||
///
|
///
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub r#type: Option<String>,
|
pub r#type: Option<String>,
|
||||||
|
|
||||||
/// URI to the resource put in relation.
|
/// URI to the resource put in relation.
|
||||||
|
@ -78,6 +80,7 @@ pub struct ResourceDescriptorLinkJRD {
|
||||||
///
|
///
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3>
|
||||||
///
|
///
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub href: Option<String>,
|
pub href: Option<String>,
|
||||||
|
|
||||||
/// Titles of the resource put in relation in various languages.
|
/// Titles of the resource put in relation in various languages.
|
||||||
|
@ -104,6 +107,7 @@ pub struct ResourceDescriptorLinkJRD {
|
||||||
///
|
///
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-4.2>
|
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-4.2>
|
||||||
///
|
///
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub template: Option<String>,
|
pub template: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +223,7 @@ impl From<ResourceDescriptorLinkXRD> for ResourceDescriptorLinkJRD {
|
||||||
|
|
||||||
impl From<ResourceDescriptorPropertyXRD> for (String, Option<String>) {
|
impl From<ResourceDescriptorPropertyXRD> for (String, Option<String>) {
|
||||||
fn from(value: ResourceDescriptorPropertyXRD) -> Self {
|
fn from(value: ResourceDescriptorPropertyXRD) -> Self {
|
||||||
(value.r#type, value.value)
|
(value.rel, value.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ use crate::jrd::{ResourceDescriptorJRD, ResourceDescriptorLinkJRD};
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-3>
|
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-3>
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename = "XRD")]
|
||||||
pub struct ResourceDescriptorXRD {
|
pub struct ResourceDescriptorXRD {
|
||||||
/// The resource this document refers to.
|
/// The resource this document refers to.
|
||||||
///
|
///
|
||||||
|
@ -17,6 +18,7 @@ pub struct ResourceDescriptorXRD {
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1>
|
||||||
///
|
///
|
||||||
#[serde(rename = "Subject")]
|
#[serde(rename = "Subject")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub subject: Option<String>,
|
pub subject: Option<String>,
|
||||||
|
|
||||||
/// Other names the resource described by this document can be referred to.
|
/// Other names the resource described by this document can be referred to.
|
||||||
|
@ -58,6 +60,7 @@ pub struct ResourceDescriptorXRD {
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1>
|
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1>
|
||||||
///
|
///
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename = "Link")]
|
||||||
pub struct ResourceDescriptorLinkXRD {
|
pub struct ResourceDescriptorLinkXRD {
|
||||||
/// The kind of relation established by the subject with the attached resource.
|
/// The kind of relation established by the subject with the attached resource.
|
||||||
///
|
///
|
||||||
|
@ -75,6 +78,7 @@ pub struct ResourceDescriptorLinkXRD {
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2>
|
||||||
///
|
///
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub r#type: Option<String>,
|
pub r#type: Option<String>,
|
||||||
|
|
||||||
/// URI to the resource put in relation.
|
/// URI to the resource put in relation.
|
||||||
|
@ -84,6 +88,7 @@ pub struct ResourceDescriptorLinkXRD {
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3>
|
||||||
///
|
///
|
||||||
#[serde(rename = "@href")]
|
#[serde(rename = "@href")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub href: Option<String>,
|
pub href: Option<String>,
|
||||||
|
|
||||||
/// Titles of the resource put in relation in various languages.
|
/// Titles of the resource put in relation in various languages.
|
||||||
|
@ -93,6 +98,7 @@ pub struct ResourceDescriptorLinkXRD {
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4>
|
||||||
///
|
///
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(rename = "Title")]
|
||||||
pub titles: Vec<ResourceDescriptorTitleXRD>,
|
pub titles: Vec<ResourceDescriptorTitleXRD>,
|
||||||
|
|
||||||
/// Additional information about the resource put in relation.
|
/// Additional information about the resource put in relation.
|
||||||
|
@ -102,6 +108,7 @@ pub struct ResourceDescriptorLinkXRD {
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.5>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.5>
|
||||||
///
|
///
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde(rename = "Property")]
|
||||||
pub properties: Vec<ResourceDescriptorPropertyXRD>,
|
pub properties: Vec<ResourceDescriptorPropertyXRD>,
|
||||||
|
|
||||||
/// Template to fill to get the URL to resource-specific information.
|
/// Template to fill to get the URL to resource-specific information.
|
||||||
|
@ -111,6 +118,7 @@ pub struct ResourceDescriptorLinkXRD {
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-4.2>
|
/// - <https://datatracker.ietf.org/doc/html/rfc6415#section-4.2>
|
||||||
///
|
///
|
||||||
#[serde(rename = "@template")]
|
#[serde(rename = "@template")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub template: Option<String>,
|
pub template: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,22 +128,26 @@ pub struct ResourceDescriptorLinkXRD {
|
||||||
///
|
///
|
||||||
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4>
|
/// - <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4>
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename = "Title")]
|
||||||
pub struct ResourceDescriptorTitleXRD {
|
pub struct ResourceDescriptorTitleXRD {
|
||||||
/// The language of the title.
|
/// The language of the title.
|
||||||
#[serde(rename = "@lang")]
|
#[serde(rename = "@lang")]
|
||||||
pub language: String,
|
pub language: String,
|
||||||
|
|
||||||
/// The title itself.
|
/// The title itself.
|
||||||
|
#[serde(rename = "$text")]
|
||||||
pub value: String,
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename = "Property")]
|
||||||
pub struct ResourceDescriptorPropertyXRD {
|
pub struct ResourceDescriptorPropertyXRD {
|
||||||
/// The property identifier, or type.
|
/// The property identifier, or type.
|
||||||
#[serde(alias = "@type")]
|
#[serde(rename = "@rel")]
|
||||||
pub r#type: String,
|
pub rel: String,
|
||||||
|
|
||||||
/// The property value.
|
/// The property value.
|
||||||
|
#[serde(rename = "$text")]
|
||||||
pub value: Option<String>,
|
pub value: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,7 +246,7 @@ impl From<ResourceDescriptorJRD> for ResourceDescriptorXRD {
|
||||||
impl From<(String, Option<String>)> for ResourceDescriptorPropertyXRD {
|
impl From<(String, Option<String>)> for ResourceDescriptorPropertyXRD {
|
||||||
fn from(value: (String, Option<String>)) -> Self {
|
fn from(value: (String, Option<String>)) -> Self {
|
||||||
Self {
|
Self {
|
||||||
r#type: value.0,
|
rel: value.0,
|
||||||
value: value.1,
|
value: value.1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
acrate-webfinger/Cargo.toml
Normal file
19
acrate-webfinger/Cargo.toml
Normal file
|
@ -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"] }
|
4
acrate-webfinger/src/config.rs
Normal file
4
acrate-webfinger/src/config.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
micronfig::config!(
|
||||||
|
ACRATE_WEBFINGER_DATABASE_URL: String,
|
||||||
|
ACRATE_WEBFINGER_BIND_ADDRESS: String,
|
||||||
|
);
|
41
acrate-webfinger/src/main.rs
Normal file
41
acrate-webfinger/src/main.rs
Normal file
|
@ -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<std::convert::Infallible> {
|
||||||
|
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");
|
||||||
|
}
|
290
acrate-webfinger/src/route.rs
Normal file
290
acrate-webfinger/src/route.rs
Normal file
|
@ -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<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub rel: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const WEBFINGER_DOC: &str = "/.well-known/webfinger";
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
|
pub async fn webfinger_handler(
|
||||||
|
Query(WebfingerQuery {resource, rel}): Query<WebfingerQuery>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(mj): Extension<Arc<minijinja::Environment<'static>>>,
|
||||||
|
) -> Result<Response<String>, 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<MetaLinkProperty>, Vec<MetaLinkTitle>)> = 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::<Vec<ResourceDescriptorLinkJRD>>();
|
||||||
|
|
||||||
|
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<ResourceDescriptorPropertyXRD> = 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::<Vec<ResourceDescriptorLinkXRD>>();
|
||||||
|
|
||||||
|
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#"<?xml version="1.0" encoding="UTF-8"?>"#);
|
||||||
|
body.push_str(&xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
},
|
||||||
|
"text/html" => {
|
||||||
|
let aliases: Vec<String> = aliases.into_iter()
|
||||||
|
.map(|alias| alias.alias)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let properties: Vec<(String, Option<String>)> = properties.into_iter()
|
||||||
|
.map(|prop| {
|
||||||
|
(prop.rel, prop.value)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let links: Vec<(String, Option<String>, Option<String>, Option<String>, Vec<(String, Option<String>)>, 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::<Vec<(String, Option<String>)>>(),
|
||||||
|
titles.into_iter()
|
||||||
|
.map(|title| (title.language, title.value))
|
||||||
|
.collect::<Vec<(String, String)>>()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.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)
|
||||||
|
}
|
192
acrate-webfinger/src/webfinger.html.j2
Normal file
192
acrate-webfinger/src/webfinger.html.j2
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{ subject }} · Acrate Webfinger</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--wf-yellow: #e2bb03;
|
||||||
|
--wf-brown: #3b351d;
|
||||||
|
--link-color: #0094b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 8px -8px;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
small span {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen {
|
||||||
|
main {
|
||||||
|
box-shadow: 4px 4px 10px currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: var(--wf-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
background-color: var(--wf-brown);
|
||||||
|
}
|
||||||
|
|
||||||
|
main, hr {
|
||||||
|
border-color: var(--wf-yellow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (prefers-color-scheme: light) {
|
||||||
|
body {
|
||||||
|
background-color: var(--wf-yellow);
|
||||||
|
color: var(--wf-brown);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
main, hr {
|
||||||
|
border-color: var(--wf-brown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>
|
||||||
|
Acrate Webfinger
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<h2 id="section-subject">
|
||||||
|
<span id="subject">{{ subject }}</span>
|
||||||
|
</h2>
|
||||||
|
{% if aliases %}
|
||||||
|
<hr>
|
||||||
|
<section id="section-aliases">
|
||||||
|
<h3>
|
||||||
|
Aliases
|
||||||
|
</h3>
|
||||||
|
<ul>
|
||||||
|
{% for alias in aliases %}
|
||||||
|
<li><a href="{{ alias }}">{{ alias }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if properties %}
|
||||||
|
<hr>
|
||||||
|
<section id="section-properties">
|
||||||
|
<h3>
|
||||||
|
Properties
|
||||||
|
</h3>
|
||||||
|
<dl>
|
||||||
|
{% for property in properties %}
|
||||||
|
<dt>
|
||||||
|
<a href="{{ property[0] }}">{{ property[0] }}</a>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<span>{{ property[1] }}</span>
|
||||||
|
</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% if links %}
|
||||||
|
<hr>
|
||||||
|
<section id="section-links">
|
||||||
|
<h3>
|
||||||
|
Links
|
||||||
|
</h3>
|
||||||
|
<dl>
|
||||||
|
{% for link in links %}
|
||||||
|
<dt>
|
||||||
|
<h4>
|
||||||
|
{% if link[0] != "self" %}
|
||||||
|
<a href="{{ link[0] }}">{{ link[0] }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span>{{ link[0] }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if link[1] is not none %}
|
||||||
|
<small>
|
||||||
|
(<span>{{ link[1] }}</span>)
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{% if link[2] is not none %}
|
||||||
|
<h5>
|
||||||
|
Link destination
|
||||||
|
</h5>
|
||||||
|
<a href="{{ link[2] }}">{{ link[2] }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if link[3] is not none %}
|
||||||
|
<h5>
|
||||||
|
Link template
|
||||||
|
</h5>
|
||||||
|
<span>{{ link[3] }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if link[4] %}
|
||||||
|
<h5>
|
||||||
|
Link properties
|
||||||
|
</h5>
|
||||||
|
<dl>
|
||||||
|
{% for property in link[4] %}
|
||||||
|
<dt>
|
||||||
|
<a href="{{ property[0] }}">{{ property[0] }}</a>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<span>{{ property[1] }}</span>
|
||||||
|
</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
{% endif %}
|
||||||
|
{% if link[5] %}
|
||||||
|
<h5>
|
||||||
|
Link titles
|
||||||
|
</h5>
|
||||||
|
<dl>
|
||||||
|
{% for title in link[5] %}
|
||||||
|
<dt>
|
||||||
|
<span>{{ title[0] }}</span>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<span>{{ title[1] }}</span>
|
||||||
|
</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</body>
|
Loading…
Reference in a new issue