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