kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
d97d606f4a
commit
046e91a9c5
|
|
@ -12,7 +12,7 @@
|
|||
"https://ap.brid.gy/"
|
||||
],
|
||||
"preferredUsername": "ap.brid.gy",
|
||||
"summary": "Bridgy Fed (https://fed.brid.gy/) bot user for the fediverse. To bridge your Bluesky account to the fediverse, follow this account.\n\nTo ask a fediverse user to bridge their account, send their address (eg @user@instance) to this account in a chat message.\n\nMore info: https://fed.brid.gy/docs",
|
||||
"summary": "Bridgy Fed (https://fed.brid.gy/) bot user for the fediverse. To bridge your account to the fediverse, follow this account.\n\nTo ask a fediverse user to bridge their account, send their address (eg @user@instance) to this account in a chat message.\n\nMore info: https://fed.brid.gy/docs",
|
||||
"name": "Bridgy Fed for the fediverse",
|
||||
"image": [
|
||||
{
|
||||
|
|
|
|||
2
app.py
2
app.py
|
|
@ -6,7 +6,7 @@ registered.
|
|||
from flask_app import app
|
||||
|
||||
# import all modules to register their Flask handlers
|
||||
import activitypub, atproto, convert, follow, pages, redirect, ui, webfinger, web
|
||||
import activitypub, atproto, convert, follow, nostr, pages, redirect, ui, webfinger, web
|
||||
|
||||
import models
|
||||
models.reset_protocol_properties()
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ class ATProto(User, Protocol):
|
|||
|
||||
@classmethod
|
||||
def owns_handle(cls, handle, allow_internal=False):
|
||||
# TODO: implement allow_internal
|
||||
# TODO: implement allow_internal?
|
||||
if not did.HANDLE_RE.fullmatch(handle):
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from oauth_dropins.webutil import appengine_config, flask_util
|
|||
import pytz
|
||||
|
||||
# all protocols
|
||||
import activitypub, atproto, web
|
||||
import activitypub, atproto, nostr, web
|
||||
import atproto_firehose
|
||||
import common
|
||||
import models
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"https://fed.brid.gy/"
|
||||
],
|
||||
"preferredUsername": "bsky.brid.gy",
|
||||
"summary": "<p><a href='https://fed.brid.gy/'>Bridgy Fed</a> bot user for <a href='https://bsky.social/'>Bluesky</a>. To bridge your fediverse account to Bluesky, follow this account. <a href='https://fed.brid.gy/docs'>More info here.</a><p>After you follow this account, it will follow you back. Accept its follow to make sure your fediverse posts get sent to the bridge and make it into Bluesky.<p>To ask a Bluesky user to bridge their account, DM their handle (eg snarfed.bsky.social) to this account.</p>",
|
||||
"summary": "<p><a href='https://fed.brid.gy/'>Bridgy Fed</a> bot user for <a href='https://bsky.social/'>Bluesky</a>. To bridge your account to Bluesky, follow this account. <a href='https://fed.brid.gy/docs'>More info here.</a><p>After you follow this account, it will follow you back. Accept its follow to make sure your posts get sent to the bridge.<p>To ask a Bluesky user to bridge their account, DM their handle (eg snarfed.bsky.social) to this account.</p>",
|
||||
"name": "Bridgy Fed for Bluesky",
|
||||
"image": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ PROTOCOL_DOMAINS = (
|
|||
'efake.brid.gy',
|
||||
'fa.brid.gy',
|
||||
'other.brid.gy',
|
||||
'nostr.brid.gy',
|
||||
)
|
||||
OTHER_DOMAINS = (
|
||||
'bridgy-federated.appspot.com',
|
||||
|
|
|
|||
55
ids.py
55
ids.py
|
|
@ -31,14 +31,14 @@ logger = logging.getLogger(__name__)
|
|||
# populated in models.reset_protocol_properties
|
||||
COPIES_PROTOCOLS = None
|
||||
|
||||
# Webfinger allows all sorts of characters that ATProto handles don't,
|
||||
# notably _ and ~. Map those to -.
|
||||
# Webfinger allows all sorts of characters that ATProto handles and Nostr usernames
|
||||
# don't, notably _ and ~. Map those to -.
|
||||
# ( : (colon) is mostly just used in the fake protocols in unit tests.)
|
||||
# https://www.rfc-editor.org/rfc/rfc7565.html#section-7
|
||||
# https://atproto.com/specs/handle
|
||||
# https://github.com/snarfed/bridgy-fed/issues/982
|
||||
# https://github.com/swicg/activitypub-webfinger/issues/9
|
||||
ATPROTO_DASH_CHARS = ('_', '~', ':')
|
||||
DASH_CHARS = ('_', '~', ':')
|
||||
|
||||
# can't use translate_user_id because Web.owns_id checks valid_domain, which
|
||||
# doesn't allow our protocol subdomains
|
||||
|
|
@ -90,6 +90,9 @@ def web_ap_base_domain(user_domain):
|
|||
def translate_user_id(*, id, from_, to):
|
||||
"""Translate a user id from one protocol to another.
|
||||
|
||||
*NOTE*: unlike :func:`translate_object_id`, if ``to`` is a ``HAS_COPIES`` protocol
|
||||
and has no copy object for ``id``, this function returns None, not ``id``!
|
||||
|
||||
TODO: unify with :func:`translate_object_id`.
|
||||
|
||||
Args:
|
||||
|
|
@ -206,6 +209,8 @@ def normalize_user_id(*, id, proto):
|
|||
normalized = util.domain_from_link(normalized)
|
||||
elif proto.LABEL == 'atproto' and id.startswith('at://'):
|
||||
normalized, _, _ = parse_at_uri(id)
|
||||
elif proto.LABEL == 'nostr':
|
||||
normalized = id.removeprefix('nostr:')
|
||||
elif proto.LABEL in ('fake', 'efake', 'other'):
|
||||
normalized = normalized.replace(':profile:', ':')
|
||||
|
||||
|
|
@ -276,23 +281,42 @@ def translate_handle(*, handle, from_, to, enhanced):
|
|||
if from_.owns_handle(handle, allow_internal=True) is False:
|
||||
raise ValueError(f'input handle {handle} is not valid for {from_.LABEL}')
|
||||
|
||||
if from_.LABEL == 'nostr':
|
||||
# _ username is NIP-05 shortcut for just the domain itself
|
||||
# https://nips.nostr.com/5#showing-just-the-domain-as-an-identifier
|
||||
handle = handle.removeprefix('_@')
|
||||
|
||||
def flattened_user_at_domain():
|
||||
# "flatten" [@]user@domain handles to just domain-like, eg user.domain,
|
||||
# and then append @[protocol domain], so we end up with
|
||||
# user.domain@proto.brid.gy.
|
||||
domain = f'{from_.ABBREV}{SUPERDOMAIN}'
|
||||
|
||||
flattened = handle.lstrip('@').replace('@', '.')
|
||||
for from_char in DASH_CHARS:
|
||||
flattened = flattened.replace(from_char, '-')
|
||||
|
||||
if enhanced or handle == PRIMARY_DOMAIN or handle in PROTOCOL_DOMAINS:
|
||||
domain = flattened
|
||||
|
||||
return f'{flattened}@{domain}'
|
||||
|
||||
output = None
|
||||
match from_.LABEL, to.LABEL:
|
||||
case _, 'activitypub':
|
||||
domain = f'{from_.ABBREV}{SUPERDOMAIN}'
|
||||
if enhanced or handle == PRIMARY_DOMAIN or handle in PROTOCOL_DOMAINS:
|
||||
domain = handle
|
||||
output = f'@{handle}@{domain}'
|
||||
output = '@' + flattened_user_at_domain()
|
||||
|
||||
case _, 'atproto':
|
||||
output = handle.lstrip('@').replace('@', '.')
|
||||
for from_char in ATPROTO_DASH_CHARS:
|
||||
output = output.replace(from_char, '-')
|
||||
if handle == PRIMARY_DOMAIN or handle in PROTOCOL_DOMAINS:
|
||||
return handle
|
||||
|
||||
if enhanced or handle == PRIMARY_DOMAIN or handle in PROTOCOL_DOMAINS:
|
||||
pass
|
||||
else:
|
||||
output = f'{output}.{from_.ABBREV}{SUPERDOMAIN}'
|
||||
output = flattened_user_at_domain().replace('@', '.')
|
||||
|
||||
case _, 'nostr':
|
||||
if handle == PRIMARY_DOMAIN or handle in PROTOCOL_DOMAINS:
|
||||
return f'_@{handle}'
|
||||
|
||||
output = flattened_user_at_domain()
|
||||
|
||||
case 'activitypub', 'web':
|
||||
user, instance = handle.lstrip('@').split('@')
|
||||
|
|
@ -319,6 +343,9 @@ def translate_handle(*, handle, from_, to, enhanced):
|
|||
def translate_object_id(*, id, from_, to):
|
||||
"""Translates a user handle from one protocol to another.
|
||||
|
||||
*NOTE*: unlike :func:`translate_user_id`, if ``to`` is a ``HAS_COPIES`` protocol
|
||||
and has no copy object for ``id``, this function returns ``id``, not None!
|
||||
|
||||
TODO: unify with :func:`translate_user_id`.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ PROTOCOLS = {label: None for label in (
|
|||
'atproto',
|
||||
'bsky',
|
||||
'ostatus',
|
||||
'nostr',
|
||||
'web',
|
||||
'webmention',
|
||||
'ui',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://purl.archive.org/miscellany",
|
||||
"https://w3id.org/security/v1",
|
||||
{"alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"}}
|
||||
],
|
||||
"type": "Service",
|
||||
"id": "https://nostr.brid.gy/nostr.brid.gy",
|
||||
"url": [
|
||||
"https://nostr.brid.gy/",
|
||||
"https://fed.brid.gy/"
|
||||
],
|
||||
"preferredUsername": "nostr.brid.gy",
|
||||
"summary": "<p><a href='https://fed.brid.gy/'>Bridgy Fed</a> bot user for <a href='https://nostr.social/'>Nostr</a>. To bridge your account to Nostr, follow this account. <a href='https://fed.brid.gy/docs'>More info here.</a><p>After you follow this account, it will follow you back. Accept its follow to make sure your posts get sent to the bridge.<p>To ask a Nostr user to bridge their account, DM their NIP-05 handle (eg ryan@snarfed.org) to this account.</p>",
|
||||
"name": "Bridgy Fed for Nostr",
|
||||
"image": [
|
||||
{
|
||||
"type": "Image",
|
||||
"url": "https://fed.brid.gy/static/bridgy_fed_banner.png"
|
||||
},
|
||||
{
|
||||
"name": "Bridgy Fed for Nostr",
|
||||
"type": "Image",
|
||||
"url": "https://fed.brid.gy/static/bridgy_logo_square.jpg"
|
||||
}
|
||||
],
|
||||
"icon": {
|
||||
"name": "Bridgy Fed for Nostr",
|
||||
"type": "Image",
|
||||
"url": "https://fed.brid.gy/static/bridgy_logo_square.jpg"
|
||||
},
|
||||
"alsoKnownAs": [
|
||||
"https://nostr.brid.gy/",
|
||||
"https://fed.brid.gy/"
|
||||
],
|
||||
"manuallyApprovesFollowers": false
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
"""Nostr protocol implementation.
|
||||
|
||||
https://github.com/nostr-protocol/nostr
|
||||
https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||
https://github.com/nostr-protocol/nips#list
|
||||
"""
|
||||
import logging
|
||||
|
||||
from google.cloud import ndb
|
||||
from granary import as1, nostr
|
||||
from requests import RequestException
|
||||
from oauth_dropins.webutil import util
|
||||
from oauth_dropins.webutil.util import add, json_dumps, json_loads
|
||||
|
||||
import common
|
||||
from common import (
|
||||
DOMAIN_BLOCKLIST,
|
||||
DOMAIN_RE,
|
||||
DOMAINS,
|
||||
error,
|
||||
USER_AGENT,
|
||||
)
|
||||
import ids
|
||||
from models import Object, PROTOCOLS, Target, User
|
||||
from protocol import Protocol
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Nostr(User, Protocol):
|
||||
"""Nostr class.
|
||||
|
||||
Key id is bech32 npub id.
|
||||
https://github.com/nostr-protocol/nips/blob/master/19.md
|
||||
"""
|
||||
ABBREV = 'nostr'
|
||||
PHRASE = 'Nostr'
|
||||
LOGO_HTML = '<img src="/static/nostr.png">'
|
||||
CONTENT_TYPE = 'application/json'
|
||||
HAS_COPIES = True
|
||||
REQUIRES_AVATAR = True
|
||||
REQUIRES_NAME = True
|
||||
DEFAULT_ENABLED_PROTOCOLS = ('web',)
|
||||
SUPPORTED_AS1_TYPES = frozenset(
|
||||
tuple(as1.ACTOR_TYPES)
|
||||
+ tuple(as1.POST_TYPES)
|
||||
+ ('post', 'delete', 'undo') # no update/edit (I think?)
|
||||
+ ('follow', 'like', 'share', 'stop-following')
|
||||
)
|
||||
SUPPORTS_DMS = False # NIP-17
|
||||
|
||||
@ndb.ComputedProperty
|
||||
def handle(self):
|
||||
"""TODO: NIP-05"""
|
||||
return None
|
||||
|
||||
def web_url(self):
|
||||
return None # TODO
|
||||
|
||||
def id_uri(self):
|
||||
return f'nostr:{self.key.id()}'
|
||||
|
||||
@classmethod
|
||||
def owns_id(cls, id):
|
||||
return (id.startswith('npub')
|
||||
or id.startswith('nevent')
|
||||
or id.startswith('note')
|
||||
or id.startswith('nprofile')
|
||||
or id.startswith('naddr')
|
||||
or id.startswith('nostr:'))
|
||||
|
||||
@classmethod
|
||||
def owns_handle(cls, handle, allow_internal=False):
|
||||
if not handle:
|
||||
return False
|
||||
|
||||
# TODO: implement allow_internal?
|
||||
return (handle.startswith('npub')
|
||||
or cls.is_user_at_domain(handle, allow_internal=True))
|
||||
|
||||
@classmethod
|
||||
def handle_to_id(cls, handle):
|
||||
if cls.owns_handle(handle) is False:
|
||||
return None
|
||||
|
||||
if handle.startswith('npub'):
|
||||
return handle
|
||||
|
||||
# TODO: implement NIP-05 resolution
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def bridged_web_url_for(cls, user, fallback=False):
|
||||
"""TODO: which client? coracle?
|
||||
"""
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def target_for(cls, obj, shared=False):
|
||||
"""Look up the author's relays and return one?
|
||||
"""
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def create_for(cls, user):
|
||||
"""Creates a Nostr profile for a non-Nostr user.
|
||||
|
||||
Args:
|
||||
user (models.User)
|
||||
"""
|
||||
pass # TODO
|
||||
|
||||
@classmethod
|
||||
def set_username(to_cls, user, username):
|
||||
"""check NIP-05 DNS, then update profile event with nip05?"""
|
||||
if not user.is_enabled(Nostr):
|
||||
raise ValueError("First, you'll need to bridge your account into Nostr by following this account.")
|
||||
|
||||
npub = user.get_copy(Nostr)
|
||||
username = username.removeprefix('@')
|
||||
|
||||
# TODO: implement NIP-05 setup
|
||||
logger.info(f'Setting Nostr NIP-05 for {user.key.id()} to {username}')
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send(to_cls, obj, url, from_user=None, orig_obj_id=None):
|
||||
"""Sends an object to a relay.
|
||||
"""
|
||||
# TODO: send to relay
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, obj, **kwargs):
|
||||
"""Tries to fetch a Nostr event from a relay.
|
||||
|
||||
Args:
|
||||
obj (models.Object): with the id to fetch. Fills data into the ``as2``
|
||||
property.
|
||||
kwargs: ignored
|
||||
|
||||
Returns:
|
||||
bool: True if the object was fetched and populated successfully,
|
||||
False otherwise
|
||||
"""
|
||||
id = obj.key.id()
|
||||
if not cls.owns_id(id):
|
||||
logger.info(f"Nostr can't fetch {id}")
|
||||
return False
|
||||
|
||||
# TODO: fetch from relay
|
||||
return False
|
||||
|
||||
|
||||
@classmethod
|
||||
def _convert(cls, obj, from_user=None):
|
||||
"""Converts a :class:`models.Object` to a Nostr event.
|
||||
|
||||
Args:
|
||||
obj (models.Object)
|
||||
from_user (models.User): user (actor) this activity/object is from
|
||||
|
||||
Returns:
|
||||
dict: JSON Nostr event
|
||||
"""
|
||||
from_proto = PROTOCOLS.get(obj.source_protocol)
|
||||
|
||||
# TODO: implement actual conversion
|
||||
if not obj.as1:
|
||||
return {}
|
||||
|
||||
return {} # TODO
|
||||
2
pages.py
2
pages.py
|
|
@ -50,6 +50,7 @@ from models import (
|
|||
PROTOCOLS,
|
||||
USER_STATUS_DESCRIPTIONS,
|
||||
)
|
||||
from nostr import Nostr
|
||||
from protocol import Protocol
|
||||
from web import Web
|
||||
import webfinger
|
||||
|
|
@ -70,6 +71,7 @@ TEMPLATE_VARS = {
|
|||
'ids': ids,
|
||||
'isinstance': isinstance,
|
||||
'logs': logs,
|
||||
'Nostr': Nostr,
|
||||
'PROTOCOLS': PROTOCOLS,
|
||||
'set': set,
|
||||
'util': util,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from oauth_dropins.webutil import (
|
|||
)
|
||||
|
||||
# all protocols
|
||||
import activitypub, atproto, web
|
||||
import activitypub, atproto, nostr, web
|
||||
import common
|
||||
import dms
|
||||
import models
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from oauth_dropins.webutil.appengine_info import DEBUG, LOCAL_SERVER
|
|||
from oauth_dropins.webutil import appengine_config, flask_util
|
||||
|
||||
# all protocols
|
||||
import activitypub, atproto, web
|
||||
import activitypub, atproto, nostr, web
|
||||
import atproto_firehose
|
||||
import models
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from oauth_dropins.webutil import appengine_config, flask_util
|
|||
import pytz
|
||||
|
||||
# all protocols
|
||||
import activitypub, atproto, web
|
||||
import activitypub, atproto, nostr, web
|
||||
import common
|
||||
import models
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ ACTOR_FAKE = {
|
|||
'following': 'https://fa.brid.gy/ap/fake:user/following',
|
||||
'followers': 'https://fa.brid.gy/ap/fake:user/followers',
|
||||
'endpoints': {'sharedInbox': 'https://fa.brid.gy/ap/sharedInbox'},
|
||||
'preferredUsername': 'fake:handle:user',
|
||||
'preferredUsername': 'fake-handle-user',
|
||||
'summary': '',
|
||||
'alsoKnownAs': ['uri:fake:user'],
|
||||
'manuallyApprovesFollowers': False,
|
||||
|
|
@ -106,7 +106,7 @@ ACTOR_FAKE_USER = {
|
|||
activitypub.SECURITY_CONTEXT,
|
||||
activitypub.AKA_CONTEXT,
|
||||
],
|
||||
'name': 'fake:handle:user',
|
||||
'name': 'fake-handle-user',
|
||||
'type': 'Person',
|
||||
'summary': '[<a href="https://fed.brid.gy/fa/fake:handle:user">bridged</a> from <a href="web:fake:user">fake:handle:user</a> on fake-phrase by <a href="https://fed.brid.gy/">Bridgy Fed</a>]',
|
||||
'discoverable': True,
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class RemoteFollowTest(TestCase):
|
|||
mock_get.return_value = WEBFINGER
|
||||
got = self.client.post('/remote-follow?address=@foo@ba.r&id=fake:user&protocol=fake')
|
||||
self.assertEqual(302, got.status_code)
|
||||
self.assertEqual('https://ba.r/follow?uri=@fake:handle:user@fa.brid.gy',
|
||||
self.assertEqual('https://ba.r/follow?uri=@fake-handle-user@fa.brid.gy',
|
||||
got.headers['Location'])
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from flask_app import app
|
|||
import ids
|
||||
from ids import translate_handle, translate_object_id, translate_user_id
|
||||
from models import Target
|
||||
from nostr import Nostr
|
||||
from .testutil import Fake, TestCase
|
||||
from web import Web
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ class IdsTest(TestCase):
|
|||
super().setUp()
|
||||
Web(id='bsky.brid.gy', ap_subdomain='bsky', has_redirects=True).put()
|
||||
Web(id='fed.brid.gy', ap_subdomain='fed', has_redirects=True).put()
|
||||
Web(id='nostr.brid.gy', ap_subdomain='nostr', has_redirects=True).put()
|
||||
|
||||
def test_translate_user_id(self):
|
||||
Web(id='user.com',
|
||||
|
|
@ -23,7 +25,7 @@ class IdsTest(TestCase):
|
|||
ActivityPub(id='https://inst/user',
|
||||
copies=[Target(uri='did:plc:456', protocol='atproto')]).put()
|
||||
fake_user = Fake(id='fake:user',
|
||||
copies=[Target(uri='did:plc:789', protocol='atproto')])
|
||||
copies=[Target(uri='did:plc:789', protocol='atproto')])
|
||||
fake_user.put()
|
||||
|
||||
# ATProto with DID docs, used to resolve handle in bsky.app URL
|
||||
|
|
@ -51,8 +53,9 @@ class IdsTest(TestCase):
|
|||
ATProto, 'did:plc:456'),
|
||||
(ActivityPub, 'https://bsky.brid.gy/ap/did:plc:456',
|
||||
Fake, 'fake:u:did:plc:456'),
|
||||
|
||||
(ATProto, 'did:plc:456', ATProto, 'did:plc:456'),
|
||||
(Nostr, 'npub123', Nostr, 'npub123'),
|
||||
|
||||
# copies
|
||||
(ATProto, 'did:plc:123', Web, 'user.com'),
|
||||
(ATProto, 'did:plc:456', ActivityPub, 'https://inst/user'),
|
||||
|
|
@ -62,9 +65,19 @@ class IdsTest(TestCase):
|
|||
(ATProto, 'did:plc:x', Web, 'https://bsky.brid.gy/web/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', ActivityPub, 'https://bsky.brid.gy/ap/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', Fake, 'fake:u:did:plc:x'),
|
||||
(ATProto, 'did:plc:456', Nostr, None),
|
||||
(ATProto, 'https://bsky.app/profile/user.com', ATProto, 'did:plc:123'),
|
||||
(ATProto, 'https://bsky.app/profile/did:plc:123', ATProto, 'did:plc:123'),
|
||||
|
||||
(Nostr, 'npub123', Web, 'https://nostr.brid.gy/web/npub123'),
|
||||
(Nostr, 'npub123', ActivityPub, 'https://nostr.brid.gy/ap/npub123'),
|
||||
(Nostr, 'npub123', ATProto, None),
|
||||
(Nostr, 'npub123', Fake, 'fake:u:npub123'),
|
||||
|
||||
(ActivityPub, 'https://inst/user', Nostr, None),
|
||||
(Web, 'user.com', Nostr, None),
|
||||
(Fake, 'fake:user', Nostr, None),
|
||||
|
||||
# user, not enabled, no copy
|
||||
(ATProto, 'did:plc:000', ActivityPub, 'https://bsky.app/profile/zero.com'),
|
||||
|
||||
|
|
@ -88,6 +101,7 @@ class IdsTest(TestCase):
|
|||
# instance actor / protocol bot users
|
||||
(Web, 'fed.brid.gy', ActivityPub, 'https://fed.brid.gy/fed.brid.gy'),
|
||||
(Web, 'bsky.brid.gy', ActivityPub, 'https://bsky.brid.gy/bsky.brid.gy'),
|
||||
(Web, 'nostr.brid.gy', ActivityPub, 'https://nostr.brid.gy/nostr.brid.gy'),
|
||||
]:
|
||||
with self.subTest(id=id, from_=from_.LABEL, to=to.LABEL):
|
||||
self.assertEqual(expected, translate_user_id(
|
||||
|
|
@ -152,6 +166,8 @@ class IdsTest(TestCase):
|
|||
(Web, 'https://user.com/', 'user.com'),
|
||||
(Web, 'https://www.user.com/', 'user.com'),
|
||||
(Web, 'm.user.com', 'user.com'),
|
||||
(Nostr, 'npub123', 'npub123'),
|
||||
(Nostr, 'nostr:nprofile1234abcd', 'nprofile1234abcd'),
|
||||
]:
|
||||
with self.subTest(id=id, proto=proto):
|
||||
self.assertEqual(expected, ids.normalize_user_id(id=id, proto=proto))
|
||||
|
|
@ -162,6 +178,7 @@ class IdsTest(TestCase):
|
|||
(ATProto, 'did:plc:123', 'at://did:plc:123/app.bsky.actor.profile/self'),
|
||||
(Fake, 'fake:user', 'fake:profile:user'),
|
||||
(Web, 'user.com', 'https://user.com/'),
|
||||
(Nostr, 'npub123', 'npub123'),
|
||||
]:
|
||||
with self.subTest(id=id, proto=proto):
|
||||
self.assertEqual(expected, ids.profile_id(id=id, proto=proto))
|
||||
|
|
@ -174,23 +191,39 @@ class IdsTest(TestCase):
|
|||
(Web, 'user.com', Fake, 'fake:handle:user.com'),
|
||||
(Web, 'u_se-r.com', Fake, 'fake:handle:u_se-r.com'),
|
||||
(Web, 'user.com', Web, 'user.com'),
|
||||
(Web, 'user.com', Nostr, 'user.com@web.brid.gy'),
|
||||
|
||||
(ActivityPub, '@user@instance', ActivityPub, '@user@instance'),
|
||||
(ActivityPub, '@user@instance', ATProto, 'user.instance.ap.brid.gy'),
|
||||
(ActivityPub, '@u_se~r@instance', ATProto, 'u-se-r.instance.ap.brid.gy'),
|
||||
(ActivityPub, '@user@instance', Fake, 'fake:handle:@user@instance'),
|
||||
(ActivityPub, '@user@instance', Web, 'https://instance/@user'),
|
||||
(ActivityPub, '@user@instance', Nostr, 'user.instance@ap.brid.gy'),
|
||||
|
||||
(ATProto, 'user.com', ActivityPub, '@user.com@bsky.brid.gy'),
|
||||
(ATProto, 'u-se-r.com', ActivityPub, '@u-se-r.com@bsky.brid.gy'),
|
||||
(ATProto, 'user.com', ATProto, 'user.com'),
|
||||
(ATProto, 'user.com', Fake, 'fake:handle:user.com'),
|
||||
(ATProto, 'user.com', Web, 'user.com'),
|
||||
(ATProto, 'user.com', Nostr, 'user.com@bsky.brid.gy'),
|
||||
|
||||
(Fake, 'fake:handle:user', ActivityPub, '@fake:handle:user@fa.brid.gy'),
|
||||
(Fake, 'fake:handle:user', ActivityPub, '@fake-handle-user@fa.brid.gy'),
|
||||
(Fake, 'fake:handle:user', ATProto, 'fake-handle-user.fa.brid.gy'),
|
||||
(Fake, 'fake:handle:user', Fake, 'fake:handle:user'),
|
||||
(Fake, 'fake:handle:user', Web, 'fake:handle:user'),
|
||||
(Fake, 'fake:handle:user', Nostr, 'fake-handle-user@fa.brid.gy'),
|
||||
|
||||
(Nostr, 'user@dom.ain', Nostr, 'user@dom.ain'),
|
||||
(Nostr, 'user@dom.ain', ActivityPub, '@user.dom.ain@nostr.brid.gy'),
|
||||
(Nostr, 'user@dom.ain', ATProto, 'user.dom.ain.nostr.brid.gy'),
|
||||
(Nostr, 'user@dom.ain', Web, 'user@dom.ain'),
|
||||
(Nostr, 'user@dom.ain', Fake, 'fake:handle:user@dom.ain'),
|
||||
|
||||
(Nostr, '_@dom.ain', Nostr, '_@dom.ain'),
|
||||
(Nostr, '_@dom.ain', ActivityPub, '@dom.ain@nostr.brid.gy'),
|
||||
(Nostr, '_@dom.ain', ATProto, 'dom.ain.nostr.brid.gy'),
|
||||
(Nostr, '_@dom.ain', Web, 'dom.ain'),
|
||||
(Nostr, '_@dom.ain', Fake, 'fake:handle:dom.ain'),
|
||||
|
||||
# instance actor, protocol bot users
|
||||
(Web, 'fed.brid.gy', ActivityPub, '@fed.brid.gy@fed.brid.gy'),
|
||||
|
|
@ -225,11 +258,16 @@ class IdsTest(TestCase):
|
|||
|
||||
def test_translate_object_id(self):
|
||||
self.store_object(id='http://po.st',
|
||||
copies=[Target(uri='at://did/web/post', protocol='atproto')])
|
||||
copies=[Target(uri='at://did/web/post', protocol='atproto'),
|
||||
Target(uri='nevent123web', protocol='nostr')])
|
||||
self.store_object(id='https://inst/post',
|
||||
copies=[Target(uri='at://did/ap/post', protocol='atproto')])
|
||||
copies=[Target(uri='at://did/ap/post', protocol='atproto'),
|
||||
Target(uri='nevent123ap', protocol='atproto')])
|
||||
self.store_object(id='fake:post',
|
||||
copies=[Target(uri='at://did/fa/post', protocol='atproto')])
|
||||
copies=[Target(uri='at://did/fa/post', protocol='atproto'),
|
||||
Target(uri='nevent123fa', protocol='nostr')])
|
||||
self.store_object(id='nevent123',
|
||||
copies=[Target(uri='at://did/no/post', protocol='atproto')])
|
||||
|
||||
# DID doc and ATProto, used to resolve handle in bsky.app URL
|
||||
did = self.store_object(id='did:plc:123', raw={
|
||||
|
|
@ -240,15 +278,26 @@ class IdsTest(TestCase):
|
|||
|
||||
for from_, id, to, expected in [
|
||||
(ActivityPub, 'https://inst/post', ActivityPub, 'https://inst/post'),
|
||||
(ActivityPub, 'https://inst/post', ATProto, 'at://did/ap/post'),
|
||||
(ActivityPub, 'https://inst/post', Fake, 'fake:o:ap:https://inst/post'),
|
||||
(ActivityPub, 'https://inst/post',
|
||||
Web, 'https://ap.brid.gy/convert/web/https://inst/post'),
|
||||
(ATProto, 'at://did/atp/post', ATProto, 'at://did/atp/post'),
|
||||
(Nostr, 'nevent123', Nostr, 'nevent123'),
|
||||
|
||||
# copies
|
||||
(ActivityPub, 'https://inst/post', ATProto, 'at://did/ap/post'),
|
||||
(ATProto, 'at://did/web/post', Web, 'http://po.st'),
|
||||
(ATProto, 'at://did/ap/post', ActivityPub, 'https://inst/post'),
|
||||
(ATProto, 'at://did/fa/post', Fake, 'fake:post'),
|
||||
(ATProto, 'at://did/no/post', Nostr, 'nevent123'),
|
||||
(Nostr, 'nevent123web', Web, 'http://po.st'),
|
||||
(Nostr, 'nevent123ap', ActivityPub, 'https://inst/post'),
|
||||
(Nostr, 'nevent123fa', Fake, 'fake:post'),
|
||||
(Nostr, 'nevent123', ATProto, 'at://did/no/post'),
|
||||
(Web, 'http://po.st', ATProto, 'at://did/web/post'),
|
||||
(Web, 'http://po.st', Nostr, 'nevent123web'),
|
||||
(Fake, 'fake:post', Nostr, 'nevent123fa'),
|
||||
|
||||
# no copies
|
||||
(ATProto, 'did:plc:x', Web, 'https://bsky.brid.gy/convert/web/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', ActivityPub, 'https://bsky.brid.gy/convert/ap/did:plc:x'),
|
||||
|
|
@ -257,17 +306,23 @@ class IdsTest(TestCase):
|
|||
ATProto, 'at://did:plc:123/app.bsky.feed.post/456'),
|
||||
(ATProto, 'https://bsky.app/profile/did:plc:123/post/456',
|
||||
ATProto, 'at://did:plc:123/app.bsky.feed.post/456'),
|
||||
(ATProto, 'did:plc:x', Nostr, 'did:plc:x'),
|
||||
(Fake, 'fake:post',
|
||||
ActivityPub, 'https://fa.brid.gy/convert/ap/fake:post'),
|
||||
(Fake, 'fake:post', ATProto, 'at://did/fa/post'),
|
||||
(Fake, 'fake:post', Fake, 'fake:post'),
|
||||
(Fake, 'fake:post', Web, 'https://fa.brid.gy/convert/web/fake:post'),
|
||||
(Fake, 'fake:other-post', Nostr, 'fake:other-post'),
|
||||
(Web, 'http://po.st', ActivityPub, 'http://localhost/r/http://po.st'),
|
||||
(Web, 'http://po.st', ATProto, 'at://did/web/post'),
|
||||
(Web, 'http://po.st', Fake, 'fake:o:web:http://po.st'),
|
||||
(Web, 'http://po.st', Web, 'http://po.st'),
|
||||
(Nostr, 'nevent456', Fake, 'fake:o:nostr:nevent456'),
|
||||
(Nostr, 'nevent456', ActivityPub,
|
||||
'https://nostr.brid.gy/convert/ap/nevent456'),
|
||||
(Nostr, 'nevent456', ATProto, 'nevent456'),
|
||||
(Nostr, 'nevent456', Web, 'https://nostr.brid.gy/convert/web/nevent456'),
|
||||
]:
|
||||
with self.subTest(from_=from_.LABEL, to=to.LABEL):
|
||||
with self.subTest(id=id, from_=from_.LABEL, to=to.LABEL):
|
||||
self.assertEqual(expected, translate_object_id(
|
||||
id=id, from_=from_, to=to))
|
||||
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ class UserTest(TestCase):
|
|||
user = self.make_user('fake:user', cls=Fake)
|
||||
self.assertEqual('fake:handle:user', user.handle_as(Fake))
|
||||
self.assertEqual('fake:handle:user', user.handle_as('fake'))
|
||||
self.assertEqual('@fake:handle:user@fa.brid.gy', user.handle_as('ap'))
|
||||
self.assertEqual('@fake-handle-user@fa.brid.gy', user.handle_as('ap'))
|
||||
|
||||
def test_handle_as_web_custom_username(self, *_):
|
||||
self.user.obj.our_as1 = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
"""Unit tests for nostr.py."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from flask_app import app
|
||||
import ids
|
||||
from ids import translate_handle, translate_object_id, translate_user_id
|
||||
from models import Target
|
||||
from nostr import Nostr
|
||||
from .testutil import Fake, TestCase
|
||||
|
||||
|
||||
class NostrTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
common.RUN_TASKS_INLINE = False
|
||||
|
|
@ -94,7 +94,7 @@ WEBFINGER_NO_HCARD = {
|
|||
}],
|
||||
}
|
||||
WEBFINGER_FAKE = {
|
||||
'subject': 'acct:fake:handle:user@fa.brid.gy',
|
||||
'subject': 'acct:fake-handle-user@fa.brid.gy',
|
||||
'aliases': ['web:fake:user'],
|
||||
'links': [{
|
||||
'rel': 'canonical_uri',
|
||||
|
|
@ -290,7 +290,7 @@ class WebfingerTest(TestCase):
|
|||
|
||||
def test_missing_user(self):
|
||||
for acct in ('nope.com', 'nope.com@nope.com', 'nope.com@web.brid.gy',
|
||||
'nope.com@fed.brid.gy', 'fake:handle:user@fake.brid.gy'):
|
||||
'nope.com@fed.brid.gy', 'fake-handle-user@fake.brid.gy'):
|
||||
got = self.client.get(f'/.well-known/webfinger?resource=acct:{acct}')
|
||||
self.assertEqual(404, got.status_code)
|
||||
|
||||
|
|
|
|||
|
|
@ -284,12 +284,12 @@ from memcache import (
|
|||
memcache,
|
||||
pickle_memcache,
|
||||
)
|
||||
from web import Web
|
||||
from flask_app import app
|
||||
from nostr import Nostr
|
||||
from web import Web
|
||||
|
||||
ActivityPub.DEFAULT_ENABLED_PROTOCOLS += ('fake', 'other')
|
||||
ATProto.DEFAULT_ENABLED_PROTOCOLS += ('fake', 'other')
|
||||
Web.DEFAULT_ENABLED_PROTOCOLS += ('fake', 'other')
|
||||
for proto in (ActivityPub, ATProto, Nostr, Web):
|
||||
proto.DEFAULT_ENABLED_PROTOCOLS += ('fake', 'other')
|
||||
|
||||
# used in TestCase.make_user() to reuse keys across Users since they're
|
||||
# expensive to generate.
|
||||
|
|
|
|||
2
web.py
2
web.py
|
|
@ -143,7 +143,7 @@ class Web(User, Protocol):
|
|||
"""
|
||||
|
||||
ap_subdomain = ndb.StringProperty(
|
||||
choices=['ap', 'bsky', 'fed', 'web', 'fake', 'other', 'efake'],
|
||||
choices=['ap', 'bsky', 'efake', 'fake', 'fed', 'nostr', 'other', 'web'],
|
||||
default='web')
|
||||
"""Originally, BF served Web users' AP actor ids on fed.brid.gy, eg
|
||||
https://fed.brid.gy/snarfed.org . When we started adding new protocols, we
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue