]> git.mxchange.org Git - friendica.git/blob - src/Protocol/ActivityPub/Receiver.php
New post class in protocol and worker classes
[friendica.git] / src / Protocol / ActivityPub / Receiver.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Protocol\ActivityPub;
23
24 use Friendica\Content\Text\BBCode;
25 use Friendica\Database\DBA;
26 use Friendica\Content\Text\HTML;
27 use Friendica\Content\Text\Markdown;
28 use Friendica\Core\Logger;
29 use Friendica\Core\Protocol;
30 use Friendica\Model\Contact;
31 use Friendica\Model\APContact;
32 use Friendica\Model\Item;
33 use Friendica\Model\Post;
34 use Friendica\Model\User;
35 use Friendica\Protocol\Activity;
36 use Friendica\Protocol\ActivityPub;
37 use Friendica\Util\HTTPSignature;
38 use Friendica\Util\JsonLD;
39 use Friendica\Util\LDSignature;
40 use Friendica\Util\Strings;
41
42 /**
43  * ActivityPub Receiver Protocol class
44  *
45  * To-Do:
46  * @todo Undo Announce
47  *
48  * Check what this is meant to do:
49  * - Add
50  * - Block
51  * - Flag
52  * - Remove
53  * - Undo Block
54  */
55 class Receiver
56 {
57         const PUBLIC_COLLECTION = 'as:Public';
58         const ACCOUNT_TYPES = ['as:Person', 'as:Organization', 'as:Service', 'as:Group', 'as:Application'];
59         const CONTENT_TYPES = ['as:Note', 'as:Article', 'as:Video', 'as:Image', 'as:Event', 'as:Audio'];
60         const ACTIVITY_TYPES = ['as:Like', 'as:Dislike', 'as:Accept', 'as:Reject', 'as:TentativeAccept'];
61
62         const TARGET_UNKNOWN = 0;
63         const TARGET_TO = 1;
64         const TARGET_CC = 2;
65         const TARGET_BTO = 3;
66         const TARGET_BCC = 4;
67         const TARGET_FOLLOWER = 5;
68         const TARGET_ANSWER = 6;
69         const TARGET_GLOBAL = 7;
70
71         /**
72          * Checks if the web request is done for the AP protocol
73          *
74          * @return bool is it AP?
75          */
76         public static function isRequest()
77         {
78                 return stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/activity+json') ||
79                         stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/ld+json');
80         }
81
82         /**
83          * Checks incoming message from the inbox
84          *
85          * @param         $body
86          * @param         $header
87          * @param integer $uid User ID
88          * @throws \Exception
89          */
90         public static function processInbox($body, $header, $uid)
91         {
92                 $activity = json_decode($body, true);
93                 if (empty($activity)) {
94                         Logger::warning('Invalid body.');
95                         return;
96                 }
97
98                 $ldactivity = JsonLD::compact($activity);
99
100                 $actor = JsonLD::fetchElement($ldactivity, 'as:actor', '@id');
101
102                 $apcontact = APContact::getByURL($actor);
103                 if (empty($apcontact)) {
104                         Logger::notice('Unable to retrieve AP contact for actor', ['actor' => $actor]);
105                 } elseif ($apcontact['type'] == 'Application' && $apcontact['nick'] == 'relay') {
106                         self::processRelayPost($ldactivity, $actor);
107                         return;
108                 } else {
109                         APContact::unmarkForArchival($apcontact);
110                 }
111
112                 $http_signer = HTTPSignature::getSigner($body, $header);
113                 if (empty($http_signer)) {
114                         Logger::warning('Invalid HTTP signature, message will be discarded.');
115                         return;
116                 } else {
117                         Logger::info('Valid HTTP signature', ['signer' => $http_signer]);
118                 }
119
120                 $signer = [$http_signer];
121
122                 Logger::info('Message for user ' . $uid . ' is from actor ' . $actor);
123
124                 if (LDSignature::isSigned($activity)) {
125                         $ld_signer = LDSignature::getSigner($activity);
126                         if (empty($ld_signer)) {
127                                 Logger::log('Invalid JSON-LD signature from ' . $actor, Logger::DEBUG);
128                         } elseif ($ld_signer != $http_signer) {
129                                 $signer[] = $ld_signer;
130                         }
131                         if (!empty($ld_signer && ($actor == $http_signer))) {
132                                 Logger::log('The HTTP and the JSON-LD signature belong to ' . $ld_signer, Logger::DEBUG);
133                                 $trust_source = true;
134                         } elseif (!empty($ld_signer)) {
135                                 Logger::log('JSON-LD signature is signed by ' . $ld_signer, Logger::DEBUG);
136                                 $trust_source = true;
137                         } elseif ($actor == $http_signer) {
138                                 Logger::log('Bad JSON-LD signature, but HTTP signer fits the actor.', Logger::DEBUG);
139                                 $trust_source = true;
140                         } else {
141                                 Logger::log('Invalid JSON-LD signature and the HTTP signer is different.', Logger::DEBUG);
142                                 $trust_source = false;
143                         }
144                 } elseif ($actor == $http_signer) {
145                         Logger::log('Trusting post without JSON-LD signature, The actor fits the HTTP signer.', Logger::DEBUG);
146                         $trust_source = true;
147                 } else {
148                         Logger::log('No JSON-LD signature, different actor.', Logger::DEBUG);
149                         $trust_source = false;
150                 }
151
152                 self::processActivity($ldactivity, $body, $uid, $trust_source, true, $signer);
153         }
154
155         /**
156          * Process incoming posts from relays
157          *
158          * @param array  $activity
159          * @param string $actor
160          * @return void
161          */
162         private static function processRelayPost(array $activity, string $actor)
163         {
164                 $type = JsonLD::fetchElement($activity, '@type');
165                 if (!$type) {
166                         Logger::info('Empty type', ['activity' => $activity]);
167                         return;
168                 }
169
170                 if ($type != 'as:Announce') {
171                         Logger::info('Not an announcement', ['activity' => $activity]);
172                         return;
173                 }
174
175                 $object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
176                 if (empty($object_id)) {
177                         Logger::info('No object id found', ['activity' => $activity]);
178                         return;
179                 }
180
181                 $contact = Contact::getByURL($actor);
182                 if (empty($contact)) {
183                         Logger::info('Relay contact not found', ['actor' => $actor]);
184                         return;
185                 }
186
187                 if (!in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) {
188                         Logger::notice('Relay is no sharer', ['actor' => $actor]);
189                         return;
190                 }
191
192                 Logger::info('Got relayed message id', ['id' => $object_id]);
193
194                 $item_id = Item::searchByLink($object_id);
195                 if ($item_id) {
196                         Logger::info('Relayed message already exists', ['id' => $object_id, 'item' => $item_id]);
197                         return;
198                 }
199
200                 $id = Processor::fetchMissingActivity($object_id, [], $actor);
201                 if (empty($id)) {
202                         Logger::notice('Relayed message had not been fetched', ['id' => $object_id]);
203                         return;
204                 }
205
206                 $item_id = Item::searchByLink($object_id);
207                 if ($item_id) {
208                         Logger::info('Relayed message had been fetched and stored', ['id' => $object_id, 'item' => $item_id]);
209                 } else {
210                         Logger::notice('Relayed message had not been stored', ['id' => $object_id]);
211                 }
212         }
213
214         /**
215          * Fetches the object type for a given object id
216          *
217          * @param array   $activity
218          * @param string  $object_id Object ID of the the provided object
219          * @param integer $uid       User ID
220          *
221          * @return string with object type
222          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
223          * @throws \ImagickException
224          */
225         private static function fetchObjectType($activity, $object_id, $uid = 0)
226         {
227                 if (!empty($activity['as:object'])) {
228                         $object_type = JsonLD::fetchElement($activity['as:object'], '@type');
229                         if (!empty($object_type)) {
230                                 return $object_type;
231                         }
232                 }
233
234                 if (Post::exists(['uri' => $object_id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]])) {
235                         // We just assume "note" since it doesn't make a difference for the further processing
236                         return 'as:Note';
237                 }
238
239                 $profile = APContact::getByURL($object_id);
240                 if (!empty($profile['type'])) {
241                         APContact::unmarkForArchival($profile);
242                         return 'as:' . $profile['type'];
243                 }
244
245                 $data = ActivityPub::fetchContent($object_id, $uid);
246                 if (!empty($data)) {
247                         $object = JsonLD::compact($data);
248                         $type = JsonLD::fetchElement($object, '@type');
249                         if (!empty($type)) {
250                                 return $type;
251                         }
252                 }
253
254                 return null;
255         }
256
257         /**
258          * Prepare the object array
259          *
260          * @param array   $activity     Array with activity data
261          * @param integer $uid          User ID
262          * @param boolean $push         Message had been pushed to our system
263          * @param boolean $trust_source Do we trust the source?
264          *
265          * @return array with object data
266          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
267          * @throws \ImagickException
268          */
269         public static function prepareObjectData($activity, $uid, $push, &$trust_source)
270         {
271                 $id = JsonLD::fetchElement($activity, '@id');
272                 if (!empty($id) && !$trust_source) {
273                         $fetched_activity = ActivityPub::fetchContent($id, $uid ?? 0);
274                         if (!empty($fetched_activity)) {
275                                 $object = JsonLD::compact($fetched_activity);
276                                 $fetched_id = JsonLD::fetchElement($object, '@id');
277                                 if ($fetched_id == $id) {
278                                         Logger::info('Activity had been fetched successfully', ['id' => $id]);
279                                         $trust_source = true;
280                                         $activity = $object;
281                                 } else {
282                                         Logger::info('Activity id is not equal', ['id' => $id, 'fetched' => $fetched_id]);
283                                 }
284                         } else {
285                                 Logger::info('Activity could not been fetched', ['id' => $id]);
286                         }
287                 }
288
289                 $actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
290                 if (empty($actor)) {
291                         Logger::info('Empty actor', ['activity' => $activity]);
292                         return [];
293                 }
294
295                 $type = JsonLD::fetchElement($activity, '@type');
296
297                 // Fetch all receivers from to, cc, bto and bcc
298                 $receiverdata = self::getReceivers($activity, $actor);
299                 $receivers = $reception_types = [];
300                 foreach ($receiverdata as $key => $data) {
301                         $receivers[$key] = $data['uid'];
302                         $reception_types[$data['uid']] = $data['type'] ?? self::TARGET_UNKNOWN;
303                 }
304
305                 // When it is a delivery to a personal inbox we add that user to the receivers
306                 if (!empty($uid)) {
307                         $additional = [$uid => $uid];
308                         $receivers = array_replace($receivers, $additional);
309                         if (empty($activity['thread-completion']) && (empty($reception_types[$uid]) || in_array($reception_types[$uid], [self::TARGET_UNKNOWN, self::TARGET_FOLLOWER, self::TARGET_ANSWER, self::TARGET_GLOBAL]))) {
310                                 $reception_types[$uid] = self::TARGET_BCC;
311                         }
312                 } else {
313                         // We possibly need some user to fetch private content,
314                         // so we fetch the first out ot the list.
315                         $uid = self::getFirstUserFromReceivers($receivers);
316                 }
317
318                 $object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
319                 if (empty($object_id)) {
320                         Logger::log('No object found', Logger::DEBUG);
321                         return [];
322                 }
323
324                 if (!is_string($object_id)) {
325                         Logger::info('Invalid object id', ['object' => $object_id]);
326                         return [];
327                 }
328
329                 $object_type = self::fetchObjectType($activity, $object_id, $uid);
330
331                 // Fetch the content only on activities where this matters
332                 if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) {
333                         // Always fetch on "Announce"
334                         $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source && ($type != 'as:Announce'), $uid);
335                         if (empty($object_data)) {
336                                 Logger::log("Object data couldn't be processed", Logger::DEBUG);
337                                 return [];
338                         }
339
340                         $object_data['object_id'] = $object_id;
341
342                         if ($type == 'as:Announce') {
343                                 $object_data['push'] = false;
344                         } else {
345                                 $object_data['push'] = $push;
346                         }
347
348                         // Test if it is an answer to a mail
349                         if (DBA::exists('mail', ['uri' => $object_data['reply-to-id']])) {
350                                 $object_data['directmessage'] = true;
351                         } else {
352                                 $object_data['directmessage'] = JsonLD::fetchElement($activity, 'litepub:directMessage');
353                         }
354                 } elseif (in_array($type, array_merge(self::ACTIVITY_TYPES, ['as:Follow'])) && in_array($object_type, self::CONTENT_TYPES)) {
355                         // Create a mostly empty array out of the activity data (instead of the object).
356                         // This way we later don't have to check for the existence of ech individual array element.
357                         $object_data = self::processObject($activity);
358                         $object_data['name'] = $type;
359                         $object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id');
360                         $object_data['object_id'] = $object_id;
361                         $object_data['object_type'] = ''; // Since we don't fetch the object, we don't know the type
362                         $object_data['push'] = $push;
363                 } elseif (in_array($type, ['as:Add'])) {
364                         $object_data = [];
365                         $object_data['id'] = JsonLD::fetchElement($activity, '@id');
366                         $object_data['target_id'] = JsonLD::fetchElement($activity, 'as:target', '@id');
367                         $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id');
368                         $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type');
369                         $object_data['object_content'] = JsonLD::fetchElement($activity['as:object'], 'as:content', '@type');
370                         $object_data['push'] = $push;
371                 } else {
372                         $object_data = [];
373                         $object_data['id'] = JsonLD::fetchElement($activity, '@id');
374                         $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id');
375                         $object_data['object_actor'] = JsonLD::fetchElement($activity['as:object'], 'as:actor', '@id');
376                         $object_data['object_object'] = JsonLD::fetchElement($activity['as:object'], 'as:object');
377                         $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type');
378                         $object_data['push'] = $push;
379
380                         // An Undo is done on the object of an object, so we need that type as well
381                         if (($type == 'as:Undo') && !empty($object_data['object_object'])) {
382                                 $object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object'], $uid);
383                         }
384                 }
385
386                 $object_data = self::addActivityFields($object_data, $activity);
387
388                 if (empty($object_data['object_type'])) {
389                         $object_data['object_type'] = $object_type;
390                 }
391
392                 $object_data['type'] = $type;
393                 $object_data['actor'] = $actor;
394                 $object_data['item_receiver'] = $receivers;
395                 $object_data['receiver'] = array_replace($object_data['receiver'] ?? [], $receivers);
396                 $object_data['reception_type'] = array_replace($object_data['reception_type'] ?? [], $reception_types);
397
398                 $author = $object_data['author'] ?? $actor;
399                 if (!empty($author) && !empty($object_data['id'])) {
400                         $author_host = parse_url($author, PHP_URL_HOST);
401                         $id_host = parse_url($object_data['id'], PHP_URL_HOST);
402                         if ($author_host == $id_host) {
403                                 Logger::info('Valid hosts', ['type' => $type, 'host' => $id_host]);
404                         } else {
405                                 Logger::notice('Differing hosts on author and id', ['type' => $type, 'author' => $author_host, 'id' => $id_host]);
406                                 $trust_source = false;
407                         }
408                 }
409
410                 Logger::log('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], Logger::DEBUG);
411
412                 return $object_data;
413         }
414
415         /**
416          * Fetches the first user id from the receiver array
417          *
418          * @param array $receivers Array with receivers
419          * @return integer user id;
420          */
421         public static function getFirstUserFromReceivers($receivers)
422         {
423                 foreach ($receivers as $receiver) {
424                         if (!empty($receiver)) {
425                                 return $receiver;
426                         }
427                 }
428                 return 0;
429         }
430
431         /**
432          * Processes the activity object
433          *
434          * @param array   $activity     Array with activity data
435          * @param string  $body
436          * @param integer $uid          User ID
437          * @param boolean $trust_source Do we trust the source?
438          * @param boolean $push         Message had been pushed to our system
439          * @throws \Exception
440          */
441         public static function processActivity($activity, string $body = '', int $uid = null, bool $trust_source = false, bool $push = false, array $signer = [])
442         {
443                 $type = JsonLD::fetchElement($activity, '@type');
444                 if (!$type) {
445                         Logger::info('Empty type', ['activity' => $activity]);
446                         return;
447                 }
448
449                 if (!JsonLD::fetchElement($activity, 'as:object', '@id')) {
450                         Logger::info('Empty object', ['activity' => $activity]);
451                         return;
452                 }
453
454                 $actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
455                 if (empty($actor)) {
456                         Logger::info('Empty actor', ['activity' => $activity]);
457                         return;
458                 }
459
460                 if (is_array($activity['as:object'])) {
461                         $attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo', '@id');
462                 } else {
463                         $attributed_to = '';
464                 }
465
466                 // Test the provided signatures against the actor and "attributedTo"
467                 if ($trust_source) {
468                         if (!empty($attributed_to) && !empty($actor)) {
469                                 $trust_source = (in_array($actor, $signer) && in_array($attributed_to, $signer));
470                         } else {
471                                 $trust_source = in_array($actor, $signer);
472                         }
473                 }
474
475                 // $trust_source is called by reference and is set to true if the content was retrieved successfully
476                 $object_data = self::prepareObjectData($activity, $uid, $push, $trust_source);
477                 if (empty($object_data)) {
478                         Logger::info('No object data found', ['activity' => $activity]);
479                         return;
480                 }
481
482                 if (!$trust_source) {
483                         Logger::info('Activity trust could not be achieved.',  ['id' => $object_data['object_id'], 'type' => $type, 'signer' => $signer, 'actor' => $actor, 'attributedTo' => $attributed_to]);
484                         return;
485                 }
486
487                 if (!empty($body) && empty($object_data['raw'])) {
488                         $object_data['raw'] = $body;
489                 }
490
491                 // Internal flag for thread completion. See Processor.php
492                 if (!empty($activity['thread-completion'])) {
493                         $object_data['thread-completion'] = $activity['thread-completion'];
494                 }
495
496                 // Internal flag for posts that arrived via relay
497                 if (!empty($activity['from-relay'])) {
498                         $object_data['from-relay'] = $activity['from-relay'];
499                 }
500                 
501                 switch ($type) {
502                         case 'as:Create':
503                                 if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
504                                         $item = ActivityPub\Processor::createItem($object_data);
505                                         ActivityPub\Processor::postItem($object_data, $item);
506                                 }
507                                 break;
508
509                         case 'as:Add':
510                                 if ($object_data['object_type'] == 'as:tag') {
511                                         ActivityPub\Processor::addTag($object_data);
512                                 }
513                                 break;
514
515                         case 'as:Announce':
516                                 if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
517                                         $object_data['thread-completion'] = Contact::getIdForURL($actor);
518
519                                         $item = ActivityPub\Processor::createItem($object_data);
520                                         if (empty($item)) {
521                                                 return;
522                                         }
523
524                                         $item['post-type'] = Item::PT_ANNOUNCEMENT;
525                                         ActivityPub\Processor::postItem($object_data, $item);
526
527                                         $announce_object_data = self::processObject($activity);
528                                         $announce_object_data['name'] = $type;
529                                         $announce_object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id');
530                                         $announce_object_data['object_id'] = $object_data['object_id'];
531                                         $announce_object_data['object_type'] = $object_data['object_type'];
532                                         $announce_object_data['push'] = $push;
533
534                                         if (!empty($body)) {
535                                                 $announce_object_data['raw'] = $body;
536                                         }
537
538                                         ActivityPub\Processor::createActivity($announce_object_data, Activity::ANNOUNCE);
539                                 }
540                                 break;
541
542                         case 'as:Like':
543                                 if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
544                                         ActivityPub\Processor::createActivity($object_data, Activity::LIKE);
545                                 }
546                                 break;
547
548                         case 'as:Dislike':
549                                 if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
550                                         ActivityPub\Processor::createActivity($object_data, Activity::DISLIKE);
551                                 }
552                                 break;
553
554                         case 'as:TentativeAccept':
555                                 if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
556                                         ActivityPub\Processor::createActivity($object_data, Activity::ATTENDMAYBE);
557                                 }
558                                 break;
559
560                         case 'as:Update':
561                                 if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
562                                         ActivityPub\Processor::updateItem($object_data);
563                                 } elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
564                                         ActivityPub\Processor::updatePerson($object_data);
565                                 }
566                                 break;
567
568                         case 'as:Delete':
569                                 if ($object_data['object_type'] == 'as:Tombstone') {
570                                         ActivityPub\Processor::deleteItem($object_data);
571                                 } elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
572                                         ActivityPub\Processor::deletePerson($object_data);
573                                 }
574                                 break;
575
576                         case 'as:Follow':
577                                 if (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
578                                         ActivityPub\Processor::followUser($object_data);
579                                 } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
580                                         $object_data['reply-to-id'] = $object_data['object_id'];
581                                         ActivityPub\Processor::createActivity($object_data, Activity::FOLLOW);
582                                 }
583                                 break;
584
585                         case 'as:Accept':
586                                 if ($object_data['object_type'] == 'as:Follow') {
587                                         ActivityPub\Processor::acceptFollowUser($object_data);
588                                 } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
589                                         ActivityPub\Processor::createActivity($object_data, Activity::ATTEND);
590                                 }
591                                 break;
592
593                         case 'as:Reject':
594                                 if ($object_data['object_type'] == 'as:Follow') {
595                                         ActivityPub\Processor::rejectFollowUser($object_data);
596                                 } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
597                                         ActivityPub\Processor::createActivity($object_data, Activity::ATTENDNO);
598                                 }
599                                 break;
600
601                         case 'as:Undo':
602                                 if (($object_data['object_type'] == 'as:Follow') &&
603                                         in_array($object_data['object_object_type'], self::ACCOUNT_TYPES)) {
604                                         ActivityPub\Processor::undoFollowUser($object_data);
605                                 } elseif (($object_data['object_type'] == 'as:Accept') &&
606                                         in_array($object_data['object_object_type'], self::ACCOUNT_TYPES)) {
607                                         ActivityPub\Processor::rejectFollowUser($object_data);
608                                 } elseif (in_array($object_data['object_type'], self::ACTIVITY_TYPES) &&
609                                         in_array($object_data['object_object_type'], self::CONTENT_TYPES)) {
610                                         ActivityPub\Processor::undoActivity($object_data);
611                                 }
612                                 break;
613
614                         default:
615                                 Logger::log('Unknown activity: ' . $type . ' ' . $object_data['object_type'], Logger::DEBUG);
616                                 break;
617                 }
618         }
619
620         /**
621          * Fetch the receiver list from an activity array
622          *
623          * @param array   $activity
624          * @param string  $actor
625          * @param array   $tags
626          * @param boolean $fetch_unlisted 
627          *
628          * @return array with receivers (user id)
629          * @throws \Exception
630          */
631         private static function getReceivers($activity, $actor, $tags = [], $fetch_unlisted = false)
632         {
633                 $reply = $receivers = [];
634
635                 // When it is an answer, we inherite the receivers from the parent
636                 $replyto = JsonLD::fetchElement($activity, 'as:inReplyTo', '@id');
637                 if (!empty($replyto)) {
638                         $reply = [$replyto];
639
640                         // Fix possibly wrong item URI (could be an answer to a plink uri)
641                         $fixedReplyTo = Item::getURIByLink($replyto);
642                         if (!empty($fixedReplyTo)) {
643                                 $reply[] = $fixedReplyTo;
644                         }
645                 }
646
647                 // Fetch all posts that refer to the object id
648                 $object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
649                 if (!empty($object_id)) {
650                         $reply[] = $object_id;
651                 }
652
653                 if (!empty($reply)) {
654                         $parents = Post::select(['uid'], ['uri' => $reply]);
655                         while ($parent = Post::fetch($parents)) {
656                                 $receivers[$parent['uid']] = ['uid' => $parent['uid'], 'type' => self::TARGET_ANSWER];
657                         }
658                         DBA::close($parents);
659                 }
660
661                 if (!empty($actor)) {
662                         $profile = APContact::getByURL($actor);
663                         $followers = $profile['followers'] ?? '';
664
665                         Logger::log('Actor: ' . $actor . ' - Followers: ' . $followers, Logger::DEBUG);
666                 } else {
667                         Logger::info('Empty actor', ['activity' => $activity]);
668                         $followers = '';
669                 }
670
671                 // We have to prevent false follower assumptions upon thread completions
672                 $follower_target = empty($activity['thread-completion']) ? self::TARGET_FOLLOWER : self::TARGET_UNKNOWN;
673
674                 foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) {
675                         $receiver_list = JsonLD::fetchElementArray($activity, $element, '@id');
676                         if (empty($receiver_list)) {
677                                 continue;
678                         }
679
680                         foreach ($receiver_list as $receiver) {
681                                 if ($receiver == self::PUBLIC_COLLECTION) {
682                                         $receivers[0] = ['uid' => 0, 'type' => self::TARGET_GLOBAL];
683                                 }
684
685                                 // Add receiver "-1" for unlisted posts 
686                                 if ($fetch_unlisted && ($receiver == self::PUBLIC_COLLECTION) && ($element == 'as:cc')) {
687                                         $receivers[-1] = ['uid' => -1, 'type' => self::TARGET_GLOBAL];
688                                 }
689
690                                 // Fetch the receivers for the public and the followers collection
691                                 if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) {
692                                         $receivers = self::getReceiverForActor($actor, $tags, $receivers, $follower_target);
693                                         continue;
694                                 }
695
696                                 // Fetching all directly addressed receivers
697                                 $condition = ['self' => true, 'nurl' => Strings::normaliseLink($receiver)];
698                                 $contact = DBA::selectFirst('contact', ['uid', 'contact-type'], $condition);
699                                 if (!DBA::isResult($contact)) {
700                                         continue;
701                                 }
702
703                                 // Check if the potential receiver is following the actor
704                                 // Exception: The receiver is targetted via "to" or this is a comment
705                                 if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) {
706                                         $networks = Protocol::FEDERATED;
707                                         $condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
708                                                 'network' => $networks, 'archive' => false, 'pending' => false, 'uid' => $contact['uid']];
709
710                                         // Forum posts are only accepted from forum contacts
711                                         if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
712                                                 $condition['rel'] = [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER];
713                                         }
714
715                                         if (!DBA::exists('contact', $condition)) {
716                                                 continue;
717                                         }
718                                 }
719
720                                 $type = $receivers[$contact['uid']]['type'] ?? self::TARGET_UNKNOWN;
721                                 if (in_array($type, [self::TARGET_UNKNOWN, self::TARGET_FOLLOWER, self::TARGET_ANSWER, self::TARGET_GLOBAL])) {
722                                         switch ($element) {
723                                                 case 'as:to':
724                                                         $type = self::TARGET_TO;
725                                                         break;
726                                                 case 'as:cc':
727                                                         $type = self::TARGET_CC;
728                                                         break;
729                                                 case 'as:bto':
730                                                         $type = self::TARGET_BTO;
731                                                         break;
732                                                 case 'as:bcc':
733                                                         $type = self::TARGET_BCC;
734                                                         break;
735                                         }
736
737                                         $receivers[$contact['uid']] = ['uid' => $contact['uid'], 'type' => $type];
738                                 }
739                         }
740                 }
741
742                 self::switchContacts($receivers, $actor);
743
744                 return $receivers;
745         }
746
747         /**
748          * Fetch the receiver list of a given actor
749          *
750          * @param string  $actor
751          * @param array   $tags
752          * @param array   $receivers
753          * @param integer $target_type
754          *
755          * @return array with receivers (user id)
756          * @throws \Exception
757          */
758         private static function getReceiverForActor($actor, $tags, $receivers, $target_type)
759         {
760                 $basecondition = ['rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER],
761                         'network' => Protocol::FEDERATED, 'archive' => false, 'pending' => false];
762
763                 $condition = DBA::mergeConditions($basecondition, ["`nurl` = ? AND `uid` != ?", Strings::normaliseLink($actor), 0]);
764                 $contacts = DBA::select('contact', ['uid', 'rel'], $condition);
765                 while ($contact = DBA::fetch($contacts)) {
766                         if (empty($receivers[$contact['uid']]) && self::isValidReceiverForActor($contact, $tags)) {
767                                 $receivers[$contact['uid']] = ['uid' => $contact['uid'], 'type' => $target_type];
768                         }
769                 }
770                 DBA::close($contacts);
771
772                 // The queries are split because of performance issues
773                 $condition = DBA::mergeConditions($basecondition, ["`alias` IN (?, ?) AND `uid` != ?", Strings::normaliseLink($actor), $actor, 0]);
774                 $contacts = DBA::select('contact', ['uid', 'rel'], $condition);
775                 while ($contact = DBA::fetch($contacts)) {
776                         if (empty($receivers[$contact['uid']]) && self::isValidReceiverForActor($contact, $tags)) {
777                                 $receivers[$contact['uid']] = ['uid' => $contact['uid'], 'type' => $target_type];
778                         }
779                 }
780                 DBA::close($contacts);
781                 return $receivers;
782         }
783
784         /**
785          * Tests if the contact is a valid receiver for this actor
786          *
787          * @param array  $contact
788          * @param string $actor
789          * @param array  $tags
790          *
791          * @return bool with receivers (user id)
792          * @throws \Exception
793          */
794         private static function isValidReceiverForActor($contact, $tags)
795         {
796                 // Are we following the contact? Then this is a valid receiver
797                 if (in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) {
798                         return true;
799                 }
800
801                 // When the possible receiver isn't a community, then it is no valid receiver
802                 $owner = User::getOwnerDataById($contact['uid']);
803                 if (empty($owner) || ($owner['contact-type'] != Contact::TYPE_COMMUNITY)) {
804                         return false;
805                 }
806
807                 // Is the community account tagged?
808                 foreach ($tags as $tag) {
809                         if ($tag['type'] != 'Mention') {
810                                 continue;
811                         }
812
813                         if (Strings::compareLink($tag['href'], $owner['url'])) {
814                                 return true;
815                         }
816                 }
817
818                 return false;
819         }
820
821         /**
822          * Switches existing contacts to ActivityPub
823          *
824          * @param integer $cid Contact ID
825          * @param integer $uid User ID
826          * @param string  $url Profile URL
827          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
828          * @throws \ImagickException
829          */
830         public static function switchContact($cid, $uid, $url)
831         {
832                 if (DBA::exists('contact', ['id' => $cid, 'network' => Protocol::ACTIVITYPUB])) {
833                         Logger::info('Contact is already ActivityPub', ['id' => $cid, 'uid' => $uid, 'url' => $url]);
834                         return;
835                 }
836
837                 if (Contact::updateFromProbe($cid)) {
838                         Logger::info('Update was successful', ['id' => $cid, 'uid' => $uid, 'url' => $url]);
839                 }
840
841                 // Send a new follow request to be sure that the connection still exists
842                 if (($uid != 0) && DBA::exists('contact', ['id' => $cid, 'rel' => [Contact::SHARING, Contact::FRIEND], 'network' => Protocol::ACTIVITYPUB])) {
843                         Logger::info('Contact had been switched to ActivityPub. Sending a new follow request.', ['uid' => $uid, 'url' => $url]);
844                         ActivityPub\Transmitter::sendActivity('Follow', $url, $uid);
845                 }
846         }
847
848         /**
849          *
850          *
851          * @param $receivers
852          * @param $actor
853          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
854          * @throws \ImagickException
855          */
856         private static function switchContacts($receivers, $actor)
857         {
858                 if (empty($actor)) {
859                         return;
860                 }
861
862                 foreach ($receivers as $receiver) {
863                         $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'nurl' => Strings::normaliseLink($actor)]);
864                         if (DBA::isResult($contact)) {
865                                 self::switchContact($contact['id'], $receiver['uid'], $actor);
866                         }
867
868                         $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'alias' => [Strings::normaliseLink($actor), $actor]]);
869                         if (DBA::isResult($contact)) {
870                                 self::switchContact($contact['id'], $receiver['uid'], $actor);
871                         }
872                 }
873         }
874
875         /**
876          *
877          *
878          * @param       $object_data
879          * @param array $activity
880          *
881          * @return mixed
882          */
883         private static function addActivityFields($object_data, $activity)
884         {
885                 if (!empty($activity['published']) && empty($object_data['published'])) {
886                         $object_data['published'] = JsonLD::fetchElement($activity, 'as:published', '@value');
887                 }
888
889                 if (!empty($activity['diaspora:guid']) && empty($object_data['diaspora:guid'])) {
890                         $object_data['diaspora:guid'] = JsonLD::fetchElement($activity, 'diaspora:guid', '@value');
891                 }
892
893                 $object_data['service'] = JsonLD::fetchElement($activity, 'as:instrument', 'as:name', '@type', 'as:Service');
894                 $object_data['service'] = JsonLD::fetchElement($object_data, 'service', '@value');
895
896                 if (!empty($object_data['object_id'])) {
897                         // Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field.
898                         $objectId = Item::getURIByLink($object_data['object_id']);
899                         if (!empty($objectId) && ($object_data['object_id'] != $objectId)) {
900                                 Logger::notice('Fix wrong object-id', ['received' => $object_data['object_id'], 'correct' => $objectId]);
901                                 $object_data['object_id'] = $objectId;
902                         }
903                 }
904
905                 return $object_data;
906         }
907
908         /**
909          * Fetches the object data from external ressources if needed
910          *
911          * @param string  $object_id    Object ID of the the provided object
912          * @param array   $object       The provided object array
913          * @param boolean $trust_source Do we trust the provided object?
914          * @param integer $uid          User ID for the signature that we use to fetch data
915          *
916          * @return array|false with trusted and valid object data
917          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
918          * @throws \ImagickException
919          */
920         private static function fetchObject(string $object_id, array $object = [], bool $trust_source = false, int $uid = 0)
921         {
922                 // By fetching the type we check if the object is complete.
923                 $type = JsonLD::fetchElement($object, '@type');
924
925                 if (!$trust_source || empty($type)) {
926                         $data = ActivityPub::fetchContent($object_id, $uid);
927                         if (!empty($data)) {
928                                 $object = JsonLD::compact($data);
929                                 Logger::log('Fetched content for ' . $object_id, Logger::DEBUG);
930                         } else {
931                                 Logger::log('Empty content for ' . $object_id . ', check if content is available locally.', Logger::DEBUG);
932
933                                 $item = Post::selectFirst([], ['uri' => $object_id]);
934                                 if (!DBA::isResult($item)) {
935                                         Logger::log('Object with url ' . $object_id . ' was not found locally.', Logger::DEBUG);
936                                         return false;
937                                 }
938                                 Logger::log('Using already stored item for url ' . $object_id, Logger::DEBUG);
939                                 $data = ActivityPub\Transmitter::createNote($item);
940                                 $object = JsonLD::compact($data);
941                         }
942
943                         $id = JsonLD::fetchElement($object, '@id');
944                         if (empty($id)) {
945                                 Logger::info('Empty id');
946                                 return false;
947                         }
948         
949                         if ($id != $object_id) {
950                                 Logger::info('Fetched id differs from provided id', ['provided' => $object_id, 'fetched' => $id]);
951                                 return false;
952                         }
953                 } else {
954                         Logger::log('Using original object for url ' . $object_id, Logger::DEBUG);
955                 }
956
957                 $type = JsonLD::fetchElement($object, '@type');
958                 if (empty($type)) {
959                         Logger::info('Empty type');
960                         return false;
961                 }
962
963                 // We currently don't handle 'pt:CacheFile', but with this step we avoid logging
964                 if (in_array($type, self::CONTENT_TYPES) || ($type == 'pt:CacheFile')) {
965                         $object_data = self::processObject($object);
966
967                         if (!empty($data)) {
968                                 $object_data['raw'] = json_encode($data);
969                         }
970                         return $object_data;
971                 }
972
973                 if ($type == 'as:Announce') {
974                         $object_id = JsonLD::fetchElement($object, 'object', '@id');
975                         if (empty($object_id) || !is_string($object_id)) {
976                                 return false;
977                         }
978                         return self::fetchObject($object_id, [], false, $uid);
979                 }
980
981                 Logger::log('Unhandled object type: ' . $type, Logger::DEBUG);
982                 return false;
983         }
984
985         /**
986          * Convert tags from JSON-LD format into a simplified format
987          *
988          * @param array $tags Tags in JSON-LD format
989          *
990          * @return array with tags in a simplified format
991          */
992         public static function processTags(array $tags)
993         {
994                 $taglist = [];
995
996                 foreach ($tags as $tag) {
997                         if (empty($tag)) {
998                                 continue;
999                         }
1000
1001                         $element = ['type' => str_replace('as:', '', JsonLD::fetchElement($tag, '@type')),
1002                                 'href' => JsonLD::fetchElement($tag, 'as:href', '@id'),
1003                                 'name' => JsonLD::fetchElement($tag, 'as:name', '@value')];
1004
1005                         if (empty($element['type'])) {
1006                                 continue;
1007                         }
1008
1009                         if (empty($element['href'])) {
1010                                 $element['href'] = $element['name'];
1011                         }
1012
1013                         $taglist[] = $element;
1014                 }
1015                 return $taglist;
1016         }
1017
1018         /**
1019          * Convert emojis from JSON-LD format into a simplified format
1020          *
1021          * @param array $emojis
1022          * @return array with emojis in a simplified format
1023          */
1024         private static function processEmojis(array $emojis)
1025         {
1026                 $emojilist = [];
1027
1028                 foreach ($emojis as $emoji) {
1029                         if (empty($emoji) || (JsonLD::fetchElement($emoji, '@type') != 'toot:Emoji') || empty($emoji['as:icon'])) {
1030                                 continue;
1031                         }
1032
1033                         $url = JsonLD::fetchElement($emoji['as:icon'], 'as:url', '@id');
1034                         $element = ['name' => JsonLD::fetchElement($emoji, 'as:name', '@value'),
1035                                 'href' => $url];
1036
1037                         $emojilist[] = $element;
1038                 }
1039
1040                 return $emojilist;
1041         }
1042
1043         /**
1044          * Convert attachments from JSON-LD format into a simplified format
1045          *
1046          * @param array $attachments Attachments in JSON-LD format
1047          *
1048          * @return array Attachments in a simplified format
1049          */
1050         private static function processAttachments(array $attachments)
1051         {
1052                 $attachlist = [];
1053
1054                 // Removes empty values
1055                 $attachments = array_filter($attachments);
1056
1057                 foreach ($attachments as $attachment) {
1058                         switch (JsonLD::fetchElement($attachment, '@type')) {
1059                                 case 'as:Page':
1060                                         $pageUrl = null;
1061                                         $pageImage = null;
1062
1063                                         $urls = JsonLD::fetchElementArray($attachment, 'as:url');
1064                                         foreach ($urls as $url) {
1065                                                 // Single scalar URL case
1066                                                 if (is_string($url)) {
1067                                                         $pageUrl = $url;
1068                                                         continue;
1069                                                 }
1070
1071                                                 $href = JsonLD::fetchElement($url, 'as:href', '@id');
1072                                                 $mediaType = JsonLD::fetchElement($url, 'as:mediaType', '@value');
1073                                                 if (Strings::startsWith($mediaType, 'image')) {
1074                                                         $pageImage = $href;
1075                                                 } else {
1076                                                         $pageUrl = $href;
1077                                                 }
1078                                         }
1079
1080                                         $attachlist[] = [
1081                                                 'type'  => 'link',
1082                                                 'title' => JsonLD::fetchElement($attachment, 'as:name', '@value'),
1083                                                 'desc'  => JsonLD::fetchElement($attachment, 'as:summary', '@value'),
1084                                                 'url'   => $pageUrl,
1085                                                 'image' => $pageImage,
1086                                         ];
1087                                         break;
1088                                 case 'as:Link':
1089                                         $attachlist[] = [
1090                                                 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')),
1091                                                 'mediaType' => JsonLD::fetchElement($attachment, 'as:mediaType', '@value'),
1092                                                 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'),
1093                                                 'url' => JsonLD::fetchElement($attachment, 'as:href', '@id')
1094                                         ];
1095                                         break;
1096                                 case 'as:Image':
1097                                         $mediaType = JsonLD::fetchElement($attachment, 'as:mediaType', '@value');
1098                                         $imageFullUrl = JsonLD::fetchElement($attachment, 'as:url', '@id');
1099                                         $imagePreviewUrl = null;
1100                                         // Multiple URLs?
1101                                         if (!$imageFullUrl && ($urls = JsonLD::fetchElementArray($attachment, 'as:url'))) {
1102                                                 $imageVariants = [];
1103                                                 $previewVariants = [];
1104                                                 foreach ($urls as $url) {
1105                                                         // Scalar URL, no discrimination possible
1106                                                         if (is_string($url)) {
1107                                                                 $imageFullUrl = $url;
1108                                                                 continue;
1109                                                         }
1110
1111                                                         // Not sure what to do with a different Link media type than the base Image, we skip
1112                                                         if ($mediaType != JsonLD::fetchElement($url, 'as:mediaType', '@value')) {
1113                                                                 continue;
1114                                                         }
1115
1116                                                         $href = JsonLD::fetchElement($url, 'as:href', '@id');
1117
1118                                                         // Default URL choice if no discriminating width is provided
1119                                                         $imageFullUrl = $href ?? $imageFullUrl;
1120
1121                                                         $width = intval(JsonLD::fetchElement($url, 'as:width', '@value') ?? 1);
1122
1123                                                         if ($href && $width) {
1124                                                                 $imageVariants[$width] = $href;
1125                                                                 // 632 is the ideal width for full screen frio posts, we compute the absolute distance to it
1126                                                                 $previewVariants[abs(632 - $width)] = $href;
1127                                                         }
1128                                                 }
1129
1130                                                 if ($imageVariants) {
1131                                                         // Taking the maximum size image
1132                                                         ksort($imageVariants);
1133                                                         $imageFullUrl = array_pop($imageVariants);
1134
1135                                                         // Taking the minimum number distance to the target distance
1136                                                         ksort($previewVariants);
1137                                                         $imagePreviewUrl = array_shift($previewVariants);
1138                                                 }
1139
1140                                                 unset($imageVariants);
1141                                                 unset($previewVariants);
1142                                         }
1143
1144                                         $attachlist[] = [
1145                                                 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')),
1146                                                 'mediaType' => $mediaType,
1147                                                 'name'  => JsonLD::fetchElement($attachment, 'as:name', '@value'),
1148                                                 'url'   => $imageFullUrl,
1149                                                 'image' => $imagePreviewUrl !== $imageFullUrl ? $imagePreviewUrl : null,
1150                                         ];
1151                                         break;
1152                                 default:
1153                                         $attachlist[] = [
1154                                                 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')),
1155                                                 'mediaType' => JsonLD::fetchElement($attachment, 'as:mediaType', '@value'),
1156                                                 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'),
1157                                                 'url' => JsonLD::fetchElement($attachment, 'as:url', '@id')
1158                                         ];
1159                         }
1160                 }
1161
1162                 return $attachlist;
1163         }
1164
1165         /**
1166          * Fetch the original source or content with the "language" Markdown or HTML
1167          *
1168          * @param array $object
1169          * @param array $object_data
1170          *
1171          * @return array
1172          * @throws \Exception
1173          */
1174         private static function getSource($object, $object_data)
1175         {
1176                 $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/bbcode');
1177                 $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value');
1178                 if (!empty($object_data['source'])) {
1179                         return $object_data;
1180                 }
1181
1182                 $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/markdown');
1183                 $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value');
1184                 if (!empty($object_data['source'])) {
1185                         $object_data['source'] = Markdown::toBBCode($object_data['source']);
1186                         return $object_data;
1187                 }
1188
1189                 $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/html');
1190                 $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value');
1191                 if (!empty($object_data['source'])) {
1192                         $object_data['source'] = HTML::toBBCode($object_data['source']);
1193                         return $object_data;
1194                 }
1195
1196                 return $object_data;
1197         }
1198
1199         /**
1200          * Check if the "as:url" element is an array with multiple links
1201          * This is the case with audio and video posts.
1202          * Then the links are added as attachments
1203          *
1204          * @param array $object      The raw object
1205          * @param array $object_data The parsed object data for later processing
1206          * @return array the object data
1207          */
1208         private static function processAttachmentUrls(array $object, array $object_data) {
1209                 // Check if this is some url with multiple links
1210                 if (empty($object['as:url'])) {
1211                         return $object_data;
1212                 }
1213                 
1214                 $urls = $object['as:url'];
1215                 $keys = array_keys($urls);
1216                 if (!is_numeric(array_pop($keys))) {
1217                         return $object_data;
1218                 }
1219
1220                 $attachments = [];
1221
1222                 foreach ($urls as $url) {
1223                         if (empty($url['@type']) || ($url['@type'] != 'as:Link')) {
1224                                 continue;
1225                         }
1226
1227                         $href = JsonLD::fetchElement($url, 'as:href', '@id');
1228                         if (empty($href)) {
1229                                 continue;
1230                         }
1231
1232                         $mediatype = JsonLD::fetchElement($url, 'as:mediaType');
1233                         if (empty($mediatype)) {
1234                                 continue;
1235                         }
1236
1237                         if ($mediatype == 'text/html') {
1238                                 $object_data['alternate-url'] = $href;
1239                         }
1240
1241                         $filetype = strtolower(substr($mediatype, 0, strpos($mediatype, '/')));
1242
1243                         if ($filetype == 'audio') {
1244                                 $attachments[$filetype] = ['type' => $mediatype, 'url' => $href, 'height' => null, 'size' => null];
1245                         } elseif ($filetype == 'video') {
1246                                 $height = (int)JsonLD::fetchElement($url, 'as:height', '@value');
1247                                 $size = (int)JsonLD::fetchElement($url, 'pt:size', '@value');
1248
1249                                 // We save bandwidth by using a moderate height (alt least 480 pixel height)
1250                                 // Peertube normally uses these heights: 240, 360, 480, 720, 1080
1251                                 if (!empty($attachments[$filetype]['height']) &&
1252                                         ($height > $attachments[$filetype]['height']) && ($attachments[$filetype]['height'] >= 480)) {
1253                                         continue;
1254                                 }
1255
1256                                 $attachments[$filetype] = ['type' => $mediatype, 'url' => $href, 'height' => $height, 'size' => $size];
1257                         } elseif (in_array($mediatype, ['application/x-bittorrent', 'application/x-bittorrent;x-scheme-handler/magnet'])) {
1258                                 $height = (int)JsonLD::fetchElement($url, 'as:height', '@value');
1259
1260                                 // For Torrent links we always store the highest resolution
1261                                 if (!empty($attachments[$mediatype]['height']) && ($height < $attachments[$mediatype]['height'])) {
1262                                         continue;
1263                                 }
1264
1265                                 $attachments[$mediatype] = ['type' => $mediatype, 'url' => $href, 'height' => $height, 'size' => null];
1266                         }
1267                 }
1268
1269                 foreach ($attachments as $type => $attachment) {
1270                         $object_data['attachments'][] = ['type' => $type,
1271                                 'mediaType' => $attachment['type'],
1272                                 'height' => $attachment['height'],
1273                                 'size' => $attachment['size'],
1274                                 'name' => '',
1275                                 'url' => $attachment['url']];
1276                 }
1277                 return $object_data;
1278         }
1279
1280         /**
1281          * Fetches data from the object part of an activity
1282          *
1283          * @param array $object
1284          *
1285          * @return array
1286          * @throws \Exception
1287          */
1288         private static function processObject($object)
1289         {
1290                 if (!JsonLD::fetchElement($object, '@id')) {
1291                         return false;
1292                 }
1293
1294                 $object_data = [];
1295                 $object_data['object_type'] = JsonLD::fetchElement($object, '@type');
1296                 $object_data['id'] = JsonLD::fetchElement($object, '@id');
1297                 $object_data['reply-to-id'] = JsonLD::fetchElement($object, 'as:inReplyTo', '@id');
1298
1299                 // An empty "id" field is translated to "./" by the compactor, so we have to check for this content
1300                 if (empty($object_data['reply-to-id']) || ($object_data['reply-to-id'] == './')) {
1301                         $object_data['reply-to-id'] = $object_data['id'];
1302                 } else {
1303                         // Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field.
1304                         $replyToId = Item::getURIByLink($object_data['reply-to-id']);
1305                         if (!empty($replyToId) && ($object_data['reply-to-id'] != $replyToId)) {
1306                                 Logger::notice('Fix wrong reply-to', ['received' => $object_data['reply-to-id'], 'correct' => $replyToId]);
1307                                 $object_data['reply-to-id'] = $replyToId;
1308                         }
1309                 }
1310
1311                 $object_data['published'] = JsonLD::fetchElement($object, 'as:published', '@value');
1312                 $object_data['updated'] = JsonLD::fetchElement($object, 'as:updated', '@value');
1313
1314                 if (empty($object_data['updated'])) {
1315                         $object_data['updated'] = $object_data['published'];
1316                 }
1317
1318                 if (empty($object_data['published']) && !empty($object_data['updated'])) {
1319                         $object_data['published'] = $object_data['updated'];
1320                 }
1321
1322                 $actor = JsonLD::fetchElement($object, 'as:attributedTo', '@id');
1323                 if (empty($actor)) {
1324                         $actor = JsonLD::fetchElement($object, 'as:actor', '@id');
1325                 }
1326
1327                 $location = JsonLD::fetchElement($object, 'as:location', 'as:name', '@type', 'as:Place');
1328                 $location = JsonLD::fetchElement($location, 'location', '@value');
1329                 if ($location) {
1330                         // Some AP software allow formatted text in post location, so we run all the text converters we have to boil
1331                         // down to HTML and then finally format to plaintext.
1332                         $location = Markdown::convert($location);
1333                         $location = BBCode::convert($location);
1334                         $location = HTML::toPlaintext($location);
1335                 }
1336
1337                 $object_data['sc:identifier'] = JsonLD::fetchElement($object, 'sc:identifier', '@value');
1338                 $object_data['diaspora:guid'] = JsonLD::fetchElement($object, 'diaspora:guid', '@value');
1339                 $object_data['diaspora:comment'] = JsonLD::fetchElement($object, 'diaspora:comment', '@value');
1340                 $object_data['diaspora:like'] = JsonLD::fetchElement($object, 'diaspora:like', '@value');
1341                 $object_data['actor'] = $object_data['author'] = $actor;
1342                 $object_data['context'] = JsonLD::fetchElement($object, 'as:context', '@id');
1343                 $object_data['conversation'] = JsonLD::fetchElement($object, 'ostatus:conversation', '@id');
1344                 $object_data['sensitive'] = JsonLD::fetchElement($object, 'as:sensitive');
1345                 $object_data['name'] = JsonLD::fetchElement($object, 'as:name', '@value');
1346                 $object_data['summary'] = JsonLD::fetchElement($object, 'as:summary', '@value');
1347                 $object_data['content'] = JsonLD::fetchElement($object, 'as:content', '@value');
1348                 $object_data = self::getSource($object, $object_data);
1349                 $object_data['start-time'] = JsonLD::fetchElement($object, 'as:startTime', '@value');
1350                 $object_data['end-time'] = JsonLD::fetchElement($object, 'as:endTime', '@value');
1351                 $object_data['location'] = $location;
1352                 $object_data['latitude'] = JsonLD::fetchElement($object, 'as:location', 'as:latitude', '@type', 'as:Place');
1353                 $object_data['latitude'] = JsonLD::fetchElement($object_data, 'latitude', '@value');
1354                 $object_data['longitude'] = JsonLD::fetchElement($object, 'as:location', 'as:longitude', '@type', 'as:Place');
1355                 $object_data['longitude'] = JsonLD::fetchElement($object_data, 'longitude', '@value');
1356                 $object_data['attachments'] = self::processAttachments(JsonLD::fetchElementArray($object, 'as:attachment') ?? []);
1357                 $object_data['tags'] = self::processTags(JsonLD::fetchElementArray($object, 'as:tag') ?? []);
1358                 $object_data['emojis'] = self::processEmojis(JsonLD::fetchElementArray($object, 'as:tag', null, '@type', 'toot:Emoji') ?? []);
1359                 $object_data['generator'] = JsonLD::fetchElement($object, 'as:generator', 'as:name', '@type', 'as:Application');
1360                 $object_data['generator'] = JsonLD::fetchElement($object_data, 'generator', '@value');
1361                 $object_data['alternate-url'] = JsonLD::fetchElement($object, 'as:url', '@id');
1362
1363                 // Special treatment for Hubzilla links
1364                 if (is_array($object_data['alternate-url'])) {
1365                         $object_data['alternate-url'] = JsonLD::fetchElement($object_data['alternate-url'], 'as:href', '@id');
1366
1367                         if (!is_string($object_data['alternate-url'])) {
1368                                 $object_data['alternate-url'] = JsonLD::fetchElement($object['as:url'], 'as:href', '@id');
1369                         }
1370                 }
1371
1372                 if (in_array($object_data['object_type'], ['as:Audio', 'as:Video'])) {
1373                         $object_data = self::processAttachmentUrls($object, $object_data);
1374                 }
1375
1376                 $receiverdata = self::getReceivers($object, $object_data['actor'], $object_data['tags'], true);
1377                 $receivers = $reception_types = [];
1378                 foreach ($receiverdata as $key => $data) {
1379                         $receivers[$key] = $data['uid'];
1380                         $reception_types[$data['uid']] = $data['type'] ?? 0;
1381                 }
1382
1383                 $object_data['receiver'] = $receivers;
1384                 $object_data['reception_type'] = $reception_types;
1385
1386                 $object_data['unlisted'] = in_array(-1, $object_data['receiver']);
1387                 unset($object_data['receiver'][-1]);
1388                 unset($object_data['reception_type'][-1]);
1389
1390                 // Common object data:
1391
1392                 // Unhandled
1393                 // @context, type, actor, signature, mediaType, duration, replies, icon
1394
1395                 // Also missing: (Defined in the standard, but currently unused)
1396                 // audience, preview, endTime, startTime, image
1397
1398                 // Data in Notes:
1399
1400                 // Unhandled
1401                 // contentMap, announcement_count, announcements, context_id, likes, like_count
1402                 // inReplyToStatusId, shares, quoteUrl, statusnetConversationId
1403
1404                 // Data in video:
1405
1406                 // To-Do?
1407                 // category, licence, language, commentsEnabled
1408
1409                 // Unhandled
1410                 // views, waitTranscoding, state, support, subtitleLanguage
1411                 // likes, dislikes, shares, comments
1412
1413                 return $object_data;
1414         }
1415 }