]> git.mxchange.org Git - friendica.git/blob - src/Protocol/ActivityPub/ClientToServer.php
Merge remote-tracking branch 'upstream/develop' into c2s-post
[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\DI;
28 use Friendica\Model\APContact;
29 use Friendica\Model\Contact;
30 use Friendica\Model\Group;
31 use Friendica\Model\Item;
32 use Friendica\Model\Post;
33 use Friendica\Model\User;
34 use Friendica\Protocol\Activity;
35 use Friendica\Util\JsonLD;
36
37 /**
38  * ActivityPub Client To Server class
39  */
40 class ClientToServer
41 {
42         /**
43          * Process client to server activities
44          *
45          * @param array $activity
46          * @param integer $uid
47          * @param array $application
48          * @return array
49          */
50         public static function processActivity(array $activity, int $uid, array $application): array
51         {
52                 $ldactivity = JsonLD::compact($activity);
53                 if (empty($ldactivity)) {
54                         Logger::notice('Invalid activity', ['activity' => $activity, 'uid' => $uid]);
55                         return [];
56                 }
57
58                 $type = JsonLD::fetchElement($ldactivity, '@type');
59                 if (!$type) {
60                         Logger::notice('Empty type', ['activity' => $ldactivity, 'uid' => $uid]);
61                         return [];
62                 }
63
64                 $object_id   = JsonLD::fetchElement($ldactivity, 'as:object', '@id') ?? '';
65                 $object_type = Receiver::fetchObjectType($ldactivity, $object_id, $uid);
66                 if (!$object_type && !$object_id) {
67                         Logger::notice('Empty object type or id', ['activity' => $ldactivity, 'uid' => $uid]);
68                         return [];
69                 }
70
71                 Logger::debug('Processing activity', ['type' => $type, 'object_type' => $object_type, 'object_id' => $object_id, 'activity' => $ldactivity]);
72                 return self::routeActivities($type, $object_type, $object_id, $uid, $application, $ldactivity);
73         }
74
75         /**
76          * Route client to server activities
77          *
78          * @param string $type
79          * @param string $object_type
80          * @param string $object_id
81          * @param integer $uid
82          * @param array $application
83          * @param array $ldactivity
84          * @return array
85          */
86         private static function routeActivities(string $type, string $object_type, string $object_id, int $uid, array $application, array $ldactivity): array
87         {
88                 switch ($type) {
89                         case 'as:Create':
90                                 if (in_array($object_type, Receiver::CONTENT_TYPES)) {
91                                         return self::createContent($uid, $application, $ldactivity);
92                                 }
93                                 break;
94                         case 'as:Update':
95                                 if (in_array($object_type, Receiver::CONTENT_TYPES) && !empty($object_id)) {
96                                         return self::updateContent($uid, $object_id, $application, $ldactivity);
97                                 }
98                                 break;
99                         case 'as:Follow':
100                                 if (in_array($object_type, Receiver::ACCOUNT_TYPES) && !empty($object_id)) {
101                                         return self::followAccount($uid, $object_id, $ldactivity);
102                                 }
103                                 break;
104                 }
105                 return [];
106         }
107
108         /**
109          * Create a new post or comment
110          *
111          * @param integer $uid
112          * @param array $application
113          * @param array $ldactivity
114          * @return array
115          */
116         private static function createContent(int $uid, array $application, array $ldactivity): array
117         {
118                 $object_data = self::processObject($ldactivity['as:object']);
119                 $item        = ClientToServer::processContent($object_data, $application, $uid);
120                 Logger::debug('Got data', ['item' => $item, 'object' => $object_data]);
121
122                 $id = Item::insert($item, true);
123                 if (!empty($id)) {
124                         $item = Post::selectFirst(['uri-id'], ['id' => $id]);
125                         if (!empty($item['uri-id'])) {
126                                 return Transmitter::createActivityFromItem($id);
127                         }
128                 }
129                 return [];
130         }
131
132         /**
133          * Update an existing post or comment
134          *
135          * @param integer $uid
136          * @param string $object_id
137          * @param array $application
138          * @param array $ldactivity
139          * @return array
140          */
141         private static function updateContent(int $uid, string $object_id, array $application, array $ldactivity):array
142         {
143                 $id            = Item::fetchByLink($object_id, $uid);
144                 $original_post = Post::selectFirst(['uri-id'], ['uid' => $uid, 'origin' => true, 'id' => $id]);
145                 if (empty($original_post)) {
146                         Logger::debug('Item not found or does not belong to the user', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]);
147                         return [];
148                 }
149
150                 $object_data = self::processObject($ldactivity['as:object']);
151                 $item        = ClientToServer::processContent($object_data, $application, $uid);
152                 if (empty($item['title']) && empty($item['body'])) {
153                         Logger::debug('Empty body and title', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]);
154                         return [];
155                 }
156                 $post = ['title' => $item['title'], 'body' => $item['body']];
157                 Logger::debug('Got data', ['id' => $id, 'uid' => $uid, 'item' => $post]);
158                 Item::update($post, ['id' => $id]);
159                 Item::updateDisplayCache($original_post['uri-id']);
160
161                 return Transmitter::createActivityFromItem($id);
162         }
163
164         /**
165          * Follow a given account
166          * @todo Check the expected return value
167          *
168          * @param integer $uid
169          * @param string $object_id
170          * @param array $ldactivity
171          * @return array
172          */
173         private static function followAccount(int $uid, string $object_id, array $ldactivity): array
174         {
175                 return [];
176         }
177
178         /**
179          * Fetches data from the object part of an client to server activity
180          *
181          * @param array $object
182          *
183          * @return array Object data
184          */
185         private static function processObject(array $object): array
186         {
187                 $object_data = Receiver::getObjectDataFromActivity($object);
188
189                 $object_data['target']   = self::getTargets($object, $object_data['actor'] ?? '');
190                 $object_data['receiver'] = [];
191
192                 return $object_data;
193         }
194
195         /**
196          * Accumulate the targets and visibility of this post
197          *
198          * @param array $object
199          * @param string $actor
200          * @return array
201          */
202         private static function getTargets(array $object, string $actor): array
203         {
204                 $profile   = APContact::getByURL($actor);
205                 $followers = $profile['followers'];
206
207                 $targets = [];
208
209                 foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) {
210                         switch ($element) {
211                                 case 'as:to':
212                                         $type = Receiver::TARGET_TO;
213                                         break;
214                                 case 'as:cc':
215                                         $type = Receiver::TARGET_CC;
216                                         break;
217                                 case 'as:bto':
218                                         $type = Receiver::TARGET_BTO;
219                                         break;
220                                 case 'as:bcc':
221                                         $type = Receiver::TARGET_BCC;
222                                         break;
223                         }
224                         $receiver_list = JsonLD::fetchElementArray($object, $element, '@id');
225                         if (empty($receiver_list)) {
226                                 continue;
227                         }
228
229                         foreach ($receiver_list as $receiver) {
230                                 if ($receiver == Receiver::PUBLIC_COLLECTION) {
231                                         $targets[Receiver::TARGET_GLOBAL] = ($element == 'as:to');
232                                         continue;
233                                 }
234
235                                 if ($receiver == $followers) {
236                                         $targets[Receiver::TARGET_FOLLOWER] = true;
237                                         continue;
238                                 }
239                                 $targets[$type][] = Contact::getIdForURL($receiver);
240                         }
241                 }
242                 return $targets;
243         }
244
245         /**
246          * Create an item array from client to server object data
247          *
248          * @param array $object_data
249          * @param array $application
250          * @param integer $uid
251          * @return array
252          */
253         private static function processContent(array $object_data, array $application, int $uid): array
254         {
255                 $owner = User::getOwnerDataById($uid);
256
257                 $item = [];
258
259                 $item['network']    = Protocol::DFRN;
260                 $item['uid']        = $uid;
261                 $item['verb']       = Activity::POST;
262                 $item['contact-id'] = $owner['id'];
263                 $item['author-id']  = $item['owner-id']  = Contact::getPublicIdByUserId($uid);
264                 $item['title']      = $object_data['name'];
265                 $item['body']       = Markdown::toBBCode($object_data['content']);
266                 $item['app']        = $application['name'] ?? 'API';
267
268                 if (!empty($object_data['target'][Receiver::TARGET_GLOBAL])) {
269                         $item['allow_cid'] = '';
270                         $item['allow_gid'] = '';
271                         $item['deny_cid']  = '';
272                         $item['deny_gid']  = '';
273                         $item['private']   = Item::PUBLIC;
274                 } elseif (isset($object_data['target'][Receiver::TARGET_GLOBAL])) {
275                         $item['allow_cid'] = '';
276                         $item['allow_gid'] = '';
277                         $item['deny_cid']  = '';
278                         $item['deny_gid']  = '';
279                         $item['private']   = Item::UNLISTED;
280                 } elseif (!empty($object_data['target'][Receiver::TARGET_FOLLOWER])) {
281                         $item['allow_cid'] = '';
282                         $item['allow_gid'] = '<' . Group::FOLLOWERS . '>';
283                         $item['deny_cid']  = '';
284                         $item['deny_gid']  = '';
285                         $item['private']   = Item::PRIVATE;
286                 } else {
287                         // @todo Set permissions via the $object_data['target'] array
288                         $item['allow_cid'] = '<' . $owner['id'] . '>';
289                         $item['allow_gid'] = '';
290                         $item['deny_cid']  = '';
291                         $item['deny_gid']  = '';
292                         $item['private']   = Item::PRIVATE;
293                 }
294
295                 if (!empty($object_data['summary'])) {
296                         $item['body'] = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $object_data['summary'] . "[/abstract]\n" . $item['body'];
297                 }
298
299                 if ($object_data['reply-to-id']) {
300                         $item['thr-parent'] = $object_data['reply-to-id'];
301                         $item['gravity']    = Item::GRAVITY_COMMENT;
302                 } else {
303                         $item['gravity'] = Item::GRAVITY_PARENT;
304                 }
305
306                 $item = DI::contentItem()->expandTags($item);
307
308                 return $item;
309         }
310 }