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