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