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