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