Skip to content

Commit

Permalink
re-activate tombstoned bridged Bluesky accounts by recreating them fr…
Browse files Browse the repository at this point in the history
…om scratch

...with a new DID etc. for #1446
  • Loading branch information
snarfed committed Nov 14, 2024
1 parent 0e2ba03 commit aa32726
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 19 deletions.
44 changes: 27 additions & 17 deletions atproto.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
next_tid,
parse_at_uri,
service_jwt,
TombstonedRepo,
TOMBSTONED,
)
import brevity
import dag_json
Expand Down Expand Up @@ -387,10 +387,21 @@ def create_for(cls, user):
assert not isinstance(user, ATProto)

if copy_did := user.get_copy(ATProto):
# already bridged and inactive
repo = arroba.server.storage.load_repo(copy_did)
arroba.server.storage.activate_repo(repo)
common.create_task(queue='atproto-commit')
return
assert repo.status
if repo.status == TOMBSTONED:
# tombstoned repos can't be reactivated, have to wipe and start fresh
user.copies = []
if user.obj:
user.obj.copies = []
user.obj.put()
# fall through to create new DID, repo
elif repo.status:
# deactivated or deleted
arroba.server.storage.activate_repo(repo)
common.create_task(queue='atproto-commit')
return

# create new DID, repo
# PDS URL shouldn't include trailing slash!
Expand Down Expand Up @@ -501,10 +512,10 @@ def set_username(to_cls, user, username):
if resolved != copy_did:
raise RuntimeError(f"""<p>You'll need to connect that domain to your bridged Bluesky account, either <a href="https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial">with DNS</a> <a href="https://atproto.com/specs/handle#handle-resolution">or HTTP</a>. Your DID is: <code>{copy_did}</code><p>Once you're done, <a href="https://bsky-debug.app/handle?handle={username}">check your work here</a>, then DM me <em>username {username}</em> again.""")

try:
repo = arroba.server.storage.load_repo(copy_did)
except TombstonedRepo:
logger.info(f'repo for {did} is tombstoned, giving up')
repo = arroba.server.storage.load_repo(copy_did)
assert repo
if repo.status:
logger.info(f'{repo.did} is {repo.status}, giving up')
return False

logger.info(f'Setting ATProto handle for {user.key.id()} to {username}')
Expand Down Expand Up @@ -589,13 +600,12 @@ def send(to_cls, obj, url, from_user=None, orig_obj_id=None):
return False

# load repo
try:
repo = arroba.server.storage.load_repo(did)
except TombstonedRepo:
logger.info(f'repo for {did} is tombstoned, giving up')
repo = arroba.server.storage.load_repo(did)
assert repo
if repo.status:
logger.info(f'{repo.did} is {repo.status}, giving up')
return False

assert repo
repo.callback = lambda _: common.create_task(queue='atproto-commit')

# non-commit operations:
Expand Down Expand Up @@ -920,10 +930,10 @@ def create_report(*, input, from_user):
if not repo_did:
return False

try:
repo = arroba.server.storage.load_repo(repo_did)
except TombstonedRepo:
logger.info(f'repo for {repo_did} is tombstoned, giving up')
repo = arroba.server.storage.load_repo(repo_did)
assert repo
if repo.status:
logger.info(f'{repo.did} is {repo.status}, giving up')
return False

mod_host = os.environ['MOD_SERVICE_HOST']
Expand Down
3 changes: 1 addition & 2 deletions templates/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,8 @@ <h3 id="about">About</h3>
<li class="answer">
<p>If you're on the fediverse or Bluesky, and you've opted in but now want to opt out, block the Bridgy Fed bot user for the network you want to opt out of. For example, on the fediverse, block <code>@[email protected]</code>. On Bluesky, block <a href="https://bsky.app/profile/ap.brid.gy">@ap.brid.gy</a>.</p>
<p>If you're on the web, feel free to <a href="mailto:[email protected]">email me</a>, or you can put the text <code>#nobridge</code> in the <a href="#web-profile">profile on your home page</a> and then <a href="#update-profile">update your profile</a> on <a href="#user-page">your user page</a>.</p>
<p>Once you've done this, Bridgy Fed will delete your bridged profile in that network, and it will no longer bridge any of your posts or interactions there.</p>
<p>Once you've done this, Bridgy Fed will deactivate your bridged profile in that network, and it will no longer bridge any of your posts or interactions there.</p>
<p>You can undo this later by un-blocking and re-following the bot user, but depending on the network, not everything will be perfectly restored. For example, when you disable bridging into the fediverse, the bridged fediverse account is deleted and all followers are disconnected. If you later re-enable it, fediverse users will have to search for your account by address manually to find and follow it again.</p>
<p>(For accounts bridged into Bluesky, you can only undo opting out if you first blocked the bot user after 2024-10-22. Before that date, we disabled Bluesky accounts <a href="https://github.com/snarfed/bridgy-fed/issues/1130#issuecomment-2229901486">with a different technique that can't yet be undone</a>.)
</li>

<li id="dms" class="question">Why is Bridgy Fed chatting or DMing with me?</li>
Expand Down
52 changes: 52 additions & 0 deletions tests/test_atproto.py
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,58 @@ def test_create_for_already_exists(self, mock_create_task):

mock_create_task.assert_called() # atproto-commit

@patch('atproto.DEBUG', new=False)
@patch.object(atproto.dns_discovery_api, 'resourceRecordSets')
@patch('google.cloud.dns.client.ManagedZone', autospec=True)
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
@patch('requests.post', return_value=requests_response('OK')) # create DID on PLC
def test_create_for_tombstoned(self, mock_post, mock_create_task, mock_zone,
mock_rrsets):
"""Should wipe existing copies and start from scratch with a new DID."""
mock_zone.return_value = zone = MagicMock()
zone.resource_record_set = MagicMock()
mock_rrsets.return_value = rrsets = MagicMock()
rrsets.list.return_value = list_ = MagicMock()
list_.execute.return_value = {'rrsets': []}

Fake.fetchable = {'fake:profile:user': ACTOR_AS}

user = self.make_user_and_repo()#obj_as1={'id': 'fake:profile:user'})
orig_did = user.get_copy(ATProto)
assert orig_did

user.obj.copies = [Target(uri='at://orig', protocol='atproto')]
user.obj.put()

repo = arroba.server.storage.load_repo('did:plc:user')
arroba.server.storage.tombstone_repo(repo)

ATProto.create_for(self.user)

# check user and repo
user = self.user.key.get()
did = user.key.get().get_copy(ATProto)
self.assertNotEqual(orig_did, did)
self.assertEqual([Target(uri=did, protocol='atproto')], user.copies)

self.assertEqual(f'at://{did}/app.bsky.actor.profile/self',
user.obj.get_copy(ATProto))

repo = arroba.server.storage.load_repo(did)
self.assertIsNone(repo.status)

# check #account event
seq = self.storage.last_seq(SUBSCRIBE_REPOS_NSID)
self.assertEqual({
'$type': 'com.atproto.sync.subscribeRepos#account',
'seq': seq,
'did': did,
'time': NOW.isoformat(),
'active': True,
}, next(self.storage.read_events_by_seq(seq)))

mock_create_task.assert_called() # atproto-commit

@patch('atproto.DEBUG', new=False)
@patch.object(google.cloud.dns.client.ManagedZone, 'changes')
@patch.object(atproto.dns_discovery_api, 'resourceRecordSets')
Expand Down

0 comments on commit aa32726

Please sign in to comment.