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