kopia lustrzana https://github.com/snarfed/bridgy-fed
Porównaj commity
21 Commity
0cabbf0813
...
257aec3916
Autor | SHA1 | Data |
---|---|---|
dependabot[bot] | 257aec3916 | |
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] | e5df116765 |
|
@ -22,7 +22,7 @@ jobs:
|
|||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apt-transport-https ca-certificates gnupg google-cloud-sdk google-cloud-sdk-datastore-emulator default-jre
|
||||
sudo apt-get install -y apt-transport-https ca-certificates gnupg google-cloud-sdk google-cloud-cli-firestore-emulator default-jre
|
||||
|
||||
- run:
|
||||
name: Python dependencies
|
||||
|
@ -40,7 +40,7 @@ jobs:
|
|||
- run:
|
||||
name: Build and test
|
||||
command: |
|
||||
CLOUDSDK_CORE_PROJECT=bridgy-federated gcloud beta emulators datastore start --no-store-on-disk --use-firestore-in-datastore-mode --host-port=localhost:8089 < /dev/null >& /dev/null &
|
||||
CLOUDSDK_CORE_PROJECT=brid-gy gcloud emulators firestore start --host-port=:8089 --database-mode=datastore-mode < /dev/null >& /tmp/firestore-emulator.log &
|
||||
sleep 5s
|
||||
python -m coverage run --source=. --omit=appengine_config.py,logs.py,tests/\* -m unittest discover -v
|
||||
python -m coverage html -d /tmp/coverage_html
|
||||
|
|
|
@ -12,7 +12,8 @@ Development
|
|||
---
|
||||
Development reference docs are at [bridgy-fed.readthedocs.io](https://bridgy-fed.readthedocs.io/). Pull requests are welcome! Feel free to [ping me in #indieweb-dev](https://indieweb.org/discuss) with any questions.
|
||||
|
||||
First, fork and clone this repo. Then, install the [Google Cloud SDK](https://cloud.google.com/sdk/) and run `gcloud components install beta cloud-datastore-emulator` to install the [datastore emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator). Once you have them, set up your environment by running these commands in the repo root directory:
|
||||
First, fork and clone this repo. Then, install the [Google Cloud SDK](https://cloud.google.com/sdk/) and run `gcloud components install cloud-firestore-emulator` to install the [Firestore emulator](https://cloud.google.com/firestore/docs/emulator). Once you have them, set up your environment by running these commands in the repo root directory:
|
||||
|
||||
|
||||
```sh
|
||||
gcloud config set project bridgy-federated
|
||||
|
@ -24,7 +25,7 @@ pip install -r requirements.txt
|
|||
Now, run the tests to check that everything is set up ok:
|
||||
|
||||
```shell
|
||||
gcloud beta emulators datastore start --use-firestore-in-datastore-mode --no-store-on-disk --host-port=localhost:8089 --quiet < /dev/null >& /dev/null &
|
||||
gcloud emulators firestore start --host-port=:8089 --database-mode=datastore-mode < /dev/null >& /dev/null &
|
||||
python3 -m unittest discover
|
||||
```
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ from common import (
|
|||
host_url,
|
||||
LOCAL_DOMAINS,
|
||||
PRIMARY_DOMAIN,
|
||||
PROTOCOL_DOMAINS,
|
||||
redirect_wrap,
|
||||
subdomain_wrap,
|
||||
unwrap,
|
||||
|
@ -56,6 +57,8 @@ WEB_OPT_OUT_DOMAINS = None
|
|||
|
||||
FEDI_URL_RE = re.compile(r'https://[^/]+/(@|users/)([^/@]+)(@[^/@]+)?(/(?:statuses/)?[0-9]+)?')
|
||||
|
||||
_BOT_ACTOR_IDS = None
|
||||
|
||||
|
||||
def instance_actor():
|
||||
global _INSTANCE_ACTOR
|
||||
|
@ -65,6 +68,15 @@ def instance_actor():
|
|||
return _INSTANCE_ACTOR
|
||||
|
||||
|
||||
def bot_actor_ids():
|
||||
global _BOT_ACTOR_IDS
|
||||
if _BOT_ACTOR_IDS is None:
|
||||
from activitypub import ActivityPub
|
||||
_BOT_ACTOR_IDS = [translate_user_id(id=domain, from_=Web, to=ActivityPub)
|
||||
for domain in PROTOCOL_DOMAINS]
|
||||
return _BOT_ACTOR_IDS
|
||||
|
||||
|
||||
class ActivityPub(User, Protocol):
|
||||
"""ActivityPub protocol class.
|
||||
|
||||
|
@ -75,6 +87,7 @@ class ActivityPub(User, Protocol):
|
|||
LOGO_HTML = '<img src="/static/fediverse_logo.svg">'
|
||||
CONTENT_TYPE = as2.CONTENT_TYPE_LD_PROFILE
|
||||
HAS_FOLLOW_ACCEPTS = True
|
||||
DEFAULT_ENABLED_PROTOCOLS = ('web',)
|
||||
|
||||
def _pre_put_hook(self):
|
||||
"""Validate id, require URL, don't allow Bridgy Fed domains.
|
||||
|
@ -363,7 +376,7 @@ class ActivityPub(User, Protocol):
|
|||
from_proto = PROTOCOLS.get(obj.source_protocol)
|
||||
user_id = from_user.key.id() if from_user and from_user.key else None
|
||||
# TODO: uncomment
|
||||
# if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
|
||||
# if from_proto and not from_proto.is_enabled_to(cls, user=from_user):
|
||||
# error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
|
||||
|
||||
if obj.as2:
|
||||
|
@ -832,8 +845,6 @@ def actor(handle_or_id):
|
|||
cls = Protocol.for_request(fed='web')
|
||||
if not cls:
|
||||
error(f"Couldn't determine protocol", status=404)
|
||||
elif not common.is_enabled(cls, ActivityPub, handle_or_id=handle_or_id):
|
||||
error(f'{cls.LABEL} <=> activitypub not enabled')
|
||||
elif cls.LABEL == 'web' and request.path.startswith('/ap/'):
|
||||
# we started out with web users' AP ids as fed.brid.gy/[domain], so we
|
||||
# need to preserve those for backward compatibility
|
||||
|
@ -851,6 +862,9 @@ def actor(handle_or_id):
|
|||
id = handle_or_id
|
||||
|
||||
assert id
|
||||
if not cls.is_enabled_to(ActivityPub, user=id):
|
||||
error(f'{cls.LABEL} user {id} not found', status=404)
|
||||
|
||||
user = cls.get_or_create(id)
|
||||
if not user:
|
||||
error(f'{cls.LABEL} user {id} not found', status=404)
|
||||
|
@ -924,6 +938,7 @@ def inbox(protocol=None, id=None):
|
|||
# follows, or other activity types, since Mastodon doesn't currently mark
|
||||
# those as explicitly public. Use as2's is_public instead of as1's because
|
||||
# as1's interprets unlisted as true.
|
||||
# TODO: move this to Protocol
|
||||
if type == 'Create' and not as2.is_public(activity, unlisted=False):
|
||||
logger.info('Dropping non-public activity')
|
||||
return 'OK'
|
||||
|
|
20
atproto.py
20
atproto.py
|
@ -34,7 +34,6 @@ from common import (
|
|||
DOMAINS,
|
||||
error,
|
||||
USER_AGENT,
|
||||
USER_ALLOWLIST,
|
||||
)
|
||||
import flask_app
|
||||
from models import Object, PROTOCOLS, Target, User
|
||||
|
@ -86,12 +85,18 @@ class ATProto(User, Protocol):
|
|||
Key id is DID, currently either did:plc or did:web.
|
||||
https://atproto.com/specs/did
|
||||
"""
|
||||
ABBREV = 'atproto'
|
||||
ABBREV = 'bsky'
|
||||
# TODO: add second bsky label? inject into PROTOCOLS?
|
||||
PHRASE = 'Bluesky'
|
||||
LOGO_HTML = '<img src="/oauth_dropins_static/bluesky.svg">'
|
||||
PDS_URL = f'https://{ABBREV}{common.SUPERDOMAIN}/'
|
||||
# note that PDS hostname is atproto.brid.gy here, not bsky.brid.gy. Bluesky
|
||||
# team currently has our hostname as atproto.brid.gy in their federation
|
||||
# test.
|
||||
# TODO: switch to bsky.brid.gy once they lift their federation limits? we'd
|
||||
# need to update serviceEndpoint in all users' DID docs. :/
|
||||
PDS_URL = f'https://atproto{common.SUPERDOMAIN}/'
|
||||
CONTENT_TYPE = 'application/json'
|
||||
DEFAULT_ENABLED_PROTOCOLS = ()
|
||||
|
||||
def _pre_put_hook(self):
|
||||
"""Validate id, require did:plc or non-blocklisted did:web."""
|
||||
|
@ -501,9 +506,8 @@ class ATProto(User, Protocol):
|
|||
dict: JSON object
|
||||
"""
|
||||
from_proto = PROTOCOLS.get(obj.source_protocol)
|
||||
user_id = from_user.key.id() if from_user and from_user.key else None
|
||||
# TODO: uncomment
|
||||
# if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
|
||||
# if from_proto and not from_proto.is_enabled_to(cls, user=from_user):
|
||||
# error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
|
||||
|
||||
if obj.bsky:
|
||||
|
@ -575,8 +579,7 @@ def poll_notifications():
|
|||
headers={'User-Agent': USER_AGENT})
|
||||
|
||||
for user in users:
|
||||
# TODO: remove for launch
|
||||
if not DEBUG and user.key.id() not in USER_ALLOWLIST:
|
||||
if not user.is_enabled_to(ATProto, user=user):
|
||||
logger.info(f'Skipping {user.key.id()}')
|
||||
continue
|
||||
|
||||
|
@ -635,8 +638,7 @@ def poll_posts():
|
|||
headers={'User-Agent': USER_AGENT})
|
||||
|
||||
for user in users:
|
||||
# TODO: remove for launch
|
||||
if not DEBUG and user.key.id() not in USER_ALLOWLIST:
|
||||
if not user.is_enabled_to(ATProto, user=user):
|
||||
logger.info(f'Skipping {user.key.id()}')
|
||||
continue
|
||||
|
||||
|
|
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'
|
||||
|
||||
# Protocol pairs that we currently support bridging between. Values must be
|
||||
# Protocol LABELs. Each pair must be lexicographically sorted!
|
||||
ENABLED_BRIDGES = frozenset((
|
||||
('activitypub', 'web'),
|
||||
('atproto', 'web'),
|
||||
))
|
||||
|
||||
PRIMARY_DOMAIN = 'fed.brid.gy'
|
||||
# protocol-specific subdomains are under this "super"domain
|
||||
SUPERDOMAIN = '.brid.gy'
|
||||
|
@ -83,15 +76,6 @@ util.set_user_agent(USER_AGENT)
|
|||
TASKS_LOCATION = 'us-central1'
|
||||
RUN_TASKS_INLINE = False # overridden by unit tests
|
||||
|
||||
USER_ALLOWLIST = (
|
||||
'snarfed.org',
|
||||
'did:plc:fdme4gb7mu7zrie7peay7tst',
|
||||
'snarfed.bsky.social',
|
||||
'did:plc:3ljmtyyjqcjee2kpewgsifvb',
|
||||
'https://indieweb.social/users/snarfed',
|
||||
'@snarfed@indieweb.social',
|
||||
)
|
||||
|
||||
|
||||
def base64_to_long(x):
|
||||
"""Converts from URL safe base64 encoding to long integer.
|
||||
|
@ -264,29 +248,13 @@ def add(seq, val):
|
|||
seq.append(val)
|
||||
|
||||
|
||||
def is_enabled(proto_a, proto_b, handle_or_id=None):
|
||||
"""Returns True if bridging the two input protocols is enabled, False otherwise.
|
||||
def remove(seq, val):
|
||||
"""Removes ``val`` to ``seq`` if seq contains it.
|
||||
|
||||
Args:
|
||||
proto_a (Protocol subclass)
|
||||
proto_b (Protocol subclass)
|
||||
handle_or_id (str): optional user handle or id
|
||||
|
||||
Returns:
|
||||
bool:
|
||||
Useful for treating repeated ndb properties like sets instead of lists.
|
||||
"""
|
||||
if proto_a == proto_b:
|
||||
return True
|
||||
|
||||
labels = tuple(sorted((proto_a.LABEL, proto_b.LABEL)))
|
||||
|
||||
if DEBUG and ('fake' in labels or 'other' in labels):
|
||||
return True
|
||||
|
||||
if handle_or_id in USER_ALLOWLIST:
|
||||
return True
|
||||
|
||||
return labels in ENABLED_BRIDGES
|
||||
if val in seq:
|
||||
seq.remove(val)
|
||||
|
||||
|
||||
def create_task(queue, delay=None, **params):
|
||||
|
|
|
@ -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 connects your web site to
|
||||
`Mastodon <https://joinmastodon.org>`__ and the
|
||||
`fediverse <https://en.wikipedia.org/wiki/Fediverse>`__ via
|
||||
`ActivityPub <https://activitypub.rocks/>`__,
|
||||
`webmentions <https://webmention.net/>`__, and
|
||||
`microformats2 <https://microformats.org/wiki/microformats2>`__. Your
|
||||
site gets its own fediverse profile, posts and avatar and header and
|
||||
all. Bridgy Fed translates likes, reposts, mentions, follows, and more
|
||||
back and forth. `See the user docs <https://fed.brid.gy/docs>`__ and
|
||||
`developer docs <https://bridgy-fed.readthedocs.io/>`__ for more
|
||||
details.
|
||||
Bridgy Fed connects different decentralized social network protocols. It
|
||||
currently supports the
|
||||
`fediverse <https://en.wikipedia.org/wiki/Fediverse>`__ (eg
|
||||
`Mastodon <https://joinmastodon.org>`__) via
|
||||
`ActivityPub <https://activitypub.rocks/>`__, and the
|
||||
`IndieWeb <https://indieweb.org/>`__ via
|
||||
`webmentions <https://webmention.net/>`__ and
|
||||
`microformats2 <https://microformats.org/wiki/microformats2>`__.
|
||||
`Bluesky/AT
|
||||
Protocol <https://github.com/snarfed/bridgy-fed/issues/381>`__ and
|
||||
`Nostr <https://github.com/snarfed/bridgy-fed/issues/446>`__ are planned
|
||||
for 2024. Bridgy Fed translates profiles, likes, reposts, mentions,
|
||||
follows, and more from any supported network to any other. `See the user
|
||||
docs <https://fed.brid.gy/docs>`__ and `developer
|
||||
docs <https://bridgy-fed.readthedocs.io/>`__ for more details.
|
||||
|
||||
https://fed.brid.gy/
|
||||
|
||||
Also see the
|
||||
`original <https://snarfed.org/indieweb-activitypub-bridge>`__
|
||||
`design <https://snarfed.org/indieweb-ostatus-bridge>`__ blog posts.
|
||||
|
||||
License: This project is placed in the public domain.
|
||||
License: This project is placed in the public domain. You may also use
|
||||
it under the `CC0
|
||||
License <https://creativecommons.org/publicdomain/zero/1.0/>`__.
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
@ -31,11 +33,11 @@ requests are welcome! Feel free to `ping me in
|
|||
|
||||
First, fork and clone this repo. Then, install the `Google Cloud
|
||||
SDK <https://cloud.google.com/sdk/>`__ and run
|
||||
``gcloud components install beta cloud-datastore-emulator`` to install
|
||||
the `datastore
|
||||
emulator <https://cloud.google.com/datastore/docs/tools/datastore-emulator>`__.
|
||||
Once you have them, set up your environment by running these commands in
|
||||
the repo root directory:
|
||||
``gcloud components install cloud-firestore-emulator`` to install the
|
||||
`Firestore
|
||||
emulator <https://cloud.google.com/firestore/docs/emulator>`__. Once you
|
||||
have them, set up your environment by running these commands in the repo
|
||||
root directory:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
|
@ -48,7 +50,7 @@ Now, run the tests to check that everything is set up ok:
|
|||
|
||||
.. code:: shell
|
||||
|
||||
gcloud beta emulators datastore start --use-firestore-in-datastore-mode --no-store-on-disk --host-port=localhost:8089 --quiet < /dev/null >& /dev/null &
|
||||
gcloud emulators firestore start --host-port=:8089 --database-mode=datastore-mode < /dev/null >& /dev/null &
|
||||
python3 -m unittest discover
|
||||
|
||||
Finally, run this in the repo root directory to start the web app
|
||||
|
@ -112,8 +114,9 @@ How to add a new protocol
|
|||
5. TODO: add a new usage section to the docs for the new protocol.
|
||||
6. TODO: does the new protocol need any new UI or signup functionality?
|
||||
Unusual, but not impossible. Add that if necessary.
|
||||
7. Add the new protocol’s logo to ``static/``, use it in
|
||||
`templates/user.html <https://github.com/snarfed/bridgy-fed/blob/main/templates/user.html>`__.
|
||||
7. Protocol logos may be emoji or image files. If this one is a file,
|
||||
add it ``static/``. Then add the emoji or file ``<img>`` tag in the
|
||||
``Protocol`` subclass’s ``LOGO_HTML`` constant.
|
||||
|
||||
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',
|
||||
view_func=atproto.poll_notifications,
|
||||
methods=['POST'])
|
||||
methods=['GET', 'POST'])
|
||||
|
||||
app.add_url_rule('/queue/atproto-poll-posts',
|
||||
view_func=atproto.poll_posts,
|
||||
methods=['POST'])
|
||||
methods=['GET', 'POST'])
|
||||
|
||||
@app.post('/queue/atproto-commit')
|
||||
@flask_util.cloud_tasks_only
|
||||
|
|
102
ids.py
102
ids.py
|
@ -53,30 +53,30 @@ def web_ap_base_domain(user_domain):
|
|||
return f'https://{subdomain}{SUPERDOMAIN}/'
|
||||
|
||||
|
||||
def translate_user_id(*, id, from_proto, to_proto):
|
||||
def translate_user_id(*, id, from_, to):
|
||||
"""Translate a user id from one protocol to another.
|
||||
|
||||
TODO: unify with :func:`translate_object_id`.
|
||||
|
||||
Args:
|
||||
id (str)
|
||||
from_proto (protocol.Protocol)
|
||||
to_proto (protocol.Protocol)
|
||||
from_ (protocol.Protocol)
|
||||
to (protocol.Protocol)
|
||||
|
||||
Returns:
|
||||
str: the corresponding id in ``to_proto``
|
||||
str: the corresponding id in ``to``
|
||||
"""
|
||||
assert id and from_proto and to_proto, (id, from_proto, to_proto)
|
||||
assert from_proto.owns_id(id) is not False or from_proto.LABEL == 'ui', \
|
||||
(id, from_proto.LABEL, to_proto.LABEL)
|
||||
assert id and from_ and to, (id, from_, to)
|
||||
assert from_.owns_id(id) is not False or from_.LABEL == 'ui', \
|
||||
(id, from_.LABEL, to.LABEL)
|
||||
|
||||
parsed = urlparse(id)
|
||||
if from_proto.LABEL == 'web' and parsed.path.strip('/') == '':
|
||||
if from_.LABEL == 'web' and parsed.path.strip('/') == '':
|
||||
# home page; replace with domain
|
||||
id = parsed.netloc
|
||||
|
||||
# bsky.app profile URL to DID
|
||||
if to_proto.LABEL == 'atproto':
|
||||
if to.LABEL == 'atproto':
|
||||
if match := BSKY_APP_URL_RE.match(id):
|
||||
repo = match.group('id')
|
||||
if repo.startswith('did:'):
|
||||
|
@ -89,25 +89,25 @@ def translate_user_id(*, id, from_proto, to_proto):
|
|||
logger.warning(e)
|
||||
return None
|
||||
|
||||
if from_proto == to_proto:
|
||||
if from_ == to:
|
||||
return id
|
||||
|
||||
# follow use_instead
|
||||
user = from_proto.get_by_id(id)
|
||||
user = from_.get_by_id(id)
|
||||
if user:
|
||||
id = user.key.id()
|
||||
|
||||
if from_proto.LABEL in COPIES_PROTOCOLS or to_proto.LABEL in COPIES_PROTOCOLS:
|
||||
if from_.LABEL in COPIES_PROTOCOLS or to.LABEL in COPIES_PROTOCOLS:
|
||||
if user:
|
||||
if copy := user.get_copy(to_proto):
|
||||
if copy := user.get_copy(to):
|
||||
return copy
|
||||
if orig := models.get_original(id):
|
||||
if isinstance(orig, to_proto):
|
||||
if isinstance(orig, to):
|
||||
return orig.key.id()
|
||||
|
||||
match from_proto.LABEL, to_proto.LABEL:
|
||||
match from_.LABEL, to.LABEL:
|
||||
case _, 'atproto' | 'nostr':
|
||||
logger.warning(f"Can't translate user id {id} to {to_proto.LABEL} , haven't copied it there yet!")
|
||||
logger.warning(f"Can't translate user id {id} to {to.LABEL} , haven't copied it there yet!")
|
||||
return None
|
||||
|
||||
case 'web', 'activitypub':
|
||||
|
@ -117,45 +117,45 @@ def translate_user_id(*, id, from_proto, to_proto):
|
|||
return id
|
||||
|
||||
case _, 'activitypub' | 'web':
|
||||
return subdomain_wrap(from_proto, f'/{to_proto.ABBREV}/{id}')
|
||||
return subdomain_wrap(from_, f'/{to.ABBREV}/{id}')
|
||||
|
||||
# only for unit tests
|
||||
case _, 'fake' | 'other':
|
||||
return f'{to_proto.LABEL}:u:{id}'
|
||||
case _, 'fake' | 'other' | 'eefake':
|
||||
return f'{to.LABEL}:u:{id}'
|
||||
case 'fake' | 'other', _:
|
||||
return id
|
||||
|
||||
assert False, (id, from_proto.LABEL, to_proto.LABEL)
|
||||
assert False, (id, from_.LABEL, to.LABEL)
|
||||
|
||||
|
||||
def translate_handle(*, handle, from_proto, to_proto, enhanced):
|
||||
def translate_handle(*, handle, from_, to, enhanced):
|
||||
"""Translates a user handle from one protocol to another.
|
||||
|
||||
Args:
|
||||
handle (str)
|
||||
from_proto (protocol.Protocol)
|
||||
to_proto (protocol.Protocol)
|
||||
from_ (protocol.Protocol)
|
||||
to (protocol.Protocol)
|
||||
enhanced (bool): whether to convert to an "enhanced" handle based on the
|
||||
user's domain
|
||||
|
||||
Returns:
|
||||
str: the corresponding handle in ``to_proto``
|
||||
str: the corresponding handle in ``to``
|
||||
"""
|
||||
assert handle and from_proto and to_proto, (handle, from_proto, to_proto)
|
||||
assert from_proto.owns_handle(handle) is not False or from_proto.LABEL == 'ui'
|
||||
assert handle and from_ and to, (handle, from_, to)
|
||||
assert from_.owns_handle(handle) is not False or from_.LABEL == 'ui'
|
||||
|
||||
if from_proto == to_proto:
|
||||
if from_ == to:
|
||||
return handle
|
||||
|
||||
match from_proto.LABEL, to_proto.LABEL:
|
||||
match from_.LABEL, to.LABEL:
|
||||
case _, 'activitypub':
|
||||
domain = handle if enhanced else f'{from_proto.ABBREV}{SUPERDOMAIN}'
|
||||
domain = handle if enhanced else f'{from_.ABBREV}{SUPERDOMAIN}'
|
||||
return f'@{handle}@{domain}'
|
||||
|
||||
case _, 'atproto' | 'nostr':
|
||||
handle = handle.lstrip('@').replace('@', '.')
|
||||
return (handle if enhanced
|
||||
else f'{handle}.{from_proto.ABBREV}{SUPERDOMAIN}')
|
||||
else f'{handle}.{from_.ABBREV}{SUPERDOMAIN}')
|
||||
|
||||
case 'activitypub', 'web':
|
||||
user, instance = handle.lstrip('@').split('@')
|
||||
|
@ -167,32 +167,30 @@ def translate_handle(*, handle, from_proto, to_proto, enhanced):
|
|||
return handle
|
||||
|
||||
# only for unit tests
|
||||
case _, 'fake':
|
||||
return f'fake:handle:{handle}'
|
||||
case _, 'other':
|
||||
return f'other:handle:{handle}'
|
||||
case _, 'fake' | 'other' | 'eefake':
|
||||
return f'{to.LABEL}:handle:{handle}'
|
||||
|
||||
assert False, (handle, from_proto.LABEL, to_proto.LABEL)
|
||||
assert False, (handle, from_.LABEL, to.LABEL)
|
||||
|
||||
|
||||
def translate_object_id(*, id, from_proto, to_proto):
|
||||
def translate_object_id(*, id, from_, to):
|
||||
"""Translates a user handle from one protocol to another.
|
||||
|
||||
TODO: unify with :func:`translate_user_id`.
|
||||
|
||||
Args:
|
||||
id (str)
|
||||
from_proto (protocol.Protocol)
|
||||
to_proto (protocol.Protocol)
|
||||
from_ (protocol.Protocol)
|
||||
to (protocol.Protocol)
|
||||
|
||||
Returns:
|
||||
str: the corresponding id in ``to_proto``
|
||||
str: the corresponding id in ``to``
|
||||
"""
|
||||
assert id and from_proto and to_proto, (id, from_proto, to_proto)
|
||||
assert from_proto.owns_id(id) is not False or from_proto.LABEL == 'ui'
|
||||
assert id and from_ and to, (id, from_, to)
|
||||
assert from_.owns_id(id) is not False or from_.LABEL == 'ui'
|
||||
|
||||
# bsky.app profile URL to DID
|
||||
if to_proto.LABEL == 'atproto':
|
||||
if to.LABEL == 'atproto':
|
||||
if match := BSKY_APP_URL_RE.match(id):
|
||||
repo = match.group('id')
|
||||
handle = None
|
||||
|
@ -207,29 +205,29 @@ def translate_object_id(*, id, from_proto, to_proto):
|
|||
|
||||
return web_url_to_at_uri(id, handle=handle, did=repo)
|
||||
|
||||
if from_proto == to_proto:
|
||||
if from_ == to:
|
||||
return id
|
||||
|
||||
if from_proto.LABEL in COPIES_PROTOCOLS or to_proto.LABEL in COPIES_PROTOCOLS:
|
||||
if obj := from_proto.load(id, remote=False):
|
||||
if copy := obj.get_copy(to_proto):
|
||||
if from_.LABEL in COPIES_PROTOCOLS or to.LABEL in COPIES_PROTOCOLS:
|
||||
if obj := from_.load(id, remote=False):
|
||||
if copy := obj.get_copy(to):
|
||||
return copy
|
||||
if orig := models.get_original(id):
|
||||
return orig.key.id()
|
||||
|
||||
match from_proto.LABEL, to_proto.LABEL:
|
||||
match from_.LABEL, to.LABEL:
|
||||
case _, 'atproto' | 'nostr':
|
||||
logger.warning(f"Can't translate object id {id} to {to_proto.LABEL} , haven't copied it there yet!")
|
||||
logger.warning(f"Can't translate object id {id} to {to.LABEL} , haven't copied it there yet!")
|
||||
return id
|
||||
|
||||
case 'web', 'activitypub':
|
||||
return urljoin(web_ap_base_domain(util.domain_from_link(id)), f'/r/{id}')
|
||||
|
||||
case _, 'activitypub' | 'web':
|
||||
return subdomain_wrap(from_proto, f'/convert/{to_proto.ABBREV}/{id}')
|
||||
return subdomain_wrap(from_, f'/convert/{to.ABBREV}/{id}')
|
||||
|
||||
# only for unit tests
|
||||
case _, 'fake' | 'other':
|
||||
return f'{to_proto.LABEL}:o:{from_proto.ABBREV}:{id}'
|
||||
case _, 'fake' | 'other' | 'eefake':
|
||||
return f'{to.LABEL}:o:{from_.ABBREV}:{id}'
|
||||
|
||||
assert False, (id, from_proto.LABEL, to_proto.LABEL)
|
||||
assert False, (id, from_.LABEL, to.LABEL)
|
||||
|
|
66
index.yaml
66
index.yaml
|
@ -6,48 +6,6 @@ indexes:
|
|||
# index.yaml file manually, remove the "# AUTOGENERATED" marker line above.
|
||||
# If you want to manage some indexes manually, move them above the marker line.
|
||||
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: domains
|
||||
- name: labels
|
||||
- name: updated
|
||||
direction: asc
|
||||
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: users
|
||||
- name: labels
|
||||
- name: updated
|
||||
direction: asc
|
||||
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: domains
|
||||
- name: labels
|
||||
- name: updated
|
||||
direction: desc
|
||||
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: users
|
||||
- name: labels
|
||||
- name: updated
|
||||
direction: desc
|
||||
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: domains
|
||||
- name: labels
|
||||
- name: created
|
||||
direction: desc
|
||||
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: users
|
||||
- name: labels
|
||||
- name: created
|
||||
direction: desc
|
||||
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: users
|
||||
|
@ -59,18 +17,6 @@ indexes:
|
|||
- name: updated
|
||||
direction: desc
|
||||
|
||||
# these two are currently unused! as of 2024-01-24
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: users
|
||||
- name: created
|
||||
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: users
|
||||
- name: created
|
||||
direction: desc
|
||||
|
||||
- kind: Object
|
||||
properties:
|
||||
- name: notify
|
||||
|
@ -93,6 +39,12 @@ indexes:
|
|||
- name: created
|
||||
direction: desc
|
||||
|
||||
- kind: Follower
|
||||
properties:
|
||||
- name: from
|
||||
- name: status
|
||||
- name: updated
|
||||
|
||||
- kind: Follower
|
||||
properties:
|
||||
- name: from
|
||||
|
@ -100,6 +52,12 @@ indexes:
|
|||
- name: updated
|
||||
direction: desc
|
||||
|
||||
- kind: Follower
|
||||
properties:
|
||||
- name: to
|
||||
- name: status
|
||||
- name: updated
|
||||
|
||||
- kind: Follower
|
||||
properties:
|
||||
- name: to
|
||||
|
|
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
|
||||
|
||||
import common
|
||||
from common import add, base64_to_long, DOMAIN_RE, long_to_base64, unwrap
|
||||
from ids import translate_handle, translate_object_id, translate_user_id
|
||||
from common import add, base64_to_long, DOMAIN_RE, long_to_base64, remove, unwrap
|
||||
import ids
|
||||
|
||||
# maps string label to Protocol subclass. populated by ProtocolUserMeta.
|
||||
# seed with old and upcoming protocols that don't have their own classes (yet).
|
||||
|
@ -114,6 +114,8 @@ def reset_protocol_properties():
|
|||
'protocol', choices=list(PROTOCOLS.keys()), required=True)
|
||||
Object.source_protocol = ndb.StringProperty(
|
||||
'source_protocol', choices=list(PROTOCOLS.keys()))
|
||||
User.enabled_protocols = ndb.StringProperty(
|
||||
'enabled_protocols', choices=list(PROTOCOLS.keys()), repeated=True)
|
||||
|
||||
abbrevs = f'({"|".join(PROTOCOLS.keys())}|fed)'
|
||||
common.SUBDOMAIN_BASE_URL_RE = re.compile(
|
||||
|
@ -157,6 +159,11 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
# #nobridge in their profile
|
||||
manual_opt_out = ndb.BooleanProperty()
|
||||
|
||||
# protocols that this user has explicitly opted into. protocols that don't
|
||||
# require explicit opt in are omitted here. choices is populated in
|
||||
# reset_protocol_properties.
|
||||
enabled_protocols = ndb.StringProperty(repeated=True, choices=[])
|
||||
|
||||
created = ndb.DateTimeProperty(auto_now_add=True)
|
||||
updated = ndb.DateTimeProperty(auto_now=True)
|
||||
|
||||
|
@ -222,13 +229,16 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
if user.status == 'opt-out':
|
||||
return None
|
||||
user.existing = True
|
||||
# override direct from False => True if set
|
||||
# TODO: propagate more props into user?
|
||||
direct = kwargs.get('direct')
|
||||
if direct and not user.direct:
|
||||
logger.info(f'Setting {user.key} direct={direct}')
|
||||
user.direct = direct
|
||||
user.put()
|
||||
|
||||
# TODO: propagate more fields?
|
||||
for field in ['direct', 'obj', 'obj_key']:
|
||||
old_val = getattr(user, field, None)
|
||||
new_val = kwargs.get(field)
|
||||
if ((old_val is None and new_val is not None)
|
||||
or (field == 'direct' and not old_val and new_val)):
|
||||
setattr(user, field, new_val)
|
||||
user.put()
|
||||
|
||||
if not propagate:
|
||||
return user
|
||||
else:
|
||||
|
@ -241,7 +251,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
|
||||
ATProto = PROTOCOLS['atproto']
|
||||
if propagate and cls.LABEL != 'atproto' and not user.get_copy(ATProto):
|
||||
if common.is_enabled(cls, ATProto, handle_or_id=id):
|
||||
if cls.is_enabled_to(ATProto, user=id):
|
||||
ATProto.create_for(user)
|
||||
else:
|
||||
logger.info(f'{cls.LABEL} <=> atproto not enabled, skipping')
|
||||
|
@ -341,6 +351,32 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
|
||||
return None
|
||||
|
||||
@ndb.transactional()
|
||||
def enable_protocol(self, to_proto):
|
||||
"""Adds ``to_proto` to :attr:`enabled_protocols`.
|
||||
|
||||
Args:
|
||||
to_proto (:class:`protocol.Protocol` subclass)
|
||||
"""
|
||||
user = self.key.get()
|
||||
add(user.enabled_protocols, to_proto.LABEL)
|
||||
user.put()
|
||||
|
||||
add(self.enabled_protocols, to_proto.LABEL)
|
||||
|
||||
@ndb.transactional()
|
||||
def disable_protocol(self, to_proto):
|
||||
"""Removes ``to_proto` from :attr:`enabled_protocols`.
|
||||
|
||||
Args:
|
||||
to_proto (:class:`protocol.Protocol` subclass)
|
||||
"""
|
||||
user = self.key.get()
|
||||
remove(user.enabled_protocols, to_proto.LABEL)
|
||||
user.put()
|
||||
|
||||
remove(self.enabled_protocols, to_proto.LABEL)
|
||||
|
||||
def handle_as(self, to_proto):
|
||||
"""Returns this user's handle in a different protocol.
|
||||
|
||||
|
@ -359,8 +395,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
if not handle:
|
||||
return None
|
||||
|
||||
return translate_handle(handle=handle, from_proto=self.__class__,
|
||||
to_proto=to_proto, enhanced=False)
|
||||
return ids.translate_handle(handle=handle, from_=self.__class__,
|
||||
to=to_proto, enhanced=False)
|
||||
|
||||
def id_as(self, to_proto):
|
||||
"""Returns this user's id in a different protocol.
|
||||
|
@ -374,8 +410,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
if isinstance(to_proto, str):
|
||||
to_proto = PROTOCOLS[to_proto]
|
||||
|
||||
return translate_user_id(id=self.key.id(), from_proto=self.__class__,
|
||||
to_proto=to_proto)
|
||||
return ids.translate_user_id(id=self.key.id(), from_=self.__class__,
|
||||
to=to_proto)
|
||||
|
||||
def handle_or_id(self):
|
||||
"""Returns handle if we know it, otherwise id."""
|
||||
|
@ -535,9 +571,6 @@ class Object(StringIdModel):
|
|||
'feed', 'notification', 'user')
|
||||
|
||||
# Keys for user(s) who created or otherwise own this activity.
|
||||
#
|
||||
# DEPRECATED: this used to include all users related the activity, including
|
||||
# followers, but we've now moved those to the notify and feed properties.
|
||||
users = ndb.KeyProperty(repeated=True)
|
||||
# User keys who should see this activity in their user page, eg in reply to,
|
||||
# reaction to, share of, etc.
|
||||
|
@ -551,8 +584,8 @@ class Object(StringIdModel):
|
|||
domains = ndb.StringProperty(repeated=True)
|
||||
|
||||
status = ndb.StringProperty(choices=STATUSES)
|
||||
# choices is populated in app, after all User subclasses are created,
|
||||
# so that PROTOCOLS is fully populated
|
||||
# choices is populated in reset_protocol_properties, after all User
|
||||
# subclasses are created, so that PROTOCOLS is fully populated.
|
||||
# TODO: nail down whether this is ABBREV or LABEL
|
||||
source_protocol = ndb.StringProperty(choices=[])
|
||||
labels = ndb.StringProperty(repeated=True, choices=LABELS)
|
||||
|
@ -921,7 +954,7 @@ class Object(StringIdModel):
|
|||
original objects in their source protocol, eg
|
||||
``at://did:plc:abc/app.bsky.feed.post/123`` => ``https://mas.to/@user/456``.
|
||||
* Bridgy Fed subdomain URLs to the ids embedded inside them, eg
|
||||
``https://atproto.brid.gy/ap/did:plc:xyz`` => ``did:plc:xyz``
|
||||
``https://bsky.brid.gy/ap/did:plc:xyz`` => ``did:plc:xyz``
|
||||
* ATProto bsky.app URLs to their DIDs or `at://` URIs, eg
|
||||
``https://bsky.app/profile/a.com`` => ``did:plc:123``
|
||||
|
||||
|
@ -1044,7 +1077,7 @@ class Object(StringIdModel):
|
|||
if not proto:
|
||||
return val
|
||||
|
||||
translated = translate_fn(id=orig, from_proto=proto, to_proto=proto)
|
||||
translated = translate_fn(id=orig, from_=proto, to=proto)
|
||||
if translated and translated != orig:
|
||||
logger.info(f'Normalized {proto.LABEL} id {orig} to {translated}')
|
||||
replaced = True
|
||||
|
@ -1060,20 +1093,21 @@ class Object(StringIdModel):
|
|||
for obj in [outer_obj] + inner_objs:
|
||||
for tag in as1.get_objects(obj, 'tags'):
|
||||
if tag.get('objectType') == 'mention':
|
||||
tag['url'] = replace(tag.get('url'), translate_user_id)
|
||||
tag['url'] = replace(tag.get('url'), ids.translate_user_id)
|
||||
for field in ['actor', 'author', 'inReplyTo']:
|
||||
fn = translate_object_id if field == 'inReplyTo' else translate_user_id
|
||||
fn = (ids.translate_object_id if field == 'inReplyTo'
|
||||
else ids.translate_user_id)
|
||||
obj[field] = [replace(val, fn) for val in util.get_list(obj, field)]
|
||||
if len(obj[field]) == 1:
|
||||
obj[field] = obj[field][0]
|
||||
|
||||
outer_obj['object'] = []
|
||||
for inner_obj in inner_objs:
|
||||
translate_fn = (translate_user_id
|
||||
translate_fn = (ids.translate_user_id
|
||||
if (as1.object_type(inner_obj) in as1.ACTOR_TYPES
|
||||
or as1.object_type(outer_obj) in
|
||||
('follow', 'stop-following'))
|
||||
else translate_object_id)
|
||||
else ids.translate_object_id)
|
||||
|
||||
got = replace(inner_obj, translate_fn)
|
||||
if isinstance(got, dict) and util.trim_nulls(got).keys() == {'id'}:
|
||||
|
@ -1180,7 +1214,7 @@ class Follower(ndb.Model):
|
|||
query = Follower.query(
|
||||
Follower.status == 'active',
|
||||
filter_prop == user.key,
|
||||
).order(-Follower.updated)
|
||||
)
|
||||
|
||||
followers, before, after = fetch_page(query, Follower, by=Follower.updated)
|
||||
users = ndb.get_multi(f.from_ if collection == 'followers' else f.to
|
||||
|
|
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.model import _entity_to_protobuf
|
||||
from granary import as1
|
||||
from oauth_dropins.webutil.appengine_info import DEBUG
|
||||
from oauth_dropins.webutil.flask_util import cloud_tasks_only
|
||||
from oauth_dropins.webutil import util
|
||||
from oauth_dropins.webutil import models
|
||||
from oauth_dropins.webutil import util
|
||||
from oauth_dropins.webutil.util import json_dumps, json_loads
|
||||
import werkzeug.exceptions
|
||||
|
||||
import common
|
||||
from common import add, DOMAIN_BLOCKLIST, DOMAIN_RE, DOMAINS, error, subdomain_wrap
|
||||
from common import (
|
||||
add,
|
||||
DOMAIN_BLOCKLIST,
|
||||
DOMAIN_RE,
|
||||
DOMAINS,
|
||||
error,
|
||||
PROTOCOL_DOMAINS,
|
||||
subdomain_wrap,
|
||||
)
|
||||
from flask_app import app
|
||||
from ids import translate_object_id, translate_user_id
|
||||
from models import Follower, get_originals, Object, PROTOCOLS, Target, User
|
||||
|
@ -28,6 +37,7 @@ SUPPORTED_TYPES = (
|
|||
'accept',
|
||||
'article',
|
||||
'audio',
|
||||
'block',
|
||||
'comment',
|
||||
'delete',
|
||||
'follow',
|
||||
|
@ -70,6 +80,8 @@ class Protocol:
|
|||
appropriate for the ``Content-Type`` HTTP header.
|
||||
HAS_FOLLOW_ACCEPTS (bool): whether this protocol supports explicit
|
||||
accept/reject activities in response to follows, eg ActivityPub
|
||||
DEFAULT_ENABLED_PROTOCOLS (list of str): labels of other protocols that
|
||||
are automatically enabled for this protocol to bridge into
|
||||
"""
|
||||
ABBREV = None
|
||||
PHRASE = None
|
||||
|
@ -77,6 +89,7 @@ class Protocol:
|
|||
LOGO_HTML = ''
|
||||
CONTENT_TYPE = None
|
||||
HAS_FOLLOW_ACCEPTS = False
|
||||
DEFAULT_ENABLED_PROTOCOLS = ()
|
||||
|
||||
def __init__(self):
|
||||
assert False
|
||||
|
@ -126,6 +139,52 @@ class Protocol:
|
|||
label = domain.removesuffix(common.SUPERDOMAIN)
|
||||
return PROTOCOLS.get(label)
|
||||
|
||||
@classmethod
|
||||
def is_enabled_to(from_cls, to_cls, user=None):
|
||||
"""Returns True if two protocols, and optionally a user, can be bridged.
|
||||
|
||||
Reasons this might return False:
|
||||
* We haven't turned on bridging these two protocols yet.
|
||||
* The user is opted out.
|
||||
* The user is on a domain that's opted out.
|
||||
* The from protocol requires opt in, and the user hasn't opted in.
|
||||
|
||||
Args:
|
||||
from_cls (Protocol subclass)
|
||||
to_cls (Protocol subclass)
|
||||
user (:class:`models.User` or str): optional, user or id
|
||||
|
||||
Returns:
|
||||
bool:
|
||||
"""
|
||||
if from_cls == to_cls:
|
||||
return True
|
||||
|
||||
from_label = from_cls.LABEL
|
||||
to_label = to_cls.LABEL
|
||||
|
||||
if DEBUG and (from_label in ('fake', 'other')
|
||||
or (to_label in ('fake', 'other') and from_label != 'eefake')):
|
||||
return True
|
||||
|
||||
user_id = None
|
||||
if isinstance(user, User):
|
||||
user_id = user.key.id() if user.key else None
|
||||
elif isinstance(user, str):
|
||||
user_id = user
|
||||
user = from_cls.get_by_id(user_id, allow_opt_out=True)
|
||||
|
||||
if user:
|
||||
if user.status == 'opt-out':
|
||||
return False
|
||||
elif to_label in user.enabled_protocols:
|
||||
return True
|
||||
|
||||
if to_label in from_cls.DEFAULT_ENABLED_PROTOCOLS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def owns_id(cls, id):
|
||||
"""Returns whether this protocol owns the id, or None if it's unclear.
|
||||
|
@ -497,7 +556,7 @@ class Protocol:
|
|||
|
||||
For example, if ``proto`` is :class:`ActivityPub`, the ATProto URI
|
||||
``at://did:plc:abc/coll/123`` will be converted to
|
||||
``https://atproto.brid.gy/ap/at://did:plc:abc/coll/123``.
|
||||
``https://bsky.brid.gy/ap/at://did:plc:abc/coll/123``.
|
||||
|
||||
Wraps these AS1 fields:
|
||||
|
||||
|
@ -539,7 +598,7 @@ class Protocol:
|
|||
# TODO: what if from_cls is None? relax translate_object_id,
|
||||
# make it a noop if we don't know enough about from/to?
|
||||
if from_cls and from_cls != to_cls:
|
||||
elem[field]['id'] = fn(id=id, from_proto=from_cls, to_proto=to_cls)
|
||||
elem[field]['id'] = fn(id=id, from_=from_cls, to=to_cls)
|
||||
if elem[field].keys() == {'id'}:
|
||||
elem[field] = elem[field]['id']
|
||||
|
||||
|
@ -736,6 +795,29 @@ class Protocol:
|
|||
|
||||
# fall through to deliver to followers
|
||||
|
||||
elif obj.type == 'block':
|
||||
proto = Protocol.for_bridgy_subdomain(inner_obj_id)
|
||||
if not proto:
|
||||
logger.info("Ignoring block, target isn't one of our protocol domains")
|
||||
return 'OK', 200
|
||||
|
||||
from_user.disable_protocol(proto)
|
||||
return 'OK', 200
|
||||
|
||||
elif obj.type == 'post':
|
||||
to_cc = (util.get_list(inner_obj_as1, 'to')
|
||||
+ util.get_list(inner_obj_as1, 'cc'))
|
||||
if len(to_cc) == 1 and to_cc[0] in PROTOCOL_DOMAINS:
|
||||
content = inner_obj_as1.get('content').strip().lower()
|
||||
logger.info(f'DM to bot user {to_cc}: {content}')
|
||||
proto = Protocol.for_bridgy_subdomain(to_cc[0])
|
||||
assert proto
|
||||
if content in ('yes', 'ok'):
|
||||
from_user.enable_protocol(proto)
|
||||
elif content == 'no':
|
||||
from_user.disable_protocol(proto)
|
||||
return 'OK', 200
|
||||
|
||||
# fetch actor if necessary
|
||||
if actor and actor.keys() == set(['id']):
|
||||
logger.info('Fetching actor so we have name, profile photo, etc')
|
||||
|
@ -759,6 +841,12 @@ class Protocol:
|
|||
}
|
||||
|
||||
if obj.type == 'follow':
|
||||
proto = Protocol.for_bridgy_subdomain(inner_obj_id)
|
||||
if proto:
|
||||
# follow of one of our protocol users; enable that protocol.
|
||||
# foll through so that we send an accept.
|
||||
from_user.enable_protocol(proto)
|
||||
|
||||
from_cls.handle_follow(obj)
|
||||
|
||||
# deliver to targets
|
||||
|
@ -790,14 +878,11 @@ class Protocol:
|
|||
from_obj.our_as1 = from_as1
|
||||
from_obj.put()
|
||||
|
||||
from_target = from_cls.target_for(from_obj)
|
||||
if not from_target:
|
||||
error(f"Couldn't find delivery target for follower {from_obj}")
|
||||
|
||||
from_key = from_cls.key_for(from_id)
|
||||
if not from_key:
|
||||
error(f'Invalid {from_cls} user key: {from_id}')
|
||||
obj.users = [from_key]
|
||||
from_user = from_cls.get_or_create(id=from_key.id(), obj=from_obj)
|
||||
|
||||
# Prepare followee (to) users' data
|
||||
to_as1s = as1.get_objects(obj.as1)
|
||||
|
@ -807,9 +892,8 @@ class Protocol:
|
|||
# Store Followers
|
||||
for to_as1 in to_as1s:
|
||||
to_id = to_as1.get('id')
|
||||
if not to_id or not from_id:
|
||||
if not to_id:
|
||||
error(f'Follow activity requires object(s). Got: {obj.as1}')
|
||||
from_user = from_cls.get_or_create(id=from_key.id(), obj=from_obj)
|
||||
|
||||
logger.info(f'Follow {from_id} => {to_id}')
|
||||
|
||||
|
@ -825,43 +909,57 @@ class Protocol:
|
|||
to_obj.our_as1 = to_as1
|
||||
to_obj.put()
|
||||
|
||||
# If followee user is already direct, follower may not know they're
|
||||
# interacting with a bridge. if followee user is indirect though,
|
||||
# follower should know, so they're direct.
|
||||
to_key = to_cls.key_for(to_id)
|
||||
if not to_key:
|
||||
logger.info(f'Skipping invalid {from_cls} user key: {from_id}')
|
||||
continue
|
||||
|
||||
# If followee user is already direct, follower may not know they're
|
||||
# interacting with a bridge. if followee user is indirect though,
|
||||
# follower should know, so they're direct.
|
||||
to_user = to_cls.get_or_create(id=to_key.id(), obj=to_obj, direct=False)
|
||||
|
||||
# HACK: we rewrite direct here for each followee, so the last one
|
||||
# wins. Could we do something better?
|
||||
from_user = from_cls.get_or_create(id=from_key.id(), obj=from_obj,
|
||||
direct=not to_user.direct)
|
||||
follower_obj = Follower.get_or_create(to=to_user, from_=from_user,
|
||||
follow=obj.key, status='active')
|
||||
obj.add('notify', to_key)
|
||||
from_cls.maybe_accept_follow(follower=from_user, followee=to_user,
|
||||
follow=obj)
|
||||
|
||||
if not to_user.HAS_FOLLOW_ACCEPTS:
|
||||
# send accept. note that this is one accept for the whole
|
||||
# follow, even if it has multiple followees!
|
||||
id = to_user.id_as('activitypub') + f'/followers#accept-{obj.key.id()}'
|
||||
accept = Object.get_or_create(id, our_as1={
|
||||
'id': id,
|
||||
'objectType': 'activity',
|
||||
'verb': 'accept',
|
||||
'actor': to_id,
|
||||
'object': obj.as1,
|
||||
})
|
||||
@classmethod
|
||||
def maybe_accept_follow(_, follower, followee, follow):
|
||||
"""Sends an accept activity for a follow.
|
||||
|
||||
sent = from_cls.send(accept, from_target, from_user=to_user)
|
||||
if sent:
|
||||
accept.populate(
|
||||
delivered=[Target(protocol=from_cls.LABEL, uri=from_target)],
|
||||
status='complete',
|
||||
)
|
||||
accept.put()
|
||||
...if the follower protocol handles accepts. Otherwise, does nothing.
|
||||
|
||||
Args:
|
||||
follower: :class:`models.User`
|
||||
followee: :class:`models.User`
|
||||
follow: :class:`models.Object`
|
||||
"""
|
||||
if followee.HAS_FOLLOW_ACCEPTS:
|
||||
return
|
||||
|
||||
# send accept. note that this is one accept for the whole
|
||||
# follow, even if it has multiple followees!
|
||||
id = f'{followee.key.id()}/followers#accept-{follow.key.id()}'
|
||||
accept = Object.get_or_create(id, our_as1={
|
||||
'id': id,
|
||||
'objectType': 'activity',
|
||||
'verb': 'accept',
|
||||
'actor': followee.key.id(),
|
||||
'object': follow.as1,
|
||||
})
|
||||
|
||||
from_target = follower.target_for(follower.obj)
|
||||
if not from_target:
|
||||
error(f"Couldn't find delivery target for follower {follower}")
|
||||
|
||||
sent = follower.send(accept, from_target, from_user=followee)
|
||||
if sent:
|
||||
accept.populate(
|
||||
delivered=[Target(protocol=follower.LABEL, uri=from_target)],
|
||||
status='complete',
|
||||
)
|
||||
accept.put()
|
||||
|
||||
@classmethod
|
||||
def handle_bare_object(cls, obj):
|
||||
|
|
|
@ -48,8 +48,8 @@ google-cloud-ndb==2.3.1
|
|||
google-cloud-tasks==2.16.3
|
||||
googleapis-common-protos==1.63.0
|
||||
grpc-google-iam-v1==0.13.0
|
||||
grpcio==1.62.1
|
||||
grpcio-status==1.62.1
|
||||
grpcio==1.62.2
|
||||
grpcio-status==1.62.2
|
||||
gunicorn==22.0.0
|
||||
h11==0.14.0
|
||||
html2text==2024.2.26
|
||||
|
@ -89,7 +89,7 @@ pytz==2024.1
|
|||
PyYAML==6.0.1
|
||||
redis==5.0.3
|
||||
requests==2.31.0
|
||||
requests-oauthlib==1.4.0
|
||||
requests-oauthlib==2.0.0
|
||||
rsa==4.9
|
||||
sgmllib3k==1.0.0
|
||||
simple-websocket==1.0.0
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
@ -59,9 +59,8 @@
|
|||
|
||||
{% set copies = user.copies|map(attribute='protocol')|list %}
|
||||
{% for proto in set(PROTOCOLS.values()) %}
|
||||
{% if proto and not isinstance(user, proto)
|
||||
and proto.LABEL not in ('ui', 'web')
|
||||
and (proto.LABEL not in ids.COPIES_PROTOCOLS or proto.LABEL in copies) %}
|
||||
{% if proto and not isinstance(user, proto) and proto.LABEL not in ('ui', 'web')
|
||||
and user.is_enabled_to(proto, user=user) %}
|
||||
{% set url = proto.bridged_web_url_for(user) %}
|
||||
·
|
||||
<nobr title="{{ proto.__name__ }} (bridged)">
|
||||
|
|
|
@ -230,7 +230,7 @@ ACCEPT_FOLLOW['object'] = 'http://localhost/user.com'
|
|||
ACCEPT = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Accept',
|
||||
'id': 'http://localhost/user.com/followers#accept-https://mas.to/6d1a',
|
||||
'id': 'http://localhost/r/user.com/followers#accept-https://mas.to/6d1a',
|
||||
'actor': 'http://localhost/user.com',
|
||||
'object': {
|
||||
'type': 'Follow',
|
||||
|
@ -440,10 +440,9 @@ class ActivityPubTest(TestCase):
|
|||
def test_actor_atproto_not_enabled(self, *_):
|
||||
self.store_object(id='did:plc:user', raw={'foo': 'baz'})
|
||||
self.make_user('did:plc:user', cls=ATProto)
|
||||
got = self.client.get('/ap/did:plc:user', base_url='https://atproto.brid.gy/')
|
||||
self.assertEqual(400, got.status_code)
|
||||
got = self.client.get('/ap/did:plc:user', base_url='https://bsky.brid.gy/')
|
||||
self.assertEqual(404, got.status_code)
|
||||
|
||||
@patch('common.ENABLED_BRIDGES', new=[('activitypub', 'atproto')])
|
||||
def test_actor_atproto_no_handle(self, *_):
|
||||
self.store_object(id='did:plc:user', raw={'foo': 'bar'})
|
||||
self.store_object(id='at://did:plc:user/app.bsky.actor.profile/self', bsky={
|
||||
|
@ -451,9 +450,9 @@ class ActivityPubTest(TestCase):
|
|||
'displayName': 'Alice',
|
||||
})
|
||||
|
||||
self.make_user('did:plc:user', cls=ATProto)
|
||||
self.make_user('did:plc:user', cls=ATProto, enabled_protocols=['activitypub'])
|
||||
|
||||
got = self.client.get('/ap/did:plc:user', base_url='https://atproto.brid.gy/')
|
||||
got = self.client.get('/ap/did:plc:user', base_url='https://bsky.brid.gy/')
|
||||
self.assertEqual(200, got.status_code)
|
||||
self.assertNotIn('preferredUsername', got.json)
|
||||
|
||||
|
@ -830,6 +829,9 @@ class ActivityPubTest(TestCase):
|
|||
def test_inbox_unlisted(self, *mocks):
|
||||
self._test_inbox_with_to_ignored(['@unlisted'], *mocks)
|
||||
|
||||
def test_inbox_dm(self, *mocks):
|
||||
self._test_inbox_with_to_ignored(['http://localhost/web/user.com'], *mocks)
|
||||
|
||||
def _test_inbox_with_to_ignored(self, to, mock_head, mock_get, mock_post):
|
||||
Follower.get_or_create(to=self.make_user(ACTOR['id'], cls=ActivityPub),
|
||||
from_=self.user)
|
||||
|
@ -1036,7 +1038,7 @@ class ActivityPubTest(TestCase):
|
|||
ignore=['created', 'updated'])
|
||||
|
||||
self.assert_user(ActivityPub, 'https://mas.to/users/swentel',
|
||||
obj_as2=ACTOR, direct=True)
|
||||
obj_as2=ACTOR, direct=False)
|
||||
self.assert_user(Web, 'user.com', direct=False,
|
||||
has_hcard=True, has_redirects=True)
|
||||
|
||||
|
@ -1097,7 +1099,7 @@ class ActivityPubTest(TestCase):
|
|||
self.assert_equals(('http://mas.to/inbox',), args)
|
||||
self.assert_equals({
|
||||
'type': 'Accept',
|
||||
'id': 'https://web.brid.gy/user.com/followers#accept-https://mas.to/6d1a',
|
||||
'id': 'https://web.brid.gy/r/user.com/followers#accept-https://mas.to/6d1a',
|
||||
'actor': 'https://web.brid.gy/user.com',
|
||||
'object': {
|
||||
'type': 'Follow',
|
||||
|
@ -1184,8 +1186,8 @@ class ActivityPubTest(TestCase):
|
|||
]
|
||||
mock_post.return_value = requests_response()
|
||||
|
||||
follower = Follower.get_or_create(to=self.user,
|
||||
from_=ActivityPub.get_or_create(ACTOR['id']),
|
||||
follower_key = ActivityPub.get_or_create(ACTOR['id'])
|
||||
follower = Follower.get_or_create(to=self.user, from_=follower_key,
|
||||
status='inactive')
|
||||
|
||||
undo_follow = copy.deepcopy(UNDO_FOLLOW_WRAPPED)
|
||||
|
@ -1200,9 +1202,9 @@ class ActivityPubTest(TestCase):
|
|||
got = self.post('/user.com/inbox', json={
|
||||
'@context': ['https://www.w3.org/ns/activitystreams'],
|
||||
'id': 'https://xoxo.zone/users/aaronpk#follows/40',
|
||||
'type': 'Block',
|
||||
'type': 'Arrive',
|
||||
'actor': 'https://xoxo.zone/users/aaronpk',
|
||||
'object': 'http://snarfed.org/',
|
||||
'object': 'http://a/place',
|
||||
})
|
||||
self.assertEqual(501, got.status_code)
|
||||
|
||||
|
@ -1649,7 +1651,7 @@ class ActivityPubTest(TestCase):
|
|||
def test_following_collection_page(self, *_):
|
||||
self.store_following()
|
||||
after = datetime(1900, 1, 1).isoformat()
|
||||
prev = Follower.query(Follower.to == ActivityPub(id='http://baz').key,
|
||||
prev = Follower.query(Follower.to == ActivityPub(id='http://bar').key,
|
||||
Follower.from_ == self.user.key,
|
||||
).get().updated.isoformat()
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ class ATProtoTest(TestCase):
|
|||
protocol='atproto')])
|
||||
|
||||
did_doc = copy.deepcopy(DID_DOC)
|
||||
did_doc['service'][0]['serviceEndpoint'] = 'https://atproto.brid.gy/'
|
||||
did_doc['service'][0]['serviceEndpoint'] = ATProto.PDS_URL
|
||||
self.store_object(id='did:plc:user', raw=did_doc)
|
||||
Repo.create(self.storage, 'did:plc:user', signing_key=ATPROTO_KEY)
|
||||
|
||||
|
@ -639,11 +639,11 @@ class ATProtoTest(TestCase):
|
|||
user = self.make_user('did:plc:user', cls=ATProto)
|
||||
|
||||
# TODO? or remove?
|
||||
# self.assertEqual('@did:plc:user@atproto.brid.gy',
|
||||
# self.assertEqual('@did:plc:user@bsky.brid.gy',
|
||||
# user.handle_as('activitypub'))
|
||||
|
||||
self.store_object(id='did:plc:user', raw=DID_DOC)
|
||||
self.assertEqual('@han.dull@atproto.brid.gy', user.handle_as('activitypub'))
|
||||
self.assertEqual('@han.dull@bsky.brid.gy', user.handle_as('activitypub'))
|
||||
|
||||
@patch('requests.get', return_value=requests_response(DID_DOC))
|
||||
def test_profile_id(self, mock_get):
|
||||
|
@ -711,7 +711,7 @@ class ATProtoTest(TestCase):
|
|||
'actor': 'fake:user',
|
||||
})
|
||||
|
||||
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
|
||||
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
|
||||
|
||||
# check DID doc
|
||||
user = user.key.get()
|
||||
|
@ -775,7 +775,7 @@ class ATProtoTest(TestCase):
|
|||
Fake.fetchable = {'fake:user': ACTOR_AS}
|
||||
|
||||
obj = self.store_object(id='fake:post', source_protocol='fake', our_as1=NOTE_AS)
|
||||
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
|
||||
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
|
||||
|
||||
# check profile, record
|
||||
user = Fake.get_by_id('fake:user')
|
||||
|
@ -812,7 +812,7 @@ class ATProtoTest(TestCase):
|
|||
user = self.make_user_and_repo()
|
||||
obj = self.store_object(id='fake:post', source_protocol='fake',
|
||||
our_as1=NOTE_AS)
|
||||
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
|
||||
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
|
||||
|
||||
# check repo, record
|
||||
did = user.key.get().get_copy(ATProto)
|
||||
|
@ -841,7 +841,7 @@ class ATProtoTest(TestCase):
|
|||
'verb': 'update',
|
||||
'object': note.our_as1,
|
||||
})
|
||||
self.assertTrue(ATProto.send(update, 'https://atproto.brid.gy/'))
|
||||
self.assertTrue(ATProto.send(update, 'https://bsky.brid.gy/'))
|
||||
|
||||
# check repo, record
|
||||
did = self.user.key.get().get_copy(ATProto)
|
||||
|
@ -863,7 +863,7 @@ class ATProtoTest(TestCase):
|
|||
'actor': 'fake:user',
|
||||
'object': 'fake:post',
|
||||
})
|
||||
self.assertTrue(ATProto.send(update, 'https://atproto.brid.gy/'))
|
||||
self.assertTrue(ATProto.send(update, 'https://bsky.brid.gy/'))
|
||||
|
||||
# check repo, record
|
||||
did = self.user.key.get().get_copy(ATProto)
|
||||
|
@ -895,7 +895,7 @@ class ATProtoTest(TestCase):
|
|||
'actor': 'fake:user',
|
||||
'object': 'at://did:plc:bob/app.bsky.feed.post/tid',
|
||||
})
|
||||
self.assertTrue(ATProto.send(like_obj, 'https://atproto.brid.gy/'))
|
||||
self.assertTrue(ATProto.send(like_obj, 'https://bsky.brid.gy/'))
|
||||
|
||||
# check repo, record
|
||||
did = user.get_copy(ATProto)
|
||||
|
@ -935,7 +935,7 @@ class ATProtoTest(TestCase):
|
|||
'actor': 'fake:user',
|
||||
'object': 'at://did:bob/app.bsky.feed.post/tid',
|
||||
})
|
||||
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
|
||||
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
|
||||
|
||||
# check repo, record
|
||||
did = user.get_copy(ATProto)
|
||||
|
@ -970,7 +970,7 @@ class ATProtoTest(TestCase):
|
|||
'actor': 'fake:user',
|
||||
'object': 'did:plc:bob',
|
||||
})
|
||||
self.assertTrue(ATProto.send(obj, 'https://atproto.brid.gy/'))
|
||||
self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/'))
|
||||
|
||||
# check repo, record
|
||||
did = user.get_copy(ATProto)
|
||||
|
@ -1005,7 +1005,7 @@ class ATProtoTest(TestCase):
|
|||
'actor': 'fake:alice',
|
||||
'object': 'fake:follow',
|
||||
})
|
||||
self.assertFalse(ATProto.send(obj, 'https://atproto.brid.gy/'))
|
||||
self.assertFalse(ATProto.send(obj, 'https://bsky.brid.gy/'))
|
||||
self.assertEqual(0, AtpBlock.query().count())
|
||||
self.assertEqual(0, AtpRepo.query().count())
|
||||
mock_create_task.assert_not_called()
|
||||
|
@ -1017,7 +1017,7 @@ class ATProtoTest(TestCase):
|
|||
copies=[Target(uri='did:plc:user', protocol='atproto')])
|
||||
obj = self.store_object(id='fake:post', source_protocol='fake',
|
||||
our_as1=NOTE_AS)
|
||||
self.assertFalse(ATProto.send(obj, 'https://atproto.brid.gy/'))
|
||||
self.assertFalse(ATProto.send(obj, 'https://bsky.brid.gy/'))
|
||||
self.assertEqual(0, AtpBlock.query().count())
|
||||
self.assertEqual(0, AtpRepo.query().count())
|
||||
mock_create_task.assert_not_called()
|
||||
|
@ -1035,7 +1035,7 @@ class ATProtoTest(TestCase):
|
|||
'actor': 'fake:user',
|
||||
},
|
||||
})
|
||||
self.assertFalse(ATProto.send(obj, 'https://atproto.brid.gy/'))
|
||||
self.assertFalse(ATProto.send(obj, 'https://bsky.brid.gy/'))
|
||||
self.assertEqual(0, AtpBlock.query().count())
|
||||
self.assertEqual(0, AtpRepo.query().count())
|
||||
mock_create_task.assert_not_called()
|
||||
|
@ -1079,7 +1079,7 @@ class ATProtoTest(TestCase):
|
|||
create = self.store_object(id='fake:reply:post', source_protocol='fake',
|
||||
our_as1=create_as1)
|
||||
|
||||
self.assertTrue(ATProto.send(create, 'https://atproto.brid.gy/'))
|
||||
self.assertTrue(ATProto.send(create, 'https://bsky.brid.gy/'))
|
||||
|
||||
repo = self.storage.load_repo(user.get_copy(ATProto))
|
||||
last_tid = arroba.util.int_to_tid(arroba.util._tid_ts_last)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from flask import g
|
||||
|
||||
# import first so that Fake is defined before URL routes are registered
|
||||
from .testutil import Fake, OtherFake, TestCase
|
||||
from .testutil import ExplicitEnableFake, Fake, OtherFake, TestCase
|
||||
|
||||
from activitypub import ActivityPub
|
||||
from atproto import ATProto
|
||||
|
@ -64,7 +64,7 @@ class CommonTest(TestCase):
|
|||
for input, expected in [
|
||||
('https://fa.brid.gy/', ''),
|
||||
('https://fa.brid.gy/ap/fake:foo', 'fake:foo'),
|
||||
('https://atproto.brid.gy/convert/ap/did:plc:123', 'did:plc:123'),
|
||||
('https://bsky.brid.gy/convert/ap/did:plc:123', 'did:plc:123'),
|
||||
]:
|
||||
self.assertEqual(expected, common.unwrap(input))
|
||||
|
||||
|
@ -99,20 +99,5 @@ class CommonTest(TestCase):
|
|||
with app.test_request_context(base_url='http://bridgy-federated.uc.r.appspot.com'):
|
||||
self.assertEqual('https://fed.brid.gy/asdf', common.host_url('asdf'))
|
||||
|
||||
with app.test_request_context(base_url='https://atproto.brid.gy', path='/foo'):
|
||||
self.assertEqual('https://atproto.brid.gy/asdf', common.host_url('asdf'))
|
||||
|
||||
def test_is_enabled(self):
|
||||
self.assertTrue(common.is_enabled(Web, ActivityPub))
|
||||
self.assertTrue(common.is_enabled(ActivityPub, Web))
|
||||
self.assertTrue(common.is_enabled(ActivityPub, ActivityPub))
|
||||
self.assertTrue(common.is_enabled(ATProto, Web))
|
||||
self.assertTrue(common.is_enabled(Fake, OtherFake))
|
||||
self.assertFalse(common.is_enabled(ATProto, ActivityPub))
|
||||
|
||||
self.assertFalse(common.is_enabled(
|
||||
ATProto, ActivityPub, handle_or_id='unknown'))
|
||||
self.assertTrue(common.is_enabled(
|
||||
ATProto, ActivityPub, handle_or_id='snarfed.org'))
|
||||
self.assertTrue(common.is_enabled(
|
||||
ATProto, ActivityPub, handle_or_id='did:plc:fdme4gb7mu7zrie7peay7tst'))
|
||||
with app.test_request_context(base_url='https://bsky.brid.gy', path='/foo'):
|
||||
self.assertEqual('https://bsky.brid.gy/asdf', common.host_url('asdf'))
|
||||
|
|
|
@ -40,8 +40,8 @@ class IdsTest(TestCase):
|
|||
(ATProto, 'did:plc:456', ActivityPub, 'https://inst/user'),
|
||||
(ATProto, 'did:plc:789', Fake, 'fake:user'),
|
||||
# no copies
|
||||
(ATProto, 'did:plc:x', Web, 'https://atproto.brid.gy/web/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', ActivityPub, 'https://atproto.brid.gy/ap/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', Web, 'https://bsky.brid.gy/web/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', ActivityPub, 'https://bsky.brid.gy/ap/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', Fake, 'fake:u:did:plc:x'),
|
||||
(ATProto, 'https://bsky.app/profile/user.com', ATProto, 'did:plc:123'),
|
||||
(ATProto, 'https://bsky.app/profile/did:plc:123', ATProto, 'did:plc:123'),
|
||||
|
@ -61,7 +61,7 @@ class IdsTest(TestCase):
|
|||
]:
|
||||
with self.subTest(from_=from_.LABEL, to=to.LABEL):
|
||||
self.assertEqual(expected, translate_user_id(
|
||||
id=id, from_proto=from_, to_proto=to))
|
||||
id=id, from_=from_, to=to))
|
||||
|
||||
def test_translate_user_id_no_copy_did_stored(self):
|
||||
for proto, id in [
|
||||
|
@ -70,8 +70,7 @@ class IdsTest(TestCase):
|
|||
(Fake, 'fake:user'),
|
||||
]:
|
||||
with self.subTest(proto=proto.LABEL):
|
||||
self.assertIsNone(translate_user_id(
|
||||
id=id, from_proto=proto, to_proto=ATProto))
|
||||
self.assertIsNone(translate_user_id(id=id, from_=proto, to=ATProto))
|
||||
|
||||
def test_translate_user_id_use_instead(self):
|
||||
did = Target(uri='did:plc:123', protocol='atproto')
|
||||
|
@ -85,18 +84,18 @@ class IdsTest(TestCase):
|
|||
]:
|
||||
with self.subTest(proto=proto.LABEL):
|
||||
self.assertEqual(expected, translate_user_id(
|
||||
id='www.user.com', from_proto=Web, to_proto=proto))
|
||||
id='www.user.com', from_=Web, to=proto))
|
||||
self.assertEqual(expected, translate_user_id(
|
||||
id='https://www.user.com/', from_proto=Web, to_proto=proto))
|
||||
id='https://www.user.com/', from_=Web, to=proto))
|
||||
|
||||
@patch('ids._FED_SUBDOMAIN_SITES', new={'on-fed.com'})
|
||||
def test_translate_user_id_web_ap_subdomain_fed(self):
|
||||
for base_url in ['https://web.brid.gy/', 'https://fed.brid.gy/']:
|
||||
with app.test_request_context('/', base_url=base_url):
|
||||
self.assertEqual('https://web.brid.gy/on-web.com', translate_user_id(
|
||||
id='on-web.com', from_proto=Web, to_proto=ActivityPub))
|
||||
id='on-web.com', from_=Web, to=ActivityPub))
|
||||
self.assertEqual('https://fed.brid.gy/on-fed.com', translate_user_id(
|
||||
id='on-fed.com', from_proto=Web, to_proto=ActivityPub))
|
||||
id='on-fed.com', from_=Web, to=ActivityPub))
|
||||
|
||||
def test_translate_handle(self):
|
||||
for from_, handle, to, expected in [
|
||||
|
@ -111,7 +110,7 @@ class IdsTest(TestCase):
|
|||
(ActivityPub, '@user@instance', Fake, 'fake:handle:@user@instance'),
|
||||
(ActivityPub, '@user@instance', Web, 'https://instance/@user'),
|
||||
|
||||
(ATProto, 'user.com', ActivityPub, '@user.com@atproto.brid.gy'),
|
||||
(ATProto, 'user.com', ActivityPub, '@user.com@bsky.brid.gy'),
|
||||
(ATProto, 'user.com', ATProto, 'user.com'),
|
||||
(ATProto, 'user.com', Fake, 'fake:handle:user.com'),
|
||||
(ATProto, 'user.com', Web, 'user.com'),
|
||||
|
@ -123,7 +122,7 @@ class IdsTest(TestCase):
|
|||
]:
|
||||
with self.subTest(from_=from_.LABEL, to=to.LABEL):
|
||||
self.assertEqual(expected, translate_handle(
|
||||
handle=handle, from_proto=from_, to_proto=to, enhanced=False))
|
||||
handle=handle, from_=from_, to=to, enhanced=False))
|
||||
|
||||
def test_translate_handle_enhanced(self):
|
||||
for from_, handle, to, expected in [
|
||||
|
@ -136,7 +135,7 @@ class IdsTest(TestCase):
|
|||
]:
|
||||
with self.subTest(from_=from_.LABEL, to=to.LABEL):
|
||||
self.assertEqual(expected, translate_handle(
|
||||
handle=handle, from_proto=from_, to_proto=to, enhanced=True))
|
||||
handle=handle, from_=from_, to=to, enhanced=True))
|
||||
|
||||
def test_translate_object_id(self):
|
||||
self.store_object(id='http://po.st',
|
||||
|
@ -165,9 +164,9 @@ class IdsTest(TestCase):
|
|||
(ATProto, 'at://did/ap/post', ActivityPub, 'https://inst/post'),
|
||||
(ATProto, 'at://did/fa/post', Fake, 'fake:post'),
|
||||
# no copies
|
||||
(ATProto, 'did:plc:x', Web, 'https://atproto.brid.gy/convert/web/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', ActivityPub, 'https://atproto.brid.gy/convert/ap/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', Fake, 'fake:o:atproto:did:plc:x'),
|
||||
(ATProto, 'did:plc:x', Web, 'https://bsky.brid.gy/convert/web/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', ActivityPub, 'https://bsky.brid.gy/convert/ap/did:plc:x'),
|
||||
(ATProto, 'did:plc:x', Fake, 'fake:o:bsky:did:plc:x'),
|
||||
(ATProto, 'https://bsky.app/profile/user.com/post/456',
|
||||
ATProto, 'at://did:plc:123/app.bsky.feed.post/456'),
|
||||
(ATProto, 'https://bsky.app/profile/did:plc:123/post/456',
|
||||
|
@ -184,16 +183,16 @@ class IdsTest(TestCase):
|
|||
]:
|
||||
with self.subTest(from_=from_.LABEL, to=to.LABEL):
|
||||
self.assertEqual(expected, translate_object_id(
|
||||
id=id, from_proto=from_, to_proto=to))
|
||||
id=id, from_=from_, to=to))
|
||||
|
||||
@patch('ids._FED_SUBDOMAIN_SITES', new={'on-fed.com'})
|
||||
def test_translate_object_id_web_ap_subdomain_fed(self):
|
||||
for base_url in ['https://web.brid.gy/', 'https://fed.brid.gy/']:
|
||||
with app.test_request_context('/', base_url=base_url):
|
||||
got = translate_object_id(id='http://on-fed.com/post', from_proto=Web,
|
||||
to_proto=ActivityPub)
|
||||
got = translate_object_id(id='http://on-fed.com/post', from_=Web,
|
||||
to=ActivityPub)
|
||||
self.assertEqual('https://fed.brid.gy/r/http://on-fed.com/post', got)
|
||||
|
||||
got = translate_object_id(id='http://on-web.com/post', from_proto=Web,
|
||||
to_proto=ActivityPub)
|
||||
got = translate_object_id(id='http://on-web.com/post', from_=Web,
|
||||
to=ActivityPub)
|
||||
self.assertEqual('https://web.brid.gy/r/http://on-web.com/post', got)
|
||||
|
|
|
@ -36,7 +36,6 @@ class IntegrationTests(TestCase):
|
|||
|
||||
@patch('requests.post')
|
||||
@patch('requests.get')
|
||||
@patch('common.ENABLED_BRIDGES', new=[('activitypub', 'atproto')])
|
||||
def test_atproto_notify_reply_to_activitypub(self, mock_get, mock_post):
|
||||
"""ATProto poll notifications, deliver reply to ActivityPub.
|
||||
|
||||
|
@ -55,6 +54,7 @@ class IntegrationTests(TestCase):
|
|||
id='http://inst/bob',
|
||||
cls=ActivityPub,
|
||||
copies=[Target(uri='did:plc:bob', protocol='atproto')],
|
||||
enabled_protocols=['atproto'],
|
||||
obj_as2={
|
||||
'id': 'http://inst/bob',
|
||||
'inbox': 'http://inst/bob/inbox',
|
||||
|
@ -110,14 +110,14 @@ class IntegrationTests(TestCase):
|
|||
web_test.assert_deliveries(mock_post, ['http://inst/bob/inbox'], data={
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Create',
|
||||
'id': 'https://atproto.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456#bridgy-fed-create',
|
||||
'actor': 'https://atproto.brid.gy/ap/did:plc:alice',
|
||||
'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456#bridgy-fed-create',
|
||||
'actor': 'https://bsky.brid.gy/ap/did:plc:alice',
|
||||
'published': '2022-01-02T03:04:05+00:00',
|
||||
'object': {
|
||||
'type': 'Note',
|
||||
'id': 'https://atproto.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456',
|
||||
'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456',
|
||||
'url': 'http://localhost/r/https://bsky.app/profile/did:plc:alice/post/456',
|
||||
'attributedTo': 'https://atproto.brid.gy/ap/did:plc:alice',
|
||||
'attributedTo': 'https://bsky.brid.gy/ap/did:plc:alice',
|
||||
'content': 'I hereby reply',
|
||||
'contentMap': {'en': 'I hereby reply'},
|
||||
'inReplyTo': 'http://inst/post',
|
||||
|
@ -145,7 +145,8 @@ class IntegrationTests(TestCase):
|
|||
storage = DatastoreStorage()
|
||||
Repo.create(storage, 'did:plc:bob', signing_key=ATPROTO_KEY)
|
||||
bob = self.make_user(id='bob.com', cls=Web,
|
||||
copies=[Target(uri='did:plc:bob', protocol='atproto')])
|
||||
copies=[Target(uri='did:plc:bob', protocol='atproto')],
|
||||
enabled_protocols=['atproto'])
|
||||
|
||||
mock_get.side_effect = [
|
||||
# ATProto listNotifications
|
||||
|
@ -178,7 +179,7 @@ class IntegrationTests(TestCase):
|
|||
|
||||
self.assert_req(mock_get, 'https://bob.com/')
|
||||
self.assert_req(mock_post, 'https://bob.com/webmention', data={
|
||||
'source': 'https://atproto.brid.gy/convert/web/at://did:plc:alice/app.bsky.graph.follow/123',
|
||||
'source': 'https://bsky.brid.gy/convert/web/at://did:plc:alice/app.bsky.graph.follow/123',
|
||||
'target': 'https://bob.com/',
|
||||
}, allow_redirects=False, headers={'Accept': '*/*'})
|
||||
|
||||
|
@ -223,13 +224,14 @@ class IntegrationTests(TestCase):
|
|||
ATProto user alice.com (did:plc:alice)
|
||||
Follow is HTML with mf2 u-follow-of of https://bsky.app/profile/alice.com
|
||||
"""
|
||||
bob = self.make_user(id='bob.com', cls=Web, obj_mf2={
|
||||
'type': ['h-card'],
|
||||
'properties': {
|
||||
'url': ['https://bob.com/'],
|
||||
'name': ['Bob'],
|
||||
},
|
||||
})
|
||||
bob = self.make_user(id='bob.com', cls=Web, enabled_protocols=['atproto'],
|
||||
obj_mf2={
|
||||
'type': ['h-card'],
|
||||
'properties': {
|
||||
'url': ['https://bob.com/'],
|
||||
'name': ['Bob'],
|
||||
},
|
||||
})
|
||||
|
||||
# send webmention
|
||||
resp = self.post('/webmention', data={
|
||||
|
@ -306,7 +308,7 @@ class IntegrationTests(TestCase):
|
|||
})
|
||||
|
||||
bob_did_doc = copy.deepcopy(test_atproto.DID_DOC)
|
||||
bob_did_doc['service'][0]['serviceEndpoint'] = 'https://atproto.brid.gy/'
|
||||
bob_did_doc['service'][0]['serviceEndpoint'] = ATProto.PDS_URL
|
||||
bob_did_doc.update({
|
||||
'id': 'did:plc:bob',
|
||||
'alsoKnownAs': ['at://bob.inst.ap.brid.gy'],
|
||||
|
@ -321,7 +323,7 @@ class IntegrationTests(TestCase):
|
|||
'type': 'Like',
|
||||
'id': 'http://inst/like',
|
||||
'actor': 'https://inst/bob',
|
||||
'object': 'https://atproto.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/123',
|
||||
'object': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/123',
|
||||
}
|
||||
resp = self.post('/ap/atproto/did:plc:alice/inbox', json=like)
|
||||
self.assertEqual(202, resp.status_code)
|
||||
|
|
|
@ -861,7 +861,7 @@ class ObjectTest(TestCase):
|
|||
'object': {
|
||||
'id': 'https://web.brid.gy/fa/fake:reply',
|
||||
'inReplyTo': 'https://ap.brid.gy/fa/fake:post',
|
||||
'author': 'https://atproto.brid.gy/ap/did:plc:123',
|
||||
'author': 'https://bsky.brid.gy/ap/did:plc:123',
|
||||
'tags': [{
|
||||
'objectType': 'mention',
|
||||
'url': 'https://ap.brid.gy/atproto/http://inst.com/@me',
|
||||
|
|
|
@ -16,7 +16,7 @@ import requests
|
|||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
# import first so that Fake is defined before URL routes are registered
|
||||
from .testutil import Fake, OtherFake, TestCase
|
||||
from .testutil import ExplicitEnableFake, Fake, OtherFake, TestCase
|
||||
|
||||
from activitypub import ActivityPub
|
||||
from app import app
|
||||
|
@ -169,6 +169,41 @@ class ProtocolTest(TestCase):
|
|||
def test_for_handle_atproto_resolve(self, _):
|
||||
self.assertEqual((ATProto, 'did:plc:123abc'), Protocol.for_handle('han.dull'))
|
||||
|
||||
def test_is_enabled_to(self):
|
||||
self.assertTrue(Web.is_enabled_to(ActivityPub))
|
||||
self.assertTrue(ActivityPub.is_enabled_to(Web))
|
||||
self.assertTrue(ActivityPub.is_enabled_to(ActivityPub))
|
||||
self.assertTrue(Fake.is_enabled_to(OtherFake))
|
||||
self.assertTrue(Fake.is_enabled_to(ExplicitEnableFake))
|
||||
|
||||
self.assertFalse(ActivityPub.is_enabled_to(ATProto))
|
||||
self.assertFalse(ATProto.is_enabled_to(ActivityPub))
|
||||
self.assertFalse(ATProto.is_enabled_to(Web))
|
||||
self.assertFalse(Web.is_enabled_to(ATProto))
|
||||
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake))
|
||||
self.assertFalse(ExplicitEnableFake.is_enabled_to(Web))
|
||||
|
||||
def test_is_enabled_to_opt_out(self):
|
||||
user = self.make_user('user.com', cls=Web)
|
||||
self.assertTrue(Web.is_enabled_to(ActivityPub, user))
|
||||
|
||||
user.manual_opt_out = True
|
||||
user.put()
|
||||
protocol.objects_cache.clear()
|
||||
self.assertFalse(Web.is_enabled_to(ActivityPub, 'user.com'))
|
||||
|
||||
def test_is_enabled_to_enabled_protocols(self):
|
||||
user = self.make_user(id='eefake:foo', cls=ExplicitEnableFake)
|
||||
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, 'eefake:foo'))
|
||||
|
||||
user.enabled_protocols = ['web']
|
||||
user.put()
|
||||
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, 'eefake:foo'))
|
||||
|
||||
user.enabled_protocols = ['web', 'fake']
|
||||
user.put()
|
||||
self.assertTrue(ExplicitEnableFake.is_enabled_to(Fake, 'eefake:foo'))
|
||||
|
||||
def test_load(self):
|
||||
Fake.fetchable['foo'] = {'x': 'y'}
|
||||
|
||||
|
@ -409,7 +444,7 @@ class ProtocolTest(TestCase):
|
|||
|
||||
def test_targets_checks_blocklisted_per_protocol(self):
|
||||
"""_targets should call the target protocol's is_blocklisted()."""
|
||||
# non-ATProto account, ATProto target (PDS) is atproto.brid.gy
|
||||
# non-ATProto account, ATProto target (PDS) is bsky.brid.gy
|
||||
# shouldn't be blocklisted
|
||||
user = self.make_user(
|
||||
id='fake:user', cls=Fake,
|
||||
|
@ -1340,7 +1375,7 @@ class ProtocolReceiveTest(TestCase):
|
|||
delivered=['fake:user:target'],
|
||||
)
|
||||
|
||||
accept_id = 'https://fa.brid.gy/ap/fake:user/followers#accept-fake:follow'
|
||||
accept_id = 'fake:user/followers#accept-fake:follow'
|
||||
accept_as1 = {
|
||||
'id': accept_id,
|
||||
'objectType': 'activity',
|
||||
|
@ -1562,9 +1597,8 @@ class ProtocolReceiveTest(TestCase):
|
|||
self.assertEqual(('OK', 202), OtherFake.receive_as1(follow_as1))
|
||||
|
||||
self.assertEqual(1, len(OtherFake.sent))
|
||||
self.assertEqual(
|
||||
'https://fa.brid.gy/ap/fake:alice/followers#accept-other:follow',
|
||||
OtherFake.sent[0][0])
|
||||
self.assertEqual('fake:alice/followers#accept-other:follow',
|
||||
OtherFake.sent[0][0])
|
||||
|
||||
self.assertEqual(1, len(Fake.sent))
|
||||
self.assertEqual('other:follow', Fake.sent[0][0])
|
||||
|
@ -1743,6 +1777,94 @@ class ProtocolReceiveTest(TestCase):
|
|||
}],
|
||||
}, obj.key.get().our_as1)
|
||||
|
||||
def test_follow_and_block_protocol_user_sets_enabled_protocols(self):
|
||||
follow = {
|
||||
'objectType': 'activity',
|
||||
'verb': 'follow',
|
||||
'id': 'eefake:follow',
|
||||
'actor': 'eefake:user',
|
||||
'object': 'fa.brid.gy',
|
||||
}
|
||||
block = {
|
||||
'objectType': 'activity',
|
||||
'verb': 'block',
|
||||
'id': 'eefake:block',
|
||||
'actor': 'eefake:user',
|
||||
'object': 'fa.brid.gy',
|
||||
}
|
||||
|
||||
user = self.make_user('eefake:user', cls=ExplicitEnableFake)
|
||||
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))
|
||||
|
||||
# fake protocol isn't enabled yet, block should be a noop
|
||||
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(block))
|
||||
user = user.key.get()
|
||||
self.assertEqual([], user.enabled_protocols)
|
||||
|
||||
# follow should add to enabled_protocols
|
||||
with self.assertRaises(NoContent):
|
||||
ExplicitEnableFake.receive_as1(follow)
|
||||
user = user.key.get()
|
||||
self.assertEqual(['fake'], user.enabled_protocols)
|
||||
self.assertTrue(ExplicitEnableFake.is_enabled_to(Fake, user))
|
||||
self.assertEqual([
|
||||
('https://fa.brid.gy//followers#accept-eefake:follow',
|
||||
'eefake:user:target'),
|
||||
], ExplicitEnableFake.sent)
|
||||
|
||||
# another follow should be a noop
|
||||
follow['id'] += '2'
|
||||
with self.assertRaises(NoContent):
|
||||
ExplicitEnableFake.receive_as1(follow)
|
||||
user = user.key.get()
|
||||
self.assertEqual(['fake'], user.enabled_protocols)
|
||||
|
||||
# block should remove from enabled_protocols
|
||||
block['id'] += '2'
|
||||
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(block))
|
||||
user = user.key.get()
|
||||
self.assertEqual([], user.enabled_protocols)
|
||||
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))
|
||||
|
||||
def test_dm_no_yes_sets_enabled_protocols(self):
|
||||
dm = {
|
||||
'objectType': 'note',
|
||||
'id': 'eefake:dm',
|
||||
'actor': 'eefake:user',
|
||||
'to': ['fa.brid.gy'],
|
||||
'content': 'no',
|
||||
}
|
||||
|
||||
user = self.make_user('eefake:user', cls=ExplicitEnableFake)
|
||||
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))
|
||||
|
||||
# fake protocol isn't enabled yet, no DM should be a noop
|
||||
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
|
||||
user = user.key.get()
|
||||
self.assertEqual([], user.enabled_protocols)
|
||||
|
||||
# yes DM should add to enabled_protocols
|
||||
dm['id'] += '2'
|
||||
dm['content'] = 'yes'
|
||||
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
|
||||
user = user.key.get()
|
||||
self.assertEqual(['fake'], user.enabled_protocols)
|
||||
self.assertTrue(ExplicitEnableFake.is_enabled_to(Fake, user))
|
||||
|
||||
# another yes DM should be a noop
|
||||
dm['id'] += '3'
|
||||
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
|
||||
user = user.key.get()
|
||||
self.assertEqual(['fake'], user.enabled_protocols)
|
||||
|
||||
# block should remove from enabled_protocols
|
||||
dm['id'] += '4'
|
||||
dm['content'] = ' \n NO '
|
||||
self.assertEqual(('OK', 200), ExplicitEnableFake.receive_as1(dm))
|
||||
user = user.key.get()
|
||||
self.assertEqual([], user.enabled_protocols)
|
||||
self.assertFalse(ExplicitEnableFake.is_enabled_to(Fake, user))
|
||||
|
||||
def test_receive_task_handler(self):
|
||||
note = {
|
||||
'id': 'fake:post',
|
||||
|
|
|
@ -2552,7 +2552,7 @@ class WebUtilTest(TestCase):
|
|||
self.assertEqual(False, Web.owns_handle('@foo@bar.com'))
|
||||
self.assertEqual(False, Web.owns_handle('foo@bar.com'))
|
||||
self.assertEqual(False, Web.owns_handle('localhost'))
|
||||
self.assertEqual(False, Web.owns_handle('atproto.brid.gy'))
|
||||
self.assertEqual(False, Web.owns_handle('bsky.brid.gy'))
|
||||
|
||||
def test_handle_to_id(self, *_):
|
||||
self.assertEqual('foo.com', Web.handle_to_id('foo.com'))
|
||||
|
|
|
@ -164,6 +164,15 @@ class OtherFake(Fake):
|
|||
return f'{obj.key.id()}:target'
|
||||
|
||||
|
||||
class ExplicitEnableFake(Fake):
|
||||
LABEL = ABBREV = 'eefake'
|
||||
CONTENT_TYPE = 'un/known'
|
||||
|
||||
fetchable = {}
|
||||
sent = []
|
||||
fetched = []
|
||||
|
||||
|
||||
# import other modules that register Flask handlers *after* Fake is defined
|
||||
models.reset_protocol_properties()
|
||||
|
||||
|
|
10
web.py
10
web.py
|
@ -98,6 +98,7 @@ class Web(User, Protocol):
|
|||
OTHER_LABELS = ('webmention',)
|
||||
LOGO_HTML = '🌐' # used to be 🕸️
|
||||
CONTENT_TYPE = common.CONTENT_TYPE_HTML
|
||||
DEFAULT_ENABLED_PROTOCOLS = ('activitypub',)
|
||||
|
||||
has_redirects = ndb.BooleanProperty()
|
||||
redirects_error = ndb.TextProperty()
|
||||
|
@ -171,8 +172,8 @@ class Web(User, Protocol):
|
|||
if isinstance(to_proto, str):
|
||||
to_proto = PROTOCOLS[to_proto]
|
||||
|
||||
converted = translate_user_id(id=self.key.id(), from_proto=self,
|
||||
to_proto=to_proto)
|
||||
converted = translate_user_id(id=self.key.id(), from_=self,
|
||||
to=to_proto)
|
||||
|
||||
if to_proto.LABEL == 'activitypub':
|
||||
other = 'web' if self.ap_subdomain == 'fed' else 'fed'
|
||||
|
@ -396,7 +397,7 @@ class Web(User, Protocol):
|
|||
return False
|
||||
|
||||
source_id = translate_object_id(
|
||||
id=obj.key.id(), from_proto=PROTOCOLS[obj.source_protocol], to_proto=Web)
|
||||
id=obj.key.id(), from_=PROTOCOLS[obj.source_protocol], to=Web)
|
||||
source_url = quote(source_id, safe=':/%+')
|
||||
logger.info(f'Sending webmention from {source_url} to {url}')
|
||||
|
||||
|
@ -537,8 +538,7 @@ class Web(User, Protocol):
|
|||
obj_as1 = obj.as1
|
||||
from_proto = PROTOCOLS.get(obj.source_protocol)
|
||||
if from_proto:
|
||||
user_id = from_user.key.id() if from_user and from_user.key else None
|
||||
if not common.is_enabled(cls, from_proto, handle_or_id=user_id):
|
||||
if not from_proto.is_enabled_to(cls, user=from_user):
|
||||
error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
|
||||
|
||||
# fill in author/actor if available
|
||||
|
|
Ładowanie…
Reference in New Issue