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