diff --git a/activitypub.py b/activitypub.py index 52e0e2c..ad11608 100644 --- a/activitypub.py +++ b/activitypub.py @@ -54,6 +54,8 @@ _INSTANCE_ACTOR = None # populated in User.status WEB_OPT_OUT_DOMAINS = None +FEDI_URL_RE = re.compile(r'https://[^/]+/(@|users/)([^/@]+)(@[^/@]+)?(/(?:statuses/)?[0-9]+)?') + def instance_actor(): global _INSTANCE_ACTOR diff --git a/atproto.py b/atproto.py index 9bd3f91..536ffd4 100644 --- a/atproto.py +++ b/atproto.py @@ -47,6 +47,14 @@ appview = Client(f'https://{os.environ["APPVIEW_HOST"]}', headers={'User-Agent': USER_AGENT}) LEXICONS = appview.defs +# https://atproto.com/guides/applications#record-types +COLLECTION_TO_TYPE = { + 'app.bsky.actor.profile': 'profile', + 'app.bsky.feed.like': 'like', + 'app.bsky.feed.post': 'post', + 'app.bsky.feed.repost': 'repost', + 'app.bsky.graph.follow': 'follow', +} DNS_GCP_PROJECT = 'brid-gy' DNS_ZONE = 'brid-gy' @@ -55,6 +63,22 @@ logger.info(f'Using GCP DNS project {DNS_GCP_PROJECT} zone {DNS_ZONE}') dns_client = dns.Client(project=DNS_GCP_PROJECT) +def did_to_handle(did): + """Resolves a DID to a handle _if_ we have the DID doc stored locally. + + Args: + did (str) + + Returns: + str: handle, or None + """ + if did_obj := ATProto.load(did, did_doc=True): + if aka := util.get_first(did_obj.raw, 'alsoKnownAs', ''): + handle, _, _ = parse_at_uri(aka) + if handle: + return handle + + class ATProto(User, Protocol): """AT Protocol class. @@ -86,11 +110,7 @@ class ATProto(User, Protocol): @ndb.ComputedProperty def handle(self): """Returns handle if the DID document includes one, otherwise None.""" - if did_obj := ATProto.load(self.key.id(), did_doc=True): - if aka := util.get_first(did_obj.raw, 'alsoKnownAs', ''): - handle, _, _ = parse_at_uri(aka) - if handle: - return handle + return did_to_handle(self.key.id()) def web_url(self): return bluesky.Bluesky.user_url(self.handle_or_id()) diff --git a/models.py b/models.py index 9c5ff31..d6163ca 100644 --- a/models.py +++ b/models.py @@ -15,7 +15,7 @@ from Crypto.PublicKey import RSA from flask import g, request from google.cloud import ndb from granary import as1, as2, atom, bluesky, microformats2 -from granary.bluesky import BSKY_APP_URL_RE +from granary.bluesky import AT_URI_PATTERN, BSKY_APP_URL_RE from granary.source import html_to_text from oauth_dropins.webutil import util from oauth_dropins.webutil.appengine_info import DEBUG @@ -1272,23 +1272,32 @@ def fetch_objects(query, by=None, user=None): 'url': id, }) elif url: - # heuristics for sniffing Mastodon and similar fediverse URLs and - # converting them to more friendly @-names + # heuristics for sniffing URLs and converting them to more friendly + # phrases and user handles. # TODO: standardize this into granary.as2 somewhere? if not content: - fedi_url = re.match( - r'https://[^/]+/(@|users/)([^/@]+)(@[^/@]+)?(/(?:statuses/)?[0-9]+)?', url) - if fedi_url: - content = '@' + fedi_url.group(2) - if fedi_url.group(4): - content += "'s post" + from activitypub import FEDI_URL_RE + from atproto import COLLECTION_TO_TYPE, did_to_handle - if not content: - if bsky_url := BSKY_APP_URL_RE.match(url): - if handle := bsky_url.group('id'): # or DID - content = '@' + handle - if bsky_url.group('tid'): - content += "'s post" + if match := FEDI_URL_RE.match(url): + content = '@' + match.group(2) + if match.group(4): + content += "'s post" + elif match := BSKY_APP_URL_RE.match(url): + id = match.group('id') + if id.startswith('did:'): + id = ATdid_to_handle(id) or id + content = '@' + id + if match.group('tid'): + content += "'s post" + elif match := AT_URI_PATTERN.match(url): + id = match.group('repo') + if id.startswith('did:'): + id = did_to_handle(id) or id + content = '@' + id + if coll := match.group('collection'): + content += f"'s {COLLECTION_TO_TYPE.get(coll) or 'post'}" + url = bluesky.at_uri_to_web_url(url) content = common.pretty_link(url, text=content, user=user)