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