2023-10-26 20:49:42 +00:00
|
|
|
"""Translates user ids, handles, and object ids between protocols.
|
2023-09-21 20:37:17 +00:00
|
|
|
|
|
|
|
https://fed.brid.gy/docs#translate
|
|
|
|
"""
|
2023-10-26 23:20:30 +00:00
|
|
|
import logging
|
2023-09-22 18:41:30 +00:00
|
|
|
import re
|
2023-11-03 18:37:36 +00:00
|
|
|
from urllib.parse import urljoin, urlparse
|
2023-09-22 18:41:30 +00:00
|
|
|
|
2023-11-03 18:37:36 +00:00
|
|
|
from flask import request
|
2023-12-24 18:04:01 +00:00
|
|
|
from google.cloud.ndb.query import FilterNode, Query
|
2024-04-09 17:04:16 +00:00
|
|
|
from granary.bluesky import BSKY_APP_URL_RE, web_url_to_at_uri
|
2023-12-24 18:20:04 +00:00
|
|
|
from oauth_dropins.webutil import util
|
2023-11-03 18:37:36 +00:00
|
|
|
|
2024-04-22 20:24:24 +00:00
|
|
|
from common import (
|
|
|
|
LOCAL_DOMAINS,
|
|
|
|
PRIMARY_DOMAIN,
|
|
|
|
PROTOCOL_DOMAINS,
|
|
|
|
subdomain_wrap,
|
|
|
|
SUPERDOMAIN,
|
|
|
|
)
|
2023-10-26 23:00:03 +00:00
|
|
|
import models
|
2023-09-21 20:37:17 +00:00
|
|
|
|
2023-10-26 23:20:30 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2023-11-07 04:17:23 +00:00
|
|
|
# Protocols to check User.copies and Object.copies before translating
|
2024-04-21 19:18:12 +00:00
|
|
|
COPIES_PROTOCOLS = ('atproto',)
|
2023-11-07 04:17:23 +00:00
|
|
|
|
2023-12-24 18:04:01 +00:00
|
|
|
# Web user domains whose AP actor ids are on fed.brid.gy, not web.brid.gy, for
|
2023-12-24 18:20:04 +00:00
|
|
|
# historical compatibility. Loaded on first call to web_ap_subdomain().
|
2024-04-22 18:12:03 +00:00
|
|
|
#
|
|
|
|
# Maps string domain to string subdomain (bsky, fed, or web).
|
|
|
|
_NON_WEB_SUBDOMAIN_SITES = None
|
2023-12-24 18:04:01 +00:00
|
|
|
|
2023-12-24 18:20:04 +00:00
|
|
|
|
|
|
|
def web_ap_base_domain(user_domain):
|
2024-04-09 16:48:16 +00:00
|
|
|
"""Returns the full Bridgy Fed domain to use for a given Web user.
|
2023-12-24 18:20:04 +00:00
|
|
|
|
|
|
|
Specifically, returns ``http://localhost/` if we're running locally,
|
|
|
|
``https://fed.brid.gy/`` if the given Web user has ``ap_subdomain='fed'``,
|
|
|
|
otherwise ``https://web.brid.gy/``.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
user_domain (str)
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
str:
|
|
|
|
"""
|
|
|
|
if request.host in LOCAL_DOMAINS:
|
|
|
|
return request.host_url
|
|
|
|
|
2024-04-22 18:12:03 +00:00
|
|
|
global _NON_WEB_SUBDOMAIN_SITES
|
|
|
|
if _NON_WEB_SUBDOMAIN_SITES is None:
|
|
|
|
_NON_WEB_SUBDOMAIN_SITES = {
|
|
|
|
user.key.id(): user.ap_subdomain
|
2024-04-22 22:15:27 +00:00
|
|
|
for user in Query('MagicKey',
|
|
|
|
filters=FilterNode('ap_subdomain', '!=', 'web'),
|
|
|
|
projection=['ap_subdomain'],
|
|
|
|
).fetch()
|
2023-12-24 18:04:01 +00:00
|
|
|
}
|
2024-04-22 18:12:03 +00:00
|
|
|
logger.info(f'Loaded {len(_NON_WEB_SUBDOMAIN_SITES)} non-web.brid.gy Web users')
|
2023-12-24 18:04:01 +00:00
|
|
|
|
2024-04-22 18:12:03 +00:00
|
|
|
subdomain = _NON_WEB_SUBDOMAIN_SITES.get(user_domain, 'web')
|
2023-12-24 18:20:04 +00:00
|
|
|
return f'https://{subdomain}{SUPERDOMAIN}/'
|
2023-12-24 18:04:01 +00:00
|
|
|
|
2023-09-21 20:37:17 +00:00
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
def translate_user_id(*, id, from_, to):
|
2023-10-26 20:49:42 +00:00
|
|
|
"""Translate a user id from one protocol to another.
|
2023-09-21 20:37:17 +00:00
|
|
|
|
2024-04-09 17:04:16 +00:00
|
|
|
TODO: unify with :func:`translate_object_id`.
|
|
|
|
|
2023-09-21 20:37:17 +00:00
|
|
|
Args:
|
|
|
|
id (str)
|
2024-04-21 04:03:06 +00:00
|
|
|
from_ (protocol.Protocol)
|
|
|
|
to (protocol.Protocol)
|
2023-09-21 20:37:17 +00:00
|
|
|
|
|
|
|
Returns:
|
2024-04-21 04:03:06 +00:00
|
|
|
str: the corresponding id in ``to``
|
2023-09-21 20:37:17 +00:00
|
|
|
"""
|
2024-04-21 04:03:06 +00:00
|
|
|
assert id and from_ and to, (id, from_, to)
|
|
|
|
assert from_.owns_id(id) is not False or from_.LABEL == 'ui', \
|
|
|
|
(id, from_.LABEL, to.LABEL)
|
2023-09-21 20:37:17 +00:00
|
|
|
|
2023-11-03 18:00:34 +00:00
|
|
|
parsed = urlparse(id)
|
2024-04-21 04:03:06 +00:00
|
|
|
if from_.LABEL == 'web' and parsed.path.strip('/') == '':
|
2023-11-03 18:00:34 +00:00
|
|
|
# home page; replace with domain
|
|
|
|
id = parsed.netloc
|
2024-04-09 16:48:16 +00:00
|
|
|
|
|
|
|
# bsky.app profile URL to DID
|
2024-04-21 04:03:06 +00:00
|
|
|
if to.LABEL == 'atproto':
|
2024-04-09 16:48:16 +00:00
|
|
|
if match := BSKY_APP_URL_RE.match(id):
|
|
|
|
repo = match.group('id')
|
|
|
|
if repo.startswith('did:'):
|
|
|
|
return repo
|
|
|
|
|
|
|
|
from atproto import ATProto
|
|
|
|
try:
|
|
|
|
return ATProto.handle_to_id(repo)
|
|
|
|
except (AssertionError, ValueError) as e:
|
|
|
|
logger.warning(e)
|
|
|
|
return None
|
2023-11-03 18:00:34 +00:00
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
if from_ == to:
|
2023-09-25 13:42:31 +00:00
|
|
|
return id
|
|
|
|
|
2023-11-03 21:53:19 +00:00
|
|
|
# follow use_instead
|
2024-04-21 04:03:06 +00:00
|
|
|
user = from_.get_by_id(id)
|
2023-11-03 21:53:19 +00:00
|
|
|
if user:
|
|
|
|
id = user.key.id()
|
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
if from_.LABEL in COPIES_PROTOCOLS or to.LABEL in COPIES_PROTOCOLS:
|
2023-11-03 21:53:19 +00:00
|
|
|
if user:
|
2024-04-21 04:03:06 +00:00
|
|
|
if copy := user.get_copy(to):
|
2023-11-02 20:08:12 +00:00
|
|
|
return copy
|
|
|
|
if orig := models.get_original(id):
|
2024-04-21 04:03:06 +00:00
|
|
|
if isinstance(orig, to):
|
2023-11-02 20:08:12 +00:00
|
|
|
return orig.key.id()
|
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
match from_.LABEL, to.LABEL:
|
2023-11-07 04:17:23 +00:00
|
|
|
case _, 'atproto' | 'nostr':
|
2024-04-21 04:03:06 +00:00
|
|
|
logger.warning(f"Can't translate user id {id} to {to.LABEL} , haven't copied it there yet!")
|
2023-11-02 20:08:12 +00:00
|
|
|
return None
|
2023-11-03 18:37:36 +00:00
|
|
|
|
2023-11-03 00:53:07 +00:00
|
|
|
case 'web', 'activitypub':
|
2023-12-24 18:20:04 +00:00
|
|
|
return urljoin(web_ap_base_domain(id), id)
|
2023-11-03 18:37:36 +00:00
|
|
|
|
2023-10-26 23:20:30 +00:00
|
|
|
case 'activitypub', 'web':
|
2023-09-21 20:37:17 +00:00
|
|
|
return id
|
2023-11-02 20:08:12 +00:00
|
|
|
|
2023-11-07 04:17:23 +00:00
|
|
|
case _, 'activitypub' | 'web':
|
2024-04-21 04:03:06 +00:00
|
|
|
return subdomain_wrap(from_, f'/{to.ABBREV}/{id}')
|
2023-11-07 04:17:23 +00:00
|
|
|
|
2023-10-27 00:18:01 +00:00
|
|
|
# only for unit tests
|
2024-04-17 23:43:10 +00:00
|
|
|
case _, 'fake' | 'other' | 'eefake':
|
2024-04-21 04:03:06 +00:00
|
|
|
return f'{to.LABEL}:u:{id}'
|
2023-10-27 00:18:01 +00:00
|
|
|
case 'fake' | 'other', _:
|
2023-11-07 04:17:23 +00:00
|
|
|
return id
|
2023-09-21 20:37:17 +00:00
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
assert False, (id, from_.LABEL, to.LABEL)
|
2023-09-22 18:41:30 +00:00
|
|
|
|
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
def translate_handle(*, handle, from_, to, enhanced):
|
2023-10-26 20:49:42 +00:00
|
|
|
"""Translates a user handle from one protocol to another.
|
2023-09-22 18:41:30 +00:00
|
|
|
|
|
|
|
Args:
|
|
|
|
handle (str)
|
2024-04-21 04:03:06 +00:00
|
|
|
from_ (protocol.Protocol)
|
|
|
|
to (protocol.Protocol)
|
2023-11-30 00:31:33 +00:00
|
|
|
enhanced (bool): whether to convert to an "enhanced" handle based on the
|
|
|
|
user's domain
|
2023-09-22 18:41:30 +00:00
|
|
|
|
|
|
|
Returns:
|
2024-04-21 04:03:06 +00:00
|
|
|
str: the corresponding handle in ``to``
|
2023-09-22 18:41:30 +00:00
|
|
|
"""
|
2024-04-21 04:03:06 +00:00
|
|
|
assert handle and from_ and to, (handle, from_, to)
|
|
|
|
assert from_.owns_handle(handle) is not False or from_.LABEL == 'ui'
|
2023-09-22 18:41:30 +00:00
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
if from_ == to:
|
2023-09-25 13:42:31 +00:00
|
|
|
return handle
|
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
match from_.LABEL, to.LABEL:
|
2023-10-26 23:20:30 +00:00
|
|
|
case _, 'activitypub':
|
2024-04-22 20:24:24 +00:00
|
|
|
domain = f'{from_.ABBREV}{SUPERDOMAIN}'
|
|
|
|
if enhanced or handle == PRIMARY_DOMAIN or handle in PROTOCOL_DOMAINS:
|
|
|
|
domain = handle
|
2023-11-30 00:31:33 +00:00
|
|
|
return f'@{handle}@{domain}'
|
2023-11-03 18:37:36 +00:00
|
|
|
|
2023-10-26 23:20:30 +00:00
|
|
|
case _, 'atproto' | 'nostr':
|
2023-09-22 18:41:30 +00:00
|
|
|
handle = handle.lstrip('@').replace('@', '.')
|
2023-11-30 00:31:33 +00:00
|
|
|
return (handle if enhanced
|
2024-04-21 04:03:06 +00:00
|
|
|
else f'{handle}.{from_.ABBREV}{SUPERDOMAIN}')
|
2023-11-03 18:37:36 +00:00
|
|
|
|
2023-10-26 23:20:30 +00:00
|
|
|
case 'activitypub', 'web':
|
2023-09-22 18:41:30 +00:00
|
|
|
user, instance = handle.lstrip('@').split('@')
|
2023-11-15 22:23:08 +00:00
|
|
|
# TODO: get this from the actor object's url field?
|
2023-11-30 00:31:33 +00:00
|
|
|
return (f'https://{user}' if user == instance
|
|
|
|
else f'https://{instance}/@{user}')
|
2023-11-03 18:37:36 +00:00
|
|
|
|
2023-10-26 23:20:30 +00:00
|
|
|
case _, 'web':
|
2023-09-22 18:41:30 +00:00
|
|
|
return handle
|
2023-11-03 18:37:36 +00:00
|
|
|
|
2023-10-27 00:18:01 +00:00
|
|
|
# only for unit tests
|
2024-04-17 23:43:10 +00:00
|
|
|
case _, 'fake' | 'other' | 'eefake':
|
2024-04-21 04:03:06 +00:00
|
|
|
return f'{to.LABEL}:handle:{handle}'
|
2023-09-22 18:41:30 +00:00
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
assert False, (handle, from_.LABEL, to.LABEL)
|
2023-10-26 23:20:30 +00:00
|
|
|
|
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
def translate_object_id(*, id, from_, to):
|
2023-10-26 23:20:30 +00:00
|
|
|
"""Translates a user handle from one protocol to another.
|
|
|
|
|
2024-04-09 17:04:16 +00:00
|
|
|
TODO: unify with :func:`translate_user_id`.
|
|
|
|
|
2023-10-26 23:20:30 +00:00
|
|
|
Args:
|
|
|
|
id (str)
|
2024-04-21 04:03:06 +00:00
|
|
|
from_ (protocol.Protocol)
|
|
|
|
to (protocol.Protocol)
|
2023-10-26 23:20:30 +00:00
|
|
|
|
|
|
|
Returns:
|
2024-04-21 04:03:06 +00:00
|
|
|
str: the corresponding id in ``to``
|
2023-10-26 23:20:30 +00:00
|
|
|
"""
|
2024-04-21 04:03:06 +00:00
|
|
|
assert id and from_ and to, (id, from_, to)
|
|
|
|
assert from_.owns_id(id) is not False or from_.LABEL == 'ui'
|
2023-10-26 23:20:30 +00:00
|
|
|
|
2024-04-09 17:04:16 +00:00
|
|
|
# bsky.app profile URL to DID
|
2024-04-21 04:03:06 +00:00
|
|
|
if to.LABEL == 'atproto':
|
2024-04-09 17:04:16 +00:00
|
|
|
if match := BSKY_APP_URL_RE.match(id):
|
|
|
|
repo = match.group('id')
|
|
|
|
handle = None
|
|
|
|
if not repo.startswith('did:'):
|
|
|
|
handle = repo
|
|
|
|
from atproto import ATProto
|
|
|
|
try:
|
|
|
|
repo = ATProto.handle_to_id(repo)
|
|
|
|
except (AssertionError, ValueError) as e:
|
|
|
|
logger.warning(e)
|
|
|
|
return None
|
|
|
|
|
|
|
|
return web_url_to_at_uri(id, handle=handle, did=repo)
|
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
if from_ == to:
|
2023-10-26 23:20:30 +00:00
|
|
|
return id
|
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
if from_.LABEL in COPIES_PROTOCOLS or to.LABEL in COPIES_PROTOCOLS:
|
|
|
|
if obj := from_.load(id, remote=False):
|
|
|
|
if copy := obj.get_copy(to):
|
2023-11-02 20:08:12 +00:00
|
|
|
return copy
|
|
|
|
if orig := models.get_original(id):
|
|
|
|
return orig.key.id()
|
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
match from_.LABEL, to.LABEL:
|
2023-11-07 04:17:23 +00:00
|
|
|
case _, 'atproto' | 'nostr':
|
2024-04-21 04:03:06 +00:00
|
|
|
logger.warning(f"Can't translate object id {id} to {to.LABEL} , haven't copied it there yet!")
|
2023-10-26 23:20:30 +00:00
|
|
|
return id
|
|
|
|
|
2023-11-03 18:37:36 +00:00
|
|
|
case 'web', 'activitypub':
|
2023-12-24 18:20:04 +00:00
|
|
|
return urljoin(web_ap_base_domain(util.domain_from_link(id)), f'/r/{id}')
|
2023-11-03 18:37:36 +00:00
|
|
|
|
2023-10-26 23:20:30 +00:00
|
|
|
case _, 'activitypub' | 'web':
|
2024-04-21 04:03:06 +00:00
|
|
|
return subdomain_wrap(from_, f'/convert/{to.ABBREV}/{id}')
|
2023-10-26 23:20:30 +00:00
|
|
|
|
2023-10-27 00:18:01 +00:00
|
|
|
# only for unit tests
|
2024-04-17 23:43:10 +00:00
|
|
|
case _, 'fake' | 'other' | 'eefake':
|
2024-04-21 04:03:06 +00:00
|
|
|
return f'{to.LABEL}:o:{from_.ABBREV}:{id}'
|
2023-10-27 00:18:01 +00:00
|
|
|
|
2024-04-21 04:03:06 +00:00
|
|
|
assert False, (id, from_.LABEL, to.LABEL)
|