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