Porównaj commity

...

19 Commity

Autor SHA1 Wiadomość Data
dependabot[bot] 0cabbf0813
Merge e5df116765 into 2036f92ddd 2024-04-17 12:50:08 +00:00
dependabot[bot] 2036f92ddd build(deps): bump itsdangerous from 2.1.2 to 2.2.0
Bumps [itsdangerous](https://github.com/pallets/itsdangerous) from 2.1.2 to 2.2.0.
- [Release notes](https://github.com/pallets/itsdangerous/releases)
- [Changelog](https://github.com/pallets/itsdangerous/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/itsdangerous/compare/2.1.2...2.2.0)

---
updated-dependencies:
- dependency-name: itsdangerous
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-17 05:50:04 -07:00
dependabot[bot] 7190503aea build(deps): bump gunicorn from 21.2.0 to 22.0.0
Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 21.2.0 to 22.0.0.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/21.2.0...22.0.0)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-16 19:30:32 -07:00
Ryan Barrett bf52d80e0f
add protocol logo to user page activities and followers
fixes #939

also drop page size from 30 to 20
2024-04-15 19:41:22 -07:00
Ryan Barrett 12a3bf0862
noop: add messages to asserts in ids, atproto 2024-04-15 16:44:33 -07:00
Ryan Barrett a70702776c
atproto: switch to Bluesky butterfly logo
also minor user page language tweak
2024-04-15 14:53:54 -07:00
Ryan Barrett 86ad33b896
user page tweak for follow accepts
for #825
2024-04-15 14:45:02 -07:00
Ryan Barrett 374af3aa5c
atproto polls: use common.USER_ALLOWLIST 2024-04-15 14:08:36 -07:00
Ryan Barrett e913ad1f53
ATProto.convert: refetch subject for cid if we don't have it 2024-04-15 07:31:05 -07:00
Ryan Barrett 2ec22de09f
abstract redirect.py to be multi-protocol
...mostly. creating the underlying user opportunistically is still Web-only.
2024-04-14 18:26:34 -07:00
Ryan Barrett 5b5ed4173a
fix tests for f840c8b784 2024-04-14 14:57:00 -07:00
Ryan Barrett f840c8b784
temporarily disable is_enabled checks in ATProto/ActivityPub.convert
for manual testing
2024-04-14 14:50:02 -07:00
Ryan Barrett cf633efecf
update tests for snarfed/granary@75a8f45825 2024-04-12 15:11:57 -07:00
Ryan Barrett 2085b131a1
atproto user pages: render DIDs as handles with profile links
for #825
2024-04-12 12:34:49 -07:00
Ryan Barrett 117e121cd2
atproto: populate author in feeds (Atom, RSS, etc) 2024-04-12 12:04:52 -07:00
Ryan Barrett 5ec2159546
user page: link to bridged Bluesky profile
for #825
2024-04-12 08:46:59 -07:00
Ryan Barrett ea1f3dce49
user page bug fix for 133d640f1d 2024-04-12 07:16:06 -07:00
Ryan Barrett f67cecd8f9
add copy protocol (eg ATProto) handles to user pages 2024-04-12 07:16:05 -07:00
dependabot[bot] e5df116765
build(deps): bump requests-oauthlib from 1.4.0 to 2.0.0
Bumps [requests-oauthlib](https://github.com/requests/requests-oauthlib) from 1.4.0 to 2.0.0.
- [Release notes](https://github.com/requests/requests-oauthlib/releases)
- [Changelog](https://github.com/requests/requests-oauthlib/blob/master/HISTORY.rst)
- [Commits](https://github.com/requests/requests-oauthlib/compare/v1.4.0...v2.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 12:34:12 +00:00
16 zmienionych plików z 312 dodań i 112 usunięć

Wyświetl plik

@ -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 {

Wyświetl plik

@ -34,6 +34,7 @@ from common import (
DOMAINS,
error,
USER_AGENT,
USER_ALLOWLIST,
)
import flask_app
from models import Object, PROTOCOLS, Target, User
@ -88,7 +89,7 @@ class ATProto(User, Protocol):
ABBREV = 'atproto'
# TODO: add second bsky label? inject into PROTOCOLS?
PHRASE = 'Bluesky'
LOGO_HTML = '<img src="/static/atproto_logo.png">'
LOGO_HTML = '<img src="/oauth_dropins_static/bluesky.svg">'
PDS_URL = f'https://{ABBREV}{common.SUPERDOMAIN}/'
CONTENT_TYPE = 'application/json'
@ -147,6 +148,23 @@ class ATProto(User, Protocol):
def profile_id(self):
return self.profile_at_uri(self.key.id())
@classmethod
def bridged_web_url_for(cls, user):
"""Returns a bridged user's profile URL on bsky.app.
For example, returns ``https://bsky.app/profile/alice.com.web.brid.gy``
for Web user ``alice.com``.
Args:
user (models.User)
Returns:
str, or None if there isn't a canonical URL
"""
if not isinstance(user, ATProto):
if did := user.get_copy(ATProto):
return bluesky.Bluesky.user_url(did_to_handle(did) or did)
@classmethod
def target_for(cls, obj, shared=False):
"""Returns our PDS URL as the target for the given object.
@ -375,8 +393,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,
@ -484,8 +502,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
@ -514,13 +533,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'])
@ -555,7 +576,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
@ -615,7 +636,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

6
ids.py
Wyświetl plik

@ -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

Wyświetl plik

@ -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
@ -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
@ -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',
@ -1271,35 +1272,38 @@ def fetch_objects(query, by=None, user=None):
'content': 'their profile',
'url': id,
})
elif url:
elif url and not content:
# heuristics for sniffing URLs and converting them to more friendly
# phrases and user handles.
# TODO: standardize this into granary.as2 somewhere?
if not content:
from activitypub import FEDI_URL_RE
from atproto import COLLECTION_TO_TYPE, did_to_handle
from activitypub import FEDI_URL_RE
from atproto import COLLECTION_TO_TYPE, did_to_handle
if match := FEDI_URL_RE.match(url):
content = '@' + match.group(2)
if match.group(4):
content += "'s post"
elif match := BSKY_APP_URL_RE.match(url):
id = match.group('id')
if id.startswith('did:'):
id = ATdid_to_handle(id) or id
content = '@' + id
if match.group('tid'):
content += "'s post"
elif match := AT_URI_PATTERN.match(url):
id = match.group('repo')
if id.startswith('did:'):
id = did_to_handle(id) or id
content = '@' + id
if coll := match.group('collection'):
content += f"'s {COLLECTION_TO_TYPE.get(coll) or 'post'}"
url = bluesky.at_uri_to_web_url(url)
handle = suffix = ''
if match := FEDI_URL_RE.match(url):
handle = match.group(2)
if match.group(4):
suffix = "'s post"
elif match := BSKY_APP_URL_RE.match(url):
handle = match.group('id')
if match.group('tid'):
suffix = "'s post"
elif match := AT_URI_PATTERN.match(url):
handle = match.group('repo')
if coll := match.group('collection'):
suffix = f"'s {COLLECTION_TO_TYPE.get(coll) or 'post'}"
url = bluesky.at_uri_to_web_url(url)
elif url.startswith('did:'):
handle = url
url = bluesky.Bluesky.user_url(handle)
content = common.pretty_link(url, text=content, user=user)
if handle:
if handle.startswith('did:'):
handle = did_to_handle(handle) or handle
content = f'@{handle}{suffix}'
if url:
content = common.pretty_link(url, text=content, user=user)
obj.content = (obj_as1.get('content')
or obj_as1.get('displayName')

Wyświetl plik

@ -23,6 +23,7 @@ from oauth_dropins.webutil.flask_util import (
import common
from common import DOMAIN_RE
from flask_app import app, cache
import ids
from models import fetch_objects, fetch_page, Follower, Object, PAGE_SIZE, PROTOCOLS
from protocol import Protocol
@ -38,6 +39,7 @@ TEMPLATE_VARS = {
'as1': as1,
'as2': as2,
'g': g,
'ids': ids,
'isinstance': isinstance,
'logs': logs,
'PROTOCOLS': PROTOCOLS,
@ -232,7 +234,16 @@ def serve_feed(*, objects, format, user, title, as_snippets=False, quiet=False):
a[f] = future.result().as1
return maybe_set
future = Object.get_by_id_async(val['id'])
# TODO: extract a Protocol class method out of User.profile_id,
# then use that here instead. the catch is that we'd need to
# determine Protocol for every id, which is expensive.
#
# same TODO is in models.fetch_objects
id = val['id']
if id.startswith('did:'):
id = f'at://{id}/app.bsky.actor.profile/self'
future = Object.get_by_id_async(id)
future.add_done_callback(hydrate(a, field))
gets.append(future)

Wyświetl plik

@ -353,6 +353,21 @@ class Protocol:
return (None, None)
@classmethod
def bridged_web_url_for(cls, user):
"""Returns the web URL for a user's bridged profile in this protocol.
For example, for Web user ``alice.com``, :meth:`ATProto.bridged_web_url_for`
returns ``https://bsky.app/profile/alice.com.web.brid.gy``
Args:
user (models.User)
Returns:
str, or None if there isn't a canonical URL
"""
return None
@classmethod
def actor_key(cls, obj):
"""Returns the :class:`User`: key for a given object's author or actor.
@ -500,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)

Wyświetl plik

@ -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,
}

Wyświetl plik

@ -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
@ -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

Wyświetl plik

@ -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">

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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>
@ -57,13 +57,18 @@
</nobr>
</span>
{% 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 ('atproto', 'ui', 'web') %}
{% 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) %}
{% set url = proto.bridged_web_url_for(user) %}
&middot;
<nobr title="{{ proto.__name__ }} (bridged)">
{% if url %} <a href="{{ url }}"> {% endif %}
<span class="logo">{{ proto.LOGO_HTML|safe }}</span>
{{ user.handle_as(proto) }}
{% if url %} </a> {% endif %}
</nobr>
{% endif %}
{% endfor %}

Wyświetl plik

@ -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):

Wyświetl plik

@ -142,6 +142,17 @@ class ATProtoTest(TestCase):
def test_handle_to_id_not_found(self, *_):
self.assertIsNone(ATProto.handle_to_id('han.dull'))
def test_bridged_web_url_for(self):
self.assertIsNone(ATProto.bridged_web_url_for(ATProto(id='did:plc:foo')))
fake = Fake(id='fake:user')
self.assertIsNone(ATProto.bridged_web_url_for(fake))
fake.copies = [Target(uri='did:plc:user', protocol='atproto')]
self.store_object(id='did:plc:user', raw=DID_DOC)
self.assertEqual('https://bsky.app/profile/han.dull',
ATProto.bridged_web_url_for(fake))
def test_pds_for_did_no_doc(self):
self.assertIsNone(ATProto.pds_for(Object(id='did:plc:user')))
@ -482,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',
@ -568,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):

Wyświetl plik

@ -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>

Wyświetl plik

@ -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
@ -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):
@ -180,6 +182,7 @@ class IntegrationTests(TestCase):
'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'] = 'https://atproto.brid.gy/'
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://atproto.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',
})

Wyświetl plik

@ -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)