yiffspace 0.0.22 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55b429503eee52d6ce298af0e2ce3e65327f630c6f7fc0114e58d49a324c2fd3
4
- data.tar.gz: 017a1eaaf6a5ee571fa8e29cc3f7a2da617d1b6de78f9d5d834bb248a9212326
3
+ metadata.gz: d7568ca2851a68d6ec6e5a66588f514bf6b95597eb8ed9fba59b6e3c204668e3
4
+ data.tar.gz: 29f61b714e981320ea89c3ea179334a0bbc2ae3fb14db8fc25129e135cb54935
5
5
  SHA512:
6
- metadata.gz: 62391e56655903180b26837749b124f5c4a7f6ac4180815be23bcf83295f221af0f6433d824664caf8f1c10f4d81f0074630e66b3bf956c82472eecf9e4a2ea8
7
- data.tar.gz: 86671d5ef0e7c213ae10721e9024f0dafc590f4b464b2311c549e666f87d2368838efa56e54ead37d16e17d1918ad465b611b3df25f3130c9f2675ed4d49dda5
6
+ metadata.gz: 4f214f02c6c540cfe1f9d64eb13c457d3ff2d37d6606ecd0e7656e630880103d8d5744fb04327d822e29d8c34b43fac42372f4c62ed89dbe54c34f32efa9e811
7
+ data.tar.gz: 07d6de0d59c2700a5543b8b1d5cf3c9691af11a5fe9696b03fdbb293a169959c07f0b3e2a6f29116c231dc313736b57da77a89a435a4be6a43645b1f2147ab33
@@ -1,6 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # desc "Explaining what the task does"
4
- # task :yiffspace do
5
- # # Task goes here
6
- # end
3
+ namespace(:yiff_space) do
4
+ desc("Report and interactively remove duplicate/orphaned Logto users left behind by " \
5
+ "get_or_create_user races. Prompts for confirmation before deleting each one. " \
6
+ "Pass AUTH_CLIENT=name to use a registered auth client other than :default " \
7
+ "(e.g. AUTH_CLIENT=yiffyapi_manage).")
8
+ task(dedupe_users: :environment) do
9
+ client_name = (ENV["AUTH_CLIENT"] || YiffSpace::Auth::DEFAULT_CLIENT_NAME).to_sym
10
+ management = YiffSpace::Auth[client_name].logto_management
11
+
12
+ puts("Scanning users via auth client #{client_name.inspect}...")
13
+ result = YiffSpace::Utils::UserDeduper.scan(management)
14
+
15
+ puts("\n#{result[:conflicts].size} discord id(s) with more than one linked user (ambiguous - not handled by this task, review manually):")
16
+ result[:conflicts].each do |discord_id, group|
17
+ puts(" discordId=#{discord_id}: #{group.map { |u| "#{u.id} (#{u.name})" }.join(', ')}")
18
+ end
19
+
20
+ puts("\n#{result[:unresolved_orphans].size} orphaned user(s) with no matching linked account (not handled by this task, review manually):")
21
+ result[:unresolved_orphans].each do |user|
22
+ puts(" #{user.id} (name=#{user.name}, avatar=#{user.data.avatar})")
23
+ end
24
+
25
+ puts("\n#{result[:deletable_orphans].size} confirmed duplicate(s) - orphan left by a losing get_or_create_user race:")
26
+ if result[:deletable_orphans].empty?
27
+ puts(" none found")
28
+ else
29
+ result[:deletable_orphans].each do |orphan, keeper|
30
+ print(" delete #{orphan.id} (name=#{orphan.name}, avatar=#{orphan.data.avatar}), keeping #{keeper.id} (name=#{keeper.name}) for discordId=#{YiffSpace::Utils::UserDeduper.discord_id_for(keeper)}? [y/N] ")
31
+ answer = $stdin.gets&.strip&.downcase
32
+ if answer == "y"
33
+ management.delete_user(orphan.id)
34
+ puts(" deleted #{orphan.id}")
35
+ else
36
+ puts(" skipped")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -75,6 +75,35 @@ module YiffSpace
75
75
  get_user(id) || create_user(id)
76
76
  end
77
77
 
78
+ # Pages through every user in the tenant. Used by maintenance tasks (e.g. the
79
+ # dedupe_users rake task) - there's no discordId to filter by when scanning the
80
+ # whole user base for duplicates/orphans.
81
+ def list_users(page_size: 100)
82
+ users = []
83
+ page = 1
84
+ loop do
85
+ response = HTTParty.get("#{auth.server_url}/api/users", {
86
+ headers: { "Authorization" => "Bearer #{get_token}" },
87
+ query: { page: page, page_size: page_size },
88
+ })
89
+ raise("failed to list users: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200 || !response.parsed_response.is_a?(Array)
90
+
91
+ batch = response.parsed_response
92
+ users.concat(batch.map { |u| Auth::ApiUser.new(u) })
93
+ break if batch.length < page_size
94
+
95
+ page += 1
96
+ end
97
+ users
98
+ end
99
+
100
+ def delete_user(logto_id)
101
+ response = HTTParty.delete("#{auth.server_url}/api/users/#{logto_id}", { headers: { "Authorization" => "Bearer #{get_token}" } })
102
+ return true if [204, 404].include?(response.code)
103
+
104
+ raise("failed to delete user #{logto_id}: #{response.code} #{response.message}\n#{response.parsed_response.inspect}")
105
+ end
106
+
78
107
  def get_user_by_id(logto_id)
79
108
  response = HTTParty.get("#{auth.server_url}/api/users/#{logto_id}", { headers: { "Authorization" => "Bearer #{get_token}" } })
80
109
  return nil if response.code == 404
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ # get_or_create_user is check-then-act (query Logto, then create) with no locking
6
+ # of its own - two concurrent calls for a discord id that doesn't exist yet will
7
+ # both create a user, and the loser's discord-identity-attach step fails once the
8
+ # winner's identity claims it first. That leaves an identity-less "orphan" user
9
+ # behind in Logto. This scans the whole user base to find those orphans (and any
10
+ # other duplicate/ambiguous state) so they can be cleaned up.
11
+ module UserDeduper
12
+ # create_user sets a new user's avatar to the discord CDN URL for the discord id
13
+ # it was created for, before the identity-attach step runs - so even an orphan
14
+ # that never got its identity attached still reveals which discord id it was
15
+ # meant for.
16
+ AVATAR_ID_PATTERN = %r{cdn\.discordapp\.com/avatars/(\d+)/}
17
+
18
+ module_function
19
+
20
+ def discord_id_for(user)
21
+ user.data.identities&.discord&.userId || avatar_discord_id_for(user)
22
+ end
23
+
24
+ def avatar_discord_id_for(user)
25
+ match = user.data.avatar.to_s.match(AVATAR_ID_PATTERN)
26
+ match && match[1]
27
+ end
28
+
29
+ def linked?(user)
30
+ user.data.identities&.discord&.userId.present?
31
+ end
32
+
33
+ # Returns a hash with:
34
+ # - :conflicts - discord id => users, for ids where more than one user has the
35
+ # identity actually attached. Ambiguous; always needs manual review, never
36
+ # auto-deleted.
37
+ # - :deletable_orphans - [orphan, keeper] pairs, where orphan has no identity
38
+ # attached but its avatar points at a discord id claimed by exactly one other
39
+ # (linked) user. This is the confirmed race-loser case - safe to delete.
40
+ # - :unresolved_orphans - users with no identity attached and no corresponding
41
+ # linked user (still mid-race, or the winner can't be identified). Reported
42
+ # only, never auto-deleted.
43
+ def scan(management)
44
+ users = management.list_users
45
+ linked, unlinked = users.partition { |u| linked?(u) }
46
+
47
+ by_discord_id = linked.group_by { |u| u.data.identities.discord.userId }
48
+ conflicts = by_discord_id.select { |_id, group| group.length > 1 }
49
+
50
+ deletable_orphans = []
51
+ unresolved_orphans = []
52
+ unlinked.each do |user|
53
+ discord_id = avatar_discord_id_for(user)
54
+ keeper = discord_id && by_discord_id[discord_id]
55
+ if keeper&.length == 1
56
+ deletable_orphans << [user, keeper.first]
57
+ else
58
+ unresolved_orphans << user
59
+ end
60
+ end
61
+
62
+ { conflicts: conflicts, deletable_orphans: deletable_orphans, unresolved_orphans: unresolved_orphans }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YiffSpace
4
- VERSION = "0.0.22"
4
+ VERSION = "0.0.23"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yiffspace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.22
4
+ version: 0.0.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Donovan_DMC
@@ -231,6 +231,7 @@ files:
231
231
  - lib/yiffspace/utils/table_builder.rb
232
232
  - lib/yiffspace/utils/trace_logger.rb
233
233
  - lib/yiffspace/utils/user_attribute.rb
234
+ - lib/yiffspace/utils/user_deduper.rb
234
235
  - lib/yiffspace/utils/user_like.rb
235
236
  - lib/yiffspace/utils/user_resolvable.rb
236
237
  - lib/yiffspace/utils/user_to_id.rb