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