social media crossposting tool. 3rd time's the charm
mastodon
misskey
crossposting
bluesky
1from abc import ABC, abstractmethod
2from typing import Any, override
3
4from atproto.identity import did_resolver, handle_resolver
5from cross.service import Service
6from util.util import normalize_service_url
7
8SERVICE = "https://bsky.app"
9
10
11def validate_and_transform(data: dict[str, Any]):
12 if not data["handle"] and not data["did"]:
13 raise KeyError("no 'handle' or 'did' specified for bluesky input!")
14
15 if "did" in data:
16 did = str(data["did"]) # only did:web and did:plc are supported
17 if not did.startswith("did:plc:") and not did.startswith("did:web:"):
18 raise ValueError(f"Invalid handle {did}!")
19
20 if "pds" in data:
21 data["pds"] = normalize_service_url(data["pds"])
22
23
24class BlueskyService(ABC, Service):
25 pds: str
26 did: str
27
28 def _init_identity(self) -> None:
29 handle, did, pds = self.get_identity_options()
30
31 if did and pds:
32 self.did = did
33 self.pds = pds
34 return
35
36 if not did:
37 if not handle:
38 raise KeyError("No did: or atproto handle provided!")
39 self.log.info("Resolving ATP identity for %s...", handle)
40 self.did = handle_resolver.resolve_handle(handle)
41
42 if not pds:
43 self.log.info("Resolving PDS from %s DID document...", did)
44 atp_pds = did_resolver.resolve_did(self.did).get_atproto_pds()
45 if not atp_pds:
46 raise Exception("Failed to resolve atproto pds for %s")
47 self.pds = atp_pds
48
49 @abstractmethod
50 def get_identity_options(self) -> tuple[str | None, str | None, str | None]:
51 pass