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