use apub_core::{
activitypub::{Activity, Actor, DeliverableObject, Object},
activitypub_ext::{ActivityExt, ActorExt, Out},
repo::Dereference,
};
use apub_publickey::SimplePublicKey;
use serde_json::Map;
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SimpleObject {
pub id: Url,
#[serde(rename = "type")]
pub kind: String,
#[serde(deserialize_with = "one_or_many")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub to: Vec<Url>,
#[serde(deserialize_with = "one_or_many")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cc: Vec<Url>,
#[serde(flatten)]
pub rest: Map<String, serde_json::Value>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SimpleActivity {
pub id: Url,
#[serde(rename = "type")]
pub kind: String,
#[serde(deserialize_with = "one_or_many")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub to: Vec<Url>,
#[serde(deserialize_with = "one_or_many")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cc: Vec<Url>,
pub actor: Either<Url, SimpleActor>,
pub object: Either<Url, Either<Box<SimpleActivity>, SimpleObject>>,
#[serde(flatten)]
pub rest: Map<String, serde_json::Value>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SimpleActor {
pub id: Url,
#[serde(rename = "type")]
pub kind: String,
pub inbox: Url,
pub outbox: Url,
pub preferred_username: String,
pub public_key: SimplePublicKey,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub endpoints: Option<SimpleEndpoints>,
#[serde(flatten)]
pub rest: Map<String, serde_json::Value>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SimpleEndpoints {
#[serde(skip_serializing_if = "Option::is_none")]
pub shared_inbox: Option<Url>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum Either<Left, Right> {
Left(Left),
Right(Right),
}
impl Object for SimpleObject {
type Kind = String;
fn id(&self) -> &Url {
&self.id
}
fn kind(&self) -> &String {
&self.kind
}
}
impl DeliverableObject for SimpleObject {
fn to(&self) -> &[Url] {
&self.to
}
fn cc(&self) -> &[Url] {
&self.cc
}
}
impl Object for SimpleActivity {
type Kind = String;
fn id(&self) -> &Url {
&self.id
}
fn kind(&self) -> &String {
&self.kind
}
}
impl DeliverableObject for SimpleActivity {
fn to(&self) -> &[Url] {
&self.to
}
fn cc(&self) -> &[Url] {
&self.cc
}
}
impl Activity for SimpleActivity {
fn actor_id(&self) -> &Url {
match &self.actor {
Either::Left(ref id) => id,
Either::Right(obj) => obj.id(),
}
}
fn object_id(&self) -> &Url {
match &self.object {
Either::Left(ref id) => id,
Either::Right(Either::Left(activity)) => activity.id(),
Either::Right(Either::Right(object)) => object.id(),
}
}
}
pub struct ActivityOrObjectId(Url);
impl From<Url> for ActivityOrObjectId {
fn from(url: Url) -> Self {
ActivityOrObjectId(url)
}
}
impl Dereference for ActivityOrObjectId {
type Output = Either<Box<SimpleActivity>, SimpleObject>;
fn url(&self) -> &Url {
&self.0
}
}
pub struct ActorId(Url);
impl From<Url> for ActorId {
fn from(url: Url) -> Self {
ActorId(url)
}
}
impl Dereference for ActorId {
type Output = SimpleActor;
fn url(&self) -> &Url {
&self.0
}
}
impl ActivityExt for SimpleActivity {
type ActorId = ActorId;
type ObjectId = ActivityOrObjectId;
fn actor(&self) -> Option<Out<Self::ActorId>> {
match &self.actor {
Either::Right(actor) => Some(actor.clone()),
_ => None,
}
}
fn object(&self) -> Option<Out<Self::ObjectId>> {
match &self.object {
Either::Right(either) => Some(either.clone()),
_ => None,
}
}
}
impl Object for SimpleActor {
type Kind = String;
fn id(&self) -> &Url {
&self.id
}
fn kind(&self) -> &Self::Kind {
&self.kind
}
}
impl Actor for SimpleActor {
fn inbox(&self) -> &Url {
&self.inbox
}
fn outbox(&self) -> &Url {
&self.outbox
}
fn preferred_username(&self) -> &str {
&self.preferred_username
}
fn public_key_id(&self) -> &Url {
&self.public_key.id
}
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
fn shared_inbox(&self) -> Option<&Url> {
self.endpoints
.as_ref()
.and_then(|endpoints| endpoints.shared_inbox.as_ref())
}
}
pub struct PublicKeyId(Url);
impl From<Url> for PublicKeyId {
fn from(url: Url) -> Self {
PublicKeyId(url)
}
}
impl Dereference for PublicKeyId {
type Output = SimplePublicKey;
fn url(&self) -> &Url {
&self.0
}
}
impl ActorExt for SimpleActor {
type PublicKeyId = PublicKeyId;
fn public_key(&self) -> Option<Out<Self::PublicKeyId>> {
Some(self.public_key.clone())
}
}
fn one_or_many<'de, D>(deserializer: D) -> Result<Vec<Url>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum OneOrMany {
Single(Url),
Many(Vec<Url>),
}
let one_or_many: Option<OneOrMany> = serde::de::Deserialize::deserialize(deserializer)?;
let v = match one_or_many {
Some(OneOrMany::Many(v)) => v,
Some(OneOrMany::Single(o)) => vec![o],
None => vec![],
};
Ok(v)
}
#[cfg(test)]
mod tests {
use super::*;
static ASONIX: &'static str = r#"{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","toot":"http://joinmastodon.org/ns#","featured":{"@id":"toot:featured","@type":"@id"},"featuredTags":{"@id":"toot:featuredTags","@type":"@id"},"alsoKnownAs":{"@id":"as:alsoKnownAs","@type":"@id"},"movedTo":{"@id":"as:movedTo","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","IdentityProof":"toot:IdentityProof","discoverable":"toot:discoverable","Device":"toot:Device","Ed25519Signature":"toot:Ed25519Signature","Ed25519Key":"toot:Ed25519Key","Curve25519Key":"toot:Curve25519Key","EncryptedMessage":"toot:EncryptedMessage","publicKeyBase64":"toot:publicKeyBase64","deviceId":"toot:deviceId","claim":{"@type":"@id","@id":"toot:claim"},"fingerprintKey":{"@type":"@id","@id":"toot:fingerprintKey"},"identityKey":{"@type":"@id","@id":"toot:identityKey"},"devices":{"@type":"@id","@id":"toot:devices"},"messageFranking":"toot:messageFranking","messageType":"toot:messageType","cipherText":"toot:cipherText","suspended":"toot:suspended","focalPoint":{"@container":"@list","@id":"toot:focalPoint"}}],"id":"https://masto.asonix.dog/users/asonix","type":"Person","following":"https://masto.asonix.dog/users/asonix/following","followers":"https://masto.asonix.dog/users/asonix/followers","inbox":"https://masto.asonix.dog/users/asonix/inbox","outbox":"https://masto.asonix.dog/users/asonix/outbox","featured":"https://masto.asonix.dog/users/asonix/collections/featured","featuredTags":"https://masto.asonix.dog/users/asonix/collections/tags","preferredUsername":"asonix","name":"Alleged Cat Crime Committer","summary":"\u003cp\u003elocal liom, friend, rust (lang) stan, bi \u003c/p\u003e\u003cp\u003eicon by \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://t.me/dropmutt\" target=\"blank\" rel=\"noopener noreferrer\" class=\"u-url mention\"\u003e@\u003cspan\u003edropmutt@telegram.org\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e\u003cbr /\u003eheader by \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://furaffinity.net/user/tronixx\" target=\"blank\" rel=\"noopener noreferrer\" class=\"u-url mention\"\u003e@\u003cspan\u003etronixx@furaffinity.net\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e\u003c/p\u003e\u003cp\u003eTestimonials:\u003c/p\u003e\u003cp\u003eStand: LIONS\u003cbr /\u003eStand User: AODE\u003cbr /\u003e- Keris (not on here)\u003c/p\u003e","url":"https://masto.asonix.dog/@asonix","manuallyApprovesFollowers":true,"discoverable":false,"published":"2021-02-09T00:00:00Z","devices":"https://masto.asonix.dog/users/asonix/collections/devices","publicKey":{"id":"https://masto.asonix.dog/users/asonix#main-key","owner":"https://masto.asonix.dog/users/asonix","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+YpyXb3bUp5EyryHqRA\npKSvl4RamJh6CLlngYYPFU8lcx92oQR8nFlqOwInAczGPoCoIfojQpZfqV4hFq1I\nlETy6jHHeoO/YkUsH2dYtz6gjEqiZFCFpoWuGxUQO3lwfmPYpxl2/GFEDR4MrUNp\n9fPn9jHUlKydiDkFQqluajqSJgv0BCwnUGBanTEfeQKahnc3OqPTi4xNbsd2cbAW\nZtJ6VYepphQCRHElvkzefe1ra5qm5i8YBdan3Z3oo5wN1vo3u41tqjVGhDptKZkv\nwBevdL0tedoLp5Lj1l/HLTSBP0D0ZT/HUFuo6Zq27PCq/4ZgJaZkMi7YCVVtpjim\nmQIDAQAB\n-----END PUBLIC KEY-----\n"},"tag":[],"attachment":[{"type":"PropertyValue","name":"pronouns","value":"he/they"},{"type":"PropertyValue","name":"software","value":"bad"},{"type":"PropertyValue","name":"gitea","value":"\u003ca href=\"https://git.asonix.dog\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egit.asonix.dog\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"join my","value":"relay"}],"endpoints":{"sharedInbox":"https://masto.asonix.dog/inbox"},"icon":{"type":"Image","mediaType":"image/png","url":"https://masto.asonix.dog/system/accounts/avatars/000/000/001/original/4f8d8f520ca26354.png"},"image":{"type":"Image","mediaType":"image/png","url":"https://masto.asonix.dog/system/accounts/headers/000/000/001/original/8122ce3e5a745385.png"}}"#;
static CREATE: &'static str = r#"{"id":"https://masto.asonix.dog/users/asonix/statuses/107355727205658651/activity","type":"Create","actor":"https://masto.asonix.dog/users/asonix","published":"2021-11-28T16:53:16Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://masto.asonix.dog/users/asonix/followers"],"object":{"id":"https://masto.asonix.dog/users/asonix/statuses/107355727205658651","type":"Note","summary":null,"inReplyTo":null,"published":"2021-11-28T16:53:16Z","url":"https://masto.asonix.dog/@asonix/107355727205658651","attributedTo":"https://masto.asonix.dog/users/asonix","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://masto.asonix.dog/users/asonix/followers"],"sensitive":false,"atomUri":"https://masto.asonix.dog/users/asonix/statuses/107355727205658651","inReplyToAtomUri":null,"conversation":"tag:masto.asonix.dog,2021-11-28:objectId=671618:objectType=Conversation","content":"\u003cp\u003eY\u0026apos;all it\u0026apos;s Masto MSunday\u003c/p\u003e","contentMap":{"lion":"\u003cp\u003eY\u0026apos;all it\u0026apos;s Masto MSunday\u003c/p\u003e"},"attachment":[],"tag":[],"replies":{"id":"https://masto.asonix.dog/users/asonix/statuses/107355727205658651/replies","type":"Collection","first":{"type":"CollectionPage","next":"https://masto.asonix.dog/users/asonix/statuses/107355727205658651/replies?only_other_accounts=true\u0026page=true","partOf":"https://masto.asonix.dog/users/asonix/statuses/107355727205658651/replies","items":[]}}}}"#;
#[test]
fn simple_actor() {
let actor: SimpleActor = serde_json::from_str(ASONIX).unwrap();
assert_eq!(actor.id().as_str(), "https://masto.asonix.dog/users/asonix");
assert_eq!(
actor.shared_inbox().unwrap().as_str(),
"https://masto.asonix.dog/inbox"
);
assert_eq!(
actor.inbox().as_str(),
"https://masto.asonix.dog/users/asonix/inbox"
);
assert_eq!(
actor.outbox().as_str(),
"https://masto.asonix.dog/users/asonix/outbox"
);
assert_eq!(
actor.public_key_id().as_str(),
"https://masto.asonix.dog/users/asonix#main-key"
);
}
#[test]
fn simple_activity() {
let activity: SimpleActivity = serde_json::from_str(CREATE).unwrap();
assert_eq!(
activity.id().as_str(),
"https://masto.asonix.dog/users/asonix/statuses/107355727205658651/activity"
);
assert_eq!(
activity.actor_id().as_str(),
"https://masto.asonix.dog/users/asonix"
);
assert_eq!(
activity.object_id().as_str(),
"https://masto.asonix.dog/users/asonix/statuses/107355727205658651"
);
assert!(&activity.is_public());
}
}