|
#!/usr/bin/env -S uv run -q |
|
# /// script |
|
# requires-python = ">=3.13" |
|
# dependencies = [ |
|
# "click", |
|
# "dnspython", |
|
# "httpx", |
|
# ] |
|
# /// |
|
|
|
# Install Astral's UV tool and then run this script with: |
|
# ./resolve.py handle @davepeck.org |
|
# or |
|
# ./resolve.py did did:plc:caznr5mgvjft4mu4p2vpttfx |
|
|
|
import sys |
|
|
|
import click |
|
import dns.resolver |
|
import httpx |
|
|
|
|
|
@click.group() |
|
def resolve(): |
|
pass |
|
|
|
|
|
def remove_prefix(handle: str) -> str: |
|
"""Returns a raw ATProto handle, without the @ prefix.""" |
|
return handle[1:] if handle.startswith("@") else handle |
|
|
|
|
|
def resolve_handle_dns(handle: str) -> str | None: |
|
""" |
|
Resolves an ATProto handle to a DID using DNS. |
|
|
|
Returns None if the handle is not found. |
|
|
|
May raise an exception if network requests fail unexpectedly. |
|
""" |
|
try: |
|
handle = remove_prefix(handle) |
|
answers = dns.resolver.resolve(f"_atproto.{handle}", "TXT") |
|
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): |
|
return None |
|
|
|
for answer in answers: |
|
txt = answer.to_text() |
|
if txt.startswith('"did='): |
|
return txt[5:-1] |
|
|
|
return None |
|
|
|
|
|
def resolve_handle_well_known(handle: str, timeout: float = 5.0) -> str | None: |
|
""" |
|
Resolves an ATProto handle to a DID using a well-known endpoint. |
|
|
|
Returns None if the handle is not found. |
|
|
|
Raises exceptions if network requests fail unexpectedly. |
|
""" |
|
import httpx |
|
|
|
try: |
|
handle = remove_prefix(handle) |
|
response = httpx.get( |
|
f"https://{handle}/.well-known/atproto-did", timeout=timeout |
|
) |
|
response.raise_for_status() |
|
except httpx.ConnectError: |
|
return None |
|
|
|
return response.text.strip() |
|
|
|
|
|
def resolve_handle(handle: str) -> str | None: |
|
""" |
|
Resolves an ATProto handle, like @bsky.app, to a DID. |
|
|
|
We resolve as follows: |
|
|
|
1. Check the _atproto DNS TXT record for the handle. |
|
2. If not found, query for a .well-known/atproto-did |
|
|
|
Returns None if the handle is not found. |
|
|
|
Raises exceptions if network requests fail. |
|
""" |
|
maybe_did = resolve_handle_dns(handle) |
|
maybe_did = maybe_did or resolve_handle_well_known(handle) |
|
return maybe_did |
|
|
|
|
|
@resolve.command() |
|
@click.argument("handle") |
|
def handle(handle: str): |
|
""" |
|
Resolves a Bluesky handle to a DID, printing the DID if found. |
|
""" |
|
did = resolve_handle(handle) |
|
if did: |
|
print(did) |
|
else: |
|
print(f"{handle} not found", file=sys.stderr) |
|
|
|
|
|
def resolve_did_web(did: str) -> str: |
|
"""Resolve a web DID to an unprefixed Bluesky handle.""" |
|
if not did.startswith("did:web:"): |
|
raise ValueError("Unexpected web DID format") |
|
return did[len("did:web:") :] |
|
|
|
|
|
def get_did_plc_data(did: str, timeout: float = 5.0) -> dict | None: |
|
""" |
|
Returns a collection of public data about a PLC DID. |
|
|
|
Returns None if the DID is not found. |
|
|
|
Raises exceptions if network requests fail. |
|
""" |
|
response = httpx.get(f"https://plc.directory/{did}", timeout=timeout) |
|
if response.status_code == 404: |
|
return None |
|
response.raise_for_status() |
|
return response.json() |
|
|
|
|
|
def resolve_did_plc(did: str, timeout: float = 5.0) -> str | None: |
|
"""Resolve a PLC DID to an unprefixed Bluesky handle.""" |
|
data = get_did_plc_data(did, timeout=timeout) |
|
if not data: |
|
return None |
|
akas = data.get("alsoKnownAs") |
|
if not akas or not isinstance(akas, list): |
|
raise ValueError("Unexpected PLC DID data") |
|
aka = akas[0] |
|
if not isinstance(aka, str): |
|
raise ValueError("Unexpected PLC DID data") |
|
if not aka.startswith("at://"): |
|
raise ValueError("Unexpected PLC DID data") |
|
return aka[len("at://") :] |
|
|
|
|
|
def resolve_did(did: str, timeout: float = 5.0) -> str | None: |
|
""" |
|
Resolves a DID to an unprefixed Bluesky handle, like `davepeck.org`. |
|
|
|
We resolve as follows: |
|
|
|
1. Check for a did:web DID. |
|
2. If not found, query the PLC directory for a did:plc DID. |
|
|
|
Returns None if the DID cannot be resolved. |
|
|
|
Raises exceptions if the DID is invalid or network requests fail. |
|
""" |
|
if did.startswith("did:web:"): |
|
return resolve_did_web(did) |
|
elif did.startswith("did:plc:"): |
|
return resolve_did_plc(did) |
|
else: |
|
raise ValueError("Unsupported DID method") |
|
|
|
|
|
@resolve.command() |
|
@click.argument("did") |
|
def did(did: str): |
|
""" |
|
Resolves a DID to a Bluesky handle, printing the handle if found. |
|
""" |
|
handle = resolve_did(did) |
|
if handle: |
|
print(handle) |
|
else: |
|
print(f"{did} not found", file=sys.stderr) |
|
|
|
|
|
if __name__ == "__main__": |
|
resolve() |