We don't store the "type" parameter anymore
[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["api_source"] = true;
805         $datarray["profile_uid"] = $uid;
806         $datarray["extid"] = NETWORK_TWITTER;
807         $datarray['message_id'] = Item::newURI($uid, NETWORK_TWITTER . ":" . $post->id);
808         // $datarray['object'] = json_encode($post); // Activate for debugging
809         $datarray["title"] = "";
810
811         if (!empty($post->retweeted_status)) {
812                 // We don't support nested shares, so we mustn't show quotes as shares on retweets
813                 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
814
815                 $datarray['body'] = "\n" . share_header(
816                         $item['author-name'],
817                         $item['author-link'],
818                         $item['author-avatar'],
819                         "",
820                         $item['created'],
821                         $item['plink']
822                 );
823
824                 $datarray['body'] .= $item['body'] . '[/share]';
825         } else {
826                 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
827
828                 $datarray['body'] = $item['body'];
829         }
830
831         $datarray["source"] = $item['app'];
832         $datarray["verb"] = $item['verb'];
833
834         if (isset($item["location"])) {
835                 $datarray["location"] = $item["location"];
836         }
837
838         if (isset($item["coord"])) {
839                 $datarray["coord"] = $item["coord"];
840         }
841
842         return $datarray;
843 }
844
845 function twitter_fetchtimeline(App $a, $uid)
846 {
847         $ckey    = Config::get('twitter', 'consumerkey');
848         $csecret = Config::get('twitter', 'consumersecret');
849         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
850         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
851         $lastid  = PConfig::get($uid, 'twitter', 'lastid');
852
853         $application_name = Config::get('twitter', 'application_name');
854
855         if ($application_name == "") {
856                 $application_name = $a->get_hostname();
857         }
858
859         $has_picture = false;
860
861         require_once 'mod/item.php';
862         require_once 'include/items.php';
863         require_once 'mod/share.php';
864
865         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
866
867         $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
868
869         $first_time = ($lastid == "");
870
871         if ($lastid != "") {
872                 $parameters["since_id"] = $lastid;
873         }
874
875         try {
876                 $items = $connection->get('statuses/user_timeline', $parameters);
877         } catch (TwitterOAuthException $e) {
878                 logger('twitter_fetchtimeline: Error fetching timeline for user ' . $uid . ': ' . $e->getMessage());
879                 return;
880         }
881
882         if (!is_array($items)) {
883                 return;
884         }
885
886         $posts = array_reverse($items);
887
888         if (count($posts)) {
889                 foreach ($posts as $post) {
890                         if ($post->id_str > $lastid) {
891                                 $lastid = $post->id_str;
892                                 PConfig::set($uid, 'twitter', 'lastid', $lastid);
893                         }
894
895                         if ($first_time) {
896                                 continue;
897                         }
898
899                         if (!stristr($post->source, $application_name)) {
900                                 $_SESSION["authenticated"] = true;
901                                 $_SESSION["uid"] = $uid;
902
903                                 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
904
905                                 logger('twitter: posting for user ' . $uid);
906
907                                 item_post($a);
908                         }
909                 }
910         }
911         PConfig::set($uid, 'twitter', 'lastid', $lastid);
912 }
913
914 function twitter_queue_hook(App $a, &$b)
915 {
916         $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
917                 dbesc(NETWORK_TWITTER)
918         );
919         if (!DBM::is_result($qi)) {
920                 return;
921         }
922
923         foreach ($qi as $x) {
924                 if ($x['network'] !== NETWORK_TWITTER) {
925                         continue;
926                 }
927
928                 logger('twitter_queue: run');
929
930                 $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
931                         WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
932                         intval($x['cid'])
933                 );
934                 if (!DBM::is_result($r)) {
935                         continue;
936                 }
937
938                 $user = $r[0];
939
940                 $ckey    = Config::get('twitter', 'consumerkey');
941                 $csecret = Config::get('twitter', 'consumersecret');
942                 $otoken  = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
943                 $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
944
945                 $success = false;
946
947                 if ($ckey && $csecret && $otoken && $osecret) {
948                         logger('twitter_queue: able to post');
949
950                         $z = unserialize($x['content']);
951
952                         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
953                         $result = $connection->post($z['url'], $z['post']);
954
955                         logger('twitter_queue: post result: ' . print_r($result, true), LOGGER_DEBUG);
956
957                         if ($result->errors) {
958                                 logger('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
959                         } else {
960                                 $success = true;
961                                 Queue::removeItem($x['id']);
962                         }
963                 } else {
964                         logger("twitter_queue: Error getting tokens for user " . $user['uid']);
965                 }
966
967                 if (!$success) {
968                         logger('twitter_queue: delayed');
969                         Queue::updateTime($x['id']);
970                 }
971         }
972 }
973
974 function twitter_fix_avatar($avatar)
975 {
976         $new_avatar = str_replace("_normal.", ".", $avatar);
977
978         $info = Image::getInfoFromURL($new_avatar);
979         if (!$info) {
980                 $new_avatar = $avatar;
981         }
982
983         return $new_avatar;
984 }
985
986 function twitter_fetch_contact($uid, $data, $create_user)
987 {
988         if ($data->id_str == "") {
989                 return -1;
990         }
991
992         $avatar = twitter_fix_avatar($data->profile_image_url_https);
993         $url = "https://twitter.com/" . $data->screen_name;
994         $addr = $data->screen_name . "@twitter.com";
995
996         GContact::update(["url" => $url, "network" => NETWORK_TWITTER,
997                 "photo" => $avatar, "hide" => true,
998                 "name" => $data->name, "nick" => $data->screen_name,
999                 "location" => $data->location, "about" => $data->description,
1000                 "addr" => $addr, "generation" => 2]);
1001
1002         $fields = ['url' => $url, 'network' => NETWORK_TWITTER,
1003                 'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
1004                 'location' => $data->location, 'about' => $data->description];
1005
1006         $cid = Contact::getIdForURL($url, 0, true, $fields);
1007         if (!empty($cid)) {
1008                 dba::update('contact', $fields, ['id' => $cid]);
1009                 Contact::updateAvatar($avatar, 0, $cid);
1010         }
1011
1012         $contact = dba::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
1013         if (!DBM::is_result($contact) && !$create_user) {
1014                 return 0;
1015         }
1016
1017         if (!DBM::is_result($contact)) {
1018                 // create contact record
1019                 $fields['uid'] = $uid;
1020                 $fields['created'] = DateTimeFormat::utcNow();
1021                 $fields['nurl'] = normalise_link($url);
1022                 $fields['alias'] = 'twitter::' . $data->id_str;
1023                 $fields['poll'] = 'twitter::' . $data->id_str;
1024                 $fields['rel'] = CONTACT_IS_FRIEND;
1025                 $fields['priority'] = 1;
1026                 $fields['writable'] = true;
1027                 $fields['blocked'] = false;
1028                 $fields['readonly'] = false;
1029                 $fields['pending'] = false;
1030
1031                 if (!dba::insert('contact', $fields)) {
1032                         return false;
1033                 }
1034
1035                 $contact_id = dba::lastInsertId();
1036
1037                 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1038
1039                 Contact::updateAvatar($avatar, $uid, $contact_id);
1040         } else {
1041                 if ($contact["readonly"] || $contact["blocked"]) {
1042                         logger("twitter_fetch_contact: Contact '" . $contact["nick"] . "' is blocked or readonly.", LOGGER_DEBUG);
1043                         return -1;
1044                 }
1045
1046                 $contact_id = $contact['id'];
1047
1048                 // update profile photos once every twelve hours as we have no notification of when they change.
1049                 $update_photo = ($contact['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
1050
1051                 // check that we have all the photos, this has been known to fail on occasion
1052                 if (empty($contact['photo']) || empty($contact['thumb']) || empty($contact['micro']) || $update_photo) {
1053                         logger("twitter_fetch_contact: Updating contact " . $data->screen_name, LOGGER_DEBUG);
1054
1055                         Contact::updateAvatar($avatar, $uid, $contact['id']);
1056
1057                         $fields['name-date'] = DateTimeFormat::utcNow();
1058                         $fields['uri-date'] = DateTimeFormat::utcNow();
1059
1060                         dba::update('contact', $fields, ['id' => $contact['id']]);
1061                 }
1062         }
1063
1064         return $contact_id;
1065 }
1066
1067 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1068 {
1069         $ckey = Config::get('twitter', 'consumerkey');
1070         $csecret = Config::get('twitter', 'consumersecret');
1071         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1072         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1073
1074         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1075                 intval($uid));
1076
1077         if (DBM::is_result($r)) {
1078                 $self = $r[0];
1079         } else {
1080                 return;
1081         }
1082
1083         $parameters = [];
1084
1085         if ($screen_name != "") {
1086                 $parameters["screen_name"] = $screen_name;
1087         }
1088
1089         if ($user_id != "") {
1090                 $parameters["user_id"] = $user_id;
1091         }
1092
1093         // Fetching user data
1094         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1095         try {
1096                 $user = $connection->get('users/show', $parameters);
1097         } catch (TwitterOAuthException $e) {
1098                 logger('twitter_fetchuser: Error fetching user ' . $uid . ': ' . $e->getMessage());
1099                 return;
1100         }
1101
1102         if (!is_object($user)) {
1103                 return;
1104         }
1105
1106         $contact_id = twitter_fetch_contact($uid, $user, true);
1107
1108         return $contact_id;
1109 }
1110
1111 function twitter_expand_entities(App $a, $body, $item, $picture)
1112 {
1113         $plain = $body;
1114
1115         $tags_arr = [];
1116
1117         foreach ($item->entities->hashtags AS $hashtag) {
1118                 $url = "#[url=" . $a->get_baseurl() . "/search?tag=" . rawurlencode($hashtag->text) . "]" . $hashtag->text . "[/url]";
1119                 $tags_arr["#" . $hashtag->text] = $url;
1120                 $body = str_replace("#" . $hashtag->text, $url, $body);
1121         }
1122
1123         foreach ($item->entities->user_mentions AS $mention) {
1124                 $url = "@[url=https://twitter.com/" . rawurlencode($mention->screen_name) . "]" . $mention->screen_name . "[/url]";
1125                 $tags_arr["@" . $mention->screen_name] = $url;
1126                 $body = str_replace("@" . $mention->screen_name, $url, $body);
1127         }
1128
1129         if (isset($item->entities->urls)) {
1130                 $type = "";
1131                 $footerurl = "";
1132                 $footerlink = "";
1133                 $footer = "";
1134
1135                 foreach ($item->entities->urls as $url) {
1136                         $plain = str_replace($url->url, '', $plain);
1137
1138                         if ($url->url && $url->expanded_url && $url->display_url) {
1139                                 $expanded_url = Network::finalUrl($url->expanded_url);
1140
1141                                 $oembed_data = OEmbed::fetchURL($expanded_url);
1142
1143                                 if (empty($oembed_data) || empty($oembed_data->type)) {
1144                                         continue;
1145                                 }
1146
1147                                 // Quickfix: Workaround for URL with "[" and "]" in it
1148                                 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1149                                         $expanded_url = $url->url;
1150                                 }
1151
1152                                 if ($type == "") {
1153                                         $type = $oembed_data->type;
1154                                 }
1155
1156                                 if ($oembed_data->type == "video") {
1157                                         //$body = str_replace($url->url,
1158                                         //              "[video]".$expanded_url."[/video]", $body);
1159                                         //$dontincludemedia = true;
1160                                         $type = $oembed_data->type;
1161                                         $footerurl = $expanded_url;
1162                                         $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1163
1164                                         $body = str_replace($url->url, $footerlink, $body);
1165                                         //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1166                                 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1167                                         $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1168                                         //$dontincludemedia = true;
1169                                 } elseif ($oembed_data->type != "link") {
1170                                         $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1171                                 } else {
1172                                         $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1173
1174                                         $tempfile = tempnam(get_temppath(), "cache");
1175                                         file_put_contents($tempfile, $img_str);
1176                                         $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1177                                         unlink($tempfile);
1178
1179                                         if (substr($mime, 0, 6) == "image/") {
1180                                                 $type = "photo";
1181                                                 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1182                                                 //$dontincludemedia = true;
1183                                         } else {
1184                                                 $type = $oembed_data->type;
1185                                                 $footerurl = $expanded_url;
1186                                                 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1187
1188                                                 $body = str_replace($url->url, $footerlink, $body);
1189                                         }
1190                                 }
1191                         }
1192                 }
1193
1194                 if ($footerurl != "") {
1195                         $footer = add_page_info($footerurl, false, $picture);
1196                 }
1197
1198                 if (($footerlink != "") && (trim($footer) != "")) {
1199                         $removedlink = trim(str_replace($footerlink, "", $body));
1200
1201                         if (($removedlink == "") || strstr($body, $removedlink)) {
1202                                 $body = $removedlink;
1203                         }
1204
1205                         $body .= $footer;
1206                 }
1207
1208                 if (($footer == "") && ($picture != "")) {
1209                         $body .= "\n\n[img]" . $picture . "[/img]\n";
1210                 } elseif (($footer == "") && ($picture == "")) {
1211                         $body = add_page_info_to_body($body);
1212                 }
1213         }
1214
1215         // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1216         $tags = get_tags($body);
1217
1218         if (count($tags)) {
1219                 foreach ($tags as $tag) {
1220                         if (strstr(trim($tag), " ")) {
1221                                 continue;
1222                         }
1223
1224                         if (strpos($tag, '#') === 0) {
1225                                 if (strpos($tag, '[url=')) {
1226                                         continue;
1227                                 }
1228
1229                                 // don't link tags that are already embedded in links
1230                                 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1231                                         continue;
1232                                 }
1233                                 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1234                                         continue;
1235                                 }
1236
1237                                 $basetag = str_replace('_', ' ', substr($tag, 1));
1238                                 $url = '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1239                                 $body = str_replace($tag, $url, $body);
1240                                 $tags_arr["#" . $basetag] = $url;
1241                         } elseif (strpos($tag, '@') === 0) {
1242                                 if (strpos($tag, '[url=')) {
1243                                         continue;
1244                                 }
1245
1246                                 $basetag = substr($tag, 1);
1247                                 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1248                                 $body = str_replace($tag, $url, $body);
1249                                 $tags_arr["@" . $basetag] = $url;
1250                         }
1251                 }
1252         }
1253
1254         $tags = implode($tags_arr, ",");
1255
1256         return ["body" => $body, "tags" => $tags, "plain" => $plain];
1257 }
1258
1259 /**
1260  * @brief Fetch media entities and add media links to the body
1261  *
1262  * @param object $post Twitter object with the post
1263  * @param array $postarray Array of the item that is about to be posted
1264  *
1265  * @return $picture string Image URL or empty string
1266  */
1267 function twitter_media_entities($post, &$postarray)
1268 {
1269         // There are no media entities? So we quit.
1270         if (empty($post->extended_entities->media)) {
1271                 return "";
1272         }
1273
1274         // When the post links to an external page, we only take one picture.
1275         // We only do this when there is exactly one media.
1276         if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1277                 $picture = "";
1278                 foreach ($post->extended_entities->media AS $medium) {
1279                         if (isset($medium->media_url_https)) {
1280                                 $picture = $medium->media_url_https;
1281                                 $postarray['body'] = str_replace($medium->url, "", $postarray['body']);
1282                         }
1283                 }
1284                 return $picture;
1285         }
1286
1287         // This is a pure media post, first search for all media urls
1288         $media = [];
1289         foreach ($post->extended_entities->media AS $medium) {
1290                 if (!isset($media[$medium->url])) {
1291                         $media[$medium->url] = '';
1292                 }
1293                 switch ($medium->type) {
1294                         case 'photo':
1295                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1296                                 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1297                                 break;
1298                         case 'video':
1299                         case 'animated_gif':
1300                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1301                                 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1302                                 if (is_array($medium->video_info->variants)) {
1303                                         $bitrate = 0;
1304                                         // We take the video with the highest bitrate
1305                                         foreach ($medium->video_info->variants AS $variant) {
1306                                                 if (($variant->content_type == "video/mp4") && ($variant->bitrate >= $bitrate)) {
1307                                                         $media[$medium->url] = "\n[video]" . $variant->url . "[/video]";
1308                                                         $bitrate = $variant->bitrate;
1309                                                 }
1310                                         }
1311                                 }
1312                                 break;
1313                         // The following code will only be activated for test reasons
1314                         //default:
1315                         //      $postarray['body'] .= print_r($medium, true);
1316                 }
1317         }
1318
1319         // Now we replace the media urls.
1320         foreach ($media AS $key => $value) {
1321                 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1322         }
1323         return "";
1324 }
1325
1326 function twitter_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact, $noquote)
1327 {
1328         $postarray = [];
1329         $postarray['network'] = NETWORK_TWITTER;
1330         $postarray['uid'] = $uid;
1331         $postarray['wall'] = 0;
1332         $postarray['uri'] = "twitter::" . $post->id_str;
1333         // $postarray['object'] = json_encode($post); // Activate for debugging
1334
1335         // Don't import our own comments
1336         if (dba::exists('item', ['extid' => $postarray['uri'], 'uid' => $uid])) {
1337                 logger("Item with extid " . $postarray['uri'] . " found.", LOGGER_DEBUG);
1338                 return [];
1339         }
1340
1341         $contactid = 0;
1342
1343         if ($post->in_reply_to_status_id_str != "") {
1344                 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1345
1346                 $fields = ['uri', 'parent-uri', 'parent'];
1347                 $parent_item = Item::selectFirst($fields, ['uri' => $parent, 'uid' => $uid]);
1348                 if (!DBM::is_result($parent_item)) {
1349                         $parent_item = Item::selectFirst($fields, ['extid' => $parent, 'uid' => $uid]);
1350                 }
1351
1352                 if (DBM::is_result($parent_item)) {
1353                         $postarray['thr-parent'] = $parent_item['uri'];
1354                         $postarray['parent-uri'] = $parent_item['parent-uri'];
1355                         $postarray['parent'] = $parent_item['parent'];
1356                         $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1357                 } else {
1358                         $postarray['thr-parent'] = $postarray['uri'];
1359                         $postarray['parent-uri'] = $postarray['uri'];
1360                         $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1361                 }
1362
1363                 // Is it me?
1364                 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1365
1366                 if ($post->user->id_str == $own_id) {
1367                         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1368                                 intval($uid));
1369
1370                         if (DBM::is_result($r)) {
1371                                 $contactid = $r[0]["id"];
1372
1373                                 $postarray['owner-name']   = $r[0]["name"];
1374                                 $postarray['owner-link']   = $r[0]["url"];
1375                                 $postarray['owner-avatar'] = $r[0]["photo"];
1376                         } else {
1377                                 logger("No self contact for user " . $uid, LOGGER_DEBUG);
1378                                 return [];
1379                         }
1380                 }
1381                 // Don't create accounts of people who just comment something
1382                 $create_user = false;
1383         } else {
1384                 $postarray['parent-uri'] = $postarray['uri'];
1385                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1386         }
1387
1388         if ($contactid == 0) {
1389                 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1390
1391                 $postarray['owner-name'] = $post->user->name;
1392                 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1393                 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1394         }
1395
1396         if (($contactid == 0) && !$only_existing_contact) {
1397                 $contactid = $self['id'];
1398         } elseif ($contactid <= 0) {
1399                 logger("Contact ID is zero or less than zero.", LOGGER_DEBUG);
1400                 return [];
1401         }
1402
1403         $postarray['contact-id'] = $contactid;
1404
1405         $postarray['verb'] = ACTIVITY_POST;
1406         $postarray['author-name'] = $postarray['owner-name'];
1407         $postarray['author-link'] = $postarray['owner-link'];
1408         $postarray['author-avatar'] = $postarray['owner-avatar'];
1409         $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1410         $postarray['app'] = strip_tags($post->source);
1411
1412         if ($post->user->protected) {
1413                 $postarray['private'] = 1;
1414                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1415         } else {
1416                 $postarray['private'] = 0;
1417                 $postarray['allow_cid'] = '';
1418         }
1419
1420         if (is_string($post->full_text)) {
1421                 $postarray['body'] = $post->full_text;
1422         } else {
1423                 $postarray['body'] = $post->text;
1424         }
1425
1426         // When the post contains links then use the correct object type
1427         if (count($post->entities->urls) > 0) {
1428                 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1429         }
1430
1431         // Search for media links
1432         $picture = twitter_media_entities($post, $postarray);
1433
1434         $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1435         $postarray['body'] = $converted["body"];
1436         $postarray['tag'] = $converted["tags"];
1437         $postarray['created'] = DateTimeFormat::utc($post->created_at);
1438         $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1439
1440         $statustext = $converted["plain"];
1441
1442         if (!empty($post->place->name)) {
1443                 $postarray["location"] = $post->place->name;
1444         }
1445         if (!empty($post->place->full_name)) {
1446                 $postarray["location"] = $post->place->full_name;
1447         }
1448         if (!empty($post->geo->coordinates)) {
1449                 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1450         }
1451         if (!empty($post->coordinates->coordinates)) {
1452                 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1453         }
1454         if (!empty($post->retweeted_status)) {
1455                 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1456
1457                 //$retweet['object'] = $postarray['object']; // Activate for debugging
1458                 $retweet['private'] = $postarray['private'];
1459                 $retweet['allow_cid'] = $postarray['allow_cid'];
1460                 $retweet['contact-id'] = $postarray['contact-id'];
1461                 $retweet['owner-name'] = $postarray['owner-name'];
1462                 $retweet['owner-link'] = $postarray['owner-link'];
1463                 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1464
1465                 $postarray = $retweet;
1466         }
1467
1468         if (!empty($post->quoted_status) && !$noquote) {
1469                 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1470
1471                 $postarray['body'] = $statustext;
1472
1473                 $postarray['body'] .= "\n" . share_header(
1474                         $quoted['author-name'],
1475                         $quoted['author-link'],
1476                         $quoted['author-avatar'],
1477                         "",
1478                         $quoted['created'],
1479                         $quoted['plink']
1480                 );
1481
1482                 $postarray['body'] .= $quoted['body'] . '[/share]';
1483         }
1484
1485         return $postarray;
1486 }
1487
1488 function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, $self, $own_id)
1489 {
1490         logger("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, LOGGER_DEBUG);
1491
1492         $posts = [];
1493
1494         while ($post->in_reply_to_status_id_str != "") {
1495                 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1496
1497                 try {
1498                         $post = $connection->get('statuses/show', $parameters);
1499                 } catch (TwitterOAuthException $e) {
1500                         logger('twitter_fetchparentposts: Error fetching for user ' . $uid . ' and post ' . $post->id_str . ': ' . $e->getMessage());
1501                         break;
1502                 }
1503
1504                 if (empty($post)) {
1505                         logger("twitter_fetchparentposts: Can't fetch post " . $parameters->id, LOGGER_DEBUG);
1506                         break;
1507                 }
1508
1509                 if (dba::exists('item', ['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
1510                         break;
1511                 }
1512
1513                 $posts[] = $post;
1514         }
1515
1516         logger("twitter_fetchparentposts: Fetching " . count($posts) . " parents", LOGGER_DEBUG);
1517
1518         $posts = array_reverse($posts);
1519
1520         if (!empty($posts)) {
1521                 foreach ($posts as $post) {
1522                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1523
1524                         if (empty($postarray['body'])) {
1525                                 continue;
1526                         }
1527
1528                         $item = Item::insert($postarray);
1529
1530                         $postarray["id"] = $item;
1531
1532                         logger('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1533                 }
1534         }
1535 }
1536
1537 function twitter_fetchhometimeline(App $a, $uid)
1538 {
1539         $ckey    = Config::get('twitter', 'consumerkey');
1540         $csecret = Config::get('twitter', 'consumersecret');
1541         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1542         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1543         $create_user = PConfig::get($uid, 'twitter', 'create_user');
1544         $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1545
1546         logger("twitter_fetchhometimeline: Fetching for user " . $uid, LOGGER_DEBUG);
1547
1548         $application_name = Config::get('twitter', 'application_name');
1549
1550         if ($application_name == "") {
1551                 $application_name = $a->get_hostname();
1552         }
1553
1554         require_once 'include/items.php';
1555
1556         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1557
1558         try {
1559                 $own_contact = twitter_fetch_own_contact($a, $uid);
1560         } catch (TwitterOAuthException $e) {
1561                 logger('twitter_fetchhometimeline: Error fetching own contact for user ' . $uid . ': ' . $e->getMessage());
1562                 return;
1563         }
1564
1565         $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1566                 intval($own_contact),
1567                 intval($uid));
1568
1569         if (DBM::is_result($r)) {
1570                 $own_id = $r[0]["nick"];
1571         } else {
1572                 logger("twitter_fetchhometimeline: Own twitter contact not found for user " . $uid, LOGGER_DEBUG);
1573                 return;
1574         }
1575
1576         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1577                 intval($uid));
1578
1579         if (DBM::is_result($r)) {
1580                 $self = $r[0];
1581         } else {
1582                 logger("twitter_fetchhometimeline: Own contact not found for user " . $uid, LOGGER_DEBUG);
1583                 return;
1584         }
1585
1586         $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1587                 intval($uid));
1588         if (!DBM::is_result($u)) {
1589                 logger("twitter_fetchhometimeline: Own user not found for user " . $uid, LOGGER_DEBUG);
1590                 return;
1591         }
1592
1593         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1594         //$parameters["count"] = 200;
1595         // Fetching timeline
1596         $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1597
1598         $first_time = ($lastid == "");
1599
1600         if ($lastid != "") {
1601                 $parameters["since_id"] = $lastid;
1602         }
1603
1604         try {
1605                 $items = $connection->get('statuses/home_timeline', $parameters);
1606         } catch (TwitterOAuthException $e) {
1607                 logger('twitter_fetchhometimeline: Error fetching home timeline: ' . $e->getMessage());
1608                 return;
1609         }
1610
1611         if (!is_array($items)) {
1612                 logger("twitter_fetchhometimeline: Error fetching home timeline: " . print_r($items, true), LOGGER_DEBUG);
1613                 return;
1614         }
1615
1616         $posts = array_reverse($items);
1617
1618         logger("twitter_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1619
1620         if (count($posts)) {
1621                 foreach ($posts as $post) {
1622                         if ($post->id_str > $lastid) {
1623                                 $lastid = $post->id_str;
1624                                 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1625                         }
1626
1627                         if ($first_time) {
1628                                 continue;
1629                         }
1630
1631                         if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1632                                 logger("twitter_fetchhometimeline: Skip previously sended post", LOGGER_DEBUG);
1633                                 continue;
1634                         }
1635
1636                         if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1637                                 logger("twitter_fetchhometimeline: Skip post that will be mirrored", LOGGER_DEBUG);
1638                                 continue;
1639                         }
1640
1641                         if ($post->in_reply_to_status_id_str != "") {
1642                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1643                         }
1644
1645                         $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1646
1647                         if (empty($postarray['body']) || trim($postarray['body']) == "") {
1648                                 continue;
1649                         }
1650
1651                         $notify = false;
1652
1653                         if ($postarray['uri'] == $postarray['parent-uri']) {
1654                                 $contact = dba::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
1655                                 if (DBM::is_result($contact)) {
1656                                         $notify = Item::isRemoteSelf($contact, $postarray);
1657                                 }
1658                         }
1659
1660                         $item = Item::insert($postarray, false, $notify);
1661                         $postarray["id"] = $item;
1662
1663                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1664                 }
1665         }
1666         PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1667
1668         // Fetching mentions
1669         $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1670
1671         $first_time = ($lastid == "");
1672
1673         if ($lastid != "") {
1674                 $parameters["since_id"] = $lastid;
1675         }
1676
1677         try {
1678                 $items = $connection->get('statuses/mentions_timeline', $parameters);
1679         } catch (TwitterOAuthException $e) {
1680                 logger('twitter_fetchhometimeline: Error fetching mentions: ' . $e->getMessage());
1681                 return;
1682         }
1683
1684         if (!is_array($items)) {
1685                 logger("twitter_fetchhometimeline: Error fetching mentions: " . print_r($items, true), LOGGER_DEBUG);
1686                 return;
1687         }
1688
1689         $posts = array_reverse($items);
1690
1691         logger("twitter_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1692
1693         if (count($posts)) {
1694                 foreach ($posts as $post) {
1695                         if ($post->id_str > $lastid) {
1696                                 $lastid = $post->id_str;
1697                         }
1698
1699                         if ($first_time) {
1700                                 continue;
1701                         }
1702
1703                         if ($post->in_reply_to_status_id_str != "") {
1704                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1705                         }
1706
1707                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1708
1709                         if (trim($postarray['body']) == "") {
1710                                 continue;
1711                         }
1712
1713                         $item = Item::insert($postarray);
1714
1715                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1716                 }
1717         }
1718
1719         PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1720 }
1721
1722 function twitter_fetch_own_contact(App $a, $uid)
1723 {
1724         $ckey    = Config::get('twitter', 'consumerkey');
1725         $csecret = Config::get('twitter', 'consumersecret');
1726         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1727         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1728
1729         $own_id = PConfig::get($uid, 'twitter', 'own_id');
1730
1731         $contact_id = 0;
1732
1733         if ($own_id == "") {
1734                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1735
1736                 // Fetching user data
1737                 // get() may throw TwitterOAuthException, but we will catch it later
1738                 $user = $connection->get('account/verify_credentials');
1739
1740                 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1741
1742                 $contact_id = twitter_fetch_contact($uid, $user, true);
1743         } else {
1744                 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1745                         intval($uid),
1746                         dbesc("twitter::" . $own_id));
1747                 if (DBM::is_result($r)) {
1748                         $contact_id = $r[0]["id"];
1749                 } else {
1750                         PConfig::delete($uid, 'twitter', 'own_id');
1751                 }
1752         }
1753
1754         return $contact_id;
1755 }
1756
1757 function twitter_is_retweet(App $a, $uid, $body)
1758 {
1759         $body = trim($body);
1760
1761         // Skip if it isn't a pure repeated messages
1762         // Does it start with a share?
1763         if (strpos($body, "[share") > 0) {
1764                 return false;
1765         }
1766
1767         // Does it end with a share?
1768         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1769                 return false;
1770         }
1771
1772         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1773         // Skip if there is no shared message in there
1774         if ($body == $attributes) {
1775                 return false;
1776         }
1777
1778         $link = "";
1779         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1780         if (!empty($matches[1])) {
1781                 $link = $matches[1];
1782         }
1783
1784         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1785         if (!empty($matches[1])) {
1786                 $link = $matches[1];
1787         }
1788
1789         $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1790         if ($id == $link) {
1791                 return false;
1792         }
1793
1794         logger('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, LOGGER_DEBUG);
1795
1796         $ckey    = Config::get('twitter', 'consumerkey');
1797         $csecret = Config::get('twitter', 'consumersecret');
1798         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1799         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1800
1801         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1802         $result = $connection->post('statuses/retweet/' . $id);
1803
1804         logger('twitter_is_retweet: result ' . print_r($result, true), LOGGER_DEBUG);
1805
1806         return !isset($result->errors);
1807 }