3 * Name: Twitter Connector
4 * Description: Bidirectional (posting, relaying and reading) connector for Twitter.
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>
10 * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel, Hypolite Petovan
11 * All rights reserved.
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.
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.
36 /* Twitter Addon for Friendica
38 * Author: Tobias Diekershoff
39 * tobias.diekershoff@gmx.net
41 * License:3-clause BSD license
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
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.
51 * Add this key pair to your global config/addon.ini.php or use the admin panel.
54 * consumerkey = your consumer_key here
55 * consumersecret = your consumer_secret here
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".
61 * Requirements: PHP5, curl
64 use Abraham\TwitterOAuth\TwitterOAuth;
65 use Abraham\TwitterOAuth\TwitterOAuthException;
67 use Friendica\Content\OEmbed;
68 use Friendica\Content\Text\Plaintext;
69 use Friendica\Core\Addon;
70 use Friendica\Core\Config;
71 use Friendica\Core\L10n;
72 use Friendica\Core\Logger;
73 use Friendica\Core\PConfig;
74 use Friendica\Core\Protocol;
75 use Friendica\Core\Renderer;
76 use Friendica\Core\Worker;
77 use Friendica\Database\DBA;
78 use Friendica\Model\Contact;
79 use Friendica\Model\Conversation;
80 use Friendica\Model\GContact;
81 use Friendica\Model\Group;
82 use Friendica\Model\Item;
83 use Friendica\Model\ItemContent;
84 use Friendica\Model\Queue;
85 use Friendica\Model\User;
86 use Friendica\Object\Image;
87 use Friendica\Util\DateTimeFormat;
88 use Friendica\Util\Network;
89 use Friendica\Util\Strings;
91 require_once 'boot.php';
92 require_once 'include/dba.php';
93 require_once 'include/enotify.php';
94 require_once 'include/text.php';
96 require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
98 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
100 function twitter_install()
102 // we need some hooks, for the configuration and for sending tweets
103 Addon::registerHook('load_config' , __FILE__, 'twitter_load_config');
104 Addon::registerHook('connector_settings' , __FILE__, 'twitter_settings');
105 Addon::registerHook('connector_settings_post', __FILE__, 'twitter_settings_post');
106 Addon::registerHook('hook_fork' , __FILE__, 'twitter_hook_fork');
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");
119 function twitter_uninstall()
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('hook_fork' , __FILE__, 'twitter_hook_fork');
125 Addon::unregisterHook('post_local' , __FILE__, 'twitter_post_local');
126 Addon::unregisterHook('notifier_normal' , __FILE__, 'twitter_post_hook');
127 Addon::unregisterHook('jot_networks' , __FILE__, 'twitter_jot_nets');
128 Addon::unregisterHook('cron' , __FILE__, 'twitter_cron');
129 Addon::unregisterHook('queue_predeliver' , __FILE__, 'twitter_queue_hook');
130 Addon::unregisterHook('follow' , __FILE__, 'twitter_follow');
131 Addon::unregisterHook('expire' , __FILE__, 'twitter_expire');
132 Addon::unregisterHook('prepare_body' , __FILE__, 'twitter_prepare_body');
133 Addon::unregisterHook('check_item_notification', __FILE__, 'twitter_check_item_notification');
135 // old setting - remove only
136 Addon::unregisterHook('post_local_end' , __FILE__, 'twitter_post_hook');
137 Addon::unregisterHook('addon_settings' , __FILE__, 'twitter_settings');
138 Addon::unregisterHook('addon_settings_post', __FILE__, 'twitter_settings_post');
141 function twitter_load_config(App $a)
143 $a->loadConfigFile(__DIR__ . '/config/twitter.ini.php');
146 function twitter_check_item_notification(App $a, array &$notification_data)
148 $own_id = PConfig::get($notification_data["uid"], 'twitter', 'own_id');
150 $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
151 intval($notification_data["uid"]),
152 DBA::escape("twitter::".$own_id)
156 $notification_data["profiles"][] = $own_user[0]["url"];
160 function twitter_follow(App $a, array &$contact)
162 Logger::log("twitter_follow: Check if contact is twitter contact. " . $contact["url"], Logger::DEBUG);
164 if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
168 // contact seems to be a twitter contact, so continue
169 $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
170 $nickname = str_replace("@twitter.com", "", $nickname);
172 $uid = $a->user["uid"];
174 $ckey = Config::get('twitter', 'consumerkey');
175 $csecret = Config::get('twitter', 'consumersecret');
176 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
177 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
179 // If the addon is not configured (general or for this user) quit here
180 if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
185 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
186 $connection->post('friendships/create', ['screen_name' => $nickname]);
188 twitter_fetchuser($a, $uid, $nickname);
190 $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
191 FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
193 DBA::escape($nickname));
194 if (DBA::isResult($r)) {
195 $contact["contact"] = $r[0];
199 function twitter_jot_nets(App $a, &$b)
205 $tw_post = PConfig::get(local_user(), 'twitter', 'post');
206 if (intval($tw_post) == 1) {
207 $tw_defpost = PConfig::get(local_user(), 'twitter', 'post_by_default');
208 $selected = ((intval($tw_defpost) == 1) ? ' checked="checked" ' : '');
209 $b .= '<div class="profile-jot-net"><input type="checkbox" name="twitter_enable"' . $selected . ' value="1" /> '
210 . L10n::t('Post to Twitter') . '</div>';
214 function twitter_settings_post(App $a)
219 // don't check twitter settings if twitter submit button is not clicked
220 if (empty($_POST['twitter-disconnect']) && empty($_POST['twitter-submit'])) {
224 if (!empty($_POST['twitter-disconnect'])) {
226 * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
227 * from the user configuration
229 PConfig::delete(local_user(), 'twitter', 'consumerkey');
230 PConfig::delete(local_user(), 'twitter', 'consumersecret');
231 PConfig::delete(local_user(), 'twitter', 'oauthtoken');
232 PConfig::delete(local_user(), 'twitter', 'oauthsecret');
233 PConfig::delete(local_user(), 'twitter', 'post');
234 PConfig::delete(local_user(), 'twitter', 'post_by_default');
235 PConfig::delete(local_user(), 'twitter', 'lastid');
236 PConfig::delete(local_user(), 'twitter', 'mirror_posts');
237 PConfig::delete(local_user(), 'twitter', 'import');
238 PConfig::delete(local_user(), 'twitter', 'create_user');
239 PConfig::delete(local_user(), 'twitter', 'own_id');
241 if (isset($_POST['twitter-pin'])) {
242 // if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
243 Logger::log('got a Twitter PIN');
244 $ckey = Config::get('twitter', 'consumerkey');
245 $csecret = Config::get('twitter', 'consumersecret');
246 // the token and secret for which the PIN was generated were hidden in the settings
247 // form as token and token2, we need a new connection to Twitter using these token
248 // and secret to request a Access Token with the PIN
250 if (empty($_POST['twitter-pin'])) {
251 throw new Exception(L10n::t('You submitted an empty PIN, please Sign In with Twitter again to get a new one.'));
254 $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
255 $token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $_POST['twitter-pin']]);
256 // ok, now that we have the Access Token, save them in the user config
257 PConfig::set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
258 PConfig::set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
259 PConfig::set(local_user(), 'twitter', 'post', 1);
260 } catch(Exception $e) {
261 info($e->getMessage());
262 } catch(TwitterOAuthException $e) {
263 info($e->getMessage());
265 // reload the Addon Settings page, if we don't do it see Bug #42
266 $a->internalRedirect('settings/connectors');
268 // if no PIN is supplied in the POST variables, the user has changed the setting
269 // to post a tweet for every new __public__ posting to the wall
270 PConfig::set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
271 PConfig::set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
272 PConfig::set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
273 PConfig::set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
274 PConfig::set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
276 if (!intval($_POST['twitter-mirror'])) {
277 PConfig::delete(local_user(), 'twitter', 'lastid');
280 info(L10n::t('Twitter settings updated.') . EOL);
285 function twitter_settings(App $a, &$s)
290 $a->page['htmlhead'] .= '<link rel="stylesheet" type="text/css" href="' . $a->getBaseURL() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
292 * 1) Check that we have global consumer key & secret
293 * 2) If no OAuthtoken & stuff is present, generate button to get some
294 * 3) Checkbox for "Send public notices (280 chars only)
296 $ckey = Config::get('twitter', 'consumerkey');
297 $csecret = Config::get('twitter', 'consumersecret');
298 $otoken = PConfig::get(local_user(), 'twitter', 'oauthtoken');
299 $osecret = PConfig::get(local_user(), 'twitter', 'oauthsecret');
301 $enabled = intval(PConfig::get(local_user(), 'twitter', 'post'));
302 $defenabled = intval(PConfig::get(local_user(), 'twitter', 'post_by_default'));
303 $mirrorenabled = intval(PConfig::get(local_user(), 'twitter', 'mirror_posts'));
304 $importenabled = intval(PConfig::get(local_user(), 'twitter', 'import'));
305 $create_userenabled = intval(PConfig::get(local_user(), 'twitter', 'create_user'));
307 $css = (($enabled) ? '' : '-disabled');
309 $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
310 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
312 $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
313 $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
314 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
317 if ((!$ckey) && (!$csecret)) {
318 /* no global consumer keys
319 * display warning and skip personal config
321 $s .= '<p>' . L10n::t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
323 // ok we have a consumer key pair now look into the OAuth stuff
324 if ((!$otoken) && (!$osecret)) {
325 /* the user has not yet connected the account to twitter...
326 * get a temporary OAuth key/secret pair and display a button with
327 * which the user can request a PIN to connect the account to a
328 * account at Twitter.
330 $connection = new TwitterOAuth($ckey, $csecret);
332 $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
333 $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>';
334 $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>';
335 $s .= '<div id="twitter-pin-wrapper">';
336 $s .= '<label id="twitter-pin-label" for="twitter-pin">' . L10n::t('Copy the PIN from Twitter here') . '</label>';
337 $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
338 $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result['oauth_token'] . '" />';
339 $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result['oauth_token_secret'] . '" />';
340 $s .= '</div><div class="clear"></div>';
341 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
342 } catch (TwitterOAuthException $e) {
343 $s .= '<p>' . L10n::t('An error occured: ') . $e->getMessage() . '</p>';
347 * we have an OAuth key / secret pair for the user
348 * so let's give a chance to disable the postings to Twitter
350 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
352 $details = $connection->get('account/verify_credentials');
354 $field_checkbox = Renderer::getMarkupTemplate('field_checkbox.tpl');
356 $s .= '<div id="twitter-info" >
357 <p>' . L10n::t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
358 <button type="submit" name="twitter-disconnect" value="1">' . L10n::t('Disconnect') . '</button>
360 <p id="twitter-info-block">
361 <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
362 <em>' . $details->description . '</em>
365 $s .= '<div class="clear"></div>';
367 $s .= Renderer::replaceMacros($field_checkbox, [
368 '$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.')]
370 if ($a->user['hidewall']) {
371 $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>';
373 $s .= Renderer::replaceMacros($field_checkbox, [
374 '$field' => ['twitter-default', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
376 $s .= Renderer::replaceMacros($field_checkbox, [
377 '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
379 $s .= Renderer::replaceMacros($field_checkbox, [
380 '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
382 $s .= Renderer::replaceMacros($field_checkbox, [
383 '$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.')]
385 $s .= '<div class="clear"></div>';
386 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
387 } catch (TwitterOAuthException $e) {
388 $s .= '<p>' . L10n::t('An error occured: ') . $e->getMessage() . '</p>';
392 $s .= '</div><div class="clear"></div>';
395 function twitter_hook_fork(App $a, array &$b)
397 if ($b['name'] != 'notifier_normal') {
403 // Deleting and editing is not supported by the addon (deleting could, but isn't by now)
404 if ($post['deleted'] || ($post['created'] !== $post['edited'])) {
405 $b['execute'] = false;
409 // if post comes from twitter don't send it back
410 if ($post['extid'] == Protocol::TWITTER) {
411 $b['execute'] = false;
415 if ($post['app'] == 'Twitter') {
416 $b['execute'] = false;
420 if (PConfig::get($post['uid'], 'twitter', 'import')) {
421 // Don't fork if it isn't a reply to a twitter post
422 if (($post['parent'] != $post['id']) && !Item::exists(['id' => $post['parent'], 'network' => Protocol::TWITTER])) {
423 Logger::log('No twitter parent found for item ' . $post['id']);
424 $b['execute'] = false;
428 // Comments are never exported when we don't import the twitter timeline
429 if (!strstr($post['postopts'], 'twitter') || ($post['parent'] != $post['id']) || $post['private']) {
430 $b['execute'] = false;
436 function twitter_post_local(App $a, array &$b)
442 if (!local_user() || (local_user() != $b['uid'])) {
446 $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
447 $twitter_enable = (($twitter_post && x($_REQUEST, 'twitter_enable')) ? intval($_REQUEST['twitter_enable']) : 0);
449 // if API is used, default to the chosen settings
450 if ($b['api_source'] && intval(PConfig::get(local_user(), 'twitter', 'post_by_default'))) {
454 if (!$twitter_enable) {
458 if (strlen($b['postopts'])) {
459 $b['postopts'] .= ',';
462 $b['postopts'] .= 'twitter';
465 function twitter_action(App $a, $uid, $pid, $action)
467 $ckey = Config::get('twitter', 'consumerkey');
468 $csecret = Config::get('twitter', 'consumersecret');
469 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
470 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
472 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
474 $post = ['id' => $pid];
476 Logger::log("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), Logger::DATA);
480 // To-Do: $result = $connection->post('statuses/destroy', $post);
484 $result = $connection->post('favorites/create', $post);
487 $result = $connection->post('favorites/destroy', $post);
490 Logger::log('Unhandled action ' . $action, Logger::DEBUG);
493 Logger::log("twitter_action '" . $action . "' send, result: " . print_r($result, true), Logger::DEBUG);
496 function twitter_post_hook(App $a, array &$b)
499 if (!PConfig::get($b["uid"], 'twitter', 'import')
500 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
504 if ($b['parent'] != $b['id']) {
505 Logger::log("twitter_post_hook: parameter " . print_r($b, true), Logger::DATA);
507 // Looking if its a reply to a twitter post
508 if ((substr($b["parent-uri"], 0, 9) != "twitter::")
509 && (substr($b["extid"], 0, 9) != "twitter::")
510 && (substr($b["thr-parent"], 0, 9) != "twitter::"))
512 Logger::log("twitter_post_hook: no twitter post " . $b["parent"]);
516 $condition = ['uri' => $b["thr-parent"], 'uid' => $b["uid"]];
517 $orig_post = Item::selectFirst([], $condition);
518 if (!DBA::isResult($orig_post)) {
519 Logger::log("twitter_post_hook: no parent found " . $b["thr-parent"]);
526 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
527 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
528 $nicknameplain = "@" . $nicknameplain;
530 Logger::log("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], Logger::DEBUG);
531 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
532 $b["body"] = $nickname . " " . $b["body"];
535 Logger::log("twitter_post_hook: parent found " . print_r($orig_post, true), Logger::DATA);
539 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
543 // Dont't post if the post doesn't belong to us.
544 // This is a check for forum postings
545 $self = DBA::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
546 if ($b['contact-id'] != $self['id']) {
551 if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
552 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
555 if ($b['verb'] == ACTIVITY_LIKE) {
556 Logger::log("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), Logger::DEBUG);
558 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
560 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
566 if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
570 // if post comes from twitter don't send it back
571 if ($b['extid'] == Protocol::TWITTER) {
575 if ($b['app'] == "Twitter") {
579 Logger::log('twitter post invoked');
581 PConfig::load($b['uid'], 'twitter');
583 $ckey = Config::get('twitter', 'consumerkey');
584 $csecret = Config::get('twitter', 'consumersecret');
585 $otoken = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
586 $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
588 if ($ckey && $csecret && $otoken && $osecret) {
589 Logger::log('twitter: we have customer key and oauth stuff, going to send.', Logger::DEBUG);
591 // If it's a repeated message from twitter then do a native retweet and exit
592 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
596 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
598 // Set the timeout for upload to 30 seconds
599 $connection->setTimeouts(10, 30);
603 // Handling non-native reshares
604 $b['body'] = Friendica\Content\Text\BBCode::convertShare(
606 function (array $attributes, array $author_contact, $content, $is_quote_share) {
607 return twitter_convert_share($attributes, $author_contact, $content, $is_quote_share);
611 $b['body'] = twitter_update_mentions($b['body']);
613 $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, 8);
614 $msg = $msgarr["text"];
616 if (($msg == "") && isset($msgarr["title"])) {
617 $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
622 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
623 $msg .= "\n" . $msgarr["url"];
629 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
630 $image = $msgarr["image"];
637 // and now tweet it :-)
640 if (!empty($image)) {
642 $img_str = Network::fetchUrl($image);
644 $tempfile = tempnam(get_temppath(), 'cache');
645 file_put_contents($tempfile, $img_str);
647 $media = $connection->upload('media/upload', ['media' => $tempfile]);
651 if (isset($media->media_id_string)) {
652 $post['media_ids'] = $media->media_id_string;
654 throw new Exception('Failed upload of ' . $image);
656 } catch (Exception $e) {
657 Logger::log('Exception when trying to send to Twitter: ' . $e->getMessage());
659 // Workaround: Remove the picture link so that the post can be reposted without it
660 // When there is another url already added, a second url would be superfluous.
662 $msg .= "\n" . $image;
669 $post['status'] = $msg;
672 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
675 $url = 'statuses/update';
676 $result = $connection->post($url, $post);
677 Logger::log('twitter_post send, result: ' . print_r($result, true), Logger::DEBUG);
679 if (!empty($result->source)) {
680 Config::set("twitter", "application_name", strip_tags($result->source));
683 if (!empty($result->errors)) {
684 Logger::log('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
686 $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
687 if (DBA::isResult($r)) {
688 $a->contact = $r[0]["id"];
691 $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
693 Queue::add($a->contact, Protocol::TWITTER, $s);
694 notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
695 } elseif ($iscomment) {
696 Logger::log('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
697 Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
702 function twitter_addon_admin_post(App $a)
704 $consumerkey = x($_POST, 'consumerkey') ? Strings::escapeTags(trim($_POST['consumerkey'])) : '';
705 $consumersecret = x($_POST, 'consumersecret') ? Strings::escapeTags(trim($_POST['consumersecret'])) : '';
706 Config::set('twitter', 'consumerkey', $consumerkey);
707 Config::set('twitter', 'consumersecret', $consumersecret);
708 info(L10n::t('Settings updated.') . EOL);
711 function twitter_addon_admin(App $a, &$o)
713 $t = Renderer::getMarkupTemplate("admin.tpl", "addon/twitter/");
715 $o = Renderer::replaceMacros($t, [
716 '$submit' => L10n::t('Save Settings'),
717 // name, label, value, help, [extra values]
718 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
719 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
723 function twitter_cron(App $a)
725 $last = Config::get('twitter', 'last_poll');
727 $poll_interval = intval(Config::get('twitter', 'poll_interval'));
728 if (!$poll_interval) {
729 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
733 $next = $last + ($poll_interval * 60);
734 if ($next > time()) {
735 Logger::log('twitter: poll intervall not reached');
739 Logger::log('twitter: cron_start');
741 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
742 if (DBA::isResult($r)) {
743 foreach ($r as $rr) {
744 Logger::log('twitter: fetching for user ' . $rr['uid']);
745 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
749 $abandon_days = intval(Config::get('system', 'account_abandon_days'));
750 if ($abandon_days < 1) {
754 $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
756 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
757 if (DBA::isResult($r)) {
758 foreach ($r as $rr) {
759 if ($abandon_days != 0) {
760 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
761 if (!DBA::isResult($user)) {
762 Logger::log('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
767 Logger::log('twitter: importing timeline from user ' . $rr['uid']);
768 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
771 // check for new contacts once a day
772 $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
773 if($last_contact_check)
774 $next_contact_check = $last_contact_check + 86400;
776 $next_contact_check = 0;
778 if($next_contact_check <= time()) {
779 pumpio_getallusers($a, $rr["uid"]);
780 PConfig::set($rr['uid'],'pumpio','contact_check',time());
786 Logger::log('twitter: cron_end');
788 Config::set('twitter', 'last_poll', time());
791 function twitter_expire(App $a)
793 $days = Config::get('twitter', 'expire');
799 $r = Item::select(['id'], ['deleted' => true, 'network' => Protocol::TWITTER]);
800 while ($row = DBA::fetch($r)) {
801 DBA::delete('item', ['id' => $row['id']]);
805 require_once "include/items.php";
807 Logger::log('twitter_expire: expire_start');
809 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
810 if (DBA::isResult($r)) {
811 foreach ($r as $rr) {
812 Logger::log('twitter_expire: user ' . $rr['uid']);
813 Item::expire($rr['uid'], $days, Protocol::TWITTER, true);
817 Logger::log('twitter_expire: expire_end');
820 function twitter_prepare_body(App $a, array &$b)
822 if ($b["item"]["network"] != Protocol::TWITTER) {
829 $item["plink"] = $a->getBaseURL() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
831 $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
832 $orig_post = Item::selectFirst(['author-link'], $condition);
833 if (DBA::isResult($orig_post)) {
834 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
835 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
836 $nicknameplain = "@" . $nicknameplain;
838 if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
839 $item["body"] = $nickname . " " . $item["body"];
843 $msgarr = ItemContent::getPlaintextPost($item, $max_char, true, 8);
844 $msg = $msgarr["text"];
846 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
847 $msg .= " " . $msgarr["url"];
850 if (isset($msgarr["image"])) {
851 $msg .= " " . $msgarr["image"];
854 $b['html'] = nl2br(htmlspecialchars($msg));
859 * @brief Build the item array for the mirrored post
861 * @param App $a Application class
862 * @param integer $uid User id
863 * @param object $post Twitter object with the post
865 * @return array item data to be posted
867 function twitter_do_mirrorpost(App $a, $uid, $post)
869 $datarray['api_source'] = true;
870 $datarray['profile_uid'] = $uid;
871 $datarray['extid'] = Protocol::TWITTER;
872 $datarray['message_id'] = Item::newURI($uid, Protocol::TWITTER . ':' . $post->id);
873 $datarray['protocol'] = Conversation::PARCEL_TWITTER;
874 $datarray['source'] = json_encode($post);
875 $datarray['title'] = '';
877 if (!empty($post->retweeted_status)) {
878 // We don't support nested shares, so we mustn't show quotes as shares on retweets
879 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
881 if (empty($item['body'])) {
885 $datarray['body'] = "\n" . share_header(
886 $item['author-name'],
887 $item['author-link'],
888 $item['author-avatar'],
894 $datarray['body'] .= $item['body'] . '[/share]';
896 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
898 if (empty($item['body'])) {
902 $datarray['body'] = $item['body'];
905 $datarray['source'] = $item['app'];
906 $datarray['verb'] = $item['verb'];
908 if (isset($item['location'])) {
909 $datarray['location'] = $item['location'];
912 if (isset($item['coord'])) {
913 $datarray['coord'] = $item['coord'];
919 function twitter_fetchtimeline(App $a, $uid)
921 $ckey = Config::get('twitter', 'consumerkey');
922 $csecret = Config::get('twitter', 'consumersecret');
923 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
924 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
925 $lastid = PConfig::get($uid, 'twitter', 'lastid');
927 $application_name = Config::get('twitter', 'application_name');
929 if ($application_name == "") {
930 $application_name = $a->getHostName();
933 $has_picture = false;
935 require_once 'mod/item.php';
936 require_once 'include/items.php';
937 require_once 'mod/share.php';
939 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
941 $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
943 $first_time = ($lastid == "");
946 $parameters["since_id"] = $lastid;
950 $items = $connection->get('statuses/user_timeline', $parameters);
951 } catch (TwitterOAuthException $e) {
952 Logger::log('Error fetching timeline for user ' . $uid . ': ' . $e->getMessage());
956 if (!is_array($items)) {
957 Logger::log('No items for user ' . $uid, Logger::INFO);
961 $posts = array_reverse($items);
963 Logger::log('Starting from ID ' . $lastid . ' for user ' . $uid, Logger::DEBUG);
966 foreach ($posts as $post) {
967 if ($post->id_str > $lastid) {
968 $lastid = $post->id_str;
969 PConfig::set($uid, 'twitter', 'lastid', $lastid);
976 if (!stristr($post->source, $application_name)) {
977 $_SESSION["authenticated"] = true;
978 $_SESSION["uid"] = $uid;
980 Logger::log('Preparing Twitter ID ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
982 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
984 if (empty($_REQUEST['body'])) {
988 Logger::log('Posting Twitter ID ' . $post->id_str . ' for user ' . $uid);
994 PConfig::set($uid, 'twitter', 'lastid', $lastid);
995 Logger::log('Last ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
998 function twitter_queue_hook(App $a)
1000 $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
1001 DBA::escape(Protocol::TWITTER)
1003 if (!DBA::isResult($qi)) {
1007 foreach ($qi as $x) {
1008 if ($x['network'] !== Protocol::TWITTER) {
1012 Logger::log('twitter_queue: run');
1014 $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
1015 WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
1018 if (!DBA::isResult($r)) {
1024 $ckey = Config::get('twitter', 'consumerkey');
1025 $csecret = Config::get('twitter', 'consumersecret');
1026 $otoken = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
1027 $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
1031 if ($ckey && $csecret && $otoken && $osecret) {
1032 Logger::log('twitter_queue: able to post');
1034 $z = unserialize($x['content']);
1036 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1037 $result = $connection->post($z['url'], $z['post']);
1039 Logger::log('twitter_queue: post result: ' . print_r($result, true), Logger::DEBUG);
1041 if ($result->errors) {
1042 Logger::log('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
1045 Queue::removeItem($x['id']);
1048 Logger::log("twitter_queue: Error getting tokens for user " . $user['uid']);
1052 Logger::log('twitter_queue: delayed');
1053 Queue::updateTime($x['id']);
1058 function twitter_fix_avatar($avatar)
1060 $new_avatar = str_replace("_normal.", ".", $avatar);
1062 $info = Image::getInfoFromURL($new_avatar);
1064 $new_avatar = $avatar;
1070 function twitter_fetch_contact($uid, $data, $create_user)
1072 if (empty($data->id_str)) {
1076 $avatar = twitter_fix_avatar($data->profile_image_url_https);
1077 $url = "https://twitter.com/" . $data->screen_name;
1078 $addr = $data->screen_name . "@twitter.com";
1080 GContact::update(["url" => $url, "network" => Protocol::TWITTER,
1081 "photo" => $avatar, "hide" => true,
1082 "name" => $data->name, "nick" => $data->screen_name,
1083 "location" => $data->location, "about" => $data->description,
1084 "addr" => $addr, "generation" => 2]);
1086 $fields = ['url' => $url, 'network' => Protocol::TWITTER,
1087 'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
1088 'location' => $data->location, 'about' => $data->description];
1090 $cid = Contact::getIdForURL($url, 0, true, $fields);
1092 DBA::update('contact', $fields, ['id' => $cid]);
1093 Contact::updateAvatar($avatar, 0, $cid);
1096 $contact = DBA::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
1097 if (!DBA::isResult($contact) && !$create_user) {
1101 if (!DBA::isResult($contact)) {
1102 // create contact record
1103 $fields['uid'] = $uid;
1104 $fields['created'] = DateTimeFormat::utcNow();
1105 $fields['nurl'] = Strings::normaliseLink($url);
1106 $fields['alias'] = 'twitter::' . $data->id_str;
1107 $fields['poll'] = 'twitter::' . $data->id_str;
1108 $fields['rel'] = Contact::FRIEND;
1109 $fields['priority'] = 1;
1110 $fields['writable'] = true;
1111 $fields['blocked'] = false;
1112 $fields['readonly'] = false;
1113 $fields['pending'] = false;
1115 if (!DBA::insert('contact', $fields)) {
1119 $contact_id = DBA::lastInsertId();
1121 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1123 Contact::updateAvatar($avatar, $uid, $contact_id);
1125 if ($contact["readonly"] || $contact["blocked"]) {
1126 Logger::log("twitter_fetch_contact: Contact '" . $contact["nick"] . "' is blocked or readonly.", Logger::DEBUG);
1130 $contact_id = $contact['id'];
1132 // update profile photos once every twelve hours as we have no notification of when they change.
1133 $update_photo = ($contact['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
1135 // check that we have all the photos, this has been known to fail on occasion
1136 if (empty($contact['photo']) || empty($contact['thumb']) || empty($contact['micro']) || $update_photo) {
1137 Logger::log("twitter_fetch_contact: Updating contact " . $data->screen_name, Logger::DEBUG);
1139 Contact::updateAvatar($avatar, $uid, $contact['id']);
1141 $fields['name-date'] = DateTimeFormat::utcNow();
1142 $fields['uri-date'] = DateTimeFormat::utcNow();
1144 DBA::update('contact', $fields, ['id' => $contact['id']]);
1151 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1153 $ckey = Config::get('twitter', 'consumerkey');
1154 $csecret = Config::get('twitter', 'consumersecret');
1155 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1156 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1158 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1161 if (DBA::isResult($r)) {
1169 if ($screen_name != "") {
1170 $parameters["screen_name"] = $screen_name;
1173 if ($user_id != "") {
1174 $parameters["user_id"] = $user_id;
1177 // Fetching user data
1178 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1180 $user = $connection->get('users/show', $parameters);
1181 } catch (TwitterOAuthException $e) {
1182 Logger::log('twitter_fetchuser: Error fetching user ' . $uid . ': ' . $e->getMessage());
1186 if (!is_object($user)) {
1190 $contact_id = twitter_fetch_contact($uid, $user, true);
1195 function twitter_expand_entities(App $a, $body, $item, $picture)
1201 foreach ($item->entities->hashtags AS $hashtag) {
1202 $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . rawurlencode($hashtag->text) . ']' . $hashtag->text . '[/url]';
1203 $tags_arr['#' . $hashtag->text] = $url;
1204 $body = str_replace('#' . $hashtag->text, $url, $body);
1207 foreach ($item->entities->user_mentions AS $mention) {
1208 $url = '@[url=https://twitter.com/' . rawurlencode($mention->screen_name) . ']' . $mention->screen_name . '[/url]';
1209 $tags_arr['@' . $mention->screen_name] = $url;
1210 $body = str_replace('@' . $mention->screen_name, $url, $body);
1213 if (isset($item->entities->urls)) {
1219 foreach ($item->entities->urls as $url) {
1220 $plain = str_replace($url->url, '', $plain);
1222 if ($url->url && $url->expanded_url && $url->display_url) {
1223 // Quote tweet, we just remove the quoted tweet URL from the body, the share block will be added later.
1224 if (isset($item->quoted_status_id_str)
1225 && substr($url->expanded_url, -strlen($item->quoted_status_id_str)) == $item->quoted_status_id_str ) {
1226 $body = str_replace($url->url, '', $body);
1230 $expanded_url = Network::finalUrl($url->expanded_url);
1232 $oembed_data = OEmbed::fetchURL($expanded_url);
1234 if (empty($oembed_data) || empty($oembed_data->type)) {
1238 // Quickfix: Workaround for URL with '[' and ']' in it
1239 if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
1240 $expanded_url = $url->url;
1244 $type = $oembed_data->type;
1247 if ($oembed_data->type == 'video') {
1248 $type = $oembed_data->type;
1249 $footerurl = $expanded_url;
1250 $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1252 $body = str_replace($url->url, $footerlink, $body);
1253 } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
1254 $body = str_replace($url->url, '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]', $body);
1255 } elseif ($oembed_data->type != 'link') {
1256 $body = str_replace($url->url, '[url=' . $expanded_url . ']' . $url->display_url . '[/url]', $body);
1258 $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1260 $tempfile = tempnam(get_temppath(), 'cache');
1261 file_put_contents($tempfile, $img_str);
1263 // See http://php.net/manual/en/function.exif-imagetype.php#79283
1264 if (filesize($tempfile) > 11) {
1265 $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1272 if (substr($mime, 0, 6) == 'image/') {
1274 $body = str_replace($url->url, '[img]' . $expanded_url . '[/img]', $body);
1276 $type = $oembed_data->type;
1277 $footerurl = $expanded_url;
1278 $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1280 $body = str_replace($url->url, $footerlink, $body);
1286 // Footer will be taken care of with a share block in the case of a quote
1287 if (empty($item->quoted_status)) {
1288 if ($footerurl != '') {
1289 $footer = add_page_info($footerurl, false, $picture);
1292 if (($footerlink != '') && (trim($footer) != '')) {
1293 $removedlink = trim(str_replace($footerlink, '', $body));
1295 if (($removedlink == '') || strstr($body, $removedlink)) {
1296 $body = $removedlink;
1302 if ($footer == '' && $picture != '') {
1303 $body .= "\n\n[img]" . $picture . "[/img]\n";
1304 } elseif ($footer == '' && $picture == '') {
1305 $body = add_page_info_to_body($body);
1310 // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1311 $tags = Strings::getTags($body);
1314 foreach ($tags as $tag) {
1315 if (strstr(trim($tag), ' ')) {
1319 if (strpos($tag, '#') === 0) {
1320 if (strpos($tag, '[url=')) {
1324 // don't link tags that are already embedded in links
1325 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1328 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1332 $basetag = str_replace('_', ' ', substr($tag, 1));
1333 $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1334 $body = str_replace($tag, $url, $body);
1335 $tags_arr['#' . $basetag] = $url;
1336 } elseif (strpos($tag, '@') === 0) {
1337 if (strpos($tag, '[url=')) {
1341 $basetag = substr($tag, 1);
1342 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1343 $body = str_replace($tag, $url, $body);
1344 $tags_arr['@' . $basetag] = $url;
1349 $tags = implode($tags_arr, ',');
1351 return ['body' => $body, 'tags' => $tags, 'plain' => $plain];
1355 * @brief Fetch media entities and add media links to the body
1357 * @param object $post Twitter object with the post
1358 * @param array $postarray Array of the item that is about to be posted
1360 * @return $picture string Image URL or empty string
1362 function twitter_media_entities($post, array &$postarray)
1364 // There are no media entities? So we quit.
1365 if (empty($post->extended_entities->media)) {
1369 // When the post links to an external page, we only take one picture.
1370 // We only do this when there is exactly one media.
1371 if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1372 $medium = $post->extended_entities->media[0];
1374 foreach ($post->entities->urls as $link) {
1375 // Let's make sure the external link url matches the media url
1376 if ($medium->url == $link->url && isset($medium->media_url_https)) {
1377 $picture = $medium->media_url_https;
1378 $postarray['body'] = str_replace($medium->url, '', $postarray['body']);
1384 // This is a pure media post, first search for all media urls
1386 foreach ($post->extended_entities->media AS $medium) {
1387 if (!isset($media[$medium->url])) {
1388 $media[$medium->url] = '';
1390 switch ($medium->type) {
1392 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1393 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1396 case 'animated_gif':
1397 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1398 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1399 if (is_array($medium->video_info->variants)) {
1401 // We take the video with the highest bitrate
1402 foreach ($medium->video_info->variants AS $variant) {
1403 if (($variant->content_type == 'video/mp4') && ($variant->bitrate >= $bitrate)) {
1404 $media[$medium->url] = "\n[video]" . $variant->url . '[/video]';
1405 $bitrate = $variant->bitrate;
1410 // The following code will only be activated for test reasons
1412 // $postarray['body'] .= print_r($medium, true);
1416 // Now we replace the media urls.
1417 foreach ($media AS $key => $value) {
1418 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1424 function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $only_existing_contact, $noquote)
1427 $postarray['network'] = Protocol::TWITTER;
1428 $postarray['uid'] = $uid;
1429 $postarray['wall'] = 0;
1430 $postarray['uri'] = "twitter::" . $post->id_str;
1431 $postarray['protocol'] = Conversation::PARCEL_TWITTER;
1432 $postarray['source'] = json_encode($post);
1434 // Don't import our own comments
1435 if (Item::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
1436 Logger::log("Item with extid " . $postarray['uri'] . " found.", Logger::DEBUG);
1442 if ($post->in_reply_to_status_id_str != "") {
1443 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1445 $fields = ['uri', 'parent-uri', 'parent'];
1446 $parent_item = Item::selectFirst($fields, ['uri' => $parent, 'uid' => $uid]);
1447 if (!DBA::isResult($parent_item)) {
1448 $parent_item = Item::selectFirst($fields, ['extid' => $parent, 'uid' => $uid]);
1451 if (DBA::isResult($parent_item)) {
1452 $postarray['thr-parent'] = $parent_item['uri'];
1453 $postarray['parent-uri'] = $parent_item['parent-uri'];
1454 $postarray['parent'] = $parent_item['parent'];
1455 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1457 $postarray['thr-parent'] = $postarray['uri'];
1458 $postarray['parent-uri'] = $postarray['uri'];
1459 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1463 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1465 if ($post->user->id_str == $own_id) {
1466 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1469 if (DBA::isResult($r)) {
1470 $contactid = $r[0]["id"];
1472 $postarray['owner-name'] = $r[0]["name"];
1473 $postarray['owner-link'] = $r[0]["url"];
1474 $postarray['owner-avatar'] = $r[0]["photo"];
1476 Logger::log("No self contact for user " . $uid, Logger::DEBUG);
1480 // Don't create accounts of people who just comment something
1481 $create_user = false;
1483 $postarray['parent-uri'] = $postarray['uri'];
1484 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1487 if ($contactid == 0) {
1488 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1490 $postarray['owner-name'] = $post->user->name;
1491 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1492 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1495 if (($contactid == 0) && !$only_existing_contact) {
1496 $contactid = $self['id'];
1497 } elseif ($contactid <= 0) {
1498 Logger::log("Contact ID is zero or less than zero.", Logger::DEBUG);
1502 $postarray['contact-id'] = $contactid;
1504 $postarray['verb'] = ACTIVITY_POST;
1505 $postarray['author-name'] = $postarray['owner-name'];
1506 $postarray['author-link'] = $postarray['owner-link'];
1507 $postarray['author-avatar'] = $postarray['owner-avatar'];
1508 $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1509 $postarray['app'] = strip_tags($post->source);
1511 if ($post->user->protected) {
1512 $postarray['private'] = 1;
1513 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1515 $postarray['private'] = 0;
1516 $postarray['allow_cid'] = '';
1519 if (!empty($post->full_text)) {
1520 $postarray['body'] = $post->full_text;
1522 $postarray['body'] = $post->text;
1525 // When the post contains links then use the correct object type
1526 if (count($post->entities->urls) > 0) {
1527 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1530 // Search for media links
1531 $picture = twitter_media_entities($post, $postarray);
1533 $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1534 $postarray['body'] = $converted["body"];
1535 $postarray['tag'] = $converted["tags"];
1536 $postarray['created'] = DateTimeFormat::utc($post->created_at);
1537 $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1539 $statustext = $converted["plain"];
1541 if (!empty($post->place->name)) {
1542 $postarray["location"] = $post->place->name;
1544 if (!empty($post->place->full_name)) {
1545 $postarray["location"] = $post->place->full_name;
1547 if (!empty($post->geo->coordinates)) {
1548 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1550 if (!empty($post->coordinates->coordinates)) {
1551 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1553 if (!empty($post->retweeted_status)) {
1554 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1556 if (empty($retweet['body'])) {
1560 $retweet['source'] = $postarray['source'];
1561 $retweet['private'] = $postarray['private'];
1562 $retweet['allow_cid'] = $postarray['allow_cid'];
1563 $retweet['contact-id'] = $postarray['contact-id'];
1564 $retweet['owner-name'] = $postarray['owner-name'];
1565 $retweet['owner-link'] = $postarray['owner-link'];
1566 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1568 $postarray = $retweet;
1571 if (!empty($post->quoted_status) && !$noquote) {
1572 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1574 if (empty($quoted['body'])) {
1578 $postarray['body'] .= "\n" . share_header(
1579 $quoted['author-name'],
1580 $quoted['author-link'],
1581 $quoted['author-avatar'],
1587 $postarray['body'] .= $quoted['body'] . '[/share]';
1593 function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, array $self)
1595 Logger::log("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, Logger::DEBUG);
1599 while (!empty($post->in_reply_to_status_id_str)) {
1600 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1603 $post = $connection->get('statuses/show', $parameters);
1604 } catch (TwitterOAuthException $e) {
1605 Logger::log('twitter_fetchparentposts: Error fetching for user ' . $uid . ' and post ' . $post->id_str . ': ' . $e->getMessage());
1610 Logger::log("twitter_fetchparentposts: Can't fetch post " . $parameters->id, Logger::DEBUG);
1614 if (empty($post->id_str)) {
1615 Logger::log("twitter_fetchparentposts: This is not a post " . json_encode($post), Logger::DEBUG);
1619 if (Item::exists(['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
1626 Logger::log("twitter_fetchparentposts: Fetching " . count($posts) . " parents", Logger::DEBUG);
1628 $posts = array_reverse($posts);
1630 if (!empty($posts)) {
1631 foreach ($posts as $post) {
1632 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1634 if (empty($postarray['body'])) {
1638 $item = Item::insert($postarray);
1640 $postarray["id"] = $item;
1642 Logger::log('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1647 function twitter_fetchhometimeline(App $a, $uid)
1649 $ckey = Config::get('twitter', 'consumerkey');
1650 $csecret = Config::get('twitter', 'consumersecret');
1651 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1652 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1653 $create_user = PConfig::get($uid, 'twitter', 'create_user');
1654 $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1656 Logger::log("Fetching timeline for user " . $uid, Logger::DEBUG);
1658 $application_name = Config::get('twitter', 'application_name');
1660 if ($application_name == "") {
1661 $application_name = $a->getHostName();
1664 require_once 'include/items.php';
1666 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1669 $own_contact = twitter_fetch_own_contact($a, $uid);
1670 } catch (TwitterOAuthException $e) {
1671 Logger::log('Error fetching own contact for user ' . $uid . ': ' . $e->getMessage());
1675 $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1676 intval($own_contact),
1679 if (DBA::isResult($r)) {
1680 $own_id = $r[0]["nick"];
1682 Logger::log("Own twitter contact not found for user " . $uid);
1686 $self = User::getOwnerDataById($uid);
1687 if ($self === false) {
1688 Logger::log("Own contact not found for user " . $uid);
1692 $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1693 //$parameters["count"] = 200;
1694 // Fetching timeline
1695 $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1697 $first_time = ($lastid == "");
1699 if ($lastid != "") {
1700 $parameters["since_id"] = $lastid;
1704 $items = $connection->get('statuses/home_timeline', $parameters);
1705 } catch (TwitterOAuthException $e) {
1706 Logger::log('Error fetching home timeline for user ' . $uid . ': ' . $e->getMessage());
1710 if (!is_array($items)) {
1711 Logger::log('No array while fetching home timeline for user ' . $uid . ': ' . print_r($items, true));
1715 if (empty($items)) {
1716 Logger::log('No new timeline content for user ' . $uid, Logger::INFO);
1720 $posts = array_reverse($items);
1722 Logger::log('Fetching timeline from ID ' . $lastid . ' for user ' . $uid . ' ' . sizeof($posts) . ' items', Logger::DEBUG);
1724 if (count($posts)) {
1725 foreach ($posts as $post) {
1726 if ($post->id_str > $lastid) {
1727 $lastid = $post->id_str;
1728 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1735 if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1736 Logger::log("Skip previously sent post", Logger::DEBUG);
1740 if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1741 Logger::log("Skip post that will be mirrored", Logger::DEBUG);
1745 if ($post->in_reply_to_status_id_str != "") {
1746 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1749 Logger::log('Preparing post ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
1751 $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1753 if (empty($postarray['body']) || trim($postarray['body']) == "") {
1754 Logger::log('Empty body for post ' . $post->id_str . ' and user ' . $uid, Logger::DEBUG);
1760 if (($postarray['uri'] == $postarray['parent-uri']) && ($postarray['author-link'] == $postarray['owner-link'])) {
1761 $contact = DBA::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
1762 if (DBA::isResult($contact)) {
1763 $notify = Item::isRemoteSelf($contact, $postarray);
1767 $item = Item::insert($postarray, false, $notify);
1768 $postarray["id"] = $item;
1770 Logger::log('User ' . $uid . ' posted home timeline item ' . $item);
1773 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1775 Logger::log('Last timeline ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1777 // Fetching mentions
1778 $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1780 $first_time = ($lastid == "");
1782 if ($lastid != "") {
1783 $parameters["since_id"] = $lastid;
1787 $items = $connection->get('statuses/mentions_timeline', $parameters);
1788 } catch (TwitterOAuthException $e) {
1789 Logger::log('Error fetching mentions: ' . $e->getMessage());
1793 if (!is_array($items)) {
1794 Logger::log("Error fetching mentions: " . print_r($items, true), Logger::DEBUG);
1798 $posts = array_reverse($items);
1800 Logger::log("Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", Logger::DEBUG);
1802 if (count($posts)) {
1803 foreach ($posts as $post) {
1804 if ($post->id_str > $lastid) {
1805 $lastid = $post->id_str;
1812 if ($post->in_reply_to_status_id_str != "") {
1813 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1816 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1818 if (empty($postarray['body'])) {
1822 $item = Item::insert($postarray);
1824 Logger::log('User ' . $uid . ' posted mention timeline item ' . $item);
1828 PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1830 Logger::log('Last mentions ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1833 function twitter_fetch_own_contact(App $a, $uid)
1835 $ckey = Config::get('twitter', 'consumerkey');
1836 $csecret = Config::get('twitter', 'consumersecret');
1837 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1838 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1840 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1844 if ($own_id == "") {
1845 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1847 // Fetching user data
1848 // get() may throw TwitterOAuthException, but we will catch it later
1849 $user = $connection->get('account/verify_credentials');
1851 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1853 $contact_id = twitter_fetch_contact($uid, $user, true);
1855 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1857 DBA::escape("twitter::" . $own_id));
1858 if (DBA::isResult($r)) {
1859 $contact_id = $r[0]["id"];
1861 PConfig::delete($uid, 'twitter', 'own_id');
1868 function twitter_is_retweet(App $a, $uid, $body)
1870 $body = trim($body);
1872 // Skip if it isn't a pure repeated messages
1873 // Does it start with a share?
1874 if (strpos($body, "[share") > 0) {
1878 // Does it end with a share?
1879 if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1883 $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1884 // Skip if there is no shared message in there
1885 if ($body == $attributes) {
1890 preg_match("/link='(.*?)'/ism", $attributes, $matches);
1891 if (!empty($matches[1])) {
1892 $link = $matches[1];
1895 preg_match('/link="(.*?)"/ism', $attributes, $matches);
1896 if (!empty($matches[1])) {
1897 $link = $matches[1];
1900 $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1905 Logger::log('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, Logger::DEBUG);
1907 $ckey = Config::get('twitter', 'consumerkey');
1908 $csecret = Config::get('twitter', 'consumersecret');
1909 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1910 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1912 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1913 $result = $connection->post('statuses/retweet/' . $id);
1915 Logger::log('twitter_is_retweet: result ' . print_r($result, true), Logger::DEBUG);
1917 return !isset($result->errors);
1920 function twitter_update_mentions($body)
1922 $URLSearchString = "^\[\]";
1923 $return = preg_replace_callback(
1924 "/@\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1925 function ($matches) {
1926 if (strpos($matches[1], 'twitter.com')) {
1927 $return = '@' . substr($matches[1], strrpos($matches[1], '/') + 1);
1929 $return = $matches[2] . ' (' . $matches[1] . ')';
1940 function twitter_convert_share(array $attributes, array $author_contact, $content, $is_quote_share)
1942 if ($author_contact['network'] == Protocol::TWITTER) {
1943 $mention = '@' . $author_contact['nickname'];
1945 $mention = $author_contact['addr'];
1948 return ($is_quote_share ? "\n\n" : '' ) . 'RT ' . $mention . ': ' . $content . "\n\n" . $attributes['link'];