merging Protocol/Web receive: reposts, deletes

for #529
pull/582/head
Ryan Barrett 2023-07-02 14:57:05 -07:00
rodzic b3a3de73f2
commit 903f26bb0a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
2 zmienionych plików z 124 dodań i 179 usunięć

Wyświetl plik

@ -385,8 +385,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', 'update')
and inner_obj_as1.keys() > set(['id'])):
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,
source_protocol=cls.LABEL)
@ -427,29 +426,26 @@ class Protocol:
elif obj.type == 'update':
if not inner_obj_id:
error("Couldn't find id of object to update")
# fall through to deliver to followers
elif obj.type == 'delete':
if not inner_obj_id:
error("Couldn't find id of object to delete")
to_delete = Object.get_by_id(inner_obj_id)
if to_delete:
logger.info(f'Marking Object {inner_obj_id} deleted')
to_delete.deleted = True
to_delete.put()
logger.info(f'Marking Object {inner_obj_id} deleted')
Object.get_or_create(inner_obj_id, deleted=True)
# assume this is an actor
# https://github.com/snarfed/bridgy-fed/issues/63
logger.info(f'Deactivating Followers from or to = {inner_obj_id}')
deleted_user = cls(id=inner_obj_id).key
deleted_user = cls.key_for(id=inner_obj_id)
followers = Follower.query(OR(Follower.to == deleted_user,
Follower.from_ == deleted_user)
).fetch()
for f in followers:
f.status = 'inactive'
obj.status = 'complete'
ndb.put_multi(followers + [obj])
return 'OK'
ndb.put_multi(followers)
# fall through to deliver to followers
# fetch actor if necessary so we have name, profile photo, etc
if actor and actor.keys() == set(['id']):
@ -459,9 +455,9 @@ class Protocol:
# fetch object if necessary so we can render it in feeds
if obj.type == 'share' and inner_obj_as1.keys() == set(['id']):
if not inner_obj:
if not inner_obj and cls.owns_id(inner_obj_id):
inner_obj = cls.load(inner_obj_id)
if inner_obj.as1:
if inner_obj and inner_obj.as1:
obj.our_as1 = {
**obj.as1,
'object': {
@ -473,23 +469,8 @@ class Protocol:
if obj.type == 'follow':
cls._accept_follow(obj)
# deliver to each target
cls._deliver(obj)
# 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 == 'post' and not is_reply))
and actor_id):
logger.info(f'Delivering to followers of {actor_id}')
for f in Follower.query(Follower.to == cls.key_for(actor_id),
Follower.status == 'active'):
add(obj.users, f.from_)
if obj.users:
add(obj.labels, 'feed')
obj.put()
return 'OK'
# deliver to targets
return cls._deliver(obj)
@classmethod
def _accept_follow(cls, obj):
@ -680,19 +661,18 @@ class Protocol:
obj.put()
obj.status = ('complete' if obj.delivered
else 'failed' if obj.failed
else 'ignored')
obj.put()
# Pass the response status code and body through as our response
if obj.delivered:
ret = 'OK', 200
ret = 'OK'
obj.status = 'complete'
elif errors:
ret = f'Delivery failed: {errors}', 502
obj.status = 'failed'
else:
ret = r'Nothing to do ¯\_(ツ)_/¯', 204
obj.status = 'ignored'
obj.put()
logger.info(f'Returning {ret}')
return ret
@ -712,7 +692,7 @@ class Protocol:
inner_obj_as1 = as1.get_object(obj.as1)
# if it's a reply, like, or repost. otherwise, it's all followers.
# if it's a reply, like, or repost, grab the object
#
# sort so order is deterministic for tests.
orig_ids = sorted(as1.get_ids(obj.as1, 'inReplyTo') +
@ -720,7 +700,8 @@ class Protocol:
verb = obj.as1.get('verb')
if orig_ids:
logger.info(f'original object ids from inReplyTo: {orig_ids}')
elif verb in as1.VERBS_WITH_OBJECT:
if verb in as1.VERBS_WITH_OBJECT:
# prefer id or url, if available
# https://github.com/snarfed/bridgy-fed/issues/307
orig_ids = (as1.get_ids(obj.as1, 'object')
@ -760,10 +741,10 @@ class Protocol:
add(obj.labels, 'notification')
# deliver 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 ('post', 'update') and not is_reply):
logger.info('Delivering to followers')
if (obj.type in ('post', 'update', 'delete', 'share')
and not (obj.type == 'comment' or inner_obj_as1.get('inReplyTo'))):
# TODO: use obj's actor/author instead of g.user?
logger.info(f'Delivering to followers of {g.user.key}')
followers = Follower.query(Follower.to == g.user.key,
Follower.status == 'active'
).fetch()

Wyświetl plik

@ -3,6 +3,7 @@ from unittest.mock import patch
from flask import g
from granary import as2
from oauth_dropins.webutil.flask_util import NoContent
from oauth_dropins.webutil.testutil import requests_response
import requests
@ -593,14 +594,42 @@ class ProtocolReceiveTest(TestCase):
delivered=['fake:post:target', 'shared:target'],
type='share',
labels=['user', 'activity', 'notification', 'feed'],
users=[g.user.key, self.bob.key, self.alice.key],
users=[g.user.key, self.alice.key, self.bob.key],
)
self.assertEqual([
(obj, 'fake:post:target'),
(obj, 'shared:target'),
], Fake.sent)
def test_inbox_like(self):
def test_repost_twitter_blocklisted(self):
self._test_repost_blocklisted_error('https://twitter.com/foo')
def test_repost_bridgy_fed_blocklisted(self):
self._test_repost_blocklisted_error('https://fed.brid.gy/foo')
def _test_repost_blocklisted_error(self, orig_url):
"""Reposts of non-fediverse (ie blocklisted) sites aren't yet supported."""
repost_as1 = {
'id': 'fake:repost',
'objectType': 'activity',
'verb': 'share',
'actor': 'fake:user',
'object': orig_url,
}
with self.assertRaises(NoContent):
Fake.receive(repost_as1)
obj = self.assert_object('fake:repost',
status='ignored',
our_as1=repost_as1,
delivered=[],
type='share',
labels=['user', 'activity', 'feed'],
users=[g.user.key],
)
self.assertEqual([], Fake.sent)
def test_like(self):
Fake.fetchable['fake:post'] = {
'objectType': 'note',
}
@ -625,113 +654,87 @@ class ProtocolReceiveTest(TestCase):
self.assertEqual([(like_obj, 'fake:post:target')], Fake.sent)
# def test_like_stored_object_without_as2(self):
# Object(id='https://mas.to/toot', mf2=NOTE_MF2, source_protocol='ap').put()
# Object(id='https://user.com/', mf2=ACTOR_MF2).put()
# mock_get.side_effect = [
# LIKE,
# ]
def test_delete(self):
self.make_followers()
# with self.assertRaises(NoContent):
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/like',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(204, got.status_code)
post_as1 = {
'id': 'fake:post',
'objectType': 'note',
'author': 'fake:user',
}
self.store_object(id='fake:post', our_as1=post_as1)
# mock_get.assert_has_calls((
# self.req('https://user.com/like'),
# ))
# mock_post.assert_not_called()
delete_as1 = {
'id': 'fake:delete',
'objectType': 'activity',
'verb': 'delete',
'object': 'fake:post',
}
self.assertEqual('OK', Fake.receive(delete_as1))
# self.assert_object('https://user.com/like',
# users=[g.user.key],
# mf2=LIKE_MF2,
# as1=microformats2.json_to_object(LIKE_MF2),
# type='like',
# labels=['user', 'activity'],
# status='ignored',
# )
self.assert_object('fake:post',
our_as1=post_as1,
deleted=True,
source_protocol=None,
)
# def test_create_author_only_url(self):
# """Mf2 author property is just a URL. We should run full authorship.
obj = self.assert_object('fake:delete',
status='complete',
our_as1=delete_as1,
delivered=['shared:target'],
type='delete',
labels=['user', 'activity', 'feed'],
users=[g.user.key, self.alice.key, self.bob.key],
)
self.assertEqual([(obj, 'shared:target')], Fake.sent)
# https://indieweb.org/authorship
# """
# repost = requests_response("""\
# <html>
# <body class="h-entry">
# <a class="u-repost-of p-name" href="https://mas.to/toot">reposted!</a>
# <a class="u-author" href="https://user.com/"></a>
# <a href="http://localhost/"></a>
# </body>
# </html>
# """, url='https://user.com/repost', content_type=CONTENT_TYPE_HTML)
# mock_get.side_effect = [repost, ACTOR, TOOT_AS2, ACTOR]
# mock_post.return_value = requests_response('abc xyz')
def test_delete_no_followers_no_stored_object(self):
delete_as1 = {
'id': 'fake:delete',
'objectType': 'activity',
'verb': 'delete',
'object': 'fake:post',
}
with self.assertRaises(NoContent):
self.assertEqual('OK', Fake.receive(delete_as1))
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/repost',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(200, got.status_code)
self.assert_object('fake:post',
deleted=True,
source_protocol=None,
)
# args, kwargs = mock_post.call_args
# self.assertEqual(('https://mas.to/inbox',), args)
# self.assert_equals(REPOST_AS2, json_loads(kwargs['data']))
self.assert_object('fake:delete',
status='ignored',
our_as1=delete_as1,
delivered=[],
type='delete',
labels=['user', 'activity', 'feed'],
users=[g.user.key],
)
self.assertEqual([], Fake.sent)
# def test_delete(self):
# mock_get.return_value = requests_response('"unused"', status=410,
# url='http://final/delete')
# mock_post.return_value = requests_response('unused', status=200)
# Object(id='https://user.com/post#bridgy-fed-create',
# mf2=NOTE_MF2, status='complete').put()
def test_delete_actor(self):
follower = Follower.get_or_create(to=g.user, from_=self.alice)
followee = Follower.get_or_create(to=self.alice, from_=self.bob)
other = Follower.get_or_create(to=g.user, from_=self.bob)
self.assertEqual(3, Follower.query().count())
# self.make_followers()
self.assertEqual('OK', Fake.receive({
'objectType': 'activity',
'verb': 'delete',
'id': 'fake:delete',
'object': 'fake:alice',
}))
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/post',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(200, got.status_code, got.text)
self.assertEqual(3, Follower.query().count())
self.assertEqual('inactive', follower.key.get().status)
self.assertEqual('inactive', followee.key.get().status)
self.assertEqual('active', other.key.get().status)
# inboxes = ('https://inbox', 'https://public/inbox', 'https://shared/inbox')
# self.assert_deliveries(mock_post, inboxes, DELETE_AS2)
# self.assert_object('https://user.com/post#bridgy-fed-delete',
# users=[g.user.key],
# status='complete',
# our_as1=DELETE_AS1,
# delivered=inboxes,
# type='delete',
# object_ids=['https://user.com/post'],
# labels=['user', 'activity'],
# )
# def test_delete_no_object(self):
# mock_get.side_effect = [
# requests_response('"unused"', status=410, url='http://final/delete'),
# ]
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/post',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(304, got.status_code, got.text)
# mock_post.assert_not_called()
# def test_delete_incomplete_response(self):
# mock_get.return_value = requests_response('"unused"', status=410,
# url='http://final/delete')
# Object(id='https://user.com/post#bridgy-fed-create',
# mf2=NOTE_MF2, status='in progress')
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/post',
# 'target': 'https://fed.brid.gy/',
# })
# self.assertEqual(304, got.status_code, got.text)
# mock_post.assert_not_called()
self.assert_object('fake:alice',
deleted=True,
source_protocol=None,
)
# def test_send_error(self):
# mock_get.side_effect = [FOLLOW, ACTOR]
@ -766,27 +769,6 @@ class ProtocolReceiveTest(TestCase):
# labels=['user', 'activity'],
# )
# def test_repost_twitter_blocklisted(self):
# self._test_repost_blocklisted_error('https://twitter.com/foo')
# def test_repost_bridgy_fed_blocklisted(self):
# self._test_repost_blocklisted_error('https://fed.brid.gy/foo')
# def _test_repost_blocklisted_error(self, orig_url):
# """Reposts of non-fediverse (ie blocklisted) sites aren't yet supported."""
# repost_html = REPOST_HTML.replace('https://mas.to/toot', orig_url)
# repost_resp = requests_response(repost_html, content_type=CONTENT_TYPE_HTML,
# url='https://user.com/repost')
# mock_get.side_effect = [repost_resp]
# got = self.client.post('/_ah/queue/webmention', data={
# 'source': 'https://user.com/repost',
# 'target': 'https://fed.brid.gy/',
# })
# with self.assertRaises(NoContent):
# self.assertEqual(204, got.status_code)
# mock_post.assert_not_called()
def test_update_profile(self):
Follower.get_or_create(to=g.user, from_=self.alice)
Follower.get_or_create(to=g.user, from_=self.bob)
@ -1021,24 +1003,6 @@ class ProtocolReceiveTest(TestCase):
}))
self.assertEqual('inactive', follower.key.get().status)
def test_delete_actor(self):
follower = Follower.get_or_create(to=g.user, from_=self.alice)
followee = Follower.get_or_create(to=self.alice, from_=self.bob)
other = Follower.get_or_create(to=g.user, from_=self.bob)
self.assertEqual(3, Follower.query().count())
self.assertEqual('OK', Fake.receive({
'objectType': 'activity',
'verb': 'delete',
'id': 'fake:delete',
'object': 'fake:alice',
}))
self.assertEqual(3, Follower.query().count())
self.assertEqual('inactive', follower.key.get().status)
self.assertEqual('inactive', followee.key.get().status)
self.assertEqual('active', other.key.get().status)
def test_receive_from_bridgy_fed_fails(self):
with self.assertRaises(BadRequest):
Fake.receive({