Skip to content

Instantly share code, notes, and snippets.

@cferdinandi
Created October 7, 2024 17:39
Show Gist options
  • Save cferdinandi/e078975e63fd9f76aeef3e04d5572ad1 to your computer and use it in GitHub Desktop.
Save cferdinandi/e078975e63fd9f76aeef3e04d5572ad1 to your computer and use it in GitHub Desktop.
A PHP endpoint for posting to Bluesky

Bluesky PHP endpoint

A PHP endpoint for posting to the Bluesky API.

On Bluesky...

  1. Click Settings.
  2. Click App Passwords.
  3. Click the Add App Password button.
  4. Give it a name, then click Create App Password.
  5. Copy and save the displayed password, as you won't be able to access it again.

On your server add three environment variables...

  • BSKY_HANDLE - Your username on Bluesky without the leading @.
  • BSKY_APP_PW - The app password you just generated.
  • BSKY_LOCAL_TOKEN - A unique token the PHP API will use to validate that requests came from you. Generate a random password for this.

To post to Bluesky using this API, drop the PHP file on your server. Then, make a POST request with the following parameters...

  • headers
    • Content-type - Use application/json or application/x-www-form-urlencoded. Either one works.
    • Authorization - Bearer followed by whatever value you set for BSKY_LOCAL_TOKEN.
  • body
    • status - Whatever text you want posted.
    • url - A URL to link display a card for (optional).
<?php
// Load HTTP library
require_once('Requests/src/Autoload.php');
WpOrg\Requests\Autoload::register();
/**
* Get the API method
* @return String The API method
*/
function get_method () {
return $_SERVER['REQUEST_METHOD'];
}
/**
* Get data object from API data
* @return Object The data object
*/
function get_request_data () {
return array_merge(empty($_POST) ? array() : $_POST, (array) json_decode(file_get_contents('php://input'), true), $_GET);
}
/**
* Send an API response
* @param array $response The API response
* @param integer $code The response code
*/
function send_response ($response, $code = 200) {
http_response_code($code);
die($response);
}
/**
* Check bearer authorization token
* @return boolean If true, request is valid
*/
function is_request_valid () {
$auth_header = $_SERVER['HTTP_AUTHORIZATION'];
// If there's no auth header, fail
if (empty($auth_header)) return false;
// Get auth token
$auth = explode('Bearer', $auth_header);
$request_token = trim($auth[1]);
// If there's no token, fail
if (empty($request_token)) return false;
// If token doesn't match
$local_token = getenv('BSKY_LOCAL_TOKEN');
if ($request_token !== $local_token) return false;
// Otherwise, return true
return true;
}
/**
* Get a BlueSky DID for the user handle
* @@link https://docs.bsky.app/docs/advanced-guides/resolving-identities
* @return string The DID
*/
function get_bluesky_did () {
// Call BlueSky API
$handle = getenv('BSKY_HANDLE');
$request = WpOrg\Requests\Requests::get('https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle));
// If there was an error
if (empty($request->success)) {
return false;
}
// Otherwise, return the DID
$response = json_decode($request->body, true);
return $response['did'];
}
/**
* Get a BlueSky API token
* @return string The token
*/
function get_bluesky_token ($did) {
// Get the app password
$app_pw = getenv('BSKY_APP_PW');
// Request options
$options = json_encode([
'identifier' => $did,
'password' => $app_pw,
]);
// Request headers
$headers = ['Content-Type' => 'application/json'];
// Call the API
$request = WpOrg\Requests\Requests::post('https://bsky.social/xrpc/com.atproto.server.createSession', $headers, $options);
// If there was an error
if (empty($request->success)) {
return false;
}
// Otherwise, return the DID
$response = json_decode($request->body, true);
return $response['accessJwt'];
}
/**
* Get the BlueSky credentials
* @return array The DID and session token
*/
function get_bluesky_auth () {
// Get the handle DID
$did = get_bluesky_did();
if (empty($did)) return false;
// Get the token
$token = get_bluesky_token($did);
if (empty($token)) return false;
// Return the credentials
return [
'did' => $did,
'token' => $token,
];
}
/**
* Get details about the post from the live page
* @param string $url The post URL
* @return array The post details
*/
function get_post_details ($url) {
// Get the URL
$request = WpOrg\Requests\Requests::get($url);
// If there was an error
if (empty($request->success)) {
return null;
}
// Otherwise, get post details
libxml_use_internal_errors(true);
$html = $request->body;
$doc = new DomDocument();
$doc->loadHTML($html);
$xpath = new DOMXPath($doc);
$query = '//*/meta[starts-with(@property, \'og:\')]';
$metas = $xpath->query($query);
$details = array();
foreach ($metas as $meta) {
$property = $meta->getAttribute('property');
$content = $meta->getAttribute('content');
$details[$property] = $content;
}
return $details;
}
/**
* Publish content on BlueSky
* @param string $status The post status
* @param string $url The post URL
*/
function publish_to_bluesky ($status, $url = '') {
// Get authorization credentials
$auth = get_bluesky_auth();
if (empty($auth)) {
send_response('BlueSky authorization failed', 400);
}
// Create request options
$options = [
'collection' => 'app.bsky.feed.post',
'repo' => $auth['did'],
'record' => [
'text' => $status,
'createdAt' => gmdate('Y-m-d\TH:i:s\Z'),
],
];
// If there's a URL, add a card
if (!empty($url)) {
$details = get_post_details($url);
$options['record']['embed'] = [
'$type' => 'app.bsky.embed.external',
'external' => [
'uri' => $url,
'title' => empty($details['og:title']) ? $status : $details['og:title'],
'description' => empty($details['og:description']) ? '' : $details['og:description'],
],
];
}
// Request headers
$headers = [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $auth['token'],
];
// Call the API
$request = WpOrg\Requests\Requests::post('https://bsky.social/xrpc/com.atproto.repo.createRecord', $headers, json_encode($options));
// If there was an error
if (empty($request->success)) {
send_response('Post failed', 400);
// send_response($request->body, 400);
}
// Otherwise, success!
send_response('ok');
}
// Get the HTTP method
$method = get_method();
// If not a post request
if ($method !== 'POST') {
send_response('Method not allowed', 405);
}
// If request not authorized
if (!is_request_valid()) {
send_response('Authorization failed', 400);
}
// Get the post data
$data = get_request_data();
publish_to_bluesky($data['status'], $data['url']);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment