Porównaj commity

...

20 Commity

Autor SHA1 Wiadomość Data
dependabot[bot] 3691b9c32b
Merge e5df116765 into 0ed24c19b3 2024-04-12 04:07:19 +00:00
dependabot[bot] 0ed24c19b3 build(deps): bump idna from 3.6 to 3.7
Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 21:07:14 -07:00
Ryan Barrett 133d640f1d
improve rendering of ATProto interactions on user pages
for #825
2024-04-11 15:02:15 -07:00
Ryan Barrett 817ef1d5d6
user pages: only hide indirect AP users, not ATProto 2024-04-11 14:24:18 -07:00
Ryan Barrett 0d549dc039
atproto: fix log message 2024-04-11 14:21:30 -07:00
dependabot[bot] c43a94d0d7 build(deps): bump ecdsa from 0.18.0 to 0.19.0
Bumps [ecdsa](https://github.com/tlsfuzzer/python-ecdsa) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/tlsfuzzer/python-ecdsa/releases)
- [Changelog](https://github.com/tlsfuzzer/python-ecdsa/blob/master/NEWS)
- [Commits](https://github.com/tlsfuzzer/python-ecdsa/compare/python-ecdsa-0.18.0...python-ecdsa-0.19.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 09:28:19 -07:00
dependabot[bot] 2bc877da71 build(deps): bump flask from 3.0.2 to 3.0.3
Bumps [flask](https://github.com/pallets/flask) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 09:28:05 -07:00
dependabot[bot] 6ceb417026 build(deps): bump cbor2 from 5.6.2 to 5.6.3
Bumps [cbor2](https://github.com/agronholm/cbor2) from 5.6.2 to 5.6.3.
- [Release notes](https://github.com/agronholm/cbor2/releases)
- [Changelog](https://github.com/agronholm/cbor2/blob/master/docs/versionhistory.rst)
- [Commits](https://github.com/agronholm/cbor2/compare/5.6.2...5.6.3)

---
updated-dependencies:
- dependency-name: cbor2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 05:10:00 -07:00
Ryan Barrett 561c763fd4
atproto handle resolution: test, comments, minor refactoring
for snarfed/granary@d4bff45637
2024-04-10 16:34:50 -07:00
Ryan Barrett c1acec1a3f
clear did.resolve_* caches in tests
for snarfed/arroba@f95365d7c3
2024-04-10 15:20:28 -07:00
Ryan Barrett bf296802d8
pass from_user through to convert() in a few more places 2024-04-10 15:16:37 -07:00
Ryan Barrett 7009267bb0
use new handle_or_id kwarg in common.is_enabled calls 2024-04-10 11:49:53 -07:00
Ryan Barrett 056644d19e
add handle_or_id kwarg to common.is_enabled
for allowlist of test users while testing a given pair of protocols
2024-04-10 11:40:17 -07:00
Ryan Barrett 5767ffabb5
add new Object.normalize_ids method, use in Protocol.receive
eg https://bsky.app/... URLs to DIDs for actors or at:// URIs for objects

this hopefully fixes web => ATProto follows of https://bsky.app/... profile URLs.
2024-04-09 13:07:57 -07:00
Ryan Barrett 131cbd9eb6
add web => ATProto follow integration test 2024-04-09 10:49:14 -07:00
Ryan Barrett c57e905204
Protocol.for_id: add remote kwarg 2024-04-09 10:48:05 -07:00
Ryan Barrett a45917cec8
ids.translate_object_id: normalize bsky.app profile URLs to DIDs 2024-04-09 10:04:16 -07:00
Ryan Barrett e3f2c2e0bc
ids.translate_user_id: normalize bsky.app profile URLs to DIDs 2024-04-09 09:48:16 -07:00
Ryan Barrett 2b7598cf94
update test_atproto for snarfed/granary@8dd5e470b2 2024-04-08 15:33:21 -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
18 zmienionych plików z 512 dodań i 101 usunięć

Wyświetl plik

@ -54,6 +54,8 @@ _INSTANCE_ACTOR = None
# populated in User.status
WEB_OPT_OUT_DOMAINS = None
FEDI_URL_RE = re.compile(r'https://[^/]+/(@|users/)([^/@]+)(@[^/@]+)?(/(?:statuses/)?[0-9]+)?')
def instance_actor():
global _INSTANCE_ACTOR
@ -226,8 +228,8 @@ class ActivityPub(User, Protocol):
logger.info(f'Skipping sending to blocklisted {url}')
return False
activity = to_cls.convert(obj, from_user=from_user,
orig_obj=to_cls.convert(orig_obj))
orig_obj = to_cls.convert(orig_obj, from_user=from_user)
activity = to_cls.convert(obj, from_user=from_user, orig_obj=orig_obj)
return signed_post(url, data=activity, from_user=from_user).ok
@ -359,7 +361,8 @@ class ActivityPub(User, Protocol):
return {}
from_proto = PROTOCOLS.get(obj.source_protocol)
if from_proto and not common.is_enabled(cls, from_proto):
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')
if obj.as2:
@ -828,7 +831,7 @@ def actor(handle_or_id):
cls = Protocol.for_request(fed='web')
if not cls:
error(f"Couldn't determine protocol", status=404)
elif not common.is_enabled(cls, ActivityPub):
elif not common.is_enabled(cls, ActivityPub, handle_or_id=handle_or_id):
error(f'{cls.LABEL} <=> activitypub not enabled')
elif cls.LABEL == 'web' and request.path.startswith('/ap/'):
# we started out with web users' AP ids as fed.brid.gy/[domain], so we

Wyświetl plik

@ -43,7 +43,18 @@ logger = logging.getLogger(__name__)
arroba.server.storage = DatastoreStorage(ndb_client=ndb_client)
LEXICONS = Client('https://unused').defs
appview = Client(f'https://{os.environ["APPVIEW_HOST"]}',
headers={'User-Agent': USER_AGENT})
LEXICONS = appview.defs
# https://atproto.com/guides/applications#record-types
COLLECTION_TO_TYPE = {
'app.bsky.actor.profile': 'profile',
'app.bsky.feed.like': 'like',
'app.bsky.feed.post': 'post',
'app.bsky.feed.repost': 'repost',
'app.bsky.graph.follow': 'follow',
}
DNS_GCP_PROJECT = 'brid-gy'
DNS_ZONE = 'brid-gy'
@ -52,6 +63,22 @@ logger.info(f'Using GCP DNS project {DNS_GCP_PROJECT} zone {DNS_ZONE}')
dns_client = dns.Client(project=DNS_GCP_PROJECT)
def did_to_handle(did):
"""Resolves a DID to a handle _if_ we have the DID doc stored locally.
Args:
did (str)
Returns:
str: handle, or None
"""
if did_obj := ATProto.load(did, did_doc=True):
if aka := util.get_first(did_obj.raw, 'alsoKnownAs', ''):
handle, _, _ = parse_at_uri(aka)
if handle:
return handle
class ATProto(User, Protocol):
"""AT Protocol class.
@ -59,6 +86,7 @@ class ATProto(User, Protocol):
https://atproto.com/specs/did
"""
ABBREV = 'atproto'
# 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}/'
@ -82,11 +110,7 @@ class ATProto(User, Protocol):
@ndb.ComputedProperty
def handle(self):
"""Returns handle if the DID document includes one, otherwise None."""
if did_obj := ATProto.load(self.key.id(), did_doc=True):
if aka := util.get_first(did_obj.raw, 'alsoKnownAs', ''):
handle, _, _ = parse_at_uri(aka)
if handle:
return handle
return did_to_handle(self.key.id())
def web_url(self):
return bluesky.Bluesky.user_url(self.handle_or_id())
@ -243,7 +267,7 @@ class ATProto(User, Protocol):
initial_writes = None
if user.obj and user.obj.as1:
# create user profile
profile = cls.convert(user.obj, fetch_blobs=True)
profile = cls.convert(user.obj, fetch_blobs=True, from_user=user)
profile.setdefault('labels', {'$type': 'com.atproto.label.defs#selfLabels'})
profile['labels'].setdefault('values', []).append({
'val' : f'bridged-from-bridgy-fed-{user.LABEL}',
@ -302,7 +326,7 @@ class ATProto(User, Protocol):
# convert to Bluesky record; short circuits on error
try:
record = to_cls.convert(base_obj, fetch_blobs=True)
record = to_cls.convert(base_obj, fetch_blobs=True, from_user=from_user)
except ValueError as e:
logger.info(f'Skipping due to {e}')
return False
@ -354,8 +378,7 @@ class ATProto(User, Protocol):
assert copy_did == did
assert coll == type
logger.info(f'Storing ATProto {action} {type} {rkey}: ' +
json_dumps(dag_json.encode(record).decode(), indent=2))
logger.info(f'Storing ATProto {action} {type} {rkey}: {dag_json.encode(record).decode()}')
repo.apply_writes([Write(action=action, collection=type, rkey=rkey,
record=record)])
@ -431,10 +454,8 @@ class ATProto(User, Protocol):
assert repo.startswith('did:')
obj.key = ndb.Key(Object, id.replace(f'at://{handle}', f'at://{repo}'))
client = Client(f'https://{os.environ["APPVIEW_HOST"]}',
headers={'User-Agent': USER_AGENT})
try:
ret = client.com.atproto.repo.getRecord(
ret = appview.com.atproto.repo.getRecord(
repo=repo, collection=collection, rkey=rkey)
except RequestException as e:
util.interpret_http_exception(e)
@ -462,7 +483,8 @@ class ATProto(User, Protocol):
dict: JSON object
"""
from_proto = PROTOCOLS.get(obj.source_protocol)
if from_proto and not common.is_enabled(cls, from_proto):
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')
if obj.bsky:
@ -481,6 +503,11 @@ class ATProto(User, Protocol):
blobs[url] = blob.as_object()
ret = bluesky.from_as1(cls.translate_ids(obj.as1), blobs=blobs)
# TODO: uncomment this and pass through client eventually? would be
# nice to start reusing granary's resolving handles and CIDs, but we
# do much of that ourselves here in BF beforehand, so granary ends
# up duplicating those network requests
# client=appview)
# fill in CIDs from Objects
def populate_cid(strong_ref):
@ -521,6 +548,8 @@ def poll_notifications():
for cls in set(PROTOCOLS.values())
if cls and cls != ATProto))
# this client needs to be request-local because we set its service token
# below per user that we're polling
client = Client(f'https://{os.environ["APPVIEW_HOST"]}',
headers={'User-Agent': USER_AGENT})
@ -579,6 +608,8 @@ def poll_posts():
for cls in set(PROTOCOLS.values())
if cls and cls != ATProto))
# this client needs to be request-local because we set its service token
# below per user that we're polling
client = Client(f'https://{os.environ["APPVIEW_HOST"]}',
headers={'User-Agent': USER_AGENT})

Wyświetl plik

@ -83,6 +83,15 @@ util.set_user_agent(USER_AGENT)
TASKS_LOCATION = 'us-central1'
RUN_TASKS_INLINE = False # overridden by unit tests
USER_ALLOWLIST = (
'snarfed.org',
'did:plc:fdme4gb7mu7zrie7peay7tst',
'snarfed.bsky.social',
'did:plc:3ljmtyyjqcjee2kpewgsifvb',
'https://indieweb.social/users/snarfed',
'@snarfed@indieweb.social',
)
def base64_to_long(x):
"""Converts from URL safe base64 encoding to long integer.
@ -255,12 +264,13 @@ def add(seq, val):
seq.append(val)
def is_enabled(proto_a, proto_b):
def is_enabled(proto_a, proto_b, handle_or_id=None):
"""Returns True if bridging the two input protocols is enabled, False otherwise.
Args:
proto_a (Protocol subclass)
proto_b (Protocol subclass)
handle_or_id (str): optional user handle or id
Returns:
bool:
@ -273,6 +283,9 @@ def is_enabled(proto_a, proto_b):
if DEBUG and ('fake' in labels or 'other' in labels):
return True
if handle_or_id in USER_ALLOWLIST:
return True
return labels in ENABLED_BRIDGES

37
ids.py
Wyświetl plik

@ -8,6 +8,7 @@ from urllib.parse import urljoin, urlparse
from flask import request
from google.cloud.ndb.query import FilterNode, Query
from granary.bluesky import BSKY_APP_URL_RE, web_url_to_at_uri
from oauth_dropins.webutil import util
from common import subdomain_wrap, LOCAL_DOMAINS, PRIMARY_DOMAIN, SUPERDOMAIN
@ -24,7 +25,7 @@ _FED_SUBDOMAIN_SITES = None
def web_ap_base_domain(user_domain):
"""Returns the full Bridgy Fed domain to user for a given Web user.
"""Returns the full Bridgy Fed domain to use for a given Web user.
Specifically, returns ``http://localhost/` if we're running locally,
``https://fed.brid.gy/`` if the given Web user has ``ap_subdomain='fed'``,
@ -55,6 +56,8 @@ def web_ap_base_domain(user_domain):
def translate_user_id(*, id, from_proto, to_proto):
"""Translate a user id from one protocol to another.
TODO: unify with :func:`translate_object_id`.
Args:
id (str)
from_proto (protocol.Protocol)
@ -72,6 +75,20 @@ def translate_user_id(*, id, from_proto, to_proto):
# home page; replace with domain
id = parsed.netloc
# bsky.app profile URL to DID
if to_proto.LABEL == 'atproto':
if match := BSKY_APP_URL_RE.match(id):
repo = match.group('id')
if repo.startswith('did:'):
return repo
from atproto import ATProto
try:
return ATProto.handle_to_id(repo)
except (AssertionError, ValueError) as e:
logger.warning(e)
return None
if from_proto == to_proto:
return id
@ -161,6 +178,8 @@ def translate_handle(*, handle, from_proto, to_proto, enhanced):
def translate_object_id(*, id, from_proto, to_proto):
"""Translates a user handle from one protocol to another.
TODO: unify with :func:`translate_user_id`.
Args:
id (str)
from_proto (protocol.Protocol)
@ -172,6 +191,22 @@ def translate_object_id(*, id, from_proto, to_proto):
assert id and from_proto and to_proto
assert from_proto.owns_id(id) is not False or from_proto.LABEL == 'ui'
# bsky.app profile URL to DID
if to_proto.LABEL == 'atproto':
if match := BSKY_APP_URL_RE.match(id):
repo = match.group('id')
handle = None
if not repo.startswith('did:'):
handle = repo
from atproto import ATProto
try:
repo = ATProto.handle_to_id(repo)
except (AssertionError, ValueError) as e:
logger.warning(e)
return None
return web_url_to_at_uri(id, handle=handle, did=repo)
if from_proto == to_proto:
return id

137
models.py
Wyświetl plik

@ -15,7 +15,7 @@ from Crypto.PublicKey import RSA
from flask import g, request
from google.cloud import ndb
from granary import as1, as2, atom, bluesky, microformats2
from granary.bluesky import BSKY_APP_URL_RE
from granary.bluesky import AT_URI_PATTERN, BSKY_APP_URL_RE
from granary.source import html_to_text
from oauth_dropins.webutil import util
from oauth_dropins.webutil.appengine_info import DEBUG
@ -28,8 +28,8 @@ from oauth_dropins.webutil.models import (
from oauth_dropins.webutil.util import json_dumps, json_loads
import common
from common import add, base64_to_long, long_to_base64, unwrap
import ids
from common import add, base64_to_long, DOMAIN_RE, long_to_base64, unwrap
from ids import translate_handle, translate_object_id, translate_user_id
# maps string label to Protocol subclass. populated by ProtocolUserMeta.
# seed with old and upcoming protocols that don't have their own classes (yet).
@ -241,7 +241,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
ATProto = PROTOCOLS['atproto']
if propagate and cls.LABEL != 'atproto' and not user.get_copy(ATProto):
if common.is_enabled(cls, ATProto):
if common.is_enabled(cls, ATProto, handle_or_id=id):
ATProto.create_for(user)
else:
logger.info(f'{cls.LABEL} <=> atproto not enabled, skipping')
@ -359,8 +359,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if not handle:
return None
return ids.translate_handle(handle=handle, from_proto=self.__class__,
to_proto=to_proto, enhanced=False)
return translate_handle(handle=handle, from_proto=self.__class__,
to_proto=to_proto, enhanced=False)
def id_as(self, to_proto):
"""Returns this user's id in a different protocol.
@ -374,8 +374,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if isinstance(to_proto, str):
to_proto = PROTOCOLS[to_proto]
return ids.translate_user_id(id=self.key.id(), from_proto=self.__class__,
to_proto=to_proto)
return translate_user_id(id=self.key.id(), from_proto=self.__class__,
to_proto=to_proto)
def handle_or_id(self):
"""Returns handle if we know it, otherwise id."""
@ -606,7 +606,7 @@ class Object(StringIdModel):
def use_urls_as_ids(obj):
"""If id field is missing or not a URL, use the url field."""
id = obj.get('id')
if not id or not util.is_web(id):
if not id or not (util.is_web(id) or re.match(DOMAIN_RE, id)):
if url := util.get_url(obj):
obj['id'] = url
@ -922,6 +922,8 @@ class Object(StringIdModel):
``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``
* ATProto bsky.app URLs to their DIDs or `at://` URIs, eg
``https://bsky.app/profile/a.com`` => ``did:plc:123``
...in these AS1 fields, in place:
@ -937,6 +939,9 @@ 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`.
"""
if not self.as1:
return
@ -974,7 +979,7 @@ class Object(StringIdModel):
if copy.protocol in (self_proto.LABEL, self_proto.ABBREV):
origs[copy.uri] = obj.key.id()
logger.debug(f'Replacing copies with originals: {origs}')
logger.debug(f'Resolving {self_proto.LABEL} ids; originals: {origs}')
replaced = False
def replace(val):
@ -1002,12 +1007,87 @@ class Object(StringIdModel):
obj[field] = obj[field][0]
outer_obj['object'] = replace(inner_obj)
if util.trim_nulls(outer_obj['object']).keys() == {'id'}:
outer_obj['object'] = outer_obj['object']['id']
if replaced:
self.our_as1 = util.trim_nulls(outer_obj)
def normalize_ids(self):
"""Normalizes ids to their protocol's canonical representation, if any.
For example, normalizes ATProto ``https://bsky.app/...`` URLs to DIDs
for profiles, ``at://`` URIs for posts.
Modifies this object in place.
TODO: unify with :meth:`resolve_ids`, :meth:`Protocol.translate_ids`.
"""
from protocol import Protocol
if not self.as1:
return
logger.debug(f'Normalizing ids')
outer_obj = copy.deepcopy(self.as1)
inner_objs = as1.get_objects(outer_obj)
replaced = False
def replace(val, translate_fn):
nonlocal replaced
orig = val.get('id') if isinstance(val, dict) else val
if not orig:
return val
proto = Protocol.for_id(orig, remote=False)
if not proto:
return val
translated = translate_fn(id=orig, from_proto=proto, to_proto=proto)
if translated and translated != orig:
logger.info(f'Normalized {proto.LABEL} id {orig} to {translated}')
replaced = True
if isinstance(val, dict):
val['id'] = translated
return val
else:
return translated
return val
# actually replace ids
for obj in [outer_obj] + inner_objs:
for tag in as1.get_objects(obj, 'tags'):
if tag.get('objectType') == 'mention':
tag['url'] = replace(tag.get('url'), translate_user_id)
for field in ['actor', 'author', 'inReplyTo']:
fn = translate_object_id if field == 'inReplyTo' else translate_user_id
obj[field] = [replace(val, fn) for val in util.get_list(obj, field)]
if len(obj[field]) == 1:
obj[field] = obj[field][0]
outer_obj['object'] = []
for inner_obj in inner_objs:
translate_fn = (translate_user_id
if (as1.object_type(inner_obj) in as1.ACTOR_TYPES
or as1.object_type(outer_obj) in
('follow', 'stop-following'))
else translate_object_id)
got = replace(inner_obj, translate_fn)
if isinstance(got, dict) and util.trim_nulls(got).keys() == {'id'}:
got = got['id']
outer_obj['object'].append(got)
if len(outer_obj['object']) == 1:
outer_obj['object'] = outer_obj['object'][0]
if replaced:
self.our_as1 = util.trim_nulls(outer_obj)
class Follower(ndb.Model):
"""A follower of a Bridgy Fed user."""
@ -1192,23 +1272,32 @@ def fetch_objects(query, by=None, user=None):
'url': id,
})
elif url:
# heuristics for sniffing Mastodon and similar fediverse URLs and
# converting them to more friendly @-names
# heuristics for sniffing URLs and converting them to more friendly
# phrases and user handles.
# TODO: standardize this into granary.as2 somewhere?
if not content:
fedi_url = re.match(
r'https://[^/]+/(@|users/)([^/@]+)(@[^/@]+)?(/(?:statuses/)?[0-9]+)?', url)
if fedi_url:
content = '@' + fedi_url.group(2)
if fedi_url.group(4):
content += "'s post"
from activitypub import FEDI_URL_RE
from atproto import COLLECTION_TO_TYPE, did_to_handle
if not content:
if bsky_url := BSKY_APP_URL_RE.match(url):
if handle := bsky_url.group('id'): # or DID
content = '@' + handle
if bsky_url.group('tid'):
content += "'s post"
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)
content = common.pretty_link(url, text=content, user=user)

Wyświetl plik

@ -60,17 +60,16 @@ def load_user(protocol, id):
:class:`werkzeug.exceptions.HTTPException` on error or redirect
"""
assert id
if protocol == 'ap' and not id.startswith('@'):
id = '@' + id
cls = PROTOCOLS[protocol]
if cls.ABBREV == 'ap' and not id.startswith('@'):
id = '@' + id
user = cls.get_by_id(id)
if protocol != 'web':
if cls.ABBREV != 'web':
if not user:
user = cls.query(OR(cls.handle == id,
cls.readable_id == id),
).get()
user = cls.query(OR(cls.handle == id, cls.handle == id)).get()
if user and user.use_instead:
user = user.use_instead.get()
@ -80,7 +79,7 @@ def load_user(protocol, id):
elif user and id != user.key.id(): # use_instead redirect
error('', status=302, location=user.user_page_path())
if user and (user.direct or protocol == 'web'):
if user and (user.direct or cls.ABBREV != 'ap'):
assert not user.use_instead
return user

Wyświetl plik

@ -226,19 +226,18 @@ class Protocol:
@cached(LRUCache(20000), lock=Lock())
@staticmethod
def for_id(id):
def for_id(id, remote=True):
"""Returns the protocol for a given id.
May incur expensive side effects like fetching the id itself over the
network or other discovery.
Args:
id (str)
remote (bool): whether to perform expensive side effects like fetching
the id itself over the network, or other discovery.
Returns:
Protocol subclass: matching protocol, or None if no known protocol
owns this id
"""
Protocol subclass: matching protocol, or None if no single known
protocol definitively owns this id
"""
logger.info(f'Determining protocol for id {id}')
if not id:
return None
@ -273,7 +272,10 @@ class Protocol:
logger.info(f' {obj.key} owned by source_protocol {obj.source_protocol}')
return PROTOCOLS[obj.source_protocol]
# step 4: fetch over the network
# step 4: fetch over the network, if necessary
if not remote:
return None
for protocol in candidates:
logger.info(f'Trying {protocol.LABEL}')
try:
@ -474,7 +476,7 @@ class Protocol:
@classmethod
def translate_ids(to_cls, obj):
"""Wraps ids and actors in an AS1 object in subdomain convert URLs.
"""Translates all ids in an AS1 object to a specific protocol.
Infers source protocol for each id value separately.
@ -497,6 +499,9 @@ class Protocol:
This is the inverse of :meth:`models.Object.resolve_ids`. Much of the
same logic is duplicated there!
TODO: unify with :meth:`Object.resolve_ids`,
:meth:`protocol.Protocol.normalize_ids`.
Args:
to_proto (Protocol subclass)
obj (dict): AS1 object or activity (not :class:`models.Object`!)
@ -792,6 +797,7 @@ class Protocol:
from_user = from_cls.get_or_create(id=from_key.id(), obj=from_obj)
logger.info(f'Follow {from_id} => {to_id}')
to_cls = Protocol.for_id(to_id)
if not to_cls:
error(f"Couldn't determine protocol for {to_id}")
@ -1172,6 +1178,7 @@ class Protocol:
return None
obj.resolve_ids()
obj.normalize_ids()
if obj.new is False:
obj.changed = obj.activity_changed(orig_as1)

Wyświetl plik

@ -15,7 +15,7 @@ beautifulsoup4==4.12.3
bech32==1.2.0
brevity==0.2.17
cachetools==5.3.0
cbor2==5.6.2
cbor2==5.6.3
certifi==2024.2.2
cffi==1.16.0
charset-normalizer==3.3.2
@ -26,12 +26,12 @@ dag-cbor==0.3.3
Deprecated==1.2.14
dnspython==2.6.1
domain2idna==1.12.0
ecdsa==0.18.0
ecdsa==0.19.0
extras==1.0.0
feedgen==1.0.0
feedparser==6.0.11
fixtures==4.1.0
Flask==3.0.2
Flask==3.0.3
Flask-Caching==2.1.0
flask-gae-static==1.0
flask-sock==0.7.0
@ -56,7 +56,7 @@ html2text==2024.2.26
html5lib==1.1
humanfriendly==10.0
humanize==4.9.0
idna==3.6
idna==3.7
iterators==0.2.0
itsdangerous==2.1.2
Jinja2==3.1.3
@ -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

@ -9,7 +9,6 @@ from arroba.datastore_storage import AtpBlock, AtpRemoteBlob, AtpRepo, Datastore
from arroba.did import encode_did_key
from arroba.repo import Repo
import arroba.util
import dns.resolver
from dns.resolver import NXDOMAIN
from flask import g
from google.cloud.tasks_v2.types import Task
@ -137,7 +136,7 @@ class ATProtoTest(TestCase):
self.make_user('did:plc:user', cls=ATProto)
self.assertEqual('did:plc:user', ATProto.handle_to_id('han.dull'))
@patch('dns.resolver.resolve', side_effect=dns.resolver.NXDOMAIN())
@patch('dns.resolver.resolve', side_effect=NXDOMAIN())
# resolving handle, HTTPS method, not found
@patch('requests.get', return_value=requests_response('', status=404))
def test_handle_to_id_not_found(self, *_):
@ -192,7 +191,7 @@ class ATProtoTest(TestCase):
id='https://bsky.app/profile/did:plc:user/post/123'))
self.assertEqual('https://some.pds', got)
@patch('dns.resolver.resolve', side_effect=dns.resolver.NXDOMAIN())
@patch('dns.resolver.resolve', side_effect=NXDOMAIN())
@patch('requests.get', side_effect=[
# resolving handle, HTTPS method
requests_response('did:plc:user', content_type='text/plain'),
@ -297,7 +296,7 @@ class ATProtoTest(TestCase):
with self.assertRaises(AssertionError):
ATProto.fetch(Object(id=uri))
@patch('dns.resolver.resolve', side_effect=dns.resolver.NXDOMAIN())
@patch('dns.resolver.resolve', side_effect=NXDOMAIN())
@patch('requests.get', return_value=requests_response(status=404))
def test_fetch_resolve_handle_fails(self, mock_get, _):
obj = Object(id='at://bad.com/app.bsky.feed.post/789')
@ -313,7 +312,7 @@ class ATProtoTest(TestCase):
bsky=ACTOR_PROFILE_BSKY)
self.assert_entities_equal(profile, ATProto.load('did:plc:user'))
@patch('dns.resolver.resolve', side_effect=dns.resolver.NXDOMAIN())
@patch('dns.resolver.resolve', side_effect=NXDOMAIN())
@patch('requests.get', side_effect=[
# resolving handle, HTTPS method
requests_response('did:plc:user', content_type='text/plain'),
@ -430,13 +429,13 @@ class ATProtoTest(TestCase):
'inReplyTo': 'at://did:plc:bob/app.bsky.feed.post/tid',
})))
@patch('dns.resolver.resolve', side_effect=dns.resolver.NXDOMAIN())
@patch('dns.resolver.resolve', side_effect=NXDOMAIN())
@patch('requests.get', side_effect=[
# resolving handle, HTTPS method
requests_response('did:plc:user', content_type='text/plain'),
# AppView getRecord
requests_response({
'uri': 'at://did:plc:bob/app.bsky.feed.post/tid',
'uri': 'at://did:plc:user/app.bsky.feed.post/tid',
'cid': 'my sidd',
'value': {
'$type': 'app.bsky.feed.post',
@ -464,7 +463,7 @@ class ATProtoTest(TestCase):
'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)
@patch('dns.resolver.resolve', side_effect=dns.resolver.NXDOMAIN())
@patch('dns.resolver.resolve', side_effect=NXDOMAIN())
# resolving handle, HTTPS method
@patch('requests.get', return_value=requests_response(status=404))
def test_convert_populate_cid_fetch_remote_record_bad_handle(self, _, __):
@ -536,6 +535,39 @@ class ATProtoTest(TestCase):
'image': [{'url': 'http://my/pic'}],
}), fetch_blobs=True))
# resolveHandle
@patch('requests.get', return_value=requests_response({'did': 'did:plc:user'}))
def test_convert_resolve_mention_handle(self, mock_get):
self.store_object(id='did:plc:user', raw=DID_DOC)
self.assertEqual({
'$type': 'app.bsky.feed.post',
'createdAt': '2022-01-02T03:04:05.000Z',
'text': 'hi @han.dull hows it going',
'facets': [{
'$type': 'app.bsky.richtext.facet',
'features': [{
'$type': 'app.bsky.richtext.facet#mention',
'did': 'did:plc:user',
}],
'index': {
'byteEnd': 12,
'byteStart': 3,
},
}],
}, ATProto.convert(Object(our_as1={
# this mention has the DID in url, and it will also be extracted
# from the link in content. make sure we merge the two and don't end
# up with a duplicate mention of the DID or a mention of the handle.
'objectType': 'note',
'content': 'hi <a href="https://bsky.app/profile/han.dull">@han.dull</a> hows it going',
'tags': [{
'objectType': 'mention',
'url': 'did:plc:user',
'displayName': '@han.dull'
}],
})))
def test_convert_protocols_not_enabled(self):
obj = Object(our_as1={'foo': 'bar'}, source_protocol='activitypub')
with self.assertRaises(BadRequest):
@ -1024,13 +1056,6 @@ class ATProtoTest(TestCase):
'cid': 'my sidd',
},
},
'facets': [{
'$type': 'app.bsky.richtext.facet',
'features': [{
'$type': 'app.bsky.richtext.facet#mention',
'did': 'did:alice',
}],
}],
}, record)
at_uri = f'at://did:plc:user/app.bsky.feed.post/{last_tid}'

Wyświetl plik

@ -109,3 +109,10 @@ class CommonTest(TestCase):
self.assertTrue(common.is_enabled(ATProto, Web))
self.assertTrue(common.is_enabled(Fake, OtherFake))
self.assertFalse(common.is_enabled(ATProto, ActivityPub))
self.assertFalse(common.is_enabled(
ATProto, ActivityPub, handle_or_id='unknown'))
self.assertTrue(common.is_enabled(
ATProto, ActivityPub, handle_or_id='snarfed.org'))
self.assertTrue(common.is_enabled(
ATProto, ActivityPub, handle_or_id='did:plc:fdme4gb7mu7zrie7peay7tst'))

Wyświetl plik

@ -19,11 +19,21 @@ class IdsTest(TestCase):
Fake(id='fake:user',
copies=[Target(uri='did:plc:789', protocol='atproto')]).put()
# DID doc and ATProto, used to resolve handle in bsky.app URL
did = self.store_object(id='did:plc:123', raw={
'id': 'did:plc:123',
'alsoKnownAs': ['at://user.com'],
})
ATProto(id='did:plc:123', obj_key=did.key).put()
for from_, id, to, expected in [
(ActivityPub, 'https://inst/user', ActivityPub, 'https://inst/user'),
(ActivityPub, 'https://inst/user', ATProto, 'did:plc:456'),
(ActivityPub, 'https://inst/user', Fake, 'fake:u:https://inst/user'),
(ActivityPub, 'https://inst/user', Web, 'https://inst/user'),
(ActivityPub, 'https://bsky.app/profile/user.com', ATProto, 'did:plc:123'),
(ActivityPub, 'https://bsky.app/profile/did:plc:123',
ATProto, 'did:plc:123'),
(ATProto, 'did:plc:456', ATProto, 'did:plc:456'),
# copies
(ATProto, 'did:plc:123', Web, 'user.com'),
@ -33,6 +43,8 @@ class IdsTest(TestCase):
(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', 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'),
(Fake, 'fake:user', ActivityPub, 'https://fa.brid.gy/ap/fake:user'),
(Fake, 'fake:user', ATProto, 'did:plc:789'),
(Fake, 'fake:user', Fake, 'fake:user'),
@ -41,6 +53,8 @@ class IdsTest(TestCase):
(Web, 'https://user.com/', ActivityPub, 'http://localhost/user.com'),
(Web, 'user.com', ATProto, 'did:plc:123'),
(Web, 'https://user.com', ATProto, 'did:plc:123'),
(Web, 'https://bsky.app/profile/user.com', ATProto, 'did:plc:123'),
(Web, 'https://bsky.app/profile/did:plc:123', ATProto, 'did:plc:123'),
(Web, 'user.com', Fake, 'fake:u:user.com'),
(Web, 'user.com', Web, 'user.com'),
(Web, 'https://user.com/', Web, 'user.com'),
@ -132,6 +146,13 @@ class IdsTest(TestCase):
self.store_object(id='fake:post',
copies=[Target(uri='at://did/fa/post', protocol='atproto')])
# DID doc and ATProto, used to resolve handle in bsky.app URL
did = self.store_object(id='did:plc:123', raw={
'id': 'did:plc:123',
'alsoKnownAs': ['at://user.com'],
})
ATProto(id='did:plc:123', obj_key=did.key).put()
for from_, id, to, expected in [
(ActivityPub, 'https://inst/post', ActivityPub, 'https://inst/post'),
(ActivityPub, 'https://inst/post', ATProto, 'at://did/ap/post'),
@ -147,6 +168,10 @@ class IdsTest(TestCase):
(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, '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',
ATProto, 'at://did:plc:123/app.bsky.feed.post/456'),
(Fake, 'fake:post',
ActivityPub, 'https://fa.brid.gy/convert/ap/fake:post'),
(Fake, 'fake:post', ATProto, 'at://did/fa/post'),

Wyświetl plik

@ -9,9 +9,10 @@ from oauth_dropins.webutil.testutil import requests_response
from activitypub import ActivityPub
import app
from atproto import ATProto
from dns.resolver import NXDOMAIN
from granary.tests.test_bluesky import ACTOR_PROFILE_BSKY
import hub
from models import Target
from models import Object, Target
from web import Web
from .testutil import ATPROTO_KEY, TestCase
@ -23,6 +24,11 @@ DID_DOC = {
'id': 'did:plc:alice',
'alsoKnownAs': ['at://alice.com'],
}
PROFILE_GETRECORD = {
'uri': 'at://did:plc:alice/app.bsky.actor.profile/self',
'cid': 'alice sidd',
'value': test_atproto.ACTOR_PROFILE_BSKY,
}
class IntegrationTests(TestCase):
@ -92,11 +98,7 @@ class IntegrationTests(TestCase):
}],
}),
# ATProto getRecord of alice's profile
requests_response({
'uri': 'at://did:plc:alice/app.bsky.actor.profile/self',
'cid': 'alice sidd',
'value': test_atproto.ACTOR_PROFILE_BSKY,
}),
requests_response(PROFILE_GETRECORD),
]
resp = self.post('/queue/atproto-poll-notifs', client=hub.app.test_client())
@ -164,11 +166,7 @@ class IntegrationTests(TestCase):
}],
}),
# ATProto getRecord of alice's profile
requests_response({
'uri': 'at://did:plc:alice/app.bsky.actor.profile/self',
'cid': 'alice sidd',
'value': test_atproto.ACTOR_PROFILE_BSKY,
}),
requests_response(PROFILE_GETRECORD),
# webmention discovery
test_web.WEBMENTION_REL_LINK,
]
@ -181,3 +179,94 @@ class IntegrationTests(TestCase):
'source': 'https://atproto.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=[
requests_response('OK'), # create DID
])
@patch('requests.get', side_effect = [
# webmention source page, follow HTML
requests_response("""\
<html>
<body class="h-entry">
<a class="u-url" href="https://bob.com/follow"></a>
<a class="u-follow-of" href="https://bsky.app/profile/alice.com"></a>
<a href="http://localhost/"></a>
</body>
</html>
"""),
# https://bob.com/ , for authorship
requests_response("""\
<html>
<body class="h-card">
<a class="p-name u-url" rel="me" href="https://bob.com/">Bob</a>
</body>
</html>
"""),
# alice.com handle resolution, HTTPS method
requests_response('did:plc:alice', content_type='text/plain'),
# alice profile
requests_response(PROFILE_GETRECORD),
# alice DID
requests_response(DID_DOC),
# alice profile
requests_response(PROFILE_GETRECORD),
])
def test_web_follow_to_atproto(self, mock_get, mock_post, _, __):
"""Incoming webmention for a web follow of an ATProto bsky.app profile URL.
Web user bob.com
ATProto user alice.com (did:plc:alice)
Follow is HTML with mf2 u-follow-of of https://bsky.app/profile/alice.com
"""
bob = self.make_user(id='bob.com', cls=Web, obj_mf2={
'type': ['h-card'],
'properties': {
'url': ['https://bob.com/'],
'name': ['Bob'],
},
})
# send webmention
resp = self.post('/webmention', data={
'source': 'https://bob.com/follow',
'target': 'http://localhost/',
})
self.assertEqual(202, resp.status_code)
# check results
bob = bob.key.get()
self.assertEqual(1, len(bob.copies))
self.assertEqual('atproto', bob.copies[0].protocol)
bob_did = bob.copies[0].uri
self.assertEqual({
'type': ['h-entry'],
'properties': {
'url': ['https://bob.com/follow'],
'follow-of': ['https://bsky.app/profile/alice.com'],
'name': [''],
'author': [{
'type': ['h-card'],
'properties': {
'name': ['Bob'],
'url': ['https://bob.com/'],
},
}],
},
}, Object.get_by_id('https://bob.com/follow').mf2)
storage = DatastoreStorage()
repo = storage.load_repo('bob.com.web.brid.gy')
self.assertEqual(bob_did, repo.did)
records = repo.get_contents()
self.assertEqual(['app.bsky.actor.profile', 'app.bsky.graph.follow'],
list(records.keys()))
self.assertEqual(['self'], list(records['app.bsky.actor.profile'].keys()))
self.assertEqual([{
'$type': 'app.bsky.graph.follow',
'subject': 'did:plc:alice',
'createdAt': '2022-01-02T03:04:05.000Z',
}], list(records['app.bsky.graph.follow'].values()))

Wyświetl plik

@ -681,6 +681,14 @@ class ObjectTest(TestCase):
self.assertNotIn('id', obj.as1['actor'])
self.assertEqual(['c', 'd'], obj.as1['object'])
obj = Object(mf2={
'properties': {
'uid': ['z.com'],
'url': ['x'],
},
})
self.assertEqual('z.com', obj.as1['id'])
def test_validate_id(self):
# DID repo ids
Object(id='at://did:plc:123/app.bsky.feed.post/abc').put()
@ -877,6 +885,65 @@ class ObjectTest(TestCase):
},
}, obj.our_as1)
def test_normalize_ids_empty(self):
obj = Object()
obj.normalize_ids()
self.assertIsNone(obj.as1)
def test_normalize_ids_follow_atproto(self):
# for ATProto handle resolution
self.store_object(id='did:plc:user', raw=DID_DOC)
alice = self.make_user(id='did:plc:user', cls=ATProto)
obj = Object(our_as1={
'objectType': 'activity',
'verb': 'follow',
'actor': 'https://bsky.app/profile/did:plc:123',
'object': 'https://bsky.app/profile/han.dull',
})
obj.normalize_ids()
self.assert_equals({
'objectType': 'activity',
'verb': 'follow',
'actor': 'did:plc:123',
'object': 'did:plc:user',
}, obj.our_as1)
def test_normalize_ids_reply(self):
# for ATProto handle resolution
self.store_object(id='did:plc:user', raw=DID_DOC)
self.make_user(id='did:plc:user', cls=ATProto)
obj = Object(our_as1={
'objectType': 'activity',
'verb': 'post',
'object': {
'id': 'https://bsky.app/profile/han.dull/post/456',
'objectType': 'note',
'inReplyTo': 'https://bsky.app/profile/did:plc:123/post/789',
'author': 'https://bsky.app/profile/han.dull',
'tags': [{
'objectType': 'mention',
'url': 'https://bsky.app/profile/did:plc:123',
}],
},
})
obj.normalize_ids()
self.assert_equals({
'objectType': 'activity',
'verb': 'post',
'object': {
'id': 'at://did:plc:user/app.bsky.feed.post/456',
'objectType': 'note',
'inReplyTo': 'at://did:plc:123/app.bsky.feed.post/789',
'author': 'did:plc:user',
'tags': [{
'objectType': 'mention',
'url': 'did:plc:123',
}],
},
}, obj.our_as1)
def test_get_originals(self):
self.assertEqual([], models.get_originals(['foo', 'did:plc:bar']))

Wyświetl plik

@ -99,11 +99,12 @@ class PagesTest(TestCase):
self.assert_equals(404, got.status_code)
def test_user_not_direct(self):
fake = self.make_user('fake:foo', cls=Fake)
fake.direct = False
fake.put()
fake = self.make_user('fake:foo', cls=Fake, direct=False)
got = self.client.get('/fake/fake:foo')
self.assert_equals(200, got.status_code)
fake = self.make_user('http://fo/o', cls=ActivityPub, direct=False)
got = self.client.get('/ap/@o@fo')
self.assert_equals(404, got.status_code)
def test_user_opted_out(self):

Wyświetl plik

@ -93,7 +93,8 @@ class ProtocolTest(TestCase):
('https://ap.brid.gy/foo/bar', ActivityPub),
('https://web.brid.gy/foo/bar', Web),
]:
self.assertEqual(expected, Protocol.for_id(id))
self.assertEqual(expected, Protocol.for_id(id, remote=False))
self.assertEqual(expected, Protocol.for_id(id, remote=True))
def test_for_id_true_overrides_none(self):
class Greedy(Protocol, User):
@ -137,6 +138,11 @@ class ProtocolTest(TestCase):
self.assertIsNone(Protocol.for_id('http://web.site/'))
self.assertIn(self.req('http://web.site/'), mock_get.mock_calls)
@patch('requests.get')
def test_for_id_web_remote_false(self, mock_get):
self.assertIsNone(Protocol.for_id('http://web.site/', remote=False))
mock_get.assert_not_called()
def test_for_handle_deterministic(self):
for handle, expected in [
(None, (None, None)),

Wyświetl plik

@ -947,6 +947,7 @@ class WebTest(TestCase):
type='share',
object_ids=['https://mas.to/toot/id'],
labels=['user', 'activity', 'notification', 'feed'],
ignore=['our_as1'],
)
def test_link_rel_alternate_as2(self, mock_get, mock_post):
@ -1005,6 +1006,7 @@ class WebTest(TestCase):
type='like',
labels=['activity', 'user'],
status='ignored',
ignore=['our_as1'],
)
def test_post_type_discovery_multiple_types(self, mock_get, mock_post):
@ -1124,6 +1126,7 @@ class WebTest(TestCase):
notify=[ndb.Key('ActivityPub', 'https://mas.to/author')],
delivered=['https://mas.to/inbox'],
status='complete',
ignore=['our_as1'],
)
def test_create_non_domain_author(self, mock_get, mock_post):
@ -1316,16 +1319,19 @@ class WebTest(TestCase):
self.assert_deliveries(mock_post, ['https://mas.to/inbox'], FOLLOW_AS2)
follow_as1 = copy.deepcopy(FOLLOW_AS1)
follow_as1['actor']['id'] = 'https://user.com/'
obj = self.assert_object('https://user.com/follow',
users=[self.user.key],
notify=[self.mrs_foo],
source_protocol='web',
status='complete',
mf2=FOLLOW_MF2,
our_as1=follow_as1,
delivered=['https://mas.to/inbox'],
type='follow',
object_ids=['https://mas.to/mrs-foo'],
labels=['user', 'activity', 'notification'],
ignore=['our_as1'],
)
to = self.assert_user(ActivityPub, 'https://mas.to/mrs-foo', obj_as2={
@ -1412,7 +1418,8 @@ class WebTest(TestCase):
delivered=['https://mas.to/inbox'],
type='follow',
object_ids=['https://mas.to/mrs-foo'],
labels=['user', 'activity', 'notification',],
labels=['user', 'activity', 'notification'],
ignore=['our_as1'],
)
followers = Follower.query().fetch()
@ -1476,7 +1483,8 @@ class WebTest(TestCase):
object_ids=['https://mas.to/mrs-foo',
'https://mas.to/mr-biff'],
labels=['user', 'activity', 'notification',],
)
ignore=['our_as1'],
)
followers = Follower.query().fetch()
self.assertEqual(2, len(followers))
@ -1590,6 +1598,7 @@ class WebTest(TestCase):
type='follow',
object_ids=['https://mas.to/mrs-foo'],
labels=['user', 'activity', 'notification',],
ignore=['our_as1'],
)
def test_repost_twitter_blocklisted(self, *mocks):

Wyświetl plik

@ -11,6 +11,7 @@ from unittest.mock import ANY, call
from urllib.parse import urlencode
import warnings
from arroba import did
import arroba.util
from arroba.util import datetime_to_tid
from bs4 import MarkupResemblesLocatorWarning
@ -197,6 +198,9 @@ class TestCase(unittest.TestCase, testutil.Asserts):
protocol.Protocol.for_id.cache.clear()
common.webmention_discover.cache.clear()
User.count_followers.cache.clear()
did.resolve_handle.cache.clear()
did.resolve_plc.cache.clear()
did.resolve_web.cache.clear()
for cls in Fake, OtherFake:
cls.fetchable = {}
@ -299,8 +303,8 @@ class TestCase(unittest.TestCase, testutil.Asserts):
mf2=obj_mf2, source_protocol=cls.LABEL
).key
kwargs.setdefault('direct', True)
user = cls(id=id,
direct=True,
mod=global_user.mod,
public_exponent=global_user.public_exponent,
private_exponent=global_user.private_exponent,

3
web.py
Wyświetl plik

@ -537,7 +537,8 @@ class Web(User, Protocol):
obj_as1 = obj.as1
from_proto = PROTOCOLS.get(obj.source_protocol)
if from_proto:
if not common.is_enabled(cls, from_proto):
user_id = from_user.key.id() if from_user and from_user.key else None
if not common.is_enabled(cls, from_proto, handle_or_id=user_id):
error(f'{cls.LABEL} <=> {from_proto.LABEL} not enabled')
# fill in author/actor if available