diff --git a/src/actor.rs b/src/actor.rs index 8b4c9ed..031b33b 100644 --- a/src/actor.rs +++ b/src/actor.rs @@ -5,9 +5,11 @@ use sigh::{PublicKey, Key}; use crate::activitypub; #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[allow(clippy::enum_variant_names)] pub enum ActorKind { TagRelay(String), InstanceRelay(String), + LanguageRelay(String), } impl ActorKind { @@ -17,6 +19,18 @@ impl ActorKind { .replace(char::is_whitespace, ""); ActorKind::TagRelay(tag) } + + pub fn from_language(language: &str) -> Option { + let language = language.to_lowercase() + .chars() + .take_while(|c| c.is_alphabetic()) + .collect::(); + if language.is_empty() { + None + } else { + Some(ActorKind::LanguageRelay(language)) + } + } } #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] @@ -32,12 +46,17 @@ impl Actor { if uri.starts_with("acct:tag-") { let off = "acct:tag-".len(); 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()); } else if uri.starts_with("acct:instance-") { let off = "acct:instance-".len(); 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()); } else if uri.starts_with("https://") { uri = &uri[8..]; @@ -53,6 +72,8 @@ impl Actor { ActorKind::TagRelay(topic.to_string()), "instance" => ActorKind::InstanceRelay(topic.to_string()), + "language" => + ActorKind::LanguageRelay(topic.to_string()), _ => return None, }; @@ -69,6 +90,8 @@ impl Actor { format!("https://{}/tag/{}", self.host, tag), ActorKind::InstanceRelay(instance) => format!("https://{}/instance/{}", self.host, instance), + ActorKind::LanguageRelay(language) => + format!("https://{}/language/{}", self.host, language), } } @@ -86,6 +109,8 @@ impl Actor { format!("#{}", tag), ActorKind::InstanceRelay(instance) => instance.to_string(), + ActorKind::LanguageRelay(language) => + format!("in {}", language), }), icon: Some(activitypub::Media { media_type: Some("Image".to_string()), @@ -107,6 +132,8 @@ impl Actor { format!("tag-{}", tag), ActorKind::InstanceRelay(instance) => format!("instance-{}", instance), + ActorKind::LanguageRelay(language) => + format!("language-{}", language), }), } } diff --git a/src/main.rs b/src/main.rs index c111cd1..1fc594e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,6 +90,22 @@ async fn get_instance_actor( .into_response() } +async fn get_language_actor( + axum::extract::State(state): axum::extract::State, + Path(language): Path +) -> 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( axum::extract::State(state): axum::extract::State, Path(tag): Path, @@ -114,6 +130,21 @@ async fn post_instance_relay( post_relay(state, endpoint, target).await } +async fn post_language_relay( + axum::extract::State(state): axum::extract::State, + Path(language): Path, + 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( state: State, endpoint: endpoint::Endpoint<'_>, @@ -362,8 +393,10 @@ async fn main() { let app = Router::new() .route("/tag/:tag", get(get_tag_actor).post(post_tag_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("/instance/:instance/outbox", get(outbox)) + .route("/language/:language/outbox", get(outbox)) .route("/.well-known/webfinger", get(webfinger)) .route("/.well-known/nodeinfo", get(nodeinfo)) .route("/api/v1/instance", get(instanceinfo)) diff --git a/src/relay.rs b/src/relay.rs index 141038b..7935d7f 100644 --- a/src/relay.rs +++ b/src/relay.rs @@ -4,9 +4,7 @@ use metrics::{increment_counter, histogram}; use serde::Deserialize; use serde_json::json; use sigh::PrivateKey; -use tokio::{ - sync::mpsc::Receiver, -}; +use tokio::sync::mpsc::Receiver; use crate::{send, actor, state::State}; #[derive(Deserialize)] @@ -14,6 +12,7 @@ struct Post<'a> { pub url: Option<&'a str>, pub uri: &'a str, pub tags: Option>>, + pub language: Option<&'a str>, } impl Post<'_> { @@ -75,6 +74,10 @@ impl Post<'_> { } }) ) + .chain( + self.language + .and_then(actor::ActorKind::from_language) + ) } pub fn relay_targets(&self, hostname: Arc) -> impl Iterator { @@ -247,10 +250,12 @@ mod test { tags: Some(vec![Tag { name: "foo", }]), + language: Some("en"), }; let mut kinds = post.relay_target_kinds(); 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::LanguageRelay("en".to_string()))); assert_eq!(kinds.next(), None); } @@ -262,6 +267,7 @@ mod test { tags: Some(vec![Tag { name: "", }]), + language: None, }; let mut kinds = post.relay_target_kinds(); assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string()))); @@ -276,6 +282,7 @@ mod test { tags: Some(vec![Tag { name: "23", }]), + language: None, }; let mut kinds = post.relay_target_kinds(); assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string()))); @@ -291,6 +298,7 @@ mod test { tags: Some(vec![Tag { name: "dd1302", }]), + language: None, }; let mut kinds = post.relay_target_kinds(); assert_eq!(kinds.next(), Some(ActorKind::InstanceRelay("example.com".to_string()))); @@ -300,17 +308,46 @@ mod test { } #[test] - fn post_relay_kind_jp() { + fn post_relay_kind_ja() { let post = Post { url: Some("http://example.com/post/1"), uri: "http://example.com/post/1", tags: Some(vec![Tag { name: "スコティッシュ・フォールド・ロングヘアー", }]), + language: Some("ja"), }; let mut kinds = post.relay_target_kinds(); 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::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); } }