Skip to content

Instantly share code, notes, and snippets.

@eMerzh
Last active November 21, 2024 15:11
Show Gist options
  • Save eMerzh/d8b36a0147d6896b082d34374c4c4067 to your computer and use it in GitHub Desktop.
Save eMerzh/d8b36a0147d6896b082d34374c4c4067 to your computer and use it in GitHub Desktop.
<?php
class BlueskyProfileBridge extends BridgeAbstract {
const NAME = 'Bluesky Profile Posts';
const URI = 'https://bsky.app';
const DESCRIPTION = 'Fetches posts from a Bluesky profile';
const MAINTAINER = 'Code modified from rsshub (TonyRL https://github.com/TonyRL) and expanded';
const PARAMETERS = [
[
'handle' => [
'name' => 'User Handle',
'type' => 'text',
'required' => true,
'exampleValue' => 'jackdodo.bsky.social',
'title' => 'Handle found in URL'
],
'filter' => [
'name' => 'Filter',
'type' => 'list',
'defaultValue' => 'posts_and_author_threads',
'values' => [
'posts_and_author_threads' => 'posts_and_author_threads',
'posts_with_replies' => 'posts_with_replies',
'posts_no_replies' => 'posts_no_replies',
'posts_with_media' => 'posts_with_media',
],
'title' => 'Combinations of post/repost types to include in response.'
]
]
];
private $profile;
public function getName() {
if (isset($this->profile)) {
return sprintf("%s (@%s) - Bluesky", $this->profile['displayName'], $this->profile['handle']);
}
return parent::getName();
}
public function getURI() {
if (isset($this->profile)) {
return self::URI . '/profile/' . $this->profile['handle'];
}
return parent::getURI();
}
public function getIcon() {
if (isset($this->profile)) {
return $this->profile['avatar'];
}
return parent::getIcon();
}
public function getDescription() {
if (isset($this->profile)) {
return $this->profile['description'];
}
return parent::getDescription();
}
private function parseExternal($external, $did) {
$description = '';
$externalUri = $external['uri'];
$externalTitle = htmlspecialchars($external['title'], ENT_QUOTES, 'UTF-8');
$externalDescription = htmlspecialchars($external['description'], ENT_QUOTES, 'UTF-8');
$thumb = $external['thumb'] ?? null;
if (preg_match('/youtube\.com\/watch\?v=([^\&\?\/]+)/', $externalUri, $id) || preg_match('/youtu\.be\/([^\&\?\/]+)/', $externalUri, $id)) {
$videoId = $id[1];
$description .= "<p>External Link: <a href=\"$externalUri\">$externalTitle</a></p>";
$description .= "<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/$videoId\" frameborder=\"0\" allowfullscreen></iframe>";
} else {
$description .= "<p>External Link: <a href=\"$externalUri\">$externalTitle</a></p>";
$description .= "<p>$externalDescription</p>";
if ($thumb) {
$thumbUrl = 'https://cdn.bsky.app/img/feed_thumbnail/plain/' . $did . '/' . $thumb['ref']['$link'] . '@jpeg';
$description .= "<p><a href=\"$externalUri\"><img src=\"$thumbUrl\" alt=\"External Thumbnail\" /></a></p>";
}
}
return $description;
}
private function textToDescription($text) {
$text = nl2br(htmlspecialchars($text, ENT_QUOTES, 'UTF-8'));
$text = preg_replace('/(https?:\/\/[^\s]+)/i', '<a href="$1">$1</a>', $text);
return $text;
}
public function collectData() {
$handle = $this->getInput('handle');
$filter = $this->getInput('filter') ?: 'posts_and_author_threads';
$did = $this->resolveHandle($handle);
$this->profile = $this->getProfile($did);
$authorFeed = $this->getAuthorFeed($did, $filter);
foreach ($authorFeed['feed'] as $post) {
$item = [];
$item['uri'] = self::URI . '/profile/' . $post['post']['author']['handle'] . '/post/' . explode('app.bsky.feed.post/', $post['post']['uri'])[1];
$item['title'] = strtok($post['post']['record']['text'], "\n");
$item['timestamp'] = strtotime($post['post']['record']['createdAt']);
$item['author'] = $this->profile['displayName'];
$description = $this->textToDescription($post['post']['record']['text']);
// Retrieve DID for constructing image URLs
$authorDid = $post['post']['author']['did'];
if (isset($post['post']['record']['embed']['$type']) && $post['post']['record']['embed']['$type'] === 'app.bsky.embed.external') {
$description .= $this->parseExternal($post['post']['record']['embed']['external'], $authorDid);
}
if (isset($post['post']['record']['embed']['$type']) && $post['post']['record']['embed']['$type'] === 'app.bsky.embed.video') {
$thumbnail = $post['post']['embed']['thumbnail'] ?? null;
if ($thumbnail) {
$itemUri = self::URI . '/profile/' . $post['post']['author']['handle'] . '/post/' . explode('app.bsky.feed.post/', $post['post']['uri'])[1];
$description .= "<p><a href=\"$itemUri\"><img src=\"$thumbnail\" alt=\"Video Thumbnail\" /></a></p>";
}
}
if (isset($post['post']['record']['embed']['$type']) && $post['post']['record']['embed']['$type'] === 'app.bsky.embed.recordWithMedia#view') {
$thumbnail = $post['post']['embed']['media']['thumbnail'] ?? null;
$playlist = $post['post']['embed']['media']['playlist'] ?? null;
if ($thumbnail) {
$description .= "<p><video controls poster=\"$thumbnail\">";
$description .= "<source src=\"$playlist\" type=\"application/x-mpegURL\">";
$description .= "Video source not supported</video></p>";
}
}
if (!empty($post['post']['record']['embed']['images'])) {
foreach ($post['post']['record']['embed']['images'] as $image) {
$linkRef = $image['image']['ref']['$link'];
$thumbnailUrl = $this->resolveThumbnailUrl($authorDid, $linkRef);
$fullsizeUrl = $this->resolveFullsizeUrl($authorDid, $linkRef);
$description .= "<br /><br /><a href=\"$fullsizeUrl\"><img src=\"$thumbnailUrl\" alt=\"Image\"></a>";
}
}
// Enhanced handling for quote posts with images
if (isset($post['post']['record']['embed']) && $post['post']['record']['embed']['$type'] === 'app.bsky.embed.record') {
$quotedRecord = $post['post']['record']['embed']['record'];
$quotedAuthor = $post['post']['embed']['record']['author']['handle'] ?? null;
$quotedDisplayName = $post['post']['embed']['record']['author']['displayName'] ?? null;
$quotedText = $post['post']['embed']['record']['value']['text'] ?? null;
if ($quotedAuthor && isset($quotedRecord['uri'])) {
$quotedPostId = end(explode('/', $quotedRecord['uri']));
$quotedPostUri = self::URI . '/profile/' . $quotedAuthor . '/post/' . $quotedPostId;
}
if ($quotedText) {
$description .= "<hr /><strong>Quote from " . htmlspecialchars($quotedDisplayName) . " (@ " . htmlspecialchars($quotedAuthor) . "):</strong><br />";
$description .= $this->textToDescription($quotedText);
if (isset($quotedPostUri)) {
$description .= "<p><a href=\"$quotedPostUri\">View original quote post</a></p>";
}
}
}
if (isset($post['post']['embed']['record']['value']['embed']['images'])) {
$quotedImages = $post['post']['embed']['record']['value']['embed']['images'];
foreach ($quotedImages as $image) {
$linkRef = $image['image']['ref']['$link'] ?? null;
if ($linkRef) {
$quotedAuthorDid = $post['post']['embed']['record']['author']['did'] ?? null;
$thumbnailUrl = $this->resolveThumbnailUrl($quotedAuthorDid, $linkRef);
$fullsizeUrl = $this->resolveFullsizeUrl($quotedAuthorDid, $linkRef);
$description .= "<br /><br /><a href=\"$fullsizeUrl\"><img src=\"$thumbnailUrl\" alt=\"Quoted Image\"></a>";
}
}
}
$item['content'] = $description;
$this->items[] = $item;
}
}
private function resolveHandle($handle) {
$uri = 'https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle);
$response = json_decode(getContents($uri), true);
return $response['did'];
}
private function getProfile($did) {
$uri = 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=' . urlencode($did);
$response = json_decode(getContents($uri), true);
return $response;
}
private function getAuthorFeed($did, $filter) {
$uri = 'https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=' . urlencode($did) . '&filter=' . urlencode($filter) . '&limit=30';
$response = json_decode(getContents($uri), true);
return $response;
}
private function resolveThumbnailUrl($authorDid, $linkRef) {
return 'https://cdn.bsky.app/img/feed_thumbnail/plain/' . $authorDid . '/' . $linkRef . '@jpeg';
}
private function resolveFullsizeUrl($authorDid, $linkRef) {
return 'https://cdn.bsky.app/img/feed_fullsize/plain/' . $authorDid . '/' . $linkRef . '@jpeg';
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment