]> git.mxchange.org Git - friendica-addons.git/blob - statusnet/statusnet.php
9e009d1b34ff5cc2444cf4e58f9f5a8ec15f50e1
[friendica-addons.git] / statusnet / statusnet.php
1 <?php
2
3 /**
4  * Name: GNU Social Connector
5  * Description: Bidirectional (posting, relaying and reading) connector for GNU Social.
6  * Version: 1.0.5
7  * Author: Tobias Diekershoff <https://f.diekershoff.de/profile/tobias>
8  * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
9  *
10  * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel
11  * All rights reserved.
12  *
13  * Redistribution and use in source and binary forms, with or without
14  * modification, are permitted provided that the following conditions are met:
15  *    * Redistributions of source code must retain the above copyright notice,
16  *     this list of conditions and the following disclaimer.
17  *    * Redistributions in binary form must reproduce the above
18  *    * copyright notice, this list of conditions and the following disclaimer in
19  *      the documentation and/or other materials provided with the distribution.
20  *    * Neither the name of the <organization> nor the names of its contributors
21  *      may be used to endorse or promote products derived from this software
22  *      without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27  * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
28  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
32  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
33  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34  *
35  */
36 define('STATUSNET_DEFAULT_POLL_INTERVAL', 5); // given in minutes
37
38 require_once __DIR__ . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'statusnetoauth.php';
39
40 use CodebirdSN\CodebirdSN;
41 use Friendica\App;
42 use Friendica\Content\OEmbed;
43 use Friendica\Content\PageInfo;
44 use Friendica\Content\Text\HTML;
45 use Friendica\Content\Text\Plaintext;
46 use Friendica\Core\Hook;
47 use Friendica\Core\Logger;
48 use Friendica\Core\Protocol;
49 use Friendica\Core\Renderer;
50 use Friendica\Core\System;
51 use Friendica\Database\DBA;
52 use Friendica\DI;
53 use Friendica\Model\Contact;
54 use Friendica\Model\Group;
55 use Friendica\Model\Item;
56 use Friendica\Model\Photo;
57 use Friendica\Model\Post;
58 use Friendica\Model\User;
59 use Friendica\Network\HTTPClient\Client\HttpClientAccept;
60 use Friendica\Protocol\Activity;
61 use Friendica\Util\DateTimeFormat;
62 use Friendica\Util\Strings;
63 use GuzzleHttp\Exception\TransferException;
64
65 function statusnet_install()
66 {
67         //  we need some hooks, for the configuration and for sending tweets
68         Hook::register('connector_settings', 'addon/statusnet/statusnet.php', 'statusnet_settings');
69         Hook::register('connector_settings_post', 'addon/statusnet/statusnet.php', 'statusnet_settings_post');
70         Hook::register('notifier_normal', 'addon/statusnet/statusnet.php', 'statusnet_post_hook');
71         Hook::register('hook_fork', 'addon/statusnet/statusnet.php', 'statusnet_hook_fork');
72         Hook::register('post_local', 'addon/statusnet/statusnet.php', 'statusnet_post_local');
73         Hook::register('jot_networks', 'addon/statusnet/statusnet.php', 'statusnet_jot_nets');
74         Hook::register('cron', 'addon/statusnet/statusnet.php', 'statusnet_cron');
75         Hook::register('prepare_body', 'addon/statusnet/statusnet.php', 'statusnet_prepare_body');
76         Hook::register('check_item_notification', 'addon/statusnet/statusnet.php', 'statusnet_check_item_notification');
77         Logger::notice('installed GNU Social');
78 }
79
80 function statusnet_check_item_notification(App $a, &$notification_data)
81 {
82         if (DI::pConfig()->get($notification_data['uid'], 'statusnet', 'post')) {
83                 $notification_data['profiles'][] = DI::pConfig()->get($notification_data['uid'], 'statusnet', 'own_url');
84         }
85 }
86
87 function statusnet_jot_nets(App $a, array &$jotnets_fields)
88 {
89         if (!DI::userSession()->getLocalUserId()) {
90                 return;
91         }
92
93         if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'post')) {
94                 $jotnets_fields[] = [
95                         'type' => 'checkbox',
96                         'field' => [
97                                 'statusnet_enable',
98                                 DI::l10n()->t('Post to GNU Social'),
99                                 DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'post_by_default')
100                         ]
101                 ];
102         }
103 }
104
105 function statusnet_settings_post(App $a, $post)
106 {
107         if (!DI::userSession()->getLocalUserId()) {
108                 return;
109         }
110         // don't check GNU Social settings if GNU Social submit button is not clicked
111         if (empty($_POST['statusnet-submit']) && empty($_POST['statusnet-disconnect'])) {
112                 return;
113         }
114
115         if (!empty($_POST['statusnet-disconnect'])) {
116                 /*               * *
117                  * if the GNU Social-disconnect button is clicked, clear the GNU Social configuration
118                  */
119                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'consumerkey');
120                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'consumersecret');
121                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'post');
122                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'post_by_default');
123                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'oauthtoken');
124                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'oauthsecret');
125                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'baseapi');
126                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'lastid');
127                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'mirror_posts');
128                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'import');
129                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'create_user');
130                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'own_url');
131         } else {
132                 if (isset($_POST['statusnet-preconf-apiurl'])) {
133                         /*                       * *
134                          * If the user used one of the preconfigured GNU Social server credentials
135                          * use them. All the data are available in the global config.
136                          * Check the API Url never the less and blame the admin if it's not working ^^
137                          */
138                         $globalsn = DI::config()->get('statusnet', 'sites');
139                         foreach ($globalsn as $asn) {
140                                 if ($asn['apiurl'] == $_POST['statusnet-preconf-apiurl']) {
141                                         $apibase = $asn['apiurl'];
142                                         $c = DI::httpClient()->fetch($apibase . 'statusnet/version.xml');
143                                         if (strlen($c) > 0) {
144                                                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'consumerkey', $asn['consumerkey']);
145                                                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'consumersecret', $asn['consumersecret']);
146                                                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'baseapi', $asn['apiurl']);
147                                                 //DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'application_name', $asn['applicationname'] );
148                                         } else {
149                                                 DI::sysmsg()->addNotice(DI::l10n()->t('Please contact your site administrator.<br />The provided API URL is not valid.') . '<br />' . $asn['apiurl']);
150                                         }
151                                 }
152                         }
153                 } else {
154                         if (isset($_POST['statusnet-consumersecret'])) {
155                                 //  check if we can reach the API of the GNU Social server
156                                 //  we'll check the API Version for that, if we don't get one we'll try to fix the path but will
157                                 //  resign quickly after this one try to fix the path ;-)
158                                 $apibase = $_POST['statusnet-baseapi'];
159                                 $c = DI::httpClient()->fetch($apibase . 'statusnet/version.xml');
160                                 if (strlen($c) > 0) {
161                                         //  ok the API path is correct, let's save the settings
162                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'consumerkey', $_POST['statusnet-consumerkey']);
163                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'consumersecret', $_POST['statusnet-consumersecret']);
164                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'baseapi', $apibase);
165                                         //DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'application_name', $_POST['statusnet-applicationname'] );
166                                 } else {
167                                         //  the API path is not correct, maybe missing trailing / ?
168                                         $apibase = $apibase . '/';
169                                         $c = DI::httpClient()->fetch($apibase . 'statusnet/version.xml');
170                                         if (strlen($c) > 0) {
171                                                 //  ok the API path is now correct, let's save the settings
172                                                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'consumerkey', $_POST['statusnet-consumerkey']);
173                                                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'consumersecret', $_POST['statusnet-consumersecret']);
174                                                 DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'baseapi', $apibase);
175                                         } else {
176                                                 //  still not the correct API base, let's do noting
177                                                 DI::sysmsg()->addNotice(DI::l10n()->t('We could not contact the GNU Social API with the Path you entered.'));
178                                         }
179                                 }
180                         } else {
181                                 if (isset($_POST['statusnet-pin'])) {
182                                         //  if the user supplied us with a PIN from GNU Social, let the magic of OAuth happen
183                                         $api = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'baseapi');
184                                         $ckey = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'consumerkey');
185                                         $csecret = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'consumersecret');
186                                         //  the token and secret for which the PIN was generated were hidden in the settings
187                                         //  form as token and token2, we need a new connection to GNU Social using these token
188                                         //  and secret to request a Access Token with the PIN
189                                         $connection = new StatusNetOAuth($api, $ckey, $csecret, $_POST['statusnet-token'], $_POST['statusnet-token2']);
190                                         $token = $connection->getAccessToken($_POST['statusnet-pin']);
191                                         //  ok, now that we have the Access Token, save them in the user config
192                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'oauthtoken', $token['oauth_token']);
193                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'oauthsecret', $token['oauth_token_secret']);
194                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'post', 1);
195                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'post_taglinks', 1);
196                                         //  reload the Addon Settings page, if we don't do it see Bug #42
197                                 } else {
198                                         //  if no PIN is supplied in the POST variables, the user has changed the setting
199                                         //  to post a dent for every new __public__ posting to the wall
200                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'post', intval($_POST['statusnet-enable']));
201                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'post_by_default', intval($_POST['statusnet-default']));
202                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'mirror_posts', intval($_POST['statusnet-mirror']));
203                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'import', intval($_POST['statusnet-import']));
204                                         DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'statusnet', 'create_user', intval($_POST['statusnet-create_user']));
205
206                                         if (!intval($_POST['statusnet-mirror']))
207                                                 DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'statusnet', 'lastid');
208                                 }
209                         }
210                 }
211         }
212 }
213
214 function statusnet_settings(App $a, array &$data)
215 {
216         if (!DI::userSession()->getLocalUserId()) {
217                 return;
218         }
219
220         DI::page()->registerStylesheet(__DIR__ . '/statusnet.css', 'all');
221
222         /*       * *
223          * 1) Check that we have a base api url and a consumer key & secret
224          * 2) If no OAuthtoken & stuff is present, generate button to get some
225          *    allow the user to cancel the connection process at this step
226          * 3) Checkbox for "Send public notices (respect size limitation)
227          */
228         $baseapi            = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'baseapi');
229         $ckey               = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'consumerkey');
230         $csecret            = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'consumersecret');
231         $otoken             = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'oauthtoken');
232         $osecret            = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'oauthsecret');
233         $enabled            = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'post', false);
234         $def_enabled        = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'post_by_default', false);
235         $mirror_enabled     = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'mirror_posts', false);
236         $createuser_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'create_user', false);
237         $import             = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'import');
238
239         // Radio button list to select existing application credentials
240         $sites = array_map(function ($site) {
241                 return ['statusnet-preconf-apiurl', $site['sitename'], $site['apiurl']];
242         }, DI::config()->get('statusnet', 'sites', []));
243
244         $submit = ['statusnet-submit' => DI::l10n()->t('Save Settings')];
245
246         if ($ckey && $csecret) {
247                 if ($otoken && $osecret) {
248                         /*                       * *
249                          *  we have an OAuth key / secret pair for the user
250                          *  so let's give a chance to disable the postings to GNU Social
251                          */
252                         $connection = new StatusNetOAuth($baseapi, $ckey, $csecret, $otoken, $osecret);
253                         $account    = $connection->get('account/verify_credentials');
254
255                         if (!empty($account)) {
256                                 $connected_account = DI::l10n()->t('Currently connected to: <a href="%s" target="_statusnet">%s</a>', $account->statusnet_profile_url, $account->screen_name);
257                         }
258
259                         $user = User::getById(DI::userSession()->getLocalUserId());
260                         if ($user['hidewall']) {
261                                 $privacy_warning = DI::l10n()->t('<strong>Note</strong>: Due your privacy settings (<em>Hide your profile details from unknown viewers?</em>) the link potentially included in public postings relayed to GNU Social will lead the visitor to a blank page informing the visitor that the access to your profile has been restricted.');
262                         }
263
264                         $submit['statusnet-disconnect'] = DI::l10n()->t('Clear OAuth configuration');
265                 } else {
266                         /*                       * *
267                          * the user has not yet connected the account to GNU Social
268                          * get a temporary OAuth key/secret pair and display a button with
269                          * which the user can request a PIN to connect the account to a
270                          * account at GNU Social
271                          */
272                         $connection    = new StatusNetOAuth($baseapi, $ckey, $csecret);
273                         $request_token = $connection->getRequestToken('oob');
274                         $authorize_url = $connection->getAuthorizeURL($request_token['oauth_token'], false);
275
276                         $submit['statusnet-disconnect'] = DI::l10n()->t('Cancel GNU Social Connection');
277                 }
278         }
279
280
281         $t    = Renderer::getMarkupTemplate('connector_settings.tpl', 'addon/statusnet/');
282         $html = Renderer::replaceMacros($t, [
283                 '$l10n' => [
284                         'global_title'      => DI::l10n()->t('Globally Available GNU Social OAuthKeys'),
285                         'global_info'       => DI::l10n()->t(DI::l10n()->t('There are preconfigured OAuth key pairs for some GNU Social servers available. If you are using one of them, please use these credentials. If not feel free to connect to any other GNU Social instance (see below).')),
286                         'credentials_title' => DI::l10n()->t('Provide your own OAuth Credentials'),
287                         'credentials_info'  => DI::l10n()->t('No consumer key pair for GNU Social found. Register your Friendica Account as a desktop application on your GNU Social account, copy the consumer key pair here and enter the API base root.<br />Before you register your own OAuth key pair ask the administrator if there is already a key pair for this Friendica installation at your favorite GNU Social installation.'),
288                         'oauth_info'        => DI::l10n()->t('To connect to your GNU Social account click the button below to get a security code from GNU Social which you have to copy into the input box below and submit the form. Only your <strong>public</strong> posts will be posted to GNU Social.'),
289                         'oauth_alt'         => DI::l10n()->t('Log in with GNU Social'),
290                         'oauth_cancel'      => DI::l10n()->t('Cancel Connection Process'),
291                         'oauth_api'         => DI::l10n()->t('Current GNU Social API is: %s', $baseapi),
292                         'connected_account' => $connected_account ?? '',
293                         'privacy_warning'   => $privacy_warning ?? '',
294                 ],
295
296                 '$ckey'    => $ckey,
297                 '$csecret' => $csecret,
298                 '$otoken'  => $otoken,
299                 '$osecret' => $osecret,
300                 '$sites'   => $sites,
301
302                 '$authorize_url' => $authorize_url ?? '',
303                 '$request_token' => $request_token ?? null,
304                 '$account'       => $account ?? null,
305
306                 '$authenticate_url' => DI::baseUrl()->get() . '/statusnet/connect',
307
308                 '$consumerkey'    => ['statusnet-consumerkey', DI::l10n()->t('OAuth Consumer Key'), '', '', false, ' size="35'],
309                 '$consumersecret' => ['statusnet-consumersecret', DI::l10n()->t('OAuth Consumer Secret'), '', '', false, ' size="35'],
310
311                 '$baseapi' => ['statusnet-baseapi', DI::l10n()->t('Base API Path (remember the trailing /)'), '', '', false, ' size="35'],
312                 '$pin'     => ['statusnet-pin', DI::l10n()->t('Copy the security code from GNU Social here')],
313
314                 '$enable'      => ['statusnet-enabled', DI::l10n()->t('Allow posting to GNU Social'), $enabled, DI::l10n()->t('If enabled all your <strong>public</strong> postings can be posted to the associated GNU Social account. You can choose to do so by default (here) or for every posting separately in the posting options when writing the entry.')],
315                 '$default'     => ['statusnet-default', DI::l10n()->t('Post to GNU Social by default'), $def_enabled],
316                 '$mirror'      => ['statusnet-mirror', DI::l10n()->t('Mirror all public posts'), $mirror_enabled],
317                 '$create_user' => ['statusnet-create_user', DI::l10n()->t('Automatically create contacts'), $createuser_enabled],
318                 '$import'      => ['statusnet-import', DI::l10n()->t('Import the remote timeline'), $import, '', [
319                         0 => DI::l10n()->t('Disabled'),
320                         1 => DI::l10n()->t('Full Timeline'),
321                         2 => DI::l10n()->t('Only Mentions'),
322                 ]],
323         ]);
324
325         $data = [
326                 'connector' => 'statusnet',
327                 'title'     => DI::l10n()->t('GNU Social Import/Export/Mirror'),
328                 'image'     => 'images/gnusocial.png',
329                 'enabled'   => $enabled,
330                 'html'      => $html,
331                 'submit'    => $submit,
332         ];
333 }
334
335 function statusnet_hook_fork(App $a, array &$b)
336 {
337         if ($b['name'] != 'notifier_normal') {
338                 return;
339         }
340
341         $post = $b['data'];
342
343         // Deleting and editing is not supported by the addon
344         if ($post['deleted'] || ($post['created'] !== $post['edited'])) {
345                 $b['execute'] = false;
346                 return;
347         }
348
349         // if post comes from GNU Social don't send it back
350         if ($post['extid'] == Protocol::STATUSNET) {
351                 $b['execute'] = false;
352                 return;
353         }
354
355         if ($post['app'] == 'StatusNet') {
356                 $b['execute'] = false;
357                 return;
358         }
359
360         if (DI::pConfig()->get($post['uid'], 'statusnet', 'import')) {
361                 // Don't fork if it isn't a reply to a GNU Social post
362                 if (($post['parent'] != $post['id']) && !Post::exists(['id' => $post['parent'], 'network' => Protocol::STATUSNET])) {
363                         Logger::notice('No GNU Social parent found for item ' . $post['id']);
364                         $b['execute'] = false;
365                         return;
366                 }
367         } else {
368                 // Comments are never exported when we don't import the GNU Social timeline
369                 if (!strstr($post['postopts'], 'statusnet') || ($post['parent'] != $post['id']) || $post['private']) {
370                         $b['execute'] = false;
371                         return;
372                 }
373         }
374 }
375
376 function statusnet_post_local(App $a, array &$b)
377 {
378         if ($b['edit']) {
379                 return;
380         }
381
382         if (!DI::userSession()->getLocalUserId() || (DI::userSession()->getLocalUserId() != $b['uid'])) {
383                 return;
384         }
385
386         $statusnet_post = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'post');
387         $statusnet_enable = (($statusnet_post && !empty($_REQUEST['statusnet_enable'])) ? intval($_REQUEST['statusnet_enable']) : 0);
388
389         // if API is used, default to the chosen settings
390         if ($b['api_source'] && intval(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'post_by_default'))) {
391                 $statusnet_enable = 1;
392         }
393
394         if (!$statusnet_enable) {
395                 return;
396         }
397
398         if (strlen($b['postopts'])) {
399                 $b['postopts'] .= ',';
400         }
401
402         $b['postopts'] .= 'statusnet';
403 }
404
405 function statusnet_action(App $a, $uid, $pid, $action)
406 {
407         $api = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
408         $ckey = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
409         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
410         $otoken = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
411         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
412
413         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
414
415         Logger::debug('statusnet_action "' . $action . '" ID: ' . $pid);
416
417         switch ($action) {
418                 case 'delete':
419                         $result = $connection->post('statuses/destroy/' . $pid);
420                         break;
421
422                 case 'like':
423                         $result = $connection->post('favorites/create/' . $pid);
424                         break;
425
426                 case 'unlike':
427                         $result = $connection->post('favorites/destroy/' . $pid);
428                         break;
429         }
430         Logger::info('statusnet_action "' . $action . '" send, result: ' . print_r($result, true));
431 }
432
433 function statusnet_post_hook(App $a, array &$b)
434 {
435         /**
436          * Post to GNU Social
437          */
438         if (!DI::pConfig()->get($b['uid'], 'statusnet', 'import')) {
439                 if ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))
440                         return;
441         }
442
443         $b['body'] = Post\Media::addAttachmentsToBody($b['uri-id'], DI::contentItem()->addSharedPost($b));
444
445         $api = DI::pConfig()->get($b['uid'], 'statusnet', 'baseapi');
446         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $api);
447
448         if ($b['parent'] != $b['id']) {
449                 Logger::debug('statusnet_post_hook: parameter ', ['b' => $b]);
450
451                 // Looking if its a reply to a GNU Social post
452                 $hostlength = strlen($hostname) + 2;
453                 if ((substr($b['parent-uri'], 0, $hostlength) != $hostname . '::') && (substr($b['extid'], 0, $hostlength) != $hostname . '::') && (substr($b['thr-parent'], 0, $hostlength) != $hostname . '::')) {
454                         Logger::notice('statusnet_post_hook: no GNU Social post ' . $b['parent']);
455                         return;
456                 }
457
458                 $condition = ['uri' => $b['thr-parent'], 'uid' => $b['uid']];
459                 $orig_post = Post::selectFirst(['author-link', 'uri'], $condition);
460                 if (!DBA::isResult($orig_post)) {
461                         Logger::notice('statusnet_post_hook: no parent found ' . $b['thr-parent']);
462                         return;
463                 } else {
464                         $iscomment = true;
465                 }
466
467                 $nick = preg_replace("=https?://(.*)/(.*)=ism", "$2", $orig_post['author-link']);
468
469                 $nickname = '@[url=' . $orig_post['author-link'] . ']' . $nick . '[/url]';
470                 $nicknameplain = '@' . $nick;
471
472                 Logger::info('statusnet_post_hook: comparing ' . $nickname . ' and ' . $nicknameplain . ' with ' . $b['body']);
473                 if ((strpos($b['body'], $nickname) === false) && (strpos($b['body'], $nicknameplain) === false)) {
474                         $b['body'] = $nickname . ' ' . $b['body'];
475                 }
476
477                 Logger::info('statusnet_post_hook: parent found ', ['orig_post' => $orig_post]);
478         } else {
479                 $iscomment = false;
480
481                 if ($b['private'] || !strstr($b['postopts'], 'statusnet')) {
482                         return;
483                 }
484
485                 // Dont't post if the post doesn't belong to us.
486                 // This is a check for forum postings
487                 $self = DBA::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
488                 if ($b['contact-id'] != $self['id']) {
489                         return;
490                 }
491         }
492
493         if (($b['verb'] == Activity::POST) && $b['deleted']) {
494                 statusnet_action($a, $b['uid'], substr($orig_post['uri'], $hostlength), 'delete');
495         }
496
497         if ($b['verb'] == Activity::LIKE) {
498                 Logger::info('statusnet_post_hook: parameter 2 ' . substr($b['thr-parent'], $hostlength));
499                 if ($b['deleted'])
500                         statusnet_action($a, $b['uid'], substr($b['thr-parent'], $hostlength), 'unlike');
501                 else
502                         statusnet_action($a, $b['uid'], substr($b['thr-parent'], $hostlength), 'like');
503                 return;
504         }
505
506         if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
507                 return;
508         }
509
510         // if posts comes from GNU Social don't send it back
511         if ($b['extid'] == Protocol::STATUSNET) {
512                 return;
513         }
514
515         if ($b['app'] == 'StatusNet') {
516                 return;
517         }
518
519         Logger::notice('GNU Socialpost invoked');
520
521         DI::pConfig()->load($b['uid'], 'statusnet');
522
523         $api     = DI::pConfig()->get($b['uid'], 'statusnet', 'baseapi');
524         $ckey    = DI::pConfig()->get($b['uid'], 'statusnet', 'consumerkey');
525         $csecret = DI::pConfig()->get($b['uid'], 'statusnet', 'consumersecret');
526         $otoken  = DI::pConfig()->get($b['uid'], 'statusnet', 'oauthtoken');
527         $osecret = DI::pConfig()->get($b['uid'], 'statusnet', 'oauthsecret');
528
529         if ($ckey && $csecret && $otoken && $osecret) {
530                 // If it's a repeated message from GNU Social then do a native retweet and exit
531                 if (statusnet_is_retweet($a, $b['uid'], $b['body'])) {
532                         return;
533                 }
534
535                 $dent = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
536                 $max_char = $dent->get_maxlength(); // max. length for a dent
537
538                 DI::pConfig()->set($b['uid'], 'statusnet', 'max_char', $max_char);
539
540                 $tempfile = '';
541                 $msgarr = Plaintext::getPost($b, $max_char, true, 7);
542                 $msg = $msgarr['text'];
543
544                 if (($msg == '') && isset($msgarr['title']))
545                         $msg = Plaintext::shorten($msgarr['title'], $max_char - 50, $b['uid']);
546
547                 $image = '';
548
549                 if (isset($msgarr['url']) && ($msgarr['type'] != 'photo')) {
550                         $msg .= " \n" . $msgarr['url'];
551                 } elseif (isset($msgarr['image']) && ($msgarr['type'] != 'video')) {
552                         $image = $msgarr['image'];
553                 }
554
555                 if ($image != '') {
556                         $img_str = DI::httpClient()->fetch($image);
557                         $tempfile = tempnam(System::getTempPath(), 'cache');
558                         file_put_contents($tempfile, $img_str);
559                         $postdata = ['status' => $msg, 'media[]' => $tempfile];
560                 } else {
561                         $postdata = ['status' => $msg];
562                 }
563
564                 // and now send it :-)
565                 if (strlen($msg)) {
566                         if ($iscomment) {
567                                 $postdata['in_reply_to_status_id'] = substr($orig_post['uri'], $hostlength);
568                                 Logger::info('statusnet_post send reply ' . print_r($postdata, true));
569                         }
570
571                         // New code that is able to post pictures
572                         require_once __DIR__ . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'codebirdsn.php';
573                         $cb = CodebirdSN::getInstance();
574                         $cb->setAPIEndpoint($api);
575                         $cb->setConsumerKey($ckey, $csecret);
576                         $cb->setToken($otoken, $osecret);
577                         $result = $cb->statuses_update($postdata);
578                         //$result = $dent->post('statuses/update', $postdata);
579                         Logger::info('statusnet_post send, result: ' . print_r($result, true) .
580                                 "\nmessage: " . $msg . "\nOriginal post: " . print_r($b, true) . "\nPost Data: " . print_r($postdata, true));
581
582                         if (!empty($result->source)) {
583                                 DI::pConfig()->set($b['uid'], 'statusnet', 'application_name', strip_tags($result->source));
584                         }
585
586                         if (!empty($result->error)) {
587                                 Logger::notice('Send to GNU Social failed: "' . $result->error . '"');
588                         } elseif ($iscomment) {
589                                 Logger::notice('statusnet_post: Update extid ' . $result->id . ' for post id ' . $b['id']);
590                                 Item::update(['extid' => $hostname . '::' . $result->id, 'body' => $result->text], ['id' => $b['id']]);
591                         }
592                 }
593                 if ($tempfile != '') {
594                         unlink($tempfile);
595                 }
596         }
597 }
598
599 function statusnet_addon_admin_post(App $a)
600 {
601         $sites = [];
602
603         foreach ($_POST['sitename'] as $id => $sitename) {
604                 $sitename = trim($sitename);
605                 $apiurl = trim($_POST['apiurl'][$id]);
606                 if (!(substr($apiurl, -1) == '/')) {
607                         $apiurl = $apiurl . '/';
608                 }
609                 $secret = trim($_POST['secret'][$id]);
610                 $key = trim($_POST['key'][$id]);
611                 //$applicationname = (!empty($_POST['applicationname']) ? Strings::escapeTags(trim($_POST['applicationname'][$id])):'');
612                 if ($sitename != '' &&
613                         $apiurl != '' &&
614                         $secret != '' &&
615                         $key != '' &&
616                         empty($_POST['delete'][$id])) {
617
618                         $sites[] = [
619                                 'sitename' => $sitename,
620                                 'apiurl' => $apiurl,
621                                 'consumersecret' => $secret,
622                                 'consumerkey' => $key,
623                                 //'applicationname' => $applicationname
624                         ];
625                 }
626         }
627
628         $sites = DI::config()->set('statusnet', 'sites', $sites);
629 }
630
631 function statusnet_addon_admin(App $a, string &$o)
632 {
633         $sites = DI::config()->get('statusnet', 'sites');
634         $sitesform = [];
635         if (is_array($sites)) {
636                 foreach ($sites as $id => $s) {
637                         $sitesform[] = [
638                                 'sitename' => ["sitename[$id]", "Site name", $s['sitename'], ""],
639                                 'apiurl' => ["apiurl[$id]", "Api url", $s['apiurl'], DI::l10n()->t("Base API Path \x28remember the trailing /\x29")],
640                                 'secret' => ["secret[$id]", "Secret", $s['consumersecret'], ""],
641                                 'key' => ["key[$id]", "Key", $s['consumerkey'], ""],
642                                 //'applicationname' => Array("applicationname[$id]", "Application name", $s['applicationname'], ""),
643                                 'delete' => ["delete[$id]", "Delete", False, "Check to delete this preset"],
644                         ];
645                 }
646         }
647         /* empty form to add new site */
648         $id = count($sitesform);
649         $sitesform[] = [
650                 'sitename' => ["sitename[$id]", DI::l10n()->t("Site name"), "", ""],
651                 'apiurl' => ["apiurl[$id]", "Api url", "", DI::l10n()->t("Base API Path \x28remember the trailing /\x29")],
652                 'secret' => ["secret[$id]", DI::l10n()->t("Consumer Secret"), "", ""],
653                 'key' => ["key[$id]", DI::l10n()->t("Consumer Key"), "", ""],
654                 //'applicationname' => Array("applicationname[$id]", DI::l10n()->t("Application name"), "", ""),
655         ];
656
657         $t = Renderer::getMarkupTemplate('admin.tpl', 'addon/statusnet/');
658         $o = Renderer::replaceMacros($t, [
659                 '$submit' => DI::l10n()->t('Save Settings'),
660                 '$sites' => $sitesform,
661         ]);
662 }
663
664 function statusnet_prepare_body(App $a, array &$b)
665 {
666         if ($b['item']['network'] != Protocol::STATUSNET) {
667                 return;
668         }
669
670         if ($b['preview']) {
671                 $max_char = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'statusnet', 'max_char');
672                 if (intval($max_char) == 0) {
673                         $max_char = 140;
674                 }
675
676                 $item = $b['item'];
677                 $item['plink'] = DI::baseUrl()->get() . '/display/' . $item['guid'];
678
679                 $condition = ['uri' => $item['thr-parent'], 'uid' => DI::userSession()->getLocalUserId()];
680                 $orig_post = Post::selectFirst(['author-link', 'uri'], $condition);
681                 if (DBA::isResult($orig_post)) {
682                         $nick = preg_replace("=https?://(.*)/(.*)=ism", "$2", $orig_post['author-link']);
683
684                         $nickname = '@[url=' . $orig_post['author-link'] . ']' . $nick . '[/url]';
685                         $nicknameplain = '@' . $nick;
686
687                         if ((strpos($item['body'], $nickname) === false) && (strpos($item['body'], $nicknameplain) === false)) {
688                                 $item['body'] = $nickname . ' ' . $item['body'];
689                         }
690                 }
691
692                 $msgarr = Plaintext::getPost($item, $max_char, true, 7);
693                 $msg = $msgarr['text'];
694
695                 if (isset($msgarr['url']) && ($msgarr['type'] != 'photo')) {
696                         $msg .= ' ' . $msgarr['url'];
697                 }
698
699                 if (isset($msgarr['image'])) {
700                         $msg .= ' ' . $msgarr['image'];
701                 }
702
703                 $b['html'] = nl2br(htmlspecialchars($msg));
704         }
705 }
706
707 function statusnet_cron(App $a, $b)
708 {
709         $last = DI::config()->get('statusnet', 'last_poll');
710
711         $poll_interval = intval(DI::config()->get('statusnet', 'poll_interval'));
712         if (!$poll_interval) {
713                 $poll_interval = STATUSNET_DEFAULT_POLL_INTERVAL;
714         }
715
716         if ($last) {
717                 $next = $last + ($poll_interval * 60);
718                 if ($next > time()) {
719                         Logger::notice('statusnet: poll intervall not reached');
720                         return;
721                 }
722         }
723         Logger::notice('statusnet: cron_start');
724
725         $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'statusnet', 'k' => 'mirror_posts', 'v' => true]);
726         foreach ($pconfigs as $rr) {
727                 Logger::notice('statusnet: fetching for user ' . $rr['uid']);
728                 statusnet_fetchtimeline($a, $rr['uid']);
729         }
730
731         $abandon_days = intval(DI::config()->get('system', 'account_abandon_days'));
732         if ($abandon_days < 1) {
733                 $abandon_days = 0;
734         }
735
736         $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
737
738         $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'statusnet', 'k' => 'import', 'v' => true]);
739         foreach ($pconfigs as $rr) {
740                 if ($abandon_days != 0) {
741                         if (!DBA::exists('user', ["`uid` = ? AND `login_date` >= ?", $rr['uid'], $abandon_limit])) {
742                                 Logger::notice('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
743                                 continue;
744                         }
745                 }
746
747                 Logger::notice('statusnet: importing timeline from user ' . $rr['uid']);
748                 statusnet_fetchhometimeline($a, $rr['uid'], $rr['v']);
749         }
750
751         Logger::notice('statusnet: cron_end');
752
753         DI::config()->set('statusnet', 'last_poll', time());
754 }
755
756 function statusnet_fetchtimeline(App $a, int $uid)
757 {
758         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
759         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
760         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
761         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
762         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
763         $lastid  = DI::pConfig()->get($uid, 'statusnet', 'lastid');
764
765         //  get the application name for the SN app
766         //  1st try personal config, then system config and fallback to the
767         //  hostname of the node if neither one is set.
768         $application_name = DI::pConfig()->get($uid, 'statusnet', 'application_name');
769         if ($application_name == '') {
770                 $application_name = DI::config()->get('statusnet', 'application_name');
771         }
772         if ($application_name == '') {
773                 $application_name = DI::baseUrl()->getHostname();
774         }
775
776         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
777
778         $parameters = [
779                 'exclude_replies' => true,
780                 'trim_user' => true,
781                 'contributor_details' => false,
782                 'include_rts' => false,
783         ];
784
785         $first_time = ($lastid == '');
786
787         if ($lastid != '') {
788                 $parameters['since_id'] = $lastid;
789         }
790
791         $items = $connection->get('statuses/user_timeline', $parameters);
792
793         if (!is_array($items)) {
794                 return;
795         }
796
797         $posts = array_reverse($items);
798
799         if (count($posts)) {
800                 foreach ($posts as $post) {
801                         if ($post->id > $lastid)
802                                 $lastid = $post->id;
803
804                         if ($first_time) {
805                                 continue;
806                         }
807
808                         if ($post->source == 'activity') {
809                                 continue;
810                         }
811
812                         if (!empty($post->retweeted_status)) {
813                                 continue;
814                         }
815
816                         if ($post->in_reply_to_status_id != '') {
817                                 continue;
818                         }
819
820                         if (!stristr($post->source, $application_name)) {
821                                 $postarray['uid'] = $uid;
822                                 $postarray['app'] = $post->source;
823                                 $postarray['extid'] = Protocol::STATUSNET;
824
825                                 $postarray['title'] = '';
826
827                                 $postarray['body'] = $post->text;
828                                 if (is_string($post->place->name)) {
829                                         $postarray['location'] = $post->place->name;
830                                 }
831
832                                 if (is_string($post->place->full_name)) {
833                                         $postarray['location'] = $post->place->full_name;
834                                 }
835
836                                 if (is_array($post->geo->coordinates)) {
837                                         $postarray['coord'] = $post->geo->coordinates[0] . ' ' . $post->geo->coordinates[1];
838                                 }
839
840                                 if (is_array($post->coordinates->coordinates)) {
841                                         $postarray['coord'] = $post->coordinates->coordinates[1] . ' ' . $post->coordinates->coordinates[0];
842                                 }
843
844                                 if ($postarray['body'] != '') {
845                                         Logger::notice('statusnet: posting for user ' . $uid);
846
847                                         Item::insert($postarray, true);
848                                 }
849                         }
850                 }
851         }
852         DI::pConfig()->set($uid, 'statusnet', 'lastid', $lastid);
853 }
854
855 function statusnet_address($contact)
856 {
857         $hostname = Strings::normaliseLink($contact->statusnet_profile_url);
858         $nickname = $contact->screen_name;
859
860         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $contact->statusnet_profile_url);
861
862         $address = $contact->screen_name . '@' . $hostname;
863
864         return $address;
865 }
866
867 function statusnet_fetch_contact($uid, $contact, $create_user)
868 {
869         if (empty($contact->statusnet_profile_url)) {
870                 return -1;
871         }
872
873         $contact_record = Contact::selectFirst([],
874                 ['alias' => Strings::normaliseLink($contact->statusnet_profile_url), 'uid' => $uid, 'network' => Protocol::STATUSNET]);
875
876         if (!DBA::isResult($contact_record) && !$create_user) {
877                 return 0;
878         }
879
880         if (DBA::isResult($contact_record) && ($contact_record['readonly'] || $contact_record['blocked'])) {
881                 Logger::info('statusnet_fetch_contact: Contact "' . $contact_record['nick'] . '" is blocked or readonly.');
882                 return -1;
883         }
884
885         if (!DBA::isResult($contact_record)) {
886                 $fields = [
887                         'uid'      => $uid,
888                         'created'  => DateTimeFormat::utcNow(),
889                         'url'      => $contact->statusnet_profile_url,
890                         'nurl'     => Strings::normaliseLink($contact->statusnet_profile_url),
891                         'addr'     => statusnet_address($contact),
892                         'alias'    => Strings::normaliseLink($contact->statusnet_profile_url),
893                         'notify'   => '',
894                         'poll'     => '',
895                         'name'     => $contact->name,
896                         'nick'     => $contact->screen_name,
897                         'photo'    => $contact->profile_image_url,
898                         'network'  => Protocol::STATUSNET,
899                         'rel'      => Contact::FRIEND,
900                         'priority' => 1,
901                         'location' => $contact->location,
902                         'about'    => $contact->description,
903                         'writable' => true,
904                         'blocked'  => false,
905                         'readonly' => false,
906                         'pending'  => false,
907                 ];
908
909                 if (!Contact::insert($fields)) {
910                         return false;
911                 }
912
913                 $contact_record = Contact::selectFirst([],
914                         ['alias' => Strings::normaliseLink($contact->statusnet_profile_url), 'uid' => $uid, 'network' => Protocol::STATUSNET]);
915                 if (!DBA::isResult($contact_record)) {
916                         return false;
917                 }
918
919                 $contact_id = $contact_record['id'];
920
921                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
922
923                 $photos = Photo::importProfilePhoto($contact->profile_image_url, $uid, $contact_id);
924
925                 Contact::update(['photo' => $photos[0], 'thumb' => $photos[1],
926                         'micro' => $photos[2], 'avatar-date' => DateTimeFormat::utcNow()], ['id' => $contact_id]);
927         } else {
928                 // update profile photos once every two weeks as we have no notification of when they change.
929                 //$update_photo = (($contact_record['avatar-date'] < DateTimeFormat::convert('now -2 days', '', '', )) ? true : false);
930                 $update_photo = ($contact_record['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
931
932                 // check that we have all the photos, this has been known to fail on occasion
933                 if ((!$contact_record['photo']) || (!$contact_record['thumb']) || (!$contact_record['micro']) || ($update_photo)) {
934                         Logger::info('statusnet_fetch_contact: Updating contact ' . $contact->screen_name);
935
936                         $photos = Photo::importProfilePhoto($contact->profile_image_url, $uid, $contact_record['id']);
937
938                         Contact::update([
939                                 'photo' => $photos[0],
940                                 'thumb' => $photos[1],
941                                 'micro' => $photos[2],
942                                 'name-date' => DateTimeFormat::utcNow(),
943                                 'uri-date' => DateTimeFormat::utcNow(),
944                                 'avatar-date' => DateTimeFormat::utcNow(),
945                                 'url' => $contact->statusnet_profile_url,
946                                 'nurl' => Strings::normaliseLink($contact->statusnet_profile_url),
947                                 'addr' => statusnet_address($contact),
948                                 'name' => $contact->name,
949                                 'nick' => $contact->screen_name,
950                                 'location' => $contact->location,
951                                 'about' => $contact->description
952                         ], ['id' => $contact_record['id']]);
953                 }
954         }
955
956         return $contact_record['id'];
957 }
958
959 function statusnet_fetchuser(App $a, $uid, $screen_name = '', $user_id = '')
960 {
961         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
962         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
963         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
964         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
965         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
966
967         require_once __DIR__ . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'codebirdsn.php';
968
969         $cb = CodebirdSN::getInstance();
970         $cb->setConsumerKey($ckey, $csecret);
971         $cb->setToken($otoken, $osecret);
972
973         $self = Contact::selectFirst([], ['self' => true, 'uid' => $uid]);
974         if (!DBA::isResult($self)) {
975                 return;
976         }
977
978         $parameters = [];
979
980         if ($screen_name != '') {
981                 $parameters['screen_name'] = $screen_name;
982         }
983
984         if ($user_id != '') {
985                 $parameters['user_id'] = $user_id;
986         }
987
988         // Fetching user data
989         $user = $cb->users_show($parameters);
990
991         if (!is_object($user)) {
992                 return;
993         }
994
995         $contact_id = statusnet_fetch_contact($uid, $user, true);
996
997         return $contact_id;
998 }
999
1000 function statusnet_createpost(App $a, int $uid, $post, $self, $create_user, bool $only_existing_contact)
1001 {
1002         Logger::info('statusnet_createpost: start');
1003
1004         $api = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1005         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $api);
1006
1007         $postarray = [];
1008         $postarray['network'] = Protocol::STATUSNET;
1009         $postarray['uid'] = $uid;
1010         $postarray['wall'] = 0;
1011
1012         if (!empty($post->retweeted_status)) {
1013                 $content = $post->retweeted_status;
1014                 statusnet_fetch_contact($uid, $content->user, false);
1015         } else {
1016                 $content = $post;
1017         }
1018
1019         $postarray['uri'] = $hostname . '::' . $content->id;
1020
1021         if (Post::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
1022                 return [];
1023         }
1024
1025         $contactId = 0;
1026
1027         if (!empty($content->in_reply_to_status_id)) {
1028                 $thr_parent = $hostname . '::' . $content->in_reply_to_status_id;
1029
1030                 $item = Post::selectFirst(['uri'], ['uri' => $thr_parent, 'uid' => $uid]);
1031                 if (!DBA::isResult($item)) {
1032                         $item = Post::selectFirst(['uri'], ['extid' => $thr_parent, 'uid' => $uid]);
1033                 }
1034
1035                 if (DBA::isResult($item)) {
1036                         $postarray['thr-parent'] = $item['uri'];
1037                         $postarray['object-type'] = Activity\ObjectType::COMMENT;
1038                 } else {
1039                         $postarray['object-type'] = Activity\ObjectType::NOTE;
1040                 }
1041
1042                 // Is it me?
1043                 $own_url = DI::pConfig()->get($uid, 'statusnet', 'own_url');
1044
1045                 if ($content->user->id == $own_url) {
1046                         $self = DBA::selectFirst('contact', [], ['self' => true, 'uid' => $uid]);
1047                         if (DBA::isResult($self)) {
1048                                 $contactId = $self['id'];
1049
1050                                 $postarray['owner-name'] = $self['name'];
1051                                 $postarray['owner-link'] = $self['url'];
1052                                 $postarray['owner-avatar'] = $self['photo'];
1053                         } else {
1054                                 return [];
1055                         }
1056                 }
1057                 // Don't create accounts of people who just comment something
1058                 $create_user = false;
1059         } else {
1060                 $postarray['object-type'] = Activity\ObjectType::NOTE;
1061         }
1062
1063         if ($contactId == 0) {
1064                 $contactId = statusnet_fetch_contact($uid, $post->user, $create_user);
1065                 $postarray['owner-name'] = $post->user->name;
1066                 $postarray['owner-link'] = $post->user->statusnet_profile_url;
1067                 $postarray['owner-avatar'] = $post->user->profile_image_url;
1068         }
1069         if (($contactId == 0) && !$only_existing_contact) {
1070                 $contactId = $self['id'];
1071         } elseif ($contactId <= 0) {
1072                 return [];
1073         }
1074
1075         $postarray['contact-id'] = $contactId;
1076
1077         $postarray['verb'] = Activity::POST;
1078
1079         $postarray['author-name'] = $content->user->name;
1080         $postarray['author-link'] = $content->user->statusnet_profile_url;
1081         $postarray['author-avatar'] = $content->user->profile_image_url;
1082
1083         // To-Do: Maybe unreliable? Can the api be entered without trailing "/"?
1084         $hostname = str_replace('/api/', '/notice/', DI::pConfig()->get($uid, 'statusnet', 'baseapi'));
1085
1086         $postarray['plink'] = $hostname . $content->id;
1087         $postarray['app'] = strip_tags($content->source);
1088
1089         if ($content->user->protected) {
1090                 $postarray['private'] = 1;
1091                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1092         }
1093
1094         $postarray['body'] = HTML::toBBCode($content->statusnet_html);
1095
1096         $postarray['body'] = statusnet_convertmsg($a, $postarray['body']);
1097
1098         $postarray['created'] = DateTimeFormat::utc($content->created_at);
1099         $postarray['edited'] = DateTimeFormat::utc($content->created_at);
1100
1101         if (!empty($content->place->name)) {
1102                 $postarray['location'] = $content->place->name;
1103         }
1104
1105         if (!empty($content->place->full_name)) {
1106                 $postarray['location'] = $content->place->full_name;
1107         }
1108
1109         if (!empty($content->geo->coordinates)) {
1110                 $postarray['coord'] = $content->geo->coordinates[0] . ' ' . $content->geo->coordinates[1];
1111         }
1112
1113         if (!empty($content->coordinates->coordinates)) {
1114                 $postarray['coord'] = $content->coordinates->coordinates[1] . ' ' . $content->coordinates->coordinates[0];
1115         }
1116
1117         Logger::info('statusnet_createpost: end');
1118
1119         return $postarray;
1120 }
1121
1122 function statusnet_fetchhometimeline(App $a, $uid, $mode = 1)
1123 {
1124         $conversations = [];
1125
1126         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1127         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1128         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1129         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1130         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1131         $create_user = DI::pConfig()->get($uid, 'statusnet', 'create_user');
1132
1133         // "create_user" is deactivated, since currently you cannot add users manually by now
1134         $create_user = true;
1135
1136         Logger::info('statusnet_fetchhometimeline: Fetching for user ' . $uid);
1137
1138         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1139
1140         $own_contact = statusnet_fetch_own_contact($a, $uid);
1141
1142         if (empty($own_contact)) {
1143                 return;
1144         }
1145
1146         $contact = Contact::selectFirst([], ['id' => $own_contact, 'uid' => $uid]);
1147         if (DBA::isResult($contact)) {
1148                 $nick = $contact['nick'];
1149         } else {
1150                 Logger::info('statusnet_fetchhometimeline: Own GNU Social contact not found for user ' . $uid);
1151                 return;
1152         }
1153
1154         $self = Contact::selectFirst([], ['self' => true, 'uid' => $uid]);
1155         if (!DBA::isResult($self)) {
1156                 Logger::info('statusnet_fetchhometimeline: Own contact not found for user ' . $uid);
1157                 return;
1158         }
1159
1160         $user = User::getById($uid);
1161         if (!DBA::isResult($user)) {
1162                 Logger::info('statusnet_fetchhometimeline: Own user not found for user ' . $uid);
1163                 return;
1164         }
1165
1166         $parameters = [
1167                 'exclude_replies' => false,
1168                 'trim_user' => false,
1169                 'contributor_details' => true,
1170                 'include_rts' => true,
1171                 //'count' => 200,
1172         ];
1173
1174         if ($mode == 1) {
1175                 // Fetching timeline
1176                 $lastid = DI::pConfig()->get($uid, 'statusnet', 'lasthometimelineid');
1177                 //$lastid = 1;
1178
1179                 $first_time = ($lastid == '');
1180
1181                 if ($lastid != '') {
1182                         $parameters['since_id'] = $lastid;
1183                 }
1184
1185                 $items = $connection->get('statuses/home_timeline', $parameters);
1186
1187                 if (!is_array($items)) {
1188                         if (is_object($items) && isset($items->error)) {
1189                                 $errormsg = $items->error;
1190                         } elseif (is_object($items)) {
1191                                 $errormsg = print_r($items, true);
1192                         } elseif (is_string($items) || is_float($items) || is_int($items)) {
1193                                 $errormsg = $items;
1194                         } else {
1195                                 $errormsg = 'Unknown error';
1196                         }
1197
1198                         Logger::info('statusnet_fetchhometimeline: Error fetching home timeline: ' . $errormsg);
1199                         return;
1200                 }
1201
1202                 $posts = array_reverse($items);
1203
1204                 Logger::info('statusnet_fetchhometimeline: Fetching timeline for user ' . $uid . ' ' . sizeof($posts) . ' items');
1205
1206                 if (count($posts)) {
1207                         foreach ($posts as $post) {
1208
1209                                 if ($post->id > $lastid) {
1210                                         $lastid = $post->id;
1211                                 }
1212
1213                                 if ($first_time) {
1214                                         continue;
1215                                 }
1216
1217                                 if (isset($post->statusnet_conversation_id)) {
1218                                         if (!isset($conversations[$post->statusnet_conversation_id])) {
1219                                                 statusnet_complete_conversation($a, $uid, $self, $create_user, $nick, $post->statusnet_conversation_id);
1220                                                 $conversations[$post->statusnet_conversation_id] = $post->statusnet_conversation_id;
1221                                         }
1222                                 } else {
1223                                         $postarray = statusnet_createpost($a, $uid, $post, $self, $create_user, true);
1224
1225                                         if (trim($postarray['body']) == '') {
1226                                                 continue;
1227                                         }
1228
1229                                         $item = Item::insert($postarray);
1230                                         $postarray['id'] = $item;
1231
1232                                         Logger::notice('statusnet_fetchhometimeline: User ' . $self['nick'] . ' posted home timeline item ' . $item);
1233                                 }
1234                         }
1235                 }
1236                 DI::pConfig()->set($uid, 'statusnet', 'lasthometimelineid', $lastid);
1237         }
1238
1239         // Fetching mentions
1240         $lastid = DI::pConfig()->get($uid, 'statusnet', 'lastmentionid');
1241         $first_time = ($lastid == '');
1242
1243         if ($lastid != '') {
1244                 $parameters['since_id'] = $lastid;
1245         }
1246
1247         $items = $connection->get('statuses/mentions_timeline', $parameters);
1248
1249         if (!is_array($items)) {
1250                 Logger::info('statusnet_fetchhometimeline: Error fetching mentions: ' . print_r($items, true));
1251                 return;
1252         }
1253
1254         $posts = array_reverse($items);
1255
1256         Logger::info('statusnet_fetchhometimeline: Fetching mentions for user ' . $uid . ' ' . sizeof($posts) . ' items');
1257
1258         if (count($posts)) {
1259                 foreach ($posts as $post) {
1260                         if ($post->id > $lastid) {
1261                                 $lastid = $post->id;
1262                         }
1263
1264                         if ($first_time) {
1265                                 continue;
1266                         }
1267
1268                         $postarray = statusnet_createpost($a, $uid, $post, $self, false, false);
1269
1270                         if (isset($post->statusnet_conversation_id)) {
1271                                 if (!isset($conversations[$post->statusnet_conversation_id])) {
1272                                         statusnet_complete_conversation($a, $uid, $self, $create_user, $nick, $post->statusnet_conversation_id);
1273                                         $conversations[$post->statusnet_conversation_id] = $post->statusnet_conversation_id;
1274                                 }
1275                         } else {
1276                                 if (trim($postarray['body']) == '') {
1277                                         continue;
1278                                 }
1279
1280                                 $item = Item::insert($postarray);
1281
1282                                 Logger::notice('statusnet_fetchhometimeline: User ' . $self['nick'] . ' posted mention timeline item ' . $item);
1283                         }
1284                 }
1285         }
1286
1287         DI::pConfig()->set($uid, 'statusnet', 'lastmentionid', $lastid);
1288 }
1289
1290 function statusnet_complete_conversation(App $a, $uid, $self, $create_user, $nick, $conversation)
1291 {
1292         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1293         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1294         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1295         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1296         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1297         $own_url = DI::pConfig()->get($uid, 'statusnet', 'own_url');
1298
1299         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1300
1301         $parameters['count'] = 200;
1302
1303         $items = $connection->get('statusnet/conversation/' . $conversation, $parameters);
1304         if (is_array($items)) {
1305                 $posts = array_reverse($items);
1306
1307                 foreach ($posts as $post) {
1308                         $postarray = statusnet_createpost($a, $uid, $post, $self, false, false);
1309
1310                         if (empty($postarray['body'])) {
1311                                 continue;
1312                         }
1313
1314                         $item = Item::insert($postarray);
1315                         $postarray['id'] = $item;
1316
1317                         Logger::notice('statusnet_complete_conversation: User ' . $self['nick'] . ' posted home timeline item ' . $item);
1318                 }
1319         }
1320 }
1321
1322 function statusnet_convertmsg(App $a, $body)
1323 {
1324         $body = preg_replace("=\[url\=https?://([0-9]*).([0-9]*).([0-9]*).([0-9]*)/([0-9]*)\](.*?)\[\/url\]=ism", "$1.$2.$3.$4/$5", $body);
1325
1326         $URLSearchString = '^\[\]';
1327         $links = preg_match_all("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $body, $matches, PREG_SET_ORDER);
1328
1329         $footer = $footerurl = $footerlink = $type = '';
1330
1331         if ($links) {
1332                 foreach ($matches AS $match) {
1333                         $search = '[url=' . $match[1] . ']' . $match[2] . '[/url]';
1334
1335                         Logger::info('statusnet_convertmsg: expanding url ' . $match[1]);
1336
1337                         try {
1338                                 $expanded_url = DI::httpClient()->finalUrl($match[1]);
1339                         } catch (TransferException $exception) {
1340                                 Logger::notice('statusnet_convertmsg: Couldn\'t get final URL.', ['url' => $match[1], 'exception' => $exception]);
1341                                 $expanded_url = $match[1];
1342                         }
1343
1344                         Logger::info('statusnet_convertmsg: fetching data for ' . $expanded_url);
1345
1346                         $oembed_data = OEmbed::fetchURL($expanded_url, true);
1347
1348                         Logger::info('statusnet_convertmsg: fetching data: done');
1349
1350                         if ($type == '') {
1351                                 $type = $oembed_data->type;
1352                         }
1353
1354                         if ($oembed_data->type == 'video') {
1355                                 //$body = str_replace($search, '[video]'.$expanded_url.'[/video]', $body);
1356                                 $type = $oembed_data->type;
1357                                 $footerurl = $expanded_url;
1358                                 $footerlink = '[url=' . $expanded_url . ']' . $expanded_url . '[/url]';
1359
1360                                 $body = str_replace($search, $footerlink, $body);
1361                         } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
1362                                 $body = str_replace($search, '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]', $body);
1363                         } elseif ($oembed_data->type != 'link') {
1364                                 $body = str_replace($search, '[url=' . $expanded_url . ']' . $expanded_url . '[/url]', $body);
1365                         } else {
1366                                 $img_str = DI::httpClient()->fetch($expanded_url, HttpClientAccept::DEFAULT, 4);
1367
1368                                 $tempfile = tempnam(System::getTempPath(), 'cache');
1369                                 file_put_contents($tempfile, $img_str);
1370                                 $mime = mime_content_type($tempfile);
1371                                 unlink($tempfile);
1372
1373                                 if (substr($mime, 0, 6) == 'image/') {
1374                                         $type = 'photo';
1375                                         $body = str_replace($search, '[img]' . $expanded_url . '[/img]', $body);
1376                                 } else {
1377                                         $type = $oembed_data->type;
1378                                         $footerurl = $expanded_url;
1379                                         $footerlink = '[url=' . $expanded_url . ']' . $expanded_url . '[/url]';
1380
1381                                         $body = str_replace($search, $footerlink, $body);
1382                                 }
1383                         }
1384                 }
1385
1386                 if ($footerurl != '') {
1387                         $footer = "\n" . PageInfo::getFooterFromUrl($footerurl);
1388                 }
1389
1390                 if (($footerlink != '') && (trim($footer) != '')) {
1391                         $removedlink = trim(str_replace($footerlink, '', $body));
1392
1393                         if (($removedlink == '') || strstr($body, $removedlink)) {
1394                                 $body = $removedlink;
1395                         }
1396
1397                         $body .= $footer;
1398                 }
1399         }
1400
1401         return $body;
1402 }
1403
1404 function statusnet_fetch_own_contact(App $a, int $uid)
1405 {
1406         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1407         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1408         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1409         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1410         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1411         $own_url = DI::pConfig()->get($uid, 'statusnet', 'own_url');
1412
1413         $contact_id = 0;
1414
1415         if ($own_url == '') {
1416                 $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1417
1418                 // Fetching user data
1419                 $user = $connection->get('account/verify_credentials');
1420
1421                 if (empty($user)) {
1422                         return false;
1423                 }
1424
1425                 DI::pConfig()->set($uid, 'statusnet', 'own_url', Strings::normaliseLink($user->statusnet_profile_url));
1426
1427                 $contact_id = statusnet_fetch_contact($uid, $user, true);
1428         } else {
1429                 $contact = Contact::selectFirst([], ['uid' => $uid, 'alias' => $own_url]);
1430                 if (DBA::isResult($contact)) {
1431                         $contact_id = $contact['id'];
1432                 } else {
1433                         DI::pConfig()->delete($uid, 'statusnet', 'own_url');
1434                 }
1435         }
1436         return $contact_id;
1437 }
1438
1439 function statusnet_is_retweet(App $a, int $uid, string $body)
1440 {
1441         $body = trim($body);
1442
1443         // Skip if it isn't a pure repeated messages
1444         // Does it start with a share?
1445         if (strpos($body, '[share') > 0) {
1446                 return false;
1447         }
1448
1449         // Does it end with a share?
1450         if (strlen($body) > (strrpos($body, '[/share]') + 8)) {
1451                 return false;
1452         }
1453
1454         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1455         // Skip if there is no shared message in there
1456         if ($body == $attributes) {
1457                 return false;
1458         }
1459
1460         $link = '';
1461         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1462         if (!empty($matches[1])) {
1463                 $link = $matches[1];
1464         }
1465
1466         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1467         if (!empty($matches[1])) {
1468                 $link = $matches[1];
1469         }
1470
1471         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1472         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1473         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1474         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1475         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1476         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $api);
1477
1478         $id = preg_replace("=https?://" . $hostname . "/notice/(.*)=ism", "$1", $link);
1479
1480         if ($id == $link) {
1481                 return false;
1482         }
1483
1484         Logger::info('statusnet_is_retweet: Retweeting id ' . $id . ' for user ' . $uid);
1485
1486         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1487
1488         $result = $connection->post('statuses/retweet/' . $id);
1489
1490         Logger::info('statusnet_is_retweet: result ' . print_r($result, true));
1491
1492         return isset($result->id);
1493 }