Skip to content

Instantly share code, notes, and snippets.

@59de44955ebd
Last active November 4, 2023 07:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 59de44955ebd/5a861ab759ec221409424132e0177d0e to your computer and use it in GitHub Desktop.
Save 59de44955ebd/5a861ab759ec221409424132e0177d0e to your computer and use it in GitHub Desktop.
Simple PHP script for generating RSS feeds from Bluesky user posts
<?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