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