Last active
November 4, 2023 07:56
-
-
Save 59de44955ebd/5a861ab759ec221409424132e0177d0e to your computer and use it in GitHub Desktop.
Simple PHP script for generating RSS feeds from Bluesky user posts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
# USAGE: | |
# https://your.domain/path/to/script?h=[some bluesky handle] | |
# https://your.domain/path/to/script?h=[some bluesky handle]&reply=exclude | |
# CONFIG | |
define('BSKY_HANDLE', '<your full bluesky handle>'); | |
define('BSKY_APP_PASSWORD', '<your bluesky app password>'); | |
define('BSKY_PDS_URL', 'https://bsky.social'); | |
define('MAX_POSTS', 20); | |
define('BSKY_SESSION_FILE', '../session.json'); # web server must have write permission for this location | |
if (!isset($_GET['h'])) | |
{ | |
http_response_code(400); | |
$url = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI']; | |
die("Usage:<br>$url?h=[some-blueky-handle]<br>$url?h=[some-blueky-handle]&reply=exclude"); | |
} | |
function login() | |
{ | |
$url = BSKY_PDS_URL . '/xrpc/com.atproto.server.createSession'; | |
$postdata = json_encode([ | |
'identifier' => BSKY_HANDLE, | |
'password' => BSKY_APP_PASSWORD, | |
]); | |
$res = file_get_contents( | |
$url, | |
false, | |
stream_context_create([ | |
'http' => [ | |
'method' => 'POST', | |
'header' => "Content-Type: application/json\r\n", | |
'content' => $postdata, | |
'ignore_errors' => true, | |
] | |
]) | |
); | |
$session = json_decode($res, true); | |
if (isset($session['accessJwt'])) | |
file_put_contents(BSKY_SESSION_FILE, $res); | |
return $session; | |
} | |
function refresh_session($refreshJwt) | |
{ | |
$url = BSKY_PDS_URL . '/xrpc/com.atproto.server.refreshSession'; | |
$res = file_get_contents( | |
$url, | |
false, | |
stream_context_create([ | |
'http' => [ | |
'method' => 'POST', | |
'header' => "Content-Type: application/json\r\n" . | |
'Authorization: Bearer ' . $refreshJwt . "\r\n", | |
'ignore_errors' => true, | |
] | |
]) | |
); | |
$session = json_decode($res, true); | |
if (isset($session['accessJwt'])) | |
file_put_contents(BSKY_SESSION_FILE, $res); | |
return $session; | |
} | |
function get_feed($author, $filter, $accessJwt) | |
{ | |
$res = file_get_contents( | |
BSKY_PDS_URL . '/xrpc/app.bsky.feed.getAuthorFeed?actor=' . $author . '&limit=' . MAX_POSTS . $filter, | |
false, | |
stream_context_create([ | |
'http' => [ | |
'method' => 'GET', | |
'header' => 'Authorization: Bearer ' . $accessJwt . "\r\n", | |
'ignore_errors' => true, | |
] | |
]) | |
); | |
return [json_decode($res, true), $http_response_header]; | |
} | |
$author = $_GET['h']; | |
$filter = @$_GET['reply'] == 'exclude' ? '&filter=posts_no_replies' : ''; | |
######################################## | |
# If there is a session stored, first try if its accessJwt is still valid. | |
# If not, try to refresh the session using the refreshJwt. | |
# IF that also fails, login and try again. | |
######################################## | |
$session = json_decode(@file_get_contents(BSKY_SESSION_FILE), true); | |
if (isset($session['accessJwt'])) | |
{ | |
list($data, $response_header) = get_feed($author, $filter, $session['accessJwt']); | |
if (isset($data['error'])) | |
{ | |
$session = refresh_session($session['refreshJwt']); | |
if (isset($session['error'])) | |
$session = login(); | |
list($data, $response_header) = get_feed($author, $filter, $session['accessJwt']); | |
} | |
} | |
else | |
{ | |
$session = login(); | |
list($data, $response_header) = get_feed($author, $filter, $session['accessJwt']); | |
} | |
if (isset($data['error'])) | |
{ | |
preg_match('{HTTP\/\S*\s(\d{3})}', $response_header[0], $match); | |
http_response_code((int)$match[1]); | |
die($data['message']); | |
} | |
$atom_url = htmlspecialchars($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI']); | |
header('Content-Type: text/xml; charset=UTF-8'); | |
echo '<?xml version="1.0" encoding="UTF-8"?> | |
<rss version="2.0" | |
xmlns:atom="http://www.w3.org/2005/Atom" | |
xmlns:dc="http://purl.org/dc/elements/1.1/"> | |
<channel> | |
<title>' . $author . ' Feed</title> | |
<atom:link href="' . $atom_url . '" rel="self" type="application/rss+xml" /> | |
<link>' . $atom_url . '</link> | |
<description>' . $author . '\'s posts in https://bsky.social</description> | |
'; | |
foreach ($data['feed'] as $item) | |
{ | |
$date = date('r', strtotime($item['post']['record']['createdAt'])); | |
$link = 'https://bsky.app/profile/' . str_replace('app.bsky.feed.', '', substr($item['post']['uri'], 5)); | |
?> | |
<item> | |
<title><?php echo htmlspecialchars($item['post']['record']['text']); ?></title> | |
<description><![CDATA[<p><?php echo htmlspecialchars($item['post']['record']['text']); ?></p> | |
<?php | |
if (isset($item['post']['embed']['images'])) | |
{ | |
foreach ($item['post']['embed']['images'] as $image) | |
{ | |
?> | |
<figure> | |
<a href="<?php echo $image['fullsize']; ?>"><img src="<?php echo $image['thumb']; ?>" alt="<?php echo htmlspecialchars(@$image['alt']); ?>" /></a> | |
</figure> | |
<?php | |
} | |
} | |
?> | |
]]></description> | |
<link><?php echo $link; ?></link> | |
<guid isPermaLink="false"><?php echo $item['post']['uri']; ?></guid> | |
<pubDate><?php echo $date; ?></pubDate> | |
<dc:creator><?php echo $author; ?></dc:creator> | |
</item> | |
<?php | |
} | |
?> | |
</channel> | |
</rss> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment