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}