]> git.mxchange.org Git - friendica-addons.git/blob - twitter/twitter.php
Merge 2020.03 into develop
[friendica-addons.git] / twitter / twitter.php
1 <?php
2 /**
3  * Name: Twitter Connector
4  * Description: Bidirectional (posting, relaying and reading) connector for Twitter.
5  * Version: 1.1.0
6  * Author: Tobias Diekershoff <https://f.diekershoff.de/profile/tobias>
7  * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
8  * Maintainer: Hypolite Petovan <https://friendica.mrpetovan.com/profile/hypolite>
9  *
10  * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel, Hypolite Petovan
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 /*   Twitter Addon for Friendica
37  *
38  *   Author: Tobias Diekershoff
39  *           tobias.diekershoff@gmx.net
40  *
41  *   License:3-clause BSD license
42  *
43  *   Configuration:
44  *     To use this addon you need a OAuth Consumer key pair (key & secret)
45  *     you can get it from Twitter at https://twitter.com/apps
46  *
47  *     Register your Friendica site as "Client" application with "Read & Write" access
48  *     we do not need "Twitter as login". When you've registered the app you get the
49  *     OAuth Consumer key and secret pair for your application/site.
50  *
51  *     Add this key pair to your global config/addon.config.php or use the admin panel.
52  *
53  *      'twitter' => [
54  *                  'consumerkey' => '',
55  *              'consumersecret' => '',
56  *      ],
57  *
58  *     To activate the addon itself add it to the system.addon
59  *     setting. After this, your user can configure their Twitter account settings
60  *     from "Settings -> Addon Settings".
61  *
62  *     Requirements: PHP5, curl
63  */
64
65 use Abraham\TwitterOAuth\TwitterOAuth;
66 use Abraham\TwitterOAuth\TwitterOAuthException;
67 use Codebird\Codebird;
68 use Friendica\App;
69 use Friendica\Content\OEmbed;
70 use Friendica\Content\Text\BBCode;
71 use Friendica\Content\Text\Plaintext;
72 use Friendica\Core\Hook;
73 use Friendica\Core\Logger;
74 use Friendica\Core\Protocol;
75 use Friendica\Core\Renderer;
76 use Friendica\Core\Worker;
77 use Friendica\Database\DBA;
78 use Friendica\DI;
79 use Friendica\Model\Contact;
80 use Friendica\Model\Conversation;
81 use Friendica\Model\Group;
82 use Friendica\Model\Item;
83 use Friendica\Model\ItemContent;
84 use Friendica\Model\User;
85 use Friendica\Protocol\Activity;
86 use Friendica\Util\ConfigFileLoader;
87 use Friendica\Util\DateTimeFormat;
88 use Friendica\Util\Images;
89 use Friendica\Util\Network;
90 use Friendica\Util\Strings;
91
92 require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
93
94 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
95
96 function twitter_install()
97 {
98         //  we need some hooks, for the configuration and for sending tweets
99         Hook::register('load_config'            , __FILE__, 'twitter_load_config');
100         Hook::register('connector_settings'     , __FILE__, 'twitter_settings');
101         Hook::register('connector_settings_post', __FILE__, 'twitter_settings_post');
102         Hook::register('hook_fork'              , __FILE__, 'twitter_hook_fork');
103         Hook::register('post_local'             , __FILE__, 'twitter_post_local');
104         Hook::register('notifier_normal'        , __FILE__, 'twitter_post_hook');
105         Hook::register('jot_networks'           , __FILE__, 'twitter_jot_nets');
106         Hook::register('cron'                   , __FILE__, 'twitter_cron');
107         Hook::register('follow'                 , __FILE__, 'twitter_follow');
108         Hook::register('expire'                 , __FILE__, 'twitter_expire');
109         Hook::register('prepare_body'           , __FILE__, 'twitter_prepare_body');
110         Hook::register('check_item_notification', __FILE__, 'twitter_check_item_notification');
111         Logger::info("installed twitter");
112 }
113
114 function twitter_uninstall()
115 {
116         Hook::unregister('load_config'            , __FILE__, 'twitter_load_config');
117         Hook::unregister('connector_settings'     , __FILE__, 'twitter_settings');
118         Hook::unregister('connector_settings_post', __FILE__, 'twitter_settings_post');
119         Hook::unregister('hook_fork'              , __FILE__, 'twitter_hook_fork');
120         Hook::unregister('post_local'             , __FILE__, 'twitter_post_local');
121         Hook::unregister('notifier_normal'        , __FILE__, 'twitter_post_hook');
122         Hook::unregister('jot_networks'           , __FILE__, 'twitter_jot_nets');
123         Hook::unregister('cron'                   , __FILE__, 'twitter_cron');
124         Hook::unregister('follow'                 , __FILE__, 'twitter_follow');
125         Hook::unregister('expire'                 , __FILE__, 'twitter_expire');
126         Hook::unregister('prepare_body'           , __FILE__, 'twitter_prepare_body');
127         Hook::unregister('check_item_notification', __FILE__, 'twitter_check_item_notification');
128
129         // old setting - remove only
130         Hook::unregister('post_local_end'     , __FILE__, 'twitter_post_hook');
131         Hook::unregister('addon_settings'     , __FILE__, 'twitter_settings');
132         Hook::unregister('addon_settings_post', __FILE__, 'twitter_settings_post');
133 }
134
135 function twitter_load_config(App $a, ConfigFileLoader $loader)
136 {
137         $a->getConfigCache()->load($loader->loadAddonConfig('twitter'));
138 }
139
140 function twitter_check_item_notification(App $a, array &$notification_data)
141 {
142         $own_id = DI::pConfig()->get($notification_data["uid"], 'twitter', 'own_id');
143
144         $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
145                         intval($notification_data["uid"]),
146                         DBA::escape("twitter::".$own_id)
147         );
148
149         if ($own_user) {
150                 $notification_data["profiles"][] = $own_user[0]["url"];
151         }
152 }
153
154 function twitter_follow(App $a, array &$contact)
155 {
156         Logger::info('Check if contact is twitter contact', ['url' => $contact["url"]]);
157
158         if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
159                 return;
160         }
161
162         // contact seems to be a twitter contact, so continue
163         $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
164         $nickname = str_replace("@twitter.com", "", $nickname);
165
166         $uid = $a->user["uid"];
167
168         $ckey = DI::config()->get('twitter', 'consumerkey');
169         $csecret = DI::config()->get('twitter', 'consumersecret');
170         $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
171         $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
172
173         // If the addon is not configured (general or for this user) quit here
174         if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
175                 $contact = false;
176                 return;
177         }
178
179         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
180         $connection->post('friendships/create', ['screen_name' => $nickname]);
181
182         twitter_fetchuser($a, $uid, $nickname);
183
184         $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
185                 FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
186                                 intval($uid),
187                                 DBA::escape($nickname));
188         if (DBA::isResult($r)) {
189                 $contact["contact"] = $r[0];
190         }
191 }
192
193 function twitter_jot_nets(App $a, array &$jotnets_fields)
194 {
195         if (!local_user()) {
196                 return;
197         }
198
199         if (DI::pConfig()->get(local_user(), 'twitter', 'post')) {
200                 $jotnets_fields[] = [
201                         'type' => 'checkbox',
202                         'field' => [
203                                 'twitter_enable',
204                                 DI::l10n()->t('Post to Twitter'),
205                                 DI::pConfig()->get(local_user(), 'twitter', 'post_by_default')
206                         ]
207                 ];
208         }
209 }
210
211
212 function twitter_settings_post(App $a)
213 {
214         if (!local_user()) {
215                 return;
216         }
217         // don't check twitter settings if twitter submit button is not clicked
218         if (empty($_POST['twitter-disconnect']) && empty($_POST['twitter-submit'])) {
219                 return;
220         }
221
222         if (!empty($_POST['twitter-disconnect'])) {
223                 /*               * *
224                  * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
225                  * from the user configuration
226                  */
227                 DI::pConfig()->delete(local_user(), 'twitter', 'consumerkey');
228                 DI::pConfig()->delete(local_user(), 'twitter', 'consumersecret');
229                 DI::pConfig()->delete(local_user(), 'twitter', 'oauthtoken');
230                 DI::pConfig()->delete(local_user(), 'twitter', 'oauthsecret');
231                 DI::pConfig()->delete(local_user(), 'twitter', 'post');
232                 DI::pConfig()->delete(local_user(), 'twitter', 'post_by_default');
233                 DI::pConfig()->delete(local_user(), 'twitter', 'lastid');
234                 DI::pConfig()->delete(local_user(), 'twitter', 'mirror_posts');
235                 DI::pConfig()->delete(local_user(), 'twitter', 'import');
236                 DI::pConfig()->delete(local_user(), 'twitter', 'create_user');
237                 DI::pConfig()->delete(local_user(), 'twitter', 'own_id');
238         } else {
239                 if (isset($_POST['twitter-pin'])) {
240                         //  if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
241                         Logger::notice('got a Twitter PIN');
242                         $ckey    = DI::config()->get('twitter', 'consumerkey');
243                         $csecret = DI::config()->get('twitter', 'consumersecret');
244                         //  the token and secret for which the PIN was generated were hidden in the settings
245                         //  form as token and token2, we need a new connection to Twitter using these token
246                         //  and secret to request a Access Token with the PIN
247                         try {
248                                 if (empty($_POST['twitter-pin'])) {
249                                         throw new Exception(DI::l10n()->t('You submitted an empty PIN, please Sign In with Twitter again to get a new one.'));
250                                 }
251
252                                 $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
253                                 $token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $_POST['twitter-pin']]);
254                                 //  ok, now that we have the Access Token, save them in the user config
255                                 DI::pConfig()->set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
256                                 DI::pConfig()->set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
257                                 DI::pConfig()->set(local_user(), 'twitter', 'post', 1);
258                         } catch(Exception $e) {
259                                 info($e->getMessage());
260                         } catch(TwitterOAuthException $e) {
261                                 info($e->getMessage());
262                         }
263                         //  reload the Addon Settings page, if we don't do it see Bug #42
264                         DI::baseUrl()->redirect('settings/connectors');
265                 } else {
266                         //  if no PIN is supplied in the POST variables, the user has changed the setting
267                         //  to post a tweet for every new __public__ posting to the wall
268                         DI::pConfig()->set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
269                         DI::pConfig()->set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
270                         DI::pConfig()->set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
271                         DI::pConfig()->set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
272                         DI::pConfig()->set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
273
274                         if (!intval($_POST['twitter-mirror'])) {
275                                 DI::pConfig()->delete(local_user(), 'twitter', 'lastid');
276                         }
277
278                         info(DI::l10n()->t('Twitter settings updated.') . EOL);
279                 }
280         }
281 }
282
283 function twitter_settings(App $a, &$s)
284 {
285         if (!local_user()) {
286                 return;
287         }
288         DI::page()['htmlhead'] .= '<link rel="stylesheet"  type="text/css" href="' . DI::baseUrl()->get() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
289         /*       * *
290          * 1) Check that we have global consumer key & secret
291          * 2) If no OAuthtoken & stuff is present, generate button to get some
292          * 3) Checkbox for "Send public notices (280 chars only)
293          */
294         $ckey    = DI::config()->get('twitter', 'consumerkey');
295         $csecret = DI::config()->get('twitter', 'consumersecret');
296         $otoken  = DI::pConfig()->get(local_user(), 'twitter', 'oauthtoken');
297         $osecret = DI::pConfig()->get(local_user(), 'twitter', 'oauthsecret');
298
299         $enabled            = intval(DI::pConfig()->get(local_user(), 'twitter', 'post'));
300         $defenabled         = intval(DI::pConfig()->get(local_user(), 'twitter', 'post_by_default'));
301         $mirrorenabled      = intval(DI::pConfig()->get(local_user(), 'twitter', 'mirror_posts'));
302         $importenabled      = intval(DI::pConfig()->get(local_user(), 'twitter', 'import'));
303         $create_userenabled = intval(DI::pConfig()->get(local_user(), 'twitter', 'create_user'));
304
305         $css = (($enabled) ? '' : '-disabled');
306
307         $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
308         $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . DI::l10n()->t('Twitter Import/Export/Mirror') . '</h3>';
309         $s .= '</span>';
310         $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
311         $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
312         $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . DI::l10n()->t('Twitter Import/Export/Mirror') . '</h3>';
313         $s .= '</span>';
314
315         if ((!$ckey) && (!$csecret)) {
316                 /* no global consumer keys
317                  * display warning and skip personal config
318                  */
319                 $s .= '<p>' . DI::l10n()->t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
320         } else {
321                 // ok we have a consumer key pair now look into the OAuth stuff
322                 if ((!$otoken) && (!$osecret)) {
323                         /* the user has not yet connected the account to twitter...
324                          * get a temporary OAuth key/secret pair and display a button with
325                          * which the user can request a PIN to connect the account to a
326                          * account at Twitter.
327                          */
328                         $connection = new TwitterOAuth($ckey, $csecret);
329                         try {
330                                 $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
331                                 $s .= '<p>' . DI::l10n()->t('At this Friendica instance the Twitter addon was enabled but you have not yet connected your account to your Twitter account. To do so click the button below to get a PIN from Twitter which you have to copy into the input box below and submit the form. Only your <strong>public</strong> posts will be posted to Twitter.') . '</p>';
332                                 $s .= '<a href="' . $connection->url('oauth/authorize', ['oauth_token' => $result['oauth_token']]) . '" target="_twitter"><img src="addon/twitter/lighter.png" alt="' . DI::l10n()->t('Log in with Twitter') . '"></a>';
333                                 $s .= '<div id="twitter-pin-wrapper">';
334                                 $s .= '<label id="twitter-pin-label" for="twitter-pin">' . DI::l10n()->t('Copy the PIN from Twitter here') . '</label>';
335                                 $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
336                                 $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result['oauth_token'] . '" />';
337                                 $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result['oauth_token_secret'] . '" />';
338                                 $s .= '</div><div class="clear"></div>';
339                                 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . DI::l10n()->t('Save Settings') . '" /></div>';
340                         } catch (TwitterOAuthException $e) {
341                                 $s .= '<p>' . DI::l10n()->t('An error occured: ') . $e->getMessage() . '</p>';
342                         }
343                 } else {
344                         /*                       * *
345                          *  we have an OAuth key / secret pair for the user
346                          *  so let's give a chance to disable the postings to Twitter
347                          */
348                         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
349                         try {
350                                 $details = $connection->get('account/verify_credentials');
351
352                                 $field_checkbox = Renderer::getMarkupTemplate('field_checkbox.tpl');
353
354                                 if (property_exists($details, 'screen_name') &&
355                                     property_exists($details, 'description') &&
356                                     property_exists($details, 'profile_image_url')) {
357                                         $s .= '<div id="twitter-info" >
358                                         <p>' . DI::l10n()->t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
359                                                 <button type="submit" name="twitter-disconnect" value="1">' . DI::l10n()->t('Disconnect') . '</button>
360                                         </p>
361                                         <p id="twitter-info-block">
362                                                 <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
363                                                 <em>' . $details->description . '</em>
364                                         </p>
365                                 </div>';
366                                 } else {
367                                         $s .= '<div id="twitter-info" >
368                                         <p>Invalid Twitter info</p>
369                                 </div>';
370                                         Logger::info('Invalid twitter info (verify credentials).', ['auth' => TwitterOAuth::class]);
371                                 }
372                                 $s .= '<div class="clear"></div>';
373
374                                 $s .= Renderer::replaceMacros($field_checkbox, [
375                                         '$field' => ['twitter-enable', DI::l10n()->t('Allow posting to Twitter'), $enabled, DI::l10n()->t('If enabled all your <strong>public</strong> postings can be posted to the associated Twitter account. You can choose to do so by default (here) or for every posting separately in the posting options when writing the entry.')]
376                                 ]);
377                                 if ($a->user['hidewall']) {
378                                         $s .= '<p>' . DI::l10n()->t('<strong>Note</strong>: Due to your privacy settings (<em>Hide your profile details from unknown viewers?</em>) the link potentially included in public postings relayed to Twitter will lead the visitor to a blank page informing the visitor that the access to your profile has been restricted.') . '</p>';
379                                 }
380                                 $s .= Renderer::replaceMacros($field_checkbox, [
381                                         '$field' => ['twitter-default', DI::l10n()->t('Send public postings to Twitter by default'), $defenabled, '']
382                                 ]);
383                                 $s .= Renderer::replaceMacros($field_checkbox, [
384                                         '$field' => ['twitter-mirror', DI::l10n()->t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
385                                 ]);
386                                 $s .= Renderer::replaceMacros($field_checkbox, [
387                                         '$field' => ['twitter-import', DI::l10n()->t('Import the remote timeline'), $importenabled, '']
388                                 ]);
389                                 $s .= Renderer::replaceMacros($field_checkbox, [
390                                         '$field' => ['twitter-create_user', DI::l10n()->t('Automatically create contacts'), $create_userenabled, DI::l10n()->t('This will automatically create a contact in Friendica as soon as you receive a message from an existing contact via the Twitter network. If you do not enable this, you need to manually add those Twitter contacts in Friendica from whom you would like to see posts here. However if enabled, you cannot merely remove a twitter contact from the Friendica contact list, as it will recreate this contact when they post again.')]
391                                 ]);
392                                 $s .= '<div class="clear"></div>';
393                                 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . DI::l10n()->t('Save Settings') . '" /></div>';
394                         } catch (TwitterOAuthException $e) {
395                                 $s .= '<p>' . DI::l10n()->t('An error occured: ') . $e->getMessage() . '</p>';
396                         }
397                 }
398         }
399         $s .= '</div><div class="clear"></div>';
400 }
401
402 function twitter_hook_fork(App $a, array &$b)
403 {
404         if ($b['name'] != 'notifier_normal') {
405                 return;
406         }
407
408         $post = $b['data'];
409
410         // Deleting and editing is not supported by the addon (deleting could, but isn't by now)
411         if ($post['deleted'] || ($post['created'] !== $post['edited'])) {
412                 $b['execute'] = false;
413                 return;
414         }
415
416         // if post comes from twitter don't send it back
417         if ($post['extid'] == Protocol::TWITTER) {
418                 $b['execute'] = false;
419                 return;
420         }
421
422         if ($post['app'] == 'Twitter') {
423                 $b['execute'] = false;
424                 return;
425         }
426
427         if (DI::pConfig()->get($post['uid'], 'twitter', 'import')) {
428                 // Don't fork if it isn't a reply to a twitter post
429                 if (($post['parent'] != $post['id']) && !Item::exists(['id' => $post['parent'], 'network' => Protocol::TWITTER])) {
430                         Logger::notice('No twitter parent found', ['item' => $post['id']]);
431                         $b['execute'] = false;
432                         return;
433                 }
434         } else {
435                 // Comments are never exported when we don't import the twitter timeline
436                 if (!strstr($post['postopts'], 'twitter') || ($post['parent'] != $post['id']) || $post['private']) {
437                         $b['execute'] = false;
438                         return;
439                 }
440         }
441 }
442
443 function twitter_post_local(App $a, array &$b)
444 {
445         if ($b['edit']) {
446                 return;
447         }
448
449         if (!local_user() || (local_user() != $b['uid'])) {
450                 return;
451         }
452
453         $twitter_post = intval(DI::pConfig()->get(local_user(), 'twitter', 'post'));
454         $twitter_enable = (($twitter_post && !empty($_REQUEST['twitter_enable'])) ? intval($_REQUEST['twitter_enable']) : 0);
455
456         // if API is used, default to the chosen settings
457         if ($b['api_source'] && intval(DI::pConfig()->get(local_user(), 'twitter', 'post_by_default'))) {
458                 $twitter_enable = 1;
459         }
460
461         if (!$twitter_enable) {
462                 return;
463         }
464
465         if (strlen($b['postopts'])) {
466                 $b['postopts'] .= ',';
467         }
468
469         $b['postopts'] .= 'twitter';
470 }
471
472 function twitter_action(App $a, $uid, $pid, $action)
473 {
474         $ckey = DI::config()->get('twitter', 'consumerkey');
475         $csecret = DI::config()->get('twitter', 'consumersecret');
476         $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
477         $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
478
479         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
480
481         $post = ['id' => $pid];
482
483         Logger::log("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), Logger::DATA);
484
485         switch ($action) {
486                 case "delete":
487                         // To-Do: $result = $connection->post('statuses/destroy', $post);
488                         $result = [];
489                         break;
490                 case "like":
491                         $result = $connection->post('favorites/create', $post);
492                         break;
493                 case "unlike":
494                         $result = $connection->post('favorites/destroy', $post);
495                         break;
496                 default:
497                         Logger::log('Unhandled action ' . $action, Logger::DEBUG);
498                         $result = [];
499         }
500         Logger::log("twitter_action '" . $action . "' send, result: " . print_r($result, true), Logger::DEBUG);
501 }
502
503 function twitter_post_hook(App $a, array &$b)
504 {
505         // Post to Twitter
506         if (!DI::pConfig()->get($b["uid"], 'twitter', 'import')
507                 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
508                 return;
509         }
510
511         if ($b['parent'] != $b['id']) {
512                 Logger::log("twitter_post_hook: parameter " . print_r($b, true), Logger::DATA);
513
514                 // Looking if its a reply to a twitter post
515                 if ((substr($b["parent-uri"], 0, 9) != "twitter::")
516                         && (substr($b["extid"], 0, 9) != "twitter::")
517                         && (substr($b["thr-parent"], 0, 9) != "twitter::"))
518                 {
519                         Logger::log("twitter_post_hook: no twitter post " . $b["parent"]);
520                         return;
521                 }
522
523                 $condition = ['uri' => $b["thr-parent"], 'uid' => $b["uid"]];
524                 $orig_post = Item::selectFirst([], $condition);
525                 if (!DBA::isResult($orig_post)) {
526                         Logger::log("twitter_post_hook: no parent found " . $b["thr-parent"]);
527                         return;
528                 } else {
529                         $iscomment = true;
530                 }
531
532
533                 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
534                 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
535                 $nicknameplain = "@" . $nicknameplain;
536
537                 Logger::log("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], Logger::DEBUG);
538                 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
539                         $b["body"] = $nickname . " " . $b["body"];
540                 }
541
542                 Logger::log("twitter_post_hook: parent found " . print_r($orig_post, true), Logger::DATA);
543         } else {
544                 $iscomment = false;
545
546                 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
547                         return;
548                 }
549
550                 // Dont't post if the post doesn't belong to us.
551                 // This is a check for forum postings
552                 $self = DBA::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
553                 if ($b['contact-id'] != $self['id']) {
554                         return;
555                 }
556         }
557
558         if (($b['verb'] == Activity::POST) && $b['deleted']) {
559                 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
560         }
561
562         if ($b['verb'] == Activity::LIKE) {
563                 Logger::log("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), Logger::DEBUG);
564                 if ($b['deleted']) {
565                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
566                 } else {
567                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
568                 }
569
570                 return;
571         }
572
573         if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
574                 return;
575         }
576
577         // if post comes from twitter don't send it back
578         if ($b['extid'] == Protocol::TWITTER) {
579                 return;
580         }
581
582         if ($b['app'] == "Twitter") {
583                 return;
584         }
585
586         Logger::notice('twitter post invoked', ['id' => $b['id'], 'guid' => $b['guid']]);
587
588         DI::pConfig()->load($b['uid'], 'twitter');
589
590         $ckey    = DI::config()->get('twitter', 'consumerkey');
591         $csecret = DI::config()->get('twitter', 'consumersecret');
592         $otoken  = DI::pConfig()->get($b['uid'], 'twitter', 'oauthtoken');
593         $osecret = DI::pConfig()->get($b['uid'], 'twitter', 'oauthsecret');
594
595         if ($ckey && $csecret && $otoken && $osecret) {
596                 Logger::log('twitter: we have customer key and oauth stuff, going to send.', Logger::DEBUG);
597
598                 // If it's a repeated message from twitter then do a native retweet and exit
599                 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
600                         return;
601                 }
602
603                 Codebird::setConsumerKey($ckey, $csecret);
604                 $cb = Codebird::getInstance();
605                 $cb->setToken($otoken, $osecret);
606
607                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
608
609                 // Set the timeout for upload to 30 seconds
610                 $connection->setTimeouts(10, 30);
611
612                 $max_char = 280;
613
614                 // Handling non-native reshares
615                 $b['body'] = Friendica\Content\Text\BBCode::convertShare(
616                         $b['body'],
617                         function (array $attributes, array $author_contact, $content, $is_quote_share) {
618                                 return twitter_convert_share($attributes, $author_contact, $content, $is_quote_share);
619                         }
620                 );
621
622                 $b['body'] = twitter_update_mentions($b['body']);
623
624                 $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, 8);
625                 Logger::info('Got plaintext', ['id' => $b['id'], 'message' => $msgarr]);
626                 $msg = $msgarr["text"];
627
628                 if (($msg == "") && isset($msgarr["title"])) {
629                         $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
630                 }
631
632                 if (!empty($msgarr['url']) && ($msgarr['url'] == $b['plink']) && !empty($msgarr['images']) && (count($msgarr['images']) <= 4)) {
633                         $url_added = false;
634                 } elseif (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
635                         $msg .= "\n" . $msgarr["url"];
636                         $url_added = true;
637                 } else {
638                         $url_added = false;
639                 }
640
641                 if (empty($msg)) {
642                         Logger::info('Empty message', ['id' => $b['id']]);
643                         return;
644                 }
645
646                 // and now tweet it :-)
647                 $post = [];
648
649                 if (!empty($msgarr['images'])) {
650                         Logger::info('Got images', ['id' => $b['id'], 'images' => $msgarr['images']]);
651                         try {
652                                 $media_ids = [];
653                                 foreach ($msgarr['images'] as $image) {
654                                         if (count($media_ids) == 4) {
655                                                 continue;
656                                         }
657
658                                         $img_str = Network::fetchUrl($image['url']);
659
660                                         $tempfile = tempnam(get_temppath(), 'cache');
661                                         file_put_contents($tempfile, $img_str);
662
663                                         Logger::info('Uploading', ['id' => $b['id'], 'image' => $image['url']]);
664                                         $media = $connection->upload('media/upload', ['media' => $tempfile]);
665
666                                         unlink($tempfile);
667
668                                         if (isset($media->media_id_string)) {
669                                                 $media_ids[] = $media->media_id_string;
670
671                                                 if (!empty($image['description'])) {
672                                                         $data = ['media_id' => $media->media_id_string,
673                                                                 'alt_text' => ['text' => substr($image['description'], 0, 420)]];
674                                                         $ret = $cb->media_metadata_create($data);
675                                                         Logger::info('Metadata create', ['id' => $b['id'], 'data' => $data, 'return' => json_encode($ret)]);
676                                                 }
677                                         } else {
678                                                 throw new Exception('Failed upload', ['id' => $b['id'], 'image' => $image['url']]);
679                                         }
680                                 }
681                                 $post['media_ids'] = implode(',', $media_ids);
682                                 if (empty($post['media_ids'])) {
683                                         unset($post['media_ids']);
684                                 }
685                         } catch (Exception $e) {
686                                 Logger::info('Exception when trying to send to Twitter', ['id' => $b['id'], 'message' => $e->getMessage()]);
687                         }
688                 }
689
690                 $post['status'] = $msg;
691
692                 if ($iscomment) {
693                         $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
694                 }
695
696                 $url = 'statuses/update';
697                 $result = $connection->post($url, $post);
698                 Logger::info('twitter_post send', ['id' => $b['id'], 'result' => $result]);
699
700                 if (!empty($result->source)) {
701                         DI::config()->set("twitter", "application_name", strip_tags($result->source));
702                 }
703
704                 if (!empty($result->errors)) {
705                         Logger::info('Send to Twitter failed', ['id' => $b['id'], 'error' => $result->errors]);
706                         Worker::defer();
707                 } elseif ($iscomment) {
708                         Logger::info('Update extid', ['id' => $b['id'], 'extid' => $result->id_str]);
709                         Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
710                 }
711         }
712 }
713
714 function twitter_addon_admin_post(App $a)
715 {
716         $consumerkey    = !empty($_POST['consumerkey'])    ? Strings::escapeTags(trim($_POST['consumerkey']))    : '';
717         $consumersecret = !empty($_POST['consumersecret']) ? Strings::escapeTags(trim($_POST['consumersecret'])) : '';
718         DI::config()->set('twitter', 'consumerkey', $consumerkey);
719         DI::config()->set('twitter', 'consumersecret', $consumersecret);
720         info(DI::l10n()->t('Settings updated.') . EOL);
721 }
722
723 function twitter_addon_admin(App $a, &$o)
724 {
725         $t = Renderer::getMarkupTemplate("admin.tpl", "addon/twitter/");
726
727         $o = Renderer::replaceMacros($t, [
728                 '$submit' => DI::l10n()->t('Save Settings'),
729                 // name, label, value, help, [extra values]
730                 '$consumerkey' => ['consumerkey', DI::l10n()->t('Consumer key'), DI::config()->get('twitter', 'consumerkey'), ''],
731                 '$consumersecret' => ['consumersecret', DI::l10n()->t('Consumer secret'), DI::config()->get('twitter', 'consumersecret'), ''],
732         ]);
733 }
734
735 function twitter_cron(App $a)
736 {
737         $last = DI::config()->get('twitter', 'last_poll');
738
739         $poll_interval = intval(DI::config()->get('twitter', 'poll_interval'));
740         if (!$poll_interval) {
741                 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
742         }
743
744         if ($last) {
745                 $next = $last + ($poll_interval * 60);
746                 if ($next > time()) {
747                         Logger::notice('twitter: poll intervall not reached');
748                         return;
749                 }
750         }
751         Logger::notice('twitter: cron_start');
752
753         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
754         if (DBA::isResult($r)) {
755                 foreach ($r as $rr) {
756                         Logger::notice('Fetching', ['user' => $rr['uid']]);
757                         Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
758                 }
759         }
760
761         $abandon_days = intval(DI::config()->get('system', 'account_abandon_days'));
762         if ($abandon_days < 1) {
763                 $abandon_days = 0;
764         }
765
766         $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
767
768         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
769         if (DBA::isResult($r)) {
770                 foreach ($r as $rr) {
771                         if ($abandon_days != 0) {
772                                 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
773                                 if (!DBA::isResult($user)) {
774                                         Logger::notice('abandoned account: timeline from user will not be imported', ['user' => $rr['uid']]);
775                                         continue;
776                                 }
777                         }
778
779                         Logger::notice('importing timeline', ['user' => $rr['uid']]);
780                         Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
781                         /*
782                           // To-Do
783                           // check for new contacts once a day
784                           $last_contact_check = DI::pConfig()->get($rr['uid'],'pumpio','contact_check');
785                           if($last_contact_check)
786                           $next_contact_check = $last_contact_check + 86400;
787                           else
788                           $next_contact_check = 0;
789
790                           if($next_contact_check <= time()) {
791                           pumpio_getallusers($a, $rr["uid"]);
792                           DI::pConfig()->set($rr['uid'],'pumpio','contact_check',time());
793                           }
794                          */
795                 }
796         }
797
798         Logger::notice('twitter: cron_end');
799
800         DI::config()->set('twitter', 'last_poll', time());
801 }
802
803 function twitter_expire(App $a)
804 {
805         $days = DI::config()->get('twitter', 'expire');
806
807         if ($days == 0) {
808                 return;
809         }
810
811         $r = Item::select(['id', 'guid'], ['deleted' => true, 'network' => Protocol::TWITTER]);
812         while ($row = DBA::fetch($r)) {
813                 Logger::info('[twitter] Delete expired item', ['id' => $row['id'], 'guid' => $row['guid'], 'callstack' => \Friendica\Core\System::callstack()]);
814                 DBA::delete('item', ['id' => $row['id']]);
815         }
816         DBA::close($r);
817
818         Logger::notice('twitter_expire: expire_start');
819
820         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
821         if (DBA::isResult($r)) {
822                 foreach ($r as $rr) {
823                         Logger::notice('twitter_expire', ['user' => $rr['uid']]);
824                         Item::expire($rr['uid'], $days, Protocol::TWITTER, true);
825                 }
826         }
827
828         Logger::notice('twitter_expire: expire_end');
829 }
830
831 function twitter_prepare_body(App $a, array &$b)
832 {
833         if ($b["item"]["network"] != Protocol::TWITTER) {
834                 return;
835         }
836
837         if ($b["preview"]) {
838                 $max_char = 280;
839                 $item = $b["item"];
840                 $item["plink"] = DI::baseUrl()->get() . "/display/" . $item["guid"];
841
842                 $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
843                 $orig_post = Item::selectFirst(['author-link'], $condition);
844                 if (DBA::isResult($orig_post)) {
845                         $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
846                         $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
847                         $nicknameplain = "@" . $nicknameplain;
848
849                         if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
850                                 $item["body"] = $nickname . " " . $item["body"];
851                         }
852                 }
853
854                 $msgarr = ItemContent::getPlaintextPost($item, $max_char, true, 8);
855                 $msg = $msgarr["text"];
856
857                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
858                         $msg .= " " . $msgarr["url"];
859                 }
860
861                 if (isset($msgarr["image"])) {
862                         $msg .= " " . $msgarr["image"];
863                 }
864
865                 $b['html'] = nl2br(htmlspecialchars($msg));
866         }
867 }
868
869 /**
870  * @brief Build the item array for the mirrored post
871  *
872  * @param App $a Application class
873  * @param integer $uid User id
874  * @param object $post Twitter object with the post
875  *
876  * @return array item data to be posted
877  */
878 function twitter_do_mirrorpost(App $a, $uid, $post)
879 {
880         $datarray['api_source'] = true;
881         $datarray['profile_uid'] = $uid;
882         $datarray['extid'] = Protocol::TWITTER;
883         $datarray['message_id'] = Item::newURI($uid, Protocol::TWITTER . ':' . $post->id);
884         $datarray['protocol'] = Conversation::PARCEL_TWITTER;
885         $datarray['source'] = json_encode($post);
886         $datarray['title'] = '';
887
888         if (!empty($post->retweeted_status)) {
889                 // We don't support nested shares, so we mustn't show quotes as shares on retweets
890                 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
891
892                 if (empty($item['body'])) {
893                         return [];
894                 }
895
896                 $datarray['body'] = "\n" . share_header(
897                         $item['author-name'],
898                         $item['author-link'],
899                         $item['author-avatar'],
900                         '',
901                         $item['created'],
902                         $item['plink']
903                 );
904
905                 $datarray['body'] .= $item['body'] . '[/share]';
906         } else {
907                 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
908
909                 if (empty($item['body'])) {
910                         return [];
911                 }
912
913                 $datarray['body'] = $item['body'];
914         }
915
916         $datarray['source'] = $item['app'];
917         $datarray['verb'] = $item['verb'];
918
919         if (isset($item['location'])) {
920                 $datarray['location'] = $item['location'];
921         }
922
923         if (isset($item['coord'])) {
924                 $datarray['coord'] = $item['coord'];
925         }
926
927         return $datarray;
928 }
929
930 function twitter_fetchtimeline(App $a, $uid)
931 {
932         $ckey    = DI::config()->get('twitter', 'consumerkey');
933         $csecret = DI::config()->get('twitter', 'consumersecret');
934         $otoken  = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
935         $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
936         $lastid  = DI::pConfig()->get($uid, 'twitter', 'lastid');
937
938         $application_name = DI::config()->get('twitter', 'application_name');
939
940         if ($application_name == "") {
941                 $application_name = DI::baseUrl()->getHostname();
942         }
943
944         $has_picture = false;
945
946         require_once 'mod/item.php';
947         require_once 'mod/share.php';
948
949         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
950
951         $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
952
953         $first_time = ($lastid == "");
954
955         if ($lastid != "") {
956                 $parameters["since_id"] = $lastid;
957         }
958
959         try {
960                 $items = $connection->get('statuses/user_timeline', $parameters);
961         } catch (TwitterOAuthException $e) {
962                 Logger::notice('Error fetching timeline', ['user' => $uid, 'message' => $e->getMessage()]);
963                 return;
964         }
965
966         if (!is_array($items)) {
967                 Logger::notice('No items', ['user' => $uid]);
968                 return;
969         }
970
971         $posts = array_reverse($items);
972
973         Logger::log('Starting from ID ' . $lastid . ' for user ' . $uid, Logger::DEBUG);
974
975         if (count($posts)) {
976                 foreach ($posts as $post) {
977                         if ($post->id_str > $lastid) {
978                                 $lastid = $post->id_str;
979                                 DI::pConfig()->set($uid, 'twitter', 'lastid', $lastid);
980                         }
981
982                         if ($first_time) {
983                                 continue;
984                         }
985
986                         if (!stristr($post->source, $application_name)) {
987                                 $_SESSION["authenticated"] = true;
988                                 $_SESSION["uid"] = $uid;
989
990                                 Logger::log('Preparing Twitter ID ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
991
992                                 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
993
994                                 if (empty($_REQUEST['body'])) {
995                                         continue;
996                                 }
997
998                                 Logger::log('Posting Twitter ID ' . $post->id_str . ' for user ' . $uid);
999
1000                                 item_post($a);
1001                         }
1002                 }
1003         }
1004         DI::pConfig()->set($uid, 'twitter', 'lastid', $lastid);
1005         Logger::log('Last ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1006 }
1007
1008 function twitter_fix_avatar($avatar)
1009 {
1010         $new_avatar = str_replace("_normal.", ".", $avatar);
1011
1012         $info = Images::getInfoFromURLCached($new_avatar);
1013         if (!$info) {
1014                 $new_avatar = $avatar;
1015         }
1016
1017         return $new_avatar;
1018 }
1019
1020 function twitter_get_relation($uid, $target, $contact = [])
1021 {
1022         if (isset($contact['rel'])) {
1023                 $relation = $contact['rel'];
1024         } else {
1025                 $relation = 0;
1026         }
1027
1028         $ckey = DI::config()->get('twitter', 'consumerkey');
1029         $csecret = DI::config()->get('twitter', 'consumersecret');
1030         $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
1031         $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
1032         $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
1033
1034         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1035         $parameters = ['source_id' => $own_id, 'target_screen_name' => $target];
1036
1037         try {
1038                 $status = $connection->get('friendships/show', $parameters);
1039         } catch (TwitterOAuthException $e) {
1040                 Logger::info('Error fetching friendship status', ['user' => $uid, 'target' => $target, 'message' => $e->getMessage()]);
1041                 return $relation;
1042         }
1043
1044         $following = $status->relationship->source->following;
1045         $followed = $status->relationship->source->followed_by;
1046
1047         if ($following && !$followed) {
1048                 $relation = Contact::SHARING;
1049         } elseif (!$following && $followed) {
1050                 $relation = Contact::FOLLOWER;
1051         } elseif ($following && $followed) {
1052                 $relation = Contact::FRIEND;
1053         } elseif (!$following && !$followed) {
1054                 $relation = 0;
1055         }
1056
1057         Logger::info('Fetched friendship relation', ['user' => $uid, 'target' => $target, 'relation' => $relation]);
1058
1059         return $relation;
1060 }
1061
1062 function twitter_fetch_contact($uid, $data, $create_user)
1063 {
1064         if (empty($data->id_str)) {
1065                 return -1;
1066         }
1067
1068         $avatar = twitter_fix_avatar($data->profile_image_url_https);
1069         $url = "https://twitter.com/" . $data->screen_name;
1070         $addr = $data->screen_name . "@twitter.com";
1071
1072         $fields = ['url' => $url, 'network' => Protocol::TWITTER,
1073                 'alias' => 'twitter::' . $data->id_str,
1074                 'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
1075                 'location' => $data->location, 'about' => $data->description];
1076
1077         // Update the public contact
1078         $pcontact = DBA::selectFirst('contact', ['id'], ['uid' => 0, 'alias' => "twitter::" . $data->id_str]);
1079         if (DBA::isResult($pcontact)) {
1080                 $cid = $pcontact['id'];
1081         } else {
1082                 $cid = Contact::getIdForURL($url, 0, true, $fields);
1083         }
1084
1085         if (!empty($cid)) {
1086                 DBA::update('contact', $fields, ['id' => $cid]);
1087                 Contact::updateAvatar($avatar, 0, $cid);
1088         }
1089
1090         $contact = DBA::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
1091         if (!DBA::isResult($contact) && !$create_user) {
1092                 Logger::info('User contact not found', ['uid' => $uid, 'twitter-id' => $data->id_str]);
1093                 return 0;
1094         }
1095
1096         if (!DBA::isResult($contact)) {
1097                 $relation = twitter_get_relation($uid, $data->screen_name);
1098
1099                 // create contact record
1100                 $fields['uid'] = $uid;
1101                 $fields['created'] = DateTimeFormat::utcNow();
1102                 $fields['nurl'] = Strings::normaliseLink($url);
1103                 $fields['poll'] = 'twitter::' . $data->id_str;
1104                 $fields['rel'] = $relation;
1105                 $fields['priority'] = 1;
1106                 $fields['writable'] = true;
1107                 $fields['blocked'] = false;
1108                 $fields['readonly'] = false;
1109                 $fields['pending'] = false;
1110
1111                 if (!DBA::insert('contact', $fields)) {
1112                         return false;
1113                 }
1114
1115                 $contact_id = DBA::lastInsertId();
1116
1117                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1118
1119                 Contact::updateAvatar($avatar, $uid, $contact_id);
1120         } else {
1121                 if ($contact["readonly"] || $contact["blocked"]) {
1122                         Logger::log("twitter_fetch_contact: Contact '" . $contact["nick"] . "' is blocked or readonly.", Logger::DEBUG);
1123                         return -1;
1124                 }
1125
1126                 $contact_id = $contact['id'];
1127                 $update = false;
1128
1129                 // Update the contact relation once per day
1130                 if ($contact['updated'] < DateTimeFormat::utc('now -24 hours')) {
1131                         $fields['rel'] = twitter_get_relation($uid, $data->screen_name, $contact);
1132                         $update = true;
1133                 }
1134
1135                 Contact::updateAvatar($avatar, $uid, $contact['id']);
1136
1137                 if ($contact['name'] != $data->name) {
1138                         $fields['name-date'] = $fields['uri-date'] = DateTimeFormat::utcNow();
1139                         $update = true;
1140                 }
1141
1142                 if ($contact['nick'] != $data->screen_name) {
1143                         $fields['uri-date'] = DateTimeFormat::utcNow();
1144                         $update = true;
1145                 }
1146
1147                 if (($contact['location'] != $data->location) || ($contact['about'] != $data->description)) {
1148                         $update = true;
1149                 }
1150
1151                 if ($update) {
1152                         $fields['updated'] = DateTimeFormat::utcNow();
1153                         DBA::update('contact', $fields, ['id' => $contact['id']]);
1154                         Logger::info('Updated contact', ['id' => $contact['id'], 'nick' => $data->screen_name]);
1155                 }
1156         }
1157
1158         return $contact_id;
1159 }
1160
1161 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1162 {
1163         $ckey = DI::config()->get('twitter', 'consumerkey');
1164         $csecret = DI::config()->get('twitter', 'consumersecret');
1165         $otoken = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
1166         $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
1167
1168         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1169                 intval($uid));
1170
1171         if (DBA::isResult($r)) {
1172                 $self = $r[0];
1173         } else {
1174                 return;
1175         }
1176
1177         $parameters = [];
1178
1179         if ($screen_name != "") {
1180                 $parameters["screen_name"] = $screen_name;
1181         }
1182
1183         if ($user_id != "") {
1184                 $parameters["user_id"] = $user_id;
1185         }
1186
1187         // Fetching user data
1188         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1189         try {
1190                 $user = $connection->get('users/show', $parameters);
1191         } catch (TwitterOAuthException $e) {
1192                 Logger::log('twitter_fetchuser: Error fetching user ' . $uid . ': ' . $e->getMessage());
1193                 return;
1194         }
1195
1196         if (!is_object($user)) {
1197                 return;
1198         }
1199
1200         $contact_id = twitter_fetch_contact($uid, $user, true);
1201
1202         return $contact_id;
1203 }
1204
1205 function twitter_expand_entities(App $a, $body, $item, $picture)
1206 {
1207         $plain = $body;
1208
1209         $tags_arr = [];
1210
1211         foreach ($item->entities->hashtags AS $hashtag) {
1212                 $url = '#[url=' . DI::baseUrl()->get() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
1213                 $tags_arr['#' . $hashtag->text] = $url;
1214                 $body = str_replace('#' . $hashtag->text, $url, $body);
1215         }
1216
1217         foreach ($item->entities->user_mentions AS $mention) {
1218                 $url = '@[url=https://twitter.com/' . rawurlencode($mention->screen_name) . ']' . $mention->screen_name . '[/url]';
1219                 $tags_arr['@' . $mention->screen_name] = $url;
1220                 $body = str_replace('@' . $mention->screen_name, $url, $body);
1221         }
1222
1223         if (isset($item->entities->urls)) {
1224                 $type = '';
1225                 $footerurl = '';
1226                 $footerlink = '';
1227                 $footer = '';
1228
1229                 foreach ($item->entities->urls as $url) {
1230                         $plain = str_replace($url->url, '', $plain);
1231
1232                         if ($url->url && $url->expanded_url && $url->display_url) {
1233                                 // Quote tweet, we just remove the quoted tweet URL from the body, the share block will be added later.
1234                                 if (!empty($item->quoted_status) && isset($item->quoted_status_id_str)
1235                                         && substr($url->expanded_url, -strlen($item->quoted_status_id_str)) == $item->quoted_status_id_str ) {
1236                                         $body = str_replace($url->url, '', $body);
1237                                         continue;
1238                                 }
1239
1240                                 $expanded_url = $url->expanded_url;
1241
1242                                 $final_url = Network::finalUrl($url->expanded_url);
1243
1244                                 $oembed_data = OEmbed::fetchURL($final_url);
1245
1246                                 if (empty($oembed_data) || empty($oembed_data->type)) {
1247                                         continue;
1248                                 }
1249
1250                                 // Quickfix: Workaround for URL with '[' and ']' in it
1251                                 if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
1252                                         $expanded_url = $url->url;
1253                                 }
1254
1255                                 if ($type == '') {
1256                                         $type = $oembed_data->type;
1257                                 }
1258
1259                                 if ($oembed_data->type == 'video') {
1260                                         $type = $oembed_data->type;
1261                                         $footerurl = $expanded_url;
1262                                         $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1263
1264                                         $body = str_replace($url->url, $footerlink, $body);
1265                                 } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
1266                                         $body = str_replace($url->url, '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]', $body);
1267                                 } elseif ($oembed_data->type != 'link') {
1268                                         $body = str_replace($url->url, '[url=' . $expanded_url . ']' . $url->display_url . '[/url]', $body);
1269                                 } else {
1270                                         $img_str = Network::fetchUrl($final_url, true, 4);
1271
1272                                         $tempfile = tempnam(get_temppath(), 'cache');
1273                                         file_put_contents($tempfile, $img_str);
1274
1275                                         // See http://php.net/manual/en/function.exif-imagetype.php#79283
1276                                         if (filesize($tempfile) > 11) {
1277                                                 $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1278                                         } else {
1279                                                 $mime = false;
1280                                         }
1281
1282                                         unlink($tempfile);
1283
1284                                         if (substr($mime, 0, 6) == 'image/') {
1285                                                 $type = 'photo';
1286                                                 $body = str_replace($url->url, '[img]' . $final_url . '[/img]', $body);
1287                                         } else {
1288                                                 $type = $oembed_data->type;
1289                                                 $footerurl = $expanded_url;
1290                                                 $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1291
1292                                                 $body = str_replace($url->url, $footerlink, $body);
1293                                         }
1294                                 }
1295                         }
1296                 }
1297
1298                 // Footer will be taken care of with a share block in the case of a quote
1299                 if (empty($item->quoted_status)) {
1300                         if ($footerurl != '') {
1301                                 $footer = add_page_info($footerurl, false, $picture);
1302                         }
1303
1304                         if (($footerlink != '') && (trim($footer) != '')) {
1305                                 $removedlink = trim(str_replace($footerlink, '', $body));
1306
1307                                 if (($removedlink == '') || strstr($body, $removedlink)) {
1308                                         $body = $removedlink;
1309                                 }
1310
1311                                 $body .= $footer;
1312                         }
1313
1314                         if ($footer == '' && $picture != '') {
1315                                 $body .= "\n\n[img]" . $picture . "[/img]\n";
1316                         } elseif ($footer == '' && $picture == '') {
1317                                 $body = add_page_info_to_body($body);
1318                         }
1319                 }
1320         }
1321
1322         // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1323         $tags = BBCode::getTags($body);
1324
1325         if (count($tags)) {
1326                 foreach ($tags as $tag) {
1327                         if (strstr(trim($tag), ' ')) {
1328                                 continue;
1329                         }
1330
1331                         if (strpos($tag, '#') === 0) {
1332                                 if (strpos($tag, '[url=')) {
1333                                         continue;
1334                                 }
1335
1336                                 // don't link tags that are already embedded in links
1337                                 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1338                                         continue;
1339                                 }
1340                                 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1341                                         continue;
1342                                 }
1343
1344                                 $basetag = str_replace('_', ' ', substr($tag, 1));
1345                                 $url = '#[url=' . DI::baseUrl()->get() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
1346                                 $body = str_replace($tag, $url, $body);
1347                                 $tags_arr['#' . $basetag] = $url;
1348                         } elseif (strpos($tag, '@') === 0) {
1349                                 if (strpos($tag, '[url=')) {
1350                                         continue;
1351                                 }
1352
1353                                 $basetag = substr($tag, 1);
1354                                 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1355                                 $body = str_replace($tag, $url, $body);
1356                                 $tags_arr['@' . $basetag] = $url;
1357                         }
1358                 }
1359         }
1360
1361         $tags = implode($tags_arr, ',');
1362
1363         return ['body' => $body, 'tags' => $tags, 'plain' => $plain];
1364 }
1365
1366 /**
1367  * @brief Fetch media entities and add media links to the body
1368  *
1369  * @param object $post Twitter object with the post
1370  * @param array $postarray Array of the item that is about to be posted
1371  *
1372  * @return $picture string Image URL or empty string
1373  */
1374 function twitter_media_entities($post, array &$postarray)
1375 {
1376         // There are no media entities? So we quit.
1377         if (empty($post->extended_entities->media)) {
1378                 return '';
1379         }
1380
1381         // When the post links to an external page, we only take one picture.
1382         // We only do this when there is exactly one media.
1383         if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1384                 $medium = $post->extended_entities->media[0];
1385                 $picture = '';
1386                 foreach ($post->entities->urls as $link) {
1387                         // Let's make sure the external link url matches the media url
1388                         if ($medium->url == $link->url && isset($medium->media_url_https)) {
1389                                 $picture = $medium->media_url_https;
1390                                 $postarray['body'] = str_replace($medium->url, '', $postarray['body']);
1391                                 return $picture;
1392                         }
1393                 }
1394         }
1395
1396         // This is a pure media post, first search for all media urls
1397         $media = [];
1398         foreach ($post->extended_entities->media AS $medium) {
1399                 if (!isset($media[$medium->url])) {
1400                         $media[$medium->url] = '';
1401                 }
1402                 switch ($medium->type) {
1403                         case 'photo':
1404                                 if (!empty($medium->ext_alt_text)) {
1405                                         Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
1406                                         $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
1407                                 } else {
1408                                         $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1409                                 }
1410
1411                                 $postarray['object-type'] = Activity\ObjectType::IMAGE;
1412                                 break;
1413                         case 'video':
1414                         case 'animated_gif':
1415                                 if (!empty($medium->ext_alt_text)) {
1416                                         Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
1417                                         $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
1418                                 } else {
1419                                         $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1420                                 }
1421
1422                                 $postarray['object-type'] = Activity\ObjectType::VIDEO;
1423                                 if (is_array($medium->video_info->variants)) {
1424                                         $bitrate = 0;
1425                                         // We take the video with the highest bitrate
1426                                         foreach ($medium->video_info->variants AS $variant) {
1427                                                 if (($variant->content_type == 'video/mp4') && ($variant->bitrate >= $bitrate)) {
1428                                                         $media[$medium->url] = "\n[video]" . $variant->url . '[/video]';
1429                                                         $bitrate = $variant->bitrate;
1430                                                 }
1431                                         }
1432                                 }
1433                                 break;
1434                         // The following code will only be activated for test reasons
1435                         //default:
1436                         //      $postarray['body'] .= print_r($medium, true);
1437                 }
1438         }
1439
1440         // Now we replace the media urls.
1441         foreach ($media AS $key => $value) {
1442                 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1443         }
1444
1445         return '';
1446 }
1447
1448 function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $only_existing_contact, $noquote)
1449 {
1450         $postarray = [];
1451         $postarray['network'] = Protocol::TWITTER;
1452         $postarray['uid'] = $uid;
1453         $postarray['wall'] = 0;
1454         $postarray['uri'] = "twitter::" . $post->id_str;
1455         $postarray['protocol'] = Conversation::PARCEL_TWITTER;
1456         $postarray['source'] = json_encode($post);
1457
1458         // Don't import our own comments
1459         if (Item::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
1460                 Logger::log("Item with extid " . $postarray['uri'] . " found.", Logger::DEBUG);
1461                 return [];
1462         }
1463
1464         $contactid = 0;
1465
1466         if ($post->in_reply_to_status_id_str != "") {
1467                 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1468
1469                 $fields = ['uri', 'parent-uri', 'parent'];
1470                 $parent_item = Item::selectFirst($fields, ['uri' => $parent, 'uid' => $uid]);
1471                 if (!DBA::isResult($parent_item)) {
1472                         $parent_item = Item::selectFirst($fields, ['extid' => $parent, 'uid' => $uid]);
1473                 }
1474
1475                 if (DBA::isResult($parent_item)) {
1476                         $postarray['thr-parent'] = $parent_item['uri'];
1477                         $postarray['parent-uri'] = $parent_item['parent-uri'];
1478                         $postarray['parent'] = $parent_item['parent'];
1479                         $postarray['object-type'] = Activity\ObjectType::COMMENT;
1480                 } else {
1481                         $postarray['thr-parent'] = $postarray['uri'];
1482                         $postarray['parent-uri'] = $postarray['uri'];
1483                         $postarray['object-type'] = Activity\ObjectType::NOTE;
1484                 }
1485
1486                 // Is it me?
1487                 $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
1488
1489                 if ($post->user->id_str == $own_id) {
1490                         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1491                                 intval($uid));
1492
1493                         if (DBA::isResult($r)) {
1494                                 $contactid = $r[0]["id"];
1495
1496                                 $postarray['owner-name']   = $r[0]["name"];
1497                                 $postarray['owner-link']   = $r[0]["url"];
1498                                 $postarray['owner-avatar'] = $r[0]["photo"];
1499                         } else {
1500                                 Logger::log("No self contact for user " . $uid, Logger::DEBUG);
1501                                 return [];
1502                         }
1503                 }
1504                 // Don't create accounts of people who just comment something
1505                 $create_user = false;
1506         } else {
1507                 $postarray['parent-uri'] = $postarray['uri'];
1508                 $postarray['object-type'] = Activity\ObjectType::NOTE;
1509         }
1510
1511         if ($contactid == 0) {
1512                 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1513
1514                 $postarray['owner-name'] = $post->user->name;
1515                 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1516                 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1517         }
1518
1519         if (($contactid == 0) && !$only_existing_contact) {
1520                 $contactid = $self['id'];
1521         } elseif ($contactid <= 0) {
1522                 Logger::log("Contact ID is zero or less than zero.", Logger::DEBUG);
1523                 return [];
1524         }
1525
1526         $postarray['contact-id'] = $contactid;
1527
1528         $postarray['verb'] = Activity::POST;
1529         $postarray['author-name'] = $postarray['owner-name'];
1530         $postarray['author-link'] = $postarray['owner-link'];
1531         $postarray['author-avatar'] = $postarray['owner-avatar'];
1532         $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1533         $postarray['app'] = strip_tags($post->source);
1534
1535         if ($post->user->protected) {
1536                 $postarray['private'] = 1;
1537                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1538         } else {
1539                 $postarray['private'] = 0;
1540                 $postarray['allow_cid'] = '';
1541         }
1542
1543         if (!empty($post->full_text)) {
1544                 $postarray['body'] = $post->full_text;
1545         } else {
1546                 $postarray['body'] = $post->text;
1547         }
1548
1549         // When the post contains links then use the correct object type
1550         if (count($post->entities->urls) > 0) {
1551                 $postarray['object-type'] = Activity\ObjectType::BOOKMARK;
1552         }
1553
1554         // Search for media links
1555         $picture = twitter_media_entities($post, $postarray);
1556
1557         $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1558         $postarray['body'] = $converted["body"];
1559         $postarray['tag'] = $converted["tags"];
1560         $postarray['created'] = DateTimeFormat::utc($post->created_at);
1561         $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1562
1563         $statustext = $converted["plain"];
1564
1565         if (!empty($post->place->name)) {
1566                 $postarray["location"] = $post->place->name;
1567         }
1568         if (!empty($post->place->full_name)) {
1569                 $postarray["location"] = $post->place->full_name;
1570         }
1571         if (!empty($post->geo->coordinates)) {
1572                 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1573         }
1574         if (!empty($post->coordinates->coordinates)) {
1575                 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1576         }
1577         if (!empty($post->retweeted_status)) {
1578                 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1579
1580                 if (empty($retweet['body'])) {
1581                         return [];
1582                 }
1583
1584                 if (!$noquote) {
1585                         // Store the original tweet
1586                         Item::insert($retweet);
1587
1588                         // CHange the other post into a reshare activity
1589                         $postarray['verb'] = Activity::ANNOUNCE;
1590                         $postarray['gravity'] = GRAVITY_ACTIVITY;
1591                         $postarray['object-type'] = Activity\ObjectType::NOTE;
1592
1593                         $postarray['thr-parent'] = $retweet['uri'];
1594                         $postarray['parent-uri'] = $retweet['uri'];
1595                 } else {
1596                         $retweet['source'] = $postarray['source'];
1597                         $retweet['private'] = $postarray['private'];
1598                         $retweet['allow_cid'] = $postarray['allow_cid'];
1599                         $retweet['contact-id'] = $postarray['contact-id'];
1600                         $retweet['owner-name'] = $postarray['owner-name'];
1601                         $retweet['owner-link'] = $postarray['owner-link'];
1602                         $retweet['owner-avatar'] = $postarray['owner-avatar'];
1603
1604                         $postarray = $retweet;
1605                 }
1606         }
1607
1608         if (!empty($post->quoted_status) && !$noquote) {
1609                 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1610
1611                 if (!empty($quoted['body'])) {
1612                         $postarray['body'] .= "\n" . share_header(
1613                                 $quoted['author-name'],
1614                                 $quoted['author-link'],
1615                                 $quoted['author-avatar'],
1616                                 "",
1617                                 $quoted['created'],
1618                                 $quoted['plink']
1619                         );
1620
1621                         $postarray['body'] .= $quoted['body'] . '[/share]';
1622                 } else {
1623                         // Quoted post author is blocked/ignored, so we just provide the link to avoid removing quote context.
1624                         $postarray['body'] .= "\n\nhttps://twitter.com/" . $post->quoted_status->user->screen_name . "/status/" . $post->quoted_status->id_str;
1625                 }
1626         }
1627
1628         return $postarray;
1629 }
1630
1631 function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, array $self)
1632 {
1633         Logger::log("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, Logger::DEBUG);
1634
1635         $posts = [];
1636
1637         while (!empty($post->in_reply_to_status_id_str)) {
1638                 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str, "include_ext_alt_text" => true];
1639
1640                 try {
1641                         $post = $connection->get('statuses/show', $parameters);
1642                 } catch (TwitterOAuthException $e) {
1643                         Logger::log('twitter_fetchparentposts: Error fetching for user ' . $uid . ' and post ' . $post->id_str . ': ' . $e->getMessage());
1644                         break;
1645                 }
1646
1647                 if (empty($post)) {
1648                         Logger::log("twitter_fetchparentposts: Can't fetch post " . $parameters['id'], Logger::DEBUG);
1649                         break;
1650                 }
1651
1652                 if (empty($post->id_str)) {
1653                         Logger::log("twitter_fetchparentposts: This is not a post " . json_encode($post), Logger::DEBUG);
1654                         break;
1655                 }
1656
1657                 if (Item::exists(['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
1658                         break;
1659                 }
1660
1661                 $posts[] = $post;
1662         }
1663
1664         Logger::log("twitter_fetchparentposts: Fetching " . count($posts) . " parents", Logger::DEBUG);
1665
1666         $posts = array_reverse($posts);
1667
1668         if (!empty($posts)) {
1669                 foreach ($posts as $post) {
1670                         $postarray = twitter_createpost($a, $uid, $post, $self, false, !DI::pConfig()->get($uid, 'twitter', 'create_user'), false);
1671
1672                         if (empty($postarray['body'])) {
1673                                 continue;
1674                         }
1675
1676                         $item = Item::insert($postarray);
1677
1678                         $postarray["id"] = $item;
1679
1680                         Logger::log('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1681                 }
1682         }
1683 }
1684
1685 function twitter_fetchhometimeline(App $a, $uid)
1686 {
1687         $ckey    = DI::config()->get('twitter', 'consumerkey');
1688         $csecret = DI::config()->get('twitter', 'consumersecret');
1689         $otoken  = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
1690         $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
1691         $create_user = DI::pConfig()->get($uid, 'twitter', 'create_user');
1692         $mirror_posts = DI::pConfig()->get($uid, 'twitter', 'mirror_posts');
1693
1694         Logger::log("Fetching timeline for user " . $uid, Logger::DEBUG);
1695
1696         $application_name = DI::config()->get('twitter', 'application_name');
1697
1698         if ($application_name == "") {
1699                 $application_name = DI::baseUrl()->getHostname();
1700         }
1701
1702         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1703
1704         try {
1705                 $own_contact = twitter_fetch_own_contact($a, $uid);
1706         } catch (TwitterOAuthException $e) {
1707                 Logger::log('Error fetching own contact for user ' . $uid . ': ' . $e->getMessage());
1708                 return;
1709         }
1710
1711         $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1712                 intval($own_contact),
1713                 intval($uid));
1714
1715         if (DBA::isResult($r)) {
1716                 $own_id = $r[0]["nick"];
1717         } else {
1718                 Logger::log("Own twitter contact not found for user " . $uid);
1719                 return;
1720         }
1721
1722         $self = User::getOwnerDataById($uid);
1723         if ($self === false) {
1724                 Logger::log("Own contact not found for user " . $uid);
1725                 return;
1726         }
1727
1728         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
1729         //$parameters["count"] = 200;
1730         // Fetching timeline
1731         $lastid = DI::pConfig()->get($uid, 'twitter', 'lasthometimelineid');
1732
1733         $first_time = ($lastid == "");
1734
1735         if ($lastid != "") {
1736                 $parameters["since_id"] = $lastid;
1737         }
1738
1739         try {
1740                 $items = $connection->get('statuses/home_timeline', $parameters);
1741         } catch (TwitterOAuthException $e) {
1742                 Logger::log('Error fetching home timeline for user ' . $uid . ': ' . $e->getMessage());
1743                 return;
1744         }
1745
1746         if (!is_array($items)) {
1747                 Logger::log('No array while fetching home timeline for user ' . $uid . ': ' . print_r($items, true));
1748                 return;
1749         }
1750
1751         if (empty($items)) {
1752                 Logger::log('No new timeline content for user ' . $uid, Logger::INFO);
1753                 return;
1754         }
1755
1756         $posts = array_reverse($items);
1757
1758         Logger::log('Fetching timeline from ID ' . $lastid . ' for user ' . $uid . ' ' . sizeof($posts) . ' items', Logger::DEBUG);
1759
1760         if (count($posts)) {
1761                 foreach ($posts as $post) {
1762                         if ($post->id_str > $lastid) {
1763                                 $lastid = $post->id_str;
1764                                 DI::pConfig()->set($uid, 'twitter', 'lasthometimelineid', $lastid);
1765                         }
1766
1767                         if ($first_time) {
1768                                 continue;
1769                         }
1770
1771                         if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1772                                 Logger::log("Skip previously sent post", Logger::DEBUG);
1773                                 continue;
1774                         }
1775
1776                         if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1777                                 Logger::log("Skip post that will be mirrored", Logger::DEBUG);
1778                                 continue;
1779                         }
1780
1781                         if ($post->in_reply_to_status_id_str != "") {
1782                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1783                         }
1784
1785                         Logger::log('Preparing post ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
1786
1787                         $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1788
1789                         if (empty($postarray['body']) || trim($postarray['body']) == "") {
1790                                 Logger::log('Empty body for post ' . $post->id_str . ' and user ' . $uid, Logger::DEBUG);
1791                                 continue;
1792                         }
1793
1794                         $notify = false;
1795
1796                         if (($postarray['uri'] == $postarray['parent-uri']) && ($postarray['author-link'] == $postarray['owner-link'])) {
1797                                 $contact = DBA::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
1798                                 if (DBA::isResult($contact)) {
1799                                         $notify = Item::isRemoteSelf($contact, $postarray);
1800                                 }
1801                         }
1802
1803                         $item = Item::insert($postarray, false, $notify);
1804                         $postarray["id"] = $item;
1805
1806                         Logger::log('User ' . $uid . ' posted home timeline item ' . $item);
1807                 }
1808         }
1809         DI::pConfig()->set($uid, 'twitter', 'lasthometimelineid', $lastid);
1810
1811         Logger::log('Last timeline ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1812
1813         // Fetching mentions
1814         $lastid = DI::pConfig()->get($uid, 'twitter', 'lastmentionid');
1815
1816         $first_time = ($lastid == "");
1817
1818         if ($lastid != "") {
1819                 $parameters["since_id"] = $lastid;
1820         }
1821
1822         try {
1823                 $items = $connection->get('statuses/mentions_timeline', $parameters);
1824         } catch (TwitterOAuthException $e) {
1825                 Logger::log('Error fetching mentions: ' . $e->getMessage());
1826                 return;
1827         }
1828
1829         if (!is_array($items)) {
1830                 Logger::log("Error fetching mentions: " . print_r($items, true), Logger::DEBUG);
1831                 return;
1832         }
1833
1834         $posts = array_reverse($items);
1835
1836         Logger::log("Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", Logger::DEBUG);
1837
1838         if (count($posts)) {
1839                 foreach ($posts as $post) {
1840                         if ($post->id_str > $lastid) {
1841                                 $lastid = $post->id_str;
1842                         }
1843
1844                         if ($first_time) {
1845                                 continue;
1846                         }
1847
1848                         if ($post->in_reply_to_status_id_str != "") {
1849                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1850                         }
1851
1852                         $postarray = twitter_createpost($a, $uid, $post, $self, false, !$create_user, false);
1853
1854                         if (empty($postarray['body'])) {
1855                                 continue;
1856                         }
1857
1858                         $item = Item::insert($postarray);
1859
1860                         Logger::log('User ' . $uid . ' posted mention timeline item ' . $item);
1861                 }
1862         }
1863
1864         DI::pConfig()->set($uid, 'twitter', 'lastmentionid', $lastid);
1865
1866         Logger::log('Last mentions ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1867 }
1868
1869 function twitter_fetch_own_contact(App $a, $uid)
1870 {
1871         $ckey    = DI::config()->get('twitter', 'consumerkey');
1872         $csecret = DI::config()->get('twitter', 'consumersecret');
1873         $otoken  = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
1874         $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
1875
1876         $own_id = DI::pConfig()->get($uid, 'twitter', 'own_id');
1877
1878         $contact_id = 0;
1879
1880         if ($own_id == "") {
1881                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1882
1883                 // Fetching user data
1884                 // get() may throw TwitterOAuthException, but we will catch it later
1885                 $user = $connection->get('account/verify_credentials');
1886                 if (empty($user) || empty($user->id_str)) {
1887                         return false;
1888                 }
1889
1890                 DI::pConfig()->set($uid, 'twitter', 'own_id', $user->id_str);
1891
1892                 $contact_id = twitter_fetch_contact($uid, $user, true);
1893         } else {
1894                 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1895                         intval($uid),
1896                         DBA::escape("twitter::" . $own_id));
1897                 if (DBA::isResult($r)) {
1898                         $contact_id = $r[0]["id"];
1899                 } else {
1900                         DI::pConfig()->delete($uid, 'twitter', 'own_id');
1901                 }
1902         }
1903
1904         return $contact_id;
1905 }
1906
1907 function twitter_is_retweet(App $a, $uid, $body)
1908 {
1909         $body = trim($body);
1910
1911         // Skip if it isn't a pure repeated messages
1912         // Does it start with a share?
1913         if (strpos($body, "[share") > 0) {
1914                 return false;
1915         }
1916
1917         // Does it end with a share?
1918         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1919                 return false;
1920         }
1921
1922         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1923         // Skip if there is no shared message in there
1924         if ($body == $attributes) {
1925                 return false;
1926         }
1927
1928         $link = "";
1929         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1930         if (!empty($matches[1])) {
1931                 $link = $matches[1];
1932         }
1933
1934         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1935         if (!empty($matches[1])) {
1936                 $link = $matches[1];
1937         }
1938
1939         $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1940         if ($id == $link) {
1941                 return false;
1942         }
1943
1944         Logger::log('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, Logger::DEBUG);
1945
1946         $ckey    = DI::config()->get('twitter', 'consumerkey');
1947         $csecret = DI::config()->get('twitter', 'consumersecret');
1948         $otoken  = DI::pConfig()->get($uid, 'twitter', 'oauthtoken');
1949         $osecret = DI::pConfig()->get($uid, 'twitter', 'oauthsecret');
1950
1951         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1952         $result = $connection->post('statuses/retweet/' . $id);
1953
1954         Logger::log('twitter_is_retweet: result ' . print_r($result, true), Logger::DEBUG);
1955
1956         return !isset($result->errors);
1957 }
1958
1959 function twitter_update_mentions($body)
1960 {
1961         $URLSearchString = "^\[\]";
1962         $return = preg_replace_callback(
1963                 "/@\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1964                 function ($matches) {
1965                         if (strpos($matches[1], 'twitter.com')) {
1966                                 $return = '@' . substr($matches[1], strrpos($matches[1], '/') + 1);
1967                         } else {
1968                                 $return = $matches[2] . ' (' . $matches[1] . ')';
1969                         }
1970
1971                         return $return;
1972                 },
1973                 $body
1974         );
1975
1976         return $return;
1977 }
1978
1979 function twitter_convert_share(array $attributes, array $author_contact, $content, $is_quote_share)
1980 {
1981         if ($author_contact['network'] == Protocol::TWITTER) {
1982                 $mention = '@' . $author_contact['nick'];
1983         } else {
1984                 $mention = $author_contact['addr'];
1985         }
1986
1987         return ($is_quote_share ? "\n\n" : '' ) . 'RT ' . $mention . ': ' . $content . "\n\n" . $attributes['link'];
1988 }