Implemented WebFinger flow and fixed incoming WebFinger
ci/woodpecker/push/ociImagePush Pipeline is running
Details
ci/woodpecker/push/ociImagePush Pipeline is running
Details
This commit is contained in:
parent
ffed556107
commit
0dff0c0785
|
@ -1679,6 +1679,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1708,6 +1709,7 @@ name = "magnetar_federation"
|
||||||
version = "0.3.0-alpha"
|
version = "0.3.0-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -1727,6 +1729,8 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use std::borrow::{Borrow, Cow};
|
use std::{
|
||||||
|
borrow::{Borrow, Cow},
|
||||||
|
fmt::Display,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub struct Acct(String);
|
pub struct Acct(String);
|
||||||
|
@ -23,6 +26,12 @@ impl AsRef<str> for Acct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Acct {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "acct:{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Serialize for Acct {
|
impl Serialize for Acct {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
|
|
|
@ -8,10 +8,12 @@ pub mod acct;
|
||||||
|
|
||||||
pub trait ContentType: Serialize {
|
pub trait ContentType: Serialize {
|
||||||
fn mime_type(&self) -> &'static str;
|
fn mime_type(&self) -> &'static str;
|
||||||
|
|
||||||
|
fn alt_mime_types(&self) -> &[&'static str];
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! content_type {
|
macro_rules! content_type {
|
||||||
($visib:vis $typ:ident, $expression:expr) => {
|
($visib:vis $typ:ident, $expression:expr $(,$alt:expr)*) => {
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
||||||
$visib struct $typ;
|
$visib struct $typ;
|
||||||
|
|
||||||
|
@ -31,13 +33,17 @@ macro_rules! content_type {
|
||||||
fn mime_type(&self) -> &'static str {
|
fn mime_type(&self) -> &'static str {
|
||||||
$expression
|
$expression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn alt_mime_types(&self) -> &[&'static str] {
|
||||||
|
&[$($alt),*]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for $typ {
|
impl<'de> Deserialize<'de> for $typ {
|
||||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
let content_type = String::deserialize(deserializer)?;
|
let content_type = String::deserialize(deserializer)?;
|
||||||
|
|
||||||
if matches!(content_type.as_ref(), $expression) {
|
if matches!(content_type.as_ref(), $expression $(| $alt)*) {
|
||||||
Ok(Self)
|
Ok(Self)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::custom(format!(
|
Err(Error::custom(format!(
|
||||||
|
@ -54,7 +60,8 @@ pub mod content_type {
|
||||||
use serde::de::Error;
|
use serde::de::Error;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
content_type!(pub ContentActivityStreams, "application/activity+json");
|
content_type!(pub ContentActivityStreams, "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "application/activity+json");
|
||||||
|
content_type!(pub ContentActivityJson, "application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"");
|
||||||
content_type!(pub ContentHtml, "text/html");
|
content_type!(pub ContentHtml, "text/html");
|
||||||
content_type!(pub ContentJson, "application/json");
|
content_type!(pub ContentJson, "application/json");
|
||||||
content_type!(pub ContentXrdXml, "application/xrd+xml");
|
content_type!(pub ContentXrdXml, "application/xrd+xml");
|
||||||
|
|
|
@ -13,16 +13,21 @@ ext_calckey_model_migration = { path = "./migration" }
|
||||||
magnetar_common = { path = "../magnetar_common" }
|
magnetar_common = { path = "../magnetar_common" }
|
||||||
magnetar_sdk = { path = "../magnetar_sdk" }
|
magnetar_sdk = { path = "../magnetar_sdk" }
|
||||||
|
|
||||||
dotenvy = { workspace = true}
|
dotenvy = { workspace = true }
|
||||||
futures-core = { workspace = true }
|
futures-core = { workspace = true }
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["full"] }
|
tokio = { workspace = true, features = ["full"] }
|
||||||
tokio-util = { workspace = true}
|
tokio-util = { workspace = true }
|
||||||
redis = { workspace = true, features = ["tokio-comp", "json", "serde_json"]}
|
redis = { workspace = true, features = ["tokio-comp", "json", "serde_json"] }
|
||||||
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }
|
sea-orm = { workspace = true, features = [
|
||||||
|
"sqlx-postgres",
|
||||||
|
"runtime-tokio-rustls",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
|
|
@ -8,6 +8,7 @@ pub mod user_model;
|
||||||
pub use ck;
|
pub use ck;
|
||||||
use ck::*;
|
use ck::*;
|
||||||
pub use sea_orm;
|
pub use sea_orm;
|
||||||
|
use url::Host;
|
||||||
use user_model::UserResolver;
|
use user_model::UserResolver;
|
||||||
|
|
||||||
use crate::model_ext::IdShape;
|
use crate::model_ext::IdShape;
|
||||||
|
@ -73,10 +74,10 @@ impl CalckeyModel {
|
||||||
pub async fn get_user_by_tag(
|
pub async fn get_user_by_tag(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
instance: Option<&str>,
|
instance: Option<&Host>,
|
||||||
) -> Result<Option<user::Model>, CalckeyDbError> {
|
) -> Result<Option<user::Model>, CalckeyDbError> {
|
||||||
let name = name.to_lowercase();
|
let name = name.to_lowercase();
|
||||||
let instance = instance.map(str::to_lowercase);
|
let instance = instance.map(Host::to_string);
|
||||||
|
|
||||||
let user = if let Some(instance) = instance {
|
let user = if let Some(instance) = instance {
|
||||||
user::Entity::find()
|
user::Entity::find()
|
||||||
|
|
|
@ -13,6 +13,7 @@ magnetar_common = { path = "../magnetar_common" }
|
||||||
magnetar_host_meta = { path = "../ext_host_meta" }
|
magnetar_host_meta = { path = "../ext_host_meta" }
|
||||||
magnetar_webfinger = { path = "../ext_webfinger" }
|
magnetar_webfinger = { path = "../ext_webfinger" }
|
||||||
|
|
||||||
|
async-trait = { workspace = true }
|
||||||
async-stream = { workspace = true }
|
async-stream = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
futures-core = { workspace = true }
|
futures-core = { workspace = true }
|
||||||
|
@ -33,6 +34,8 @@ percent-encoding = { workspace = true }
|
||||||
reqwest = { workspace = true, features = ["stream"] }
|
reqwest = { workspace = true, features = ["stream"] }
|
||||||
|
|
||||||
tokio = { workspace = true, features = ["full"] }
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
miette = { workspace = true, features = ["fancy"] }
|
miette = { workspace = true, features = ["fancy"] }
|
||||||
|
|
|
@ -1,56 +1,43 @@
|
||||||
use std::{io::Cursor, sync::Arc};
|
use magnetar_common::util::{FediverseTag, ValidName};
|
||||||
|
use magnetar_host_meta::Xrd;
|
||||||
use client::federation_client::{FederationClient, FederationClientError};
|
|
||||||
use magnetar_common::{config::MagnetarNetworkingProtocol, util::FediverseTag};
|
|
||||||
use magnetar_host_meta::{Xrd, XrdXml};
|
|
||||||
use magnetar_webfinger::webfinger::WebFinger;
|
use magnetar_webfinger::webfinger::WebFinger;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::{Host, Url};
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
pub mod lookup_flow;
|
||||||
|
|
||||||
/// The *visible* domain of fediverse handles, that gets resolved by WebFinger
|
/// The *visible* domain of fediverse handles, that gets resolved by WebFinger
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct HostUnmapped(String);
|
pub struct HostUnmapped(Host);
|
||||||
|
|
||||||
|
impl AsRef<Host> for HostUnmapped {
|
||||||
|
fn as_ref(&self) -> &Host {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The real domain of fediverse handles used for federation
|
/// The real domain of fediverse handles used for federation
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct HostMapped(String);
|
pub struct HostMapped(Host);
|
||||||
|
|
||||||
pub trait HostMetaResolverService {
|
impl AsRef<Host> for HostMapped {
|
||||||
|
fn as_ref(&self) -> &Host {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait HostMetaResolverService: Send + Sync {
|
||||||
type Error;
|
type Error;
|
||||||
|
|
||||||
async fn resolve(&self, host: &HostUnmapped) -> Result<Xrd, Self::Error>;
|
async fn resolve(&self, host: &HostUnmapped) -> Result<Xrd, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HostMetaResolverProviderDefault {
|
#[async_trait::async_trait]
|
||||||
client: Arc<FederationClient>,
|
pub trait WebFingerResolverService: Send + Sync {
|
||||||
protocol: MagnetarNetworkingProtocol,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HostMetaResolverService for HostMetaResolverProviderDefault {
|
|
||||||
type Error = FederationClientError;
|
|
||||||
|
|
||||||
async fn resolve(&self, HostUnmapped(host): &HostUnmapped) -> Result<Xrd, Self::Error> {
|
|
||||||
let host_meta_xml = self
|
|
||||||
.client
|
|
||||||
.get(Url::parse(&format!(
|
|
||||||
"{}://{}/.well-known/host-meta",
|
|
||||||
self.protocol.as_ref(),
|
|
||||||
host
|
|
||||||
))?)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let XrdXml::Xrd(xrd) = quick_xml::de::from_reader(Cursor::new(host_meta_xml))?;
|
|
||||||
|
|
||||||
Ok(xrd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait WebFingerResolverService {
|
|
||||||
type Error;
|
type Error;
|
||||||
|
|
||||||
async fn resolve_url(&self, url: &str) -> Result<WebFinger, Self::Error>;
|
async fn resolve_url(&self, url: &str) -> Result<WebFinger, Self::Error>;
|
||||||
|
@ -62,7 +49,7 @@ pub trait WebFingerResolverService {
|
||||||
) -> Result<WebFinger, Self::Error> {
|
) -> Result<WebFinger, Self::Error> {
|
||||||
self.resolve_url(
|
self.resolve_url(
|
||||||
&template_url.replace(
|
&template_url.replace(
|
||||||
"{}",
|
"{uri}",
|
||||||
&percent_encoding::utf8_percent_encode(
|
&percent_encoding::utf8_percent_encode(
|
||||||
resolved_uri,
|
resolved_uri,
|
||||||
percent_encoding::NON_ALPHANUMERIC,
|
percent_encoding::NON_ALPHANUMERIC,
|
||||||
|
@ -74,29 +61,32 @@ pub trait WebFingerResolverService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WebFingerResolverProviderDefault {
|
#[derive(Debug)]
|
||||||
client: Arc<FederationClient>,
|
pub struct UnmappedUser {
|
||||||
|
pub name: ValidName,
|
||||||
|
pub host: HostUnmapped,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebFingerResolverService for WebFingerResolverProviderDefault {
|
impl From<&UnmappedUser> for FediverseTag {
|
||||||
type Error = FederationClientError;
|
fn from(user: &UnmappedUser) -> Self {
|
||||||
|
FediverseTag {
|
||||||
async fn resolve_url(&self, url: &str) -> Result<WebFinger, Self::Error> {
|
name: user.name.clone(),
|
||||||
let host_meta_xml = self.client.get(Url::parse(url)?).send().await?;
|
host: Some(user.host.as_ref().clone()),
|
||||||
let webfinger = serde_json::from_reader(Cursor::new(host_meta_xml))?;
|
}
|
||||||
Ok(webfinger)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MappedUser {
|
pub struct MappedUser {
|
||||||
pub host_meta: Xrd,
|
pub host_meta: Option<Xrd>,
|
||||||
pub webfinger: WebFinger,
|
pub webfinger: WebFinger,
|
||||||
pub tag_mapped: FediverseTag,
|
pub tag_mapped: FediverseTag,
|
||||||
|
pub ap_url: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FederationService {
|
#[async_trait::async_trait]
|
||||||
|
pub trait FederationLookupService {
|
||||||
type Error;
|
type Error;
|
||||||
|
|
||||||
async fn map_fedi_tag(&self, host: &HostUnmapped) -> Result<MappedUser, Self::Error>;
|
async fn map_fedi_tag(&self, user: &UnmappedUser) -> Result<MappedUser, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,217 @@
|
||||||
|
use std::{io::Cursor, sync::Arc};
|
||||||
|
|
||||||
|
use magnetar_common::{
|
||||||
|
config::MagnetarNetworkingProtocol,
|
||||||
|
util::{FediverseTag, FediverseTagDisplay, FediverseTagParseError},
|
||||||
|
};
|
||||||
|
use magnetar_core::web_model::{acct::Acct, content_type::ContentJrdJson};
|
||||||
|
use magnetar_host_meta::{Xrd, XrdXml};
|
||||||
|
use magnetar_webfinger::webfinger::{WebFinger, WebFingerRel, WebFingerSubject};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::trace;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
client::federation_client::{FederationClient, FederationClientError},
|
||||||
|
FederationLookupService, HostMetaResolverService, HostUnmapped, MappedUser, UnmappedUser,
|
||||||
|
WebFingerResolverService,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct HostMetaResolverProviderDefault {
|
||||||
|
client: Arc<FederationClient>,
|
||||||
|
protocol: MagnetarNetworkingProtocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl HostMetaResolverService for HostMetaResolverProviderDefault {
|
||||||
|
type Error = FederationClientError;
|
||||||
|
|
||||||
|
async fn resolve(&self, HostUnmapped(host): &HostUnmapped) -> Result<Xrd, Self::Error> {
|
||||||
|
let host_meta_xml = self
|
||||||
|
.client
|
||||||
|
.get(Url::parse(&format!(
|
||||||
|
"{}://{}/.well-known/host-meta",
|
||||||
|
self.protocol.as_ref(),
|
||||||
|
host
|
||||||
|
))?)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let XrdXml::Xrd(xrd) = quick_xml::de::from_reader(Cursor::new(host_meta_xml))?;
|
||||||
|
|
||||||
|
Ok(xrd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WebFingerResolverProviderDefault {
|
||||||
|
client: Arc<FederationClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl WebFingerResolverService for WebFingerResolverProviderDefault {
|
||||||
|
type Error = FederationClientError;
|
||||||
|
|
||||||
|
async fn resolve_url(&self, url: &str) -> Result<WebFinger, Self::Error> {
|
||||||
|
let host_meta_xml = self
|
||||||
|
.client
|
||||||
|
.get(Url::parse(url)?)
|
||||||
|
.content_type(ContentJrdJson)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let webfinger = serde_json::from_reader(Cursor::new(host_meta_xml))?;
|
||||||
|
Ok(webfinger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum FederationLookupErrror {
|
||||||
|
#[error("Federation client error: {0}")]
|
||||||
|
FederationClientError(#[from] FederationClientError),
|
||||||
|
#[error("Fediverse tag parse error: {0}")]
|
||||||
|
FediverseTagParseError(#[from] FediverseTagParseError),
|
||||||
|
#[error("URL parse error: {0}")]
|
||||||
|
UrlParseError(#[from] url::ParseError),
|
||||||
|
#[error("Missing ActivityStreams URL in WebFinger")]
|
||||||
|
MissingApUrl,
|
||||||
|
#[error("Missing Acct URI in WebFinger")]
|
||||||
|
MissingAcctUri,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FederationLookupServiceProviderDefault {
|
||||||
|
host_meta_resolver: Arc<dyn HostMetaResolverService<Error = FederationClientError>>,
|
||||||
|
webfinger_resolver: Arc<dyn WebFingerResolverService<Error = FederationClientError>>,
|
||||||
|
protocol: MagnetarNetworkingProtocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FederationLookupServiceProviderDefault {
|
||||||
|
pub fn new(
|
||||||
|
host_meta_resolver: Arc<dyn HostMetaResolverService<Error = FederationClientError>>,
|
||||||
|
webfinger_resolver: Arc<dyn WebFingerResolverService<Error = FederationClientError>>,
|
||||||
|
protocol: MagnetarNetworkingProtocol,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
host_meta_resolver,
|
||||||
|
webfinger_resolver,
|
||||||
|
protocol,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl FederationLookupService for FederationLookupServiceProviderDefault {
|
||||||
|
type Error = FederationLookupErrror;
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(self))]
|
||||||
|
async fn map_fedi_tag(
|
||||||
|
&self,
|
||||||
|
user: &UnmappedUser,
|
||||||
|
) -> Result<MappedUser, FederationLookupErrror> {
|
||||||
|
trace!("Fetching flow initiated");
|
||||||
|
let host_meta = self.host_meta_resolver.resolve(&user.host).await;
|
||||||
|
let webfinger_template = match &host_meta {
|
||||||
|
Ok(h) => {
|
||||||
|
trace!("host-meta found: {:?}", h);
|
||||||
|
h.get_webfinger_template().map(str::to_owned)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
trace!("host-meta fetch failed: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
Xrd::default_host_meta(self.protocol.as_ref(), &user.host.as_ref().to_string())
|
||||||
|
.get_webfinger_template()
|
||||||
|
.expect("default WebFinger template")
|
||||||
|
.to_owned()
|
||||||
|
});
|
||||||
|
|
||||||
|
let webfinger = self
|
||||||
|
.webfinger_resolver
|
||||||
|
.resolve(
|
||||||
|
&webfinger_template,
|
||||||
|
Acct::from(FediverseTag::from(user)).as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
trace!("Webfinger fetched: {:?}", webfinger);
|
||||||
|
|
||||||
|
let real_tag = match &webfinger.subject {
|
||||||
|
WebFingerSubject::Acct(acct) => Some(acct.clone()),
|
||||||
|
_ => webfinger
|
||||||
|
.aliases
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.find_map(|alias| match alias {
|
||||||
|
WebFingerSubject::Acct(acct) => Some(acct.clone()),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.ok_or(FederationLookupErrror::MissingAcctUri)?;
|
||||||
|
|
||||||
|
let ap_url = webfinger
|
||||||
|
.links
|
||||||
|
.iter()
|
||||||
|
.find_map(|link| match link {
|
||||||
|
WebFingerRel::RelSelf { href, .. } | WebFingerRel::RelSelfAlt { href, .. } => {
|
||||||
|
Some(href)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.ok_or(FederationLookupErrror::MissingApUrl)?
|
||||||
|
.parse()?;
|
||||||
|
|
||||||
|
Ok(MappedUser {
|
||||||
|
host_meta: host_meta.ok(),
|
||||||
|
webfinger,
|
||||||
|
tag_mapped: FediverseTag::try_from(&FediverseTagDisplay::try_from(&real_tag)?)?,
|
||||||
|
ap_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use magnetar_common::{config::MagnetarNetworkingProtocol, util::FediverseTag};
|
||||||
|
use tracing::{info, Level};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
client::federation_client::FederationClient, FederationLookupService, HostUnmapped,
|
||||||
|
UnmappedUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
FederationLookupServiceProviderDefault, HostMetaResolverProviderDefault,
|
||||||
|
WebFingerResolverProviderDefault,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn should_resolve() {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(Level::TRACE)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let client = Arc::new(FederationClient::new(true, 64000, 20).unwrap());
|
||||||
|
|
||||||
|
let federation_lookup = FederationLookupServiceProviderDefault::new(
|
||||||
|
Arc::new(HostMetaResolverProviderDefault {
|
||||||
|
protocol: MagnetarNetworkingProtocol::Https,
|
||||||
|
client: client.clone(),
|
||||||
|
}),
|
||||||
|
Arc::new(WebFingerResolverProviderDefault { client }),
|
||||||
|
MagnetarNetworkingProtocol::Https,
|
||||||
|
);
|
||||||
|
let tag: FediverseTag = "@natty@astolfo.social".parse().unwrap();
|
||||||
|
info!("Resolving: {}", tag);
|
||||||
|
let resolved = federation_lookup
|
||||||
|
.map_fedi_tag(&UnmappedUser {
|
||||||
|
name: tag.name,
|
||||||
|
host: HostUnmapped(tag.host.unwrap()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
info!("Resolved: {:#?}", resolved);
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,10 @@ impl Xrd {
|
||||||
pub fn get_webfinger_template(&self) -> Option<&str> {
|
pub fn get_webfinger_template(&self) -> Option<&str> {
|
||||||
self.links
|
self.links
|
||||||
.iter()
|
.iter()
|
||||||
.find(|l| l.rel.as_deref() == Some(ContentXrdXml.as_ref()))
|
.find(|l| {
|
||||||
|
l.r#type.as_deref() == Some(ContentXrdXml.as_ref())
|
||||||
|
&& l.rel.as_deref() == Some(RelLrdd.as_ref())
|
||||||
|
})
|
||||||
.and_then(|l| l.template.as_deref())
|
.and_then(|l| l.template.as_deref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use magnetar_core::web_model::acct::Acct;
|
use magnetar_core::web_model::acct::Acct;
|
||||||
use magnetar_core::web_model::content_type::{ContentActivityStreams, ContentHtml};
|
use magnetar_core::web_model::content_type::{
|
||||||
|
ContentActivityJson, ContentActivityStreams, ContentHtml,
|
||||||
|
};
|
||||||
use magnetar_core::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage};
|
use magnetar_core::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct WebFinger {
|
pub struct WebFinger {
|
||||||
|
@ -34,10 +37,18 @@ pub enum WebFingerRel {
|
||||||
content_type: ContentActivityStreams,
|
content_type: ContentActivityStreams,
|
||||||
href: String,
|
href: String,
|
||||||
},
|
},
|
||||||
|
// Alternative content-type for some implementations
|
||||||
|
RelSelfAlt {
|
||||||
|
rel: RelSelf,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
content_type: ContentActivityJson,
|
||||||
|
href: String,
|
||||||
|
},
|
||||||
RelOStatusSubscribe {
|
RelOStatusSubscribe {
|
||||||
rel: RelOStatusSubscribe,
|
rel: RelOStatusSubscribe,
|
||||||
template: String,
|
template: String,
|
||||||
},
|
},
|
||||||
|
Other(Value),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub struct MagnetarNetworking {
|
||||||
pub proxy_remote_files: bool,
|
pub proxy_remote_files: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug, Clone, Copy)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum MagnetarNetworkingProtocol {
|
pub enum MagnetarNetworkingProtocol {
|
||||||
Http,
|
Http,
|
||||||
|
|
|
@ -2,21 +2,166 @@ use magnetar_core::web_model::acct::Acct;
|
||||||
use magnetar_sdk::mmm;
|
use magnetar_sdk::mmm;
|
||||||
use magnetar_sdk::mmm::Token;
|
use magnetar_sdk::mmm::Token;
|
||||||
use percent_encoding::percent_decode_str;
|
use percent_encoding::percent_decode_str;
|
||||||
use std::borrow::Cow;
|
use std::fmt::Display;
|
||||||
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use url::Host;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ValidName(String);
|
||||||
|
|
||||||
|
impl FromStr for ValidName {
|
||||||
|
type Err = FediverseTagParseError;
|
||||||
|
|
||||||
|
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||||
|
if name
|
||||||
|
.chars()
|
||||||
|
.any(|c| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.')
|
||||||
|
{
|
||||||
|
return Err(FediverseTagParseError::InvalidChar(name.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(name.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for ValidName {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ValidName {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct FediverseTag {
|
pub struct FediverseTag {
|
||||||
pub name: String,
|
pub name: ValidName,
|
||||||
|
pub host: Option<Host>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FediverseTag {
|
||||||
|
pub fn from_parts(parts: (&str, Option<&str>)) -> Result<Self, FediverseTagParseError> {
|
||||||
|
Self::try_from(FediverseTagDisplay::from_parts(parts)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<FediverseTagDisplay> for FediverseTag {
|
||||||
|
type Error = FediverseTagParseError;
|
||||||
|
|
||||||
|
fn try_from(
|
||||||
|
FediverseTagDisplay { name, host }: FediverseTagDisplay,
|
||||||
|
) -> Result<Self, FediverseTagParseError> {
|
||||||
|
Ok(FediverseTag {
|
||||||
|
name,
|
||||||
|
host: host.as_deref().map(Host::parse).transpose()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&FediverseTagDisplay> for FediverseTag {
|
||||||
|
type Error = FediverseTagParseError;
|
||||||
|
|
||||||
|
fn try_from(
|
||||||
|
FediverseTagDisplay { name, host }: &FediverseTagDisplay,
|
||||||
|
) -> Result<Self, FediverseTagParseError> {
|
||||||
|
Ok(FediverseTag {
|
||||||
|
name: name.clone(),
|
||||||
|
host: host.as_deref().map(Host::parse).transpose()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&FediverseTag> for Acct {
|
||||||
|
fn from(tag: &FediverseTag) -> Self {
|
||||||
|
Acct::new(tag.to_string().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for FediverseTag {
|
||||||
|
type Err = FediverseTagParseError;
|
||||||
|
|
||||||
|
fn from_str(tag: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::try_from(FediverseTagDisplay::from_str(tag)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FediverseTag {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(ref host) = self.host {
|
||||||
|
write!(f, "{}@{}", self.name, host)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FediverseTagDisplay {
|
||||||
|
pub name: ValidName,
|
||||||
pub host: Option<String>,
|
pub host: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FediverseTagDisplay {
|
||||||
|
pub fn from_parts((name, host): (&str, Option<&str>)) -> Result<Self, FediverseTagParseError> {
|
||||||
|
Ok(FediverseTagDisplay {
|
||||||
|
name: name.parse()?,
|
||||||
|
host: host.map(str::to_owned),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&FediverseTag> for FediverseTagDisplay {
|
||||||
|
fn from(FediverseTag { name, host }: &FediverseTag) -> Self {
|
||||||
|
FediverseTagDisplay {
|
||||||
|
name: name.clone(),
|
||||||
|
host: host
|
||||||
|
.as_ref()
|
||||||
|
.map(Host::to_string)
|
||||||
|
.as_deref()
|
||||||
|
.map(idna::domain_to_unicode)
|
||||||
|
.map(|v| v.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Acct> for FediverseTagDisplay {
|
||||||
|
type Error = FediverseTagParseError;
|
||||||
|
|
||||||
|
fn try_from(acct: &Acct) -> Result<Self, Self::Error> {
|
||||||
|
acct.as_ref().parse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for FediverseTagDisplay {
|
||||||
|
type Err = FediverseTagParseError;
|
||||||
|
|
||||||
|
fn from_str(tag: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::from_parts(split_tag_inner(tag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FediverseTagDisplay {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(ref host) = self.host {
|
||||||
|
write!(f, "{}@{}", self.name, host)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum FediverseTagParseError {
|
pub enum FediverseTagParseError {
|
||||||
#[error("Invalid char in tag: {0}")]
|
#[error("Invalid char in tag: {0}")]
|
||||||
InvalidChar(String),
|
InvalidChar(String),
|
||||||
#[error("Invalid UTF-8 in tag: {0}")]
|
#[error("Invalid UTF-8 in tag: {0}")]
|
||||||
InvalidUtf8(#[from] std::str::Utf8Error),
|
InvalidUtf8(#[from] std::str::Utf8Error),
|
||||||
|
#[error("Invalid host in tag: {0}")]
|
||||||
|
InvalidHost(#[from] url::ParseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&FediverseTagParseError> for &str {
|
impl From<&FediverseTagParseError> for &str {
|
||||||
|
@ -25,94 +170,30 @@ impl From<&FediverseTagParseError> for &str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S1: AsRef<str>, S2: AsRef<str>> From<(S1, Option<S2>)> for FediverseTag {
|
|
||||||
fn from((name, host): (S1, Option<S2>)) -> Self {
|
|
||||||
Self {
|
|
||||||
name: name.as_ref().to_owned(),
|
|
||||||
host: host.as_ref().map(S2::as_ref).map(str::to_owned),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<FediverseTag> for Acct {
|
impl From<FediverseTag> for Acct {
|
||||||
fn from(value: FediverseTag) -> Self {
|
fn from(value: FediverseTag) -> Self {
|
||||||
value.to_string().into()
|
value.to_string().into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for FediverseTag {
|
fn split_tag_inner(tag: &str) -> (&str, Option<&str>) {
|
||||||
fn to_string(&self) -> String {
|
|
||||||
if let Some(ref host) = self.host {
|
|
||||||
format!("{}@{host}", self.name)
|
|
||||||
} else {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lenient_parse_acct(acct: &Acct) -> Result<FediverseTag, FediverseTagParseError> {
|
|
||||||
lenient_parse_tag(acct.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_tag_inner(tag: impl AsRef<str>) -> (String, Option<String>) {
|
|
||||||
let tag = tag.as_ref();
|
|
||||||
let tag = tag.strip_prefix('@').unwrap_or(tag.as_ref());
|
let tag = tag.strip_prefix('@').unwrap_or(tag.as_ref());
|
||||||
|
|
||||||
match tag.split_once('@') {
|
match tag.split_once('@') {
|
||||||
Some((name, "")) => (name.to_owned(), None),
|
Some((name, "")) => (name, None),
|
||||||
Some((name, host)) => (name.to_owned(), Some(host.to_owned())),
|
Some((name, host)) => (name, Some(host)),
|
||||||
None => (tag.to_owned(), None),
|
None => (tag, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_tag_inner((name, host): (&str, Option<&str>)) -> Result<(), FediverseTagParseError> {
|
|
||||||
if name
|
|
||||||
.chars()
|
|
||||||
.any(|c| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.')
|
|
||||||
{
|
|
||||||
return Err(FediverseTagParseError::InvalidChar(name.to_owned()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(host_str) = host {
|
|
||||||
if host_str
|
|
||||||
.chars()
|
|
||||||
.any(|c| c.is_control() || c.is_whitespace() || c == '/' || c == '#')
|
|
||||||
{
|
|
||||||
return Err(FediverseTagParseError::InvalidChar(name.to_owned()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lenient_parse_tag(tag: impl AsRef<str>) -> Result<FediverseTag, FediverseTagParseError> {
|
|
||||||
let (name, host) = split_tag_inner(tag);
|
|
||||||
|
|
||||||
validate_tag_inner((&name, host.as_ref().map(String::as_ref)))?;
|
|
||||||
|
|
||||||
Ok(FediverseTag { name, host })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lenient_parse_acct_decode(acct: &Acct) -> Result<FediverseTag, FediverseTagParseError> {
|
|
||||||
lenient_parse_tag_decode(acct.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lenient_parse_tag_decode(
|
pub fn lenient_parse_tag_decode(
|
||||||
tag: impl AsRef<str>,
|
tag: impl AsRef<str>,
|
||||||
) -> Result<FediverseTag, FediverseTagParseError> {
|
) -> Result<FediverseTag, FediverseTagParseError> {
|
||||||
let (name, host) = split_tag_inner(tag);
|
percent_decode_str(tag.as_ref()).decode_utf8()?.parse()
|
||||||
|
}
|
||||||
|
|
||||||
let name_decoded = percent_decode_str(&name).decode_utf8()?;
|
pub fn lenient_parse_acct_decode(acct: &Acct) -> Result<FediverseTag, FediverseTagParseError> {
|
||||||
let host_decoded = host
|
lenient_parse_tag_decode(acct.as_ref())
|
||||||
.map(|host| percent_decode_str(&host).decode_utf8().map(Cow::into_owned))
|
|
||||||
.transpose()?;
|
|
||||||
|
|
||||||
validate_tag_inner((&name_decoded, host_decoded.as_deref()))?;
|
|
||||||
|
|
||||||
Ok(FediverseTag {
|
|
||||||
name: name_decoded.into_owned(),
|
|
||||||
host: host_decoded,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -79,14 +79,14 @@ pub async fn handle_user_info_by_acct(
|
||||||
MaybeUser(self_user): MaybeUser,
|
MaybeUser(self_user): MaybeUser,
|
||||||
) -> Result<Json<Res<GetUserByAcct>>, ApiError> {
|
) -> Result<Json<Res<GetUserByAcct>>, ApiError> {
|
||||||
let mut tag = lenient_parse_tag_decode(&tag_str)?;
|
let mut tag = lenient_parse_tag_decode(&tag_str)?;
|
||||||
if matches!(&tag.host, Some(host) if host == &service.config.networking.host) {
|
if matches!(&tag.host, Some(host) if host.to_string() == service.config.networking.host) {
|
||||||
tag.host = None;
|
tag.host = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctx = PackingContext::new(service.clone(), self_user).await?;
|
let ctx = PackingContext::new(service.clone(), self_user).await?;
|
||||||
let user_model = service
|
let user_model = service
|
||||||
.db
|
.db
|
||||||
.get_user_by_tag(&tag.name, tag.host.as_deref())
|
.get_user_by_tag(tag.name.as_ref(), tag.host.as_ref())
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ObjectNotFound(tag_str))?;
|
.ok_or(ObjectNotFound(tag_str))?;
|
||||||
|
|
||||||
|
|
154
src/webfinger.rs
154
src/webfinger.rs
|
@ -7,11 +7,14 @@ use magnetar_calckey_model::CalckeyModel;
|
||||||
use magnetar_common::config::MagnetarConfig;
|
use magnetar_common::config::MagnetarConfig;
|
||||||
use magnetar_common::util::{lenient_parse_acct_decode, FediverseTag};
|
use magnetar_common::util::{lenient_parse_acct_decode, FediverseTag};
|
||||||
use magnetar_core::web_model::acct::Acct;
|
use magnetar_core::web_model::acct::Acct;
|
||||||
use magnetar_core::web_model::content_type::{ContentActivityStreams, ContentHtml, ContentJrdJson};
|
use magnetar_core::web_model::content_type::{
|
||||||
|
ContentActivityJson, ContentActivityStreams, ContentHtml, ContentJrdJson,
|
||||||
|
};
|
||||||
use magnetar_core::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage};
|
use magnetar_core::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage};
|
||||||
use magnetar_webfinger::webfinger::{WebFinger, WebFingerRel, WebFingerSubject};
|
use magnetar_webfinger::webfinger::{WebFinger, WebFingerRel, WebFingerSubject};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct WebFingerQuery {
|
pub struct WebFingerQuery {
|
||||||
|
@ -19,11 +22,13 @@ pub struct WebFingerQuery {
|
||||||
rel: Option<Vec<String>>,
|
rel: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: We don't have this endpoint in Magnetar yet, but make sure it's not hardcoded when
|
||||||
|
// we do
|
||||||
|
const USER_AP_ENDPOINT: &str = "/users/";
|
||||||
|
|
||||||
// TODO: Filter by rel
|
// TODO: Filter by rel
|
||||||
pub async fn handle_webfinger(
|
pub async fn handle_webfinger(
|
||||||
Query(WebFingerQuery {
|
Query(WebFingerQuery { resource, .. }): Query<WebFingerQuery>,
|
||||||
resource, rel: _, ..
|
|
||||||
}): Query<WebFingerQuery>,
|
|
||||||
State((config, ck)): State<(&'static MagnetarConfig, CalckeyModel)>,
|
State((config, ck)): State<(&'static MagnetarConfig, CalckeyModel)>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, StatusCode> {
|
||||||
let resource = match resource {
|
let resource = match resource {
|
||||||
|
@ -42,79 +47,104 @@ pub async fn handle_webfinger(
|
||||||
StatusCode::UNPROCESSABLE_ENTITY
|
StatusCode::UNPROCESSABLE_ENTITY
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
ck.get_user_by_tag(
|
ck.get_user_by_tag(tag.name.as_ref(), tag.host.as_ref())
|
||||||
&tag.name,
|
.await
|
||||||
tag.host
|
.map_err(|e| {
|
||||||
.filter(|host| *host != config.networking.host)
|
error!("Data error: {e}");
|
||||||
.as_deref(),
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
)
|
})?
|
||||||
.await
|
}
|
||||||
.map_err(|e| {
|
WebFingerSubject::Url(url) => {
|
||||||
error!("Data error: {e}");
|
let object_url = url.parse::<Url>().map_err(|e| {
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
error!("URL parse error: {e}");
|
||||||
})?
|
StatusCode::UNPROCESSABLE_ENTITY
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// FIXME: Jank
|
||||||
|
let path = object_url.path().strip_prefix(USER_AP_ENDPOINT);
|
||||||
|
match path {
|
||||||
|
Some(user_id) if !user_id.is_empty() && user_id.chars().all(|c| c != '/') => {
|
||||||
|
ck.get_user_by_id(user_id).await.map_err(|e| {
|
||||||
|
error!("Data error: {e}");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
_ => ck.get_user_by_uri(&url).await.map_err(|e| {
|
||||||
|
error!("Data error: {e}");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TODO: Make this work for local users
|
|
||||||
WebFingerSubject::Url(url) => ck.get_user_by_uri(&url).await.map_err(|e| {
|
|
||||||
error!("Data error: {e}");
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if user.is_none() {
|
let Some(user) = user else {
|
||||||
return Err(StatusCode::NOT_FOUND);
|
return Err(StatusCode::NOT_FOUND);
|
||||||
}
|
};
|
||||||
|
|
||||||
let user = user.unwrap();
|
let tag = FediverseTag::from_parts((
|
||||||
let tag = FediverseTag::from((
|
|
||||||
&user.username,
|
&user.username,
|
||||||
user.host.as_ref().or(Some(&config.networking.host)),
|
user.host.as_deref().or(Some(&config.networking.host)),
|
||||||
));
|
))
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("URL parse error: {e}");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut links = Vec::new();
|
let mut links = Vec::new();
|
||||||
let mut aliases = Vec::new();
|
let mut aliases = Vec::new();
|
||||||
|
|
||||||
match tag.host {
|
if user.host.is_some() {
|
||||||
Some(ref host) if host != &config.networking.host => {
|
if let Some(uri) = user.uri {
|
||||||
if let Some(uri) = user.uri {
|
|
||||||
links.push(WebFingerRel::RelSelf {
|
|
||||||
rel: RelSelf,
|
|
||||||
content_type: ContentActivityStreams,
|
|
||||||
href: uri,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
links.push(WebFingerRel::RelOStatusSubscribe {
|
|
||||||
rel: RelOStatusSubscribe,
|
|
||||||
template: format!(
|
|
||||||
"{}://{}/authorize-follow?acct={{uri}}",
|
|
||||||
config.networking.protocol, config.networking.host
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
let user_url = format!(
|
|
||||||
"{}://{}/@{}",
|
|
||||||
config.networking.protocol, config.networking.host, tag.name
|
|
||||||
);
|
|
||||||
|
|
||||||
links.push(WebFingerRel::RelWebFingerProfilePage {
|
|
||||||
rel: RelWebFingerProfilePage,
|
|
||||||
content_type: ContentHtml,
|
|
||||||
href: user_url.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
aliases.push(WebFingerSubject::Url(user_url));
|
|
||||||
|
|
||||||
links.push(WebFingerRel::RelSelf {
|
links.push(WebFingerRel::RelSelf {
|
||||||
rel: RelSelf,
|
rel: RelSelf,
|
||||||
content_type: ContentActivityStreams,
|
content_type: ContentActivityStreams,
|
||||||
href: format!(
|
href: uri.clone(),
|
||||||
"{}://{}/users/{}",
|
});
|
||||||
config.networking.protocol, config.networking.host, user.id
|
|
||||||
),
|
links.push(WebFingerRel::RelSelfAlt {
|
||||||
|
rel: RelSelf,
|
||||||
|
content_type: ContentActivityJson,
|
||||||
|
href: uri,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
links.push(WebFingerRel::RelOStatusSubscribe {
|
||||||
|
rel: RelOStatusSubscribe,
|
||||||
|
template: format!(
|
||||||
|
"{}://{}/authorize-follow?acct={{uri}}",
|
||||||
|
config.networking.protocol, config.networking.host
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
let user_url = format!(
|
||||||
|
"{}://{}/@{}",
|
||||||
|
config.networking.protocol, config.networking.host, tag.name
|
||||||
|
);
|
||||||
|
|
||||||
|
links.push(WebFingerRel::RelWebFingerProfilePage {
|
||||||
|
rel: RelWebFingerProfilePage,
|
||||||
|
content_type: ContentHtml,
|
||||||
|
href: user_url.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
aliases.push(WebFingerSubject::Url(user_url));
|
||||||
|
|
||||||
|
let self_url = format!(
|
||||||
|
"{}://{}{}{}",
|
||||||
|
config.networking.protocol, config.networking.host, USER_AP_ENDPOINT, user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
links.push(WebFingerRel::RelSelf {
|
||||||
|
rel: RelSelf,
|
||||||
|
content_type: ContentActivityStreams,
|
||||||
|
href: self_url.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
links.push(WebFingerRel::RelSelfAlt {
|
||||||
|
rel: RelSelf,
|
||||||
|
content_type: ContentActivityJson,
|
||||||
|
href: self_url,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
|
Loading…
Reference in New Issue