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 +4 -4
- data/lib/tasks/yiff_space_tasks.rake +39 -4
- data/lib/yiffspace/logto_management_client.rb +29 -0
- data/lib/yiffspace/utils/user_deduper.rb +66 -0
- data/lib/yiffspace/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7568ca2851a68d6ec6e5a66588f514bf6b95597eb8ed9fba59b6e3c204668e3
|
|
4
|
+
data.tar.gz: 29f61b714e981320ea89c3ea179334a0bbc2ae3fb14db8fc25129e135cb54935
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f214f02c6c540cfe1f9d64eb13c457d3ff2d37d6606ecd0e7656e630880103d8d5744fb04327d822e29d8c34b43fac42372f4c62ed89dbe54c34f32efa9e811
|
|
7
|
+
data.tar.gz: 07d6de0d59c2700a5543b8b1d5cf3c9691af11a5fe9696b03fdbb293a169959c07f0b3e2a6f29116c231dc313736b57da77a89a435a4be6a43645b1f2147ab33
|
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
data/lib/yiffspace/version.rb
CHANGED
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.
|
|
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
|