add opt out via #nobridge or #nobot text in user profile bio

fixes #666

(unfortunate issue number 😆)
pull/684/head
Ryan Barrett 2023-10-13 12:36:31 -07:00
rodzic fbf1e645ef
commit d0da119b07
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
7 zmienionych plików z 104 dodań i 11 usunięć

Wyświetl plik

@ -48,6 +48,9 @@ OBJECT_EXPIRE_TYPES = (
)
OBJECT_EXPIRE_AGE = timedelta(days=90)
OPT_OUT_TAGS = frozenset(('#nobot', '#nobridge'))
logger = logging.getLogger(__name__)
@ -314,6 +317,29 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
"""DEPRECATED: replaced by handle. Kept for backward compatibility."""
return None
@ndb.ComputedProperty
def status(self):
"""Whether this user has explicitly opted out of Bridgy Fed.
Optional. Current possible values:
* ``opt-out``
Currently just looks for ``#nobridge`` or ``#nobot`` in the profile
description/bio.
https://github.com/snarfed/bridgy-fed/issues/666
"""
if not self.obj or not self.obj.as1:
return None
for field in 'summary', 'displayName':
val = self.obj.as1.get(field)
for tag in OPT_OUT_TAGS:
if val and tag in val:
return 'opt-out'
return None
def handle_as(self, to_proto):
"""Returns this user's handle in a different protocol.
@ -991,6 +1017,7 @@ class Follower(ndb.Model):
for f, u in zip(followers, users):
f.user = u
followers = [f for f in followers if f.user.status != 'opt-out']
return followers, before, after

Wyświetl plik

@ -71,7 +71,7 @@ def load_user(protocol, id):
elif g.user and id != g.user.key.id(): # use_instead redirect
error('', status=302, location=g.user.user_page_path())
if not g.user or not g.user.direct:
if not g.user or not g.user.direct or g.user.status == 'opt-out':
# TODO: switch back to USER_NOT_FOUND_HTML
# not easy via exception/abort because this uses Werkzeug's built in
# NotFound exception subclass, and we'd need to make it implement

Wyświetl plik

@ -214,11 +214,17 @@ class Protocol:
valid :class:`User` id for this protocol.
"""
if cls == Protocol:
return Protocol.for_id(id).key_for(id)
proto = Protocol.for_id(id)
return proto.key_for(id) if proto else None
# load user so that we follow use_instead
existing = cls.get_by_id(id)
return existing.key if existing else cls(id=id).key
if existing:
if existing.status == 'opt-out':
return None
return existing.key
return cls(id=id).key
@staticmethod
def for_id(id):
@ -330,10 +336,12 @@ class Protocol:
# step 2: look for matching User in the datastore
for proto in candidates:
user = proto.query(proto.handle == handle).get(keys_only=True)
user = proto.query(proto.handle == handle).get()
if user:
logger.info(f' user {user} owns handle {handle}')
return (proto, user.id())
if user.status == 'opt-out':
return (None, None)
logger.info(f' user {user.key} owns handle {handle}')
return (proto, user.key.id())
# step 3: resolve handle to id
for proto in candidates:
@ -361,7 +369,7 @@ class Protocol:
owner = as1.get_owner(obj.as1)
if owner:
return cls.key_for(owner)
elif default_g_user and g.user:
elif default_g_user and g.user and g.user.status != 'opt-out':
return g.user.key
@classmethod
@ -534,6 +542,7 @@ class Protocol:
error(f'Sorry, {obj.type} activities are not supported yet.', status=501)
# add owner(s)
# (actor_key returns None if the user is opted out)
actor_key = from_cls.actor_key(obj, default_g_user=False)
if actor_key:
obj.add('users', actor_key)

Wyświetl plik

@ -36,6 +36,7 @@ Bridgy Fed takes some technical know-how to set up, and there are simpler (but l
<li><a href="#update-profile">How do I update my profile?</a></li>
<li><a href="#user-page">Where's my user page and dashboard?</a></li>
<li><a href="#mastodon-link-verification">How do I verify my profile links (ie get green checks) in Mastodon?</a></li>
<li><a href="#opt-out">I hate this! How do I opt out?</a></li>
<br>
<p><em>Usage</em></p>
@ -218,6 +219,11 @@ Your site's fediverse profile comes from the <a href="https://microformats.org/w
<p>When you're logged into a Mastodon instance, searching for your Bridgy Fed user triggers that instance to check and verify its profile link(s) in the background. This only works when you're logged in with a native Mastodon account. Also, each instance does this independently; verified links are not synched across instances.</p>
</li>
<li id="opt-out" class="question">I hate this! How do I opt out?</li>
<li class="answer">
<p>Sorry to hear it! Just put the text <code>#nobridge</code> or <code>#nobot</code> in your profile bio, refresh your profile on your Bridgy Fed user page, and it will stop bridging your account.</p>
</li>
<br>
<h3 id="usage">Usage</h3>

Wyświetl plik

@ -211,13 +211,32 @@ class UserTest(TestCase):
user = g.user.key.get()
self.assertFalse(hasattr(user, '_obj'))
self.assertFalse(hasattr(alice, '_obj'))
self.assertFalse(hasattr(bob, '_obj'))
self.assertIsNone(bob._obj)
User.load_multi([user, alice, bob])
self.assertIsNotNone(user._obj)
self.assertIsNone(alice._obj)
self.assertIsNone(bob._obj)
def test_status(self):
self.assertIsNone(g.user.status)
user = self.make_user('fake:user', cls=Fake, obj_as1={
'summary': 'I like this',
})
self.assertIsNone(user.status)
user.obj.our_as1.update({
'summary': 'well #nobot yeah',
})
self.assertEqual('opt-out', user.status)
user.obj.our_as1.update({
'summary': '🤷',
'displayName': 'well #nobridge yeah',
})
self.assertEqual('opt-out', user.status)
class ObjectTest(TestCase):
def setUp(self):

Wyświetl plik

@ -101,6 +101,12 @@ class PagesTest(TestCase):
got = self.client.get('/web/user.com')
self.assert_equals(404, got.status_code)
def test_user_opted_out(self):
self.user.obj.our_as1 = {'summary': '#nobridge'}
self.user.obj.put()
got = self.client.get('/web/user.com')
self.assert_equals(404, got.status_code)
def test_user_web_redirect(self):
got = self.client.get('/user/user.com')
self.assert_equals(301, got.status_code)

Wyświetl plik

@ -155,6 +155,11 @@ class ProtocolTest(TestCase):
self.assertEqual('user.com', user.handle)
self.assertEqual((Web, 'user.com'), Protocol.for_handle('user.com'))
def test_for_handle_opted_out_user(self):
user = self.make_user(id='user.com', cls=Web, obj_as1={'summary': '#nobot'})
self.assertEqual('user.com', user.handle)
self.assertEqual((None, None), Protocol.for_handle('user.com'))
@patch('dns.resolver.resolve', return_value = dns_answer(
'_atproto.han.dull.', '"did=did:plc:123abc"'))
def test_for_handle_atproto_resolve(self, _):
@ -282,7 +287,7 @@ class ProtocolTest(TestCase):
Fake.load('nope', local=False, remote=False)
def test_actor_key(self):
user = Fake(id='fake:a')
user = self.make_user(id='fake:a', cls=Fake)
a_key = user.key
for expected, obj in [
@ -300,15 +305,22 @@ class ProtocolTest(TestCase):
self.assertEqual(a_key, Fake.actor_key(Object()))
self.assertIsNone(Fake.actor_key(Object(), default_g_user=False))
g.user.obj.our_as1 = {'summary': '#nobot'}
self.assertIsNone(Fake.actor_key(Object(), default_g_user=True))
def test_key_for(self):
self.assertEqual(self.user.key, Protocol.key_for(self.user.key.id()))
Fake(id='fake:other', use_instead=self.user.key).put()
user = Fake(id='fake:other', use_instead=self.user.key).put()
self.assertEqual(self.user.key, Protocol.key_for('fake:other'))
# no stored user
self.assertEqual(ndb.Key('Fake', 'fake:foo'), Protocol.key_for('fake:foo'))
self.user.obj.our_as1 = {'summary': '#nobridge'}
self.user.obj.put()
self.assertIsNone(Protocol.key_for(self.user.key.id()))
def test_targets_checks_blocklisted_per_protocol(self):
"""_targets should call the target protocol's is_blocklisted()."""
# non-ATProto account, ATProto target (PDS) is http://localhost,
@ -909,7 +921,7 @@ class ProtocolReceiveTest(TestCase):
'object': 'fake:post',
}
with self.assertRaises(NoContent):
self.assertEqual('OK', Fake.receive_as1(delete_as1))
Fake.receive_as1(delete_as1)
self.assert_object('fake:post',
deleted=True,
@ -1364,3 +1376,17 @@ class ProtocolReceiveTest(TestCase):
self.client.post('/queue/receive', data={'obj': obj.key.urlsafe()})
obj = Object.get_by_id('fake:post#bridgy-fed-create')
self.assertEqual('ignored', obj.status)
def test_g_user_opted_out(self):
self.make_followers()
g.user.obj.our_as1 = {'summary': '#nobot'}
g.user.obj.put()
with self.assertRaises(NoContent):
Fake.receive_as1({
'id': 'fake:post',
'objectType': 'note',
'author': 'fake:user',
})
self.assertEqual([], Fake.sent)