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