2024-01-27 22:10:51 +00:00
|
|
|
"""Opts a user out and deletes their bridged profiles in other networks.
|
|
|
|
|
|
|
|
https://github.com/snarfed/bridgy-fed/issues/783
|
|
|
|
|
|
|
|
Usage: opt_out.py [PROTOCOL] [USER_ID] [EXTRA_TARGETS ...]
|
|
|
|
|
|
|
|
PROTOCOL: protocol label, eg web, activitypub, atproto
|
|
|
|
USER_ID: key id of the user entity
|
2024-02-15 21:28:55 +00:00
|
|
|
EXTRA_TARGETS: bridged profiles will also be deleted here. currently AP only!
|
2024-01-27 22:10:51 +00:00
|
|
|
|
|
|
|
Run with:
|
|
|
|
|
|
|
|
source local/bin/activate.csh
|
|
|
|
env PYTHONPATH=. GOOGLE_APPLICATION_CREDENTIALS=service_account_creds.json \
|
|
|
|
python scripts/opt_out.py ...
|
|
|
|
"""
|
|
|
|
import logging
|
|
|
|
import sys
|
|
|
|
|
|
|
|
from google.cloud import ndb
|
|
|
|
from oauth_dropins.webutil import appengine_info
|
|
|
|
appengine_info.DEBUG = False
|
|
|
|
from oauth_dropins.webutil import appengine_config, flask_util, util
|
|
|
|
|
|
|
|
import ids
|
|
|
|
from models import Object, Target
|
|
|
|
import protocol
|
2024-02-15 21:28:55 +00:00
|
|
|
from activitypub import ActivityPub
|
|
|
|
from atproto import ATProto
|
|
|
|
from web import Web
|
2024-01-27 22:10:51 +00:00
|
|
|
from app import app
|
|
|
|
|
|
|
|
appengine_config.error_reporting_client.host = 'localhost:9999'
|
|
|
|
appengine_config.error_reporting_client.secure = False
|
|
|
|
|
|
|
|
# logging.basicConfig(level=logging.DEBUG)
|
|
|
|
|
|
|
|
|
|
|
|
# Includes top 20-40 each from fedidb.org and fediverse.observer on 2024-01-23
|
|
|
|
AP_BASE_TARGETS = [
|
|
|
|
# Diaspora
|
|
|
|
# 'https://joindiaspora.com/',
|
|
|
|
# 'https://diasp.org/',
|
|
|
|
|
|
|
|
# Friendica
|
|
|
|
'https://venera.social/inbox',
|
|
|
|
|
|
|
|
# kbin (not sharedInbox)
|
|
|
|
'https://kbin.social/i/inbox',
|
|
|
|
|
|
|
|
# Lemmy (not sharedInbox)
|
|
|
|
'https://alien.top/inbox',
|
|
|
|
# 'https://enterprise.lemmy.ml/inbox',
|
|
|
|
'https://lemmy.ml/inbox',
|
|
|
|
'https://lemmy.world/inbox',
|
|
|
|
'https://pasta.faith/inbox',
|
|
|
|
|
|
|
|
# Mastodon
|
|
|
|
'https://baraag.net/inbox',
|
|
|
|
'https://c.im/inbox',
|
|
|
|
'https://daystorm.netz.org/inbox',
|
|
|
|
'https://fosstodon.org/inbox',
|
|
|
|
'https://gc2.jp/inbox',
|
|
|
|
'https://hachyderm.io/inbox',
|
|
|
|
'https://indieweb.social/inbox',
|
|
|
|
'https://infosec.exchange/inbox',
|
|
|
|
'https://mas.to/inbox',
|
|
|
|
'https://masto.ai/inbox',
|
|
|
|
'https://mastodon.cloud/inbox',
|
|
|
|
'https://mastodon.online/inbox',
|
|
|
|
'https://mastodon.sdf.org/inbox',
|
|
|
|
'https://mastodon.social/inbox',
|
|
|
|
'https://mastodon.top/inbox',
|
|
|
|
'https://mastodon.uno/inbox',
|
|
|
|
'https://mastodon.world/inbox',
|
|
|
|
'https://mastodonapp.uk/inbox',
|
|
|
|
'https://mstdn.jp/inbox',
|
|
|
|
'https://mstdn.social/inbox',
|
|
|
|
'https://pawoo.net/inbox',
|
|
|
|
'https://pravda.me/inbox',
|
|
|
|
'https://r-sauna.fi/inbox',
|
|
|
|
'https://techhub.social/inbox',
|
|
|
|
'https://universeodon.com/inbox',
|
|
|
|
|
|
|
|
# Misskey
|
|
|
|
'https://misskey.io/inbox',
|
|
|
|
|
|
|
|
# micro.blog
|
|
|
|
'https://micro.blog/activitypub/shared/inbox',
|
|
|
|
|
|
|
|
# PixelFed (not sharedInbox)
|
|
|
|
'https://pixelfed.social/i/actor/inbox',
|
|
|
|
|
|
|
|
# Twitter bridge
|
|
|
|
# 'https://bird.makeup/inbox',
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def run():
|
|
|
|
assert len(sys.argv) >= 3
|
|
|
|
proto, user_id, extra_targets = sys.argv[1], sys.argv[2], sys.argv[3:]
|
|
|
|
|
|
|
|
from_proto = protocol.PROTOCOLS[proto]
|
|
|
|
kind = from_proto._get_kind()
|
2024-02-15 21:28:55 +00:00
|
|
|
|
|
|
|
if proto == 'activitypub' and user_id.count('@') == 1:
|
|
|
|
instance, user = user_id.strip().removeprefix('https://').split('/@')
|
|
|
|
user_id = f'@{user}@{instance}'
|
|
|
|
print(f'Cleaned up user id to {user_id}')
|
|
|
|
|
|
|
|
if (from_proto.owns_id(user_id) is False
|
|
|
|
and from_proto.owns_handle(user_id) is not False):
|
|
|
|
handle = user_id
|
|
|
|
user_id = from_proto.handle_to_id(handle)
|
|
|
|
print(f'Converted {proto} handle {handle} to user id {user_id}')
|
|
|
|
assert from_proto.owns_id(user_id) is not False
|
|
|
|
|
|
|
|
# can't do get_by_id because they might be opted out
|
2024-01-27 22:10:51 +00:00
|
|
|
user = ndb.Key(kind, user_id).get()
|
|
|
|
|
2024-02-15 21:28:55 +00:00
|
|
|
if not user:
|
|
|
|
print(f"user {kind} {user_id} doesn't exist. Creating new and marking as opted out.")
|
|
|
|
from_proto(id=user_id, manual_opt_out=True).put()
|
|
|
|
return
|
|
|
|
|
|
|
|
if user.manual_opt_out:
|
|
|
|
# needed for key_for etc in misc downstream code below
|
|
|
|
user.manual_opt_out = False
|
|
|
|
user.put()
|
2024-01-27 22:10:51 +00:00
|
|
|
|
2024-02-15 21:28:55 +00:00
|
|
|
delete_base_id = user.web_url() if from_proto is Web else user_id
|
|
|
|
delete_id = f'{delete_base_id}#bridgy-fed-delete-{util.now().isoformat()}'
|
|
|
|
delete_as1 = {
|
|
|
|
'objectType': 'activity',
|
|
|
|
'verb': 'delete',
|
|
|
|
'id': delete_id,
|
|
|
|
'actor': user_id,
|
|
|
|
'object': {
|
|
|
|
# needed to make Protocol.translate_ids convert this id as a user id
|
|
|
|
# and not an object id
|
|
|
|
'objectType': 'person',
|
|
|
|
'id': user_id,
|
|
|
|
},
|
|
|
|
}
|
2024-01-27 22:10:51 +00:00
|
|
|
obj = Object(id=delete_id, status='new', source_protocol=from_proto.LABEL,
|
2024-02-15 21:28:55 +00:00
|
|
|
our_as1=delete_as1)
|
2024-01-27 22:10:51 +00:00
|
|
|
obj.put()
|
|
|
|
|
2024-02-05 22:06:26 +00:00
|
|
|
|
2024-05-06 00:13:42 +00:00
|
|
|
targets = list(user.targets(obj, from_user=user).keys())
|
2024-02-15 21:28:55 +00:00
|
|
|
|
|
|
|
if from_proto != ActivityPub:
|
|
|
|
targets += [Target(protocol='activitypub', uri=t)
|
|
|
|
for t in AP_BASE_TARGETS + extra_targets]
|
|
|
|
|
|
|
|
if from_proto != ATProto and user.get_copy(ATProto):
|
2024-06-22 17:51:54 +00:00
|
|
|
targets.append(Target(protocol='atproto', uri=ATProto.PDS_URL))
|
2024-02-15 21:28:55 +00:00
|
|
|
|
2024-02-05 22:06:26 +00:00
|
|
|
obj.undelivered = targets
|
2024-02-15 21:28:55 +00:00
|
|
|
obj.put()
|
2024-02-05 22:06:26 +00:00
|
|
|
|
2024-01-27 22:10:51 +00:00
|
|
|
for target in targets:
|
2024-02-05 22:06:26 +00:00
|
|
|
assert util.is_web(target.uri), f'Non-URL target: {target.uri}'
|
2024-01-27 22:10:51 +00:00
|
|
|
params = {
|
2024-02-15 21:28:55 +00:00
|
|
|
'protocol': target.protocol,
|
2024-01-27 22:10:51 +00:00
|
|
|
'url': target.uri,
|
|
|
|
'obj': obj.key.urlsafe(),
|
|
|
|
'user': user.key.urlsafe(),
|
|
|
|
'force': 'true',
|
|
|
|
}
|
|
|
|
with app.test_request_context('/queue/send', base_url='https://fed.brid.gy/',
|
|
|
|
data=params, headers={
|
|
|
|
flask_util.CLOUD_TASKS_QUEUE_HEADER: '',
|
|
|
|
}):
|
2024-02-15 21:28:55 +00:00
|
|
|
# in ActivityPub, if the actor is already deleted on this instance,
|
|
|
|
# it may return 502 here because it no longer has the actor's public
|
|
|
|
# key, so it can't verify the HTTP Sig. (eg Mastodon does this; it
|
|
|
|
# uses LD Sigs for its actor deletes instead.)
|
|
|
|
#
|
|
|
|
# an alternative is to use the instance actor:
|
|
|
|
#
|
|
|
|
# activitypub.instance_actor().key.urlsafe()
|
|
|
|
#
|
|
|
|
# ...which gets accepted, but I'm not sure all
|
|
|
|
# implementations accept the instance actor as authorized
|
|
|
|
# to delete a different actor.
|
2024-01-27 22:10:51 +00:00
|
|
|
protocol.send_task()
|
|
|
|
|
2024-02-05 22:06:26 +00:00
|
|
|
if not user.manual_opt_out:
|
|
|
|
user.manual_opt_out = True
|
|
|
|
user.put()
|
|
|
|
|
2024-01-27 22:10:51 +00:00
|
|
|
|
|
|
|
with appengine_config.ndb_client.context(), \
|
|
|
|
app.test_request_context(base_url='https://fed.brid.gy/'):
|
|
|
|
run()
|