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