kopia lustrzana https://github.com/snarfed/bridgy-fed
Porównaj commity
19 Commity
3691b9c32b
...
0cabbf0813
Autor | SHA1 | Data |
---|---|---|
dependabot[bot] | 0cabbf0813 | |
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 | |
Ryan Barrett | 2085b131a1 | |
Ryan Barrett | 117e121cd2 | |
Ryan Barrett | 5ec2159546 | |
Ryan Barrett | ea1f3dce49 | |
Ryan Barrett | f67cecd8f9 | |
dependabot[bot] | e5df116765 |
|
@ -362,8 +362,9 @@ class ActivityPub(User, Protocol):
|
||||||
|
|
||||||
from_proto = PROTOCOLS.get(obj.source_protocol)
|
from_proto = PROTOCOLS.get(obj.source_protocol)
|
||||||
user_id = from_user.key.id() if from_user and from_user.key else None
|
user_id = from_user.key.id() if from_user and from_user.key else None
|
||||||
if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
|
# TODO: uncomment
|
||||||
error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
|
# 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:
|
if obj.as2:
|
||||||
return {
|
return {
|
||||||
|
|
35
atproto.py
35
atproto.py
|
@ -34,6 +34,7 @@ from common import (
|
||||||
DOMAINS,
|
DOMAINS,
|
||||||
error,
|
error,
|
||||||
USER_AGENT,
|
USER_AGENT,
|
||||||
|
USER_ALLOWLIST,
|
||||||
)
|
)
|
||||||
import flask_app
|
import flask_app
|
||||||
from models import Object, PROTOCOLS, Target, User
|
from models import Object, PROTOCOLS, Target, User
|
||||||
|
@ -88,7 +89,7 @@ class ATProto(User, Protocol):
|
||||||
ABBREV = 'atproto'
|
ABBREV = 'atproto'
|
||||||
# TODO: add second bsky label? inject into PROTOCOLS?
|
# TODO: add second bsky label? inject into PROTOCOLS?
|
||||||
PHRASE = 'Bluesky'
|
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}/'
|
PDS_URL = f'https://{ABBREV}{common.SUPERDOMAIN}/'
|
||||||
CONTENT_TYPE = 'application/json'
|
CONTENT_TYPE = 'application/json'
|
||||||
|
|
||||||
|
@ -147,6 +148,23 @@ class ATProto(User, Protocol):
|
||||||
def profile_id(self):
|
def profile_id(self):
|
||||||
return self.profile_at_uri(self.key.id())
|
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
|
@classmethod
|
||||||
def target_for(cls, obj, shared=False):
|
def target_for(cls, obj, shared=False):
|
||||||
"""Returns our PDS URL as the target for the given object.
|
"""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)
|
copy = base_obj.get_copy(to_cls)
|
||||||
assert copy
|
assert copy
|
||||||
copy_did, coll, rkey = parse_at_uri(copy)
|
copy_did, coll, rkey = parse_at_uri(copy)
|
||||||
assert copy_did == did
|
assert copy_did == did, (copy_did, did)
|
||||||
assert coll == type
|
assert coll == type, (coll, type)
|
||||||
|
|
||||||
logger.info(f'Storing ATProto {action} {type} {rkey}: {dag_json.encode(record).decode()}')
|
logger.info(f'Storing ATProto {action} {type} {rkey}: {dag_json.encode(record).decode()}')
|
||||||
repo.apply_writes([Write(action=action, collection=type, rkey=rkey,
|
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)
|
from_proto = PROTOCOLS.get(obj.source_protocol)
|
||||||
user_id = from_user.key.id() if from_user and from_user.key else None
|
user_id = from_user.key.id() if from_user and from_user.key else None
|
||||||
if from_proto and not common.is_enabled(cls, from_proto, handle_or_id=user_id):
|
# TODO: uncomment
|
||||||
error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
|
# 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:
|
if obj.bsky:
|
||||||
return obj.bsky
|
return obj.bsky
|
||||||
|
@ -514,6 +533,8 @@ class ATProto(User, Protocol):
|
||||||
if uri := strong_ref.get('uri'):
|
if uri := strong_ref.get('uri'):
|
||||||
# TODO: fail if this load fails? since we don't populate CID
|
# TODO: fail if this load fails? since we don't populate CID
|
||||||
if ref_obj := ATProto.load(uri):
|
if ref_obj := ATProto.load(uri):
|
||||||
|
if not ref_obj.bsky.get('cid'):
|
||||||
|
ref_obj = ATProto.load(uri, remote=True)
|
||||||
strong_ref.update({
|
strong_ref.update({
|
||||||
'cid': ref_obj.bsky.get('cid'),
|
'cid': ref_obj.bsky.get('cid'),
|
||||||
'uri': ref_obj.key.id(),
|
'uri': ref_obj.key.id(),
|
||||||
|
@ -555,7 +576,7 @@ def poll_notifications():
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
# TODO: remove for launch
|
# 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()}')
|
logger.info(f'Skipping {user.key.id()}')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -615,7 +636,7 @@ def poll_posts():
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
# TODO: remove for launch
|
# 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()}')
|
logger.info(f'Skipping {user.key.id()}')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
6
ids.py
6
ids.py
|
@ -66,7 +66,7 @@ def translate_user_id(*, id, from_proto, to_proto):
|
||||||
Returns:
|
Returns:
|
||||||
str: the corresponding id in ``to_proto``
|
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', \
|
assert from_proto.owns_id(id) is not False or from_proto.LABEL == 'ui', \
|
||||||
(id, from_proto.LABEL, to_proto.LABEL)
|
(id, from_proto.LABEL, to_proto.LABEL)
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ def translate_handle(*, handle, from_proto, to_proto, enhanced):
|
||||||
Returns:
|
Returns:
|
||||||
str: the corresponding handle in ``to_proto``
|
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'
|
assert from_proto.owns_handle(handle) is not False or from_proto.LABEL == 'ui'
|
||||||
|
|
||||||
if from_proto == to_proto:
|
if from_proto == to_proto:
|
||||||
|
@ -188,7 +188,7 @@ def translate_object_id(*, id, from_proto, to_proto):
|
||||||
Returns:
|
Returns:
|
||||||
str: the corresponding id in ``to_proto``
|
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'
|
assert from_proto.owns_id(id) is not False or from_proto.LABEL == 'ui'
|
||||||
|
|
||||||
# bsky.app profile URL to DID
|
# bsky.app profile URL to DID
|
||||||
|
|
38
models.py
38
models.py
|
@ -37,7 +37,7 @@ PROTOCOLS = {'ostatus': None}
|
||||||
|
|
||||||
# 2048 bits makes tests slow, so use 1024 for them
|
# 2048 bits makes tests slow, so use 1024 for them
|
||||||
KEY_BITS = 1024 if DEBUG else 2048
|
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
|
# auto delete old objects of these types via the Object.expire property
|
||||||
# https://cloud.google.com/datastore/docs/ttl
|
# 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.
|
:meth:`protocol.Protocol.translate_ids` is partly the inverse of this.
|
||||||
Much of the same logic is duplicated there!
|
Much of the same logic is duplicated there!
|
||||||
|
|
||||||
TODO: unify with :meth:`normalize_ids`,
|
TODO: unify with :meth:`normalize_ids`, :meth:`Object.normalize_ids`.
|
||||||
:meth:`protocol.Protocol.normalize_ids`.
|
|
||||||
"""
|
"""
|
||||||
if not self.as1:
|
if not self.as1:
|
||||||
return
|
return
|
||||||
|
@ -1231,7 +1230,9 @@ def fetch_objects(query, by=None, user=None):
|
||||||
if inner_type:
|
if inner_type:
|
||||||
type = inner_type
|
type = inner_type
|
||||||
|
|
||||||
|
# AS1 verb => human-readable phrase
|
||||||
phrases = {
|
phrases = {
|
||||||
|
'accept': 'accepted',
|
||||||
'article': 'posted',
|
'article': 'posted',
|
||||||
'comment': 'replied',
|
'comment': 'replied',
|
||||||
'delete': 'deleted',
|
'delete': 'deleted',
|
||||||
|
@ -1271,34 +1272,37 @@ def fetch_objects(query, by=None, user=None):
|
||||||
'content': 'their profile',
|
'content': 'their profile',
|
||||||
'url': id,
|
'url': id,
|
||||||
})
|
})
|
||||||
elif url:
|
elif url and not content:
|
||||||
# heuristics for sniffing URLs and converting them to more friendly
|
# heuristics for sniffing URLs and converting them to more friendly
|
||||||
# phrases and user handles.
|
# phrases and user handles.
|
||||||
# TODO: standardize this into granary.as2 somewhere?
|
# TODO: standardize this into granary.as2 somewhere?
|
||||||
if not content:
|
|
||||||
from activitypub import FEDI_URL_RE
|
from activitypub import FEDI_URL_RE
|
||||||
from atproto import COLLECTION_TO_TYPE, did_to_handle
|
from atproto import COLLECTION_TO_TYPE, did_to_handle
|
||||||
|
|
||||||
|
handle = suffix = ''
|
||||||
if match := FEDI_URL_RE.match(url):
|
if match := FEDI_URL_RE.match(url):
|
||||||
content = '@' + match.group(2)
|
handle = match.group(2)
|
||||||
if match.group(4):
|
if match.group(4):
|
||||||
content += "'s post"
|
suffix = "'s post"
|
||||||
elif match := BSKY_APP_URL_RE.match(url):
|
elif match := BSKY_APP_URL_RE.match(url):
|
||||||
id = match.group('id')
|
handle = match.group('id')
|
||||||
if id.startswith('did:'):
|
|
||||||
id = ATdid_to_handle(id) or id
|
|
||||||
content = '@' + id
|
|
||||||
if match.group('tid'):
|
if match.group('tid'):
|
||||||
content += "'s post"
|
suffix = "'s post"
|
||||||
elif match := AT_URI_PATTERN.match(url):
|
elif match := AT_URI_PATTERN.match(url):
|
||||||
id = match.group('repo')
|
handle = match.group('repo')
|
||||||
if id.startswith('did:'):
|
|
||||||
id = did_to_handle(id) or id
|
|
||||||
content = '@' + id
|
|
||||||
if coll := match.group('collection'):
|
if coll := match.group('collection'):
|
||||||
content += f"'s {COLLECTION_TO_TYPE.get(coll) or 'post'}"
|
suffix = f"'s {COLLECTION_TO_TYPE.get(coll) or 'post'}"
|
||||||
url = bluesky.at_uri_to_web_url(url)
|
url = bluesky.at_uri_to_web_url(url)
|
||||||
|
elif url.startswith('did:'):
|
||||||
|
handle = url
|
||||||
|
url = bluesky.Bluesky.user_url(handle)
|
||||||
|
|
||||||
|
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)
|
content = common.pretty_link(url, text=content, user=user)
|
||||||
|
|
||||||
obj.content = (obj_as1.get('content')
|
obj.content = (obj_as1.get('content')
|
||||||
|
|
13
pages.py
13
pages.py
|
@ -23,6 +23,7 @@ from oauth_dropins.webutil.flask_util import (
|
||||||
import common
|
import common
|
||||||
from common import DOMAIN_RE
|
from common import DOMAIN_RE
|
||||||
from flask_app import app, cache
|
from flask_app import app, cache
|
||||||
|
import ids
|
||||||
from models import fetch_objects, fetch_page, Follower, Object, PAGE_SIZE, PROTOCOLS
|
from models import fetch_objects, fetch_page, Follower, Object, PAGE_SIZE, PROTOCOLS
|
||||||
from protocol import Protocol
|
from protocol import Protocol
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ TEMPLATE_VARS = {
|
||||||
'as1': as1,
|
'as1': as1,
|
||||||
'as2': as2,
|
'as2': as2,
|
||||||
'g': g,
|
'g': g,
|
||||||
|
'ids': ids,
|
||||||
'isinstance': isinstance,
|
'isinstance': isinstance,
|
||||||
'logs': logs,
|
'logs': logs,
|
||||||
'PROTOCOLS': PROTOCOLS,
|
'PROTOCOLS': PROTOCOLS,
|
||||||
|
@ -232,7 +234,16 @@ def serve_feed(*, objects, format, user, title, as_snippets=False, quiet=False):
|
||||||
a[f] = future.result().as1
|
a[f] = future.result().as1
|
||||||
return maybe_set
|
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))
|
future.add_done_callback(hydrate(a, field))
|
||||||
gets.append(future)
|
gets.append(future)
|
||||||
|
|
||||||
|
|
17
protocol.py
17
protocol.py
|
@ -353,6 +353,21 @@ class Protocol:
|
||||||
|
|
||||||
return (None, None)
|
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
|
@classmethod
|
||||||
def actor_key(cls, obj):
|
def actor_key(cls, obj):
|
||||||
"""Returns the :class:`User`: key for a given object's author or actor.
|
"""Returns the :class:`User`: key for a given object's author or actor.
|
||||||
|
@ -500,7 +515,7 @@ class Protocol:
|
||||||
same logic is duplicated there!
|
same logic is duplicated there!
|
||||||
|
|
||||||
TODO: unify with :meth:`Object.resolve_ids`,
|
TODO: unify with :meth:`Object.resolve_ids`,
|
||||||
:meth:`protocol.Protocol.normalize_ids`.
|
:meth:`models.Object.normalize_ids`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
to_proto (Protocol subclass)
|
to_proto (Protocol subclass)
|
||||||
|
|
57
redirect.py
57
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 oauth_dropins.webutil.util import json_dumps, json_loads
|
||||||
|
|
||||||
from activitypub import ActivityPub
|
from activitypub import ActivityPub
|
||||||
from flask_app import app, cache
|
|
||||||
from common import CACHE_TIME, CONTENT_TYPE_HTML
|
from common import CACHE_TIME, CONTENT_TYPE_HTML
|
||||||
|
from flask_app import app, cache
|
||||||
|
from protocol import Protocol
|
||||||
from web import Web
|
from web import Web
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -85,28 +86,53 @@ def redir(to):
|
||||||
domains = set((util.domain_from_link(to, minimize=True),
|
domains = set((util.domain_from_link(to, minimize=True),
|
||||||
util.domain_from_link(to, minimize=False),
|
util.domain_from_link(to, minimize=False),
|
||||||
to_domain))
|
to_domain))
|
||||||
|
web_user = None
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
if domain:
|
if domain:
|
||||||
if domain in DOMAIN_ALLOWLIST:
|
if domain in DOMAIN_ALLOWLIST:
|
||||||
break
|
break
|
||||||
if Web.get_by_id(domain):
|
if web_user := Web.get_by_id(domain):
|
||||||
logger.info(f'Found web user for domain {domain}')
|
logger.info(f'Found web user for domain {domain}')
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if not accept_as2:
|
if not accept_as2:
|
||||||
return f'No web user found for any of {domains}', 404, VARY_HEADER
|
return f'No web user found for any of {domains}', 404, VARY_HEADER
|
||||||
|
|
||||||
if accept_as2:
|
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
|
# AS2 requested, fetch and convert and serve
|
||||||
obj = Web.load(to, check_backlink=False)
|
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:
|
if not obj or obj.deleted:
|
||||||
return f'Object not found: {to}', 404, VARY_HEADER
|
return f'Object not found: {to}', 404, VARY_HEADER
|
||||||
|
|
||||||
user = Web.get_or_create(util.domain_from_link(to), direct=False, obj=obj)
|
# TODO: do this for other protocols too?
|
||||||
if not user:
|
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
|
return f'Object not found: {to}', 404, VARY_HEADER
|
||||||
|
|
||||||
ret = ActivityPub.convert(obj, from_user=user)
|
ret = ActivityPub.convert(obj, from_user=web_user)
|
||||||
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
|
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
|
||||||
return ret, {
|
return ret, {
|
||||||
'Content-Type': (as2.CONTENT_TYPE_LD_PROFILE
|
'Content-Type': (as2.CONTENT_TYPE_LD_PROFILE
|
||||||
|
@ -116,20 +142,3 @@ def redir(to):
|
||||||
**VARY_HEADER,
|
**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
|
grpc-google-iam-v1==0.13.0
|
||||||
grpcio==1.62.1
|
grpcio==1.62.1
|
||||||
grpcio-status==1.62.1
|
grpcio-status==1.62.1
|
||||||
gunicorn==21.2.0
|
gunicorn==22.0.0
|
||||||
h11==0.14.0
|
h11==0.14.0
|
||||||
html2text==2024.2.26
|
html2text==2024.2.26
|
||||||
html5lib==1.1
|
html5lib==1.1
|
||||||
|
@ -58,7 +58,7 @@ humanfriendly==10.0
|
||||||
humanize==4.9.0
|
humanize==4.9.0
|
||||||
idna==3.7
|
idna==3.7
|
||||||
iterators==0.2.0
|
iterators==0.2.0
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.2.0
|
||||||
Jinja2==3.1.3
|
Jinja2==3.1.3
|
||||||
jsonschema==4.21.1
|
jsonschema==4.21.1
|
||||||
lxml==5.2.1
|
lxml==5.2.1
|
||||||
|
@ -89,7 +89,7 @@ pytz==2024.1
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
redis==5.0.3
|
redis==5.0.3
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
requests-oauthlib==1.4.0
|
requests-oauthlib==2.0.0
|
||||||
rsa==4.9
|
rsa==4.9
|
||||||
sgmllib3k==1.0.0
|
sgmllib3k==1.0.0
|
||||||
simple-websocket==1.0.0
|
simple-websocket==1.0.0
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
<li class="row">
|
<li class="row">
|
||||||
{% with url=f.user.web_url(), user_as1=f.user.obj.as1 or {} %}
|
{% 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 }}">
|
<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') %}
|
{% with picture=util.get_url(user_as1, 'icon') or util.get_url(user_as1, 'image') %}
|
||||||
{% if picture %}
|
{% if picture %}
|
||||||
<img class="profile u-photo" src="{{ picture }}" width="48px">
|
<img class="profile u-photo" src="{{ picture }}" width="48px">
|
||||||
|
|
|
@ -3,11 +3,14 @@
|
||||||
{% for obj in objects %}
|
{% for obj in objects %}
|
||||||
<li class="row h-entry">
|
<li class="row h-entry">
|
||||||
<div class="e-content col-xs-{{ 5 if show_users else 8 }}">
|
<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.actor_link(user=user)|safe }}
|
||||||
{{ obj.phrase|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) }}
|
{{ obj.content|default('--', true)|striptags|truncate(50) }}
|
||||||
</a>
|
{% if obj.url %}</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if show_users %}
|
{% if show_users %}
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
<nobr>
|
<nobr>
|
||||||
|
|
||||||
<a href="{{ user.web_url() }}"
|
<a href="{{ user.web_url() }}"
|
||||||
title="{{ user.__class__.__name__ }} (native)">
|
title="{{ user.__class__.__name__ }} (original)">
|
||||||
<span class="logo">{{ user.LOGO_HTML|safe }}</span>
|
<span class="logo">{{ user.LOGO_HTML|safe }}</span>
|
||||||
{{ user.handle_or_id() }}
|
{{ user.handle_or_id() }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -57,13 +57,18 @@
|
||||||
</nobr>
|
</nobr>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{% set copies = user.copies|map(attribute='protocol')|list %}
|
||||||
{% for proto in set(PROTOCOLS.values()) %}
|
{% for proto in set(PROTOCOLS.values()) %}
|
||||||
{% if proto and not isinstance(user, proto)
|
{% if proto and not isinstance(user, proto)
|
||||||
and proto.LABEL not in ('atproto', 'ui', 'web') %}
|
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) %}
|
||||||
·
|
·
|
||||||
<nobr title="{{ proto.__name__ }} (bridged)">
|
<nobr title="{{ proto.__name__ }} (bridged)">
|
||||||
|
{% if url %} <a href="{{ url }}"> {% endif %}
|
||||||
<span class="logo">{{ proto.LOGO_HTML|safe }}</span>
|
<span class="logo">{{ proto.LOGO_HTML|safe }}</span>
|
||||||
{{ user.handle_as(proto) }}
|
{{ user.handle_as(proto) }}
|
||||||
|
{% if url %} </a> {% endif %}
|
||||||
</nobr>
|
</nobr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -2218,6 +2218,8 @@ class ActivityPubUtilsTest(TestCase):
|
||||||
'object': ACTOR,
|
'object': ACTOR,
|
||||||
}, ActivityPub.convert(obj))
|
}, ActivityPub.convert(obj))
|
||||||
|
|
||||||
|
# TODO: remove
|
||||||
|
@skip
|
||||||
def test_convert_protocols_not_enabled(self):
|
def test_convert_protocols_not_enabled(self):
|
||||||
obj = Object(our_as1={'foo': 'bar'}, source_protocol='atproto')
|
obj = Object(our_as1={'foo': 'bar'}, source_protocol='atproto')
|
||||||
with self.assertRaises(BadRequest):
|
with self.assertRaises(BadRequest):
|
||||||
|
|
|
@ -142,6 +142,17 @@ class ATProtoTest(TestCase):
|
||||||
def test_handle_to_id_not_found(self, *_):
|
def test_handle_to_id_not_found(self, *_):
|
||||||
self.assertIsNone(ATProto.handle_to_id('han.dull'))
|
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):
|
def test_pds_for_did_no_doc(self):
|
||||||
self.assertIsNone(ATProto.pds_for(Object(id='did:plc:user')))
|
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',
|
'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):
|
def test_convert_blobs_false(self):
|
||||||
self.assertEqual({
|
self.assertEqual({
|
||||||
'$type': 'app.bsky.actor.profile',
|
'$type': 'app.bsky.actor.profile',
|
||||||
|
@ -568,6 +609,8 @@ class ATProtoTest(TestCase):
|
||||||
}],
|
}],
|
||||||
})))
|
})))
|
||||||
|
|
||||||
|
# TODO: remove
|
||||||
|
@skip
|
||||||
def test_convert_protocols_not_enabled(self):
|
def test_convert_protocols_not_enabled(self):
|
||||||
obj = Object(our_as1={'foo': 'bar'}, source_protocol='activitypub')
|
obj = Object(our_as1={'foo': 'bar'}, source_protocol='activitypub')
|
||||||
with self.assertRaises(BadRequest):
|
with self.assertRaises(BadRequest):
|
||||||
|
|
|
@ -40,6 +40,7 @@ HTML = """\
|
||||||
A ☕ reply
|
A ☕ reply
|
||||||
</div>
|
</div>
|
||||||
<a class="u-in-reply-to" href="https://fake.com/123"></a>
|
<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>
|
</article>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -64,6 +65,7 @@ AUTHOR_HTML = """\
|
||||||
A ☕ reply
|
A ☕ reply
|
||||||
</div>
|
</div>
|
||||||
<a class="u-in-reply-to" href="https://fake.com/123"></a>
|
<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>
|
</article>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Integration tests."""
|
"""Integration tests."""
|
||||||
|
import copy
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from arroba.datastore_storage import DatastoreStorage
|
from arroba.datastore_storage import DatastoreStorage
|
||||||
|
@ -10,7 +11,7 @@ from activitypub import ActivityPub
|
||||||
import app
|
import app
|
||||||
from atproto import ATProto
|
from atproto import ATProto
|
||||||
from dns.resolver import NXDOMAIN
|
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
|
import hub
|
||||||
from models import Object, Target
|
from models import Object, Target
|
||||||
from web import Web
|
from web import Web
|
||||||
|
@ -127,6 +128,7 @@ class IntegrationTests(TestCase):
|
||||||
'to': ['https://www.w3.org/ns/activitystreams#Public'],
|
'to': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@patch('requests.post', return_value=requests_response(''))
|
@patch('requests.post', return_value=requests_response(''))
|
||||||
@patch('requests.get')
|
@patch('requests.get')
|
||||||
def test_atproto_follow_to_web(self, mock_get, mock_post):
|
def test_atproto_follow_to_web(self, mock_get, mock_post):
|
||||||
|
@ -180,6 +182,7 @@ class IntegrationTests(TestCase):
|
||||||
'target': 'https://bob.com/',
|
'target': 'https://bob.com/',
|
||||||
}, allow_redirects=False, headers={'Accept': '*/*'})
|
}, allow_redirects=False, headers={'Accept': '*/*'})
|
||||||
|
|
||||||
|
|
||||||
@patch('dns.resolver.resolve', side_effect=NXDOMAIN())
|
@patch('dns.resolver.resolve', side_effect=NXDOMAIN())
|
||||||
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
|
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
|
||||||
@patch('requests.post', side_effect=[
|
@patch('requests.post', side_effect=[
|
||||||
|
@ -213,7 +216,7 @@ class IntegrationTests(TestCase):
|
||||||
# alice profile
|
# alice profile
|
||||||
requests_response(PROFILE_GETRECORD),
|
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.
|
"""Incoming webmention for a web follow of an ATProto bsky.app profile URL.
|
||||||
|
|
||||||
Web user bob.com
|
Web user bob.com
|
||||||
|
@ -270,3 +273,82 @@ class IntegrationTests(TestCase):
|
||||||
'subject': 'did:plc:alice',
|
'subject': 'did:plc:alice',
|
||||||
'createdAt': '2022-01-02T03:04:05.000Z',
|
'createdAt': '2022-01-02T03:04:05.000Z',
|
||||||
}], list(records['app.bsky.graph.follow'].values()))
|
}], 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',
|
||||||
|
})
|
||||||
|
|
|
@ -21,6 +21,7 @@ from .test_web import (
|
||||||
REPOST_AS2,
|
REPOST_AS2,
|
||||||
REPOST_HTML,
|
REPOST_HTML,
|
||||||
TOOT_AS2,
|
TOOT_AS2,
|
||||||
|
TOOT_AS2_DATA,
|
||||||
)
|
)
|
||||||
|
|
||||||
REPOST_AS2 = {
|
REPOST_AS2 = {
|
||||||
|
@ -82,7 +83,8 @@ class RedirectTest(testutil.TestCase):
|
||||||
self._test_as2(as2.CONTENT_TYPE_LD_PROFILE)
|
self._test_as2(as2.CONTENT_TYPE_LD_PROFILE)
|
||||||
|
|
||||||
def test_as2_creates_user(self):
|
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()
|
self.user.key.delete()
|
||||||
|
|
||||||
|
@ -96,34 +98,19 @@ class RedirectTest(testutil.TestCase):
|
||||||
|
|
||||||
@patch('requests.get')
|
@patch('requests.get')
|
||||||
def test_as2_fetch_post(self, mock_get):
|
def test_as2_fetch_post(self, mock_get):
|
||||||
mock_get.side_effect = [
|
mock_get.return_value = TOOT_AS2 # from Protocol.for_id
|
||||||
requests_response(REPOST_HTML),
|
|
||||||
TOOT_AS2,
|
|
||||||
]
|
|
||||||
|
|
||||||
resp = self.client.get('/r/https://user.com/repost',
|
resp = self.client.get('/r/https://user.com/repost',
|
||||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||||
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
|
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'])
|
self.assertEqual('Accept', resp.headers['Vary'])
|
||||||
|
|
||||||
@patch('requests.get')
|
@patch('requests.get', side_effect=[
|
||||||
def test_as2_fetch_post_no_backlink(self, mock_get):
|
requests_response(ACTOR_HTML), # AS2 fetch
|
||||||
mock_get.side_effect = [
|
requests_response(ACTOR_HTML), # web fetch
|
||||||
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')
|
|
||||||
def test_as2_no_user_fetch_homepage(self, mock_get):
|
def test_as2_no_user_fetch_homepage(self, mock_get):
|
||||||
mock_get.return_value = requests_response(ACTOR_HTML)
|
|
||||||
self.user.key.delete()
|
self.user.key.delete()
|
||||||
self.user.obj_key.delete()
|
self.user.obj_key.delete()
|
||||||
protocol.objects_cache.clear()
|
protocol.objects_cache.clear()
|
||||||
|
@ -174,7 +161,8 @@ class RedirectTest(testutil.TestCase):
|
||||||
self.assertEqual('https://user.com/bar', resp.headers['Location'])
|
self.assertEqual('https://user.com/bar', resp.headers['Location'])
|
||||||
|
|
||||||
def _test_as2(self, content_type):
|
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})
|
resp = self.client.get('/r/https://user.com/', headers={'Accept': content_type})
|
||||||
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
|
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'])
|
self.assertEqual('Accept', resp.headers['Vary'])
|
||||||
|
|
||||||
def test_as2_deleted(self):
|
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',
|
resp = self.client.get('/r/https://user.com/bar',
|
||||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||||
|
@ -196,3 +185,13 @@ class RedirectTest(testutil.TestCase):
|
||||||
resp = self.client.get('/r/https://user.com/',
|
resp = self.client.get('/r/https://user.com/',
|
||||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||||
self.assertEqual(404, resp.status_code, resp.get_data(as_text=True))
|
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)
|
||||||
|
|
Ładowanie…
Reference in New Issue