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