From b42a993176bc1e291f52b4de4ce3397fa307e84b Mon Sep 17 00:00:00 2001 From: thomas-333 <144368654+thomas-333@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:01:37 +0000 Subject: [PATCH] [Bluesky] New bridge (#4341) * Create BlueskyProfileBridge.php Bridge for Bluesky * Update BlueskyProfileBridge.php Attempt to fix test error * Rename BlueskyProfileBridge.php to BlueskyBridge.php and add list of select data source * Update BlueskyBridge.php to pass lint checks --- bridges/BlueskyBridge.php | 230 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 bridges/BlueskyBridge.php diff --git a/bridges/BlueskyBridge.php b/bridges/BlueskyBridge.php new file mode 100644 index 00000000..8dab82f4 --- /dev/null +++ b/bridges/BlueskyBridge.php @@ -0,0 +1,230 @@ + [ + 'name' => 'Bluesky Data Source', + 'type' => 'list', + 'defaultValue' => 'Profile', + 'values' => [ + 'Profile' => 'getAuthorFeed', + ], + 'title' => 'Select the type of data source to fetch from Bluesky.' + ], + '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 .= "

External Link: $externalTitle

"; + $description .= ""; + } else { + $description .= "

External Link: $externalTitle

"; + $description .= "

$externalDescription

"; + + if ($thumb) { + $thumbUrl = 'https://cdn.bsky.app/img/feed_thumbnail/plain/' . $did . '/' . $thumb['ref']['$link'] . '@jpeg'; + $description .= "

\"External

"; + } + } + return $description; + } + + private function textToDescription($text) + { + $text = nl2br(htmlspecialchars($text, ENT_QUOTES, 'UTF-8')); + $text = preg_replace('/(https?:\/\/[^\s]+)/i', '$1', $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 .= "

\"Video

"; + } + } + + 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 .= "

'; + } + } + + 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 .= "

\"Image\""; + } + } + + // 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'])) { + $parts = explode('/', $quotedRecord['uri']); + $quotedPostId = end($parts); + $quotedPostUri = self::URI . '/profile/' . $quotedAuthor . '/post/' . $quotedPostId; + } + + if ($quotedText) { + $description .= '
Quote from ' . htmlspecialchars($quotedDisplayName) . ' (@ ' . htmlspecialchars($quotedAuthor) . '):
'; + $description .= $this->textToDescription($quotedText); + if (isset($quotedPostUri)) { + $description .= "

View original quote post

"; + } + } + } + + 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 .= "

\"Quoted"; + } + } + } + + $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'; + } +}