From d14b1bc04c9176872ee3d92d7e34be66743a543e Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Tue, 5 Dec 2023 08:51:27 -0800 Subject: [PATCH] add Web.maybe_superfeedr_unsubscribe for #550 --- tests/test_web.py | 45 ++++++++++++++++++++++++++++++++++++++++----- web.py | 27 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/tests/test_web.py b/tests/test_web.py index 4a5b05e..52b0645 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,5 +1,6 @@ """Unit tests for webmention.py.""" import copy +from datetime import timedelta from unittest.mock import ANY, patch from flask import g, get_flashed_messages @@ -518,9 +519,11 @@ class WebTest(TestCase): user = Web.get_or_create('user.com') self.assertEqual('user.com', user.key.id()) - self.assert_entities_equal(user, self.user, - ignore=['superfeedr_subscribed', 'updated']) - self.assertTrue(user.superfeedr_subscribed) + self.assert_entities_equal( + user, self.user, + ignore=['superfeedr_subscribed', 'superfeedr_subscribed_feed', 'updated']) + self.assertEqual(NOW, user.superfeedr_subscribed) + self.assertEqual('https://foo/atom', user.superfeedr_subscribed_feed) self.assert_req(mock_post, SUPERFEEDR_PUSH_API, data={ 'hub.mode': 'subscribe', @@ -1813,7 +1816,6 @@ class WebTest(TestCase): self.assertEqual(400, got.status_code) @patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task') - @patch('oauth_dropins.webutil.appengine_info.LOCAL_SERVER', False) def test_maybe_superfeedr_subscribe(self, mock_create_task, mock_get, mock_post): common.RUN_TASKS_INLINE = False @@ -1862,6 +1864,8 @@ class WebTest(TestCase): 'retrieve': 'true', }, auth=ANY) self.assertEqual(NOW, self.user.key.get().superfeedr_subscribed) + self.assertEqual('https://foo/atom', + self.user.key.get().superfeedr_subscribed_feed) obj = Object.get_by_id('http://domain.tld/entry/1') self.assertIn(entries[0], obj.atom) @@ -1873,7 +1877,6 @@ class WebTest(TestCase): self.assert_task(mock_create_task, 'receive', '/queue/receive', obj=obj.key.urlsafe(), authed_as='user.com') - def test_maybe_superfeedr_subscribe_no_feed(self, mock_get, mock_post): self.user.obj.mf2 = ACTOR_MF2 # no rel-urls web.maybe_superfeedr_subscribe(self.user) @@ -1885,6 +1888,38 @@ class WebTest(TestCase): web.maybe_superfeedr_subscribe(self.user) # should be a noop + def test_maybe_superfeedr_unsubscribe(self, mock_get, mock_post): + appengine_info.LOCAL_SERVER = False + + self.user.superfeedr_subscribed = NOW + self.user.superfeedr_subscribed_feed = 'http://feed' + self.user.put() + + mock_post.return_value = requests_response('OK') + + web.maybe_superfeedr_unsubscribe(self.user) + self.assert_req(mock_post, SUPERFEEDR_PUSH_API, data={ + 'hub.mode': 'unsubscribe', + 'hub.topic': 'http://feed', + 'hub.callback': 'http://localhost/superfeedr/notify/user.com', + }, auth=ANY) + + def test_maybe_superfeedr_unsubscribe_not_subscribed(self, mock_get, mock_post): + appengine_info.LOCAL_SERVER = False + + web.maybe_superfeedr_unsubscribe(self.user) + mock_post.assert_not_called() + + def test_maybe_superfeedr_unsubscribe_last_webmention_later(self, _, mock_post): + appengine_info.LOCAL_SERVER = False + + self.user.superfeedr_subscribed = NOW + self.user.last_webmention_in = NOW + timedelta(days=1) + self.user.put() + + web.maybe_superfeedr_unsubscribe(self.user) + mock_post.assert_not_called() + def _test_verify(self, redirects, hcard, actor, redirects_error=None): self.user.has_redirects = False self.user.put() diff --git a/web.py b/web.py index 311acb2..01f823d 100644 --- a/web.py +++ b/web.py @@ -97,6 +97,7 @@ class Web(User, Protocol): has_hcard = ndb.BooleanProperty() last_webmention_in = ndb.DateTimeProperty(tzinfo=timezone.utc) superfeedr_subscribed = ndb.DateTimeProperty(tzinfo=timezone.utc) + superfeedr_subscribed_feed = ndb.StringProperty() # Originally, BF served Web users' AP actor ids on fed.brid.gy, eg # https://fed.brid.gy/snarfed.org . When we started adding new protocols, we @@ -642,6 +643,7 @@ def maybe_superfeedr_subscribe(user): resp.raise_for_status() user.superfeedr_subscribed = util.now() + user.superfeedr_subscribed_feed = url user.put() # handle feed entries (posts) @@ -650,6 +652,31 @@ def maybe_superfeedr_subscribe(user): return _superfeedr_notify(resp.text, user=user) +def maybe_superfeedr_unsubscribe(user): + """Subscribes to a user's Atom or RSS feed in Superfeedr. + + Args: + user (Web) + """ + if (not user.superfeedr_subscribed + or not user.superfeedr_subscribed_feed + or (user.last_webmention_in + and user.last_webmention_in > user.superfeedr_subscribed) + or appengine_info.LOCAL_SERVER): + return + + # unsubscribe + logger.info(f'Unsubscribing from Superfeedr for {user.superfeedr_subscribed_feed}') + + auth = HTTPBasicAuth(SUPERFEEDR_USERNAME, SUPERFEEDR_TOKEN) + resp = util.requests_post(SUPERFEEDR_PUSH_API, auth=auth, data={ + 'hub.mode': 'unsubscribe', + 'hub.topic': user.superfeedr_subscribed_feed, + 'hub.callback': common.host_url(f'/superfeedr/notify/{user.key.id()}'), + }) + resp.raise_for_status() + + # generate/check per-user token for auth? # or https://documentation.superfeedr.com/subscribers.html#http-authentication ? @app.post(f'/superfeedr/notify/')