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