kopia lustrzana https://github.com/snarfed/bridgy-fed
Porównaj commity
14 Commity
2085b131a1
...
f02ba80304
Autor | SHA1 | Data |
---|---|---|
Ryan Barrett | f02ba80304 | |
Ryan Barrett | 393605bde9 | |
dependabot[bot] | 2036f92ddd | |
dependabot[bot] | 7190503aea | |
Ryan Barrett | bf52d80e0f | |
Ryan Barrett | 12a3bf0862 | |
Ryan Barrett | a70702776c | |
Ryan Barrett | 86ad33b896 | |
Ryan Barrett | 374af3aa5c | |
Ryan Barrett | e913ad1f53 | |
Ryan Barrett | 2ec22de09f | |
Ryan Barrett | 5b5ed4173a | |
Ryan Barrett | f840c8b784 | |
Ryan Barrett | cf633efecf |
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -362,8 +362,9 @@ 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
|
||||
if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
|
||||
error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
|
||||
# TODO: uncomment
|
||||
# if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
|
||||
# error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
|
||||
|
||||
if obj.as2:
|
||||
return {
|
||||
|
|
29
atproto.py
29
atproto.py
|
@ -34,6 +34,7 @@ from common import (
|
|||
DOMAINS,
|
||||
error,
|
||||
USER_AGENT,
|
||||
USER_ALLOWLIST,
|
||||
)
|
||||
import flask_app
|
||||
from models import Object, PROTOCOLS, Target, User
|
||||
|
@ -85,11 +86,16 @@ 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="/static/atproto_logo.png">'
|
||||
PDS_URL = f'https://{ABBREV}{common.SUPERDOMAIN}/'
|
||||
LOGO_HTML = '<img src="/oauth_dropins_static/bluesky.svg">'
|
||||
# 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'
|
||||
|
||||
def _pre_put_hook(self):
|
||||
|
@ -392,8 +398,8 @@ class ATProto(User, Protocol):
|
|||
copy = base_obj.get_copy(to_cls)
|
||||
assert copy
|
||||
copy_did, coll, rkey = parse_at_uri(copy)
|
||||
assert copy_did == did
|
||||
assert coll == type
|
||||
assert copy_did == did, (copy_did, did)
|
||||
assert coll == type, (coll, type)
|
||||
|
||||
logger.info(f'Storing ATProto {action} {type} {rkey}: {dag_json.encode(record).decode()}')
|
||||
repo.apply_writes([Write(action=action, collection=type, rkey=rkey,
|
||||
|
@ -501,8 +507,9 @@ class ATProto(User, Protocol):
|
|||
"""
|
||||
from_proto = PROTOCOLS.get(obj.source_protocol)
|
||||
user_id = from_user.key.id() if from_user and from_user.key else None
|
||||
if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
|
||||
error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
|
||||
# TODO: uncomment
|
||||
# if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
|
||||
# error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
|
||||
|
||||
if obj.bsky:
|
||||
return obj.bsky
|
||||
|
@ -531,13 +538,15 @@ class ATProto(User, Protocol):
|
|||
if uri := strong_ref.get('uri'):
|
||||
# TODO: fail if this load fails? since we don't populate CID
|
||||
if ref_obj := ATProto.load(uri):
|
||||
if not ref_obj.bsky.get('cid'):
|
||||
ref_obj = ATProto.load(uri, remote=True)
|
||||
strong_ref.update({
|
||||
'cid': ref_obj.bsky.get('cid'),
|
||||
'uri': ref_obj.key.id(),
|
||||
})
|
||||
|
||||
match ret.get('$type'):
|
||||
case 'app.bsky.feed.like' | 'app.bsky.feed.repost':
|
||||
case 'app.bsky.feed.like' | 'app.bsky.feed.repost':
|
||||
populate_cid(ret['subject'])
|
||||
case 'app.bsky.feed.post' if ret.get('reply'):
|
||||
populate_cid(ret['reply']['root'])
|
||||
|
@ -572,7 +581,7 @@ def poll_notifications():
|
|||
|
||||
for user in users:
|
||||
# TODO: remove for launch
|
||||
if not DEBUG and user.key.id() not in ['indieweb.org', 'snarfed.org']:
|
||||
if not DEBUG and user.key.id() not in USER_ALLOWLIST:
|
||||
logger.info(f'Skipping {user.key.id()}')
|
||||
continue
|
||||
|
||||
|
@ -632,7 +641,7 @@ def poll_posts():
|
|||
|
||||
for user in users:
|
||||
# TODO: remove for launch
|
||||
if not DEBUG and user.key.id() not in ['indieweb.org', 'snarfed.org']:
|
||||
if not DEBUG and user.key.id() not in USER_ALLOWLIST:
|
||||
logger.info(f'Skipping {user.key.id()}')
|
||||
continue
|
||||
|
||||
|
|
|
@ -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
|
||||
-----
|
||||
|
|
6
ids.py
6
ids.py
|
@ -66,7 +66,7 @@ def translate_user_id(*, id, from_proto, to_proto):
|
|||
Returns:
|
||||
str: the corresponding id in ``to_proto``
|
||||
"""
|
||||
assert id and from_proto and to_proto
|
||||
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)
|
||||
|
||||
|
@ -141,7 +141,7 @@ def translate_handle(*, handle, from_proto, to_proto, enhanced):
|
|||
Returns:
|
||||
str: the corresponding handle in ``to_proto``
|
||||
"""
|
||||
assert handle and from_proto and to_proto
|
||||
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'
|
||||
|
||||
if from_proto == to_proto:
|
||||
|
@ -188,7 +188,7 @@ def translate_object_id(*, id, from_proto, to_proto):
|
|||
Returns:
|
||||
str: the corresponding id in ``to_proto``
|
||||
"""
|
||||
assert id and from_proto and to_proto
|
||||
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'
|
||||
|
||||
# bsky.app profile URL to DID
|
||||
|
|
14
models.py
14
models.py
|
@ -37,7 +37,7 @@ PROTOCOLS = {'ostatus': None}
|
|||
|
||||
# 2048 bits makes tests slow, so use 1024 for them
|
||||
KEY_BITS = 1024 if DEBUG else 2048
|
||||
PAGE_SIZE = 30
|
||||
PAGE_SIZE = 20
|
||||
|
||||
# auto delete old objects of these types via the Object.expire property
|
||||
# https://cloud.google.com/datastore/docs/ttl
|
||||
|
@ -921,7 +921,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``
|
||||
|
||||
|
@ -940,8 +940,7 @@ class Object(StringIdModel):
|
|||
:meth:`protocol.Protocol.translate_ids` is partly the inverse of this.
|
||||
Much of the same logic is duplicated there!
|
||||
|
||||
TODO: unify with :meth:`normalize_ids`,
|
||||
:meth:`protocol.Protocol.normalize_ids`.
|
||||
TODO: unify with :meth:`normalize_ids`, :meth:`Object.normalize_ids`.
|
||||
"""
|
||||
if not self.as1:
|
||||
return
|
||||
|
@ -1181,7 +1180,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
|
||||
|
@ -1231,7 +1230,9 @@ def fetch_objects(query, by=None, user=None):
|
|||
if inner_type:
|
||||
type = inner_type
|
||||
|
||||
# AS1 verb => human-readable phrase
|
||||
phrases = {
|
||||
'accept': 'accepted',
|
||||
'article': 'posted',
|
||||
'comment': 'replied',
|
||||
'delete': 'deleted',
|
||||
|
@ -1301,7 +1302,8 @@ def fetch_objects(query, by=None, user=None):
|
|||
handle = did_to_handle(handle) or handle
|
||||
content = f'@{handle}{suffix}'
|
||||
|
||||
content = common.pretty_link(url, text=content, user=user)
|
||||
if url:
|
||||
content = common.pretty_link(url, text=content, user=user)
|
||||
|
||||
obj.content = (obj_as1.get('content')
|
||||
or obj_as1.get('displayName')
|
||||
|
|
|
@ -497,7 +497,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:
|
||||
|
||||
|
@ -515,7 +515,7 @@ class Protocol:
|
|||
same logic is duplicated there!
|
||||
|
||||
TODO: unify with :meth:`Object.resolve_ids`,
|
||||
:meth:`protocol.Protocol.normalize_ids`.
|
||||
:meth:`models.Object.normalize_ids`.
|
||||
|
||||
Args:
|
||||
to_proto (Protocol subclass)
|
||||
|
|
81
redirect.py
81
redirect.py
|
@ -26,8 +26,9 @@ from oauth_dropins.webutil.flask_util import error
|
|||
from oauth_dropins.webutil.util import json_dumps, json_loads
|
||||
|
||||
from activitypub import ActivityPub
|
||||
from flask_app import app, cache
|
||||
from common import CACHE_TIME, CONTENT_TYPE_HTML
|
||||
from flask_app import app, cache
|
||||
from protocol import Protocol
|
||||
from web import Web
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -85,51 +86,59 @@ def redir(to):
|
|||
domains = set((util.domain_from_link(to, minimize=True),
|
||||
util.domain_from_link(to, minimize=False),
|
||||
to_domain))
|
||||
web_user = None
|
||||
for domain in domains:
|
||||
if domain:
|
||||
if domain in DOMAIN_ALLOWLIST:
|
||||
break
|
||||
if Web.get_by_id(domain):
|
||||
if web_user := Web.get_by_id(domain):
|
||||
logger.info(f'Found web user for domain {domain}')
|
||||
break
|
||||
else:
|
||||
if not accept_as2:
|
||||
return f'No web user found for any of {domains}', 404, VARY_HEADER
|
||||
|
||||
if accept_as2:
|
||||
# AS2 requested, fetch and convert and serve
|
||||
obj = Web.load(to, check_backlink=False)
|
||||
if not obj or obj.deleted:
|
||||
if not accept_as2:
|
||||
# redirect. include rel-alternate link to make posts discoverable by entering
|
||||
# https://fed.brid.gy/r/[URL] in a fediverse instance's search.
|
||||
logger.info(f'redirecting to {to}')
|
||||
return f"""\
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="{request.url}" rel="alternate" type="application/activity+json">
|
||||
</head>
|
||||
<title>Redirecting...</title>
|
||||
<h1>Redirecting...</h1>
|
||||
<p>You should be redirected automatically to the target URL: <a href="{to}">{to}</a>. If not, click the link.
|
||||
</html>
|
||||
""", 301, {
|
||||
'Location': to,
|
||||
**VARY_HEADER,
|
||||
}
|
||||
|
||||
# AS2 requested, fetch and convert and serve
|
||||
proto = Protocol.for_id(to)
|
||||
if not proto:
|
||||
return f"Couldn't determine protocol for {to}", 404, VARY_HEADER
|
||||
|
||||
obj = proto.load(to)
|
||||
if not obj or obj.deleted:
|
||||
return f'Object not found: {to}', 404, VARY_HEADER
|
||||
|
||||
# TODO: do this for other protocols too?
|
||||
if proto == Web and not web_user:
|
||||
web_user = Web.get_or_create(util.domain_from_link(to), direct=False, obj=obj)
|
||||
if not web_user:
|
||||
return f'Object not found: {to}', 404, VARY_HEADER
|
||||
|
||||
user = Web.get_or_create(util.domain_from_link(to), direct=False, obj=obj)
|
||||
if not user:
|
||||
return f'Object not found: {to}', 404, VARY_HEADER
|
||||
ret = ActivityPub.convert(obj, from_user=web_user)
|
||||
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
|
||||
return ret, {
|
||||
'Content-Type': (as2.CONTENT_TYPE_LD_PROFILE
|
||||
if accept_type == as2.CONTENT_TYPE_LD
|
||||
else accept_type),
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
**VARY_HEADER,
|
||||
}
|
||||
|
||||
ret = ActivityPub.convert(obj, from_user=user)
|
||||
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
|
||||
return ret, {
|
||||
'Content-Type': (as2.CONTENT_TYPE_LD_PROFILE
|
||||
if accept_type == as2.CONTENT_TYPE_LD
|
||||
else accept_type),
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
**VARY_HEADER,
|
||||
}
|
||||
|
||||
# redirect. include rel-alternate link to make posts discoverable by entering
|
||||
# https://fed.brid.gy/r/[URL] in a fediverse instance's search.
|
||||
logger.info(f'redirecting to {to}')
|
||||
return f"""\
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="{request.url}" rel="alternate" type="application/activity+json">
|
||||
</head>
|
||||
<title>Redirecting...</title>
|
||||
<h1>Redirecting...</h1>
|
||||
<p>You should be redirected automatically to the target URL: <a href="{to}">{to}</a>. If not, click the link.
|
||||
</html>
|
||||
""", 301, {
|
||||
'Location': to,
|
||||
**VARY_HEADER,
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ googleapis-common-protos==1.63.0
|
|||
grpc-google-iam-v1==0.13.0
|
||||
grpcio==1.62.1
|
||||
grpcio-status==1.62.1
|
||||
gunicorn==21.2.0
|
||||
gunicorn==22.0.0
|
||||
h11==0.14.0
|
||||
html2text==2024.2.26
|
||||
html5lib==1.1
|
||||
|
@ -58,7 +58,7 @@ humanfriendly==10.0
|
|||
humanize==4.9.0
|
||||
idna==3.7
|
||||
iterators==0.2.0
|
||||
itsdangerous==2.1.2
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.3
|
||||
jsonschema==4.21.1
|
||||
lxml==5.2.1
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
<li class="row">
|
||||
{% with url=f.user.web_url(), user_as1=f.user.obj.as1 or {} %}
|
||||
<a class="follower col-xs-10 col-sm-10 col-lg-6" href="{{ url }}">
|
||||
<span class="logo" title="{{ user.__class__.__name__ }}">
|
||||
{{ f.user.LOGO_HTML|safe }}
|
||||
</span>
|
||||
{% with picture=util.get_url(user_as1, 'icon') or util.get_url(user_as1, 'image') %}
|
||||
{% if picture %}
|
||||
<img class="profile u-photo" src="{{ picture }}" width="48px">
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
{% for obj in objects %}
|
||||
<li class="row h-entry">
|
||||
<div class="e-content col-xs-{{ 5 if show_users else 8 }}">
|
||||
{% if obj.source_protocol %}
|
||||
<span class="logo">{{ PROTOCOLS[obj.source_protocol].LOGO_HTML|safe }}</span>
|
||||
{% endif %}
|
||||
{{ obj.actor_link(user=user)|safe }}
|
||||
{{ obj.phrase|safe }}
|
||||
<a target="_blank" href="{{ obj.url }}" class="u-url">
|
||||
{% if obj.url %}<a target="_blank" href="{{ obj.url }}" class="u-url">{% endif %}
|
||||
{{ obj.content|default('--', true)|striptags|truncate(50) }}
|
||||
</a>
|
||||
{% if obj.url %}</a>{% endif %}
|
||||
</div>
|
||||
|
||||
{% if show_users %}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<nobr>
|
||||
|
||||
<a href="{{ user.web_url() }}"
|
||||
title="{{ user.__class__.__name__ }} (native)">
|
||||
title="{{ user.__class__.__name__ }} (original)">
|
||||
<span class="logo">{{ user.LOGO_HTML|safe }}</span>
|
||||
{{ user.handle_or_id() }}
|
||||
</a>
|
||||
|
|
|
@ -440,7 +440,7 @@ 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/')
|
||||
got = self.client.get('/ap/did:plc:user', base_url='https://bsky.brid.gy/')
|
||||
self.assertEqual(400, got.status_code)
|
||||
|
||||
@patch('common.ENABLED_BRIDGES', new=[('activitypub', 'atproto')])
|
||||
|
@ -453,7 +453,7 @@ class ActivityPubTest(TestCase):
|
|||
|
||||
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(200, got.status_code)
|
||||
self.assertNotIn('preferredUsername', got.json)
|
||||
|
||||
|
@ -1649,7 +1649,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()
|
||||
|
||||
|
@ -2218,6 +2218,8 @@ class ActivityPubUtilsTest(TestCase):
|
|||
'object': ACTOR,
|
||||
}, ActivityPub.convert(obj))
|
||||
|
||||
# TODO: remove
|
||||
@skip
|
||||
def test_convert_protocols_not_enabled(self):
|
||||
obj = Object(our_as1={'foo': 'bar'}, source_protocol='atproto')
|
||||
with self.assertRaises(BadRequest):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
@ -493,6 +493,36 @@ class ATProtoTest(TestCase):
|
|||
'object': 'at://bob.net/app.bsky.feed.post/tid',
|
||||
})))
|
||||
|
||||
@patch('requests.get', return_value=requests_response({ # AppView getRecord
|
||||
'uri': 'at://did:plc:user/app.bsky.feed.post/tid',
|
||||
'cid': 'my sidd',
|
||||
'value': {'$type': 'app.bsky.feed.post'},
|
||||
}))
|
||||
def test_convert_populate_cid_refetch_cid(self, mock_get):
|
||||
# existing Object with post but missing cid
|
||||
self.store_object(id='did:plc:user', raw=DID_DOC)
|
||||
self.store_object(id='at://did:plc:user/app.bsky.feed.post/tid', bsky={
|
||||
'$type': 'app.bsky.feed.post',
|
||||
'cid': '',
|
||||
})
|
||||
|
||||
self.assertEqual({
|
||||
'$type': 'app.bsky.feed.like',
|
||||
'subject': {
|
||||
'uri': 'at://did:plc:user/app.bsky.feed.post/tid',
|
||||
'cid': 'my sidd',
|
||||
},
|
||||
'createdAt': '2022-01-02T03:04:05.000Z',
|
||||
}, ATProto.convert(Object(our_as1={
|
||||
'objectType': 'activity',
|
||||
'verb': 'like',
|
||||
'object': 'at://did:plc:user/app.bsky.feed.post/tid',
|
||||
})))
|
||||
|
||||
mock_get.assert_called_with(
|
||||
'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.feed.post&rkey=tid',
|
||||
json=None, data=None, headers=ANY)
|
||||
|
||||
def test_convert_blobs_false(self):
|
||||
self.assertEqual({
|
||||
'$type': 'app.bsky.actor.profile',
|
||||
|
@ -579,6 +609,8 @@ class ATProtoTest(TestCase):
|
|||
}],
|
||||
})))
|
||||
|
||||
# TODO: remove
|
||||
@skip
|
||||
def test_convert_protocols_not_enabled(self):
|
||||
obj = Object(our_as1={'foo': 'bar'}, source_protocol='activitypub')
|
||||
with self.assertRaises(BadRequest):
|
||||
|
@ -607,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):
|
||||
|
@ -679,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()
|
||||
|
@ -743,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')
|
||||
|
@ -780,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)
|
||||
|
@ -809,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)
|
||||
|
@ -831,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)
|
||||
|
@ -863,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)
|
||||
|
@ -903,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)
|
||||
|
@ -938,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)
|
||||
|
@ -973,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()
|
||||
|
@ -985,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()
|
||||
|
@ -1003,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()
|
||||
|
@ -1047,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)
|
||||
|
|
|
@ -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,8 +99,8 @@ 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'))
|
||||
with app.test_request_context(base_url='https://bsky.brid.gy', path='/foo'):
|
||||
self.assertEqual('https://bsky.brid.gy/asdf', common.host_url('asdf'))
|
||||
|
||||
def test_is_enabled(self):
|
||||
self.assertTrue(common.is_enabled(Web, ActivityPub))
|
||||
|
|
|
@ -40,6 +40,7 @@ HTML = """\
|
|||
A ☕ reply
|
||||
</div>
|
||||
<a class="u-in-reply-to" href="https://fake.com/123"></a>
|
||||
<a class="u-in-reply-to" href="tag:fake.com:123"></a>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -64,6 +65,7 @@ AUTHOR_HTML = """\
|
|||
A ☕ reply
|
||||
</div>
|
||||
<a class="u-in-reply-to" href="https://fake.com/123"></a>
|
||||
<a class="u-in-reply-to" href="tag:fake.com:123"></a>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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'),
|
||||
|
@ -111,7 +111,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'),
|
||||
|
@ -165,9 +165,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',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Integration tests."""
|
||||
import copy
|
||||
from unittest.mock import patch
|
||||
|
||||
from arroba.datastore_storage import DatastoreStorage
|
||||
|
@ -10,7 +11,7 @@ from activitypub import ActivityPub
|
|||
import app
|
||||
from atproto import ATProto
|
||||
from dns.resolver import NXDOMAIN
|
||||
from granary.tests.test_bluesky import ACTOR_PROFILE_BSKY
|
||||
from granary.tests.test_bluesky import ACTOR_PROFILE_BSKY, POST_BSKY
|
||||
import hub
|
||||
from models import Object, Target
|
||||
from web import Web
|
||||
|
@ -109,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',
|
||||
|
@ -127,6 +128,7 @@ class IntegrationTests(TestCase):
|
|||
'to': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
})
|
||||
|
||||
|
||||
@patch('requests.post', return_value=requests_response(''))
|
||||
@patch('requests.get')
|
||||
def test_atproto_follow_to_web(self, mock_get, mock_post):
|
||||
|
@ -176,10 +178,11 @@ 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': '*/*'})
|
||||
|
||||
|
||||
@patch('dns.resolver.resolve', side_effect=NXDOMAIN())
|
||||
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
|
||||
@patch('requests.post', side_effect=[
|
||||
|
@ -213,7 +216,7 @@ class IntegrationTests(TestCase):
|
|||
# alice profile
|
||||
requests_response(PROFILE_GETRECORD),
|
||||
])
|
||||
def test_web_follow_to_atproto(self, mock_get, mock_post, _, __):
|
||||
def test_web_follow_of_atproto(self, mock_get, mock_post, _, __):
|
||||
"""Incoming webmention for a web follow of an ATProto bsky.app profile URL.
|
||||
|
||||
Web user bob.com
|
||||
|
@ -270,3 +273,82 @@ class IntegrationTests(TestCase):
|
|||
'subject': 'did:plc:alice',
|
||||
'createdAt': '2022-01-02T03:04:05.000Z',
|
||||
}], list(records['app.bsky.graph.follow'].values()))
|
||||
|
||||
|
||||
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
|
||||
@patch('requests.get', side_effect=[
|
||||
# getRecord of original post
|
||||
# alice profile
|
||||
requests_response({
|
||||
'uri': 'at://did:plc:alice/app.bsky.feed.post/123',
|
||||
'cid': 'sydd',
|
||||
'value': POST_BSKY,
|
||||
}),
|
||||
])
|
||||
def test_activitypub_like_of_atproto(self, mock_get, _):
|
||||
"""AP inbox delivery of a Like of an ATProto bsky.app profile URL.
|
||||
|
||||
ActivityPub user @bob@inst , https://inst/bob
|
||||
ATProto user alice.com (did:plc:alice)
|
||||
Like is https://inst/like
|
||||
"""
|
||||
self.store_object(id='did:plc:alice', raw=DID_DOC)
|
||||
alice = self.make_user(id='did:plc:alice', cls=ATProto)
|
||||
|
||||
storage = DatastoreStorage()
|
||||
Repo.create(storage, 'did:plc:bob', signing_key=ATPROTO_KEY)
|
||||
bob = self.make_user(id='https://inst/bob', cls=ActivityPub,
|
||||
copies=[Target(uri='did:plc:bob', protocol='atproto')],
|
||||
obj_as2={
|
||||
'type': 'Person',
|
||||
'id': 'https://inst/bob',
|
||||
'name': 'Bob',
|
||||
})
|
||||
|
||||
bob_did_doc = copy.deepcopy(test_atproto.DID_DOC)
|
||||
bob_did_doc['service'][0]['serviceEndpoint'] = ATProto.PDS_URL
|
||||
bob_did_doc.update({
|
||||
'id': 'did:plc:bob',
|
||||
'alsoKnownAs': ['at://bob.inst.ap.brid.gy'],
|
||||
})
|
||||
self.store_object(id='did:plc:bob', raw=bob_did_doc)
|
||||
|
||||
# existing Object with original post, *without* cid. we should refetch.
|
||||
Object(id='at://did:plc:alice/app.bsky.feed.post/123', bsky=POST_BSKY).put()
|
||||
|
||||
# inbox delivery
|
||||
like = {
|
||||
'type': 'Like',
|
||||
'id': 'http://inst/like',
|
||||
'actor': 'https://inst/bob',
|
||||
'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)
|
||||
|
||||
# check results
|
||||
self.assertEqual({
|
||||
**like,
|
||||
# TODO: stop normalizing this in the original protocol's data
|
||||
'object': 'at://did:plc:alice/app.bsky.feed.post/123',
|
||||
}, Object.get_by_id('http://inst/like').as2)
|
||||
|
||||
repo = storage.load_repo('did:plc:bob')
|
||||
|
||||
records = repo.get_contents()
|
||||
self.assertEqual(['app.bsky.feed.like'], list(records.keys()))
|
||||
self.assertEqual([{
|
||||
'$type': 'app.bsky.feed.like',
|
||||
'subject': {
|
||||
'uri': 'at://did:plc:alice/app.bsky.feed.post/123',
|
||||
'cid': 'sydd',
|
||||
},
|
||||
'createdAt': '2022-01-02T03:04:05.000Z',
|
||||
}], list(records['app.bsky.feed.like'].values()))
|
||||
|
||||
# we needed to refetch the original post
|
||||
self.assert_object(id='at://did:plc:alice/app.bsky.feed.post/123',
|
||||
source_protocol='atproto', bsky={
|
||||
**POST_BSKY,
|
||||
'cid': 'sydd',
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -409,7 +409,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,
|
||||
|
|
|
@ -21,6 +21,7 @@ from .test_web import (
|
|||
REPOST_AS2,
|
||||
REPOST_HTML,
|
||||
TOOT_AS2,
|
||||
TOOT_AS2_DATA,
|
||||
)
|
||||
|
||||
REPOST_AS2 = {
|
||||
|
@ -82,7 +83,8 @@ class RedirectTest(testutil.TestCase):
|
|||
self._test_as2(as2.CONTENT_TYPE_LD_PROFILE)
|
||||
|
||||
def test_as2_creates_user(self):
|
||||
Object(id='https://user.com/repost', as2=REPOST_AS2).put()
|
||||
Object(id='https://user.com/repost', source_protocol='web',
|
||||
as2=REPOST_AS2).put()
|
||||
|
||||
self.user.key.delete()
|
||||
|
||||
|
@ -96,34 +98,19 @@ class RedirectTest(testutil.TestCase):
|
|||
|
||||
@patch('requests.get')
|
||||
def test_as2_fetch_post(self, mock_get):
|
||||
mock_get.side_effect = [
|
||||
requests_response(REPOST_HTML),
|
||||
TOOT_AS2,
|
||||
]
|
||||
mock_get.return_value = TOOT_AS2 # from Protocol.for_id
|
||||
|
||||
resp = self.client.get('/r/https://user.com/repost',
|
||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
|
||||
self.assert_equals(REPOST_AS2, resp.json)
|
||||
self.assert_equals(TOOT_AS2_DATA, resp.json)
|
||||
self.assertEqual('Accept', resp.headers['Vary'])
|
||||
|
||||
@patch('requests.get')
|
||||
def test_as2_fetch_post_no_backlink(self, mock_get):
|
||||
mock_get.side_effect = [
|
||||
requests_response(
|
||||
REPOST_HTML.replace('<a href="http://localhost/"></a>', '')),
|
||||
TOOT_AS2,
|
||||
]
|
||||
|
||||
resp = self.client.get('/r/https://user.com/repost',
|
||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
|
||||
self.assert_equals(REPOST_AS2, resp.json)
|
||||
self.assertEqual('Accept', resp.headers['Vary'])
|
||||
|
||||
@patch('requests.get')
|
||||
@patch('requests.get', side_effect=[
|
||||
requests_response(ACTOR_HTML), # AS2 fetch
|
||||
requests_response(ACTOR_HTML), # web fetch
|
||||
])
|
||||
def test_as2_no_user_fetch_homepage(self, mock_get):
|
||||
mock_get.return_value = requests_response(ACTOR_HTML)
|
||||
self.user.key.delete()
|
||||
self.user.obj_key.delete()
|
||||
protocol.objects_cache.clear()
|
||||
|
@ -174,7 +161,8 @@ class RedirectTest(testutil.TestCase):
|
|||
self.assertEqual('https://user.com/bar', resp.headers['Location'])
|
||||
|
||||
def _test_as2(self, content_type):
|
||||
self.obj = Object(id='https://user.com/', as2=REPOST_AS2).put()
|
||||
self.obj = Object(id='https://user.com/', source_protocol='web',
|
||||
as2=REPOST_AS2).put()
|
||||
|
||||
resp = self.client.get('/r/https://user.com/', headers={'Accept': content_type})
|
||||
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
|
||||
|
@ -183,7 +171,8 @@ class RedirectTest(testutil.TestCase):
|
|||
self.assertEqual('Accept', resp.headers['Vary'])
|
||||
|
||||
def test_as2_deleted(self):
|
||||
Object(id='https://user.com/bar', as2={}, deleted=True).put()
|
||||
Object(id='https://user.com/bar', as2={}, source_protocol='web',
|
||||
deleted=True).put()
|
||||
|
||||
resp = self.client.get('/r/https://user.com/bar',
|
||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||
|
@ -196,3 +185,13 @@ class RedirectTest(testutil.TestCase):
|
|||
resp = self.client.get('/r/https://user.com/',
|
||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||
self.assertEqual(404, resp.status_code, resp.get_data(as_text=True))
|
||||
|
||||
def test_as2_atproto_normalize_id(self):
|
||||
self.obj = Object(id='at://did:plc:foo/app.bsky.feed.post/123',
|
||||
source_protocol='atproto', as2=REPOST_AS2).put()
|
||||
|
||||
resp = self.client.get('/r/https://bsky.app/profile/did:plc:foo/post/123',
|
||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
|
||||
self.assertEqual(as2.CONTENT_TYPE_LD_PROFILE, resp.content_type)
|
||||
self.assert_equals(REPOST_AS2, resp.json)
|
||||
|
|
|
@ -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'))
|
||||
|
|
Ładowanie…
Reference in New Issue