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