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