Skip to content

find_missing_follows.rb

Example: fetch the list of accounts followed by a given user and check which of them have been deleted / deactivated.

rb
require 'didkit'
require 'minisky'

$handle = ARGV[0]

if $handle.nil?
  puts "Usage: #{$PROGRAM_NAME} <handle | did>"
  exit 1
end

$did = DID.resolve_handle($handle)

if $did.nil?
  puts "Couldn't resolve handle: #{$handle}"
  exit 1
end

# first, we'll get the list of all follow records from the user's PDS
# (and to do that, we need to look up the PDS hostname):

pds_host = $did.document.pds_host
pds = Minisky.new(pds_host, nil, progress: '.')

print "Fetching all follows of #{$handle} from #{pds_host}: "

follows = pds.fetch_all('com.atproto.repo.listRecords',
  { repo: $did, collection: 'app.bsky.graph.follow', limit: 100 },
  field: 'records')

puts
puts "Found #{follows.length} follows."

# next, we'll check which of those are visible on the AppView:

appview = Minisky.new('api.bsky.app', nil)

profiles = []
i = 0

puts
print "Fetching profiles of all followed accounts: "

# getProfiles lets us load multiple profiles in one request, but only up to 25,
# so we slice them into batches of 25 each
#
# (alternatively, we could also use #fetch_all and the getFollowers endpoint
# here to achieve more or less the same thing)

while i < follows.length
  batch = follows[i...i+25]
  dids = batch.map { |x| x['value']['subject'] }
  print '.'
  result = appview.get_request('app.bsky.actor.getProfiles', { actors: dids })
  profiles += result['profiles']
  i += 25
end

# now, here are the DIDs that are on the follows list,
# but aren't being returned from getProfiles:

missing = follows.map { |x| x['value']['subject'] } - profiles.map { |x| x['did'] }

puts
puts "#{missing.length} followed accounts are missing:"
puts

# for each of those, try to find out what is the reason:

missing.each do |did|
  begin
    did = DID.new(did)
    document = did.document
    handle = document.handles.first
  rescue StandardError
    puts "#{did} (?) => DID not found"
    next
  end

  begin
    status = did.account_status || 'deleted'
    puts "#{did} (@#{handle}) => #{status}"
  rescue StandardError => e
    puts "#{did} (@#{handle}) => Error: #{e}"
  end
end