]> git.mxchange.org Git - friendica.git/blob - src/Protocol/ActivityPub.php
ab5da4666cde647cec9701ba4891e0bc803b5cc8
[friendica.git] / src / Protocol / ActivityPub.php
1 <?php
2 /**
3  * @file src/Protocol/ActivityPub.php
4  */
5 namespace Friendica\Protocol;
6
7 use Friendica\Database\DBA;
8 use Friendica\Core\System;
9 use Friendica\BaseObject;
10 use Friendica\Util\Network;
11 use Friendica\Util\HTTPSignature;
12 use Friendica\Core\Protocol;
13 use Friendica\Model\Conversation;
14 use Friendica\Model\Contact;
15 use Friendica\Model\APContact;
16 use Friendica\Model\Item;
17 use Friendica\Model\Profile;
18 use Friendica\Model\Term;
19 use Friendica\Model\User;
20 use Friendica\Util\DateTimeFormat;
21 use Friendica\Util\Crypto;
22 use Friendica\Content\Text\BBCode;
23 use Friendica\Content\Text\HTML;
24 use Friendica\Util\JsonLD;
25 use Friendica\Util\LDSignature;
26 use Friendica\Core\Config;
27
28 /**
29  * @brief ActivityPub Protocol class
30  * The ActivityPub Protocol is a message exchange protocol defined by the W3C.
31  * https://www.w3.org/TR/activitypub/
32  * https://www.w3.org/TR/activitystreams-core/
33  * https://www.w3.org/TR/activitystreams-vocabulary/
34  *
35  * https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
36  * https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/
37  *
38  * Digest: https://tools.ietf.org/html/rfc5843
39  * https://tools.ietf.org/html/draft-cavage-http-signatures-10#ref-15
40  *
41  * Mastodon implementation of supported activities:
42  * https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/activity.rb#L26
43  *
44  * To-do:
45  *
46  * Receiver:
47  * - Update Note
48  * - Delete Note
49  * - Delete Person
50  * - Undo Announce
51  * - Reject Follow
52  * - Undo Accept
53  * - Undo Follow
54  * - Add
55  * - Create Image
56  * - Create Video
57  * - Event
58  * - Remove
59  * - Block
60  * - Flag
61  *
62  * Transmitter:
63  * - Announce
64  * - Undo Announce
65  * - Update Person
66  * - Reject Follow
67  * - Event
68  *
69  * General:
70  * - Attachments
71  * - nsfw (sensitive)
72  * - Queueing unsucessful deliveries
73  * - Polling the outboxes for missing content?
74  * - Possibly using the LD-JSON parser
75  */
76 class ActivityPub
77 {
78         const PUBLIC_COLLECTION = 'https://www.w3.org/ns/activitystreams#Public';
79         const CONTEXT = ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1',
80                 ['vcard' => 'http://www.w3.org/2006/vcard/ns#',
81                 'diaspora' => 'https://diasporafoundation.org/ns/',
82                 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
83                 'sensitive' => 'as:sensitive', 'Hashtag' => 'as:Hashtag']];
84
85         /**
86          * @brief Checks if the web request is done for the AP protocol
87          *
88          * @return is it AP?
89          */
90         public static function isRequest()
91         {
92                 return stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json') ||
93                         stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/ld+json');
94         }
95
96         /**
97          * @brief collects the lost of followers of the given owner
98          *
99          * @param array $owner Owner array
100          * @param integer $page Page number
101          *
102          * @return array of owners
103          */
104         public static function getFollowers($owner, $page = null)
105         {
106                 $condition = ['rel' => [Contact::FOLLOWER, Contact::FRIEND], 'network' => Protocol::NATIVE_SUPPORT, 'uid' => $owner['uid'],
107                         'self' => false, 'hidden' => false, 'archive' => false, 'pending' => false];
108                 $count = DBA::count('contact', $condition);
109
110                 $data = ['@context' => self::CONTEXT];
111                 $data['id'] = System::baseUrl() . '/followers/' . $owner['nickname'];
112                 $data['type'] = 'OrderedCollection';
113                 $data['totalItems'] = $count;
114
115                 // When we hide our friends we will only show the pure number but don't allow more.
116                 $profile = Profile::getByUID($owner['uid']);
117                 if (!empty($profile['hide-friends'])) {
118                         return $data;
119                 }
120
121                 if (empty($page)) {
122                         $data['first'] = System::baseUrl() . '/followers/' . $owner['nickname'] . '?page=1';
123                 } else {
124                         $list = [];
125
126                         $contacts = DBA::select('contact', ['url'], $condition, ['limit' => [($page - 1) * 100, 100]]);
127                         while ($contact = DBA::fetch($contacts)) {
128                                 $list[] = $contact['url'];
129                         }
130
131                         if (!empty($list)) {
132                                 $data['next'] = System::baseUrl() . '/followers/' . $owner['nickname'] . '?page=' . ($page + 1);
133                         }
134
135                         $data['partOf'] = System::baseUrl() . '/followers/' . $owner['nickname'];
136
137                         $data['orderedItems'] = $list;
138                 }
139
140                 return $data;
141         }
142
143         /**
144          * @brief Create list of following contacts
145          *
146          * @param array $owner Owner array
147          * @param integer $page Page numbe
148          *
149          * @return array of following contacts
150          */
151         public static function getFollowing($owner, $page = null)
152         {
153                 $condition = ['rel' => [Contact::SHARING, Contact::FRIEND], 'network' => Protocol::NATIVE_SUPPORT, 'uid' => $owner['uid'],
154                         'self' => false, 'hidden' => false, 'archive' => false, 'pending' => false];
155                 $count = DBA::count('contact', $condition);
156
157                 $data = ['@context' => self::CONTEXT];
158                 $data['id'] = System::baseUrl() . '/following/' . $owner['nickname'];
159                 $data['type'] = 'OrderedCollection';
160                 $data['totalItems'] = $count;
161
162                 // When we hide our friends we will only show the pure number but don't allow more.
163                 $profile = Profile::getByUID($owner['uid']);
164                 if (!empty($profile['hide-friends'])) {
165                         return $data;
166                 }
167
168                 if (empty($page)) {
169                         $data['first'] = System::baseUrl() . '/following/' . $owner['nickname'] . '?page=1';
170                 } else {
171                         $list = [];
172
173                         $contacts = DBA::select('contact', ['url'], $condition, ['limit' => [($page - 1) * 100, 100]]);
174                         while ($contact = DBA::fetch($contacts)) {
175                                 $list[] = $contact['url'];
176                         }
177
178                         if (!empty($list)) {
179                                 $data['next'] = System::baseUrl() . '/following/' . $owner['nickname'] . '?page=' . ($page + 1);
180                         }
181
182                         $data['partOf'] = System::baseUrl() . '/following/' . $owner['nickname'];
183
184                         $data['orderedItems'] = $list;
185                 }
186
187                 return $data;
188         }
189
190         /**
191          * @brief Public posts for the given owner
192          *
193          * @param array $owner Owner array
194          * @param integer $page Page numbe
195          *
196          * @return array of posts
197          */
198         public static function getOutbox($owner, $page = null)
199         {
200                 $public_contact = Contact::getIdForURL($owner['url'], 0, true);
201
202                 $condition = ['uid' => $owner['uid'], 'contact-id' => $owner['id'], 'author-id' => $public_contact,
203                         'wall' => true, 'private' => false, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT],
204                         'deleted' => false, 'visible' => true];
205                 $count = DBA::count('item', $condition);
206
207                 $data = ['@context' => self::CONTEXT];
208                 $data['id'] = System::baseUrl() . '/outbox/' . $owner['nickname'];
209                 $data['type'] = 'OrderedCollection';
210                 $data['totalItems'] = $count;
211
212                 if (empty($page)) {
213                         $data['first'] = System::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=1';
214                 } else {
215                         $list = [];
216
217                         $condition['parent-network'] = Protocol::NATIVE_SUPPORT;
218
219                         $items = Item::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]);
220                         while ($item = Item::fetch($items)) {
221                                 $object = self::createObjectFromItemID($item['id']);
222                                 unset($object['@context']);
223                                 $list[] = $object;
224                         }
225
226                         if (!empty($list)) {
227                                 $data['next'] = System::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=' . ($page + 1);
228                         }
229
230                         $data['partOf'] = System::baseUrl() . '/outbox/' . $owner['nickname'];
231
232                         $data['orderedItems'] = $list;
233                 }
234
235                 return $data;
236         }
237
238         /**
239          * Return the ActivityPub profile of the given user
240          *
241          * @param integer $uid User ID
242          * @return profile array
243          */
244         public static function profile($uid)
245         {
246                 $accounttype = ['Person', 'Organization', 'Service', 'Group', 'Application'];
247                 $condition = ['uid' => $uid, 'blocked' => false, 'account_expired' => false,
248                         'account_removed' => false, 'verified' => true];
249                 $fields = ['guid', 'nickname', 'pubkey', 'account-type', 'page-flags'];
250                 $user = DBA::selectFirst('user', $fields, $condition);
251                 if (!DBA::isResult($user)) {
252                         return [];
253                 }
254
255                 $fields = ['locality', 'region', 'country-name'];
256                 $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid, 'is-default' => true]);
257                 if (!DBA::isResult($profile)) {
258                         return [];
259                 }
260
261                 $fields = ['name', 'url', 'location', 'about', 'avatar'];
262                 $contact = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]);
263                 if (!DBA::isResult($contact)) {
264                         return [];
265                 }
266
267                 $data = ['@context' => self::CONTEXT];
268                 $data['id'] = $contact['url'];
269                 $data['diaspora:guid'] = $user['guid'];
270                 $data['type'] = $accounttype[$user['account-type']];
271                 $data['following'] = System::baseUrl() . '/following/' . $user['nickname'];
272                 $data['followers'] = System::baseUrl() . '/followers/' . $user['nickname'];
273                 $data['inbox'] = System::baseUrl() . '/inbox/' . $user['nickname'];
274                 $data['outbox'] = System::baseUrl() . '/outbox/' . $user['nickname'];
275                 $data['preferredUsername'] = $user['nickname'];
276                 $data['name'] = $contact['name'];
277                 $data['vcard:hasAddress'] = ['@type' => 'vcard:Home', 'vcard:country-name' => $profile['country-name'],
278                         'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']];
279                 $data['summary'] = $contact['about'];
280                 $data['url'] = $contact['url'];
281                 $data['manuallyApprovesFollowers'] = in_array($user['page-flags'], [Contact::PAGE_NORMAL, Contact::PAGE_PRVGROUP]);
282                 $data['publicKey'] = ['id' => $contact['url'] . '#main-key',
283                         'owner' => $contact['url'],
284                         'publicKeyPem' => $user['pubkey']];
285                 $data['endpoints'] = ['sharedInbox' => System::baseUrl() . '/inbox'];
286                 $data['icon'] = ['type' => 'Image',
287                         'url' => $contact['avatar']];
288
289                 // tags: https://kitty.town/@inmysocks/100656097926961126.json
290                 return $data;
291         }
292
293         /**
294          * @brief Returns an array with permissions of a given item array
295          *
296          * @param array $item
297          *
298          * @return array with permissions
299          */
300         private static function fetchPermissionBlockFromConversation($item)
301         {
302                 if (empty($item['thr-parent'])) {
303                         return [];
304                 }
305
306                 $condition = ['item-uri' => $item['thr-parent'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
307                 $conversation = DBA::selectFirst('conversation', ['source'], $condition);
308                 if (!DBA::isResult($conversation)) {
309                         return [];
310                 }
311
312                 $activity = json_decode($conversation['source'], true);
313
314                 $actor = JsonLD::fetchElement($activity, 'actor', 'id');
315                 $profile = APContact::getByURL($actor);
316
317                 $item_profile = APContact::getByURL($item['author-link']);
318                 $exclude[] = $item['author-link'];
319
320                 if ($item['gravity'] == GRAVITY_PARENT) {
321                         $exclude[] = $item['owner-link'];
322                 }
323
324                 $permissions['to'][] = $actor;
325
326                 foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
327                         if (empty($activity[$element])) {
328                                 continue;
329                         }
330                         if (is_string($activity[$element])) {
331                                 $activity[$element] = [$activity[$element]];
332                         }
333
334                         foreach ($activity[$element] as $receiver) {
335                                 if ($receiver == $profile['followers'] && !empty($item_profile['followers'])) {
336                                         $receiver = $item_profile['followers'];
337                                 }
338                                 if (!in_array($receiver, $exclude)) {
339                                         $permissions[$element][] = $receiver;
340                                 }
341                         }
342                 }
343                 return $permissions;
344         }
345
346         /**
347          * @brief Creates an array of permissions from an item thread
348          *
349          * @param array $item
350          *
351          * @return permission array
352          */
353         public static function createPermissionBlockForItem($item)
354         {
355                 $data = ['to' => [], 'cc' => []];
356
357                 $data = array_merge($data, self::fetchPermissionBlockFromConversation($item));
358
359                 $actor_profile = APContact::getByURL($item['author-link']);
360
361                 $terms = Term::tagArrayFromItemId($item['id'], TERM_MENTION);
362
363                 $contacts[$item['author-link']] = $item['author-link'];
364
365                 if (!$item['private']) {
366                         $data['to'][] = self::PUBLIC_COLLECTION;
367                         if (!empty($actor_profile['followers'])) {
368                                 $data['cc'][] = $actor_profile['followers'];
369                         }
370
371                         foreach ($terms as $term) {
372                                 $profile = APContact::getByURL($term['url'], false);
373                                 if (!empty($profile) && empty($contacts[$profile['url']])) {
374                                         $data['cc'][] = $profile['url'];
375                                         $contacts[$profile['url']] = $profile['url'];
376                                 }
377                         }
378                 } else {
379                         $receiver_list = Item::enumeratePermissions($item);
380
381                         $mentioned = [];
382
383                         foreach ($terms as $term) {
384                                 $cid = Contact::getIdForURL($term['url'], $item['uid']);
385                                 if (!empty($cid) && in_array($cid, $receiver_list)) {
386                                         $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid, 'network' => Protocol::ACTIVITYPUB]);
387                                         $data['to'][] = $contact['url'];
388                                         $contacts[$contact['url']] = $contact['url'];
389                                 }
390                         }
391
392                         foreach ($receiver_list as $receiver) {
393                                 $contact = DBA::selectFirst('contact', ['url'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]);
394                                 if (empty($contacts[$contact['url']])) {
395                                         $data['cc'][] = $contact['url'];
396                                         $contacts[$contact['url']] = $contact['url'];
397                                 }
398                         }
399                 }
400
401                 $parents = Item::select(['id', 'author-link', 'owner-link', 'gravity'], ['parent' => $item['parent']]);
402                 while ($parent = Item::fetch($parents)) {
403                         // Don't include data from future posts
404                         if ($parent['id'] >= $item['id']) {
405                                 continue;
406                         }
407
408                         $profile = APContact::getByURL($parent['author-link'], false);
409                         if (!empty($profile) && empty($contacts[$profile['url']])) {
410                                 $data['cc'][] = $profile['url'];
411                                 $contacts[$profile['url']] = $profile['url'];
412                         }
413
414                         if ($item['gravity'] != GRAVITY_PARENT) {
415                                 continue;
416                         }
417
418                         $profile = APContact::getByURL($parent['owner-link'], false);
419                         if (!empty($profile) && empty($contacts[$profile['url']])) {
420                                 $data['cc'][] = $profile['url'];
421                                 $contacts[$profile['url']] = $profile['url'];
422                         }
423                 }
424                 DBA::close($parents);
425
426                 if (empty($data['to'])) {
427                         $data['to'] = $data['cc'];
428                         $data['cc'] = [];
429                 }
430
431                 return $data;
432         }
433
434         /**
435          * @brief Fetches an array of inboxes for the given item and user
436          *
437          * @param array $item
438          * @param integer $uid User ID
439          *
440          * @return array with inboxes
441          */
442         public static function fetchTargetInboxes($item, $uid)
443         {
444                 $permissions = self::createPermissionBlockForItem($item);
445                 if (empty($permissions)) {
446                         return [];
447                 }
448
449                 $inboxes = [];
450
451                 if ($item['gravity'] == GRAVITY_ACTIVITY) {
452                         $item_profile = APContact::getByURL($item['author-link']);
453                 } else {
454                         $item_profile = APContact::getByURL($item['owner-link']);
455                 }
456
457                 foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
458                         if (empty($permissions[$element])) {
459                                 continue;
460                         }
461
462                         foreach ($permissions[$element] as $receiver) {
463                                 if ($receiver == $item_profile['followers']) {
464                                         $contacts = DBA::select('contact', ['notify', 'batch'], ['uid' => $uid,
465                                                 'rel' => [Contact::FOLLOWER, Contact::FRIEND], 'network' => Protocol::ACTIVITYPUB,
466                                                 'archive' => false, 'pending' => false]);
467                                         while ($contact = DBA::fetch($contacts)) {
468                                                 $contact = defaults($contact, 'batch', $contact['notify']);
469                                                 $inboxes[$contact] = $contact;
470                                         }
471                                         DBA::close($contacts);
472                                 } else {
473                                         $profile = APContact::getByURL($receiver);
474                                         if (!empty($profile)) {
475                                                 $target = defaults($profile, 'sharedinbox', $profile['inbox']);
476                                                 $inboxes[$target] = $target;
477                                         }
478                                 }
479                         }
480                 }
481
482                 return $inboxes;
483         }
484
485         /**
486          * @brief Returns the activity type of a given item
487          *
488          * @param array $item
489          *
490          * @return activity type
491          */
492         public static function getTypeOfItem($item)
493         {
494                 if ($item['verb'] == ACTIVITY_POST) {
495                         if ($item['created'] == $item['edited']) {
496                                 $type = 'Create';
497                         } else {
498                                 $type = 'Update';
499                         }
500                 } elseif ($item['verb'] == ACTIVITY_LIKE) {
501                         $type = 'Like';
502                 } elseif ($item['verb'] == ACTIVITY_DISLIKE) {
503                         $type = 'Dislike';
504                 } elseif ($item['verb'] == ACTIVITY_ATTEND) {
505                         $type = 'Accept';
506                 } elseif ($item['verb'] == ACTIVITY_ATTENDNO) {
507                         $type = 'Reject';
508                 } elseif ($item['verb'] == ACTIVITY_ATTENDMAYBE) {
509                         $type = 'TentativeAccept';
510                 } else {
511                         $type = '';
512                 }
513
514                 return $type;
515         }
516
517         /**
518          * @brief Creates an activity array for a given item id
519          *
520          * @param integer $item_id
521          * @param boolean $object_mode Is the activity item is used inside another object?
522          *
523          * @return array of activity
524          */
525         public static function createActivityFromItem($item_id, $object_mode = false)
526         {
527                 $item = Item::selectFirst([], ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]);
528
529                 if (!DBA::isResult($item)) {
530                         return false;
531                 }
532
533                 $condition = ['item-uri' => $item['uri'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
534                 $conversation = DBA::selectFirst('conversation', ['source'], $condition);
535                 if (DBA::isResult($conversation)) {
536                         $data = json_decode($conversation['source']);
537                         if (!empty($data)) {
538                                 return $data;
539                         }
540                 }
541
542                 $type = self::getTypeOfItem($item);
543
544                 if (!$object_mode) {
545                         $data = ['@context' => self::CONTEXT];
546
547                         if ($item['deleted'] && ($item['gravity'] == GRAVITY_ACTIVITY)) {
548                                 $type = 'Undo';
549                         } elseif ($item['deleted']) {
550                                 $type = 'Delete';
551                         }
552                 } else {
553                         $data = [];
554                 }
555
556                 $data['id'] = $item['uri'] . '#' . $type;
557                 $data['type'] = $type;
558                 $data['actor'] = $item['author-link'];
559
560                 $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM);
561
562                 if ($item["created"] != $item["edited"]) {
563                         $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM);
564                 }
565
566                 $data['context'] = self::fetchContextURLForItem($item);
567
568                 $data = array_merge($data, ActivityPub::createPermissionBlockForItem($item));
569
570                 if (in_array($data['type'], ['Create', 'Update', 'Announce', 'Delete'])) {
571                         $data['object'] = self::createNote($item);
572                 } elseif ($data['type'] == 'Undo') {
573                         $data['object'] = self::createActivityFromItem($item_id, true);
574                 } else {
575                         $data['object'] = $item['thr-parent'];
576                 }
577
578                 $owner = User::getOwnerDataById($item['uid']);
579
580                 if (!$object_mode) {
581                         return LDSignature::sign($data, $owner);
582                 } else {
583                         return $data;
584                 }
585         }
586
587         /**
588          * @brief Creates an object array for a given item id
589          *
590          * @param integer $item_id
591          *
592          * @return object array
593          */
594         public static function createObjectFromItemID($item_id)
595         {
596                 $item = Item::selectFirst([], ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]);
597
598                 if (!DBA::isResult($item)) {
599                         return false;
600                 }
601
602                 $data = ['@context' => self::CONTEXT];
603                 $data = array_merge($data, self::createNote($item));
604
605                 return $data;
606         }
607
608         /**
609          * @brief Returns a tag array for a given item array
610          *
611          * @param array $item
612          *
613          * @return array of tags
614          */
615         private static function createTagList($item)
616         {
617                 $tags = [];
618
619                 $terms = Term::tagArrayFromItemId($item['id'], TERM_MENTION);
620                 foreach ($terms as $term) {
621                         $contact = Contact::getDetailsByURL($term['url']);
622                         if (!empty($contact['addr'])) {
623                                 $mention = '@' . $contact['addr'];
624                         } else {
625                                 $mention = '@' . $term['url'];
626                         }
627
628                         $tags[] = ['type' => 'Mention', 'href' => $term['url'], 'name' => $mention];
629                 }
630                 return $tags;
631         }
632
633         /**
634          * @brief Fetches the "context" value for a givem item array from the "conversation" table
635          *
636          * @param array $item
637          *
638          * @return string with context url
639          */
640         private static function fetchContextURLForItem($item)
641         {
642                 $conversation = DBA::selectFirst('conversation', ['conversation-href', 'conversation-uri'], ['item-uri' => $item['parent-uri']]);
643                 if (DBA::isResult($conversation) && !empty($conversation['conversation-href'])) {
644                         $context_uri = $conversation['conversation-href'];
645                 } elseif (DBA::isResult($conversation) && !empty($conversation['conversation-uri'])) {
646                         $context_uri = $conversation['conversation-uri'];
647                 } else {
648                         $context_uri = str_replace('/object/', '/context/', $item['parent-uri']);
649                 }
650                 return $context_uri;
651         }
652
653         /**
654          * @brief Creates a note/article object array
655          *
656          * @param array $item
657          *
658          * @return object array
659          */
660         private static function createNote($item)
661         {
662                 if (!empty($item['title'])) {
663                         $type = 'Article';
664                 } else {
665                         $type = 'Note';
666                 }
667
668                 if ($item['deleted']) {
669                         $type = 'Tombstone';
670                 }
671
672                 $data = [];
673                 $data['id'] = $item['uri'];
674                 $data['type'] = $type;
675
676                 if ($item['deleted']) {
677                         return $data;
678                 }
679
680                 $data['summary'] = null; // Ignore by now
681
682                 if ($item['uri'] != $item['thr-parent']) {
683                         $data['inReplyTo'] = $item['thr-parent'];
684                 } else {
685                         $data['inReplyTo'] = null;
686                 }
687
688                 $data['diaspora:guid'] = $item['guid'];
689                 $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM);
690
691                 if ($item["created"] != $item["edited"]) {
692                         $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM);
693                 }
694
695                 $data['url'] = $item['plink'];
696                 $data['attributedTo'] = $item['author-link'];
697                 $data['actor'] = $item['author-link'];
698                 $data['sensitive'] = false; // - Query NSFW
699                 $data['context'] = self::fetchContextURLForItem($item);
700
701                 if (!empty($item['title'])) {
702                         $data['name'] = BBCode::convert($item['title'], false, 7);
703                 }
704
705                 $data['content'] = BBCode::convert($item['body'], false, 7);
706                 $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"];
707
708                 if (!empty($item['signed_text']) && ($item['uri'] != $item['thr-parent'])) {
709                         $data['diaspora:comment'] = $item['signed_text'];
710                 }
711
712                 $data['attachment'] = []; // @ToDo
713                 $data['tag'] = self::createTagList($item);
714                 $data = array_merge($data, ActivityPub::createPermissionBlockForItem($item));
715
716                 return $data;
717         }
718
719         /**
720          * @brief Transmits a given activity to a target
721          *
722          * @param array $activity
723          * @param string $target Target profile
724          * @param integer $uid User ID
725          */
726         public static function transmitActivity($activity, $target, $uid)
727         {
728                 $profile = APContact::getByURL($target);
729
730                 $owner = User::getOwnerDataById($uid);
731
732                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
733                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
734                         'type' => $activity,
735                         'actor' => $owner['url'],
736                         'object' => $profile['url'],
737                         'to' => $profile['url']];
738
739                 logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG);
740
741                 $signed = LDSignature::sign($data, $owner);
742                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
743         }
744
745         /**
746          * @brief Transmit a message that the contact request had been accepted
747          *
748          * @param string $target Target profile
749          * @param $id
750          * @param integer $uid User ID
751          */
752         public static function transmitContactAccept($target, $id, $uid)
753         {
754                 $profile = APContact::getByURL($target);
755
756                 $owner = User::getOwnerDataById($uid);
757                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
758                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
759                         'type' => 'Accept',
760                         'actor' => $owner['url'],
761                         'object' => ['id' => $id, 'type' => 'Follow',
762                                 'actor' => $profile['url'],
763                                 'object' => $owner['url']],
764                         'to' => $profile['url']];
765
766                 logger('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG);
767
768                 $signed = LDSignature::sign($data, $owner);
769                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
770         }
771
772         /**
773          * @brief 
774          *
775          * @param string $target Target profile
776          * @param $id
777          * @param integer $uid User ID
778          */
779         public static function transmitContactReject($target, $id, $uid)
780         {
781                 $profile = APContact::getByURL($target);
782
783                 $owner = User::getOwnerDataById($uid);
784                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
785                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
786                         'type' => 'Reject',
787                         'actor' => $owner['url'],
788                         'object' => ['id' => $id, 'type' => 'Follow',
789                                 'actor' => $profile['url'],
790                                 'object' => $owner['url']],
791                         'to' => $profile['url']];
792
793                 logger('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG);
794
795                 $signed = LDSignature::sign($data, $owner);
796                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
797         }
798
799         /**
800          * @brief 
801          *
802          * @param string $target Target profile
803          * @param integer $uid User ID
804          */
805         public static function transmitContactUndo($target, $uid)
806         {
807                 $profile = APContact::getByURL($target);
808
809                 $id = System::baseUrl() . '/activity/' . System::createGUID();
810
811                 $owner = User::getOwnerDataById($uid);
812                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
813                         'id' => $id,
814                         'type' => 'Undo',
815                         'actor' => $owner['url'],
816                         'object' => ['id' => $id, 'type' => 'Follow',
817                                 'actor' => $owner['url'],
818                                 'object' => $profile['url']],
819                         'to' => $profile['url']];
820
821                 logger('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG);
822
823                 $signed = LDSignature::sign($data, $owner);
824                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
825         }
826
827         /**
828          * Fetches ActivityPub content from the given url
829          *
830          * @param string $url content url
831          * @return array
832          */
833         public static function fetchContent($url)
834         {
835                 $ret = Network::curl($url, false, $redirects, ['accept_content' => 'application/activity+json, application/ld+json']);
836                 if (!$ret['success'] || empty($ret['body'])) {
837                         return;
838                 }
839                 return json_decode($ret['body'], true);
840         }
841
842         /**
843          * Fetches a profile from the given url into an array that is compatible to Probe::uri
844          *
845          * @param string $url profile url
846          * @return array
847          */
848         public static function probeProfile($url)
849         {
850                 $apcontact = APContact::getByURL($url, true);
851                 if (empty($apcontact)) {
852                         return false;
853                 }
854
855                 $profile = ['network' => Protocol::ACTIVITYPUB];
856                 $profile['nick'] = $apcontact['nick'];
857                 $profile['name'] = $apcontact['name'];
858                 $profile['guid'] = $apcontact['uuid'];
859                 $profile['url'] = $apcontact['url'];
860                 $profile['addr'] = $apcontact['addr'];
861                 $profile['alias'] = $apcontact['alias'];
862                 $profile['photo'] = $apcontact['photo'];
863                 // $profile['community']
864                 // $profile['keywords']
865                 // $profile['location']
866                 $profile['about'] = $apcontact['about'];
867                 $profile['batch'] = $apcontact['sharedinbox'];
868                 $profile['notify'] = $apcontact['inbox'];
869                 $profile['poll'] = $apcontact['outbox'];
870                 $profile['pubkey'] = $apcontact['pubkey'];
871                 $profile['baseurl'] = $apcontact['baseurl'];
872
873                 // Remove all "null" fields
874                 foreach ($profile as $field => $content) {
875                         if (is_null($content)) {
876                                 unset($profile[$field]);
877                         }
878                 }
879
880                 return $profile;
881         }
882
883         /**
884          * @brief 
885          *
886          * @param $body
887          * @param $header
888          * @param integer $uid User ID
889          */
890         public static function processInbox($body, $header, $uid)
891         {
892                 $http_signer = HTTPSignature::getSigner($body, $header);
893                 if (empty($http_signer)) {
894                         logger('Invalid HTTP signature, message will be discarded.', LOGGER_DEBUG);
895                         return;
896                 } else {
897                         logger('HTTP signature is signed by ' . $http_signer, LOGGER_DEBUG);
898                 }
899
900                 $activity = json_decode($body, true);
901
902                 $actor = JsonLD::fetchElement($activity, 'actor', 'id');
903                 logger('Message for user ' . $uid . ' is from actor ' . $actor, LOGGER_DEBUG);
904
905                 if (empty($activity)) {
906                         logger('Invalid body.', LOGGER_DEBUG);
907                         return;
908                 }
909
910                 if (LDSignature::isSigned($activity)) {
911                         $ld_signer = LDSignature::getSigner($activity);
912                         if (empty($ld_signer)) {
913                                 logger('Invalid JSON-LD signature from ' . $actor, LOGGER_DEBUG);
914                         }
915                         if (!empty($ld_signer && ($actor == $http_signer))) {
916                                 logger('The HTTP and the JSON-LD signature belong to ' . $ld_signer, LOGGER_DEBUG);
917                                 $trust_source = true;
918                         } elseif (!empty($ld_signer)) {
919                                 logger('JSON-LD signature is signed by ' . $ld_signer, LOGGER_DEBUG);
920                                 $trust_source = true;
921                         } elseif ($actor == $http_signer) {
922                                 logger('Bad JSON-LD signature, but HTTP signer fits the actor.', LOGGER_DEBUG);
923                                 $trust_source = true;
924                         } else {
925                                 logger('Invalid JSON-LD signature and the HTTP signer is different.', LOGGER_DEBUG);
926                                 $trust_source = false;
927                         }
928                 } elseif ($actor == $http_signer) {
929                         logger('Trusting post without JSON-LD signature, The actor fits the HTTP signer.', LOGGER_DEBUG);
930                         $trust_source = true;
931                 } else {
932                         logger('No JSON-LD signature, different actor.', LOGGER_DEBUG);
933                         $trust_source = false;
934                 }
935
936                 self::processActivity($activity, $body, $uid, $trust_source);
937         }
938
939         /**
940          * @brief 
941          *
942          * @param $url
943          * @param integer $uid User ID
944          */
945         public static function fetchOutbox($url, $uid)
946         {
947                 $data = self::fetchContent($url);
948                 if (empty($data)) {
949                         return;
950                 }
951
952                 if (!empty($data['orderedItems'])) {
953                         $items = $data['orderedItems'];
954                 } elseif (!empty($data['first']['orderedItems'])) {
955                         $items = $data['first']['orderedItems'];
956                 } elseif (!empty($data['first'])) {
957                         self::fetchOutbox($data['first'], $uid);
958                         return;
959                 } else {
960                         $items = [];
961                 }
962
963                 foreach ($items as $activity) {
964                         self::processActivity($activity, '', $uid, true);
965                 }
966         }
967
968         /**
969          * @brief 
970          *
971          * @param array $activity
972          * @param integer $uid User ID
973          * @param $trust_source
974          *
975          * @return 
976          */
977         private static function prepareObjectData($activity, $uid, &$trust_source)
978         {
979                 $actor = JsonLD::fetchElement($activity, 'actor', 'id');
980                 if (empty($actor)) {
981                         logger('Empty actor', LOGGER_DEBUG);
982                         return [];
983                 }
984
985                 // Fetch all receivers from to, cc, bto and bcc
986                 $receivers = self::getReceivers($activity, $actor);
987
988                 // When it is a delivery to a personal inbox we add that user to the receivers
989                 if (!empty($uid)) {
990                         $owner = User::getOwnerDataById($uid);
991                         $additional = ['uid:' . $uid => $uid];
992                         $receivers = array_merge($receivers, $additional);
993                 }
994
995                 logger('Receivers: ' . json_encode($receivers), LOGGER_DEBUG);
996
997                 $object_id = JsonLD::fetchElement($activity, 'object', 'id');
998                 if (empty($object_id)) {
999                         logger('No object found', LOGGER_DEBUG);
1000                         return [];
1001                 }
1002
1003                 // Fetch the content only on activities where this matters
1004                 if (in_array($activity['type'], ['Create', 'Announce'])) {
1005                         $object_data = self::fetchObject($object_id, $activity['object'], $trust_source);
1006                         if (empty($object_data)) {
1007                                 logger("Object data couldn't be processed", LOGGER_DEBUG);
1008                                 return [];
1009                         }
1010                         // We had been able to retrieve the object data - so we can trust the source
1011                         $trust_source = true;
1012                 } elseif (in_array($activity['type'], ['Like', 'Dislike'])) {
1013                         // Create a mostly empty array out of the activity data (instead of the object).
1014                         // This way we later don't have to check for the existence of ech individual array element.
1015                         $object_data = self::processObject($activity);
1016                         $object_data['name'] = $activity['type'];
1017                         $object_data['author'] = $activity['actor'];
1018                         $object_data['object'] = $object_id;
1019                         $object_data['object_type'] = ''; // Since we don't fetch the object, we don't know the type
1020                 } else {
1021                         $object_data = [];
1022                         $object_data['id'] = $activity['id'];
1023                         $object_data['object'] = $activity['object'];
1024                         $object_data['object_type'] = JsonLD::fetchElement($activity, 'object', 'type');
1025                 }
1026
1027                 $object_data = self::addActivityFields($object_data, $activity);
1028
1029                 $object_data['type'] = $activity['type'];
1030                 $object_data['owner'] = $actor;
1031                 $object_data['receiver'] = array_merge(defaults($object_data, 'receiver', []), $receivers);
1032
1033                 logger('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], LOGGER_DEBUG);
1034
1035                 return $object_data;
1036         }
1037
1038         /**
1039          * @brief 
1040          *
1041          * @param array $activity
1042          * @param $body
1043          * @param integer $uid User ID
1044          * @param $trust_source
1045          */
1046         private static function processActivity($activity, $body = '', $uid = null, $trust_source = false)
1047         {
1048                 if (empty($activity['type'])) {
1049                         logger('Empty type', LOGGER_DEBUG);
1050                         return;
1051                 }
1052
1053                 if (empty($activity['object'])) {
1054                         logger('Empty object', LOGGER_DEBUG);
1055                         return;
1056                 }
1057
1058                 if (empty($activity['actor'])) {
1059                         logger('Empty actor', LOGGER_DEBUG);
1060                         return;
1061
1062                 }
1063
1064                 // $trust_source is called by reference and is set to true if the content was retrieved successfully
1065                 $object_data = self::prepareObjectData($activity, $uid, $trust_source);
1066                 if (empty($object_data)) {
1067                         logger('No object data found', LOGGER_DEBUG);
1068                         return;
1069                 }
1070
1071                 if (!$trust_source) {
1072                         logger('No trust for activity type "' . $activity['type'] . '", so we quit now.', LOGGER_DEBUG);
1073                 }
1074
1075                 switch ($activity['type']) {
1076                         case 'Create':
1077                         case 'Announce':
1078                                 self::createItem($object_data, $body);
1079                                 break;
1080
1081                         case 'Like':
1082                                 self::likeItem($object_data, $body);
1083                                 break;
1084
1085                         case 'Dislike':
1086                                 self::dislikeItem($object_data, $body);
1087                                 break;
1088
1089                         case 'Update':
1090                                 if (in_array($object_data['object_type'], ['Person', 'Organization', 'Service', 'Group', 'Application'])) {
1091                                         self::updatePerson($object_data, $body);
1092                                 }
1093                                 break;
1094
1095                         case 'Delete':
1096                                 break;
1097
1098                         case 'Follow':
1099                                 self::followUser($object_data);
1100                                 break;
1101
1102                         case 'Accept':
1103                                 if ($object_data['object_type'] == 'Follow') {
1104                                         self::acceptFollowUser($object_data);
1105                                 }
1106                                 break;
1107
1108                         case 'Undo':
1109                                 if ($object_data['object_type'] == 'Follow') {
1110                                         self::undoFollowUser($object_data);
1111                                 } elseif (in_array($object_data['object_type'], ['Like', 'Dislike', 'Accept', 'Reject', 'TentativeAccept'])) {
1112                                         self::undoActivity($object_data);
1113                                 }
1114                                 break;
1115
1116                         default:
1117                                 logger('Unknown activity: ' . $activity['type'], LOGGER_DEBUG);
1118                                 break;
1119                 }
1120         }
1121
1122         /**
1123          * @brief 
1124          *
1125          * @param array $activity
1126          * @param $actor
1127          *
1128          * @return 
1129          */
1130         private static function getReceivers($activity, $actor)
1131         {
1132                 $receivers = [];
1133
1134                 // When it is an answer, we inherite the receivers from the parent
1135                 $replyto = JsonLD::fetchElement($activity, 'inReplyTo', 'id');
1136                 if (!empty($replyto)) {
1137                         $parents = Item::select(['uid'], ['uri' => $replyto]);
1138                         while ($parent = Item::fetch($parents)) {
1139                                 $receivers['uid:' . $parent['uid']] = $parent['uid'];
1140                         }
1141                 }
1142
1143                 if (!empty($actor)) {
1144                         $profile = APContact::getByURL($actor);
1145                         $followers = defaults($profile, 'followers', '');
1146
1147                         logger('Actor: ' . $actor . ' - Followers: ' . $followers, LOGGER_DEBUG);
1148                 } else {
1149                         logger('Empty actor', LOGGER_DEBUG);
1150                         $followers = '';
1151                 }
1152
1153                 foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
1154                         if (empty($activity[$element])) {
1155                                 continue;
1156                         }
1157
1158                         // The receiver can be an array or a string
1159                         if (is_string($activity[$element])) {
1160                                 $activity[$element] = [$activity[$element]];
1161                         }
1162
1163                         foreach ($activity[$element] as $receiver) {
1164                                 if ($receiver == self::PUBLIC_COLLECTION) {
1165                                         $receivers['uid:0'] = 0;
1166                                 }
1167
1168                                 if (($receiver == self::PUBLIC_COLLECTION) && !empty($actor)) {
1169                                         // This will most likely catch all OStatus connections to Mastodon
1170                                         $condition = ['alias' => [$actor, normalise_link($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND]
1171                                                 , 'archive' => false, 'pending' => false];
1172                                         $contacts = DBA::select('contact', ['uid'], $condition);
1173                                         while ($contact = DBA::fetch($contacts)) {
1174                                                 if ($contact['uid'] != 0) {
1175                                                         $receivers['uid:' . $contact['uid']] = $contact['uid'];
1176                                                 }
1177                                         }
1178                                         DBA::close($contacts);
1179                                 }
1180
1181                                 if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) {
1182                                         $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
1183                                                 'network' => Protocol::ACTIVITYPUB, 'archive' => false, 'pending' => false];
1184                                         $contacts = DBA::select('contact', ['uid'], $condition);
1185                                         while ($contact = DBA::fetch($contacts)) {
1186                                                 if ($contact['uid'] != 0) {
1187                                                         $receivers['uid:' . $contact['uid']] = $contact['uid'];
1188                                                 }
1189                                         }
1190                                         DBA::close($contacts);
1191                                         continue;
1192                                 }
1193
1194                                 $condition = ['self' => true, 'nurl' => normalise_link($receiver)];
1195                                 $contact = DBA::selectFirst('contact', ['uid'], $condition);
1196                                 if (!DBA::isResult($contact)) {
1197                                         continue;
1198                                 }
1199                                 $receivers['uid:' . $contact['uid']] = $contact['uid'];
1200                         }
1201                 }
1202
1203                 self::switchContacts($receivers, $actor);
1204
1205                 return $receivers;
1206         }
1207
1208         /**
1209          * @brief 
1210          *
1211          * @param $cid
1212          * @param integer $uid User ID
1213          * @param $url
1214          */
1215         private static function switchContact($cid, $uid, $url)
1216         {
1217                 $profile = ActivityPub::probeProfile($url);
1218                 if (empty($profile)) {
1219                         return;
1220                 }
1221
1222                 logger('Switch contact ' . $cid . ' (' . $profile['url'] . ') for user ' . $uid . ' from OStatus to ActivityPub');
1223
1224                 $photo = $profile['photo'];
1225                 unset($profile['photo']);
1226                 unset($profile['baseurl']);
1227
1228                 $profile['nurl'] = normalise_link($profile['url']);
1229                 DBA::update('contact', $profile, ['id' => $cid]);
1230
1231                 Contact::updateAvatar($photo, $uid, $cid);
1232         }
1233
1234         /**
1235          * @brief 
1236          *
1237          * @param $receivers
1238          * @param $actor
1239          */
1240         private static function switchContacts($receivers, $actor)
1241         {
1242                 if (empty($actor)) {
1243                         return;
1244                 }
1245
1246                 foreach ($receivers as $receiver) {
1247                         $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'nurl' => normalise_link($actor)]);
1248                         if (DBA::isResult($contact)) {
1249                                 self::switchContact($contact['id'], $receiver, $actor);
1250                         }
1251
1252                         $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'alias' => [normalise_link($actor), $actor]]);
1253                         if (DBA::isResult($contact)) {
1254                                 self::switchContact($contact['id'], $receiver, $actor);
1255                         }
1256                 }
1257         }
1258
1259         /**
1260          * @brief 
1261          *
1262          * @param $object_data
1263          * @param array $activity
1264          *
1265          * @return 
1266          */
1267         private static function addActivityFields($object_data, $activity)
1268         {
1269                 if (!empty($activity['published']) && empty($object_data['published'])) {
1270                         $object_data['published'] = $activity['published'];
1271                 }
1272
1273                 if (!empty($activity['updated']) && empty($object_data['updated'])) {
1274                         $object_data['updated'] = $activity['updated'];
1275                 }
1276
1277                 if (!empty($activity['inReplyTo']) && empty($object_data['parent-uri'])) {
1278                         $object_data['parent-uri'] = JsonLD::fetchElement($activity, 'inReplyTo', 'id');
1279                 }
1280
1281                 if (!empty($activity['instrument'])) {
1282                         $object_data['service'] = JsonLD::fetchElement($activity, 'instrument', 'name', 'type', 'Service');
1283                 }
1284                 return $object_data;
1285         }
1286
1287         /**
1288          * @brief 
1289          *
1290          * @param $object_id
1291          * @param $object
1292          * @param $trust_source
1293          *
1294          * @return 
1295          */
1296         private static function fetchObject($object_id, $object = [], $trust_source = false)
1297         {
1298                 if (!$trust_source || is_string($object)) {
1299                         $data = self::fetchContent($object_id);
1300                         if (empty($data)) {
1301                                 logger('Empty content for ' . $object_id . ', check if content is available locally.', LOGGER_DEBUG);
1302                                 $data = $object_id;
1303                         } else {
1304                                 logger('Fetched content for ' . $object_id, LOGGER_DEBUG);
1305                         }
1306                 } else {
1307                         logger('Using original object for url ' . $object_id, LOGGER_DEBUG);
1308                         $data = $object;
1309                 }
1310
1311                 if (is_string($data)) {
1312                         $item = Item::selectFirst([], ['uri' => $data]);
1313                         if (!DBA::isResult($item)) {
1314                                 logger('Object with url ' . $data . ' was not found locally.', LOGGER_DEBUG);
1315                                 return false;
1316                         }
1317                         logger('Using already stored item for url ' . $object_id, LOGGER_DEBUG);
1318                         $data = self::createNote($item);
1319                 }
1320
1321                 if (empty($data['type'])) {
1322                         logger('Empty type', LOGGER_DEBUG);
1323                         return false;
1324                 }
1325
1326                 switch ($data['type']) {
1327                         case 'Note':
1328                         case 'Article':
1329                         case 'Video':
1330                                 return self::processObject($data);
1331
1332                         case 'Announce':
1333                                 if (empty($data['object'])) {
1334                                         return false;
1335                                 }
1336                                 return self::fetchObject($data['object']);
1337
1338                         case 'Person':
1339                         case 'Tombstone':
1340                                 break;
1341
1342                         default:
1343                                 logger('Unknown object type: ' . $data['type'], LOGGER_DEBUG);
1344                                 break;
1345                 }
1346         }
1347
1348         /**
1349          * @brief 
1350          *
1351          * @param $object
1352          *
1353          * @return 
1354          */
1355         private static function processObject(&$object)
1356         {
1357                 if (empty($object['id'])) {
1358                         return false;
1359                 }
1360
1361                 $object_data = [];
1362                 $object_data['object_type'] = $object['type'];
1363                 $object_data['id'] = $object['id'];
1364
1365                 if (!empty($object['inReplyTo'])) {
1366                         $object_data['reply-to-id'] = JsonLD::fetchElement($object, 'inReplyTo', 'id');
1367                 } else {
1368                         $object_data['reply-to-id'] = $object_data['id'];
1369                 }
1370
1371                 $object_data['published'] = defaults($object, 'published', null);
1372                 $object_data['updated'] = defaults($object, 'updated', $object_data['published']);
1373
1374                 if (empty($object_data['published']) && !empty($object_data['updated'])) {
1375                         $object_data['published'] = $object_data['updated'];
1376                 }
1377
1378                 $actor = JsonLD::fetchElement($object, 'attributedTo', 'id');
1379                 if (empty($actor)) {
1380                         $actor = defaults($object, 'actor', null);
1381                 }
1382
1383                 $object_data['diaspora:guid'] = defaults($object, 'diaspora:guid', null);
1384                 $object_data['owner'] = $object_data['author'] = $actor;
1385                 $object_data['context'] = defaults($object, 'context', null);
1386                 $object_data['conversation'] = defaults($object, 'conversation', null);
1387                 $object_data['sensitive'] = defaults($object, 'sensitive', null);
1388                 $object_data['name'] = defaults($object, 'title', null);
1389                 $object_data['name'] = defaults($object, 'name', $object_data['name']);
1390                 $object_data['summary'] = defaults($object, 'summary', null);
1391                 $object_data['content'] = defaults($object, 'content', null);
1392                 $object_data['source'] = defaults($object, 'source', null);
1393                 $object_data['location'] = JsonLD::fetchElement($object, 'location', 'name', 'type', 'Place');
1394                 $object_data['attachments'] = defaults($object, 'attachment', null);
1395                 $object_data['tags'] = defaults($object, 'tag', null);
1396                 $object_data['service'] = JsonLD::fetchElement($object, 'instrument', 'name', 'type', 'Service');
1397                 $object_data['alternate-url'] = JsonLD::fetchElement($object, 'url', 'href');
1398                 $object_data['receiver'] = self::getReceivers($object, $object_data['owner']);
1399
1400                 // Common object data:
1401
1402                 // Unhandled
1403                 // @context, type, actor, signature, mediaType, duration, replies, icon
1404
1405                 // Also missing: (Defined in the standard, but currently unused)
1406                 // audience, preview, endTime, startTime, generator, image
1407
1408                 // Data in Notes:
1409
1410                 // Unhandled
1411                 // contentMap, announcement_count, announcements, context_id, likes, like_count
1412                 // inReplyToStatusId, shares, quoteUrl, statusnetConversationId
1413
1414                 // Data in video:
1415
1416                 // To-Do?
1417                 // category, licence, language, commentsEnabled
1418
1419                 // Unhandled
1420                 // views, waitTranscoding, state, support, subtitleLanguage
1421                 // likes, dislikes, shares, comments
1422
1423                 return $object_data;
1424         }
1425
1426         /**
1427          * @brief Converts mentions from Pleroma into the Friendica format
1428          *
1429          * @param string $body
1430          *
1431          * @return converted body
1432          */
1433         private static function convertMentions($body)
1434         {
1435                 $URLSearchString = "^\[\]";
1436                 $body = preg_replace("/\[url\=([$URLSearchString]*)\]([#@!])(.*?)\[\/url\]/ism", '$2[url=$1]$3[/url]', $body);
1437
1438                 return $body;
1439         }
1440
1441         /**
1442          * @brief Constructs a string with tags for a given tag array
1443          *
1444          * @param array $tags
1445          * @param boolean $sensitive
1446          *
1447          * @return string with tags
1448          */
1449         private static function constructTagList($tags, $sensitive)
1450         {
1451                 if (empty($tags)) {
1452                         return '';
1453                 }
1454
1455                 $tag_text = '';
1456                 foreach ($tags as $tag) {
1457                         if (in_array($tag['type'], ['Mention', 'Hashtag'])) {
1458                                 if (!empty($tag_text)) {
1459                                         $tag_text .= ',';
1460                                 }
1461
1462                                 $tag_text .= substr($tag['name'], 0, 1) . '[url=' . $tag['href'] . ']' . substr($tag['name'], 1) . '[/url]';
1463                         }
1464                 }
1465
1466                 /// @todo add nsfw for $sensitive
1467
1468                 return $tag_text;
1469         }
1470
1471         /**
1472          * @brief 
1473          *
1474          * @param $attachments
1475          * @param array $item
1476          *
1477          * @return item array
1478          */
1479         private static function constructAttachList($attachments, $item)
1480         {
1481                 if (empty($attachments)) {
1482                         return $item;
1483                 }
1484
1485                 foreach ($attachments as $attach) {
1486                         $filetype = strtolower(substr($attach['mediaType'], 0, strpos($attach['mediaType'], '/')));
1487                         if ($filetype == 'image') {
1488                                 $item['body'] .= "\n[img]".$attach['url'].'[/img]';
1489                         } else {
1490                                 if (!empty($item["attach"])) {
1491                                         $item["attach"] .= ',';
1492                                 } else {
1493                                         $item["attach"] = '';
1494                                 }
1495                                 if (!isset($attach['length'])) {
1496                                         $attach['length'] = "0";
1497                                 }
1498                                 $item["attach"] .= '[attach]href="'.$attach['url'].'" length="'.$attach['length'].'" type="'.$attach['mediaType'].'" title="'.defaults($attach, 'name', '').'"[/attach]';
1499                         }
1500                 }
1501
1502                 return $item;
1503         }
1504
1505         /**
1506          * @brief 
1507          *
1508          * @param array $activity
1509          * @param $body
1510          */
1511         private static function createItem($activity, $body)
1512         {
1513                 $item = [];
1514                 $item['verb'] = ACTIVITY_POST;
1515                 $item['parent-uri'] = $activity['reply-to-id'];
1516
1517                 if ($activity['reply-to-id'] == $activity['id']) {
1518                         $item['gravity'] = GRAVITY_PARENT;
1519                         $item['object-type'] = ACTIVITY_OBJ_NOTE;
1520                 } else {
1521                         $item['gravity'] = GRAVITY_COMMENT;
1522                         $item['object-type'] = ACTIVITY_OBJ_COMMENT;
1523                 }
1524
1525                 if (($activity['id'] != $activity['reply-to-id']) && !Item::exists(['uri' => $activity['reply-to-id']])) {
1526                         logger('Parent ' . $activity['reply-to-id'] . ' not found. Try to refetch it.');
1527                         self::fetchMissingActivity($activity['reply-to-id'], $activity);
1528                 }
1529
1530                 self::postItem($activity, $item, $body);
1531         }
1532
1533         /**
1534          * @brief 
1535          *
1536          * @param array $activity
1537          * @param $body
1538          */
1539         private static function likeItem($activity, $body)
1540         {
1541                 $item = [];
1542                 $item['verb'] = ACTIVITY_LIKE;
1543                 $item['parent-uri'] = $activity['object'];
1544                 $item['gravity'] = GRAVITY_ACTIVITY;
1545                 $item['object-type'] = ACTIVITY_OBJ_NOTE;
1546
1547                 self::postItem($activity, $item, $body);
1548         }
1549
1550         /**
1551          * @brief 
1552          *
1553          * @param array $activity
1554          * @param $body
1555          */
1556         private static function dislikeItem($activity, $body)
1557         {
1558                 $item = [];
1559                 $item['verb'] = ACTIVITY_DISLIKE;
1560                 $item['parent-uri'] = $activity['object'];
1561                 $item['gravity'] = GRAVITY_ACTIVITY;
1562                 $item['object-type'] = ACTIVITY_OBJ_NOTE;
1563
1564                 self::postItem($activity, $item, $body);
1565         }
1566
1567         /**
1568          * @brief 
1569          *
1570          * @param array $activity
1571          * @param array $item
1572          * @param $body
1573          */
1574         private static function postItem($activity, $item, $body)
1575         {
1576                 /// @todo What to do with $activity['context']?
1577
1578                 if (($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['parent-uri']])) {
1579                         logger('Parent ' . $item['parent-uri'] . ' not found, message will be discarded.', LOGGER_DEBUG);
1580                         return;
1581                 }
1582
1583                 $item['network'] = Protocol::ACTIVITYPUB;
1584                 $item['private'] = !in_array(0, $activity['receiver']);
1585                 $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true);
1586                 $item['owner-id'] = Contact::getIdForURL($activity['owner'], 0, true);
1587                 $item['uri'] = $activity['id'];
1588                 $item['created'] = $activity['published'];
1589                 $item['edited'] = $activity['updated'];
1590                 $item['guid'] = $activity['diaspora:guid'];
1591                 $item['title'] = HTML::toBBCode($activity['name']);
1592                 $item['content-warning'] = HTML::toBBCode($activity['summary']);
1593                 $item['body'] = self::convertMentions(HTML::toBBCode($activity['content']));
1594                 $item['location'] = $activity['location'];
1595                 $item['tag'] = self::constructTagList($activity['tags'], $activity['sensitive']);
1596                 $item['app'] = $activity['service'];
1597                 $item['plink'] = defaults($activity, 'alternate-url', $item['uri']);
1598
1599                 $item = self::constructAttachList($activity['attachments'], $item);
1600
1601                 $source = JsonLD::fetchElement($activity, 'source', 'content', 'mediaType', 'text/bbcode');
1602                 if (!empty($source)) {
1603                         $item['body'] = $source;
1604                 }
1605
1606                 $item['protocol'] = Conversation::PARCEL_ACTIVITYPUB;
1607                 $item['source'] = $body;
1608                 $item['conversation-href'] = $activity['context'];
1609                 $item['conversation-uri'] = $activity['conversation'];
1610
1611                 foreach ($activity['receiver'] as $receiver) {
1612                         $item['uid'] = $receiver;
1613                         $item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, true);
1614
1615                         if (($receiver != 0) && empty($item['contact-id'])) {
1616                                 $item['contact-id'] = Contact::getIdForURL($activity['author'], 0, true);
1617                         }
1618
1619                         $item_id = Item::insert($item);
1620                         logger('Storing for user ' . $item['uid'] . ': ' . $item_id);
1621                 }
1622         }
1623
1624         /**
1625          * @brief 
1626          *
1627          * @param $url
1628          * @param $child
1629          */
1630         private static function fetchMissingActivity($url, $child)
1631         {
1632                 if (Config::get('system', 'ostatus_full_threads')) {
1633                         return;
1634                 }
1635
1636                 $object = ActivityPub::fetchContent($url);
1637                 if (empty($object)) {
1638                         logger('Activity ' . $url . ' was not fetchable, aborting.');
1639                         return;
1640                 }
1641
1642                 $activity = [];
1643                 $activity['@context'] = $object['@context'];
1644                 unset($object['@context']);
1645                 $activity['id'] = $object['id'];
1646                 $activity['to'] = defaults($object, 'to', []);
1647                 $activity['cc'] = defaults($object, 'cc', []);
1648                 $activity['actor'] = $child['author'];
1649                 $activity['object'] = $object;
1650                 $activity['published'] = $object['published'];
1651                 $activity['type'] = 'Create';
1652
1653                 self::processActivity($activity);
1654                 logger('Activity ' . $url . ' had been fetched and processed.');
1655         }
1656
1657         /**
1658          * @brief perform a "follow" request
1659          *
1660          * @param array $activity
1661          */
1662         private static function followUser($activity)
1663         {
1664                 $actor = JsonLD::fetchElement($activity, 'object', 'id');
1665                 $uid = User::getIdForURL($actor);
1666                 if (empty($uid)) {
1667                         return;
1668                 }
1669
1670                 $owner = User::getOwnerDataById($uid);
1671
1672                 $cid = Contact::getIdForURL($activity['owner'], $uid);
1673                 if (!empty($cid)) {
1674                         $contact = DBA::selectFirst('contact', [], ['id' => $cid]);
1675                 } else {
1676                         $contact = false;
1677                 }
1678
1679                 $item = ['author-id' => Contact::getIdForURL($activity['owner']),
1680                         'author-link' => $activity['owner']];
1681
1682                 Contact::addRelationship($owner, $contact, $item);
1683                 $cid = Contact::getIdForURL($activity['owner'], $uid);
1684                 if (empty($cid)) {
1685                         return;
1686                 }
1687
1688                 $contact = DBA::selectFirst('contact', ['network'], ['id' => $cid]);
1689                 if ($contact['network'] != Protocol::ACTIVITYPUB) {
1690                         Contact::updateFromProbe($cid, Protocol::ACTIVITYPUB);
1691                 }
1692
1693                 DBA::update('contact', ['hub-verify' => $activity['id']], ['id' => $cid]);
1694                 logger('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']);
1695         }
1696
1697         /**
1698          * @brief Update the given profile
1699          *
1700          * @param array $activity
1701          */
1702         private static function updatePerson($activity)
1703         {
1704                 if (empty($activity['object']['id'])) {
1705                         return;
1706                 }
1707
1708                 logger('Updating profile for ' . $activity['object']['id'], LOGGER_DEBUG);
1709                 APContact::getByURL($activity['object']['id'], true);
1710         }
1711
1712         /**
1713          * @brief Accept a follow request
1714          *
1715          * @param array $activity
1716          */
1717         private static function acceptFollowUser($activity)
1718         {
1719                 $actor = JsonLD::fetchElement($activity, 'object', 'actor');
1720                 $uid = User::getIdForURL($actor);
1721                 if (empty($uid)) {
1722                         return;
1723                 }
1724
1725                 $owner = User::getOwnerDataById($uid);
1726
1727                 $cid = Contact::getIdForURL($activity['owner'], $uid);
1728                 if (empty($cid)) {
1729                         logger('No contact found for ' . $activity['owner'], LOGGER_DEBUG);
1730                         return;
1731                 }
1732
1733                 $fields = ['pending' => false];
1734
1735                 $contact = DBA::selectFirst('contact', ['rel'], ['id' => $cid]);
1736                 if ($contact['rel'] == Contact::FOLLOWER) {
1737                         $fields['rel'] = Contact::FRIEND;
1738                 }
1739
1740                 $condition = ['id' => $cid];
1741                 DBA::update('contact', $fields, $condition);
1742                 logger('Accept contact request from contact ' . $cid . ' for user ' . $uid, LOGGER_DEBUG);
1743         }
1744
1745         /**
1746          * @brief Undo activity like "like" or "dislike"
1747          *
1748          * @param array $activity
1749          */
1750         private static function undoActivity($activity)
1751         {
1752                 $activity_url = JsonLD::fetchElement($activity, 'object', 'id');
1753                 if (empty($activity_url)) {
1754                         return;
1755                 }
1756
1757                 $actor = JsonLD::fetchElement($activity, 'object', 'actor');
1758                 if (empty($actor)) {
1759                         return;
1760                 }
1761
1762                 $author_id = Contact::getIdForURL($actor);
1763                 if (empty($author_id)) {
1764                         return;
1765                 }
1766
1767                 Item::delete(['uri' => $activity_url, 'author-id' => $author_id, 'gravity' => GRAVITY_ACTIVITY]);
1768         }
1769
1770         /**
1771          * @brief Activity to remove a follower
1772          *
1773          * @param array $activity
1774          */
1775         private static function undoFollowUser($activity)
1776         {
1777                 $object = JsonLD::fetchElement($activity, 'object', 'object');
1778                 $uid = User::getIdForURL($object);
1779                 if (empty($uid)) {
1780                         return;
1781                 }
1782
1783                 $owner = User::getOwnerDataById($uid);
1784
1785                 $cid = Contact::getIdForURL($activity['owner'], $uid);
1786                 if (empty($cid)) {
1787                         logger('No contact found for ' . $activity['owner'], LOGGER_DEBUG);
1788                         return;
1789                 }
1790
1791                 $contact = DBA::selectFirst('contact', [], ['id' => $cid]);
1792                 if (!DBA::isResult($contact)) {
1793                         return;
1794                 }
1795
1796                 Contact::removeFollower($owner, $contact);
1797                 logger('Undo following request from contact ' . $cid . ' for user ' . $uid, LOGGER_DEBUG);
1798         }
1799 }