Some notices had been removed / expiring of item related tables in twitter (#643)
[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                         $result = [];
426                         break;
427                 case "like":
428                         $result = $connection->post('favorites/create', $post);
429                         break;
430                 case "unlike":
431                         $result = $connection->post('favorites/destroy', $post);
432                         break;
433                 default:
434                         logger('Unhandled action ' . $action, LOGGER_DEBUG);
435                         $result = [];
436         }
437         logger("twitter_action '" . $action . "' send, result: " . print_r($result, true), LOGGER_DEBUG);
438 }
439
440 function twitter_post_hook(App $a, &$b)
441 {
442         // Post to Twitter
443         if (!PConfig::get($b["uid"], 'twitter', 'import')
444                 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
445                 return;
446         }
447
448         if ($b['parent'] != $b['id']) {
449                 logger("twitter_post_hook: parameter " . print_r($b, true), LOGGER_DATA);
450
451                 // Looking if its a reply to a twitter post
452                 if ((substr($b["parent-uri"], 0, 9) != "twitter::")
453                         && (substr($b["extid"], 0, 9) != "twitter::")
454                         && (substr($b["thr-parent"], 0, 9) != "twitter::"))
455                 {
456                         logger("twitter_post_hook: no twitter post " . $b["parent"]);
457                         return;
458                 }
459
460                 $condition = ['uri' => $b["thr-parent"], 'uid' => $b["uid"]];
461                 $orig_post = Item::selectFirst([], $condition);
462                 if (!DBM::is_result($orig_post)) {
463                         logger("twitter_post_hook: no parent found " . $b["thr-parent"]);
464                         return;
465                 } else {
466                         $iscomment = true;
467                 }
468
469
470                 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
471                 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
472                 $nicknameplain = "@" . $nicknameplain;
473
474                 logger("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], LOGGER_DEBUG);
475                 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
476                         $b["body"] = $nickname . " " . $b["body"];
477                 }
478
479                 logger("twitter_post_hook: parent found " . print_r($orig_post, true), LOGGER_DATA);
480         } else {
481                 $iscomment = false;
482
483                 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
484                         return;
485                 }
486
487                 // Dont't post if the post doesn't belong to us.
488                 // This is a check for forum postings
489                 $self = dba::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
490                 if ($b['contact-id'] != $self['id']) {
491                         return;
492                 }
493         }
494
495         if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
496                 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
497         }
498
499         if ($b['verb'] == ACTIVITY_LIKE) {
500                 logger("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), LOGGER_DEBUG);
501                 if ($b['deleted']) {
502                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
503                 } else {
504                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
505                 }
506
507                 return;
508         }
509
510         if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
511                 return;
512         }
513
514         // if post comes from twitter don't send it back
515         if ($b['extid'] == NETWORK_TWITTER) {
516                 return;
517         }
518
519         if ($b['app'] == "Twitter") {
520                 return;
521         }
522
523         logger('twitter post invoked');
524
525         PConfig::load($b['uid'], 'twitter');
526
527         $ckey    = Config::get('twitter', 'consumerkey');
528         $csecret = Config::get('twitter', 'consumersecret');
529         $otoken  = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
530         $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
531
532         if ($ckey && $csecret && $otoken && $osecret) {
533                 logger('twitter: we have customer key and oauth stuff, going to send.', LOGGER_DEBUG);
534
535                 // If it's a repeated message from twitter then do a native retweet and exit
536                 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
537                         return;
538                 }
539
540                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
541
542                 // Set the timeout for upload to 30 seconds
543                 $connection->setTimeouts(10, 30);
544
545                 $max_char = 280;
546                 $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, 8);
547                 $msg = $msgarr["text"];
548
549                 if (($msg == "") && isset($msgarr["title"])) {
550                         $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
551                 }
552
553                 $image = "";
554
555                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
556                         $msg .= "\n" . $msgarr["url"];
557                         $url_added = true;
558                 } else {
559                         $url_added = false;
560                 }
561
562                 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
563                         $image = $msgarr["image"];
564                 }
565
566                 if (empty($msg)) {
567                         return;
568                 }
569
570                 // and now tweet it :-)
571                 $post = [];
572
573                 if (!empty($image)) {
574                         try {
575                                 $img_str = Network::fetchUrl($image);
576
577                                 $tempfile = tempnam(get_temppath(), 'cache');
578                                 file_put_contents($tempfile, $img_str);
579
580                                 $media = $connection->upload('media/upload', ['media' => $tempfile]);
581
582                                 unlink($tempfile);
583
584                                 $post['media_ids'] = $media->media_id_string;
585                         } catch (Exception $e) {
586                                 logger('Exception when trying to send to Twitter: ' . $e->getMessage());
587
588                                 // Workaround: Remove the picture link so that the post can be reposted without it
589                                 // When there is another url already added, a second url would be superfluous.
590                                 if (!$url_added) {
591                                         $msg .= "\n" . $image;
592                                 }
593
594                                 $image = "";
595                         }
596                 }
597
598                 $post['status'] = $msg;
599
600                 if ($iscomment) {
601                         $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
602                 }
603
604                 $url = 'statuses/update';
605                 $result = $connection->post($url, $post);
606                 logger('twitter_post send, result: ' . print_r($result, true), LOGGER_DEBUG);
607
608                 if (!empty($result->source)) {
609                         Config::set("twitter", "application_name", strip_tags($result->source));
610                 }
611
612                 if (!empty($result->errors)) {
613                         logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
614
615                         $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
616                         if (DBM::is_result($r)) {
617                                 $a->contact = $r[0]["id"];
618                         }
619
620                         $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
621
622                         Queue::add($a->contact, NETWORK_TWITTER, $s);
623                         notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
624                 } elseif ($iscomment) {
625                         logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
626                         Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
627                 }
628         }
629 }
630
631 function twitter_addon_admin_post(App $a)
632 {
633         $consumerkey    = x($_POST, 'consumerkey')    ? notags(trim($_POST['consumerkey']))    : '';
634         $consumersecret = x($_POST, 'consumersecret') ? notags(trim($_POST['consumersecret'])) : '';
635         Config::set('twitter', 'consumerkey', $consumerkey);
636         Config::set('twitter', 'consumersecret', $consumersecret);
637         info(L10n::t('Settings updated.') . EOL);
638 }
639
640 function twitter_addon_admin(App $a, &$o)
641 {
642         $t = get_markup_template("admin.tpl", "addon/twitter/");
643
644         $o = replace_macros($t, [
645                 '$submit' => L10n::t('Save Settings'),
646                 // name, label, value, help, [extra values]
647                 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
648                 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
649         ]);
650 }
651
652 function twitter_cron(App $a, $b)
653 {
654         $last = Config::get('twitter', 'last_poll');
655
656         $poll_interval = intval(Config::get('twitter', 'poll_interval'));
657         if (!$poll_interval) {
658                 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
659         }
660
661         if ($last) {
662                 $next = $last + ($poll_interval * 60);
663                 if ($next > time()) {
664                         logger('twitter: poll intervall not reached');
665                         return;
666                 }
667         }
668         logger('twitter: cron_start');
669
670         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
671         if (DBM::is_result($r)) {
672                 foreach ($r as $rr) {
673                         logger('twitter: fetching for user ' . $rr['uid']);
674                         Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
675                 }
676         }
677
678         $abandon_days = intval(Config::get('system', 'account_abandon_days'));
679         if ($abandon_days < 1) {
680                 $abandon_days = 0;
681         }
682
683         $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
684
685         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
686         if (DBM::is_result($r)) {
687                 foreach ($r as $rr) {
688                         if ($abandon_days != 0) {
689                                 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
690                                 if (!DBM::is_result($user)) {
691                                         logger('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
692                                         continue;
693                                 }
694                         }
695
696                         logger('twitter: importing timeline from user ' . $rr['uid']);
697                         Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
698                         /*
699                           // To-Do
700                           // check for new contacts once a day
701                           $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
702                           if($last_contact_check)
703                           $next_contact_check = $last_contact_check + 86400;
704                           else
705                           $next_contact_check = 0;
706
707                           if($next_contact_check <= time()) {
708                           pumpio_getallusers($a, $rr["uid"]);
709                           PConfig::set($rr['uid'],'pumpio','contact_check',time());
710                           }
711                          */
712                 }
713         }
714
715         logger('twitter: cron_end');
716
717         Config::set('twitter', 'last_poll', time());
718 }
719
720 function twitter_expire(App $a, $b)
721 {
722         $days = Config::get('twitter', 'expire');
723
724         if ($days == 0) {
725                 return;
726         }
727
728         $r = dba::select('item', ['id', 'iaid', 'icid'], ['deleted' => true, 'network' => NETWORK_TWITTER]);
729         while ($row = dba::fetch($r)) {
730                 dba::delete('item', ['id' => $row['id']]);
731                 if (!empty($row['iaid']) && !dba::exists('item', ['iaid' => $row['iaid']])) {
732                         dba::delete('item-activity', ['id' => $row['iaid']]);
733                 }
734                 if (!empty($row['icid']) && !dba::exists('item', ['icid' => $row['icid']])) {
735                         dba::delete('item-content', ['id' => $row['icid']]);
736                 }
737         }
738         dba::close($r);
739
740         require_once "include/items.php";
741
742         logger('twitter_expire: expire_start');
743
744         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
745         if (DBM::is_result($r)) {
746                 foreach ($r as $rr) {
747                         logger('twitter_expire: user ' . $rr['uid']);
748                         Item::expire($rr['uid'], $days, NETWORK_TWITTER, true);
749                 }
750         }
751
752         logger('twitter_expire: expire_end');
753 }
754
755 function twitter_prepare_body(App $a, &$b)
756 {
757         if ($b["item"]["network"] != NETWORK_TWITTER) {
758                 return;
759         }
760
761         if ($b["preview"]) {
762                 $max_char = 280;
763                 $item = $b["item"];
764                 $item["plink"] = $a->get_baseurl() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
765
766                 $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
767                 $orig_post = Item::selectFirst(['author-link'], $condition);
768                 if (DBM::is_result($orig_post)) {
769                         $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
770                         $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
771                         $nicknameplain = "@" . $nicknameplain;
772
773                         if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
774                                 $item["body"] = $nickname . " " . $item["body"];
775                         }
776                 }
777
778                 $msgarr = ItemContent::getPlaintextPost($item, $max_char, true, 8);
779                 $msg = $msgarr["text"];
780
781                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
782                         $msg .= " " . $msgarr["url"];
783                 }
784
785                 if (isset($msgarr["image"])) {
786                         $msg .= " " . $msgarr["image"];
787                 }
788
789                 $b['html'] = nl2br(htmlspecialchars($msg));
790         }
791 }
792
793 /**
794  * @brief Build the item array for the mirrored post
795  *
796  * @param App $a Application class
797  * @param integer $uid User id
798  * @param object $post Twitter object with the post
799  *
800  * @return array item data to be posted
801  */
802 function twitter_do_mirrorpost(App $a, $uid, $post)
803 {
804         $datarray["type"] = "wall";
805         $datarray["api_source"] = true;
806         $datarray["profile_uid"] = $uid;
807         $datarray["extid"] = NETWORK_TWITTER;
808         $datarray['message_id'] = Item::newURI($uid, NETWORK_TWITTER . ":" . $post->id);
809         // $datarray['object'] = json_encode($post); // Activate for debugging
810         $datarray["title"] = "";
811
812         if (!empty($post->retweeted_status)) {
813                 // We don't support nested shares, so we mustn't show quotes as shares on retweets
814                 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
815
816                 $datarray['body'] = "\n" . share_header(
817                         $item['author-name'],
818                         $item['author-link'],
819                         $item['author-avatar'],
820                         "",
821                         $item['created'],
822                         $item['plink']
823                 );
824
825                 $datarray['body'] .= $item['body'] . '[/share]';
826         } else {
827                 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
828
829                 $datarray['body'] = $item['body'];
830         }
831
832         $datarray["source"] = $item['app'];
833         $datarray["verb"] = $item['verb'];
834
835         if (isset($item["location"])) {
836                 $datarray["location"] = $item["location"];
837         }
838
839         if (isset($item["coord"])) {
840                 $datarray["coord"] = $item["coord"];
841         }
842
843         return $datarray;
844 }
845
846 function twitter_fetchtimeline(App $a, $uid)
847 {
848         $ckey    = Config::get('twitter', 'consumerkey');
849         $csecret = Config::get('twitter', 'consumersecret');
850         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
851         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
852         $lastid  = PConfig::get($uid, 'twitter', 'lastid');
853
854         $application_name = Config::get('twitter', 'application_name');
855
856         if ($application_name == "") {
857                 $application_name = $a->get_hostname();
858         }
859
860         $has_picture = false;
861
862         require_once 'mod/item.php';
863         require_once 'include/items.php';
864         require_once 'mod/share.php';
865
866         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
867
868         $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
869
870         $first_time = ($lastid == "");
871
872         if ($lastid != "") {
873                 $parameters["since_id"] = $lastid;
874         }
875
876         try {
877                 $items = $connection->get('statuses/user_timeline', $parameters);
878         } catch (TwitterOAuthException $e) {
879                 logger('twitter_fetchtimeline: Error fetching timeline for user ' . $uid . ': ' . $e->getMessage());
880                 return;
881         }
882
883         if (!is_array($items)) {
884                 return;
885         }
886
887         $posts = array_reverse($items);
888
889         if (count($posts)) {
890                 foreach ($posts as $post) {
891                         if ($post->id_str > $lastid) {
892                                 $lastid = $post->id_str;
893                                 PConfig::set($uid, 'twitter', 'lastid', $lastid);
894                         }
895
896                         if ($first_time) {
897                                 continue;
898                         }
899
900                         if (!stristr($post->source, $application_name)) {
901                                 $_SESSION["authenticated"] = true;
902                                 $_SESSION["uid"] = $uid;
903
904                                 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
905
906                                 logger('twitter: posting for user ' . $uid);
907
908                                 item_post($a);
909                         }
910                 }
911         }
912         PConfig::set($uid, 'twitter', 'lastid', $lastid);
913 }
914
915 function twitter_queue_hook(App $a, &$b)
916 {
917         $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
918                 dbesc(NETWORK_TWITTER)
919         );
920         if (!DBM::is_result($qi)) {
921                 return;
922         }
923
924         foreach ($qi as $x) {
925                 if ($x['network'] !== NETWORK_TWITTER) {
926                         continue;
927                 }
928
929                 logger('twitter_queue: run');
930
931                 $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
932                         WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
933                         intval($x['cid'])
934                 );
935                 if (!DBM::is_result($r)) {
936                         continue;
937                 }
938
939                 $user = $r[0];
940
941                 $ckey    = Config::get('twitter', 'consumerkey');
942                 $csecret = Config::get('twitter', 'consumersecret');
943                 $otoken  = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
944                 $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
945
946                 $success = false;
947
948                 if ($ckey && $csecret && $otoken && $osecret) {
949                         logger('twitter_queue: able to post');
950
951                         $z = unserialize($x['content']);
952
953                         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
954                         $result = $connection->post($z['url'], $z['post']);
955
956                         logger('twitter_queue: post result: ' . print_r($result, true), LOGGER_DEBUG);
957
958                         if ($result->errors) {
959                                 logger('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
960                         } else {
961                                 $success = true;
962                                 Queue::removeItem($x['id']);
963                         }
964                 } else {
965                         logger("twitter_queue: Error getting tokens for user " . $user['uid']);
966                 }
967
968                 if (!$success) {
969                         logger('twitter_queue: delayed');
970                         Queue::updateTime($x['id']);
971                 }
972         }
973 }
974
975 function twitter_fix_avatar($avatar)
976 {
977         $new_avatar = str_replace("_normal.", ".", $avatar);
978
979         $info = Image::getInfoFromURL($new_avatar);
980         if (!$info) {
981                 $new_avatar = $avatar;
982         }
983
984         return $new_avatar;
985 }
986
987 function twitter_fetch_contact($uid, $data, $create_user)
988 {
989         if ($data->id_str == "") {
990                 return -1;
991         }
992
993         $avatar = twitter_fix_avatar($data->profile_image_url_https);
994         $url = "https://twitter.com/" . $data->screen_name;
995         $addr = $data->screen_name . "@twitter.com";
996
997         GContact::update(["url" => $url, "network" => NETWORK_TWITTER,
998                 "photo" => $avatar, "hide" => true,
999                 "name" => $data->name, "nick" => $data->screen_name,
1000                 "location" => $data->location, "about" => $data->description,
1001                 "addr" => $addr, "generation" => 2]);
1002
1003         $fields = ['url' => $url, 'network' => NETWORK_TWITTER,
1004                 'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
1005                 'location' => $data->location, 'about' => $data->description];
1006
1007         $cid = Contact::getIdForURL($url, 0, true, $fields);
1008         if (!empty($cid)) {
1009                 dba::update('contact', $fields, ['id' => $cid]);
1010                 Contact::updateAvatar($avatar, 0, $cid);
1011         }
1012
1013         $contact = dba::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
1014         if (!DBM::is_result($contact) && !$create_user) {
1015                 return 0;
1016         }
1017
1018         if (!DBM::is_result($contact)) {
1019                 // create contact record
1020                 $fields['uid'] = $uid;
1021                 $fields['created'] = DateTimeFormat::utcNow();
1022                 $fields['nurl'] = normalise_link($url);
1023                 $fields['alias'] = 'twitter::' . $data->id_str;
1024                 $fields['poll'] = 'twitter::' . $data->id_str;
1025                 $fields['rel'] = CONTACT_IS_FRIEND;
1026                 $fields['priority'] = 1;
1027                 $fields['writable'] = true;
1028                 $fields['blocked'] = false;
1029                 $fields['readonly'] = false;
1030                 $fields['pending'] = false;
1031
1032                 if (!dba::insert('contact', $fields)) {
1033                         return false;
1034                 }
1035
1036                 $contact_id = dba::lastInsertId();
1037
1038                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1039
1040                 Contact::updateAvatar($avatar, $uid, $contact_id);
1041         } else {
1042                 if ($contact["readonly"] || $contact["blocked"]) {
1043                         logger("twitter_fetch_contact: Contact '" . $contact["nick"] . "' is blocked or readonly.", LOGGER_DEBUG);
1044                         return -1;
1045                 }
1046
1047                 $contact_id = $contact['id'];
1048
1049                 // update profile photos once every twelve hours as we have no notification of when they change.
1050                 $update_photo = ($contact['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
1051
1052                 // check that we have all the photos, this has been known to fail on occasion
1053                 if (empty($contact['photo']) || empty($contact['thumb']) || empty($contact['micro']) || $update_photo) {
1054                         logger("twitter_fetch_contact: Updating contact " . $data->screen_name, LOGGER_DEBUG);
1055
1056                         Contact::updateAvatar($avatar, $uid, $contact['id']);
1057
1058                         $fields['name-date'] = DateTimeFormat::utcNow();
1059                         $fields['uri-date'] = DateTimeFormat::utcNow();
1060
1061                         dba::update('contact', $fields, ['id' => $contact['id']]);
1062                 }
1063         }
1064
1065         return $contact_id;
1066 }
1067
1068 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1069 {
1070         $ckey = Config::get('twitter', 'consumerkey');
1071         $csecret = Config::get('twitter', 'consumersecret');
1072         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1073         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1074
1075         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1076                 intval($uid));
1077
1078         if (DBM::is_result($r)) {
1079                 $self = $r[0];
1080         } else {
1081                 return;
1082         }
1083
1084         $parameters = [];
1085
1086         if ($screen_name != "") {
1087                 $parameters["screen_name"] = $screen_name;
1088         }
1089
1090         if ($user_id != "") {
1091                 $parameters["user_id"] = $user_id;
1092         }
1093
1094         // Fetching user data
1095         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1096         try {
1097                 $user = $connection->get('users/show', $parameters);
1098         } catch (TwitterOAuthException $e) {
1099                 logger('twitter_fetchuser: Error fetching user ' . $uid . ': ' . $e->getMessage());
1100                 return;
1101         }
1102
1103         if (!is_object($user)) {
1104                 return;
1105         }
1106
1107         $contact_id = twitter_fetch_contact($uid, $user, true);
1108
1109         return $contact_id;
1110 }
1111
1112 function twitter_expand_entities(App $a, $body, $item, $picture)
1113 {
1114         $plain = $body;
1115
1116         $tags_arr = [];
1117
1118         foreach ($item->entities->hashtags AS $hashtag) {
1119                 $url = "#[url=" . $a->get_baseurl() . "/search?tag=" . rawurlencode($hashtag->text) . "]" . $hashtag->text . "[/url]";
1120                 $tags_arr["#" . $hashtag->text] = $url;
1121                 $body = str_replace("#" . $hashtag->text, $url, $body);
1122         }
1123
1124         foreach ($item->entities->user_mentions AS $mention) {
1125                 $url = "@[url=https://twitter.com/" . rawurlencode($mention->screen_name) . "]" . $mention->screen_name . "[/url]";
1126                 $tags_arr["@" . $mention->screen_name] = $url;
1127                 $body = str_replace("@" . $mention->screen_name, $url, $body);
1128         }
1129
1130         if (isset($item->entities->urls)) {
1131                 $type = "";
1132                 $footerurl = "";
1133                 $footerlink = "";
1134                 $footer = "";
1135
1136                 foreach ($item->entities->urls as $url) {
1137                         $plain = str_replace($url->url, '', $plain);
1138
1139                         if ($url->url && $url->expanded_url && $url->display_url) {
1140                                 $expanded_url = Network::finalUrl($url->expanded_url);
1141
1142                                 $oembed_data = OEmbed::fetchURL($expanded_url);
1143
1144                                 if (empty($oembed_data) || empty($oembed_data->type)) {
1145                                         continue;
1146                                 }
1147
1148                                 // Quickfix: Workaround for URL with "[" and "]" in it
1149                                 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1150                                         $expanded_url = $url->url;
1151                                 }
1152
1153                                 if ($type == "") {
1154                                         $type = $oembed_data->type;
1155                                 }
1156
1157                                 if ($oembed_data->type == "video") {
1158                                         //$body = str_replace($url->url,
1159                                         //              "[video]".$expanded_url."[/video]", $body);
1160                                         //$dontincludemedia = true;
1161                                         $type = $oembed_data->type;
1162                                         $footerurl = $expanded_url;
1163                                         $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1164
1165                                         $body = str_replace($url->url, $footerlink, $body);
1166                                         //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1167                                 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1168                                         $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1169                                         //$dontincludemedia = true;
1170                                 } elseif ($oembed_data->type != "link") {
1171                                         $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1172                                 } else {
1173                                         $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1174
1175                                         $tempfile = tempnam(get_temppath(), "cache");
1176                                         file_put_contents($tempfile, $img_str);
1177                                         $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1178                                         unlink($tempfile);
1179
1180                                         if (substr($mime, 0, 6) == "image/") {
1181                                                 $type = "photo";
1182                                                 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1183                                                 //$dontincludemedia = true;
1184                                         } else {
1185                                                 $type = $oembed_data->type;
1186                                                 $footerurl = $expanded_url;
1187                                                 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1188
1189                                                 $body = str_replace($url->url, $footerlink, $body);
1190                                         }
1191                                 }
1192                         }
1193                 }
1194
1195                 if ($footerurl != "") {
1196                         $footer = add_page_info($footerurl, false, $picture);
1197                 }
1198
1199                 if (($footerlink != "") && (trim($footer) != "")) {
1200                         $removedlink = trim(str_replace($footerlink, "", $body));
1201
1202                         if (($removedlink == "") || strstr($body, $removedlink)) {
1203                                 $body = $removedlink;
1204                         }
1205
1206                         $body .= $footer;
1207                 }
1208
1209                 if (($footer == "") && ($picture != "")) {
1210                         $body .= "\n\n[img]" . $picture . "[/img]\n";
1211                 } elseif (($footer == "") && ($picture == "")) {
1212                         $body = add_page_info_to_body($body);
1213                 }
1214         }
1215
1216         // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1217         $tags = get_tags($body);
1218
1219         if (count($tags)) {
1220                 foreach ($tags as $tag) {
1221                         if (strstr(trim($tag), " ")) {
1222                                 continue;
1223                         }
1224
1225                         if (strpos($tag, '#') === 0) {
1226                                 if (strpos($tag, '[url=')) {
1227                                         continue;
1228                                 }
1229
1230                                 // don't link tags that are already embedded in links
1231                                 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1232                                         continue;
1233                                 }
1234                                 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1235                                         continue;
1236                                 }
1237
1238                                 $basetag = str_replace('_', ' ', substr($tag, 1));
1239                                 $url = '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1240                                 $body = str_replace($tag, $url, $body);
1241                                 $tags_arr["#" . $basetag] = $url;
1242                         } elseif (strpos($tag, '@') === 0) {
1243                                 if (strpos($tag, '[url=')) {
1244                                         continue;
1245                                 }
1246
1247                                 $basetag = substr($tag, 1);
1248                                 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1249                                 $body = str_replace($tag, $url, $body);
1250                                 $tags_arr["@" . $basetag] = $url;
1251                         }
1252                 }
1253         }
1254
1255         $tags = implode($tags_arr, ",");
1256
1257         return ["body" => $body, "tags" => $tags, "plain" => $plain];
1258 }
1259
1260 /**
1261  * @brief Fetch media entities and add media links to the body
1262  *
1263  * @param object $post Twitter object with the post
1264  * @param array $postarray Array of the item that is about to be posted
1265  *
1266  * @return $picture string Image URL or empty string
1267  */
1268 function twitter_media_entities($post, &$postarray)
1269 {
1270         // There are no media entities? So we quit.
1271         if (empty($post->extended_entities->media)) {
1272                 return "";
1273         }
1274
1275         // When the post links to an external page, we only take one picture.
1276         // We only do this when there is exactly one media.
1277         if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1278                 $picture = "";
1279                 foreach ($post->extended_entities->media AS $medium) {
1280                         if (isset($medium->media_url_https)) {
1281                                 $picture = $medium->media_url_https;
1282                                 $postarray['body'] = str_replace($medium->url, "", $postarray['body']);
1283                         }
1284                 }
1285                 return $picture;
1286         }
1287
1288         // This is a pure media post, first search for all media urls
1289         $media = [];
1290         foreach ($post->extended_entities->media AS $medium) {
1291                 if (!isset($media[$medium->url])) {
1292                         $media[$medium->url] = '';
1293                 }
1294                 switch ($medium->type) {
1295                         case 'photo':
1296                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1297                                 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1298                                 break;
1299                         case 'video':
1300                         case 'animated_gif':
1301                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1302                                 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1303                                 if (is_array($medium->video_info->variants)) {
1304                                         $bitrate = 0;
1305                                         // We take the video with the highest bitrate
1306                                         foreach ($medium->video_info->variants AS $variant) {
1307                                                 if (($variant->content_type == "video/mp4") && ($variant->bitrate >= $bitrate)) {
1308                                                         $media[$medium->url] = "\n[video]" . $variant->url . "[/video]";
1309                                                         $bitrate = $variant->bitrate;
1310                                                 }
1311                                         }
1312                                 }
1313                                 break;
1314                         // The following code will only be activated for test reasons
1315                         //default:
1316                         //      $postarray['body'] .= print_r($medium, true);
1317                 }
1318         }
1319
1320         // Now we replace the media urls.
1321         foreach ($media AS $key => $value) {
1322                 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1323         }
1324         return "";
1325 }
1326
1327 function twitter_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact, $noquote)
1328 {
1329         $postarray = [];
1330         $postarray['network'] = NETWORK_TWITTER;
1331         $postarray['uid'] = $uid;
1332         $postarray['wall'] = 0;
1333         $postarray['uri'] = "twitter::" . $post->id_str;
1334         // $postarray['object'] = json_encode($post); // Activate for debugging
1335
1336         // Don't import our own comments
1337         if (dba::exists('item', ['extid' => $postarray['uri'], 'uid' => $uid])) {
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                 $fields = ['uri', 'parent-uri', 'parent'];
1348                 $parent_item = Item::selectFirst($fields, ['uri' => $parent, 'uid' => $uid]);
1349                 if (!DBM::is_result($parent_item)) {
1350                         $parent_item = Item::selectFirst($fields, ['extid' => $parent, 'uid' => $uid]);
1351                 }
1352
1353                 if (DBM::is_result($parent_item)) {
1354                         $postarray['thr-parent'] = $parent_item['uri'];
1355                         $postarray['parent-uri'] = $parent_item['parent-uri'];
1356                         $postarray['parent'] = $parent_item['parent'];
1357                         $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1358                 } else {
1359                         $postarray['thr-parent'] = $postarray['uri'];
1360                         $postarray['parent-uri'] = $postarray['uri'];
1361                         $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1362                 }
1363
1364                 // Is it me?
1365                 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1366
1367                 if ($post->user->id_str == $own_id) {
1368                         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1369                                 intval($uid));
1370
1371                         if (DBM::is_result($r)) {
1372                                 $contactid = $r[0]["id"];
1373
1374                                 $postarray['owner-name']   = $r[0]["name"];
1375                                 $postarray['owner-link']   = $r[0]["url"];
1376                                 $postarray['owner-avatar'] = $r[0]["photo"];
1377                         } else {
1378                                 logger("No self contact for user " . $uid, LOGGER_DEBUG);
1379                                 return [];
1380                         }
1381                 }
1382                 // Don't create accounts of people who just comment something
1383                 $create_user = false;
1384         } else {
1385                 $postarray['parent-uri'] = $postarray['uri'];
1386                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1387         }
1388
1389         if ($contactid == 0) {
1390                 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1391
1392                 $postarray['owner-name'] = $post->user->name;
1393                 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1394                 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1395         }
1396
1397         if (($contactid == 0) && !$only_existing_contact) {
1398                 $contactid = $self['id'];
1399         } elseif ($contactid <= 0) {
1400                 logger("Contact ID is zero or less than zero.", LOGGER_DEBUG);
1401                 return [];
1402         }
1403
1404         $postarray['contact-id'] = $contactid;
1405
1406         $postarray['verb'] = ACTIVITY_POST;
1407         $postarray['author-name'] = $postarray['owner-name'];
1408         $postarray['author-link'] = $postarray['owner-link'];
1409         $postarray['author-avatar'] = $postarray['owner-avatar'];
1410         $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1411         $postarray['app'] = strip_tags($post->source);
1412
1413         if ($post->user->protected) {
1414                 $postarray['private'] = 1;
1415                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1416         } else {
1417                 $postarray['private'] = 0;
1418                 $postarray['allow_cid'] = '';
1419         }
1420
1421         if (is_string($post->full_text)) {
1422                 $postarray['body'] = $post->full_text;
1423         } else {
1424                 $postarray['body'] = $post->text;
1425         }
1426
1427         // When the post contains links then use the correct object type
1428         if (count($post->entities->urls) > 0) {
1429                 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1430         }
1431
1432         // Search for media links
1433         $picture = twitter_media_entities($post, $postarray);
1434
1435         $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1436         $postarray['body'] = $converted["body"];
1437         $postarray['tag'] = $converted["tags"];
1438         $postarray['created'] = DateTimeFormat::utc($post->created_at);
1439         $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1440
1441         $statustext = $converted["plain"];
1442
1443         if (!empty($post->place->name)) {
1444                 $postarray["location"] = $post->place->name;
1445         }
1446         if (!empty($post->place->full_name)) {
1447                 $postarray["location"] = $post->place->full_name;
1448         }
1449         if (!empty($post->geo->coordinates)) {
1450                 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1451         }
1452         if (!empty($post->coordinates->coordinates)) {
1453                 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1454         }
1455         if (!empty($post->retweeted_status)) {
1456                 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1457
1458                 //$retweet['object'] = $postarray['object']; // Activate for debugging
1459                 $retweet['private'] = $postarray['private'];
1460                 $retweet['allow_cid'] = $postarray['allow_cid'];
1461                 $retweet['contact-id'] = $postarray['contact-id'];
1462                 $retweet['owner-name'] = $postarray['owner-name'];
1463                 $retweet['owner-link'] = $postarray['owner-link'];
1464                 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1465
1466                 $postarray = $retweet;
1467         }
1468
1469         if (!empty($post->quoted_status) && !$noquote) {
1470                 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1471
1472                 $postarray['body'] = $statustext;
1473
1474                 $postarray['body'] .= "\n" . share_header(
1475                         $quoted['author-name'],
1476                         $quoted['author-link'],
1477                         $quoted['author-avatar'],
1478                         "",
1479                         $quoted['created'],
1480                         $quoted['plink']
1481                 );
1482
1483                 $postarray['body'] .= $quoted['body'] . '[/share]';
1484         }
1485
1486         return $postarray;
1487 }
1488
1489 function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, $self, $own_id)
1490 {
1491         logger("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, LOGGER_DEBUG);
1492
1493         $posts = [];
1494
1495         while ($post->in_reply_to_status_id_str != "") {
1496                 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1497
1498                 try {
1499                         $post = $connection->get('statuses/show', $parameters);
1500                 } catch (TwitterOAuthException $e) {
1501                         logger('twitter_fetchparentposts: Error fetching for user ' . $uid . ' and post ' . $post->id_str . ': ' . $e->getMessage());
1502                         break;
1503                 }
1504
1505                 if (empty($post)) {
1506                         logger("twitter_fetchparentposts: Can't fetch post " . $parameters->id, LOGGER_DEBUG);
1507                         break;
1508                 }
1509
1510                 if (dba::exists('item', ['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
1511                         break;
1512                 }
1513
1514                 $posts[] = $post;
1515         }
1516
1517         logger("twitter_fetchparentposts: Fetching " . count($posts) . " parents", LOGGER_DEBUG);
1518
1519         $posts = array_reverse($posts);
1520
1521         if (!empty($posts)) {
1522                 foreach ($posts as $post) {
1523                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1524
1525                         if (empty($postarray['body'])) {
1526                                 continue;
1527                         }
1528
1529                         $item = Item::insert($postarray);
1530
1531                         $postarray["id"] = $item;
1532
1533                         logger('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1534                 }
1535         }
1536 }
1537
1538 function twitter_fetchhometimeline(App $a, $uid)
1539 {
1540         $ckey    = Config::get('twitter', 'consumerkey');
1541         $csecret = Config::get('twitter', 'consumersecret');
1542         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1543         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1544         $create_user = PConfig::get($uid, 'twitter', 'create_user');
1545         $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1546
1547         logger("twitter_fetchhometimeline: Fetching for user " . $uid, LOGGER_DEBUG);
1548
1549         $application_name = Config::get('twitter', 'application_name');
1550
1551         if ($application_name == "") {
1552                 $application_name = $a->get_hostname();
1553         }
1554
1555         require_once 'include/items.php';
1556
1557         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1558
1559         try {
1560                 $own_contact = twitter_fetch_own_contact($a, $uid);
1561         } catch (TwitterOAuthException $e) {
1562                 logger('twitter_fetchhometimeline: Error fetching own contact for user ' . $uid . ': ' . $e->getMessage());
1563                 return;
1564         }
1565
1566         $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1567                 intval($own_contact),
1568                 intval($uid));
1569
1570         if (DBM::is_result($r)) {
1571                 $own_id = $r[0]["nick"];
1572         } else {
1573                 logger("twitter_fetchhometimeline: Own twitter contact not found for user " . $uid, LOGGER_DEBUG);
1574                 return;
1575         }
1576
1577         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1578                 intval($uid));
1579
1580         if (DBM::is_result($r)) {
1581                 $self = $r[0];
1582         } else {
1583                 logger("twitter_fetchhometimeline: Own contact not found for user " . $uid, LOGGER_DEBUG);
1584                 return;
1585         }
1586
1587         $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1588                 intval($uid));
1589         if (!DBM::is_result($u)) {
1590                 logger("twitter_fetchhometimeline: Own user not found for user " . $uid, LOGGER_DEBUG);
1591                 return;
1592         }
1593
1594         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1595         //$parameters["count"] = 200;
1596         // Fetching timeline
1597         $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1598
1599         $first_time = ($lastid == "");
1600
1601         if ($lastid != "") {
1602                 $parameters["since_id"] = $lastid;
1603         }
1604
1605         try {
1606                 $items = $connection->get('statuses/home_timeline', $parameters);
1607         } catch (TwitterOAuthException $e) {
1608                 logger('twitter_fetchhometimeline: Error fetching home timeline: ' . $e->getMessage());
1609                 return;
1610         }
1611
1612         if (!is_array($items)) {
1613                 logger("twitter_fetchhometimeline: Error fetching home timeline: " . print_r($items, true), LOGGER_DEBUG);
1614                 return;
1615         }
1616
1617         $posts = array_reverse($items);
1618
1619         logger("twitter_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1620
1621         if (count($posts)) {
1622                 foreach ($posts as $post) {
1623                         if ($post->id_str > $lastid) {
1624                                 $lastid = $post->id_str;
1625                                 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1626                         }
1627
1628                         if ($first_time) {
1629                                 continue;
1630                         }
1631
1632                         if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1633                                 logger("twitter_fetchhometimeline: Skip previously sended post", LOGGER_DEBUG);
1634                                 continue;
1635                         }
1636
1637                         if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1638                                 logger("twitter_fetchhometimeline: Skip post that will be mirrored", LOGGER_DEBUG);
1639                                 continue;
1640                         }
1641
1642                         if ($post->in_reply_to_status_id_str != "") {
1643                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1644                         }
1645
1646                         $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1647
1648                         if (empty($postarray['body']) || trim($postarray['body']) == "") {
1649                                 continue;
1650                         }
1651
1652                         $notify = false;
1653
1654                         if ($postarray['uri'] == $postarray['parent-uri']) {
1655                                 $contact = dba::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
1656                                 if (DBM::is_result($contact)) {
1657                                         $notify = Item::isRemoteSelf($contact, $postarray);
1658                                 }
1659                         }
1660
1661                         $item = Item::insert($postarray, false, $notify);
1662                         $postarray["id"] = $item;
1663
1664                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1665                 }
1666         }
1667         PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1668
1669         // Fetching mentions
1670         $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1671
1672         $first_time = ($lastid == "");
1673
1674         if ($lastid != "") {
1675                 $parameters["since_id"] = $lastid;
1676         }
1677
1678         try {
1679                 $items = $connection->get('statuses/mentions_timeline', $parameters);
1680         } catch (TwitterOAuthException $e) {
1681                 logger('twitter_fetchhometimeline: Error fetching mentions: ' . $e->getMessage());
1682                 return;
1683         }
1684
1685         if (!is_array($items)) {
1686                 logger("twitter_fetchhometimeline: Error fetching mentions: " . print_r($items, true), LOGGER_DEBUG);
1687                 return;
1688         }
1689
1690         $posts = array_reverse($items);
1691
1692         logger("twitter_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1693
1694         if (count($posts)) {
1695                 foreach ($posts as $post) {
1696                         if ($post->id_str > $lastid) {
1697                                 $lastid = $post->id_str;
1698                         }
1699
1700                         if ($first_time) {
1701                                 continue;
1702                         }
1703
1704                         if ($post->in_reply_to_status_id_str != "") {
1705                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1706                         }
1707
1708                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1709
1710                         if (trim($postarray['body']) == "") {
1711                                 continue;
1712                         }
1713
1714                         $item = Item::insert($postarray);
1715
1716                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1717                 }
1718         }
1719
1720         PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1721 }
1722
1723 function twitter_fetch_own_contact(App $a, $uid)
1724 {
1725         $ckey    = Config::get('twitter', 'consumerkey');
1726         $csecret = Config::get('twitter', 'consumersecret');
1727         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1728         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1729
1730         $own_id = PConfig::get($uid, 'twitter', 'own_id');
1731
1732         $contact_id = 0;
1733
1734         if ($own_id == "") {
1735                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1736
1737                 // Fetching user data
1738                 // get() may throw TwitterOAuthException, but we will catch it later
1739                 $user = $connection->get('account/verify_credentials');
1740
1741                 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1742
1743                 $contact_id = twitter_fetch_contact($uid, $user, true);
1744         } else {
1745                 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1746                         intval($uid),
1747                         dbesc("twitter::" . $own_id));
1748                 if (DBM::is_result($r)) {
1749                         $contact_id = $r[0]["id"];
1750                 } else {
1751                         PConfig::delete($uid, 'twitter', 'own_id');
1752                 }
1753         }
1754
1755         return $contact_id;
1756 }
1757
1758 function twitter_is_retweet(App $a, $uid, $body)
1759 {
1760         $body = trim($body);
1761
1762         // Skip if it isn't a pure repeated messages
1763         // Does it start with a share?
1764         if (strpos($body, "[share") > 0) {
1765                 return false;
1766         }
1767
1768         // Does it end with a share?
1769         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1770                 return false;
1771         }
1772
1773         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1774         // Skip if there is no shared message in there
1775         if ($body == $attributes) {
1776                 return false;
1777         }
1778
1779         $link = "";
1780         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1781         if (!empty($matches[1])) {
1782                 $link = $matches[1];
1783         }
1784
1785         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1786         if (!empty($matches[1])) {
1787                 $link = $matches[1];
1788         }
1789
1790         $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1791         if ($id == $link) {
1792                 return false;
1793         }
1794
1795         logger('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, LOGGER_DEBUG);
1796
1797         $ckey    = Config::get('twitter', 'consumerkey');
1798         $csecret = Config::get('twitter', 'consumersecret');
1799         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1800         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1801
1802         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1803         $result = $connection->post('statuses/retweet/' . $id);
1804
1805         logger('twitter_is_retweet: result ' . print_r($result, true), LOGGER_DEBUG);
1806
1807         return !isset($result->errors);
1808 }