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>
9 * Copyright (c) 2011-2013 Tobias Diekershoff, Michael Vogel
10 * All rights reserved.
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions are met:
14 * * Redistributions of source code must retain the above copyright notice,
15 * this list of conditions and the following disclaimer.
16 * * Redistributions in binary form must reproduce the above
17 * * copyright notice, this list of conditions and the following disclaimer in
18 * the documentation and/or other materials provided with the distribution.
19 * * Neither the name of the <organization> nor the names of its contributors
20 * may be used to endorse or promote products derived from this software
21 * without specific prior written permission.
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
27 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
31 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
32 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 /* Twitter Addon for Friendica
37 * Author: Tobias Diekershoff
38 * tobias.diekershoff@gmx.net
40 * License:3-clause BSD license
43 * To use this addon you need a OAuth Consumer key pair (key & secret)
44 * you can get it from Twitter at https://twitter.com/apps
46 * Register your Friendica site as "Client" application with "Read & Write" access
47 * we do not need "Twitter as login". When you've registered the app you get the
48 * OAuth Consumer key and secret pair for your application/site.
50 * Add this key pair to your global .htconfig.php or use the admin panel.
52 * $a->config['twitter']['consumerkey'] = 'your consumer_key here';
53 * $a->config['twitter']['consumersecret'] = 'your consumer_secret here';
55 * To activate the addon itself add it to the $a->config['system']['addon']
56 * setting. After this, your user can configure their Twitter account settings
57 * from "Settings -> Addon Settings".
59 * Requirements: PHP5, curl [Slinky library]
63 use Friendica\Content\OEmbed;
64 use Friendica\Content\Text\BBCode;
65 use Friendica\Core\Addon;
66 use Friendica\Core\Config;
67 use Friendica\Core\L10n;
68 use Friendica\Core\PConfig;
69 use Friendica\Core\Worker;
70 use Friendica\Model\GContact;
71 use Friendica\Model\Group;
72 use Friendica\Model\Photo;
73 use Friendica\Model\Queue;
74 use Friendica\Model\User;
75 use Friendica\Object\Image;
77 require_once 'include/enotify.php';
79 define('TWITTER_DEFAULT_POLL_INTERVAL', 5); // given in minutes
81 function twitter_install()
83 // we need some hooks, for the configuration and for sending tweets
84 Addon::registerHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
85 Addon::registerHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
86 Addon::registerHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
87 Addon::registerHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
88 Addon::registerHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
89 Addon::registerHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
90 Addon::registerHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
91 Addon::registerHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
92 Addon::registerHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
93 Addon::registerHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
94 Addon::registerHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
95 logger("installed twitter");
98 function twitter_uninstall()
100 Addon::unregisterHook('connector_settings', 'addon/twitter/twitter.php', 'twitter_settings');
101 Addon::unregisterHook('connector_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
102 Addon::unregisterHook('post_local', 'addon/twitter/twitter.php', 'twitter_post_local');
103 Addon::unregisterHook('notifier_normal', 'addon/twitter/twitter.php', 'twitter_post_hook');
104 Addon::unregisterHook('jot_networks', 'addon/twitter/twitter.php', 'twitter_jot_nets');
105 Addon::unregisterHook('cron', 'addon/twitter/twitter.php', 'twitter_cron');
106 Addon::unregisterHook('queue_predeliver', 'addon/twitter/twitter.php', 'twitter_queue_hook');
107 Addon::unregisterHook('follow', 'addon/twitter/twitter.php', 'twitter_follow');
108 Addon::unregisterHook('expire', 'addon/twitter/twitter.php', 'twitter_expire');
109 Addon::unregisterHook('prepare_body', 'addon/twitter/twitter.php', 'twitter_prepare_body');
110 Addon::unregisterHook('check_item_notification', 'addon/twitter/twitter.php', 'twitter_check_item_notification');
112 // old setting - remove only
113 Addon::unregisterHook('post_local_end', 'addon/twitter/twitter.php', 'twitter_post_hook');
114 Addon::unregisterHook('addon_settings', 'addon/twitter/twitter.php', 'twitter_settings');
115 Addon::unregisterHook('addon_settings_post', 'addon/twitter/twitter.php', 'twitter_settings_post');
118 function twitter_check_item_notification(App $a, &$notification_data)
120 $own_id = PConfig::get($notification_data["uid"], 'twitter', 'own_id');
122 $own_user = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
123 intval($notification_data["uid"]),
124 dbesc("twitter::".$own_id)
128 $notification_data["profiles"][] = $own_user[0]["url"];
132 function twitter_follow(App $a, &$contact)
134 logger("twitter_follow: Check if contact is twitter contact. " . $contact["url"], LOGGER_DEBUG);
136 if (!strstr($contact["url"], "://twitter.com") && !strstr($contact["url"], "@twitter.com")) {
140 // contact seems to be a twitter contact, so continue
141 $nickname = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $contact["url"]);
142 $nickname = str_replace("@twitter.com", "", $nickname);
144 $uid = $a->user["uid"];
146 $ckey = Config::get('twitter', 'consumerkey');
147 $csecret = Config::get('twitter', 'consumersecret');
148 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
149 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
151 require_once "addon/twitter/codebird.php";
153 $cb = \Codebird\Codebird::getInstance();
154 $cb->setConsumerKey($ckey, $csecret);
155 $cb->setToken($otoken, $osecret);
158 $parameters["screen_name"] = $nickname;
160 $user = $cb->friendships_create($parameters);
162 twitter_fetchuser($a, $uid, $nickname);
164 $r = q("SELECT name,nick,url,addr,batch,notify,poll,request,confirm,poco,photo,priority,network,alias,pubkey
165 FROM `contact` WHERE `uid` = %d AND `nick` = '%s'",
169 $contact["contact"] = $r[0];
173 function twitter_jot_nets(App $a, &$b)
179 $tw_post = PConfig::get(local_user(), 'twitter', 'post');
180 if (intval($tw_post) == 1) {
181 $tw_defpost = PConfig::get(local_user(), 'twitter', 'post_by_default');
182 $selected = ((intval($tw_defpost) == 1) ? ' checked="checked" ' : '');
183 $b .= '<div class="profile-jot-net"><input type="checkbox" name="twitter_enable"' . $selected . ' value="1" /> '
184 . L10n::t('Post to Twitter') . '</div>';
188 function twitter_settings_post(App $a, $post)
193 // don't check twitter settings if twitter submit button is not clicked
194 if (!x($_POST, 'twitter-submit')) {
198 if (isset($_POST['twitter-disconnect'])) {
200 * if the twitter-disconnect checkbox is set, clear the OAuth key/secret pair
201 * from the user configuration
203 PConfig::delete(local_user(), 'twitter', 'consumerkey');
204 PConfig::delete(local_user(), 'twitter', 'consumersecret');
205 PConfig::delete(local_user(), 'twitter', 'oauthtoken');
206 PConfig::delete(local_user(), 'twitter', 'oauthsecret');
207 PConfig::delete(local_user(), 'twitter', 'post');
208 PConfig::delete(local_user(), 'twitter', 'post_by_default');
209 PConfig::delete(local_user(), 'twitter', 'lastid');
210 PConfig::delete(local_user(), 'twitter', 'mirror_posts');
211 PConfig::delete(local_user(), 'twitter', 'import');
212 PConfig::delete(local_user(), 'twitter', 'create_user');
213 PConfig::delete(local_user(), 'twitter', 'own_id');
215 if (isset($_POST['twitter-pin'])) {
216 // if the user supplied us with a PIN from Twitter, let the magic of OAuth happen
217 logger('got a Twitter PIN');
218 require_once 'library/twitteroauth.php';
219 $ckey = Config::get('twitter', 'consumerkey');
220 $csecret = Config::get('twitter', 'consumersecret');
221 // the token and secret for which the PIN was generated were hidden in the settings
222 // form as token and token2, we need a new connection to Twitter using these token
223 // and secret to request a Access Token with the PIN
224 $connection = new TwitterOAuth($ckey, $csecret, $_POST['twitter-token'], $_POST['twitter-token2']);
225 $token = $connection->getAccessToken($_POST['twitter-pin']);
226 // ok, now that we have the Access Token, save them in the user config
227 PConfig::set(local_user(), 'twitter', 'oauthtoken', $token['oauth_token']);
228 PConfig::set(local_user(), 'twitter', 'oauthsecret', $token['oauth_token_secret']);
229 PConfig::set(local_user(), 'twitter', 'post', 1);
230 // reload the Addon Settings page, if we don't do it see Bug #42
231 goaway('settings/connectors');
233 // if no PIN is supplied in the POST variables, the user has changed the setting
234 // to post a tweet for every new __public__ posting to the wall
235 PConfig::set(local_user(), 'twitter', 'post', intval($_POST['twitter-enable']));
236 PConfig::set(local_user(), 'twitter', 'post_by_default', intval($_POST['twitter-default']));
237 PConfig::set(local_user(), 'twitter', 'mirror_posts', intval($_POST['twitter-mirror']));
238 PConfig::set(local_user(), 'twitter', 'import', intval($_POST['twitter-import']));
239 PConfig::set(local_user(), 'twitter', 'create_user', intval($_POST['twitter-create_user']));
241 if (!intval($_POST['twitter-mirror'])) {
242 PConfig::delete(local_user(), 'twitter', 'lastid');
245 info(L10n::t('Twitter settings updated.') . EOL);
250 function twitter_settings(App $a, &$s)
255 $a->page['htmlhead'] .= '<link rel="stylesheet" type="text/css" href="' . $a->get_baseurl() . '/addon/twitter/twitter.css' . '" media="all" />' . "\r\n";
257 * 1) Check that we have global consumer key & secret
258 * 2) If no OAuthtoken & stuff is present, generate button to get some
259 * 3) Checkbox for "Send public notices (280 chars only)
261 $ckey = Config::get('twitter', 'consumerkey' );
262 $csecret = Config::get('twitter', 'consumersecret' );
263 $otoken = PConfig::get(local_user(), 'twitter', 'oauthtoken' );
264 $osecret = PConfig::get(local_user(), 'twitter', 'oauthsecret' );
266 $enabled = intval(PConfig::get(local_user(), 'twitter', 'post'));
267 $defenabled = intval(PConfig::get(local_user(), 'twitter', 'post_by_default'));
268 $mirrorenabled = intval(PConfig::get(local_user(), 'twitter', 'mirror_posts'));
269 $importenabled = intval(PConfig::get(local_user(), 'twitter', 'import'));
270 $create_userenabled = intval(PConfig::get(local_user(), 'twitter', 'create_user'));
272 $css = (($enabled) ? '' : '-disabled');
274 $s .= '<span id="settings_twitter_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
275 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
277 $s .= '<div id="settings_twitter_expanded" class="settings-block" style="display: none;">';
278 $s .= '<span class="fakelink" onclick="openClose(\'settings_twitter_expanded\'); openClose(\'settings_twitter_inflated\');">';
279 $s .= '<img class="connector' . $css . '" src="images/twitter.png" /><h3 class="connector">' . L10n::t('Twitter Import/Export/Mirror') . '</h3>';
282 if ((!$ckey) && (!$csecret)) {
284 * no global consumer keys
285 * display warning and skip personal config
287 $s .= '<p>' . L10n::t('No consumer key pair for Twitter found. Please contact your site administrator.') . '</p>';
290 * ok we have a consumer key pair now look into the OAuth stuff
292 if ((!$otoken) && (!$osecret)) {
294 * the user has not yet connected the account to twitter...
295 * get a temporary OAuth key/secret pair and display a button with
296 * which the user can request a PIN to connect the account to a
297 * account at Twitter.
299 require_once 'library/twitteroauth.php';
300 $connection = new TwitterOAuth($ckey, $csecret);
301 $request_token = $connection->getRequestToken();
302 $token = $request_token['oauth_token'];
304 * make some nice form
306 $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>';
307 $s .= '<a href="' . $connection->getAuthorizeURL($token) . '" target="_twitter"><img src="addon/twitter/lighter.png" alt="' . L10n::t('Log in with Twitter') . '"></a>';
308 $s .= '<div id="twitter-pin-wrapper">';
309 $s .= '<label id="twitter-pin-label" for="twitter-pin">' . L10n::t('Copy the PIN from Twitter here') . '</label>';
310 $s .= '<input id="twitter-pin" type="text" name="twitter-pin" />';
311 $s .= '<input id="twitter-token" type="hidden" name="twitter-token" value="' . $token . '" />';
312 $s .= '<input id="twitter-token2" type="hidden" name="twitter-token2" value="' . $request_token['oauth_token_secret'] . '" />';
313 $s .= '</div><div class="clear"></div>';
314 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
317 * we have an OAuth key / secret pair for the user
318 * so let's give a chance to disable the postings to Twitter
320 require_once 'library/twitteroauth.php';
321 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
322 $details = $connection->get('account/verify_credentials');
324 $field_checkbox = get_markup_template('field_checkbox.tpl');
326 $s .= '<div id="twitter-info" >
327 <p>' . L10n::t('Currently connected to: ') . '<a href="https://twitter.com/' . $details->screen_name . '" target="_twitter">' . $details->screen_name . '</a>
328 <button type="submit" name="twitter-disconnect" value="1">' . L10n::t('Disconnect') . '</button>
330 <p id="twitter-info-block">
331 <a href="https://twitter.com/' . $details->screen_name . '" target="_twitter"><img id="twitter-avatar" src="' . $details->profile_image_url . '" /></a>
332 <em>' . $details->description . '</em>
335 $s .= '<div class="clear"></div>';
337 $s .= replace_macros($field_checkbox, [
338 '$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.')]
340 if ($a->user['hidewall']) {
341 $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>';
343 $s .= replace_macros($field_checkbox, [
344 '$field' => ['twitter-default', L10n::t('Send public postings to Twitter by default'), $defenabled, '']
346 $s .= replace_macros($field_checkbox, [
347 '$field' => ['twitter-mirror', L10n::t('Mirror all posts from twitter that are no replies'), $mirrorenabled, '']
349 $s .= replace_macros($field_checkbox, [
350 '$field' => ['twitter-import', L10n::t('Import the remote timeline'), $importenabled, '']
352 $s .= replace_macros($field_checkbox, [
353 '$field' => ['twitter-create_user', L10n::t('Automatically create contacts'), $create_userenabled, '']
356 $s .= '<div class="clear"></div>';
357 $s .= '<div class="settings-submit-wrapper" ><input type="submit" name="twitter-submit" class="settings-submit" value="' . L10n::t('Save Settings') . '" /></div>';
360 $s .= '</div><div class="clear"></div>';
363 function twitter_post_local(App $a, &$b)
369 if (!local_user() || (local_user() != $b['uid'])) {
373 $twitter_post = intval(PConfig::get(local_user(), 'twitter', 'post'));
374 $twitter_enable = (($twitter_post && x($_REQUEST, 'twitter_enable')) ? intval($_REQUEST['twitter_enable']) : 0);
376 // if API is used, default to the chosen settings
377 if ($b['api_source'] && intval(PConfig::get(local_user(), 'twitter', 'post_by_default'))) {
381 if (!$twitter_enable) {
385 if (strlen($b['postopts'])) {
386 $b['postopts'] .= ',';
389 $b['postopts'] .= 'twitter';
392 function twitter_action(App $a, $uid, $pid, $action)
394 $ckey = Config::get('twitter', 'consumerkey');
395 $csecret = Config::get('twitter', 'consumersecret');
396 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
397 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
399 require_once "addon/twitter/codebird.php";
401 $cb = \Codebird\Codebird::getInstance();
402 $cb->setConsumerKey($ckey, $csecret);
403 $cb->setToken($otoken, $osecret);
405 $post = ['id' => $pid];
407 logger("twitter_action '" . $action . "' ID: " . $pid . " data: " . print_r($post, true), LOGGER_DATA);
411 // To-Do: $result = $cb->statuses_destroy($post);
414 $result = $cb->favorites_create($post);
417 $result = $cb->favorites_destroy($post);
420 logger("twitter_action '" . $action . "' send, result: " . print_r($result, true), LOGGER_DEBUG);
423 function twitter_post_hook(App $a, &$b)
426 require_once "include/network.php";
428 if (!PConfig::get($b["uid"], 'twitter', 'import')
429 && ($b['deleted'] || $b['private'] || ($b['created'] !== $b['edited']))) {
433 if ($b['parent'] != $b['id']) {
434 logger("twitter_post_hook: parameter " . print_r($b, true), LOGGER_DATA);
436 // Looking if its a reply to a twitter post
437 if ((substr($b["parent-uri"], 0, 9) != "twitter::") && (substr($b["extid"], 0, 9) != "twitter::") && (substr($b["thr-parent"], 0, 9) != "twitter::")) {
438 logger("twitter_post_hook: no twitter post " . $b["parent"]);
442 $r = q("SELECT * FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
443 dbesc($b["thr-parent"]),
447 logger("twitter_post_hook: no parent found " . $b["thr-parent"]);
455 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
456 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
457 $nicknameplain = "@" . $nicknameplain;
459 logger("twitter_post_hook: comparing " . $nickname . " and " . $nicknameplain . " with " . $b["body"], LOGGER_DEBUG);
460 if ((strpos($b["body"], $nickname) === false) && (strpos($b["body"], $nicknameplain) === false)) {
461 $b["body"] = $nickname . " " . $b["body"];
464 logger("twitter_post_hook: parent found " . print_r($orig_post, true), LOGGER_DATA);
468 if ($b['private'] || !strstr($b['postopts'], 'twitter')) {
472 // Dont't post if the post doesn't belong to us.
473 // This is a check for forum postings
474 $self = dba::selectFirst('contact', ['id'], ['uid' => $b['uid'], 'self' => true]);
475 if ($b['contact-id'] != $self['id']) {
480 if (($b['verb'] == ACTIVITY_POST) && $b['deleted']) {
481 twitter_action($a, $b["uid"], substr($orig_post["uri"], 9), "delete");
484 if ($b['verb'] == ACTIVITY_LIKE) {
485 logger("twitter_post_hook: parameter 2 " . substr($b["thr-parent"], 9), LOGGER_DEBUG);
487 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "unlike");
489 twitter_action($a, $b["uid"], substr($b["thr-parent"], 9), "like");
493 if ($b['deleted'] || ($b['created'] !== $b['edited'])) {
497 // if post comes from twitter don't send it back
498 if ($b['extid'] == NETWORK_TWITTER) {
502 if ($b['app'] == "Twitter") {
506 logger('twitter post invoked');
508 PConfig::load($b['uid'], 'twitter');
510 $ckey = Config::get('twitter', 'consumerkey');
511 $csecret = Config::get('twitter', 'consumersecret');
512 $otoken = PConfig::get($b['uid'], 'twitter', 'oauthtoken');
513 $osecret = PConfig::get($b['uid'], 'twitter', 'oauthsecret');
515 if ($ckey && $csecret && $otoken && $osecret) {
516 logger('twitter: we have customer key and oauth stuff, going to send.', LOGGER_DEBUG);
518 // If it's a repeated message from twitter then do a native retweet and exit
519 if (twitter_is_retweet($a, $b['uid'], $b['body'])) {
523 require_once 'library/twitteroauth.php';
524 require_once 'include/bbcode.php';
525 $tweet = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
528 $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
529 $msg = $msgarr["text"];
531 if (($msg == "") && isset($msgarr["title"])) {
532 $msg = BBCode::shortenMsg($msgarr["title"], $max_char - 50);
537 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
538 $msg .= "\n" . $msgarr["url"];
541 if (isset($msgarr["image"]) && ($msgarr["type"] != "video")) {
542 $image = $msgarr["image"];
545 // and now tweet it :-)
546 if (strlen($msg) && ($image != "")) {
547 $img_str = fetch_url($image);
549 $tempfile = tempnam(get_temppath(), "cache");
550 file_put_contents($tempfile, $img_str);
552 // Twitter had changed something so that the old library doesn't work anymore
553 // so we are using a new library for twitter
555 // Switching completely to this library with all functions
556 require_once "addon/twitter/codebird.php";
558 $cb = \Codebird\Codebird::getInstance();
559 $cb->setConsumerKey($ckey, $csecret);
560 $cb->setToken($otoken, $osecret);
562 $post = ['status' => $msg, 'media[]' => $tempfile];
565 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
568 $result = $cb->statuses_updateWithMedia($post);
571 logger('twitter_post_with_media send, result: ' . print_r($result, true), LOGGER_DEBUG);
573 if ($result->source) {
574 Config::set("twitter", "application_name", strip_tags($result->source));
577 if ($result->errors || $result->error) {
578 logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
580 // Workaround: Remove the picture link so that the post can be reposted without it
581 $msg .= " " . $image;
583 } elseif ($iscomment) {
584 logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
585 q("UPDATE `item` SET `extid` = '%s', `body` = '%s' WHERE `id` = %d",
586 dbesc("twitter::" . $result->id_str),
587 dbesc($result->text),
593 if (strlen($msg) && ($image == "")) {
596 $msgarr = BBCode::toPlaintext($b, $max_char, true, 8);
597 $msg = $msgarr["text"];
599 if (($msg == "") && isset($msgarr["title"])) {
600 $msg = BBCode::shortenMsg($msgarr["title"], $max_char - 50);
603 if (isset($msgarr["url"])) {
604 $msg .= "\n" . $msgarr["url"];
607 $url = 'statuses/update';
608 $post = ['status' => $msg, 'weighted_character_count' => 'true'];
611 $post["in_reply_to_status_id"] = substr($orig_post["uri"], 9);
614 $result = $tweet->post($url, $post);
615 logger('twitter_post send, result: ' . print_r($result, true), LOGGER_DEBUG);
617 if ($result->source) {
618 Config::set("twitter", "application_name", strip_tags($result->source));
621 if ($result->errors) {
622 logger('Send to Twitter failed: "' . print_r($result->errors, true) . '"');
624 $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `self`", intval($b['uid']));
626 $a->contact = $r[0]["id"];
629 $s = serialize(['url' => $url, 'item' => $b['id'], 'post' => $post]);
631 Queue::add($a->contact, NETWORK_TWITTER, $s);
632 notice(L10n::t('Twitter post failed. Queued for retry.') . EOL);
633 } elseif ($iscomment) {
634 logger('twitter_post: Update extid ' . $result->id_str . " for post id " . $b['id']);
635 q("UPDATE `item` SET `extid` = '%s' WHERE `id` = %d",
636 dbesc("twitter::" . $result->id_str),
644 function twitter_addon_admin_post(App $a)
646 $consumerkey = x($_POST, 'consumerkey') ? notags(trim($_POST['consumerkey'])) : '';
647 $consumersecret = x($_POST, 'consumersecret') ? notags(trim($_POST['consumersecret'])) : '';
648 Config::set('twitter', 'consumerkey', $consumerkey);
649 Config::set('twitter', 'consumersecret', $consumersecret);
650 info(L10n::t('Settings updated.') . EOL);
653 function twitter_addon_admin(App $a, &$o)
655 $t = get_markup_template("admin.tpl", "addon/twitter/");
657 $o = replace_macros($t, [
658 '$submit' => L10n::t('Save Settings'),
659 // name, label, value, help, [extra values]
660 '$consumerkey' => ['consumerkey', L10n::t('Consumer key'), Config::get('twitter', 'consumerkey'), ''],
661 '$consumersecret' => ['consumersecret', L10n::t('Consumer secret'), Config::get('twitter', 'consumersecret'), ''],
665 function twitter_cron(App $a, $b)
667 $last = Config::get('twitter', 'last_poll');
669 $poll_interval = intval(Config::get('twitter', 'poll_interval'));
670 if (!$poll_interval) {
671 $poll_interval = TWITTER_DEFAULT_POLL_INTERVAL;
675 $next = $last + ($poll_interval * 60);
676 if ($next > time()) {
677 logger('twitter: poll intervall not reached');
681 logger('twitter: cron_start');
683 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'mirror_posts' AND `v` = '1'");
685 foreach ($r as $rr) {
686 logger('twitter: fetching for user ' . $rr['uid']);
687 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 1, (int) $rr['uid']);
691 $abandon_days = intval(Config::get('system', 'account_abandon_days'));
692 if ($abandon_days < 1)
695 $abandon_limit = date("Y-m-d H:i:s", time() - $abandon_days * 86400);
697 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1'");
699 foreach ($r as $rr) {
700 if ($abandon_days != 0) {
701 $user = q("SELECT `login_date` FROM `user` WHERE uid=%d AND `login_date` >= '%s'", $rr['uid'], $abandon_limit);
703 logger('abandoned account: timeline from user ' . $rr['uid'] . ' will not be imported');
708 logger('twitter: importing timeline from user ' . $rr['uid']);
709 Worker::add(PRIORITY_MEDIUM, "addon/twitter/twitter_sync.php", 2, (int) $rr['uid']);
712 // check for new contacts once a day
713 $last_contact_check = PConfig::get($rr['uid'],'pumpio','contact_check');
714 if($last_contact_check)
715 $next_contact_check = $last_contact_check + 86400;
717 $next_contact_check = 0;
719 if($next_contact_check <= time()) {
720 pumpio_getallusers($a, $rr["uid"]);
721 PConfig::set($rr['uid'],'pumpio','contact_check',time());
727 logger('twitter: cron_end');
729 Config::set('twitter', 'last_poll', time());
732 function twitter_expire(App $a, $b)
734 $days = Config::get('twitter', 'expire');
740 if (method_exists('dba', 'delete')) {
741 $r = dba::select('item', ['id'], ['deleted' => true, 'network' => NETWORK_TWITTER]);
742 while ($row = dba::fetch($r)) {
743 dba::delete('item', ['id' => $row['id']]);
747 $r = q("DELETE FROM `item` WHERE `deleted` AND `network` = '%s'", dbesc(NETWORK_TWITTER));
750 require_once "include/items.php";
752 logger('twitter_expire: expire_start');
754 $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'twitter' AND `k` = 'import' AND `v` = '1' ORDER BY RAND()");
756 foreach ($r as $rr) {
757 logger('twitter_expire: user ' . $rr['uid']);
758 item_expire($rr['uid'], $days, NETWORK_TWITTER, true);
762 logger('twitter_expire: expire_end');
765 function twitter_prepare_body(App $a, &$b)
767 if ($b["item"]["network"] != NETWORK_TWITTER) {
774 $item["plink"] = $a->get_baseurl() . "/display/" . $a->user["nickname"] . "/" . $item["parent"];
776 $r = q("SELECT `author-link` FROM item WHERE item.uri = '%s' AND item.uid = %d LIMIT 1",
777 dbesc($item["thr-parent"]),
778 intval(local_user()));
783 $nicknameplain = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $orig_post["author-link"]);
784 $nickname = "@[url=" . $orig_post["author-link"] . "]" . $nicknameplain . "[/url]";
785 $nicknameplain = "@" . $nicknameplain;
787 if ((strpos($item["body"], $nickname) === false) && (strpos($item["body"], $nicknameplain) === false)) {
788 $item["body"] = $nickname . " " . $item["body"];
792 $msgarr = BBCode::toPlaintext($item, $max_char, true, 8);
793 $msg = $msgarr["text"];
795 if (isset($msgarr["url"]) && ($msgarr["type"] != "photo")) {
796 $msg .= " " . $msgarr["url"];
799 if (isset($msgarr["image"])) {
800 $msg .= " " . $msgarr["image"];
803 $b['html'] = nl2br(htmlspecialchars($msg));
808 * @brief Build the item array for the mirrored post
810 * @param App $a Application class
811 * @param integer $uid User id
812 * @param object $post Twitter object with the post
814 * @return array item data to be posted
816 function twitter_do_mirrorpost(App $a, $uid, $post)
818 $datarray["type"] = "wall";
819 $datarray["api_source"] = true;
820 $datarray["profile_uid"] = $uid;
821 $datarray["extid"] = NETWORK_TWITTER;
822 $datarray['message_id'] = item_new_uri($a->get_hostname(), $uid, NETWORK_TWITTER . ":" . $post->id);
823 $datarray['object'] = json_encode($post);
824 $datarray["title"] = "";
826 if (is_object($post->retweeted_status)) {
827 // We don't support nested shares, so we mustn't show quotes as shares on retweets
828 $item = twitter_createpost($a, $uid, $post->retweeted_status, ['id' => 0], false, false, true);
830 $datarray['body'] = "\n" . share_header($item['author-name'], $item['author-link'], $item['author-avatar'], "", $item['created'], $item['plink']);
832 $datarray['body'] .= $item['body'] . '[/share]';
834 $item = twitter_createpost($a, $uid, $post, ['id' => 0], false, false, false);
836 $datarray['body'] = $item['body'];
839 $datarray["source"] = $item['app'];
840 $datarray["verb"] = $item['verb'];
842 if (isset($item["location"])) {
843 $datarray["location"] = $item["location"];
846 if (isset($item["coord"])) {
847 $datarray["coord"] = $item["coord"];
853 function twitter_fetchtimeline(App $a, $uid)
855 $ckey = Config::get('twitter', 'consumerkey');
856 $csecret = Config::get('twitter', 'consumersecret');
857 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
858 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
859 $lastid = PConfig::get($uid, 'twitter', 'lastid');
861 $application_name = Config::get('twitter', 'application_name');
863 if ($application_name == "") {
864 $application_name = $a->get_hostname();
867 $has_picture = false;
869 require_once 'mod/item.php';
870 require_once 'include/items.php';
871 require_once 'mod/share.php';
873 require_once 'library/twitteroauth.php';
874 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
876 $parameters = ["exclude_replies" => true, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
878 $first_time = ($lastid == "");
881 $parameters["since_id"] = $lastid;
884 $items = $connection->get('statuses/user_timeline', $parameters);
886 if (!is_array($items)) {
890 $posts = array_reverse($items);
893 foreach ($posts as $post) {
894 if ($post->id_str > $lastid) {
895 $lastid = $post->id_str;
896 PConfig::set($uid, 'twitter', 'lastid', $lastid);
903 if (!stristr($post->source, $application_name)) {
904 $_SESSION["authenticated"] = true;
905 $_SESSION["uid"] = $uid;
907 $_REQUEST = twitter_do_mirrorpost($a, $uid, $post);
909 logger('twitter: posting for user ' . $uid);
915 PConfig::set($uid, 'twitter', 'lastid', $lastid);
918 function twitter_queue_hook(App $a, &$b)
920 $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'",
921 dbesc(NETWORK_TWITTER)
927 foreach ($qi as $x) {
928 if ($x['network'] !== NETWORK_TWITTER) {
932 logger('twitter_queue: run');
934 $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid`
935 WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1",
944 $ckey = Config::get('twitter', 'consumerkey');
945 $csecret = Config::get('twitter', 'consumersecret');
946 $otoken = PConfig::get($user['uid'], 'twitter', 'oauthtoken');
947 $osecret = PConfig::get($user['uid'], 'twitter', 'oauthsecret');
951 if ($ckey && $csecret && $otoken && $osecret) {
952 logger('twitter_queue: able to post');
954 $z = unserialize($x['content']);
956 require_once "addon/twitter/codebird.php";
958 $cb = \Codebird\Codebird::getInstance();
959 $cb->setConsumerKey($ckey, $csecret);
960 $cb->setToken($otoken, $osecret);
962 if ($z['url'] == "statuses/update") {
963 $result = $cb->statuses_update($z['post']);
966 logger('twitter_queue: post result: ' . print_r($result, true), LOGGER_DEBUG);
968 if ($result->errors) {
969 logger('twitter_queue: Send to Twitter failed: "' . print_r($result->errors, true) . '"');
972 Queue::removeItem($x['id']);
975 logger("twitter_queue: Error getting tokens for user " . $user['uid']);
979 logger('twitter_queue: delayed');
980 Queue::updateTime($x['id']);
985 function twitter_fix_avatar($avatar)
987 $new_avatar = str_replace("_normal.", ".", $avatar);
989 $info = Image::getInfoFromURL($new_avatar);
991 $new_avatar = $avatar;
997 function twitter_fetch_contact($uid, $contact, $create_user)
999 if ($contact->id_str == "") {
1003 $avatar = twitter_fix_avatar($contact->profile_image_url_https);
1005 GContact::update(["url" => "https://twitter.com/" . $contact->screen_name,
1006 "network" => NETWORK_TWITTER, "photo" => $avatar, "hide" => true,
1007 "name" => $contact->name, "nick" => $contact->screen_name,
1008 "location" => $contact->location, "about" => $contact->description,
1009 "addr" => $contact->screen_name . "@twitter.com", "generation" => 2]);
1011 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1012 intval($uid), dbesc("twitter::" . $contact->id_str));
1014 if (!count($r) && !$create_user) {
1018 if (count($r) && ($r[0]["readonly"] || $r[0]["blocked"])) {
1019 logger("twitter_fetch_contact: Contact '" . $r[0]["nick"] . "' is blocked or readonly.", LOGGER_DEBUG);
1024 // create contact record
1025 q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `addr`, `alias`, `notify`, `poll`,
1026 `name`, `nick`, `photo`, `network`, `rel`, `priority`,
1027 `location`, `about`, `writable`, `blocked`, `readonly`, `pending`)
1028 VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', %d, 0, 0, 0)",
1030 dbesc(datetime_convert()),
1031 dbesc("https://twitter.com/" . $contact->screen_name),
1032 dbesc(normalise_link("https://twitter.com/" . $contact->screen_name)),
1033 dbesc($contact->screen_name."@twitter.com"),
1034 dbesc("twitter::" . $contact->id_str),
1036 dbesc("twitter::" . $contact->id_str),
1037 dbesc($contact->name),
1038 dbesc($contact->screen_name),
1040 dbesc(NETWORK_TWITTER),
1041 intval(CONTACT_IS_FRIEND),
1043 dbesc($contact->location),
1044 dbesc($contact->description),
1048 $r = q("SELECT * FROM `contact` WHERE `alias` = '%s' AND `uid` = %d LIMIT 1",
1049 dbesc("twitter::".$contact->id_str),
1057 $contact_id = $r[0]['id'];
1059 Group::addMember(User::getDefaultGroup($uid), $contact_id);
1061 $photos = Photo::importProfilePhoto($avatar, $uid, $contact_id, true);
1064 q("UPDATE `contact` SET `photo` = '%s',
1069 `avatar-date` = '%s'
1074 dbesc(datetime_convert()),
1075 dbesc(datetime_convert()),
1076 dbesc(datetime_convert()),
1081 // update profile photos once every two weeks as we have no notification of when they change.
1082 //$update_photo = (($r[0]['avatar-date'] < datetime_convert('','','now -2 days')) ? true : false);
1083 $update_photo = ($r[0]['avatar-date'] < datetime_convert('', '', 'now -12 hours'));
1085 // check that we have all the photos, this has been known to fail on occasion
1086 if ((!$r[0]['photo']) || (!$r[0]['thumb']) || (!$r[0]['micro']) || ($update_photo)) {
1087 logger("twitter_fetch_contact: Updating contact " . $contact->screen_name, LOGGER_DEBUG);
1089 $photos = Photo::importProfilePhoto($avatar, $uid, $r[0]['id'], true);
1092 q("UPDATE `contact` SET `photo` = '%s',
1097 `avatar-date` = '%s',
1109 dbesc(datetime_convert()),
1110 dbesc(datetime_convert()),
1111 dbesc(datetime_convert()),
1112 dbesc("https://twitter.com/".$contact->screen_name),
1113 dbesc(normalise_link("https://twitter.com/".$contact->screen_name)),
1114 dbesc($contact->screen_name."@twitter.com"),
1115 dbesc($contact->name),
1116 dbesc($contact->screen_name),
1117 dbesc($contact->location),
1118 dbesc($contact->description),
1128 function twitter_fetchuser(App $a, $uid, $screen_name = "", $user_id = "")
1130 $ckey = Config::get('twitter', 'consumerkey');
1131 $csecret = Config::get('twitter', 'consumersecret');
1132 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1133 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1135 require_once "addon/twitter/codebird.php";
1137 $cb = \Codebird\Codebird::getInstance();
1138 $cb->setConsumerKey($ckey, $csecret);
1139 $cb->setToken($otoken, $osecret);
1141 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1152 if ($screen_name != "") {
1153 $parameters["screen_name"] = $screen_name;
1156 if ($user_id != "") {
1157 $parameters["user_id"] = $user_id;
1160 // Fetching user data
1161 $user = $cb->users_show($parameters);
1163 if (!is_object($user)) {
1167 $contact_id = twitter_fetch_contact($uid, $user, true);
1172 function twitter_expand_entities(App $a, $body, $item, $no_tags = false, $picture)
1174 require_once "include/network.php";
1180 if (isset($item->entities->urls)) {
1186 foreach ($item->entities->urls AS $url) {
1187 $plain = str_replace($url->url, '', $plain);
1189 if ($url->url && $url->expanded_url && $url->display_url) {
1190 $expanded_url = original_url($url->expanded_url);
1192 $oembed_data = OEmbed::fetchURL($expanded_url);
1194 // Quickfix: Workaround for URL with "[" and "]" in it
1195 if (strpos($expanded_url, "[") || strpos($expanded_url, "]")) {
1196 $expanded_url = $url->url;
1200 $type = $oembed_data->type;
1203 if ($oembed_data->type == "video") {
1204 //$body = str_replace($url->url,
1205 // "[video]".$expanded_url."[/video]", $body);
1206 //$dontincludemedia = true;
1207 $type = $oembed_data->type;
1208 $footerurl = $expanded_url;
1209 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1211 $body = str_replace($url->url, $footerlink, $body);
1212 //} elseif (($oembed_data->type == "photo") AND isset($oembed_data->url) AND !$dontincludemedia) {
1213 } elseif (($oembed_data->type == "photo") && isset($oembed_data->url)) {
1214 $body = str_replace($url->url, "[url=" . $expanded_url . "][img]" . $oembed_data->url . "[/img][/url]", $body);
1215 //$dontincludemedia = true;
1216 } elseif ($oembed_data->type != "link") {
1217 $body = str_replace($url->url, "[url=" . $expanded_url . "]" . $expanded_url . "[/url]", $body);
1219 $img_str = fetch_url($expanded_url, true, $redirects, 4);
1221 $tempfile = tempnam(get_temppath(), "cache");
1222 file_put_contents($tempfile, $img_str);
1223 $mime = image_type_to_mime_type(exif_imagetype($tempfile));
1226 if (substr($mime, 0, 6) == "image/") {
1228 $body = str_replace($url->url, "[img]" . $expanded_url . "[/img]", $body);
1229 //$dontincludemedia = true;
1231 $type = $oembed_data->type;
1232 $footerurl = $expanded_url;
1233 $footerlink = "[url=" . $expanded_url . "]" . $expanded_url . "[/url]";
1235 $body = str_replace($url->url, $footerlink, $body);
1241 if ($footerurl != "") {
1242 $footer = add_page_info($footerurl, false, $picture);
1245 if (($footerlink != "") && (trim($footer) != "")) {
1246 $removedlink = trim(str_replace($footerlink, "", $body));
1248 if (($removedlink == "") || strstr($body, $removedlink)) {
1249 $body = $removedlink;
1255 if (($footer == "") && ($picture != "")) {
1256 $body .= "\n\n[img]" . $picture . "[/img]\n";
1257 } elseif (($footer == "") && ($picture == "")) {
1258 $body = add_page_info_to_body($body);
1262 return ["body" => $body, "tags" => "", "plain" => $plain];
1267 foreach ($item->entities->hashtags AS $hashtag) {
1268 $url = "#[url=" . $a->get_baseurl() . "/search?tag=" . rawurlencode($hashtag->text) . "]" . $hashtag->text . "[/url]";
1269 $tags_arr["#" . $hashtag->text] = $url;
1270 $body = str_replace("#" . $hashtag->text, $url, $body);
1273 foreach ($item->entities->user_mentions AS $mention) {
1274 $url = "@[url=https://twitter.com/" . rawurlencode($mention->screen_name) . "]" . $mention->screen_name . "[/url]";
1275 $tags_arr["@" . $mention->screen_name] = $url;
1276 $body = str_replace("@" . $mention->screen_name, $url, $body);
1279 // it seems as if the entities aren't always covering all mentions. So the rest will be checked here
1280 $tags = get_tags($body);
1283 foreach ($tags as $tag) {
1284 if (strstr(trim($tag), " ")) {
1288 if (strpos($tag, '#') === 0) {
1289 if (strpos($tag, '[url=')) {
1293 // don't link tags that are already embedded in links
1294 if (preg_match('/\[(.*?)' . preg_quote($tag, '/') . '(.*?)\]/', $body)) {
1297 if (preg_match('/\[(.*?)\]\((.*?)' . preg_quote($tag, '/') . '(.*?)\)/', $body)) {
1301 $basetag = str_replace('_', ' ', substr($tag, 1));
1302 $url = '#[url=' . $a->get_baseurl() . '/search?tag=' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1303 $body = str_replace($tag, $url, $body);
1304 $tags_arr["#" . $basetag] = $url;
1305 } elseif (strpos($tag, '@') === 0) {
1306 if (strpos($tag, '[url=')) {
1310 $basetag = substr($tag, 1);
1311 $url = '@[url=https://twitter.com/' . rawurlencode($basetag) . ']' . $basetag . '[/url]';
1312 $body = str_replace($tag, $url, $body);
1313 $tags_arr["@" . $basetag] = $url;
1318 $tags = implode($tags_arr, ",");
1320 return ["body" => $body, "tags" => $tags, "plain" => $plain];
1324 * @brief Fetch media entities and add media links to the body
1326 * @param object $post Twitter object with the post
1327 * @param array $postarray Array of the item that is about to be posted
1329 * @return $picture string Returns a a single picture string if it isn't a media post
1331 function twitter_media_entities($post, &$postarray)
1333 // There are no media entities? So we quit.
1334 if (!is_array($post->extended_entities->media)) {
1338 // When the post links to an external page, we only take one picture.
1339 // We only do this when there is exactly one media.
1340 if ((count($post->entities->urls) > 0) && (count($post->extended_entities->media) == 1)) {
1342 foreach ($post->extended_entities->media AS $medium) {
1343 if (isset($medium->media_url_https)) {
1344 $picture = $medium->media_url_https;
1345 $postarray['body'] = str_replace($medium->url, "", $postarray['body']);
1351 // This is a pure media post, first search for all media urls
1353 foreach ($post->extended_entities->media AS $medium) {
1354 switch ($medium->type) {
1356 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1357 $postarray['object-type'] = ACTIVITY_OBJ_IMAGE;
1360 case 'animated_gif':
1361 $media[$medium->url] .= "\n[img]" . $medium->media_url_https . "[/img]";
1362 $postarray['object-type'] = ACTIVITY_OBJ_VIDEO;
1363 if (is_array($medium->video_info->variants)) {
1365 // We take the video with the highest bitrate
1366 foreach ($medium->video_info->variants AS $variant) {
1367 if (($variant->content_type == "video/mp4") && ($variant->bitrate >= $bitrate)) {
1368 $media[$medium->url] = "\n[video]" . $variant->url . "[/video]";
1369 $bitrate = $variant->bitrate;
1374 // The following code will only be activated for test reasons
1376 // $postarray['body'] .= print_r($medium, true);
1380 // Now we replace the media urls.
1381 foreach ($media AS $key => $value) {
1382 $postarray['body'] = str_replace($key, "\n" . $value . "\n", $postarray['body']);
1387 function twitter_createpost(App $a, $uid, $post, $self, $create_user, $only_existing_contact, $noquote)
1390 $postarray['network'] = NETWORK_TWITTER;
1391 $postarray['gravity'] = 0;
1392 $postarray['uid'] = $uid;
1393 $postarray['wall'] = 0;
1394 $postarray['uri'] = "twitter::" . $post->id_str;
1395 $postarray['object'] = json_encode($post);
1397 // Don't import our own comments
1398 $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1399 dbesc($postarray['uri']),
1404 logger("Item with extid " . $postarray['uri'] . " found.", LOGGER_DEBUG);
1410 if ($post->in_reply_to_status_id_str != "") {
1411 $parent = "twitter::" . $post->in_reply_to_status_id_str;
1413 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1418 $postarray['thr-parent'] = $r[0]["uri"];
1419 $postarray['parent-uri'] = $r[0]["parent-uri"];
1420 $postarray['parent'] = $r[0]["parent"];
1421 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1423 $r = q("SELECT * FROM `item` WHERE `extid` = '%s' AND `uid` = %d LIMIT 1",
1428 $postarray['thr-parent'] = $r[0]['uri'];
1429 $postarray['parent-uri'] = $r[0]['parent-uri'];
1430 $postarray['parent'] = $r[0]['parent'];
1431 $postarray['object-type'] = ACTIVITY_OBJ_COMMENT;
1433 $postarray['thr-parent'] = $postarray['uri'];
1434 $postarray['parent-uri'] = $postarray['uri'];
1435 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1440 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1442 if ($post->user->id_str == $own_id) {
1443 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1447 $contactid = $r[0]["id"];
1449 $postarray['owner-name'] = $r[0]["name"];
1450 $postarray['owner-link'] = $r[0]["url"];
1451 $postarray['owner-avatar'] = $r[0]["photo"];
1453 logger("No self contact for user " . $uid, LOGGER_DEBUG);
1457 // Don't create accounts of people who just comment something
1458 $create_user = false;
1460 $postarray['parent-uri'] = $postarray['uri'];
1461 $postarray['object-type'] = ACTIVITY_OBJ_NOTE;
1464 if ($contactid == 0) {
1465 $contactid = twitter_fetch_contact($uid, $post->user, $create_user);
1467 $postarray['owner-name'] = $post->user->name;
1468 $postarray['owner-link'] = "https://twitter.com/" . $post->user->screen_name;
1469 $postarray['owner-avatar'] = twitter_fix_avatar($post->user->profile_image_url_https);
1472 if (($contactid == 0) && !$only_existing_contact) {
1473 $contactid = $self['id'];
1474 } elseif ($contactid <= 0) {
1475 logger("Contact ID is zero or less than zero.", LOGGER_DEBUG);
1479 $postarray['contact-id'] = $contactid;
1481 $postarray['verb'] = ACTIVITY_POST;
1482 $postarray['author-name'] = $postarray['owner-name'];
1483 $postarray['author-link'] = $postarray['owner-link'];
1484 $postarray['author-avatar'] = $postarray['owner-avatar'];
1485 $postarray['plink'] = "https://twitter.com/" . $post->user->screen_name . "/status/" . $post->id_str;
1486 $postarray['app'] = strip_tags($post->source);
1488 if ($post->user->protected) {
1489 $postarray['private'] = 1;
1490 $postarray['allow_cid'] = '<' . $self['id'] . '>';
1493 if (is_string($post->full_text)) {
1494 $postarray['body'] = $post->full_text;
1496 $postarray['body'] = $post->text;
1499 // When the post contains links then use the correct object type
1500 if (count($post->entities->urls) > 0) {
1501 $postarray['object-type'] = ACTIVITY_OBJ_BOOKMARK;
1504 // Search for media links
1505 $picture = twitter_media_entities($post, $postarray);
1507 $converted = twitter_expand_entities($a, $postarray['body'], $post, false, $picture);
1508 $postarray['body'] = $converted["body"];
1509 $postarray['tag'] = $converted["tags"];
1510 $postarray['created'] = datetime_convert('UTC', 'UTC', $post->created_at);
1511 $postarray['edited'] = datetime_convert('UTC', 'UTC', $post->created_at);
1513 $statustext = $converted["plain"];
1515 if (is_string($post->place->name)) {
1516 $postarray["location"] = $post->place->name;
1518 if (is_string($post->place->full_name)) {
1519 $postarray["location"] = $post->place->full_name;
1521 if (is_array($post->geo->coordinates)) {
1522 $postarray["coord"] = $post->geo->coordinates[0] . " " . $post->geo->coordinates[1];
1524 if (is_array($post->coordinates->coordinates)) {
1525 $postarray["coord"] = $post->coordinates->coordinates[1] . " " . $post->coordinates->coordinates[0];
1527 if (is_object($post->retweeted_status)) {
1528 $retweet = twitter_createpost($a, $uid, $post->retweeted_status, $self, false, false, $noquote);
1530 $retweet['object'] = $postarray['object'];
1531 $retweet['private'] = $postarray['private'];
1532 $retweet['allow_cid'] = $postarray['allow_cid'];
1533 $retweet['contact-id'] = $postarray['contact-id'];
1534 $retweet['owner-name'] = $postarray['owner-name'];
1535 $retweet['owner-link'] = $postarray['owner-link'];
1536 $retweet['owner-avatar'] = $postarray['owner-avatar'];
1538 $postarray = $retweet;
1541 if (is_object($post->quoted_status) && !$noquote) {
1542 $quoted = twitter_createpost($a, $uid, $post->quoted_status, $self, false, false, true);
1544 $postarray['body'] = $statustext;
1546 $postarray['body'] .= "\n" . share_header($quoted['author-name'], $quoted['author-link'], $quoted['author-avatar'], "", $quoted['created'], $quoted['plink']);
1548 $postarray['body'] .= $quoted['body'] . '[/share]';
1554 function twitter_checknotification(App $a, $uid, $own_id, $top_item, $postarray)
1556 /// TODO: this whole function doesn't seem to work. Needs complete check
1557 $user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` LIMIT 1",
1561 if (!count($user)) {
1566 if (link_compare($user[0]["url"], $postarray['author-link'])) {
1570 $own_user = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1572 dbesc("twitter::".$own_id)
1575 if (!count($own_user)) {
1579 // Is it me from twitter?
1580 if (link_compare($own_user[0]["url"], $postarray['author-link'])) {
1584 $myconv = q("SELECT `author-link`, `author-avatar`, `parent` FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `parent` != 0 AND `deleted` = 0",
1585 dbesc($postarray['parent-uri']),
1589 if (count($myconv)) {
1590 foreach ($myconv as $conv) {
1591 // now if we find a match, it means we're in this conversation
1592 if (!link_compare($conv['author-link'], $user[0]["url"]) && !link_compare($conv['author-link'], $own_user[0]["url"])) {
1596 require_once 'include/enotify.php';
1598 $conv_parent = $conv['parent'];
1601 'type' => NOTIFY_COMMENT,
1602 'notify_flags' => $user[0]['notify-flags'],
1603 'language' => $user[0]['language'],
1604 'to_name' => $user[0]['username'],
1605 'to_email' => $user[0]['email'],
1606 'uid' => $user[0]['uid'],
1607 'item' => $postarray,
1608 'link' => $a->get_baseurl() . '/display/' . urlencode(get_item_guid($top_item)),
1609 'source_name' => $postarray['author-name'],
1610 'source_link' => $postarray['author-link'],
1611 'source_photo' => $postarray['author-avatar'],
1612 'verb' => ACTIVITY_POST,
1614 'parent' => $conv_parent,
1617 // only send one notification
1623 function twitter_fetchparentposts(App $a, $uid, $post, $connection, $self, $own_id)
1625 logger("twitter_fetchparentposts: Fetching for user " . $uid . " and post " . $post->id_str, LOGGER_DEBUG);
1629 while ($post->in_reply_to_status_id_str != "") {
1630 $parameters = ["trim_user" => false, "tweet_mode" => "extended", "id" => $post->in_reply_to_status_id_str];
1632 $post = $connection->get('statuses/show', $parameters);
1634 if (!count($post)) {
1635 logger("twitter_fetchparentposts: Can't fetch post " . $parameters->id, LOGGER_DEBUG);
1639 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1640 dbesc("twitter::".$post->id_str),
1651 logger("twitter_fetchparentposts: Fetching " . count($posts) . " parents", LOGGER_DEBUG);
1653 $posts = array_reverse($posts);
1655 if (count($posts)) {
1656 foreach ($posts as $post) {
1657 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1659 if (trim($postarray['body']) == "")
1662 $item = item_store($postarray);
1663 $postarray["id"] = $item;
1665 logger('twitter_fetchparentpost: User ' . $self["nick"] . ' posted parent timeline item ' . $item);
1667 if ($item && !function_exists("check_item_notification")) {
1668 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1674 function twitter_fetchhometimeline(App $a, $uid)
1676 $ckey = Config::get('twitter', 'consumerkey');
1677 $csecret = Config::get('twitter', 'consumersecret');
1678 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1679 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1680 $create_user = PConfig::get($uid, 'twitter', 'create_user');
1681 $mirror_posts = PConfig::get($uid, 'twitter', 'mirror_posts');
1683 logger("twitter_fetchhometimeline: Fetching for user " . $uid, LOGGER_DEBUG);
1685 $application_name = Config::get('twitter', 'application_name');
1687 if ($application_name == "") {
1688 $application_name = $a->get_hostname();
1691 require_once 'library/twitteroauth.php';
1692 require_once 'include/items.php';
1694 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1696 $own_contact = twitter_fetch_own_contact($a, $uid);
1698 $r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
1699 intval($own_contact),
1703 $own_id = $r[0]["nick"];
1705 logger("twitter_fetchhometimeline: Own twitter contact not found for user " . $uid, LOGGER_DEBUG);
1709 $r = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1",
1715 logger("twitter_fetchhometimeline: Own contact not found for user " . $uid, LOGGER_DEBUG);
1719 $u = q("SELECT * FROM user WHERE uid = %d LIMIT 1",
1722 logger("twitter_fetchhometimeline: Own user not found for user " . $uid, LOGGER_DEBUG);
1726 $parameters = ["exclude_replies" => false, "trim_user" => false, "contributor_details" => true, "include_rts" => true, "tweet_mode" => "extended"];
1727 //$parameters["count"] = 200;
1728 // Fetching timeline
1729 $lastid = PConfig::get($uid, 'twitter', 'lasthometimelineid');
1731 $first_time = ($lastid == "");
1733 if ($lastid != "") {
1734 $parameters["since_id"] = $lastid;
1737 $items = $connection->get('statuses/home_timeline', $parameters);
1739 if (!is_array($items)) {
1740 logger("twitter_fetchhometimeline: Error fetching home timeline: " . print_r($items, true), LOGGER_DEBUG);
1744 $posts = array_reverse($items);
1746 logger("twitter_fetchhometimeline: Fetching timeline for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1748 if (count($posts)) {
1749 foreach ($posts as $post) {
1750 if ($post->id_str > $lastid) {
1751 $lastid = $post->id_str;
1752 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1759 if (stristr($post->source, $application_name) && $post->user->screen_name == $own_id) {
1760 logger("twitter_fetchhometimeline: Skip previously sended post", LOGGER_DEBUG);
1764 if ($mirror_posts && $post->user->screen_name == $own_id && $post->in_reply_to_status_id_str == "") {
1765 logger("twitter_fetchhometimeline: Skip post that will be mirrored", LOGGER_DEBUG);
1769 if ($post->in_reply_to_status_id_str != "") {
1770 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1773 $postarray = twitter_createpost($a, $uid, $post, $self, $create_user, true, false);
1775 if (trim($postarray['body']) == "") {
1779 $item = item_store($postarray);
1780 $postarray["id"] = $item;
1782 logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted home timeline item ' . $item);
1784 if ($item && !function_exists("check_item_notification")) {
1785 twitter_checknotification($a, $uid, $own_id, $item, $postarray);
1789 PConfig::set($uid, 'twitter', 'lasthometimelineid', $lastid);
1791 // Fetching mentions
1792 $lastid = PConfig::get($uid, 'twitter', 'lastmentionid');
1794 $first_time = ($lastid == "");
1796 if ($lastid != "") {
1797 $parameters["since_id"] = $lastid;
1800 $items = $connection->get('statuses/mentions_timeline', $parameters);
1802 if (!is_array($items)) {
1803 logger("twitter_fetchhometimeline: Error fetching mentions: " . print_r($items, true), LOGGER_DEBUG);
1807 $posts = array_reverse($items);
1809 logger("twitter_fetchhometimeline: Fetching mentions for user " . $uid . " " . sizeof($posts) . " items", LOGGER_DEBUG);
1811 if (count($posts)) {
1812 foreach ($posts as $post) {
1813 if ($post->id_str > $lastid) {
1814 $lastid = $post->id_str;
1821 if ($post->in_reply_to_status_id_str != "") {
1822 twitter_fetchparentposts($a, $uid, $post, $connection, $self, $own_id);
1825 $postarray = twitter_createpost($a, $uid, $post, $self, false, false, false);
1827 if (trim($postarray['body']) == "") {
1831 $item = item_store($postarray);
1832 $postarray["id"] = $item;
1834 if ($item && function_exists("check_item_notification")) {
1835 check_item_notification($item, $uid, NOTIFY_TAGSELF);
1838 if (!isset($postarray["parent"]) || ($postarray["parent"] == 0)) {
1839 $postarray["parent"] = $item;
1842 logger('twitter_fetchhometimeline: User ' . $self["nick"] . ' posted mention timeline item ' . $item);
1845 $r = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d LIMIT 1",
1846 dbesc($postarray['uri']),
1850 $item = $r[0]['id'];
1851 $parent_id = $r[0]['parent'];
1854 $parent_id = $postarray['parent'];
1856 if (($item != 0) && !function_exists("check_item_notification")) {
1857 require_once 'include/enotify.php';
1859 'type' => NOTIFY_TAGSELF,
1860 'notify_flags' => $u[0]['notify-flags'],
1861 'language' => $u[0]['language'],
1862 'to_name' => $u[0]['username'],
1863 'to_email' => $u[0]['email'],
1864 'uid' => $u[0]['uid'],
1865 'item' => $postarray,
1866 'link' => $a->get_baseurl() . '/display/' . urlencode(get_item_guid($item)),
1867 'source_name' => $postarray['author-name'],
1868 'source_link' => $postarray['author-link'],
1869 'source_photo' => $postarray['author-avatar'],
1870 'verb' => ACTIVITY_TAG,
1872 'parent' => $parent_id
1878 PConfig::set($uid, 'twitter', 'lastmentionid', $lastid);
1881 function twitter_fetch_own_contact(App $a, $uid)
1883 $ckey = Config::get('twitter', 'consumerkey');
1884 $csecret = Config::get('twitter', 'consumersecret');
1885 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1886 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1888 $own_id = PConfig::get($uid, 'twitter', 'own_id');
1892 if ($own_id == "") {
1893 require_once 'library/twitteroauth.php';
1895 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1897 // Fetching user data
1898 $user = $connection->get('account/verify_credentials');
1900 PConfig::set($uid, 'twitter', 'own_id', $user->id_str);
1902 $contact_id = twitter_fetch_contact($uid, $user, true);
1904 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `alias` = '%s' LIMIT 1",
1906 dbesc("twitter::" . $own_id));
1908 $contact_id = $r[0]["id"];
1910 PConfig::delete($uid, 'twitter', 'own_id');
1917 function twitter_is_retweet(App $a, $uid, $body)
1919 $body = trim($body);
1921 // Skip if it isn't a pure repeated messages
1922 // Does it start with a share?
1923 if (strpos($body, "[share") > 0) {
1927 // Does it end with a share?
1928 if (strlen($body) > (strrpos($body, "[/share]") + 8)) {
1932 $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "$1", $body);
1933 // Skip if there is no shared message in there
1934 if ($body == $attributes) {
1939 preg_match("/link='(.*?)'/ism", $attributes, $matches);
1940 if ($matches[1] != "") {
1941 $link = $matches[1];
1944 preg_match('/link="(.*?)"/ism', $attributes, $matches);
1945 if ($matches[1] != "") {
1946 $link = $matches[1];
1949 $id = preg_replace("=https?://twitter.com/(.*)/status/(.*)=ism", "$2", $link);
1954 logger('twitter_is_retweet: Retweeting id ' . $id . ' for user ' . $uid, LOGGER_DEBUG);
1956 $ckey = Config::get('twitter', 'consumerkey');
1957 $csecret = Config::get('twitter', 'consumersecret');
1958 $otoken = PConfig::get($uid, 'twitter', 'oauthtoken');
1959 $osecret = PConfig::get($uid, 'twitter', 'oauthsecret');
1961 require_once 'library/twitteroauth.php';
1962 $connection = new TwitterOAuth($ckey, $csecret, $otoken, $osecret);
1964 $result = $connection->post('statuses/retweet/' . $id);
1966 logger('twitter_is_retweet: result ' . print_r($result, true), LOGGER_DEBUG);
1968 return !isset($result->errors);