Merge pull request #556 from tobiasd/20180321-nl
[friendica-addons.git] / twitter / twitter.php
1 <?php
2 /**
3  * Name: Twitter Connector
4  * Description: Bidirectional (posting, relaying and reading) connector for Twitter.
5  * Version: 1.1.0
6  * Author: Tobias Diekershoff <https://f.diekershoff.de/profile/tobias>
7  * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
8  * Maintainer: Hypolite Petovan <https://friendica.mrpetovan.com/profile/hypolite>
9  *
10  * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel, Hypolite Petovan
11  * All rights reserved.
12  *
13  * Redistribution and use in source and binary forms, with or without
14  * modification, are permitted provided that the following conditions are met:
15  *    * Redistributions of source code must retain the above copyright notice,
16  *     this list of conditions and the following disclaimer.
17  *    * Redistributions in binary form must reproduce the above
18  *    * copyright notice, this list of conditions and the following disclaimer in
19  *      the documentation and/or other materials provided with the distribution.
20  *    * Neither the name of the <organization> nor the names of its contributors
21  *      may be used to endorse or promote products derived from this software
22  *      without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27  * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
28  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
32  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
33  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34  *
35  */
36 /*   Twitter Addon for Friendica
37  *
38  *   Author: Tobias Diekershoff
39  *           tobias.diekershoff@gmx.net
40  *
41  *   License:3-clause BSD license
42  *
43  *   Configuration:
44  *     To use this addon you need a OAuth Consumer key pair (key & secret)
45  *     you can get it from Twitter at https://twitter.com/apps
46  *
47  *     Register your Friendica site as "Client" application with "Read & Write" access
48  *     we do not need "Twitter as login". When you've registered the app you get the
49  *     OAuth Consumer key and secret pair for your application/site.
50  *
51  *     Add this key pair to your global .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, $picture)
1153 {
1154         $plain = $body;
1155
1156         $tags_arr = [];
1157
1158         foreach ($item->entities->hashtags AS $hashtag) {
1159                 $url = "#[url=" . $a->get_baseurl() . "/search?tag=" . rawurlencode($hashtag->text) . "]" . $hashtag->text . "[/url]";
1160                 $tags_arr["#" . $hashtag->text] = $url;
1161                 $body = str_replace("#" . $hashtag->text, $url, $body);
1162         }
1163
1164         foreach ($item->entities->user_mentions AS $mention) {
1165                 $url = "@[url=https://twitter.com/" . rawurlencode($mention->screen_name) . "]" . $mention->screen_name . "[/url]";
1166                 $tags_arr["@" . $mention->screen_name] = $url;
1167                 $body = str_replace("@" . $mention->screen_name, $url, $body);
1168         }
1169
1170         if (isset($item->entities->urls)) {
1171                 $type = "";
1172                 $footerurl = "";
1173                 $footerlink = "";
1174                 $footer = "";
1175
1176                 foreach ($item->entities->urls as $url) {
1177                         $plain = str_replace($url->url, '', $plain);
1178
1179                         if ($url->url && $url->expanded_url && $url->display_url) {
1180                                 $expanded_url = Network::finalUrl($url->expanded_url);
1181
1182                                 $oembed_data = OEmbed::fetchURL($expanded_url);
1183
1184                                 // Quickfix: Workaround for URL with "[" and "]" in it
1185                                 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1186                                         $expanded_url = $url->url;
1187                                 }
1188
1189                                 if ($type == "") {
1190                                         $type = $oembed_data->type;
1191                                 }
1192
1193                                 if ($oembed_data->type == "video") {
1194                                         //$body = str_replace($url->url,
1195                                         //              "[video]".$expanded_url."[/video]", $body);
1196                                         //$dontincludemedia = true;
1197                                         $type = $oembed_data->type;
1198                                         $footerurl = $expanded_url;
1199                                         $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1200
1201                                         $body = str_replace($url->url, $footerlink, $body);
1202                                         //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1203                                 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1204                                         $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1205                                         //$dontincludemedia = true;
1206                                 } elseif ($oembed_data->type != "link") {
1207                                         $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1208                                 } else {
1209                                         $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1210
1211                                         $tempfile = tempnam(get_temppath(), "cache");
1212                                         file_put_contents($tempfile, $img_str);
1213                                         $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1214                                         unlink($tempfile);
1215
1216                                         if (substr($mime, 0, 6) == "image/") {
1217                                                 $type = "photo";
1218                                                 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1219                                                 //$dontincludemedia = true;
1220                                         } else {
1221                                                 $type = $oembed_data->type;
1222                                                 $footerurl = $expanded_url;
1223                                                 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1224
1225                                                 $body = str_replace($url->url, $footerlink, $body);
1226                                         }
1227                                 }
1228                         }
1229                 }
1230
1231                 if ($footerurl != "") {
1232                         $footer = add_page_info($footerurl, false, $picture);
1233                 }
1234
1235                 if (($footerlink != "") && (trim($footer) != "")) {
1236                         $removedlink = trim(str_replace($footerlink, "", $body));
1237
1238                         if (($removedlink == "") || strstr($body, $removedlink)) {
1239                                 $body = $removedlink;
1240                         }
1241
1242                         $body .= $footer;
1243                 }
1244
1245                 if (($footer == "") && ($picture != "")) {
1246                         $body .= "\n\n[img]" . $picture . "[/img]\n";
1247                 } elseif (($footer == "") && ($picture == "")) {
1248                         $body = add_page_info_to_body($body);
1249                 }
1250         }
1251
1252         // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1253         $tags = get_tags($body);
1254
1255         if (count($tags)) {
1256                 foreach ($tags as $tag) {
1257                         if (strstr(trim($tag), " ")) {
1258                                 continue;
1259                         }
1260
1261                         if (strpos($tag, '#') === 0) {
1262                                 if (strpos($tag, '[url=')) {
1263                                         continue;
1264                                 }
1265
1266                                 // don't link tags that are already embedded in links
1267                                 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1268                                         continue;
1269                                 }
1270                                 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1271                                         continue;
1272                                 }
1273
1274                                 $basetag = str_replace('_', ' ', substr($tag, 1));
1275                                 $url = '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1276                                 $body = str_replace($tag, $url, $body);
1277                                 $tags_arr["#" . $basetag] = $url;
1278                         } elseif (strpos($tag, '@') === 0) {
1279                                 if (strpos($tag, '[url=')) {
1280                                         continue;
1281                                 }
1282
1283                                 $basetag = substr($tag, 1);
1284                                 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1285                                 $body = str_replace($tag, $url, $body);
1286                                 $tags_arr["@" . $basetag] = $url;
1287                         }
1288                 }
1289         }
1290
1291         $tags = implode($tags_arr, ",");
1292
1293         return ["body" => $body, "tags" => $tags, "plain" => $plain];
1294 }
1295
1296 /**
1297  * @brief Fetch media entities and add media links to the body
1298  *
1299  * @param object $post Twitter object with the post
1300  * @param array $postarray Array of the item that is about to be posted
1301  *
1302  * @return $picture string Image URL or empty string
1303  */
1304 function twitter_media_entities($post, &$postarray)
1305 {
1306         // There are no media entities? So we quit.
1307         if (!is_array($post->extended_entities->media)) {
1308                 return "";
1309         }
1310
1311         // When the post links to an external page, we only take one picture.
1312         // We only do this when there is exactly one media.
1313         if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1314                 $picture = "";
1315                 foreach ($post->extended_entities->media AS $medium) {
1316                         if (isset($medium->media_url_https)) {
1317                                 $picture = $medium->media_url_https;
1318                                 $postarray['body'] = str_replace($medium->url, "", $postarray['body']);
1319                         }
1320                 }
1321                 return $picture;
1322         }
1323
1324         // This is a pure media post, first search for all media urls
1325         $media = [];
1326         foreach ($post->extended_entities->media AS $medium) {
1327                 switch ($medium->type) {
1328                         case 'photo':
1329                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1330                                 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1331                                 break;
1332                         case 'video':
1333                         case 'animated_gif':
1334                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1335                                 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1336                                 if (is_array($medium->video_info->variants)) {
1337                                         $bitrate = 0;
1338                                         // We take the video with the highest bitrate
1339                                         foreach ($medium->video_info->variants AS $variant) {
1340                                                 if (($variant->content_type == "video/mp4") && ($variant->bitrate >= $bitrate)) {
1341                                                         $media[$medium->url] = "\n[video]" . $variant->url . "[/video]";
1342                                                         $bitrate = $variant->bitrate;
1343                                                 }
1344                                         }
1345                                 }
1346                                 break;
1347                         // The following code will only be activated for test reasons
1348                         //default:
1349                         //      $postarray['body'] .= print_r($medium, true);
1350                 }
1351         }
1352
1353         // Now we replace the media urls.
1354         foreach ($media AS $key => $value) {
1355                 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1356         }
1357         return "";
1358 }
1359
1360 function twitter_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact, $noquote)
1361 {
1362         $postarray = [];
1363         $postarray['network'] = NETWORK_TWITTER;
1364         $postarray['gravity'] = 0;
1365         $postarray['uid'] = $uid;
1366         $postarray['wall'] = 0;
1367         $postarray['uri'] = "twitter::" . $post->id_str;
1368         $postarray['object'] = json_encode($post);
1369
1370         // Don't import our own comments
1371         $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1372                 dbesc($postarray['uri']),
1373                 intval($uid)
1374         );
1375
1376         if (count($r)) {
1377                 logger("Item with extid " . $postarray['uri'] . " found.", LOGGER_DEBUG);
1378                 return [];
1379         }
1380
1381         $contactid = 0;
1382
1383         if ($post->in_reply_to_status_id_str != "") {
1384                 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1385
1386                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1387                         dbesc($parent),
1388                         intval($uid)
1389                 );
1390                 if (count($r)) {
1391                         $postarray['thr-parent'] = $r[0]["uri"];
1392                         $postarray['parent-uri'] = $r[0]["parent-uri"];
1393                         $postarray['parent'] = $r[0]["parent"];
1394                         $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1395                 } else {
1396                         $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1397                                 dbesc($parent),
1398                                 intval($uid)
1399                         );
1400                         if (count($r)) {
1401                                 $postarray['thr-parent'] = $r[0]['uri'];
1402                                 $postarray['parent-uri'] = $r[0]['parent-uri'];
1403                                 $postarray['parent'] = $r[0]['parent'];
1404                                 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1405                         } else {
1406                                 $postarray['thr-parent'] = $postarray['uri'];
1407                                 $postarray['parent-uri'] = $postarray['uri'];
1408                                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1409                         }
1410                 }
1411
1412                 // Is it me?
1413                 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1414
1415                 if ($post->user->id_str == $own_id) {
1416                         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1417                                 intval($uid));
1418
1419                         if (count($r)) {
1420                                 $contactid = $r[0]["id"];
1421
1422                                 $postarray['owner-name']   = $r[0]["name"];
1423                                 $postarray['owner-link']   = $r[0]["url"];
1424                                 $postarray['owner-avatar'] = $r[0]["photo"];
1425                         } else {
1426                                 logger("No self contact for user " . $uid, LOGGER_DEBUG);
1427                                 return [];
1428                         }
1429                 }
1430                 // Don't create accounts of people who just comment something
1431                 $create_user = false;
1432         } else {
1433                 $postarray['parent-uri'] = $postarray['uri'];
1434                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1435         }
1436
1437         if ($contactid == 0) {
1438                 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1439
1440                 $postarray['owner-name'] = $post->user->name;
1441                 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1442                 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1443         }
1444
1445         if (($contactid == 0) && !$only_existing_contact) {
1446                 $contactid = $self['id'];
1447         } elseif ($contactid <= 0) {
1448                 logger("Contact ID is zero or less than zero.", LOGGER_DEBUG);
1449                 return [];
1450         }
1451
1452         $postarray['contact-id'] = $contactid;
1453
1454         $postarray['verb'] = ACTIVITY_POST;
1455         $postarray['author-name'] = $postarray['owner-name'];
1456         $postarray['author-link'] = $postarray['owner-link'];
1457         $postarray['author-avatar'] = $postarray['owner-avatar'];
1458         $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1459         $postarray['app'] = strip_tags($post->source);
1460
1461         if ($post->user->protected) {
1462                 $postarray['private'] = 1;
1463                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1464         }
1465
1466         if (is_string($post->full_text)) {
1467                 $postarray['body'] = $post->full_text;
1468         } else {
1469                 $postarray['body'] = $post->text;
1470         }
1471
1472         // When the post contains links then use the correct object type
1473         if (count($post->entities->urls) > 0) {
1474                 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1475         }
1476
1477         // Search for media links
1478         $picture = twitter_media_entities($post, $postarray);
1479
1480         $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1481         $postarray['body'] = $converted["body"];
1482         $postarray['tag'] = $converted["tags"];
1483         $postarray['created'] = DateTimeFormat::utc($post->created_at);
1484         $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1485
1486         $statustext = $converted["plain"];
1487
1488         if (is_string($post->place->name)) {
1489                 $postarray["location"] = $post->place->name;
1490         }
1491         if (is_string($post->place->full_name)) {
1492                 $postarray["location"] = $post->place->full_name;
1493         }
1494         if (is_array($post->geo->coordinates)) {
1495                 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1496         }
1497         if (is_array($post->coordinates->coordinates)) {
1498                 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1499         }
1500         if (is_object($post->retweeted_status)) {
1501                 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1502
1503                 $retweet['object'] = $postarray['object'];
1504                 $retweet['private'] = $postarray['private'];
1505                 $retweet['allow_cid'] = $postarray['allow_cid'];
1506                 $retweet['contact-id'] = $postarray['contact-id'];
1507                 $retweet['owner-name'] = $postarray['owner-name'];
1508                 $retweet['owner-link'] = $postarray['owner-link'];
1509                 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1510
1511                 $postarray = $retweet;
1512         }
1513
1514         if (is_object($post->quoted_status) && !$noquote) {
1515                 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1516
1517                 $postarray['body'] = $statustext;
1518
1519                 $postarray['body'] .= "\n" . share_header(
1520                         $quoted['author-name'],
1521                         $quoted['author-link'],
1522                         $quoted['author-avatar'],
1523                         "",
1524                         $quoted['created'],
1525                         $quoted['plink']
1526                 );
1527
1528                 $postarray['body'] .= $quoted['body'] . '[/share]';
1529         }
1530
1531         return $postarray;
1532 }
1533
1534 function twitter_checknotification(App $a, $uid, $own_id, $top_item, $postarray)
1535 {
1536         /// TODO: this whole function doesn't seem to work. Needs complete check
1537         $user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` LIMIT 1",
1538                 intval($uid)
1539         );
1540
1541         if (!count($user)) {
1542                 return;
1543         }
1544
1545         // Is it me?
1546         if (link_compare($user[0]["url"], $postarray['author-link'])) {
1547                 return;
1548         }
1549
1550         $own_user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1551                 intval($uid),
1552                 dbesc("twitter::".$own_id)
1553         );
1554
1555         if (!count($own_user)) {
1556                 return;
1557         }
1558
1559         // Is it me from twitter?
1560         if (link_compare($own_user[0]["url"], $postarray['author-link'])) {
1561                 return;
1562         }
1563
1564         $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 AND `deleted` = 0",
1565                 dbesc($postarray['parent-uri']),
1566                 intval($uid)
1567         );
1568
1569         if (count($myconv)) {
1570                 foreach ($myconv as $conv) {
1571                         // now if we find a match, it means we're in this conversation
1572                         if (!link_compare($conv['author-link'], $user[0]["url"]) && !link_compare($conv['author-link'], $own_user[0]["url"])) {
1573                                 continue;
1574                         }
1575
1576                         require_once 'include/enotify.php';
1577
1578                         $conv_parent = $conv['parent'];
1579
1580                         notification([
1581                                 'type' => NOTIFY_COMMENT,
1582                                 'notify_flags' => $user[0]['notify-flags'],
1583                                 'language' => $user[0]['language'],
1584                                 'to_name' => $user[0]['username'],
1585                                 'to_email' => $user[0]['email'],
1586                                 'uid' => $user[0]['uid'],
1587                                 'item' => $postarray,
1588                                 'link' => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($top_item)),
1589                                 'source_name' => $postarray['author-name'],
1590                                 'source_link' => $postarray['author-link'],
1591                                 'source_photo' => $postarray['author-avatar'],
1592                                 'verb' => ACTIVITY_POST,
1593                                 'otype' => 'item',
1594                                 'parent' => $conv_parent,
1595                         ]);
1596
1597                         // only send one notification
1598                         break;
1599                 }
1600         }
1601 }
1602
1603 function twitter_fetchparentposts(App $a, $uid, $post, $connection, $self, $own_id)
1604 {
1605         logger("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, LOGGER_DEBUG);
1606
1607         $posts = [];
1608
1609         while ($post->in_reply_to_status_id_str != "") {
1610                 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1611
1612                 $post = $connection->get('statuses/show', $parameters);
1613
1614                 if (!count($post)) {
1615                         logger("twitter_fetchparentposts: Can't fetch post " . $parameters->id, LOGGER_DEBUG);
1616                         break;
1617                 }
1618
1619                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1620                         dbesc("twitter::".$post->id_str),
1621                         intval($uid)
1622                 );
1623
1624                 if (count($r)) {
1625                         break;
1626                 }
1627
1628                 $posts[] = $post;
1629         }
1630
1631         logger("twitter_fetchparentposts: Fetching " . count($posts) . " parents", LOGGER_DEBUG);
1632
1633         $posts = array_reverse($posts);
1634
1635         if (count($posts)) {
1636                 foreach ($posts as $post) {
1637                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1638
1639                         if (trim($postarray['body']) == "") {
1640                                 continue;
1641                         }
1642
1643                         $item = Item::insert($postarray);
1644                         $postarray["id"] = $item;
1645
1646                         logger('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1647
1648                         if ($item && !function_exists("check_item_notification")) {
1649                                 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1650                         }
1651                 }
1652         }
1653 }
1654
1655 function twitter_fetchhometimeline(App $a, $uid)
1656 {
1657         $ckey    = Config::get('twitter', 'consumerkey');
1658         $csecret = Config::get('twitter', 'consumersecret');
1659         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1660         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1661         $create_user = PConfig::get($uid, 'twitter', 'create_user');
1662         $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1663
1664         logger("twitter_fetchhometimeline: Fetching for user " . $uid, LOGGER_DEBUG);
1665
1666         $application_name = Config::get('twitter', 'application_name');
1667
1668         if ($application_name == "") {
1669                 $application_name = $a->get_hostname();
1670         }
1671
1672         require_once 'include/items.php';
1673
1674         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1675
1676         $own_contact = twitter_fetch_own_contact($a, $uid);
1677
1678         $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1679                 intval($own_contact),
1680                 intval($uid));
1681
1682         if (count($r)) {
1683                 $own_id = $r[0]["nick"];
1684         } else {
1685                 logger("twitter_fetchhometimeline: Own twitter contact not found for user " . $uid, LOGGER_DEBUG);
1686                 return;
1687         }
1688
1689         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1690                 intval($uid));
1691
1692         if (count($r)) {
1693                 $self = $r[0];
1694         } else {
1695                 logger("twitter_fetchhometimeline: Own contact not found for user " . $uid, LOGGER_DEBUG);
1696                 return;
1697         }
1698
1699         $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1700                 intval($uid));
1701         if (!count($u)) {
1702                 logger("twitter_fetchhometimeline: Own user not found for user " . $uid, LOGGER_DEBUG);
1703                 return;
1704         }
1705
1706         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1707         //$parameters["count"] = 200;
1708         // Fetching timeline
1709         $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1710
1711         $first_time = ($lastid == "");
1712
1713         if ($lastid != "") {
1714                 $parameters["since_id"] = $lastid;
1715         }
1716
1717         $items = $connection->get('statuses/home_timeline', $parameters);
1718
1719         if (!is_array($items)) {
1720                 logger("twitter_fetchhometimeline: Error fetching home timeline: " . print_r($items, true), LOGGER_DEBUG);
1721                 return;
1722         }
1723
1724         $posts = array_reverse($items);
1725
1726         logger("twitter_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1727
1728         if (count($posts)) {
1729                 foreach ($posts as $post) {
1730                         if ($post->id_str > $lastid) {
1731                                 $lastid = $post->id_str;
1732                                 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1733                         }
1734
1735                         if ($first_time) {
1736                                 continue;
1737                         }
1738
1739                         if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1740                                 logger("twitter_fetchhometimeline: Skip previously sended post", LOGGER_DEBUG);
1741                                 continue;
1742                         }
1743
1744                         if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1745                                 logger("twitter_fetchhometimeline: Skip post that will be mirrored", LOGGER_DEBUG);
1746                                 continue;
1747                         }
1748
1749                         if ($post->in_reply_to_status_id_str != "") {
1750                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1751                         }
1752
1753                         $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1754
1755                         if (trim($postarray['body']) == "") {
1756                                 continue;
1757                         }
1758
1759                         $item = Item::insert($postarray);
1760                         $postarray["id"] = $item;
1761
1762                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1763
1764                         if ($item && !function_exists("check_item_notification")) {
1765                                 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1766                         }
1767                 }
1768         }
1769         PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1770
1771         // Fetching mentions
1772         $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1773
1774         $first_time = ($lastid == "");
1775
1776         if ($lastid != "") {
1777                 $parameters["since_id"] = $lastid;
1778         }
1779
1780         $items = $connection->get('statuses/mentions_timeline', $parameters);
1781
1782         if (!is_array($items)) {
1783                 logger("twitter_fetchhometimeline: Error fetching mentions: " . print_r($items, true), LOGGER_DEBUG);
1784                 return;
1785         }
1786
1787         $posts = array_reverse($items);
1788
1789         logger("twitter_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1790
1791         if (count($posts)) {
1792                 foreach ($posts as $post) {
1793                         if ($post->id_str > $lastid) {
1794                                 $lastid = $post->id_str;
1795                         }
1796
1797                         if ($first_time) {
1798                                 continue;
1799                         }
1800
1801                         if ($post->in_reply_to_status_id_str != "") {
1802                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1803                         }
1804
1805                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1806
1807                         if (trim($postarray['body']) == "") {
1808                                 continue;
1809                         }
1810
1811                         $item = Item::insert($postarray);
1812                         $postarray["id"] = $item;
1813
1814                         if ($item && function_exists("check_item_notification")) {
1815                                 check_item_notification($item, $uid, NOTIFY_TAGSELF);
1816                         }
1817
1818                         if (!isset($postarray["parent"]) || ($postarray["parent"] == 0)) {
1819                                 $postarray["parent"] = $item;
1820                         }
1821
1822                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1823
1824                         if ($item == 0) {
1825                                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1826                                         dbesc($postarray['uri']),
1827                                         intval($uid)
1828                                 );
1829                                 if (count($r)) {
1830                                         $item = $r[0]['id'];
1831                                         $parent_id = $r[0]['parent'];
1832                                 }
1833                         } else {
1834                                 $parent_id = $postarray['parent'];
1835                         }
1836
1837                         if (($item != 0) && !function_exists("check_item_notification")) {
1838                                 require_once 'include/enotify.php';
1839                                 notification([
1840                                         'type'         => NOTIFY_TAGSELF,
1841                                         'notify_flags' => $u[0]['notify-flags'],
1842                                         'language'     => $u[0]['language'],
1843                                         'to_name'      => $u[0]['username'],
1844                                         'to_email'     => $u[0]['email'],
1845                                         'uid'          => $u[0]['uid'],
1846                                         'item'         => $postarray,
1847                                         'link'         => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($item)),
1848                                         'source_name'  => $postarray['author-name'],
1849                                         'source_link'  => $postarray['author-link'],
1850                                         'source_photo' => $postarray['author-avatar'],
1851                                         'verb'         => ACTIVITY_TAG,
1852                                         'otype'        => 'item',
1853                                         'parent'       => $parent_id
1854                                 ]);
1855                         }
1856                 }
1857         }
1858
1859         PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1860 }
1861
1862 function twitter_fetch_own_contact(App $a, $uid)
1863 {
1864         $ckey    = Config::get('twitter', 'consumerkey');
1865         $csecret = Config::get('twitter', 'consumersecret');
1866         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1867         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1868
1869         $own_id = PConfig::get($uid, 'twitter', 'own_id');
1870
1871         $contact_id = 0;
1872
1873         if ($own_id == "") {
1874                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1875
1876                 // Fetching user data
1877                 $user = $connection->get('account/verify_credentials');
1878
1879                 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1880
1881                 $contact_id = twitter_fetch_contact($uid, $user, true);
1882         } else {
1883                 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1884                         intval($uid),
1885                         dbesc("twitter::" . $own_id));
1886                 if (count($r)) {
1887                         $contact_id = $r[0]["id"];
1888                 } else {
1889                         PConfig::delete($uid, 'twitter', 'own_id');
1890                 }
1891         }
1892
1893         return $contact_id;
1894 }
1895
1896 function twitter_is_retweet(App $a, $uid, $body)
1897 {
1898         $body = trim($body);
1899
1900         // Skip if it isn't a pure repeated messages
1901         // Does it start with a share?
1902         if (strpos($body, "[share") > 0) {
1903                 return false;
1904         }
1905
1906         // Does it end with a share?
1907         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1908                 return false;
1909         }
1910
1911         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1912         // Skip if there is no shared message in there
1913         if ($body == $attributes) {
1914                 return false;
1915         }
1916
1917         $link = "";
1918         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1919         if ($matches[1] != "") {
1920                 $link = $matches[1];
1921         }
1922
1923         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1924         if ($matches[1] != "") {
1925                 $link = $matches[1];
1926         }
1927
1928         $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1929         if ($id == $link) {
1930                 return false;
1931         }
1932
1933         logger('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, LOGGER_DEBUG);
1934
1935         $ckey    = Config::get('twitter', 'consumerkey');
1936         $csecret = Config::get('twitter', 'consumersecret');
1937         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1938         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1939
1940         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1941         $result = $connection->post('statuses/retweet/' . $id);
1942
1943         logger('twitter_is_retweet: result ' . print_r($result, true), LOGGER_DEBUG);
1944
1945         return !isset($result->errors);
1946 }