From 2a44a006b222c51a421b2f27f0539263d5bf3bcf Mon Sep 17 00:00:00 2001
From: mruac External Link: $externalTitle External Link: $externalTitle $externalDescription ';
+ //post
+ $description .= $this->getPostDescription(
+ $postDisplayName,
+ $postAuthorHandle,
+ $postUri,
+ $postRecord,
+ 'post'
+ );
+
+ if (isset($postRecord['embed']['$type'])) {
+ //post link embed
+ if ($postRecord['embed']['$type'] === 'app.bsky.embed.external') {
+ $description .= $this->parseExternal($postRecord['embed']['external'], $postAuthorDID);
+ } elseif (
+ $postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
+ $postRecord['embed']['media']['$type'] === 'app.bsky.embed.external'
+ ) {
+ $description .= $this->parseExternal($postRecord['embed']['media']['external'], $postAuthorDID);
+ }
+
+ //post images
+ if (
+ $postRecord['embed']['$type'] === 'app.bsky.embed.images' ||
+ (
+ $postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
+ $postRecord['embed']['media']['$type'] === 'app.bsky.embed.images'
+ )
+ ) {
+ $images = $post['post']['embed']['images'] ?? $post['post']['embed']['media']['images'];
+ foreach ($images as $image) {
+ $description .= $this->getPostImageDescription($image);
+ }
+ }
+
+ //post video
+ if (
+ $postRecord['embed']['$type'] === 'app.bsky.embed.video' ||
+ (
+ $postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
+ $postRecord['embed']['media']['$type'] === 'app.bsky.embed.video'
+ )
+ ) {
+ $description .= $this->getPostVideoDescription(
+ $postRecord['embed']['video'] ?? $postRecord['embed']['media']['video'],
+ $postAuthorDID
+ );
}
}
+ $description .= ' ';
+ $quotedRecord = $post['post']['embed']['record']['record'] ?? $post['post']['embed']['record'];
- 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 .= "' . $externalTitle . '';
+ $description .= '
';
}
return $description;
}
- private function textToDescription($text)
+ private function textToDescription($record)
{
- $text = nl2br(htmlspecialchars($text, ENT_QUOTES, 'UTF-8'));
- $text = preg_replace('/(https?:\/\/[^\s]+)/i', '$1', $text);
-
+ if (isset($record['value'])) {
+ $record = $record['value'];
+ }
+ $text = $record['text'];
+ $text_copy = $text;
+ $text = nl2br(e($text));
+ if (isset($record['facets'])) {
+ $facets = $record['facets'];
+ foreach ($facets as $facet) {
+ if ($facet['features'][0]['$type'] === 'app.bsky.richtext.facet#link') {
+ $substring = substr($text_copy, $facet['index']['byteStart'], $facet['index']['byteEnd'] - $facet['index']['byteStart']);
+ $text = str_replace($substring, '' . $substring . '', $text);
+ }
+ }
+ }
return $text;
}
public function collectData()
{
- $handle = $this->getInput('handle');
- $filter = $this->getInput('filter') ?: 'posts_and_author_threads';
+ $user_id = $this->getInput('user_id');
+ $handle_match = preg_match('/(?:[a-zA-Z]*\.)+([a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)/', $user_id, $handle_res); //gets the TLD in $handle_match[1]
+ $did_match = preg_match('/did:plc:[a-z2-7]{24}/', $user_id); //https://github.com/did-method-plc/did-method-plc#identifier-syntax
+ $exclude = ['alt', 'arpa', 'example', 'internal', 'invalid', 'local', 'localhost', 'onion']; //https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains
+ if ($handle_match == true && array_search($handle_res[1], $exclude) == false) {
+ //valid bsky handle
+ $did = $this->resolveHandle($user_id);
+ } elseif ($did_match == true) {
+ //valid DID
+ $did = $user_id;
+ } else {
+ returnClientError('Invalid ATproto handle or DID provided.');
+ }
+
+ $filter = $this->getInput('feed_filter') ?: 'posts_and_author_threads';
+ $replyContext = $this->getInput('include_reply_context');
- $did = $this->resolveHandle($handle);
$this->profile = $this->getProfile($did);
$authorFeed = $this->getAuthorFeed($did, $filter);
foreach ($authorFeed['feed'] as $post) {
+ $postRecord = $post['post']['record'];
+
$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'];
+ $item['uri'] = self::URI . '/profile/' . $this->fallbackAuthor($post['post']['author'], 'url') . '/post/' . explode('app.bsky.feed.post/', $post['post']['uri'])[1];
+ $item['title'] = $this->getInput('verbose_title') ? $this->generateVerboseTitle($post) : strtok($postRecord['text'], "\n");
+ $item['timestamp'] = strtotime($postRecord['createdAt']);
+ $item['author'] = $this->fallbackAuthor($post['post']['author'], 'display');
- $description = $this->textToDescription($post['post']['record']['text']);
+ $postAuthorDID = $post['post']['author']['did'];
+ $postAuthorHandle = $post['post']['author']['handle'] !== 'handle.invalid' ? '@' . $post['post']['author']['handle'] . ' ' : '';
+ $postDisplayName = $post['post']['author']['displayName'] ?? '';
+ $postDisplayName = e($postDisplayName);
+ $postUri = $item['uri'];
- // 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 (Debug::isEnabled()) {
+ $url = explode('/', $post['post']['uri']);
+ error_log('https://bsky.app/profile/' . $url[2] . '/post/' . $url[4]);
}
- 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 .= "";
+ $description = '';
+ $description .= '
";
- }
- }
+ if (isset($quotedRecord['notFound']) && $quotedRecord['notFound']) { //deleted post
+ $description .= 'Quoted post deleted.';
+ } elseif (isset($quotedRecord['detached']) && $quotedRecord['detached']) { //detached quote
+ $uri_explode = explode('/', $quotedRecord['uri']);
+ $uri_reconstructed = self::URI . '/profile/' . $uri_explode[2] . '/post/' . $uri_explode[4];
+ $description .= 'Quoted post detached.';
+ } elseif (isset($quotedRecord['blocked']) && $quotedRecord['blocked']) { //blocked by quote author
+ $description .= 'Author of quoted post has blocked OP.';
+ } else {
+ $quotedAuthorDid = $quotedRecord['author']['did'];
+ $quotedDisplayName = $quotedRecord['author']['displayName'] ?? '';
+ $quotedDisplayName = e($quotedDisplayName);
+ $quotedAuthorHandle = $quotedRecord['author']['handle'] !== 'handle.invalid' ? '@' . $quotedRecord['author']['handle'] . '' : '';
- // 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;
- }
+ $quotedPostUri = self::URI . '/profile/' . $this->fallbackAuthor($quotedRecord['author'], 'url') . '/post/' . $quotedPostId;
- if ($quotedText) {
- $description .= '
Quote from ' . htmlspecialchars($quotedDisplayName) . ' (@ ' . htmlspecialchars($quotedAuthor) . '):
';
- $description .= $this->textToDescription($quotedText);
- if (isset($quotedPostUri)) {
- $description .= "";
+ //quoted post - post
+ $description .= $this->getPostDescription(
+ $quotedDisplayName,
+ $quotedAuthorHandle,
+ $quotedPostUri,
+ $quotedRecord,
+ 'quote'
+ );
+
+ if (isset($quotedRecord['value']['embed']['$type'])) {
+ //quoted post - post link embed
+ if ($quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.external') {
+ $description .= $this->parseExternal($quotedRecord['value']['embed']['external'], $quotedAuthorDid);
+ }
+
+ //quoted post - post video
+ if (
+ $quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.video' ||
+ (
+ $quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
+ $quotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.video'
+ )
+ ) {
+ $description .= $this->getPostVideoDescription(
+ $quotedRecord['value']['embed']['video'] ?? $quotedRecord['value']['embed']['media']['video'],
+ $quotedAuthorDid
+ );
+ }
+
+ //quoted post - post images
+ if (
+ $quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.images' ||
+ (
+ $quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
+ $quotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.images'
+ )
+ ) {
+ foreach ($quotedRecord['embeds'] as $embed) {
+ if (
+ $embed['$type'] === 'app.bsky.embed.images#view' ||
+ ($embed['$type'] === 'app.bsky.embed.recordWithMedia#view' && $embed['media']['$type'] === 'app.bsky.embed.images#view')
+ ) {
+ $images = $embed['images'] ?? $embed['media']['images'];
+ foreach ($images as $image) {
+ $description .= $this->getPostImageDescription($image);
+ }
+ }
+ }
+ }
}
}
+ $description .= '
'; + + $replyPostAuthorDID = $replyPost['author']['did']; + $replyPostAuthorHandle = $replyPost['author']['handle'] !== 'handle.invalid' ? '@' . $replyPost['author']['handle'] . ' ' : ''; + $replyPostDisplayName = $replyPost['author']['displayName'] ?? ''; + $replyPostDisplayName = e($replyPostDisplayName); + $replyPostUri = self::URI . '/profile/' . $this->fallbackAuthor($replyPost['author'], 'url') . '/post/' . explode('app.bsky.feed.post/', $replyPost['uri'])[1]; + + // reply post + $description .= $this->getPostDescription( + $replyPostDisplayName, + $replyPostAuthorHandle, + $replyPostUri, + $replyPostRecord, + 'reply' + ); + + if (isset($replyPostRecord['embed']['$type'])) { + //post link embed + if ($replyPostRecord['embed']['$type'] === 'app.bsky.embed.external') { + $description .= $this->parseExternal($replyPostRecord['embed']['external'], $replyPostAuthorDID); + } elseif ( + $replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' && + $replyPostRecord['embed']['media']['$type'] === 'app.bsky.embed.external' + ) { + $description .= $this->parseExternal($replyPostRecord['embed']['media']['external'], $replyPostAuthorDID); } + + //post images + if ( + $replyPostRecord['embed']['$type'] === 'app.bsky.embed.images' || + ( + $replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' && + $replyPostRecord['embed']['media']['$type'] === 'app.bsky.embed.images' + ) + ) { + $images = $replyPost['embed']['images'] ?? $replyPost['embed']['media']['images']; + foreach ($images as $image) { + $description .= $this->getPostImageDescription($image); + } + } + + //post video + if ( + $replyPostRecord['embed']['$type'] === 'app.bsky.embed.video' || + ( + $replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' && + $replyPostRecord['embed']['media']['$type'] === 'app.bsky.embed.video' + ) + ) { + $description .= $this->getPostVideoDescription( + $replyPostRecord['embed']['video'] ?? $replyPostRecord['embed']['media']['video'], + $replyPostAuthorDID + ); + } + } + $description .= '
'; + + //quote post + if ( + isset($replyPostRecord['embed']) && + ($replyPostRecord['embed']['$type'] === 'app.bsky.embed.record' || $replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia') && + isset($replyPost['embed']['record']) + ) { + $description .= ''; + $replyQuotedRecord = $replyPost['embed']['record']['record'] ?? $replyPost['embed']['record']; + + if (isset($replyQuotedRecord['notFound']) && $replyQuotedRecord['notFound']) { //deleted post + $description .= 'Quoted post deleted.'; + } elseif (isset($replyQuotedRecord['detached']) && $replyQuotedRecord['detached']) { //detached quote + $uri_explode = explode('/', $replyQuotedRecord['uri']); + $uri_reconstructed = self::URI . '/profile/' . $uri_explode[2] . '/post/' . $uri_explode[4]; + $description .= 'Quoted post detached.'; + } elseif (isset($replyQuotedRecord['blocked']) && $replyQuotedRecord['blocked']) { //blocked by quote author + $description .= 'Author of quoted post has blocked OP.'; + } else { + $quotedAuthorDid = $replyQuotedRecord['author']['did']; + $quotedDisplayName = $replyQuotedRecord['author']['displayName'] ?? ''; + $quotedDisplayName = e($quotedDisplayName); + $quotedAuthorHandle = $replyQuotedRecord['author']['handle'] !== 'handle.invalid' ? '@' . $replyQuotedRecord['author']['handle'] . '' : ''; + + $parts = explode('/', $replyQuotedRecord['uri']); + $quotedPostId = end($parts); + $quotedPostUri = self::URI . '/profile/' . $this->fallbackAuthor($replyQuotedRecord['author'], 'url') . '/post/' . $quotedPostId; + + //quoted post - post + $description .= $this->getPostDescription( + $quotedDisplayName, + $quotedAuthorHandle, + $quotedPostUri, + $replyQuotedRecord, + 'quote' + ); + + if (isset($replyQuotedRecord['value']['embed']['$type'])) { + //quoted post - post link embed + if ($replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.external') { + $description .= $this->parseExternal($replyQuotedRecord['value']['embed']['external'], $quotedAuthorDid); + } + + //quoted post - post video + if ( + $replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.video' || + ( + $replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' && + $replyQuotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.video' + ) + ) { + $description .= $this->getPostVideoDescription( + $replyQuotedRecord['value']['embed']['video'] ?? $replyQuotedRecord['value']['embed']['media']['video'], + $quotedAuthorDid + ); + } + + //quoted post - post images + if ( + $replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.images' || + ( + $replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' && + $replyQuotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.images' + ) + ) { + foreach ($replyQuotedRecord['embeds'] as $embed) { + if ( + $embed['$type'] === 'app.bsky.embed.images#view' || + ($embed['$type'] === 'app.bsky.embed.recordWithMedia#view' && $embed['media']['$type'] === 'app.bsky.embed.images#view') + ) { + $images = $embed['images'] ?? $embed['media']['images']; + foreach ($images as $image) { + $description .= $this->getPostImageDescription($image); + } + } + } + } + } + } + $description .= '
'; } } @@ -197,6 +469,98 @@ class BlueskyBridge extends BridgeAbstract } } + private function getPostVideoDescription(array $video, $authorDID) + { + //https://video.bsky.app/watch/$did/$cid/thumbnail.jpg + $videoCID = $video['ref']['$link']; + $videoMime = $video['mimeType']; + $thumbnail = "poster=\"https://video.bsky.app/watch/$authorDID/$videoCID/thumbnail.jpg\"" ?? ''; + $videoURL = "https://bsky.social/xrpc/com.atproto.sync.getBlob?did=$authorDID&cid=$videoCID"; + return ""; + } + + private function getPostImageDescription(array $image) + { + $thumbnailUrl = $image['thumb']; + $fullsizeUrl = $image['fullsize']; + $alt = strlen($image['alt']) > 0 ? '