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