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