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