apub-simple-activitypub 0.2.0

Utilities for building activitypub servers
Documentation
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());
    }
}