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