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