lots more on merging Protocol/Web receive and deliver

for #529
merge-web-protocol-receive
Ryan Barrett 2023-06-28 22:46:53 -07:00
rodzic cd4e2aa490
commit f10eeca219
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
4 zmienionych plików z 248 dodań i 322 usunięć

Wyświetl plik

@ -21,7 +21,6 @@ SUPPORTED_TYPES = (
'article',
'audio',
'comment',
'create',
'delete',
'follow',
'image',
@ -360,7 +359,7 @@ class Protocol:
inner_obj_as1 = as1.get_object(obj.as1)
inner_obj_id = inner_obj_as1.get('id')
inner_obj = None
if (obj.type in ('post', 'create', 'update')
if (obj.type in ('post', 'update')
and inner_obj_as1.keys() > set(['id'])):
inner_obj = Object.get_or_insert(inner_obj_id)
inner_obj.populate(our_as1=inner_obj_as1,
@ -448,7 +447,7 @@ class Protocol:
# deliver original posts and reposts to followers
is_reply = (obj.type == 'comment' or
(inner_obj_as1 and inner_obj_as1.get('inReplyTo')))
if ((obj.type == 'share' or obj.type in ('create', 'post') and not is_reply)
if ((obj.type == 'share' or obj.type in ('post', 'update') and not is_reply)
and actor_id):
logger.info(f'Delivering to followers of {actor_id}')
for f in Follower.query(Follower.to == from_cls.key_for(actor_id),
@ -542,7 +541,7 @@ class Protocol:
inner_obj = as1.get_object(obj.as1)
obj_url = util.get_url(inner_obj) or inner_obj.get('id')
if not source or obj.type in ('create', 'post', 'update'):
if not source or obj.type in ('post', 'update'):
source = obj_url
if not source:
error("Couldn't find source post URL")

Wyświetl plik

@ -42,13 +42,6 @@ class ProtocolTest(TestCase):
PROTOCOLS.pop('greedy', None)
super().tearDown()
@staticmethod
def store_object(**kwargs):
obj = Object(**kwargs)
obj.put()
del protocol.objects_cache[obj.key.id()]
return obj
def test_protocols_global(self):
self.assertEqual(Fake, PROTOCOLS['fake'])
self.assertEqual(Web, PROTOCOLS['web'])
@ -301,195 +294,185 @@ class ProtocolReceiveTest(TestCase):
if 'our_as1' in props and field not in props:
ignore.append(field)
return super().assert_object(id, delivered_protocol='fake',
return super().assert_object(id, source_protocol='fake',
delivered_protocol='fake',
ignore=ignore, **props)
def make_followers(self):
from_ = self.make_user(id, cls=ActivityPub, obj_as2=actor)
Follower.get_or_create(to=g.user, from_=from_, **kwargs)
Follower.get_or_create(to=g.user, from_=self.alice)
Follower.get_or_create(to=g.user, from_=self.bob)
Follower.get_or_create(to=g.user, from_=Fake(id='fake:eve'),
status='inactive')
for id, kwargs, actor in [
('fake:a', {}, None),
('fake:b', {}, None),
('https://mastodon/bbb', {}, {
'publicInbox': 'https://public/inbox',
'inbox': 'https://unused',
}),
('https://mastodon/ccc', {}, {
'endpoints': {
'sharedInbox': 'https://shared/inbox',
},
}),
('https://mastodon/ddd', {}, {
'inbox': 'https://inbox',
}),
('https://mastodon/ggg', {'status': 'inactive'}, {
'inbox': 'https://unused/2',
}),
('https://mastodon/hhh', {}, {
# dupe of ddd; should be de-duped
'inbox': 'https://inbox',
}),
]:
from_ = self.make_user(id, cls=ActivityPub, obj_as2=actor)
Follower.get_or_create(to=g.user, from_=from_, **kwargs)
def test_create_post(self):
self.make_followers()
# def test_create_post(self):
# mock_get.side_effect = [NOTE, ACTOR]
# mock_post.return_value = requests_response('abc xyz')
# self.make_followers()
post_as1 = {
'id': 'fake:post',
'objectType': 'note',
}
create_as1 = {
'id': 'fake:create',
'objectType': 'activity',
'verb': 'create',
'actor': 'fake:user',
'object': post_as1,
}
self.assertEqual('OK', Fake.receive('fake:create', our_as1=create_as1))
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/post',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(200, got.status_code)
self.assert_object('fake:post',
our_as1=post_as1,
type='note',
)
obj = self.assert_object('fake:create',
status='complete',
our_as1=create_as1,
delivered=['shared:target'],
type='create',
labels=['user', 'activity', 'feed'],
users=[g.user.key, self.alice.key, self.bob.key],
)
# mock_get.assert_has_calls((
# self.req('https://user.com/post'),
# ))
# inboxes = ('https://inbox', 'https://public/inbox', 'https://shared/inbox')
# self.assert_deliveries(mock_post, inboxes, CREATE_AS2)
self.assertEqual([(obj, 'shared:target')], Fake.sent)
# self.assert_object('https://user.com/post',
# users=[g.user.key],
# mf2=NOTE_MF2,
# type='note',
# source_protocol='web',
# )
# self.assert_object('https://user.com/post#bridgy-fed-create',
# users=[g.user.key],
# source_protocol='web',
# status='complete',
# mf2=NOTE_MF2,
# our_as1=CREATE_AS1,
# delivered=inboxes,
# type='post',
# labels=['user', 'activity'],
# )
def test_create_post_bare_object(self):
self.make_followers()
# def test_update_post(self):
# mock_get.side_effect = [NOTE, ACTOR]
# mock_post.return_value = requests_response('abc xyz')
post_as1 = {
'id': 'fake:post',
'objectType': 'note',
'author': 'fake:user',
}
self.assertEqual('OK', Fake.receive('fake:post', our_as1=post_as1))
# mf2 = copy.deepcopy(NOTE_MF2)
# mf2['properties']['content'] = 'different'
# Object(id='https://user.com/post', users=[g.user.key], mf2=mf2).put()
self.assert_object('fake:post',
our_as1=post_as1,
type='note',
users=[g.user.key],
)
# self.make_followers()
obj = self.assert_object('fake:post#bridgy-fed-create',
status='complete',
our_as1={
'objectType': 'activity',
'verb': 'post',
'id': 'fake:post#bridgy-fed-create',
'actor': 'http://bf/fake/fake:user/ap',
'object': post_as1,
'published': '2022-01-02T03:04:05+00:00',
},
delivered=['shared:target'],
type='post',
labels=['user', 'activity', 'feed'],
users=[g.user.key, self.alice.key, self.bob.key],
)
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/post',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(200, got.status_code)
self.assertEqual([(obj, 'shared:target')], Fake.sent)
# mock_get.assert_has_calls((
# self.req('https://user.com/post'),
# ))
# inboxes = ('https://inbox', 'https://public/inbox', 'https://shared/inbox')
# self.assert_deliveries(mock_post, inboxes, UPDATE_AS2)
def test_update_post(self):
self.make_followers()
# update_as1 = {
# 'objectType': 'activity',
# 'verb': 'update',
# 'id': 'https://user.com/post#bridgy-fed-update-2022-01-02T03:04:05+00:00',
# 'actor': 'http://localhost/user.com',
# 'object': {
# **NOTE_AS1,
# 'updated': '2022-01-02T03:04:05+00:00',
# },
# }
# self.assert_object(
# f'https://user.com/post#bridgy-fed-update-2022-01-02T03:04:05+00:00',
# users=[g.user.key],
# source_protocol='web',
# status='complete',
# mf2=NOTE_MF2,
# our_as1=update_as1,
# delivered=inboxes,
# type='update',
# labels=['user', 'activity'],
# )
post_as1 = {
'id': 'fake:post',
'objectType': 'note',
}
self.store_object(id='fake:post', our_as1=post_as1)
# def test_update_skip_if_content_unchanged(self):
# """https://github.com/snarfed/bridgy-fed/issues/78"""
# Object(id='https://user.com/reply', mf2=REPLY_MF2).put()
update_as1 = {
'id': 'fake:update',
'objectType': 'activity',
'verb': 'update',
'actor': 'fake:user',
'object': post_as1,
}
self.assertEqual('OK', Fake.receive('fake:update', our_as1=update_as1))
# mock_get.side_effect = ACTIVITYPUB_GETS
self.assert_object('fake:post',
our_as1=post_as1,
type='note',
)
obj = self.assert_object('fake:update',
status='complete',
our_as1=update_as1,
delivered=['shared:target'],
type='update',
labels=['user', 'activity'],
users=[g.user.key],
)
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/reply',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(204, got.status_code)
# mock_post.assert_not_called()
self.assertEqual([(obj, 'shared:target')], Fake.sent)
# def test_create_with_image(self):
# create_html = NOTE_HTML.replace(
# '</body>', '<img class="u-photo" src="http://im/age" />\n</body>')
# mock_get.side_effect = [
# requests_response(create_html, url='https://user.com/post',
# content_type=CONTENT_TYPE_HTML),
# ACTOR,
# ]
# mock_post.return_value = requests_response('abc xyz ')
def test_update_post_bare_object(self):
self.make_followers()
# Follower.get_or_create(
# to=g.user,
# from_=self.make_user('http://a', cls=ActivityPub,
# obj_as2={'inbox': 'https://inbox'}))
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/post',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(200, got.status_code)
post_as1 = {
'id': 'fake:post',
'objectType': 'note',
'content': 'first',
}
self.store_object(id='fake:post', our_as1=post_as1)
# self.assertEqual(('https://inbox',), mock_post.call_args[0])
# create = copy.deepcopy(CREATE_AS2)
# create['object'].update({
# 'image': {'url': 'http://im/age', 'type': 'Image'},
# 'attachment': [{'url': 'http://im/age', 'type': 'Image'}],
# })
# self.assert_equals(create, json_loads(mock_post.call_args[1]['data']))
post_as1['content'] = 'second'
self.assertEqual('OK', Fake.receive('fake:post', our_as1=post_as1))
# def test_create_reply(self):
# mock_get.side_effect = ACTIVITYPUB_GETS
# mock_post.return_value = requests_response('abc xyz')
self.assert_object('fake:post',
our_as1=post_as1,
type='note',
users=[g.user.key],
labels=['user'],
)
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/reply',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(200, got.status_code)
update_id = 'fake:post#bridgy-fed-update-2022-01-02T03:04:05+00:00'
obj = self.assert_object(update_id,
status='complete',
our_as1={
'objectType': 'activity',
'verb': 'post',
'id': update_id,
'actor': 'http://bf/fake/fake:user/ap',
'object': post_as1,
'published': '2022-01-02T03:04:05+00:00',
},
delivered=['shared:target'],
type='update',
labels=['user', 'activity', 'feed'],
users=[g.user.key, self.alice.key, self.bob.key],
)
# mock_get.assert_has_calls((
# self.req('https://user.com/reply'),
# self.as2_req('http://not/fediverse'),
# self.req('http://not/fediverse'),
# self.as2_req('https://mas.to/toot'),
# self.as2_req('https://mas.to/author'),
# ))
self.assertEqual([(obj, 'shared:target')], Fake.sent)
# self.assert_deliveries(mock_post, ['https://mas.to/inbox'], AS2_CREATE)
def test_create_reply(self):
self.make_followers()
# self.assert_object('https://user.com/reply',
# users=[g.user.key],
# source_protocol='web',
# mf2=REPLY_MF2,
# as1=REPLY_AS1,
# type='comment',
# )
# self.assert_object('https://user.com/reply#bridgy-fed-create',
# users=[g.user.key],
# source_protocol='web',
# status='complete',
# mf2=REPLY_MF2,
# our_as1=CREATE_REPLY_AS1,
# delivered=['https://mas.to/inbox'],
# type='post',
# labels=['user', 'activity'],
# )
reply_as1 = {
'id': 'fake:reply',
'objectType': 'note',
'inReplyTo': 'fake:post',
'author': 'fake:alice',
}
create_as1 = {
'id': 'fake:create',
'objectType': 'activity',
'verb': 'create',
'actor': 'fake:user',
'object': reply_as1,
}
self.assertEqual('OK', Fake.receive('fake:create', our_as1=create_as1))
self.assert_object('fake:reply',
our_as1=reply_as1,
type='note',
)
obj = self.assert_object('fake:create',
status='complete',
our_as1=create_as1,
delivered=['fake:post:target'],
type='create',
labels=['user', 'activity', 'notification'],
users=[g.user.key, self.alice.key],
)
self.assertEqual([(obj, 'fake:post:target')], Fake.sent)
# def test_update_reply(self):
# self.make_followers()
@ -511,31 +494,35 @@ class ProtocolReceiveTest(TestCase):
# self.assertEqual(200, got.status_code)
# self.assertEqual((AS2_UPDATE, 'https://mas.to/inbox'), Fake.sent)
@patch('requests.get')
def test_receive_reply_not_feed_not_notification(self, mock_get):
Follower.get_or_create(to=Fake.get_or_create(id=ACTOR['id']),
from_=Fake.get_or_create(id='foo.com'))
other_user = self.make_user('user.com', cls=Web)
def test_receive_reply_not_feed_not_notification(self):
Follower.get_or_create(to=g.user, from_=self.alice)
# user.com webmention discovery
mock_get.return_value = requests_response('<html></html>')
reply_as1 = {
'objectType': 'note',
'id': 'fake:reply',
'author': 'fake:bob',
'content': 'A ☕ reply',
'inReplyTo': 'fake:post',
}
create_as1 = {
'objectType': 'create',
'id': 'fake:create',
'object': reply_as1,
}
Fake.receive('fake:create', our_as1=create_as1)
Fake.receive(REPLY['id'], as2=REPLY)
self.assert_object(REPLY['id'],
as2=REPLY,
type='post',
users=[other_user.key],
self.assert_object('fake:create',
our_as1=reply_as1,
type='create',
users=[g.user.key],
# not feed since it's a reply
# not notification since it doesn't involve the user
labels=['activity'],
labels=['user'],
status='complete',
source_protocol='fake',
)
self.assert_object(REPLY['object']['id'],
our_as1=as2.to_as1(REPLY['object']),
our_as1=create_as1,
type='comment',
source_protocol='fake',
)
# def test_follow(self):
@ -557,7 +544,6 @@ class ProtocolReceiveTest(TestCase):
# obj = self.assert_object('https://user.com/follow',
# users=[g.user.key],
# source_protocol='web',
# status='complete',
# mf2=FOLLOW_MF2,
# as1=FOLLOW_AS1,
@ -636,7 +622,6 @@ class ProtocolReceiveTest(TestCase):
follow_obj = self.assert_object('fake:follow',
our_as1=follow_as1,
type='follow',
source_protocol='fake',
labels=['user', 'activity'],
status='complete',
delivered=['fake:bob:target'],
@ -671,76 +656,6 @@ class ProtocolReceiveTest(TestCase):
Follower.query().get(),
ignore=['created', 'updated'])
# def test_follow_multiple(self):
# html = FOLLOW_HTML.replace(
# '<a class="u-follow-of" href="https://mas.to/mrs-foo"></a>',
# '<a class="u-follow-of" href="https://mas.to/mrs-foo"></a> '
# '<a class="u-follow-of" href="https://mas.to/mr-biff"></a>')
# mock_get.side_effect = [
# requests_response(
# html, url='https://user.com/follow',
# content_type=CONTENT_TYPE_HTML),
# self.as2_resp({
# 'objectType': 'Person',
# 'displayName': 'Mr. ☕ Biff',
# 'id': 'https://mas.to/mr-biff',
# 'inbox': 'https://mas.to/inbox/biff',
# }),
# ACTOR,
# ]
# mock_post.return_value = requests_response('unused')
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/follow',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(200, got.status_code)
# mock_get.assert_has_calls((
# self.req('https://user.com/follow'),
# self.as2_req('https://mas.to/mr-biff'),
# self.as2_req('https://mas.to/mrs-foo'),
# ))
# calls = mock_post.call_args_list
# self.assertEqual('https://mas.to/inbox', calls[0][0][0])
# self.assertEqual(FOLLOW_AS2, json_loads(calls[0][1]['data']))
# self.assertEqual('https://mas.to/inbox/biff', calls[1][0][0])
# self.assertEqual({
# **FOLLOW_AS2,
# 'object': 'https://mas.to/mr-biff',
# }, json_loads(calls[1][1]['data']))
# mf2 = util.parse_mf2(html)['items'][0]
# as1 = microformats2.json_to_object(mf2)
# obj = self.assert_object('https://user.com/follow',
# users=[g.user.key],
# source_protocol='web',
# status='complete',
# mf2=mf2,
# as1=as1,
# delivered=['https://mas.to/inbox',
# 'https://mas.to/inbox/biff'],
# type='follow',
# object_ids=['https://mas.to/mrs-foo',
# 'https://mas.to/mr-biff'],
# labels=['user', 'activity'],
# )
# followers = Follower.query().fetch()
# self.assertEqual(2, len(followers))
# self.assertEqual(g.user.key, followers[0].from_)
# self.assertEqual(ActivityPub(id='https://mas.to/mr-biff').key,
# followers[0].to)
# self.assert_equals(obj.key, followers[0].follow)
# self.assertEqual(g.user.key, followers[1].from_)
# self.assertEqual(ActivityPub(id='https://mas.to/mrs-foo').key,
# followers[1].to)
# self.assert_equals(obj.key, followers[1].follow)
# def test_repost(self):
# self._test_repost(REPOST_HTML, REPOST_AS2)
@ -782,7 +697,6 @@ class ProtocolReceiveTest(TestCase):
# mf2 = util.parse_mf2(html)['items'][0]
# self.assert_object('https://user.com/repost',
# users=[g.user.key],
# source_protocol='web',
# status='complete',
# mf2=mf2,
# as1=microformats2.json_to_object(mf2),
@ -823,7 +737,6 @@ class ProtocolReceiveTest(TestCase):
like_obj = self.assert_object('fake:like',
users=[g.user.key],
source_protocol='fake',
status='complete',
our_as1=like_as1,
delivered=['fake:post:target'],
@ -853,7 +766,6 @@ class ProtocolReceiveTest(TestCase):
# self.assert_object('https://user.com/like',
# users=[g.user.key],
# source_protocol='web',
# mf2=LIKE_MF2,
# as1=microformats2.json_to_object(LIKE_MF2),
# type='like',
@ -908,7 +820,6 @@ class ProtocolReceiveTest(TestCase):
# self.assert_object('https://user.com/post#bridgy-fed-delete',
# users=[g.user.key],
# source_protocol='web',
# status='complete',
# our_as1=DELETE_AS1,
# delivered=inboxes,
@ -966,7 +877,6 @@ class ProtocolReceiveTest(TestCase):
# self.assert_object('https://user.com/follow',
# users=[g.user.key],
# source_protocol='web',
# status='failed',
# mf2=FOLLOW_MF2,
# as1=FOLLOW_AS1,
@ -1019,7 +929,6 @@ class ProtocolReceiveTest(TestCase):
# profile object
self.assert_object('fake:user',
source_protocol='fake',
our_as1=update_as1['object'],
type='person',
)
@ -1029,7 +938,6 @@ class ProtocolReceiveTest(TestCase):
update_obj = self.assert_object(
id,
users=[g.user.key],
source_protocol='fake',
status='complete',
our_as1=update_as1,
delivered=['shared:target'],
@ -1065,7 +973,6 @@ class ProtocolReceiveTest(TestCase):
# expected_as2 = copy.deepcopy(MENTION_OBJECT)
# expected_as2['tag'][1]['href'] = 'https://tar.get/'
# self.assert_object(MENTION_OBJECT['id'],
# source_protocol='activitypub',
# as2=expected_as2,
# type='note')
@ -1098,7 +1005,6 @@ class ProtocolReceiveTest(TestCase):
# expected_as2 = common.redirect_unwrap(mention)
# self.assert_object(mention['id'],
# users=[Web(id='tar.get').key],
# source_protocol='activitypub',
# status='complete',
# as2=expected_as2,
# delivered=['https://tar.get/'],
@ -1148,7 +1054,6 @@ class ProtocolReceiveTest(TestCase):
# obj = self.assert_object('fake:follow',
# users=[g.user.key],
# source_protocol='fake',
# status='complete',
# our_as1=follow_as1,
# delivered=['fake:user'],
@ -1230,7 +1135,6 @@ class ProtocolReceiveTest(TestCase):
# })
# self.assert_object('https://mas.to/6d1a',
# users=[g.user.key],
# source_protocol='activitypub',
# status='complete',
# as2=follow,
# delivered=['https://user.com/'],
@ -1310,4 +1214,3 @@ class ProtocolReceiveTest(TestCase):
self.assertEqual('inactive', follower.key.get().status)
self.assertEqual('inactive', followee.key.get().status)
self.assertEqual('active', other.key.get().status)

Wyświetl plik

@ -71,7 +71,7 @@ class Fake(User, protocol.Protocol):
@classmethod
def fetch(cls, obj, **kwargs):
id = obj.key.id()
logger.info(f'Fake.load {id}')
logger.info(f'Fake.fetch {id}')
cls.fetched.append(id)
if id in cls.fetchable:
@ -213,22 +213,31 @@ class TestCase(unittest.TestCase, testutil.Asserts):
return user
def add_objects(self):
with self.request_context:
# post
Object(id='a', domains=['user.com'], labels=['feed', 'notification'],
as2=as2.from_as1(NOTE)).put()
# different domain
Object(id='b', domains=['nope.org'], labels=['feed', 'notification'],
as2=as2.from_as1(MENTION)).put()
# reply
Object(id='d', domains=['user.com'], labels=['feed', 'notification'],
as2=as2.from_as1(COMMENT)).put()
# not feed/notif
Object(id='e', domains=['user.com'],
as2=as2.from_as1(NOTE)).put()
# deleted
Object(id='f', domains=['user.com'], labels=['feed', 'notification', 'user'],
as2=as2.from_as1(NOTE), deleted=True).put()
# post
self.store_object(id='a', domains=['user.com'],
labels=['feed', 'notification'],
as2=as2.from_as1(NOTE))
# different domain
self.store_object(id='b', domains=['nope.org'],
labels=['feed', 'notification'],
as2=as2.from_as1(MENTION))
# reply
self.store_object(id='d', domains=['user.com'],
labels=['feed', 'notification'],
as2=as2.from_as1(COMMENT))
# not feed/notif
self.store_object(id='e', domains=['user.com'], as2=as2.from_as1(NOTE))
# deleted
self.store_object(id='f', domains=['user.com'],
labels=['feed', 'notification', 'user'],
as2=as2.from_as1(NOTE), deleted=True)
@staticmethod
def store_object(**kwargs):
obj = Object(**kwargs)
obj.put()
del protocol.objects_cache[obj.key.id()]
return obj
@staticmethod
def random_keys_and_cids(num):

73
web.py
Wyświetl plik

@ -498,7 +498,7 @@ def webmention_task():
# fetch source page
try:
obj = Web.load(source, remote=True, check_backlink=True)
obj = Web.load(source, local=False, remote=True, check_backlink=True)
except BadRequest as e:
error(str(e.description), status=304)
except HTTPError as e:
@ -557,25 +557,18 @@ def webmention_task():
def _deliver(obj):
targets = _targets(obj) # maps Target to Object or None
if not targets:
add(obj.labels, 'user')
obj.status = 'ignored'
obj.put()
return 'No targets', 204
err = None
last_success = None
log_data = True
# if this is a post, wrap it in a create or update activity, if necessary
now = util.now().isoformat()
if obj.type in ('note', 'article', 'comment'):
# have we already seen this object? has it changed? or is it new?
if obj.new is None and obj.changed is None:
# check if we've seen this object, and if it's changed since then
existing = Object.get_by_id(obj.key.id())
obj.new = existing is not None
obj.changed = existing and as1.activity_changed(existing.as1, obj.as1)
if obj.changed:
logger.info(f'Content has changed from last time at {obj.updated}! Redelivering to all inboxes')
updated = util.now().isoformat()
id = f'{obj.key.id()}#bridgy-fed-update-{updated}'
id = f'{obj.key.id()}#bridgy-fed-update-{now}'
logger.info(f'Wrapping in update activity {id}')
obj.put()
update_as1 = {
@ -589,12 +582,12 @@ def _deliver(obj):
# https://docs.joinmastodon.org/spec/activitypub/#supported-activities-for-statuses
# https://socialhub.activitypub.rocks/t/what-could-be-the-reason-that-my-update-activity-does-not-work/2893/4
# https://github.com/mastodon/documentation/pull/1150
'updated': updated,
'updated': now,
**obj.as1,
},
}
obj = Object(id=id, mf2=obj.mf2, our_as1=update_as1, labels=['user'],
users=[g.user.key], source_protocol='web')
obj = Object(id=id, our_as1=update_as1, users=[g.user.key],
source_protocol=obj.source_protocol)
elif obj.new or 'force' in request.form:
logger.info(f'New Object {obj.key.id()}')
@ -607,28 +600,43 @@ def _deliver(obj):
'id': id,
'actor': g.user.ap_actor(),
'object': obj.as1,
'published': now,
}
obj = Object(id=id, mf2=obj.mf2, our_as1=create_as1,
users=[g.user.key], labels=['user'],
source_protocol='web')
source_protocol = obj.source_protocol
obj = Object.get_or_insert(id)
obj.populate(our_as1=create_as1, users=[g.user.key],
source_protocol=source_protocol)
else:
msg = f'{obj.key.id()} is unchanged, nothing to do'
logger.info(msg)
return msg, 204
# find delivery targets
# sort targets so order is deterministic for tests, debugging, etc
targets = _targets(obj) # maps Target to Object or None
if not targets:
add(obj.labels, 'user')
obj.status = 'ignored'
obj.put()
return 'No targets', 204
sorted_targets = sorted(targets.items(), key=lambda t: t[0].uri)
obj.populate(
status='in progress',
labels=['user'],
delivered=[],
failed=[],
undelivered=[t for t, _ in sorted_targets],
)
add(obj.labels, 'user')
logger.info(f'Delivering to: {obj.undelivered}')
# make copy of undelivered because we modify it below.
# sort targets so order is deterministic for tests.
err = None
last_success = None
log_data = True
# deliver!
for target, orig_obj in sorted_targets:
assert target.uri
protocol = PROTOCOLS[target.protocol]
@ -713,6 +721,8 @@ def _targets(obj):
logger.info(f"Couldn't load {id}")
continue
# TODO: attach orig_obj's author/actor to obj.users
target = protocol.target_for(orig_obj)
if target:
targets[Target(protocol=protocol.LABEL, uri=target)] = orig_obj
@ -722,14 +732,19 @@ def _targets(obj):
# TODO: surface errors like this somehow?
logger.error(f"Can't find delivery target for {id}")
if not targets or verb == 'share':
# deliver to followers?
inner_obj_as1 = as1.get_object(obj.as1)
is_reply = (obj.type == 'comment' or
(inner_obj_as1 and inner_obj_as1.get('inReplyTo')))
if obj.type == 'share' or (obj.type in ('post', 'update') and not is_reply):
logger.info('Delivering to followers')
followers = Follower.query(Follower.to == g.user.key,
Follower.status == 'active'
).fetch()
users = ndb.get_multi(f.from_ for f in followers)
users = [u for u in users if u]
users = [u for u in ndb.get_multi(f.from_ for f in followers) if u]
User.load_multi(users)
obj.users.extend(u.key for u in users)
add(obj.labels, 'feed')
for user in users:
# TODO: should we pass remote=False through here to Protocol.load?