actor: implement LanguageRelay

Closes Github issue #1
This commit is contained in:
Astro 2023-11-26 19:56:46 +01:00
parent c823236a3c
commit 2c625af1f8
3 changed files with 103 additions and 6 deletions

View file

@ -5,9 +5,11 @@ use sigh::{PublicKey, Key};
use crate::activitypub; use crate::activitypub;
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[allow(clippy::enum_variant_names)]
pub enum ActorKind { pub enum ActorKind {
TagRelay(String), TagRelay(String),
InstanceRelay(String), InstanceRelay(String),
LanguageRelay(String),
} }
impl ActorKind { impl ActorKind {
@ -17,6 +19,18 @@ impl ActorKind {
.replace(char::is_whitespace, ""); .replace(char::is_whitespace, "");
ActorKind::TagRelay(tag) ActorKind::TagRelay(tag)
} }
pub fn from_language(language: &str) -> Option<Self> {
let language = language.to_lowercase()
.chars()
.take_while(|c| c.is_alphabetic())
.collect::<String>();
if language.is_empty() {
None
} else {
Some(ActorKind::LanguageRelay(language))
}
}
} }
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
@ -32,12 +46,17 @@ impl Actor {
if uri.starts_with("acct:tag-") { if uri.starts_with("acct:tag-") {
let off = "acct:tag-".len(); let off = "acct:tag-".len();
let Some(at) = uri.find('@') else { return None; }; let Some(at) = uri.find('@') else { return None; };
kind = ActorKind::TagRelay(uri[off..at].to_string()); kind = ActorKind::from_tag(&uri[off..at]);
host = Arc::new(uri[at + 1..].to_string()); host = Arc::new(uri[at + 1..].to_string());
} else if uri.starts_with("acct:instance-") { } else if uri.starts_with("acct:instance-") {
let off = "acct:instance-".len(); let off = "acct:instance-".len();
let Some(at) = uri.find('@') else { return None; }; let Some(at) = uri.find('@') else { return None; };
kind = ActorKind::InstanceRelay(uri[off..at].to_string()); kind = ActorKind::InstanceRelay(uri[off..at].to_lowercase());
host = Arc::new(uri[at + 1..].to_string());
} else if uri.starts_with("acct:language-") {
let off = "acct:language-".len();
let Some(at) = uri.find('@') else { return None; };
kind = ActorKind::from_language(&uri[off..at])?;
host = Arc::new(uri[at + 1..].to_string()); host = Arc::new(uri[at + 1..].to_string());
} else if uri.starts_with("https://") { } else if uri.starts_with("https://") {
uri = &uri[8..]; uri = &uri[8..];
@ -53,6 +72,8 @@ impl Actor {
ActorKind::TagRelay(topic.to_string()), ActorKind::TagRelay(topic.to_string()),
"instance" => "instance" =>
ActorKind::InstanceRelay(topic.to_string()), ActorKind::InstanceRelay(topic.to_string()),
"language" =>
ActorKind::LanguageRelay(topic.to_string()),
_ => _ =>
return None, return None,
}; };
@ -69,6 +90,8 @@ impl Actor {
format!("https://{}/tag/{}", self.host, tag), format!("https://{}/tag/{}", self.host, tag),
ActorKind::InstanceRelay(instance) => ActorKind::InstanceRelay(instance) =>
format!("https://{}/instance/{}", self.host, instance), format!("https://{}/instance/{}", self.host, instance),
ActorKind::LanguageRelay(language) =>
format!("https://{}/language/{}", self.host, language),
} }
} }
@ -86,6 +109,8 @@ impl Actor {
format!("#{}", tag), format!("#{}", tag),
ActorKind::InstanceRelay(instance) => ActorKind::InstanceRelay(instance) =>
instance.to_string(), instance.to_string(),
ActorKind::LanguageRelay(language) =>
format!("in {}", language),
}), }),
icon: Some(activitypub::Media { icon: Some(activitypub::Media {
media_type: Some("Image".to_string()), media_type: Some("Image".to_string()),
@ -107,6 +132,8 @@ impl Actor {
format!("tag-{}", tag), format!("tag-{}", tag),
ActorKind::InstanceRelay(instance) => ActorKind::InstanceRelay(instance) =>
format!("instance-{}", instance), format!("instance-{}", instance),
ActorKind::LanguageRelay(language) =>
format!("language-{}", language),
}), }),
} }
} }

View file

@ -90,6 +90,22 @@ async fn get_instance_actor(
.into_response() .into_response()
} }
async fn get_language_actor(
axum::extract::State(state): axum::extract::State<State>,
Path(language): Path<String>
) -> Response {
track_request("GET", "actor", "language");
let Some(kind) = actor::ActorKind::from_language(&language) else {
return StatusCode::NOT_FOUND.into_response();
};
let target = actor::Actor {
host: state.hostname.clone(),
kind,
};
target.as_activitypub(&state.pub_key)
.into_response()
}
async fn post_tag_relay( async fn post_tag_relay(
axum::extract::State(state): axum::extract::State<State>, axum::extract::State(state): axum::extract::State<State>,
Path(tag): Path<String>, Path(tag): Path<String>,
@ -114,6 +130,21 @@ async fn post_instance_relay(
post_relay(state, endpoint, target).await post_relay(state, endpoint, target).await
} }
async fn post_language_relay(
axum::extract::State(state): axum::extract::State<State>,
Path(language): Path<String>,
endpoint: endpoint::Endpoint<'_>
) -> Response {
let Some(kind) = actor::ActorKind::from_language(&language) else {
return StatusCode::NOT_FOUND.into_response();
};
let target = actor::Actor {
host: state.hostname.clone(),
kind,
};
post_relay(state, endpoint, target).await
}
async fn post_relay( async fn post_relay(
state: State, state: State,
endpoint: endpoint::Endpoint<'_>, endpoint: endpoint::Endpoint<'_>,
@ -362,8 +393,10 @@ async fn main() {
let app = Router::new() let app = Router::new()
.route("/tag/:tag", get(get_tag_actor).post(post_tag_relay)) .route("/tag/:tag", get(get_tag_actor).post(post_tag_relay))
.route("/instance/:instance", get(get_instance_actor).post(post_instance_relay)) .route("/instance/:instance", get(get_instance_actor).post(post_instance_relay))
.route("/language/:language", get(get_language_actor).post(post_language_relay))
.route("/tag/:tag/outbox", get(outbox)) .route("/tag/:tag/outbox", get(outbox))
.route("/instance/:instance/outbox", get(outbox)) .route("/instance/:instance/outbox", get(outbox))
.route("/language/:language/outbox", get(outbox))
.route("/.well-known/webfinger", get(webfinger)) .route("/.well-known/webfinger", get(webfinger))
.route("/.well-known/nodeinfo", get(nodeinfo)) .route("/.well-known/nodeinfo", get(nodeinfo))
.route("/api/v1/instance", get(instanceinfo)) .route("/api/v1/instance", get(instanceinfo))

View file

@ -4,9 +4,7 @@ use metrics::{increment_counter, histogram};
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use sigh::PrivateKey; use sigh::PrivateKey;
use tokio::{ use tokio::sync::mpsc::Receiver;
sync::mpsc::Receiver,
};
use crate::{send, actor, state::State}; use crate::{send, actor, state::State};
#[derive(Deserialize)] #[derive(Deserialize)]
@ -14,6 +12,7 @@ struct Post<'a> {
pub url: Option<&'a str>, pub url: Option<&'a str>,
pub uri: &'a str, pub uri: &'a str,
pub tags: Option<Vec<Tag<'a>>>, pub tags: Option<Vec<Tag<'a>>>,
pub language: Option<&'a str>,
} }
impl Post<'_> { impl Post<'_> {
@ -75,6 +74,10 @@ impl Post<'_> {
} }
}) })
) )
.chain(
self.language
.and_then(actor::ActorKind::from_language)
)
} }
pub fn relay_targets(&self, hostname: Arc<String>) -> impl Iterator<Item = actor::Actor> { pub fn relay_targets(&self, hostname: Arc<String>) -> impl Iterator<Item = actor::Actor> {
@ -247,10 +250,12 @@ mod test {
tags: Some(vec![Tag { tags: Some(vec![Tag {
name: "foo", name: "foo",
}]), }]),
language: Some("en"),
}; };
let mut kinds = post.relay_target_kinds(); let mut kinds = post.relay_target_kinds();
assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string()))); assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string())));
assert_eq!(kinds.next(), Some(ActorKind::TagRelay("foo".to_string()))); assert_eq!(kinds.next(), Some(ActorKind::TagRelay("foo".to_string())));
assert_eq!(kinds.next(), Some(ActorKind::LanguageRelay("en".to_string())));
assert_eq!(kinds.next(), None); assert_eq!(kinds.next(), None);
} }
@ -262,6 +267,7 @@ mod test {
tags: Some(vec![Tag { tags: Some(vec![Tag {
name: "", name: "",
}]), }]),
language: None,
}; };
let mut kinds = post.relay_target_kinds(); let mut kinds = post.relay_target_kinds();
assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string()))); assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string())));
@ -276,6 +282,7 @@ mod test {
tags: Some(vec![Tag { tags: Some(vec![Tag {
name: "23", name: "23",
}]), }]),
language: None,
}; };
let mut kinds = post.relay_target_kinds(); let mut kinds = post.relay_target_kinds();
assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string()))); assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string())));
@ -291,6 +298,7 @@ mod test {
tags: Some(vec![Tag { tags: Some(vec![Tag {
name: "dd1302", name: "dd1302",
}]), }]),
language: None,
}; };
let mut kinds = post.relay_target_kinds(); let mut kinds = post.relay_target_kinds();
assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string()))); assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string())));
@ -300,17 +308,46 @@ mod test {
} }
#[test] #[test]
fn post_relay_kind_jp() { fn post_relay_kind_ja() {
let post = Post { let post = Post {
url: Some("http://example.com/post/1"), url: Some("http://example.com/post/1"),
uri: "http://example.com/post/1", uri: "http://example.com/post/1",
tags: Some(vec![Tag { tags: Some(vec![Tag {
name: "スコティッシュ・フォールド・ロングヘアー", name: "スコティッシュ・フォールド・ロングヘアー",
}]), }]),
language: Some("ja"),
}; };
let mut kinds = post.relay_target_kinds(); let mut kinds = post.relay_target_kinds();
assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string()))); assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string())));
assert_eq!(kinds.next(), Some(ActorKind::TagRelay("sukoteitusiyuhuorudoronguhea".to_string()))); assert_eq!(kinds.next(), Some(ActorKind::TagRelay("sukoteitusiyuhuorudoronguhea".to_string())));
assert_eq!(kinds.next(), Some(ActorKind::LanguageRelay("ja".to_string())));
assert_eq!(kinds.next(), None);
}
#[test]
fn post_relay_language_long() {
let post = Post {
url: Some("http://example.com/post/1"),
uri: "http://example.com/post/1",
tags: None,
language: Some("de_CH"),
};
let mut kinds = post.relay_target_kinds();
assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string())));
assert_eq!(kinds.next(), Some(ActorKind::LanguageRelay("de".to_string())));
assert_eq!(kinds.next(), None);
}
#[test]
fn post_relay_language_invalid() {
let post = Post {
url: Some("http://example.com/post/1"),
uri: "http://example.com/post/1",
tags: None,
language: Some("23q"),
};
let mut kinds = post.relay_target_kinds();
assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string())));
assert_eq!(kinds.next(), None); assert_eq!(kinds.next(), None);
} }
} }