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