Skip to content

Instantly share code, notes, and snippets.

@davepeck
Last active November 24, 2024 19:09
Show Gist options
  • Save davepeck/d90977ed0ec06640dd3ebf64ac059fb5 to your computer and use it in GitHub Desktop.
Save davepeck/d90977ed0ec06640dd3ebf64ac059fb5 to your computer and use it in GitHub Desktop.
A Tiny Python script to convert Bluesky handles to DIDs, and DIDs to handles

A command-line script to convert between Bluesky DIDs and handles

This small Python script makes it easy to convert from ATProto DIDs to their associated handles and back again:

$ ./resolve.py handle @davepeck.org
did:plc:caznr5mgvjft4mu4p2vpttfx
$ ./resolve.py did did:plc:caznr5mgvjft4mu4p2vpttfx
davepeck.org

It's easy to use! Just install Astral's UV.

Then you can run it as:

$ uv run https://gist.githubusercontent.com/davepeck/d90977ed0ec06640dd3ebf64ac059fb5/raw/807d7e2d34fe47938134ed77517ade5ae58947a4/resolve.py handle @davepeck.org
did:plc:caznr5mgvjft4mu4p2vpttfx

Or, you can download the script locally and run it as:

$ ./resolve.py handle @davepeck.org
did:plc:caznr5mgvjft4mu4p2vpttfx

I've written detailed notes on how this script works as well as a other notes about Bluesky and Python.

#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment