]> git.mxchange.org Git - friendica-addons.git/blob - twitter/twitter.php
Refactor deprecated App::internalRedirect() to DI::baseUrl()->redirect()
[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\Config;
73 use Friendica\Core\Hook;
74 use Friendica\Core\L10n;
75 use Friendica\Core\Logger;
76 use Friendica\Core\PConfig;
77 use Friendica\Core\Protocol;
78 use Friendica\Core\Renderer;
79 use Friendica\Core\Worker;
80 use Friendica\Database\DBA;
81 use Friendica\DI;
82 use Friendica\Model\Contact;
83 use Friendica\Model\Conversation;
84 use Friendica\Model\Group;
85 use Friendica\Model\Item;
86 use Friendica\Model\ItemContent;
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 = 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 = Config::get('twitter', 'consumerkey');
172         $csecret = Config::get('twitter', 'consumersecret');
173         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
174         $osecret = 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 (PConfig::get(local_user(), 'twitter', 'post')) {
203                 $jotnets_fields[] = [
204                         'type' => 'checkbox',
205                         'field' => [
206                                 'twitter_enable',
207                                 L10n::t('Post to Twitter'),
208                                 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                 PConfig::delete(local_user(), 'twitter', 'consumerkey');
231                 PConfig::delete(local_user(), 'twitter', 'consumersecret');
232                 PConfig::delete(local_user(), 'twitter', 'oauthtoken');
233                 PConfig::delete(local_user(), 'twitter', 'oauthsecret');
234                 PConfig::delete(local_user(), 'twitter', 'post');
235                 PConfig::delete(local_user(), 'twitter', 'post_by_default');
236                 PConfig::delete(local_user(), 'twitter', 'lastid');
237                 PConfig::delete(local_user(), 'twitter', 'mirror_posts');
238                 PConfig::delete(local_user(), 'twitter', 'import');
239                 PConfig::delete(local_user(), 'twitter', 'create_user');
240                 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    = Config::get('twitter', 'consumerkey');
246                         $csecret = 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(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                                 PConfig::set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
259                                 PConfig::set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
260                                 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                         PConfig::set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
272                         PConfig::set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
273                         PConfig::set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
274                         PConfig::set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
275                         PConfig::set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
276
277                         if (!intval($_POST['twitter-mirror'])) {
278                                 PConfig::delete(local_user(), 'twitter', 'lastid');
279                         }
280
281                         info(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         $a->page['htmlhead'] .= '<link rel="stylesheet"  type="text/css" href="' . $a->getBaseURL() . '/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    = Config::get('twitter', 'consumerkey');
298         $csecret = Config::get('twitter', 'consumersecret');
299         $otoken  = PConfig::get(local_user(), 'twitter', 'oauthtoken');
300         $osecret = PConfig::get(local_user(), 'twitter', 'oauthsecret');
301
302         $enabled            = intval(PConfig::get(local_user(), 'twitter', 'post'));
303         $defenabled         = intval(PConfig::get(local_user(), 'twitter', 'post_by_default'));
304         $mirrorenabled      = intval(PConfig::get(local_user(), 'twitter', 'mirror_posts'));
305         $importenabled      = intval(PConfig::get(local_user(), 'twitter', 'import'));
306         $create_userenabled = intval(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">' . 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">' . 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>' . 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>' . 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="' . L10n::t('Log in with Twitter') . '"></a>';
336                                 $s .= '<div id="twitter-pin-wrapper">';
337                                 $s .= '<label id="twitter-pin-label" for="twitter-pin">' . 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="' . L10n::t('Save Settings') . '" /></div>';
343                         } catch (TwitterOAuthException $e) {
344                                 $s .= '<p>' . 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>' . 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">' . 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', L10n::t('Allow posting to Twitter'), $enabled, 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>' . 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', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
385                                 ]);
386                                 $s .= Renderer::replaceMacros($field_checkbox, [
387                                         '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
388                                 ]);
389                                 $s .= Renderer::replaceMacros($field_checkbox, [
390                                         '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
391                                 ]);
392                                 $s .= Renderer::replaceMacros($field_checkbox, [
393                                         '$field' => ['twitter-create_user', L10n::t('Automatically create contacts'), $create_userenabled, 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="' . L10n::t('Save Settings') . '" /></div>';
397                         } catch (TwitterOAuthException $e) {
398                                 $s .= '<p>' . 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 (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(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(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 = Config::get('twitter', 'consumerkey');
478         $csecret = Config::get('twitter', 'consumersecret');
479         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
480         $osecret = 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 (!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         PConfig::load($b['uid'], 'twitter');
592
593         $ckey    = Config::get('twitter', 'consumerkey');
594         $csecret = Config::get('twitter', 'consumersecret');
595         $otoken  = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
596         $osecret = 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, 8);
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                         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         Config::set('twitter', 'consumerkey', $consumerkey);
722         Config::set('twitter', 'consumersecret', $consumersecret);
723         info(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' => L10n::t('Save Settings'),
732                 // name, label, value, help, [extra values]
733                 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
734                 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
735         ]);
736 }
737
738 function twitter_cron(App $a)
739 {
740         $last = Config::get('twitter', 'last_poll');
741
742         $poll_interval = intval(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(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 = 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                           PConfig::set($rr['uid'],'pumpio','contact_check',time());
796                           }
797                          */
798                 }
799         }
800
801         Logger::notice('twitter: cron_end');
802
803         Config::set('twitter', 'last_poll', time());
804 }
805
806 function twitter_expire(App $a)
807 {
808         $days = Config::get('twitter', 'expire');
809
810         if ($days == 0) {
811                 return;
812         }
813
814         $r = Item::select(['id'], ['deleted' => true, 'network' => Protocol::TWITTER]);
815         while ($row = DBA::fetch($r)) {
816                 DBA::delete('item', ['id' => $row['id']]);
817         }
818         DBA::close($r);
819
820         Logger::notice('twitter_expire: expire_start');
821
822         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
823         if (DBA::isResult($r)) {
824                 foreach ($r as $rr) {
825                         Logger::notice('twitter_expire', ['user' => $rr['uid']]);
826                         Item::expire($rr['uid'], $days, Protocol::TWITTER, true);
827                 }
828         }
829
830         Logger::notice('twitter_expire: expire_end');
831 }
832
833 function twitter_prepare_body(App $a, array &$b)
834 {
835         if ($b["item"]["network"] != Protocol::TWITTER) {
836                 return;
837         }
838
839         if ($b["preview"]) {
840                 $max_char = 280;
841                 $item = $b["item"];
842                 $item["plink"] = $a->getBaseURL() . "/display/" . $item["guid"];
843
844                 $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
845                 $orig_post = Item::selectFirst(['author-link'], $condition);
846                 if (DBA::isResult($orig_post)) {
847                         $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
848                         $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
849                         $nicknameplain = "@" . $nicknameplain;
850
851                         if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
852                                 $item["body"] = $nickname . " " . $item["body"];
853                         }
854                 }
855
856                 $msgarr = ItemContent::getPlaintextPost($item, $max_char, true, 8);
857                 $msg = $msgarr["text"];
858
859                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
860                         $msg .= " " . $msgarr["url"];
861                 }
862
863                 if (isset($msgarr["image"])) {
864                         $msg .= " " . $msgarr["image"];
865                 }
866
867                 $b['html'] = nl2br(htmlspecialchars($msg));
868         }
869 }
870
871 /**
872  * @brief Build the item array for the mirrored post
873  *
874  * @param App $a Application class
875  * @param integer $uid User id
876  * @param object $post Twitter object with the post
877  *
878  * @return array item data to be posted
879  */
880 function twitter_do_mirrorpost(App $a, $uid, $post)
881 {
882         $datarray['api_source'] = true;
883         $datarray['profile_uid'] = $uid;
884         $datarray['extid'] = Protocol::TWITTER;
885         $datarray['message_id'] = Item::newURI($uid, Protocol::TWITTER . ':' . $post->id);
886         $datarray['protocol'] = Conversation::PARCEL_TWITTER;
887         $datarray['source'] = json_encode($post);
888         $datarray['title'] = '';
889
890         if (!empty($post->retweeted_status)) {
891                 // We don't support nested shares, so we mustn't show quotes as shares on retweets
892                 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
893
894                 if (empty($item['body'])) {
895                         return [];
896                 }
897
898                 $datarray['body'] = "\n" . share_header(
899                         $item['author-name'],
900                         $item['author-link'],
901                         $item['author-avatar'],
902                         '',
903                         $item['created'],
904                         $item['plink']
905                 );
906
907                 $datarray['body'] .= $item['body'] . '[/share]';
908         } else {
909                 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
910
911                 if (empty($item['body'])) {
912                         return [];
913                 }
914
915                 $datarray['body'] = $item['body'];
916         }
917
918         $datarray['source'] = $item['app'];
919         $datarray['verb'] = $item['verb'];
920
921         if (isset($item['location'])) {
922                 $datarray['location'] = $item['location'];
923         }
924
925         if (isset($item['coord'])) {
926                 $datarray['coord'] = $item['coord'];
927         }
928
929         return $datarray;
930 }
931
932 function twitter_fetchtimeline(App $a, $uid)
933 {
934         $ckey    = Config::get('twitter', 'consumerkey');
935         $csecret = Config::get('twitter', 'consumersecret');
936         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
937         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
938         $lastid  = PConfig::get($uid, 'twitter', 'lastid');
939
940         $application_name = Config::get('twitter', 'application_name');
941
942         if ($application_name == "") {
943                 $application_name = $a->getHostName();
944         }
945
946         $has_picture = false;
947
948         require_once 'mod/item.php';
949         require_once 'mod/share.php';
950
951         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
952
953         $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
954
955         $first_time = ($lastid == "");
956
957         if ($lastid != "") {
958                 $parameters["since_id"] = $lastid;
959         }
960
961         try {
962                 $items = $connection->get('statuses/user_timeline', $parameters);
963         } catch (TwitterOAuthException $e) {
964                 Logger::notice('Error fetching timeline', ['user' => $uid, 'message' => $e->getMessage()]);
965                 return;
966         }
967
968         if (!is_array($items)) {
969                 Logger::notice('No items', ['user' => $uid]);
970                 return;
971         }
972
973         $posts = array_reverse($items);
974
975         Logger::log('Starting from ID ' . $lastid . ' for user ' . $uid, Logger::DEBUG);
976
977         if (count($posts)) {
978                 foreach ($posts as $post) {
979                         if ($post->id_str > $lastid) {
980                                 $lastid = $post->id_str;
981                                 PConfig::set($uid, 'twitter', 'lastid', $lastid);
982                         }
983
984                         if ($first_time) {
985                                 continue;
986                         }
987
988                         if (!stristr($post->source, $application_name)) {
989                                 $_SESSION["authenticated"] = true;
990                                 $_SESSION["uid"] = $uid;
991
992                                 Logger::log('Preparing Twitter ID ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
993
994                                 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
995
996                                 if (empty($_REQUEST['body'])) {
997                                         continue;
998                                 }
999
1000                                 Logger::log('Posting Twitter ID ' . $post->id_str . ' for user ' . $uid);
1001
1002                                 item_post($a);
1003                         }
1004                 }
1005         }
1006         PConfig::set($uid, 'twitter', 'lastid', $lastid);
1007         Logger::log('Last ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1008 }
1009
1010 function twitter_fix_avatar($avatar)
1011 {
1012         $new_avatar = str_replace("_normal.", ".", $avatar);
1013
1014         $info = Images::getInfoFromURLCached($new_avatar);
1015         if (!$info) {
1016                 $new_avatar = $avatar;
1017         }
1018
1019         return $new_avatar;
1020 }
1021
1022 function twitter_fetch_contact($uid, $data, $create_user)
1023 {
1024         if (empty($data->id_str)) {
1025                 return -1;
1026         }
1027
1028         $avatar = twitter_fix_avatar($data->profile_image_url_https);
1029         $url = "https://twitter.com/" . $data->screen_name;
1030         $addr = $data->screen_name . "@twitter.com";
1031
1032         $fields = ['url' => $url, 'network' => Protocol::TWITTER,
1033                 'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
1034                 'location' => $data->location, 'about' => $data->description];
1035
1036         $cid = Contact::getIdForURL($url, 0, true, $fields);
1037         if (!empty($cid)) {
1038                 DBA::update('contact', $fields, ['id' => $cid]);
1039                 Contact::updateAvatar($avatar, 0, $cid);
1040         }
1041
1042         $contact = DBA::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
1043         if (!DBA::isResult($contact) && !$create_user) {
1044                 Logger::info('User contact not found', ['uid' => $uid, 'twitter-id' => $data->id_str]);
1045                 return 0;
1046         }
1047
1048         if (!DBA::isResult($contact)) {
1049                 // create contact record
1050                 $fields['uid'] = $uid;
1051                 $fields['created'] = DateTimeFormat::utcNow();
1052                 $fields['nurl'] = Strings::normaliseLink($url);
1053                 $fields['alias'] = 'twitter::' . $data->id_str;
1054                 $fields['poll'] = 'twitter::' . $data->id_str;
1055                 $fields['rel'] = Contact::FRIEND;
1056                 $fields['priority'] = 1;
1057                 $fields['writable'] = true;
1058                 $fields['blocked'] = false;
1059                 $fields['readonly'] = false;
1060                 $fields['pending'] = false;
1061
1062                 if (!DBA::insert('contact', $fields)) {
1063                         return false;
1064                 }
1065
1066                 $contact_id = DBA::lastInsertId();
1067
1068                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1069
1070                 Contact::updateAvatar($avatar, $uid, $contact_id);
1071         } else {
1072                 if ($contact["readonly"] || $contact["blocked"]) {
1073                         Logger::log("twitter_fetch_contact: Contact '" . $contact["nick"] . "' is blocked or readonly.", Logger::DEBUG);
1074                         return -1;
1075                 }
1076
1077                 $contact_id = $contact['id'];
1078
1079                 // update profile photos once every twelve hours as we have no notification of when they change.
1080                 $update_photo = ($contact['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
1081
1082                 // check that we have all the photos, this has been known to fail on occasion
1083                 if (empty($contact['photo']) || empty($contact['thumb']) || empty($contact['micro']) || $update_photo) {
1084                         Logger::log("twitter_fetch_contact: Updating contact " . $data->screen_name, Logger::DEBUG);
1085
1086                         Contact::updateAvatar($avatar, $uid, $contact['id']);
1087
1088                         $fields['name-date'] = DateTimeFormat::utcNow();
1089                         $fields['uri-date'] = DateTimeFormat::utcNow();
1090
1091                         DBA::update('contact', $fields, ['id' => $contact['id']]);
1092                 }
1093         }
1094
1095         return $contact_id;
1096 }
1097
1098 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1099 {
1100         $ckey = Config::get('twitter', 'consumerkey');
1101         $csecret = Config::get('twitter', 'consumersecret');
1102         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1103         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1104
1105         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1106                 intval($uid));
1107
1108         if (DBA::isResult($r)) {
1109                 $self = $r[0];
1110         } else {
1111                 return;
1112         }
1113
1114         $parameters = [];
1115
1116         if ($screen_name != "") {
1117                 $parameters["screen_name"] = $screen_name;
1118         }
1119
1120         if ($user_id != "") {
1121                 $parameters["user_id"] = $user_id;
1122         }
1123
1124         // Fetching user data
1125         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1126         try {
1127                 $user = $connection->get('users/show', $parameters);
1128         } catch (TwitterOAuthException $e) {
1129                 Logger::log('twitter_fetchuser: Error fetching user ' . $uid . ': ' . $e->getMessage());
1130                 return;
1131         }
1132
1133         if (!is_object($user)) {
1134                 return;
1135         }
1136
1137         $contact_id = twitter_fetch_contact($uid, $user, true);
1138
1139         return $contact_id;
1140 }
1141
1142 function twitter_expand_entities(App $a, $body, $item, $picture)
1143 {
1144         $plain = $body;
1145
1146         $tags_arr = [];
1147
1148         foreach ($item->entities->hashtags AS $hashtag) {
1149                 $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
1150                 $tags_arr['#' . $hashtag->text] = $url;
1151                 $body = str_replace('#' . $hashtag->text, $url, $body);
1152         }
1153
1154         foreach ($item->entities->user_mentions AS $mention) {
1155                 $url = '@[url=https://twitter.com/' . rawurlencode($mention->screen_name) . ']' . $mention->screen_name . '[/url]';
1156                 $tags_arr['@' . $mention->screen_name] = $url;
1157                 $body = str_replace('@' . $mention->screen_name, $url, $body);
1158         }
1159
1160         if (isset($item->entities->urls)) {
1161                 $type = '';
1162                 $footerurl = '';
1163                 $footerlink = '';
1164                 $footer = '';
1165
1166                 foreach ($item->entities->urls as $url) {
1167                         $plain = str_replace($url->url, '', $plain);
1168
1169                         if ($url->url && $url->expanded_url && $url->display_url) {
1170                                 // Quote tweet, we just remove the quoted tweet URL from the body, the share block will be added later.
1171                                 if (!empty($item->quoted_status) && isset($item->quoted_status_id_str)
1172                                         && substr($url->expanded_url, -strlen($item->quoted_status_id_str)) == $item->quoted_status_id_str ) {
1173                                         $body = str_replace($url->url, '', $body);
1174                                         continue;
1175                                 }
1176
1177                                 $expanded_url = $url->expanded_url;
1178
1179                                 $final_url = Network::finalUrl($url->expanded_url);
1180
1181                                 $oembed_data = OEmbed::fetchURL($final_url);
1182
1183                                 if (empty($oembed_data) || empty($oembed_data->type)) {
1184                                         continue;
1185                                 }
1186
1187                                 // Quickfix: Workaround for URL with '[' and ']' in it
1188                                 if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
1189                                         $expanded_url = $url->url;
1190                                 }
1191
1192                                 if ($type == '') {
1193                                         $type = $oembed_data->type;
1194                                 }
1195
1196                                 if ($oembed_data->type == 'video') {
1197                                         $type = $oembed_data->type;
1198                                         $footerurl = $expanded_url;
1199                                         $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1200
1201                                         $body = str_replace($url->url, $footerlink, $body);
1202                                 } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
1203                                         $body = str_replace($url->url, '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]', $body);
1204                                 } elseif ($oembed_data->type != 'link') {
1205                                         $body = str_replace($url->url, '[url=' . $expanded_url . ']' . $url->display_url . '[/url]', $body);
1206                                 } else {
1207                                         $img_str = Network::fetchUrl($final_url, true, 4);
1208
1209                                         $tempfile = tempnam(get_temppath(), 'cache');
1210                                         file_put_contents($tempfile, $img_str);
1211
1212                                         // See http://php.net/manual/en/function.exif-imagetype.php#79283
1213                                         if (filesize($tempfile) > 11) {
1214                                                 $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1215                                         } else {
1216                                                 $mime = false;
1217                                         }
1218
1219                                         unlink($tempfile);
1220
1221                                         if (substr($mime, 0, 6) == 'image/') {
1222                                                 $type = 'photo';
1223                                                 $body = str_replace($url->url, '[img]' . $final_url . '[/img]', $body);
1224                                         } else {
1225                                                 $type = $oembed_data->type;
1226                                                 $footerurl = $expanded_url;
1227                                                 $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1228
1229                                                 $body = str_replace($url->url, $footerlink, $body);
1230                                         }
1231                                 }
1232                         }
1233                 }
1234
1235                 // Footer will be taken care of with a share block in the case of a quote
1236                 if (empty($item->quoted_status)) {
1237                         if ($footerurl != '') {
1238                                 $footer = add_page_info($footerurl, false, $picture);
1239                         }
1240
1241                         if (($footerlink != '') && (trim($footer) != '')) {
1242                                 $removedlink = trim(str_replace($footerlink, '', $body));
1243
1244                                 if (($removedlink == '') || strstr($body, $removedlink)) {
1245                                         $body = $removedlink;
1246                                 }
1247
1248                                 $body .= $footer;
1249                         }
1250
1251                         if ($footer == '' && $picture != '') {
1252                                 $body .= "\n\n[img]" . $picture . "[/img]\n";
1253                         } elseif ($footer == '' && $picture == '') {
1254                                 $body = add_page_info_to_body($body);
1255                         }
1256                 }
1257         }
1258
1259         // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1260         $tags = BBCode::getTags($body);
1261
1262         if (count($tags)) {
1263                 foreach ($tags as $tag) {
1264                         if (strstr(trim($tag), ' ')) {
1265                                 continue;
1266                         }
1267
1268                         if (strpos($tag, '#') === 0) {
1269                                 if (strpos($tag, '[url=')) {
1270                                         continue;
1271                                 }
1272
1273                                 // don't link tags that are already embedded in links
1274                                 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1275                                         continue;
1276                                 }
1277                                 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1278                                         continue;
1279                                 }
1280
1281                                 $basetag = str_replace('_', ' ', substr($tag, 1));
1282                                 $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
1283                                 $body = str_replace($tag, $url, $body);
1284                                 $tags_arr['#' . $basetag] = $url;
1285                         } elseif (strpos($tag, '@') === 0) {
1286                                 if (strpos($tag, '[url=')) {
1287                                         continue;
1288                                 }
1289
1290                                 $basetag = substr($tag, 1);
1291                                 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1292                                 $body = str_replace($tag, $url, $body);
1293                                 $tags_arr['@' . $basetag] = $url;
1294                         }
1295                 }
1296         }
1297
1298         $tags = implode($tags_arr, ',');
1299
1300         return ['body' => $body, 'tags' => $tags, 'plain' => $plain];
1301 }
1302
1303 /**
1304  * @brief Fetch media entities and add media links to the body
1305  *
1306  * @param object $post Twitter object with the post
1307  * @param array $postarray Array of the item that is about to be posted
1308  *
1309  * @return $picture string Image URL or empty string
1310  */
1311 function twitter_media_entities($post, array &$postarray)
1312 {
1313         // There are no media entities? So we quit.
1314         if (empty($post->extended_entities->media)) {
1315                 return '';
1316         }
1317
1318         // When the post links to an external page, we only take one picture.
1319         // We only do this when there is exactly one media.
1320         if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1321                 $medium = $post->extended_entities->media[0];
1322                 $picture = '';
1323                 foreach ($post->entities->urls as $link) {
1324                         // Let's make sure the external link url matches the media url
1325                         if ($medium->url == $link->url && isset($medium->media_url_https)) {
1326                                 $picture = $medium->media_url_https;
1327                                 $postarray['body'] = str_replace($medium->url, '', $postarray['body']);
1328                                 return $picture;
1329                         }
1330                 }
1331         }
1332
1333         // This is a pure media post, first search for all media urls
1334         $media = [];
1335         foreach ($post->extended_entities->media AS $medium) {
1336                 if (!isset($media[$medium->url])) {
1337                         $media[$medium->url] = '';
1338                 }
1339                 switch ($medium->type) {
1340                         case 'photo':
1341                                 if (!empty($medium->ext_alt_text)) {
1342                                         Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
1343                                         $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
1344                                 } else {
1345                                         $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1346                                 }
1347
1348                                 $postarray['object-type'] = Activity\ObjectType::IMAGE;
1349                                 break;
1350                         case 'video':
1351                         case 'animated_gif':
1352                                 if (!empty($medium->ext_alt_text)) {
1353                                         Logger::info('Got text description', ['alt_text' => $medium->ext_alt_text]);
1354                                         $media[$medium->url] .= "\n[img=" . $medium->media_url_https .']' . $medium->ext_alt_text . '[/img]';
1355                                 } else {
1356                                         $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1357                                 }
1358
1359                                 $postarray['object-type'] = Activity\ObjectType::VIDEO;
1360                                 if (is_array($medium->video_info->variants)) {
1361                                         $bitrate = 0;
1362                                         // We take the video with the highest bitrate
1363                                         foreach ($medium->video_info->variants AS $variant) {
1364                                                 if (($variant->content_type == 'video/mp4') && ($variant->bitrate >= $bitrate)) {
1365                                                         $media[$medium->url] = "\n[video]" . $variant->url . '[/video]';
1366                                                         $bitrate = $variant->bitrate;
1367                                                 }
1368                                         }
1369                                 }
1370                                 break;
1371                         // The following code will only be activated for test reasons
1372                         //default:
1373                         //      $postarray['body'] .= print_r($medium, true);
1374                 }
1375         }
1376
1377         // Now we replace the media urls.
1378         foreach ($media AS $key => $value) {
1379                 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1380         }
1381
1382         return '';
1383 }
1384
1385 function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $only_existing_contact, $noquote)
1386 {
1387         $postarray = [];
1388         $postarray['network'] = Protocol::TWITTER;
1389         $postarray['uid'] = $uid;
1390         $postarray['wall'] = 0;
1391         $postarray['uri'] = "twitter::" . $post->id_str;
1392         $postarray['protocol'] = Conversation::PARCEL_TWITTER;
1393         $postarray['source'] = json_encode($post);
1394
1395         // Don't import our own comments
1396         if (Item::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
1397                 Logger::log("Item with extid " . $postarray['uri'] . " found.", Logger::DEBUG);
1398                 return [];
1399         }
1400
1401         $contactid = 0;
1402
1403         if ($post->in_reply_to_status_id_str != "") {
1404                 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1405
1406                 $fields = ['uri', 'parent-uri', 'parent'];
1407                 $parent_item = Item::selectFirst($fields, ['uri' => $parent, 'uid' => $uid]);
1408                 if (!DBA::isResult($parent_item)) {
1409                         $parent_item = Item::selectFirst($fields, ['extid' => $parent, 'uid' => $uid]);
1410                 }
1411
1412                 if (DBA::isResult($parent_item)) {
1413                         $postarray['thr-parent'] = $parent_item['uri'];
1414                         $postarray['parent-uri'] = $parent_item['parent-uri'];
1415                         $postarray['parent'] = $parent_item['parent'];
1416                         $postarray['object-type'] = Activity\ObjectType::COMMENT;
1417                 } else {
1418                         $postarray['thr-parent'] = $postarray['uri'];
1419                         $postarray['parent-uri'] = $postarray['uri'];
1420                         $postarray['object-type'] = Activity\ObjectType::NOTE;
1421                 }
1422
1423                 // Is it me?
1424                 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1425
1426                 if ($post->user->id_str == $own_id) {
1427                         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1428                                 intval($uid));
1429
1430                         if (DBA::isResult($r)) {
1431                                 $contactid = $r[0]["id"];
1432
1433                                 $postarray['owner-name']   = $r[0]["name"];
1434                                 $postarray['owner-link']   = $r[0]["url"];
1435                                 $postarray['owner-avatar'] = $r[0]["photo"];
1436                         } else {
1437                                 Logger::log("No self contact for user " . $uid, Logger::DEBUG);
1438                                 return [];
1439                         }
1440                 }
1441                 // Don't create accounts of people who just comment something
1442                 $create_user = false;
1443         } else {
1444                 $postarray['parent-uri'] = $postarray['uri'];
1445                 $postarray['object-type'] = Activity\ObjectType::NOTE;
1446         }
1447
1448         if ($contactid == 0) {
1449                 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1450
1451                 $postarray['owner-name'] = $post->user->name;
1452                 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1453                 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1454         }
1455
1456         if (($contactid == 0) && !$only_existing_contact) {
1457                 $contactid = $self['id'];
1458         } elseif ($contactid <= 0) {
1459                 Logger::log("Contact ID is zero or less than zero.", Logger::DEBUG);
1460                 return [];
1461         }
1462
1463         $postarray['contact-id'] = $contactid;
1464
1465         $postarray['verb'] = Activity::POST;
1466         $postarray['author-name'] = $postarray['owner-name'];
1467         $postarray['author-link'] = $postarray['owner-link'];
1468         $postarray['author-avatar'] = $postarray['owner-avatar'];
1469         $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1470         $postarray['app'] = strip_tags($post->source);
1471
1472         if ($post->user->protected) {
1473                 $postarray['private'] = 1;
1474                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1475         } else {
1476                 $postarray['private'] = 0;
1477                 $postarray['allow_cid'] = '';
1478         }
1479
1480         if (!empty($post->full_text)) {
1481                 $postarray['body'] = $post->full_text;
1482         } else {
1483                 $postarray['body'] = $post->text;
1484         }
1485
1486         // When the post contains links then use the correct object type
1487         if (count($post->entities->urls) > 0) {
1488                 $postarray['object-type'] = Activity\ObjectType::BOOKMARK;
1489         }
1490
1491         // Search for media links
1492         $picture = twitter_media_entities($post, $postarray);
1493
1494         $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1495         $postarray['body'] = $converted["body"];
1496         $postarray['tag'] = $converted["tags"];
1497         $postarray['created'] = DateTimeFormat::utc($post->created_at);
1498         $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1499
1500         $statustext = $converted["plain"];
1501
1502         if (!empty($post->place->name)) {
1503                 $postarray["location"] = $post->place->name;
1504         }
1505         if (!empty($post->place->full_name)) {
1506                 $postarray["location"] = $post->place->full_name;
1507         }
1508         if (!empty($post->geo->coordinates)) {
1509                 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1510         }
1511         if (!empty($post->coordinates->coordinates)) {
1512                 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1513         }
1514         if (!empty($post->retweeted_status)) {
1515                 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1516
1517                 if (empty($retweet['body'])) {
1518                         return [];
1519                 }
1520
1521                 if (!$noquote) {
1522                         // Store the original tweet
1523                         Item::insert($retweet);
1524
1525                         // CHange the other post into a reshare activity
1526                         $postarray['verb'] = Activity::ANNOUNCE;
1527                         $postarray['gravity'] = GRAVITY_ACTIVITY;
1528                         $postarray['object-type'] = Activity\ObjectType::NOTE;
1529
1530                         $postarray['thr-parent'] = $retweet['uri'];
1531                         $postarray['parent-uri'] = $retweet['uri'];
1532                 } else {
1533                         $retweet['source'] = $postarray['source'];
1534                         $retweet['private'] = $postarray['private'];
1535                         $retweet['allow_cid'] = $postarray['allow_cid'];
1536                         $retweet['contact-id'] = $postarray['contact-id'];
1537                         $retweet['owner-name'] = $postarray['owner-name'];
1538                         $retweet['owner-link'] = $postarray['owner-link'];
1539                         $retweet['owner-avatar'] = $postarray['owner-avatar'];
1540
1541                         $postarray = $retweet;
1542                 }
1543         }
1544
1545         if (!empty($post->quoted_status) && !$noquote) {
1546                 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1547
1548                 if (!empty($quoted['body'])) {
1549                         $postarray['body'] .= "\n" . share_header(
1550                                 $quoted['author-name'],
1551                                 $quoted['author-link'],
1552                                 $quoted['author-avatar'],
1553                                 "",
1554                                 $quoted['created'],
1555                                 $quoted['plink']
1556                         );
1557
1558                         $postarray['body'] .= $quoted['body'] . '[/share]';
1559                 } else {
1560                         // Quoted post author is blocked/ignored, so we just provide the link to avoid removing quote context.
1561                         $postarray['body'] .= "\n\nhttps://twitter.com/" . $post->quoted_status->user->screen_name . "/status/" . $post->quoted_status->id_str;
1562                 }
1563         }
1564
1565         return $postarray;
1566 }
1567
1568 function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, array $self)
1569 {
1570         Logger::log("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, Logger::DEBUG);
1571
1572         $posts = [];
1573
1574         while (!empty($post->in_reply_to_status_id_str)) {
1575                 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str, "include_ext_alt_text" => true];
1576
1577                 try {
1578                         $post = $connection->get('statuses/show', $parameters);
1579                 } catch (TwitterOAuthException $e) {
1580                         Logger::log('twitter_fetchparentposts: Error fetching for user ' . $uid . ' and post ' . $post->id_str . ': ' . $e->getMessage());
1581                         break;
1582                 }
1583
1584                 if (empty($post)) {
1585                         Logger::log("twitter_fetchparentposts: Can't fetch post " . $parameters['id'], Logger::DEBUG);
1586                         break;
1587                 }
1588
1589                 if (empty($post->id_str)) {
1590                         Logger::log("twitter_fetchparentposts: This is not a post " . json_encode($post), Logger::DEBUG);
1591                         break;
1592                 }
1593
1594                 if (Item::exists(['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
1595                         break;
1596                 }
1597
1598                 $posts[] = $post;
1599         }
1600
1601         Logger::log("twitter_fetchparentposts: Fetching " . count($posts) . " parents", Logger::DEBUG);
1602
1603         $posts = array_reverse($posts);
1604
1605         if (!empty($posts)) {
1606                 foreach ($posts as $post) {
1607                         $postarray = twitter_createpost($a, $uid, $post, $self, false, !PConfig::get($uid, 'twitter', 'create_user'), false);
1608
1609                         if (empty($postarray['body'])) {
1610                                 continue;
1611                         }
1612
1613                         $item = Item::insert($postarray);
1614
1615                         $postarray["id"] = $item;
1616
1617                         Logger::log('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1618                 }
1619         }
1620 }
1621
1622 function twitter_fetchhometimeline(App $a, $uid)
1623 {
1624         $ckey    = Config::get('twitter', 'consumerkey');
1625         $csecret = Config::get('twitter', 'consumersecret');
1626         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1627         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1628         $create_user = PConfig::get($uid, 'twitter', 'create_user');
1629         $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1630
1631         Logger::log("Fetching timeline for user " . $uid, Logger::DEBUG);
1632
1633         $application_name = Config::get('twitter', 'application_name');
1634
1635         if ($application_name == "") {
1636                 $application_name = $a->getHostName();
1637         }
1638
1639         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1640
1641         try {
1642                 $own_contact = twitter_fetch_own_contact($a, $uid);
1643         } catch (TwitterOAuthException $e) {
1644                 Logger::log('Error fetching own contact for user ' . $uid . ': ' . $e->getMessage());
1645                 return;
1646         }
1647
1648         $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1649                 intval($own_contact),
1650                 intval($uid));
1651
1652         if (DBA::isResult($r)) {
1653                 $own_id = $r[0]["nick"];
1654         } else {
1655                 Logger::log("Own twitter contact not found for user " . $uid);
1656                 return;
1657         }
1658
1659         $self = User::getOwnerDataById($uid);
1660         if ($self === false) {
1661                 Logger::log("Own contact not found for user " . $uid);
1662                 return;
1663         }
1664
1665         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended", "include_ext_alt_text" => true];
1666         //$parameters["count"] = 200;
1667         // Fetching timeline
1668         $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1669
1670         $first_time = ($lastid == "");
1671
1672         if ($lastid != "") {
1673                 $parameters["since_id"] = $lastid;
1674         }
1675
1676         try {
1677                 $items = $connection->get('statuses/home_timeline', $parameters);
1678         } catch (TwitterOAuthException $e) {
1679                 Logger::log('Error fetching home timeline for user ' . $uid . ': ' . $e->getMessage());
1680                 return;
1681         }
1682
1683         if (!is_array($items)) {
1684                 Logger::log('No array while fetching home timeline for user ' . $uid . ': ' . print_r($items, true));
1685                 return;
1686         }
1687
1688         if (empty($items)) {
1689                 Logger::log('No new timeline content for user ' . $uid, Logger::INFO);
1690                 return;
1691         }
1692
1693         $posts = array_reverse($items);
1694
1695         Logger::log('Fetching timeline from ID ' . $lastid . ' for user ' . $uid . ' ' . sizeof($posts) . ' items', Logger::DEBUG);
1696
1697         if (count($posts)) {
1698                 foreach ($posts as $post) {
1699                         if ($post->id_str > $lastid) {
1700                                 $lastid = $post->id_str;
1701                                 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1702                         }
1703
1704                         if ($first_time) {
1705                                 continue;
1706                         }
1707
1708                         if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1709                                 Logger::log("Skip previously sent post", Logger::DEBUG);
1710                                 continue;
1711                         }
1712
1713                         if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1714                                 Logger::log("Skip post that will be mirrored", Logger::DEBUG);
1715                                 continue;
1716                         }
1717
1718                         if ($post->in_reply_to_status_id_str != "") {
1719                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1720                         }
1721
1722                         Logger::log('Preparing post ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
1723
1724                         $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1725
1726                         if (empty($postarray['body']) || trim($postarray['body']) == "") {
1727                                 Logger::log('Empty body for post ' . $post->id_str . ' and user ' . $uid, Logger::DEBUG);
1728                                 continue;
1729                         }
1730
1731                         $notify = false;
1732
1733                         if (($postarray['uri'] == $postarray['parent-uri']) && ($postarray['author-link'] == $postarray['owner-link'])) {
1734                                 $contact = DBA::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
1735                                 if (DBA::isResult($contact)) {
1736                                         $notify = Item::isRemoteSelf($contact, $postarray);
1737                                 }
1738                         }
1739
1740                         $item = Item::insert($postarray, false, $notify);
1741                         $postarray["id"] = $item;
1742
1743                         Logger::log('User ' . $uid . ' posted home timeline item ' . $item);
1744                 }
1745         }
1746         PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1747
1748         Logger::log('Last timeline ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1749
1750         // Fetching mentions
1751         $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1752
1753         $first_time = ($lastid == "");
1754
1755         if ($lastid != "") {
1756                 $parameters["since_id"] = $lastid;
1757         }
1758
1759         try {
1760                 $items = $connection->get('statuses/mentions_timeline', $parameters);
1761         } catch (TwitterOAuthException $e) {
1762                 Logger::log('Error fetching mentions: ' . $e->getMessage());
1763                 return;
1764         }
1765
1766         if (!is_array($items)) {
1767                 Logger::log("Error fetching mentions: " . print_r($items, true), Logger::DEBUG);
1768                 return;
1769         }
1770
1771         $posts = array_reverse($items);
1772
1773         Logger::log("Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", Logger::DEBUG);
1774
1775         if (count($posts)) {
1776                 foreach ($posts as $post) {
1777                         if ($post->id_str > $lastid) {
1778                                 $lastid = $post->id_str;
1779                         }
1780
1781                         if ($first_time) {
1782                                 continue;
1783                         }
1784
1785                         if ($post->in_reply_to_status_id_str != "") {
1786                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1787                         }
1788
1789                         $postarray = twitter_createpost($a, $uid, $post, $self, false, !$create_user, false);
1790
1791                         if (empty($postarray['body'])) {
1792                                 continue;
1793                         }
1794
1795                         $item = Item::insert($postarray);
1796
1797                         Logger::log('User ' . $uid . ' posted mention timeline item ' . $item);
1798                 }
1799         }
1800
1801         PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1802
1803         Logger::log('Last mentions ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1804 }
1805
1806 function twitter_fetch_own_contact(App $a, $uid)
1807 {
1808         $ckey    = Config::get('twitter', 'consumerkey');
1809         $csecret = Config::get('twitter', 'consumersecret');
1810         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1811         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1812
1813         $own_id = PConfig::get($uid, 'twitter', 'own_id');
1814
1815         $contact_id = 0;
1816
1817         if ($own_id == "") {
1818                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1819
1820                 // Fetching user data
1821                 // get() may throw TwitterOAuthException, but we will catch it later
1822                 $user = $connection->get('account/verify_credentials');
1823                 if (empty($user) || empty($user->id_str)) {
1824                         return false;
1825                 }
1826
1827                 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1828
1829                 $contact_id = twitter_fetch_contact($uid, $user, true);
1830         } else {
1831                 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1832                         intval($uid),
1833                         DBA::escape("twitter::" . $own_id));
1834                 if (DBA::isResult($r)) {
1835                         $contact_id = $r[0]["id"];
1836                 } else {
1837                         PConfig::delete($uid, 'twitter', 'own_id');
1838                 }
1839         }
1840
1841         return $contact_id;
1842 }
1843
1844 function twitter_is_retweet(App $a, $uid, $body)
1845 {
1846         $body = trim($body);
1847
1848         // Skip if it isn't a pure repeated messages
1849         // Does it start with a share?
1850         if (strpos($body, "[share") > 0) {
1851                 return false;
1852         }
1853
1854         // Does it end with a share?
1855         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1856                 return false;
1857         }
1858
1859         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1860         // Skip if there is no shared message in there
1861         if ($body == $attributes) {
1862                 return false;
1863         }
1864
1865         $link = "";
1866         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1867         if (!empty($matches[1])) {
1868                 $link = $matches[1];
1869         }
1870
1871         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1872         if (!empty($matches[1])) {
1873                 $link = $matches[1];
1874         }
1875
1876         $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1877         if ($id == $link) {
1878                 return false;
1879         }
1880
1881         Logger::log('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, Logger::DEBUG);
1882
1883         $ckey    = Config::get('twitter', 'consumerkey');
1884         $csecret = Config::get('twitter', 'consumersecret');
1885         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1886         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1887
1888         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1889         $result = $connection->post('statuses/retweet/' . $id);
1890
1891         Logger::log('twitter_is_retweet: result ' . print_r($result, true), Logger::DEBUG);
1892
1893         return !isset($result->errors);
1894 }
1895
1896 function twitter_update_mentions($body)
1897 {
1898         $URLSearchString = "^\[\]";
1899         $return = preg_replace_callback(
1900                 "/@\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1901                 function ($matches) {
1902                         if (strpos($matches[1], 'twitter.com')) {
1903                                 $return = '@' . substr($matches[1], strrpos($matches[1], '/') + 1);
1904                         } else {
1905                                 $return = $matches[2] . ' (' . $matches[1] . ')';
1906                         }
1907
1908                         return $return;
1909                 },
1910                 $body
1911         );
1912
1913         return $return;
1914 }
1915
1916 function twitter_convert_share(array $attributes, array $author_contact, $content, $is_quote_share)
1917 {
1918         if ($author_contact['network'] == Protocol::TWITTER) {
1919                 $mention = '@' . $author_contact['nick'];
1920         } else {
1921                 $mention = $author_contact['addr'];
1922         }
1923
1924         return ($is_quote_share ? "\n\n" : '' ) . 'RT ' . $mention . ': ' . $content . "\n\n" . $attributes['link'];
1925 }