]> git.mxchange.org Git - friendica-addons.git/blob - statusnet/statusnet.php
Merge pull request #1248 from tobiasd/20220502-daDK
[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                 case "like":
422                         $result = $connection->post("favorites/create/" . $pid);
423                         break;
424                 case "unlike":
425                         $result = $connection->post("favorites/destroy/" . $pid);
426                         break;
427         }
428         Logger::info("statusnet_action '" . $action . "' send, result: " . print_r($result, true));
429 }
430
431 function statusnet_post_hook(App $a, &$b)
432 {
433         /**
434          * Post to GNU Social
435          */
436         if (!DI::pConfig()->get($b["uid"], 'statusnet', 'import')) {
437                 if ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))
438                         return;
439         }
440
441         $b['body'] = Post\Media::addAttachmentsToBody($b['uri-id'], $b['body']);
442
443         $api = DI::pConfig()->get($b["uid"], 'statusnet', 'baseapi');
444         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $api);
445
446         if ($b['parent'] != $b['id']) {
447                 Logger::debug("statusnet_post_hook: parameter " . print_r($b, true));
448
449                 // Looking if its a reply to a GNU Social post
450                 $hostlength = strlen($hostname) + 2;
451                 if ((substr($b["parent-uri"], 0, $hostlength) != $hostname . "::") && (substr($b["extid"], 0, $hostlength) != $hostname . "::") && (substr($b["thr-parent"], 0, $hostlength) != $hostname . "::")) {
452                         Logger::notice("statusnet_post_hook: no GNU Social post " . $b["parent"]);
453                         return;
454                 }
455
456                 $condition = ['uri' => $b["thr-parent"], 'uid' => $b["uid"]];
457                 $orig_post = Post::selectFirst(['author-link', 'uri'], $condition);
458                 if (!DBA::isResult($orig_post)) {
459                         Logger::notice("statusnet_post_hook: no parent found " . $b["thr-parent"]);
460                         return;
461                 } else {
462                         $iscomment = true;
463                 }
464
465                 $nick = preg_replace("=https?://(.*)/(.*)=ism", "$2", $orig_post["author-link"]);
466
467                 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nick . "[/url]";
468                 $nicknameplain = "@" . $nick;
469
470                 Logger::info("statusnet_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"]);
471                 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
472                         $b["body"] = $nickname . " " . $b["body"];
473                 }
474
475                 Logger::info("statusnet_post_hook: parent found " . print_r($orig_post, true));
476         } else {
477                 $iscomment = false;
478
479                 if ($b['private'] || !strstr($b['postopts'], 'statusnet')) {
480                         return;
481                 }
482
483                 // Dont't post if the post doesn't belong to us.
484                 // This is a check for forum postings
485                 $self = DBA::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
486                 if ($b['contact-id'] != $self['id']) {
487                         return;
488                 }
489         }
490
491         if (($b['verb'] == Activity::POST) && $b['deleted']) {
492                 statusnet_action($a, $b["uid"], substr($orig_post["uri"], $hostlength), "delete");
493         }
494
495         if ($b['verb'] == Activity::LIKE) {
496                 Logger::info("statusnet_post_hook: parameter 2 " . substr($b["thr-parent"], $hostlength));
497                 if ($b['deleted'])
498                         statusnet_action($a, $b["uid"], substr($b["thr-parent"], $hostlength), "unlike");
499                 else
500                         statusnet_action($a, $b["uid"], substr($b["thr-parent"], $hostlength), "like");
501                 return;
502         }
503
504         if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
505                 return;
506         }
507
508         // if posts comes from GNU Social don't send it back
509         if ($b['extid'] == Protocol::STATUSNET) {
510                 return;
511         }
512
513         if ($b['app'] == "StatusNet") {
514                 return;
515         }
516
517         Logger::notice('GNU Socialpost invoked');
518
519         DI::pConfig()->load($b['uid'], 'statusnet');
520
521         $api     = DI::pConfig()->get($b['uid'], 'statusnet', 'baseapi');
522         $ckey    = DI::pConfig()->get($b['uid'], 'statusnet', 'consumerkey');
523         $csecret = DI::pConfig()->get($b['uid'], 'statusnet', 'consumersecret');
524         $otoken  = DI::pConfig()->get($b['uid'], 'statusnet', 'oauthtoken');
525         $osecret = DI::pConfig()->get($b['uid'], 'statusnet', 'oauthsecret');
526
527         if ($ckey && $csecret && $otoken && $osecret) {
528                 // If it's a repeated message from GNU Social then do a native retweet and exit
529                 if (statusnet_is_retweet($a, $b['uid'], $b['body'])) {
530                         return;
531                 }
532
533                 $dent = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
534                 $max_char = $dent->get_maxlength(); // max. length for a dent
535
536                 DI::pConfig()->set($b['uid'], 'statusnet', 'max_char', $max_char);
537
538                 $tempfile = "";
539                 $msgarr = Plaintext::getPost($b, $max_char, true, 7);
540                 $msg = $msgarr["text"];
541
542                 if (($msg == "") && isset($msgarr["title"]))
543                         $msg = Plaintext::shorten($msgarr["title"], $max_char - 50, $b['uid']);
544
545                 $image = "";
546
547                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
548                         $msg .= " \n" . $msgarr["url"];
549                 } elseif (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
550                         $image = $msgarr["image"];
551                 }
552
553                 if ($image != "") {
554                         $img_str = DI::httpClient()->fetch($image);
555                         $tempfile = tempnam(System::getTempPath(), "cache");
556                         file_put_contents($tempfile, $img_str);
557                         $postdata = ["status" => $msg, "media[]" => $tempfile];
558                 } else {
559                         $postdata = ["status" => $msg];
560                 }
561
562                 // and now send it :-)
563                 if (strlen($msg)) {
564                         if ($iscomment) {
565                                 $postdata["in_reply_to_status_id"] = substr($orig_post["uri"], $hostlength);
566                                 Logger::info('statusnet_post send reply ' . print_r($postdata, true));
567                         }
568
569                         // New code that is able to post pictures
570                         require_once __DIR__ . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'codebirdsn.php';
571                         $cb = CodebirdSN::getInstance();
572                         $cb->setAPIEndpoint($api);
573                         $cb->setConsumerKey($ckey, $csecret);
574                         $cb->setToken($otoken, $osecret);
575                         $result = $cb->statuses_update($postdata);
576                         //$result = $dent->post('statuses/update', $postdata);
577                         Logger::info('statusnet_post send, result: ' . print_r($result, true) .
578                                 "\nmessage: " . $msg . "\nOriginal post: " . print_r($b, true) . "\nPost Data: " . print_r($postdata, true));
579
580                         if (!empty($result->source)) {
581                                 DI::pConfig()->set($b["uid"], "statusnet", "application_name", strip_tags($result->source));
582                         }
583
584                         if (!empty($result->error)) {
585                                 Logger::notice('Send to GNU Social failed: "' . $result->error . '"');
586                         } elseif ($iscomment) {
587                                 Logger::notice('statusnet_post: Update extid ' . $result->id . " for post id " . $b['id']);
588                                 Item::update(['extid' => $hostname . "::" . $result->id, 'body' => $result->text], ['id' => $b['id']]);
589                         }
590                 }
591                 if ($tempfile != "") {
592                         unlink($tempfile);
593                 }
594         }
595 }
596
597 function statusnet_addon_admin_post(App $a)
598 {
599         $sites = [];
600
601         foreach ($_POST['sitename'] as $id => $sitename) {
602                 $sitename = trim($sitename);
603                 $apiurl = trim($_POST['apiurl'][$id]);
604                 if (!(substr($apiurl, -1) == '/')) {
605                         $apiurl = $apiurl . '/';
606                 }
607                 $secret = trim($_POST['secret'][$id]);
608                 $key = trim($_POST['key'][$id]);
609                 //$applicationname = (!empty($_POST['applicationname']) ? Strings::escapeTags(trim($_POST['applicationname'][$id])):'');
610                 if ($sitename != "" &&
611                         $apiurl != "" &&
612                         $secret != "" &&
613                         $key != "" &&
614                         empty($_POST['delete'][$id])) {
615
616                         $sites[] = [
617                                 'sitename' => $sitename,
618                                 'apiurl' => $apiurl,
619                                 'consumersecret' => $secret,
620                                 'consumerkey' => $key,
621                                 //'applicationname' => $applicationname
622                         ];
623                 }
624         }
625
626         $sites = DI::config()->set('statusnet', 'sites', $sites);
627 }
628
629 function statusnet_addon_admin(App $a, &$o)
630 {
631         $sites = DI::config()->get('statusnet', 'sites');
632         $sitesform = [];
633         if (is_array($sites)) {
634                 foreach ($sites as $id => $s) {
635                         $sitesform[] = [
636                                 'sitename' => ["sitename[$id]", "Site name", $s['sitename'], ""],
637                                 'apiurl' => ["apiurl[$id]", "Api url", $s['apiurl'], DI::l10n()->t("Base API Path \x28remember the trailing /\x29")],
638                                 'secret' => ["secret[$id]", "Secret", $s['consumersecret'], ""],
639                                 'key' => ["key[$id]", "Key", $s['consumerkey'], ""],
640                                 //'applicationname' => Array("applicationname[$id]", "Application name", $s['applicationname'], ""),
641                                 'delete' => ["delete[$id]", "Delete", False, "Check to delete this preset"],
642                         ];
643                 }
644         }
645         /* empty form to add new site */
646         $id = count($sitesform);
647         $sitesform[] = [
648                 'sitename' => ["sitename[$id]", DI::l10n()->t("Site name"), "", ""],
649                 'apiurl' => ["apiurl[$id]", "Api url", "", DI::l10n()->t("Base API Path \x28remember the trailing /\x29")],
650                 'secret' => ["secret[$id]", DI::l10n()->t("Consumer Secret"), "", ""],
651                 'key' => ["key[$id]", DI::l10n()->t("Consumer Key"), "", ""],
652                 //'applicationname' => Array("applicationname[$id]", DI::l10n()->t("Application name"), "", ""),
653         ];
654
655         $t = Renderer::getMarkupTemplate("admin.tpl", "addon/statusnet/");
656         $o = Renderer::replaceMacros($t, [
657                 '$submit' => DI::l10n()->t('Save Settings'),
658                 '$sites' => $sitesform,
659         ]);
660 }
661
662 function statusnet_prepare_body(App $a, &$b)
663 {
664         if ($b["item"]["network"] != Protocol::STATUSNET) {
665                 return;
666         }
667
668         if ($b["preview"]) {
669                 $max_char = DI::pConfig()->get(local_user(), 'statusnet', 'max_char');
670                 if (intval($max_char) == 0) {
671                         $max_char = 140;
672                 }
673
674                 $item = $b["item"];
675                 $item["plink"] = DI::baseUrl()->get() . "/display/" . $item["guid"];
676
677                 $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
678                 $orig_post = Post::selectFirst(['author-link', 'uri'], $condition);
679                 if (DBA::isResult($orig_post)) {
680                         $nick = preg_replace("=https?://(.*)/(.*)=ism", "$2", $orig_post["author-link"]);
681
682                         $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nick . "[/url]";
683                         $nicknameplain = "@" . $nick;
684
685                         if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
686                                 $item["body"] = $nickname . " " . $item["body"];
687                         }
688                 }
689
690                 $msgarr = Plaintext::getPost($item, $max_char, true, 7);
691                 $msg = $msgarr["text"];
692
693                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
694                         $msg .= " " . $msgarr["url"];
695                 }
696
697                 if (isset($msgarr["image"])) {
698                         $msg .= " " . $msgarr["image"];
699                 }
700
701                 $b['html'] = nl2br(htmlspecialchars($msg));
702         }
703 }
704
705 function statusnet_cron(App $a, $b)
706 {
707         $last = DI::config()->get('statusnet', 'last_poll');
708
709         $poll_interval = intval(DI::config()->get('statusnet', 'poll_interval'));
710         if (!$poll_interval) {
711                 $poll_interval = STATUSNET_DEFAULT_POLL_INTERVAL;
712         }
713
714         if ($last) {
715                 $next = $last + ($poll_interval * 60);
716                 if ($next > time()) {
717                         Logger::notice('statusnet: poll intervall not reached');
718                         return;
719                 }
720         }
721         Logger::notice('statusnet: cron_start');
722
723         $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'statusnet', 'k' => 'mirror_posts', 'v' => true]);
724         foreach ($pconfigs as $rr) {
725                 Logger::notice('statusnet: fetching for user ' . $rr['uid']);
726                 statusnet_fetchtimeline($a, $rr['uid']);
727         }
728
729         $abandon_days = intval(DI::config()->get('system', 'account_abandon_days'));
730         if ($abandon_days < 1) {
731                 $abandon_days = 0;
732         }
733
734         $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
735
736         $pconfigs = DBA::selectToArray('pconfig', [], ['cat' => 'statusnet', 'k' => 'import', 'v' => true]);
737         foreach ($pconfigs as $rr) {
738                 if ($abandon_days != 0) {
739                         if (!DBA::exists('user', ["`uid` = ? AND `login_date` >= ?", $rr['uid'], $abandon_limit])) {
740                                 Logger::notice('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
741                                 continue;
742                         }
743                 }
744
745                 Logger::notice('statusnet: importing timeline from user ' . $rr['uid']);
746                 statusnet_fetchhometimeline($a, $rr["uid"], $rr["v"]);
747         }
748
749         Logger::notice('statusnet: cron_end');
750
751         DI::config()->set('statusnet', 'last_poll', time());
752 }
753
754 function statusnet_fetchtimeline(App $a, $uid)
755 {
756         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
757         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
758         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
759         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
760         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
761         $lastid  = DI::pConfig()->get($uid, 'statusnet', 'lastid');
762
763         require_once 'mod/item.php';
764         //  get the application name for the SN app
765         //  1st try personal config, then system config and fallback to the
766         //  hostname of the node if neither one is set.
767         $application_name = DI::pConfig()->get($uid, 'statusnet', 'application_name');
768         if ($application_name == "") {
769                 $application_name = DI::config()->get('statusnet', 'application_name');
770         }
771         if ($application_name == "") {
772                 $application_name = DI::baseUrl()->getHostname();
773         }
774
775         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
776
777         $parameters = ["exclude_replies" => true, "trim_user" => true, "contributor_details" => false, "include_rts" => false];
778
779         $first_time = ($lastid == "");
780
781         if ($lastid <> "") {
782                 $parameters["since_id"] = $lastid;
783         }
784
785         $items = $connection->get('statuses/user_timeline', $parameters);
786
787         if (!is_array($items)) {
788                 return;
789         }
790
791         $posts = array_reverse($items);
792
793         if (count($posts)) {
794                 foreach ($posts as $post) {
795                         if ($post->id > $lastid)
796                                 $lastid = $post->id;
797
798                         if ($first_time) {
799                                 continue;
800                         }
801
802                         if ($post->source == "activity") {
803                                 continue;
804                         }
805
806                         if (!empty($post->retweeted_status)) {
807                                 continue;
808                         }
809
810                         if ($post->in_reply_to_status_id != "") {
811                                 continue;
812                         }
813
814                         if (!stristr($post->source, $application_name)) {
815                                 $_SESSION["authenticated"] = true;
816                                 $_SESSION["uid"] = $uid;
817
818                                 unset($_REQUEST);
819                                 $_REQUEST["api_source"] = true;
820                                 $_REQUEST["profile_uid"] = $uid;
821                                 //$_REQUEST["source"] = "StatusNet";
822                                 $_REQUEST["source"] = $post->source;
823                                 $_REQUEST["extid"] = Protocol::STATUSNET;
824
825                                 if (isset($post->id)) {
826                                         $_REQUEST['message_id'] = Item::newURI($uid, Protocol::STATUSNET . ":" . $post->id);
827                                 }
828
829                                 //$_REQUEST["date"] = $post->created_at;
830
831                                 $_REQUEST["title"] = "";
832
833                                 $_REQUEST["body"] = $post->text;
834                                 if (is_string($post->place->name)) {
835                                         $_REQUEST["location"] = $post->place->name;
836                                 }
837
838                                 if (is_string($post->place->full_name)) {
839                                         $_REQUEST["location"] = $post->place->full_name;
840                                 }
841
842                                 if (is_array($post->geo->coordinates)) {
843                                         $_REQUEST["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
844                                 }
845
846                                 if (is_array($post->coordinates->coordinates)) {
847                                         $_REQUEST["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
848                                 }
849
850                                 //print_r($_REQUEST);
851                                 if ($_REQUEST["body"] != "") {
852                                         Logger::notice('statusnet: posting for user ' . $uid);
853
854                                         item_post($a);
855                                 }
856                         }
857                 }
858         }
859         DI::pConfig()->set($uid, 'statusnet', 'lastid', $lastid);
860 }
861
862 function statusnet_address($contact)
863 {
864         $hostname = Strings::normaliseLink($contact->statusnet_profile_url);
865         $nickname = $contact->screen_name;
866
867         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $contact->statusnet_profile_url);
868
869         $address = $contact->screen_name . "@" . $hostname;
870
871         return $address;
872 }
873
874 function statusnet_fetch_contact($uid, $contact, $create_user)
875 {
876         if (empty($contact->statusnet_profile_url)) {
877                 return -1;
878         }
879
880         $contact_record = Contact::selectFirst([],
881                 ['alias' => Strings::normaliseLink($contact->statusnet_profile_url), 'uid' => $uid, 'network' => Protocol::STATUSNET]);
882
883         if (!DBA::isResult($contact_record) && !$create_user) {
884                 return 0;
885         }
886
887         if (DBA::isResult($contact_record) && ($contact_record["readonly"] || $contact_record["blocked"])) {
888                 Logger::info("statusnet_fetch_contact: Contact '" . $contact_record["nick"] . "' is blocked or readonly.");
889                 return -1;
890         }
891
892         if (!DBA::isResult($contact_record)) {
893                 $fields = [
894                         'uid'      => $uid,
895                         'created'  => DateTimeFormat::utcNow(),
896                         'url'      => $contact->statusnet_profile_url,
897                         'nurl'     => Strings::normaliseLink($contact->statusnet_profile_url),
898                         'addr'     => statusnet_address($contact),
899                         'alias'    => Strings::normaliseLink($contact->statusnet_profile_url),
900                         'notify'   => '',
901                         'poll'     => '',
902                         'name'     => $contact->name,
903                         'nick'     => $contact->screen_name,
904                         'photo'    => $contact->profile_image_url,
905                         'network'  => Protocol::STATUSNET,
906                         'rel'      => Contact::FRIEND,
907                         'priority' => 1,
908                         'location' => $contact->location,
909                         'about'    => $contact->description,
910                         'writable' => true,
911                         'blocked'  => false,
912                         'readonly' => false,
913                         'pending'  => false,
914                 ];
915
916                 if (!Contact::insert($fields)) {
917                         return false;
918                 }
919
920                 $contact_record = Contact::selectFirst([],
921                         ['alias' => Strings::normaliseLink($contact->statusnet_profile_url), 'uid' => $uid, 'network' => Protocol::STATUSNET]);
922                 if (!DBA::isResult($contact_record)) {
923                         return false;
924                 }
925
926                 $contact_id = $contact_record['id'];
927
928                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
929
930                 $photos = Photo::importProfilePhoto($contact->profile_image_url, $uid, $contact_id);
931
932                 Contact::update(['photo' => $photos[0], 'thumb' => $photos[1],
933                         'micro' => $photos[2], 'avatar-date' => DateTimeFormat::utcNow()], ['id' => $contact_id]);
934         } else {
935                 // update profile photos once every two weeks as we have no notification of when they change.
936                 //$update_photo = (($contact_record['avatar-date'] < DateTimeFormat::convert('now -2 days', '', '', )) ? true : false);
937                 $update_photo = ($contact_record['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
938
939                 // check that we have all the photos, this has been known to fail on occasion
940                 if ((!$contact_record['photo']) || (!$contact_record['thumb']) || (!$contact_record['micro']) || ($update_photo)) {
941                         Logger::info("statusnet_fetch_contact: Updating contact " . $contact->screen_name);
942
943                         $photos = Photo::importProfilePhoto($contact->profile_image_url, $uid, $contact_record['id']);
944
945                         Contact::update([
946                                 'photo' => $photos[0],
947                                 'thumb' => $photos[1],
948                                 'micro' => $photos[2],
949                                 'name-date' => DateTimeFormat::utcNow(),
950                                 'uri-date' => DateTimeFormat::utcNow(),
951                                 'avatar-date' => DateTimeFormat::utcNow(),
952                                 'url' => $contact->statusnet_profile_url,
953                                 'nurl' => Strings::normaliseLink($contact->statusnet_profile_url),
954                                 'addr' => statusnet_address($contact),
955                                 'name' => $contact->name,
956                                 'nick' => $contact->screen_name,
957                                 'location' => $contact->location,
958                                 'about' => $contact->description
959                         ], ['id' => $contact_record['id']]);
960                 }
961         }
962
963         return $contact_record["id"];
964 }
965
966 function statusnet_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
967 {
968         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
969         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
970         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
971         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
972         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
973
974         require_once __DIR__ . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'codebirdsn.php';
975
976         $cb = CodebirdSN::getInstance();
977         $cb->setConsumerKey($ckey, $csecret);
978         $cb->setToken($otoken, $osecret);
979
980         $self = Contact::selectFirst([], ['self' => true, 'uid' => $uid]);
981         if (!DBA::isResult($self)) {
982                 return;
983         }
984
985         $parameters = [];
986
987         if ($screen_name != "") {
988                 $parameters["screen_name"] = $screen_name;
989         }
990
991         if ($user_id != "") {
992                 $parameters["user_id"] = $user_id;
993         }
994
995         // Fetching user data
996         $user = $cb->users_show($parameters);
997
998         if (!is_object($user)) {
999                 return;
1000         }
1001
1002         $contact_id = statusnet_fetch_contact($uid, $user, true);
1003
1004         return $contact_id;
1005 }
1006
1007 function statusnet_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact)
1008 {
1009         Logger::info("statusnet_createpost: start");
1010
1011         $api = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1012         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $api);
1013
1014         $postarray = [];
1015         $postarray['network'] = Protocol::STATUSNET;
1016         $postarray['uid'] = $uid;
1017         $postarray['wall'] = 0;
1018
1019         if (!empty($post->retweeted_status)) {
1020                 $content = $post->retweeted_status;
1021                 statusnet_fetch_contact($uid, $content->user, false);
1022         } else {
1023                 $content = $post;
1024         }
1025
1026         $postarray['uri'] = $hostname . "::" . $content->id;
1027
1028         if (Post::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
1029                 return [];
1030         }
1031
1032         $contactid = 0;
1033
1034         if (!empty($content->in_reply_to_status_id)) {
1035                 $thr_parent = $hostname . "::" . $content->in_reply_to_status_id;
1036
1037                 $item = Post::selectFirst(['uri'], ['uri' => $thr_parent, 'uid' => $uid]);
1038                 if (!DBA::isResult($item)) {
1039                         $item = Post::selectFirst(['uri'], ['extid' => $thr_parent, 'uid' => $uid]);
1040                 }
1041
1042                 if (DBA::isResult($item)) {
1043                         $postarray['thr-parent'] = $item['uri'];
1044                         $postarray['object-type'] = Activity\ObjectType::COMMENT;
1045                 } else {
1046                         $postarray['object-type'] = Activity\ObjectType::NOTE;
1047                 }
1048
1049                 // Is it me?
1050                 $own_url = DI::pConfig()->get($uid, 'statusnet', 'own_url');
1051
1052                 if ($content->user->id == $own_url) {
1053                         $self = DBA::selectFirst([], ['self' => true, 'uid' => $uid]);
1054                         if (DBA::isResult($self)) {
1055                                 $contactid = $self["id"];
1056
1057                                 $postarray['owner-name'] = $self["name"];
1058                                 $postarray['owner-link'] = $self["url"];
1059                                 $postarray['owner-avatar'] = $self["photo"];
1060                         } else {
1061                                 return [];
1062                         }
1063                 }
1064                 // Don't create accounts of people who just comment something
1065                 $create_user = false;
1066         } else {
1067                 $postarray['object-type'] = Activity\ObjectType::NOTE;
1068         }
1069
1070         if ($contactid == 0) {
1071                 $contactid = statusnet_fetch_contact($uid, $post->user, $create_user);
1072                 $postarray['owner-name'] = $post->user->name;
1073                 $postarray['owner-link'] = $post->user->statusnet_profile_url;
1074                 $postarray['owner-avatar'] = $post->user->profile_image_url;
1075         }
1076         if (($contactid == 0) && !$only_existing_contact) {
1077                 $contactid = $self['id'];
1078         } elseif ($contactid <= 0) {
1079                 return [];
1080         }
1081
1082         $postarray['contact-id'] = $contactid;
1083
1084         $postarray['verb'] = Activity::POST;
1085
1086         $postarray['author-name'] = $content->user->name;
1087         $postarray['author-link'] = $content->user->statusnet_profile_url;
1088         $postarray['author-avatar'] = $content->user->profile_image_url;
1089
1090         // To-Do: Maybe unreliable? Can the api be entered without trailing "/"?
1091         $hostname = str_replace("/api/", "/notice/", DI::pConfig()->get($uid, 'statusnet', 'baseapi'));
1092
1093         $postarray['plink'] = $hostname . $content->id;
1094         $postarray['app'] = strip_tags($content->source);
1095
1096         if ($content->user->protected) {
1097                 $postarray['private'] = 1;
1098                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1099         }
1100
1101         $postarray['body'] = HTML::toBBCode($content->statusnet_html);
1102
1103         $postarray['body'] = statusnet_convertmsg($a, $postarray['body']);
1104
1105         $postarray['created'] = DateTimeFormat::utc($content->created_at);
1106         $postarray['edited'] = DateTimeFormat::utc($content->created_at);
1107
1108         if (!empty($content->place->name)) {
1109                 $postarray["location"] = $content->place->name;
1110         }
1111
1112         if (!empty($content->place->full_name)) {
1113                 $postarray["location"] = $content->place->full_name;
1114         }
1115
1116         if (!empty($content->geo->coordinates)) {
1117                 $postarray["coord"] = $content->geo->coordinates[0] . " " . $content->geo->coordinates[1];
1118         }
1119
1120         if (!empty($content->coordinates->coordinates)) {
1121                 $postarray["coord"] = $content->coordinates->coordinates[1] . " " . $content->coordinates->coordinates[0];
1122         }
1123
1124         Logger::info("statusnet_createpost: end");
1125
1126         return $postarray;
1127 }
1128
1129 function statusnet_fetchhometimeline(App $a, $uid, $mode = 1)
1130 {
1131         $conversations = [];
1132
1133         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1134         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1135         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1136         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1137         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1138         $create_user = DI::pConfig()->get($uid, 'statusnet', 'create_user');
1139
1140         // "create_user" is deactivated, since currently you cannot add users manually by now
1141         $create_user = true;
1142
1143         Logger::info("statusnet_fetchhometimeline: Fetching for user " . $uid);
1144
1145         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1146
1147         $own_contact = statusnet_fetch_own_contact($a, $uid);
1148
1149         if (empty($own_contact)) {
1150                 return;
1151         }
1152
1153         $contact = Contact::selectFirst([], ['id' => $own_contact, 'uid' => $uid]);
1154         if (DBA::isResult($contact)) {
1155                 $nick = $contact["nick"];
1156         } else {
1157                 Logger::info("statusnet_fetchhometimeline: Own GNU Social contact not found for user " . $uid);
1158                 return;
1159         }
1160
1161         $self = Contact::selectFirst([], ['self' => true, 'uid' => $uid]);
1162         if (!DBA::isResult($self)) {
1163                 Logger::info("statusnet_fetchhometimeline: Own contact not found for user " . $uid);
1164                 return;
1165         }
1166
1167         $user = User::getById($uid);
1168         if (!DBA::isResult($user)) {
1169                 Logger::info("statusnet_fetchhometimeline: Own user not found for user " . $uid);
1170                 return;
1171         }
1172
1173         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true];
1174         //$parameters["count"] = 200;
1175
1176         if ($mode == 1) {
1177                 // Fetching timeline
1178                 $lastid = DI::pConfig()->get($uid, 'statusnet', 'lasthometimelineid');
1179                 //$lastid = 1;
1180
1181                 $first_time = ($lastid == "");
1182
1183                 if ($lastid != "") {
1184                         $parameters["since_id"] = $lastid;
1185                 }
1186
1187                 $items = $connection->get('statuses/home_timeline', $parameters);
1188
1189                 if (!is_array($items)) {
1190                         if (is_object($items) && isset($items->error)) {
1191                                 $errormsg = $items->error;
1192                         } elseif (is_object($items)) {
1193                                 $errormsg = print_r($items, true);
1194                         } elseif (is_string($items) || is_float($items) || is_int($items)) {
1195                                 $errormsg = $items;
1196                         } else {
1197                                 $errormsg = "Unknown error";
1198                         }
1199
1200                         Logger::info("statusnet_fetchhometimeline: Error fetching home timeline: " . $errormsg);
1201                         return;
1202                 }
1203
1204                 $posts = array_reverse($items);
1205
1206                 Logger::info("statusnet_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items");
1207
1208                 if (count($posts)) {
1209                         foreach ($posts as $post) {
1210
1211                                 if ($post->id > $lastid) {
1212                                         $lastid = $post->id;
1213                                 }
1214
1215                                 if ($first_time) {
1216                                         continue;
1217                                 }
1218
1219                                 if (isset($post->statusnet_conversation_id)) {
1220                                         if (!isset($conversations[$post->statusnet_conversation_id])) {
1221                                                 statusnet_complete_conversation($a, $uid, $self, $create_user, $nick, $post->statusnet_conversation_id);
1222                                                 $conversations[$post->statusnet_conversation_id] = $post->statusnet_conversation_id;
1223                                         }
1224                                 } else {
1225                                         $postarray = statusnet_createpost($a, $uid, $post, $self, $create_user, true);
1226
1227                                         if (trim($postarray['body']) == "") {
1228                                                 continue;
1229                                         }
1230
1231                                         $item = Item::insert($postarray);
1232                                         $postarray["id"] = $item;
1233
1234                                         Logger::notice('statusnet_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1235                                 }
1236                         }
1237                 }
1238                 DI::pConfig()->set($uid, 'statusnet', 'lasthometimelineid', $lastid);
1239         }
1240
1241         // Fetching mentions
1242         $lastid = DI::pConfig()->get($uid, 'statusnet', 'lastmentionid');
1243         $first_time = ($lastid == "");
1244
1245         if ($lastid != "") {
1246                 $parameters["since_id"] = $lastid;
1247         }
1248
1249         $items = $connection->get('statuses/mentions_timeline', $parameters);
1250
1251         if (!is_array($items)) {
1252                 Logger::info("statusnet_fetchhometimeline: Error fetching mentions: " . print_r($items, true));
1253                 return;
1254         }
1255
1256         $posts = array_reverse($items);
1257
1258         Logger::info("statusnet_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items");
1259
1260         if (count($posts)) {
1261                 foreach ($posts as $post) {
1262                         if ($post->id > $lastid) {
1263                                 $lastid = $post->id;
1264                         }
1265
1266                         if ($first_time) {
1267                                 continue;
1268                         }
1269
1270                         $postarray = statusnet_createpost($a, $uid, $post, $self, false, false);
1271
1272                         if (isset($post->statusnet_conversation_id)) {
1273                                 if (!isset($conversations[$post->statusnet_conversation_id])) {
1274                                         statusnet_complete_conversation($a, $uid, $self, $create_user, $nick, $post->statusnet_conversation_id);
1275                                         $conversations[$post->statusnet_conversation_id] = $post->statusnet_conversation_id;
1276                                 }
1277                         } else {
1278                                 if (trim($postarray['body']) == "") {
1279                                         continue;
1280                                 }
1281
1282                                 $item = Item::insert($postarray);
1283
1284                                 Logger::notice('statusnet_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1285                         }
1286                 }
1287         }
1288
1289         DI::pConfig()->set($uid, 'statusnet', 'lastmentionid', $lastid);
1290 }
1291
1292 function statusnet_complete_conversation(App $a, $uid, $self, $create_user, $nick, $conversation)
1293 {
1294         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1295         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1296         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1297         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1298         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1299         $own_url = DI::pConfig()->get($uid, 'statusnet', 'own_url');
1300
1301         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1302
1303         $parameters["count"] = 200;
1304
1305         $items = $connection->get('statusnet/conversation/' . $conversation, $parameters);
1306         if (is_array($items)) {
1307                 $posts = array_reverse($items);
1308
1309                 foreach ($posts as $post) {
1310                         $postarray = statusnet_createpost($a, $uid, $post, $self, false, false);
1311
1312                         if (empty($postarray['body'])) {
1313                                 continue;
1314                         }
1315
1316                         $item = Item::insert($postarray);
1317                         $postarray["id"] = $item;
1318
1319                         Logger::notice('statusnet_complete_conversation: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1320                 }
1321         }
1322 }
1323
1324 function statusnet_convertmsg(App $a, $body)
1325 {
1326         $body = preg_replace("=\[url\=https?://([0-9]*).([0-9]*).([0-9]*).([0-9]*)/([0-9]*)\](.*?)\[\/url\]=ism", "$1.$2.$3.$4/$5", $body);
1327
1328         $URLSearchString = "^\[\]";
1329         $links = preg_match_all("/[^!#@]\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $body, $matches, PREG_SET_ORDER);
1330
1331         $footer = "";
1332         $footerurl = "";
1333         $footerlink = "";
1334         $type = "";
1335
1336         if ($links) {
1337                 foreach ($matches AS $match) {
1338                         $search = "[url=" . $match[1] . "]" . $match[2] . "[/url]";
1339
1340                         Logger::info("statusnet_convertmsg: expanding url " . $match[1]);
1341
1342                         try {
1343                                 $expanded_url = DI::httpClient()->finalUrl($match[1]);
1344                         } catch (TransferException $exception) {
1345                                 Logger::notice('statusnet_convertmsg: Couldn\'t get final URL.', ['url' => $match[1], 'exception' => $exception]);
1346                                 $expanded_url = $match[1];
1347                         }
1348
1349                         Logger::info("statusnet_convertmsg: fetching data for " . $expanded_url);
1350
1351                         $oembed_data = OEmbed::fetchURL($expanded_url, true);
1352
1353                         Logger::info("statusnet_convertmsg: fetching data: done");
1354
1355                         if ($type == "") {
1356                                 $type = $oembed_data->type;
1357                         }
1358
1359                         if ($oembed_data->type == "video") {
1360                                 //$body = str_replace($search, "[video]".$expanded_url."[/video]", $body);
1361                                 $type = $oembed_data->type;
1362                                 $footerurl = $expanded_url;
1363                                 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1364
1365                                 $body = str_replace($search, $footerlink, $body);
1366                         } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1367                                 $body = str_replace($search, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1368                         } elseif ($oembed_data->type != "link") {
1369                                 $body = str_replace($search, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1370                         } else {
1371                                 $img_str = DI::httpClient()->fetch($expanded_url, HttpClientAccept::DEFAULT, 4);
1372
1373                                 $tempfile = tempnam(System::getTempPath(), "cache");
1374                                 file_put_contents($tempfile, $img_str);
1375                                 $mime = mime_content_type($tempfile);
1376                                 unlink($tempfile);
1377
1378                                 if (substr($mime, 0, 6) == "image/") {
1379                                         $type = "photo";
1380                                         $body = str_replace($search, "[img]" . $expanded_url . "[/img]", $body);
1381                                 } else {
1382                                         $type = $oembed_data->type;
1383                                         $footerurl = $expanded_url;
1384                                         $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1385
1386                                         $body = str_replace($search, $footerlink, $body);
1387                                 }
1388                         }
1389                 }
1390
1391                 if ($footerurl != "") {
1392                         $footer = "\n" . PageInfo::getFooterFromUrl($footerurl);
1393                 }
1394
1395                 if (($footerlink != "") && (trim($footer) != "")) {
1396                         $removedlink = trim(str_replace($footerlink, "", $body));
1397
1398                         if (($removedlink == "") || strstr($body, $removedlink)) {
1399                                 $body = $removedlink;
1400                         }
1401
1402                         $body .= $footer;
1403                 }
1404         }
1405
1406         return $body;
1407 }
1408
1409 function statusnet_fetch_own_contact(App $a, $uid)
1410 {
1411         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1412         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1413         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1414         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1415         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1416         $own_url = DI::pConfig()->get($uid, 'statusnet', 'own_url');
1417
1418         $contact_id = 0;
1419
1420         if ($own_url == "") {
1421                 $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1422
1423                 // Fetching user data
1424                 $user = $connection->get('account/verify_credentials');
1425
1426                 if (empty($user)) {
1427                         return false;
1428                 }
1429
1430                 DI::pConfig()->set($uid, 'statusnet', 'own_url', Strings::normaliseLink($user->statusnet_profile_url));
1431
1432                 $contact_id = statusnet_fetch_contact($uid, $user, true);
1433         } else {
1434                 $contact = Contact::selectFirst([], ['uid' => $uid, 'alias' => $own_url]);
1435                 if (DBA::isResult($contact)) {
1436                         $contact_id = $contact["id"];
1437                 } else {
1438                         DI::pConfig()->delete($uid, 'statusnet', 'own_url');
1439                 }
1440         }
1441         return $contact_id;
1442 }
1443
1444 function statusnet_is_retweet(App $a, $uid, $body)
1445 {
1446         $body = trim($body);
1447
1448         // Skip if it isn't a pure repeated messages
1449         // Does it start with a share?
1450         if (strpos($body, "[share") > 0) {
1451                 return false;
1452         }
1453
1454         // Does it end with a share?
1455         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1456                 return false;
1457         }
1458
1459         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1460         // Skip if there is no shared message in there
1461         if ($body == $attributes) {
1462                 return false;
1463         }
1464
1465         $link = "";
1466         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1467         if (!empty($matches[1])) {
1468                 $link = $matches[1];
1469         }
1470
1471         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1472         if (!empty($matches[1])) {
1473                 $link = $matches[1];
1474         }
1475
1476         $ckey    = DI::pConfig()->get($uid, 'statusnet', 'consumerkey');
1477         $csecret = DI::pConfig()->get($uid, 'statusnet', 'consumersecret');
1478         $api     = DI::pConfig()->get($uid, 'statusnet', 'baseapi');
1479         $otoken  = DI::pConfig()->get($uid, 'statusnet', 'oauthtoken');
1480         $osecret = DI::pConfig()->get($uid, 'statusnet', 'oauthsecret');
1481         $hostname = preg_replace("=https?://([\w\.]*)/.*=ism", "$1", $api);
1482
1483         $id = preg_replace("=https?://" . $hostname . "/notice/(.*)=ism", "$1", $link);
1484
1485         if ($id == $link) {
1486                 return false;
1487         }
1488
1489         Logger::info('statusnet_is_retweet: Retweeting id ' . $id . ' for user ' . $uid);
1490
1491         $connection = new StatusNetOAuth($api, $ckey, $csecret, $otoken, $osecret);
1492
1493         $result = $connection->post('statuses/retweet/' . $id);
1494
1495         Logger::info('statusnet_is_retweet: result ' . print_r($result, true));
1496
1497         return isset($result->id);
1498 }