]> git.mxchange.org Git - friendica-addons.git/blob - twitter/twitter.php
irc added NL translation THX Pascal
[friendica-addons.git] / twitter / twitter.php
1 <?php
2 /**
3  * Name: Twitter Connector
4  * Description: Bidirectional (posting, relaying and reading) connector for Twitter.
5  * Version: 1.1.0
6  * Author: Tobias Diekershoff <https://f.diekershoff.de/profile/tobias>
7  * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
8  * Maintainer: Hypolite Petovan <https://friendica.mrpetovan.com/profile/hypolite>
9  *
10  * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel, Hypolite Petovan
11  * All rights reserved.
12  *
13  * Redistribution and use in source and binary forms, with or without
14  * modification, are permitted provided that the following conditions are met:
15  *    * Redistributions of source code must retain the above copyright notice,
16  *     this list of conditions and the following disclaimer.
17  *    * Redistributions in binary form must reproduce the above
18  *    * copyright notice, this list of conditions and the following disclaimer in
19  *      the documentation and/or other materials provided with the distribution.
20  *    * Neither the name of the <organization> nor the names of its contributors
21  *      may be used to endorse or promote products derived from this software
22  *      without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27  * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
28  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
32  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
33  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34  *
35  */
36 /*   Twitter Addon for Friendica
37  *
38  *   Author: Tobias Diekershoff
39  *           tobias.diekershoff@gmx.net
40  *
41  *   License:3-clause BSD license
42  *
43  *   Configuration:
44  *     To use this addon you need a OAuth Consumer key pair (key & secret)
45  *     you can get it from Twitter at https://twitter.com/apps
46  *
47  *     Register your Friendica site as "Client" application with "Read & Write" access
48  *     we do not need "Twitter as login". When you've registered the app you get the
49  *     OAuth Consumer key and secret pair for your application/site.
50  *
51  *     Add this key pair to your global .htconfig.php or use the admin panel.
52  *
53  *     $a->config['twitter']['consumerkey'] = 'your consumer_key here';
54  *     $a->config['twitter']['consumersecret'] = 'your consumer_secret here';
55  *
56  *     To activate the addon itself add it to the $a->config['system']['addon']
57  *     setting. After this, your user can configure their Twitter account settings
58  *     from "Settings -> Addon Settings".
59  *
60  *     Requirements: PHP5, curl
61  */
62
63 use Abraham\TwitterOAuth\TwitterOAuth;
64 use Abraham\TwitterOAuth\TwitterOAuthException;
65 use Friendica\App;
66 use Friendica\Content\OEmbed;
67 use Friendica\Content\Text\Plaintext;
68 use Friendica\Core\Addon;
69 use Friendica\Core\Config;
70 use Friendica\Core\L10n;
71 use Friendica\Core\PConfig;
72 use Friendica\Core\Worker;
73 use Friendica\Model\GContact;
74 use Friendica\Model\Contact;
75 use Friendica\Model\Group;
76 use Friendica\Model\Item;
77 use Friendica\Model\ItemContent;
78 use Friendica\Model\Photo;
79 use Friendica\Model\Queue;
80 use Friendica\Model\User;
81 use Friendica\Object\Image;
82 use Friendica\Util\DateTimeFormat;
83 use Friendica\Util\Network;
84 use Friendica\Database\DBM;
85
86 require_once 'boot.php';
87 require_once 'include/dba.php';
88 require_once 'include/enotify.php';
89 require_once 'include/text.php';
90
91 require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
92
93 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
94
95 function twitter_install()
96 {
97         //  we need some hooks, for the configuration and for sending tweets
98         Addon::registerHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
99         Addon::registerHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
100         Addon::registerHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
101         Addon::registerHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
102         Addon::registerHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
103         Addon::registerHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
104         Addon::registerHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
105         Addon::registerHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
106         Addon::registerHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
107         Addon::registerHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
108         Addon::registerHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
109         logger("installed twitter");
110 }
111
112 function twitter_uninstall()
113 {
114         Addon::unregisterHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
115         Addon::unregisterHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
116         Addon::unregisterHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
117         Addon::unregisterHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
118         Addon::unregisterHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
119         Addon::unregisterHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
120         Addon::unregisterHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
121         Addon::unregisterHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
122         Addon::unregisterHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
123         Addon::unregisterHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
124         Addon::unregisterHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
125
126         // old setting - remove only
127         Addon::unregisterHook('post_local_end', 'addon/twitter/twitter.php', 'twitter_post_hook');
128         Addon::unregisterHook('addon_settings', 'addon/twitter/twitter.php', 'twitter_settings');
129         Addon::unregisterHook('addon_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
130 }
131
132 function twitter_check_item_notification(App $a, &$notification_data)
133 {
134         $own_id = PConfig::get($notification_data["uid"], 'twitter', 'own_id');
135
136         $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
137                         intval($notification_data["uid"]),
138                         dbesc("twitter::".$own_id)
139         );
140
141         if ($own_user) {
142                 $notification_data["profiles"][] = $own_user[0]["url"];
143         }
144 }
145
146 function twitter_follow(App $a, &$contact)
147 {
148         logger("twitter_follow: Check if contact is twitter contact. " . $contact["url"], LOGGER_DEBUG);
149
150         if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
151                 return;
152         }
153
154         // contact seems to be a twitter contact, so continue
155         $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
156         $nickname = str_replace("@twitter.com", "", $nickname);
157
158         $uid = $a->user["uid"];
159
160         $ckey = Config::get('twitter', 'consumerkey');
161         $csecret = Config::get('twitter', 'consumersecret');
162         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
163         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
164
165         // If the addon is not configured (general or for this user) quit here
166         if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
167                 $contact = false;
168                 return;
169         }
170
171         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
172         $connection->post('friendships/create', ['screen_name' => $nickname]);
173
174         twitter_fetchuser($a, $uid, $nickname);
175
176         $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
177                 FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
178                                 intval($uid),
179                                 dbesc($nickname));
180         if (DBM::is_result($r)) {
181                 $contact["contact"] = $r[0];
182         }
183 }
184
185 function twitter_jot_nets(App $a, &$b)
186 {
187         if (!local_user()) {
188                 return;
189         }
190
191         $tw_post = PConfig::get(local_user(), 'twitter', 'post');
192         if (intval($tw_post) == 1) {
193                 $tw_defpost = PConfig::get(local_user(), 'twitter', 'post_by_default');
194                 $selected = ((intval($tw_defpost) == 1) ? ' checked="checked" ' : '');
195                 $b .= '<div class="profile-jot-net"><input type="checkbox" name="twitter_enable"' . $selected . ' value="1" /> '
196                         . L10n::t('Post to Twitter') . '</div>';
197         }
198 }
199
200 function twitter_settings_post(App $a, $post)
201 {
202         if (!local_user()) {
203                 return;
204         }
205         // don't check twitter settings if twitter submit button is not clicked
206         if (empty($_POST['twitter-disconnect']) && empty($_POST['twitter-submit'])) {
207                 return;
208         }
209
210         if (!empty($_POST['twitter-disconnect'])) {
211                 /*               * *
212                  * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
213                  * from the user configuration
214                  */
215                 PConfig::delete(local_user(), 'twitter', 'consumerkey');
216                 PConfig::delete(local_user(), 'twitter', 'consumersecret');
217                 PConfig::delete(local_user(), 'twitter', 'oauthtoken');
218                 PConfig::delete(local_user(), 'twitter', 'oauthsecret');
219                 PConfig::delete(local_user(), 'twitter', 'post');
220                 PConfig::delete(local_user(), 'twitter', 'post_by_default');
221                 PConfig::delete(local_user(), 'twitter', 'lastid');
222                 PConfig::delete(local_user(), 'twitter', 'mirror_posts');
223                 PConfig::delete(local_user(), 'twitter', 'import');
224                 PConfig::delete(local_user(), 'twitter', 'create_user');
225                 PConfig::delete(local_user(), 'twitter', 'own_id');
226         } else {
227                 if (isset($_POST['twitter-pin'])) {
228                         //  if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
229                         logger('got a Twitter PIN');
230                         $ckey    = Config::get('twitter', 'consumerkey');
231                         $csecret = Config::get('twitter', 'consumersecret');
232                         //  the token and secret for which the PIN was generated were hidden in the settings
233                         //  form as token and token2, we need a new connection to Twitter using these token
234                         //  and secret to request a Access Token with the PIN
235                         try {
236                                 if (empty($_POST['twitter-pin'])) {
237                                         throw new Exception(L10n::t('You submitted an empty PIN, please Sign In with Twitter again to get a new one.'));
238                                 }
239
240                                 $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
241                                 $token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $_POST['twitter-pin']]);
242                                 //  ok, now that we have the Access Token, save them in the user config
243                                 PConfig::set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
244                                 PConfig::set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
245                                 PConfig::set(local_user(), 'twitter', 'post', 1);
246                         } catch(Exception $e) {
247                                 info($e->getMessage());
248                         } catch(TwitterOAuthException $e) {
249                                 info($e->getMessage());
250                         }
251                         //  reload the Addon Settings page, if we don't do it see Bug #42
252                         goaway('settings/connectors');
253                 } else {
254                         //  if no PIN is supplied in the POST variables, the user has changed the setting
255                         //  to post a tweet for every new __public__ posting to the wall
256                         PConfig::set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
257                         PConfig::set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
258                         PConfig::set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
259                         PConfig::set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
260                         PConfig::set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
261
262                         if (!intval($_POST['twitter-mirror'])) {
263                                 PConfig::delete(local_user(), 'twitter', 'lastid');
264                         }
265
266                         info(L10n::t('Twitter settings updated.') . EOL);
267                 }
268         }
269 }
270
271 function twitter_settings(App $a, &$s)
272 {
273         if (!local_user()) {
274                 return;
275         }
276         $a->page['htmlhead'] .= '<link rel="stylesheet"  type="text/css" href="' . $a->get_baseurl() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
277         /*       * *
278          * 1) Check that we have global consumer key & secret
279          * 2) If no OAuthtoken & stuff is present, generate button to get some
280          * 3) Checkbox for "Send public notices (280 chars only)
281          */
282         $ckey    = Config::get('twitter', 'consumerkey');
283         $csecret = Config::get('twitter', 'consumersecret');
284         $otoken  = PConfig::get(local_user(), 'twitter', 'oauthtoken');
285         $osecret = PConfig::get(local_user(), 'twitter', 'oauthsecret');
286
287         $enabled            = intval(PConfig::get(local_user(), 'twitter', 'post'));
288         $defenabled         = intval(PConfig::get(local_user(), 'twitter', 'post_by_default'));
289         $mirrorenabled      = intval(PConfig::get(local_user(), 'twitter', 'mirror_posts'));
290         $importenabled      = intval(PConfig::get(local_user(), 'twitter', 'import'));
291         $create_userenabled = intval(PConfig::get(local_user(), 'twitter', 'create_user'));
292
293         $css = (($enabled) ? '' : '-disabled');
294
295         $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
296         $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
297         $s .= '</span>';
298         $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
299         $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
300         $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
301         $s .= '</span>';
302
303         if ((!$ckey) && (!$csecret)) {
304                 /* no global consumer keys
305                  * display warning and skip personal config
306                  */
307                 $s .= '<p>' . L10n::t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
308         } else {
309                 // ok we have a consumer key pair now look into the OAuth stuff
310                 if ((!$otoken) && (!$osecret)) {
311                         /* the user has not yet connected the account to twitter...
312                          * get a temporary OAuth key/secret pair and display a button with
313                          * which the user can request a PIN to connect the account to a
314                          * account at Twitter.
315                          */
316                         $connection = new TwitterOAuth($ckey, $csecret);
317                         try {
318                                 $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
319                                 $s .= '<p>' . L10n::t('At this Friendica instance the Twitter addon was enabled but you have not yet connected your account to your Twitter account. To do so click the button below to get a PIN from Twitter which you have to copy into the input box below and submit the form. Only your <strong>public</strong> posts will be posted to Twitter.') . '</p>';
320                                 $s .= '<a href="' . $connection->url('oauth/authorize', ['oauth_token' => $result['oauth_token']]) . '" target="_twitter"><img src="addon/twitter/lighter.png" alt="' . L10n::t('Log in with Twitter') . '"></a>';
321                                 $s .= '<div id="twitter-pin-wrapper">';
322                                 $s .= '<label id="twitter-pin-label" for="twitter-pin">' . L10n::t('Copy the PIN from Twitter here') . '</label>';
323                                 $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
324                                 $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result['oauth_token'] . '" />';
325                                 $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result['oauth_token_secret'] . '" />';
326                                 $s .= '</div><div class="clear"></div>';
327                                 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
328                         } catch (TwitterOAuthException $e) {
329                                 $s .= '<p>' . L10n::t('An error occured: ') . $e->getMessage() . '</p>';
330                         }
331                 } else {
332                         /*                       * *
333                          *  we have an OAuth key / secret pair for the user
334                          *  so let's give a chance to disable the postings to Twitter
335                          */
336                         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
337                         try {
338                                 $details = $connection->get('account/verify_credentials');
339
340                                 $field_checkbox = get_markup_template('field_checkbox.tpl');
341
342                                 $s .= '<div id="twitter-info" >
343                                         <p>' . L10n::t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
344                                                 <button type="submit" name="twitter-disconnect" value="1">' . L10n::t('Disconnect') . '</button>
345                                         </p>
346                                         <p id="twitter-info-block">
347                                                 <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
348                                                 <em>' . $details->description . '</em>
349                                         </p>
350                                 </div>';
351                                 $s .= '<div class="clear"></div>';
352
353                                 $s .= replace_macros($field_checkbox, [
354                                         '$field' => ['twitter-enable', L10n::t('Allow posting to Twitter'), $enabled, L10n::t('If enabled all your <strong>public</strong> postings can be posted to the associated Twitter account. You can choose to do so by default (here) or for every posting separately in the posting options when writing the entry.')]
355                                 ]);
356                                 if ($a->user['hidewall']) {
357                                         $s .= '<p>' . L10n::t('<strong>Note</strong>: Due to your privacy settings (<em>Hide your profile details from unknown viewers?</em>) the link potentially included in public postings relayed to Twitter will lead the visitor to a blank page informing the visitor that the access to your profile has been restricted.') . '</p>';
358                                 }
359                                 $s .= replace_macros($field_checkbox, [
360                                         '$field' => ['twitter-default', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
361                                 ]);
362                                 $s .= replace_macros($field_checkbox, [
363                                         '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
364                                 ]);
365                                 $s .= replace_macros($field_checkbox, [
366                                         '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
367                                 ]);
368                                 $s .= replace_macros($field_checkbox, [
369                                         '$field' => ['twitter-create_user', L10n::t('Automatically create contacts'), $create_userenabled, L10n::t('This will automatically create a contact in Friendica as soon as you receive a message from an existing contact via the Twitter network. If you do not enable this, you need to manually add those Twitter contacts in Friendica from whom you would like to see posts here. However if enabled, you cannot merely remove a twitter contact from the Friendica contact list, as it will recreate this contact when they post again.')]
370                                 ]);
371                                 $s .= '<div class="clear"></div>';
372                                 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
373                         } catch (TwitterOAuthException $e) {
374                                 $s .= '<p>' . L10n::t('An error occured: ') . $e->getMessage() . '</p>';
375                         }
376                 }
377         }
378         $s .= '</div><div class="clear"></div>';
379 }
380
381 function twitter_post_local(App $a, &$b)
382 {
383         if ($b['edit']) {
384                 return;
385         }
386
387         if (!local_user() || (local_user() != $b['uid'])) {
388                 return;
389         }
390
391         $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
392         $twitter_enable = (($twitter_post && x($_REQUEST, 'twitter_enable')) ? intval($_REQUEST['twitter_enable']) : 0);
393
394         // if API is used, default to the chosen settings
395         if ($b['api_source'] && intval(PConfig::get(local_user(), 'twitter', 'post_by_default'))) {
396                 $twitter_enable = 1;
397         }
398
399         if (!$twitter_enable) {
400                 return;
401         }
402
403         if (strlen($b['postopts'])) {
404                 $b['postopts'] .= ',';
405         }
406
407         $b['postopts'] .= 'twitter';
408 }
409
410 function twitter_action(App $a, $uid, $pid, $action)
411 {
412         $ckey = Config::get('twitter', 'consumerkey');
413         $csecret = Config::get('twitter', 'consumersecret');
414         $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
415         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
416
417         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
418
419         $post = ['id' => $pid];
420
421         logger("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), LOGGER_DATA);
422
423         switch ($action) {
424                 case "delete":
425                         // To-Do: $result = $connection->post('statuses/destroy', $post);
426                         break;
427                 case "like":
428                         $result = $connection->post('favorites/create', $post);
429                         break;
430                 case "unlike":
431                         $result = $connection->post('favorites/destroy', $post);
432                         break;
433         }
434         logger("twitter_action '" . $action . "' send, result: " . print_r($result, true), LOGGER_DEBUG);
435 }
436
437 function twitter_post_hook(App $a, &$b)
438 {
439         // Post to Twitter
440         if (!PConfig::get($b["uid"], 'twitter', 'import')
441                 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
442                 return;
443         }
444
445         if ($b['parent'] != $b['id']) {
446                 logger("twitter_post_hook: parameter " . print_r($b, true), LOGGER_DATA);
447
448                 // Looking if its a reply to a twitter post
449                 if ((substr($b["parent-uri"], 0, 9) != "twitter::")
450                         && (substr($b["extid"], 0, 9) != "twitter::")
451                         && (substr($b["thr-parent"], 0, 9) != "twitter::"))
452                 {
453                         logger("twitter_post_hook: no twitter post " . $b["parent"]);
454                         return;
455                 }
456
457                 $r = q("SELECT * FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
458                         dbesc($b["thr-parent"]),
459                         intval($b["uid"]));
460
461                 if (!DBM::is_result($r)) {
462                         logger("twitter_post_hook: no parent found " . $b["thr-parent"]);
463                         return;
464                 } else {
465                         $iscomment = true;
466                         $orig_post = $r[0];
467                 }
468
469
470                 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
471                 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
472                 $nicknameplain = "@" . $nicknameplain;
473
474                 logger("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], LOGGER_DEBUG);
475                 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
476                         $b["body"] = $nickname . " " . $b["body"];
477                 }
478
479                 logger("twitter_post_hook: parent found " . print_r($orig_post, true), LOGGER_DATA);
480         } else {
481                 $iscomment = false;
482
483                 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
484                         return;
485                 }
486
487                 // Dont't post if the post doesn't belong to us.
488                 // This is a check for forum postings
489                 $self = dba::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
490                 if ($b['contact-id'] != $self['id']) {
491                         return;
492                 }
493         }
494
495         if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
496                 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
497         }
498
499         if ($b['verb'] == ACTIVITY_LIKE) {
500                 logger("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), LOGGER_DEBUG);
501                 if ($b['deleted']) {
502                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
503                 } else {
504                         twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
505                 }
506
507                 return;
508         }
509
510         if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
511                 return;
512         }
513
514         // if post comes from twitter don't send it back
515         if ($b['extid'] == NETWORK_TWITTER) {
516                 return;
517         }
518
519         if ($b['app'] == "Twitter") {
520                 return;
521         }
522
523         logger('twitter post invoked');
524
525         PConfig::load($b['uid'], 'twitter');
526
527         $ckey    = Config::get('twitter', 'consumerkey');
528         $csecret = Config::get('twitter', 'consumersecret');
529         $otoken  = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
530         $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
531
532         if ($ckey && $csecret && $otoken && $osecret) {
533                 logger('twitter: we have customer key and oauth stuff, going to send.', LOGGER_DEBUG);
534
535                 // If it's a repeated message from twitter then do a native retweet and exit
536                 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
537                         return;
538                 }
539
540                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
541
542                 // Set the timeout for upload to 30 seconds
543                 $connection->setTimeouts(10, 30);
544
545                 $max_char = 280;
546                 $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, 8);
547                 $msg = $msgarr["text"];
548
549                 if (($msg == "") && isset($msgarr["title"])) {
550                         $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
551                 }
552
553                 $image = "";
554
555                 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
556                         $msg .= "\n" . $msgarr["url"];
557                         $url_added = true;
558                 } else {
559                         $url_added = false;
560                 }
561
562                 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
563                         $image = $msgarr["image"];
564                 }
565
566                 if (empty($msg)) {
567                         return;
568                 }
569
570                 // and now tweet it :-)
571                 $post = [];
572
573                 if (!empty($image)) {
574                         try {
575                                 $img_str = Network::fetchUrl($image);
576
577                                 $tempfile = tempnam(get_temppath(), 'cache');
578                                 file_put_contents($tempfile, $img_str);
579
580                                 $media = $connection->upload('media/upload', ['media' => $tempfile]);
581
582                                 unlink($tempfile);
583
584                                 $post['media_ids'] = $media->media_id_string;
585                         } catch (Exception $e) {
586                                 logger('Exception when trying to send to Twitter: ' . $e->getMessage());
587
588                                 // Workaround: Remove the picture link so that the post can be reposted without it
589                                 // When there is another url already added, a second url would be superfluous.
590                                 if (!$url_added) {
591                                         $msg .= "\n" . $image;
592                                 }
593
594                                 $image = "";
595                         }
596                 }
597
598                 $post['status'] = $msg;
599
600                 if ($iscomment) {
601                         $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
602                 }
603
604                 $url = 'statuses/update';
605                 $result = $connection->post($url, $post);
606                 logger('twitter_post send, result: ' . print_r($result, true), LOGGER_DEBUG);
607
608                 if ($result->source) {
609                         Config::set("twitter", "application_name", strip_tags($result->source));
610                 }
611
612                 if ($result->errors) {
613                         logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
614
615                         $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
616                         if (DBM::is_result($r)) {
617                                 $a->contact = $r[0]["id"];
618                         }
619
620                         $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
621
622                         Queue::add($a->contact, NETWORK_TWITTER, $s);
623                         notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
624                 } elseif ($iscomment) {
625                         logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
626                         Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
627                 }
628         }
629 }
630
631 function twitter_addon_admin_post(App $a)
632 {
633         $consumerkey    = x($_POST, 'consumerkey')    ? notags(trim($_POST['consumerkey']))    : '';
634         $consumersecret = x($_POST, 'consumersecret') ? notags(trim($_POST['consumersecret'])) : '';
635         Config::set('twitter', 'consumerkey', $consumerkey);
636         Config::set('twitter', 'consumersecret', $consumersecret);
637         info(L10n::t('Settings updated.') . EOL);
638 }
639
640 function twitter_addon_admin(App $a, &$o)
641 {
642         $t = get_markup_template("admin.tpl", "addon/twitter/");
643
644         $o = replace_macros($t, [
645                 '$submit' => L10n::t('Save Settings'),
646                 // name, label, value, help, [extra values]
647                 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
648                 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
649         ]);
650 }
651
652 function twitter_cron(App $a, $b)
653 {
654         $last = Config::get('twitter', 'last_poll');
655
656         $poll_interval = intval(Config::get('twitter', 'poll_interval'));
657         if (!$poll_interval) {
658                 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
659         }
660
661         if ($last) {
662                 $next = $last + ($poll_interval * 60);
663                 if ($next > time()) {
664                         logger('twitter: poll intervall not reached');
665                         return;
666                 }
667         }
668         logger('twitter: cron_start');
669
670         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
671         if (DBM::is_result($r)) {
672                 foreach ($r as $rr) {
673                         logger('twitter: fetching for user ' . $rr['uid']);
674                         Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
675                 }
676         }
677
678         $abandon_days = intval(Config::get('system', 'account_abandon_days'));
679         if ($abandon_days < 1) {
680                 $abandon_days = 0;
681         }
682
683         $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
684
685         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
686         if (DBM::is_result($r)) {
687                 foreach ($r as $rr) {
688                         if ($abandon_days != 0) {
689                                 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
690                                 if (!DBM::is_result($user)) {
691                                         logger('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
692                                         continue;
693                                 }
694                         }
695
696                         logger('twitter: importing timeline from user ' . $rr['uid']);
697                         Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
698                         /*
699                           // To-Do
700                           // check for new contacts once a day
701                           $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
702                           if($last_contact_check)
703                           $next_contact_check = $last_contact_check + 86400;
704                           else
705                           $next_contact_check = 0;
706
707                           if($next_contact_check <= time()) {
708                           pumpio_getallusers($a, $rr["uid"]);
709                           PConfig::set($rr['uid'],'pumpio','contact_check',time());
710                           }
711                          */
712                 }
713         }
714
715         logger('twitter: cron_end');
716
717         Config::set('twitter', 'last_poll', time());
718 }
719
720 function twitter_expire(App $a, $b)
721 {
722         $days = Config::get('twitter', 'expire');
723
724         if ($days == 0) {
725                 return;
726         }
727
728         if (method_exists('dba', 'delete')) {
729                 $r = dba::select('item', ['id'], ['deleted' => true, 'network' => NETWORK_TWITTER]);
730                 while ($row = dba::fetch($r)) {
731                         dba::delete('item', ['id' => $row['id']]);
732                 }
733                 dba::close($r);
734         } else {
735                 $r = q("DELETE FROM `item` WHERE `deleted` AND `network` = '%s'", dbesc(NETWORK_TWITTER));
736         }
737
738         require_once "include/items.php";
739
740         logger('twitter_expire: expire_start');
741
742         $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
743         if (DBM::is_result($r)) {
744                 foreach ($r as $rr) {
745                         logger('twitter_expire: user ' . $rr['uid']);
746                         Item::expire($rr['uid'], $days, NETWORK_TWITTER, true);
747                 }
748         }
749
750         logger('twitter_expire: expire_end');
751 }
752
753 function twitter_prepare_body(App $a, &$b)
754 {
755         if ($b["item"]["network"] != NETWORK_TWITTER) {
756                 return;
757         }
758
759         if ($b["preview"]) {
760                 $max_char = 280;
761                 $item = $b["item"];
762                 $item["plink"] = $a->get_baseurl() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
763
764                 $r = q("SELECT `author-link` FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
765                         dbesc($item["thr-parent"]),
766                         intval(local_user()));
767
768                 if (DBM::is_result($r)) {
769                         $orig_post = $r[0];
770
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_new_uri($a->get_hostname(), $uid, NETWORK_TWITTER . ":" . $post->id);
811         $datarray['object'] = json_encode($post);
812         $datarray["title"] = "";
813
814         if (is_object($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                                 // Quickfix: Workaround for URL with "[" and "]" in it
1147                                 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1148                                         $expanded_url = $url->url;
1149                                 }
1150
1151                                 if ($type == "") {
1152                                         $type = $oembed_data->type;
1153                                 }
1154
1155                                 if ($oembed_data->type == "video") {
1156                                         //$body = str_replace($url->url,
1157                                         //              "[video]".$expanded_url."[/video]", $body);
1158                                         //$dontincludemedia = true;
1159                                         $type = $oembed_data->type;
1160                                         $footerurl = $expanded_url;
1161                                         $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1162
1163                                         $body = str_replace($url->url, $footerlink, $body);
1164                                         //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1165                                 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1166                                         $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1167                                         //$dontincludemedia = true;
1168                                 } elseif ($oembed_data->type != "link") {
1169                                         $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1170                                 } else {
1171                                         $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1172
1173                                         $tempfile = tempnam(get_temppath(), "cache");
1174                                         file_put_contents($tempfile, $img_str);
1175                                         $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1176                                         unlink($tempfile);
1177
1178                                         if (substr($mime, 0, 6) == "image/") {
1179                                                 $type = "photo";
1180                                                 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1181                                                 //$dontincludemedia = true;
1182                                         } else {
1183                                                 $type = $oembed_data->type;
1184                                                 $footerurl = $expanded_url;
1185                                                 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1186
1187                                                 $body = str_replace($url->url, $footerlink, $body);
1188                                         }
1189                                 }
1190                         }
1191                 }
1192
1193                 if ($footerurl != "") {
1194                         $footer = add_page_info($footerurl, false, $picture);
1195                 }
1196
1197                 if (($footerlink != "") && (trim($footer) != "")) {
1198                         $removedlink = trim(str_replace($footerlink, "", $body));
1199
1200                         if (($removedlink == "") || strstr($body, $removedlink)) {
1201                                 $body = $removedlink;
1202                         }
1203
1204                         $body .= $footer;
1205                 }
1206
1207                 if (($footer == "") && ($picture != "")) {
1208                         $body .= "\n\n[img]" . $picture . "[/img]\n";
1209                 } elseif (($footer == "") && ($picture == "")) {
1210                         $body = add_page_info_to_body($body);
1211                 }
1212         }
1213
1214         // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1215         $tags = get_tags($body);
1216
1217         if (count($tags)) {
1218                 foreach ($tags as $tag) {
1219                         if (strstr(trim($tag), " ")) {
1220                                 continue;
1221                         }
1222
1223                         if (strpos($tag, '#') === 0) {
1224                                 if (strpos($tag, '[url=')) {
1225                                         continue;
1226                                 }
1227
1228                                 // don't link tags that are already embedded in links
1229                                 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1230                                         continue;
1231                                 }
1232                                 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1233                                         continue;
1234                                 }
1235
1236                                 $basetag = str_replace('_', ' ', substr($tag, 1));
1237                                 $url = '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1238                                 $body = str_replace($tag, $url, $body);
1239                                 $tags_arr["#" . $basetag] = $url;
1240                         } elseif (strpos($tag, '@') === 0) {
1241                                 if (strpos($tag, '[url=')) {
1242                                         continue;
1243                                 }
1244
1245                                 $basetag = substr($tag, 1);
1246                                 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1247                                 $body = str_replace($tag, $url, $body);
1248                                 $tags_arr["@" . $basetag] = $url;
1249                         }
1250                 }
1251         }
1252
1253         $tags = implode($tags_arr, ",");
1254
1255         return ["body" => $body, "tags" => $tags, "plain" => $plain];
1256 }
1257
1258 /**
1259  * @brief Fetch media entities and add media links to the body
1260  *
1261  * @param object $post Twitter object with the post
1262  * @param array $postarray Array of the item that is about to be posted
1263  *
1264  * @return $picture string Image URL or empty string
1265  */
1266 function twitter_media_entities($post, &$postarray)
1267 {
1268         // There are no media entities? So we quit.
1269         if (!is_array($post->extended_entities->media)) {
1270                 return "";
1271         }
1272
1273         // When the post links to an external page, we only take one picture.
1274         // We only do this when there is exactly one media.
1275         if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1276                 $picture = "";
1277                 foreach ($post->extended_entities->media AS $medium) {
1278                         if (isset($medium->media_url_https)) {
1279                                 $picture = $medium->media_url_https;
1280                                 $postarray['body'] = str_replace($medium->url, "", $postarray['body']);
1281                         }
1282                 }
1283                 return $picture;
1284         }
1285
1286         // This is a pure media post, first search for all media urls
1287         $media = [];
1288         foreach ($post->extended_entities->media AS $medium) {
1289                 switch ($medium->type) {
1290                         case 'photo':
1291                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1292                                 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1293                                 break;
1294                         case 'video':
1295                         case 'animated_gif':
1296                                 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1297                                 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1298                                 if (is_array($medium->video_info->variants)) {
1299                                         $bitrate = 0;
1300                                         // We take the video with the highest bitrate
1301                                         foreach ($medium->video_info->variants AS $variant) {
1302                                                 if (($variant->content_type == "video/mp4") && ($variant->bitrate >= $bitrate)) {
1303                                                         $media[$medium->url] = "\n[video]" . $variant->url . "[/video]";
1304                                                         $bitrate = $variant->bitrate;
1305                                                 }
1306                                         }
1307                                 }
1308                                 break;
1309                         // The following code will only be activated for test reasons
1310                         //default:
1311                         //      $postarray['body'] .= print_r($medium, true);
1312                 }
1313         }
1314
1315         // Now we replace the media urls.
1316         foreach ($media AS $key => $value) {
1317                 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1318         }
1319         return "";
1320 }
1321
1322 function twitter_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact, $noquote)
1323 {
1324         $postarray = [];
1325         $postarray['network'] = NETWORK_TWITTER;
1326         $postarray['gravity'] = 0;
1327         $postarray['uid'] = $uid;
1328         $postarray['wall'] = 0;
1329         $postarray['uri'] = "twitter::" . $post->id_str;
1330         $postarray['object'] = json_encode($post);
1331
1332         // Don't import our own comments
1333         $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1334                 dbesc($postarray['uri']),
1335                 intval($uid)
1336         );
1337
1338         if (DBM::is_result($r)) {
1339                 logger("Item with extid " . $postarray['uri'] . " found.", LOGGER_DEBUG);
1340                 return [];
1341         }
1342
1343         $contactid = 0;
1344
1345         if ($post->in_reply_to_status_id_str != "") {
1346                 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1347
1348                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1349                         dbesc($parent),
1350                         intval($uid)
1351                 );
1352                 if (DBM::is_result($r)) {
1353                         $postarray['thr-parent'] = $r[0]["uri"];
1354                         $postarray['parent-uri'] = $r[0]["parent-uri"];
1355                         $postarray['parent'] = $r[0]["parent"];
1356                         $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1357                 } else {
1358                         $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1359                                 dbesc($parent),
1360                                 intval($uid)
1361                         );
1362                         if (DBM::is_result($r)) {
1363                                 $postarray['thr-parent'] = $r[0]['uri'];
1364                                 $postarray['parent-uri'] = $r[0]['parent-uri'];
1365                                 $postarray['parent'] = $r[0]['parent'];
1366                                 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1367                         } else {
1368                                 $postarray['thr-parent'] = $postarray['uri'];
1369                                 $postarray['parent-uri'] = $postarray['uri'];
1370                                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1371                         }
1372                 }
1373
1374                 // Is it me?
1375                 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1376
1377                 if ($post->user->id_str == $own_id) {
1378                         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1379                                 intval($uid));
1380
1381                         if (DBM::is_result($r)) {
1382                                 $contactid = $r[0]["id"];
1383
1384                                 $postarray['owner-name']   = $r[0]["name"];
1385                                 $postarray['owner-link']   = $r[0]["url"];
1386                                 $postarray['owner-avatar'] = $r[0]["photo"];
1387                         } else {
1388                                 logger("No self contact for user " . $uid, LOGGER_DEBUG);
1389                                 return [];
1390                         }
1391                 }
1392                 // Don't create accounts of people who just comment something
1393                 $create_user = false;
1394         } else {
1395                 $postarray['parent-uri'] = $postarray['uri'];
1396                 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1397         }
1398
1399         if ($contactid == 0) {
1400                 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1401
1402                 $postarray['owner-name'] = $post->user->name;
1403                 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1404                 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1405         }
1406
1407         if (($contactid == 0) && !$only_existing_contact) {
1408                 $contactid = $self['id'];
1409         } elseif ($contactid <= 0) {
1410                 logger("Contact ID is zero or less than zero.", LOGGER_DEBUG);
1411                 return [];
1412         }
1413
1414         $postarray['contact-id'] = $contactid;
1415
1416         $postarray['verb'] = ACTIVITY_POST;
1417         $postarray['author-name'] = $postarray['owner-name'];
1418         $postarray['author-link'] = $postarray['owner-link'];
1419         $postarray['author-avatar'] = $postarray['owner-avatar'];
1420         $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1421         $postarray['app'] = strip_tags($post->source);
1422
1423         if ($post->user->protected) {
1424                 $postarray['private'] = 1;
1425                 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1426         }
1427
1428         if (is_string($post->full_text)) {
1429                 $postarray['body'] = $post->full_text;
1430         } else {
1431                 $postarray['body'] = $post->text;
1432         }
1433
1434         // When the post contains links then use the correct object type
1435         if (count($post->entities->urls) > 0) {
1436                 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1437         }
1438
1439         // Search for media links
1440         $picture = twitter_media_entities($post, $postarray);
1441
1442         $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1443         $postarray['body'] = $converted["body"];
1444         $postarray['tag'] = $converted["tags"];
1445         $postarray['created'] = DateTimeFormat::utc($post->created_at);
1446         $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1447
1448         $statustext = $converted["plain"];
1449
1450         if (is_string($post->place->name)) {
1451                 $postarray["location"] = $post->place->name;
1452         }
1453         if (is_string($post->place->full_name)) {
1454                 $postarray["location"] = $post->place->full_name;
1455         }
1456         if (is_array($post->geo->coordinates)) {
1457                 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1458         }
1459         if (is_array($post->coordinates->coordinates)) {
1460                 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1461         }
1462         if (is_object($post->retweeted_status)) {
1463                 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1464
1465                 $retweet['object'] = $postarray['object'];
1466                 $retweet['private'] = $postarray['private'];
1467                 $retweet['allow_cid'] = $postarray['allow_cid'];
1468                 $retweet['contact-id'] = $postarray['contact-id'];
1469                 $retweet['owner-name'] = $postarray['owner-name'];
1470                 $retweet['owner-link'] = $postarray['owner-link'];
1471                 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1472
1473                 $postarray = $retweet;
1474         }
1475
1476         if (is_object($post->quoted_status) && !$noquote) {
1477                 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1478
1479                 $postarray['body'] = $statustext;
1480
1481                 $postarray['body'] .= "\n" . share_header(
1482                         $quoted['author-name'],
1483                         $quoted['author-link'],
1484                         $quoted['author-avatar'],
1485                         "",
1486                         $quoted['created'],
1487                         $quoted['plink']
1488                 );
1489
1490                 $postarray['body'] .= $quoted['body'] . '[/share]';
1491         }
1492
1493         return $postarray;
1494 }
1495
1496 function twitter_checknotification(App $a, $uid, $own_id, $top_item, $postarray)
1497 {
1498         /// TODO: this whole function doesn't seem to work. Needs complete check
1499         $user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` LIMIT 1",
1500                 intval($uid)
1501         );
1502
1503         if (!DBM::is_result($user)) {
1504                 return;
1505         }
1506
1507         // Is it me?
1508         if (link_compare($user[0]["url"], $postarray['author-link'])) {
1509                 return;
1510         }
1511
1512         $own_user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1513                 intval($uid),
1514                 dbesc("twitter::".$own_id)
1515         );
1516
1517         if (!DBM::is_result($own_user)) {
1518                 return;
1519         }
1520
1521         // Is it me from twitter?
1522         if (link_compare($own_user[0]["url"], $postarray['author-link'])) {
1523                 return;
1524         }
1525
1526         $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 AND `deleted` = 0",
1527                 dbesc($postarray['parent-uri']),
1528                 intval($uid)
1529         );
1530
1531         if (DBM::is_result($myconv)) {
1532                 foreach ($myconv as $conv) {
1533                         // now if we find a match, it means we're in this conversation
1534                         if (!link_compare($conv['author-link'], $user[0]["url"]) && !link_compare($conv['author-link'], $own_user[0]["url"])) {
1535                                 continue;
1536                         }
1537
1538                         require_once 'include/enotify.php';
1539
1540                         $conv_parent = $conv['parent'];
1541
1542                         notification([
1543                                 'type' => NOTIFY_COMMENT,
1544                                 'notify_flags' => $user[0]['notify-flags'],
1545                                 'language' => $user[0]['language'],
1546                                 'to_name' => $user[0]['username'],
1547                                 'to_email' => $user[0]['email'],
1548                                 'uid' => $user[0]['uid'],
1549                                 'item' => $postarray,
1550                                 'link' => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($top_item)),
1551                                 'source_name' => $postarray['author-name'],
1552                                 'source_link' => $postarray['author-link'],
1553                                 'source_photo' => $postarray['author-avatar'],
1554                                 'verb' => ACTIVITY_POST,
1555                                 'otype' => 'item',
1556                                 'parent' => $conv_parent,
1557                         ]);
1558
1559                         // only send one notification
1560                         break;
1561                 }
1562         }
1563 }
1564
1565 function twitter_fetchparentposts(App $a, $uid, $post, $connection, $self, $own_id)
1566 {
1567         logger("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, LOGGER_DEBUG);
1568
1569         $posts = [];
1570
1571         while ($post->in_reply_to_status_id_str != "") {
1572                 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1573
1574                 try {
1575                         $post = $connection->get('statuses/show', $parameters);
1576                 } catch (TwitterOAuthException $e) {
1577                         logger('twitter_fetchparentposts: Error fetching for user ' . $uid . ' and post ' . $post->id_str . ': ' . $e->getMessage());
1578                         break;
1579                 }
1580
1581                 if (!count($post)) {
1582                         logger("twitter_fetchparentposts: Can't fetch post " . $parameters->id, LOGGER_DEBUG);
1583                         break;
1584                 }
1585
1586                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1587                         dbesc("twitter::".$post->id_str),
1588                         intval($uid)
1589                 );
1590
1591                 if (DBM::is_result($r)) {
1592                         break;
1593                 }
1594
1595                 $posts[] = $post;
1596         }
1597
1598         logger("twitter_fetchparentposts: Fetching " . count($posts) . " parents", LOGGER_DEBUG);
1599
1600         $posts = array_reverse($posts);
1601
1602         if (count($posts)) {
1603                 foreach ($posts as $post) {
1604                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1605
1606                         if (trim($postarray['body']) == "") {
1607                                 continue;
1608                         }
1609
1610                         $item = Item::insert($postarray);
1611
1612                         if ($notify) {
1613                                 $item = $notify;
1614                         }
1615
1616                         $postarray["id"] = $item;
1617
1618                         logger('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1619
1620                         if ($item && !function_exists("check_item_notification")) {
1621                                 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1622                         }
1623                 }
1624         }
1625 }
1626
1627 function twitter_fetchhometimeline(App $a, $uid)
1628 {
1629         $ckey    = Config::get('twitter', 'consumerkey');
1630         $csecret = Config::get('twitter', 'consumersecret');
1631         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1632         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1633         $create_user = PConfig::get($uid, 'twitter', 'create_user');
1634         $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1635
1636         logger("twitter_fetchhometimeline: Fetching for user " . $uid, LOGGER_DEBUG);
1637
1638         $application_name = Config::get('twitter', 'application_name');
1639
1640         if ($application_name == "") {
1641                 $application_name = $a->get_hostname();
1642         }
1643
1644         require_once 'include/items.php';
1645
1646         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1647
1648         try {
1649                 $own_contact = twitter_fetch_own_contact($a, $uid);
1650         } catch (TwitterOAuthException $e) {
1651                 logger('twitter_fetchhometimeline: Error fetching own contact for user ' . $uid . ': ' . $e->getMessage());
1652                 return;
1653         }
1654
1655         $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1656                 intval($own_contact),
1657                 intval($uid));
1658
1659         if (DBM::is_result($r)) {
1660                 $own_id = $r[0]["nick"];
1661         } else {
1662                 logger("twitter_fetchhometimeline: Own twitter contact not found for user " . $uid, LOGGER_DEBUG);
1663                 return;
1664         }
1665
1666         $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1667                 intval($uid));
1668
1669         if (DBM::is_result($r)) {
1670                 $self = $r[0];
1671         } else {
1672                 logger("twitter_fetchhometimeline: Own contact not found for user " . $uid, LOGGER_DEBUG);
1673                 return;
1674         }
1675
1676         $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1677                 intval($uid));
1678         if (!DBM::is_result($u)) {
1679                 logger("twitter_fetchhometimeline: Own user not found for user " . $uid, LOGGER_DEBUG);
1680                 return;
1681         }
1682
1683         $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1684         //$parameters["count"] = 200;
1685         // Fetching timeline
1686         $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1687
1688         $first_time = ($lastid == "");
1689
1690         if ($lastid != "") {
1691                 $parameters["since_id"] = $lastid;
1692         }
1693
1694         try {
1695                 $items = $connection->get('statuses/home_timeline', $parameters);
1696         } catch (TwitterOAuthException $e) {
1697                 logger('twitter_fetchhometimeline: Error fetching home timeline: ' . $e->getMessage());
1698                 return;
1699         }
1700
1701         if (!is_array($items)) {
1702                 logger("twitter_fetchhometimeline: Error fetching home timeline: " . print_r($items, true), LOGGER_DEBUG);
1703                 return;
1704         }
1705
1706         $posts = array_reverse($items);
1707
1708         logger("twitter_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1709
1710         if (count($posts)) {
1711                 foreach ($posts as $post) {
1712                         if ($post->id_str > $lastid) {
1713                                 $lastid = $post->id_str;
1714                                 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1715                         }
1716
1717                         if ($first_time) {
1718                                 continue;
1719                         }
1720
1721                         if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1722                                 logger("twitter_fetchhometimeline: Skip previously sended post", LOGGER_DEBUG);
1723                                 continue;
1724                         }
1725
1726                         if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1727                                 logger("twitter_fetchhometimeline: Skip post that will be mirrored", LOGGER_DEBUG);
1728                                 continue;
1729                         }
1730
1731                         if ($post->in_reply_to_status_id_str != "") {
1732                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1733                         }
1734
1735                         $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1736
1737                         if (trim($postarray['body']) == "") {
1738                                 continue;
1739                         }
1740
1741                         $notify = false;
1742
1743                         if ($postarray['uri'] == $postarray['parent-uri']) {
1744                                 $contact = dba::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
1745                                 if (DBM::is_result($contact)) {
1746                                         $notify = Item::isRemoteSelf($contact, $postarray);
1747                                 }
1748                         }
1749
1750                         $item = Item::insert($postarray, false, $notify);
1751                         $postarray["id"] = $item;
1752
1753                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1754
1755                         if ($item && !function_exists("check_item_notification")) {
1756                                 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1757                         }
1758                 }
1759         }
1760         PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1761
1762         // Fetching mentions
1763         $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1764
1765         $first_time = ($lastid == "");
1766
1767         if ($lastid != "") {
1768                 $parameters["since_id"] = $lastid;
1769         }
1770
1771         try {
1772                 $items = $connection->get('statuses/mentions_timeline', $parameters);
1773         } catch (TwitterOAuthException $e) {
1774                 logger('twitter_fetchhometimeline: Error fetching mentions: ' . $e->getMessage());
1775                 return;
1776         }
1777
1778         if (!is_array($items)) {
1779                 logger("twitter_fetchhometimeline: Error fetching mentions: " . print_r($items, true), LOGGER_DEBUG);
1780                 return;
1781         }
1782
1783         $posts = array_reverse($items);
1784
1785         logger("twitter_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1786
1787         if (count($posts)) {
1788                 foreach ($posts as $post) {
1789                         if ($post->id_str > $lastid) {
1790                                 $lastid = $post->id_str;
1791                         }
1792
1793                         if ($first_time) {
1794                                 continue;
1795                         }
1796
1797                         if ($post->in_reply_to_status_id_str != "") {
1798                                 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1799                         }
1800
1801                         $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1802
1803                         if (trim($postarray['body']) == "") {
1804                                 continue;
1805                         }
1806
1807                         $item = Item::insert($postarray);
1808                         $postarray["id"] = $item;
1809
1810                         if ($item && function_exists("check_item_notification")) {
1811                                 check_item_notification($item, $uid, NOTIFY_TAGSELF);
1812                         }
1813
1814                         if (!isset($postarray["parent"]) || ($postarray["parent"] == 0)) {
1815                                 $postarray["parent"] = $item;
1816                         }
1817
1818                         logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1819
1820                         if ($item == 0) {
1821                                 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1822                                         dbesc($postarray['uri']),
1823                                         intval($uid)
1824                                 );
1825                                 if (DBM::is_result($r)) {
1826                                         $item = $r[0]['id'];
1827                                         $parent_id = $r[0]['parent'];
1828                                 }
1829                         } else {
1830                                 $parent_id = $postarray['parent'];
1831                         }
1832
1833                         if (($item != 0) && !function_exists("check_item_notification")) {
1834                                 require_once 'include/enotify.php';
1835                                 notification([
1836                                         'type'         => NOTIFY_TAGSELF,
1837                                         'notify_flags' => $u[0]['notify-flags'],
1838                                         'language'     => $u[0]['language'],
1839                                         'to_name'      => $u[0]['username'],
1840                                         'to_email'     => $u[0]['email'],
1841                                         'uid'          => $u[0]['uid'],
1842                                         'item'         => $postarray,
1843                                         'link'         => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($item)),
1844                                         'source_name'  => $postarray['author-name'],
1845                                         'source_link'  => $postarray['author-link'],
1846                                         'source_photo' => $postarray['author-avatar'],
1847                                         'verb'         => ACTIVITY_TAG,
1848                                         'otype'        => 'item',
1849                                         'parent'       => $parent_id
1850                                 ]);
1851                         }
1852                 }
1853         }
1854
1855         PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1856 }
1857
1858 function twitter_fetch_own_contact(App $a, $uid)
1859 {
1860         $ckey    = Config::get('twitter', 'consumerkey');
1861         $csecret = Config::get('twitter', 'consumersecret');
1862         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1863         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1864
1865         $own_id = PConfig::get($uid, 'twitter', 'own_id');
1866
1867         $contact_id = 0;
1868
1869         if ($own_id == "") {
1870                 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1871
1872                 // Fetching user data
1873                 // get() may throw TwitterOAuthException, but we will catch it later
1874                 $user = $connection->get('account/verify_credentials');
1875
1876                 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1877
1878                 $contact_id = twitter_fetch_contact($uid, $user, true);
1879         } else {
1880                 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1881                         intval($uid),
1882                         dbesc("twitter::" . $own_id));
1883                 if (DBM::is_result($r)) {
1884                         $contact_id = $r[0]["id"];
1885                 } else {
1886                         PConfig::delete($uid, 'twitter', 'own_id');
1887                 }
1888         }
1889
1890         return $contact_id;
1891 }
1892
1893 function twitter_is_retweet(App $a, $uid, $body)
1894 {
1895         $body = trim($body);
1896
1897         // Skip if it isn't a pure repeated messages
1898         // Does it start with a share?
1899         if (strpos($body, "[share") > 0) {
1900                 return false;
1901         }
1902
1903         // Does it end with a share?
1904         if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1905                 return false;
1906         }
1907
1908         $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1909         // Skip if there is no shared message in there
1910         if ($body == $attributes) {
1911                 return false;
1912         }
1913
1914         $link = "";
1915         preg_match("/link='(.*?)'/ism", $attributes, $matches);
1916         if ($matches[1] != "") {
1917                 $link = $matches[1];
1918         }
1919
1920         preg_match('/link="(.*?)"/ism', $attributes, $matches);
1921         if ($matches[1] != "") {
1922                 $link = $matches[1];
1923         }
1924
1925         $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1926         if ($id == $link) {
1927                 return false;
1928         }
1929
1930         logger('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, LOGGER_DEBUG);
1931
1932         $ckey    = Config::get('twitter', 'consumerkey');
1933         $csecret = Config::get('twitter', 'consumersecret');
1934         $otoken  = PConfig::get($uid, 'twitter', 'oauthtoken');
1935         $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1936
1937         $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1938         $result = $connection->post('statuses/retweet/' . $id);
1939
1940         logger('twitter_is_retweet: result ' . print_r($result, true), LOGGER_DEBUG);
1941
1942         return !isset($result->errors);
1943 }