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