]> git.mxchange.org Git - friendica.git/blob - src/Protocol/ActivityPub/Transmitter.php
afd9582c2d93017c8ef04e64fefa2103e4a33ceb
[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  * - Type "note": Remove inline images and add them as attachments
39  * - Type "article": Leave imaged embedded and don't add them as attachments
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 profile array
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 permission array
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 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['author-link'];
525
526                 $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM);
527
528                 if ($item["created"] != $item["edited"]) {
529                         $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM);
530                 }
531
532                 $data['context'] = self::fetchContextURLForItem($item);
533
534                 $data = array_merge($data, self::createPermissionBlockForItem($item));
535
536                 if (in_array($data['type'], ['Create', 'Update', 'Announce', 'Delete'])) {
537                         $data['object'] = self::createNote($item);
538                 } elseif ($data['type'] == 'Undo') {
539                         $data['object'] = self::createActivityFromItem($item_id, true);
540                 } else {
541                         $data['object'] = $item['thr-parent'];
542                 }
543
544                 $owner = User::getOwnerDataById($item['uid']);
545
546                 if (!$object_mode) {
547                         return LDSignature::sign($data, $owner);
548                 } else {
549                         return $data;
550                 }
551         }
552
553         /**
554          * @brief Creates an object array for a given item id
555          *
556          * @param integer $item_id
557          *
558          * @return object array
559          */
560         public static function createObjectFromItemID($item_id)
561         {
562                 $item = Item::selectFirst([], ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]);
563
564                 if (!DBA::isResult($item)) {
565                         return false;
566                 }
567
568                 $data = ['@context' => ActivityPub::CONTEXT];
569                 $data = array_merge($data, self::createNote($item));
570
571                 return $data;
572         }
573
574         /**
575          * @brief Returns a tag array for a given item array
576          *
577          * @param array $item
578          *
579          * @return array of tags
580          */
581         private static function createTagList($item)
582         {
583                 $tags = [];
584
585                 $terms = Term::tagArrayFromItemId($item['id']);
586                 foreach ($terms as $term) {
587                         if ($term['type'] == TERM_HASHTAG) {
588                                 $tags[] = ['type' => 'Hashtag', 'href' => $term['url'], 'name' => '#' . $term['term']];
589                         } elseif ($term['type'] == TERM_MENTION) {
590                                 $contact = Contact::getDetailsByURL($term['url']);
591                                 if (!empty($contact['addr'])) {
592                                         $mention = '@' . $contact['addr'];
593                                 } else {
594                                         $mention = '@' . $term['url'];
595                                 }
596
597                                 $tags[] = ['type' => 'Mention', 'href' => $term['url'], 'name' => $mention];
598                         }
599                 }
600                 return $tags;
601         }
602
603         /**
604          * @brief Adds attachment data to the JSON document
605          *
606          * @param array $item Data of the item that is to be posted
607          * @return attachment array
608          */
609
610         private static function createAttachmentList($item)
611         {
612                 $attachments = [];
613
614                 $siteinfo = BBCode::getAttachedData($item['body']);
615
616                 switch ($siteinfo['type']) {
617                         case 'photo':
618                                 if (!empty($siteinfo['image'])) {
619                                         $imgdata = Image::getInfoFromURL($siteinfo['image']);
620                                         if ($imgdata) {
621                                                 $attachments[] = ['type' => 'Document',
622                                                                 'mediaType' => $imgdata['mime'],
623                                                                 'url' => $siteinfo['image'],
624                                                                 'name' => null];
625                                         }
626                                 }
627                                 break;
628                         case 'video':
629                                 $attachments[] = ['type' => 'Document',
630                                                 'mediaType' => 'text/html; charset=UTF-8',
631                                                 'url' => $siteinfo['url'],
632                                                 'name' => defaults($siteinfo, 'title', $siteinfo['url'])];
633                                 break;
634                         default:
635                                 break;
636                 }
637
638                 if (!Config::get('system', 'ostatus_not_attach_preview') && ($siteinfo['type'] != 'photo') && isset($siteinfo['image'])) {
639                         $imgdata = Image::getInfoFromURL($siteinfo['image']);
640                         if ($imgdata) {
641                                 $attachments[] = ['type' => 'Document',
642                                                 'mediaType' => $imgdata['mime'],
643                                                 'url' => $siteinfo['image'],
644                                                 'name' => null];
645                         }
646                 }
647
648                 $arr = explode('[/attach],', $item['attach']);
649                 if (count($arr)) {
650                         foreach ($arr as $r) {
651                                 $matches = false;
652                                 $cnt = preg_match('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\" title=\"(.*?)\"|', $r, $matches);
653                                 if ($cnt) {
654                                         $attributes = ['type' => 'Document',
655                                                         'mediaType' => $matches[3],
656                                                         'url' => $matches[1],
657                                                         'name' => null];
658
659                                         if (trim($matches[4]) != '') {
660                                                 $attributes['name'] = trim($matches[4]);
661                                         }
662
663                                         $attachments[] = $attributes;
664                                 }
665                         }
666                 }
667
668                 return $attachments;
669         }
670
671         /**
672          * @brief Fetches the "context" value for a givem item array from the "conversation" table
673          *
674          * @param array $item
675          *
676          * @return string with context url
677          */
678         private static function fetchContextURLForItem($item)
679         {
680                 $conversation = DBA::selectFirst('conversation', ['conversation-href', 'conversation-uri'], ['item-uri' => $item['parent-uri']]);
681                 if (DBA::isResult($conversation) && !empty($conversation['conversation-href'])) {
682                         $context_uri = $conversation['conversation-href'];
683                 } elseif (DBA::isResult($conversation) && !empty($conversation['conversation-uri'])) {
684                         $context_uri = $conversation['conversation-uri'];
685                 } else {
686                         $context_uri = str_replace('/objects/', '/context/', $item['parent-uri']);
687                 }
688                 return $context_uri;
689         }
690
691         private static function fetchSensitive($item_id)
692         {
693                 $condition = ['otype' => TERM_OBJ_POST, 'oid' => $item_id, 'type' => TERM_HASHTAG, 'term' => 'nsfw'];
694                 return DBA::exists('term', $condition);
695         }
696
697         /**
698          * @brief Creates a note/article object array
699          *
700          * @param array $item
701          *
702          * @return object array
703          */
704         public static function createNote($item)
705         {
706                 if (!empty($item['title'])) {
707                         $type = 'Article';
708                 } else {
709                         $type = 'Note';
710                 }
711
712                 if ($item['deleted']) {
713                         $type = 'Tombstone';
714                 }
715
716                 $data = [];
717                 $data['id'] = $item['uri'];
718                 $data['type'] = $type;
719
720                 if ($item['deleted']) {
721                         return $data;
722                 }
723
724                 $data['summary'] = null; // Ignore by now
725
726                 if ($item['uri'] != $item['thr-parent']) {
727                         $data['inReplyTo'] = $item['thr-parent'];
728                 } else {
729                         $data['inReplyTo'] = null;
730                 }
731
732                 $data['diaspora:guid'] = $item['guid'];
733                 $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM);
734
735                 if ($item["created"] != $item["edited"]) {
736                         $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM);
737                 }
738
739                 $data['url'] = $item['plink'];
740                 $data['attributedTo'] = $item['author-link'];
741                 $data['actor'] = $item['author-link'];
742                 $data['sensitive'] = self::fetchSensitive($item['id']);
743                 $data['context'] = self::fetchContextURLForItem($item);
744
745                 if (!empty($item['title'])) {
746                         $data['name'] = BBCode::convert($item['title'], false, 7);
747                 }
748
749                 $data['content'] = BBCode::convert($item['body'], false, 7);
750                 $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"];
751
752                 if (!empty($item['signed_text']) && ($item['uri'] != $item['thr-parent'])) {
753                         $data['diaspora:comment'] = $item['signed_text'];
754                 }
755
756                 $data['attachment'] = self::createAttachmentList($item);
757                 $data['tag'] = self::createTagList($item);
758                 $data = array_merge($data, self::createPermissionBlockForItem($item));
759
760                 return $data;
761         }
762
763         /**
764          * @brief Transmits a profile deletion to a given inbox
765          *
766          * @param integer $uid User ID
767          * @param string $inbox Target inbox
768          */
769         public static function sendProfileDeletion($uid, $inbox)
770         {
771                 $owner = User::getOwnerDataById($uid);
772                 $profile = APContact::getByURL($owner['url']);
773
774                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
775                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
776                         'type' => 'Delete',
777                         'actor' => $owner['url'],
778                         'object' => $owner['url'],
779                         'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
780                         'to' => [ActivityPub::PUBLIC_COLLECTION],
781                         'cc' => []];
782
783                 $signed = LDSignature::sign($data, $owner);
784
785                 logger('Deliver profile deletion for user ' . $uid . ' to ' . $inbox .' via ActivityPub', LOGGER_DEBUG);
786                 HTTPSignature::transmit($signed, $inbox, $uid);
787         }
788
789         /**
790          * @brief Transmits a profile change to a given inbox
791          *
792          * @param integer $uid User ID
793          * @param string $inbox Target inbox
794          */
795         public static function sendProfileUpdate($uid, $inbox)
796         {
797                 $owner = User::getOwnerDataById($uid);
798                 $profile = APContact::getByURL($owner['url']);
799
800                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
801                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
802                         'type' => 'Update',
803                         'actor' => $owner['url'],
804                         'object' => self::getProfile($uid),
805                         'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
806                         'to' => [$profile['followers']],
807                         'cc' => []];
808
809                 $signed = LDSignature::sign($data, $owner);
810
811                 logger('Deliver profile update for user ' . $uid . ' to ' . $inbox .' via ActivityPub', LOGGER_DEBUG);
812                 HTTPSignature::transmit($signed, $inbox, $uid);
813         }
814
815         /**
816          * @brief Transmits a given activity to a target
817          *
818          * @param array $activity
819          * @param string $target Target profile
820          * @param integer $uid User ID
821          */
822         public static function sendActivity($activity, $target, $uid)
823         {
824                 $profile = APContact::getByURL($target);
825
826                 $owner = User::getOwnerDataById($uid);
827
828                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
829                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
830                         'type' => $activity,
831                         'actor' => $owner['url'],
832                         'object' => $profile['url'],
833                         'to' => $profile['url']];
834
835                 logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG);
836
837                 $signed = LDSignature::sign($data, $owner);
838                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
839         }
840
841         /**
842          * @brief Transmit a message that the contact request had been accepted
843          *
844          * @param string $target Target profile
845          * @param $id
846          * @param integer $uid User ID
847          */
848         public static function sendContactAccept($target, $id, $uid)
849         {
850                 $profile = APContact::getByURL($target);
851
852                 $owner = User::getOwnerDataById($uid);
853                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
854                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
855                         'type' => 'Accept',
856                         'actor' => $owner['url'],
857                         'object' => ['id' => $id, 'type' => 'Follow',
858                                 'actor' => $profile['url'],
859                                 'object' => $owner['url']],
860                         'to' => $profile['url']];
861
862                 logger('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG);
863
864                 $signed = LDSignature::sign($data, $owner);
865                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
866         }
867
868         /**
869          * @brief 
870          *
871          * @param string $target Target profile
872          * @param $id
873          * @param integer $uid User ID
874          */
875         public static function sendContactReject($target, $id, $uid)
876         {
877                 $profile = APContact::getByURL($target);
878
879                 $owner = User::getOwnerDataById($uid);
880                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
881                         'id' => System::baseUrl() . '/activity/' . System::createGUID(),
882                         'type' => 'Reject',
883                         'actor' => $owner['url'],
884                         'object' => ['id' => $id, 'type' => 'Follow',
885                                 'actor' => $profile['url'],
886                                 'object' => $owner['url']],
887                         'to' => $profile['url']];
888
889                 logger('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG);
890
891                 $signed = LDSignature::sign($data, $owner);
892                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
893         }
894
895         /**
896          * @brief 
897          *
898          * @param string $target Target profile
899          * @param integer $uid User ID
900          */
901         public static function sendContactUndo($target, $uid)
902         {
903                 $profile = APContact::getByURL($target);
904
905                 $id = System::baseUrl() . '/activity/' . System::createGUID();
906
907                 $owner = User::getOwnerDataById($uid);
908                 $data = ['@context' => 'https://www.w3.org/ns/activitystreams',
909                         'id' => $id,
910                         'type' => 'Undo',
911                         'actor' => $owner['url'],
912                         'object' => ['id' => $id, 'type' => 'Follow',
913                                 'actor' => $owner['url'],
914                                 'object' => $profile['url']],
915                         'to' => $profile['url']];
916
917                 logger('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG);
918
919                 $signed = LDSignature::sign($data, $owner);
920                 HTTPSignature::transmit($signed, $profile['inbox'], $uid);
921         }
922 }