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