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