Module: YiffSpace::Utils::UserDeduper

Defined in:
lib/yiffspace/utils/user_deduper.rb

Overview

get_or_create_user is check-then-act (query Logto, then create) with no locking of its own - two concurrent calls for a discord id that doesn't exist yet will both create a user, and the loser's discord-identity-attach step fails once the winner's identity claims it first. That leaves an identity-less "orphan" user behind in Logto. This scans the whole user base to find those orphans (and any other duplicate/ambiguous state) so they can be cleaned up.

Constant Summary collapse

AVATAR_ID_PATTERN =

create_user sets a new user's avatar to the discord CDN URL for the discord id it was created for, before the identity-attach step runs - so even an orphan that never got its identity attached still reveals which discord id it was meant for.

%r{cdn\.discordapp\.com/avatars/(\d+)/}

Class Method Summary collapse

Class Method Details

.avatar_discord_id_for(user) ⇒ Object



24
25
26
27
# File 'lib/yiffspace/utils/user_deduper.rb', line 24

def avatar_discord_id_for(user)
  match = user.data.avatar.to_s.match(AVATAR_ID_PATTERN)
  match && match[1]
end

.discord_id_for(user) ⇒ Object



20
21
22
# File 'lib/yiffspace/utils/user_deduper.rb', line 20

def discord_id_for(user)
  user.data.identities&.discord&.userId || avatar_discord_id_for(user)
end

.linked?(user) ⇒ Boolean

Returns:

  • (Boolean)


29
30
31
# File 'lib/yiffspace/utils/user_deduper.rb', line 29

def linked?(user)
  user.data.identities&.discord&.userId.present?
end

.scan(management) ⇒ Object

Returns a hash with:

  • :conflicts - discord id => users, for ids where more than one user has the identity actually attached. Ambiguous; always needs manual review, never auto-deleted.
  • :deletable_orphans - [orphan, keeper] pairs, where orphan has no identity attached but its avatar points at a discord id claimed by exactly one other (linked) user. This is the confirmed race-loser case - safe to delete.
  • :unresolved_orphans - users with no identity attached and no corresponding linked user (still mid-race, or the winner can't be identified). Reported only, never auto-deleted.


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/yiffspace/utils/user_deduper.rb', line 43

def scan(management)
  users = management.list_users
  linked, unlinked = users.partition { |u| linked?(u) }

  by_discord_id = linked.group_by { |u| u.data.identities.discord.userId }
  conflicts = by_discord_id.select { |_id, group| group.length > 1 }

  deletable_orphans = []
  unresolved_orphans = []
  unlinked.each do |user|
    discord_id = avatar_discord_id_for(user)
    keeper = discord_id && by_discord_id[discord_id]
    if keeper&.length == 1
      deletable_orphans << [user, keeper.first]
    else
      unresolved_orphans << user
    end
  end

  { conflicts: conflicts, deletable_orphans: deletable_orphans, unresolved_orphans: unresolved_orphans }
end