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 .htconfig.php or use the admin panel.
53 * $a->config['twitter']['consumerkey'] = 'your consumer_key here';
54 * $a->config['twitter']['consumersecret'] = 'your consumer_secret here';
56 * To activate the addon itself add it to the $a->config['system']['addon']
57 * setting. After this, your user can configure their Twitter account settings
58 * from "Settings -> Addon Settings".
60 * Requirements: PHP5, curl
63 use Abraham\TwitterOAuth\TwitterOAuth;
65 use Friendica\Content\OEmbed;
66 use Friendica\Content\Text\BBCode;
67 use Friendica\Content\Text\Plaintext;
68 use Friendica\Core\Addon;
69 use Friendica\Core\Config;
70 use Friendica\Core\L10n;
71 use Friendica\Core\PConfig;
72 use Friendica\Core\Worker;
73 use Friendica\Model\GContact;
74 use Friendica\Model\Group;
75 use Friendica\Model\Item;
76 use Friendica\Model\Photo;
77 use Friendica\Model\Queue;
78 use Friendica\Model\User;
79 use Friendica\Object\Image;
80 use Friendica\Util\DateTimeFormat;
81 use Friendica\Util\Network;
83 require_once 'boot.php';
84 require_once 'include/dba.php';
85 require_once 'include/enotify.php';
86 require_once 'include/text.php';
88 require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
90 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
92 function twitter_install()
94 // we need some hooks, for the configuration and for sending tweets
95 Addon::registerHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
96 Addon::registerHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
97 Addon::registerHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
98 Addon::registerHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
99 Addon::registerHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
100 Addon::registerHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
101 Addon::registerHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
102 Addon::registerHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
103 Addon::registerHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
104 Addon::registerHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
105 Addon::registerHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
106 logger("installed twitter");
109 function twitter_uninstall()
111 Addon::unregisterHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
112 Addon::unregisterHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
113 Addon::unregisterHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
114 Addon::unregisterHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
115 Addon::unregisterHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
116 Addon::unregisterHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
117 Addon::unregisterHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
118 Addon::unregisterHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
119 Addon::unregisterHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
120 Addon::unregisterHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
121 Addon::unregisterHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
123 // old setting - remove only
124 Addon::unregisterHook('post_local_end', 'addon/twitter/twitter.php', 'twitter_post_hook');
125 Addon::unregisterHook('addon_settings', 'addon/twitter/twitter.php', 'twitter_settings');
126 Addon::unregisterHook('addon_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
129 function twitter_check_item_notification(App $a, &$notification_data)
131 $own_id = PConfig::get($notification_data["uid"], 'twitter', 'own_id');
133 $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
134 intval($notification_data["uid"]),
135 dbesc("twitter::".$own_id)
139 $notification_data["profiles"][] = $own_user[0]["url"];
143 function twitter_follow(App $a, &$contact)
145 logger("twitter_follow: Check if contact is twitter contact. " . $contact["url"], LOGGER_DEBUG);
147 if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
151 // contact seems to be a twitter contact, so continue
152 $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
153 $nickname = str_replace("@twitter.com", "", $nickname);
155 $uid = $a->user["uid"];
157 $ckey = Config::get('twitter', 'consumerkey');
158 $csecret = Config::get('twitter', 'consumersecret');
159 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
160 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
162 // If the addon is not configured (general or for this user) quit here
163 if (empty($ckey) || empty($csecret) || empty($otoken) || empty($osecret)) {
168 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
169 $connection->post('friendships/create', ['screen_name' => $nickname]);
171 twitter_fetchuser($a, $uid, $nickname);
173 $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
174 FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
178 $contact["contact"] = $r[0];
182 function twitter_jot_nets(App $a, &$b)
188 $tw_post = PConfig::get(local_user(), 'twitter', 'post');
189 if (intval($tw_post) == 1) {
190 $tw_defpost = PConfig::get(local_user(), 'twitter', 'post_by_default');
191 $selected = ((intval($tw_defpost) == 1) ? ' checked="checked" ' : '');
192 $b .= '<div class="profile-jot-net"><input type="checkbox" name="twitter_enable"' . $selected . ' value="1" /> '
193 . L10n::t('Post to Twitter') . '</div>';
197 function twitter_settings_post(App $a, $post)
202 // don't check twitter settings if twitter submit button is not clicked
203 if (empty($_POST['twitter-disconnect']) && empty($_POST['twitter-submit'])) {
207 if (!empty($_POST['twitter-disconnect'])) {
209 * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
210 * from the user configuration
212 PConfig::delete(local_user(), 'twitter', 'consumerkey');
213 PConfig::delete(local_user(), 'twitter', 'consumersecret');
214 PConfig::delete(local_user(), 'twitter', 'oauthtoken');
215 PConfig::delete(local_user(), 'twitter', 'oauthsecret');
216 PConfig::delete(local_user(), 'twitter', 'post');
217 PConfig::delete(local_user(), 'twitter', 'post_by_default');
218 PConfig::delete(local_user(), 'twitter', 'lastid');
219 PConfig::delete(local_user(), 'twitter', 'mirror_posts');
220 PConfig::delete(local_user(), 'twitter', 'import');
221 PConfig::delete(local_user(), 'twitter', 'create_user');
222 PConfig::delete(local_user(), 'twitter', 'own_id');
224 if (isset($_POST['twitter-pin'])) {
225 // if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
226 logger('got a Twitter PIN');
227 $ckey = Config::get('twitter', 'consumerkey');
228 $csecret = Config::get('twitter', 'consumersecret');
229 // the token and secret for which the PIN was generated were hidden in the settings
230 // form as token and token2, we need a new connection to Twitter using these token
231 // and secret to request a Access Token with the PIN
233 if (empty($_POST['twitter-pin'])) {
234 throw new Exception(L10n::t('You submitted an empty PIN, please Sign In with Twitter again to get a new one.'));
237 $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
238 $token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $_POST['twitter-pin']]);
239 // ok, now that we have the Access Token, save them in the user config
240 PConfig::set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
241 PConfig::set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
242 PConfig::set(local_user(), 'twitter', 'post', 1);
243 } catch(Exception $e) {
244 info($e->getMessage());
246 // reload the Addon Settings page, if we don't do it see Bug #42
247 goaway('settings/connectors');
249 // if no PIN is supplied in the POST variables, the user has changed the setting
250 // to post a tweet for every new __public__ posting to the wall
251 PConfig::set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
252 PConfig::set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
253 PConfig::set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
254 PConfig::set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
255 PConfig::set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
257 if (!intval($_POST['twitter-mirror'])) {
258 PConfig::delete(local_user(), 'twitter', 'lastid');
261 info(L10n::t('Twitter settings updated.') . EOL);
266 function twitter_settings(App $a, &$s)
271 $a->page['htmlhead'] .= '<link rel="stylesheet" type="text/css" href="' . $a->get_baseurl() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
273 * 1) Check that we have global consumer key & secret
274 * 2) If no OAuthtoken & stuff is present, generate button to get some
275 * 3) Checkbox for "Send public notices (280 chars only)
277 $ckey = Config::get('twitter', 'consumerkey');
278 $csecret = Config::get('twitter', 'consumersecret');
279 $otoken = PConfig::get(local_user(), 'twitter', 'oauthtoken');
280 $osecret = PConfig::get(local_user(), 'twitter', 'oauthsecret');
282 $enabled = intval(PConfig::get(local_user(), 'twitter', 'post'));
283 $defenabled = intval(PConfig::get(local_user(), 'twitter', 'post_by_default'));
284 $mirrorenabled = intval(PConfig::get(local_user(), 'twitter', 'mirror_posts'));
285 $importenabled = intval(PConfig::get(local_user(), 'twitter', 'import'));
286 $create_userenabled = intval(PConfig::get(local_user(), 'twitter', 'create_user'));
288 $css = (($enabled) ? '' : '-disabled');
290 $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
291 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
293 $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
294 $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
295 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
298 if ((!$ckey) && (!$csecret)) {
299 /* no global consumer keys
300 * display warning and skip personal config
302 $s .= '<p>' . L10n::t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
304 // ok we have a consumer key pair now look into the OAuth stuff
305 if ((!$otoken) && (!$osecret)) {
306 /* the user has not yet connected the account to twitter...
307 * get a temporary OAuth key/secret pair and display a button with
308 * which the user can request a PIN to connect the account to a
309 * account at Twitter.
311 $connection = new TwitterOAuth($ckey, $csecret);
312 $result = $connection->oauth('oauth/request_token', ['oauth_callback' => 'oob']);
314 $s .= '<p>' . L10n::t('At this Friendica instance the Twitter addon was enabled but you have not yet connected your account to your Twitter account. To do so click the button below to get a PIN from Twitter which you have to copy into the input box below and submit the form. Only your <strong>public</strong> posts will be posted to Twitter.') . '</p>';
315 $s .= '<a href="' . $connection->url('oauth/authorize', ['oauth_token' => $result['oauth_token']]) . '" target="_twitter"><img src="addon/twitter/lighter.png" alt="' . L10n::t('Log in with Twitter') . '"></a>';
316 $s .= '<div id="twitter-pin-wrapper">';
317 $s .= '<label id="twitter-pin-label" for="twitter-pin">' . L10n::t('Copy the PIN from Twitter here') . '</label>';
318 $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
319 $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $result['oauth_token'] . '" />';
320 $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $result['oauth_token_secret'] . '" />';
321 $s .= '</div><div class="clear"></div>';
322 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
325 * we have an OAuth key / secret pair for the user
326 * so let's give a chance to disable the postings to Twitter
328 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
329 $details = $connection->get('account/verify_credentials');
331 $field_checkbox = get_markup_template('field_checkbox.tpl');
333 $s .= '<div id="twitter-info" >
334 <p>' . L10n::t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
335 <button type="submit" name="twitter-disconnect" value="1">' . L10n::t('Disconnect') . '</button>
337 <p id="twitter-info-block">
338 <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
339 <em>' . $details->description . '</em>
342 $s .= '<div class="clear"></div>';
344 $s .= replace_macros($field_checkbox, [
345 '$field' => ['twitter-enable', L10n::t('Allow posting to Twitter'), $enabled, L10n::t('If enabled all your <strong>public</strong> postings can be posted to the associated Twitter account. You can choose to do so by default (here) or for every posting separately in the posting options when writing the entry.')]
347 if ($a->user['hidewall']) {
348 $s .= '<p>' . L10n::t('<strong>Note</strong>: Due to your privacy settings (<em>Hide your profile details from unknown viewers?</em>) the link potentially included in public postings relayed to Twitter will lead the visitor to a blank page informing the visitor that the access to your profile has been restricted.') . '</p>';
350 $s .= replace_macros($field_checkbox, [
351 '$field' => ['twitter-default', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
353 $s .= replace_macros($field_checkbox, [
354 '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
356 $s .= replace_macros($field_checkbox, [
357 '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
359 $s .= replace_macros($field_checkbox, [
360 '$field' => ['twitter-create_user', L10n::t('Automatically create contacts'), $create_userenabled, '']
363 $s .= '<div class="clear"></div>';
364 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
367 $s .= '</div><div class="clear"></div>';
370 function twitter_post_local(App $a, &$b)
376 if (!local_user() || (local_user() != $b['uid'])) {
380 $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
381 $twitter_enable = (($twitter_post && x($_REQUEST, 'twitter_enable')) ? intval($_REQUEST['twitter_enable']) : 0);
383 // if API is used, default to the chosen settings
384 if ($b['api_source'] && intval(PConfig::get(local_user(), 'twitter', 'post_by_default'))) {
388 if (!$twitter_enable) {
392 if (strlen($b['postopts'])) {
393 $b['postopts'] .= ',';
396 $b['postopts'] .= 'twitter';
399 function twitter_action(App $a, $uid, $pid, $action)
401 $ckey = Config::get('twitter', 'consumerkey');
402 $csecret = Config::get('twitter', 'consumersecret');
403 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
404 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
406 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
408 $post = ['id' => $pid];
410 logger("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), LOGGER_DATA);
414 // To-Do: $result = $connection->post('statuses/destroy', $post);
417 $result = $connection->post('favorites/create', $post);
420 $result = $connection->post('favorites/destroy', $post);
423 logger("twitter_action '" . $action . "' send, result: " . print_r($result, true), LOGGER_DEBUG);
426 function twitter_post_hook(App $a, &$b)
429 if (!PConfig::get($b["uid"], 'twitter', 'import')
430 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
434 if ($b['parent'] != $b['id']) {
435 logger("twitter_post_hook: parameter " . print_r($b, true), LOGGER_DATA);
437 // Looking if its a reply to a twitter post
438 if ((substr($b["parent-uri"], 0, 9) != "twitter::")
439 && (substr($b["extid"], 0, 9) != "twitter::")
440 && (substr($b["thr-parent"], 0, 9) != "twitter::"))
442 logger("twitter_post_hook: no twitter post " . $b["parent"]);
446 $r = q("SELECT * FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
447 dbesc($b["thr-parent"]),
451 logger("twitter_post_hook: no parent found " . $b["thr-parent"]);
459 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
460 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
461 $nicknameplain = "@" . $nicknameplain;
463 logger("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], LOGGER_DEBUG);
464 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
465 $b["body"] = $nickname . " " . $b["body"];
468 logger("twitter_post_hook: parent found " . print_r($orig_post, true), LOGGER_DATA);
472 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
476 // Dont't post if the post doesn't belong to us.
477 // This is a check for forum postings
478 $self = dba::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
479 if ($b['contact-id'] != $self['id']) {
484 if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
485 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
488 if ($b['verb'] == ACTIVITY_LIKE) {
489 logger("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), LOGGER_DEBUG);
491 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
493 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
499 if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
503 // if post comes from twitter don't send it back
504 if ($b['extid'] == NETWORK_TWITTER) {
508 if ($b['app'] == "Twitter") {
512 logger('twitter post invoked');
514 PConfig::load($b['uid'], 'twitter');
516 $ckey = Config::get('twitter', 'consumerkey');
517 $csecret = Config::get('twitter', 'consumersecret');
518 $otoken = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
519 $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
521 if ($ckey && $csecret && $otoken && $osecret) {
522 logger('twitter: we have customer key and oauth stuff, going to send.', LOGGER_DEBUG);
524 // If it's a repeated message from twitter then do a native retweet and exit
525 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
529 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
532 $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
533 $msg = $msgarr["text"];
535 if (($msg == "") && isset($msgarr["title"])) {
536 $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
541 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
542 $msg .= "\n" . $msgarr["url"];
545 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
546 $image = $msgarr["image"];
549 // and now tweet it :-)
550 if (strlen($msg) && ($image != "")) {
552 $media = $connection->upload('media/upload', ['media' => $image]);
554 $post = ['status' => $msg, 'media_ids' => $media->media_id_string];
557 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
560 $result = $connection->post('statuses/update', $post);
562 logger('twitter_post_with_media send, result: ' . print_r($result, true), LOGGER_DEBUG);
564 if ($result->source) {
565 Config::set("twitter", "application_name", strip_tags($result->source));
568 if ($result->errors || $result->error) {
569 logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
571 // Workaround: Remove the picture link so that the post can be reposted without it
572 $msg .= " " . $image;
574 } elseif ($iscomment) {
575 logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
576 Item::update(['extid' => "twitter::" . $result->id_str, 'body' => $result->text], ['id' => $b['id']]);
578 } catch (Exception $e) {
579 logger('Exception when trying to send to Twitter: ' . $e->getMessage());
581 // Workaround: Remove the picture link so that the post can be reposted without it
582 $msg .= " " . $image;
587 if (strlen($msg) && ($image == "")) {
590 $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
591 $msg = $msgarr["text"];
593 if (($msg == "") && isset($msgarr["title"])) {
594 $msg = Plaintext::shorten($msgarr["title"], $max_char - 50);
597 if (isset($msgarr["url"])) {
598 $msg .= "\n" . $msgarr["url"];
601 $url = 'statuses/update';
602 $post = ['status' => $msg, 'weighted_character_count' => 'true'];
605 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
608 $result = $connection->post($url, $post);
609 logger('twitter_post send, result: ' . print_r($result, true), LOGGER_DEBUG);
611 if ($result->source) {
612 Config::set("twitter", "application_name", strip_tags($result->source));
615 if ($result->errors) {
616 logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
618 $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
620 $a->contact = $r[0]["id"];
623 $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
625 Queue::add($a->contact, NETWORK_TWITTER, $s);
626 notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
627 } elseif ($iscomment) {
628 logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
629 Item::update(['extid' => "twitter::" . $result->id_str], ['id' => $b['id']]);
635 function twitter_addon_admin_post(App $a)
637 $consumerkey = x($_POST, 'consumerkey') ? notags(trim($_POST['consumerkey'])) : '';
638 $consumersecret = x($_POST, 'consumersecret') ? notags(trim($_POST['consumersecret'])) : '';
639 Config::set('twitter', 'consumerkey', $consumerkey);
640 Config::set('twitter', 'consumersecret', $consumersecret);
641 info(L10n::t('Settings updated.') . EOL);
644 function twitter_addon_admin(App $a, &$o)
646 $t = get_markup_template("admin.tpl", "addon/twitter/");
648 $o = replace_macros($t, [
649 '$submit' => L10n::t('Save Settings'),
650 // name, label, value, help, [extra values]
651 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
652 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
656 function twitter_cron(App $a, $b)
658 $last = Config::get('twitter', 'last_poll');
660 $poll_interval = intval(Config::get('twitter', 'poll_interval'));
661 if (!$poll_interval) {
662 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
666 $next = $last + ($poll_interval * 60);
667 if ($next > time()) {
668 logger('twitter: poll intervall not reached');
672 logger('twitter: cron_start');
674 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
676 foreach ($r as $rr) {
677 logger('twitter: fetching for user ' . $rr['uid']);
678 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
682 $abandon_days = intval(Config::get('system', 'account_abandon_days'));
683 if ($abandon_days < 1) {
687 $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400);
689 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
691 foreach ($r as $rr) {
692 if ($abandon_days != 0) {
693 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
695 logger('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
700 logger('twitter: importing timeline from user ' . $rr['uid']);
701 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
704 // check for new contacts once a day
705 $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
706 if($last_contact_check)
707 $next_contact_check = $last_contact_check + 86400;
709 $next_contact_check = 0;
711 if($next_contact_check <= time()) {
712 pumpio_getallusers($a, $rr["uid"]);
713 PConfig::set($rr['uid'],'pumpio','contact_check',time());
719 logger('twitter: cron_end');
721 Config::set('twitter', 'last_poll', time());
724 function twitter_expire(App $a, $b)
726 $days = Config::get('twitter', 'expire');
732 if (method_exists('dba', 'delete')) {
733 $r = dba::select('item', ['id'], ['deleted' => true, 'network' => NETWORK_TWITTER]);
734 while ($row = dba::fetch($r)) {
735 dba::delete('item', ['id' => $row['id']]);
739 $r = q("DELETE FROM `item` WHERE `deleted` AND `network` = '%s'", dbesc(NETWORK_TWITTER));
742 require_once "include/items.php";
744 logger('twitter_expire: expire_start');
746 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
748 foreach ($r as $rr) {
749 logger('twitter_expire: user ' . $rr['uid']);
750 Item::expire($rr['uid'], $days, NETWORK_TWITTER, true);
754 logger('twitter_expire: expire_end');
757 function twitter_prepare_body(App $a, &$b)
759 if ($b["item"]["network"] != NETWORK_TWITTER) {
766 $item["plink"] = $a->get_baseurl() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
768 $r = q("SELECT `author-link` FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
769 dbesc($item["thr-parent"]),
770 intval(local_user()));
775 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
776 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
777 $nicknameplain = "@" . $nicknameplain;
779 if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
780 $item["body"] = $nickname . " " . $item["body"];
784 $msgarr = BBCode::toPlaintext($item, $max_char, true, 8);
785 $msg = $msgarr["text"];
787 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
788 $msg .= " " . $msgarr["url"];
791 if (isset($msgarr["image"])) {
792 $msg .= " " . $msgarr["image"];
795 $b['html'] = nl2br(htmlspecialchars($msg));
800 * @brief Build the item array for the mirrored post
802 * @param App $a Application class
803 * @param integer $uid User id
804 * @param object $post Twitter object with the post
806 * @return array item data to be posted
808 function twitter_do_mirrorpost(App $a, $uid, $post)
810 $datarray["type"] = "wall";
811 $datarray["api_source"] = true;
812 $datarray["profile_uid"] = $uid;
813 $datarray["extid"] = NETWORK_TWITTER;
814 $datarray['message_id'] = item_new_uri($a->get_hostname(), $uid, NETWORK_TWITTER . ":" . $post->id);
815 $datarray['object'] = json_encode($post);
816 $datarray["title"] = "";
818 if (is_object($post->retweeted_status)) {
819 // We don't support nested shares, so we mustn't show quotes as shares on retweets
820 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
822 $datarray['body'] = "\n" . share_header(
823 $item['author-name'],
824 $item['author-link'],
825 $item['author-avatar'],
831 $datarray['body'] .= $item['body'] . '[/share]';
833 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
835 $datarray['body'] = $item['body'];
838 $datarray["source"] = $item['app'];
839 $datarray["verb"] = $item['verb'];
841 if (isset($item["location"])) {
842 $datarray["location"] = $item["location"];
845 if (isset($item["coord"])) {
846 $datarray["coord"] = $item["coord"];
852 function twitter_fetchtimeline(App $a, $uid)
854 $ckey = Config::get('twitter', 'consumerkey');
855 $csecret = Config::get('twitter', 'consumersecret');
856 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
857 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
858 $lastid = PConfig::get($uid, 'twitter', 'lastid');
860 $application_name = Config::get('twitter', 'application_name');
862 if ($application_name == "") {
863 $application_name = $a->get_hostname();
866 $has_picture = false;
868 require_once 'mod/item.php';
869 require_once 'include/items.php';
870 require_once 'mod/share.php';
872 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
874 $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
876 $first_time = ($lastid == "");
879 $parameters["since_id"] = $lastid;
882 $items = $connection->get('statuses/user_timeline', $parameters);
884 if (!is_array($items)) {
888 $posts = array_reverse($items);
891 foreach ($posts as $post) {
892 if ($post->id_str > $lastid) {
893 $lastid = $post->id_str;
894 PConfig::set($uid, 'twitter', 'lastid', $lastid);
901 if (!stristr($post->source, $application_name)) {
902 $_SESSION["authenticated"] = true;
903 $_SESSION["uid"] = $uid;
905 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
907 logger('twitter: posting for user ' . $uid);
913 PConfig::set($uid, 'twitter', 'lastid', $lastid);
916 function twitter_queue_hook(App $a, &$b)
918 $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
919 dbesc(NETWORK_TWITTER)
925 foreach ($qi as $x) {
926 if ($x['network'] !== NETWORK_TWITTER) {
930 logger('twitter_queue: run');
932 $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
933 WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
942 $ckey = Config::get('twitter', 'consumerkey');
943 $csecret = Config::get('twitter', 'consumersecret');
944 $otoken = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
945 $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
949 if ($ckey && $csecret && $otoken && $osecret) {
950 logger('twitter_queue: able to post');
952 $z = unserialize($x['content']);
954 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
955 $result = $connection->post($z['url'], $z['post']);
957 logger('twitter_queue: post result: ' . print_r($result, true), LOGGER_DEBUG);
959 if ($result->errors) {
960 logger('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
963 Queue::removeItem($x['id']);
966 logger("twitter_queue: Error getting tokens for user " . $user['uid']);
970 logger('twitter_queue: delayed');
971 Queue::updateTime($x['id']);
976 function twitter_fix_avatar($avatar)
978 $new_avatar = str_replace("_normal.", ".", $avatar);
980 $info = Image::getInfoFromURL($new_avatar);
982 $new_avatar = $avatar;
988 function twitter_fetch_contact($uid, $contact, $create_user)
990 if ($contact->id_str == "") {
994 $avatar = twitter_fix_avatar($contact->profile_image_url_https);
996 GContact::update(["url" => "https://twitter.com/" . $contact->screen_name,
997 "network" => NETWORK_TWITTER, "photo" => $avatar, "hide" => true,
998 "name" => $contact->name, "nick" => $contact->screen_name,
999 "location" => $contact->location, "about" => $contact->description,
1000 "addr" => $contact->screen_name . "@twitter.com", "generation" => 2]);
1002 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1004 dbesc("twitter::" . $contact->id_str));
1006 if (!count($r) && !$create_user) {
1010 if (count($r) && ($r[0]["readonly"] || $r[0]["blocked"])) {
1011 logger("twitter_fetch_contact: Contact '" . $r[0]["nick"] . "' is blocked or readonly.", LOGGER_DEBUG);
1016 // create contact record
1017 q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `addr`, `alias`, `notify`, `poll`,
1018 `name`, `nick`, `photo`, `network`, `rel`, `priority`,
1019 `location`, `about`, `writable`, `blocked`, `readonly`, `pending`)
1020 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', %d, 0, 0, 0)",
1022 dbesc(DateTimeFormat::utcNow()),
1023 dbesc("https://twitter.com/" . $contact->screen_name),
1024 dbesc(normalise_link("https://twitter.com/" . $contact->screen_name)),
1025 dbesc($contact->screen_name."@twitter.com"),
1026 dbesc("twitter::" . $contact->id_str),
1028 dbesc("twitter::" . $contact->id_str),
1029 dbesc($contact->name),
1030 dbesc($contact->screen_name),
1032 dbesc(NETWORK_TWITTER),
1033 intval(CONTACT_IS_FRIEND),
1035 dbesc($contact->location),
1036 dbesc($contact->description),
1040 $r = q("SELECT * FROM `contact` WHERE `alias` = '%s' AND `uid` = %d LIMIT 1",
1041 dbesc("twitter::".$contact->id_str),
1049 $contact_id = $r[0]['id'];
1051 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1053 $photos = Photo::importProfilePhoto($avatar, $uid, $contact_id, true);
1056 q("UPDATE `contact` SET `photo` = '%s',
1061 `avatar-date` = '%s'
1066 dbesc(DateTimeFormat::utcNow()),
1067 dbesc(DateTimeFormat::utcNow()),
1068 dbesc(DateTimeFormat::utcNow()),
1073 // update profile photos once every two weeks as we have no notification of when they change.
1074 //$update_photo = (($r[0]['avatar-date'] < DateTimeFormat::convert('now -2 days', '', '', )) ? true : false);
1075 $update_photo = ($r[0]['avatar-date'] < DateTimeFormat::utc('now -12 hours'));
1077 // check that we have all the photos, this has been known to fail on occasion
1078 if ((!$r[0]['photo']) || (!$r[0]['thumb']) || (!$r[0]['micro']) || ($update_photo)) {
1079 logger("twitter_fetch_contact: Updating contact " . $contact->screen_name, LOGGER_DEBUG);
1081 $photos = Photo::importProfilePhoto($avatar, $uid, $r[0]['id'], true);
1084 q("UPDATE `contact` SET `photo` = '%s',
1089 `avatar-date` = '%s',
1101 dbesc(DateTimeFormat::utcNow()),
1102 dbesc(DateTimeFormat::utcNow()),
1103 dbesc(DateTimeFormat::utcNow()),
1104 dbesc("https://twitter.com/".$contact->screen_name),
1105 dbesc(normalise_link("https://twitter.com/".$contact->screen_name)),
1106 dbesc($contact->screen_name."@twitter.com"),
1107 dbesc($contact->name),
1108 dbesc($contact->screen_name),
1109 dbesc($contact->location),
1110 dbesc($contact->description),
1120 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1122 $ckey = Config::get('twitter', 'consumerkey');
1123 $csecret = Config::get('twitter', 'consumersecret');
1124 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1125 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1127 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1138 if ($screen_name != "") {
1139 $parameters["screen_name"] = $screen_name;
1142 if ($user_id != "") {
1143 $parameters["user_id"] = $user_id;
1146 // Fetching user data
1147 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1148 $user = $connection->get('users/show', $parameters);
1150 if (!is_object($user)) {
1154 $contact_id = twitter_fetch_contact($uid, $user, true);
1159 function twitter_expand_entities(App $a, $body, $item, $picture)
1165 foreach ($item->entities->hashtags AS $hashtag) {
1166 $url = "#[url=" . $a->get_baseurl() . "/search?tag=" . rawurlencode($hashtag->text) . "]" . $hashtag->text . "[/url]";
1167 $tags_arr["#" . $hashtag->text] = $url;
1168 $body = str_replace("#" . $hashtag->text, $url, $body);
1171 foreach ($item->entities->user_mentions AS $mention) {
1172 $url = "@[url=https://twitter.com/" . rawurlencode($mention->screen_name) . "]" . $mention->screen_name . "[/url]";
1173 $tags_arr["@" . $mention->screen_name] = $url;
1174 $body = str_replace("@" . $mention->screen_name, $url, $body);
1177 if (isset($item->entities->urls)) {
1183 foreach ($item->entities->urls as $url) {
1184 $plain = str_replace($url->url, '', $plain);
1186 if ($url->url && $url->expanded_url && $url->display_url) {
1187 $expanded_url = Network::finalUrl($url->expanded_url);
1189 $oembed_data = OEmbed::fetchURL($expanded_url);
1191 // Quickfix: Workaround for URL with "[" and "]" in it
1192 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1193 $expanded_url = $url->url;
1197 $type = $oembed_data->type;
1200 if ($oembed_data->type == "video") {
1201 //$body = str_replace($url->url,
1202 // "[video]".$expanded_url."[/video]", $body);
1203 //$dontincludemedia = true;
1204 $type = $oembed_data->type;
1205 $footerurl = $expanded_url;
1206 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1208 $body = str_replace($url->url, $footerlink, $body);
1209 //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1210 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1211 $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1212 //$dontincludemedia = true;
1213 } elseif ($oembed_data->type != "link") {
1214 $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1216 $img_str = Network::fetchUrl($expanded_url, true, $redirects, 4);
1218 $tempfile = tempnam(get_temppath(), "cache");
1219 file_put_contents($tempfile, $img_str);
1220 $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1223 if (substr($mime, 0, 6) == "image/") {
1225 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1226 //$dontincludemedia = true;
1228 $type = $oembed_data->type;
1229 $footerurl = $expanded_url;
1230 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1232 $body = str_replace($url->url, $footerlink, $body);
1238 if ($footerurl != "") {
1239 $footer = add_page_info($footerurl, false, $picture);
1242 if (($footerlink != "") && (trim($footer) != "")) {
1243 $removedlink = trim(str_replace($footerlink, "", $body));
1245 if (($removedlink == "") || strstr($body, $removedlink)) {
1246 $body = $removedlink;
1252 if (($footer == "") && ($picture != "")) {
1253 $body .= "\n\n[img]" . $picture . "[/img]\n";
1254 } elseif (($footer == "") && ($picture == "")) {
1255 $body = add_page_info_to_body($body);
1259 // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1260 $tags = get_tags($body);
1263 foreach ($tags as $tag) {
1264 if (strstr(trim($tag), " ")) {
1268 if (strpos($tag, '#') === 0) {
1269 if (strpos($tag, '[url=')) {
1273 // don't link tags that are already embedded in links
1274 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1277 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1281 $basetag = str_replace('_', ' ', substr($tag, 1));
1282 $url = '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1283 $body = str_replace($tag, $url, $body);
1284 $tags_arr["#" . $basetag] = $url;
1285 } elseif (strpos($tag, '@') === 0) {
1286 if (strpos($tag, '[url=')) {
1290 $basetag = substr($tag, 1);
1291 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1292 $body = str_replace($tag, $url, $body);
1293 $tags_arr["@" . $basetag] = $url;
1298 $tags = implode($tags_arr, ",");
1300 return ["body" => $body, "tags" => $tags, "plain" => $plain];
1304 * @brief Fetch media entities and add media links to the body
1306 * @param object $post Twitter object with the post
1307 * @param array $postarray Array of the item that is about to be posted
1309 * @return $picture string Image URL or empty string
1311 function twitter_media_entities($post, &$postarray)
1313 // There are no media entities? So we quit.
1314 if (!is_array($post->extended_entities->media)) {
1318 // When the post links to an external page, we only take one picture.
1319 // We only do this when there is exactly one media.
1320 if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1322 foreach ($post->extended_entities->media AS $medium) {
1323 if (isset($medium->media_url_https)) {
1324 $picture = $medium->media_url_https;
1325 $postarray['body'] = str_replace($medium->url, "", $postarray['body']);
1331 // This is a pure media post, first search for all media urls
1333 foreach ($post->extended_entities->media AS $medium) {
1334 switch ($medium->type) {
1336 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1337 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1340 case 'animated_gif':
1341 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1342 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1343 if (is_array($medium->video_info->variants)) {
1345 // We take the video with the highest bitrate
1346 foreach ($medium->video_info->variants AS $variant) {
1347 if (($variant->content_type == "video/mp4") && ($variant->bitrate >= $bitrate)) {
1348 $media[$medium->url] = "\n[video]" . $variant->url . "[/video]";
1349 $bitrate = $variant->bitrate;
1354 // The following code will only be activated for test reasons
1356 // $postarray['body'] .= print_r($medium, true);
1360 // Now we replace the media urls.
1361 foreach ($media AS $key => $value) {
1362 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1367 function twitter_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact, $noquote)
1370 $postarray['network'] = NETWORK_TWITTER;
1371 $postarray['gravity'] = 0;
1372 $postarray['uid'] = $uid;
1373 $postarray['wall'] = 0;
1374 $postarray['uri'] = "twitter::" . $post->id_str;
1375 $postarray['object'] = json_encode($post);
1377 // Don't import our own comments
1378 $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1379 dbesc($postarray['uri']),
1384 logger("Item with extid " . $postarray['uri'] . " found.", LOGGER_DEBUG);
1390 if ($post->in_reply_to_status_id_str != "") {
1391 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1393 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1398 $postarray['thr-parent'] = $r[0]["uri"];
1399 $postarray['parent-uri'] = $r[0]["parent-uri"];
1400 $postarray['parent'] = $r[0]["parent"];
1401 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1403 $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1408 $postarray['thr-parent'] = $r[0]['uri'];
1409 $postarray['parent-uri'] = $r[0]['parent-uri'];
1410 $postarray['parent'] = $r[0]['parent'];
1411 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1413 $postarray['thr-parent'] = $postarray['uri'];
1414 $postarray['parent-uri'] = $postarray['uri'];
1415 $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",
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("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("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'] . '>';
1473 if (is_string($post->full_text)) {
1474 $postarray['body'] = $post->full_text;
1476 $postarray['body'] = $post->text;
1479 // When the post contains links then use the correct object type
1480 if (count($post->entities->urls) > 0) {
1481 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1484 // Search for media links
1485 $picture = twitter_media_entities($post, $postarray);
1487 $converted = twitter_expand_entities($a, $postarray['body'], $post, $picture);
1488 $postarray['body'] = $converted["body"];
1489 $postarray['tag'] = $converted["tags"];
1490 $postarray['created'] = DateTimeFormat::utc($post->created_at);
1491 $postarray['edited'] = DateTimeFormat::utc($post->created_at);
1493 $statustext = $converted["plain"];
1495 if (is_string($post->place->name)) {
1496 $postarray["location"] = $post->place->name;
1498 if (is_string($post->place->full_name)) {
1499 $postarray["location"] = $post->place->full_name;
1501 if (is_array($post->geo->coordinates)) {
1502 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1504 if (is_array($post->coordinates->coordinates)) {
1505 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1507 if (is_object($post->retweeted_status)) {
1508 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1510 $retweet['object'] = $postarray['object'];
1511 $retweet['private'] = $postarray['private'];
1512 $retweet['allow_cid'] = $postarray['allow_cid'];
1513 $retweet['contact-id'] = $postarray['contact-id'];
1514 $retweet['owner-name'] = $postarray['owner-name'];
1515 $retweet['owner-link'] = $postarray['owner-link'];
1516 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1518 $postarray = $retweet;
1521 if (is_object($post->quoted_status) && !$noquote) {
1522 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1524 $postarray['body'] = $statustext;
1526 $postarray['body'] .= "\n" . share_header(
1527 $quoted['author-name'],
1528 $quoted['author-link'],
1529 $quoted['author-avatar'],
1535 $postarray['body'] .= $quoted['body'] . '[/share]';
1541 function twitter_checknotification(App $a, $uid, $own_id, $top_item, $postarray)
1543 /// TODO: this whole function doesn't seem to work. Needs complete check
1544 $user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` LIMIT 1",
1548 if (!count($user)) {
1553 if (link_compare($user[0]["url"], $postarray['author-link'])) {
1557 $own_user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1559 dbesc("twitter::".$own_id)
1562 if (!count($own_user)) {
1566 // Is it me from twitter?
1567 if (link_compare($own_user[0]["url"], $postarray['author-link'])) {
1571 $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 AND `deleted` = 0",
1572 dbesc($postarray['parent-uri']),
1576 if (count($myconv)) {
1577 foreach ($myconv as $conv) {
1578 // now if we find a match, it means we're in this conversation
1579 if (!link_compare($conv['author-link'], $user[0]["url"]) && !link_compare($conv['author-link'], $own_user[0]["url"])) {
1583 require_once 'include/enotify.php';
1585 $conv_parent = $conv['parent'];
1588 'type' => NOTIFY_COMMENT,
1589 'notify_flags' => $user[0]['notify-flags'],
1590 'language' => $user[0]['language'],
1591 'to_name' => $user[0]['username'],
1592 'to_email' => $user[0]['email'],
1593 'uid' => $user[0]['uid'],
1594 'item' => $postarray,
1595 'link' => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($top_item)),
1596 'source_name' => $postarray['author-name'],
1597 'source_link' => $postarray['author-link'],
1598 'source_photo' => $postarray['author-avatar'],
1599 'verb' => ACTIVITY_POST,
1601 'parent' => $conv_parent,
1604 // only send one notification
1610 function twitter_fetchparentposts(App $a, $uid, $post, $connection, $self, $own_id)
1612 logger("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, LOGGER_DEBUG);
1616 while ($post->in_reply_to_status_id_str != "") {
1617 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1619 $post = $connection->get('statuses/show', $parameters);
1621 if (!count($post)) {
1622 logger("twitter_fetchparentposts: Can't fetch post " . $parameters->id, LOGGER_DEBUG);
1626 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1627 dbesc("twitter::".$post->id_str),
1638 logger("twitter_fetchparentposts: Fetching " . count($posts) . " parents", LOGGER_DEBUG);
1640 $posts = array_reverse($posts);
1642 if (count($posts)) {
1643 foreach ($posts as $post) {
1644 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1646 if (trim($postarray['body']) == "") {
1650 $item = Item::insert($postarray);
1651 $postarray["id"] = $item;
1653 logger('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1655 if ($item && !function_exists("check_item_notification")) {
1656 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1662 function twitter_fetchhometimeline(App $a, $uid)
1664 $ckey = Config::get('twitter', 'consumerkey');
1665 $csecret = Config::get('twitter', 'consumersecret');
1666 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1667 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1668 $create_user = PConfig::get($uid, 'twitter', 'create_user');
1669 $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1671 logger("twitter_fetchhometimeline: Fetching for user " . $uid, LOGGER_DEBUG);
1673 $application_name = Config::get('twitter', 'application_name');
1675 if ($application_name == "") {
1676 $application_name = $a->get_hostname();
1679 require_once 'include/items.php';
1681 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1683 $own_contact = twitter_fetch_own_contact($a, $uid);
1685 $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1686 intval($own_contact),
1690 $own_id = $r[0]["nick"];
1692 logger("twitter_fetchhometimeline: Own twitter contact not found for user " . $uid, LOGGER_DEBUG);
1696 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1702 logger("twitter_fetchhometimeline: Own contact not found for user " . $uid, LOGGER_DEBUG);
1706 $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1709 logger("twitter_fetchhometimeline: Own user not found for user " . $uid, LOGGER_DEBUG);
1713 $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1714 //$parameters["count"] = 200;
1715 // Fetching timeline
1716 $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1718 $first_time = ($lastid == "");
1720 if ($lastid != "") {
1721 $parameters["since_id"] = $lastid;
1724 $items = $connection->get('statuses/home_timeline', $parameters);
1726 if (!is_array($items)) {
1727 logger("twitter_fetchhometimeline: Error fetching home timeline: " . print_r($items, true), LOGGER_DEBUG);
1731 $posts = array_reverse($items);
1733 logger("twitter_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1735 if (count($posts)) {
1736 foreach ($posts as $post) {
1737 if ($post->id_str > $lastid) {
1738 $lastid = $post->id_str;
1739 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1746 if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1747 logger("twitter_fetchhometimeline: Skip previously sended post", LOGGER_DEBUG);
1751 if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1752 logger("twitter_fetchhometimeline: Skip post that will be mirrored", LOGGER_DEBUG);
1756 if ($post->in_reply_to_status_id_str != "") {
1757 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1760 $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1762 if (trim($postarray['body']) == "") {
1766 $item = Item::insert($postarray);
1767 $postarray["id"] = $item;
1769 logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1771 if ($item && !function_exists("check_item_notification")) {
1772 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1776 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1778 // Fetching mentions
1779 $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1781 $first_time = ($lastid == "");
1783 if ($lastid != "") {
1784 $parameters["since_id"] = $lastid;
1787 $items = $connection->get('statuses/mentions_timeline', $parameters);
1789 if (!is_array($items)) {
1790 logger("twitter_fetchhometimeline: Error fetching mentions: " . print_r($items, true), LOGGER_DEBUG);
1794 $posts = array_reverse($items);
1796 logger("twitter_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1798 if (count($posts)) {
1799 foreach ($posts as $post) {
1800 if ($post->id_str > $lastid) {
1801 $lastid = $post->id_str;
1808 if ($post->in_reply_to_status_id_str != "") {
1809 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1812 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1814 if (trim($postarray['body']) == "") {
1818 $item = Item::insert($postarray);
1819 $postarray["id"] = $item;
1821 if ($item && function_exists("check_item_notification")) {
1822 check_item_notification($item, $uid, NOTIFY_TAGSELF);
1825 if (!isset($postarray["parent"]) || ($postarray["parent"] == 0)) {
1826 $postarray["parent"] = $item;
1829 logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1832 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1833 dbesc($postarray['uri']),
1837 $item = $r[0]['id'];
1838 $parent_id = $r[0]['parent'];
1841 $parent_id = $postarray['parent'];
1844 if (($item != 0) && !function_exists("check_item_notification")) {
1845 require_once 'include/enotify.php';
1847 'type' => NOTIFY_TAGSELF,
1848 'notify_flags' => $u[0]['notify-flags'],
1849 'language' => $u[0]['language'],
1850 'to_name' => $u[0]['username'],
1851 'to_email' => $u[0]['email'],
1852 'uid' => $u[0]['uid'],
1853 'item' => $postarray,
1854 'link' => $a->get_baseurl() . '/display/' . urlencode(Item::getGuidById($item)),
1855 'source_name' => $postarray['author-name'],
1856 'source_link' => $postarray['author-link'],
1857 'source_photo' => $postarray['author-avatar'],
1858 'verb' => ACTIVITY_TAG,
1860 'parent' => $parent_id
1866 PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1869 function twitter_fetch_own_contact(App $a, $uid)
1871 $ckey = Config::get('twitter', 'consumerkey');
1872 $csecret = Config::get('twitter', 'consumersecret');
1873 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1874 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1876 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1880 if ($own_id == "") {
1881 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1883 // Fetching user data
1884 $user = $connection->get('account/verify_credentials');
1886 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1888 $contact_id = twitter_fetch_contact($uid, $user, true);
1890 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1892 dbesc("twitter::" . $own_id));
1894 $contact_id = $r[0]["id"];
1896 PConfig::delete($uid, 'twitter', 'own_id');
1903 function twitter_is_retweet(App $a, $uid, $body)
1905 $body = trim($body);
1907 // Skip if it isn't a pure repeated messages
1908 // Does it start with a share?
1909 if (strpos($body, "[share") > 0) {
1913 // Does it end with a share?
1914 if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1918 $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1919 // Skip if there is no shared message in there
1920 if ($body == $attributes) {
1925 preg_match("/link='(.*?)'/ism", $attributes, $matches);
1926 if ($matches[1] != "") {
1927 $link = $matches[1];
1930 preg_match('/link="(.*?)"/ism', $attributes, $matches);
1931 if ($matches[1] != "") {
1932 $link = $matches[1];
1935 $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1940 logger('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, LOGGER_DEBUG);
1942 $ckey = Config::get('twitter', 'consumerkey');
1943 $csecret = Config::get('twitter', 'consumersecret');
1944 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1945 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1947 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1948 $result = $connection->post('statuses/retweet/' . $id);
1950 logger('twitter_is_retweet: result ' . print_r($result, true), LOGGER_DEBUG);
1952 return !isset($result->errors);