]> git.mxchange.org Git - friendica-addons.git/blob - twitter/twitter.php
279a660f02f506223795e1c13bc3e58586646171
[friendica-addons.git] / twitter / twitter.php
1 <?php
2 /**
3  * Name: Twitter Connector
4  * Description: Bidirectional (posting, relaying and reading) connector for Twitter.
5  * Version: 1.1.0
6  * Author: Tobias Diekershoff <https://f.diekershoff.de/profile/tobias>
7  * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
8  * Maintainer: Hypolite Petovan <https://friendica.mrpetovan.com/profile/hypolite>
9  *
10  * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel, Hypolite Petovan
11  * All rights reserved.
12  *
13  * Redistribution and use in source and binary forms, with or without
14  * modification, are permitted provided that the following conditions are met:
15  *    * Redistributions of source code must retain the above copyright notice,
16  *     this list of conditions and the following disclaimer.
17  *    * Redistributions in binary form must reproduce the above
18  *    * copyright notice, this list of conditions and the following disclaimer in
19  *      the documentation and/or other materials provided with the distribution.
20  *    * Neither the name of the <organization> nor the names of its contributors
21  *      may be used to endorse or promote products derived from this software
22  *      without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27  * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
28  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
32  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
33  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34  *
35  */
36 /*   Twitter Addon for Friendica
37  *
38  *   Author: Tobias Diekershoff
39  *           tobias.diekershoff@gmx.net
40  *
41  *   License:3-clause BSD license
42  *
43  *   Configuration:
44  *     To use this addon you need a OAuth Consumer key pair (key & secret)
45  *     you can get it from Twitter at https://twitter.com/apps
46  *
47  *     Register your Friendica site as "Client" application with "Read & Write" access
48  *     we do not need "Twitter as login". When you've registered the app you get the
49  *     OAuth Consumer key and secret pair for your application/site.
50  *
51  *     Add this key pair to your global .htconfig.php or use the admin panel.
52  *
53  *     $a->config['twitter']['consumerkey'] = 'your consumer_key here';
54  *     $a->config['twitter']['consumersecret'] = 'your consumer_secret here';
55  *
56  *     To activate the addon itself add it to the $a->config['system']['addon']
57  *     setting. After this, your user can configure their Twitter account settings
58  *     from "Settings -> Addon Settings".
59  *
60  *     Requirements: PHP5, curl
61  */
62
63 use Abraham\TwitterOAuth\TwitterOAuth;
64 use Friendica\App;
65 use Friendica\Content\OEmbed;
66 use Friendica\Content\Text\BBCode;
67 use Friendica\Content\Text\Plaintext;
68 use Friendica\Core\Addon;
69 use Friendica\Core\Config;
70 use Friendica\Core\L10n;
71 use Friendica\Core\PConfig;
72 use Friendica\Core\Worker;
73 use Friendica\Model\GContact;
74 use Friendica\Model\Group;
75 use Friendica\Model\Item;
76 use Friendica\Model\Photo;
77 use Friendica\Model\Queue;
78 use Friendica\Model\User;
79 use Friendica\Object\Image;
80 use Friendica\Util\DateTimeFormat;
81 use Friendica\Util\Network;
82
83 require_once 'boot.php';
84 require_once 'include/dba.php';
85 require_once 'include/enotify.php';
86 require_once 'include/text.php';
87
88 require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
89
90 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
91
92 function twitter_install()
93 {
94         //  we need some hooks, for the configuration and for sending tweets
95         Addon::registerHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
96         Addon::registerHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
97         Addon::registerHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
98         Addon::registerHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
99         Addon::registerHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
100         Addon::registerHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
101         Addon::registerHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
102         Addon::registerHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
103         Addon::registerHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
104         Addon::registerHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
105         Addon::registerHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
106         logger("installed twitter");
107 }
108
109 function twitter_uninstall()
110 {
111         Addon::unregisterHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
112         Addon::unregisterHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
113         Addon::unregisterHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
114         Addon::unregisterHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
115         Addon::unregisterHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
116         Addon::unregisterHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
117         Addon::unregisterHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
118         Addon::unregisterHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
119         Addon::unregisterHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
120         Addon::unregisterHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
121         Addon::unregisterHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
122
123         // old setting - remove only
124         Addon::unregisterHook('post_local_end', 'addon/twitter/twitter.php', 'twitter_post_hook');
125         Addon::unregisterHook('addon_settings', 'addon/twitter/twitter.php', 'twitter_settings');
126         Addon::unregisterHook('addon_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
127 }
128
129 function twitter_check_item_notification(App $a, &$notification_data)
130 {
131         $own_id = PConfig::get($notification_data["uid"], 'twitter', 'own_id');
132
133         $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
134                         intval($notification_data["uid"]),
135                         dbesc("twitter::".$own_id)
136         );
137
138         if ($own_user) {
139                 $notification_data["profiles"][] = $own_user[0]["url"];
140         }
141 }
142
143 function twitter_follow(App $a, &$contact)
144 {
145         logger("twitter_follow: Check if contact is twitter contact. " . $contact["url"], LOGGER_DEBUG);
146
147         if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
148                 return;
149         }
150
151         // contact seems to be a twitter contact, so continue
152         $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
153         $nickname = str_replace("@twitter.com", "", $nickname);
154
155         $uid = $a->user["uid"];
156
157         $ckey = Config::get('twitter', 'consumerkey');
158         $csecret = Config::get('twitter', 'consumersecret');
159         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
160         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
161
162         // If the addon is not configured (general or for this user) quit here
163         if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
164                 $contact = false;
165                 return;
166         }
167
168         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
169         $connection->post('friendships/create', ['screen_name' => $nickname]);
170
171         twitter_fetchuser($a, $uid, $nickname);
172
173         $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
174                 FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
175                                 intval($uid),
176                                 dbesc($nickname));
177         if (count($r)) {
178                 $contact["contact"] = $r[0];
179         }
180 }
181
182 function twitter_jot_nets(App $a, &$b)
183 {
184         if (!local_user()) {
185                 return;
186         }
187
188         $tw_post = PConfig::get(local_user(), 'twitter', 'post');
189         if (intval($tw_post) == 1) {
190                 $tw_defpost = PConfig::get(local_user(), 'twitter', 'post_by_default');
191                 $selected = ((intval($tw_defpost) == 1) ? ' checked="checked" ' : '');
192                 $b .= '<div class="profile-jot-net"><input type="checkbox" name="twitter_enable"' . $selected . ' value="1" /> '
193                         . L10n::t('Post to Twitter') . '</div>';
194         }
195 }
196
197 function twitter_settings_post(App $a, $post)
198 {
199         if (!local_user()) {
200                 return;
201         }
202         // don't check twitter settings if twitter submit button is not clicked
203         if (!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                                 Item::update(['extid' => "twitter::" . $result->id_str, 'body' => $result->text], ['id' => $b['id']]);
575                         }
576                 }
577
578                 if (strlen($msg) && ($image == "")) {
579 // -----------------
580                         $max_char = 280;
581                         $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
582                         $msg = $msgarr["text"];
583
584                         if (($msg == "") && isset($msgarr["title"])) {
585                                 $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
586                         }
587
588                         if (isset($msgarr["url"])) {
589                                 $msg .= "\n" . $msgarr["url"];
590                         }
591 // -----------------
592                         $url = 'statuses/update';
593                         $post = ['status' => $msg, 'weighted_character_count' => 'true'];
594
595                         if ($iscomment) {
596                                 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
597                         }
598
599                         $result = $connection->post($url, $post);
600                         logger('twitter_post send, result: ' . print_r($result, true), LOGGER_DEBUG);
601
602                         if ($result->source) {
603                                 Config::set("twitter", "application_name", strip_tags($result->source));
604                         }
605
606                         if ($result->errors) {
607                                 logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
608
609                                 $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
610                                 if (count($r)) {
611                                         $a->contact = $r[0]["id"];
612                                 }
613
614                                 $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
615
616                                 Queue::add($a->contact, NETWORK_TWITTER, $s);
617                                 notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
618                         } elseif ($iscomment) {
619                                 logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
620                                 Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
621                         }
622                 }
623         }
624 }
625
626 function twitter_addon_admin_post(App $a)
627 {
628         $consumerkey    = x($_POST, 'consumerkey')    ? notags(trim($_POST['consumerkey']))    : '';
629         $consumersecret = x($_POST, 'consumersecret') ? notags(trim($_POST['consumersecret'])) : '';
630         Config::set('twitter', 'consumerkey', $consumerkey);
631         Config::set('twitter', 'consumersecret', $consumersecret);
632         info(L10n::t('Settings updated.') . EOL);
633 }
634
635 function twitter_addon_admin(App $a, &$o)
636 {
637         $t = get_markup_template("admin.tpl", "addon/twitter/");
638
639         $o = replace_macros($t, [
640                 '$submit' => L10n::t('Save Settings'),
641                 // name, label, value, help, [extra values]
642                 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
643                 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
644         ]);
645 }
646
647 function twitter_cron(App $a, $b)
648 {
649         $last = Config::get('twitter', 'last_poll');
650
651         $poll_interval = intval(Config::get('twitter', 'poll_interval'));
652         if (!$poll_interval) {
653                 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
654         }
655
656         if ($last) {
657                 $next = $last + ($poll_interval * 60);
658                 if ($next > time()) {
659                         logger('twitter: poll intervall not reached');
660                         return;
661                 }
662         }
663         logger('twitter: cron_start');
664
665         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
666         if (count($r)) {
667                 foreach ($r as $rr) {
668                         logger('twitter: fetching for user ' . $rr['uid']);
669                         Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
670                 }
671         }
672
673         $abandon_days = intval(Config::get('system', 'account_abandon_days'));
674         if ($abandon_days < 1) {
675                 $abandon_days = 0;
676         }
677
678         $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
679
680         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
681         if (count($r)) {
682                 foreach ($r as $rr) {
683                         if ($abandon_days != 0) {
684                                 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
685                                 if (!count($user)) {
686                                         logger('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
687                                         continue;
688                                 }
689                         }
690
691                         logger('twitter: importing timeline from user ' . $rr['uid']);
692                         Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
693                         /*
694                           // To-Do
695                           // check for new contacts once a day
696                           $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
697                           if($last_contact_check)
698                           $next_contact_check = $last_contact_check + 86400;
699                           else
700                           $next_contact_check = 0;
701
702                           if($next_contact_check <= time()) {
703                           pumpio_getallusers($a, $rr["uid"]);
704                           PConfig::set($rr['uid'],'pumpio','contact_check',time());
705                           }
706                          */
707                 }
708         }
709
710         logger('twitter: cron_end');
711
712         Config::set('twitter', 'last_poll', time());
713 }
714
715 function twitter_expire(App $a, $b)
716 {
717         $days = Config::get('twitter', 'expire');
718
719         if ($days == 0) {
720                 return;
721         }
722
723         if (method_exists('dba', 'delete')) {
724                 $r = dba::select('item', ['id'], ['deleted' => true, 'network' => NETWORK_TWITTER]);
725                 while ($row = dba::fetch($r)) {
726                         dba::delete('item', ['id' => $row['id']]);
727                 }
728                 dba::close($r);
729         } else {
730                 $r = q("DELETE FROM `item` WHERE `deleted` AND `network` = '%s'", dbesc(NETWORK_TWITTER));
731         }
732
733         require_once "include/items.php";
734
735         logger('twitter_expire: expire_start');
736
737         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
738         if (count($r)) {
739                 foreach ($r as $rr) {
740                         logger('twitter_expire: user ' . $rr['uid']);
741                         Item::expire($rr['uid'], $days, NETWORK_TWITTER, true);
742                 }
743         }
744
745         logger('twitter_expire: expire_end');
746 }
747
748 function twitter_prepare_body(App $a, &$b)
749 {
750         if ($b["item"]["network"] != NETWORK_TWITTER) {
751                 return;
752         }
753
754         if ($b["preview"]) {
755                 $max_char = 280;
756                 $item = $b["item"];
757                 $item["plink"] = $a->get_baseurl() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
758
759                 $r = q("SELECT `author-link` FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
760                         dbesc($item["thr-parent"]),
761                         intval(local_user()));
762
763                 if (count($r)) {
764                         $orig_post = $r[0];
765
766                         $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
767                         $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
768                         $nicknameplain = "@" . $nicknameplain;
769
770                         if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
771                                 $item["body"] = $nickname . " " . $item["body"];
772                         }
773                 }
774
775                 $msgarr = BBCode::toPlaintext($item, $max_char, true, 8);
776                 $msg = $msgarr["text"];
777
778                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
779                         $msg .= " " . $msgarr["url"];
780                 }
781
782                 if (isset($msgarr["image"])) {
783                         $msg .= " " . $msgarr["image"];
784                 }
785
786                 $b['html'] = nl2br(htmlspecialchars($msg));
787         }
788 }
789
790 /**
791  * @brief Build the item array for the mirrored post
792  *
793  * @param App $a Application class
794  * @param integer $uid User id
795  * @param object $post Twitter object with the post
796  *
797  * @return array item data to be posted
798  */
799 function twitter_do_mirrorpost(App $a, $uid, $post)
800 {
801         $datarray["type"] = "wall";
802         $datarray["api_source"] = true;
803         $datarray["profile_uid"] = $uid;
804         $datarray["extid"] = NETWORK_TWITTER;
805         $datarray['message_id'] = item_new_uri($a->get_hostname(), $uid, NETWORK_TWITTER . ":" . $post->id);
806         $datarray['object'] = json_encode($post);
807         $datarray["title"] = "";
808
809         if (is_object($post->retweeted_status)) {
810                 // We don't support nested shares, so we mustn't show quotes as shares on retweets
811                 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
812
813                 $datarray['body'] = "\n" . share_header(
814                         $item['author-name'],
815                         $item['author-link'],
816                         $item['author-avatar'],
817                         "",
818                         $item['created'],
819                         $item['plink']
820                 );
821
822                 $datarray['body'] .= $item['body'] . '[/share]';
823         } else {
824                 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
825
826                 $datarray['body'] = $item['body'];
827         }
828
829         $datarray["source"] = $item['app'];
830         $datarray["verb"] = $item['verb'];
831
832         if (isset($item["location"])) {
833                 $datarray["location"] = $item["location"];
834         }
835
836         if (isset($item["coord"])) {
837                 $datarray["coord"] = $item["coord"];
838         }
839
840         return $datarray;
841 }
842
843 function twitter_fetchtimeline(App $a, $uid)
844 {
845         $ckey    = Config::get('twitter', 'consumerkey');
846         $csecret = Config::get('twitter', 'consumersecret');
847         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
848         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
849         $lastid  = PConfig::get($uid, 'twitter', 'lastid');
850
851         $application_name = Config::get('twitter', 'application_name');
852
853         if ($application_name == "") {
854                 $application_name = $a->get_hostname();
855         }
856
857         $has_picture = false;
858
859         require_once 'mod/item.php';
860         require_once 'include/items.php';
861         require_once 'mod/share.php';
862
863         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
864
865         $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
866
867         $first_time = ($lastid == "");
868
869         if ($lastid != "") {
870                 $parameters["since_id"] = $lastid;
871         }
872
873         $items = $connection->get('statuses/user_timeline', $parameters);
874
875         if (!is_array($items)) {
876                 return;
877         }
878
879         $posts = array_reverse($items);
880
881         if (count($posts)) {
882                 foreach ($posts as $post) {
883                         if ($post->id_str > $lastid) {
884                                 $lastid = $post->id_str;
885                                 PConfig::set($uid, 'twitter', 'lastid', $lastid);
886                         }
887
888                         if ($first_time) {
889                                 continue;
890                         }
891
892                         if (!stristr($post->source, $application_name)) {
893                                 $_SESSION["authenticated"] = true;
894                                 $_SESSION["uid"] = $uid;
895
896                                 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
897
898                                 logger('twitter: posting for user ' . $uid);
899
900                                 item_post($a);
901                         }
902                 }
903         }
904         PConfig::set($uid, 'twitter', 'lastid', $lastid);
905 }
906
907 function twitter_queue_hook(App $a, &$b)
908 {
909         $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
910                 dbesc(NETWORK_TWITTER)
911         );
912         if (!count($qi)) {
913                 return;
914         }
915
916         foreach ($qi as $x) {
917                 if ($x['network'] !== NETWORK_TWITTER) {
918                         continue;
919                 }
920
921                 logger('twitter_queue: run');
922
923                 $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
924                         WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
925                         intval($x['cid'])
926                 );
927                 if (!count($r)) {
928                         continue;
929                 }
930
931                 $user = $r[0];
932
933                 $ckey    = Config::get('twitter', 'consumerkey');
934                 $csecret = Config::get('twitter', 'consumersecret');
935                 $otoken  = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
936                 $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
937
938                 $success = false;
939
940                 if ($ckey && $csecret && $otoken && $osecret) {
941                         logger('twitter_queue: able to post');
942
943                         $z = unserialize($x['content']);
944
945                         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
946                         $result = $connection->post($z['url'], $z['post']);
947
948                         logger('twitter_queue: post result: ' . print_r($result, true), LOGGER_DEBUG);
949
950                         if ($result->errors) {
951                                 logger('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
952                         } else {
953                                 $success = true;
954                                 Queue::removeItem($x['id']);
955                         }
956                 } else {
957                         logger("twitter_queue: Error getting tokens for user " . $user['uid']);
958                 }
959
960                 if (!$success) {
961                         logger('twitter_queue: delayed');
962                         Queue::updateTime($x['id']);
963                 }
964         }
965 }
966
967 function twitter_fix_avatar($avatar)
968 {
969         $new_avatar = str_replace("_normal.", ".", $avatar);
970
971         $info = Image::getInfoFromURL($new_avatar);
972         if (!$info) {
973                 $new_avatar = $avatar;
974         }
975
976         return $new_avatar;
977 }
978
979 function twitter_fetch_contact($uid, $contact, $create_user)
980 {
981         if ($contact->id_str == "") {
982                 return -1;
983         }
984
985         $avatar = twitter_fix_avatar($contact->profile_image_url_https);
986
987         GContact::update(["url" => "https://twitter.com/" . $contact->screen_name,
988                 "network" => NETWORK_TWITTER, "photo" => $avatar, "hide" => true,
989                 "name" => $contact->name, "nick" => $contact->screen_name,
990                 "location" => $contact->location, "about" => $contact->description,
991                 "addr" => $contact->screen_name . "@twitter.com", "generation" => 2]);
992
993         $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
994                 intval($uid),
995                 dbesc("twitter::" . $contact->id_str));
996
997         if (!count($r) && !$create_user) {
998                 return 0;
999         }
1000
1001         if (count($r) && ($r[0]["readonly"] || $r[0]["blocked"])) {
1002                 logger("twitter_fetch_contact: Contact '" . $r[0]["nick"] . "' is blocked or readonly.", LOGGER_DEBUG);
1003                 return -1;
1004         }
1005
1006         if (!count($r)) {
1007                 // create contact record
1008                 q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `addr`, `alias`, `notify`, `poll`,
1009                                         `name`, `nick`, `photo`, `network`, `rel`, `priority`,
1010                                         `location`, `about`, `writable`, `blocked`, `readonly`, `pending`)
1011                                         VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', %d, 0, 0, 0)",
1012                         intval($uid),
1013                         dbesc(DateTimeFormat::utcNow()),
1014                         dbesc("https://twitter.com/" . $contact->screen_name),
1015                         dbesc(normalise_link("https://twitter.com/" . $contact->screen_name)),
1016                         dbesc($contact->screen_name."@twitter.com"),
1017                         dbesc("twitter::" . $contact->id_str),
1018                         dbesc(''),
1019                         dbesc("twitter::" . $contact->id_str),
1020                         dbesc($contact->name),
1021                         dbesc($contact->screen_name),
1022                         dbesc($avatar),
1023                         dbesc(NETWORK_TWITTER),
1024                         intval(CONTACT_IS_FRIEND),
1025                         intval(1),
1026                         dbesc($contact->location),
1027                         dbesc($contact->description),
1028                         intval(1)
1029                 );
1030
1031                 $r = q("SELECT * FROM `contact` WHERE `alias` = '%s' AND `uid` = %d LIMIT 1",
1032                         dbesc("twitter::".$contact->id_str),
1033                         intval($uid)
1034                 );
1035
1036                 if (!count($r)) {
1037                         return false;
1038                 }
1039
1040                 $contact_id = $r[0]['id'];
1041
1042                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1043
1044                 $photos = Photo::importProfilePhoto($avatar, $uid, $contact_id, true);
1045
1046                 if ($photos) {
1047                         q("UPDATE `contact` SET `photo` = '%s',
1048                                                 `thumb` = '%s',
1049                                                 `micro` = '%s',
1050                                                 `name-date` = '%s',
1051                                                 `uri-date` = '%s',
1052                                                         `avatar-date` = '%s'
1053                                         WHERE `id` = %d",
1054                                 dbesc($photos[0]),
1055                                 dbesc($photos[1]),
1056                                 dbesc($photos[2]),
1057                                 dbesc(DateTimeFormat::utcNow()),
1058                                 dbesc(DateTimeFormat::utcNow()),
1059                                 dbesc(DateTimeFormat::utcNow()),
1060                                 intval($contact_id)
1061                         );
1062                 }
1063         } else {
1064                 // update profile photos once every two weeks as we have no notification of when they change.
1065                 //$update_photo = (($r[0]['avatar-date'] < DateTimeFormat::convert('now -2 days', '', '', )) ? true : false);
1066                 $update_photo = ($r[0]['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
1067
1068                 // check that we have all the photos, this has been known to fail on occasion
1069                 if ((!$r[0]['photo']) || (!$r[0]['thumb']) || (!$r[0]['micro']) || ($update_photo)) {
1070                         logger("twitter_fetch_contact: Updating contact " . $contact->screen_name, LOGGER_DEBUG);
1071
1072                         $photos = Photo::importProfilePhoto($avatar, $uid, $r[0]['id'], true);
1073
1074                         if ($photos) {
1075                                 q("UPDATE `contact` SET `photo` = '%s',
1076                                                         `thumb` = '%s',
1077                                                         `micro` = '%s',
1078                                                         `name-date` = '%s',
1079                                                         `uri-date` = '%s',
1080                                                         `avatar-date` = '%s',
1081                                                         `url` = '%s',
1082                                                         `nurl` = '%s',
1083                                                         `addr` = '%s',
1084                                                         `name` = '%s',
1085                                                         `nick` = '%s',
1086                                                         `location` = '%s',
1087                                                         `about` = '%s'
1088                                                 WHERE `id` = %d",
1089                                         dbesc($photos[0]),
1090                                         dbesc($photos[1]),
1091                                         dbesc($photos[2]),
1092                                         dbesc(DateTimeFormat::utcNow()),
1093                                         dbesc(DateTimeFormat::utcNow()),
1094                                         dbesc(DateTimeFormat::utcNow()),
1095                                         dbesc("https://twitter.com/".$contact->screen_name),
1096                                         dbesc(normalise_link("https://twitter.com/".$contact->screen_name)),
1097                                         dbesc($contact->screen_name."@twitter.com"),
1098                                         dbesc($contact->name),
1099                                         dbesc($contact->screen_name),
1100                                         dbesc($contact->location),
1101                                         dbesc($contact->description),
1102                                         intval($r[0]['id'])
1103                                 );
1104                         }
1105                 }
1106         }
1107
1108         return $r[0]["id"];
1109 }
1110
1111 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1112 {
1113         $ckey = Config::get('twitter', 'consumerkey');
1114         $csecret = Config::get('twitter', 'consumersecret');
1115         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1116         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1117
1118         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1119                 intval($uid));
1120
1121         if (count($r)) {
1122                 $self = $r[0];
1123         } else {
1124                 return;
1125         }
1126
1127         $parameters = [];
1128
1129         if ($screen_name != "") {
1130                 $parameters["screen_name"] = $screen_name;
1131         }
1132
1133         if ($user_id != "") {
1134                 $parameters["user_id"] = $user_id;
1135         }
1136
1137         // Fetching user data
1138         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1139         $user = $connection->get('users/show', $parameters);
1140
1141         if (!is_object($user)) {
1142                 return;
1143         }
1144
1145         $contact_id = twitter_fetch_contact($uid, $user, true);
1146
1147         return $contact_id;
1148 }
1149
1150 function twitter_expand_entities(App $a, $body, $item, $picture)
1151 {
1152         $plain = $body;
1153
1154         if (isset($item->entities->urls)) {
1155                 $type = "";
1156                 $footerurl = "";
1157                 $footerlink = "";
1158                 $footer = "";
1159
1160                 foreach ($item->entities->urls as $url) {
1161                         $plain = str_replace($url->url, '', $plain);
1162
1163                         if ($url->url && $url->expanded_url && $url->display_url) {
1164                                 $expanded_url = Network::finalUrl($url->expanded_url);
1165
1166                                 $oembed_data = OEmbed::fetchURL($expanded_url);
1167
1168                                 // Quickfix: Workaround for URL with "[" and "]" in it
1169                                 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1170                                         $expanded_url = $url->url;
1171                                 }
1172
1173                                 if ($type == "") {
1174                                         $type = $oembed_data->type;
1175                                 }
1176
1177                                 if ($oembed_data->type == "video") {
1178                                         //$body = str_replace($url->url,
1179                                         //              "[video]".$expanded_url."[/video]", $body);
1180                                         //$dontincludemedia = true;
1181                                         $type = $oembed_data->type;
1182                                         $footerurl = $expanded_url;
1183                                         $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1184
1185                                         $body = str_replace($url->url, $footerlink, $body);
1186                                         //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1187                                 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1188                                         $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1189                                         //$dontincludemedia = true;
1190                                 } elseif ($oembed_data->type != "link") {
1191                                         $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1192                                 } else {
1193                                         $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1194
1195                                         $tempfile = tempnam(get_temppath(), "cache");
1196                                         file_put_contents($tempfile, $img_str);
1197                                         $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1198                                         unlink($tempfile);
1199
1200                                         if (substr($mime, 0, 6) == "image/") {
1201                                                 $type = "photo";
1202                                                 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1203                                                 //$dontincludemedia = true;
1204                                         } else {
1205                                                 $type = $oembed_data->type;
1206                                                 $footerurl = $expanded_url;
1207                                                 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1208
1209                                                 $body = str_replace($url->url, $footerlink, $body);
1210                                         }
1211                                 }
1212                         }
1213                 }
1214
1215                 if ($footerurl != "") {
1216                         $footer = add_page_info($footerurl, false, $picture);
1217                 }
1218
1219                 if (($footerlink != "") && (trim($footer) != "")) {
1220                         $removedlink = trim(str_replace($footerlink, "", $body));
1221
1222                         if (($removedlink == "") || strstr($body, $removedlink)) {
1223                                 $body = $removedlink;
1224                         }
1225
1226                         $body .= $footer;
1227                 }
1228
1229                 if (($footer == "") && ($picture != "")) {
1230                         $body .= "\n\n[img]" . $picture . "[/img]\n";
1231                 } elseif (($footer == "") && ($picture == "")) {
1232                         $body = add_page_info_to_body($body);
1233                 }
1234
1235                 $tags_arr = [];
1236
1237                 foreach ($item->entities->hashtags AS $hashtag) {
1238                         $url = "#[url=" . $a->get_baseurl() . "/search?tag=" . rawurlencode($hashtag->text) . "]" . $hashtag->text . "[/url]";
1239                         $tags_arr["#" . $hashtag->text] = $url;
1240                         $body = str_replace("#" . $hashtag->text, $url, $body);
1241                 }
1242
1243                 foreach ($item->entities->user_mentions AS $mention) {
1244                         $url = "@[url=https://twitter.com/" . rawurlencode($mention->screen_name) . "]" . $mention->screen_name . "[/url]";
1245                         $tags_arr["@" . $mention->screen_name] = $url;
1246                         $body = str_replace("@" . $mention->screen_name, $url, $body);
1247                 }
1248
1249                 // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1250                 $tags = get_tags($body);
1251
1252                 if (count($tags)) {
1253                         foreach ($tags as $tag) {
1254                                 if (strstr(trim($tag), " ")) {
1255                                         continue;
1256                                 }
1257
1258                                 if (strpos($tag, '#') === 0) {
1259                                         if (strpos($tag, '[url=')) {
1260                                                 continue;
1261                                         }
1262
1263                                         // don't link tags that are already embedded in links
1264                                         if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1265                                                 continue;
1266                                         }
1267                                         if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1268                                                 continue;
1269                                         }
1270
1271                                         $basetag = str_replace('_', ' ', substr($tag, 1));
1272                                         $url = '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1273                                         $body = str_replace($tag, $url, $body);
1274                                         $tags_arr["#" . $basetag] = $url;
1275                                 } elseif (strpos($tag, '@') === 0) {
1276                                         if (strpos($tag, '[url=')) {
1277                                                 continue;
1278                                         }
1279
1280                                         $basetag = substr($tag, 1);
1281                                         $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1282                                         $body = str_replace($tag, $url, $body);
1283                                         $tags_arr["@" . $basetag] = $url;
1284                                 }
1285                         }
1286                 }
1287
1288                 $tags = implode($tags_arr, ",");
1289         }
1290         return ["body" => $body, "tags" => $tags, "plain" => $plain];
1291 }
1292
1293 /**
1294  * @brief Fetch media entities and add media links to the body
1295  *
1296  * @param object $post Twitter object with the post
1297  * @param array $postarray Array of the item that is about to be posted
1298  *
1299  * @return $picture string Image URL or empty string
1300  */
1301 function twitter_media_entities($post, &$postarray)
1302 {
1303         // There are no media entities? So we quit.
1304         if (!is_array($post->extended_entities->media)) {
1305                 return "";
1306         }
1307
1308         // When the post links to an external page, we only take one picture.
1309         // We only do this when there is exactly one media.
1310         if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1311                 $picture = "";
1312                 foreach ($post->extended_entities->media AS $medium) {
1313                         if (isset($medium->media_url_https)) {
1314                                 $picture = $medium->media_url_https;
1315                                 $postarray['body'] = str_replace($medium->url, "", $postarray['body']);
1316                         }
1317                 }
1318                 return $picture;
1319         }
1320
1321         // This is a pure media post, first search for all media urls
1322         $media = [];
1323         foreach ($post->extended_entities->media AS $medium) {
1324                 switch ($medium->type) {
1325                         case 'photo':
1326                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1327                                 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1328                                 break;
1329                         case 'video':
1330                         case 'animated_gif':
1331                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1332                                 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1333                                 if (is_array($medium->video_info->variants)) {
1334                                         $bitrate = 0;
1335                                         // We take the video with the highest bitrate
1336                                         foreach ($medium->video_info->variants AS $variant) {
1337                                                 if (($variant->content_type == "video/mp4") && ($variant->bitrate >= $bitrate)) {
1338                                                         $media[$medium->url] = "\n[video]" . $variant->url . "[/video]";
1339                                                         $bitrate = $variant->bitrate;
1340                                                 }
1341                                         }
1342                                 }
1343                                 break;
1344                         // The following code will only be activated for test reasons
1345                         //default:
1346                         //      $postarray['body'] .= print_r($medium, true);
1347                 }
1348         }
1349
1350         // Now we replace the media urls.
1351         foreach ($media AS $key => $value) {
1352                 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1353         }
1354         return "";
1355 }
1356
1357 function twitter_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact, $noquote)
1358 {
1359         $postarray = [];
1360         $postarray['network'] = NETWORK_TWITTER;
1361         $postarray['gravity'] = 0;
1362         $postarray['uid'] = $uid;
1363         $postarray['wall'] = 0;
1364         $postarray['uri'] = "twitter::" . $post->id_str;
1365         $postarray['object'] = json_encode($post);
1366
1367         // Don't import our own comments
1368         $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1369                 dbesc($postarray['uri']),
1370                 intval($uid)
1371         );
1372
1373         if (count($r)) {
1374                 logger("Item with extid " . $postarray['uri'] . " found.", LOGGER_DEBUG);
1375                 return [];
1376         }
1377
1378         $contactid = 0;
1379
1380         if ($post->in_reply_to_status_id_str != "") {
1381                 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1382
1383                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1384                         dbesc($parent),
1385                         intval($uid)
1386                 );
1387                 if (count($r)) {
1388                         $postarray['thr-parent'] = $r[0]["uri"];
1389                         $postarray['parent-uri'] = $r[0]["parent-uri"];
1390                         $postarray['parent'] = $r[0]["parent"];
1391                         $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1392                 } else {
1393                         $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1394                                 dbesc($parent),
1395                                 intval($uid)
1396                         );
1397                         if (count($r)) {
1398                                 $postarray['thr-parent'] = $r[0]['uri'];
1399                                 $postarray['parent-uri'] = $r[0]['parent-uri'];
1400                                 $postarray['parent'] = $r[0]['parent'];
1401                                 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1402                         } else {
1403                                 $postarray['thr-parent'] = $postarray['uri'];
1404                                 $postarray['parent-uri'] = $postarray['uri'];
1405                                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1406                         }
1407                 }
1408
1409                 // Is it me?
1410                 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1411
1412                 if ($post->user->id_str == $own_id) {
1413                         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1414                                 intval($uid));
1415
1416                         if (count($r)) {
1417                                 $contactid = $r[0]["id"];
1418
1419                                 $postarray['owner-name']   = $r[0]["name"];
1420                                 $postarray['owner-link']   = $r[0]["url"];
1421                                 $postarray['owner-avatar'] = $r[0]["photo"];
1422                         } else {
1423                                 logger("No self contact for user " . $uid, LOGGER_DEBUG);
1424                                 return [];
1425                         }
1426                 }
1427                 // Don't create accounts of people who just comment something
1428                 $create_user = false;
1429         } else {
1430                 $postarray['parent-uri'] = $postarray['uri'];
1431                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1432         }
1433
1434         if ($contactid == 0) {
1435                 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1436
1437                 $postarray['owner-name'] = $post->user->name;
1438                 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1439                 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1440         }
1441
1442         if (($contactid == 0) && !$only_existing_contact) {
1443                 $contactid = $self['id'];
1444         } elseif ($contactid <= 0) {
1445                 logger("Contact ID is zero or less than zero.", LOGGER_DEBUG);
1446                 return [];
1447         }
1448
1449         $postarray['contact-id'] = $contactid;
1450
1451         $postarray['verb'] = ACTIVITY_POST;
1452         $postarray['author-name'] = $postarray['owner-name'];
1453         $postarray['author-link'] = $postarray['owner-link'];
1454         $postarray['author-avatar'] = $postarray['owner-avatar'];
1455         $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1456         $postarray['app'] = strip_tags($post->source);
1457
1458         if ($post->user->protected) {
1459                 $postarray['private'] = 1;
1460                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1461         }
1462
1463         if (is_string($post->full_text)) {
1464                 $postarray['body'] = $post->full_text;
1465         } else {
1466                 $postarray['body'] = $post->text;
1467         }
1468
1469         // When the post contains links then use the correct object type
1470         if (count($post->entities->urls) > 0) {
1471                 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1472         }
1473
1474         // Search for media links
1475         $picture = twitter_media_entities($post, $postarray);
1476
1477         $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1478         $postarray['body'] = $converted["body"];
1479         $postarray['tag'] = $converted["tags"];
1480         $postarray['created'] = DateTimeFormat::utc($post->created_at);
1481         $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1482
1483         $statustext = $converted["plain"];
1484
1485         if (is_string($post->place->name)) {
1486                 $postarray["location"] = $post->place->name;
1487         }
1488         if (is_string($post->place->full_name)) {
1489                 $postarray["location"] = $post->place->full_name;
1490         }
1491         if (is_array($post->geo->coordinates)) {
1492                 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1493         }
1494         if (is_array($post->coordinates->coordinates)) {
1495                 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1496         }
1497         if (is_object($post->retweeted_status)) {
1498                 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1499
1500                 $retweet['object'] = $postarray['object'];
1501                 $retweet['private'] = $postarray['private'];
1502                 $retweet['allow_cid'] = $postarray['allow_cid'];
1503                 $retweet['contact-id'] = $postarray['contact-id'];
1504                 $retweet['owner-name'] = $postarray['owner-name'];
1505                 $retweet['owner-link'] = $postarray['owner-link'];
1506                 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1507
1508                 $postarray = $retweet;
1509         }
1510
1511         if (is_object($post->quoted_status) && !$noquote) {
1512                 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1513
1514                 $postarray['body'] = $statustext;
1515
1516                 $postarray['body'] .= "\n" . share_header(
1517                         $quoted['author-name'],
1518                         $quoted['author-link'],
1519                         $quoted['author-avatar'],
1520                         "",
1521                         $quoted['created'],
1522                         $quoted['plink']
1523                 );
1524
1525                 $postarray['body'] .= $quoted['body'] . '[/share]';
1526         }
1527
1528         return $postarray;
1529 }
1530
1531 function twitter_checknotification(App $a, $uid, $own_id, $top_item, $postarray)
1532 {
1533         /// TODO: this whole function doesn't seem to work. Needs complete check
1534         $user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` LIMIT 1",
1535                 intval($uid)
1536         );
1537
1538         if (!count($user)) {
1539                 return;
1540         }
1541
1542         // Is it me?
1543         if (link_compare($user[0]["url"], $postarray['author-link'])) {
1544                 return;
1545         }
1546
1547         $own_user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1548                 intval($uid),
1549                 dbesc("twitter::".$own_id)
1550         );
1551
1552         if (!count($own_user)) {
1553                 return;
1554         }
1555
1556         // Is it me from twitter?
1557         if (link_compare($own_user[0]["url"], $postarray['author-link'])) {
1558                 return;
1559         }
1560
1561         $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 AND `deleted` = 0",
1562                 dbesc($postarray['parent-uri']),
1563                 intval($uid)
1564         );
1565
1566         if (count($myconv)) {
1567                 foreach ($myconv as $conv) {
1568                         // now if we find a match, it means we're in this conversation
1569                         if (!link_compare($conv['author-link'], $user[0]["url"]) && !link_compare($conv['author-link'], $own_user[0]["url"])) {
1570                                 continue;
1571                         }
1572
1573                         require_once 'include/enotify.php';
1574
1575                         $conv_parent = $conv['parent'];
1576
1577                         notification([
1578                                 'type' => NOTIFY_COMMENT,
1579                                 'notify_flags' => $user[0]['notify-flags'],
1580                                 'language' => $user[0]['language'],
1581                                 'to_name' => $user[0]['username'],
1582                                 'to_email' => $user[0]['email'],
1583                                 'uid' => $user[0]['uid'],
1584                                 'item' => $postarray,
1585                                 'link' => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($top_item)),
1586                                 'source_name' => $postarray['author-name'],
1587                                 'source_link' => $postarray['author-link'],
1588                                 'source_photo' => $postarray['author-avatar'],
1589                                 'verb' => ACTIVITY_POST,
1590                                 'otype' => 'item',
1591                                 'parent' => $conv_parent,
1592                         ]);
1593
1594                         // only send one notification
1595                         break;
1596                 }
1597         }
1598 }
1599
1600 function twitter_fetchparentposts(App $a, $uid, $post, $connection, $self, $own_id)
1601 {
1602         logger("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, LOGGER_DEBUG);
1603
1604         $posts = [];
1605
1606         while ($post->in_reply_to_status_id_str != "") {
1607                 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1608
1609                 $post = $connection->get('statuses/show', $parameters);
1610
1611                 if (!count($post)) {
1612                         logger("twitter_fetchparentposts: Can't fetch post " . $parameters->id, LOGGER_DEBUG);
1613                         break;
1614                 }
1615
1616                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1617                         dbesc("twitter::".$post->id_str),
1618                         intval($uid)
1619                 );
1620
1621                 if (count($r)) {
1622                         break;
1623                 }
1624
1625                 $posts[] = $post;
1626         }
1627
1628         logger("twitter_fetchparentposts: Fetching " . count($posts) . " parents", LOGGER_DEBUG);
1629
1630         $posts = array_reverse($posts);
1631
1632         if (count($posts)) {
1633                 foreach ($posts as $post) {
1634                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1635
1636                         if (trim($postarray['body']) == "") {
1637                                 continue;
1638                         }
1639
1640                         $item = Item::insert($postarray);
1641                         $postarray["id"] = $item;
1642
1643                         logger('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1644
1645                         if ($item && !function_exists("check_item_notification")) {
1646                                 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1647                         }
1648                 }
1649         }
1650 }
1651
1652 function twitter_fetchhometimeline(App $a, $uid)
1653 {
1654         $ckey    = Config::get('twitter', 'consumerkey');
1655         $csecret = Config::get('twitter', 'consumersecret');
1656         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1657         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1658         $create_user = PConfig::get($uid, 'twitter', 'create_user');
1659         $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1660
1661         logger("twitter_fetchhometimeline: Fetching for user " . $uid, LOGGER_DEBUG);
1662
1663         $application_name = Config::get('twitter', 'application_name');
1664
1665         if ($application_name == "") {
1666                 $application_name = $a->get_hostname();
1667         }
1668
1669         require_once 'include/items.php';
1670
1671         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1672
1673         $own_contact = twitter_fetch_own_contact($a, $uid);
1674
1675         $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1676                 intval($own_contact),
1677                 intval($uid));
1678
1679         if (count($r)) {
1680                 $own_id = $r[0]["nick"];
1681         } else {
1682                 logger("twitter_fetchhometimeline: Own twitter contact not found for user " . $uid, LOGGER_DEBUG);
1683                 return;
1684         }
1685
1686         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1687                 intval($uid));
1688
1689         if (count($r)) {
1690                 $self = $r[0];
1691         } else {
1692                 logger("twitter_fetchhometimeline: Own contact not found for user " . $uid, LOGGER_DEBUG);
1693                 return;
1694         }
1695
1696         $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1697                 intval($uid));
1698         if (!count($u)) {
1699                 logger("twitter_fetchhometimeline: Own user not found for user " . $uid, LOGGER_DEBUG);
1700                 return;
1701         }
1702
1703         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1704         //$parameters["count"] = 200;
1705         // Fetching timeline
1706         $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1707
1708         $first_time = ($lastid == "");
1709
1710         if ($lastid != "") {
1711                 $parameters["since_id"] = $lastid;
1712         }
1713
1714         $items = $connection->get('statuses/home_timeline', $parameters);
1715
1716         if (!is_array($items)) {
1717                 logger("twitter_fetchhometimeline: Error fetching home timeline: " . print_r($items, true), LOGGER_DEBUG);
1718                 return;
1719         }
1720
1721         $posts = array_reverse($items);
1722
1723         logger("twitter_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1724
1725         if (count($posts)) {
1726                 foreach ($posts as $post) {
1727                         if ($post->id_str > $lastid) {
1728                                 $lastid = $post->id_str;
1729                                 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1730                         }
1731
1732                         if ($first_time) {
1733                                 continue;
1734                         }
1735
1736                         if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1737                                 logger("twitter_fetchhometimeline: Skip previously sended post", LOGGER_DEBUG);
1738                                 continue;
1739                         }
1740
1741                         if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1742                                 logger("twitter_fetchhometimeline: Skip post that will be mirrored", LOGGER_DEBUG);
1743                                 continue;
1744                         }
1745
1746                         if ($post->in_reply_to_status_id_str != "") {
1747                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1748                         }
1749
1750                         $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1751
1752                         if (trim($postarray['body']) == "") {
1753                                 continue;
1754                         }
1755
1756                         $item = Item::insert($postarray);
1757                         $postarray["id"] = $item;
1758
1759                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1760
1761                         if ($item && !function_exists("check_item_notification")) {
1762                                 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1763                         }
1764                 }
1765         }
1766         PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1767
1768         // Fetching mentions
1769         $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1770
1771         $first_time = ($lastid == "");
1772
1773         if ($lastid != "") {
1774                 $parameters["since_id"] = $lastid;
1775         }
1776
1777         $items = $connection->get('statuses/mentions_timeline', $parameters);
1778
1779         if (!is_array($items)) {
1780                 logger("twitter_fetchhometimeline: Error fetching mentions: " . print_r($items, true), LOGGER_DEBUG);
1781                 return;
1782         }
1783
1784         $posts = array_reverse($items);
1785
1786         logger("twitter_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1787
1788         if (count($posts)) {
1789                 foreach ($posts as $post) {
1790                         if ($post->id_str > $lastid) {
1791                                 $lastid = $post->id_str;
1792                         }
1793
1794                         if ($first_time) {
1795                                 continue;
1796                         }
1797
1798                         if ($post->in_reply_to_status_id_str != "") {
1799                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1800                         }
1801
1802                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1803
1804                         if (trim($postarray['body']) == "") {
1805                                 continue;
1806                         }
1807
1808                         $item = Item::insert($postarray);
1809                         $postarray["id"] = $item;
1810
1811                         if ($item && function_exists("check_item_notification")) {
1812                                 check_item_notification($item, $uid, NOTIFY_TAGSELF);
1813                         }
1814
1815                         if (!isset($postarray["parent"]) || ($postarray["parent"] == 0)) {
1816                                 $postarray["parent"] = $item;
1817                         }
1818
1819                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1820
1821                         if ($item == 0) {
1822                                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1823                                         dbesc($postarray['uri']),
1824                                         intval($uid)
1825                                 );
1826                                 if (count($r)) {
1827                                         $item = $r[0]['id'];
1828                                         $parent_id = $r[0]['parent'];
1829                                 }
1830                         } else {
1831                                 $parent_id = $postarray['parent'];
1832                         }
1833
1834                         if (($item != 0) && !function_exists("check_item_notification")) {
1835                                 require_once 'include/enotify.php';
1836                                 notification([
1837                                         'type'         => NOTIFY_TAGSELF,
1838                                         'notify_flags' => $u[0]['notify-flags'],
1839                                         'language'     => $u[0]['language'],
1840                                         'to_name'      => $u[0]['username'],
1841                                         'to_email'     => $u[0]['email'],
1842                                         'uid'          => $u[0]['uid'],
1843                                         'item'         => $postarray,
1844                                         'link'         => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($item)),
1845                                         'source_name'  => $postarray['author-name'],
1846                                         'source_link'  => $postarray['author-link'],
1847                                         'source_photo' => $postarray['author-avatar'],
1848                                         'verb'         => ACTIVITY_TAG,
1849                                         'otype'        => 'item',
1850                                         'parent'       => $parent_id
1851                                 ]);
1852                         }
1853                 }
1854         }
1855
1856         PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1857 }
1858
1859 function twitter_fetch_own_contact(App $a, $uid)
1860 {
1861         $ckey    = Config::get('twitter', 'consumerkey');
1862         $csecret = Config::get('twitter', 'consumersecret');
1863         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1864         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1865
1866         $own_id = PConfig::get($uid, 'twitter', 'own_id');
1867
1868         $contact_id = 0;
1869
1870         if ($own_id == "") {
1871                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1872
1873                 // Fetching user data
1874                 $user = $connection->get('account/verify_credentials');
1875
1876                 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1877
1878                 $contact_id = twitter_fetch_contact($uid, $user, true);
1879         } else {
1880                 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1881                         intval($uid),
1882                         dbesc("twitter::" . $own_id));
1883                 if (count($r)) {
1884                         $contact_id = $r[0]["id"];
1885                 } else {
1886                         PConfig::delete($uid, 'twitter', 'own_id');
1887                 }
1888         }
1889
1890         return $contact_id;
1891 }
1892
1893 function twitter_is_retweet(App $a, $uid, $body)
1894 {
1895         $body = trim($body);
1896
1897         // Skip if it isn't a pure repeated messages
1898         // Does it start with a share?
1899         if (strpos($body, "[share") > 0) {
1900                 return false;
1901         }
1902
1903         // Does it end with a share?
1904         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1905                 return false;
1906         }
1907
1908         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1909         // Skip if there is no shared message in there
1910         if ($body == $attributes) {
1911                 return false;
1912         }
1913
1914         $link = "";
1915         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1916         if ($matches[1] != "") {
1917                 $link = $matches[1];
1918         }
1919
1920         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1921         if ($matches[1] != "") {
1922                 $link = $matches[1];
1923         }
1924
1925         $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1926         if ($id == $link) {
1927                 return false;
1928         }
1929
1930         logger('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, LOGGER_DEBUG);
1931
1932         $ckey    = Config::get('twitter', 'consumerkey');
1933         $csecret = Config::get('twitter', 'consumersecret');
1934         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1935         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1936
1937         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1938         $result = $connection->post('statuses/retweet/' . $id);
1939
1940         logger('twitter_is_retweet: result ' . print_r($result, true), LOGGER_DEBUG);
1941
1942         return !isset($result->errors);
1943 }