Porównaj commity

...

21 Commity

Autor SHA1 Wiadomość Data
dependabot[bot] eb6d1098de
Merge 2f824410ff into 1686a2ba91 2024-04-21 11:50:51 -07:00
Ryan Barrett 1686a2ba91
opt in/out prompt: accept yes/no DMs to bot users to enable/disable protocols
for #880
2024-04-21 08:08:12 -07:00
Ryan Barrett 0c37d94191
ids.translate_* noop refactoring: from_proto => from_, to_proto => to 2024-04-20 21:03:06 -07:00
Ryan Barrett 7c34689c9f
index.yaml: remove obsolete datastore indices 2024-04-20 21:02:14 -07:00
Ryan Barrett 70da21a7f3
Protocol.receive: send accepts for bot user follows 2024-04-20 21:02:14 -07:00
Ryan Barrett 1981c8eba8
User.get_or_create: propagate obj into existing user 2024-04-19 12:53:44 -07:00
Ryan Barrett 20e061f476
Protocol.receive: extract out maybe_accept_follow method
for #880
2024-04-19 12:53:44 -07:00
Ryan Barrett 3c55d7c145
protocol: extract out enable/disable_protocol methods
for #880
2024-04-19 12:53:44 -07:00
dependabot[bot] 64a196a8c8 build(deps): bump grpcio-status from 1.62.1 to 1.62.2
Bumps [grpcio-status](https://grpc.io) from 1.62.1 to 1.62.2.

---
updated-dependencies:
- dependency-name: grpcio-status
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-19 06:05:10 -07:00
dependabot[bot] 7dbab83a17 build(deps): bump grpcio from 1.62.1 to 1.62.2
Bumps [grpcio](https://github.com/grpc/grpc) from 1.62.1 to 1.62.2.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Changelog](https://github.com/grpc/grpc/blob/master/doc/grpc_release_schedule.md)
- [Commits](https://github.com/grpc/grpc/compare/v1.62.1...v1.62.2)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-19 05:33:19 -07:00
Ryan Barrett 2886ae180d
remove common.ENABLED_PROTOCOLS, use Protocol.DEFAULT_ENABLED_PROTOCOLS instead
also use is_enabled_to in user page template
2024-04-18 16:39:15 -07:00
Ryan Barrett 8bcae4c09d
Protocol.receive: following protocol user enables that protocol
for #880
2024-04-18 16:09:09 -07:00
Ryan Barrett d36885728f
Protocol.receive: blocking protocol user disables that protocol
for #880
2024-04-18 16:03:51 -07:00
Ryan Barrett 917732ad4b
demote models import in ids.py to top-level to avoid circular import 2024-04-18 07:09:52 -07:00
Ryan Barrett 5556f2756b
add cron jobs for ATProto polling posts and notifications
fixes #942
2024-04-17 20:20:50 -07:00
Ryan Barrett 8077a7f4ca
remove activitypub from ATProto.enabled_protocols, for now 2024-04-17 19:17:04 -07:00
Ryan Barrett 39a641e000
remove USER_ALLOWLIST in favor of User.enabled_protocols 2024-04-17 17:02:17 -07:00
Ryan Barrett 259b7d72dd
start on conditional opt in
* add Protocol.DEFAULT_ENABLED_PROTOCOLS
* add User.enabled_protocols
* move common.is_enabled to Protocol.is_enabled_to, include opt out/in
2024-04-17 16:43:10 -07:00
Ryan Barrett f02ba80304
switch from gcloud datastore emulator to firestore emulator
...since the datastore emulator evidently doesn't support != query filters: https://github.com/googleapis/python-ndb/issues/962
2024-04-17 11:36:28 -07:00
Ryan Barrett 393605bde9
change ATProto.ABBREV to bsky
🤞, for #961
2024-04-17 06:54:16 -07:00
dependabot[bot] 2f824410ff
build(deps): bump protobuf from 4.24.3 to 5.26.1
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 4.24.3 to 5.26.1.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v4.24.3...v5.26.1)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-01 12:10:44 +00:00
25 zmienionych plików z 558 dodań i 349 usunięć

Wyświetl plik

@ -22,7 +22,7 @@ jobs:
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates gnupg google-cloud-sdk google-cloud-sdk-datastore-emulator default-jre
sudo apt-get install -y apt-transport-https ca-certificates gnupg google-cloud-sdk google-cloud-cli-firestore-emulator default-jre
- run:
name: Python dependencies
@ -40,7 +40,7 @@ jobs:
- run:
name: Build and test
command: |
CLOUDSDK_CORE_PROJECT=bridgy-federated gcloud beta emulators datastore start --no-store-on-disk --use-firestore-in-datastore-mode --host-port=localhost:8089 < /dev/null >& /dev/null &
CLOUDSDK_CORE_PROJECT=brid-gy gcloud emulators firestore start --host-port=:8089 --database-mode=datastore-mode < /dev/null >& /tmp/firestore-emulator.log &
sleep 5s
python -m coverage run --source=. --omit=appengine_config.py,logs.py,tests/\* -m unittest discover -v
python -m coverage html -d /tmp/coverage_html

Wyświetl plik

@ -12,7 +12,8 @@ Development
---
Development reference docs are at [bridgy-fed.readthedocs.io](https://bridgy-fed.readthedocs.io/). Pull requests are welcome! Feel free to [ping me in #indieweb-dev](https://indieweb.org/discuss) with any questions.
First, fork and clone this repo. Then, install the [Google Cloud SDK](https://cloud.google.com/sdk/) and run `gcloud components install beta cloud-datastore-emulator` to install the [datastore emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator). Once you have them, set up your environment by running these commands in the repo root directory:
First, fork and clone this repo. Then, install the [Google Cloud SDK](https://cloud.google.com/sdk/) and run `gcloud components install cloud-firestore-emulator` to install the [Firestore emulator](https://cloud.google.com/firestore/docs/emulator). Once you have them, set up your environment by running these commands in the repo root directory:
```sh
gcloud config set project bridgy-federated
@ -24,7 +25,7 @@ pip install -r requirements.txt
Now, run the tests to check that everything is set up ok:
```shell
gcloud beta emulators datastore start --use-firestore-in-datastore-mode --no-store-on-disk --host-port=localhost:8089 --quiet < /dev/null >& /dev/null &
gcloud emulators firestore start --host-port=:8089 --database-mode=datastore-mode < /dev/null >& /dev/null &
python3 -m unittest discover
```

Wyświetl plik

@ -30,6 +30,7 @@ from common import (
host_url,
LOCAL_DOMAINS,
PRIMARY_DOMAIN,
PROTOCOL_DOMAINS,
redirect_wrap,
subdomain_wrap,
unwrap,
@ -56,6 +57,8 @@ WEB_OPT_OUT_DOMAINS = None
FEDI_URL_RE = re.compile(r'https://[^/]+/(@|users/)([^/@]+)(@[^/@]+)?(/(?:statuses/)?[0-9]+)?')
_BOT_ACTOR_IDS = None
def instance_actor():
global _INSTANCE_ACTOR
@ -65,6 +68,15 @@ def instance_actor():
return _INSTANCE_ACTOR
def bot_actor_ids():
global _BOT_ACTOR_IDS
if _BOT_ACTOR_IDS is None:
from activitypub import ActivityPub
_BOT_ACTOR_IDS = [translate_user_id(id=domain, from_=Web, to=ActivityPub)
for domain in PROTOCOL_DOMAINS]
return _BOT_ACTOR_IDS
class ActivityPub(User, Protocol):
"""ActivityPub protocol class.
@ -75,6 +87,7 @@ class ActivityPub(User, Protocol):
LOGO_HTML = '<img src="/static/fediverse_logo.svg">'
CONTENT_TYPE = as2.CONTENT_TYPE_LD_PROFILE
HAS_FOLLOW_ACCEPTS = True
DEFAULT_ENABLED_PROTOCOLS = ('web',)
def _pre_put_hook(self):
"""Validate id, require URL, don't allow Bridgy Fed domains.
@ -363,7 +376,7 @@ class ActivityPub(User, Protocol):
from_proto = PROTOCOLS.get(obj.source_protocol)
user_id = from_user.key.id() if from_user and from_user.key else None
# TODO: uncomment
# if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
# if from_proto and not from_proto.is_enabled_to(cls, user=from_user):
# error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
if obj.as2:
@ -832,8 +845,6 @@ def actor(handle_or_id):
cls = Protocol.for_request(fed='web')
if not cls:
error(f"Couldn't determine protocol", status=404)
elif not common.is_enabled(cls, ActivityPub, handle_or_id=handle_or_id):
error(f'{cls.LABEL} <=> activitypub not enabled')
elif cls.LABEL == 'web' and request.path.startswith('/ap/'):
# we started out with web users' AP ids as fed.brid.gy/[domain], so we
# need to preserve those for backward compatibility
@ -851,6 +862,9 @@ def actor(handle_or_id):
id = handle_or_id
assert id
if not cls.is_enabled_to(ActivityPub, user=id):
error(f'{cls.LABEL} user {id} not found', status=404)
user = cls.get_or_create(id)
if not user:
error(f'{cls.LABEL} user {id} not found', status=404)
@ -924,6 +938,7 @@ def inbox(protocol=None, id=None):
# follows, or other activity types, since Mastodon doesn't currently mark
# those as explicitly public. Use as2's is_public instead of as1's because
# as1's interprets unlisted as true.
# TODO: move this to Protocol
if type == 'Create' and not as2.is_public(activity, unlisted=False):
logger.info('Dropping non-public activity')
return 'OK'

Wyświetl plik

@ -34,7 +34,6 @@ from common import (
DOMAINS,
error,
USER_AGENT,
USER_ALLOWLIST,
)
import flask_app
from models import Object, PROTOCOLS, Target, User
@ -86,12 +85,18 @@ class ATProto(User, Protocol):
Key id is DID, currently either did:plc or did:web.
https://atproto.com/specs/did
"""
ABBREV = 'atproto'
ABBREV = 'bsky'
# TODO: add second bsky label? inject into PROTOCOLS?
PHRASE = 'Bluesky'
LOGO_HTML = '<img src="/oauth_dropins_static/bluesky.svg">'
PDS_URL = f'https://{ABBREV}{common.SUPERDOMAIN}/'
# note that PDS hostname is atproto.brid.gy here, not bsky.brid.gy. Bluesky
# team currently has our hostname as atproto.brid.gy in their federation
# test.
# TODO: switch to bsky.brid.gy once they lift their federation limits? we'd
# need to update serviceEndpoint in all users' DID docs. :/
PDS_URL = f'https://atproto{common.SUPERDOMAIN}/'
CONTENT_TYPE = 'application/json'
DEFAULT_ENABLED_PROTOCOLS = ()
def _pre_put_hook(self):
"""Validate id, require did:plc or non-blocklisted did:web."""
@ -501,9 +506,8 @@ class ATProto(User, Protocol):
dict: JSON object
"""
from_proto = PROTOCOLS.get(obj.source_protocol)
user_id = from_user.key.id() if from_user and from_user.key else None
# TODO: uncomment
# if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
# if from_proto and not from_proto.is_enabled_to(cls, user=from_user):
# error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
if obj.bsky:
@ -575,8 +579,7 @@ def poll_notifications():
headers={'User-Agent': USER_AGENT})
for user in users:
# TODO: remove for launch
if not DEBUG and user.key.id() not in USER_ALLOWLIST:
if not user.is_enabled_to(ATProto, user=user):
logger.info(f'Skipping {user.key.id()}')
continue
@ -635,8 +638,7 @@ def poll_posts():
headers={'User-Agent': USER_AGENT})
for user in users:
# TODO: remove for launch
if not DEBUG and user.key.id() not in USER_ALLOWLIST:
if not user.is_enabled_to(ATProto, user=user):
logger.info(f'Skipping {user.key.id()}')
continue

Wyświetl plik

@ -31,13 +31,6 @@ TLD_BLOCKLIST = ('7z', 'asp', 'aspx', 'gif', 'html', 'ico', 'jpg', 'jpeg', 'js',
CONTENT_TYPE_HTML = 'text/html; charset=utf-8'
# Protocol pairs that we currently support bridging between. Values must be
# Protocol LABELs. Each pair must be lexicographically sorted!
ENABLED_BRIDGES = frozenset((
('activitypub', 'web'),
('atproto', 'web'),
))
PRIMARY_DOMAIN = 'fed.brid.gy'
# protocol-specific subdomains are under this "super"domain
SUPERDOMAIN = '.brid.gy'
@ -83,15 +76,6 @@ util.set_user_agent(USER_AGENT)
TASKS_LOCATION = 'us-central1'
RUN_TASKS_INLINE = False # overridden by unit tests
USER_ALLOWLIST = (
'snarfed.org',
'did:plc:fdme4gb7mu7zrie7peay7tst',
'snarfed.bsky.social',
'did:plc:3ljmtyyjqcjee2kpewgsifvb',
'https://indieweb.social/users/snarfed',
'@snarfed@indieweb.social',
)
def base64_to_long(x):
"""Converts from URL safe base64 encoding to long integer.
@ -264,29 +248,13 @@ def add(seq, val):
seq.append(val)
def is_enabled(proto_a, proto_b, handle_or_id=None):
"""Returns True if bridging the two input protocols is enabled, False otherwise.
def remove(seq, val):
"""Removes ``val`` to ``seq`` if seq contains it.
Args:
proto_a (Protocol subclass)
proto_b (Protocol subclass)
handle_or_id (str): optional user handle or id
Returns:
bool:
Useful for treating repeated ndb properties like sets instead of lists.
"""
if proto_a == proto_b:
return True
labels = tuple(sorted((proto_a.LABEL, proto_b.LABEL)))
if DEBUG and ('fake' in labels or 'other' in labels):
return True
if handle_or_id in USER_ALLOWLIST:
return True
return labels in ENABLED_BRIDGES
if val in seq:
seq.remove(val)
def create_task(queue, delay=None, **params):

13
cron.yaml 100644
Wyświetl plik

@ -0,0 +1,13 @@
# timezone defaults to UTC
# docs: https://cloud.google.com/appengine/docs/standard/python3/scheduling-jobs-with-cron-yaml
cron:
- description: ATProto poll posts
url: /queue/atproto-poll-posts
schedule: every 15 minutes
target: hub
- description: ATProto poll notifications
url: /queue/atproto-poll-notifs
schedule: every 15 minutes
target: hub

Wyświetl plik

@ -1,25 +1,27 @@
Bridgy Fed developer documentation
----------------------------------
Bridgy Fed connects your web site to
`Mastodon <https://joinmastodon.org>`__ and the
`fediverse <https://en.wikipedia.org/wiki/Fediverse>`__ via
`ActivityPub <https://activitypub.rocks/>`__,
`webmentions <https://webmention.net/>`__, and
`microformats2 <https://microformats.org/wiki/microformats2>`__. Your
site gets its own fediverse profile, posts and avatar and header and
all. Bridgy Fed translates likes, reposts, mentions, follows, and more
back and forth. `See the user docs <https://fed.brid.gy/docs>`__ and
`developer docs <https://bridgy-fed.readthedocs.io/>`__ for more
details.
Bridgy Fed connects different decentralized social network protocols. It
currently supports the
`fediverse <https://en.wikipedia.org/wiki/Fediverse>`__ (eg
`Mastodon <https://joinmastodon.org>`__) via
`ActivityPub <https://activitypub.rocks/>`__, and the
`IndieWeb <https://indieweb.org/>`__ via
`webmentions <https://webmention.net/>`__ and
`microformats2 <https://microformats.org/wiki/microformats2>`__.
`Bluesky/AT
Protocol <https://github.com/snarfed/bridgy-fed/issues/381>`__ and
`Nostr <https://github.com/snarfed/bridgy-fed/issues/446>`__ are planned
for 2024. Bridgy Fed translates profiles, likes, reposts, mentions,
follows, and more from any supported network to any other. `See the user
docs <https://fed.brid.gy/docs>`__ and `developer
docs <https://bridgy-fed.readthedocs.io/>`__ for more details.
https://fed.brid.gy/
Also see the
`original <https://snarfed.org/indieweb-activitypub-bridge>`__
`design <https://snarfed.org/indieweb-ostatus-bridge>`__ blog posts.
License: This project is placed in the public domain.
License: This project is placed in the public domain. You may also use
it under the `CC0
License <https://creativecommons.org/publicdomain/zero/1.0/>`__.
Development
-----------
@ -31,11 +33,11 @@ requests are welcome! Feel free to `ping me in
First, fork and clone this repo. Then, install the `Google Cloud
SDK <https://cloud.google.com/sdk/>`__ and run
``gcloud components install beta cloud-datastore-emulator`` to install
the `datastore
emulator <https://cloud.google.com/datastore/docs/tools/datastore-emulator>`__.
Once you have them, set up your environment by running these commands in
the repo root directory:
``gcloud components install cloud-firestore-emulator`` to install the
`Firestore
emulator <https://cloud.google.com/firestore/docs/emulator>`__. Once you
have them, set up your environment by running these commands in the repo
root directory:
.. code:: sh
@ -48,7 +50,7 @@ Now, run the tests to check that everything is set up ok:
.. code:: shell
gcloud beta emulators datastore start --use-firestore-in-datastore-mode --no-store-on-disk --host-port=localhost:8089 --quiet < /dev/null >& /dev/null &
gcloud emulators firestore start --host-port=:8089 --database-mode=datastore-mode < /dev/null >& /dev/null &
python3 -m unittest discover
Finally, run this in the repo root directory to start the web app
@ -112,8 +114,9 @@ How to add a new protocol
5. TODO: add a new usage section to the docs for the new protocol.
6. TODO: does the new protocol need any new UI or signup functionality?
Unusual, but not impossible. Add that if necessary.
7. Add the new protocols logo to ``static/``, use it in
`templates/user.html <https://github.com/snarfed/bridgy-fed/blob/main/templates/user.html>`__.
7. Protocol logos may be emoji or image files. If this one is a file,
add it ``static/``. Then add the emoji or file ``<img>`` tag in the
``Protocol`` subclasss ``LOGO_HTML`` constant.
Stats
-----

4
hub.py
Wyświetl plik

@ -60,11 +60,11 @@ lexrpc.flask_server.init_flask(arroba.server.server, app)
app.add_url_rule('/queue/atproto-poll-notifs',
view_func=atproto.poll_notifications,
methods=['POST'])
methods=['GET', 'POST'])
app.add_url_rule('/queue/atproto-poll-posts',
view_func=atproto.poll_posts,
methods=['POST'])
methods=['GET', 'POST'])
@app.post('/queue/atproto-commit')
@flask_util.cloud_tasks_only

102
ids.py
Wyświetl plik

@ -53,30 +53,30 @@ def web_ap_base_domain(user_domain):
return f'https://{subdomain}{SUPERDOMAIN}/'
def translate_user_id(*, id, from_proto, to_proto):
def translate_user_id(*, id, from_, to):
"""Translate a user id from one protocol to another.
TODO: unify with :func:`translate_object_id`.
Args:
id (str)
from_proto (protocol.Protocol)
to_proto (protocol.Protocol)
from_ (protocol.Protocol)
to (protocol.Protocol)
Returns:
str: the corresponding id in ``to_proto``
str: the corresponding id in ``to``
"""
assert id and from_proto and to_proto, (id, from_proto, to_proto)
assert from_proto.owns_id(id) is not False or from_proto.LABEL == 'ui', \
(id, from_proto.LABEL, to_proto.LABEL)
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)
parsed = urlparse(id)
if from_proto.LABEL == 'web' and parsed.path.strip('/') == '':
if from_.LABEL == 'web' and parsed.path.strip('/') == '':
# home page; replace with domain
id = parsed.netloc
# bsky.app profile URL to DID
if to_proto.LABEL == 'atproto':
if to.LABEL == 'atproto':
if match := BSKY_APP_URL_RE.match(id):
repo = match.group('id')
if repo.startswith('did:'):
@ -89,25 +89,25 @@ def translate_user_id(*, id, from_proto, to_proto):
logger.warning(e)
return None
if from_proto == to_proto:
if from_ == to:
return id
# follow use_instead
user = from_proto.get_by_id(id)
user = from_.get_by_id(id)
if user:
id = user.key.id()
if from_proto.LABEL in COPIES_PROTOCOLS or to_proto.LABEL in COPIES_PROTOCOLS:
if from_.LABEL in COPIES_PROTOCOLS or to.LABEL in COPIES_PROTOCOLS:
if user:
if copy := user.get_copy(to_proto):
if copy := user.get_copy(to):
return copy
if orig := models.get_original(id):
if isinstance(orig, to_proto):
if isinstance(orig, to):
return orig.key.id()
match from_proto.LABEL, to_proto.LABEL:
match from_.LABEL, to.LABEL:
case _, 'atproto' | 'nostr':
logger.warning(f"Can't translate user id {id} to {to_proto.LABEL} , haven't copied it there yet!")
logger.warning(f"Can't translate user id {id} to {to.LABEL} , haven't copied it there yet!")
return None
case 'web', 'activitypub':
@ -117,45 +117,45 @@ def translate_user_id(*, id, from_proto, to_proto):
return id
case _, 'activitypub' | 'web':
return subdomain_wrap(from_proto, f'/{to_proto.ABBREV}/{id}')
return subdomain_wrap(from_, f'/{to.ABBREV}/{id}')
# only for unit tests
case _, 'fake' | 'other':
return f'{to_proto.LABEL}:u:{id}'
case _, 'fake' | 'other' | 'eefake':
return f'{to.LABEL}:u:{id}'
case 'fake' | 'other', _:
return id
assert False, (id, from_proto.LABEL, to_proto.LABEL)
assert False, (id, from_.LABEL, to.LABEL)
def translate_handle(*, handle, from_proto, to_proto, enhanced):
def translate_handle(*, handle, from_, to, enhanced):
"""Translates a user handle from one protocol to another.
Args:
handle (str)
from_proto (protocol.Protocol)
to_proto (protocol.Protocol)
from_ (protocol.Protocol)
to (protocol.Protocol)
enhanced (bool): whether to convert to an "enhanced" handle based on the
user's domain
Returns:
str: the corresponding handle in ``to_proto``
str: the corresponding handle in ``to``
"""
assert handle and from_proto and to_proto, (handle, from_proto, to_proto)
assert from_proto.owns_handle(handle) is not False or from_proto.LABEL == 'ui'
assert handle and from_ and to, (handle, from_, to)
assert from_.owns_handle(handle) is not False or from_.LABEL == 'ui'
if from_proto == to_proto:
if from_ == to:
return handle
match from_proto.LABEL, to_proto.LABEL:
match from_.LABEL, to.LABEL:
case _, 'activitypub':
domain = handle if enhanced else f'{from_proto.ABBREV}{SUPERDOMAIN}'
domain = handle if enhanced else f'{from_.ABBREV}{SUPERDOMAIN}'
return f'@{handle}@{domain}'
case _, 'atproto' | 'nostr':
handle = handle.lstrip('@').replace('@', '.')
return (handle if enhanced
else f'{handle}.{from_proto.ABBREV}{SUPERDOMAIN}')
else f'{handle}.{from_.ABBREV}{SUPERDOMAIN}')
case 'activitypub', 'web':
user, instance = handle.lstrip('@').split('@')
@ -167,32 +167,30 @@ def translate_handle(*, handle, from_proto, to_proto, enhanced):
return handle
# only for unit tests
case _, 'fake':
return f'fake:handle:{handle}'
case _, 'other':
return f'other:handle:{handle}'
case _, 'fake' | 'other' | 'eefake':
return f'{to.LABEL}:handle:{handle}'
assert False, (handle, from_proto.LABEL, to_proto.LABEL)
assert False, (handle, from_.LABEL, to.LABEL)
def translate_object_id(*, id, from_proto, to_proto):
def translate_object_id(*, id, from_, to):
"""Translates a user handle from one protocol to another.
TODO: unify with :func:`translate_user_id`.
Args:
id (str)
from_proto (protocol.Protocol)
to_proto (protocol.Protocol)
from_ (protocol.Protocol)
to (protocol.Protocol)
Returns:
str: the corresponding id in ``to_proto``
str: the corresponding id in ``to``
"""
assert id and from_proto and to_proto, (id, from_proto, to_proto)
assert from_proto.owns_id(id) is not False or from_proto.LABEL == 'ui'
assert id and from_ and to, (id, from_, to)
assert from_.owns_id(id) is not False or from_.LABEL == 'ui'
# bsky.app profile URL to DID
if to_proto.LABEL == 'atproto':
if to.LABEL == 'atproto':
if match := BSKY_APP_URL_RE.match(id):
repo = match.group('id')
handle = None
@ -207,29 +205,29 @@ def translate_object_id(*, id, from_proto, to_proto):
return web_url_to_at_uri(id, handle=handle, did=repo)
if from_proto == to_proto:
if from_ == to:
return id
if from_proto.LABEL in COPIES_PROTOCOLS or to_proto.LABEL in COPIES_PROTOCOLS:
if obj := from_proto.load(id, remote=False):
if copy := obj.get_copy(to_proto):
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):
return copy
if orig := models.get_original(id):
return orig.key.id()
match from_proto.LABEL, to_proto.LABEL:
match from_.LABEL, to.LABEL:
case _, 'atproto' | 'nostr':
logger.warning(f"Can't translate object id {id} to {to_proto.LABEL} , haven't copied it there yet!")
logger.warning(f"Can't translate object id {id} to {to.LABEL} , haven't copied it there yet!")
return id
case 'web', 'activitypub':
return urljoin(web_ap_base_domain(util.domain_from_link(id)), f'/r/{id}')
case _, 'activitypub' | 'web':
return subdomain_wrap(from_proto, f'/convert/{to_proto.ABBREV}/{id}')
return subdomain_wrap(from_, f'/convert/{to.ABBREV}/{id}')
# only for unit tests
case _, 'fake' | 'other':
return f'{to_proto.LABEL}:o:{from_proto.ABBREV}:{id}'
case _, 'fake' | 'other' | 'eefake':
return f'{to.LABEL}:o:{from_.ABBREV}:{id}'
assert False, (id, from_proto.LABEL, to_proto.LABEL)
assert False, (id, from_.LABEL, to.LABEL)

Wyświetl plik

@ -6,48 +6,6 @@ indexes:
# index.yaml file manually, remove the "# AUTOGENERATED" marker line above.
# If you want to manage some indexes manually, move them above the marker line.
- kind: Object
properties:
- name: domains
- name: labels
- name: updated
direction: asc
- kind: Object
properties:
- name: users
- name: labels
- name: updated
direction: asc
- kind: Object
properties:
- name: domains
- name: labels
- name: updated
direction: desc
- kind: Object
properties:
- name: users
- name: labels
- name: updated
direction: desc
- kind: Object
properties:
- name: domains
- name: labels
- name: created
direction: desc
- kind: Object
properties:
- name: users
- name: labels
- name: created
direction: desc
- kind: Object
properties:
- name: users
@ -59,18 +17,6 @@ indexes:
- name: updated
direction: desc
# these two are currently unused! as of 2024-01-24
- kind: Object
properties:
- name: users
- name: created
- kind: Object
properties:
- name: users
- name: created
direction: desc
- kind: Object
properties:
- name: notify
@ -93,6 +39,12 @@ indexes:
- name: created
direction: desc
- kind: Follower
properties:
- name: from
- name: status
- name: updated
- kind: Follower
properties:
- name: from
@ -100,6 +52,12 @@ indexes:
- name: updated
direction: desc
- kind: Follower
properties:
- name: to
- name: status
- name: updated
- kind: Follower
properties:
- name: to

Wyświetl plik

@ -28,8 +28,8 @@ from oauth_dropins.webutil.models import (
from oauth_dropins.webutil.util import json_dumps, json_loads
import common
from common import add, base64_to_long, DOMAIN_RE, long_to_base64, unwrap
from ids import translate_handle, translate_object_id, translate_user_id
from common import add, base64_to_long, DOMAIN_RE, long_to_base64, remove, unwrap
import ids
# maps string label to Protocol subclass. populated by ProtocolUserMeta.
# seed with old and upcoming protocols that don't have their own classes (yet).
@ -114,6 +114,8 @@ def reset_protocol_properties():
'protocol', choices=list(PROTOCOLS.keys()), required=True)
Object.source_protocol = ndb.StringProperty(
'source_protocol', choices=list(PROTOCOLS.keys()))
User.enabled_protocols = ndb.StringProperty(
'enabled_protocols', choices=list(PROTOCOLS.keys()), repeated=True)
abbrevs = f'({"|".join(PROTOCOLS.keys())}|fed)'
common.SUBDOMAIN_BASE_URL_RE = re.compile(
@ -157,6 +159,11 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
# #nobridge in their profile
manual_opt_out = ndb.BooleanProperty()
# protocols that this user has explicitly opted into. protocols that don't
# require explicit opt in are omitted here. choices is populated in
# reset_protocol_properties.
enabled_protocols = ndb.StringProperty(repeated=True, choices=[])
created = ndb.DateTimeProperty(auto_now_add=True)
updated = ndb.DateTimeProperty(auto_now=True)
@ -222,13 +229,16 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if user.status == 'opt-out':
return None
user.existing = True
# override direct from False => True if set
# TODO: propagate more props into user?
direct = kwargs.get('direct')
if direct and not user.direct:
logger.info(f'Setting {user.key} direct={direct}')
user.direct = direct
user.put()
# TODO: propagate more fields?
for field in ['direct', 'obj', 'obj_key']:
old_val = getattr(user, field, None)
new_val = kwargs.get(field)
if ((old_val is None and new_val is not None)
or (field == 'direct' and not old_val and new_val)):
setattr(user, field, new_val)
user.put()
if not propagate:
return user
else:
@ -241,7 +251,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
ATProto = PROTOCOLS['atproto']
if propagate and cls.LABEL != 'atproto' and not user.get_copy(ATProto):
if common.is_enabled(cls, ATProto, handle_or_id=id):
if cls.is_enabled_to(ATProto, user=id):
ATProto.create_for(user)
else:
logger.info(f'{cls.LABEL} <=> atproto not enabled, skipping')
@ -341,6 +351,32 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
return None
@ndb.transactional()
def enable_protocol(self, to_proto):
"""Adds ``to_proto` to :attr:`enabled_protocols`.
Args:
to_proto (:class:`protocol.Protocol` subclass)
"""
user = self.key.get()
add(user.enabled_protocols, to_proto.LABEL)
user.put()
add(self.enabled_protocols, to_proto.LABEL)
@ndb.transactional()
def disable_protocol(self, to_proto):
"""Removes ``to_proto` from :attr:`enabled_protocols`.
Args:
to_proto (:class:`protocol.Protocol` subclass)
"""
user = self.key.get()
remove(user.enabled_protocols, to_proto.LABEL)
user.put()
remove(self.enabled_protocols, to_proto.LABEL)
def handle_as(self, to_proto):
"""Returns this user's handle in a different protocol.
@ -359,8 +395,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if not handle:
return None
return translate_handle(handle=handle, from_proto=self.__class__,
to_proto=to_proto, enhanced=False)
return ids.translate_handle(handle=handle, from_=self.__class__,
to=to_proto, enhanced=False)
def id_as(self, to_proto):
"""Returns this user's id in a different protocol.
@ -374,8 +410,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if isinstance(to_proto, str):
to_proto = PROTOCOLS[to_proto]
return translate_user_id(id=self.key.id(), from_proto=self.__class__,
to_proto=to_proto)
return ids.translate_user_id(id=self.key.id(), from_=self.__class__,
to=to_proto)
def handle_or_id(self):
"""Returns handle if we know it, otherwise id."""
@ -535,9 +571,6 @@ class Object(StringIdModel):
'feed', 'notification', 'user')
# Keys for user(s) who created or otherwise own this activity.
#
# DEPRECATED: this used to include all users related the activity, including
# followers, but we've now moved those to the notify and feed properties.
users = ndb.KeyProperty(repeated=True)
# User keys who should see this activity in their user page, eg in reply to,
# reaction to, share of, etc.
@ -551,8 +584,8 @@ class Object(StringIdModel):
domains = ndb.StringProperty(repeated=True)
status = ndb.StringProperty(choices=STATUSES)
# choices is populated in app, after all User subclasses are created,
# so that PROTOCOLS is fully populated
# choices is populated in reset_protocol_properties, after all User
# subclasses are created, so that PROTOCOLS is fully populated.
# TODO: nail down whether this is ABBREV or LABEL
source_protocol = ndb.StringProperty(choices=[])
labels = ndb.StringProperty(repeated=True, choices=LABELS)
@ -921,7 +954,7 @@ class Object(StringIdModel):
original objects in their source protocol, eg
``at://did:plc:abc/app.bsky.feed.post/123`` => ``https://mas.to/@user/456``.
* Bridgy Fed subdomain URLs to the ids embedded inside them, eg
``https://atproto.brid.gy/ap/did:plc:xyz`` => ``did:plc:xyz``
``https://bsky.brid.gy/ap/did:plc:xyz`` => ``did:plc:xyz``
* ATProto bsky.app URLs to their DIDs or `at://` URIs, eg
``https://bsky.app/profile/a.com`` => ``did:plc:123``
@ -1044,7 +1077,7 @@ class Object(StringIdModel):
if not proto:
return val
translated = translate_fn(id=orig, from_proto=proto, to_proto=proto)
translated = translate_fn(id=orig, from_=proto, to=proto)
if translated and translated != orig:
logger.info(f'Normalized {proto.LABEL} id {orig} to {translated}')
replaced = True
@ -1060,20 +1093,21 @@ class Object(StringIdModel):
for obj in [outer_obj] + inner_objs:
for tag in as1.get_objects(obj, 'tags'):
if tag.get('objectType') == 'mention':
tag['url'] = replace(tag.get('url'), translate_user_id)
tag['url'] = replace(tag.get('url'), ids.translate_user_id)
for field in ['actor', 'author', 'inReplyTo']:
fn = translate_object_id if field == 'inReplyTo' else translate_user_id
fn = (ids.translate_object_id if field == 'inReplyTo'
else ids.translate_user_id)
obj[field] = [replace(val, fn) for val in util.get_list(obj, field)]
if len(obj[field]) == 1:
obj[field] = obj[field][0]
outer_obj['object'] = []
for inner_obj in inner_objs:
translate_fn = (translate_user_id
translate_fn = (ids.translate_user_id
if (as1.object_type(inner_obj) in as1.ACTOR_TYPES
or as1.object_type(outer_obj) in
('follow', 'stop-following'))
else translate_object_id)
else ids.translate_object_id)
got = replace(inner_obj, translate_fn)
if isinstance(got, dict) and util.trim_nulls(got).keys() == {'id'}:
@ -1180,7 +1214,7 @@ class Follower(ndb.Model):
query = Follower.query(
Follower.status == 'active',
filter_prop == user.key,
).order(-Follower.updated)
)
followers, before, after = fetch_page(query, Follower, by=Follower.updated)
users = ndb.get_multi(f.from_ if collection == 'followers' else f.to

Wyświetl plik

@ -12,14 +12,23 @@ from google.cloud import ndb
from google.cloud.ndb import OR
from google.cloud.ndb.model import _entity_to_protobuf
from granary import as1
from oauth_dropins.webutil.appengine_info import DEBUG
from oauth_dropins.webutil.flask_util import cloud_tasks_only
from oauth_dropins.webutil import util
from oauth_dropins.webutil import models
from oauth_dropins.webutil import util
from oauth_dropins.webutil.util import json_dumps, json_loads
import werkzeug.exceptions
import common
from common import add, DOMAIN_BLOCKLIST, DOMAIN_RE, DOMAINS, error, subdomain_wrap
from common import (
add,
DOMAIN_BLOCKLIST,
DOMAIN_RE,
DOMAINS,
error,
PROTOCOL_DOMAINS,
subdomain_wrap,
)
from flask_app import app
from ids import translate_object_id, translate_user_id
from models import Follower, get_originals, Object, PROTOCOLS, Target, User
@ -28,6 +37,7 @@ SUPPORTED_TYPES = (
'accept',
'article',
'audio',
'block',
'comment',
'delete',
'follow',
@ -70,6 +80,8 @@ class Protocol:
appropriate for the ``Content-Type`` HTTP header.
HAS_FOLLOW_ACCEPTS (bool): whether this protocol supports explicit
accept/reject activities in response to follows, eg ActivityPub
DEFAULT_ENABLED_PROTOCOLS (list of str): labels of other protocols that
are automatically enabled for this protocol to bridge into
"""
ABBREV = None
PHRASE = None
@ -77,6 +89,7 @@ class Protocol:
LOGO_HTML = ''
CONTENT_TYPE = None
HAS_FOLLOW_ACCEPTS = False
DEFAULT_ENABLED_PROTOCOLS = ()
def __init__(self):
assert False
@ -126,6 +139,52 @@ class Protocol:
label = domain.removesuffix(common.SUPERDOMAIN)
return PROTOCOLS.get(label)
@classmethod
def is_enabled_to(from_cls, to_cls, user=None):
"""Returns True if two protocols, and optionally a user, can be bridged.
Reasons this might return False:
* We haven't turned on bridging these two protocols yet.
* The user is opted out.
* The user is on a domain that's opted out.
* The from protocol requires opt in, and the user hasn't opted in.
Args:
from_cls (Protocol subclass)
to_cls (Protocol subclass)
user (:class:`models.User` or str): optional, user or id
Returns:
bool:
"""
if from_cls == to_cls:
return True
from_label = from_cls.LABEL
to_label = to_cls.LABEL
if DEBUG and (from_label in ('fake', 'other')
or (to_label in ('fake', 'other') and from_label != 'eefake')):
return True
user_id = None
if isinstance(user, User):
user_id = user.key.id() if user.key else None
elif isinstance(user, str):
user_id = user
user = from_cls.get_by_id(user_id, allow_opt_out=True)
if user:
if user.status == 'opt-out':
return False
elif to_label in user.enabled_protocols:
return True
if to_label in from_cls.DEFAULT_ENABLED_PROTOCOLS:
return True
return False
@classmethod
def owns_id(cls, id):
"""Returns whether this protocol owns the id, or None if it's unclear.
@ -497,7 +556,7 @@ class Protocol:
For example, if ``proto`` is :class:`ActivityPub`, the ATProto URI
``at://did:plc:abc/coll/123`` will be converted to
``https://atproto.brid.gy/ap/at://did:plc:abc/coll/123``.
``https://bsky.brid.gy/ap/at://did:plc:abc/coll/123``.
Wraps these AS1 fields:
@ -539,7 +598,7 @@ class Protocol:
# TODO: what if from_cls is None? relax translate_object_id,
# make it a noop if we don't know enough about from/to?
if from_cls and from_cls != to_cls:
elem[field]['id'] = fn(id=id, from_proto=from_cls, to_proto=to_cls)
elem[field]['id'] = fn(id=id, from_=from_cls, to=to_cls)
if elem[field].keys() == {'id'}:
elem[field] = elem[field]['id']
@ -736,6 +795,29 @@ class Protocol:
# fall through to deliver to followers
elif obj.type == 'block':
proto = Protocol.for_bridgy_subdomain(inner_obj_id)
if not proto:
logger.info("Ignoring block, target isn't one of our protocol domains")
return 'OK', 200
from_user.disable_protocol(proto)
return 'OK', 200
elif obj.type == 'post':
to_cc = (util.get_list(inner_obj_as1, 'to')
+ util.get_list(inner_obj_as1, 'cc'))
if len(to_cc) == 1 and to_cc[0] in PROTOCOL_DOMAINS:
content = inner_obj_as1.get('content').strip().lower()
logger.info(f'DM to bot user {to_cc}: {content}')
proto = Protocol.for_bridgy_subdomain(to_cc[0])
assert proto
if content in ('yes', 'ok'):
from_user.enable_protocol(proto)
elif content == 'no':
from_user.disable_protocol(proto)
return 'OK', 200
# fetch actor if necessary
if actor and actor.keys() == set(['id']):
logger.info('Fetching actor so we have name, profile photo, etc')
@ -759,6 +841,12 @@ class Protocol:
}
if obj.type == 'follow':
proto = Protocol.for_bridgy_subdomain(inner_obj_id)
if proto:
# follow of one of our protocol users; enable that protocol.
# foll through so that we send an accept.
from_user.enable_protocol(proto)
from_cls.handle_follow(obj)
# deliver to targets
@ -790,14 +878,11 @@ class Protocol:
from_obj.our_as1 = from_as1
from_obj.put()
from_target = from_cls.target_for(from_obj)
if not from_target:
error(f"Couldn't find delivery target for follower {from_obj}")
from_key = from_cls.key_for(from_id)
if not from_key:
error(f'Invalid {from_cls} user key: {from_id}')
obj.users = [from_key]
from_user = from_cls.get_or_create(id=from_key.id(), obj=from_obj)
# Prepare followee (to) users' data
to_as1s = as1.get_objects(obj.as1)
@ -807,9 +892,8 @@ class Protocol:
# Store Followers
for to_as1 in to_as1s:
to_id = to_as1.get('id')
if not to_id or not from_id:
if not to_id:
error(f'Follow activity requires object(s). Got: {obj.as1}')
from_user = from_cls.get_or_create(id=from_key.id(), obj=from_obj)
logger.info(f'Follow {from_id} => {to_id}')
@ -825,43 +909,57 @@ class Protocol:
to_obj.our_as1 = to_as1
to_obj.put()
# If followee user is already direct, follower may not know they're
# interacting with a bridge. if followee user is indirect though,
# follower should know, so they're direct.
to_key = to_cls.key_for(to_id)
if not to_key:
logger.info(f'Skipping invalid {from_cls} user key: {from_id}')
continue
# If followee user is already direct, follower may not know they're
# interacting with a bridge. if followee user is indirect though,
# follower should know, so they're direct.
to_user = to_cls.get_or_create(id=to_key.id(), obj=to_obj, direct=False)
# HACK: we rewrite direct here for each followee, so the last one
# wins. Could we do something better?
from_user = from_cls.get_or_create(id=from_key.id(), obj=from_obj,
direct=not to_user.direct)
follower_obj = Follower.get_or_create(to=to_user, from_=from_user,
follow=obj.key, status='active')
obj.add('notify', to_key)
from_cls.maybe_accept_follow(follower=from_user, followee=to_user,
follow=obj)
if not to_user.HAS_FOLLOW_ACCEPTS:
# send accept. note that this is one accept for the whole
# follow, even if it has multiple followees!
id = to_user.id_as('activitypub') + f'/followers#accept-{obj.key.id()}'
accept = Object.get_or_create(id, our_as1={
'id': id,
'objectType': 'activity',
'verb': 'accept',
'actor': to_id,
'object': obj.as1,
})
@classmethod
def maybe_accept_follow(_, follower, followee, follow):
"""Sends an accept activity for a follow.
sent = from_cls.send(accept, from_target, from_user=to_user)
if sent:
accept.populate(
delivered=[Target(protocol=from_cls.LABEL, uri=from_target)],
status='complete',
)
accept.put()
...if the follower protocol handles accepts. Otherwise, does nothing.
Args:
follower: :class:`models.User`
followee: :class:`models.User`
follow: :class:`models.Object`
"""
if followee.HAS_FOLLOW_ACCEPTS:
return
# send accept. note that this is one accept for the whole
# follow, even if it has multiple followees!
id = f'{followee.key.id()}/followers#accept-{follow.key.id()}'
accept = Object.get_or_create(id, our_as1={
'id': id,
'objectType': 'activity',
'verb': 'accept',
'actor': followee.key.id(),
'object': follow.as1,
})
from_target = follower.target_for(follower.obj)
if not from_target:
error(f"Couldn't find delivery target for follower {follower}")
sent = follower.send(accept, from_target, from_user=followee)
if sent:
accept.populate(
delivered=[Target(protocol=follower.LABEL, uri=from_target)],
status='complete',
)
accept.put()
@classmethod
def handle_bare_object(cls, obj):

Wyświetl plik

@ -48,8 +48,8 @@ google-cloud-ndb==2.3.1
google-cloud-tasks==2.16.3
googleapis-common-protos==1.63.0
grpc-google-iam-v1==0.13.0
grpcio==1.62.1
grpcio-status==1.62.1
grpcio==1.62.2
grpcio-status==1.62.2
gunicorn==22.0.0
h11==0.14.0
html2text==2024.2.26
@ -74,7 +74,7 @@ pkce==1.0.3
praw==7.7.1
prawcore==2.4.0
proto-plus==1.23.0
protobuf==4.24.3
protobuf==5.26.1
pyasn1==0.6.0
pyasn1-modules==0.4.0
pycparser==2.22

Wyświetl plik

@ -1,3 +1,4 @@
{% extends "base.html" %}
{% block content %}

Wyświetl plik

@ -59,9 +59,8 @@
{% set copies = user.copies|map(attribute='protocol')|list %}
{% for proto in set(PROTOCOLS.values()) %}
{% if proto and not isinstance(user, proto)
and proto.LABEL not in ('ui', 'web')
and (proto.LABEL not in ids.COPIES_PROTOCOLS or proto.LABEL in copies) %}
{% if proto and not isinstance(user, proto) and proto.LABEL not in ('ui', 'web')
and user.is_enabled_to(proto, user=user) %}
{% set url = proto.bridged_web_url_for(user) %}
&middot;
<nobr title="{{ proto.__name__ }} (bridged)">

Wyświetl plik

@ -230,7 +230,7 @@ ACCEPT_FOLLOW['object'] = 'http://localhost/user.com'
ACCEPT = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Accept',
'id': 'http://localhost/user.com/followers#accept-https://mas.to/6d1a',
'id': 'http://localhost/r/user.com/followers#accept-https://mas.to/6d1a',
'actor': 'http://localhost/user.com',
'object': {
'type': 'Follow',
@ -440,10 +440,9 @@ class ActivityPubTest(TestCase):
def test_actor_atproto_not_enabled(self, *_):
self.store_object(id='did:plc:user', raw={'foo': 'baz'})
self.make_user('did:plc:user', cls=ATProto)
got = self.client.get('/ap/did:plc:user', base_url='https://atproto.brid.gy/')
self.assertEqual(400, got.status_code)
got = self.client.get('/ap/did:plc:user', base_url='https://bsky.brid.gy/')
self.assertEqual(404, got.status_code)
@patch('common.ENABLED_BRIDGES', new=[('activitypub', 'atproto')])
def test_actor_atproto_no_handle(self, *_):
self.store_object(id='did:plc:user', raw={'foo': 'bar'})
self.store_object(id='at://did:plc:user/app.bsky.actor.profile/self', bsky={
@ -451,9 +450,9 @@ class ActivityPubTest(TestCase):
'displayName': 'Alice',
})
self.make_user('did:plc:user', cls=ATProto)
self.make_user('did:plc:user', cls=ATProto, enabled_protocols=['activitypub'])
got = self.client.get('/ap/did:plc:user', base_url='https://atproto.brid.gy/')
got = self.client.get('/ap/did:plc:user', base_url='https://bsky.brid.gy/')
self.assertEqual(200, got.status_code)
self.assertNotIn('preferredUsername', got.json)
@ -830,6 +829,9 @@ class ActivityPubTest(TestCase):
def test_inbox_unlisted(self, *mocks):
self._test_inbox_with_to_ignored(['@unlisted'], *mocks)
def test_inbox_dm(self, *mocks):
self._test_inbox_with_to_ignored(['http://localhost/web/user.com'], *mocks)
def _test_inbox_with_to_ignored(self, to, mock_head, mock_get, mock_post):
Follower.get_or_create(to=self.make_user(ACTOR['id'], cls=ActivityPub),
from_=self.user)
@ -1036,7 +1038,7 @@ class ActivityPubTest(TestCase):
ignore=['created', 'updated'])
self.assert_user(ActivityPub, 'https://mas.to/users/swentel',
obj_as2=ACTOR, direct=True)
obj_as2=ACTOR, direct=False)
self.assert_user(Web, 'user.com', direct=False,
has_hcard=True, has_redirects=True)
@ -1097,7 +1099,7 @@ class ActivityPubTest(TestCase):
self.assert_equals(('http://mas.to/inbox',), args)
self.assert_equals({
'type': 'Accept',
'id': 'https://web.brid.gy/user.com/followers#accept-https://mas.to/6d1a',
'id': 'https://web.brid.gy/r/user.com/followers#accept-https://mas.to/6d1a',
'actor': 'https://web.brid.gy/user.com',
'object': {
'type': 'Follow',
@ -1184,8 +1186,8 @@ class ActivityPubTest(TestCase):
]
mock_post.return_value = requests_response()
follower = Follower.get_or_create(to=self.user,
from_=ActivityPub.get_or_create(ACTOR['id']),
follower_key = ActivityPub.get_or_create(ACTOR['id'])
follower = Follower.get_or_create(to=self.user, from_=follower_key,
status='inactive')
undo_follow = copy.deepcopy(UNDO_FOLLOW_WRAPPED)
@ -1200,9 +1202,9 @@ class ActivityPubTest(TestCase):
got = self.post('/user.com/inbox', json={
'@context': ['https://www.w3.org/ns/activitystreams'],
'id': 'https://xoxo.zone/users/aaronpk#follows/40',
'type': 'Block',
'type': 'Arrive',
'actor': 'https://xoxo.zone/users/aaronpk',
'object': 'http://snarfed.org/',
'object': 'http://a/place',
})
self.assertEqual(501, got.status_code)
@ -1649,7 +1651,7 @@ class ActivityPubTest(TestCase):
def test_following_collection_page(self, *_):
self.store_following()
after = datetime(1900, 1, 1).isoformat()
prev = Follower.query(Follower.to == ActivityPub(id='http://baz').key,
prev = Follower.query(Follower.to == ActivityPub(id='http://bar').key,
Follower.from_ == self.user.key,
).get().updated.isoformat()

Wyświetl plik

@ -73,7 +73,7 @@ class ATProtoTest(TestCase):
protocol='atproto')])
did_doc = copy.deepcopy(DID_DOC)
did_doc['service'][0]['serviceEndpoint'] = 'https://atproto.brid.gy/'
did_doc['service'][0]['serviceEndpoint'] = ATProto.PDS_URL
self.store_object(id='did:plc:user', raw=did_doc)
Repo.create(self.storage, 'did:plc:user', signing_key=ATPROTO_KEY)
@ -639,11 +639,11 @@ class ATProtoTest(TestCase):
user = self.make_user('did:plc:user', cls=ATProto)
# TODO? or remove?
# self.assertEqual('@did:plc:user@atproto.brid.gy',
# self.assertEqual('@did:plc:user@bsky.brid.gy',
# user.handle_as('activitypub'))
self.store_object(id='did:plc:user', raw=DID_DOC)
self.assertEqual('@han.dull@atproto.brid.gy', user.handle_as('activitypub'))
self.assertEqual('@han.dull@bsky.brid.gy', user.handle_as('activitypub'))
@patch('requests.get', return_value=requests_response(DID_DOC))
def test_profile_id(self, mock_get):
@ -711,7 +711,7 @@ class ATProtoTest(TestCase):
'actor': 'fake:user',
})
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
# check DID doc
user = user.key.get()
@ -775,7 +775,7 @@ class ATProtoTest(TestCase):
Fake.fetchable = {'fake:user': ACTOR_AS}
obj = self.store_object(id='fake:post', source_protocol='fake', our_as1=NOTE_AS)
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
# check profile, record
user = Fake.get_by_id('fake:user')
@ -812,7 +812,7 @@ class ATProtoTest(TestCase):
user = self.make_user_and_repo()
obj = self.store_object(id='fake:post', source_protocol='fake',
our_as1=NOTE_AS)
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
# check repo, record
did = user.key.get().get_copy(ATProto)
@ -841,7 +841,7 @@ class ATProtoTest(TestCase):
'verb': 'update',
'object': note.our_as1,
})
self.assertTrue(ATProto.send(update, 'https://atproto.brid.gy/'))
self.assertTrue(ATProto.send(update, 'https://bsky.brid.gy/'))
# check repo, record
did = self.user.key.get().get_copy(ATProto)
@ -863,7 +863,7 @@ class ATProtoTest(TestCase):
'actor': 'fake:user',
'object': 'fake:post',
})
self.assertTrue(ATProto.send(update, 'https://atproto.brid.gy/'))
self.assertTrue(ATProto.send(update, 'https://bsky.brid.gy/'))
# check repo, record
did = self.user.key.get().get_copy(ATProto)
@ -895,7 +895,7 @@ class ATProtoTest(TestCase):
'actor': 'fake:user',
'object': 'at://did:plc:bob/app.bsky.feed.post/tid',
})
self.assertTrue(ATProto.send(like_obj, 'https://atproto.brid.gy/'))
self.assertTrue(ATProto.send(like_obj, 'https://bsky.brid.gy/'))
# check repo, record
did = user.get_copy(ATProto)
@ -935,7 +935,7 @@ class ATProtoTest(TestCase):
'actor': 'fake:user',
'object': 'at://did:bob/app.bsky.feed.post/tid',
})
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
# check repo, record
did = user.get_copy(ATProto)
@ -970,7 +970,7 @@ class ATProtoTest(TestCase):
'actor': 'fake:user',
'object': 'did:plc:bob',
})
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
# check repo, record
did = user.get_copy(ATProto)
@ -1005,7 +1005,7 @@ class ATProtoTest(TestCase):
'actor': 'fake:alice',
'object': 'fake:follow',
})
self.assertFalse(ATProto.send(obj, 'https://atproto.brid.gy/'))
self.assertFalse(ATProto.send(obj, 'https://bsky.brid.gy/'))
self.assertEqual(0, AtpBlock.query().count())
self.assertEqual(0, AtpRepo.query().count())
mock_create_task.assert_not_called()
@ -1017,7 +1017,7 @@ class ATProtoTest(TestCase):
copies=[Target(uri='did:plc:user', protocol='atproto')])
obj = self.store_object(id='fake:post', source_protocol='fake',
our_as1=NOTE_AS)
self.assertFalse(ATProto.send(obj, 'https://atproto.brid.gy/'))
self.assertFalse(ATProto.send(obj, 'https://bsky.brid.gy/'))
self.assertEqual(0, AtpBlock.query().count())
self.assertEqual(0, AtpRepo.query().count())
mock_create_task.assert_not_called()
@ -1035,7 +1035,7 @@ class ATProtoTest(TestCase):
'actor': 'fake:user',
},
})
self.assertFalse(ATProto.send(obj, 'https://atproto.brid.gy/'))
self.assertFalse(ATProto.send(obj, 'https://bsky.brid.gy/'))
self.assertEqual(0, AtpBlock.query().count())
self.assertEqual(0, AtpRepo.query().count())
mock_create_task.assert_not_called()
@ -1079,7 +1079,7 @@ class ATProtoTest(TestCase):
create = self.store_object(id='fake:reply:post', source_protocol='fake',
our_as1=create_as1)
self.assertTrue(ATProto.send(create, 'https://atproto.brid.gy/'))
self.assertTrue(ATProto.send(create, 'https://bsky.brid.gy/'))
repo = self.storage.load_repo(user.get_copy(ATProto))
last_tid = arroba.util.int_to_tid(arroba.util._tid_ts_last)

Wyświetl plik

@ -2,7 +2,7 @@
from flask import g
# import first so that Fake is defined before URL routes are registered
from .testutil import Fake, OtherFake, TestCase
from .testutil import ExplicitEnableFake, Fake, OtherFake, TestCase
from activitypub import ActivityPub
from atproto import ATProto
@ -64,7 +64,7 @@ class CommonTest(TestCase):
for input, expected in [
('https://fa.brid.gy/', ''),
('https://fa.brid.gy/ap/fake:foo', 'fake:foo'),
('https://atproto.brid.gy/convert/ap/did:plc:123', 'did:plc:123'),
('https://bsky.brid.gy/convert/ap/did:plc:123', 'did:plc:123'),
]:
self.assertEqual(expected, common.unwrap(input))
@ -99,20 +99,5 @@ class CommonTest(TestCase):
with app.test_request_context(base_url='http://bridgy-federated.uc.r.appspot.com'):
self.assertEqual('https://fed.brid.gy/asdf', common.host_url('asdf'))
with app.test_request_context(base_url='https://atproto.brid.gy', path='/foo'):
self.assertEqual('https://atproto.brid.gy/asdf', common.host_url('asdf'))
def test_is_enabled(self):
self.assertTrue(common.is_enabled(Web, ActivityPub))
self.assertTrue(common.is_enabled(ActivityPub, Web))
self.assertTrue(common.is_enabled(ActivityPub, ActivityPub))
self.assertTrue(common.is_enabled(ATProto, Web))
self.assertTrue(common.is_enabled(Fake, OtherFake))
self.assertFalse(common.is_enabled(ATProto, ActivityPub))
self.assertFalse(common.is_enabled(
ATProto, ActivityPub, handle_or_id='unknown'))
self.assertTrue(common.is_enabled(
ATProto, ActivityPub, handle_or_id='snarfed.org'))
self.assertTrue(common.is_enabled(
ATProto, ActivityPub, handle_or_id='did:plc:fdme4gb7mu7zrie7peay7tst'))
with app.test_request_context(base_url='https://bsky.brid.gy', path='/foo'):
self.assertEqual('https://bsky.brid.gy/asdf', common.host_url('asdf'))

Wyświetl plik

@ -40,8 +40,8 @@ class IdsTest(TestCase):
(ATProto, 'did:plc:456', ActivityPub, 'https://inst/user'),
(ATProto, 'did:plc:789', Fake, 'fake:user'),
# no copies
(ATProto, 'did:plc:x', Web, 'https://atproto.brid.gy/web/did:plc:x'),
(ATProto, 'did:plc:x', ActivityPub, 'https://atproto.brid.gy/ap/did:plc:x'),
(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, 'https://bsky.app/profile/user.com', ATProto, 'did:plc:123'),
(ATProto, 'https://bsky.app/profile/did:plc:123', ATProto, 'did:plc:123'),
@ -61,7 +61,7 @@ class IdsTest(TestCase):
]:
with self.subTest(from_=from_.LABEL, to=to.LABEL):
self.assertEqual(expected, translate_user_id(
id=id, from_proto=from_, to_proto=to))
id=id, from_=from_, to=to))
def test_translate_user_id_no_copy_did_stored(self):
for proto, id in [
@ -70,8 +70,7 @@ class IdsTest(TestCase):
(Fake, 'fake:user'),
]:
with self.subTest(proto=proto.LABEL):
self.assertIsNone(translate_user_id(
id=id, from_proto=proto, to_proto=ATProto))
self.assertIsNone(translate_user_id(id=id, from_=proto, to=ATProto))
def test_translate_user_id_use_instead(self):
did = Target(uri='did:plc:123', protocol='atproto')
@ -85,18 +84,18 @@ class IdsTest(TestCase):
]:
with self.subTest(proto=proto.LABEL):
self.assertEqual(expected, translate_user_id(
id='www.user.com', from_proto=Web, to_proto=proto))
id='www.user.com', from_=Web, to=proto))
self.assertEqual(expected, translate_user_id(
id='https://www.user.com/', from_proto=Web, to_proto=proto))
id='https://www.user.com/', from_=Web, to=proto))
@patch('ids._FED_SUBDOMAIN_SITES', new={'on-fed.com'})
def test_translate_user_id_web_ap_subdomain_fed(self):
for base_url in ['https://web.brid.gy/', 'https://fed.brid.gy/']:
with app.test_request_context('/', base_url=base_url):
self.assertEqual('https://web.brid.gy/on-web.com', translate_user_id(
id='on-web.com', from_proto=Web, to_proto=ActivityPub))
id='on-web.com', from_=Web, to=ActivityPub))
self.assertEqual('https://fed.brid.gy/on-fed.com', translate_user_id(
id='on-fed.com', from_proto=Web, to_proto=ActivityPub))
id='on-fed.com', from_=Web, to=ActivityPub))
def test_translate_handle(self):
for from_, handle, to, expected in [
@ -111,7 +110,7 @@ class IdsTest(TestCase):
(ActivityPub, '@user@instance', Fake, 'fake:handle:@user@instance'),
(ActivityPub, '@user@instance', Web, 'https://instance/@user'),
(ATProto, 'user.com', ActivityPub, '@user.com@atproto.brid.gy'),
(ATProto, 'user.com', ActivityPub, '@user.com@bsky.brid.gy'),
(ATProto, 'user.com', ATProto, 'user.com'),
(ATProto, 'user.com', Fake, 'fake:handle:user.com'),
(ATProto, 'user.com', Web, 'user.com'),
@ -123,7 +122,7 @@ class IdsTest(TestCase):
]:
with self.subTest(from_=from_.LABEL, to=to.LABEL):
self.assertEqual(expected, translate_handle(
handle=handle, from_proto=from_, to_proto=to, enhanced=False))
handle=handle, from_=from_, to=to, enhanced=False))
def test_translate_handle_enhanced(self):
for from_, handle, to, expected in [
@ -136,7 +135,7 @@ class IdsTest(TestCase):
]:
with self.subTest(from_=from_.LABEL, to=to.LABEL):
self.assertEqual(expected, translate_handle(
handle=handle, from_proto=from_, to_proto=to, enhanced=True))
handle=handle, from_=from_, to=to, enhanced=True))
def test_translate_object_id(self):
self.store_object(id='http://po.st',
@ -165,9 +164,9 @@ class IdsTest(TestCase):
(ATProto, 'at://did/ap/post', ActivityPub, 'https://inst/post'),
(ATProto, 'at://did/fa/post', Fake, 'fake:post'),
# no copies
(ATProto, 'did:plc:x', Web, 'https://atproto.brid.gy/convert/web/did:plc:x'),
(ATProto, 'did:plc:x', ActivityPub, 'https://atproto.brid.gy/convert/ap/did:plc:x'),
(ATProto, 'did:plc:x', Fake, 'fake:o:atproto:did:plc:x'),
(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'),
(ATProto, 'did:plc:x', Fake, 'fake:o:bsky:did:plc:x'),
(ATProto, 'https://bsky.app/profile/user.com/post/456',
ATProto, 'at://did:plc:123/app.bsky.feed.post/456'),
(ATProto, 'https://bsky.app/profile/did:plc:123/post/456',
@ -184,16 +183,16 @@ class IdsTest(TestCase):
]:
with self.subTest(from_=from_.LABEL, to=to.LABEL):
self.assertEqual(expected, translate_object_id(
id=id, from_proto=from_, to_proto=to))
id=id, from_=from_, to=to))
@patch('ids._FED_SUBDOMAIN_SITES', new={'on-fed.com'})
def test_translate_object_id_web_ap_subdomain_fed(self):
for base_url in ['https://web.brid.gy/', 'https://fed.brid.gy/']:
with app.test_request_context('/', base_url=base_url):
got = translate_object_id(id='http://on-fed.com/post', from_proto=Web,
to_proto=ActivityPub)
got = translate_object_id(id='http://on-fed.com/post', from_=Web,
to=ActivityPub)
self.assertEqual('https://fed.brid.gy/r/http://on-fed.com/post', got)
got = translate_object_id(id='http://on-web.com/post', from_proto=Web,
to_proto=ActivityPub)
got = translate_object_id(id='http://on-web.com/post', from_=Web,
to=ActivityPub)
self.assertEqual('https://web.brid.gy/r/http://on-web.com/post', got)

Wyświetl plik

@ -36,7 +36,6 @@ class IntegrationTests(TestCase):
@patch('requests.post')
@patch('requests.get')
@patch('common.ENABLED_BRIDGES', new=[('activitypub', 'atproto')])
def test_atproto_notify_reply_to_activitypub(self, mock_get, mock_post):
"""ATProto poll notifications, deliver reply to ActivityPub.
@ -55,6 +54,7 @@ class IntegrationTests(TestCase):
id='http://inst/bob',
cls=ActivityPub,
copies=[Target(uri='did:plc:bob', protocol='atproto')],
enabled_protocols=['atproto'],
obj_as2={
'id': 'http://inst/bob',
'inbox': 'http://inst/bob/inbox',
@ -110,14 +110,14 @@ class IntegrationTests(TestCase):
web_test.assert_deliveries(mock_post, ['http://inst/bob/inbox'], data={
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Create',
'id': 'https://atproto.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456#bridgy-fed-create',
'actor': 'https://atproto.brid.gy/ap/did:plc:alice',
'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456#bridgy-fed-create',
'actor': 'https://bsky.brid.gy/ap/did:plc:alice',
'published': '2022-01-02T03:04:05+00:00',
'object': {
'type': 'Note',
'id': 'https://atproto.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456',
'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456',
'url': 'http://localhost/r/https://bsky.app/profile/did:plc:alice/post/456',
'attributedTo': 'https://atproto.brid.gy/ap/did:plc:alice',
'attributedTo': 'https://bsky.brid.gy/ap/did:plc:alice',
'content': 'I hereby reply',
'contentMap': {'en': 'I hereby reply'},
'inReplyTo': 'http://inst/post',
@ -145,7 +145,8 @@ class IntegrationTests(TestCase):
storage = DatastoreStorage()
Repo.create(storage, 'did:plc:bob', signing_key=ATPROTO_KEY)
bob = self.make_user(id='bob.com', cls=Web,
copies=[Target(uri='did:plc:bob', protocol='atproto')])
copies=[Target(uri='did:plc:bob', protocol='atproto')],
enabled_protocols=['atproto'])
mock_get.side_effect = [
# ATProto listNotifications
@ -178,7 +179,7 @@ class IntegrationTests(TestCase):
self.assert_req(mock_get, 'https://bob.com/')
self.assert_req(mock_post, 'https://bob.com/webmention', data={
'source': 'https://atproto.brid.gy/convert/web/at://did:plc:alice/app.bsky.graph.follow/123',
'source': 'https://bsky.brid.gy/convert/web/at://did:plc:alice/app.bsky.graph.follow/123',
'target': 'https://bob.com/',
}, allow_redirects=False, headers={'Accept': '*/*'})
@ -223,13 +224,14 @@ class IntegrationTests(TestCase):
ATProto user alice.com (did:plc:alice)
Follow is HTML with mf2 u-follow-of of https://bsky.app/profile/alice.com
"""
bob = self.make_user(id='bob.com', cls=Web, obj_mf2={
'type': ['h-card'],
'properties': {
'url': ['https://bob.com/'],
'name': ['Bob'],
},
})
bob = self.make_user(id='bob.com', cls=Web, enabled_protocols=['atproto'],
obj_mf2={
'type': ['h-card'],
'properties': {
'url': ['https://bob.com/'],
'name': ['Bob'],
},
})
# send webmention
resp = self.post('/webmention', data={
@ -306,7 +308,7 @@ class IntegrationTests(TestCase):
})
bob_did_doc = copy.deepcopy(test_atproto.DID_DOC)
bob_did_doc['service'][0]['serviceEndpoint'] = 'https://atproto.brid.gy/'
bob_did_doc['service'][0]['serviceEndpoint'] = ATProto.PDS_URL
bob_did_doc.update({
'id': 'did:plc:bob',
'alsoKnownAs': ['at://bob.inst.ap.brid.gy'],
@ -321,7 +323,7 @@ class IntegrationTests(TestCase):
'type': 'Like',
'id': 'http://inst/like',
'actor': 'https://inst/bob',
'object': 'https://atproto.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/123',
'object': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/123',
}
resp = self.post('/ap/atproto/did:plc:alice/inbox', json=like)
self.assertEqual(202, resp.status_code)

Wyświetl plik

@ -861,7 +861,7 @@ class ObjectTest(TestCase):
'object': {
'id': 'https://web.brid.gy/fa/fake:reply',
'inReplyTo': 'https://ap.brid.gy/fa/fake:post',
'author': 'https://atproto.brid.gy/ap/did:plc:123',
'author': 'https://bsky.brid.gy/ap/did:plc:123',
'tags': [{
'objectType': 'mention',
'url': 'https://ap.brid.gy/atproto/http://inst.com/@me',

Wyświetl plik

@ -16,7 +16,7 @@ import requests
from werkzeug.exceptions import BadRequest
# import first so that Fake is defined before URL routes are registered
from .testutil import Fake, OtherFake, TestCase
from .testutil import ExplicitEnableFake, Fake, OtherFake, TestCase
from activitypub import ActivityPub
from app import app
@ -169,6 +169,41 @@ class ProtocolTest(TestCase):
def test_for_handle_atproto_resolve(self, _):
self.assertEqual((ATProto, 'did:plc:123abc'), Protocol.for_handle('han.dull'))
def test_is_enabled_to(self):
self.assertTrue(Web.is_enabled_to(ActivityPub))
self.assertTrue(ActivityPub.is_enabled_to(Web))
self.assertTrue(ActivityPub.is_enabled_to(ActivityPub))
self.assertTrue(Fake.is_enabled_to(OtherFake))
self.assertTrue(Fake.is_enabled_to(ExplicitEnableFake))
self.assertFalse(ActivityPub.is_enabled_to(ATProto))
self.assertFalse(ATProto.is_enabled_to(ActivityPub))
self.assertFalse(ATProto.is_enabled_to(Web))
self.assertFalse(Web.is_enabled_to(ATProto))
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake))
self.assertFalse(ExplicitEnableFake.is_enabled_to(Web))
def test_is_enabled_to_opt_out(self):
user = self.make_user('user.com', cls=Web)
self.assertTrue(Web.is_enabled_to(ActivityPub, user))
user.manual_opt_out = True
user.put()
protocol.objects_cache.clear()
self.assertFalse(Web.is_enabled_to(ActivityPub, 'user.com'))
def test_is_enabled_to_enabled_protocols(self):
user = self.make_user(id='eefake:foo', cls=ExplicitEnableFake)
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, 'eefake:foo'))
user.enabled_protocols = ['web']
user.put()
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, 'eefake:foo'))
user.enabled_protocols = ['web', 'fake']
user.put()
self.assertTrue(ExplicitEnableFake.is_enabled_to(Fake, 'eefake:foo'))
def test_load(self):
Fake.fetchable['foo'] = {'x': 'y'}
@ -409,7 +444,7 @@ class ProtocolTest(TestCase):
def test_targets_checks_blocklisted_per_protocol(self):
"""_targets should call the target protocol's is_blocklisted()."""
# non-ATProto account, ATProto target (PDS) is atproto.brid.gy
# non-ATProto account, ATProto target (PDS) is bsky.brid.gy
# shouldn't be blocklisted
user = self.make_user(
id='fake:user', cls=Fake,
@ -1340,7 +1375,7 @@ class ProtocolReceiveTest(TestCase):
delivered=['fake:user:target'],
)
accept_id = 'https://fa.brid.gy/ap/fake:user/followers#accept-fake:follow'
accept_id = 'fake:user/followers#accept-fake:follow'
accept_as1 = {
'id': accept_id,
'objectType': 'activity',
@ -1562,9 +1597,8 @@ class ProtocolReceiveTest(TestCase):
self.assertEqual(('OK', 202), OtherFake.receive_as1(follow_as1))
self.assertEqual(1, len(OtherFake.sent))
self.assertEqual(
'https://fa.brid.gy/ap/fake:alice/followers#accept-other:follow',
OtherFake.sent[0][0])
self.assertEqual('fake:alice/followers#accept-other:follow',
OtherFake.sent[0][0])
self.assertEqual(1, len(Fake.sent))
self.assertEqual('other:follow', Fake.sent[0][0])
@ -1743,6 +1777,94 @@ class ProtocolReceiveTest(TestCase):
}],
}, obj.key.get().our_as1)
def test_follow_and_block_protocol_user_sets_enabled_protocols(self):
follow = {
'objectType': 'activity',
'verb': 'follow',
'id': 'eefake:follow',
'actor': 'eefake:user',
'object': 'fa.brid.gy',
}
block = {
'objectType': 'activity',
'verb': 'block',
'id': 'eefake:block',
'actor': 'eefake:user',
'object': 'fa.brid.gy',
}
user = self.make_user('eefake:user', cls=ExplicitEnableFake)
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))
# fake protocol isn't enabled yet, block should be a noop
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(block))
user = user.key.get()
self.assertEqual([], user.enabled_protocols)
# follow should add to enabled_protocols
with self.assertRaises(NoContent):
ExplicitEnableFake.receive_as1(follow)
user = user.key.get()
self.assertEqual(['fake'], user.enabled_protocols)
self.assertTrue(ExplicitEnableFake.is_enabled_to(Fake, user))
self.assertEqual([
('https://fa.brid.gy//followers#accept-eefake:follow',
'eefake:user:target'),
], ExplicitEnableFake.sent)
# another follow should be a noop
follow['id'] += '2'
with self.assertRaises(NoContent):
ExplicitEnableFake.receive_as1(follow)
user = user.key.get()
self.assertEqual(['fake'], user.enabled_protocols)
# block should remove from enabled_protocols
block['id'] += '2'
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(block))
user = user.key.get()
self.assertEqual([], user.enabled_protocols)
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))
def test_dm_no_yes_sets_enabled_protocols(self):
dm = {
'objectType': 'note',
'id': 'eefake:dm',
'actor': 'eefake:user',
'to': ['fa.brid.gy'],
'content': 'no',
}
user = self.make_user('eefake:user', cls=ExplicitEnableFake)
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))
# fake protocol isn't enabled yet, no DM should be a noop
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
user = user.key.get()
self.assertEqual([], user.enabled_protocols)
# yes DM should add to enabled_protocols
dm['id'] += '2'
dm['content'] = 'yes'
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
user = user.key.get()
self.assertEqual(['fake'], user.enabled_protocols)
self.assertTrue(ExplicitEnableFake.is_enabled_to(Fake, user))
# another yes DM should be a noop
dm['id'] += '3'
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
user = user.key.get()
self.assertEqual(['fake'], user.enabled_protocols)
# block should remove from enabled_protocols
dm['id'] += '4'
dm['content'] = ' \n NO '
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
user = user.key.get()
self.assertEqual([], user.enabled_protocols)
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))
def test_receive_task_handler(self):
note = {
'id': 'fake:post',

Wyświetl plik

@ -2552,7 +2552,7 @@ class WebUtilTest(TestCase):
self.assertEqual(False, Web.owns_handle('@foo@bar.com'))
self.assertEqual(False, Web.owns_handle('foo@bar.com'))
self.assertEqual(False, Web.owns_handle('localhost'))
self.assertEqual(False, Web.owns_handle('atproto.brid.gy'))
self.assertEqual(False, Web.owns_handle('bsky.brid.gy'))
def test_handle_to_id(self, *_):
self.assertEqual('foo.com', Web.handle_to_id('foo.com'))

Wyświetl plik

@ -164,6 +164,15 @@ class OtherFake(Fake):
return f'{obj.key.id()}:target'
class ExplicitEnableFake(Fake):
LABEL = ABBREV = 'eefake'
CONTENT_TYPE = 'un/known'
fetchable = {}
sent = []
fetched = []
# import other modules that register Flask handlers *after* Fake is defined
models.reset_protocol_properties()

10
web.py
Wyświetl plik

@ -98,6 +98,7 @@ class Web(User, Protocol):
OTHER_LABELS = ('webmention',)
LOGO_HTML = '🌐' # used to be 🕸️
CONTENT_TYPE = common.CONTENT_TYPE_HTML
DEFAULT_ENABLED_PROTOCOLS = ('activitypub',)
has_redirects = ndb.BooleanProperty()
redirects_error = ndb.TextProperty()
@ -171,8 +172,8 @@ class Web(User, Protocol):
if isinstance(to_proto, str):
to_proto = PROTOCOLS[to_proto]
converted = translate_user_id(id=self.key.id(), from_proto=self,
to_proto=to_proto)
converted = translate_user_id(id=self.key.id(), from_=self,
to=to_proto)
if to_proto.LABEL == 'activitypub':
other = 'web' if self.ap_subdomain == 'fed' else 'fed'
@ -396,7 +397,7 @@ class Web(User, Protocol):
return False
source_id = translate_object_id(
id=obj.key.id(), from_proto=PROTOCOLS[obj.source_protocol], to_proto=Web)
id=obj.key.id(), from_=PROTOCOLS[obj.source_protocol], to=Web)
source_url = quote(source_id, safe=':/%+')
logger.info(f'Sending webmention from {source_url} to {url}')
@ -537,8 +538,7 @@ class Web(User, Protocol):
obj_as1 = obj.as1
from_proto = PROTOCOLS.get(obj.source_protocol)
if from_proto:
user_id = from_user.key.id() if from_user and from_user.key else None
if not common.is_enabled(cls, from_proto, handle_or_id=user_id):
if not from_proto.is_enabled_to(cls, user=from_user):
error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
# fill in author/actor if available