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