]> git.mxchange.org Git - friendica.git/blob - src/Protocol/ActivityPub/Processor.php
AP: We now transmit and process events
[friendica.git] / src / Protocol / ActivityPub / Processor.php
1 <?php
2 /**
3  * @file src/Protocol/ActivityPub/Processor.php
4  */
5 namespace Friendica\Protocol\ActivityPub;
6
7 use Friendica\Database\DBA;
8 use Friendica\Core\Protocol;
9 use Friendica\Model\Conversation;
10 use Friendica\Model\Contact;
11 use Friendica\Model\APContact;
12 use Friendica\Model\Item;
13 use Friendica\Model\Event;
14 use Friendica\Model\User;
15 use Friendica\Content\Text\HTML;
16 use Friendica\Util\JsonLD;
17 use Friendica\Core\Config;
18 use Friendica\Protocol\ActivityPub;
19
20 /**
21  * ActivityPub Processor Protocol class
22  */
23 class Processor
24 {
25         /**
26          * Converts mentions from Pleroma into the Friendica format
27          *
28          * @param string $body
29          *
30          * @return converted body
31          */
32         private static function convertMentions($body)
33         {
34                 $URLSearchString = "^\[\]";
35                 $body = preg_replace("/\[url\=([$URLSearchString]*)\]([#@!])(.*?)\[\/url\]/ism", '$2[url=$1]$3[/url]', $body);
36
37                 return $body;
38         }
39
40         /**
41          * Constructs a string with tags for a given tag array
42          *
43          * @param array $tags
44          * @param boolean $sensitive
45          *
46          * @return string with tags
47          */
48         private static function constructTagList($tags, $sensitive)
49         {
50                 if (empty($tags)) {
51                         return '';
52                 }
53
54                 $tag_text = '';
55                 foreach ($tags as $tag) {
56                         if (in_array(defaults($tag, 'type', ''), ['Mention', 'Hashtag'])) {
57                                 if (!empty($tag_text)) {
58                                         $tag_text .= ',';
59                                 }
60
61                                 $tag_text .= substr($tag['name'], 0, 1) . '[url=' . $tag['href'] . ']' . substr($tag['name'], 1) . '[/url]';
62                         }
63                 }
64
65                 /// @todo add nsfw for $sensitive
66
67                 return $tag_text;
68         }
69
70         /**
71          * Add attachment data to the item array
72          *
73          * @param array $attachments
74          * @param array $item
75          *
76          * @return item array
77          */
78         private static function constructAttachList($attachments, $item)
79         {
80                 if (empty($attachments)) {
81                         return $item;
82                 }
83
84                 foreach ($attachments as $attach) {
85                         $filetype = strtolower(substr($attach['mediaType'], 0, strpos($attach['mediaType'], '/')));
86                         if ($filetype == 'image') {
87                                 $item['body'] .= "\n[img]" . $attach['url'] . '[/img]';
88                         } else {
89                                 if (!empty($item["attach"])) {
90                                         $item["attach"] .= ',';
91                                 } else {
92                                         $item["attach"] = '';
93                                 }
94                                 if (!isset($attach['length'])) {
95                                         $attach['length'] = "0";
96                                 }
97                                 $item["attach"] .= '[attach]href="'.$attach['url'].'" length="'.$attach['length'].'" type="'.$attach['mediaType'].'" title="'.defaults($attach, 'name', '').'"[/attach]';
98                         }
99                 }
100
101                 return $item;
102         }
103
104         /**
105          * Prepares data for a message
106          *
107          * @param array  $activity Activity array
108          */
109         public static function createItem($activity)
110         {
111                 $item = [];
112                 $item['verb'] = ACTIVITY_POST;
113                 $item['parent-uri'] = $activity['reply-to-id'];
114
115                 if ($activity['reply-to-id'] == $activity['id']) {
116                         $item['gravity'] = GRAVITY_PARENT;
117                         $item['object-type'] = ACTIVITY_OBJ_NOTE;
118                 } else {
119                         $item['gravity'] = GRAVITY_COMMENT;
120                         $item['object-type'] = ACTIVITY_OBJ_COMMENT;
121                 }
122
123                 if (($activity['id'] != $activity['reply-to-id']) && !Item::exists(['uri' => $activity['reply-to-id']])) {
124                         logger('Parent ' . $activity['reply-to-id'] . ' not found. Try to refetch it.');
125                         self::fetchMissingActivity($activity['reply-to-id'], $activity);
126                 }
127
128                 self::postItem($activity, $item);
129         }
130
131         /**
132          * Prepare the item array for a "like"
133          *
134          * @param array  $activity Activity array
135          */
136         public static function likeItem($activity)
137         {
138                 $item = [];
139                 $item['verb'] = ACTIVITY_LIKE;
140                 $item['parent-uri'] = $activity['object_id'];
141                 $item['gravity'] = GRAVITY_ACTIVITY;
142                 $item['object-type'] = ACTIVITY_OBJ_NOTE;
143
144                 self::postItem($activity, $item);
145         }
146
147         /**
148          * Delete items
149          *
150          * @param array $activity
151          */
152         public static function deleteItem($activity)
153         {
154                 $owner = Contact::getIdForURL($activity['actor']);
155
156                 logger('Deleting item ' . $activity['object_id'] . ' from ' . $owner, LOGGER_DEBUG);
157                 Item::delete(['uri' => $activity['object_id'], 'owner-id' => $owner]);
158         }
159
160         /**
161          * Prepare the item array for a "dislike"
162          *
163          * @param array  $activity Activity array
164          */
165         public static function dislikeItem($activity)
166         {
167                 $item = [];
168                 $item['verb'] = ACTIVITY_DISLIKE;
169                 $item['parent-uri'] = $activity['object_id'];
170                 $item['gravity'] = GRAVITY_ACTIVITY;
171                 $item['object-type'] = ACTIVITY_OBJ_NOTE;
172
173                 self::postItem($activity, $item);
174         }
175
176         /**
177          * Create an event
178          *
179          * @param array  $activity Activity array
180          */
181         public static function createEvent($activity, $item)
182         {
183                 $event['summary'] = $activity['name'];
184                 $event['desc'] = $activity['content'];
185                 $event['start'] = $activity['start-time'];
186                 $event['finish'] = $activity['end-time'];
187                 $event['nofinish'] = empty($event['finish']);
188                 $event['location'] = $activity['location'];
189                 $event['adjust'] = true;
190                 $event['cid'] = $item['contact-id'];
191                 $event['uid'] = $item['uid'];
192                 $event['uri'] = $item['uri'];
193                 $event['edited'] = $item['edited'];
194                 $event['private'] = $item['private'];
195                 $event['guid'] = $item['guid'];
196                 $event['plink'] = $item['plink'];
197
198                 $condition = ['uri' => $item['uri'], 'uid' => $item['uid']];
199                 $ev = DBA::selectFirst('event', ['id'], $condition);
200                 if (DBA::isResult($ev)) {
201                         $event['id'] = $ev['id'];
202                 }
203
204                 $event_id = Event::store($event);
205                 logger('Event '.$event_id.' was stored', LOGGER_DEBUG);
206         }
207
208         /**
209          * Creates an item post
210          *
211          * @param array  $activity Activity data
212          * @param array  $item     item array
213          */
214         private static function postItem($activity, $item)
215         {
216                 /// @todo What to do with $activity['context']?
217
218                 if (($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['parent-uri']])) {
219                         logger('Parent ' . $item['parent-uri'] . ' not found, message will be discarded.', LOGGER_DEBUG);
220                         return;
221                 }
222
223                 $item['network'] = Protocol::ACTIVITYPUB;
224                 $item['private'] = !in_array(0, $activity['receiver']);
225                 $item['author-link'] = $activity['author'];
226                 $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true);
227
228                 if (empty($activity['thread-completion'])) {
229                         $item['owner-link'] = $activity['actor'];
230                         $item['owner-id'] = Contact::getIdForURL($activity['actor'], 0, true);
231                 } else {
232                         logger('Ignoring actor because of thread completion.', LOGGER_DEBUG);
233                         $item['owner-link'] = $item['author-link'];
234                         $item['owner-id'] = $item['author-id'];
235                 }
236
237                 $item['uri'] = $activity['id'];
238                 $item['created'] = $activity['published'];
239                 $item['edited'] = $activity['updated'];
240                 $item['guid'] = $activity['diaspora:guid'];
241                 $item['title'] = HTML::toBBCode($activity['name']);
242                 $item['content-warning'] = HTML::toBBCode($activity['summary']);
243                 $item['body'] = self::convertMentions(HTML::toBBCode($activity['content']));
244
245                 if (($activity['object_type'] == 'as:Video') && !empty($activity['alternate-url'])) {
246                         $item['body'] .= "\n[video]" . $activity['alternate-url'] . '[/video]';
247                 }
248
249                 $item['location'] = $activity['location'];
250
251                 if (!empty($item['latitude']) && !empty($item['longitude'])) {
252                         $item['coord'] = $item['latitude'] . ' ' . $item['longitude'];
253                 }
254
255                 $item['tag'] = self::constructTagList($activity['tags'], $activity['sensitive']);
256                 $item['app'] = $activity['generator'];
257                 $item['plink'] = defaults($activity, 'alternate-url', $item['uri']);
258                 $item['diaspora_signed_text'] = defaults($activity, 'diaspora:comment', '');
259
260                 $item = self::constructAttachList($activity['attachments'], $item);
261
262                 if (!empty($activity['source'])) {
263                         $item['body'] = $activity['source'];
264                 }
265
266                 foreach ($activity['receiver'] as $receiver) {
267                         $item['uid'] = $receiver;
268                         $item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, true);
269
270                         if (($receiver != 0) && empty($item['contact-id'])) {
271                                 $item['contact-id'] = Contact::getIdForURL($activity['author'], 0, true);
272                         }
273
274                         if ($activity['object_type'] == 'as:Event') {
275                                 self::createEvent($activity, $item);
276                         }
277
278                         $item_id = Item::insert($item);
279                         logger('Storing for user ' . $item['uid'] . ': ' . $item_id);
280                 }
281         }
282
283         /**
284          * Fetches missing posts
285          *
286          * @param $url
287          * @param $child
288          */
289         private static function fetchMissingActivity($url, $child)
290         {
291                 if (Config::get('system', 'ostatus_full_threads')) {
292                         return;
293                 }
294
295                 $object = ActivityPub::fetchContent($url);
296                 if (empty($object)) {
297                         logger('Activity ' . $url . ' was not fetchable, aborting.');
298                         return;
299                 }
300
301                 $activity = [];
302                 $activity['@context'] = $object['@context'];
303                 unset($object['@context']);
304                 $activity['id'] = $object['id'];
305                 $activity['to'] = defaults($object, 'to', []);
306                 $activity['cc'] = defaults($object, 'cc', []);
307                 $activity['actor'] = $child['author'];
308                 $activity['object'] = $object;
309                 $activity['published'] = defaults($object, 'published', $child['published']);
310                 $activity['type'] = 'Create';
311
312                 $ldactivity = JsonLD::compact($activity);
313
314                 $ldactivity['thread-completion'] = true;
315
316                 ActivityPub\Receiver::processActivity($ldactivity);
317                 logger('Activity ' . $url . ' had been fetched and processed.');
318         }
319
320         /**
321          * perform a "follow" request
322          *
323          * @param array $activity
324          */
325         public static function followUser($activity)
326         {
327                 $uid = User::getIdForURL($activity['object_id']);
328                 if (empty($uid)) {
329                         return;
330                 }
331
332                 $owner = User::getOwnerDataById($uid);
333
334                 $cid = Contact::getIdForURL($activity['actor'], $uid);
335                 if (!empty($cid)) {
336                         self::switchContact($cid);
337                         $contact = DBA::selectFirst('contact', [], ['id' => $cid, 'network' => Protocol::NATIVE_SUPPORT]);
338                 } else {
339                         $contact = false;
340                 }
341
342                 $item = ['author-id' => Contact::getIdForURL($activity['actor']),
343                         'author-link' => $activity['actor']];
344
345                 // Ensure that the contact has got the right network type
346                 self::switchContact($item['author-id']);
347
348                 Contact::addRelationship($owner, $contact, $item);
349                 $cid = Contact::getIdForURL($activity['actor'], $uid);
350                 if (empty($cid)) {
351                         return;
352                 }
353
354                 DBA::update('contact', ['hub-verify' => $activity['id']], ['id' => $cid]);
355                 logger('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']);
356         }
357
358         /**
359          * Update the given profile
360          *
361          * @param array $activity
362          */
363         public static function updatePerson($activity)
364         {
365                 if (empty($activity['object_id'])) {
366                         return;
367                 }
368
369                 logger('Updating profile for ' . $activity['object_id'], LOGGER_DEBUG);
370                 APContact::getByURL($activity['object_id'], true);
371         }
372
373         /**
374          * Delete the given profile
375          *
376          * @param array $activity
377          */
378         public static function deletePerson($activity)
379         {
380                 if (empty($activity['object_id']) || empty($activity['actor'])) {
381                         logger('Empty object id or actor.', LOGGER_DEBUG);
382                         return;
383                 }
384
385                 if ($activity['object_id'] != $activity['actor']) {
386                         logger('Object id does not match actor.', LOGGER_DEBUG);
387                         return;
388                 }
389
390                 $contacts = DBA::select('contact', ['id'], ['nurl' => normalise_link($activity['object_id'])]);
391                 while ($contact = DBA::fetch($contacts)) {
392                         Contact::remove($contact['id']);
393                 }
394                 DBA::close($contacts);
395
396                 logger('Deleted contact ' . $activity['object_id'], LOGGER_DEBUG);
397         }
398
399         /**
400          * Accept a follow request
401          *
402          * @param array $activity
403          */
404         public static function acceptFollowUser($activity)
405         {
406                 $uid = User::getIdForURL($activity['object_actor']);
407                 if (empty($uid)) {
408                         return;
409                 }
410
411                 $owner = User::getOwnerDataById($uid);
412
413                 $cid = Contact::getIdForURL($activity['actor'], $uid);
414                 if (empty($cid)) {
415                         logger('No contact found for ' . $activity['actor'], LOGGER_DEBUG);
416                         return;
417                 }
418
419                 self::switchContact($cid);
420
421                 $fields = ['pending' => false];
422
423                 $contact = DBA::selectFirst('contact', ['rel'], ['id' => $cid]);
424                 if ($contact['rel'] == Contact::FOLLOWER) {
425                         $fields['rel'] = Contact::FRIEND;
426                 }
427
428                 $condition = ['id' => $cid];
429                 DBA::update('contact', $fields, $condition);
430                 logger('Accept contact request from contact ' . $cid . ' for user ' . $uid, LOGGER_DEBUG);
431         }
432
433         /**
434          * Reject a follow request
435          *
436          * @param array $activity
437          */
438         public static function rejectFollowUser($activity)
439         {
440                 $uid = User::getIdForURL($activity['object_actor']);
441                 if (empty($uid)) {
442                         return;
443                 }
444
445                 $owner = User::getOwnerDataById($uid);
446
447                 $cid = Contact::getIdForURL($activity['actor'], $uid);
448                 if (empty($cid)) {
449                         logger('No contact found for ' . $activity['actor'], LOGGER_DEBUG);
450                         return;
451                 }
452
453                 self::switchContact($cid);
454
455                 if (DBA::exists('contact', ['id' => $cid, 'rel' => Contact::SHARING, 'pending' => true])) {
456                         Contact::remove($cid);
457                         logger('Rejected contact request from contact ' . $cid . ' for user ' . $uid . ' - contact had been removed.', LOGGER_DEBUG);
458                 } else {
459                         logger('Rejected contact request from contact ' . $cid . ' for user ' . $uid . '.', LOGGER_DEBUG);
460                 }
461         }
462
463         /**
464          * Undo activity like "like" or "dislike"
465          *
466          * @param array $activity
467          */
468         public static function undoActivity($activity)
469         {
470                 if (empty($activity['object_id'])) {
471                         return;
472                 }
473
474                 if (empty($activity['object_actor'])) {
475                         return;
476                 }
477
478                 $author_id = Contact::getIdForURL($activity['object_actor']);
479                 if (empty($author_id)) {
480                         return;
481                 }
482
483                 Item::delete(['uri' => $activity['object_id'], 'author-id' => $author_id, 'gravity' => GRAVITY_ACTIVITY]);
484         }
485
486         /**
487          * Activity to remove a follower
488          *
489          * @param array $activity
490          */
491         public static function undoFollowUser($activity)
492         {
493                 $uid = User::getIdForURL($activity['object_object']);
494                 if (empty($uid)) {
495                         return;
496                 }
497
498                 $owner = User::getOwnerDataById($uid);
499
500                 $cid = Contact::getIdForURL($activity['actor'], $uid);
501                 if (empty($cid)) {
502                         logger('No contact found for ' . $activity['actor'], LOGGER_DEBUG);
503                         return;
504                 }
505
506                 self::switchContact($cid);
507
508                 $contact = DBA::selectFirst('contact', [], ['id' => $cid]);
509                 if (!DBA::isResult($contact)) {
510                         return;
511                 }
512
513                 Contact::removeFollower($owner, $contact);
514                 logger('Undo following request from contact ' . $cid . ' for user ' . $uid, LOGGER_DEBUG);
515         }
516
517         /**
518          * Switches a contact to AP if needed
519          *
520          * @param integer $cid Contact ID
521          */
522         private static function switchContact($cid)
523         {
524                 $contact = DBA::selectFirst('contact', ['network'], ['id' => $cid, 'network' => Protocol::NATIVE_SUPPORT]);
525                 if (!DBA::isResult($contact) || ($contact['network'] == Protocol::ACTIVITYPUB)) {
526                         return;
527                 }
528
529                 logger('Change existing contact ' . $cid . ' from ' . $contact['network'] . ' to ActivityPub.');
530                 Contact::updateFromProbe($cid, Protocol::ACTIVITYPUB);
531         }
532 }