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 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 - 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 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: - run:
name: Python dependencies name: Python dependencies
@ -40,7 +40,7 @@ jobs:
- run: - run:
name: Build and test name: Build and test
command: | 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 sleep 5s
python -m coverage run --source=. --omit=appengine_config.py,logs.py,tests/\* -m unittest discover -v python -m coverage run --source=. --omit=appengine_config.py,logs.py,tests/\* -m unittest discover -v
python -m coverage html -d /tmp/coverage_html 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. 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 ```sh
gcloud config set project bridgy-federated 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: Now, run the tests to check that everything is set up ok:
```shell ```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 python3 -m unittest discover
``` ```

Wyświetl plik

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

Wyświetl plik

@ -34,7 +34,6 @@ from common import (
DOMAINS, DOMAINS,
error, error,
USER_AGENT, USER_AGENT,
USER_ALLOWLIST,
) )
import flask_app import flask_app
from models import Object, PROTOCOLS, Target, User 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. Key id is DID, currently either did:plc or did:web.
https://atproto.com/specs/did https://atproto.com/specs/did
""" """
ABBREV = 'atproto' ABBREV = 'bsky'
# TODO: add second bsky label? inject into PROTOCOLS? # TODO: add second bsky label? inject into PROTOCOLS?
PHRASE = 'Bluesky' PHRASE = 'Bluesky'
LOGO_HTML = '<img src="/oauth_dropins_static/bluesky.svg">' 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' CONTENT_TYPE = 'application/json'
DEFAULT_ENABLED_PROTOCOLS = ()
def _pre_put_hook(self): def _pre_put_hook(self):
"""Validate id, require did:plc or non-blocklisted did:web.""" """Validate id, require did:plc or non-blocklisted did:web."""
@ -501,9 +506,8 @@ class ATProto(User, Protocol):
dict: JSON object dict: JSON object
""" """
from_proto = PROTOCOLS.get(obj.source_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 # 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') # error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
if obj.bsky: if obj.bsky:
@ -575,8 +579,7 @@ def poll_notifications():
headers={'User-Agent': USER_AGENT}) headers={'User-Agent': USER_AGENT})
for user in users: for user in users:
# TODO: remove for launch if not user.is_enabled_to(ATProto, user=user):
if not DEBUG and user.key.id() not in USER_ALLOWLIST:
logger.info(f'Skipping {user.key.id()}') logger.info(f'Skipping {user.key.id()}')
continue continue
@ -635,8 +638,7 @@ def poll_posts():
headers={'User-Agent': USER_AGENT}) headers={'User-Agent': USER_AGENT})
for user in users: for user in users:
# TODO: remove for launch if not user.is_enabled_to(ATProto, user=user):
if not DEBUG and user.key.id() not in USER_ALLOWLIST:
logger.info(f'Skipping {user.key.id()}') logger.info(f'Skipping {user.key.id()}')
continue 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' 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' PRIMARY_DOMAIN = 'fed.brid.gy'
# protocol-specific subdomains are under this "super"domain # protocol-specific subdomains are under this "super"domain
SUPERDOMAIN = '.brid.gy' SUPERDOMAIN = '.brid.gy'
@ -83,15 +76,6 @@ util.set_user_agent(USER_AGENT)
TASKS_LOCATION = 'us-central1' TASKS_LOCATION = 'us-central1'
RUN_TASKS_INLINE = False # overridden by unit tests 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): def base64_to_long(x):
"""Converts from URL safe base64 encoding to long integer. """Converts from URL safe base64 encoding to long integer.
@ -264,29 +248,13 @@ def add(seq, val):
seq.append(val) seq.append(val)
def is_enabled(proto_a, proto_b, handle_or_id=None): def remove(seq, val):
"""Returns True if bridging the two input protocols is enabled, False otherwise. """Removes ``val`` to ``seq`` if seq contains it.
Args: Useful for treating repeated ndb properties like sets instead of lists.
proto_a (Protocol subclass)
proto_b (Protocol subclass)
handle_or_id (str): optional user handle or id
Returns:
bool:
""" """
if proto_a == proto_b: if val in seq:
return True seq.remove(val)
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
def create_task(queue, delay=None, **params): 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 developer documentation
---------------------------------- ----------------------------------
Bridgy Fed connects your web site to Bridgy Fed connects different decentralized social network protocols. It
`Mastodon <https://joinmastodon.org>`__ and the currently supports the
`fediverse <https://en.wikipedia.org/wiki/Fediverse>`__ via `fediverse <https://en.wikipedia.org/wiki/Fediverse>`__ (eg
`ActivityPub <https://activitypub.rocks/>`__, `Mastodon <https://joinmastodon.org>`__) via
`webmentions <https://webmention.net/>`__, and `ActivityPub <https://activitypub.rocks/>`__, and the
`microformats2 <https://microformats.org/wiki/microformats2>`__. Your `IndieWeb <https://indieweb.org/>`__ via
site gets its own fediverse profile, posts and avatar and header and `webmentions <https://webmention.net/>`__ and
all. Bridgy Fed translates likes, reposts, mentions, follows, and more `microformats2 <https://microformats.org/wiki/microformats2>`__.
back and forth. `See the user docs <https://fed.brid.gy/docs>`__ and `Bluesky/AT
`developer docs <https://bridgy-fed.readthedocs.io/>`__ for more Protocol <https://github.com/snarfed/bridgy-fed/issues/381>`__ and
details. `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/ https://fed.brid.gy/
Also see the License: This project is placed in the public domain. You may also use
`original <https://snarfed.org/indieweb-activitypub-bridge>`__ it under the `CC0
`design <https://snarfed.org/indieweb-ostatus-bridge>`__ blog posts. License <https://creativecommons.org/publicdomain/zero/1.0/>`__.
License: This project is placed in the public domain.
Development 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 First, fork and clone this repo. Then, install the `Google Cloud
SDK <https://cloud.google.com/sdk/>`__ and run SDK <https://cloud.google.com/sdk/>`__ and run
``gcloud components install beta cloud-datastore-emulator`` to install ``gcloud components install cloud-firestore-emulator`` to install the
the `datastore `Firestore
emulator <https://cloud.google.com/datastore/docs/tools/datastore-emulator>`__. emulator <https://cloud.google.com/firestore/docs/emulator>`__. Once you
Once you have them, set up your environment by running these commands in have them, set up your environment by running these commands in the repo
the repo root directory: root directory:
.. code:: sh .. code:: sh
@ -48,7 +50,7 @@ Now, run the tests to check that everything is set up ok:
.. code:: shell .. 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 python3 -m unittest discover
Finally, run this in the repo root directory to start the web app 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. 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? 6. TODO: does the new protocol need any new UI or signup functionality?
Unusual, but not impossible. Add that if necessary. Unusual, but not impossible. Add that if necessary.
7. Add the new protocols logo to ``static/``, use it in 7. Protocol logos may be emoji or image files. If this one is a file,
`templates/user.html <https://github.com/snarfed/bridgy-fed/blob/main/templates/user.html>`__. add it ``static/``. Then add the emoji or file ``<img>`` tag in the
``Protocol`` subclasss ``LOGO_HTML`` constant.
Stats 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', app.add_url_rule('/queue/atproto-poll-notifs',
view_func=atproto.poll_notifications, view_func=atproto.poll_notifications,
methods=['POST']) methods=['GET', 'POST'])
app.add_url_rule('/queue/atproto-poll-posts', app.add_url_rule('/queue/atproto-poll-posts',
view_func=atproto.poll_posts, view_func=atproto.poll_posts,
methods=['POST']) methods=['GET', 'POST'])
@app.post('/queue/atproto-commit') @app.post('/queue/atproto-commit')
@flask_util.cloud_tasks_only @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}/' 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. """Translate a user id from one protocol to another.
TODO: unify with :func:`translate_object_id`. TODO: unify with :func:`translate_object_id`.
Args: Args:
id (str) id (str)
from_proto (protocol.Protocol) from_ (protocol.Protocol)
to_proto (protocol.Protocol) to (protocol.Protocol)
Returns: 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 id and from_ and to, (id, from_, to)
assert from_proto.owns_id(id) is not False or from_proto.LABEL == 'ui', \ assert from_.owns_id(id) is not False or from_.LABEL == 'ui', \
(id, from_proto.LABEL, to_proto.LABEL) (id, from_.LABEL, to.LABEL)
parsed = urlparse(id) 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 # home page; replace with domain
id = parsed.netloc id = parsed.netloc
# bsky.app profile URL to DID # bsky.app profile URL to DID
if to_proto.LABEL == 'atproto': if to.LABEL == 'atproto':
if match := BSKY_APP_URL_RE.match(id): if match := BSKY_APP_URL_RE.match(id):
repo = match.group('id') repo = match.group('id')
if repo.startswith('did:'): if repo.startswith('did:'):
@ -89,25 +89,25 @@ def translate_user_id(*, id, from_proto, to_proto):
logger.warning(e) logger.warning(e)
return None return None
if from_proto == to_proto: if from_ == to:
return id return id
# follow use_instead # follow use_instead
user = from_proto.get_by_id(id) user = from_.get_by_id(id)
if user: if user:
id = user.key.id() 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 user:
if copy := user.get_copy(to_proto): if copy := user.get_copy(to):
return copy return copy
if orig := models.get_original(id): if orig := models.get_original(id):
if isinstance(orig, to_proto): if isinstance(orig, to):
return orig.key.id() return orig.key.id()
match from_proto.LABEL, to_proto.LABEL: match from_.LABEL, to.LABEL:
case _, 'atproto' | 'nostr': 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 return None
case 'web', 'activitypub': case 'web', 'activitypub':
@ -117,45 +117,45 @@ def translate_user_id(*, id, from_proto, to_proto):
return id return id
case _, 'activitypub' | 'web': 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 # only for unit tests
case _, 'fake' | 'other': case _, 'fake' | 'other' | 'eefake':
return f'{to_proto.LABEL}:u:{id}' return f'{to.LABEL}:u:{id}'
case 'fake' | 'other', _: case 'fake' | 'other', _:
return id 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. """Translates a user handle from one protocol to another.
Args: Args:
handle (str) handle (str)
from_proto (protocol.Protocol) from_ (protocol.Protocol)
to_proto (protocol.Protocol) to (protocol.Protocol)
enhanced (bool): whether to convert to an "enhanced" handle based on the enhanced (bool): whether to convert to an "enhanced" handle based on the
user's domain user's domain
Returns: 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 handle and from_ and to, (handle, from_, to)
assert from_proto.owns_handle(handle) is not False or from_proto.LABEL == 'ui' assert from_.owns_handle(handle) is not False or from_.LABEL == 'ui'
if from_proto == to_proto: if from_ == to:
return handle return handle
match from_proto.LABEL, to_proto.LABEL: match from_.LABEL, to.LABEL:
case _, 'activitypub': 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}' return f'@{handle}@{domain}'
case _, 'atproto' | 'nostr': case _, 'atproto' | 'nostr':
handle = handle.lstrip('@').replace('@', '.') handle = handle.lstrip('@').replace('@', '.')
return (handle if enhanced return (handle if enhanced
else f'{handle}.{from_proto.ABBREV}{SUPERDOMAIN}') else f'{handle}.{from_.ABBREV}{SUPERDOMAIN}')
case 'activitypub', 'web': case 'activitypub', 'web':
user, instance = handle.lstrip('@').split('@') user, instance = handle.lstrip('@').split('@')
@ -167,32 +167,30 @@ def translate_handle(*, handle, from_proto, to_proto, enhanced):
return handle return handle
# only for unit tests # only for unit tests
case _, 'fake': case _, 'fake' | 'other' | 'eefake':
return f'fake:handle:{handle}' return f'{to.LABEL}:handle:{handle}'
case _, 'other':
return f'other: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. """Translates a user handle from one protocol to another.
TODO: unify with :func:`translate_user_id`. TODO: unify with :func:`translate_user_id`.
Args: Args:
id (str) id (str)
from_proto (protocol.Protocol) from_ (protocol.Protocol)
to_proto (protocol.Protocol) to (protocol.Protocol)
Returns: 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 id and from_ and to, (id, from_, to)
assert from_proto.owns_id(id) is not False or from_proto.LABEL == 'ui' assert from_.owns_id(id) is not False or from_.LABEL == 'ui'
# bsky.app profile URL to DID # bsky.app profile URL to DID
if to_proto.LABEL == 'atproto': if to.LABEL == 'atproto':
if match := BSKY_APP_URL_RE.match(id): if match := BSKY_APP_URL_RE.match(id):
repo = match.group('id') repo = match.group('id')
handle = None 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) return web_url_to_at_uri(id, handle=handle, did=repo)
if from_proto == to_proto: if from_ == to:
return id return 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 obj := from_proto.load(id, remote=False): if obj := from_.load(id, remote=False):
if copy := obj.get_copy(to_proto): if copy := obj.get_copy(to):
return copy return copy
if orig := models.get_original(id): if orig := models.get_original(id):
return orig.key.id() return orig.key.id()
match from_proto.LABEL, to_proto.LABEL: match from_.LABEL, to.LABEL:
case _, 'atproto' | 'nostr': 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 return id
case 'web', 'activitypub': case 'web', 'activitypub':
return urljoin(web_ap_base_domain(util.domain_from_link(id)), f'/r/{id}') return urljoin(web_ap_base_domain(util.domain_from_link(id)), f'/r/{id}')
case _, 'activitypub' | 'web': 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 # only for unit tests
case _, 'fake' | 'other': case _, 'fake' | 'other' | 'eefake':
return f'{to_proto.LABEL}:o:{from_proto.ABBREV}:{id}' 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. # index.yaml file manually, remove the "# AUTOGENERATED" marker line above.
# If you want to manage some indexes manually, move them above the marker line. # 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 - kind: Object
properties: properties:
- name: users - name: users
@ -59,18 +17,6 @@ indexes:
- name: updated - name: updated
direction: desc 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 - kind: Object
properties: properties:
- name: notify - name: notify
@ -93,6 +39,12 @@ indexes:
- name: created - name: created
direction: desc direction: desc
- kind: Follower
properties:
- name: from
- name: status
- name: updated
- kind: Follower - kind: Follower
properties: properties:
- name: from - name: from
@ -100,6 +52,12 @@ indexes:
- name: updated - name: updated
direction: desc direction: desc
- kind: Follower
properties:
- name: to
- name: status
- name: updated
- kind: Follower - kind: Follower
properties: properties:
- name: to - 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 from oauth_dropins.webutil.util import json_dumps, json_loads
import common import common
from common import add, base64_to_long, DOMAIN_RE, long_to_base64, unwrap from common import add, base64_to_long, DOMAIN_RE, long_to_base64, remove, unwrap
from ids import translate_handle, translate_object_id, translate_user_id import ids
# maps string label to Protocol subclass. populated by ProtocolUserMeta. # maps string label to Protocol subclass. populated by ProtocolUserMeta.
# seed with old and upcoming protocols that don't have their own classes (yet). # 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) 'protocol', choices=list(PROTOCOLS.keys()), required=True)
Object.source_protocol = ndb.StringProperty( Object.source_protocol = ndb.StringProperty(
'source_protocol', choices=list(PROTOCOLS.keys())) '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)' abbrevs = f'({"|".join(PROTOCOLS.keys())}|fed)'
common.SUBDOMAIN_BASE_URL_RE = re.compile( common.SUBDOMAIN_BASE_URL_RE = re.compile(
@ -157,6 +159,11 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
# #nobridge in their profile # #nobridge in their profile
manual_opt_out = ndb.BooleanProperty() 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) created = ndb.DateTimeProperty(auto_now_add=True)
updated = ndb.DateTimeProperty(auto_now=True) updated = ndb.DateTimeProperty(auto_now=True)
@ -222,13 +229,16 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if user.status == 'opt-out': if user.status == 'opt-out':
return None return None
user.existing = True user.existing = True
# override direct from False => True if set
# TODO: propagate more props into user? # TODO: propagate more fields?
direct = kwargs.get('direct') for field in ['direct', 'obj', 'obj_key']:
if direct and not user.direct: old_val = getattr(user, field, None)
logger.info(f'Setting {user.key} direct={direct}') new_val = kwargs.get(field)
user.direct = direct if ((old_val is None and new_val is not None)
user.put() or (field == 'direct' and not old_val and new_val)):
setattr(user, field, new_val)
user.put()
if not propagate: if not propagate:
return user return user
else: else:
@ -241,7 +251,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
ATProto = PROTOCOLS['atproto'] ATProto = PROTOCOLS['atproto']
if propagate and cls.LABEL != 'atproto' and not user.get_copy(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) ATProto.create_for(user)
else: else:
logger.info(f'{cls.LABEL} <=> atproto not enabled, skipping') logger.info(f'{cls.LABEL} <=> atproto not enabled, skipping')
@ -341,6 +351,32 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
return None 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): def handle_as(self, to_proto):
"""Returns this user's handle in a different protocol. """Returns this user's handle in a different protocol.
@ -359,8 +395,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if not handle: if not handle:
return None return None
return translate_handle(handle=handle, from_proto=self.__class__, return ids.translate_handle(handle=handle, from_=self.__class__,
to_proto=to_proto, enhanced=False) to=to_proto, enhanced=False)
def id_as(self, to_proto): def id_as(self, to_proto):
"""Returns this user's id in a different protocol. """Returns this user's id in a different protocol.
@ -374,8 +410,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if isinstance(to_proto, str): if isinstance(to_proto, str):
to_proto = PROTOCOLS[to_proto] to_proto = PROTOCOLS[to_proto]
return translate_user_id(id=self.key.id(), from_proto=self.__class__, return ids.translate_user_id(id=self.key.id(), from_=self.__class__,
to_proto=to_proto) to=to_proto)
def handle_or_id(self): def handle_or_id(self):
"""Returns handle if we know it, otherwise id.""" """Returns handle if we know it, otherwise id."""
@ -535,9 +571,6 @@ class Object(StringIdModel):
'feed', 'notification', 'user') 'feed', 'notification', 'user')
# Keys for user(s) who created or otherwise own this activity. # 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) users = ndb.KeyProperty(repeated=True)
# User keys who should see this activity in their user page, eg in reply to, # User keys who should see this activity in their user page, eg in reply to,
# reaction to, share of, etc. # reaction to, share of, etc.
@ -551,8 +584,8 @@ class Object(StringIdModel):
domains = ndb.StringProperty(repeated=True) domains = ndb.StringProperty(repeated=True)
status = ndb.StringProperty(choices=STATUSES) status = ndb.StringProperty(choices=STATUSES)
# choices is populated in app, after all User subclasses are created, # choices is populated in reset_protocol_properties, after all User
# so that PROTOCOLS is fully populated # subclasses are created, so that PROTOCOLS is fully populated.
# TODO: nail down whether this is ABBREV or LABEL # TODO: nail down whether this is ABBREV or LABEL
source_protocol = ndb.StringProperty(choices=[]) source_protocol = ndb.StringProperty(choices=[])
labels = ndb.StringProperty(repeated=True, choices=LABELS) labels = ndb.StringProperty(repeated=True, choices=LABELS)
@ -921,7 +954,7 @@ class Object(StringIdModel):
original objects in their source protocol, eg original objects in their source protocol, eg
``at://did:plc:abc/app.bsky.feed.post/123`` => ``https://mas.to/@user/456``. ``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 * 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 * ATProto bsky.app URLs to their DIDs or `at://` URIs, eg
``https://bsky.app/profile/a.com`` => ``did:plc:123`` ``https://bsky.app/profile/a.com`` => ``did:plc:123``
@ -1044,7 +1077,7 @@ class Object(StringIdModel):
if not proto: if not proto:
return val 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: if translated and translated != orig:
logger.info(f'Normalized {proto.LABEL} id {orig} to {translated}') logger.info(f'Normalized {proto.LABEL} id {orig} to {translated}')
replaced = True replaced = True
@ -1060,20 +1093,21 @@ class Object(StringIdModel):
for obj in [outer_obj] + inner_objs: for obj in [outer_obj] + inner_objs:
for tag in as1.get_objects(obj, 'tags'): for tag in as1.get_objects(obj, 'tags'):
if tag.get('objectType') == 'mention': 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']: 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)] obj[field] = [replace(val, fn) for val in util.get_list(obj, field)]
if len(obj[field]) == 1: if len(obj[field]) == 1:
obj[field] = obj[field][0] obj[field] = obj[field][0]
outer_obj['object'] = [] outer_obj['object'] = []
for inner_obj in inner_objs: 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 if (as1.object_type(inner_obj) in as1.ACTOR_TYPES
or as1.object_type(outer_obj) in or as1.object_type(outer_obj) in
('follow', 'stop-following')) ('follow', 'stop-following'))
else translate_object_id) else ids.translate_object_id)
got = replace(inner_obj, translate_fn) got = replace(inner_obj, translate_fn)
if isinstance(got, dict) and util.trim_nulls(got).keys() == {'id'}: if isinstance(got, dict) and util.trim_nulls(got).keys() == {'id'}:
@ -1180,7 +1214,7 @@ class Follower(ndb.Model):
query = Follower.query( query = Follower.query(
Follower.status == 'active', Follower.status == 'active',
filter_prop == user.key, filter_prop == user.key,
).order(-Follower.updated) )
followers, before, after = fetch_page(query, Follower, by=Follower.updated) followers, before, after = fetch_page(query, Follower, by=Follower.updated)
users = ndb.get_multi(f.from_ if collection == 'followers' else f.to 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 import OR
from google.cloud.ndb.model import _entity_to_protobuf from google.cloud.ndb.model import _entity_to_protobuf
from granary import as1 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.flask_util import cloud_tasks_only
from oauth_dropins.webutil import util
from oauth_dropins.webutil import models from oauth_dropins.webutil import models
from oauth_dropins.webutil import util
from oauth_dropins.webutil.util import json_dumps, json_loads from oauth_dropins.webutil.util import json_dumps, json_loads
import werkzeug.exceptions import werkzeug.exceptions
import common 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 flask_app import app
from ids import translate_object_id, translate_user_id from ids import translate_object_id, translate_user_id
from models import Follower, get_originals, Object, PROTOCOLS, Target, User from models import Follower, get_originals, Object, PROTOCOLS, Target, User
@ -28,6 +37,7 @@ SUPPORTED_TYPES = (
'accept', 'accept',
'article', 'article',
'audio', 'audio',
'block',
'comment', 'comment',
'delete', 'delete',
'follow', 'follow',
@ -70,6 +80,8 @@ class Protocol:
appropriate for the ``Content-Type`` HTTP header. appropriate for the ``Content-Type`` HTTP header.
HAS_FOLLOW_ACCEPTS (bool): whether this protocol supports explicit HAS_FOLLOW_ACCEPTS (bool): whether this protocol supports explicit
accept/reject activities in response to follows, eg ActivityPub 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 ABBREV = None
PHRASE = None PHRASE = None
@ -77,6 +89,7 @@ class Protocol:
LOGO_HTML = '' LOGO_HTML = ''
CONTENT_TYPE = None CONTENT_TYPE = None
HAS_FOLLOW_ACCEPTS = False HAS_FOLLOW_ACCEPTS = False
DEFAULT_ENABLED_PROTOCOLS = ()
def __init__(self): def __init__(self):
assert False assert False
@ -126,6 +139,52 @@ class Protocol:
label = domain.removesuffix(common.SUPERDOMAIN) label = domain.removesuffix(common.SUPERDOMAIN)
return PROTOCOLS.get(label) 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 @classmethod
def owns_id(cls, id): def owns_id(cls, id):
"""Returns whether this protocol owns the id, or None if it's unclear. """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 For example, if ``proto`` is :class:`ActivityPub`, the ATProto URI
``at://did:plc:abc/coll/123`` will be converted to ``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: Wraps these AS1 fields:
@ -539,7 +598,7 @@ class Protocol:
# TODO: what if from_cls is None? relax translate_object_id, # TODO: what if from_cls is None? relax translate_object_id,
# make it a noop if we don't know enough about from/to? # make it a noop if we don't know enough about from/to?
if from_cls and from_cls != to_cls: 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'}: if elem[field].keys() == {'id'}:
elem[field] = elem[field]['id'] elem[field] = elem[field]['id']
@ -736,6 +795,29 @@ class Protocol:
# fall through to deliver to followers # 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 # fetch actor if necessary
if actor and actor.keys() == set(['id']): if actor and actor.keys() == set(['id']):
logger.info('Fetching actor so we have name, profile photo, etc') logger.info('Fetching actor so we have name, profile photo, etc')
@ -759,6 +841,12 @@ class Protocol:
} }
if obj.type == 'follow': 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) from_cls.handle_follow(obj)
# deliver to targets # deliver to targets
@ -790,14 +878,11 @@ class Protocol:
from_obj.our_as1 = from_as1 from_obj.our_as1 = from_as1
from_obj.put() 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) from_key = from_cls.key_for(from_id)
if not from_key: if not from_key:
error(f'Invalid {from_cls} user key: {from_id}') error(f'Invalid {from_cls} user key: {from_id}')
obj.users = [from_key] obj.users = [from_key]
from_user = from_cls.get_or_create(id=from_key.id(), obj=from_obj)
# Prepare followee (to) users' data # Prepare followee (to) users' data
to_as1s = as1.get_objects(obj.as1) to_as1s = as1.get_objects(obj.as1)
@ -807,9 +892,8 @@ class Protocol:
# Store Followers # Store Followers
for to_as1 in to_as1s: for to_as1 in to_as1s:
to_id = to_as1.get('id') 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}') 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}') logger.info(f'Follow {from_id} => {to_id}')
@ -825,43 +909,57 @@ class Protocol:
to_obj.our_as1 = to_as1 to_obj.our_as1 = to_as1
to_obj.put() 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) to_key = to_cls.key_for(to_id)
if not to_key: if not to_key:
logger.info(f'Skipping invalid {from_cls} user key: {from_id}') logger.info(f'Skipping invalid {from_cls} user key: {from_id}')
continue 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) 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, follower_obj = Follower.get_or_create(to=to_user, from_=from_user,
follow=obj.key, status='active') follow=obj.key, status='active')
obj.add('notify', to_key) 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: @classmethod
# send accept. note that this is one accept for the whole def maybe_accept_follow(_, follower, followee, follow):
# follow, even if it has multiple followees! """Sends an accept activity for a follow.
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,
})
sent = from_cls.send(accept, from_target, from_user=to_user) ...if the follower protocol handles accepts. Otherwise, does nothing.
if sent:
accept.populate( Args:
delivered=[Target(protocol=from_cls.LABEL, uri=from_target)], follower: :class:`models.User`
status='complete', followee: :class:`models.User`
) follow: :class:`models.Object`
accept.put() """
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 @classmethod
def handle_bare_object(cls, obj): 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 google-cloud-tasks==2.16.3
googleapis-common-protos==1.63.0 googleapis-common-protos==1.63.0
grpc-google-iam-v1==0.13.0 grpc-google-iam-v1==0.13.0
grpcio==1.62.1 grpcio==1.62.2
grpcio-status==1.62.1 grpcio-status==1.62.2
gunicorn==22.0.0 gunicorn==22.0.0
h11==0.14.0 h11==0.14.0
html2text==2024.2.26 html2text==2024.2.26
@ -74,7 +74,7 @@ pkce==1.0.3
praw==7.7.1 praw==7.7.1
prawcore==2.4.0 prawcore==2.4.0
proto-plus==1.23.0 proto-plus==1.23.0
protobuf==4.24.3 protobuf==5.26.1
pyasn1==0.6.0 pyasn1==0.6.0
pyasn1-modules==0.4.0 pyasn1-modules==0.4.0
pycparser==2.22 pycparser==2.22

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -2,7 +2,7 @@
from flask import g from flask import g
# import first so that Fake is defined before URL routes are registered # 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 activitypub import ActivityPub
from atproto import ATProto from atproto import ATProto
@ -64,7 +64,7 @@ class CommonTest(TestCase):
for input, expected in [ for input, expected in [
('https://fa.brid.gy/', ''), ('https://fa.brid.gy/', ''),
('https://fa.brid.gy/ap/fake:foo', 'fake:foo'), ('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)) 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'): 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')) self.assertEqual('https://fed.brid.gy/asdf', common.host_url('asdf'))
with app.test_request_context(base_url='https://atproto.brid.gy', path='/foo'): with app.test_request_context(base_url='https://bsky.brid.gy', path='/foo'):
self.assertEqual('https://atproto.brid.gy/asdf', common.host_url('asdf')) self.assertEqual('https://bsky.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'))

Wyświetl plik

@ -40,8 +40,8 @@ class IdsTest(TestCase):
(ATProto, 'did:plc:456', ActivityPub, 'https://inst/user'), (ATProto, 'did:plc:456', ActivityPub, 'https://inst/user'),
(ATProto, 'did:plc:789', Fake, 'fake:user'), (ATProto, 'did:plc:789', Fake, 'fake:user'),
# no copies # no copies
(ATProto, 'did:plc:x', Web, 'https://atproto.brid.gy/web/did:plc:x'), (ATProto, 'did:plc:x', Web, 'https://bsky.brid.gy/web/did:plc:x'),
(ATProto, 'did:plc:x', ActivityPub, 'https://atproto.brid.gy/ap/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:x', Fake, 'fake:u:did:plc:x'),
(ATProto, 'https://bsky.app/profile/user.com', ATProto, 'did:plc:123'), (ATProto, 'https://bsky.app/profile/user.com', ATProto, 'did:plc:123'),
(ATProto, 'https://bsky.app/profile/did:plc:123', 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): with self.subTest(from_=from_.LABEL, to=to.LABEL):
self.assertEqual(expected, translate_user_id( 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): def test_translate_user_id_no_copy_did_stored(self):
for proto, id in [ for proto, id in [
@ -70,8 +70,7 @@ class IdsTest(TestCase):
(Fake, 'fake:user'), (Fake, 'fake:user'),
]: ]:
with self.subTest(proto=proto.LABEL): with self.subTest(proto=proto.LABEL):
self.assertIsNone(translate_user_id( self.assertIsNone(translate_user_id(id=id, from_=proto, to=ATProto))
id=id, from_proto=proto, to_proto=ATProto))
def test_translate_user_id_use_instead(self): def test_translate_user_id_use_instead(self):
did = Target(uri='did:plc:123', protocol='atproto') did = Target(uri='did:plc:123', protocol='atproto')
@ -85,18 +84,18 @@ class IdsTest(TestCase):
]: ]:
with self.subTest(proto=proto.LABEL): with self.subTest(proto=proto.LABEL):
self.assertEqual(expected, translate_user_id( 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( 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'}) @patch('ids._FED_SUBDOMAIN_SITES', new={'on-fed.com'})
def test_translate_user_id_web_ap_subdomain_fed(self): def test_translate_user_id_web_ap_subdomain_fed(self):
for base_url in ['https://web.brid.gy/', 'https://fed.brid.gy/']: for base_url in ['https://web.brid.gy/', 'https://fed.brid.gy/']:
with app.test_request_context('/', base_url=base_url): with app.test_request_context('/', base_url=base_url):
self.assertEqual('https://web.brid.gy/on-web.com', translate_user_id( 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( 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): def test_translate_handle(self):
for from_, handle, to, expected in [ for from_, handle, to, expected in [
@ -111,7 +110,7 @@ class IdsTest(TestCase):
(ActivityPub, '@user@instance', Fake, 'fake:handle:@user@instance'), (ActivityPub, '@user@instance', Fake, 'fake:handle:@user@instance'),
(ActivityPub, '@user@instance', Web, 'https://instance/@user'), (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', ATProto, 'user.com'),
(ATProto, 'user.com', Fake, 'fake:handle:user.com'), (ATProto, 'user.com', Fake, 'fake:handle:user.com'),
(ATProto, 'user.com', Web, 'user.com'), (ATProto, 'user.com', Web, 'user.com'),
@ -123,7 +122,7 @@ class IdsTest(TestCase):
]: ]:
with self.subTest(from_=from_.LABEL, to=to.LABEL): with self.subTest(from_=from_.LABEL, to=to.LABEL):
self.assertEqual(expected, translate_handle( 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): def test_translate_handle_enhanced(self):
for from_, handle, to, expected in [ for from_, handle, to, expected in [
@ -136,7 +135,7 @@ class IdsTest(TestCase):
]: ]:
with self.subTest(from_=from_.LABEL, to=to.LABEL): with self.subTest(from_=from_.LABEL, to=to.LABEL):
self.assertEqual(expected, translate_handle( 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): def test_translate_object_id(self):
self.store_object(id='http://po.st', 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/ap/post', ActivityPub, 'https://inst/post'),
(ATProto, 'at://did/fa/post', Fake, 'fake:post'), (ATProto, 'at://did/fa/post', Fake, 'fake:post'),
# no copies # no copies
(ATProto, 'did:plc:x', Web, 'https://atproto.brid.gy/convert/web/did:plc:x'), (ATProto, 'did:plc:x', Web, 'https://bsky.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', ActivityPub, 'https://bsky.brid.gy/convert/ap/did:plc:x'),
(ATProto, 'did:plc:x', Fake, 'fake:o:atproto:did:plc:x'), (ATProto, 'did:plc:x', Fake, 'fake:o:bsky:did:plc:x'),
(ATProto, 'https://bsky.app/profile/user.com/post/456', (ATProto, 'https://bsky.app/profile/user.com/post/456',
ATProto, 'at://did:plc:123/app.bsky.feed.post/456'), ATProto, 'at://did:plc:123/app.bsky.feed.post/456'),
(ATProto, 'https://bsky.app/profile/did:plc:123/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): with self.subTest(from_=from_.LABEL, to=to.LABEL):
self.assertEqual(expected, translate_object_id( 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'}) @patch('ids._FED_SUBDOMAIN_SITES', new={'on-fed.com'})
def test_translate_object_id_web_ap_subdomain_fed(self): def test_translate_object_id_web_ap_subdomain_fed(self):
for base_url in ['https://web.brid.gy/', 'https://fed.brid.gy/']: for base_url in ['https://web.brid.gy/', 'https://fed.brid.gy/']:
with app.test_request_context('/', base_url=base_url): with app.test_request_context('/', base_url=base_url):
got = translate_object_id(id='http://on-fed.com/post', from_proto=Web, got = translate_object_id(id='http://on-fed.com/post', from_=Web,
to_proto=ActivityPub) to=ActivityPub)
self.assertEqual('https://fed.brid.gy/r/http://on-fed.com/post', got) 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, got = translate_object_id(id='http://on-web.com/post', from_=Web,
to_proto=ActivityPub) to=ActivityPub)
self.assertEqual('https://web.brid.gy/r/http://on-web.com/post', got) 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.post')
@patch('requests.get') @patch('requests.get')
@patch('common.ENABLED_BRIDGES', new=[('activitypub', 'atproto')])
def test_atproto_notify_reply_to_activitypub(self, mock_get, mock_post): def test_atproto_notify_reply_to_activitypub(self, mock_get, mock_post):
"""ATProto poll notifications, deliver reply to ActivityPub. """ATProto poll notifications, deliver reply to ActivityPub.
@ -55,6 +54,7 @@ class IntegrationTests(TestCase):
id='http://inst/bob', id='http://inst/bob',
cls=ActivityPub, cls=ActivityPub,
copies=[Target(uri='did:plc:bob', protocol='atproto')], copies=[Target(uri='did:plc:bob', protocol='atproto')],
enabled_protocols=['atproto'],
obj_as2={ obj_as2={
'id': 'http://inst/bob', 'id': 'http://inst/bob',
'inbox': 'http://inst/bob/inbox', 'inbox': 'http://inst/bob/inbox',
@ -110,14 +110,14 @@ class IntegrationTests(TestCase):
web_test.assert_deliveries(mock_post, ['http://inst/bob/inbox'], data={ web_test.assert_deliveries(mock_post, ['http://inst/bob/inbox'], data={
'@context': 'https://www.w3.org/ns/activitystreams', '@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Create', 'type': 'Create',
'id': 'https://atproto.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456#bridgy-fed-create', 'id': 'https://bsky.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', 'actor': 'https://bsky.brid.gy/ap/did:plc:alice',
'published': '2022-01-02T03:04:05+00:00', 'published': '2022-01-02T03:04:05+00:00',
'object': { 'object': {
'type': 'Note', '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', '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', 'content': 'I hereby reply',
'contentMap': {'en': 'I hereby reply'}, 'contentMap': {'en': 'I hereby reply'},
'inReplyTo': 'http://inst/post', 'inReplyTo': 'http://inst/post',
@ -145,7 +145,8 @@ class IntegrationTests(TestCase):
storage = DatastoreStorage() storage = DatastoreStorage()
Repo.create(storage, 'did:plc:bob', signing_key=ATPROTO_KEY) Repo.create(storage, 'did:plc:bob', signing_key=ATPROTO_KEY)
bob = self.make_user(id='bob.com', cls=Web, 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 = [ mock_get.side_effect = [
# ATProto listNotifications # ATProto listNotifications
@ -178,7 +179,7 @@ class IntegrationTests(TestCase):
self.assert_req(mock_get, 'https://bob.com/') self.assert_req(mock_get, 'https://bob.com/')
self.assert_req(mock_post, 'https://bob.com/webmention', data={ 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/', 'target': 'https://bob.com/',
}, allow_redirects=False, headers={'Accept': '*/*'}) }, allow_redirects=False, headers={'Accept': '*/*'})
@ -223,13 +224,14 @@ class IntegrationTests(TestCase):
ATProto user alice.com (did:plc:alice) ATProto user alice.com (did:plc:alice)
Follow is HTML with mf2 u-follow-of of https://bsky.app/profile/alice.com 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={ bob = self.make_user(id='bob.com', cls=Web, enabled_protocols=['atproto'],
'type': ['h-card'], obj_mf2={
'properties': { 'type': ['h-card'],
'url': ['https://bob.com/'], 'properties': {
'name': ['Bob'], 'url': ['https://bob.com/'],
}, 'name': ['Bob'],
}) },
})
# send webmention # send webmention
resp = self.post('/webmention', data={ resp = self.post('/webmention', data={
@ -306,7 +308,7 @@ class IntegrationTests(TestCase):
}) })
bob_did_doc = copy.deepcopy(test_atproto.DID_DOC) 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({ bob_did_doc.update({
'id': 'did:plc:bob', 'id': 'did:plc:bob',
'alsoKnownAs': ['at://bob.inst.ap.brid.gy'], 'alsoKnownAs': ['at://bob.inst.ap.brid.gy'],
@ -321,7 +323,7 @@ class IntegrationTests(TestCase):
'type': 'Like', 'type': 'Like',
'id': 'http://inst/like', 'id': 'http://inst/like',
'actor': 'https://inst/bob', '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) resp = self.post('/ap/atproto/did:plc:alice/inbox', json=like)
self.assertEqual(202, resp.status_code) self.assertEqual(202, resp.status_code)

Wyświetl plik

@ -861,7 +861,7 @@ class ObjectTest(TestCase):
'object': { 'object': {
'id': 'https://web.brid.gy/fa/fake:reply', 'id': 'https://web.brid.gy/fa/fake:reply',
'inReplyTo': 'https://ap.brid.gy/fa/fake:post', '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': [{ 'tags': [{
'objectType': 'mention', 'objectType': 'mention',
'url': 'https://ap.brid.gy/atproto/http://inst.com/@me', 'url': 'https://ap.brid.gy/atproto/http://inst.com/@me',

Wyświetl plik

@ -16,7 +16,7 @@ import requests
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
# import first so that Fake is defined before URL routes are registered # 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 activitypub import ActivityPub
from app import app from app import app
@ -169,6 +169,41 @@ class ProtocolTest(TestCase):
def test_for_handle_atproto_resolve(self, _): def test_for_handle_atproto_resolve(self, _):
self.assertEqual((ATProto, 'did:plc:123abc'), Protocol.for_handle('han.dull')) 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): def test_load(self):
Fake.fetchable['foo'] = {'x': 'y'} Fake.fetchable['foo'] = {'x': 'y'}
@ -409,7 +444,7 @@ class ProtocolTest(TestCase):
def test_targets_checks_blocklisted_per_protocol(self): def test_targets_checks_blocklisted_per_protocol(self):
"""_targets should call the target protocol's is_blocklisted().""" """_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 # shouldn't be blocklisted
user = self.make_user( user = self.make_user(
id='fake:user', cls=Fake, id='fake:user', cls=Fake,
@ -1340,7 +1375,7 @@ class ProtocolReceiveTest(TestCase):
delivered=['fake:user:target'], 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 = { accept_as1 = {
'id': accept_id, 'id': accept_id,
'objectType': 'activity', 'objectType': 'activity',
@ -1562,9 +1597,8 @@ class ProtocolReceiveTest(TestCase):
self.assertEqual(('OK', 202), OtherFake.receive_as1(follow_as1)) self.assertEqual(('OK', 202), OtherFake.receive_as1(follow_as1))
self.assertEqual(1, len(OtherFake.sent)) self.assertEqual(1, len(OtherFake.sent))
self.assertEqual( self.assertEqual('fake:alice/followers#accept-other:follow',
'https://fa.brid.gy/ap/fake:alice/followers#accept-other:follow', OtherFake.sent[0][0])
OtherFake.sent[0][0])
self.assertEqual(1, len(Fake.sent)) self.assertEqual(1, len(Fake.sent))
self.assertEqual('other:follow', Fake.sent[0][0]) self.assertEqual('other:follow', Fake.sent[0][0])
@ -1743,6 +1777,94 @@ class ProtocolReceiveTest(TestCase):
}], }],
}, obj.key.get().our_as1) }, 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): def test_receive_task_handler(self):
note = { note = {
'id': 'fake:post', '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('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('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, *_): def test_handle_to_id(self, *_):
self.assertEqual('foo.com', Web.handle_to_id('foo.com')) 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' 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 # import other modules that register Flask handlers *after* Fake is defined
models.reset_protocol_properties() models.reset_protocol_properties()

10
web.py
Wyświetl plik

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