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