kopia lustrzana https://github.com/snarfed/bridgy-fed
Porównaj commity
21 Commity
0ef3faee70
...
eb6d1098de
Autor | SHA1 | Data |
---|---|---|
dependabot[bot] | eb6d1098de | |
Ryan Barrett | 1686a2ba91 | |
Ryan Barrett | 0c37d94191 | |
Ryan Barrett | 7c34689c9f | |
Ryan Barrett | 70da21a7f3 | |
Ryan Barrett | 1981c8eba8 | |
Ryan Barrett | 20e061f476 | |
Ryan Barrett | 3c55d7c145 | |
dependabot[bot] | 64a196a8c8 | |
dependabot[bot] | 7dbab83a17 | |
Ryan Barrett | 2886ae180d | |
Ryan Barrett | 8bcae4c09d | |
Ryan Barrett | d36885728f | |
Ryan Barrett | 917732ad4b | |
Ryan Barrett | 5556f2756b | |
Ryan Barrett | 8077a7f4ca | |
Ryan Barrett | 39a641e000 | |
Ryan Barrett | 259b7d72dd | |
Ryan Barrett | f02ba80304 | |
Ryan Barrett | 393605bde9 | |
dependabot[bot] | 2f824410ff |
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
20
atproto.py
20
atproto.py
|
@ -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
|
||||||
|
|
||||||
|
|
42
common.py
42
common.py
|
@ -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):
|
||||||
|
|
|
@ -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
|
|
@ -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 protocol’s 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`` subclass’s ``LOGO_HTML`` constant.
|
||||||
|
|
||||||
Stats
|
Stats
|
||||||
-----
|
-----
|
||||||
|
|
4
hub.py
4
hub.py
|
@ -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
102
ids.py
|
@ -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)
|
||||||
|
|
66
index.yaml
66
index.yaml
|
@ -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
|
||||||
|
|
86
models.py
86
models.py
|
@ -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
|
||||||
|
|
170
protocol.py
170
protocol.py
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -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) %}
|
||||||
·
|
·
|
||||||
<nobr title="{{ proto.__name__ }} (bridged)">
|
<nobr title="{{ proto.__name__ }} (bridged)">
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'))
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
|
@ -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
10
web.py
|
@ -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
|
||||||
|
|
Ładowanie…
Reference in New Issue