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