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
Ryan Barrett 2019-08-01 07:32:45 -07:00
rodzic ad55cc95f3
commit 4b70a89046
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
3 zmienionych plików z 75 dodań i 2 usunięć

Wyświetl plik

@ -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),

Wyświetl plik

@ -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)

Wyświetl plik

@ -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'],