From c8a0a15a98ff424966513884451a6e7f557d72cb Mon Sep 17 00:00:00 2001 From: blued_gear Date: Fri, 7 Aug 2026 18:30:58 +0000 Subject: [PATCH 01/11] fix missed type handling --- src/Service/ActivityPubManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php index f982dda27..59e78641d 100644 --- a/src/Service/ActivityPubManager.php +++ b/src/Service/ActivityPubManager.php @@ -1225,7 +1225,7 @@ public function getActorFromAttributedTo(string|array|null $attributedTo, bool $ } elseif (\is_array($attributedTo)) { $actors = array_filter($attributedTo, fn ($item) => \is_string($item) || (\is_array($item) && !empty($item['type']) && (!$filterForPerson || 'Person' === $item['type']))); - return array_map(fn ($item) => $item['id'], $actors); + return array_map(fn ($item) => \is_string($item) ? $item : $item['id'], $actors); } return []; From d0b22b196e19ba3bd45428e927a90649e7015104 Mon Sep 17 00:00:00 2001 From: blued_gear Date: Fri, 7 Aug 2026 18:31:06 +0000 Subject: [PATCH 02/11] add useful log --- src/Controller/SearchController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/SearchController.php b/src/Controller/SearchController.php index ad9883dcf..03c1035c6 100644 --- a/src/Controller/SearchController.php +++ b/src/Controller/SearchController.php @@ -91,6 +91,7 @@ private function findObjectsByAp(string $urlOrHandle): array foreach ($result['errors'] as $error) { /** @var \Throwable $error */ + $this->logger->warning('SearchController: exception occurred while searching with ActivityPub: {e}', ['e' => $error]); $this->addFlash('error', $error->getMessage()); } From 40dd6bd16411917c9de2d67a6a5e02d7f8eadeaf Mon Sep 17 00:00:00 2001 From: blued_gear Date: Sat, 8 Aug 2026 15:54:46 +0000 Subject: [PATCH 03/11] fix skipped images of remote posts --- src/Service/ActivityPub/ApObjectExtractor.php | 5 +++-- src/Service/ActivityPub/Note.php | 2 +- src/Service/ActivityPubManager.php | 13 ++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Service/ActivityPub/ApObjectExtractor.php b/src/Service/ActivityPub/ApObjectExtractor.php index 93f27d0ba..59c0369be 100644 --- a/src/Service/ActivityPub/ApObjectExtractor.php +++ b/src/Service/ActivityPub/ApObjectExtractor.php @@ -4,6 +4,7 @@ namespace App\Service\ActivityPub; +use App\DTO\ImageDto; use App\Service\ActivityPubManager; class ApObjectExtractor @@ -42,14 +43,14 @@ public function getMarkdownBody(array $object): ?string return ''; } - public function getExternalMediaBody(array $object): ?string + public function getExternalMediaBody(array $object, ?ImageDto $consumedImage): ?string { $body = null; if (isset($object['attachment'])) { $attachments = $object['attachment']; - if ($images = $this->activityPubManager->handleExternalImages($attachments)) { + if ($images = $this->activityPubManager->handleExternalImages($attachments, $consumedImage)) { $body .= "\n\n".implode( " \n", array_map( diff --git a/src/Service/ActivityPub/Note.php b/src/Service/ActivityPub/Note.php index 63c573ab4..81ad914dd 100644 --- a/src/Service/ActivityPub/Note.php +++ b/src/Service/ActivityPub/Note.php @@ -196,7 +196,7 @@ private function createPost(array $object, bool $stickyIt = false): Post } $dto->body = $this->objectExtractor->getMarkdownBody($object); - if ($media = $this->objectExtractor->getExternalMediaBody($object)) { + if ($media = $this->objectExtractor->getExternalMediaBody($object, $dto->image)) { $dto->body .= $media; } diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php index 59e78641d..9a231f2ca 100644 --- a/src/Service/ActivityPubManager.php +++ b/src/Service/ActivityPubManager.php @@ -531,9 +531,9 @@ public function handleImages(array|string $attachment): ?Image try { $imageObject = $images[array_key_first($images)]; if (isset($imageObject['height'])) { - // determine the highest resolution image + // determine the highest resolution image for the same image (equality is ducktyped by comparing the alt text) foreach ($images as $i) { - if (isset($i['height']) && $i['height'] ?? 0 > $imageObject['height'] ?? 0) { + if (isset($i['height']) && $i['height'] ?? 0 > $imageObject['height'] ?? 0 && $i['name'] ?? '' === $imageObject['name'] ?? '') { $imageObject = $i; } } @@ -965,15 +965,14 @@ public function handleVideos(array $attachment): ?VideoDto return null; } - public function handleExternalImages(array $attachment): ?array + public function handleExternalImages(array $attachment, ?\App\DTO\ImageDto $consumedImage): ?array { + $imageUrlToSkip = $consumedImage?->sourceUrl; $images = array_filter( $attachment, - fn ($val) => $this->isImageAttachment($val) + fn ($val) => $this->isImageAttachment($val) && $val['url'] !== $imageUrlToSkip ); - array_shift($images); - if (\count($images)) { return array_map(fn ($val) => (new ImageDto())->create( $val['url'], @@ -1080,7 +1079,7 @@ public static function getReceivers(array $object): array private function isImageAttachment(array $object): bool { // attachment object has acceptable object type - if (!\in_array($object['type'], ['Document', 'Image'])) { + if (!\in_array($object['type'], ['Document', 'Image']) || !isset($object['url']) || !is_string($object['url'])) { return false; } From aff94bf9a875ba2edcdd05549296f63f568e8b21 Mon Sep 17 00:00:00 2001 From: blued_gear Date: Sat, 8 Aug 2026 15:55:11 +0000 Subject: [PATCH 04/11] please the linter --- src/Service/ActivityPubManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php index 9a231f2ca..e3175650d 100644 --- a/src/Service/ActivityPubManager.php +++ b/src/Service/ActivityPubManager.php @@ -1079,7 +1079,7 @@ public static function getReceivers(array $object): array private function isImageAttachment(array $object): bool { // attachment object has acceptable object type - if (!\in_array($object['type'], ['Document', 'Image']) || !isset($object['url']) || !is_string($object['url'])) { + if (!\in_array($object['type'], ['Document', 'Image']) || !isset($object['url']) || !\is_string($object['url'])) { return false; } From ee4d1c863cfee99ccb36d597741e0e6acc26617a Mon Sep 17 00:00:00 2001 From: blued_gear Date: Sat, 8 Aug 2026 16:51:01 +0000 Subject: [PATCH 05/11] forgot some function usages --- src/Service/ActivityPub/Note.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service/ActivityPub/Note.php b/src/Service/ActivityPub/Note.php index 81ad914dd..140f05504 100644 --- a/src/Service/ActivityPub/Note.php +++ b/src/Service/ActivityPub/Note.php @@ -140,7 +140,7 @@ private function createEntryComment(array $object, ActivityPubActivityInterface throw new UserDeletedException(); } $dto->body = $this->objectExtractor->getMarkdownBody($object); - if ($media = $this->objectExtractor->getExternalMediaBody($object)) { + if ($media = $this->objectExtractor->getExternalMediaBody($object, $dto->image)) { $dto->body .= $media; } @@ -261,7 +261,7 @@ private function createPostComment(array $object, ActivityPubActivityInterface $ throw new UserDeletedException(); } $dto->body = $this->objectExtractor->getMarkdownBody($object); - if ($media = $this->objectExtractor->getExternalMediaBody($object)) { + if ($media = $this->objectExtractor->getExternalMediaBody($object, $dto->image)) { $dto->body .= $media; } From 54bac4cd973a8404721dfdf5a3a5eb38401e3f91 Mon Sep 17 00:00:00 2001 From: blued_gear Date: Sat, 15 Aug 2026 17:43:50 +0000 Subject: [PATCH 06/11] support some more embedded video formats --- src/Service/ActivityPubManager.php | 2 +- src/Service/VideoManager.php | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php index e3175650d..7189eea0c 100644 --- a/src/Service/ActivityPubManager.php +++ b/src/Service/ActivityPubManager.php @@ -988,7 +988,7 @@ public function handleExternalVideos(array $attachment): ?array { $videos = array_filter( $attachment, - fn ($val) => \in_array($val['type'], ['Document', 'Video']) && VideoManager::isVideoUrl($val['url']) + fn ($val) => \in_array($val['type'], ['Document', 'Video']) && (VideoManager::isSupportedVideoMimeType($val['mediaType']) || VideoManager::isVideoUrl($val['url'])) ); if (\count($videos)) { diff --git a/src/Service/VideoManager.php b/src/Service/VideoManager.php index 005947425..3e1a32c27 100644 --- a/src/Service/VideoManager.php +++ b/src/Service/VideoManager.php @@ -6,7 +6,8 @@ class VideoManager { - public const VIDEO_MIMETYPES = ['video/mp4', 'video/webm']; + public const array VIDEO_MIMETYPES = ['video/mp4', 'video/webm', 'video/quicktime', 'video/ogg']; + public const array VIDEO_FILE_EXTENSIONS = ['mp4', 'webm', 'mov', 'ogv', 'ogg']; public static function isVideoUrl(string $url): bool { @@ -17,9 +18,7 @@ public static function isVideoUrl(string $url): bool $path = (string) parse_url($url, PHP_URL_PATH); $urlExt = strtolower(pathinfo($path, PATHINFO_EXTENSION)); - $types = array_map(fn ($type) => str_replace('video/', '', $type), self::VIDEO_MIMETYPES); - - return \in_array($urlExt, $types, false); + return \in_array($urlExt, self::VIDEO_FILE_EXTENSIONS, false); } public static function isSupportedVideoMimeType(?string $mimeType): bool From 76208575eb05c410dab3e28b6cd04d0653d1f843 Mon Sep 17 00:00:00 2001 From: blued_gear Date: Sat, 15 Aug 2026 18:54:39 +0000 Subject: [PATCH 07/11] fix AP 'inReplyTo' parsing --- .../ActivityPub/Inbox/ChainActivityHandler.php | 3 ++- .../ActivityPub/Inbox/CreateHandler.php | 3 ++- src/Service/ActivityPub/ApHttpClient.php | 14 ++++++++++++-- src/Service/ActivityPub/Note.php | 2 ++ src/Utils/JsonldUtils.php | 15 +++++++++++++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/MessageHandler/ActivityPub/Inbox/ChainActivityHandler.php b/src/MessageHandler/ActivityPub/Inbox/ChainActivityHandler.php index 73fa51d5e..c275de90a 100644 --- a/src/MessageHandler/ActivityPub/Inbox/ChainActivityHandler.php +++ b/src/MessageHandler/ActivityPub/Inbox/ChainActivityHandler.php @@ -26,6 +26,7 @@ use App\Service\ActivityPub\Note; use App\Service\ActivityPub\Page; use App\Service\SettingsManager; +use App\Utils\JsonldUtils; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpKernel\KernelInterface; @@ -119,7 +120,7 @@ private function retrieveObject(string $apUrl): Entry|EntryComment|Post|PostComm } if (\array_key_exists('inReplyTo', $object) && null !== $object['inReplyTo']) { - $parentUrl = \is_string($object['inReplyTo']) ? $object['inReplyTo'] : $object['inReplyTo']['id']; + $parentUrl = JsonldUtils::getApId($object['inReplyTo']); $meta = $this->repository->findByObjectId($parentUrl); if (!$meta) { $this->retrieveObject($parentUrl); diff --git a/src/MessageHandler/ActivityPub/Inbox/CreateHandler.php b/src/MessageHandler/ActivityPub/Inbox/CreateHandler.php index 7d2035816..495976d01 100644 --- a/src/MessageHandler/ActivityPub/Inbox/CreateHandler.php +++ b/src/MessageHandler/ActivityPub/Inbox/CreateHandler.php @@ -29,6 +29,7 @@ use App\Service\ActivityPub\Note; use App\Service\ActivityPub\Page; use App\Service\MessageManager; +use App\Utils\JsonldUtils; use App\Utils\UrlUtils; use Doctrine\DBAL\Exception; use Doctrine\ORM\EntityManagerInterface; @@ -134,7 +135,7 @@ public function doWork(MessageInterface $message): void private function handleChain(array $object, bool $stickyIt, ?array $fullCreatePayload): void { if (isset($object['inReplyTo']) && $object['inReplyTo']) { - $existed = $this->repository->findByObjectId($object['inReplyTo']); + $existed = $this->repository->findByObjectId(JsonldUtils::getApId($object['inReplyTo'])); if (!$existed) { $this->bus->dispatch(new ChainActivityMessage([$object])); diff --git a/src/Service/ActivityPub/ApHttpClient.php b/src/Service/ActivityPub/ApHttpClient.php index 0e60228f1..8ba61ebf3 100644 --- a/src/Service/ActivityPub/ApHttpClient.php +++ b/src/Service/ActivityPub/ApHttpClient.php @@ -46,8 +46,13 @@ enum ApRequestType class ApHttpClient implements ApHttpClientInterface { - public const TIMEOUT = 8; - public const MAX_DURATION = 15; + public const int TIMEOUT = 8; + public const int MAX_DURATION = 15; + + /** + * useful when running a local dev instance as it can be treated as invalid. + */ + private const bool SKIP_HTTP_SIGNATURE = false; public function __construct( private readonly string $kbinDomain, @@ -699,6 +704,11 @@ private function getInstanceHeaders(string $url, ?array $body = null, string $me $headers['User-Agent'] = $this->projectInfo->getUserAgent(); $headers = array_merge($headers, $this->getFetchAcceptHeaders($requestType)); + if (self::SKIP_HTTP_SIGNATURE) { + unset($headers['Signature']); + unset($headers['Date']); + } + return $headers; } diff --git a/src/Service/ActivityPub/Note.php b/src/Service/ActivityPub/Note.php index 140f05504..5c13e9962 100644 --- a/src/Service/ActivityPub/Note.php +++ b/src/Service/ActivityPub/Note.php @@ -27,6 +27,7 @@ use App\Service\PostCommentManager; use App\Service\PostManager; use App\Service\SettingsManager; +use App\Utils\JsonldUtils; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; @@ -79,6 +80,7 @@ public function create(array $object, ?array $root = null, bool $stickyIt = fals if (isset($object['inReplyTo']) && $replyTo = $object['inReplyTo']) { // Create post or entry comment + $replyTo = JsonldUtils::getApId($replyTo); $parentObjectId = $this->repository->findByObjectId($replyTo); $parent = $this->entityManager->getRepository($parentObjectId['type'])->find((int) $parentObjectId['id']); diff --git a/src/Utils/JsonldUtils.php b/src/Utils/JsonldUtils.php index 54364f79f..c19915ccc 100644 --- a/src/Utils/JsonldUtils.php +++ b/src/Utils/JsonldUtils.php @@ -17,4 +17,19 @@ public static function getArrayValue(array $object, string $key): array return [$object[$key]]; } + + public static function getApId(string|array $value): string + { + if (\is_string($value)) { + return $value; + } elseif (\is_array($value)) { + $value = $value['id'] ?? null; + if (!\is_string($value)) { + throw new \LogicException('JsonldUtils::getApId(): value is array but did not contain valid `id` element'); + } + return $value; + } else { + throw new \LogicException('JsonldUtils::getApId(): value is neither a string nor an array'); + } + } } From 94edc1d8cab50fd2cce8888b627b7860da30e696 Mon Sep 17 00:00:00 2001 From: blued_gear Date: Tue, 25 Aug 2026 21:15:18 +0000 Subject: [PATCH 08/11] fix usage of wrong AP username in MentionLinkParser --- src/Markdown/CommonMark/MentionLinkParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markdown/CommonMark/MentionLinkParser.php b/src/Markdown/CommonMark/MentionLinkParser.php index 1136f85bc..2787b6af9 100644 --- a/src/Markdown/CommonMark/MentionLinkParser.php +++ b/src/Markdown/CommonMark/MentionLinkParser.php @@ -56,7 +56,7 @@ public function parse(InlineParserContext $ctx): bool $data->apPublicUrl, '@'.$username, '@'.$data->apId, - '@'.$data->apId, + $data->username, MentionType::RemoteUser, ) ); From ec06377930e0ee91649fa6689b9032fe0947a8bb Mon Sep 17 00:00:00 2001 From: blued_gear Date: Wed, 9 Sep 2026 18:49:16 +0000 Subject: [PATCH 09/11] operator precedence --- src/Service/ActivityPubManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php index 7189eea0c..ef220313d 100644 --- a/src/Service/ActivityPubManager.php +++ b/src/Service/ActivityPubManager.php @@ -533,7 +533,7 @@ public function handleImages(array|string $attachment): ?Image if (isset($imageObject['height'])) { // determine the highest resolution image for the same image (equality is ducktyped by comparing the alt text) foreach ($images as $i) { - if (isset($i['height']) && $i['height'] ?? 0 > $imageObject['height'] ?? 0 && $i['name'] ?? '' === $imageObject['name'] ?? '') { + if (isset($i['height']) && $i['height'] > ($imageObject['height'] ?? 0) && ($i['name'] ?? '') === ($imageObject['name'] ?? '')) { $imageObject = $i; } } From c9a6b6f3a111b7c525db8fea5a8c95f2c3c8195b Mon Sep 17 00:00:00 2001 From: blued_gear Date: Wed, 9 Sep 2026 19:38:32 +0000 Subject: [PATCH 10/11] add ActivityPubManagerTest and fix ActivityPubManager::handleImages() --- src/Service/ActivityPubManager.php | 14 +- .../ActivityPub/ActivityPubManagerTest.php | 195 ++++++++++++++++++ 2 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Service/ActivityPub/ActivityPubManagerTest.php diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php index ef220313d..69fc74f68 100644 --- a/src/Service/ActivityPubManager.php +++ b/src/Service/ActivityPubManager.php @@ -507,21 +507,31 @@ private function updateUser(string $actorUrl): ?User public function handleImages(array|string $attachment): ?Image { - if (\is_string($attachment) && filter_var($attachment, FILTER_VALIDATE_URL)) { + if (\is_string($attachment)) { + if (!filter_var($attachment, FILTER_VALIDATE_URL)) { + return null; + } + $path = parse_url($attachment, PHP_URL_PATH); - $query = parse_url($attachment, PHP_URL_QUERY); + $query = parse_url($attachment, PHP_URL_QUERY) ?? ''; $attachment = [ [ 'url' => $attachment, 'type' => 'Image', ], ]; + + if (null === $path) { + return null; + } + if (str_contains($path, 'jpg') || str_contains($path, 'jpeg') || str_contains($query, 'jpg') || str_contains($query, 'jpeg')) { $attachment[0]['mediaType'] = 'image/jpeg'; } elseif (str_contains($path, 'png') || str_contains($query, 'png')) { $attachment[0]['mediaType'] = 'image/png'; } } + $images = array_filter( $attachment, fn ($val) => $this->isImageAttachment($val) diff --git a/tests/Unit/Service/ActivityPub/ActivityPubManagerTest.php b/tests/Unit/Service/ActivityPub/ActivityPubManagerTest.php new file mode 100644 index 000000000..fbe8d2a2f --- /dev/null +++ b/tests/Unit/Service/ActivityPub/ActivityPubManagerTest.php @@ -0,0 +1,195 @@ + 'Image', + 'url' => 'https://example.com/image1.jpg', + ], + ]; + + $imageManager = $this->createStub(ImageManager::class); + $imageRepository = $this->createStub(ImageRepository::class); + $imageManager->method('download')->willReturnArgument(0); + $imageRepository->method('findOrCreateFromPath')->with('https://example.com/image1.jpg')->willReturn( + new Image('success.jpg', '', '0000000000000000000000000000000000000000000000000000000000000000', 1, 1, '') + ); + + $mng = $this->initiateMocked(ActivityPubManager::class, ['imageManager' => $imageManager, 'imageRepository' => $imageRepository]); + $img = $mng->handleImages($attachment); + + self::assertNotNull($img); + self::assertSame('success.jpg', $img->fileName); + } + + public function testHandleImagesFindsBestImageOfMultiAscResolution() + { + $attachment = [ + [ + 'type' => 'Image', + 'name' => 'A', + 'url' => 'https://example.com/image-a-1.jpg', + 'height' => 100, + 'width' => 101, + ], + [ + 'type' => 'Image', + 'name' => 'B', + 'url' => 'https://example.com/image-b-1.jpg', + 'height' => 200, + 'width' => 201, + ], + [ + 'type' => 'Image', + 'name' => 'A', + 'url' => 'https://example.com/image-a-2.jpg', + 'height' => 110, + 'width' => 111, + ], + ]; + + $imageManager = $this->createStub(ImageManager::class); + $imageRepository = $this->createStub(ImageRepository::class); + $imageManager->method('download')->willReturnArgument(0); + $imageRepository->method('findOrCreateFromPath')->with('https://example.com/image-a-2.jpg')->willReturn( + new Image('success.jpg', '', '0000000000000000000000000000000000000000000000000000000000000000', 1, 1, '') + ); + + $mng = $this->initiateMocked(ActivityPubManager::class, ['imageManager' => $imageManager, 'imageRepository' => $imageRepository]); + $img = $mng->handleImages($attachment); + + self::assertNotNull($img); + self::assertSame('success.jpg', $img->fileName); + } + + public function testHandleImagesFindsBestImageOfMultiDescResolution() + { + $attachment = [ + [ + 'type' => 'Image', + 'name' => 'A', + 'url' => 'https://example.com/image-a-1.jpg', + 'height' => 110, + 'width' => 111, + ], + [ + 'type' => 'Image', + 'name' => 'B', + 'url' => 'https://example.com/image-b-1.jpg', + 'height' => 200, + 'width' => 201, + ], + [ + 'type' => 'Image', + 'name' => 'A', + 'url' => 'https://example.com/image-a-2.jpg', + 'height' => 100, + 'width' => 101, + ], + ]; + + $imageManager = $this->createStub(ImageManager::class); + $imageRepository = $this->createStub(ImageRepository::class); + $imageManager->method('download')->willReturnArgument(0); + $imageRepository->method('findOrCreateFromPath')->with('https://example.com/image-a-1.jpg')->willReturn( + new Image('success.jpg', '', '0000000000000000000000000000000000000000000000000000000000000000', 1, 1, '') + ); + + $mng = $this->initiateMocked(ActivityPubManager::class, ['imageManager' => $imageManager, 'imageRepository' => $imageRepository]); + $img = $mng->handleImages($attachment); + + self::assertNotNull($img); + self::assertSame('success.jpg', $img->fileName); + } + + public function testHandleImagesIgnoresNonImage() + { + $attachment = [ + [ + 'type' => 'Other', + 'url' => 'https://example.com/document.pdf', + ], + ]; + + $imageManager = $this->createStub(ImageManager::class); + $imageRepository = $this->createStub(ImageRepository::class); + $imageManager->method('download')->willReturnArgument(0); + $imageRepository->method('findOrCreateFromPath')->willReturn( + new Image('success.jpg', '', '0000000000000000000000000000000000000000000000000000000000000000', 1, 1, '') + ); + + $mng = $this->initiateMocked(ActivityPubManager::class, ['imageManager' => $imageManager, 'imageRepository' => $imageRepository]); + $img = $mng->handleImages($attachment); + + self::assertNull($img); + } + + public function testHandleImagesHandlesValidString() + { + $attachment = 'https://example.com/image.png'; + + $imageManager = $this->createStub(ImageManager::class); + $imageRepository = $this->createStub(ImageRepository::class); + $imageManager->method('download')->willReturnArgument(0); + $imageRepository->method('findOrCreateFromPath')->with('https://example.com/image.png')->willReturn( + new Image('success.jpg', '', '0000000000000000000000000000000000000000000000000000000000000000', 1, 1, '') + ); + + $mng = $this->initiateMocked(ActivityPubManager::class, ['imageManager' => $imageManager, 'imageRepository' => $imageRepository]); + $img = $mng->handleImages($attachment); + + self::assertNotNull($img); + self::assertSame('success.jpg', $img->fileName); + } + + public function testHandleImagesHandlesInvalidString() + { + $attachment = 'image.png'; + + $imageManager = $this->createStub(ImageManager::class); + $imageRepository = $this->createStub(ImageRepository::class); + $imageManager->method('download')->willReturnArgument(0); + $imageRepository->method('findOrCreateFromPath')->with('image.png')->willReturn( + new Image('dummy.jpg', '', '0000000000000000000000000000000000000000000000000000000000000000', 1, 1, '') + ); + + $mng = $this->initiateMocked(ActivityPubManager::class, ['imageManager' => $imageManager, 'imageRepository' => $imageRepository]); + $img = $mng->handleImages($attachment); + + self::assertNull($img); + } + + /** + * @template T + * + * @param class-string $class + * @param array $mocks + * + * @return T + */ + private function initiateMocked(string $class, array $mocks = []): object + { + $reflect = new \ReflectionClass($class); + $constructor = $reflect->getConstructor(); + + $consParams = []; + foreach ($constructor->getParameters() as $param) { + $mock = $mocks[$param->getName()] ?? $this->createMock($param->getType()->getName()); + $consParams[] = $mock; + } + + return $reflect->newInstanceArgs($consParams); + } +} From 803dfe83f0daefdb5f7ac417037a6a8762d8b8df Mon Sep 17 00:00:00 2001 From: blued_gear Date: Thu, 10 Sep 2026 22:02:01 +0000 Subject: [PATCH 11/11] filter out consumed images of same content --- src/Service/ActivityPubManager.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php index 69fc74f68..fc16dd719 100644 --- a/src/Service/ActivityPubManager.php +++ b/src/Service/ActivityPubManager.php @@ -977,10 +977,9 @@ public function handleVideos(array $attachment): ?VideoDto public function handleExternalImages(array $attachment, ?\App\DTO\ImageDto $consumedImage): ?array { - $imageUrlToSkip = $consumedImage?->sourceUrl; $images = array_filter( $attachment, - fn ($val) => $this->isImageAttachment($val) && $val['url'] !== $imageUrlToSkip + fn ($val) => $this->isImageAttachment($val) && !$this->describeSameImage($val, $consumedImage) ); if (\count($images)) { @@ -1100,6 +1099,21 @@ private function isImageAttachment(array $object): bool || ImageManager::isImageUrl($object['url']); } + private function describeSameImage(array $imgA, ?\App\DTO\ImageDto $imgB): bool + { + if (null === $imgB) { + return false; + } + + if ($imgA['url'] === $imgB->sourceUrl) { + return true; + } + + $altTextA = $imgA['name'] ?? null; + $altTextB = $imgB->altText ?? null; + return null !== $altTextA && '' !== $altTextA && $altTextA === $altTextB; + } + /** * @param string|array $apObject the object that should be like, so a post of any kind in its AP array representation or a URL * @param array $fullPayload the full message payload, only used to log it