apub_simple_activitypub/
lib.rs

1use apub_core::{
2    activitypub::{Activity, Actor, DeliverableObject, Object},
3    activitypub_ext::{ActivityExt, ActorExt, Out},
4    repo::Dereference,
5};
6use apub_publickey::SimplePublicKey;
7use serde_json::Map;
8use url::Url;
9
10#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct SimpleObject {
13    pub id: Url,
14
15    #[serde(rename = "type")]
16    pub kind: String,
17
18    #[serde(deserialize_with = "one_or_many")]
19    #[serde(skip_serializing_if = "Vec::is_empty")]
20    pub to: Vec<Url>,
21
22    #[serde(deserialize_with = "one_or_many")]
23    #[serde(skip_serializing_if = "Vec::is_empty")]
24    pub cc: Vec<Url>,
25
26    #[serde(flatten)]
27    pub rest: Map<String, serde_json::Value>,
28}
29
30#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct SimpleActivity {
33    pub id: Url,
34
35    #[serde(rename = "type")]
36    pub kind: String,
37
38    #[serde(deserialize_with = "one_or_many")]
39    #[serde(skip_serializing_if = "Vec::is_empty")]
40    pub to: Vec<Url>,
41
42    #[serde(deserialize_with = "one_or_many")]
43    #[serde(skip_serializing_if = "Vec::is_empty")]
44    pub cc: Vec<Url>,
45
46    pub actor: Either<Url, SimpleActor>,
47
48    pub object: Either<Url, Either<Box<SimpleActivity>, SimpleObject>>,
49
50    #[serde(flatten)]
51    pub rest: Map<String, serde_json::Value>,
52}
53
54#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct SimpleActor {
57    pub id: Url,
58
59    #[serde(rename = "type")]
60    pub kind: String,
61
62    pub inbox: Url,
63
64    pub outbox: Url,
65
66    pub preferred_username: String,
67
68    pub public_key: SimplePublicKey,
69
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub name: Option<String>,
72
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub endpoints: Option<SimpleEndpoints>,
75
76    #[serde(flatten)]
77    pub rest: Map<String, serde_json::Value>,
78}
79
80#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
81#[serde(rename_all = "camelCase")]
82pub struct SimpleEndpoints {
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub shared_inbox: Option<Url>,
85}
86
87#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
88#[serde(untagged)]
89pub enum Either<Left, Right> {
90    Left(Left),
91    Right(Right),
92}
93
94impl Object for SimpleObject {
95    type Kind = String;
96
97    fn id(&self) -> &Url {
98        &self.id
99    }
100
101    fn kind(&self) -> &String {
102        &self.kind
103    }
104}
105
106impl DeliverableObject for SimpleObject {
107    fn to(&self) -> &[Url] {
108        &self.to
109    }
110
111    fn cc(&self) -> &[Url] {
112        &self.cc
113    }
114}
115
116impl Object for SimpleActivity {
117    type Kind = String;
118
119    fn id(&self) -> &Url {
120        &self.id
121    }
122
123    fn kind(&self) -> &String {
124        &self.kind
125    }
126}
127
128impl DeliverableObject for SimpleActivity {
129    fn to(&self) -> &[Url] {
130        &self.to
131    }
132
133    fn cc(&self) -> &[Url] {
134        &self.cc
135    }
136}
137
138impl Activity for SimpleActivity {
139    fn actor_id(&self) -> &Url {
140        match &self.actor {
141            Either::Left(ref id) => id,
142            Either::Right(obj) => obj.id(),
143        }
144    }
145
146    fn object_id(&self) -> &Url {
147        match &self.object {
148            Either::Left(ref id) => id,
149            Either::Right(Either::Left(activity)) => activity.id(),
150            Either::Right(Either::Right(object)) => object.id(),
151        }
152    }
153}
154
155pub struct ActivityOrObjectId(Url);
156impl From<Url> for ActivityOrObjectId {
157    fn from(url: Url) -> Self {
158        ActivityOrObjectId(url)
159    }
160}
161impl Dereference for ActivityOrObjectId {
162    type Output = Either<Box<SimpleActivity>, SimpleObject>;
163
164    fn url(&self) -> &Url {
165        &self.0
166    }
167}
168
169pub struct ActorId(Url);
170impl From<Url> for ActorId {
171    fn from(url: Url) -> Self {
172        ActorId(url)
173    }
174}
175impl Dereference for ActorId {
176    type Output = SimpleActor;
177
178    fn url(&self) -> &Url {
179        &self.0
180    }
181}
182
183impl ActivityExt for SimpleActivity {
184    type ActorId = ActorId;
185    type ObjectId = ActivityOrObjectId;
186
187    fn actor(&self) -> Option<Out<Self::ActorId>> {
188        match &self.actor {
189            Either::Right(actor) => Some(actor.clone()),
190            _ => None,
191        }
192    }
193
194    fn object(&self) -> Option<Out<Self::ObjectId>> {
195        match &self.object {
196            Either::Right(either) => Some(either.clone()),
197            _ => None,
198        }
199    }
200}
201
202impl Object for SimpleActor {
203    type Kind = String;
204
205    fn id(&self) -> &Url {
206        &self.id
207    }
208
209    fn kind(&self) -> &Self::Kind {
210        &self.kind
211    }
212}
213
214impl Actor for SimpleActor {
215    fn inbox(&self) -> &Url {
216        &self.inbox
217    }
218
219    fn outbox(&self) -> &Url {
220        &self.outbox
221    }
222
223    fn preferred_username(&self) -> &str {
224        &self.preferred_username
225    }
226
227    fn public_key_id(&self) -> &Url {
228        &self.public_key.id
229    }
230
231    fn name(&self) -> Option<&str> {
232        self.name.as_deref()
233    }
234
235    fn shared_inbox(&self) -> Option<&Url> {
236        self.endpoints
237            .as_ref()
238            .and_then(|endpoints| endpoints.shared_inbox.as_ref())
239    }
240}
241
242pub struct PublicKeyId(Url);
243impl From<Url> for PublicKeyId {
244    fn from(url: Url) -> Self {
245        PublicKeyId(url)
246    }
247}
248impl Dereference for PublicKeyId {
249    type Output = SimplePublicKey;
250
251    fn url(&self) -> &Url {
252        &self.0
253    }
254}
255
256impl ActorExt for SimpleActor {
257    type PublicKeyId = PublicKeyId;
258
259    fn public_key(&self) -> Option<Out<Self::PublicKeyId>> {
260        Some(self.public_key.clone())
261    }
262}
263
264fn one_or_many<'de, D>(deserializer: D) -> Result<Vec<Url>, D::Error>
265where
266    D: serde::de::Deserializer<'de>,
267{
268    #[derive(serde::Deserialize)]
269    #[serde(untagged)]
270    enum OneOrMany {
271        Single(Url),
272        Many(Vec<Url>),
273    }
274
275    let one_or_many: Option<OneOrMany> = serde::de::Deserialize::deserialize(deserializer)?;
276
277    let v = match one_or_many {
278        Some(OneOrMany::Many(v)) => v,
279        Some(OneOrMany::Single(o)) => vec![o],
280        None => vec![],
281    };
282
283    Ok(v)
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    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"}}"#;
291    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":[]}}}}"#;
292
293    #[test]
294    fn simple_actor() {
295        let actor: SimpleActor = serde_json::from_str(ASONIX).unwrap();
296
297        assert_eq!(actor.id().as_str(), "https://masto.asonix.dog/users/asonix");
298        assert_eq!(
299            actor.shared_inbox().unwrap().as_str(),
300            "https://masto.asonix.dog/inbox"
301        );
302        assert_eq!(
303            actor.inbox().as_str(),
304            "https://masto.asonix.dog/users/asonix/inbox"
305        );
306        assert_eq!(
307            actor.outbox().as_str(),
308            "https://masto.asonix.dog/users/asonix/outbox"
309        );
310
311        assert_eq!(
312            actor.public_key_id().as_str(),
313            "https://masto.asonix.dog/users/asonix#main-key"
314        );
315    }
316
317    #[test]
318    fn simple_activity() {
319        let activity: SimpleActivity = serde_json::from_str(CREATE).unwrap();
320
321        assert_eq!(
322            activity.id().as_str(),
323            "https://masto.asonix.dog/users/asonix/statuses/107355727205658651/activity"
324        );
325        assert_eq!(
326            activity.actor_id().as_str(),
327            "https://masto.asonix.dog/users/asonix"
328        );
329        assert_eq!(
330            activity.object_id().as_str(),
331            "https://masto.asonix.dog/users/asonix/statuses/107355727205658651"
332        );
333
334        assert!(&activity.is_public());
335    }
336}