]> git.mxchange.org Git - friendica.git/blob - src/Protocol/ActivityPub/ClientToServer.php
Merge pull request #13070 from xundeenergie/fix-share-via
[friendica.git] / src / Protocol / ActivityPub / ClientToServer.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Protocol\ActivityPub;
23
24 use Friendica\Content\Text\Markdown;
25 use Friendica\Core\Logger;
26 use Friendica\Core\Protocol;
27 use Friendica\Database\DBA;
28 use Friendica\DI;
29 use Friendica\Model\APContact;
30 use Friendica\Model\Contact;
31 use Friendica\Model\Group;
32 use Friendica\Model\Item;
33 use Friendica\Model\Post;
34 use Friendica\Model\User;
35 use Friendica\Protocol\Activity;
36 use Friendica\Protocol\ActivityPub;
37 use Friendica\Util\JsonLD;
38
39 /**
40  * ActivityPub Client To Server class
41  */
42 class ClientToServer
43 {
44         /**
45          * Process client to server activities
46          *
47          * @param array $activity
48          * @param integer $uid
49          * @param array $application
50          * @return array
51          */
52         public static function processActivity(array $activity, int $uid, array $application): array
53         {
54                 $ldactivity = JsonLD::compact($activity);
55                 if (empty($ldactivity)) {
56                         Logger::notice('Invalid activity', ['activity' => $activity, 'uid' => $uid]);
57                         return [];
58                 }
59
60                 $type = JsonLD::fetchElement($ldactivity, '@type');
61                 if (!$type) {
62                         Logger::notice('Empty type', ['activity' => $ldactivity, 'uid' => $uid]);
63                         return [];
64                 }
65
66                 $object_id   = JsonLD::fetchElement($ldactivity, 'as:object', '@id') ?? '';
67                 $object_type = Receiver::fetchObjectType($ldactivity, $object_id, $uid);
68                 if (!$object_type && !$object_id) {
69                         Logger::notice('Empty object type or id', ['activity' => $ldactivity, 'uid' => $uid]);
70                         return [];
71                 }
72
73                 Logger::debug('Processing activity', ['type' => $type, 'object_type' => $object_type, 'object_id' => $object_id, 'activity' => $ldactivity]);
74                 return self::routeActivities($type, $object_type, $object_id, $uid, $application, $ldactivity);
75         }
76
77         /**
78          * Route client to server activities
79          *
80          * @param string $type
81          * @param string $object_type
82          * @param string $object_id
83          * @param integer $uid
84          * @param array $application
85          * @param array $ldactivity
86          * @return array
87          */
88         private static function routeActivities(string $type, string $object_type, string $object_id, int $uid, array $application, array $ldactivity): array
89         {
90                 switch ($type) {
91                         case 'as:Create':
92                                 if (in_array($object_type, Receiver::CONTENT_TYPES)) {
93                                         return self::createContent($uid, $application, $ldactivity);
94                                 }
95                                 break;
96                         case 'as:Update':
97                                 if (in_array($object_type, Receiver::CONTENT_TYPES) && !empty($object_id)) {
98                                         return self::updateContent($uid, $object_id, $application, $ldactivity);
99                                 }
100                                 break;
101                         case 'as:Follow':
102                                 if (in_array($object_type, Receiver::ACCOUNT_TYPES) && !empty($object_id)) {
103                                         return self::followAccount($uid, $object_id, $ldactivity);
104                                 }
105                                 break;
106                 }
107                 return [];
108         }
109
110         /**
111          * Create a new post or comment
112          *
113          * @param integer $uid
114          * @param array $application
115          * @param array $ldactivity
116          * @return array
117          */
118         private static function createContent(int $uid, array $application, array $ldactivity): array
119         {
120                 $object_data = self::processObject($ldactivity['as:object']);
121                 $item        = ClientToServer::processContent($object_data, $application, $uid);
122                 Logger::debug('Got data', ['item' => $item, 'object' => $object_data]);
123
124                 $id = Item::insert($item, true);
125                 if (!empty($id)) {
126                         $item = Post::selectFirst(['uri-id'], ['id' => $id]);
127                         if (!empty($item['uri-id'])) {
128                                 return Transmitter::createActivityFromItem($id);
129                         }
130                 }
131                 return [];
132         }
133
134         /**
135          * Update an existing post or comment
136          *
137          * @param integer $uid
138          * @param string $object_id
139          * @param array $application
140          * @param array $ldactivity
141          * @return array
142          */
143         private static function updateContent(int $uid, string $object_id, array $application, array $ldactivity): array
144         {
145                 $id            = Item::fetchByLink($object_id, $uid);
146                 $original_post = Post::selectFirst(['uri-id'], ['uid' => $uid, 'origin' => true, 'id' => $id]);
147                 if (empty($original_post)) {
148                         Logger::debug('Item not found or does not belong to the user', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]);
149                         return [];
150                 }
151
152                 $object_data = self::processObject($ldactivity['as:object']);
153                 $item        = ClientToServer::processContent($object_data, $application, $uid);
154                 if (empty($item['title']) && empty($item['body'])) {
155                         Logger::debug('Empty body and title', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]);
156                         return [];
157                 }
158                 $post = ['title' => $item['title'], 'body' => $item['body']];
159                 Logger::debug('Got data', ['id' => $id, 'uid' => $uid, 'item' => $post]);
160                 Item::update($post, ['id' => $id]);
161                 Item::updateDisplayCache($original_post['uri-id']);
162
163                 return Transmitter::createActivityFromItem($id);
164         }
165
166         /**
167          * Follow a given account
168          * @todo Check the expected return value
169          *
170          * @param integer $uid
171          * @param string $object_id
172          * @param array $ldactivity
173          * @return array
174          */
175         private static function followAccount(int $uid, string $object_id, array $ldactivity): array
176         {
177                 return [];
178         }
179
180         /**
181          * Fetches data from the object part of an client to server activity
182          *
183          * @param array $object
184          *
185          * @return array Object data
186          */
187         private static function processObject(array $object): array
188         {
189                 $object_data = Receiver::getObjectDataFromActivity($object);
190
191                 $object_data['target']   = self::getTargets($object, $object_data['actor'] ?? '');
192                 $object_data['receiver'] = [];
193
194                 return $object_data;
195         }
196
197         /**
198          * Accumulate the targets and visibility of this post
199          *
200          * @param array $object
201          * @param string $actor
202          * @return array
203          */
204         private static function getTargets(array $object, string $actor): array
205         {
206                 $profile   = APContact::getByURL($actor);
207                 $followers = $profile['followers'];
208
209                 $targets = [];
210
211                 foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc', 'as:audience'] as $element) {
212                         switch ($element) {
213                                 case 'as:to':
214                                         $type = Receiver::TARGET_TO;
215                                         break;
216                                 case 'as:cc':
217                                         $type = Receiver::TARGET_CC;
218                                         break;
219                                 case 'as:bto':
220                                         $type = Receiver::TARGET_BTO;
221                                         break;
222                                 case 'as:bcc':
223                                         $type = Receiver::TARGET_BCC;
224                                         break;
225                                 case 'as:audience':
226                                         $type = Receiver::TARGET_AUDIENCE;
227                                         break;
228                         }
229                         $receiver_list = JsonLD::fetchElementArray($object, $element, '@id');
230                         if (empty($receiver_list)) {
231                                 continue;
232                         }
233
234                         foreach ($receiver_list as $receiver) {
235                                 if ($receiver == Receiver::PUBLIC_COLLECTION) {
236                                         $targets[Receiver::TARGET_GLOBAL] = ($element == 'as:to');
237                                         continue;
238                                 }
239
240                                 if ($receiver == $followers) {
241                                         $targets[Receiver::TARGET_FOLLOWER] = true;
242                                         continue;
243                                 }
244                                 $targets[$type][] = Contact::getIdForURL($receiver);
245                         }
246                 }
247                 return $targets;
248         }
249
250         /**
251          * Create an item array from client to server object data
252          *
253          * @param array $object_data
254          * @param array $application
255          * @param integer $uid
256          * @return array
257          */
258         private static function processContent(array $object_data, array $application, int $uid): array
259         {
260                 $owner = User::getOwnerDataById($uid);
261
262                 $item = [];
263
264                 $item['network']    = Protocol::DFRN;
265                 $item['uid']        = $uid;
266                 $item['verb']       = Activity::POST;
267                 $item['contact-id'] = $owner['id'];
268                 $item['author-id']  = $item['owner-id']  = Contact::getPublicIdByUserId($uid);
269                 $item['title']      = $object_data['name'];
270                 $item['body']       = Markdown::toBBCode($object_data['content'] ?? '');
271                 $item['app']        = $application['name'] ?? 'API';
272
273                 if (!empty($object_data['target'][Receiver::TARGET_GLOBAL])) {
274                         $item['allow_cid'] = '';
275                         $item['allow_gid'] = '';
276                         $item['deny_cid']  = '';
277                         $item['deny_gid']  = '';
278                         $item['private']   = Item::PUBLIC;
279                 } elseif (isset($object_data['target'][Receiver::TARGET_GLOBAL])) {
280                         $item['allow_cid'] = '';
281                         $item['allow_gid'] = '';
282                         $item['deny_cid']  = '';
283                         $item['deny_gid']  = '';
284                         $item['private']   = Item::UNLISTED;
285                 } elseif (!empty($object_data['target'][Receiver::TARGET_FOLLOWER])) {
286                         $item['allow_cid'] = '';
287                         $item['allow_gid'] = '<' . Group::FOLLOWERS . '>';
288                         $item['deny_cid']  = '';
289                         $item['deny_gid']  = '';
290                         $item['private']   = Item::PRIVATE;
291                 } else {
292                         // @todo Set permissions via the $object_data['target'] array
293                         $item['allow_cid'] = '<' . $owner['id'] . '>';
294                         $item['allow_gid'] = '';
295                         $item['deny_cid']  = '';
296                         $item['deny_gid']  = '';
297                         $item['private']   = Item::PRIVATE;
298                 }
299
300                 if (!empty($object_data['summary'])) {
301                         $item['body'] = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $object_data['summary'] . "[/abstract]\n" . $item['body'];
302                 }
303
304                 if ($object_data['reply-to-id']) {
305                         $item['thr-parent'] = $object_data['reply-to-id'];
306                         $item['gravity']    = Item::GRAVITY_COMMENT;
307                 } else {
308                         $item['gravity'] = Item::GRAVITY_PARENT;
309                 }
310
311                 $item = DI::contentItem()->expandTags($item);
312
313                 return $item;
314         }
315
316         /**
317          * Public posts for the given owner
318          *
319          * @param array   $owner     Owner array
320          * @param integer $uid       User id
321          * @param integer $page      Page number
322          * @param integer $max_id    Maximum ID
323          * @param string  $requester URL of requesting account
324          * @param boolean $nocache   Wether to bypass caching
325          * @return array of posts
326          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
327          * @throws \ImagickException
328          */
329         public static function getOutbox(array $owner, int $uid, int $page = null, int $max_id = null, string $requester = ''): array
330         {
331                 $condition = [
332                         'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT],
333                         'private' => [Item::PUBLIC, Item::UNLISTED]
334                 ];
335
336                 if (!empty($requester)) {
337                         $requester_id = Contact::getIdForURL($requester, $owner['uid']);
338                         if (!empty($requester_id)) {
339                                 $permissionSets = DI::permissionSet()->selectByContactId($requester_id, $owner['uid']);
340                                 if (!empty($permissionSets)) {
341                                         $condition = ['psid' => array_merge($permissionSets->column('id'),
342                                                 [DI::permissionSet()->selectPublicForUser($owner['uid'])])];
343                                 }
344                         }
345                 }
346
347                 $condition = array_merge($condition, [
348                         'uid'            => $owner['uid'],
349                         'author-id'      => Contact::getIdForURL($owner['url'], 0, false),
350                         'gravity'        => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT],
351                         'network'        => Protocol::FEDERATED,
352                         'parent-network' => Protocol::FEDERATED,
353                         'origin'         => true,
354                         'deleted'        => false,
355                         'visible'        => true
356                 ]);
357
358                 $apcontact = APContact::getByURL($owner['url']);
359
360                 if (empty($apcontact)) {
361                         throw new \Friendica\Network\HTTPException\NotFoundException();
362                 }
363
364                 return self::getCollection($condition, DI::baseUrl() . '/outbox/' . $owner['nickname'], $page, $max_id, $uid, $apcontact['statuses_count']);
365         }
366
367         public static function getInbox(int $uid, int $page = null, int $max_id = null)
368         {
369                 $owner = User::getOwnerDataById($uid);
370
371                 $condition = [
372                         'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT],
373                         'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN],
374                         'uid'     => $uid
375                 ];
376
377                 return self::getCollection($condition, DI::baseUrl() . '/inbox/' . $owner['nickname'], $page, $max_id, $uid, null);
378         }
379
380         public static function getPublicInbox(int $uid, int $page = null, int $max_id = null)
381         {
382                 $condition = [
383                         'gravity'        => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT],
384                         'private'        => Item::PUBLIC,
385                         'network'        => [Protocol::ACTIVITYPUB, Protocol::DFRN],
386                         'author-blocked' => false,
387                         'author-hidden'  => false
388                 ];
389
390                 return self::getCollection($condition, DI::baseUrl() . '/inbox', $page, $max_id, $uid, null);
391         }
392
393         private static function getCollection(array $condition, string $path, int $page = null, int $max_id = null, int $uid = null, int $total_items = null)
394         {
395                 $data = ['@context' => ActivityPub::CONTEXT];
396
397                 $data['id']   = $path;
398                 $data['type'] = 'OrderedCollection';
399
400                 if (!is_null($total_items)) {
401                         $data['totalItems'] = $total_items;
402                 }
403
404                 if (!empty($page)) {
405                         $data['id'] .= '?' . http_build_query(['page' => $page]);
406                 }
407
408                 if (empty($page) && empty($max_id)) {
409                         $data['first'] = $path . '?page=1';
410                 } else {
411                         $data['type'] = 'OrderedCollectionPage';
412
413                         $list = [];
414
415                         if (!empty($max_id)) {
416                                 $condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]);
417                         }
418
419                         if (!empty($page)) {
420                                 $params = ['limit' => [($page - 1) * 20, 20], 'order' => ['uri-id' => true]];
421                         } else {
422                                 $params = ['limit' => 20, 'order' => ['uri-id' => true]];
423                         }
424
425                         if (!is_null($uid)) {
426                                 $items = Post::selectForUser($uid, ['id', 'uri-id'], $condition, $params);
427                         } else {
428                                 $items = Post::select(['id', 'uri-id'], $condition, $params);
429                         }
430
431                         $last_id = 0;
432
433                         while ($item = Post::fetch($items)) {
434                                 $activity = Transmitter::createActivityFromItem($item['id'], false, !is_null($uid));
435                                 if (!empty($activity)) {
436                                         $list[]  = $activity;
437                                         $last_id = $item['uri-id'];
438                                         continue;
439                                 }
440                         }
441                         DBA::close($items);
442
443                         if (count($list) == 20) {
444                                 $data['next'] = $path . '?max_id=' . $last_id;
445                         }
446
447                         // Fix the cached total item count when it is lower than the real count
448                         if (!is_null($total_items)) {
449                                 $total = (($page - 1) * 20) + $data['totalItems'];
450                                 if ($total > $data['totalItems']) {
451                                         $data['totalItems'] = $total;
452                                 }
453                         }
454
455                         $data['partOf'] = $path;
456
457                         $data['orderedItems'] = $list;
458                 }
459
460                 return $data;
461         }
462 }