From 94800fdb2be819e4c487d1e5bee40c28f3e30f0f Mon Sep 17 00:00:00 2001 From: zenfyr Date: Fri, 7 Aug 2026 19:21:44 +0700 Subject: [PATCH] bluesky: add gallery support to output. shouuuld work fine for now? tests with the test account did not fail. --- atproto/models.py | 8 +++++ bluesky/client.py | 18 ++++++++---- bluesky/output.py | 75 ++++++++++++++++++++++++++--------------------- 3 files changed, 62 insertions(+), 39 deletions(-) diff --git a/atproto/models.py b/atproto/models.py index f11d9bc..79b152d 100644 --- a/atproto/models.py +++ b/atproto/models.py @@ -143,6 +143,14 @@ class ImageEmbed: return data +@dataclass(kw_only=True) +class GalleryImage(ImageEmbed): + def to_dict(self, blob_ref: dict[str, Any]) -> dict[str, Any]: + result = super().to_dict(blob_ref) + result["$type"] = "app.bsky.embed.gallery#image" + return result + + @dataclass(kw_only=True) class VideoEmbed: video: bytes diff --git a/bluesky/client.py b/bluesky/client.py index 0385785..d441ec2 100644 --- a/bluesky/client.py +++ b/bluesky/client.py @@ -100,15 +100,23 @@ class BlueskyClient: time_iso: str | None = None, ) -> CreateRecordResponse: image_refs: list[dict[str, Any]] = [] - for img in images[:4]: + for img in images: blob_ref = self._upload_blob(img.image, "image/jpeg") image_data = img.to_dict(blob_ref["blob"]) image_refs.append(image_data) - image_embed: dict[str, Any] = { - "$type": "app.bsky.embed.images", - "images": image_refs, - } + image_embed: dict[str, Any] + + if len(image_refs) > 4: + image_embed = { + "$type": "app.bsky.embed.gallery", + "items": image_refs, + } + else: + image_embed = { + "$type": "app.bsky.embed.images", + "images": image_refs, + } if embed: combined_embed: dict[str, Any] = { diff --git a/bluesky/output.py b/bluesky/output.py index da53264..7246f26 100644 --- a/bluesky/output.py +++ b/bluesky/output.py @@ -1,7 +1,7 @@ import json import re -from dataclasses import dataclass -from typing import Any, override +from dataclasses import asdict, dataclass +from typing import Any, Literal, TypedDict, override import grapheme import httpx @@ -9,6 +9,7 @@ import httpx import misskey.mfm as mfm from atproto.models import ( Facet, + GalleryImage, ImageEmbed, RecordEmbed, ReplyRef, @@ -37,6 +38,15 @@ from database.connection import DatabasePool from util.splitter import TokenSplitter +AttachmentType = Literal["none"] | Literal["image"] | Literal["video"] + + +class PostWithAattachment(TypedDict): + type: AttachmentType + tokens: list[Any] + attachments: list[Blob] + + ALLOWED_GATES: list[str] = ["mentioned", "following", "followers"] ADULT_PATTERN = re.compile(r"\b(adult|sexual|nsfw)\b", re.IGNORECASE) @@ -127,15 +137,16 @@ class BlueskyOutputService(BlueskyService, OutputService): self, token_blocks: list[list[Any]], media: list[Blob], - ) -> list[tuple[list[Any], list[Blob]]]: - posts: list[dict[str, Any]] = [ - {"tokens": block, "attachments": []} for block in token_blocks + ) -> list[PostWithAattachment]: + posts: list[PostWithAattachment] = [ + {"type": "none", "tokens": block, "attachments": []} + for block in token_blocks ] available_indices: list[int] = list(range(len(posts))) current_image_post_idx: int | None = None - def make_blank_post() -> dict[str, Any]: - return {"tokens": [], "attachments": []} + def make_blank_post() -> PostWithAattachment: + return {"type": "none", "tokens": [], "attachments": []} def pop_next_empty_index() -> int: if available_indices: @@ -149,22 +160,21 @@ class BlueskyOutputService(BlueskyService, OutputService): current_image_post_idx = None idx = pop_next_empty_index() posts[idx]["attachments"].append(blob) + posts[idx]["type"] = "video" elif blob.mime.startswith("image/"): if ( current_image_post_idx is not None - and len(posts[current_image_post_idx]["attachments"]) < 4 + and len(posts[current_image_post_idx]["attachments"]) < 10 ): posts[current_image_post_idx]["attachments"].append(blob) + posts[current_image_post_idx]["type"] = "image" else: idx = pop_next_empty_index() posts[idx]["attachments"].append(blob) + posts[idx]["type"] = "image" current_image_post_idx = idx - result: list[tuple[list[Any], list[Blob]]] = [] - for p in posts: - result.append((p["tokens"], p["attachments"])) - - return result + return posts def _build_labels( self, @@ -390,8 +400,8 @@ class BlueskyOutputService(BlueskyService, OutputService): richtext_index = 0 - for i, (block_tokens, attachments) in enumerate(baked_media): - if block_tokens and richtext_index < len(precomputed_richtexts): + for i, pwa in enumerate(baked_media): + if pwa["tokens"] and richtext_index < len(precomputed_richtexts): text, facets = precomputed_richtexts[richtext_index] richtext_index += 1 else: @@ -407,16 +417,11 @@ class BlueskyOutputService(BlueskyService, OutputService): embed: dict[str, Any] | None = None if i == 0 and quoted_uri and quoted_cid: - if attachments and attachments[0].mime.startswith("image/"): - embed = RecordEmbed( - record=StrongRef(uri=quoted_uri, cid=quoted_cid) - ).to_dict() - else: - embed = RecordEmbed( - record=StrongRef(uri=quoted_uri, cid=quoted_cid) - ).to_dict() + embed = RecordEmbed( + record=StrongRef(uri=quoted_uri, cid=quoted_cid) + ).to_dict() - if not attachments: + if not pwa["attachments"] or pwa["type"] == "none": response = self._client.send_post( text=text or " ", facets=facets or None, @@ -425,11 +430,12 @@ class BlueskyOutputService(BlueskyService, OutputService): labels=labels, langs=langs, ) - elif attachments[0].mime.startswith("image/"): + elif pwa["type"] == "image": images: list[ImageEmbed] = [] - for img_blob in attachments[:4]: + images_len: int = len(pwa["attachments"]) + for img_blob in pwa["attachments"]: image_io = img_blob.io - if len(image_io) > 1_000_000: + if len(image_io) > 2_000_000: self.log.info("Compressing %s...", img_blob.name or "image") compressed = compress_image(img_blob) image_io = compressed.io @@ -441,13 +447,14 @@ class BlueskyOutputService(BlueskyService, OutputService): self.log.error(e) aspect_ratio = None - images.append( - ImageEmbed( - image=image_io, - alt=img_blob.alt, - aspect_ratio=aspect_ratio, - ) + final_embed = ImageEmbed( + image=image_io, + alt=img_blob.alt, + aspect_ratio=aspect_ratio, ) + if images_len > 4: + final_embed = GalleryImage(**asdict(final_embed)) + images.append(final_embed) response = self._client.send_images( text=text or "", @@ -459,7 +466,7 @@ class BlueskyOutputService(BlueskyService, OutputService): langs=langs, ) else: - video_blob = attachments[0] + video_blob = pwa["attachments"][0] video_io = video_blob.io if video_blob.mime != "video/mp4": -- 2.51.2