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