]> git.mxchange.org Git - friendica.git/blob - src/Protocol/ActivityPub/Transmitter.php
f5765ccdd31f5e509d7a34cda6887791b5ffe9af
[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\Database\DBA;
8 use Friendica\Core\System;
9 use Friendica\Util\HTTPSignature;
10 use Friendica\Core\Protocol;
11 use Friendica\Model\Conversation;
12 use Friendica\Model\Contact;
13 use Friendica\Model\APContact;
14 use Friendica\Model\Item;
15 use Friendica\Model\Term;
16 use Friendica\Model\User;
17 use Friendica\Util\DateTimeFormat;
18 use Friendica\Content\Text\BBCode;
19 use Friendica\Util\JsonLD;
20 use Friendica\Util\LDSignature;
21 use Friendica\Model\Profile;
22 use Friendica\Core\Config;
23 use Friendica\Object\Image;
24 use Friendica\Protocol\ActivityPub;
25 use Friendica\Protocol\Diaspora;
26 use Friendica\Core\Cache;
27
28 require_once 'include/api.php';
29
30 /**
31  * @brief ActivityPub Transmitter Protocol class
32  *
33  * To-Do:
34  *
35  * Missing object fields:
36  * - service (App)
37  * - location
38  *
39  * Missing object types:
40  * - Event
41  *
42  * Complicated object types:
43  * - Undo Announce
44  *
45  * General:
46  * - Queueing unsucessful deliveries
47  */
48 class Transmitter
49 {
50         /**
51          * collects the lost of followers of the given owner
52          *
53          * @param array $owner Owner array
54          * @param integer $page Page number
55          *
56          * @return array of owners
57          */
58         public static function getFollowers($owner, $page = null)
59         {
60                 $condition = ['rel' => [Contact::FOLLOWER, Contact::FRIEND], 'network' => Protocol::NATIVE_SUPPORT, 'uid' => $owner['uid'],
61                         'self' => false, 'hidden' => false, 'archive' => false, 'pending' => false];
62                 $count = DBA::count('contact', $condition);
63
64                 $data = ['@context' => ActivityPub::CONTEXT];
65                 $data['id'] = System::baseUrl() . '/followers/' . $owner['nickname'];
66                 $data['type'] = 'OrderedCollection';
67                 $data['totalItems'] = $count;
68
69                 // When we hide our friends we will only show the pure number but don't allow more.
70                 $profile = Profile::getByUID($owner['uid']);
71                 if (!empty($profile['hide-friends'])) {
72                         return $data;
73                 }
74
75                 if (empty($page)) {
76                         $data['first'] = System::baseUrl() . '/followers/' . $owner['nickname'] . '?page=1';
77                 } else {
78                         $list = [];
79
80                         $contacts = DBA::select('contact', ['url'], $condition, ['limit' => [($page - 1) * 100, 100]]);
81                         while ($contact = DBA::fetch($contacts)) {
82                                 $list[] = $contact['url'];
83                         }
84
85                         if (!empty($list)) {
86                                 $data['next'] = System::baseUrl() . '/followers/' . $owner['nickname'] . '?page=' . ($page + 1);
87                         }
88
89                         $data['partOf'] = System::baseUrl() . '/followers/' . $owner['nickname'];
90
91                         $data['orderedItems'] = $list;
92                 }
93
94                 return $data;
95         }
96
97         /**
98          * Create list of following contacts
99          *
100          * @param array $owner Owner array
101          * @param integer $page Page numbe
102          *
103          * @return array of following contacts
104          */
105         public static function getFollowing($owner, $page = null)
106         {
107                 $condition = ['rel' => [Contact::SHARING, Contact::FRIEND], 'network' => Protocol::NATIVE_SUPPORT, 'uid' => $owner['uid'],
108                         'self' => false, 'hidden' => false, 'archive' => false, 'pending' => false];
109                 $count = DBA::count('contact', $condition);
110
111                 $data = ['@context' => ActivityPub::CONTEXT];
112                 $data['id'] = System::baseUrl() . '/following/' . $owner['nickname'];
113                 $data['type'] = 'OrderedCollection';
114                 $data['totalItems'] = $count;
115
116                 // When we hide our friends we will only show the pure number but don't allow more.
117                 $profile = Profile::getByUID($owner['uid']);
118                 if (!empty($profile['hide-friends'])) {
119                         return $data;
120                 }
121
122                 if (empty($page)) {
123                         $data['first'] = System::baseUrl() . '/following/' . $owner['nickname'] . '?page=1';
124                 } else {
125                         $list = [];
126
127                         $contacts = DBA::select('contact', ['url'], $condition, ['limit' => [($page - 1) * 100, 100]]);
128                         while ($contact = DBA::fetch($contacts)) {
129                                 $list[] = $contact['url'];
130                         }
131
132                         if (!empty($list)) {
133                                 $data['next'] = System::baseUrl() . '/following/' . $owner['nickname'] . '?page=' . ($page + 1);
134                         }
135
136                         $data['partOf'] = System::baseUrl() . '/following/' . $owner['nickname'];
137
138                         $data['orderedItems'] = $list;
139                 }
140
141                 return $data;
142         }
143
144         /**
145          * Public posts for the given owner
146          *
147          * @param array $owner Owner array
148          * @param integer $page Page numbe
149          *
150          * @return array of posts
151          */
152         public static function getOutbox($owner, $page = null)
153         {
154                 $public_contact = Contact::getIdForURL($owner['url'], 0, true);
155
156                 $condition = ['uid' => $owner['uid'], 'contact-id' => $owner['id'], 'author-id' => $public_contact,
157                         'wall' => true, 'private' => false, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT],
158                         'deleted' => false, 'visible' => true];
159                 $count = DBA::count('item', $condition);
160
161                 $data = ['@context' => ActivityPub::CONTEXT];
162                 $data['id'] = System::baseUrl() . '/outbox/' . $owner['nickname'];
163                 $data['type'] = 'OrderedCollection';
164                 $data['totalItems'] = $count;
165
166                 if (empty($page)) {
167                         $data['first'] = System::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=1';
168                 } else {
169                         $list = [];
170
171                         $condition['parent-network'] = Protocol::NATIVE_SUPPORT;
172
173                         $items = Item::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]);
174                         while ($item = Item::fetch($items)) {
175                                 $object = self::createObjectFromItemID($item['id']);
176                                 unset($object['@context']);
177                                 $list[] = $object;
178                         }
179
180                         if (!empty($list)) {
181                                 $data['next'] = System::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=' . ($page + 1);
182                         }
183
184                         $data['partOf'] = System::baseUrl() . '/outbox/' . $owner['nickname'];
185
186                         $data['orderedItems'] = $list;
187                 }
188
189                 return $data;
190         }
191
192         /**
193          * Return the ActivityPub profile of the given user
194          *
195          * @param integer $uid User ID
196          * @return array with profile data
197          */
198         public static function getProfile($uid)
199         {
200                 $condition = ['uid' => $uid, 'blocked' => false, 'account_expired' => false,
201                         'account_removed' => false, 'verified' => true];
202                 $fields = ['guid', 'nickname', 'pubkey', 'account-type', 'page-flags'];
203                 $user = DBA::selectFirst('user', $fields, $condition);
204                 if (!DBA::isResult($user)) {
205                         return [];
206                 }
207
208                 $fields = ['locality', 'region', 'country-name'];
209                 $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid, 'is-default' => true]);
210                 if (!DBA::isResult($profile)) {
211                         return [];
212                 }
213
214                 $fields = ['name', 'url', 'location', 'about', 'avatar', 'photo'];
215                 $contact = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]);
216                 if (!DBA::isResult($contact)) {
217                         return [];
218                 }
219
220                 // On old installations and never changed contacts this might not be filled
221                 if (empty($contact['avatar'])) {
222                         $contact['avatar'] = $contact['photo'];
223                 }
224
225                 $data = ['@context' => ActivityPub::CONTEXT];
226                 $data['id'] = $contact['url'];
227                 $data['diaspora:guid'] = $user['guid'];
228                 $data['type'] = ActivityPub::ACCOUNT_TYPES[$user['account-type']];
229                 $data['following'] = System::baseUrl() . '/following/' . $user['nickname'];
230                 $data['followers'] = System::baseUrl() . '/followers/' . $user['nickname'];
231                 $data['inbox'] = System::baseUrl() . '/inbox/' . $user['nickname'];
232                 $data['outbox'] = System::baseUrl() . '/outbox/' . $user['nickname'];
233                 $data['preferredUsername'] = $user['nickname'];
234                 $data['name'] = $contact['name'];
235                 $data['vcard:hasAddress'] = ['@type' => 'vcard:Home', 'vcard:country-name' => $profile['country-name'],
236                         'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']];
237                 $data['summary'] = $contact['about'];
238                 $data['url'] = $contact['url'];
239                 $data['manuallyApprovesFollowers'] = in_array($user['page-flags'], [Contact::PAGE_NORMAL, Contact::PAGE_PRVGROUP]);
240                 $data['publicKey'] = ['id' => $contact['url'] . '#main-key',
241                         'owner' => $contact['url'],
242                         'publicKeyPem' => $user['pubkey']];
243                 $data['endpoints'] = ['sharedInbox' => System::baseUrl() . '/inbox'];
244                 $data['icon'] = ['type' => 'Image',
245                         'url' => $contact['avatar']];
246
247                 // tags: https://kitty.town/@inmysocks/100656097926961126.json
248                 return $data;
249         }
250
251         /**
252          * Returns an array with permissions of a given item array
253          *
254          * @param array $item
255          *
256          * @return array with permissions
257          */
258         private static function fetchPermissionBlockFromConversation($item)
259         {
260                 if (empty($item['thr-parent'])) {
261                         return [];
262                 }
263
264                 $condition = ['item-uri' => $item['thr-parent'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
265                 $conversation = DBA::selectFirst('conversation', ['source'], $condition);
266                 if (!DBA::isResult($conversation)) {
267                         return [];
268                 }
269
270                 $activity = json_decode($conversation['source'], true);
271
272                 $actor = JsonLD::fetchElement($activity, 'actor', 'id');
273                 $profile = APContact::getByURL($actor);
274
275                 $item_profile = APContact::getByURL($item['author-link']);
276                 $exclude[] = $item['author-link'];
277
278                 if ($item['gravity'] == GRAVITY_PARENT) {
279                         $exclude[] = $item['owner-link'];
280                 }
281
282                 $permissions['to'][] = $actor;
283
284                 foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
285                         if (empty($activity[$element])) {
286                                 continue;
287                         }
288                         if (is_string($activity[$element])) {
289                                 $activity[$element] = [$activity[$element]];
290                         }
291
292                         foreach ($activity[$element] as $receiver) {
293                                 if ($receiver == $profile['followers'] && !empty($item_profile['followers'])) {
294                                         $receiver = $item_profile['followers'];
295                                 }
296                                 if (!in_array($receiver, $exclude)) {
297                                         $permissions[$element][] = $receiver;
298                                 }
299                         }
300                 }
301                 return $permissions;
302         }
303
304         /**
305          * Creates an array of permissions from an item thread
306          *
307          * @param array $item
308          *
309          * @return array with permission data
310          */
311         private static function createPermissionBlockForItem($item)
312         {
313                 $data = ['to' => [], 'cc' => []];
314
315                 $data = array_merge($data, self::fetchPermissionBlockFromConversation($item));
316
317                 $actor_profile = APContact::getByURL($item['author-link']);
318
319                 $terms = Term::tagArrayFromItemId($item['id'], TERM_MENTION);
320
321                 $contacts[$item['author-link']] = $item['author-link'];
322
323                 if (!$item['private']) {
324                         $data['to'][] = ActivityPub::PUBLIC_COLLECTION;
325                         if (!empty($actor_profile['followers'])) {
326                                 $data['cc'][] = $actor_profile['followers'];
327                         }
328
329                         foreach ($terms as $term) {
330                                 $profile = APContact::getByURL($term['url'], false);
331                                 if (!empty($profile) && empty($contacts[$profile['url']])) {
332                                         $data['to'][] = $profile['url'];
333                                         $contacts[$profile['url']] = $profile['url'];
334                                 }
335                         }
336                 } else {
337                         $receiver_list = Item::enumeratePermissions($item);
338
339                         $mentioned = [];
340
341                         foreach ($terms as $term) {
342                                 $cid = Contact::getIdForURL($term['url'], $item['uid']);
343                                 if (!empty($cid) && in_array($cid, $receiver_list)) {
344                                         $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid, 'network' => Protocol::ACTIVITYPUB]);
345                                         $data['to'][] = $contact['url'];
346                                         $contacts[$contact['url']] = $contact['url'];
347                                 }
348                         }
349
350                         foreach ($receiver_list as $receiver) {
351                                 $contact = DBA::selectFirst('contact', ['url'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]);
352                                 if (empty($contacts[$contact['url']])) {
353                                         $data['cc'][] = $contact['url'];
354                                         $contacts[$contact['url']] = $contact['url'];
355                                 }
356                         }
357                 }
358
359                 $parents = Item::select(['id', 'author-link', 'owner-link', 'gravity'], ['parent' => $item['parent']]);
360                 while ($parent = Item::fetch($parents)) {
361                         // Don't include data from future posts
362                         if ($parent['id'] >= $item['id']) {
363                                 continue;
364                         }
365
366                         $profile = APContact::getByURL($parent['author-link'], false);
367                         if (!empty($profile) && empty($contacts[$profile['url']])) {
368                                 $data['cc'][] = $profile['url'];
369                                 $contacts[$profile['url']] = $profile['url'];
370                         }
371
372                         if ($item['gravity'] != GRAVITY_PARENT) {
373                                 continue;
374                         }
375
376                         $profile = APContact::getByURL($parent['owner-link'], false);
377                         if (!empty($profile) && empty($contacts[$profile['url']])) {
378                                 $data['cc'][] = $profile['url'];
379                                 $contacts[$profile['url']] = $profile['url'];
380                         }
381                 }
382                 DBA::close($parents);
383
384                 return $data;
385         }
386
387         /**
388          * Fetches a list of inboxes of followers of a given user
389          *
390          * @param integer $uid User ID
391          *
392          * @return array of follower inboxes
393          */
394         public static function fetchTargetInboxesforUser($uid)
395         {
396                 $inboxes = [];
397
398                 $condition = ['uid' => $uid, 'network' => Protocol::ACTIVITYPUB, 'archive' => false, 'pending' => false];
399
400                 if (!empty($uid)) {
401                         $condition['rel'] = [Contact::FOLLOWER, Contact::FRIEND];
402                 }
403
404                 $contacts = DBA::select('contact', ['notify', 'batch'], $condition);
405                 while ($contact = DBA::fetch($contacts)) {
406                         $contact = defaults($contact, 'batch', $contact['notify']);
407                         $inboxes[$contact] = $contact;
408                 }
409                 DBA::close($contacts);
410
411                 return $inboxes;
412         }
413
414         /**
415          * Fetches an array of inboxes for the given item and user
416          *
417          * @param array $item
418          * @param integer $uid User ID
419          *
420          * @return array with inboxes
421          */
422         public static function fetchTargetInboxes($item, $uid)
423         {
424                 $permissions = self::createPermissionBlockForItem($item);
425                 if (empty($permissions)) {
426                         return [];
427                 }
428
429                 $inboxes = [];
430
431                 if ($item['gravity'] == GRAVITY_ACTIVITY) {
432                         $item_profile = APContact::getByURL($item['author-link']);
433                 } else {
434                         $item_profile = APContact::getByURL($item['owner-link']);
435                 }
436
437                 foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
438                         if (empty($permissions[$element])) {
439                                 continue;
440                         }
441
442                         foreach ($permissions[$element] as $receiver) {
443                                 if ($receiver == $item_profile['followers']) {
444                                         $inboxes = self::fetchTargetInboxesforUser($uid);
445                                 } else {
446                                         $profile = APContact::getByURL($receiver);
447                                         if (!empty($profile)) {
448                                                 $target = defaults($profile, 'sharedinbox', $profile['inbox']);
449                                                 $inboxes[$target] = $target;
450                                         }
451                                 }
452                         }
453                 }
454
455                 return $inboxes;
456         }
457
458         /**
459          * Returns the activity type of a given item
460          *
461          * @param array $item
462          *
463          * @return string with activity type
464          */
465         private static function getTypeOfItem($item)
466         {
467                 if (!empty(Diaspora::isReshare($item['body'], false))) {
468                         $type = 'Announce';
469                 } elseif ($item['verb'] == ACTIVITY_POST) {
470                         if ($item['created'] == $item['edited']) {
471                                 $type = 'Create';
472                         } else {
473                                 $type = 'Update';
474                         }
475                 } elseif ($item['verb'] == ACTIVITY_LIKE) {
476                         $type = 'Like';
477                 } elseif ($item['verb'] == ACTIVITY_DISLIKE) {
478                         $type = 'Dislike';
479                 } elseif ($item['verb'] == ACTIVITY_ATTEND) {
480                         $type = 'Accept';
481                 } elseif ($item['verb'] == ACTIVITY_ATTENDNO) {
482                         $type = 'Reject';
483                 } elseif ($item['verb'] == ACTIVITY_ATTENDMAYBE) {
484                         $type = 'TentativeAccept';
485                 } else {
486                         $type = '';
487                 }
488
489                 return $type;
490         }
491
492         /**
493          * Creates the activity or fetches it from the cache
494          *
495          * @param integer $item_id
496          *
497          * @return array with the activity
498          */
499         public static function createCachedActivityFromItem($item_id)
500         {
501                 $cachekey = 'APDelivery:createActivity:' . $item_id;
502                 $data = Cache::get($cachekey);
503                 if (!is_null($data)) {
504                         return $data;
505                 }
506
507                 $data = ActivityPub\Transmitter::createActivityFromItem($item_id);
508
509                 Cache::set($cachekey, $data, CACHE_QUARTER_HOUR);
510                 return $data;
511         }
512
513         /**
514          * Creates an activity array for a given item id
515          *
516          * @param integer $item_id
517          * @param boolean $object_mode Is the activity item is used inside another object?
518          *
519          * @return array of activity
520          */
521         public static function createActivityFromItem($item_id, $object_mode = false)
522         {
523                 $item = Item::selectFirst([], ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]);
524
525                 if (!DBA::isResult($item)) {
526                         return false;
527                 }
528
529                 $condition = ['item-uri' => $item['uri'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
530                 $conversation = DBA::selectFirst('conversation', ['source'], $condition);
531                 if (DBA::isResult($conversation)) {
532                         $data = json_decode($conversation['source']);
533                         if (!empty($data)) {
534                                 return $data;
535                         }
536                 }
537
538                 $type = self::getTypeOfItem($item);
539
540                 if (!$object_mode) {
541                         $data = ['@context' => ActivityPub::CONTEXT];
542
543                         if ($item['deleted'] && ($item['gravity'] == GRAVITY_ACTIVITY)) {
544                                 $type = 'Undo';
545                         } elseif ($item['deleted']) {
546                                 $type = 'Delete';
547                         }
548                 } else {
549                         $data = [];
550                 }
551
552                 $data['id'] = $item['uri'] . '#' . $type;
553                 $data['type'] = $type;
554                 $data['actor'] = $item['owner-link'];
555
556                 $data['published'] = DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM);
557
558                 $data = array_merge($data, self::createPermissionBlockForItem($item));
559
560                 if (in_array($data['type'], ['Create', 'Update', 'Delete'])) {
561                         $data['object'] = self::createNote($item);
562                 } elseif ($data['type'] == 'Announce') {
563                         $data['object'] = self::createAnnounce($item);
564                 } elseif ($data['type'] == 'Undo') {
565                         $data['object'] = self::createActivityFromItem($item_id, true);
566                 } else {
567                         $data['diaspora:guid'] = $item['guid'];
568                         $data['object'] = $item['thr-parent'];
569                 }
570
571                 $owner = User::getOwnerDataById($item['uid']);
572
573                 if (!$object_mode) {
574                         return LDSignature::sign($data, $owner);
575                 } else {
576                         return $data;
577                 }
578
579                 /// @todo Create "conversation" entry
580         }
581
582         /**
583          * Creates an object array for a given item id
584          *
585          * @param integer $item_id
586          *
587          * @return array with the object data
588          */
589         public static function createObjectFromItemID($item_id)
590         {
591                 $item = Item::selectFirst([], ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]);
592
593                 if (!DBA::isResult($item)) {
594                         return false;
595                 }
596
597                 $data = ['@context' => ActivityPub::CONTEXT];
598                 $data = array_merge($data, self::createNote($item));
599
600                 return $data;
601         }
602
603         /**
604          * Returns a tag array for a given item array
605          *
606          * @param array $item
607          *
608          * @return array of tags
609          */
610         private static function createTagList($item)
611         {
612                 $tags = [];
613
614                 $terms = Term::tagArrayFromItemId($item['id']);
615                 foreach ($terms as $term) {
616                         if ($term['type'] == TERM_HASHTAG) {
617                                 $tags[] = ['type' => 'Hashtag', 'href' => $term['url'], 'name' => '#' . $term['term']];
618                         } elseif ($term['type'] == TERM_MENTION) {
619                                 $contact = Contact::getDetailsByURL($term['url']);
620                                 if (!empty($contact['addr'])) {
621                                         $mention = '@' . $contact['addr'];
622                                 } else {
623                                         $mention = '@' . $term['url'];
624                                 }
625
626                                 $tags[] = ['type' => 'Mention', 'href' => $term['url'], 'name' => $mention];
627                         }
628                 }
629                 return $tags;
630         }
631
632         /**
633          * Adds attachment data to the JSON document
634          *
635          * @param array $item Data of the item that is to be posted
636          * @param text $type Object type
637          *
638          * @return array with attachment data
639          */
640         private static function createAttachmentList($item, $type)
641         {
642                 $attachments = [];
643
644                 $arr = explode('[/attach],', $item['attach']);
645                 if (count($arr)) {
646                         foreach ($arr as $r) {
647                                 $matches = false;
648                                 $cnt = preg_match('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\" title=\"(.*?)\"|', $r, $matches);
649                                 if ($cnt) {
650                                         $attributes = ['type' => 'Document',
651                                                         'mediaType' => $matches[3],
652                                                         'url' => $matches[1],
653                                                         'name' => null];
654
655                                         if (trim($matches[4]) != '') {
656                                                 $attributes['name'] = trim($matches[4]);
657                                         }
658
659                                         $attachments[] = $attributes;
660                                 }
661                         }
662                 }
663
664                 if ($type != 'Note') {
665                         return $attachments;
666                 }
667
668                 // Simplify image codes
669                 $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $item['body']);
670
671                 // Grab all pictures and create attachments out of them
672                 if (preg_match_all("/\[img\]([^\[\]]*)\[\/img\]/Usi", $body, $pictures)) {
673                         foreach ($pictures[1] as $picture) {
674                                 $imgdata = Image::getInfoFromURL($picture);
675                                 if ($imgdata) {
676                                         $attachments[] = ['type' => 'Document',
677                                                 'mediaType' => $imgdata['mime'],
678                                                 'url' => $picture,
679                                                 'name' => null];
680                                 }
681                         }
682                 }
683
684                 return $attachments;
685         }
686
687         /**
688          * Remove image elements and replaces them with links to the image
689          *
690          * @param string $body
691          *
692          * @return string with replaced elements
693          */
694         private static function removePictures($body)
695         {
696                 // Simplify image codes
697                 $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
698
699                 $body = preg_replace("/\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]/Usi", '[url]$1[/url]', $body);
700                 $body = preg_replace("/\[img\]([^\[\]]*)\[\/img\]/Usi", '[url]$1[/url]', $body);
701
702                 return $body;
703         }
704
705         /**
706          * Fetches the "context" value for a givem item array from the "conversation" table
707          *
708          * @param array $item
709          *
710          * @return string with context url
711          */
712         private static function fetchContextURLForItem($item)
713         {
714                 $conversation = DBA::selectFirst('conversation', ['conversation-href', 'conversation-uri'], ['item-uri' => $item['parent-uri']]);
715                 if (DBA::isResult($conversation) && !empty($conversation['conversation-href'])) {
716                         $context_uri = $conversation['conversation-href'];
717                 } elseif (DBA::isResult($conversation) && !empty($conversation['conversation-uri'])) {
718                         $context_uri = $conversation['conversation-uri'];
719                 } else {
720                         $context_uri = $item['parent-uri'] . '#context';
721                 }
722                 return $context_uri;
723         }
724
725         /**
726          * Returns if the post contains sensitive content ("nsfw")
727          *
728          * @param integer $item_id
729          *
730          * @return boolean
731          */
732         private static function isSensitive($item_id)
733         {
734                 $condition = ['otype' => TERM_OBJ_POST, 'oid' => $item_id, 'type' => TERM_HASHTAG, 'term' => 'nsfw'];
735                 return DBA::exists('term', $condition);
736         }
737
738         /**
739          * Creates a note/article object array
740          *
741          * @param array $item
742          *
743          * @return array with the object data
744          */
745         public static function createNote($item)
746         {
747                 if (!empty($item['title'])) {
748                         $type = 'Article';
749                 } else {
750                         $type = 'Note';
751                 }
752
753                 if ($item['deleted']) {
754                         $type = 'Tombstone';
755                 }
756
757                 $data = [];
758                 $data['id'] = $item['uri'];
759                 $data['type'] = $type;
760
761                 if ($item['deleted']) {
762                         return $data;
763                 }
764
765                 $data['summary'] = null; // Ignore by now
766
767                 if ($item['uri'] != $item['thr-parent']) {
768                         $data['inReplyTo'] = $item['thr-parent'];
769                 } else {
770                         $data['inReplyTo'] = null;
771                 }
772
773                 $data['diaspora:guid'] = $item['guid'];
774                 $data['published'] = DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM);
775
776                 if ($item['created'] != $item['edited']) {
777                         $data['updated'] = DateTimeFormat::utc($item['edited'] . '+00:00', DateTimeFormat::ATOM);
778                 }
779
780                 $data['url'] = $item['plink'];
781                 $data['attributedTo'] = $item['author-link'];
782                 $data['sensitive'] = self::isSensitive($item['id']);
783                 $data['context'] = self::fetchContextURLForItem($item);
784
785                 if (!empty($item['title'])) {
786                         $data['name'] = BBCode::toPlaintext($item['title'], false);
787                 }
788
789                 $body = $item['body'];
790
791                 if ($type == 'Note') {
792                         $body = self::removePictures($body);
793                 }
794
795                 $data['content'] = BBCode::convert($body, false, 7);
796                 $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"];
797
798                 if (!empty($item['signed_text']) && ($item['uri'] != $item['thr-parent'])) {
799                         $data['diaspora:comment'] = $item['signed_text'];
800                 }
801
802                 $data['attachment'] = self::createAttachmentList($item, $type);
803                 $data['tag'] = self::createTagList($item);
804                 $data = array_merge($data, self::createPermissionBlockForItem($item));
805
806                 return $data;
807         }
808
809         /**
810          * Creates an announce object entry
811          *
812          * @param array $item
813          *
814          * @return string with announced object url
815          */
816         public static function createAnnounce($item)
817         {
818                 $announce = api_share_as_retweet($item);
819                 if (empty($announce['plink'])) {
820                         return self::createNote($item);
821                 }
822
823                 return $announce['plink'];
824         }
825
826         /**
827          * Transmits a contact suggestion to a given inbox
828          *
829          * @param integer $uid User ID
830          * @param string $inbox Target inbox
831          * @param integer $suggestion_id Suggestion ID
832          */
833         public static function sendContactSuggestion($uid, $inbox, $suggestion_id)
834         {
835                 $owner = User::getOwnerDataById($uid);
836                 $profile = APContact::getByURL($owner['url']);
837
838                 $suggestion = DBA::selectFirst('fsuggest', ['url', 'note', 'created'], ['id' => $suggestion_id]);
839
840                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
841                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
842                         'type' => 'Announce',
843                         'actor' => $owner['url'],
844                         'object' => $suggestion['url'],
845                         'content' => $suggestion['note'],
846                         'published' => DateTimeFormat::utc($suggestion['created'] . '+00:00', DateTimeFormat::ATOM),
847                         'to' => [ActivityPub::PUBLIC_COLLECTION],
848                         'cc' => []];
849
850                 $signed = LDSignature::sign($data, $owner);
851
852                 logger('Deliver profile deletion for user ' . $uid . ' to ' . $inbox . ' via ActivityPub', LOGGER_DEBUG);
853                 HTTPSignature::transmit($signed, $inbox, $uid);
854         }
855
856         /**
857          * Transmits a profile deletion to a given inbox
858          *
859          * @param integer $uid User ID
860          * @param string $inbox Target inbox
861          */
862         public static function sendProfileDeletion($uid, $inbox)
863         {
864                 $owner = User::getOwnerDataById($uid);
865                 $profile = APContact::getByURL($owner['url']);
866
867                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
868                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
869                         'type' => 'Delete',
870                         'actor' => $owner['url'],
871                         'object' => $owner['url'],
872                         'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
873                         'to' => [ActivityPub::PUBLIC_COLLECTION],
874                         'cc' => []];
875
876                 $signed = LDSignature::sign($data, $owner);
877
878                 logger('Deliver profile deletion for user ' . $uid . ' to ' . $inbox . ' via ActivityPub', LOGGER_DEBUG);
879                 HTTPSignature::transmit($signed, $inbox, $uid);
880         }
881
882         /**
883          * Transmits a profile change to a given inbox
884          *
885          * @param integer $uid User ID
886          * @param string $inbox Target inbox
887          */
888         public static function sendProfileUpdate($uid, $inbox)
889         {
890                 $owner = User::getOwnerDataById($uid);
891                 $profile = APContact::getByURL($owner['url']);
892
893                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
894                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
895                         'type' => 'Update',
896                         'actor' => $owner['url'],
897                         'object' => self::getProfile($uid),
898                         'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
899                         'to' => [$profile['followers']],
900                         'cc' => []];
901
902                 $signed = LDSignature::sign($data, $owner);
903
904                 logger('Deliver profile update for user ' . $uid . ' to ' . $inbox . ' via ActivityPub', LOGGER_DEBUG);
905                 HTTPSignature::transmit($signed, $inbox, $uid);
906         }
907
908         /**
909          * Transmits a given activity to a target
910          *
911          * @param array $activity
912          * @param string $target Target profile
913          * @param integer $uid User ID
914          */
915         public static function sendActivity($activity, $target, $uid)
916         {
917                 $profile = APContact::getByURL($target);
918
919                 $owner = User::getOwnerDataById($uid);
920
921                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
922                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
923                         'type' => $activity,
924                         'actor' => $owner['url'],
925                         'object' => $profile['url'],
926                         'to' => $profile['url']];
927
928                 logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG);
929
930                 $signed = LDSignature::sign($data, $owner);
931                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
932         }
933
934         /**
935          * Transmit a message that the contact request had been accepted
936          *
937          * @param string $target Target profile
938          * @param $id
939          * @param integer $uid User ID
940          */
941         public static function sendContactAccept($target, $id, $uid)
942         {
943                 $profile = APContact::getByURL($target);
944
945                 $owner = User::getOwnerDataById($uid);
946                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
947                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
948                         'type' => 'Accept',
949                         'actor' => $owner['url'],
950                         'object' => ['id' => $id, 'type' => 'Follow',
951                                 'actor' => $profile['url'],
952                                 'object' => $owner['url']],
953                         'to' => $profile['url']];
954
955                 logger('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG);
956
957                 $signed = LDSignature::sign($data, $owner);
958                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
959         }
960
961         /**
962          * Reject a contact request or terminates the contact relation
963          *
964          * @param string $target Target profile
965          * @param $id
966          * @param integer $uid User ID
967          */
968         public static function sendContactReject($target, $id, $uid)
969         {
970                 $profile = APContact::getByURL($target);
971
972                 $owner = User::getOwnerDataById($uid);
973                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
974                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
975                         'type' => 'Reject',
976                         'actor' => $owner['url'],
977                         'object' => ['id' => $id, 'type' => 'Follow',
978                                 'actor' => $profile['url'],
979                                 'object' => $owner['url']],
980                         'to' => $profile['url']];
981
982                 logger('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG);
983
984                 $signed = LDSignature::sign($data, $owner);
985                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
986         }
987
988         /**
989          * Transmits a message that we don't want to follow this contact anymore
990          *
991          * @param string $target Target profile
992          * @param integer $uid User ID
993          */
994         public static function sendContactUndo($target, $uid)
995         {
996                 $profile = APContact::getByURL($target);
997
998                 $id = System::baseUrl() . '/activity/' . System::createGUID();
999
1000                 $owner = User::getOwnerDataById($uid);
1001                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
1002                         'id' => $id,
1003                         'type' => 'Undo',
1004                         'actor' => $owner['url'],
1005                         'object' => ['id' => $id, 'type' => 'Follow',
1006                                 'actor' => $owner['url'],
1007                                 'object' => $profile['url']],
1008                         'to' => $profile['url']];
1009
1010                 logger('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG);
1011
1012                 $signed = LDSignature::sign($data, $owner);
1013                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
1014         }
1015 }