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