caveman/cave/src/activitypub/mod.rs

147 lines
4.4 KiB
Rust

pub mod digest;
pub mod send;
pub mod fetch;
mod error;
pub use error::Error;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
const MIME_TYPE: &str = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Actor {
#[serde(rename = "@context")]
pub jsonld_context: Option<serde_json::Value>,
#[serde(rename = "type")]
pub actor_type: String,
pub id: String,
pub url: Option<String>,
pub name: String,
pub icon: Option<Media>,
pub inbox: String,
pub outbox: String,
#[serde(rename = "publicKey")]
pub public_key: ActorPublicKey,
#[serde(rename = "preferredUsername")]
pub preferred_username: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActorPublicKey {
pub id: String,
pub owner: Option<String>,
#[serde(rename = "publicKeyPem")]
pub pem: String,
}
/// `ActivityPub` "activity"
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action<O> {
#[serde(rename = "@context")]
pub jsonld_context: Option<serde_json::Value>,
#[serde(rename = "type")]
pub action_type: String,
pub id: String,
pub actor: String,
pub to: Option<serde_json::Value>,
pub object: O,
}
impl Action<serde_json::Value> {
#[must_use] pub fn object_id(&self) -> Option<&str> {
if let Some(id) = self.object.as_str() {
Some(id)
} else if let Some(object) = self.object.as_object() {
object.get("id").and_then(|id| id.as_str())
} else {
None
}
}
}
// hack for misskey
fn media_default_content_type() -> String {
"image/jpeg".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Media {
#[serde(rename = "type")]
pub media_type: String,
#[serde(rename = "mediaType", default = "media_default_content_type")]
pub content_type: String,
pub url: String,
}
/// This is unfortunately strictly separate from the Client/Server
/// data schema in `cave::feed::Post`
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Post {
pub published: String,
pub id: String,
pub url: Option<String>,
#[serde(rename = "attributedTo")]
pub attributed_to: Option<String>,
#[serde(default = "String::new")]
pub content: String,
#[serde(rename = "contentMap", default = "HashMap::new")]
pub content_map: HashMap<String, String>,
#[serde(default)]
pub tag: Vec<super::feed::Tag>,
pub sensitive: Option<bool>,
#[serde(rename = "attachment", default = "Vec::new")]
pub attachments: Vec<Media>,
}
impl Post {
#[must_use] pub fn language(&self) -> Option<&str> {
self.content_map.keys()
.next().map(std::string::String::as_str)
}
/// Translate `ActivityPub` post to Mastodon client API post format
#[must_use] pub fn to_feed_post(self, actor: Actor) -> super::feed::Post {
let language = self.language()
.map(std::string::ToString::to_string);
super::feed::Post {
created_at: self.published,
url: self.url.unwrap_or(self.id),
content: self.content,
account: super::feed::Account {
username: actor.preferred_username,
display_name: actor.name,
url: actor.url.unwrap_or(actor.id),
bot: actor.actor_type != "Person",
},
tags: self.tag.into_iter().filter_map(|mut tag| {
while tag.name.starts_with('#') {
tag.name.remove(0);
}
if let Some(c) = tag.name.chars().next() {
// check the first letter
if ['@', ':'].contains(&c) {
// these are weird
None
} else {
Some(tag)
}
} else {
// no first letter
None
}
}).collect(),
sensitive: self.sensitive,
mentions: vec![],
language,
media_attachments: self.attachments.into_iter().map(|media| {
super::feed::MediaAttachment {
media_type: media.media_type,
remote_url: Some(media.url),
}
}).collect(),
reblog: None,
}
}
}