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