]> git.mxchange.org Git - friendica-addons.git/blob - statusnet/statusnet.php
b7d3f372147dd2b14c18637e1a63c30435c35c7f
[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 (!local_user()) {
90                 return;
91         }
92
93         if (DI::pConfig()->get(local_user(), '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(local_user(), 'statusnet', 'post_by_default')
100                         ]
101                 ];
102         }
103 }
104
105 function statusnet_settings_post(App $a, $post)
106 {
107         if (!local_user()) {
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(local_user(), 'statusnet', 'consumerkey');
120                 DI::pConfig()->delete(local_user(), 'statusnet', 'consumersecret');
121                 DI::pConfig()->delete(local_user(), 'statusnet', 'post');
122                 DI::pConfig()->delete(local_user(), 'statusnet', 'post_by_default');
123                 DI::pConfig()->delete(local_user(), 'statusnet', 'oauthtoken');
124                 DI::pConfig()->delete(local_user(), 'statusnet', 'oauthsecret');
125                 DI::pConfig()->delete(local_user(), 'statusnet', 'baseapi');
126                 DI::pConfig()->delete(local_user(), 'statusnet', 'lastid');
127                 DI::pConfig()->delete(local_user(), 'statusnet', 'mirror_posts');
128                 DI::pConfig()->delete(local_user(), 'statusnet', 'import');
129                 DI::pConfig()->delete(local_user(), 'statusnet', 'create_user');
130                 DI::pConfig()->delete(local_user(), '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(local_user(), 'statusnet', 'consumerkey', $asn['consumerkey']);
145                                                 DI::pConfig()->set(local_user(), 'statusnet', 'consumersecret', $asn['consumersecret']);
146                                                 DI::pConfig()->set(local_user(), 'statusnet', 'baseapi', $asn['apiurl']);
147                                                 //DI::pConfig()->set(local_user(), 'statusnet', 'application_name', $asn['applicationname'] );
148                                         } else {
149                                                 notice(DI::l10n()->t('Please contact your site administrator.<br />The provided API URL is not valid.') . EOL . $asn['apiurl'] . EOL);
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(local_user(), 'statusnet', 'consumerkey', $_POST['statusnet-consumerkey']);
163                                         DI::pConfig()->set(local_user(), 'statusnet', 'consumersecret', $_POST['statusnet-consumersecret']);
164                                         DI::pConfig()->set(local_user(), 'statusnet', 'baseapi', $apibase);
165                                         //DI::pConfig()->set(local_user(), '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(local_user(), 'statusnet', 'consumerkey', $_POST['statusnet-consumerkey']);
173                                                 DI::pConfig()->set(local_user(), 'statusnet', 'consumersecret', $_POST['statusnet-consumersecret']);
174                                                 DI::pConfig()->set(local_user(), 'statusnet', 'baseapi', $apibase);
175                                         } else {
176                                                 //  still not the correct API base, let's do noting
177                                                 notice(DI::l10n()->t('We could not contact the GNU Social API with the Path you entered.') . EOL);
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(local_user(), 'statusnet', 'baseapi');
184                                         $ckey = DI::pConfig()->get(local_user(), 'statusnet', 'consumerkey');
185                                         $csecret = DI::pConfig()->get(local_user(), '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(local_user(), 'statusnet', 'oauthtoken', $token['oauth_token']);
193                                         DI::pConfig()->set(local_user(), 'statusnet', 'oauthsecret', $token['oauth_token_secret']);
194                                         DI::pConfig()->set(local_user(), 'statusnet', 'post', 1);
195                                         DI::pConfig()->set(local_user(), '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(local_user(), 'statusnet', 'post', intval($_POST['statusnet-enable']));
201                                         DI::pConfig()->set(local_user(), 'statusnet', 'post_by_default', intval($_POST['statusnet-default']));
202                                         DI::pConfig()->set(local_user(), 'statusnet', 'mirror_posts', intval($_POST['statusnet-mirror']));
203                                         DI::pConfig()->set(local_user(), 'statusnet', 'import', intval($_POST['statusnet-import']));
204                                         DI::pConfig()->set(local_user(), 'statusnet', 'create_user', intval($_POST['statusnet-create_user']));
205
206                                         if (!intval($_POST['statusnet-mirror']))
207                                                 DI::pConfig()->delete(local_user(), 'statusnet', 'lastid');
208                                 }
209                         }
210                 }
211         }
212 }
213
214 function statusnet_settings(App $a, array &$data)
215 {
216         if (!local_user()) {
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(local_user(), 'statusnet', 'baseapi');
229         $ckey               = DI::pConfig()->get(local_user(), 'statusnet', 'consumerkey');
230         $csecret            = DI::pConfig()->get(local_user(), 'statusnet', 'consumersecret');
231         $otoken             = DI::pConfig()->get(local_user(), 'statusnet', 'oauthtoken');
232         $osecret            = DI::pConfig()->get(local_user(), 'statusnet', 'oauthsecret');
233         $enabled            = DI::pConfig()->get(local_user(), 'statusnet', 'post', false);
234         $def_enabled        = DI::pConfig()->get(local_user(), 'statusnet', 'post_by_default', false);
235         $mirror_enabled     = DI::pConfig()->get(local_user(), 'statusnet', 'mirror_posts', false);
236         $createuser_enabled = DI::pConfig()->get(local_user(), 'statusnet', 'create_user', false);
237         $import             = DI::pConfig()->get(local_user(), '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(local_user());
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, &$b)
377 {
378         if ($b['edit']) {
379                 return;
380         }
381
382         if (!local_user() || (local_user() != $b['uid'])) {
383                 return;
384         }
385
386         $statusnet_post = DI::pConfig()->get(local_user(), '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(local_user(), '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, &$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'], $b['body']);
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, &$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, &$b)
665 {
666         if ($b['item']['network'] != Protocol::STATUSNET) {
667                 return;
668         }
669
670         if ($b['preview']) {
671                 $max_char = DI::pConfig()->get(local_user(), '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' => local_user()];
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, $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         require_once 'mod/item.php';
766         //  get the application name for the SN app
767         //  1st try personal config, then system config and fallback to the
768         //  hostname of the node if neither one is set.
769         $application_name = DI::pConfig()->get($uid, 'statusnet', 'application_name');
770         if ($application_name == '') {
771                 $application_name = DI::config()->get('statusnet', 'application_name');
772         }
773         if ($application_name == '') {
774                 $application_name = DI::baseUrl()->getHostname();
775         }
776
777         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
778
779         $parameters = [
780                 'exclude_replies' => true,
781                 'trim_user' => true,
782                 'contributor_details' => false,
783                 'include_rts' => false,
784         ];
785
786         $first_time = ($lastid == '');
787
788         if ($lastid != '') {
789                 $parameters['since_id'] = $lastid;
790         }
791
792         $items = $connection->get('statuses/user_timeline', $parameters);
793
794         if (!is_array($items)) {
795                 return;
796         }
797
798         $posts = array_reverse($items);
799
800         if (count($posts)) {
801                 foreach ($posts as $post) {
802                         if ($post->id > $lastid)
803                                 $lastid = $post->id;
804
805                         if ($first_time) {
806                                 continue;
807                         }
808
809                         if ($post->source == 'activity') {
810                                 continue;
811                         }
812
813                         if (!empty($post->retweeted_status)) {
814                                 continue;
815                         }
816
817                         if ($post->in_reply_to_status_id != '') {
818                                 continue;
819                         }
820
821                         if (!stristr($post->source, $application_name)) {
822                                 $_SESSION['authenticated'] = true;
823                                 $_SESSION['uid'] = $uid;
824
825                                 unset($_REQUEST);
826                                 $_REQUEST['api_source'] = true;
827                                 $_REQUEST['profile_uid'] = $uid;
828                                 //$_REQUEST['source'] = 'StatusNet';
829                                 $_REQUEST['source'] = $post->source;
830                                 $_REQUEST['extid'] = Protocol::STATUSNET;
831
832                                 if (isset($post->id)) {
833                                         $_REQUEST['message_id'] = Item::newURI($uid, Protocol::STATUSNET . ':' . $post->id);
834                                 }
835
836                                 //$_REQUEST['date'] = $post->created_at;
837
838                                 $_REQUEST['title'] = '';
839
840                                 $_REQUEST['body'] = $post->text;
841                                 if (is_string($post->place->name)) {
842                                         $_REQUEST['location'] = $post->place->name;
843                                 }
844
845                                 if (is_string($post->place->full_name)) {
846                                         $_REQUEST['location'] = $post->place->full_name;
847                                 }
848
849                                 if (is_array($post->geo->coordinates)) {
850                                         $_REQUEST['coord'] = $post->geo->coordinates[0] . ' ' . $post->geo->coordinates[1];
851                                 }
852
853                                 if (is_array($post->coordinates->coordinates)) {
854                                         $_REQUEST['coord'] = $post->coordinates->coordinates[1] . ' ' . $post->coordinates->coordinates[0];
855                                 }
856
857                                 //print_r($_REQUEST);
858                                 if ($_REQUEST['body'] != '') {
859                                         Logger::notice('statusnet: posting for user ' . $uid);
860
861                                         item_post($a);
862                                 }
863                         }
864                 }
865         }
866         DI::pConfig()->set($uid, 'statusnet', 'lastid', $lastid);
867 }
868
869 function statusnet_address($contact)
870 {
871         $hostname = Strings::normaliseLink($contact->statusnet_profile_url);
872         $nickname = $contact->screen_name;
873
874         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $contact->statusnet_profile_url);
875
876         $address = $contact->screen_name . '@' . $hostname;
877
878         return $address;
879 }
880
881 function statusnet_fetch_contact($uid, $contact, $create_user)
882 {
883         if (empty($contact->statusnet_profile_url)) {
884                 return -1;
885         }
886
887         $contact_record = Contact::selectFirst([],
888                 ['alias' => Strings::normaliseLink($contact->statusnet_profile_url), 'uid' => $uid, 'network' => Protocol::STATUSNET]);
889
890         if (!DBA::isResult($contact_record) && !$create_user) {
891                 return 0;
892         }
893
894         if (DBA::isResult($contact_record) && ($contact_record['readonly'] || $contact_record['blocked'])) {
895                 Logger::info('statusnet_fetch_contact: Contact "' . $contact_record['nick'] . '" is blocked or readonly.');
896                 return -1;
897         }
898
899         if (!DBA::isResult($contact_record)) {
900                 $fields = [
901                         'uid'      => $uid,
902                         'created'  => DateTimeFormat::utcNow(),
903                         'url'      => $contact->statusnet_profile_url,
904                         'nurl'     => Strings::normaliseLink($contact->statusnet_profile_url),
905                         'addr'     => statusnet_address($contact),
906                         'alias'    => Strings::normaliseLink($contact->statusnet_profile_url),
907                         'notify'   => '',
908                         'poll'     => '',
909                         'name'     => $contact->name,
910                         'nick'     => $contact->screen_name,
911                         'photo'    => $contact->profile_image_url,
912                         'network'  => Protocol::STATUSNET,
913                         'rel'      => Contact::FRIEND,
914                         'priority' => 1,
915                         'location' => $contact->location,
916                         'about'    => $contact->description,
917                         'writable' => true,
918                         'blocked'  => false,
919                         'readonly' => false,
920                         'pending'  => false,
921                 ];
922
923                 if (!Contact::insert($fields)) {
924                         return false;
925                 }
926
927                 $contact_record = Contact::selectFirst([],
928                         ['alias' => Strings::normaliseLink($contact->statusnet_profile_url), 'uid' => $uid, 'network' => Protocol::STATUSNET]);
929                 if (!DBA::isResult($contact_record)) {
930                         return false;
931                 }
932
933                 $contact_id = $contact_record['id'];
934
935                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
936
937                 $photos = Photo::importProfilePhoto($contact->profile_image_url, $uid, $contact_id);
938
939                 Contact::update(['photo' => $photos[0], 'thumb' => $photos[1],
940                         'micro' => $photos[2], 'avatar-date' => DateTimeFormat::utcNow()], ['id' => $contact_id]);
941         } else {
942                 // update profile photos once every two weeks as we have no notification of when they change.
943                 //$update_photo = (($contact_record['avatar-date'] < DateTimeFormat::convert('now -2 days', '', '', )) ? true : false);
944                 $update_photo = ($contact_record['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
945
946                 // check that we have all the photos, this has been known to fail on occasion
947                 if ((!$contact_record['photo']) || (!$contact_record['thumb']) || (!$contact_record['micro']) || ($update_photo)) {
948                         Logger::info('statusnet_fetch_contact: Updating contact ' . $contact->screen_name);
949
950                         $photos = Photo::importProfilePhoto($contact->profile_image_url, $uid, $contact_record['id']);
951
952                         Contact::update([
953                                 'photo' => $photos[0],
954                                 'thumb' => $photos[1],
955                                 'micro' => $photos[2],
956                                 'name-date' => DateTimeFormat::utcNow(),
957                                 'uri-date' => DateTimeFormat::utcNow(),
958                                 'avatar-date' => DateTimeFormat::utcNow(),
959                                 'url' => $contact->statusnet_profile_url,
960                                 'nurl' => Strings::normaliseLink($contact->statusnet_profile_url),
961                                 'addr' => statusnet_address($contact),
962                                 'name' => $contact->name,
963                                 'nick' => $contact->screen_name,
964                                 'location' => $contact->location,
965                                 'about' => $contact->description
966                         ], ['id' => $contact_record['id']]);
967                 }
968         }
969
970         return $contact_record['id'];
971 }
972
973 function statusnet_fetchuser(App $a, $uid, $screen_name = '', $user_id = '')
974 {
975         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
976         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
977         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
978         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
979         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
980
981         require_once __DIR__ . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'codebirdsn.php';
982
983         $cb = CodebirdSN::getInstance();
984         $cb->setConsumerKey($ckey, $csecret);
985         $cb->setToken($otoken, $osecret);
986
987         $self = Contact::selectFirst([], ['self' => true, 'uid' => $uid]);
988         if (!DBA::isResult($self)) {
989                 return;
990         }
991
992         $parameters = [];
993
994         if ($screen_name != '') {
995                 $parameters['screen_name'] = $screen_name;
996         }
997
998         if ($user_id != '') {
999                 $parameters['user_id'] = $user_id;
1000         }
1001
1002         // Fetching user data
1003         $user = $cb->users_show($parameters);
1004
1005         if (!is_object($user)) {
1006                 return;
1007         }
1008
1009         $contact_id = statusnet_fetch_contact($uid, $user, true);
1010
1011         return $contact_id;
1012 }
1013
1014 function statusnet_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact)
1015 {
1016         Logger::info('statusnet_createpost: start');
1017
1018         $api = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1019         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $api);
1020
1021         $postarray = [];
1022         $postarray['network'] = Protocol::STATUSNET;
1023         $postarray['uid'] = $uid;
1024         $postarray['wall'] = 0;
1025
1026         if (!empty($post->retweeted_status)) {
1027                 $content = $post->retweeted_status;
1028                 statusnet_fetch_contact($uid, $content->user, false);
1029         } else {
1030                 $content = $post;
1031         }
1032
1033         $postarray['uri'] = $hostname . '::' . $content->id;
1034
1035         if (Post::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
1036                 return [];
1037         }
1038
1039         $contactid = 0;
1040
1041         if (!empty($content->in_reply_to_status_id)) {
1042                 $thr_parent = $hostname . '::' . $content->in_reply_to_status_id;
1043
1044                 $item = Post::selectFirst(['uri'], ['uri' => $thr_parent, 'uid' => $uid]);
1045                 if (!DBA::isResult($item)) {
1046                         $item = Post::selectFirst(['uri'], ['extid' => $thr_parent, 'uid' => $uid]);
1047                 }
1048
1049                 if (DBA::isResult($item)) {
1050                         $postarray['thr-parent'] = $item['uri'];
1051                         $postarray['object-type'] = Activity\ObjectType::COMMENT;
1052                 } else {
1053                         $postarray['object-type'] = Activity\ObjectType::NOTE;
1054                 }
1055
1056                 // Is it me?
1057                 $own_url = DI::pConfig()->get($uid, 'statusnet', 'own_url');
1058
1059                 if ($content->user->id == $own_url) {
1060                         $self = DBA::selectFirst('*', ['self' => true, 'uid' => $uid]);
1061                         if (DBA::isResult($self)) {
1062                                 $contactid = $self['id'];
1063
1064                                 $postarray['owner-name'] = $self['name'];
1065                                 $postarray['owner-link'] = $self['url'];
1066                                 $postarray['owner-avatar'] = $self['photo'];
1067                         } else {
1068                                 return [];
1069                         }
1070                 }
1071                 // Don't create accounts of people who just comment something
1072                 $create_user = false;
1073         } else {
1074                 $postarray['object-type'] = Activity\ObjectType::NOTE;
1075         }
1076
1077         if ($contactid == 0) {
1078                 $contactid = statusnet_fetch_contact($uid, $post->user, $create_user);
1079                 $postarray['owner-name'] = $post->user->name;
1080                 $postarray['owner-link'] = $post->user->statusnet_profile_url;
1081                 $postarray['owner-avatar'] = $post->user->profile_image_url;
1082         }
1083         if (($contactid == 0) && !$only_existing_contact) {
1084                 $contactid = $self['id'];
1085         } elseif ($contactid <= 0) {
1086                 return [];
1087         }
1088
1089         $postarray['contact-id'] = $contactid;
1090
1091         $postarray['verb'] = Activity::POST;
1092
1093         $postarray['author-name'] = $content->user->name;
1094         $postarray['author-link'] = $content->user->statusnet_profile_url;
1095         $postarray['author-avatar'] = $content->user->profile_image_url;
1096
1097         // To-Do: Maybe unreliable? Can the api be entered without trailing "/"?
1098         $hostname = str_replace('/api/', '/notice/', DI::pConfig()->get($uid, 'statusnet', 'baseapi'));
1099
1100         $postarray['plink'] = $hostname . $content->id;
1101         $postarray['app'] = strip_tags($content->source);
1102
1103         if ($content->user->protected) {
1104                 $postarray['private'] = 1;
1105                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1106         }
1107
1108         $postarray['body'] = HTML::toBBCode($content->statusnet_html);
1109
1110         $postarray['body'] = statusnet_convertmsg($a, $postarray['body']);
1111
1112         $postarray['created'] = DateTimeFormat::utc($content->created_at);
1113         $postarray['edited'] = DateTimeFormat::utc($content->created_at);
1114
1115         if (!empty($content->place->name)) {
1116                 $postarray['location'] = $content->place->name;
1117         }
1118
1119         if (!empty($content->place->full_name)) {
1120                 $postarray['location'] = $content->place->full_name;
1121         }
1122
1123         if (!empty($content->geo->coordinates)) {
1124                 $postarray['coord'] = $content->geo->coordinates[0] . ' ' . $content->geo->coordinates[1];
1125         }
1126
1127         if (!empty($content->coordinates->coordinates)) {
1128                 $postarray['coord'] = $content->coordinates->coordinates[1] . ' ' . $content->coordinates->coordinates[0];
1129         }
1130
1131         Logger::info('statusnet_createpost: end');
1132
1133         return $postarray;
1134 }
1135
1136 function statusnet_fetchhometimeline(App $a, $uid, $mode = 1)
1137 {
1138         $conversations = [];
1139
1140         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1141         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1142         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1143         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1144         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1145         $create_user = DI::pConfig()->get($uid, 'statusnet', 'create_user');
1146
1147         // "create_user" is deactivated, since currently you cannot add users manually by now
1148         $create_user = true;
1149
1150         Logger::info('statusnet_fetchhometimeline: Fetching for user ' . $uid);
1151
1152         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1153
1154         $own_contact = statusnet_fetch_own_contact($a, $uid);
1155
1156         if (empty($own_contact)) {
1157                 return;
1158         }
1159
1160         $contact = Contact::selectFirst([], ['id' => $own_contact, 'uid' => $uid]);
1161         if (DBA::isResult($contact)) {
1162                 $nick = $contact['nick'];
1163         } else {
1164                 Logger::info('statusnet_fetchhometimeline: Own GNU Social contact not found for user ' . $uid);
1165                 return;
1166         }
1167
1168         $self = Contact::selectFirst([], ['self' => true, 'uid' => $uid]);
1169         if (!DBA::isResult($self)) {
1170                 Logger::info('statusnet_fetchhometimeline: Own contact not found for user ' . $uid);
1171                 return;
1172         }
1173
1174         $user = User::getById($uid);
1175         if (!DBA::isResult($user)) {
1176                 Logger::info('statusnet_fetchhometimeline: Own user not found for user ' . $uid);
1177                 return;
1178         }
1179
1180         $parameters = [
1181                 'exclude_replies' => false,
1182                 'trim_user' => false,
1183                 'contributor_details' => true,
1184                 'include_rts' => true,
1185                 //'count' => 200,
1186         ];
1187
1188         if ($mode == 1) {
1189                 // Fetching timeline
1190                 $lastid = DI::pConfig()->get($uid, 'statusnet', 'lasthometimelineid');
1191                 //$lastid = 1;
1192
1193                 $first_time = ($lastid == '');
1194
1195                 if ($lastid != '') {
1196                         $parameters['since_id'] = $lastid;
1197                 }
1198
1199                 $items = $connection->get('statuses/home_timeline', $parameters);
1200
1201                 if (!is_array($items)) {
1202                         if (is_object($items) && isset($items->error)) {
1203                                 $errormsg = $items->error;
1204                         } elseif (is_object($items)) {
1205                                 $errormsg = print_r($items, true);
1206                         } elseif (is_string($items) || is_float($items) || is_int($items)) {
1207                                 $errormsg = $items;
1208                         } else {
1209                                 $errormsg = 'Unknown error';
1210                         }
1211
1212                         Logger::info('statusnet_fetchhometimeline: Error fetching home timeline: ' . $errormsg);
1213                         return;
1214                 }
1215
1216                 $posts = array_reverse($items);
1217
1218                 Logger::info('statusnet_fetchhometimeline: Fetching timeline for user ' . $uid . ' ' . sizeof($posts) . ' items');
1219
1220                 if (count($posts)) {
1221                         foreach ($posts as $post) {
1222
1223                                 if ($post->id > $lastid) {
1224                                         $lastid = $post->id;
1225                                 }
1226
1227                                 if ($first_time) {
1228                                         continue;
1229                                 }
1230
1231                                 if (isset($post->statusnet_conversation_id)) {
1232                                         if (!isset($conversations[$post->statusnet_conversation_id])) {
1233                                                 statusnet_complete_conversation($a, $uid, $self, $create_user, $nick, $post->statusnet_conversation_id);
1234                                                 $conversations[$post->statusnet_conversation_id] = $post->statusnet_conversation_id;
1235                                         }
1236                                 } else {
1237                                         $postarray = statusnet_createpost($a, $uid, $post, $self, $create_user, true);
1238
1239                                         if (trim($postarray['body']) == '') {
1240                                                 continue;
1241                                         }
1242
1243                                         $item = Item::insert($postarray);
1244                                         $postarray['id'] = $item;
1245
1246                                         Logger::notice('statusnet_fetchhometimeline: User ' . $self['nick'] . ' posted home timeline item ' . $item);
1247                                 }
1248                         }
1249                 }
1250                 DI::pConfig()->set($uid, 'statusnet', 'lasthometimelineid', $lastid);
1251         }
1252
1253         // Fetching mentions
1254         $lastid = DI::pConfig()->get($uid, 'statusnet', 'lastmentionid');
1255         $first_time = ($lastid == '');
1256
1257         if ($lastid != '') {
1258                 $parameters['since_id'] = $lastid;
1259         }
1260
1261         $items = $connection->get('statuses/mentions_timeline', $parameters);
1262
1263         if (!is_array($items)) {
1264                 Logger::info('statusnet_fetchhometimeline: Error fetching mentions: ' . print_r($items, true));
1265                 return;
1266         }
1267
1268         $posts = array_reverse($items);
1269
1270         Logger::info('statusnet_fetchhometimeline: Fetching mentions for user ' . $uid . ' ' . sizeof($posts) . ' items');
1271
1272         if (count($posts)) {
1273                 foreach ($posts as $post) {
1274                         if ($post->id > $lastid) {
1275                                 $lastid = $post->id;
1276                         }
1277
1278                         if ($first_time) {
1279                                 continue;
1280                         }
1281
1282                         $postarray = statusnet_createpost($a, $uid, $post, $self, false, false);
1283
1284                         if (isset($post->statusnet_conversation_id)) {
1285                                 if (!isset($conversations[$post->statusnet_conversation_id])) {
1286                                         statusnet_complete_conversation($a, $uid, $self, $create_user, $nick, $post->statusnet_conversation_id);
1287                                         $conversations[$post->statusnet_conversation_id] = $post->statusnet_conversation_id;
1288                                 }
1289                         } else {
1290                                 if (trim($postarray['body']) == '') {
1291                                         continue;
1292                                 }
1293
1294                                 $item = Item::insert($postarray);
1295
1296                                 Logger::notice('statusnet_fetchhometimeline: User ' . $self['nick'] . ' posted mention timeline item ' . $item);
1297                         }
1298                 }
1299         }
1300
1301         DI::pConfig()->set($uid, 'statusnet', 'lastmentionid', $lastid);
1302 }
1303
1304 function statusnet_complete_conversation(App $a, $uid, $self, $create_user, $nick, $conversation)
1305 {
1306         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1307         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1308         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1309         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1310         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1311         $own_url = DI::pConfig()->get($uid, 'statusnet', 'own_url');
1312
1313         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1314
1315         $parameters['count'] = 200;
1316
1317         $items = $connection->get('statusnet/conversation/' . $conversation, $parameters);
1318         if (is_array($items)) {
1319                 $posts = array_reverse($items);
1320
1321                 foreach ($posts as $post) {
1322                         $postarray = statusnet_createpost($a, $uid, $post, $self, false, false);
1323
1324                         if (empty($postarray['body'])) {
1325                                 continue;
1326                         }
1327
1328                         $item = Item::insert($postarray);
1329                         $postarray['id'] = $item;
1330
1331                         Logger::notice('statusnet_complete_conversation: User ' . $self['nick'] . ' posted home timeline item ' . $item);
1332                 }
1333         }
1334 }
1335
1336 function statusnet_convertmsg(App $a, $body)
1337 {
1338         $body = preg_replace("=\[url\=https?://([0-9]*).([0-9]*).([0-9]*).([0-9]*)/([0-9]*)\](.*?)\[\/url\]=ism", "$1.$2.$3.$4/$5", $body);
1339
1340         $URLSearchString = '^\[\]';
1341         $links = preg_match_all("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $body, $matches, PREG_SET_ORDER);
1342
1343         $footer = $footerurl = $footerlink = $type = '';
1344
1345         if ($links) {
1346                 foreach ($matches AS $match) {
1347                         $search = '[url=' . $match[1] . ']' . $match[2] . '[/url]';
1348
1349                         Logger::info('statusnet_convertmsg: expanding url ' . $match[1]);
1350
1351                         try {
1352                                 $expanded_url = DI::httpClient()->finalUrl($match[1]);
1353                         } catch (TransferException $exception) {
1354                                 Logger::notice('statusnet_convertmsg: Couldn\'t get final URL.', ['url' => $match[1], 'exception' => $exception]);
1355                                 $expanded_url = $match[1];
1356                         }
1357
1358                         Logger::info('statusnet_convertmsg: fetching data for ' . $expanded_url);
1359
1360                         $oembed_data = OEmbed::fetchURL($expanded_url, true);
1361
1362                         Logger::info('statusnet_convertmsg: fetching data: done');
1363
1364                         if ($type == '') {
1365                                 $type = $oembed_data->type;
1366                         }
1367
1368                         if ($oembed_data->type == 'video') {
1369                                 //$body = str_replace($search, '[video]'.$expanded_url.'[/video]', $body);
1370                                 $type = $oembed_data->type;
1371                                 $footerurl = $expanded_url;
1372                                 $footerlink = '[url=' . $expanded_url . ']' . $expanded_url . '[/url]';
1373
1374                                 $body = str_replace($search, $footerlink, $body);
1375                         } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
1376                                 $body = str_replace($search, '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]', $body);
1377                         } elseif ($oembed_data->type != 'link') {
1378                                 $body = str_replace($search, '[url=' . $expanded_url . ']' . $expanded_url . '[/url]', $body);
1379                         } else {
1380                                 $img_str = DI::httpClient()->fetch($expanded_url, HttpClientAccept::DEFAULT, 4);
1381
1382                                 $tempfile = tempnam(System::getTempPath(), 'cache');
1383                                 file_put_contents($tempfile, $img_str);
1384                                 $mime = mime_content_type($tempfile);
1385                                 unlink($tempfile);
1386
1387                                 if (substr($mime, 0, 6) == 'image/') {
1388                                         $type = 'photo';
1389                                         $body = str_replace($search, '[img]' . $expanded_url . '[/img]', $body);
1390                                 } else {
1391                                         $type = $oembed_data->type;
1392                                         $footerurl = $expanded_url;
1393                                         $footerlink = '[url=' . $expanded_url . ']' . $expanded_url . '[/url]';
1394
1395                                         $body = str_replace($search, $footerlink, $body);
1396                                 }
1397                         }
1398                 }
1399
1400                 if ($footerurl != '') {
1401                         $footer = "\n" . PageInfo::getFooterFromUrl($footerurl);
1402                 }
1403
1404                 if (($footerlink != '') && (trim($footer) != '')) {
1405                         $removedlink = trim(str_replace($footerlink, '', $body));
1406
1407                         if (($removedlink == '') || strstr($body, $removedlink)) {
1408                                 $body = $removedlink;
1409                         }
1410
1411                         $body .= $footer;
1412                 }
1413         }
1414
1415         return $body;
1416 }
1417
1418 function statusnet_fetch_own_contact(App $a, $uid)
1419 {
1420         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1421         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1422         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1423         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1424         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1425         $own_url = DI::pConfig()->get($uid, 'statusnet', 'own_url');
1426
1427         $contact_id = 0;
1428
1429         if ($own_url == '') {
1430                 $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1431
1432                 // Fetching user data
1433                 $user = $connection->get('account/verify_credentials');
1434
1435                 if (empty($user)) {
1436                         return false;
1437                 }
1438
1439                 DI::pConfig()->set($uid, 'statusnet', 'own_url', Strings::normaliseLink($user->statusnet_profile_url));
1440
1441                 $contact_id = statusnet_fetch_contact($uid, $user, true);
1442         } else {
1443                 $contact = Contact::selectFirst([], ['uid' => $uid, 'alias' => $own_url]);
1444                 if (DBA::isResult($contact)) {
1445                         $contact_id = $contact['id'];
1446                 } else {
1447                         DI::pConfig()->delete($uid, 'statusnet', 'own_url');
1448                 }
1449         }
1450         return $contact_id;
1451 }
1452
1453 function statusnet_is_retweet(App $a, $uid, $body)
1454 {
1455         $body = trim($body);
1456
1457         // Skip if it isn't a pure repeated messages
1458         // Does it start with a share?
1459         if (strpos($body, '[share') > 0) {
1460                 return false;
1461         }
1462
1463         // Does it end with a share?
1464         if (strlen($body) > (strrpos($body, '[/share]') + 8)) {
1465                 return false;
1466         }
1467
1468         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1469         // Skip if there is no shared message in there
1470         if ($body == $attributes) {
1471                 return false;
1472         }
1473
1474         $link = '';
1475         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1476         if (!empty($matches[1])) {
1477                 $link = $matches[1];
1478         }
1479
1480         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1481         if (!empty($matches[1])) {
1482                 $link = $matches[1];
1483         }
1484
1485         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1486         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1487         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1488         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1489         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1490         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $api);
1491
1492         $id = preg_replace("=https?://" . $hostname . "/notice/(.*)=ism", "$1", $link);
1493
1494         if ($id == $link) {
1495                 return false;
1496         }
1497
1498         Logger::info('statusnet_is_retweet: Retweeting id ' . $id . ' for user ' . $uid);
1499
1500         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1501
1502         $result = $connection->post('statuses/retweet/' . $id);
1503
1504         Logger::info('statusnet_is_retweet: result ' . print_r($result, true));
1505
1506         return isset($result->id);
1507 }