Merge pull request #566 from MrPetovan/task/add-content_filter-hook
[friendica-addons.git/.git] / twitter / twitter.php
1 <?php
2 /**
3  * Name: Twitter Connector
4  * Description: Bidirectional (posting, relaying and reading) connector for Twitter.
5  * Version: 1.1.0
6  * Author: Tobias Diekershoff <https://f.diekershoff.de/profile/tobias>
7  * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
8  * Maintainer: Hypolite Petovan <https://friendica.mrpetovan.com/profile/hypolite>
9  *
10  * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel, Hypolite Petovan
11  * All rights reserved.
12  *
13  * Redistribution and use in source and binary forms, with or without
14  * modification, are permitted provided that the following conditions are met:
15  *    * Redistributions of source code must retain the above copyright notice,
16  *     this list of conditions and the following disclaimer.
17  *    * Redistributions in binary form must reproduce the above
18  *    * copyright notice, this list of conditions and the following disclaimer in
19  *      the documentation and/or other materials provided with the distribution.
20  *    * Neither the name of the <organization> nor the names of its contributors
21  *      may be used to endorse or promote products derived from this software
22  *      without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27  * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
28  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
32  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
33  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34  *
35  */
36 /*   Twitter Addon for Friendica
37  *
38  *   Author: Tobias Diekershoff
39  *           tobias.diekershoff@gmx.net
40  *
41  *   License:3-clause BSD license
42  *
43  *   Configuration:
44  *     To use this addon you need a OAuth Consumer key pair (key & secret)
45  *     you can get it from Twitter at https://twitter.com/apps
46  *
47  *     Register your Friendica site as "Client" application with "Read & Write" access
48  *     we do not need "Twitter as login". When you've registered the app you get the
49  *     OAuth Consumer key and secret pair for your application/site.
50  *
51  *     Add this key pair to your global .htconfig.php or use the admin panel.
52  *
53  *     $a->config['twitter']['consumerkey'] = 'your consumer_key here';
54  *     $a->config['twitter']['consumersecret'] = 'your consumer_secret here';
55  *
56  *     To activate the addon itself add it to the $a->config['system']['addon']
57  *     setting. After this, your user can configure their Twitter account settings
58  *     from "Settings -> Addon Settings".
59  *
60  *     Requirements: PHP5, curl
61  */
62
63 use Abraham\TwitterOAuth\TwitterOAuth;
64 use Friendica\App;
65 use Friendica\Content\OEmbed;
66 use Friendica\Content\Text\BBCode;
67 use Friendica\Content\Text\Plaintext;
68 use Friendica\Core\Addon;
69 use Friendica\Core\Config;
70 use Friendica\Core\L10n;
71 use Friendica\Core\PConfig;
72 use Friendica\Core\Worker;
73 use Friendica\Model\GContact;
74 use Friendica\Model\Group;
75 use Friendica\Model\Item;
76 use Friendica\Model\Photo;
77 use Friendica\Model\Queue;
78 use Friendica\Model\User;
79 use Friendica\Object\Image;
80 use Friendica\Util\DateTimeFormat;
81 use Friendica\Util\Network;
82
83 require_once 'boot.php';
84 require_once 'include/dba.php';
85 require_once 'include/enotify.php';
86 require_once 'include/text.php';
87
88 require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
89
90 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
91
92 function twitter_install()
93 {
94         //  we need some hooks, for the configuration and for sending tweets
95         Addon::registerHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
96         Addon::registerHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
97         Addon::registerHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
98         Addon::registerHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
99         Addon::registerHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
100         Addon::registerHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
101         Addon::registerHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
102         Addon::registerHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
103         Addon::registerHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
104         Addon::registerHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
105         Addon::registerHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
106         logger("installed twitter");
107 }
108
109 function twitter_uninstall()
110 {
111         Addon::unregisterHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
112         Addon::unregisterHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
113         Addon::unregisterHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
114         Addon::unregisterHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
115         Addon::unregisterHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
116         Addon::unregisterHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
117         Addon::unregisterHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
118         Addon::unregisterHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
119         Addon::unregisterHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
120         Addon::unregisterHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
121         Addon::unregisterHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
122
123         // old setting - remove only
124         Addon::unregisterHook('post_local_end', 'addon/twitter/twitter.php', 'twitter_post_hook');
125         Addon::unregisterHook('addon_settings', 'addon/twitter/twitter.php', 'twitter_settings');
126         Addon::unregisterHook('addon_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
127 }
128
129 function twitter_check_item_notification(App $a, &$notification_data)
130 {
131         $own_id = PConfig::get($notification_data["uid"], 'twitter', 'own_id');
132
133         $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
134                         intval($notification_data["uid"]),
135                         dbesc("twitter::".$own_id)
136         );
137
138         if ($own_user) {
139                 $notification_data["profiles"][] = $own_user[0]["url"];
140         }
141 }
142
143 function twitter_follow(App $a, &$contact)
144 {
145         logger("twitter_follow: Check if contact is twitter contact. " . $contact["url"], LOGGER_DEBUG);
146
147         if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
148                 return;
149         }
150
151         // contact seems to be a twitter contact, so continue
152         $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
153         $nickname = str_replace("@twitter.com", "", $nickname);
154
155         $uid = $a->user["uid"];
156
157         $ckey = Config::get('twitter', 'consumerkey');
158         $csecret = Config::get('twitter', 'consumersecret');
159         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
160         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
161
162         // If the addon is not configured (general or for this user) quit here
163         if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
164                 $contact = false;
165                 return;
166         }
167
168         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
169         $connection->post('friendships/create', ['screen_name' => $nickname]);
170
171         twitter_fetchuser($a, $uid, $nickname);
172
173         $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
174                 FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
175                                 intval($uid),
176                                 dbesc($nickname));
177         if (count($r)) {
178                 $contact["contact"] = $r[0];
179         }
180 }
181
182 function twitter_jot_nets(App $a, &$b)
183 {
184         if (!local_user()) {
185                 return;
186         }
187
188         $tw_post = PConfig::get(local_user(), 'twitter', 'post');
189         if (intval($tw_post) == 1) {
190                 $tw_defpost = PConfig::get(local_user(), 'twitter', 'post_by_default');
191                 $selected = ((intval($tw_defpost) == 1) ? ' checked="checked" ' : '');
192                 $b .= '<div class="profile-jot-net"><input type="checkbox" name="twitter_enable"' . $selected . ' value="1" /> '
193                         . L10n::t('Post to Twitter') . '</div>';
194         }
195 }
196
197 function twitter_settings_post(App $a, $post)
198 {
199         if (!local_user()) {
200                 return;
201         }
202         // don't check twitter settings if twitter submit button is not clicked
203         if (empty($_POST['twitter-disconnect']) && empty($_POST['twitter-submit'])) {
204                 return;
205         }
206
207         if (!empty($_POST['twitter-disconnect'])) {
208                 /*               * *
209                  * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
210                  * from the user configuration
211                  */
212                 PConfig::delete(local_user(), 'twitter', 'consumerkey');
213                 PConfig::delete(local_user(), 'twitter', 'consumersecret');
214                 PConfig::delete(local_user(), 'twitter', 'oauthtoken');
215                 PConfig::delete(local_user(), 'twitter', 'oauthsecret');
216                 PConfig::delete(local_user(), 'twitter', 'post');
217                 PConfig::delete(local_user(), 'twitter', 'post_by_default');
218                 PConfig::delete(local_user(), 'twitter', 'lastid');
219                 PConfig::delete(local_user(), 'twitter', 'mirror_posts');
220                 PConfig::delete(local_user(), 'twitter', 'import');
221                 PConfig::delete(local_user(), 'twitter', 'create_user');
222                 PConfig::delete(local_user(), 'twitter', 'own_id');
223         } else {
224                 if (isset($_POST['twitter-pin'])) {
225                         //  if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
226                         logger('got a Twitter PIN');
227                         $ckey    = Config::get('twitter', 'consumerkey');
228                         $csecret = Config::get('twitter', 'consumersecret');
229                         //  the token and secret for which the PIN was generated were hidden in the settings
230                         //  form as token and token2, we need a new connection to Twitter using these token
231                         //  and secret to request a Access Token with the PIN
232                         try {
233                                 if (empty($_POST['twitter-pin'])) {
234                                         throw new Exception(L10n::t('You submitted an empty PIN, please Sign In with Twitter again to get a new one.'));
235                                 }
236
237                                 $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
238                                 $token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $_POST['twitter-pin']]);
239                                 //  ok, now that we have the Access Token, save them in the user config
240                                 PConfig::set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
241                                 PConfig::set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
242                                 PConfig::set(local_user(), 'twitter', 'post', 1);
243                         } catch(Exception $e) {
244                                 info($e->getMessage());
245                         }
246                         //  reload the Addon Settings page, if we don't do it see Bug #42
247                         goaway('settings/connectors');
248                 } else {
249                         //  if no PIN is supplied in the POST variables, the user has changed the setting
250                         //  to post a tweet for every new __public__ posting to the wall
251                         PConfig::set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
252                         PConfig::set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
253                         PConfig::set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
254                         PConfig::set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
255                         PConfig::set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
256
257                         if (!intval($_POST['twitter-mirror'])) {
258                                 PConfig::delete(local_user(), 'twitter', 'lastid');
259                         }
260
261                         info(L10n::t('Twitter settings updated.') . EOL);
262                 }
263         }
264 }
265
266 function twitter_settings(App $a, &$s)
267 {
268         if (!local_user()) {
269                 return;
270         }
271         $a->page['htmlhead'] .= '<link rel="stylesheet"  type="text/css" href="' . $a->get_baseurl() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
272         /*       * *
273          * 1) Check that we have global consumer key & secret
274          * 2) If no OAuthtoken & stuff is present, generate button to get some
275          * 3) Checkbox for "Send public notices (280 chars only)
276          */
277         $ckey    = Config::get('twitter', 'consumerkey');
278         $csecret = Config::get('twitter', 'consumersecret');
279         $otoken  = PConfig::get(local_user(), 'twitter', 'oauthtoken');
280         $osecret = PConfig::get(local_user(), 'twitter', 'oauthsecret');
281
282         $enabled            = intval(PConfig::get(local_user(), 'twitter', 'post'));
283         $defenabled         = intval(PConfig::get(local_user(), 'twitter', 'post_by_default'));
284         $mirrorenabled      = intval(PConfig::get(local_user(), 'twitter', 'mirror_posts'));
285         $importenabled      = intval(PConfig::get(local_user(), 'twitter', 'import'));
286         $create_userenabled = intval(PConfig::get(local_user(), 'twitter', 'create_user'));
287
288         $css = (($enabled) ? '' : '-disabled');
289
290         $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
291         $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
292         $s .= '</span>';
293         $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
294         $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
295         $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
296         $s .= '</span>';
297
298         if ((!$ckey) && (!$csecret)) {
299                 /* no global consumer keys
300                  * display warning and skip personal config
301                  */
302                 $s .= '<p>' . L10n::t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
303         } else {
304                 // ok we have a consumer key pair now look into the OAuth stuff
305                 if ((!$otoken) && (!$osecret)) {
306                         /* the user has not yet connected the account to twitter...
307                          * get a temporary OAuth key/secret pair and display a button with
308                          * which the user can request a PIN to connect the account to a
309                          * account at Twitter.
310                          */
311                         $connection = new TwitterOAuth($ckey, $csecret);
312                         $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
313
314                         $s .= '<p>' . L10n::t('At this Friendica instance the Twitter addon was enabled but you have not yet connected your account to your Twitter account. To do so click the button below to get a PIN from Twitter which you have to copy into the input box below and submit the form. Only your <strong>public</strong> posts will be posted to Twitter.') . '</p>';
315                         $s .= '<a href="' . $connection->url('oauth/authorize', ['oauth_token' => $result['oauth_token']]) . '" target="_twitter"><img src="addon/twitter/lighter.png" alt="' . L10n::t('Log in with Twitter') . '"></a>';
316                         $s .= '<div id="twitter-pin-wrapper">';
317                         $s .= '<label id="twitter-pin-label" for="twitter-pin">' . L10n::t('Copy the PIN from Twitter here') . '</label>';
318                         $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
319                         $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result['oauth_token'] . '" />';
320                         $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result['oauth_token_secret'] . '" />';
321                         $s .= '</div><div class="clear"></div>';
322                         $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
323                 } else {
324                         /*                       * *
325                          *  we have an OAuth key / secret pair for the user
326                          *  so let's give a chance to disable the postings to Twitter
327                          */
328                         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
329                         $details = $connection->get('account/verify_credentials');
330
331                         $field_checkbox = get_markup_template('field_checkbox.tpl');
332
333                         $s .= '<div id="twitter-info" >
334                                 <p>' . L10n::t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
335                                         <button type="submit" name="twitter-disconnect" value="1">' . L10n::t('Disconnect') . '</button>
336                                 </p>
337                                 <p id="twitter-info-block">
338                                         <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
339                                         <em>' . $details->description . '</em>
340                                 </p>
341                         </div>';
342                         $s .= '<div class="clear"></div>';
343
344                         $s .= replace_macros($field_checkbox, [
345                                 '$field' => ['twitter-enable', L10n::t('Allow posting to Twitter'), $enabled, L10n::t('If enabled all your <strong>public</strong> postings can be posted to the associated Twitter account. You can choose to do so by default (here) or for every posting separately in the posting options when writing the entry.')]
346                         ]);
347                         if ($a->user['hidewall']) {
348                                 $s .= '<p>' . L10n::t('<strong>Note</strong>: Due to your privacy settings (<em>Hide your profile details from unknown viewers?</em>) the link potentially included in public postings relayed to Twitter will lead the visitor to a blank page informing the visitor that the access to your profile has been restricted.') . '</p>';
349                         }
350                         $s .= replace_macros($field_checkbox, [
351                                 '$field' => ['twitter-default', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
352                         ]);
353                         $s .= replace_macros($field_checkbox, [
354                                 '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
355                         ]);
356                         $s .= replace_macros($field_checkbox, [
357                                 '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
358                         ]);
359                         $s .= replace_macros($field_checkbox, [
360                                 '$field' => ['twitter-create_user', L10n::t('Automatically create contacts'), $create_userenabled, '']
361                         ]);
362
363                         $s .= '<div class="clear"></div>';
364                         $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
365                 }
366         }
367         $s .= '</div><div class="clear"></div>';
368 }
369
370 function twitter_post_local(App $a, &$b)
371 {
372         if ($b['edit']) {
373                 return;
374         }
375
376         if (!local_user() || (local_user() != $b['uid'])) {
377                 return;
378         }
379
380         $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
381         $twitter_enable = (($twitter_post && x($_REQUEST, 'twitter_enable')) ? intval($_REQUEST['twitter_enable']) : 0);
382
383         // if API is used, default to the chosen settings
384         if ($b['api_source'] && intval(PConfig::get(local_user(), 'twitter', 'post_by_default'))) {
385                 $twitter_enable = 1;
386         }
387
388         if (!$twitter_enable) {
389                 return;
390         }
391
392         if (strlen($b['postopts'])) {
393                 $b['postopts'] .= ',';
394         }
395
396         $b['postopts'] .= 'twitter';
397 }
398
399 function twitter_action(App $a, $uid, $pid, $action)
400 {
401         $ckey = Config::get('twitter', 'consumerkey');
402         $csecret = Config::get('twitter', 'consumersecret');
403         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
404         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
405
406         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
407
408         $post = ['id' => $pid];
409
410         logger("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), LOGGER_DATA);
411
412         switch ($action) {
413                 case "delete":
414                         // To-Do: $result = $connection->post('statuses/destroy', $post);
415                         break;
416                 case "like":
417                         $result = $connection->post('favorites/create', $post);
418                         break;
419                 case "unlike":
420                         $result = $connection->post('favorites/destroy', $post);
421                         break;
422         }
423         logger("twitter_action '" . $action . "' send, result: " . print_r($result, true), LOGGER_DEBUG);
424 }
425
426 function twitter_post_hook(App $a, &$b)
427 {
428         // Post to Twitter
429         if (!PConfig::get($b["uid"], 'twitter', 'import')
430                 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
431                 return;
432         }
433
434         if ($b['parent'] != $b['id']) {
435                 logger("twitter_post_hook: parameter " . print_r($b, true), LOGGER_DATA);
436
437                 // Looking if its a reply to a twitter post
438                 if ((substr($b["parent-uri"], 0, 9) != "twitter::")
439                         && (substr($b["extid"], 0, 9) != "twitter::")
440                         && (substr($b["thr-parent"], 0, 9) != "twitter::"))
441                 {
442                         logger("twitter_post_hook: no twitter post " . $b["parent"]);
443                         return;
444                 }
445
446                 $r = q("SELECT * FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
447                         dbesc($b["thr-parent"]),
448                         intval($b["uid"]));
449
450                 if (!count($r)) {
451                         logger("twitter_post_hook: no parent found " . $b["thr-parent"]);
452                         return;
453                 } else {
454                         $iscomment = true;
455                         $orig_post = $r[0];
456                 }
457
458
459                 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
460                 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
461                 $nicknameplain = "@" . $nicknameplain;
462
463                 logger("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], LOGGER_DEBUG);
464                 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
465                         $b["body"] = $nickname . " " . $b["body"];
466                 }
467
468                 logger("twitter_post_hook: parent found " . print_r($orig_post, true), LOGGER_DATA);
469         } else {
470                 $iscomment = false;
471
472                 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
473                         return;
474                 }
475
476                 // Dont't post if the post doesn't belong to us.
477                 // This is a check for forum postings
478                 $self = dba::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
479                 if ($b['contact-id'] != $self['id']) {
480                         return;
481                 }
482         }
483
484         if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
485                 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
486         }
487
488         if ($b['verb'] == ACTIVITY_LIKE) {
489                 logger("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), LOGGER_DEBUG);
490                 if ($b['deleted']) {
491                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
492                 } else {
493                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
494                 }
495
496                 return;
497         }
498
499         if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
500                 return;
501         }
502
503         // if post comes from twitter don't send it back
504         if ($b['extid'] == NETWORK_TWITTER) {
505                 return;
506         }
507
508         if ($b['app'] == "Twitter") {
509                 return;
510         }
511
512         logger('twitter post invoked');
513
514         PConfig::load($b['uid'], 'twitter');
515
516         $ckey    = Config::get('twitter', 'consumerkey');
517         $csecret = Config::get('twitter', 'consumersecret');
518         $otoken  = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
519         $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
520
521         if ($ckey && $csecret && $otoken && $osecret) {
522                 logger('twitter: we have customer key and oauth stuff, going to send.', LOGGER_DEBUG);
523
524                 // If it's a repeated message from twitter then do a native retweet and exit
525                 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
526                         return;
527                 }
528
529                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
530
531                 // Set the timeout for upload to 30 seconds
532                 $connection->setTimeouts(10, 30);
533
534                 $max_char = 280;
535                 $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
536                 $msg = $msgarr["text"];
537
538                 if (($msg == "") && isset($msgarr["title"])) {
539                         $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
540                 }
541
542                 $image = "";
543
544                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
545                         $msg .= "\n" . $msgarr["url"];
546                         $url_added = true;
547                 } else {
548                         $url_added = false;
549                 }
550
551                 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
552                         $image = $msgarr["image"];
553                 }
554
555                 if (empty($msg)) {
556                         return;
557                 }
558
559                 // and now tweet it :-)
560                 $post = [];
561
562                 if (!empty($image)) {
563                         try {
564                                 $img_str = Network::fetchUrl($image);
565
566                                 $tempfile = tempnam(get_temppath(), 'cache');
567                                 file_put_contents($tempfile, $img_str);
568
569                                 $media = $connection->upload('media/upload', ['media' => $tempfile]);
570
571                                 unlink($tempfile);
572
573                                 $post['media_ids'] = $media->media_id_string;
574                         } catch (Exception $e) {
575                                 logger('Exception when trying to send to Twitter: ' . $e->getMessage());
576
577                                 // Workaround: Remove the picture link so that the post can be reposted without it
578                                 // When there is another url already added, a second url would be superfluous.
579                                 if (!$url_added) {
580                                         $msg .= "\n" . $image;
581                                 }
582
583                                 $image = "";
584                         }
585                 }
586
587                 $post['status'] = $msg;
588
589                 if ($iscomment) {
590                         $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
591                 }
592
593                 $url = 'statuses/update';
594                 $result = $connection->post($url, $post);
595                 logger('twitter_post send, result: ' . print_r($result, true), LOGGER_DEBUG);
596
597                 if ($result->source) {
598                         Config::set("twitter", "application_name", strip_tags($result->source));
599                 }
600
601                 if ($result->errors) {
602                         logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
603
604                         $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
605                         if (count($r)) {
606                                 $a->contact = $r[0]["id"];
607                         }
608
609                         $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
610
611                         Queue::add($a->contact, NETWORK_TWITTER, $s);
612                         notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
613                 } elseif ($iscomment) {
614                         logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
615                         Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
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, $picture)
1145 {
1146         $plain = $body;
1147
1148         $tags_arr = [];
1149
1150         foreach ($item->entities->hashtags AS $hashtag) {
1151                 $url = "#[url=" . $a->get_baseurl() . "/search?tag=" . rawurlencode($hashtag->text) . "]" . $hashtag->text . "[/url]";
1152                 $tags_arr["#" . $hashtag->text] = $url;
1153                 $body = str_replace("#" . $hashtag->text, $url, $body);
1154         }
1155
1156         foreach ($item->entities->user_mentions AS $mention) {
1157                 $url = "@[url=https://twitter.com/" . rawurlencode($mention->screen_name) . "]" . $mention->screen_name . "[/url]";
1158                 $tags_arr["@" . $mention->screen_name] = $url;
1159                 $body = str_replace("@" . $mention->screen_name, $url, $body);
1160         }
1161
1162         if (isset($item->entities->urls)) {
1163                 $type = "";
1164                 $footerurl = "";
1165                 $footerlink = "";
1166                 $footer = "";
1167
1168                 foreach ($item->entities->urls as $url) {
1169                         $plain = str_replace($url->url, '', $plain);
1170
1171                         if ($url->url && $url->expanded_url && $url->display_url) {
1172                                 $expanded_url = Network::finalUrl($url->expanded_url);
1173
1174                                 $oembed_data = OEmbed::fetchURL($expanded_url);
1175
1176                                 // Quickfix: Workaround for URL with "[" and "]" in it
1177                                 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1178                                         $expanded_url = $url->url;
1179                                 }
1180
1181                                 if ($type == "") {
1182                                         $type = $oembed_data->type;
1183                                 }
1184
1185                                 if ($oembed_data->type == "video") {
1186                                         //$body = str_replace($url->url,
1187                                         //              "[video]".$expanded_url."[/video]", $body);
1188                                         //$dontincludemedia = true;
1189                                         $type = $oembed_data->type;
1190                                         $footerurl = $expanded_url;
1191                                         $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1192
1193                                         $body = str_replace($url->url, $footerlink, $body);
1194                                         //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1195                                 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1196                                         $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1197                                         //$dontincludemedia = true;
1198                                 } elseif ($oembed_data->type != "link") {
1199                                         $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1200                                 } else {
1201                                         $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1202
1203                                         $tempfile = tempnam(get_temppath(), "cache");
1204                                         file_put_contents($tempfile, $img_str);
1205                                         $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1206                                         unlink($tempfile);
1207
1208                                         if (substr($mime, 0, 6) == "image/") {
1209                                                 $type = "photo";
1210                                                 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1211                                                 //$dontincludemedia = true;
1212                                         } else {
1213                                                 $type = $oembed_data->type;
1214                                                 $footerurl = $expanded_url;
1215                                                 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1216
1217                                                 $body = str_replace($url->url, $footerlink, $body);
1218                                         }
1219                                 }
1220                         }
1221                 }
1222
1223                 if ($footerurl != "") {
1224                         $footer = add_page_info($footerurl, false, $picture);
1225                 }
1226
1227                 if (($footerlink != "") && (trim($footer) != "")) {
1228                         $removedlink = trim(str_replace($footerlink, "", $body));
1229
1230                         if (($removedlink == "") || strstr($body, $removedlink)) {
1231                                 $body = $removedlink;
1232                         }
1233
1234                         $body .= $footer;
1235                 }
1236
1237                 if (($footer == "") && ($picture != "")) {
1238                         $body .= "\n\n[img]" . $picture . "[/img]\n";
1239                 } elseif (($footer == "") && ($picture == "")) {
1240                         $body = add_page_info_to_body($body);
1241                 }
1242         }
1243
1244         // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1245         $tags = get_tags($body);
1246
1247         if (count($tags)) {
1248                 foreach ($tags as $tag) {
1249                         if (strstr(trim($tag), " ")) {
1250                                 continue;
1251                         }
1252
1253                         if (strpos($tag, '#') === 0) {
1254                                 if (strpos($tag, '[url=')) {
1255                                         continue;
1256                                 }
1257
1258                                 // don't link tags that are already embedded in links
1259                                 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1260                                         continue;
1261                                 }
1262                                 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1263                                         continue;
1264                                 }
1265
1266                                 $basetag = str_replace('_', ' ', substr($tag, 1));
1267                                 $url = '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1268                                 $body = str_replace($tag, $url, $body);
1269                                 $tags_arr["#" . $basetag] = $url;
1270                         } elseif (strpos($tag, '@') === 0) {
1271                                 if (strpos($tag, '[url=')) {
1272                                         continue;
1273                                 }
1274
1275                                 $basetag = substr($tag, 1);
1276                                 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1277                                 $body = str_replace($tag, $url, $body);
1278                                 $tags_arr["@" . $basetag] = $url;
1279                         }
1280                 }
1281         }
1282
1283         $tags = implode($tags_arr, ",");
1284
1285         return ["body" => $body, "tags" => $tags, "plain" => $plain];
1286 }
1287
1288 /**
1289  * @brief Fetch media entities and add media links to the body
1290  *
1291  * @param object $post Twitter object with the post
1292  * @param array $postarray Array of the item that is about to be posted
1293  *
1294  * @return $picture string Image URL or empty string
1295  */
1296 function twitter_media_entities($post, &$postarray)
1297 {
1298         // There are no media entities? So we quit.
1299         if (!is_array($post->extended_entities->media)) {
1300                 return "";
1301         }
1302
1303         // When the post links to an external page, we only take one picture.
1304         // We only do this when there is exactly one media.
1305         if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1306                 $picture = "";
1307                 foreach ($post->extended_entities->media AS $medium) {
1308                         if (isset($medium->media_url_https)) {
1309                                 $picture = $medium->media_url_https;
1310                                 $postarray['body'] = str_replace($medium->url, "", $postarray['body']);
1311                         }
1312                 }
1313                 return $picture;
1314         }
1315
1316         // This is a pure media post, first search for all media urls
1317         $media = [];
1318         foreach ($post->extended_entities->media AS $medium) {
1319                 switch ($medium->type) {
1320                         case 'photo':
1321                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1322                                 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1323                                 break;
1324                         case 'video':
1325                         case 'animated_gif':
1326                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1327                                 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1328                                 if (is_array($medium->video_info->variants)) {
1329                                         $bitrate = 0;
1330                                         // We take the video with the highest bitrate
1331                                         foreach ($medium->video_info->variants AS $variant) {
1332                                                 if (($variant->content_type == "video/mp4") && ($variant->bitrate >= $bitrate)) {
1333                                                         $media[$medium->url] = "\n[video]" . $variant->url . "[/video]";
1334                                                         $bitrate = $variant->bitrate;
1335                                                 }
1336                                         }
1337                                 }
1338                                 break;
1339                         // The following code will only be activated for test reasons
1340                         //default:
1341                         //      $postarray['body'] .= print_r($medium, true);
1342                 }
1343         }
1344
1345         // Now we replace the media urls.
1346         foreach ($media AS $key => $value) {
1347                 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1348         }
1349         return "";
1350 }
1351
1352 function twitter_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact, $noquote)
1353 {
1354         $postarray = [];
1355         $postarray['network'] = NETWORK_TWITTER;
1356         $postarray['gravity'] = 0;
1357         $postarray['uid'] = $uid;
1358         $postarray['wall'] = 0;
1359         $postarray['uri'] = "twitter::" . $post->id_str;
1360         $postarray['object'] = json_encode($post);
1361
1362         // Don't import our own comments
1363         $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1364                 dbesc($postarray['uri']),
1365                 intval($uid)
1366         );
1367
1368         if (count($r)) {
1369                 logger("Item with extid " . $postarray['uri'] . " found.", LOGGER_DEBUG);
1370                 return [];
1371         }
1372
1373         $contactid = 0;
1374
1375         if ($post->in_reply_to_status_id_str != "") {
1376                 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1377
1378                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1379                         dbesc($parent),
1380                         intval($uid)
1381                 );
1382                 if (count($r)) {
1383                         $postarray['thr-parent'] = $r[0]["uri"];
1384                         $postarray['parent-uri'] = $r[0]["parent-uri"];
1385                         $postarray['parent'] = $r[0]["parent"];
1386                         $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1387                 } else {
1388                         $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1389                                 dbesc($parent),
1390                                 intval($uid)
1391                         );
1392                         if (count($r)) {
1393                                 $postarray['thr-parent'] = $r[0]['uri'];
1394                                 $postarray['parent-uri'] = $r[0]['parent-uri'];
1395                                 $postarray['parent'] = $r[0]['parent'];
1396                                 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1397                         } else {
1398                                 $postarray['thr-parent'] = $postarray['uri'];
1399                                 $postarray['parent-uri'] = $postarray['uri'];
1400                                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1401                         }
1402                 }
1403
1404                 // Is it me?
1405                 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1406
1407                 if ($post->user->id_str == $own_id) {
1408                         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1409                                 intval($uid));
1410
1411                         if (count($r)) {
1412                                 $contactid = $r[0]["id"];
1413
1414                                 $postarray['owner-name']   = $r[0]["name"];
1415                                 $postarray['owner-link']   = $r[0]["url"];
1416                                 $postarray['owner-avatar'] = $r[0]["photo"];
1417                         } else {
1418                                 logger("No self contact for user " . $uid, LOGGER_DEBUG);
1419                                 return [];
1420                         }
1421                 }
1422                 // Don't create accounts of people who just comment something
1423                 $create_user = false;
1424         } else {
1425                 $postarray['parent-uri'] = $postarray['uri'];
1426                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1427         }
1428
1429         if ($contactid == 0) {
1430                 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1431
1432                 $postarray['owner-name'] = $post->user->name;
1433                 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1434                 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1435         }
1436
1437         if (($contactid == 0) && !$only_existing_contact) {
1438                 $contactid = $self['id'];
1439         } elseif ($contactid <= 0) {
1440                 logger("Contact ID is zero or less than zero.", LOGGER_DEBUG);
1441                 return [];
1442         }
1443
1444         $postarray['contact-id'] = $contactid;
1445
1446         $postarray['verb'] = ACTIVITY_POST;
1447         $postarray['author-name'] = $postarray['owner-name'];
1448         $postarray['author-link'] = $postarray['owner-link'];
1449         $postarray['author-avatar'] = $postarray['owner-avatar'];
1450         $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1451         $postarray['app'] = strip_tags($post->source);
1452
1453         if ($post->user->protected) {
1454                 $postarray['private'] = 1;
1455                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1456         }
1457
1458         if (is_string($post->full_text)) {
1459                 $postarray['body'] = $post->full_text;
1460         } else {
1461                 $postarray['body'] = $post->text;
1462         }
1463
1464         // When the post contains links then use the correct object type
1465         if (count($post->entities->urls) > 0) {
1466                 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1467         }
1468
1469         // Search for media links
1470         $picture = twitter_media_entities($post, $postarray);
1471
1472         $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1473         $postarray['body'] = $converted["body"];
1474         $postarray['tag'] = $converted["tags"];
1475         $postarray['created'] = DateTimeFormat::utc($post->created_at);
1476         $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1477
1478         $statustext = $converted["plain"];
1479
1480         if (is_string($post->place->name)) {
1481                 $postarray["location"] = $post->place->name;
1482         }
1483         if (is_string($post->place->full_name)) {
1484                 $postarray["location"] = $post->place->full_name;
1485         }
1486         if (is_array($post->geo->coordinates)) {
1487                 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1488         }
1489         if (is_array($post->coordinates->coordinates)) {
1490                 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1491         }
1492         if (is_object($post->retweeted_status)) {
1493                 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1494
1495                 $retweet['object'] = $postarray['object'];
1496                 $retweet['private'] = $postarray['private'];
1497                 $retweet['allow_cid'] = $postarray['allow_cid'];
1498                 $retweet['contact-id'] = $postarray['contact-id'];
1499                 $retweet['owner-name'] = $postarray['owner-name'];
1500                 $retweet['owner-link'] = $postarray['owner-link'];
1501                 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1502
1503                 $postarray = $retweet;
1504         }
1505
1506         if (is_object($post->quoted_status) && !$noquote) {
1507                 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1508
1509                 $postarray['body'] = $statustext;
1510
1511                 $postarray['body'] .= "\n" . share_header(
1512                         $quoted['author-name'],
1513                         $quoted['author-link'],
1514                         $quoted['author-avatar'],
1515                         "",
1516                         $quoted['created'],
1517                         $quoted['plink']
1518                 );
1519
1520                 $postarray['body'] .= $quoted['body'] . '[/share]';
1521         }
1522
1523         return $postarray;
1524 }
1525
1526 function twitter_checknotification(App $a, $uid, $own_id, $top_item, $postarray)
1527 {
1528         /// TODO: this whole function doesn't seem to work. Needs complete check
1529         $user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` LIMIT 1",
1530                 intval($uid)
1531         );
1532
1533         if (!count($user)) {
1534                 return;
1535         }
1536
1537         // Is it me?
1538         if (link_compare($user[0]["url"], $postarray['author-link'])) {
1539                 return;
1540         }
1541
1542         $own_user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1543                 intval($uid),
1544                 dbesc("twitter::".$own_id)
1545         );
1546
1547         if (!count($own_user)) {
1548                 return;
1549         }
1550
1551         // Is it me from twitter?
1552         if (link_compare($own_user[0]["url"], $postarray['author-link'])) {
1553                 return;
1554         }
1555
1556         $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 AND `deleted` = 0",
1557                 dbesc($postarray['parent-uri']),
1558                 intval($uid)
1559         );
1560
1561         if (count($myconv)) {
1562                 foreach ($myconv as $conv) {
1563                         // now if we find a match, it means we're in this conversation
1564                         if (!link_compare($conv['author-link'], $user[0]["url"]) && !link_compare($conv['author-link'], $own_user[0]["url"])) {
1565                                 continue;
1566                         }
1567
1568                         require_once 'include/enotify.php';
1569
1570                         $conv_parent = $conv['parent'];
1571
1572                         notification([
1573                                 'type' => NOTIFY_COMMENT,
1574                                 'notify_flags' => $user[0]['notify-flags'],
1575                                 'language' => $user[0]['language'],
1576                                 'to_name' => $user[0]['username'],
1577                                 'to_email' => $user[0]['email'],
1578                                 'uid' => $user[0]['uid'],
1579                                 'item' => $postarray,
1580                                 'link' => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($top_item)),
1581                                 'source_name' => $postarray['author-name'],
1582                                 'source_link' => $postarray['author-link'],
1583                                 'source_photo' => $postarray['author-avatar'],
1584                                 'verb' => ACTIVITY_POST,
1585                                 'otype' => 'item',
1586                                 'parent' => $conv_parent,
1587                         ]);
1588
1589                         // only send one notification
1590                         break;
1591                 }
1592         }
1593 }
1594
1595 function twitter_fetchparentposts(App $a, $uid, $post, $connection, $self, $own_id)
1596 {
1597         logger("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, LOGGER_DEBUG);
1598
1599         $posts = [];
1600
1601         while ($post->in_reply_to_status_id_str != "") {
1602                 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1603
1604                 $post = $connection->get('statuses/show', $parameters);
1605
1606                 if (!count($post)) {
1607                         logger("twitter_fetchparentposts: Can't fetch post " . $parameters->id, LOGGER_DEBUG);
1608                         break;
1609                 }
1610
1611                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1612                         dbesc("twitter::".$post->id_str),
1613                         intval($uid)
1614                 );
1615
1616                 if (count($r)) {
1617                         break;
1618                 }
1619
1620                 $posts[] = $post;
1621         }
1622
1623         logger("twitter_fetchparentposts: Fetching " . count($posts) . " parents", LOGGER_DEBUG);
1624
1625         $posts = array_reverse($posts);
1626
1627         if (count($posts)) {
1628                 foreach ($posts as $post) {
1629                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1630
1631                         if (trim($postarray['body']) == "") {
1632                                 continue;
1633                         }
1634
1635                         $item = Item::insert($postarray);
1636                         $postarray["id"] = $item;
1637
1638                         logger('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1639
1640                         if ($item && !function_exists("check_item_notification")) {
1641                                 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1642                         }
1643                 }
1644         }
1645 }
1646
1647 function twitter_fetchhometimeline(App $a, $uid)
1648 {
1649         $ckey    = Config::get('twitter', 'consumerkey');
1650         $csecret = Config::get('twitter', 'consumersecret');
1651         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1652         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1653         $create_user = PConfig::get($uid, 'twitter', 'create_user');
1654         $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1655
1656         logger("twitter_fetchhometimeline: Fetching for user " . $uid, LOGGER_DEBUG);
1657
1658         $application_name = Config::get('twitter', 'application_name');
1659
1660         if ($application_name == "") {
1661                 $application_name = $a->get_hostname();
1662         }
1663
1664         require_once 'include/items.php';
1665
1666         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1667
1668         $own_contact = twitter_fetch_own_contact($a, $uid);
1669
1670         $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1671                 intval($own_contact),
1672                 intval($uid));
1673
1674         if (count($r)) {
1675                 $own_id = $r[0]["nick"];
1676         } else {
1677                 logger("twitter_fetchhometimeline: Own twitter contact not found for user " . $uid, LOGGER_DEBUG);
1678                 return;
1679         }
1680
1681         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1682                 intval($uid));
1683
1684         if (count($r)) {
1685                 $self = $r[0];
1686         } else {
1687                 logger("twitter_fetchhometimeline: Own contact not found for user " . $uid, LOGGER_DEBUG);
1688                 return;
1689         }
1690
1691         $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1692                 intval($uid));
1693         if (!count($u)) {
1694                 logger("twitter_fetchhometimeline: Own user not found for user " . $uid, LOGGER_DEBUG);
1695                 return;
1696         }
1697
1698         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1699         //$parameters["count"] = 200;
1700         // Fetching timeline
1701         $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1702
1703         $first_time = ($lastid == "");
1704
1705         if ($lastid != "") {
1706                 $parameters["since_id"] = $lastid;
1707         }
1708
1709         $items = $connection->get('statuses/home_timeline', $parameters);
1710
1711         if (!is_array($items)) {
1712                 logger("twitter_fetchhometimeline: Error fetching home timeline: " . print_r($items, true), LOGGER_DEBUG);
1713                 return;
1714         }
1715
1716         $posts = array_reverse($items);
1717
1718         logger("twitter_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1719
1720         if (count($posts)) {
1721                 foreach ($posts as $post) {
1722                         if ($post->id_str > $lastid) {
1723                                 $lastid = $post->id_str;
1724                                 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1725                         }
1726
1727                         if ($first_time) {
1728                                 continue;
1729                         }
1730
1731                         if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1732                                 logger("twitter_fetchhometimeline: Skip previously sended post", LOGGER_DEBUG);
1733                                 continue;
1734                         }
1735
1736                         if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1737                                 logger("twitter_fetchhometimeline: Skip post that will be mirrored", LOGGER_DEBUG);
1738                                 continue;
1739                         }
1740
1741                         if ($post->in_reply_to_status_id_str != "") {
1742                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1743                         }
1744
1745                         $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1746
1747                         if (trim($postarray['body']) == "") {
1748                                 continue;
1749                         }
1750
1751                         $item = Item::insert($postarray);
1752                         $postarray["id"] = $item;
1753
1754                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1755
1756                         if ($item && !function_exists("check_item_notification")) {
1757                                 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1758                         }
1759                 }
1760         }
1761         PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1762
1763         // Fetching mentions
1764         $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1765
1766         $first_time = ($lastid == "");
1767
1768         if ($lastid != "") {
1769                 $parameters["since_id"] = $lastid;
1770         }
1771
1772         $items = $connection->get('statuses/mentions_timeline', $parameters);
1773
1774         if (!is_array($items)) {
1775                 logger("twitter_fetchhometimeline: Error fetching mentions: " . print_r($items, true), LOGGER_DEBUG);
1776                 return;
1777         }
1778
1779         $posts = array_reverse($items);
1780
1781         logger("twitter_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1782
1783         if (count($posts)) {
1784                 foreach ($posts as $post) {
1785                         if ($post->id_str > $lastid) {
1786                                 $lastid = $post->id_str;
1787                         }
1788
1789                         if ($first_time) {
1790                                 continue;
1791                         }
1792
1793                         if ($post->in_reply_to_status_id_str != "") {
1794                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1795                         }
1796
1797                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1798
1799                         if (trim($postarray['body']) == "") {
1800                                 continue;
1801                         }
1802
1803                         $item = Item::insert($postarray);
1804                         $postarray["id"] = $item;
1805
1806                         if ($item && function_exists("check_item_notification")) {
1807                                 check_item_notification($item, $uid, NOTIFY_TAGSELF);
1808                         }
1809
1810                         if (!isset($postarray["parent"]) || ($postarray["parent"] == 0)) {
1811                                 $postarray["parent"] = $item;
1812                         }
1813
1814                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1815
1816                         if ($item == 0) {
1817                                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1818                                         dbesc($postarray['uri']),
1819                                         intval($uid)
1820                                 );
1821                                 if (count($r)) {
1822                                         $item = $r[0]['id'];
1823                                         $parent_id = $r[0]['parent'];
1824                                 }
1825                         } else {
1826                                 $parent_id = $postarray['parent'];
1827                         }
1828
1829                         if (($item != 0) && !function_exists("check_item_notification")) {
1830                                 require_once 'include/enotify.php';
1831                                 notification([
1832                                         'type'         => NOTIFY_TAGSELF,
1833                                         'notify_flags' => $u[0]['notify-flags'],
1834                                         'language'     => $u[0]['language'],
1835                                         'to_name'      => $u[0]['username'],
1836                                         'to_email'     => $u[0]['email'],
1837                                         'uid'          => $u[0]['uid'],
1838                                         'item'         => $postarray,
1839                                         'link'         => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($item)),
1840                                         'source_name'  => $postarray['author-name'],
1841                                         'source_link'  => $postarray['author-link'],
1842                                         'source_photo' => $postarray['author-avatar'],
1843                                         'verb'         => ACTIVITY_TAG,
1844                                         'otype'        => 'item',
1845                                         'parent'       => $parent_id
1846                                 ]);
1847                         }
1848                 }
1849         }
1850
1851         PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1852 }
1853
1854 function twitter_fetch_own_contact(App $a, $uid)
1855 {
1856         $ckey    = Config::get('twitter', 'consumerkey');
1857         $csecret = Config::get('twitter', 'consumersecret');
1858         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1859         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1860
1861         $own_id = PConfig::get($uid, 'twitter', 'own_id');
1862
1863         $contact_id = 0;
1864
1865         if ($own_id == "") {
1866                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1867
1868                 // Fetching user data
1869                 $user = $connection->get('account/verify_credentials');
1870
1871                 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1872
1873                 $contact_id = twitter_fetch_contact($uid, $user, true);
1874         } else {
1875                 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1876                         intval($uid),
1877                         dbesc("twitter::" . $own_id));
1878                 if (count($r)) {
1879                         $contact_id = $r[0]["id"];
1880                 } else {
1881                         PConfig::delete($uid, 'twitter', 'own_id');
1882                 }
1883         }
1884
1885         return $contact_id;
1886 }
1887
1888 function twitter_is_retweet(App $a, $uid, $body)
1889 {
1890         $body = trim($body);
1891
1892         // Skip if it isn't a pure repeated messages
1893         // Does it start with a share?
1894         if (strpos($body, "[share") > 0) {
1895                 return false;
1896         }
1897
1898         // Does it end with a share?
1899         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1900                 return false;
1901         }
1902
1903         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1904         // Skip if there is no shared message in there
1905         if ($body == $attributes) {
1906                 return false;
1907         }
1908
1909         $link = "";
1910         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1911         if ($matches[1] != "") {
1912                 $link = $matches[1];
1913         }
1914
1915         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1916         if ($matches[1] != "") {
1917                 $link = $matches[1];
1918         }
1919
1920         $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1921         if ($id == $link) {
1922                 return false;
1923         }
1924
1925         logger('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, LOGGER_DEBUG);
1926
1927         $ckey    = Config::get('twitter', 'consumerkey');
1928         $csecret = Config::get('twitter', 'consumersecret');
1929         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1930         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1931
1932         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1933         $result = $connection->post('statuses/retweet/' . $id);
1934
1935         logger('twitter_is_retweet: result ' . print_r($result, true), LOGGER_DEBUG);
1936
1937         return !isset($result->errors);
1938 }