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