]> git.mxchange.org Git - friendica-addons.git/blob - twitter/twitter.php
[twitter] Fix TwitterOauth library usage
[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                 /* no global consumer keys
292                  * display warning and skip personal config
293                  */
294                 $s .= '<p>' . L10n::t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
295         } else {
296                 // ok we have a consumer key pair now look into the OAuth stuff
297                 if ((!$otoken) && (!$osecret)) {
298                         /* the user has not yet connected the account to twitter...
299                          * get a temporary OAuth key/secret pair and display a button with
300                          * which the user can request a PIN to connect the account to a
301                          * account at Twitter.
302                          */
303                         $connection = new TwitterOAuth($ckey, $csecret);
304                         $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
305
306                         $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>';
307                         $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>';
308                         $s .= '<div id="twitter-pin-wrapper">';
309                         $s .= '<label id="twitter-pin-label" for="twitter-pin">' . L10n::t('Copy the PIN from Twitter here') . '</label>';
310                         $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
311                         $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result['oauth_token'] . '" />';
312                         $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result['oauth_token_secret'] . '" />';
313                         $s .= '</div><div class="clear"></div>';
314                         $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
315                 } else {
316                         /*                       * *
317                          *  we have an OAuth key / secret pair for the user
318                          *  so let's give a chance to disable the postings to Twitter
319                          */
320                         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
321                         $details = $connection->get('account/verify_credentials');
322
323                         $field_checkbox = get_markup_template('field_checkbox.tpl');
324
325                         $s .= '<div id="twitter-info" >
326                                 <p>' . L10n::t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
327                                         <button type="submit" name="twitter-disconnect" value="1">' . L10n::t('Disconnect') . '</button>
328                                 </p>
329                                 <p id="twitter-info-block">
330                                         <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
331                                         <em>' . $details->description . '</em>
332                                 </p>
333                         </div>';
334                         $s .= '<div class="clear"></div>';
335
336                         $s .= replace_macros($field_checkbox, [
337                                 '$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.')]
338                         ]);
339                         if ($a->user['hidewall']) {
340                                 $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>';
341                         }
342                         $s .= replace_macros($field_checkbox, [
343                                 '$field' => ['twitter-default', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
344                         ]);
345                         $s .= replace_macros($field_checkbox, [
346                                 '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
347                         ]);
348                         $s .= replace_macros($field_checkbox, [
349                                 '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
350                         ]);
351                         $s .= replace_macros($field_checkbox, [
352                                 '$field' => ['twitter-create_user', L10n::t('Automatically create contacts'), $create_userenabled, '']
353                         ]);
354
355                         $s .= '<div class="clear"></div>';
356                         $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
357                 }
358         }
359         $s .= '</div><div class="clear"></div>';
360 }
361
362 function twitter_post_local(App $a, &$b)
363 {
364         if ($b['edit']) {
365                 return;
366         }
367
368         if (!local_user() || (local_user() != $b['uid'])) {
369                 return;
370         }
371
372         $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
373         $twitter_enable = (($twitter_post && x($_REQUEST, 'twitter_enable')) ? intval($_REQUEST['twitter_enable']) : 0);
374
375         // if API is used, default to the chosen settings
376         if ($b['api_source'] && intval(PConfig::get(local_user(), 'twitter', 'post_by_default'))) {
377                 $twitter_enable = 1;
378         }
379
380         if (!$twitter_enable) {
381                 return;
382         }
383
384         if (strlen($b['postopts'])) {
385                 $b['postopts'] .= ',';
386         }
387
388         $b['postopts'] .= 'twitter';
389 }
390
391 function twitter_action(App $a, $uid, $pid, $action)
392 {
393         $ckey = Config::get('twitter', 'consumerkey');
394         $csecret = Config::get('twitter', 'consumersecret');
395         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
396         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
397
398         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
399
400         $post = ['id' => $pid];
401
402         logger("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), LOGGER_DATA);
403
404         switch ($action) {
405                 case "delete":
406                         // To-Do: $result = $connection->post('statuses/destroy', $post);
407                         break;
408                 case "like":
409                         $result = $connection->post('favorites/create', $post);
410                         break;
411                 case "unlike":
412                         $result = $connection->post('favorites/destroy', $post);
413                         break;
414         }
415         logger("twitter_action '" . $action . "' send, result: " . print_r($result, true), LOGGER_DEBUG);
416 }
417
418 function twitter_post_hook(App $a, &$b)
419 {
420         // Post to Twitter
421         if (!PConfig::get($b["uid"], 'twitter', 'import')
422                 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
423                 return;
424         }
425
426         if ($b['parent'] != $b['id']) {
427                 logger("twitter_post_hook: parameter " . print_r($b, true), LOGGER_DATA);
428
429                 // Looking if its a reply to a twitter post
430                 if ((substr($b["parent-uri"], 0, 9) != "twitter::")
431                         && (substr($b["extid"], 0, 9) != "twitter::")
432                         && (substr($b["thr-parent"], 0, 9) != "twitter::"))
433                 {
434                         logger("twitter_post_hook: no twitter post " . $b["parent"]);
435                         return;
436                 }
437
438                 $r = q("SELECT * FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
439                         dbesc($b["thr-parent"]),
440                         intval($b["uid"]));
441
442                 if (!count($r)) {
443                         logger("twitter_post_hook: no parent found " . $b["thr-parent"]);
444                         return;
445                 } else {
446                         $iscomment = true;
447                         $orig_post = $r[0];
448                 }
449
450
451                 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
452                 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
453                 $nicknameplain = "@" . $nicknameplain;
454
455                 logger("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], LOGGER_DEBUG);
456                 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
457                         $b["body"] = $nickname . " " . $b["body"];
458                 }
459
460                 logger("twitter_post_hook: parent found " . print_r($orig_post, true), LOGGER_DATA);
461         } else {
462                 $iscomment = false;
463
464                 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
465                         return;
466                 }
467
468                 // Dont't post if the post doesn't belong to us.
469                 // This is a check for forum postings
470                 $self = dba::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
471                 if ($b['contact-id'] != $self['id']) {
472                         return;
473                 }
474         }
475
476         if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
477                 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
478         }
479
480         if ($b['verb'] == ACTIVITY_LIKE) {
481                 logger("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), LOGGER_DEBUG);
482                 if ($b['deleted']) {
483                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
484                 } else {
485                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
486                 }
487
488                 return;
489         }
490
491         if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
492                 return;
493         }
494
495         // if post comes from twitter don't send it back
496         if ($b['extid'] == NETWORK_TWITTER) {
497                 return;
498         }
499
500         if ($b['app'] == "Twitter") {
501                 return;
502         }
503
504         logger('twitter post invoked');
505
506         PConfig::load($b['uid'], 'twitter');
507
508         $ckey    = Config::get('twitter', 'consumerkey');
509         $csecret = Config::get('twitter', 'consumersecret');
510         $otoken  = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
511         $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
512
513         if ($ckey && $csecret && $otoken && $osecret) {
514                 logger('twitter: we have customer key and oauth stuff, going to send.', LOGGER_DEBUG);
515
516                 // If it's a repeated message from twitter then do a native retweet and exit
517                 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
518                         return;
519                 }
520
521                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
522
523                 $max_char = 280;
524                 $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
525                 $msg = $msgarr["text"];
526
527                 if (($msg == "") && isset($msgarr["title"])) {
528                         $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
529                 }
530
531                 $image = "";
532
533                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
534                         $msg .= "\n" . $msgarr["url"];
535                 }
536
537                 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
538                         $image = $msgarr["image"];
539                 }
540
541                 // and now tweet it :-)
542                 if (strlen($msg) && ($image != "")) {
543                         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
544                         $media = $connection->upload('media/upload', ['media' => $image]);
545
546                         $post = ['status' => $msg, 'media_ids' => $media->media_id_string];
547
548                         if ($iscomment) {
549                                 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
550                         }
551
552                         $result = $connection->post('statuses/update', $post);
553
554                         logger('twitter_post_with_media send, result: ' . print_r($result, true), LOGGER_DEBUG);
555
556                         if ($result->source) {
557                                 Config::set("twitter", "application_name", strip_tags($result->source));
558                         }
559
560                         if ($result->errors || $result->error) {
561                                 logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
562
563                                 // Workaround: Remove the picture link so that the post can be reposted without it
564                                 $msg .= " " . $image;
565                                 $image = "";
566                         } elseif ($iscomment) {
567                                 logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
568                                 Item::update(['extid' => "twitter::" . $result->id_str, 'body' => $result->text], ['id' => $b['id']]);
569                         }
570                 }
571
572                 if (strlen($msg) && ($image == "")) {
573 // -----------------
574                         $max_char = 280;
575                         $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
576                         $msg = $msgarr["text"];
577
578                         if (($msg == "") && isset($msgarr["title"])) {
579                                 $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
580                         }
581
582                         if (isset($msgarr["url"])) {
583                                 $msg .= "\n" . $msgarr["url"];
584                         }
585 // -----------------
586                         $url = 'statuses/update';
587                         $post = ['status' => $msg, 'weighted_character_count' => 'true'];
588
589                         if ($iscomment) {
590                                 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
591                         }
592
593                         $result = $connection->post($url, $post);
594                         logger('twitter_post send, result: ' . print_r($result, true), LOGGER_DEBUG);
595
596                         if ($result->source) {
597                                 Config::set("twitter", "application_name", strip_tags($result->source));
598                         }
599
600                         if ($result->errors) {
601                                 logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
602
603                                 $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
604                                 if (count($r)) {
605                                         $a->contact = $r[0]["id"];
606                                 }
607
608                                 $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
609
610                                 Queue::add($a->contact, NETWORK_TWITTER, $s);
611                                 notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
612                         } elseif ($iscomment) {
613                                 logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
614                                 Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
615                         }
616                 }
617         }
618 }
619
620 function twitter_addon_admin_post(App $a)
621 {
622         $consumerkey    = x($_POST, 'consumerkey')    ? notags(trim($_POST['consumerkey']))    : '';
623         $consumersecret = x($_POST, 'consumersecret') ? notags(trim($_POST['consumersecret'])) : '';
624         Config::set('twitter', 'consumerkey', $consumerkey);
625         Config::set('twitter', 'consumersecret', $consumersecret);
626         info(L10n::t('Settings updated.') . EOL);
627 }
628
629 function twitter_addon_admin(App $a, &$o)
630 {
631         $t = get_markup_template("admin.tpl", "addon/twitter/");
632
633         $o = replace_macros($t, [
634                 '$submit' => L10n::t('Save Settings'),
635                 // name, label, value, help, [extra values]
636                 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
637                 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
638         ]);
639 }
640
641 function twitter_cron(App $a, $b)
642 {
643         $last = Config::get('twitter', 'last_poll');
644
645         $poll_interval = intval(Config::get('twitter', 'poll_interval'));
646         if (!$poll_interval) {
647                 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
648         }
649
650         if ($last) {
651                 $next = $last + ($poll_interval * 60);
652                 if ($next > time()) {
653                         logger('twitter: poll intervall not reached');
654                         return;
655                 }
656         }
657         logger('twitter: cron_start');
658
659         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
660         if (count($r)) {
661                 foreach ($r as $rr) {
662                         logger('twitter: fetching for user ' . $rr['uid']);
663                         Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
664                 }
665         }
666
667         $abandon_days = intval(Config::get('system', 'account_abandon_days'));
668         if ($abandon_days < 1) {
669                 $abandon_days = 0;
670         }
671
672         $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
673
674         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
675         if (count($r)) {
676                 foreach ($r as $rr) {
677                         if ($abandon_days != 0) {
678                                 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
679                                 if (!count($user)) {
680                                         logger('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
681                                         continue;
682                                 }
683                         }
684
685                         logger('twitter: importing timeline from user ' . $rr['uid']);
686                         Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
687                         /*
688                           // To-Do
689                           // check for new contacts once a day
690                           $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
691                           if($last_contact_check)
692                           $next_contact_check = $last_contact_check + 86400;
693                           else
694                           $next_contact_check = 0;
695
696                           if($next_contact_check <= time()) {
697                           pumpio_getallusers($a, $rr["uid"]);
698                           PConfig::set($rr['uid'],'pumpio','contact_check',time());
699                           }
700                          */
701                 }
702         }
703
704         logger('twitter: cron_end');
705
706         Config::set('twitter', 'last_poll', time());
707 }
708
709 function twitter_expire(App $a, $b)
710 {
711         $days = Config::get('twitter', 'expire');
712
713         if ($days == 0) {
714                 return;
715         }
716
717         if (method_exists('dba', 'delete')) {
718                 $r = dba::select('item', ['id'], ['deleted' => true, 'network' => NETWORK_TWITTER]);
719                 while ($row = dba::fetch($r)) {
720                         dba::delete('item', ['id' => $row['id']]);
721                 }
722                 dba::close($r);
723         } else {
724                 $r = q("DELETE FROM `item` WHERE `deleted` AND `network` = '%s'", dbesc(NETWORK_TWITTER));
725         }
726
727         require_once "include/items.php";
728
729         logger('twitter_expire: expire_start');
730
731         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
732         if (count($r)) {
733                 foreach ($r as $rr) {
734                         logger('twitter_expire: user ' . $rr['uid']);
735                         Item::expire($rr['uid'], $days, NETWORK_TWITTER, true);
736                 }
737         }
738
739         logger('twitter_expire: expire_end');
740 }
741
742 function twitter_prepare_body(App $a, &$b)
743 {
744         if ($b["item"]["network"] != NETWORK_TWITTER) {
745                 return;
746         }
747
748         if ($b["preview"]) {
749                 $max_char = 280;
750                 $item = $b["item"];
751                 $item["plink"] = $a->get_baseurl() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
752
753                 $r = q("SELECT `author-link` FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
754                         dbesc($item["thr-parent"]),
755                         intval(local_user()));
756
757                 if (count($r)) {
758                         $orig_post = $r[0];
759
760                         $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
761                         $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
762                         $nicknameplain = "@" . $nicknameplain;
763
764                         if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
765                                 $item["body"] = $nickname . " " . $item["body"];
766                         }
767                 }
768
769                 $msgarr = BBCode::toPlaintext($item, $max_char, true, 8);
770                 $msg = $msgarr["text"];
771
772                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
773                         $msg .= " " . $msgarr["url"];
774                 }
775
776                 if (isset($msgarr["image"])) {
777                         $msg .= " " . $msgarr["image"];
778                 }
779
780                 $b['html'] = nl2br(htmlspecialchars($msg));
781         }
782 }
783
784 /**
785  * @brief Build the item array for the mirrored post
786  *
787  * @param App $a Application class
788  * @param integer $uid User id
789  * @param object $post Twitter object with the post
790  *
791  * @return array item data to be posted
792  */
793 function twitter_do_mirrorpost(App $a, $uid, $post)
794 {
795         $datarray["type"] = "wall";
796         $datarray["api_source"] = true;
797         $datarray["profile_uid"] = $uid;
798         $datarray["extid"] = NETWORK_TWITTER;
799         $datarray['message_id'] = item_new_uri($a->get_hostname(), $uid, NETWORK_TWITTER . ":" . $post->id);
800         $datarray['object'] = json_encode($post);
801         $datarray["title"] = "";
802
803         if (is_object($post->retweeted_status)) {
804                 // We don't support nested shares, so we mustn't show quotes as shares on retweets
805                 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
806
807                 $datarray['body'] = "\n" . share_header(
808                         $item['author-name'],
809                         $item['author-link'],
810                         $item['author-avatar'],
811                         "",
812                         $item['created'],
813                         $item['plink']
814                 );
815
816                 $datarray['body'] .= $item['body'] . '[/share]';
817         } else {
818                 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
819
820                 $datarray['body'] = $item['body'];
821         }
822
823         $datarray["source"] = $item['app'];
824         $datarray["verb"] = $item['verb'];
825
826         if (isset($item["location"])) {
827                 $datarray["location"] = $item["location"];
828         }
829
830         if (isset($item["coord"])) {
831                 $datarray["coord"] = $item["coord"];
832         }
833
834         return $datarray;
835 }
836
837 function twitter_fetchtimeline(App $a, $uid)
838 {
839         $ckey    = Config::get('twitter', 'consumerkey');
840         $csecret = Config::get('twitter', 'consumersecret');
841         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
842         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
843         $lastid  = PConfig::get($uid, 'twitter', 'lastid');
844
845         $application_name = Config::get('twitter', 'application_name');
846
847         if ($application_name == "") {
848                 $application_name = $a->get_hostname();
849         }
850
851         $has_picture = false;
852
853         require_once 'mod/item.php';
854         require_once 'include/items.php';
855         require_once 'mod/share.php';
856
857         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
858
859         $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
860
861         $first_time = ($lastid == "");
862
863         if ($lastid != "") {
864                 $parameters["since_id"] = $lastid;
865         }
866
867         $items = $connection->get('statuses/user_timeline', $parameters);
868
869         if (!is_array($items)) {
870                 return;
871         }
872
873         $posts = array_reverse($items);
874
875         if (count($posts)) {
876                 foreach ($posts as $post) {
877                         if ($post->id_str > $lastid) {
878                                 $lastid = $post->id_str;
879                                 PConfig::set($uid, 'twitter', 'lastid', $lastid);
880                         }
881
882                         if ($first_time) {
883                                 continue;
884                         }
885
886                         if (!stristr($post->source, $application_name)) {
887                                 $_SESSION["authenticated"] = true;
888                                 $_SESSION["uid"] = $uid;
889
890                                 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
891
892                                 logger('twitter: posting for user ' . $uid);
893
894                                 item_post($a);
895                         }
896                 }
897         }
898         PConfig::set($uid, 'twitter', 'lastid', $lastid);
899 }
900
901 function twitter_queue_hook(App $a, &$b)
902 {
903         $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
904                 dbesc(NETWORK_TWITTER)
905         );
906         if (!count($qi)) {
907                 return;
908         }
909
910         foreach ($qi as $x) {
911                 if ($x['network'] !== NETWORK_TWITTER) {
912                         continue;
913                 }
914
915                 logger('twitter_queue: run');
916
917                 $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
918                         WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
919                         intval($x['cid'])
920                 );
921                 if (!count($r)) {
922                         continue;
923                 }
924
925                 $user = $r[0];
926
927                 $ckey    = Config::get('twitter', 'consumerkey');
928                 $csecret = Config::get('twitter', 'consumersecret');
929                 $otoken  = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
930                 $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
931
932                 $success = false;
933
934                 if ($ckey && $csecret && $otoken && $osecret) {
935                         logger('twitter_queue: able to post');
936
937                         $z = unserialize($x['content']);
938
939                         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
940                         $result = $connection->post($z['url'], $z['post']);
941
942                         logger('twitter_queue: post result: ' . print_r($result, true), LOGGER_DEBUG);
943
944                         if ($result->errors) {
945                                 logger('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
946                         } else {
947                                 $success = true;
948                                 Queue::removeItem($x['id']);
949                         }
950                 } else {
951                         logger("twitter_queue: Error getting tokens for user " . $user['uid']);
952                 }
953
954                 if (!$success) {
955                         logger('twitter_queue: delayed');
956                         Queue::updateTime($x['id']);
957                 }
958         }
959 }
960
961 function twitter_fix_avatar($avatar)
962 {
963         $new_avatar = str_replace("_normal.", ".", $avatar);
964
965         $info = Image::getInfoFromURL($new_avatar);
966         if (!$info) {
967                 $new_avatar = $avatar;
968         }
969
970         return $new_avatar;
971 }
972
973 function twitter_fetch_contact($uid, $contact, $create_user)
974 {
975         if ($contact->id_str == "") {
976                 return -1;
977         }
978
979         $avatar = twitter_fix_avatar($contact->profile_image_url_https);
980
981         GContact::update(["url" => "https://twitter.com/" . $contact->screen_name,
982                 "network" => NETWORK_TWITTER, "photo" => $avatar, "hide" => true,
983                 "name" => $contact->name, "nick" => $contact->screen_name,
984                 "location" => $contact->location, "about" => $contact->description,
985                 "addr" => $contact->screen_name . "@twitter.com", "generation" => 2]);
986
987         $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
988                 intval($uid),
989                 dbesc("twitter::" . $contact->id_str));
990
991         if (!count($r) && !$create_user) {
992                 return 0;
993         }
994
995         if (count($r) && ($r[0]["readonly"] || $r[0]["blocked"])) {
996                 logger("twitter_fetch_contact: Contact '" . $r[0]["nick"] . "' is blocked or readonly.", LOGGER_DEBUG);
997                 return -1;
998         }
999
1000         if (!count($r)) {
1001                 // create contact record
1002                 q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `addr`, `alias`, `notify`, `poll`,
1003                                         `name`, `nick`, `photo`, `network`, `rel`, `priority`,
1004                                         `location`, `about`, `writable`, `blocked`, `readonly`, `pending`)
1005                                         VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', %d, 0, 0, 0)",
1006                         intval($uid),
1007                         dbesc(DateTimeFormat::utcNow()),
1008                         dbesc("https://twitter.com/" . $contact->screen_name),
1009                         dbesc(normalise_link("https://twitter.com/" . $contact->screen_name)),
1010                         dbesc($contact->screen_name."@twitter.com"),
1011                         dbesc("twitter::" . $contact->id_str),
1012                         dbesc(''),
1013                         dbesc("twitter::" . $contact->id_str),
1014                         dbesc($contact->name),
1015                         dbesc($contact->screen_name),
1016                         dbesc($avatar),
1017                         dbesc(NETWORK_TWITTER),
1018                         intval(CONTACT_IS_FRIEND),
1019                         intval(1),
1020                         dbesc($contact->location),
1021                         dbesc($contact->description),
1022                         intval(1)
1023                 );
1024
1025                 $r = q("SELECT * FROM `contact` WHERE `alias` = '%s' AND `uid` = %d LIMIT 1",
1026                         dbesc("twitter::".$contact->id_str),
1027                         intval($uid)
1028                 );
1029
1030                 if (!count($r)) {
1031                         return false;
1032                 }
1033
1034                 $contact_id = $r[0]['id'];
1035
1036                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1037
1038                 $photos = Photo::importProfilePhoto($avatar, $uid, $contact_id, true);
1039
1040                 if ($photos) {
1041                         q("UPDATE `contact` SET `photo` = '%s',
1042                                                 `thumb` = '%s',
1043                                                 `micro` = '%s',
1044                                                 `name-date` = '%s',
1045                                                 `uri-date` = '%s',
1046                                                         `avatar-date` = '%s'
1047                                         WHERE `id` = %d",
1048                                 dbesc($photos[0]),
1049                                 dbesc($photos[1]),
1050                                 dbesc($photos[2]),
1051                                 dbesc(DateTimeFormat::utcNow()),
1052                                 dbesc(DateTimeFormat::utcNow()),
1053                                 dbesc(DateTimeFormat::utcNow()),
1054                                 intval($contact_id)
1055                         );
1056                 }
1057         } else {
1058                 // update profile photos once every two weeks as we have no notification of when they change.
1059                 //$update_photo = (($r[0]['avatar-date'] < DateTimeFormat::convert('now -2 days', '', '', )) ? true : false);
1060                 $update_photo = ($r[0]['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
1061
1062                 // check that we have all the photos, this has been known to fail on occasion
1063                 if ((!$r[0]['photo']) || (!$r[0]['thumb']) || (!$r[0]['micro']) || ($update_photo)) {
1064                         logger("twitter_fetch_contact: Updating contact " . $contact->screen_name, LOGGER_DEBUG);
1065
1066                         $photos = Photo::importProfilePhoto($avatar, $uid, $r[0]['id'], true);
1067
1068                         if ($photos) {
1069                                 q("UPDATE `contact` SET `photo` = '%s',
1070                                                         `thumb` = '%s',
1071                                                         `micro` = '%s',
1072                                                         `name-date` = '%s',
1073                                                         `uri-date` = '%s',
1074                                                         `avatar-date` = '%s',
1075                                                         `url` = '%s',
1076                                                         `nurl` = '%s',
1077                                                         `addr` = '%s',
1078                                                         `name` = '%s',
1079                                                         `nick` = '%s',
1080                                                         `location` = '%s',
1081                                                         `about` = '%s'
1082                                                 WHERE `id` = %d",
1083                                         dbesc($photos[0]),
1084                                         dbesc($photos[1]),
1085                                         dbesc($photos[2]),
1086                                         dbesc(DateTimeFormat::utcNow()),
1087                                         dbesc(DateTimeFormat::utcNow()),
1088                                         dbesc(DateTimeFormat::utcNow()),
1089                                         dbesc("https://twitter.com/".$contact->screen_name),
1090                                         dbesc(normalise_link("https://twitter.com/".$contact->screen_name)),
1091                                         dbesc($contact->screen_name."@twitter.com"),
1092                                         dbesc($contact->name),
1093                                         dbesc($contact->screen_name),
1094                                         dbesc($contact->location),
1095                                         dbesc($contact->description),
1096                                         intval($r[0]['id'])
1097                                 );
1098                         }
1099                 }
1100         }
1101
1102         return $r[0]["id"];
1103 }
1104
1105 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1106 {
1107         $ckey = Config::get('twitter', 'consumerkey');
1108         $csecret = Config::get('twitter', 'consumersecret');
1109         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1110         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1111
1112         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1113                 intval($uid));
1114
1115         if (count($r)) {
1116                 $self = $r[0];
1117         } else {
1118                 return;
1119         }
1120
1121         $parameters = [];
1122
1123         if ($screen_name != "") {
1124                 $parameters["screen_name"] = $screen_name;
1125         }
1126
1127         if ($user_id != "") {
1128                 $parameters["user_id"] = $user_id;
1129         }
1130
1131         // Fetching user data
1132         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1133         $user = $connection->get('users/show', $parameters);
1134
1135         if (!is_object($user)) {
1136                 return;
1137         }
1138
1139         $contact_id = twitter_fetch_contact($uid, $user, true);
1140
1141         return $contact_id;
1142 }
1143
1144 function twitter_expand_entities(App $a, $body, $item, $no_tags = false, $picture)
1145 {
1146         $tags = "";
1147
1148         $plain = $body;
1149
1150         if (isset($item->entities->urls)) {
1151                 $type = "";
1152                 $footerurl = "";
1153                 $footerlink = "";
1154                 $footer = "";
1155
1156                 foreach ($item->entities->urls as $url) {
1157                         $plain = str_replace($url->url, '', $plain);
1158
1159                         if ($url->url && $url->expanded_url && $url->display_url) {
1160                                 $expanded_url = Network::finalUrl($url->expanded_url);
1161
1162                                 $oembed_data = OEmbed::fetchURL($expanded_url);
1163
1164                                 // Quickfix: Workaround for URL with "[" and "]" in it
1165                                 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1166                                         $expanded_url = $url->url;
1167                                 }
1168
1169                                 if ($type == "") {
1170                                         $type = $oembed_data->type;
1171                                 }
1172
1173                                 if ($oembed_data->type == "video") {
1174                                         //$body = str_replace($url->url,
1175                                         //              "[video]".$expanded_url."[/video]", $body);
1176                                         //$dontincludemedia = true;
1177                                         $type = $oembed_data->type;
1178                                         $footerurl = $expanded_url;
1179                                         $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1180
1181                                         $body = str_replace($url->url, $footerlink, $body);
1182                                         //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1183                                 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1184                                         $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1185                                         //$dontincludemedia = true;
1186                                 } elseif ($oembed_data->type != "link") {
1187                                         $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1188                                 } else {
1189                                         $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1190
1191                                         $tempfile = tempnam(get_temppath(), "cache");
1192                                         file_put_contents($tempfile, $img_str);
1193                                         $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1194                                         unlink($tempfile);
1195
1196                                         if (substr($mime, 0, 6) == "image/") {
1197                                                 $type = "photo";
1198                                                 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1199                                                 //$dontincludemedia = true;
1200                                         } else {
1201                                                 $type = $oembed_data->type;
1202                                                 $footerurl = $expanded_url;
1203                                                 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1204
1205                                                 $body = str_replace($url->url, $footerlink, $body);
1206                                         }
1207                                 }
1208                         }
1209                 }
1210
1211                 if ($footerurl != "") {
1212                         $footer = add_page_info($footerurl, false, $picture);
1213                 }
1214
1215                 if (($footerlink != "") && (trim($footer) != "")) {
1216                         $removedlink = trim(str_replace($footerlink, "", $body));
1217
1218                         if (($removedlink == "") || strstr($body, $removedlink)) {
1219                                 $body = $removedlink;
1220                         }
1221
1222                         $body .= $footer;
1223                 }
1224
1225                 if (($footer == "") && ($picture != "")) {
1226                         $body .= "\n\n[img]" . $picture . "[/img]\n";
1227                 } elseif (($footer == "") && ($picture == "")) {
1228                         $body = add_page_info_to_body($body);
1229                 }
1230
1231                 if ($no_tags) {
1232                         return ["body" => $body, "tags" => "", "plain" => $plain];
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 Returns a a single picture string if it isn't a media post
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, false, $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 }