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