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