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('post_local' , __FILE__, 'twitter_post_local');
107 Addon::registerHook('notifier_normal' , __FILE__, 'twitter_post_hook');
108 Addon::registerHook('jot_networks' , __FILE__, 'twitter_jot_nets');
109 Addon::registerHook('cron' , __FILE__, 'twitter_cron');
110 Addon::registerHook('queue_predeliver' , __FILE__, 'twitter_queue_hook');
111 Addon::registerHook('follow' , __FILE__, 'twitter_follow');
112 Addon::registerHook('expire' , __FILE__, 'twitter_expire');
113 Addon::registerHook('prepare_body' , __FILE__, 'twitter_prepare_body');
114 Addon::registerHook('check_item_notification', __FILE__, 'twitter_check_item_notification');
115 Logger::log("installed twitter");
118 function twitter_uninstall()
120 Addon::unregisterHook('load_config' , __FILE__, 'twitter_load_config');
121 Addon::unregisterHook('connector_settings' , __FILE__, 'twitter_settings');
122 Addon::unregisterHook('connector_settings_post', __FILE__, 'twitter_settings_post');
123 Addon::unregisterHook('post_local' , __FILE__, 'twitter_post_local');
124 Addon::unregisterHook('notifier_normal' , __FILE__, 'twitter_post_hook');
125 Addon::unregisterHook('jot_networks' , __FILE__, 'twitter_jot_nets');
126 Addon::unregisterHook('cron' , __FILE__, 'twitter_cron');
127 Addon::unregisterHook('queue_predeliver' , __FILE__, 'twitter_queue_hook');
128 Addon::unregisterHook('follow' , __FILE__, 'twitter_follow');
129 Addon::unregisterHook('expire' , __FILE__, 'twitter_expire');
130 Addon::unregisterHook('prepare_body' , __FILE__, 'twitter_prepare_body');
131 Addon::unregisterHook('check_item_notification', __FILE__, 'twitter_check_item_notification');
133 // old setting - remove only
134 Addon::unregisterHook('post_local_end' , __FILE__, 'twitter_post_hook');
135 Addon::unregisterHook('addon_settings' , __FILE__, 'twitter_settings');
136 Addon::unregisterHook('addon_settings_post', __FILE__, 'twitter_settings_post');
139 function twitter_load_config(App $a)
141 $a->loadConfigFile(__DIR__ . '/config/twitter.ini.php');
144 function twitter_check_item_notification(App $a, array &$notification_data)
146 $own_id = PConfig::get($notification_data["uid"], 'twitter', 'own_id');
148 $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
149 intval($notification_data["uid"]),
150 DBA::escape("twitter::".$own_id)
154 $notification_data["profiles"][] = $own_user[0]["url"];
158 function twitter_follow(App $a, array &$contact)
160 Logger::log("twitter_follow: Check if contact is twitter contact. " . $contact["url"], Logger::DEBUG);
162 if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
166 // contact seems to be a twitter contact, so continue
167 $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
168 $nickname = str_replace("@twitter.com", "", $nickname);
170 $uid = $a->user["uid"];
172 $ckey = Config::get('twitter', 'consumerkey');
173 $csecret = Config::get('twitter', 'consumersecret');
174 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
175 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
177 // If the addon is not configured (general or for this user) quit here
178 if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
183 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
184 $connection->post('friendships/create', ['screen_name' => $nickname]);
186 twitter_fetchuser($a, $uid, $nickname);
188 $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
189 FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
191 DBA::escape($nickname));
192 if (DBA::isResult($r)) {
193 $contact["contact"] = $r[0];
197 function twitter_jot_nets(App $a, &$b)
203 $tw_post = PConfig::get(local_user(), 'twitter', 'post');
204 if (intval($tw_post) == 1) {
205 $tw_defpost = PConfig::get(local_user(), 'twitter', 'post_by_default');
206 $selected = ((intval($tw_defpost) == 1) ? ' checked="checked" ' : '');
207 $b .= '<div class="profile-jot-net"><input type="checkbox" name="twitter_enable"' . $selected . ' value="1" /> '
208 . L10n::t('Post to Twitter') . '</div>';
212 function twitter_settings_post(App $a)
217 // don't check twitter settings if twitter submit button is not clicked
218 if (empty($_POST['twitter-disconnect']) && empty($_POST['twitter-submit'])) {
222 if (!empty($_POST['twitter-disconnect'])) {
224 * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
225 * from the user configuration
227 PConfig::delete(local_user(), 'twitter', 'consumerkey');
228 PConfig::delete(local_user(), 'twitter', 'consumersecret');
229 PConfig::delete(local_user(), 'twitter', 'oauthtoken');
230 PConfig::delete(local_user(), 'twitter', 'oauthsecret');
231 PConfig::delete(local_user(), 'twitter', 'post');
232 PConfig::delete(local_user(), 'twitter', 'post_by_default');
233 PConfig::delete(local_user(), 'twitter', 'lastid');
234 PConfig::delete(local_user(), 'twitter', 'mirror_posts');
235 PConfig::delete(local_user(), 'twitter', 'import');
236 PConfig::delete(local_user(), 'twitter', 'create_user');
237 PConfig::delete(local_user(), 'twitter', 'own_id');
239 if (isset($_POST['twitter-pin'])) {
240 // if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
241 Logger::log('got a Twitter PIN');
242 $ckey = Config::get('twitter', 'consumerkey');
243 $csecret = Config::get('twitter', 'consumersecret');
244 // the token and secret for which the PIN was generated were hidden in the settings
245 // form as token and token2, we need a new connection to Twitter using these token
246 // and secret to request a Access Token with the PIN
248 if (empty($_POST['twitter-pin'])) {
249 throw new Exception(L10n::t('You submitted an empty PIN, please Sign In with Twitter again to get a new one.'));
252 $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
253 $token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $_POST['twitter-pin']]);
254 // ok, now that we have the Access Token, save them in the user config
255 PConfig::set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
256 PConfig::set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
257 PConfig::set(local_user(), 'twitter', 'post', 1);
258 } catch(Exception $e) {
259 info($e->getMessage());
260 } catch(TwitterOAuthException $e) {
261 info($e->getMessage());
263 // reload the Addon Settings page, if we don't do it see Bug #42
264 $a->internalRedirect('settings/connectors');
266 // if no PIN is supplied in the POST variables, the user has changed the setting
267 // to post a tweet for every new __public__ posting to the wall
268 PConfig::set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
269 PConfig::set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
270 PConfig::set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
271 PConfig::set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
272 PConfig::set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
274 if (!intval($_POST['twitter-mirror'])) {
275 PConfig::delete(local_user(), 'twitter', 'lastid');
278 info(L10n::t('Twitter settings updated.') . EOL);
283 function twitter_settings(App $a, &$s)
288 $a->page['htmlhead'] .= '<link rel="stylesheet" type="text/css" href="' . $a->getBaseURL() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
290 * 1) Check that we have global consumer key & secret
291 * 2) If no OAuthtoken & stuff is present, generate button to get some
292 * 3) Checkbox for "Send public notices (280 chars only)
294 $ckey = Config::get('twitter', 'consumerkey');
295 $csecret = Config::get('twitter', 'consumersecret');
296 $otoken = PConfig::get(local_user(), 'twitter', 'oauthtoken');
297 $osecret = PConfig::get(local_user(), 'twitter', 'oauthsecret');
299 $enabled = intval(PConfig::get(local_user(), 'twitter', 'post'));
300 $defenabled = intval(PConfig::get(local_user(), 'twitter', 'post_by_default'));
301 $mirrorenabled = intval(PConfig::get(local_user(), 'twitter', 'mirror_posts'));
302 $importenabled = intval(PConfig::get(local_user(), 'twitter', 'import'));
303 $create_userenabled = intval(PConfig::get(local_user(), 'twitter', 'create_user'));
305 $css = (($enabled) ? '' : '-disabled');
307 $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
308 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
310 $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
311 $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
312 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
315 if ((!$ckey) && (!$csecret)) {
316 /* no global consumer keys
317 * display warning and skip personal config
319 $s .= '<p>' . L10n::t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
321 // ok we have a consumer key pair now look into the OAuth stuff
322 if ((!$otoken) && (!$osecret)) {
323 /* the user has not yet connected the account to twitter...
324 * get a temporary OAuth key/secret pair and display a button with
325 * which the user can request a PIN to connect the account to a
326 * account at Twitter.
328 $connection = new TwitterOAuth($ckey, $csecret);
330 $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
331 $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>';
332 $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>';
333 $s .= '<div id="twitter-pin-wrapper">';
334 $s .= '<label id="twitter-pin-label" for="twitter-pin">' . L10n::t('Copy the PIN from Twitter here') . '</label>';
335 $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
336 $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result['oauth_token'] . '" />';
337 $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result['oauth_token_secret'] . '" />';
338 $s .= '</div><div class="clear"></div>';
339 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
340 } catch (TwitterOAuthException $e) {
341 $s .= '<p>' . L10n::t('An error occured: ') . $e->getMessage() . '</p>';
345 * we have an OAuth key / secret pair for the user
346 * so let's give a chance to disable the postings to Twitter
348 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
350 $details = $connection->get('account/verify_credentials');
352 $field_checkbox = Renderer::getMarkupTemplate('field_checkbox.tpl');
354 $s .= '<div id="twitter-info" >
355 <p>' . L10n::t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
356 <button type="submit" name="twitter-disconnect" value="1">' . L10n::t('Disconnect') . '</button>
358 <p id="twitter-info-block">
359 <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
360 <em>' . $details->description . '</em>
363 $s .= '<div class="clear"></div>';
365 $s .= Renderer::replaceMacros($field_checkbox, [
366 '$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 if ($a->user['hidewall']) {
369 $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 $s .= Renderer::replaceMacros($field_checkbox, [
372 '$field' => ['twitter-default', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
374 $s .= Renderer::replaceMacros($field_checkbox, [
375 '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
377 $s .= Renderer::replaceMacros($field_checkbox, [
378 '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
380 $s .= Renderer::replaceMacros($field_checkbox, [
381 '$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 $s .= '<div class="clear"></div>';
384 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
385 } catch (TwitterOAuthException $e) {
386 $s .= '<p>' . L10n::t('An error occured: ') . $e->getMessage() . '</p>';
390 $s .= '</div><div class="clear"></div>';
393 function twitter_post_local(App $a, array &$b)
399 if (!local_user() || (local_user() != $b['uid'])) {
403 $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
404 $twitter_enable = (($twitter_post && x($_REQUEST, 'twitter_enable')) ? intval($_REQUEST['twitter_enable']) : 0);
406 // if API is used, default to the chosen settings
407 if ($b['api_source'] && intval(PConfig::get(local_user(), 'twitter', 'post_by_default'))) {
411 if (!$twitter_enable) {
415 if (strlen($b['postopts'])) {
416 $b['postopts'] .= ',';
419 $b['postopts'] .= 'twitter';
422 function twitter_action(App $a, $uid, $pid, $action)
424 $ckey = Config::get('twitter', 'consumerkey');
425 $csecret = Config::get('twitter', 'consumersecret');
426 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
427 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
429 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
431 $post = ['id' => $pid];
433 Logger::log("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), Logger::DATA);
437 // To-Do: $result = $connection->post('statuses/destroy', $post);
441 $result = $connection->post('favorites/create', $post);
444 $result = $connection->post('favorites/destroy', $post);
447 Logger::log('Unhandled action ' . $action, Logger::DEBUG);
450 Logger::log("twitter_action '" . $action . "' send, result: " . print_r($result, true), Logger::DEBUG);
453 function twitter_post_hook(App $a, array &$b)
456 if (!PConfig::get($b["uid"], 'twitter', 'import')
457 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
461 if ($b['parent'] != $b['id']) {
462 Logger::log("twitter_post_hook: parameter " . print_r($b, true), Logger::DATA);
464 // Looking if its a reply to a twitter post
465 if ((substr($b["parent-uri"], 0, 9) != "twitter::")
466 && (substr($b["extid"], 0, 9) != "twitter::")
467 && (substr($b["thr-parent"], 0, 9) != "twitter::"))
469 Logger::log("twitter_post_hook: no twitter post " . $b["parent"]);
473 $condition = ['uri' => $b["thr-parent"], 'uid' => $b["uid"]];
474 $orig_post = Item::selectFirst([], $condition);
475 if (!DBA::isResult($orig_post)) {
476 Logger::log("twitter_post_hook: no parent found " . $b["thr-parent"]);
483 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
484 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
485 $nicknameplain = "@" . $nicknameplain;
487 Logger::log("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], Logger::DEBUG);
488 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
489 $b["body"] = $nickname . " " . $b["body"];
492 Logger::log("twitter_post_hook: parent found " . print_r($orig_post, true), Logger::DATA);
496 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
500 // Dont't post if the post doesn't belong to us.
501 // This is a check for forum postings
502 $self = DBA::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
503 if ($b['contact-id'] != $self['id']) {
508 if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
509 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
512 if ($b['verb'] == ACTIVITY_LIKE) {
513 Logger::log("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), Logger::DEBUG);
515 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
517 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
523 if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
527 // if post comes from twitter don't send it back
528 if ($b['extid'] == Protocol::TWITTER) {
532 if ($b['app'] == "Twitter") {
536 Logger::log('twitter post invoked');
538 PConfig::load($b['uid'], 'twitter');
540 $ckey = Config::get('twitter', 'consumerkey');
541 $csecret = Config::get('twitter', 'consumersecret');
542 $otoken = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
543 $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
545 if ($ckey && $csecret && $otoken && $osecret) {
546 Logger::log('twitter: we have customer key and oauth stuff, going to send.', Logger::DEBUG);
548 // If it's a repeated message from twitter then do a native retweet and exit
549 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
553 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
555 // Set the timeout for upload to 30 seconds
556 $connection->setTimeouts(10, 30);
560 // Handling non-native reshares
561 $b['body'] = Friendica\Content\Text\BBCode::convertShare(
563 function (array $attributes, array $author_contact, $content, $is_quote_share) {
564 return twitter_convert_share($attributes, $author_contact, $content, $is_quote_share);
568 $b['body'] = twitter_update_mentions($b['body']);
570 $msgarr = ItemContent::getPlaintextPost($b, $max_char, true, 8);
571 $msg = $msgarr["text"];
573 if (($msg == "") && isset($msgarr["title"])) {
574 $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
579 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
580 $msg .= "\n" . $msgarr["url"];
586 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
587 $image = $msgarr["image"];
594 // and now tweet it :-)
597 if (!empty($image)) {
599 $img_str = Network::fetchUrl($image);
601 $tempfile = tempnam(get_temppath(), 'cache');
602 file_put_contents($tempfile, $img_str);
604 $media = $connection->upload('media/upload', ['media' => $tempfile]);
608 if (isset($media->media_id_string)) {
609 $post['media_ids'] = $media->media_id_string;
611 throw new Exception('Failed upload of ' . $image);
613 } catch (Exception $e) {
614 Logger::log('Exception when trying to send to Twitter: ' . $e->getMessage());
616 // Workaround: Remove the picture link so that the post can be reposted without it
617 // When there is another url already added, a second url would be superfluous.
619 $msg .= "\n" . $image;
626 $post['status'] = $msg;
629 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
632 $url = 'statuses/update';
633 $result = $connection->post($url, $post);
634 Logger::log('twitter_post send, result: ' . print_r($result, true), Logger::DEBUG);
636 if (!empty($result->source)) {
637 Config::set("twitter", "application_name", strip_tags($result->source));
640 if (!empty($result->errors)) {
641 Logger::log('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
643 $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
644 if (DBA::isResult($r)) {
645 $a->contact = $r[0]["id"];
648 $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
650 Queue::add($a->contact, Protocol::TWITTER, $s);
651 notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
652 } elseif ($iscomment) {
653 Logger::log('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
654 Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
659 function twitter_addon_admin_post(App $a)
661 $consumerkey = x($_POST, 'consumerkey') ? Strings::escapeTags(trim($_POST['consumerkey'])) : '';
662 $consumersecret = x($_POST, 'consumersecret') ? Strings::escapeTags(trim($_POST['consumersecret'])) : '';
663 Config::set('twitter', 'consumerkey', $consumerkey);
664 Config::set('twitter', 'consumersecret', $consumersecret);
665 info(L10n::t('Settings updated.') . EOL);
668 function twitter_addon_admin(App $a, &$o)
670 $t = Renderer::getMarkupTemplate("admin.tpl", "addon/twitter/");
672 $o = Renderer::replaceMacros($t, [
673 '$submit' => L10n::t('Save Settings'),
674 // name, label, value, help, [extra values]
675 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
676 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
680 function twitter_cron(App $a)
682 $last = Config::get('twitter', 'last_poll');
684 $poll_interval = intval(Config::get('twitter', 'poll_interval'));
685 if (!$poll_interval) {
686 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
690 $next = $last + ($poll_interval * 60);
691 if ($next > time()) {
692 Logger::log('twitter: poll intervall not reached');
696 Logger::log('twitter: cron_start');
698 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
699 if (DBA::isResult($r)) {
700 foreach ($r as $rr) {
701 Logger::log('twitter: fetching for user ' . $rr['uid']);
702 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
706 $abandon_days = intval(Config::get('system', 'account_abandon_days'));
707 if ($abandon_days < 1) {
711 $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
713 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
714 if (DBA::isResult($r)) {
715 foreach ($r as $rr) {
716 if ($abandon_days != 0) {
717 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
718 if (!DBA::isResult($user)) {
719 Logger::log('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
724 Logger::log('twitter: importing timeline from user ' . $rr['uid']);
725 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
728 // check for new contacts once a day
729 $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
730 if($last_contact_check)
731 $next_contact_check = $last_contact_check + 86400;
733 $next_contact_check = 0;
735 if($next_contact_check <= time()) {
736 pumpio_getallusers($a, $rr["uid"]);
737 PConfig::set($rr['uid'],'pumpio','contact_check',time());
743 Logger::log('twitter: cron_end');
745 Config::set('twitter', 'last_poll', time());
748 function twitter_expire(App $a)
750 $days = Config::get('twitter', 'expire');
756 $r = Item::select(['id'], ['deleted' => true, 'network' => Protocol::TWITTER]);
757 while ($row = DBA::fetch($r)) {
758 DBA::delete('item', ['id' => $row['id']]);
762 require_once "include/items.php";
764 Logger::log('twitter_expire: expire_start');
766 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
767 if (DBA::isResult($r)) {
768 foreach ($r as $rr) {
769 Logger::log('twitter_expire: user ' . $rr['uid']);
770 Item::expire($rr['uid'], $days, Protocol::TWITTER, true);
774 Logger::log('twitter_expire: expire_end');
777 function twitter_prepare_body(App $a, array &$b)
779 if ($b["item"]["network"] != Protocol::TWITTER) {
786 $item["plink"] = $a->getBaseURL() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
788 $condition = ['uri' => $item["thr-parent"], 'uid' => local_user()];
789 $orig_post = Item::selectFirst(['author-link'], $condition);
790 if (DBA::isResult($orig_post)) {
791 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
792 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
793 $nicknameplain = "@" . $nicknameplain;
795 if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
796 $item["body"] = $nickname . " " . $item["body"];
800 $msgarr = ItemContent::getPlaintextPost($item, $max_char, true, 8);
801 $msg = $msgarr["text"];
803 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
804 $msg .= " " . $msgarr["url"];
807 if (isset($msgarr["image"])) {
808 $msg .= " " . $msgarr["image"];
811 $b['html'] = nl2br(htmlspecialchars($msg));
816 * @brief Build the item array for the mirrored post
818 * @param App $a Application class
819 * @param integer $uid User id
820 * @param object $post Twitter object with the post
822 * @return array item data to be posted
824 function twitter_do_mirrorpost(App $a, $uid, $post)
826 $datarray['api_source'] = true;
827 $datarray['profile_uid'] = $uid;
828 $datarray['extid'] = Protocol::TWITTER;
829 $datarray['message_id'] = Item::newURI($uid, Protocol::TWITTER . ':' . $post->id);
830 $datarray['protocol'] = Conversation::PARCEL_TWITTER;
831 $datarray['source'] = json_encode($post);
832 $datarray['title'] = '';
834 if (!empty($post->retweeted_status)) {
835 // We don't support nested shares, so we mustn't show quotes as shares on retweets
836 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
838 if (empty($item['body'])) {
842 $datarray['body'] = "\n" . share_header(
843 $item['author-name'],
844 $item['author-link'],
845 $item['author-avatar'],
851 $datarray['body'] .= $item['body'] . '[/share]';
853 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
855 if (empty($item['body'])) {
859 $datarray['body'] = $item['body'];
862 $datarray['source'] = $item['app'];
863 $datarray['verb'] = $item['verb'];
865 if (isset($item['location'])) {
866 $datarray['location'] = $item['location'];
869 if (isset($item['coord'])) {
870 $datarray['coord'] = $item['coord'];
876 function twitter_fetchtimeline(App $a, $uid)
878 $ckey = Config::get('twitter', 'consumerkey');
879 $csecret = Config::get('twitter', 'consumersecret');
880 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
881 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
882 $lastid = PConfig::get($uid, 'twitter', 'lastid');
884 $application_name = Config::get('twitter', 'application_name');
886 if ($application_name == "") {
887 $application_name = $a->getHostName();
890 $has_picture = false;
892 require_once 'mod/item.php';
893 require_once 'include/items.php';
894 require_once 'mod/share.php';
896 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
898 $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
900 $first_time = ($lastid == "");
903 $parameters["since_id"] = $lastid;
907 $items = $connection->get('statuses/user_timeline', $parameters);
908 } catch (TwitterOAuthException $e) {
909 Logger::log('Error fetching timeline for user ' . $uid . ': ' . $e->getMessage());
913 if (!is_array($items)) {
914 Logger::log('No items for user ' . $uid, Logger::INFO);
918 $posts = array_reverse($items);
920 Logger::log('Starting from ID ' . $lastid . ' for user ' . $uid, Logger::DEBUG);
923 foreach ($posts as $post) {
924 if ($post->id_str > $lastid) {
925 $lastid = $post->id_str;
926 PConfig::set($uid, 'twitter', 'lastid', $lastid);
933 if (!stristr($post->source, $application_name)) {
934 $_SESSION["authenticated"] = true;
935 $_SESSION["uid"] = $uid;
937 Logger::log('Preparing Twitter ID ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
939 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
941 if (empty($_REQUEST['body'])) {
945 Logger::log('Posting Twitter ID ' . $post->id_str . ' for user ' . $uid);
951 PConfig::set($uid, 'twitter', 'lastid', $lastid);
952 Logger::log('Last ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
955 function twitter_queue_hook(App $a)
957 $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
958 DBA::escape(Protocol::TWITTER)
960 if (!DBA::isResult($qi)) {
964 foreach ($qi as $x) {
965 if ($x['network'] !== Protocol::TWITTER) {
969 Logger::log('twitter_queue: run');
971 $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
972 WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
975 if (!DBA::isResult($r)) {
981 $ckey = Config::get('twitter', 'consumerkey');
982 $csecret = Config::get('twitter', 'consumersecret');
983 $otoken = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
984 $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
988 if ($ckey && $csecret && $otoken && $osecret) {
989 Logger::log('twitter_queue: able to post');
991 $z = unserialize($x['content']);
993 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
994 $result = $connection->post($z['url'], $z['post']);
996 Logger::log('twitter_queue: post result: ' . print_r($result, true), Logger::DEBUG);
998 if ($result->errors) {
999 Logger::log('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
1002 Queue::removeItem($x['id']);
1005 Logger::log("twitter_queue: Error getting tokens for user " . $user['uid']);
1009 Logger::log('twitter_queue: delayed');
1010 Queue::updateTime($x['id']);
1015 function twitter_fix_avatar($avatar)
1017 $new_avatar = str_replace("_normal.", ".", $avatar);
1019 $info = Image::getInfoFromURL($new_avatar);
1021 $new_avatar = $avatar;
1027 function twitter_fetch_contact($uid, $data, $create_user)
1029 if (empty($data->id_str)) {
1033 $avatar = twitter_fix_avatar($data->profile_image_url_https);
1034 $url = "https://twitter.com/" . $data->screen_name;
1035 $addr = $data->screen_name . "@twitter.com";
1037 GContact::update(["url" => $url, "network" => Protocol::TWITTER,
1038 "photo" => $avatar, "hide" => true,
1039 "name" => $data->name, "nick" => $data->screen_name,
1040 "location" => $data->location, "about" => $data->description,
1041 "addr" => $addr, "generation" => 2]);
1043 $fields = ['url' => $url, 'network' => Protocol::TWITTER,
1044 'name' => $data->name, 'nick' => $data->screen_name, 'addr' => $addr,
1045 'location' => $data->location, 'about' => $data->description];
1047 $cid = Contact::getIdForURL($url, 0, true, $fields);
1049 DBA::update('contact', $fields, ['id' => $cid]);
1050 Contact::updateAvatar($avatar, 0, $cid);
1053 $contact = DBA::selectFirst('contact', [], ['uid' => $uid, 'alias' => "twitter::" . $data->id_str]);
1054 if (!DBA::isResult($contact) && !$create_user) {
1058 if (!DBA::isResult($contact)) {
1059 // create contact record
1060 $fields['uid'] = $uid;
1061 $fields['created'] = DateTimeFormat::utcNow();
1062 $fields['nurl'] = Strings::normaliseLink($url);
1063 $fields['alias'] = 'twitter::' . $data->id_str;
1064 $fields['poll'] = 'twitter::' . $data->id_str;
1065 $fields['rel'] = Contact::FRIEND;
1066 $fields['priority'] = 1;
1067 $fields['writable'] = true;
1068 $fields['blocked'] = false;
1069 $fields['readonly'] = false;
1070 $fields['pending'] = false;
1072 if (!DBA::insert('contact', $fields)) {
1076 $contact_id = DBA::lastInsertId();
1078 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1080 Contact::updateAvatar($avatar, $uid, $contact_id);
1082 if ($contact["readonly"] || $contact["blocked"]) {
1083 Logger::log("twitter_fetch_contact: Contact '" . $contact["nick"] . "' is blocked or readonly.", Logger::DEBUG);
1087 $contact_id = $contact['id'];
1089 // update profile photos once every twelve hours as we have no notification of when they change.
1090 $update_photo = ($contact['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
1092 // check that we have all the photos, this has been known to fail on occasion
1093 if (empty($contact['photo']) || empty($contact['thumb']) || empty($contact['micro']) || $update_photo) {
1094 Logger::log("twitter_fetch_contact: Updating contact " . $data->screen_name, Logger::DEBUG);
1096 Contact::updateAvatar($avatar, $uid, $contact['id']);
1098 $fields['name-date'] = DateTimeFormat::utcNow();
1099 $fields['uri-date'] = DateTimeFormat::utcNow();
1101 DBA::update('contact', $fields, ['id' => $contact['id']]);
1108 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1110 $ckey = Config::get('twitter', 'consumerkey');
1111 $csecret = Config::get('twitter', 'consumersecret');
1112 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1113 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1115 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1118 if (DBA::isResult($r)) {
1126 if ($screen_name != "") {
1127 $parameters["screen_name"] = $screen_name;
1130 if ($user_id != "") {
1131 $parameters["user_id"] = $user_id;
1134 // Fetching user data
1135 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1137 $user = $connection->get('users/show', $parameters);
1138 } catch (TwitterOAuthException $e) {
1139 Logger::log('twitter_fetchuser: Error fetching user ' . $uid . ': ' . $e->getMessage());
1143 if (!is_object($user)) {
1147 $contact_id = twitter_fetch_contact($uid, $user, true);
1152 function twitter_expand_entities(App $a, $body, $item, $picture)
1158 foreach ($item->entities->hashtags AS $hashtag) {
1159 $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . rawurlencode($hashtag->text) . ']' . $hashtag->text . '[/url]';
1160 $tags_arr['#' . $hashtag->text] = $url;
1161 $body = str_replace('#' . $hashtag->text, $url, $body);
1164 foreach ($item->entities->user_mentions AS $mention) {
1165 $url = '@[url=https://twitter.com/' . rawurlencode($mention->screen_name) . ']' . $mention->screen_name . '[/url]';
1166 $tags_arr['@' . $mention->screen_name] = $url;
1167 $body = str_replace('@' . $mention->screen_name, $url, $body);
1170 if (isset($item->entities->urls)) {
1176 foreach ($item->entities->urls as $url) {
1177 $plain = str_replace($url->url, '', $plain);
1179 if ($url->url && $url->expanded_url && $url->display_url) {
1180 // Quote tweet, we just remove the quoted tweet URL from the body, the share block will be added later.
1181 if (isset($item->quoted_status_id_str)
1182 && substr($url->expanded_url, -strlen($item->quoted_status_id_str)) == $item->quoted_status_id_str ) {
1183 $body = str_replace($url->url, '', $body);
1187 $expanded_url = Network::finalUrl($url->expanded_url);
1189 $oembed_data = OEmbed::fetchURL($expanded_url);
1191 if (empty($oembed_data) || empty($oembed_data->type)) {
1195 // Quickfix: Workaround for URL with '[' and ']' in it
1196 if (strpos($expanded_url, '[') || strpos($expanded_url, ']')) {
1197 $expanded_url = $url->url;
1201 $type = $oembed_data->type;
1204 if ($oembed_data->type == 'video') {
1205 $type = $oembed_data->type;
1206 $footerurl = $expanded_url;
1207 $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1209 $body = str_replace($url->url, $footerlink, $body);
1210 } elseif (($oembed_data->type == 'photo') && isset($oembed_data->url)) {
1211 $body = str_replace($url->url, '[url=' . $expanded_url . '][img]' . $oembed_data->url . '[/img][/url]', $body);
1212 } elseif ($oembed_data->type != 'link') {
1213 $body = str_replace($url->url, '[url=' . $expanded_url . ']' . $url->display_url . '[/url]', $body);
1215 $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1217 $tempfile = tempnam(get_temppath(), 'cache');
1218 file_put_contents($tempfile, $img_str);
1220 // See http://php.net/manual/en/function.exif-imagetype.php#79283
1221 if (filesize($tempfile) > 11) {
1222 $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1229 if (substr($mime, 0, 6) == 'image/') {
1231 $body = str_replace($url->url, '[img]' . $expanded_url . '[/img]', $body);
1233 $type = $oembed_data->type;
1234 $footerurl = $expanded_url;
1235 $footerlink = '[url=' . $expanded_url . ']' . $url->display_url . '[/url]';
1237 $body = str_replace($url->url, $footerlink, $body);
1243 // Footer will be taken care of with a share block in the case of a quote
1244 if (empty($item->quoted_status)) {
1245 if ($footerurl != '') {
1246 $footer = add_page_info($footerurl, false, $picture);
1249 if (($footerlink != '') && (trim($footer) != '')) {
1250 $removedlink = trim(str_replace($footerlink, '', $body));
1252 if (($removedlink == '') || strstr($body, $removedlink)) {
1253 $body = $removedlink;
1259 if ($footer == '' && $picture != '') {
1260 $body .= "\n\n[img]" . $picture . "[/img]\n";
1261 } elseif ($footer == '' && $picture == '') {
1262 $body = add_page_info_to_body($body);
1267 // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1268 $tags = Strings::getTags($body);
1271 foreach ($tags as $tag) {
1272 if (strstr(trim($tag), ' ')) {
1276 if (strpos($tag, '#') === 0) {
1277 if (strpos($tag, '[url=')) {
1281 // don't link tags that are already embedded in links
1282 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1285 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1289 $basetag = str_replace('_', ' ', substr($tag, 1));
1290 $url = '#[url=' . $a->getBaseURL() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1291 $body = str_replace($tag, $url, $body);
1292 $tags_arr['#' . $basetag] = $url;
1293 } elseif (strpos($tag, '@') === 0) {
1294 if (strpos($tag, '[url=')) {
1298 $basetag = substr($tag, 1);
1299 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1300 $body = str_replace($tag, $url, $body);
1301 $tags_arr['@' . $basetag] = $url;
1306 $tags = implode($tags_arr, ',');
1308 return ['body' => $body, 'tags' => $tags, 'plain' => $plain];
1312 * @brief Fetch media entities and add media links to the body
1314 * @param object $post Twitter object with the post
1315 * @param array $postarray Array of the item that is about to be posted
1317 * @return $picture string Image URL or empty string
1319 function twitter_media_entities($post, array &$postarray)
1321 // There are no media entities? So we quit.
1322 if (empty($post->extended_entities->media)) {
1326 // When the post links to an external page, we only take one picture.
1327 // We only do this when there is exactly one media.
1328 if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1329 $medium = $post->extended_entities->media[0];
1331 foreach ($post->entities->urls as $link) {
1332 // Let's make sure the external link url matches the media url
1333 if ($medium->url == $link->url && isset($medium->media_url_https)) {
1334 $picture = $medium->media_url_https;
1335 $postarray['body'] = str_replace($medium->url, '', $postarray['body']);
1341 // This is a pure media post, first search for all media urls
1343 foreach ($post->extended_entities->media AS $medium) {
1344 if (!isset($media[$medium->url])) {
1345 $media[$medium->url] = '';
1347 switch ($medium->type) {
1349 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1350 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1353 case 'animated_gif':
1354 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . '[/img]';
1355 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1356 if (is_array($medium->video_info->variants)) {
1358 // We take the video with the highest bitrate
1359 foreach ($medium->video_info->variants AS $variant) {
1360 if (($variant->content_type == 'video/mp4') && ($variant->bitrate >= $bitrate)) {
1361 $media[$medium->url] = "\n[video]" . $variant->url . '[/video]';
1362 $bitrate = $variant->bitrate;
1367 // The following code will only be activated for test reasons
1369 // $postarray['body'] .= print_r($medium, true);
1373 // Now we replace the media urls.
1374 foreach ($media AS $key => $value) {
1375 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1381 function twitter_createpost(App $a, $uid, $post, array $self, $create_user, $only_existing_contact, $noquote)
1384 $postarray['network'] = Protocol::TWITTER;
1385 $postarray['uid'] = $uid;
1386 $postarray['wall'] = 0;
1387 $postarray['uri'] = "twitter::" . $post->id_str;
1388 $postarray['protocol'] = Conversation::PARCEL_TWITTER;
1389 $postarray['source'] = json_encode($post);
1391 // Don't import our own comments
1392 if (Item::exists(['extid' => $postarray['uri'], 'uid' => $uid])) {
1393 Logger::log("Item with extid " . $postarray['uri'] . " found.", Logger::DEBUG);
1399 if ($post->in_reply_to_status_id_str != "") {
1400 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1402 $fields = ['uri', 'parent-uri', 'parent'];
1403 $parent_item = Item::selectFirst($fields, ['uri' => $parent, 'uid' => $uid]);
1404 if (!DBA::isResult($parent_item)) {
1405 $parent_item = Item::selectFirst($fields, ['extid' => $parent, 'uid' => $uid]);
1408 if (DBA::isResult($parent_item)) {
1409 $postarray['thr-parent'] = $parent_item['uri'];
1410 $postarray['parent-uri'] = $parent_item['parent-uri'];
1411 $postarray['parent'] = $parent_item['parent'];
1412 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1414 $postarray['thr-parent'] = $postarray['uri'];
1415 $postarray['parent-uri'] = $postarray['uri'];
1416 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1420 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1422 if ($post->user->id_str == $own_id) {
1423 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1426 if (DBA::isResult($r)) {
1427 $contactid = $r[0]["id"];
1429 $postarray['owner-name'] = $r[0]["name"];
1430 $postarray['owner-link'] = $r[0]["url"];
1431 $postarray['owner-avatar'] = $r[0]["photo"];
1433 Logger::log("No self contact for user " . $uid, Logger::DEBUG);
1437 // Don't create accounts of people who just comment something
1438 $create_user = false;
1440 $postarray['parent-uri'] = $postarray['uri'];
1441 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1444 if ($contactid == 0) {
1445 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1447 $postarray['owner-name'] = $post->user->name;
1448 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1449 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1452 if (($contactid == 0) && !$only_existing_contact) {
1453 $contactid = $self['id'];
1454 } elseif ($contactid <= 0) {
1455 Logger::log("Contact ID is zero or less than zero.", Logger::DEBUG);
1459 $postarray['contact-id'] = $contactid;
1461 $postarray['verb'] = ACTIVITY_POST;
1462 $postarray['author-name'] = $postarray['owner-name'];
1463 $postarray['author-link'] = $postarray['owner-link'];
1464 $postarray['author-avatar'] = $postarray['owner-avatar'];
1465 $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1466 $postarray['app'] = strip_tags($post->source);
1468 if ($post->user->protected) {
1469 $postarray['private'] = 1;
1470 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1472 $postarray['private'] = 0;
1473 $postarray['allow_cid'] = '';
1476 if (!empty($post->full_text)) {
1477 $postarray['body'] = $post->full_text;
1479 $postarray['body'] = $post->text;
1482 // When the post contains links then use the correct object type
1483 if (count($post->entities->urls) > 0) {
1484 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1487 // Search for media links
1488 $picture = twitter_media_entities($post, $postarray);
1490 $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1491 $postarray['body'] = $converted["body"];
1492 $postarray['tag'] = $converted["tags"];
1493 $postarray['created'] = DateTimeFormat::utc($post->created_at);
1494 $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1496 $statustext = $converted["plain"];
1498 if (!empty($post->place->name)) {
1499 $postarray["location"] = $post->place->name;
1501 if (!empty($post->place->full_name)) {
1502 $postarray["location"] = $post->place->full_name;
1504 if (!empty($post->geo->coordinates)) {
1505 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1507 if (!empty($post->coordinates->coordinates)) {
1508 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1510 if (!empty($post->retweeted_status)) {
1511 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1513 if (empty($retweet['body'])) {
1517 $retweet['source'] = $postarray['source'];
1518 $retweet['private'] = $postarray['private'];
1519 $retweet['allow_cid'] = $postarray['allow_cid'];
1520 $retweet['contact-id'] = $postarray['contact-id'];
1521 $retweet['owner-name'] = $postarray['owner-name'];
1522 $retweet['owner-link'] = $postarray['owner-link'];
1523 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1525 $postarray = $retweet;
1528 if (!empty($post->quoted_status) && !$noquote) {
1529 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1531 if (empty($quoted['body'])) {
1535 $postarray['body'] .= "\n" . share_header(
1536 $quoted['author-name'],
1537 $quoted['author-link'],
1538 $quoted['author-avatar'],
1544 $postarray['body'] .= $quoted['body'] . '[/share]';
1550 function twitter_fetchparentposts(App $a, $uid, $post, TwitterOAuth $connection, array $self)
1552 Logger::log("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, Logger::DEBUG);
1556 while (!empty($post->in_reply_to_status_id_str)) {
1557 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1560 $post = $connection->get('statuses/show', $parameters);
1561 } catch (TwitterOAuthException $e) {
1562 Logger::log('twitter_fetchparentposts: Error fetching for user ' . $uid . ' and post ' . $post->id_str . ': ' . $e->getMessage());
1567 Logger::log("twitter_fetchparentposts: Can't fetch post " . $parameters->id, Logger::DEBUG);
1571 if (empty($post->id_str)) {
1572 Logger::log("twitter_fetchparentposts: This is not a post " . json_encode($post), Logger::DEBUG);
1576 if (Item::exists(['uri' => 'twitter::' . $post->id_str, 'uid' => $uid])) {
1583 Logger::log("twitter_fetchparentposts: Fetching " . count($posts) . " parents", Logger::DEBUG);
1585 $posts = array_reverse($posts);
1587 if (!empty($posts)) {
1588 foreach ($posts as $post) {
1589 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1591 if (empty($postarray['body'])) {
1595 $item = Item::insert($postarray);
1597 $postarray["id"] = $item;
1599 Logger::log('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1604 function twitter_fetchhometimeline(App $a, $uid)
1606 $ckey = Config::get('twitter', 'consumerkey');
1607 $csecret = Config::get('twitter', 'consumersecret');
1608 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1609 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1610 $create_user = PConfig::get($uid, 'twitter', 'create_user');
1611 $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1613 Logger::log("Fetching timeline for user " . $uid, Logger::DEBUG);
1615 $application_name = Config::get('twitter', 'application_name');
1617 if ($application_name == "") {
1618 $application_name = $a->getHostName();
1621 require_once 'include/items.php';
1623 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1626 $own_contact = twitter_fetch_own_contact($a, $uid);
1627 } catch (TwitterOAuthException $e) {
1628 Logger::log('Error fetching own contact for user ' . $uid . ': ' . $e->getMessage());
1632 $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1633 intval($own_contact),
1636 if (DBA::isResult($r)) {
1637 $own_id = $r[0]["nick"];
1639 Logger::log("Own twitter contact not found for user " . $uid);
1643 $self = User::getOwnerDataById($uid);
1644 if ($self === false) {
1645 Logger::log("Own contact not found for user " . $uid);
1649 $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1650 //$parameters["count"] = 200;
1651 // Fetching timeline
1652 $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1654 $first_time = ($lastid == "");
1656 if ($lastid != "") {
1657 $parameters["since_id"] = $lastid;
1661 $items = $connection->get('statuses/home_timeline', $parameters);
1662 } catch (TwitterOAuthException $e) {
1663 Logger::log('Error fetching home timeline for user ' . $uid . ': ' . $e->getMessage());
1667 if (!is_array($items)) {
1668 Logger::log('No array while fetching home timeline for user ' . $uid . ': ' . print_r($items, true));
1672 if (empty($items)) {
1673 Logger::log('No new timeline content for user ' . $uid, Logger::INFO);
1677 $posts = array_reverse($items);
1679 Logger::log('Fetching timeline from ID ' . $lastid . ' for user ' . $uid . ' ' . sizeof($posts) . ' items', Logger::DEBUG);
1681 if (count($posts)) {
1682 foreach ($posts as $post) {
1683 if ($post->id_str > $lastid) {
1684 $lastid = $post->id_str;
1685 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1692 if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1693 Logger::log("Skip previously sent post", Logger::DEBUG);
1697 if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1698 Logger::log("Skip post that will be mirrored", Logger::DEBUG);
1702 if ($post->in_reply_to_status_id_str != "") {
1703 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1706 Logger::log('Preparing post ' . $post->id_str . ' for user ' . $uid, Logger::DEBUG);
1708 $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1710 if (empty($postarray['body']) || trim($postarray['body']) == "") {
1711 Logger::log('Empty body for post ' . $post->id_str . ' and user ' . $uid, Logger::DEBUG);
1717 if (($postarray['uri'] == $postarray['parent-uri']) && ($postarray['author-link'] == $postarray['owner-link'])) {
1718 $contact = DBA::selectFirst('contact', [], ['id' => $postarray['contact-id'], 'self' => false]);
1719 if (DBA::isResult($contact)) {
1720 $notify = Item::isRemoteSelf($contact, $postarray);
1724 $item = Item::insert($postarray, false, $notify);
1725 $postarray["id"] = $item;
1727 Logger::log('User ' . $uid . ' posted home timeline item ' . $item);
1730 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1732 Logger::log('Last timeline ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1734 // Fetching mentions
1735 $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1737 $first_time = ($lastid == "");
1739 if ($lastid != "") {
1740 $parameters["since_id"] = $lastid;
1744 $items = $connection->get('statuses/mentions_timeline', $parameters);
1745 } catch (TwitterOAuthException $e) {
1746 Logger::log('Error fetching mentions: ' . $e->getMessage());
1750 if (!is_array($items)) {
1751 Logger::log("Error fetching mentions: " . print_r($items, true), Logger::DEBUG);
1755 $posts = array_reverse($items);
1757 Logger::log("Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", Logger::DEBUG);
1759 if (count($posts)) {
1760 foreach ($posts as $post) {
1761 if ($post->id_str > $lastid) {
1762 $lastid = $post->id_str;
1769 if ($post->in_reply_to_status_id_str != "") {
1770 twitter_fetchparentposts($a, $uid, $post, $connection, $self);
1773 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1775 if (empty($postarray['body'])) {
1779 $item = Item::insert($postarray);
1781 Logger::log('User ' . $uid . ' posted mention timeline item ' . $item);
1785 PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1787 Logger::log('Last mentions ID for user ' . $uid . ' is now ' . $lastid, Logger::DEBUG);
1790 function twitter_fetch_own_contact(App $a, $uid)
1792 $ckey = Config::get('twitter', 'consumerkey');
1793 $csecret = Config::get('twitter', 'consumersecret');
1794 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1795 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1797 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1801 if ($own_id == "") {
1802 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1804 // Fetching user data
1805 // get() may throw TwitterOAuthException, but we will catch it later
1806 $user = $connection->get('account/verify_credentials');
1807 if (empty($user) || empty($user->id_str)) {
1811 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1813 $contact_id = twitter_fetch_contact($uid, $user, true);
1815 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1817 DBA::escape("twitter::" . $own_id));
1818 if (DBA::isResult($r)) {
1819 $contact_id = $r[0]["id"];
1821 PConfig::delete($uid, 'twitter', 'own_id');
1828 function twitter_is_retweet(App $a, $uid, $body)
1830 $body = trim($body);
1832 // Skip if it isn't a pure repeated messages
1833 // Does it start with a share?
1834 if (strpos($body, "[share") > 0) {
1838 // Does it end with a share?
1839 if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1843 $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1844 // Skip if there is no shared message in there
1845 if ($body == $attributes) {
1850 preg_match("/link='(.*?)'/ism", $attributes, $matches);
1851 if (!empty($matches[1])) {
1852 $link = $matches[1];
1855 preg_match('/link="(.*?)"/ism', $attributes, $matches);
1856 if (!empty($matches[1])) {
1857 $link = $matches[1];
1860 $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1865 Logger::log('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, Logger::DEBUG);
1867 $ckey = Config::get('twitter', 'consumerkey');
1868 $csecret = Config::get('twitter', 'consumersecret');
1869 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1870 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1872 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1873 $result = $connection->post('statuses/retweet/' . $id);
1875 Logger::log('twitter_is_retweet: result ' . print_r($result, true), Logger::DEBUG);
1877 return !isset($result->errors);
1880 function twitter_update_mentions($body)
1882 $URLSearchString = "^\[\]";
1883 $return = preg_replace_callback(
1884 "/@\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
1885 function ($matches) {
1886 if (strpos($matches[1], 'twitter.com')) {
1887 $return = '@' . substr($matches[1], strrpos($matches[1], '/') + 1);
1889 $return = $matches[2] . ' (' . $matches[1] . ')';
1900 function twitter_convert_share(array $attributes, array $author_contact, $content, $is_quote_share)
1902 if ($author_contact['network'] == Protocol::TWITTER) {
1903 $mention = '@' . $author_contact['nickname'];
1905 $mention = $author_contact['addr'];
1908 return ($is_quote_share ? "\n\n" : '' ) . 'RT ' . $mention . ': ' . $content . "\n\n" . $attributes['link'];