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