diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyOptions.php b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyOptions.php new file mode 100644 index 0000000000000..fe576986164e4 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyOptions.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Bluesky; + +use Symfony\Component\Mime\Part\File; +use Symfony\Component\Notifier\Message\MessageOptionsInterface; + +final class BlueskyOptions implements MessageOptionsInterface +{ + public function __construct( + private array $options = [], + ) { + } + + public function toArray(): array + { + return $this->options; + } + + public function getRecipientId(): ?string + { + return null; + } + + /** + * @return $this + */ + public function attachMedia(File $file, string $description = ''): static + { + $this->options['attach'][] = [ + 'file' => $file, + 'description' => $description, + ]; + + return $this; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php index 2a3552e83402e..71e6af7ede51e 100644 --- a/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/BlueskyTransport.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Notifier\Bridge\Bluesky; use Psr\Log\LoggerInterface; +use Symfony\Component\Mime\Part\File; use Symfony\Component\Notifier\Exception\TransportException; use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; use Symfony\Component\Notifier\Message\ChatMessage; @@ -65,19 +66,28 @@ protected function doSend(MessageInterface $message): SentMessage $post = [ '$type' => 'app.bsky.feed.post', 'text' => $message->getSubject(), - 'createdAt' => (new \DateTimeImmutable())->format('Y-m-d\\TH:i:s.u\\Z'), + 'createdAt' => \DateTimeImmutable::createFromFormat('U', time())->format('Y-m-d\\TH:i:s.u\\Z'), ]; if ([] !== $facets = $this->parseFacets($post['text'])) { $post['facets'] = $facets; } + $options = $message->getOptions()?->toArray() ?? []; + $options['repo'] = $this->authSession['did'] ?? null; + $options['collection'] = 'app.bsky.feed.post'; + $options['record'] = $post; + + if (isset($options['attach'])) { + $options['record']['embed'] = [ + '$type' => 'app.bsky.embed.images', + 'images' => $this->uploadMedia($options['attach']), + ]; + unset($options['attach']); + } + $response = $this->client->request('POST', sprintf('https://%s/xrpc/com.atproto.repo.createRecord', $this->getEndpoint()), [ 'auth_bearer' => $this->authSession['accessJwt'] ?? null, - 'json' => [ - 'repo' => $this->authSession['did'] ?? null, - 'collection' => 'app.bsky.feed.post', - 'record' => $post, - ], + 'json' => $options, ]); try { @@ -222,4 +232,51 @@ private function getMatchAndPosition(AbstractString $text, string $regex): array return $output; } + + /** + * @param array $media + * + * @return array + */ + private function uploadMedia(array $media): array + { + $pool = []; + + foreach ($media as ['file' => $file, 'description' => $description]) { + $pool[] = [ + 'description' => $description, + 'response' => $this->client->request('POST', sprintf('https://%s/xrpc/com.atproto.repo.uploadBlob', $this->getEndpoint()), [ + 'auth_bearer' => $this->authSession['accessJwt'] ?? null, + 'headers' => [ + 'Content-Type: '.$file->getContentType(), + ], + 'body' => fopen($file->getPath(), 'r'), + ]), + ]; + } + + $embeds = []; + + try { + foreach ($pool as $i => ['description' => $description, 'response' => $response]) { + unset($pool[$i]); + $result = $response->toArray(false); + + if (300 <= $response->getStatusCode()) { + throw new TransportException('Unable to embed medias.', $response); + } + + $embeds[] = [ + 'alt' => $description, + 'image' => $result['blob'], + ]; + } + } finally { + foreach ($pool as ['response' => $response]) { + $response->cancel(); + } + } + + return $embeds; + } } diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md index 5be39cbeeb951..d337db00df015 100644 --- a/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Add option to attach a media + 7.1 --- diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php index f6c5005c666cf..59a6e76194e2f 100644 --- a/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/BlueskyTransportTest.php @@ -12,8 +12,11 @@ namespace Symfony\Component\Notifier\Bridge\Bluesky\Tests; use Psr\Log\NullLogger; +use Symfony\Bridge\PhpUnit\ClockMock; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\JsonMockResponse; +use Symfony\Component\Mime\Part\File; +use Symfony\Component\Notifier\Bridge\Bluesky\BlueskyOptions; use Symfony\Component\Notifier\Bridge\Bluesky\BlueskyTransport; use Symfony\Component\Notifier\Exception\LogicException; use Symfony\Component\Notifier\Message\ChatMessage; @@ -25,6 +28,12 @@ final class BlueskyTransportTest extends TransportTestCase { + protected function setUp(): void + { + ClockMock::register(self::class); + ClockMock::withClockMock(1714293617); + } + public static function createTransport(?HttpClientInterface $client = null): BlueskyTransport { $blueskyTransport = new BlueskyTransport('username', 'password', new NullLogger(), $client ?? new MockHttpClient()); @@ -264,6 +273,48 @@ public function testParseFacetsUrlWithTrickyRegex() $this->assertEquals($expected, $this->parseFacets($input)); } + public function testWithMedia() + { + $transport = $this->createTransport(new MockHttpClient((function () { + yield function (string $method, string $url, array $options) { + $this->assertSame('POST', $method); + $this->assertSame('https://bsky.social/xrpc/com.atproto.server.createSession', $url); + + return new JsonMockResponse(['accessJwt' => 'foo']); + }; + + yield function (string $method, string $url, array $options) { + $this->assertSame('POST', $method); + $this->assertSame('https://bsky.social/xrpc/com.atproto.repo.uploadBlob', $url); + $this->assertArrayHasKey('authorization', $options['normalized_headers']); + + return new JsonMockResponse(['blob' => [ + '$type' => 'blob', + 'ref' => [ + '$link' => 'bafkreibabalobzn6cd366ukcsjycp4yymjymgfxcv6xczmlgpemzkz3cfa', + ], + 'mimeType' => 'image/png', + 'size' => 760898, + ]]); + }; + + yield function (string $method, string $url, array $options) { + $this->assertSame('POST', $method); + $this->assertSame('https://bsky.social/xrpc/com.atproto.repo.createRecord', $url); + $this->assertArrayHasKey('authorization', $options['normalized_headers']); + $this->assertSame('{"repo":null,"collection":"app.bsky.feed.post","record":{"$type":"app.bsky.feed.post","text":"Hello World!","createdAt":"2024-04-28T08:40:17.000000Z","embed":{"$type":"app.bsky.embed.images","images":[{"alt":"A fixture","image":{"$type":"blob","ref":{"$link":"bafkreibabalobzn6cd366ukcsjycp4yymjymgfxcv6xczmlgpemzkz3cfa"},"mimeType":"image\/png","size":760898}}]}}}', $options['body']); + + return new JsonMockResponse(['cid' => '103254962155278888']); + }; + })())); + + $options = (new BlueskyOptions()) + ->attachMedia(new File(__DIR__.'/fixtures.gif'), 'A fixture'); + $result = $transport->send(new ChatMessage('Hello World!', $options)); + + $this->assertSame('103254962155278888', $result->getMessageId()); + } + /** * A small helper function to test BlueskyTransport::parseFacets(). */ diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/fixtures.gif b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/fixtures.gif new file mode 100644 index 0000000000000..443aca422f762 Binary files /dev/null and b/src/Symfony/Component/Notifier/Bridge/Bluesky/Tests/fixtures.gif differ diff --git a/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json b/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json index 453dd757bc574..3f5fa25583790 100644 --- a/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Bluesky/composer.json @@ -23,9 +23,12 @@ "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/http-client": "^6.4|^7.0", - "symfony/notifier": "^7.1", + "symfony/notifier": "^7.2", "symfony/string": "^6.4|^7.0" }, + "require-dev": { + "symfony/mime": "^6.4|^7.0" + }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Bluesky\\": "" }, "exclude-from-classmap": [ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy