Last active
November 21, 2024 15:11
-
-
Save eMerzh/d8b36a0147d6896b082d34374c4c4067 to your computer and use it in GitHub Desktop.
This file contains 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 | |
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