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