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