]> git.mxchange.org Git - friendica-addons.git/blob - twitter/twitter.php
Merge pull request #849 from annando/false-positive
[friendica-addons.git] / twitter / twitter.php
1 <?php
2 /**
3  * Name: Twitter Connector
4  * Description: Bidirectional (posting, relaying and reading) connector for Twitter.
5  * Version: 1.1.0
6  * Author: Tobias Diekershoff <https://f.diekershoff.de/profile/tobias>
7  * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
8  * Maintainer: Hypolite Petovan <https://friendica.mrpetovan.com/profile/hypolite>
9  *
10  * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel, Hypolite Petovan
11  * All rights reserved.
12  *
13  * Redistribution and use in source and binary forms, with or without
14  * modification, are permitted provided that the following conditions are met:
15  *    * Redistributions of source code must retain the above copyright notice,
16  *     this list of conditions and the following disclaimer.
17  *    * Redistributions in binary form must reproduce the above
18  *    * copyright notice, this list of conditions and the following disclaimer in
19  *      the documentation and/or other materials provided with the distribution.
20  *    * Neither the name of the <organization> nor the names of its contributors
21  *      may be used to endorse or promote products derived from this software
22  *      without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27  * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
28  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
32  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
33  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34  *
35  */
36 /*   Twitter Addon for Friendica
37  *
38  *   Author: Tobias Diekershoff
39  *           tobias.diekershoff@gmx.net
40  *
41  *   License:3-clause BSD license
42  *
43  *   Configuration:
44  *     To use this addon you need a OAuth Consumer key pair (key & secret)
45  *     you can get it from Twitter at https://twitter.com/apps
46  *
47  *     Register your Friendica site as "Client" application with "Read & Write" access
48  *     we do not need "Twitter as login". When you've registered the app you get the
49  *     OAuth Consumer key and secret pair for your application/site.
50  *
51  *     Add this key pair to your global config/addon.config.php or use the admin panel.
52  *
53  *      'twitter' => [
54  *                  'consumerkey' => '',
55  *              'consumersecret' => '',
56  *      ],
57  *
58  *     To activate the addon itself add it to the system.addon
59  *     setting. After this, your user can configure their Twitter account settings
60  *     from "Settings -> Addon Settings".
61  *
62  *     Requirements: PHP5, curl
63  */
64
65 use Abraham\TwitterOAuth\TwitterOAuth;
66 use Abraham\TwitterOAuth\TwitterOAuthException;
67 use Friendica\App;
68 use Friendica\Content\OEmbed;
69 use Friendica\Content\Text\BBCode;
70 use Friendica\Content\Text\Plaintext;
71 use Friendica\Core\Config;
72 use Friendica\Core\Hook;
73 use Friendica\Core\L10n;
74 use Friendica\Core\Logger;
75 use Friendica\Core\PConfig;
76 use Friendica\Core\Protocol;
77 use Friendica\Core\Renderer;
78 use Friendica\Core\Worker;
79 use Friendica\Database\DBA;
80 use Friendica\Model\Contact;
81 use Friendica\Model\Conversation;
82 use Friendica\Model\GContact;
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\Config\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                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
596
597                 // Set the timeout for upload to 30 seconds
598                 $connection->setTimeouts(10, 30);
599
600                 $max_char = 280;
601
602                 // Handling non-native reshares
603                 $b['body'] = Friendica\Content\Text\BBCode::convertShare(
604                         $b['body'],
605                         function (array $attributes, array $author_contact, $content, $is_quote_share) {
606                                 return twitter_convert_share($attributes, $author_contact, $content, $is_quote_share);
607                         }
608                 );
609
610                 $b['body'] = twitter_update_mentions($b['body']);
611
612                 $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, 8);
613                 Logger::info('Got plaintext', $msgarr);
614                 $msg = $msgarr["text"];
615
616                 if (($msg == "") && isset($msgarr["title"])) {
617                         $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
618                 }
619
620                 $image = "";
621
622                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
623                         $msg .= "\n" . $msgarr["url"];
624                         $url_added = true;
625                 } else {
626                         $url_added = false;
627                 }
628
629                 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
630                         $image = $msgarr["image"];
631                 }
632
633                 if (empty($msg)) {
634                         return;
635                 }
636
637                 // and now tweet it :-)
638                 $post = [];
639
640                 if (!empty($image)) {
641                         try {
642                                 $img_str = Network::fetchUrl($image);
643
644                                 $tempfile = tempnam(get_temppath(), 'cache');
645                                 file_put_contents($tempfile, $img_str);
646
647                                 $media = $connection->upload('media/upload', ['media' => $tempfile]);
648
649                                 unlink($tempfile);
650
651                                 if (isset($media->media_id_string)) {
652                                         $post['media_ids'] = $media->media_id_string;
653                                 } else {
654                                         throw new Exception('Failed upload of ' . $image);
655                                 }
656                         } catch (Exception $e) {
657                                 Logger::log('Exception when trying to send to Twitter: ' . $e->getMessage());
658
659                                 // Workaround: Remove the picture link so that the post can be reposted without it
660                                 // When there is another url already added, a second url would be superfluous.
661                                 if (!$url_added) {
662                                         $msg .= "\n" . $image;
663                                 }
664
665                                 $image = "";
666                         }
667                 }
668
669                 $post['status'] = $msg;
670
671                 if ($iscomment) {
672                         $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
673                 }
674
675                 $url = 'statuses/update';
676                 $result = $connection->post($url, $post);
677                 Logger::log('twitter_post send, result: ' . print_r($result, true), Logger::DEBUG);
678
679                 if (!empty($result->source)) {
680                         Config::set("twitter", "application_name", strip_tags($result->source));
681                 }
682
683                 if (!empty($result->errors)) {
684                         Logger::log('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
685                         Worker::defer();
686                 } elseif ($iscomment) {
687                         Logger::log('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
688                         Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
689                 }
690         }
691 }
692
693 function twitter_addon_admin_post(App $a)
694 {
695         $consumerkey    = !empty($_POST['consumerkey'])    ? Strings::escapeTags(trim($_POST['consumerkey']))    : '';
696         $consumersecret = !empty($_POST['consumersecret']) ? Strings::escapeTags(trim($_POST['consumersecret'])) : '';
697         Config::set('twitter', 'consumerkey', $consumerkey);
698         Config::set('twitter', 'consumersecret', $consumersecret);
699         info(L10n::t('Settings updated.') . EOL);
700 }
701
702 function twitter_addon_admin(App $a, &$o)
703 {
704         $t = Renderer::getMarkupTemplate("admin.tpl", "addon/twitter/");
705
706         $o = Renderer::replaceMacros($t, [
707                 '$submit' => L10n::t('Save Settings'),
708                 // name, label, value, help, [extra values]
709                 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
710                 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
711         ]);
712 }
713
714 function twitter_cron(App $a)
715 {
716         $last = Config::get('twitter', 'last_poll');
717
718         $poll_interval = intval(Config::get('twitter', 'poll_interval'));
719         if (!$poll_interval) {
720                 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
721         }
722
723         if ($last) {
724                 $next = $last + ($poll_interval * 60);
725                 if ($next > time()) {
726                         Logger::log('twitter: poll intervall not reached');
727                         return;
728                 }
729         }
730         Logger::log('twitter: cron_start');
731
732         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
733         if (DBA::isResult($r)) {
734                 foreach ($r as $rr) {
735                         Logger::log('twitter: fetching for user ' . $rr['uid']);
736                         Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
737                 }
738         }
739
740         $abandon_days = intval(Config::get('system', 'account_abandon_days'));
741         if ($abandon_days < 1) {
742                 $abandon_days = 0;
743         }
744
745         $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
746
747         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
748         if (DBA::isResult($r)) {
749                 foreach ($r as $rr) {
750                         if ($abandon_days != 0) {
751                                 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
752                                 if (!DBA::isResult($user)) {
753                                         Logger::log('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
754                                         continue;
755                                 }
756                         }
757
758                         Logger::log('twitter: importing timeline from user ' . $rr['uid']);
759                         Worker::add(['priority' => PRIORITY_MEDIUM, 'force_priority' => true], "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
760                         /*
761                           // To-Do
762                           // check for new contacts once a day
763                           $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
764                           if($last_contact_check)
765                           $next_contact_check = $last_contact_check + 86400;
766                           else
767                           $next_contact_check = 0;
768
769                           if($next_contact_check <= time()) {
770                           pumpio_getallusers($a, $rr["uid"]);
771                           PConfig::set($rr['uid'],'pumpio','contact_check',time());
772                           }
773                          */
774                 }
775         }
776
777         Logger::log('twitter: cron_end');
778
779         Config::set('twitter', 'last_poll', time());
780 }
781
782 function twitter_expire(App $a)
783 {
784         $days = Config::get('twitter', 'expire');
785
786         if ($days == 0) {
787                 return;
788         }
789
790         $r = Item::select(['id'], ['deleted' => true, 'network' => Protocol::TWITTER]);
791         while ($row = DBA::fetch($r)) {
792                 DBA::delete('item', ['id' => $row['id']]);
793         }
794         DBA::close($r);
795
796         Logger::log('twitter_expire: expire_start');
797
798         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
799         if (DBA::isResult($r)) {
800                 foreach ($r as $rr) {
801                         Logger::log('twitter_expire: user ' . $rr['uid']);
802                         Item::expire($rr['uid'], $days, Protocol::TWITTER, true);
803                 }
804         }
805
806         Logger::log('twitter_expire: expire_end');
807 }
808
809 function twitter_prepare_body(App $a, array &$b)
810 {
811         if ($b["item"]["network"] != Protocol::TWITTER) {
812                 return;
813         }
814
815         if ($b["preview"]) {
816                 $max_char = 280;
817                 $item = $b["item"];
818                 $item["plink"] = $a->getBaseURL() . "/display/" . $item["guid"];
819
820                 $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
821                 $orig_post = Item::selectFirst(['author-link'], $condition);
822                 if (DBA::isResult($orig_post)) {
823                         $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
824                         $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
825                         $nicknameplain = "@" . $nicknameplain;
826
827                         if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
828                                 $item["body"] = $nickname . " " . $item["body"];
829                         }
830                 }
831
832                 $msgarr = ItemContent::getPlaintextPost($item, $max_char, true, 8);
833                 $msg = $msgarr["text"];
834
835                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
836                         $msg .= " " . $msgarr["url"];
837                 }
838
839                 if (isset($msgarr["image"])) {
840                         $msg .= " " . $msgarr["image"];
841                 }
842
843                 $b['html'] = nl2br(htmlspecialchars($msg));
844         }
845 }
846
847 /**
848  * @brief Build the item array for the mirrored post
849  *
850  * @param App $a Application class
851  * @param integer $uid User id
852  * @param object $post Twitter object with the post
853  *
854  * @return array item data to be posted
855  */
856 function twitter_do_mirrorpost(App $a, $uid, $post)
857 {
858         $datarray['api_source'] = true;
859         $datarray['profile_uid'] = $uid;
860         $datarray['extid'] = Protocol::TWITTER;
861         $datarray['message_id'] = Item::newURI($uid, Protocol::TWITTER . ':' . $post->id);
862         $datarray['protocol'] = Conversation::PARCEL_TWITTER;
863         $datarray['source'] = json_encode($post);
864         $datarray['title'] = '';
865
866         if (!empty($post->retweeted_status)) {
867                 // We don't support nested shares, so we mustn't show quotes as shares on retweets
868                 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
869
870                 if (empty($item['body'])) {
871                         return [];
872                 }
873
874                 $datarray['body'] = "\n" . share_header(
875                         $item['author-name'],
876                         $item['author-link'],
877                         $item['author-avatar'],
878                         '',
879                         $item['created'],
880                         $item['plink']
881                 );
882
883                 $datarray['body'] .= $item['body'] . '[/share]';
884         } else {
885                 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
886
887                 if (empty($item['body'])) {
888                         return [];
889                 }
890
891                 $datarray['body'] = $item['body'];
892         }
893
894         $datarray['source'] = $item['app'];
895         $datarray['verb'] = $item['verb'];
896
897         if (isset($item['location'])) {
898                 $datarray['location'] = $item['location'];
899         }
900
901         if (isset($item['coord'])) {
902                 $datarray['coord'] = $item['coord'];
903         }
904
905         return $datarray;
906 }
907
908 function twitter_fetchtimeline(App $a, $uid)
909 {
910         $ckey    = Config::get('twitter', 'consumerkey');
911         $csecret = Config::get('twitter', 'consumersecret');
912         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
913         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
914         $lastid  = PConfig::get($uid, 'twitter', 'lastid');
915
916         $application_name = Config::get('twitter', 'application_name');
917
918         if ($application_name == "") {
919                 $application_name = $a->getHostName();
920         }
921
922         $has_picture = false;
923
924         require_once 'mod/item.php';
925         require_once 'mod/share.php';
926
927         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
928
929         $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
930
931         $first_time = ($lastid == "");
932
933         if ($lastid != "") {
934                 $parameters["since_id"] = $lastid;
935         }
936
937         try {
938                 $items = $connection->get('statuses/user_timeline', $parameters);
939         } catch (TwitterOAuthException $e) {
940                 Logger::log('Error fetching timeline for user ' . $uid . ': ' . $e->getMessage());
941                 return;
942         }
943
944         if (!is_array($items)) {
945                 Logger::log('No items for user ' . $uid, Logger::INFO);
946                 return;
947         }
948
949         $posts = array_reverse($items);
950
951         Logger::log('Starting from ID ' . $lastid . ' for user ' . $uid, Logger::DEBUG);
952
953         if (count($posts)) {
954                 foreach ($posts as $post) {
955                         if ($post->id_str > $lastid) {
956                                 $lastid = $post->id_str;
957                                 PConfig::set($uid, 'twitter', 'lastid', $lastid);
958                         }
959
960                         if ($first_time) {
961                                 continue;
962                         }
963
964                         if (!stristr($post->source, $application_name)) {
965                                 $_SESSION["authenticated"] = true;
966                                 $_SESSION["uid"] = $uid;
967
968                                 Logger::log('Preparing Twitter ID ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
969
970                                 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
971
972                                 if (empty($_REQUEST['body'])) {
973                                         continue;
974                                 }
975
976                                 Logger::log('Posting Twitter ID ' . $post->id_str . ' for user ' . $uid);
977
978                                 item_post($a);
979                         }
980                 }
981         }
982         PConfig::set($uid, 'twitter', 'lastid', $lastid);
983         Logger::log('Last ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
984 }
985
986 function twitter_fix_avatar($avatar)
987 {
988         $new_avatar = str_replace("_normal.", ".", $avatar);
989
990         $info = Image::getInfoFromURL($new_avatar);
991         if (!$info) {
992                 $new_avatar = $avatar;
993         }
994
995         return $new_avatar;
996 }
997
998 function twitter_fetch_contact($uid, $data, $create_user)
999 {
1000         if (empty($data->id_str)) {
1001                 return -1;
1002         }
1003
1004         $avatar = twitter_fix_avatar($data->profile_image_url_https);
1005         $url = "https://twitter.com/" . $data->screen_name;
1006         $addr = $data->screen_name . "@twitter.com";
1007
1008         GContact::update(["url" => $url, "network" => Protocol::TWITTER,
1009                 "photo" => $avatar, "hide" => true,
1010                 "name" => $data->name, "nick" => $data->screen_name,
1011                 "location" => $data->location, "about" => $data->description,
1012                 "addr" => $addr, "generation" => 2]);
1013
1014         $fields = ['url' => $url, 'network' => Protocol::TWITTER,
1015                 'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
1016                 'location' => $data->location, 'about' => $data->description];
1017
1018         $cid = Contact::getIdForURL($url, 0, true, $fields);
1019         if (!empty($cid)) {
1020                 DBA::update('contact', $fields, ['id' => $cid]);
1021                 Contact::updateAvatar($avatar, 0, $cid);
1022         }
1023
1024         $contact = DBA::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
1025         if (!DBA::isResult($contact) && !$create_user) {
1026                 return 0;
1027         }
1028
1029         if (!DBA::isResult($contact)) {
1030                 // create contact record
1031                 $fields['uid'] = $uid;
1032                 $fields['created'] = DateTimeFormat::utcNow();
1033                 $fields['nurl'] = Strings::normaliseLink($url);
1034                 $fields['alias'] = 'twitter::' . $data->id_str;
1035                 $fields['poll'] = 'twitter::' . $data->id_str;
1036                 $fields['rel'] = Contact::FRIEND;
1037                 $fields['priority'] = 1;
1038                 $fields['writable'] = true;
1039                 $fields['blocked'] = false;
1040                 $fields['readonly'] = false;
1041                 $fields['pending'] = false;
1042
1043                 if (!DBA::insert('contact', $fields)) {
1044                         return false;
1045                 }
1046
1047                 $contact_id = DBA::lastInsertId();
1048
1049                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1050
1051                 Contact::updateAvatar($avatar, $uid, $contact_id);
1052         } else {
1053                 if ($contact["readonly"] || $contact["blocked"]) {
1054                         Logger::log("twitter_fetch_contact: Contact '" . $contact["nick"] . "' is blocked or readonly.", Logger::DEBUG);
1055                         return -1;
1056                 }
1057
1058                 $contact_id = $contact['id'];
1059
1060                 // update profile photos once every twelve hours as we have no notification of when they change.
1061                 $update_photo = ($contact['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
1062
1063                 // check that we have all the photos, this has been known to fail on occasion
1064                 if (empty($contact['photo']) || empty($contact['thumb']) || empty($contact['micro']) || $update_photo) {
1065                         Logger::log("twitter_fetch_contact: Updating contact " . $data->screen_name, Logger::DEBUG);
1066
1067                         Contact::updateAvatar($avatar, $uid, $contact['id']);
1068
1069                         $fields['name-date'] = DateTimeFormat::utcNow();
1070                         $fields['uri-date'] = DateTimeFormat::utcNow();
1071
1072                         DBA::update('contact', $fields, ['id' => $contact['id']]);
1073                 }
1074         }
1075
1076         return $contact_id;
1077 }
1078
1079 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1080 {
1081         $ckey = Config::get('twitter', 'consumerkey');
1082         $csecret = Config::get('twitter', 'consumersecret');
1083         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1084         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1085
1086         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1087                 intval($uid));
1088
1089         if (DBA::isResult($r)) {
1090                 $self = $r[0];
1091         } else {
1092                 return;
1093         }
1094
1095         $parameters = [];
1096
1097         if ($screen_name != "") {
1098                 $parameters["screen_name"] = $screen_name;
1099         }
1100
1101         if ($user_id != "") {
1102                 $parameters["user_id"] = $user_id;
1103         }
1104
1105         // Fetching user data
1106         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1107         try {
1108                 $user = $connection->get('users/show', $parameters);
1109         } catch (TwitterOAuthException $e) {
1110                 Logger::log('twitter_fetchuser: Error fetching user ' . $uid . ': ' . $e->getMessage());
1111                 return;
1112         }
1113
1114         if (!is_object($user)) {
1115                 return;
1116         }
1117
1118         $contact_id = twitter_fetch_contact($uid, $user, true);
1119
1120         return $contact_id;
1121 }
1122
1123 function twitter_expand_entities(App $a, $body, $item, $picture)
1124 {
1125         $plain = $body;
1126
1127         $tags_arr = [];
1128
1129         foreach ($item->entities->hashtags AS $hashtag) {
1130                 $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . $hashtag->text . ']' . $hashtag->text . '[/url]';
1131                 $tags_arr['#' . $hashtag->text] = $url;
1132                 $body = str_replace('#' . $hashtag->text, $url, $body);
1133         }
1134
1135         foreach ($item->entities->user_mentions AS $mention) {
1136                 $url = '@[url=https://twitter.com/' . rawurlencode($mention->screen_name) . ']' . $mention->screen_name . '[/url]';
1137                 $tags_arr['@' . $mention->screen_name] = $url;
1138                 $body = str_replace('@' . $mention->screen_name, $url, $body);
1139         }
1140
1141         if (isset($item->entities->urls)) {
1142                 $type = '';
1143                 $footerurl = '';
1144                 $footerlink = '';
1145                 $footer = '';
1146
1147                 foreach ($item->entities->urls as $url) {
1148                         $plain = str_replace($url->url, '', $plain);
1149
1150                         if ($url->url && $url->expanded_url && $url->display_url) {
1151                                 // Quote tweet, we just remove the quoted tweet URL from the body, the share block will be added later.
1152                                 if (isset($item->quoted_status_id_str)
1153                                         && substr($url->expanded_url, -strlen($item->quoted_status_id_str)) == $item->quoted_status_id_str ) {
1154                                         $body = str_replace($url->url, '', $body);
1155                                         continue;
1156                                 }
1157
1158                                 $expanded_url = $url->expanded_url;
1159
1160                                 $final_url = Network::finalUrl($url->expanded_url);
1161
1162                                 $oembed_data = OEmbed::fetchURL($final_url);
1163
1164                                 if (empty($oembed_data) || empty($oembed_data->type)) {
1165                                         continue;
1166                                 }
1167
1168                                 // Quickfix: Workaround for URL with '[' and ']' in it
1169                                 if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
1170                                         $expanded_url = $url->url;
1171                                 }
1172
1173                                 if ($type == '') {
1174                                         $type = $oembed_data->type;
1175                                 }
1176
1177                                 if ($oembed_data->type == 'video') {
1178                                         $type = $oembed_data->type;
1179                                         $footerurl = $expanded_url;
1180                                         $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1181
1182                                         $body = str_replace($url->url, $footerlink, $body);
1183                                 } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
1184                                         $body = str_replace($url->url, '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]', $body);
1185                                 } elseif ($oembed_data->type != 'link') {
1186                                         $body = str_replace($url->url, '[url=' . $expanded_url . ']' . $url->display_url . '[/url]', $body);
1187                                 } else {
1188                                         $img_str = Network::fetchUrl($final_url, true, 4);
1189
1190                                         $tempfile = tempnam(get_temppath(), 'cache');
1191                                         file_put_contents($tempfile, $img_str);
1192
1193                                         // See http://php.net/manual/en/function.exif-imagetype.php#79283
1194                                         if (filesize($tempfile) > 11) {
1195                                                 $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1196                                         } else {
1197                                                 $mime = false;
1198                                         }
1199
1200                                         unlink($tempfile);
1201
1202                                         if (substr($mime, 0, 6) == 'image/') {
1203                                                 $type = 'photo';
1204                                                 $body = str_replace($url->url, '[img]' . $final_url . '[/img]', $body);
1205                                         } else {
1206                                                 $type = $oembed_data->type;
1207                                                 $footerurl = $expanded_url;
1208                                                 $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1209
1210                                                 $body = str_replace($url->url, $footerlink, $body);
1211                                         }
1212                                 }
1213                         }
1214                 }
1215
1216                 // Footer will be taken care of with a share block in the case of a quote
1217                 if (empty($item->quoted_status)) {
1218                         if ($footerurl != '') {
1219                                 $footer = add_page_info($footerurl, false, $picture);
1220                         }
1221
1222                         if (($footerlink != '') && (trim($footer) != '')) {
1223                                 $removedlink = trim(str_replace($footerlink, '', $body));
1224
1225                                 if (($removedlink == '') || strstr($body, $removedlink)) {
1226                                         $body = $removedlink;
1227                                 }
1228
1229                                 $body .= $footer;
1230                         }
1231
1232                         if ($footer == '' && $picture != '') {
1233                                 $body .= "\n\n[img]" . $picture . "[/img]\n";
1234                         } elseif ($footer == '' && $picture == '') {
1235                                 $body = add_page_info_to_body($body);
1236                         }
1237                 }
1238         }
1239
1240         // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1241         $tags = BBCode::getTags($body);
1242
1243         if (count($tags)) {
1244                 foreach ($tags as $tag) {
1245                         if (strstr(trim($tag), ' ')) {
1246                                 continue;
1247                         }
1248
1249                         if (strpos($tag, '#') === 0) {
1250                                 if (strpos($tag, '[url=')) {
1251                                         continue;
1252                                 }
1253
1254                                 // don't link tags that are already embedded in links
1255                                 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1256                                         continue;
1257                                 }
1258                                 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1259                                         continue;
1260                                 }
1261
1262                                 $basetag = str_replace('_', ' ', substr($tag, 1));
1263                                 $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
1264                                 $body = str_replace($tag, $url, $body);
1265                                 $tags_arr['#' . $basetag] = $url;
1266                         } elseif (strpos($tag, '@') === 0) {
1267                                 if (strpos($tag, '[url=')) {
1268                                         continue;
1269                                 }
1270
1271                                 $basetag = substr($tag, 1);
1272                                 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1273                                 $body = str_replace($tag, $url, $body);
1274                                 $tags_arr['@' . $basetag] = $url;
1275                         }
1276                 }
1277         }
1278
1279         $tags = implode($tags_arr, ',');
1280
1281         return ['body' => $body, 'tags' => $tags, 'plain' => $plain];
1282 }
1283
1284 /**
1285  * @brief Fetch media entities and add media links to the body
1286  *
1287  * @param object $post Twitter object with the post
1288  * @param array $postarray Array of the item that is about to be posted
1289  *
1290  * @return $picture string Image URL or empty string
1291  */
1292 function twitter_media_entities($post, array &$postarray)
1293 {
1294         // There are no media entities? So we quit.
1295         if (empty($post->extended_entities->media)) {
1296                 return '';
1297         }
1298
1299         // When the post links to an external page, we only take one picture.
1300         // We only do this when there is exactly one media.
1301         if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1302                 $medium = $post->extended_entities->media[0];
1303                 $picture = '';
1304                 foreach ($post->entities->urls as $link) {
1305                         // Let's make sure the external link url matches the media url
1306                         if ($medium->url == $link->url && isset($medium->media_url_https)) {
1307                                 $picture = $medium->media_url_https;
1308                                 $postarray['body'] = str_replace($medium->url, '', $postarray['body']);
1309                                 return $picture;
1310                         }
1311                 }
1312         }
1313
1314         // This is a pure media post, first search for all media urls
1315         $media = [];
1316         foreach ($post->extended_entities->media AS $medium) {
1317                 if (!isset($media[$medium->url])) {
1318                         $media[$medium->url] = '';
1319                 }
1320                 switch ($medium->type) {
1321                         case 'photo':
1322                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1323                                 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1324                                 break;
1325                         case 'video':
1326                         case 'animated_gif':
1327                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1328                                 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1329                                 if (is_array($medium->video_info->variants)) {
1330                                         $bitrate = 0;
1331                                         // We take the video with the highest bitrate
1332                                         foreach ($medium->video_info->variants AS $variant) {
1333                                                 if (($variant->content_type == 'video/mp4') && ($variant->bitrate >= $bitrate)) {
1334                                                         $media[$medium->url] = "\n[video]" . $variant->url . '[/video]';
1335                                                         $bitrate = $variant->bitrate;
1336                                                 }
1337                                         }
1338                                 }
1339                                 break;
1340                         // The following code will only be activated for test reasons
1341                         //default:
1342                         //      $postarray['body'] .= print_r($medium, true);
1343                 }
1344         }
1345
1346         // Now we replace the media urls.
1347         foreach ($media AS $key => $value) {
1348                 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1349         }
1350
1351         return '';
1352 }
1353
1354 function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $only_existing_contact, $noquote)
1355 {
1356         $postarray = [];
1357         $postarray['network'] = Protocol::TWITTER;
1358         $postarray['uid'] = $uid;
1359         $postarray['wall'] = 0;
1360         $postarray['uri'] = "twitter::" . $post->id_str;
1361         $postarray['protocol'] = Conversation::PARCEL_TWITTER;
1362         $postarray['source'] = json_encode($post);
1363
1364         // Don't import our own comments
1365         if (Item::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
1366                 Logger::log("Item with extid " . $postarray['uri'] . " found.", Logger::DEBUG);
1367                 return [];
1368         }
1369
1370         $contactid = 0;
1371
1372         if ($post->in_reply_to_status_id_str != "") {
1373                 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1374
1375                 $fields = ['uri', 'parent-uri', 'parent'];
1376                 $parent_item = Item::selectFirst($fields, ['uri' => $parent, 'uid' => $uid]);
1377                 if (!DBA::isResult($parent_item)) {
1378                         $parent_item = Item::selectFirst($fields, ['extid' => $parent, 'uid' => $uid]);
1379                 }
1380
1381                 if (DBA::isResult($parent_item)) {
1382                         $postarray['thr-parent'] = $parent_item['uri'];
1383                         $postarray['parent-uri'] = $parent_item['parent-uri'];
1384                         $postarray['parent'] = $parent_item['parent'];
1385                         $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1386                 } else {
1387                         $postarray['thr-parent'] = $postarray['uri'];
1388                         $postarray['parent-uri'] = $postarray['uri'];
1389                         $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1390                 }
1391
1392                 // Is it me?
1393                 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1394
1395                 if ($post->user->id_str == $own_id) {
1396                         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1397                                 intval($uid));
1398
1399                         if (DBA::isResult($r)) {
1400                                 $contactid = $r[0]["id"];
1401
1402                                 $postarray['owner-name']   = $r[0]["name"];
1403                                 $postarray['owner-link']   = $r[0]["url"];
1404                                 $postarray['owner-avatar'] = $r[0]["photo"];
1405                         } else {
1406                                 Logger::log("No self contact for user " . $uid, Logger::DEBUG);
1407                                 return [];
1408                         }
1409                 }
1410                 // Don't create accounts of people who just comment something
1411                 $create_user = false;
1412         } else {
1413                 $postarray['parent-uri'] = $postarray['uri'];
1414                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1415         }
1416
1417         if ($contactid == 0) {
1418                 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1419
1420                 $postarray['owner-name'] = $post->user->name;
1421                 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1422                 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1423         }
1424
1425         if (($contactid == 0) && !$only_existing_contact) {
1426                 $contactid = $self['id'];
1427         } elseif ($contactid <= 0) {
1428                 Logger::log("Contact ID is zero or less than zero.", Logger::DEBUG);
1429                 return [];
1430         }
1431
1432         $postarray['contact-id'] = $contactid;
1433
1434         $postarray['verb'] = ACTIVITY_POST;
1435         $postarray['author-name'] = $postarray['owner-name'];
1436         $postarray['author-link'] = $postarray['owner-link'];
1437         $postarray['author-avatar'] = $postarray['owner-avatar'];
1438         $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1439         $postarray['app'] = strip_tags($post->source);
1440
1441         if ($post->user->protected) {
1442                 $postarray['private'] = 1;
1443                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1444         } else {
1445                 $postarray['private'] = 0;
1446                 $postarray['allow_cid'] = '';
1447         }
1448
1449         if (!empty($post->full_text)) {
1450                 $postarray['body'] = $post->full_text;
1451         } else {
1452                 $postarray['body'] = $post->text;
1453         }
1454
1455         // When the post contains links then use the correct object type
1456         if (count($post->entities->urls) > 0) {
1457                 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1458         }
1459
1460         // Search for media links
1461         $picture = twitter_media_entities($post, $postarray);
1462
1463         $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1464         $postarray['body'] = $converted["body"];
1465         $postarray['tag'] = $converted["tags"];
1466         $postarray['created'] = DateTimeFormat::utc($post->created_at);
1467         $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1468
1469         $statustext = $converted["plain"];
1470
1471         if (!empty($post->place->name)) {
1472                 $postarray["location"] = $post->place->name;
1473         }
1474         if (!empty($post->place->full_name)) {
1475                 $postarray["location"] = $post->place->full_name;
1476         }
1477         if (!empty($post->geo->coordinates)) {
1478                 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1479         }
1480         if (!empty($post->coordinates->coordinates)) {
1481                 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1482         }
1483         if (!empty($post->retweeted_status)) {
1484                 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1485
1486                 if (empty($retweet['body'])) {
1487                         return [];
1488                 }
1489
1490                 if (!$noquote) {
1491                         // Store the original tweet
1492                         Item::insert($retweet);
1493
1494                         // CHange the other post into a reshare activity
1495                         $postarray['verb'] = ACTIVITY2_ANNOUNCE;
1496                         $postarray['gravity'] = GRAVITY_ACTIVITY;
1497                         $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1498
1499                         $postarray['thr-parent'] = $retweet['uri'];
1500                         $postarray['parent-uri'] = $retweet['uri'];
1501                 } else {
1502                         $retweet['source'] = $postarray['source'];
1503                         $retweet['private'] = $postarray['private'];
1504                         $retweet['allow_cid'] = $postarray['allow_cid'];
1505                         $retweet['contact-id'] = $postarray['contact-id'];
1506                         $retweet['owner-name'] = $postarray['owner-name'];
1507                         $retweet['owner-link'] = $postarray['owner-link'];
1508                         $retweet['owner-avatar'] = $postarray['owner-avatar'];
1509
1510                         $postarray = $retweet;
1511                 }
1512         }
1513
1514         if (!empty($post->quoted_status) && !$noquote) {
1515                 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1516
1517                 if (empty($quoted['body'])) {
1518                         return [];
1519                 }
1520
1521                 $postarray['body'] .= "\n" . share_header(
1522                         $quoted['author-name'],
1523                         $quoted['author-link'],
1524                         $quoted['author-avatar'],
1525                         "",
1526                         $quoted['created'],
1527                         $quoted['plink']
1528                 );
1529
1530                 $postarray['body'] .= $quoted['body'] . '[/share]';
1531         }
1532
1533         return $postarray;
1534 }
1535
1536 function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, array $self)
1537 {
1538         Logger::log("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, Logger::DEBUG);
1539
1540         $posts = [];
1541
1542         while (!empty($post->in_reply_to_status_id_str)) {
1543                 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1544
1545                 try {
1546                         $post = $connection->get('statuses/show', $parameters);
1547                 } catch (TwitterOAuthException $e) {
1548                         Logger::log('twitter_fetchparentposts: Error fetching for user ' . $uid . ' and post ' . $post->id_str . ': ' . $e->getMessage());
1549                         break;
1550                 }
1551
1552                 if (empty($post)) {
1553                         Logger::log("twitter_fetchparentposts: Can't fetch post " . $parameters['id'], Logger::DEBUG);
1554                         break;
1555                 }
1556
1557                 if (empty($post->id_str)) {
1558                         Logger::log("twitter_fetchparentposts: This is not a post " . json_encode($post), Logger::DEBUG);
1559                         break;
1560                 }
1561
1562                 if (Item::exists(['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
1563                         break;
1564                 }
1565
1566                 $posts[] = $post;
1567         }
1568
1569         Logger::log("twitter_fetchparentposts: Fetching " . count($posts) . " parents", Logger::DEBUG);
1570
1571         $posts = array_reverse($posts);
1572
1573         if (!empty($posts)) {
1574                 foreach ($posts as $post) {
1575                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1576
1577                         if (empty($postarray['body'])) {
1578                                 continue;
1579                         }
1580
1581                         $item = Item::insert($postarray);
1582
1583                         $postarray["id"] = $item;
1584
1585                         Logger::log('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1586                 }
1587         }
1588 }
1589
1590 function twitter_fetchhometimeline(App $a, $uid)
1591 {
1592         $ckey    = Config::get('twitter', 'consumerkey');
1593         $csecret = Config::get('twitter', 'consumersecret');
1594         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1595         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1596         $create_user = PConfig::get($uid, 'twitter', 'create_user');
1597         $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1598
1599         Logger::log("Fetching timeline for user " . $uid, Logger::DEBUG);
1600
1601         $application_name = Config::get('twitter', 'application_name');
1602
1603         if ($application_name == "") {
1604                 $application_name = $a->getHostName();
1605         }
1606
1607         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1608
1609         try {
1610                 $own_contact = twitter_fetch_own_contact($a, $uid);
1611         } catch (TwitterOAuthException $e) {
1612                 Logger::log('Error fetching own contact for user ' . $uid . ': ' . $e->getMessage());
1613                 return;
1614         }
1615
1616         $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1617                 intval($own_contact),
1618                 intval($uid));
1619
1620         if (DBA::isResult($r)) {
1621                 $own_id = $r[0]["nick"];
1622         } else {
1623                 Logger::log("Own twitter contact not found for user " . $uid);
1624                 return;
1625         }
1626
1627         $self = User::getOwnerDataById($uid);
1628         if ($self === false) {
1629                 Logger::log("Own contact not found for user " . $uid);
1630                 return;
1631         }
1632
1633         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1634         //$parameters["count"] = 200;
1635         // Fetching timeline
1636         $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1637
1638         $first_time = ($lastid == "");
1639
1640         if ($lastid != "") {
1641                 $parameters["since_id"] = $lastid;
1642         }
1643
1644         try {
1645                 $items = $connection->get('statuses/home_timeline', $parameters);
1646         } catch (TwitterOAuthException $e) {
1647                 Logger::log('Error fetching home timeline for user ' . $uid . ': ' . $e->getMessage());
1648                 return;
1649         }
1650
1651         if (!is_array($items)) {
1652                 Logger::log('No array while fetching home timeline for user ' . $uid . ': ' . print_r($items, true));
1653                 return;
1654         }
1655
1656         if (empty($items)) {
1657                 Logger::log('No new timeline content for user ' . $uid, Logger::INFO);
1658                 return;
1659         }
1660
1661         $posts = array_reverse($items);
1662
1663         Logger::log('Fetching timeline from ID ' . $lastid . ' for user ' . $uid . ' ' . sizeof($posts) . ' items', Logger::DEBUG);
1664
1665         if (count($posts)) {
1666                 foreach ($posts as $post) {
1667                         if ($post->id_str > $lastid) {
1668                                 $lastid = $post->id_str;
1669                                 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1670                         }
1671
1672                         if ($first_time) {
1673                                 continue;
1674                         }
1675
1676                         if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1677                                 Logger::log("Skip previously sent post", Logger::DEBUG);
1678                                 continue;
1679                         }
1680
1681                         if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1682                                 Logger::log("Skip post that will be mirrored", Logger::DEBUG);
1683                                 continue;
1684                         }
1685
1686                         if ($post->in_reply_to_status_id_str != "") {
1687                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1688                         }
1689
1690                         Logger::log('Preparing post ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
1691
1692                         $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1693
1694                         if (empty($postarray['body']) || trim($postarray['body']) == "") {
1695                                 Logger::log('Empty body for post ' . $post->id_str . ' and user ' . $uid, Logger::DEBUG);
1696                                 continue;
1697                         }
1698
1699                         $notify = false;
1700
1701                         if (($postarray['uri'] == $postarray['parent-uri']) && ($postarray['author-link'] == $postarray['owner-link'])) {
1702                                 $contact = DBA::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
1703                                 if (DBA::isResult($contact)) {
1704                                         $notify = Item::isRemoteSelf($contact, $postarray);
1705                                 }
1706                         }
1707
1708                         $item = Item::insert($postarray, false, $notify);
1709                         $postarray["id"] = $item;
1710
1711                         Logger::log('User ' . $uid . ' posted home timeline item ' . $item);
1712                 }
1713         }
1714         PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1715
1716         Logger::log('Last timeline ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1717
1718         // Fetching mentions
1719         $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1720
1721         $first_time = ($lastid == "");
1722
1723         if ($lastid != "") {
1724                 $parameters["since_id"] = $lastid;
1725         }
1726
1727         try {
1728                 $items = $connection->get('statuses/mentions_timeline', $parameters);
1729         } catch (TwitterOAuthException $e) {
1730                 Logger::log('Error fetching mentions: ' . $e->getMessage());
1731                 return;
1732         }
1733
1734         if (!is_array($items)) {
1735                 Logger::log("Error fetching mentions: " . print_r($items, true), Logger::DEBUG);
1736                 return;
1737         }
1738
1739         $posts = array_reverse($items);
1740
1741         Logger::log("Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", Logger::DEBUG);
1742
1743         if (count($posts)) {
1744                 foreach ($posts as $post) {
1745                         if ($post->id_str > $lastid) {
1746                                 $lastid = $post->id_str;
1747                         }
1748
1749                         if ($first_time) {
1750                                 continue;
1751                         }
1752
1753                         if ($post->in_reply_to_status_id_str != "") {
1754                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1755                         }
1756
1757                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1758
1759                         if (empty($postarray['body'])) {
1760                                 continue;
1761                         }
1762
1763                         $item = Item::insert($postarray);
1764
1765                         Logger::log('User ' . $uid . ' posted mention timeline item ' . $item);
1766                 }
1767         }
1768
1769         PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1770
1771         Logger::log('Last mentions ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1772 }
1773
1774 function twitter_fetch_own_contact(App $a, $uid)
1775 {
1776         $ckey    = Config::get('twitter', 'consumerkey');
1777         $csecret = Config::get('twitter', 'consumersecret');
1778         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1779         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1780
1781         $own_id = PConfig::get($uid, 'twitter', 'own_id');
1782
1783         $contact_id = 0;
1784
1785         if ($own_id == "") {
1786                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1787
1788                 // Fetching user data
1789                 // get() may throw TwitterOAuthException, but we will catch it later
1790                 $user = $connection->get('account/verify_credentials');
1791                 if (empty($user) || empty($user->id_str)) {
1792                         return false;
1793                 }
1794
1795                 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1796
1797                 $contact_id = twitter_fetch_contact($uid, $user, true);
1798         } else {
1799                 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1800                         intval($uid),
1801                         DBA::escape("twitter::" . $own_id));
1802                 if (DBA::isResult($r)) {
1803                         $contact_id = $r[0]["id"];
1804                 } else {
1805                         PConfig::delete($uid, 'twitter', 'own_id');
1806                 }
1807         }
1808
1809         return $contact_id;
1810 }
1811
1812 function twitter_is_retweet(App $a, $uid, $body)
1813 {
1814         $body = trim($body);
1815
1816         // Skip if it isn't a pure repeated messages
1817         // Does it start with a share?
1818         if (strpos($body, "[share") > 0) {
1819                 return false;
1820         }
1821
1822         // Does it end with a share?
1823         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1824                 return false;
1825         }
1826
1827         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1828         // Skip if there is no shared message in there
1829         if ($body == $attributes) {
1830                 return false;
1831         }
1832
1833         $link = "";
1834         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1835         if (!empty($matches[1])) {
1836                 $link = $matches[1];
1837         }
1838
1839         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1840         if (!empty($matches[1])) {
1841                 $link = $matches[1];
1842         }
1843
1844         $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1845         if ($id == $link) {
1846                 return false;
1847         }
1848
1849         Logger::log('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, Logger::DEBUG);
1850
1851         $ckey    = Config::get('twitter', 'consumerkey');
1852         $csecret = Config::get('twitter', 'consumersecret');
1853         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1854         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1855
1856         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1857         $result = $connection->post('statuses/retweet/' . $id);
1858
1859         Logger::log('twitter_is_retweet: result ' . print_r($result, true), Logger::DEBUG);
1860
1861         return !isset($result->errors);
1862 }
1863
1864 function twitter_update_mentions($body)
1865 {
1866         $URLSearchString = "^\[\]";
1867         $return = preg_replace_callback(
1868                 "/@\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1869                 function ($matches) {
1870                         if (strpos($matches[1], 'twitter.com')) {
1871                                 $return = '@' . substr($matches[1], strrpos($matches[1], '/') + 1);
1872                         } else {
1873                                 $return = $matches[2] . ' (' . $matches[1] . ')';
1874                         }
1875
1876                         return $return;
1877                 },
1878                 $body
1879         );
1880
1881         return $return;
1882 }
1883
1884 function twitter_convert_share(array $attributes, array $author_contact, $content, $is_quote_share)
1885 {
1886         if ($author_contact['network'] == Protocol::TWITTER) {
1887                 $mention = '@' . $author_contact['nickname'];
1888         } else {
1889                 $mention = $author_contact['addr'];
1890         }
1891
1892         return ($is_quote_share ? "\n\n" : '' ) . 'RT ' . $mention . ': ' . $content . "\n\n" . $attributes['link'];
1893 }