kopia lustrzana https://github.com/snarfed/bridgy-fed
handle AP Undo of Follow
needed for eg mastodon when you unfollow and then try to refollow someone. on unfollow, mastodon sends an Undo, and waits for a 200 before it shows success and will let you refollow.pull/59/head
rodzic
ad55cc95f3
commit
4b70a89046
|
@ -7,6 +7,7 @@ import string
|
|||
|
||||
import appengine_config
|
||||
|
||||
from google.appengine.ext import ndb
|
||||
from granary import as2, microformats2
|
||||
import mf2py
|
||||
import mf2util
|
||||
|
@ -30,6 +31,7 @@ SUPPORTED_TYPES = (
|
|||
'Image',
|
||||
'Like',
|
||||
'Note',
|
||||
'Undo',
|
||||
'Video',
|
||||
)
|
||||
|
||||
|
@ -129,6 +131,10 @@ class InboxHandler(webapp2.RequestHandler):
|
|||
|
||||
# TODO: verify signature if there is one
|
||||
|
||||
if type == 'Undo' and obj.get('type') == 'Follow':
|
||||
# skip actor fetch below; we don't need it to undo a follow
|
||||
return self.undo_follow(common.redirect_unwrap(activity))
|
||||
|
||||
# fetch actor if necessary so we have name, profile photo, etc
|
||||
for elem in obj, activity:
|
||||
actor = elem.get('actor')
|
||||
|
@ -137,8 +143,7 @@ class InboxHandler(webapp2.RequestHandler):
|
|||
|
||||
activity_unwrapped = common.redirect_unwrap(activity)
|
||||
if type == 'Follow':
|
||||
self.accept_follow(activity, activity_unwrapped)
|
||||
return
|
||||
return self.accept_follow(activity, activity_unwrapped)
|
||||
|
||||
# send webmentions to each target
|
||||
as1 = as2.to_as1(activity)
|
||||
|
@ -191,6 +196,33 @@ class InboxHandler(webapp2.RequestHandler):
|
|||
self, as2.to_as1(follow), proxy=True, protocol='activitypub',
|
||||
source_as2=json.dumps(follow_unwrapped))
|
||||
|
||||
@ndb.transactional
|
||||
def undo_follow(self, undo_unwrapped):
|
||||
"""Replies to an AP Follow request with an Accept request.
|
||||
|
||||
Args:
|
||||
undo_unwrapped: dict, AP Undo activity with redirect URLs unwrapped
|
||||
"""
|
||||
logging.info('Undoing Follow')
|
||||
|
||||
follow = undo_unwrapped.get('object', {})
|
||||
follower = follow.get('actor')
|
||||
followee = follow.get('object')
|
||||
if not follower or not followee:
|
||||
common.error(self, 'Undo of Follow requires object with actor and object. Got: %s' % follow)
|
||||
|
||||
# deactivate Follower
|
||||
user_domain = util.domain_from_link(followee)
|
||||
follower_obj = Follower.get_by_id(Follower._id(user_domain, follower))
|
||||
if not follower_obj:
|
||||
common.error(self, '%s has never followed %s' % (follower, user_domain))
|
||||
|
||||
logging.info('Marking %s as inactive' % follower_obj.key)
|
||||
follower_obj.status = 'inactive'
|
||||
follower_obj.put()
|
||||
|
||||
# TODO send webmention with 410 of u-follow
|
||||
|
||||
|
||||
app = webapp2.WSGIApplication([
|
||||
(r'/%s/?' % common.DOMAIN_RE, ActorHandler),
|
||||
|
|
|
@ -133,9 +133,12 @@ class Follower(StringIdModel):
|
|||
Key name is 'USER_DOMAIN FOLLOWER_ID', e.g.:
|
||||
'snarfed.org https://mastodon.social/@swentel'.
|
||||
"""
|
||||
STATUSES = ('active', 'inactive')
|
||||
|
||||
# most recent AP Follow activity (JSON). must have a composite actor object
|
||||
# with an inbox, publicInbox, or sharedInbox!
|
||||
last_follow = ndb.TextProperty()
|
||||
status = ndb.StringProperty(choices=STATUSES, default='active')
|
||||
|
||||
created = ndb.DateTimeProperty(auto_now_add=True)
|
||||
updated = ndb.DateTimeProperty(auto_now=True)
|
||||
|
|
|
@ -125,6 +125,14 @@ ACCEPT = {
|
|||
}
|
||||
}
|
||||
|
||||
UNDO_FOLLOW_WRAPPED = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': 'https://mastodon.social/6d1b',
|
||||
'type': 'Undo',
|
||||
'actor': 'https://mastodon.social/users/swentel',
|
||||
'object': FOLLOW_WRAPPED,
|
||||
}
|
||||
|
||||
@patch('requests.post')
|
||||
@patch('requests.get')
|
||||
@patch('requests.head')
|
||||
|
@ -347,8 +355,38 @@ class ActivityPubTest(testutil.TestCase):
|
|||
|
||||
# check that we stored a Follower object
|
||||
follower = Follower.get_by_id('realize.be %s' % (FOLLOW['actor']))
|
||||
self.assertEqual('active', follower.status)
|
||||
self.assertEqual(FOLLOW_WRAPPED_WITH_ACTOR, json.loads(follower.last_follow))
|
||||
|
||||
def test_inbox_undo_follow(self, mock_head, mock_get, mock_post):
|
||||
mock_head.return_value = requests_response(url='https://realize.be/')
|
||||
|
||||
Follower(id=Follower._id('realize.be', FOLLOW['actor'])).put()
|
||||
|
||||
got = app.get_response('/foo.com/inbox', method='POST',
|
||||
body=json.dumps(UNDO_FOLLOW_WRAPPED))
|
||||
self.assertEquals(200, got.status_int)
|
||||
|
||||
follower = Follower.get_by_id('realize.be %s' % FOLLOW['actor'])
|
||||
self.assertEqual('inactive', follower.status)
|
||||
|
||||
def test_inbox_undo_follow_doesnt_exist(self, mock_head, mock_get, mock_post):
|
||||
mock_head.return_value = requests_response(url='https://realize.be/')
|
||||
|
||||
got = app.get_response('/foo.com/inbox', method='POST',
|
||||
body=json.dumps(UNDO_FOLLOW_WRAPPED))
|
||||
self.assertEquals(400, got.status_int)
|
||||
self.assertIn('has never followed realize.be', got.text)
|
||||
|
||||
def test_inbox_undo_follow_inactive(self, mock_head, mock_get, mock_post):
|
||||
mock_head.return_value = requests_response(url='https://realize.be/')
|
||||
Follower(id=Follower._id('realize.be', 'https://mastodon.social/users/swentel'),
|
||||
status='inactive').put()
|
||||
|
||||
got = app.get_response('/foo.com/inbox', method='POST',
|
||||
body=json.dumps(UNDO_FOLLOW_WRAPPED))
|
||||
self.assertEquals(200, got.status_int)
|
||||
|
||||
def test_inbox_unsupported_type(self, *_):
|
||||
got = app.get_response('/foo.com/inbox', method='POST', body=json.dumps({
|
||||
'@context': ['https://www.w3.org/ns/activitystreams'],
|
||||
|
|
Ładowanie…
Reference in New Issue