start on Nostr!

for #446
pull/1962/head
Ryan Barrett 2025-05-30 13:00:50 -07:00
rodzic d97d606f4a
commit 046e91a9c5
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
22 zmienionych plików z 353 dodań i 42 usunięć

Wyświetl plik

@ -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
Wyświetl plik

@ -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()

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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": [
{

Wyświetl plik

@ -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
Wyświetl plik

@ -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:

Wyświetl plik

@ -49,6 +49,7 @@ PROTOCOLS = {label: None for label in (
'atproto',
'bsky',
'ostatus',
'nostr',
'web',
'webmention',
'ui',

Wyświetl plik

@ -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
}

172
nostr.py 100644
Wyświetl plik

@ -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

Wyświetl plik

@ -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,

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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,

Wyświetl plik

@ -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((

Wyświetl plik

@ -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))

Wyświetl plik

@ -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 = {

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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
Wyświetl plik

@ -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