basic anti-spam: add new Protocol.REQUIRES_AVATAR/NAME constants

...and start returning `blocked` from User.status for them
in-reply-to-bridged
Ryan Barrett 2024-05-11 16:03:07 -07:00
rodzic a7c099fc08
commit de0af66979
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
4 zmienionych plików z 45 dodań i 12 usunięć

Wyświetl plik

@ -132,8 +132,9 @@ class ActivityPub(User, Protocol):
if status:
return status
return util.domain_or_parent_in(util.domain_from_link(self.key.id()),
WEB_OPT_OUT_DOMAINS)
if util.domain_or_parent_in(util.domain_from_link(self.key.id()),
WEB_OPT_OUT_DOMAINS):
return 'opt-out'
@classmethod

Wyświetl plik

@ -341,13 +341,17 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
@ndb.ComputedProperty
def status(self):
"""Whether this user has explicitly opted out of Bridgy Fed.
"""Whether this user is blocked or opted out.
Optional. Current possible values:
* ``opt-out``
Currently just looks for ``#nobridge`` or ``#nobot`` in the profile
description/bio.
* ``opt-out``: if ``#nobridge`` or ``#nobot`` is in the profile
description/bio, or if the user or domain has manually opted out.
Some protocols also have protocol-specific opt out logic, eg Bluesky
accounts that have disabled logged out view.
* ``blocked``: if the user fails our validation checks, eg
``REQUIRES_NAME`` or ``REQUIRES_AVATAR`` if either of those are
``True` for this protocol.
Duplicates ``util.is_opt_out`` in Bridgy!
@ -359,6 +363,10 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if not self.obj or not self.obj.as1:
return None
if ((self.REQUIRES_AVATAR and not self.obj.as1.get('image')) or
(self.REQUIRES_NAME and not self.obj.as1.get('displayName'))):
return 'blocked'
if not as1.is_public(self.obj.as1, unlisted=False):
return 'opt-out'
@ -368,15 +376,13 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if tag in text:
return 'opt-out'
return None
def is_enabled(self, to_proto):
"""Returns True if this user can be bridged to a given protocol.
Reasons this might return False:
* We haven't turned on bridging these two protocols yet.
* The user is opted out.
* The user is on a domain that's opted out.
* The user is opted out or blocked.
* The user is on a domain that's opted out or blocked.
* The from protocol requires opt in, and the user hasn't opted in.
Args:

Wyświetl plik

@ -83,6 +83,12 @@ class Protocol:
HAS_COPIES (bool): whether this protocol is push and needs us to
proactively create "copy" users and objects, as opposed to pulling
converted objects on demand
REQUIRES_AVATAR (bool): whether accounts on this protocol are required
to have a profile picture. If they don't, their ``User.status`` will be
``blocked``.
REQUIRES_NAME (bool): whether accounts on this protocol are required to
have a profile name that's different than their handle or id. If they
don't, their ``User.status`` will be ``blocked``.
DEFAULT_ENABLED_PROTOCOLS (list of str): labels of other protocols that
are automatically enabled for this protocol to bridge into
"""
@ -93,6 +99,8 @@ class Protocol:
CONTENT_TYPE = None
HAS_FOLLOW_ACCEPTS = False
HAS_COPIES = False
REQUIRES_AVATAR = False
REQUIRES_NAME = False
DEFAULT_ENABLED_PROTOCOLS = ()
def __init__(self):
@ -237,7 +245,7 @@ class Protocol:
# load user so that we follow use_instead
existing = cls.get_by_id(id, allow_opt_out=True)
if existing:
if existing.status == 'opt-out':
if existing.status is not None:
return None
return existing.key
@ -359,7 +367,7 @@ class Protocol:
for proto in candidates:
user = proto.query(proto.handle == handle).get()
if user:
if user.status == 'opt-out':
if user.status is not None:
return (None, None)
logger.info(f' user {user.key} owns handle {handle}')
return (proto, user.key.id())

Wyświetl plik

@ -281,6 +281,24 @@ class UserTest(TestCase):
user = User(manual_opt_out=True)
self.assertEqual('opt-out', user.status)
@patch.object(Fake, 'REQUIRES_AVATAR', True)
def test_requires_avatar(self):
user = self.make_user(id='fake:user', cls=Fake,
obj_as1={'displayName': 'Alice'})
self.assertEqual('blocked', user.status)
user.obj.our_as1['image'] = 'http://pic'
self.assertIsNone(user.status)
@patch.object(Fake, 'REQUIRES_NAME', True)
def test_requires_avatar(self):
user = self.make_user(id='fake:user', cls=Fake,
obj_as1={'image': 'http://pic'})
self.assertEqual('blocked', user.status)
user.obj.our_as1['displayName'] = 'Alice'
self.assertIsNone(user.status)
def test_get_copy(self):
user = Fake(id='x')
self.assertEqual('x', user.get_copy(Fake))