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