]> git.mxchange.org Git - friendica.git/blob - src/Protocol/ActivityPub/Transmitter.php
0d024d26fc097fac9c2700a10c759e4a3704cc8f
[friendica.git] / src / Protocol / ActivityPub / Transmitter.php
1 <?php
2 /**
3  * @file src/Protocol/ActivityPub/Transmitter.php
4  */
5 namespace Friendica\Protocol\ActivityPub;
6
7 use Friendica\BaseObject;
8 use Friendica\Content\Feature;
9 use Friendica\Database\DBA;
10 use Friendica\Core\Config;
11 use Friendica\Core\Logger;
12 use Friendica\Core\System;
13 use Friendica\Util\HTTPSignature;
14 use Friendica\Core\Protocol;
15 use Friendica\Model\Conversation;
16 use Friendica\Model\Contact;
17 use Friendica\Model\APContact;
18 use Friendica\Model\Item;
19 use Friendica\Model\Term;
20 use Friendica\Model\User;
21 use Friendica\Util\DateTimeFormat;
22 use Friendica\Content\Text\BBCode;
23 use Friendica\Content\Text\Plaintext;
24 use Friendica\Util\JsonLD;
25 use Friendica\Util\LDSignature;
26 use Friendica\Model\Profile;
27 use Friendica\Object\Image;
28 use Friendica\Protocol\ActivityPub;
29 use Friendica\Core\Cache;
30 use Friendica\Util\Map;
31 use Friendica\Util\Network;
32
33 require_once 'include/api.php';
34 require_once 'mod/share.php';
35
36 /**
37  * @brief ActivityPub Transmitter Protocol class
38  *
39  * To-Do:
40  * - Undo Announce
41  */
42 class Transmitter
43 {
44         /**
45          * collects the lost of followers of the given owner
46          *
47          * @param array   $owner Owner array
48          * @param integer $page  Page number
49          *
50          * @return array of owners
51          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
52          */
53         public static function getFollowers($owner, $page = null)
54         {
55                 $condition = ['rel' => [Contact::FOLLOWER, Contact::FRIEND], 'network' => Protocol::NATIVE_SUPPORT, 'uid' => $owner['uid'],
56                         'self' => false, 'deleted' => false, 'hidden' => false, 'archive' => false, 'pending' => false];
57                 $count = DBA::count('contact', $condition);
58
59                 $data = ['@context' => ActivityPub::CONTEXT];
60                 $data['id'] = System::baseUrl() . '/followers/' . $owner['nickname'];
61                 $data['type'] = 'OrderedCollection';
62                 $data['totalItems'] = $count;
63
64                 // When we hide our friends we will only show the pure number but don't allow more.
65                 $profile = Profile::getByUID($owner['uid']);
66                 if (!empty($profile['hide-friends'])) {
67                         return $data;
68                 }
69
70                 if (empty($page)) {
71                         $data['first'] = System::baseUrl() . '/followers/' . $owner['nickname'] . '?page=1';
72                 } else {
73                         $list = [];
74
75                         $contacts = DBA::select('contact', ['url'], $condition, ['limit' => [($page - 1) * 100, 100]]);
76                         while ($contact = DBA::fetch($contacts)) {
77                                 $list[] = $contact['url'];
78                         }
79
80                         if (!empty($list)) {
81                                 $data['next'] = System::baseUrl() . '/followers/' . $owner['nickname'] . '?page=' . ($page + 1);
82                         }
83
84                         $data['partOf'] = System::baseUrl() . '/followers/' . $owner['nickname'];
85
86                         $data['orderedItems'] = $list;
87                 }
88
89                 return $data;
90         }
91
92         /**
93          * Create list of following contacts
94          *
95          * @param array   $owner Owner array
96          * @param integer $page  Page numbe
97          *
98          * @return array of following contacts
99          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
100          */
101         public static function getFollowing($owner, $page = null)
102         {
103                 $condition = ['rel' => [Contact::SHARING, Contact::FRIEND], 'network' => Protocol::NATIVE_SUPPORT, 'uid' => $owner['uid'],
104                         'self' => false, 'deleted' => false, 'hidden' => false, 'archive' => false, 'pending' => false];
105                 $count = DBA::count('contact', $condition);
106
107                 $data = ['@context' => ActivityPub::CONTEXT];
108                 $data['id'] = System::baseUrl() . '/following/' . $owner['nickname'];
109                 $data['type'] = 'OrderedCollection';
110                 $data['totalItems'] = $count;
111
112                 // When we hide our friends we will only show the pure number but don't allow more.
113                 $profile = Profile::getByUID($owner['uid']);
114                 if (!empty($profile['hide-friends'])) {
115                         return $data;
116                 }
117
118                 if (empty($page)) {
119                         $data['first'] = System::baseUrl() . '/following/' . $owner['nickname'] . '?page=1';
120                 } else {
121                         $list = [];
122
123                         $contacts = DBA::select('contact', ['url'], $condition, ['limit' => [($page - 1) * 100, 100]]);
124                         while ($contact = DBA::fetch($contacts)) {
125                                 $list[] = $contact['url'];
126                         }
127
128                         if (!empty($list)) {
129                                 $data['next'] = System::baseUrl() . '/following/' . $owner['nickname'] . '?page=' . ($page + 1);
130                         }
131
132                         $data['partOf'] = System::baseUrl() . '/following/' . $owner['nickname'];
133
134                         $data['orderedItems'] = $list;
135                 }
136
137                 return $data;
138         }
139
140         /**
141          * Public posts for the given owner
142          *
143          * @param array   $owner Owner array
144          * @param integer $page  Page numbe
145          *
146          * @return array of posts
147          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
148          * @throws \ImagickException
149          */
150         public static function getOutbox($owner, $page = null)
151         {
152                 $public_contact = Contact::getIdForURL($owner['url'], 0, true);
153
154                 $condition = ['uid' => 0, 'contact-id' => $public_contact, 'author-id' => $public_contact,
155                         'private' => false, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT],
156                         'deleted' => false, 'visible' => true, 'moderated' => false];
157                 $count = DBA::count('item', $condition);
158
159                 $data = ['@context' => ActivityPub::CONTEXT];
160                 $data['id'] = System::baseUrl() . '/outbox/' . $owner['nickname'];
161                 $data['type'] = 'OrderedCollection';
162                 $data['totalItems'] = $count;
163
164                 if (empty($page)) {
165                         $data['first'] = System::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=1';
166                 } else {
167                         $list = [];
168
169                         $condition['parent-network'] = Protocol::NATIVE_SUPPORT;
170
171                         $items = Item::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]);
172                         while ($item = Item::fetch($items)) {
173                                 $object = self::createObjectFromItemID($item['id']);
174                                 unset($object['@context']);
175                                 $list[] = $object;
176                         }
177
178                         if (!empty($list)) {
179                                 $data['next'] = System::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=' . ($page + 1);
180                         }
181
182                         $data['partOf'] = System::baseUrl() . '/outbox/' . $owner['nickname'];
183
184                         $data['orderedItems'] = $list;
185                 }
186
187                 return $data;
188         }
189
190         /**
191          * Return the service array containing information the used software and it's url
192          *
193          * @return array with service data
194          */
195         private static function getService()
196         {
197                 return ['type' => 'Service',
198                         'name' =>  FRIENDICA_PLATFORM . " '" . FRIENDICA_CODENAME . "' " . FRIENDICA_VERSION . '-' . DB_UPDATE_VERSION,
199                         'url' => BaseObject::getApp()->getBaseURL()];
200         }
201
202         /**
203          * Return the ActivityPub profile of the given user
204          *
205          * @param integer $uid User ID
206          * @return array with profile data
207          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
208          */
209         public static function getProfile($uid)
210         {
211                 $condition = ['uid' => $uid, 'blocked' => false, 'account_expired' => false,
212                         'account_removed' => false, 'verified' => true];
213                 $fields = ['guid', 'nickname', 'pubkey', 'account-type', 'page-flags'];
214                 $user = DBA::selectFirst('user', $fields, $condition);
215                 if (!DBA::isResult($user)) {
216                         return [];
217                 }
218
219                 $fields = ['locality', 'region', 'country-name'];
220                 $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid, 'is-default' => true]);
221                 if (!DBA::isResult($profile)) {
222                         return [];
223                 }
224
225                 $fields = ['name', 'url', 'location', 'about', 'avatar', 'photo'];
226                 $contact = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]);
227                 if (!DBA::isResult($contact)) {
228                         return [];
229                 }
230
231                 // On old installations and never changed contacts this might not be filled
232                 if (empty($contact['avatar'])) {
233                         $contact['avatar'] = $contact['photo'];
234                 }
235
236                 $data = ['@context' => ActivityPub::CONTEXT];
237                 $data['id'] = $contact['url'];
238                 $data['diaspora:guid'] = $user['guid'];
239                 $data['type'] = ActivityPub::ACCOUNT_TYPES[$user['account-type']];
240                 $data['following'] = System::baseUrl() . '/following/' . $user['nickname'];
241                 $data['followers'] = System::baseUrl() . '/followers/' . $user['nickname'];
242                 $data['inbox'] = System::baseUrl() . '/inbox/' . $user['nickname'];
243                 $data['outbox'] = System::baseUrl() . '/outbox/' . $user['nickname'];
244                 $data['preferredUsername'] = $user['nickname'];
245                 $data['name'] = $contact['name'];
246                 $data['vcard:hasAddress'] = ['@type' => 'vcard:Home', 'vcard:country-name' => $profile['country-name'],
247                         'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']];
248                 $data['summary'] = $contact['about'];
249                 $data['url'] = $contact['url'];
250                 $data['manuallyApprovesFollowers'] = in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]);
251                 $data['publicKey'] = ['id' => $contact['url'] . '#main-key',
252                         'owner' => $contact['url'],
253                         'publicKeyPem' => $user['pubkey']];
254                 $data['endpoints'] = ['sharedInbox' => System::baseUrl() . '/inbox'];
255                 $data['icon'] = ['type' => 'Image',
256                         'url' => $contact['avatar']];
257
258                 $data['generator'] = self::getService();
259
260                 // tags: https://kitty.town/@inmysocks/100656097926961126.json
261                 return $data;
262         }
263
264         /**
265          * @param string $username
266          * @return array
267          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
268          */
269         public static function getDeletedUser($username)
270         {
271                 return [
272                         '@context' => ActivityPub::CONTEXT,
273                         'id' => System::baseUrl() . '/profile/' . $username,
274                         'type' => 'Tombstone',
275                         'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
276                         'updated' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
277                         'deleted' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
278                 ];
279         }
280
281         /**
282          * Returns an array with permissions of a given item array
283          *
284          * @param array $item
285          *
286          * @return array with permissions
287          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
288          * @throws \ImagickException
289          */
290         private static function fetchPermissionBlockFromConversation($item)
291         {
292                 if (empty($item['thr-parent'])) {
293                         return [];
294                 }
295
296                 $condition = ['item-uri' => $item['thr-parent'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
297                 $conversation = DBA::selectFirst('conversation', ['source'], $condition);
298                 if (!DBA::isResult($conversation)) {
299                         return [];
300                 }
301
302                 $activity = json_decode($conversation['source'], true);
303
304                 $actor = JsonLD::fetchElement($activity, 'actor', 'id');
305                 $profile = APContact::getByURL($actor);
306
307                 $item_profile = APContact::getByURL($item['author-link']);
308                 $exclude[] = $item['author-link'];
309
310                 if ($item['gravity'] == GRAVITY_PARENT) {
311                         $exclude[] = $item['owner-link'];
312                 }
313
314                 $permissions['to'][] = $actor;
315
316                 foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
317                         if (empty($activity[$element])) {
318                                 continue;
319                         }
320                         if (is_string($activity[$element])) {
321                                 $activity[$element] = [$activity[$element]];
322                         }
323
324                         foreach ($activity[$element] as $receiver) {
325                                 if ($receiver == $profile['followers'] && !empty($item_profile['followers'])) {
326                                         $permissions[$element][] = $item_profile['followers'];
327                                 } elseif (!in_array($receiver, $exclude)) {
328                                         $permissions[$element][] = $receiver;
329                                 }
330                         }
331                 }
332                 return $permissions;
333         }
334
335         /**
336          * Creates an array of permissions from an item thread
337          *
338          * @param array   $item       Item array
339          * @param boolean $blindcopy  addressing via "bcc" or "cc"?
340          * @param integer $last_id    Last item id for adding receivers
341          * @param boolean $forum_mode "true" means that we are sending content to a forum
342          *
343          * @return array with permission data
344          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
345          * @throws \ImagickException
346          */
347         private static function createPermissionBlockForItem($item, $blindcopy, $last_id = 0, $forum_mode = false)
348         {
349                 if ($last_id == 0) {
350                         $last_id = $item['id'];
351                 }
352
353                 $always_bcc = false;
354
355                 // Check if we should always deliver our stuff via BCC
356                 if (!empty($item['uid'])) {
357                         $profile = Profile::getByUID($item['uid']);
358                         if (!empty($profile)) {
359                                 $always_bcc = $profile['hide-friends'];
360                         }
361                 }
362
363                 if (Config::get('debug', 'total_ap_delivery')) {
364                         // Will be activated in a later step
365                         $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
366                 } else {
367                         // For now only send to these contacts:
368                         $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS];
369                 }
370
371                 $data = ['to' => [], 'cc' => [], 'bcc' => []];
372
373                 if ($item['gravity'] == GRAVITY_PARENT) {
374                         $actor_profile = APContact::getByURL($item['owner-link']);
375                 } else {
376                         $actor_profile = APContact::getByURL($item['author-link']);
377                 }
378
379                 $terms = Term::tagArrayFromItemId($item['id'], [Term::MENTION, Term::IMPLICIT_MENTION]);
380
381                 if (!$item['private']) {
382                         $data = array_merge($data, self::fetchPermissionBlockFromConversation($item));
383
384                         $data['to'][] = ActivityPub::PUBLIC_COLLECTION;
385
386                         foreach ($terms as $term) {
387                                 $profile = APContact::getByURL($term['url'], false);
388                                 if (!empty($profile)) {
389                                         $data['to'][] = $profile['url'];
390                                 }
391                         }
392                 } else {
393                         $receiver_list = Item::enumeratePermissions($item);
394
395                         foreach ($terms as $term) {
396                                 $cid = Contact::getIdForURL($term['url'], $item['uid']);
397                                 if (!empty($cid) && in_array($cid, $receiver_list)) {
398                                         $contact = DBA::selectFirst('contact', ['url', 'network', 'protocol'], ['id' => $cid]);
399                                         if (!DBA::isResult($contact) || (!in_array($contact['network'], $networks) && ($contact['protocol'] != Protocol::ACTIVITYPUB))) {
400                                                 continue;
401                                         }
402
403                                         if (!empty($profile = APContact::getByURL($contact['url'], false))) {
404                                                 $data['to'][] = $profile['url'];
405                                         }
406                                 }
407                         }
408
409                         foreach ($receiver_list as $receiver) {
410                                 $contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol'], ['id' => $receiver]);
411                                 if (!DBA::isResult($contact) || (!in_array($contact['network'], $networks) && ($contact['protocol'] != Protocol::ACTIVITYPUB))) {
412                                         continue;
413                                 }
414
415                                 if (!empty($profile = APContact::getByURL($contact['url'], false))) {
416                                         if ($contact['hidden'] || $always_bcc) {
417                                                 $data['bcc'][] = $profile['url'];
418                                         } else {
419                                                 $data['cc'][] = $profile['url'];
420                                         }
421                                 }
422                         }
423                 }
424
425                 $parents = Item::select(['id', 'author-link', 'owner-link', 'gravity', 'uri'], ['parent' => $item['parent']]);
426                 while ($parent = Item::fetch($parents)) {
427                         if ($parent['gravity'] == GRAVITY_PARENT) {
428                                 $profile = APContact::getByURL($parent['owner-link'], false);
429                                 if (!empty($profile)) {
430                                         if ($item['gravity'] != GRAVITY_PARENT) {
431                                                 // Comments to forums are directed to the forum
432                                                 // But comments to forums aren't directed to the followers collection
433                                                 if ($profile['type'] == 'Group') {
434                                                         $data['to'][] = $profile['url'];
435                                                 } else {
436                                                         $data['cc'][] = $profile['url'];
437                                                         if (!$item['private']) {
438                                                                 $data['cc'][] = $actor_profile['followers'];
439                                                         }
440                                                 }
441                                         } else {
442                                                 // Public thread parent post always are directed to the followes
443                                                 if (!$item['private'] && !$forum_mode) {
444                                                         $data['cc'][] = $actor_profile['followers'];
445                                                 }
446                                         }
447                                 }
448                         }
449
450                         // Don't include data from future posts
451                         if ($parent['id'] >= $last_id) {
452                                 continue;
453                         }
454
455                         $profile = APContact::getByURL($parent['author-link'], false);
456                         if (!empty($profile)) {
457                                 if (($profile['type'] == 'Group') || ($parent['uri'] == $item['thr-parent'])) {
458                                         $data['to'][] = $profile['url'];
459                                 } else {
460                                         $data['cc'][] = $profile['url'];
461                                 }
462                         }
463                 }
464                 DBA::close($parents);
465
466                 $data['to'] = array_unique($data['to']);
467                 $data['cc'] = array_unique($data['cc']);
468                 $data['bcc'] = array_unique($data['bcc']);
469
470                 if (($key = array_search($item['author-link'], $data['to'])) !== false) {
471                         unset($data['to'][$key]);
472                 }
473
474                 if (($key = array_search($item['author-link'], $data['cc'])) !== false) {
475                         unset($data['cc'][$key]);
476                 }
477
478                 if (($key = array_search($item['author-link'], $data['bcc'])) !== false) {
479                         unset($data['bcc'][$key]);
480                 }
481
482                 foreach ($data['to'] as $to) {
483                         if (($key = array_search($to, $data['cc'])) !== false) {
484                                 unset($data['cc'][$key]);
485                         }
486
487                         if (($key = array_search($to, $data['bcc'])) !== false) {
488                                 unset($data['bcc'][$key]);
489                         }
490                 }
491
492                 foreach ($data['cc'] as $cc) {
493                         if (($key = array_search($cc, $data['bcc'])) !== false) {
494                                 unset($data['bcc'][$key]);
495                         }
496                 }
497
498                 $receivers = ['to' => array_values($data['to']), 'cc' => array_values($data['cc']), 'bcc' => array_values($data['bcc'])];
499
500                 if (!$blindcopy) {
501                         unset($receivers['bcc']);
502                 }
503
504                 return $receivers;
505         }
506
507         /**
508          * Check if an inbox is archived
509          *
510          * @param string $url Inbox url
511          *
512          * @return boolean "true" if inbox is archived
513          */
514         private static function archivedInbox($url)
515         {
516                 return DBA::exists('inbox-status', ['url' => $url, 'archive' => true]);
517         }
518
519         /**
520          * Fetches a list of inboxes of followers of a given user
521          *
522          * @param integer $uid      User ID
523          * @param boolean $personal fetch personal inboxes
524          *
525          * @return array of follower inboxes
526          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
527          * @throws \ImagickException
528          */
529         public static function fetchTargetInboxesforUser($uid, $personal = false)
530         {
531                 $inboxes = [];
532
533                 if (Config::get('debug', 'total_ap_delivery')) {
534                         // Will be activated in a later step
535                         $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
536                 } else {
537                         // For now only send to these contacts:
538                         $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS];
539                 }
540
541                 $condition = ['uid' => $uid, 'archive' => false, 'pending' => false];
542
543                 if (!empty($uid)) {
544                         $condition['rel'] = [Contact::FOLLOWER, Contact::FRIEND];
545                 }
546
547                 $contacts = DBA::select('contact', ['url', 'network', 'protocol'], $condition);
548                 while ($contact = DBA::fetch($contacts)) {
549                         if (!in_array($contact['network'], $networks) && ($contact['protocol'] != Protocol::ACTIVITYPUB)) {
550                                 continue;
551                         }
552
553                         if (Network::isUrlBlocked($contact['url'])) {
554                                 continue;
555                         }
556
557                         $profile = APContact::getByURL($contact['url'], false);
558                         if (!empty($profile)) {
559                                 if (empty($profile['sharedinbox']) || $personal) {
560                                         $target = $profile['inbox'];
561                                 } else {
562                                         $target = $profile['sharedinbox'];
563                                 }
564                                 if (!self::archivedInbox($target)) {
565                                         $inboxes[$target] = $target;
566                                 }
567                         }
568                 }
569                 DBA::close($contacts);
570
571                 return $inboxes;
572         }
573
574         /**
575          * Fetches an array of inboxes for the given item and user
576          *
577          * @param array   $item       Item array
578          * @param integer $uid        User ID
579          * @param boolean $personal   fetch personal inboxes
580          * @param integer $last_id    Last item id for adding receivers
581          * @param boolean $forum_mode "true" means that we are sending content to a forum
582          * @return array with inboxes
583          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
584          * @throws \ImagickException
585          */
586         public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0, $forum_mode = false)
587         {
588                 $permissions = self::createPermissionBlockForItem($item, true, $last_id, $forum_mode);
589                 if (empty($permissions)) {
590                         return [];
591                 }
592
593                 $inboxes = [];
594
595                 if ($item['gravity'] == GRAVITY_ACTIVITY) {
596                         $item_profile = APContact::getByURL($item['author-link'], false);
597                 } else {
598                         $item_profile = APContact::getByURL($item['owner-link'], false);
599                 }
600
601                 foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
602                         if (empty($permissions[$element])) {
603                                 continue;
604                         }
605
606                         $blindcopy = in_array($element, ['bto', 'bcc']);
607
608                         foreach ($permissions[$element] as $receiver) {
609                                 if (Network::isUrlBlocked($receiver)) {
610                                         continue;
611                                 }
612
613                                 if ($receiver == $item_profile['followers']) {
614                                         $inboxes = array_merge($inboxes, self::fetchTargetInboxesforUser($uid, $personal));
615                                 } else {
616                                         $profile = APContact::getByURL($receiver, false);
617                                         if (!empty($profile)) {
618                                                 if (empty($profile['sharedinbox']) || $personal || $blindcopy) {
619                                                         $target = $profile['inbox'];
620                                                 } else {
621                                                         $target = $profile['sharedinbox'];
622                                                 }
623                                                 if (!self::archivedInbox($target)) {
624                                                         $inboxes[$target] = $target;
625                                                 }
626                                         }
627                                 }
628                         }
629                 }
630
631                 return $inboxes;
632         }
633
634         /**
635          * Returns the activity type of a given item
636          *
637          * @param array $item
638          *
639          * @return string with activity type
640          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
641          * @throws \ImagickException
642          */
643         private static function getTypeOfItem($item)
644         {
645                 $reshared = false;
646
647                 // Only check for a reshare, if it is a real reshare and no quoted reshare
648                 if (strpos($item['body'], "[share") === 0) {
649                         $announce = api_share_as_retweet($item);
650                         $reshared = !empty($announce['plink']);
651                 }
652
653                 if ($reshared) {
654                         $type = 'Announce';
655                 } elseif ($item['verb'] == ACTIVITY_POST) {
656                         if ($item['created'] == $item['edited']) {
657                                 $type = 'Create';
658                         } else {
659                                 $type = 'Update';
660                         }
661                 } elseif ($item['verb'] == ACTIVITY_LIKE) {
662                         $type = 'Like';
663                 } elseif ($item['verb'] == ACTIVITY_DISLIKE) {
664                         $type = 'Dislike';
665                 } elseif ($item['verb'] == ACTIVITY_ATTEND) {
666                         $type = 'Accept';
667                 } elseif ($item['verb'] == ACTIVITY_ATTENDNO) {
668                         $type = 'Reject';
669                 } elseif ($item['verb'] == ACTIVITY_ATTENDMAYBE) {
670                         $type = 'TentativeAccept';
671                 } elseif ($item['verb'] == ACTIVITY_FOLLOW) {
672                         $type = 'Follow';
673                 } else {
674                         $type = '';
675                 }
676
677                 return $type;
678         }
679
680         /**
681          * Creates the activity or fetches it from the cache
682          *
683          * @param integer $item_id
684          * @param boolean $force Force new cache entry
685          *
686          * @return array with the activity
687          * @throws \Exception
688          */
689         public static function createCachedActivityFromItem($item_id, $force = false)
690         {
691                 $cachekey = 'APDelivery:createActivity:' . $item_id;
692
693                 if (!$force) {
694                         $data = Cache::get($cachekey);
695                         if (!is_null($data)) {
696                                 return $data;
697                         }
698                 }
699
700                 $data = ActivityPub\Transmitter::createActivityFromItem($item_id);
701
702                 Cache::set($cachekey, $data, Cache::QUARTER_HOUR);
703                 return $data;
704         }
705
706         /**
707          * Creates an activity array for a given item id
708          *
709          * @param integer $item_id
710          * @param boolean $object_mode Is the activity item is used inside another object?
711          *
712          * @return array of activity
713          * @throws \Exception
714          */
715         public static function createActivityFromItem($item_id, $object_mode = false)
716         {
717                 $item = Item::selectFirst([], ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]);
718
719                 if (!DBA::isResult($item)) {
720                         return false;
721                 }
722
723                 if ($item['wall'] && ($item['uri'] == $item['parent-uri'])) {
724                         $owner = User::getOwnerDataById($item['uid']);
725                         if (($owner['account-type'] == User::ACCOUNT_TYPE_COMMUNITY) && ($item['author-link'] != $owner['url'])) {
726                                 $type = 'Announce';
727
728                                 // Disguise forum posts as reshares. Will later be converted to a real announce
729                                 $item['body'] = share_header($item['author-name'], $item['author-link'], $item['author-avatar'],
730                                         $item['guid'], $item['created'], $item['plink']) . $item['body'] . '[/share]';
731                         }
732                 }
733
734                 if (empty($type)) {
735                         $condition = ['item-uri' => $item['uri'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
736                         $conversation = DBA::selectFirst('conversation', ['source'], $condition);
737                         if (DBA::isResult($conversation)) {
738                                 $data = json_decode($conversation['source'], true);
739                                 if (!empty($data)) {
740                                         return $data;
741                                 }
742                         }
743
744                         $type = self::getTypeOfItem($item);
745                 }
746
747                 if (!$object_mode) {
748                         $data = ['@context' => ActivityPub::CONTEXT];
749
750                         if ($item['deleted'] && ($item['gravity'] == GRAVITY_ACTIVITY)) {
751                                 $type = 'Undo';
752                         } elseif ($item['deleted']) {
753                                 $type = 'Delete';
754                         }
755                 } else {
756                         $data = [];
757                 }
758
759                 $data['id'] = $item['uri'] . '#' . $type;
760                 $data['type'] = $type;
761
762                 if (Item::isForumPost($item) && ($type != 'Announce')) {
763                         $data['actor'] = $item['author-link'];
764                 } else {
765                         $data['actor'] = $item['owner-link'];
766                 }
767
768                 $data['published'] = DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM);
769
770                 $data['instrument'] = self::getService();
771
772                 $data = array_merge($data, self::createPermissionBlockForItem($item, false));
773
774                 if (in_array($data['type'], ['Create', 'Update', 'Delete'])) {
775                         $data['object'] = self::createNote($item);
776                 } elseif ($data['type'] == 'Announce') {
777                         $data = self::createAnnounce($item, $data);
778                 } elseif ($data['type'] == 'Follow') {
779                         $data['object'] = $item['parent-uri'];
780                 } elseif ($data['type'] == 'Undo') {
781                         $data['object'] = self::createActivityFromItem($item_id, true);
782                 } else {
783                         $data['diaspora:guid'] = $item['guid'];
784                         if (!empty($item['signed_text'])) {
785                                 $data['diaspora:like'] = $item['signed_text'];
786                         }
787                         $data['object'] = $item['thr-parent'];
788                 }
789
790                 if (!empty($item['contact-uid'])) {
791                         $uid = $item['contact-uid'];
792                 } else {
793                         $uid = $item['uid'];
794                 }
795
796                 $owner = User::getOwnerDataById($uid);
797
798                 if (!$object_mode && !empty($owner)) {
799                         return LDSignature::sign($data, $owner);
800                 } else {
801                         return $data;
802                 }
803
804                 /// @todo Create "conversation" entry
805         }
806
807         /**
808          * Creates an object array for a given item id
809          *
810          * @param integer $item_id
811          *
812          * @return array with the object data
813          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
814          * @throws \ImagickException
815          */
816         public static function createObjectFromItemID($item_id)
817         {
818                 $item = Item::selectFirst([], ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]);
819
820                 if (!DBA::isResult($item)) {
821                         return false;
822                 }
823
824                 $data = ['@context' => ActivityPub::CONTEXT];
825                 $data = array_merge($data, self::createNote($item));
826
827                 return $data;
828         }
829
830         /**
831          * Creates a location entry for a given item array
832          *
833          * @param array $item
834          *
835          * @return array with location array
836          */
837         private static function createLocation($item)
838         {
839                 $location = ['type' => 'Place'];
840
841                 if (!empty($item['location'])) {
842                         $location['name'] = $item['location'];
843                 }
844
845                 $coord = [];
846
847                 if (empty($item['coord'])) {
848                         $coord = Map::getCoordinates($item['location']);
849                 } else {
850                         $coords = explode(' ', $item['coord']);
851                         if (count($coords) == 2) {
852                                 $coord = ['lat' => $coords[0], 'lon' => $coords[1]];
853                         }
854                 }
855
856                 if (!empty($coord['lat']) && !empty($coord['lon'])) {
857                         $location['latitude'] = $coord['lat'];
858                         $location['longitude'] = $coord['lon'];
859                 }
860
861                 return $location;
862         }
863
864         /**
865          * Returns a tag array for a given item array
866          *
867          * @param array $item
868          *
869          * @return array of tags
870          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
871          */
872         private static function createTagList($item)
873         {
874                 $tags = [];
875
876                 $terms = Term::tagArrayFromItemId($item['id'], [Term::HASHTAG, Term::MENTION, Term::IMPLICIT_MENTION]);
877                 foreach ($terms as $term) {
878                         if ($term['type'] == Term::HASHTAG) {
879                                 $url = System::baseUrl() . '/search?tag=' . urlencode($term['term']);
880                                 $tags[] = ['type' => 'Hashtag', 'href' => $url, 'name' => '#' . $term['term']];
881                         } elseif ($term['type'] == Term::MENTION || $term['type'] == Term::IMPLICIT_MENTION) {
882                                 $contact = Contact::getDetailsByURL($term['url']);
883                                 if (!empty($contact['addr'])) {
884                                         $mention = '@' . $contact['addr'];
885                                 } else {
886                                         $mention = '@' . $term['url'];
887                                 }
888
889                                 $tags[] = ['type' => 'Mention', 'href' => $term['url'], 'name' => $mention];
890                         }
891                 }
892                 return $tags;
893         }
894
895         /**
896          * Adds attachment data to the JSON document
897          *
898          * @param array  $item Data of the item that is to be posted
899          * @param string $type Object type
900          *
901          * @return array with attachment data
902          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
903          */
904         private static function createAttachmentList($item, $type)
905         {
906                 $attachments = [];
907
908                 $arr = explode('[/attach],', $item['attach']);
909                 if (count($arr)) {
910                         foreach ($arr as $r) {
911                                 $matches = false;
912                                 $cnt = preg_match('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\" title=\"(.*?)\"|', $r, $matches);
913                                 if ($cnt) {
914                                         $attributes = ['type' => 'Document',
915                                                         'mediaType' => $matches[3],
916                                                         'url' => $matches[1],
917                                                         'name' => null];
918
919                                         if (trim($matches[4]) != '') {
920                                                 $attributes['name'] = trim($matches[4]);
921                                         }
922
923                                         $attachments[] = $attributes;
924                                 }
925                         }
926                 }
927
928                 if ($type != 'Note') {
929                         return $attachments;
930                 }
931
932                 // Simplify image codes
933                 $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $item['body']);
934
935                 // Grab all pictures and create attachments out of them
936                 if (preg_match_all("/\[img\]([^\[\]]*)\[\/img\]/Usi", $body, $pictures)) {
937                         foreach ($pictures[1] as $picture) {
938                                 $imgdata = Image::getInfoFromURL($picture);
939                                 if ($imgdata) {
940                                         $attachments[] = ['type' => 'Document',
941                                                 'mediaType' => $imgdata['mime'],
942                                                 'url' => $picture,
943                                                 'name' => null];
944                                 }
945                         }
946                 }
947
948                 return $attachments;
949         }
950
951         /**
952          * @brief Callback function to replace a Friendica style mention in a mention that is used on AP
953          *
954          * @param array $match Matching values for the callback
955          * @return string Replaced mention
956          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
957          */
958         private static function mentionCallback($match)
959         {
960                 if (empty($match[1])) {
961                         return '';
962                 }
963
964                 $data = Contact::getDetailsByURL($match[1]);
965                 if (empty($data['nick'])) {
966                         return $match[0];
967                 }
968
969                 return '@[url=' . $data['url'] . ']' . $data['nick'] . '[/url]';
970         }
971
972         /**
973          * Remove image elements and replaces them with links to the image
974          *
975          * @param string $body
976          *
977          * @return string with replaced elements
978          */
979         private static function removePictures($body)
980         {
981                 // Simplify image codes
982                 $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
983
984                 $body = preg_replace("/\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]/Usi", '[url]$1[/url]', $body);
985                 $body = preg_replace("/\[img\]([^\[\]]*)\[\/img\]/Usi", '[url]$1[/url]', $body);
986
987                 return $body;
988         }
989
990         /**
991          * Fetches the "context" value for a givem item array from the "conversation" table
992          *
993          * @param array $item
994          *
995          * @return string with context url
996          * @throws \Exception
997          */
998         private static function fetchContextURLForItem($item)
999         {
1000                 $conversation = DBA::selectFirst('conversation', ['conversation-href', 'conversation-uri'], ['item-uri' => $item['parent-uri']]);
1001                 if (DBA::isResult($conversation) && !empty($conversation['conversation-href'])) {
1002                         $context_uri = $conversation['conversation-href'];
1003                 } elseif (DBA::isResult($conversation) && !empty($conversation['conversation-uri'])) {
1004                         $context_uri = $conversation['conversation-uri'];
1005                 } else {
1006                         $context_uri = $item['parent-uri'] . '#context';
1007                 }
1008                 return $context_uri;
1009         }
1010
1011         /**
1012          * Returns if the post contains sensitive content ("nsfw")
1013          *
1014          * @param integer $item_id
1015          *
1016          * @return boolean
1017          * @throws \Exception
1018          */
1019         private static function isSensitive($item_id)
1020         {
1021                 $condition = ['otype' => TERM_OBJ_POST, 'oid' => $item_id, 'type' => TERM_HASHTAG, 'term' => 'nsfw'];
1022                 return DBA::exists('term', $condition);
1023         }
1024
1025         /**
1026          * Creates event data
1027          *
1028          * @param array $item
1029          *
1030          * @return array with the event data
1031          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1032          */
1033         public static function createEvent($item)
1034         {
1035                 $event = [];
1036                 $event['name'] = $item['event-summary'];
1037                 $event['content'] = BBCode::convert($item['event-desc'], false, 7);
1038                 $event['startTime'] = DateTimeFormat::utc($item['event-start'] . '+00:00', DateTimeFormat::ATOM);
1039
1040                 if (!$item['event-nofinish']) {
1041                         $event['endTime'] = DateTimeFormat::utc($item['event-finish'] . '+00:00', DateTimeFormat::ATOM);
1042                 }
1043
1044                 if (!empty($item['event-location'])) {
1045                         $item['location'] = $item['event-location'];
1046                         $event['location'] = self::createLocation($item);
1047                 }
1048
1049                 return $event;
1050         }
1051
1052         /**
1053          * Creates a note/article object array
1054          *
1055          * @param array $item
1056          *
1057          * @return array with the object data
1058          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1059          * @throws \ImagickException
1060          */
1061         public static function createNote($item)
1062         {
1063                 if ($item['event-type'] == 'event') {
1064                         $type = 'Event';
1065                 } elseif (!empty($item['title'])) {
1066                         $type = 'Article';
1067                 } else {
1068                         $type = 'Note';
1069                 }
1070
1071                 if ($item['deleted']) {
1072                         $type = 'Tombstone';
1073                 }
1074
1075                 $data = [];
1076                 $data['id'] = $item['uri'];
1077                 $data['type'] = $type;
1078
1079                 if ($item['deleted']) {
1080                         return $data;
1081                 }
1082
1083                 $data['summary'] = BBCode::toPlaintext(BBCode::getAbstract($item['body'], Protocol::ACTIVITYPUB));
1084
1085                 if ($item['uri'] != $item['thr-parent']) {
1086                         $data['inReplyTo'] = $item['thr-parent'];
1087                 } else {
1088                         $data['inReplyTo'] = null;
1089                 }
1090
1091                 $data['diaspora:guid'] = $item['guid'];
1092                 $data['published'] = DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM);
1093
1094                 if ($item['created'] != $item['edited']) {
1095                         $data['updated'] = DateTimeFormat::utc($item['edited'] . '+00:00', DateTimeFormat::ATOM);
1096                 }
1097
1098                 $data['url'] = $item['plink'];
1099                 $data['attributedTo'] = $item['author-link'];
1100                 $data['sensitive'] = self::isSensitive($item['id']);
1101                 $data['context'] = self::fetchContextURLForItem($item);
1102
1103                 if (!empty($item['title'])) {
1104                         $data['name'] = BBCode::toPlaintext($item['title'], false);
1105                 }
1106
1107                 $permission_block = self::createPermissionBlockForItem($item, false);
1108
1109                 $body = $item['body'];
1110
1111                 if (empty($item['uid']) || !Feature::isEnabled($item['uid'], 'explicit_mentions')) {
1112                         $body = self::prependMentions($body, $permission_block);
1113                 }
1114
1115                 if ($type == 'Note') {
1116                         $body = self::removePictures($body);
1117                 } elseif (($type == 'Article') && empty($data['summary'])) {
1118                         $data['summary'] = BBCode::toPlaintext(Plaintext::shorten(self::removePictures($body), 1000));
1119                 }
1120
1121                 if ($type == 'Event') {
1122                         $data = array_merge($data, self::createEvent($item));
1123                 } else {
1124                         $regexp = "/[@!]\[url\=([^\[\]]*)\].*?\[\/url\]/ism";
1125                         $body = preg_replace_callback($regexp, ['self', 'mentionCallback'], $body);
1126
1127                         $data['content'] = BBCode::convert($body, false, 7);
1128                 }
1129
1130                 $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"];
1131
1132                 if (!empty($item['signed_text']) && ($item['uri'] != $item['thr-parent'])) {
1133                         $data['diaspora:comment'] = $item['signed_text'];
1134                 }
1135
1136                 $data['attachment'] = self::createAttachmentList($item, $type);
1137                 $data['tag'] = self::createTagList($item);
1138
1139                 if (empty($data['location']) && (!empty($item['coord']) || !empty($item['location']))) {
1140                         $data['location'] = self::createLocation($item);
1141                 }
1142
1143                 if (!empty($item['app'])) {
1144                         $data['generator'] = ['type' => 'Application', 'name' => $item['app']];
1145                 }
1146
1147                 $data = array_merge($data, $permission_block);
1148
1149                 return $data;
1150         }
1151
1152         /**
1153          * Creates an announce object entry
1154          *
1155          * @param array $item
1156          * @param array $data activity data
1157          *
1158          * @return array with activity data
1159          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1160          * @throws \ImagickException
1161          */
1162         private static function createAnnounce($item, $data)
1163         {
1164                 $announce = api_share_as_retweet($item);
1165                 if (empty($announce['plink'])) {
1166                         $data['type'] = 'Create';
1167                         $data['object'] = self::createNote($item);
1168                         return $data;
1169                 }
1170
1171                 // Fetch the original id of the object
1172                 $activity = ActivityPub::fetchContent($announce['plink'], $item['uid']);
1173                 if (!empty($activity)) {
1174                         $ldactivity = JsonLD::compact($activity);
1175                         $id = JsonLD::fetchElement($ldactivity, '@id');
1176                         if (!empty($id)) {
1177                                 $data['object'] = $id;
1178                                 return $data;
1179                         }
1180                 }
1181
1182                 $data['type'] = 'Create';
1183                 $data['object'] = self::createNote($item);
1184                 return $data;
1185         }
1186
1187         /**
1188          * Creates an activity id for a given contact id
1189          *
1190          * @param integer $cid Contact ID of target
1191          *
1192          * @return bool|string activity id
1193          */
1194         public static function activityIDFromContact($cid)
1195         {
1196                 $contact = DBA::selectFirst('contact', ['uid', 'id', 'created'], ['id' => $cid]);
1197                 if (!DBA::isResult($contact)) {
1198                         return false;
1199                 }
1200
1201                 $hash = hash('ripemd128', $contact['uid'].'-'.$contact['id'].'-'.$contact['created']);
1202                 $uuid = substr($hash, 0, 8). '-' . substr($hash, 8, 4) . '-' . substr($hash, 12, 4) . '-' . substr($hash, 16, 4) . '-' . substr($hash, 20, 12);
1203                 return System::baseUrl() . '/activity/' . $uuid;
1204         }
1205
1206         /**
1207          * Transmits a contact suggestion to a given inbox
1208          *
1209          * @param integer $uid           User ID
1210          * @param string  $inbox         Target inbox
1211          * @param integer $suggestion_id Suggestion ID
1212          *
1213          * @return boolean was the transmission successful?
1214          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1215          */
1216         public static function sendContactSuggestion($uid, $inbox, $suggestion_id)
1217         {
1218                 $owner = User::getOwnerDataById($uid);
1219
1220                 $suggestion = DBA::selectFirst('fsuggest', ['url', 'note', 'created'], ['id' => $suggestion_id]);
1221
1222                 $data = ['@context' => ActivityPub::CONTEXT,
1223                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
1224                         'type' => 'Announce',
1225                         'actor' => $owner['url'],
1226                         'object' => $suggestion['url'],
1227                         'content' => $suggestion['note'],
1228                         'instrument' => self::getService(),
1229                         'to' => [ActivityPub::PUBLIC_COLLECTION],
1230                         'cc' => []];
1231
1232                 $signed = LDSignature::sign($data, $owner);
1233
1234                 Logger::log('Deliver profile deletion for user ' . $uid . ' to ' . $inbox . ' via ActivityPub', Logger::DEBUG);
1235                 return HTTPSignature::transmit($signed, $inbox, $uid);
1236         }
1237
1238         /**
1239          * Transmits a profile relocation to a given inbox
1240          *
1241          * @param integer $uid   User ID
1242          * @param string  $inbox Target inbox
1243          *
1244          * @return boolean was the transmission successful?
1245          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1246          */
1247         public static function sendProfileRelocation($uid, $inbox)
1248         {
1249                 $owner = User::getOwnerDataById($uid);
1250
1251                 $data = ['@context' => ActivityPub::CONTEXT,
1252                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
1253                         'type' => 'dfrn:relocate',
1254                         'actor' => $owner['url'],
1255                         'object' => $owner['url'],
1256                         'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
1257                         'instrument' => self::getService(),
1258                         'to' => [ActivityPub::PUBLIC_COLLECTION],
1259                         'cc' => []];
1260
1261                 $signed = LDSignature::sign($data, $owner);
1262
1263                 Logger::log('Deliver profile relocation for user ' . $uid . ' to ' . $inbox . ' via ActivityPub', Logger::DEBUG);
1264                 return HTTPSignature::transmit($signed, $inbox, $uid);
1265         }
1266
1267         /**
1268          * Transmits a profile deletion to a given inbox
1269          *
1270          * @param integer $uid   User ID
1271          * @param string  $inbox Target inbox
1272          *
1273          * @return boolean was the transmission successful?
1274          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1275          */
1276         public static function sendProfileDeletion($uid, $inbox)
1277         {
1278                 $owner = User::getOwnerDataById($uid);
1279
1280                 if (empty($owner)) {
1281                         Logger::error('No owner data found, the deletion message cannot be processed.', ['user' => $uid]);
1282                         return false;
1283                 }
1284
1285                 if (empty($owner['uprvkey'])) {
1286                         Logger::error('No private key for owner found, the deletion message cannot be processed.', ['user' => $uid]);
1287                         return false;
1288                 }
1289
1290                 $data = ['@context' => ActivityPub::CONTEXT,
1291                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
1292                         'type' => 'Delete',
1293                         'actor' => $owner['url'],
1294                         'object' => $owner['url'],
1295                         'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
1296                         'instrument' => self::getService(),
1297                         'to' => [ActivityPub::PUBLIC_COLLECTION],
1298                         'cc' => []];
1299
1300                 $signed = LDSignature::sign($data, $owner);
1301
1302                 Logger::log('Deliver profile deletion for user ' . $uid . ' to ' . $inbox . ' via ActivityPub', Logger::DEBUG);
1303                 return HTTPSignature::transmit($signed, $inbox, $uid);
1304         }
1305
1306         /**
1307          * Transmits a profile change to a given inbox
1308          *
1309          * @param integer $uid   User ID
1310          * @param string  $inbox Target inbox
1311          *
1312          * @return boolean was the transmission successful?
1313          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1314          * @throws \ImagickException
1315          */
1316         public static function sendProfileUpdate($uid, $inbox)
1317         {
1318                 $owner = User::getOwnerDataById($uid);
1319                 $profile = APContact::getByURL($owner['url']);
1320
1321                 $data = ['@context' => ActivityPub::CONTEXT,
1322                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
1323                         'type' => 'Update',
1324                         'actor' => $owner['url'],
1325                         'object' => self::getProfile($uid),
1326                         'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
1327                         'instrument' => self::getService(),
1328                         'to' => [$profile['followers']],
1329                         'cc' => []];
1330
1331                 $signed = LDSignature::sign($data, $owner);
1332
1333                 Logger::log('Deliver profile update for user ' . $uid . ' to ' . $inbox . ' via ActivityPub', Logger::DEBUG);
1334                 return HTTPSignature::transmit($signed, $inbox, $uid);
1335         }
1336
1337         /**
1338          * Transmits a given activity to a target
1339          *
1340          * @param string  $activity Type name
1341          * @param string  $target   Target profile
1342          * @param integer $uid      User ID
1343          * @return bool
1344          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1345          * @throws \ImagickException
1346          * @throws \Exception
1347          */
1348         public static function sendActivity($activity, $target, $uid, $id = '')
1349         {
1350                 $profile = APContact::getByURL($target);
1351
1352                 $owner = User::getOwnerDataById($uid);
1353
1354                 if (empty($id)) {
1355                         $id = System::baseUrl() . '/activity/' . System::createGUID();
1356                 }
1357
1358                 $data = ['@context' => ActivityPub::CONTEXT,
1359                         'id' => $id,
1360                         'type' => $activity,
1361                         'actor' => $owner['url'],
1362                         'object' => $profile['url'],
1363                         'instrument' => self::getService(),
1364                         'to' => [$profile['url']]];
1365
1366                 Logger::log('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, Logger::DEBUG);
1367
1368                 $signed = LDSignature::sign($data, $owner);
1369                 return HTTPSignature::transmit($signed, $profile['inbox'], $uid);
1370         }
1371
1372         /**
1373          * Transmits a "follow object" activity to a target
1374          * This is a preparation for sending automated "follow" requests when receiving "Announce" messages
1375          *
1376          * @param string  $object Object URL
1377          * @param string  $target Target profile
1378          * @param integer $uid    User ID
1379          * @return bool
1380          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1381          * @throws \ImagickException
1382          * @throws \Exception
1383          */
1384         public static function sendFollowObject($object, $target, $uid = 0)
1385         {
1386                 $profile = APContact::getByURL($target);
1387
1388                 if (empty($uid)) {
1389                         // Fetch the list of administrators
1390                         $admin_mail = explode(',', str_replace(' ', '', Config::get('config', 'admin_email')));
1391
1392                         // We need to use some user as a sender. It doesn't care who it will send. We will use an administrator account.
1393                         $condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false, 'email' => $admin_mail];
1394                         $first_user = DBA::selectFirst('user', ['uid'], $condition);
1395                         $uid = $first_user['uid'];
1396                 }
1397
1398                 $condition = ['verb' => ACTIVITY_FOLLOW, 'uid' => 0, 'parent-uri' => $object,
1399                         'author-id' => Contact::getPublicIdByUserId($uid)];
1400                 if (Item::exists($condition)) {
1401                         Logger::log('Follow for ' . $object . ' for user ' . $uid . ' does already exist.', Logger::DEBUG);
1402                         return false;
1403                 }
1404
1405                 $owner = User::getOwnerDataById($uid);
1406
1407                 $data = ['@context' => ActivityPub::CONTEXT,
1408                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
1409                         'type' => 'Follow',
1410                         'actor' => $owner['url'],
1411                         'object' => $object,
1412                         'instrument' => self::getService(),
1413                         'to' => [$profile['url']]];
1414
1415                 Logger::log('Sending follow ' . $object . ' to ' . $target . ' for user ' . $uid, Logger::DEBUG);
1416
1417                 $signed = LDSignature::sign($data, $owner);
1418                 return HTTPSignature::transmit($signed, $profile['inbox'], $uid);
1419         }
1420
1421         /**
1422          * Transmit a message that the contact request had been accepted
1423          *
1424          * @param string  $target Target profile
1425          * @param         $id
1426          * @param integer $uid    User ID
1427          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1428          * @throws \ImagickException
1429          */
1430         public static function sendContactAccept($target, $id, $uid)
1431         {
1432                 $profile = APContact::getByURL($target);
1433
1434                 $owner = User::getOwnerDataById($uid);
1435                 $data = ['@context' => ActivityPub::CONTEXT,
1436                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
1437                         'type' => 'Accept',
1438                         'actor' => $owner['url'],
1439                         'object' => ['id' => $id, 'type' => 'Follow',
1440                                 'actor' => $profile['url'],
1441                                 'object' => $owner['url']],
1442                         'instrument' => self::getService(),
1443                         'to' => [$profile['url']]];
1444
1445                 Logger::log('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, Logger::DEBUG);
1446
1447                 $signed = LDSignature::sign($data, $owner);
1448                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
1449         }
1450
1451         /**
1452          * Reject a contact request or terminates the contact relation
1453          *
1454          * @param string  $target Target profile
1455          * @param         $id
1456          * @param integer $uid    User ID
1457          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1458          * @throws \ImagickException
1459          */
1460         public static function sendContactReject($target, $id, $uid)
1461         {
1462                 $profile = APContact::getByURL($target);
1463
1464                 $owner = User::getOwnerDataById($uid);
1465                 $data = ['@context' => ActivityPub::CONTEXT,
1466                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
1467                         'type' => 'Reject',
1468                         'actor' => $owner['url'],
1469                         'object' => ['id' => $id, 'type' => 'Follow',
1470                                 'actor' => $profile['url'],
1471                                 'object' => $owner['url']],
1472                         'instrument' => self::getService(),
1473                         'to' => [$profile['url']]];
1474
1475                 Logger::log('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, Logger::DEBUG);
1476
1477                 $signed = LDSignature::sign($data, $owner);
1478                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
1479         }
1480
1481         /**
1482          * Transmits a message that we don't want to follow this contact anymore
1483          *
1484          * @param string  $target Target profile
1485          * @param integer $uid    User ID
1486          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1487          * @throws \ImagickException
1488          * @throws \Exception
1489          */
1490         public static function sendContactUndo($target, $cid, $uid)
1491         {
1492                 $profile = APContact::getByURL($target);
1493
1494                 $object_id = self::activityIDFromContact($cid);
1495                 if (empty($object_id)) {
1496                         return;
1497                 }
1498
1499                 $id = System::baseUrl() . '/activity/' . System::createGUID();
1500
1501                 $owner = User::getOwnerDataById($uid);
1502                 $data = ['@context' => ActivityPub::CONTEXT,
1503                         'id' => $id,
1504                         'type' => 'Undo',
1505                         'actor' => $owner['url'],
1506                         'object' => ['id' => $object_id, 'type' => 'Follow',
1507                                 'actor' => $owner['url'],
1508                                 'object' => $profile['url']],
1509                         'instrument' => self::getService(),
1510                         'to' => [$profile['url']]];
1511
1512                 Logger::log('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, Logger::DEBUG);
1513
1514                 $signed = LDSignature::sign($data, $owner);
1515                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
1516         }
1517
1518         private static function prependMentions($body, array $permission_block)
1519         {
1520                 if (Config::get('system', 'disable_implicit_mentions')) {
1521                         return $body;
1522                 }
1523
1524                 $mentions = [];
1525
1526                 foreach ($permission_block['to'] as $profile_url) {
1527                         $profile = Contact::getDetailsByURL($profile_url);
1528                         if (!empty($profile['addr'])
1529                                 && $profile['contact-type'] != Contact::TYPE_COMMUNITY
1530                                 && !strstr($body, $profile['addr'])
1531                                 && !strstr($body, $profile_url)
1532                         ) {
1533                                 $mentions[] = '@[url=' . $profile_url . ']' . $profile['nick'] . '[/url]';
1534                         }
1535                 }
1536
1537                 $mentions[] = $body;
1538
1539                 return implode(' ', $mentions);
1540         }
1541 }