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