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