Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Notifier] [Bluesky] Allow to attach image #54756

Merged
merged 1 commit into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
jdecool marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'),
Nyholm marked this conversation as resolved.
Show resolved Hide resolved
];
if ([] !== $facets = $this->parseFacets($post['text'])) {
$post['facets'] = $facets;
}

$options = $message->getOptions()?->toArray() ?? [];
OskarStark marked this conversation as resolved.
Show resolved Hide resolved
$options['repo'] = $this->authSession['did'] ?? null;
$options['collection'] = 'app.bsky.feed.post';
$options['record'] = $post;

if (isset($options['attach'])) {
jdecool marked this conversation as resolved.
Show resolved Hide resolved
$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 {
Expand Down Expand Up @@ -222,4 +232,51 @@ private function getMatchAndPosition(AbstractString $text, string $regex): array

return $output;
}

/**
* @param array<array{file: File, description: string}> $media
Nyholm marked this conversation as resolved.
Show resolved Hide resolved
*
* @return array<array{alt: string, image: array{$type: string, ref: array{$link: string}, mimeType: string, size: int}}>
*/
private function uploadMedia(array $media): array
jdecool marked this conversation as resolved.
Show resolved Hide resolved
{
$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()), [
OskarStark marked this conversation as resolved.
Show resolved Hide resolved
'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;
}
}
5 changes: 5 additions & 0 deletions src/Symfony/Component/Notifier/Bridge/Bluesky/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.2
---

* Add option to attach a media

7.1
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
Expand Down Expand Up @@ -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().
*/
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down