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