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