rhythia-api 241.0.0 → 243.0.0
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.
- package/api/createBeatmap.ts +57 -39
- package/api/editProfile.ts +4 -56
- package/api/editProfileFriend.ts +122 -0
- package/api/executeAdminOperation.ts +1030 -681
- package/api/getAvatarUploadUrl.ts +90 -85
- package/api/getBeatmapPage.ts +2 -0
- package/api/getBeatmapPageById.ts +2 -0
- package/api/getBeatmaps.ts +110 -197
- package/api/getCollection.ts +44 -31
- package/api/getFriends.ts +154 -0
- package/api/getMapUploadUrl.ts +90 -93
- package/api/getProfile.ts +195 -127
- package/api/getScore.ts +2 -0
- package/api/getVideoUploadUrl.ts +90 -85
- package/api/submitScoreInternal.ts +506 -461
- package/api/updateBeatmapPage.ts +6 -0
- package/beatmap-file-urls.json +29398 -0
- package/handleApi.ts +24 -21
- package/index.ts +183 -137
- package/package.json +5 -2
- package/queries/admin_delete_user.sql +42 -39
- package/queries/admin_remove_all_scores.sql +6 -3
- package/queries/admin_remove_score.sql +107 -0
- package/queries/admin_update_profile.sql +22 -0
- package/queries/get_beatmaps_v2.sql +48 -0
- package/queries/get_top_scores_for_beatmap3.sql +47 -38
- package/queries/profile_update_guards.sql +66 -0
- package/types/database.ts +1525 -1414
- package/utils/beatmapFiles.ts +102 -0
- package/utils/beatmapHash.ts +336 -0
- package/utils/beatmapTopScores.ts +68 -84
- package/utils/getUserBySession.ts +3 -1
- package/utils/profileUpdateValidation.ts +51 -0
- package/utils/redis.ts +24 -0
- package/utils/rhrReplay.ts +122 -0
- package/utils/star-calc/sspmParser.ts +294 -160
- package/worker.ts +195 -191
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.admin_remove_score(user_id integer, score_id bigint)
|
|
2
|
+
RETURNS boolean
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
DECLARE
|
|
7
|
+
removed_score public.scores%ROWTYPE;
|
|
8
|
+
best_score RECORD;
|
|
9
|
+
total_sp numeric;
|
|
10
|
+
spin_total_sp numeric;
|
|
11
|
+
BEGIN
|
|
12
|
+
DELETE FROM public.scores
|
|
13
|
+
WHERE id = score_id
|
|
14
|
+
AND "userId" = user_id
|
|
15
|
+
RETURNING * INTO removed_score;
|
|
16
|
+
|
|
17
|
+
IF removed_score.id IS NULL THEN
|
|
18
|
+
RETURN false;
|
|
19
|
+
END IF;
|
|
20
|
+
|
|
21
|
+
SELECT id, awarded_sp
|
|
22
|
+
INTO best_score
|
|
23
|
+
FROM public.scores
|
|
24
|
+
WHERE "userId" = user_id
|
|
25
|
+
AND "beatmapHash" = removed_score."beatmapHash"
|
|
26
|
+
AND passed = true
|
|
27
|
+
ORDER BY awarded_sp DESC NULLS LAST
|
|
28
|
+
LIMIT 1;
|
|
29
|
+
|
|
30
|
+
IF best_score.id IS NULL THEN
|
|
31
|
+
DELETE FROM public.leaderboard_map_user
|
|
32
|
+
WHERE "userId" = user_id
|
|
33
|
+
AND "beatmapHash" = removed_score."beatmapHash";
|
|
34
|
+
ELSE
|
|
35
|
+
INSERT INTO public.leaderboard_map_user (
|
|
36
|
+
"beatmapHash",
|
|
37
|
+
"userId",
|
|
38
|
+
best_score_id,
|
|
39
|
+
best_sp
|
|
40
|
+
)
|
|
41
|
+
VALUES (
|
|
42
|
+
removed_score."beatmapHash",
|
|
43
|
+
user_id,
|
|
44
|
+
best_score.id,
|
|
45
|
+
best_score.awarded_sp
|
|
46
|
+
)
|
|
47
|
+
ON CONFLICT ("beatmapHash", "userId")
|
|
48
|
+
DO UPDATE SET
|
|
49
|
+
best_score_id = excluded.best_score_id,
|
|
50
|
+
best_sp = excluded.best_sp;
|
|
51
|
+
END IF;
|
|
52
|
+
|
|
53
|
+
WITH best_scores AS (
|
|
54
|
+
SELECT DISTINCT ON ("beatmapHash")
|
|
55
|
+
"beatmapHash",
|
|
56
|
+
awarded_sp
|
|
57
|
+
FROM public.scores
|
|
58
|
+
WHERE "userId" = user_id
|
|
59
|
+
AND passed = true
|
|
60
|
+
AND awarded_sp <> 0
|
|
61
|
+
ORDER BY "beatmapHash", awarded_sp DESC
|
|
62
|
+
),
|
|
63
|
+
weighted AS (
|
|
64
|
+
SELECT
|
|
65
|
+
awarded_sp,
|
|
66
|
+
power(0.97, row_number() over (ORDER BY awarded_sp DESC) - 1) AS weight
|
|
67
|
+
FROM best_scores
|
|
68
|
+
)
|
|
69
|
+
SELECT COALESCE(round(sum(awarded_sp * weight) FILTER (WHERE weight >= 0.05), 2), 0)
|
|
70
|
+
INTO total_sp
|
|
71
|
+
FROM weighted;
|
|
72
|
+
|
|
73
|
+
WITH best_scores AS (
|
|
74
|
+
SELECT DISTINCT ON ("beatmapHash")
|
|
75
|
+
"beatmapHash",
|
|
76
|
+
awarded_sp
|
|
77
|
+
FROM public.scores
|
|
78
|
+
WHERE "userId" = user_id
|
|
79
|
+
AND passed = true
|
|
80
|
+
AND spin = true
|
|
81
|
+
AND awarded_sp <> 0
|
|
82
|
+
ORDER BY "beatmapHash", awarded_sp DESC
|
|
83
|
+
),
|
|
84
|
+
weighted AS (
|
|
85
|
+
SELECT
|
|
86
|
+
awarded_sp,
|
|
87
|
+
power(0.97, row_number() over (ORDER BY awarded_sp DESC) - 1) AS weight
|
|
88
|
+
FROM best_scores
|
|
89
|
+
)
|
|
90
|
+
SELECT COALESCE(round(sum(awarded_sp * weight) FILTER (WHERE weight >= 0.05), 2), 0)
|
|
91
|
+
INTO spin_total_sp
|
|
92
|
+
FROM weighted;
|
|
93
|
+
|
|
94
|
+
UPDATE public.profiles
|
|
95
|
+
SET
|
|
96
|
+
play_count = (
|
|
97
|
+
SELECT count(*)
|
|
98
|
+
FROM public.scores
|
|
99
|
+
WHERE "userId" = user_id
|
|
100
|
+
),
|
|
101
|
+
skill_points = total_sp,
|
|
102
|
+
spin_skill_points = spin_total_sp
|
|
103
|
+
WHERE id = user_id;
|
|
104
|
+
|
|
105
|
+
RETURN true;
|
|
106
|
+
END;
|
|
107
|
+
$function$
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.admin_update_profile(user_id integer, profile_data jsonb)
|
|
2
|
+
RETURNS SETOF public.profiles
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
BEGIN
|
|
7
|
+
PERFORM set_config('app.bypass_profile_limits', 'on', true);
|
|
8
|
+
|
|
9
|
+
RETURN QUERY
|
|
10
|
+
UPDATE public.profiles
|
|
11
|
+
SET
|
|
12
|
+
username = CASE WHEN profile_data ? 'username' THEN profile_data->>'username' ELSE profiles.username END,
|
|
13
|
+
"computedUsername" = CASE WHEN profile_data ? 'username' THEN lower(profile_data->>'username') ELSE profiles."computedUsername" END,
|
|
14
|
+
avatar_url = CASE WHEN profile_data ? 'avatar_url' THEN profile_data->>'avatar_url' ELSE profiles.avatar_url END,
|
|
15
|
+
flag = CASE WHEN profile_data ? 'flag' THEN profile_data->>'flag' ELSE profiles.flag END,
|
|
16
|
+
profile_image = CASE WHEN profile_data ? 'profile_image' THEN profile_data->>'profile_image' ELSE profiles.profile_image END,
|
|
17
|
+
verified = CASE WHEN profile_data ? 'verified' THEN (profile_data->>'verified')::boolean ELSE profiles.verified END,
|
|
18
|
+
"verificationDeadline" = CASE WHEN profile_data ? 'verified' THEN 2524608000000 ELSE profiles."verificationDeadline" END
|
|
19
|
+
WHERE id = user_id
|
|
20
|
+
RETURNING profiles.*;
|
|
21
|
+
END;
|
|
22
|
+
$function$
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
DROP FUNCTION IF EXISTS public.get_beatmaps_v2(integer, integer, text, text, text, double precision, double precision, double precision, double precision, bigint, text);
|
|
2
|
+
|
|
3
|
+
CREATE OR REPLACE FUNCTION public.get_beatmaps_v2(page_number integer DEFAULT 1, items_per_page integer DEFAULT 50, text_filter text DEFAULT NULL::text, author_filter text DEFAULT NULL::text, tags_filter text DEFAULT NULL::text, max_stars double precision DEFAULT NULL::double precision, min_length double precision DEFAULT NULL::double precision, max_length double precision DEFAULT NULL::double precision, min_stars double precision DEFAULT NULL::double precision, creator_filter bigint DEFAULT NULL::bigint, status_filter text DEFAULT NULL::text)
|
|
4
|
+
RETURNS TABLE(total_count bigint, owner bigint, created_at timestamp with time zone, id bigint, status text, qualified boolean, tags text, video_url text, map_hash text, playcount bigint, ranked boolean, beatmap_file text, image text, star_rating double precision, difficulty bigint, length double precision, title text, owner_username text)
|
|
5
|
+
LANGUAGE sql
|
|
6
|
+
STABLE
|
|
7
|
+
AS $function$
|
|
8
|
+
SELECT
|
|
9
|
+
COUNT(*) OVER () AS total_count,
|
|
10
|
+
bp.owner::bigint,
|
|
11
|
+
bp.created_at,
|
|
12
|
+
bp.id::bigint,
|
|
13
|
+
bp.status,
|
|
14
|
+
bp.qualified,
|
|
15
|
+
bp.tags,
|
|
16
|
+
bp.video_url,
|
|
17
|
+
bp."mapHash",
|
|
18
|
+
b.playcount::bigint,
|
|
19
|
+
b.ranked,
|
|
20
|
+
b."beatmapFile",
|
|
21
|
+
b.image,
|
|
22
|
+
b."starRating"::double precision,
|
|
23
|
+
b.difficulty::bigint,
|
|
24
|
+
b.length::double precision,
|
|
25
|
+
b.title,
|
|
26
|
+
p.username
|
|
27
|
+
FROM public."beatmapPages" bp
|
|
28
|
+
INNER JOIN public.beatmaps b ON b."beatmapHash" = bp."latestBeatmapHash"
|
|
29
|
+
INNER JOIN public.profiles p ON p.id = bp.owner
|
|
30
|
+
WHERE (text_filter IS NULL OR b.title ILIKE '%' || text_filter || '%' OR p.username ILIKE '%' || text_filter || '%')
|
|
31
|
+
AND (author_filter IS NULL OR p.username ILIKE '%' || author_filter || '%')
|
|
32
|
+
AND (tags_filter IS NULL OR bp.tags ILIKE '%' || tags_filter || '%')
|
|
33
|
+
AND (min_stars IS NULL OR b."starRating" > min_stars)
|
|
34
|
+
AND (max_stars IS NULL OR b."starRating" < max_stars)
|
|
35
|
+
AND (min_length IS NULL OR b.length > min_length)
|
|
36
|
+
AND (max_length IS NULL OR b.length < max_length)
|
|
37
|
+
AND (creator_filter IS NULL OR bp.owner = creator_filter)
|
|
38
|
+
AND (
|
|
39
|
+
status_filter IS NULL
|
|
40
|
+
OR (UPPER(status_filter) = 'QUALIFIED' AND bp.qualified = true)
|
|
41
|
+
OR (UPPER(status_filter) <> 'QUALIFIED' AND bp.status = status_filter)
|
|
42
|
+
)
|
|
43
|
+
ORDER BY
|
|
44
|
+
CASE WHEN UPPER(status_filter) = 'RANKED' THEN bp.ranked_at END DESC NULLS LAST,
|
|
45
|
+
CASE WHEN UPPER(status_filter) = 'RANKED' THEN NULL ELSE bp.created_at END DESC NULLS LAST
|
|
46
|
+
LIMIT items_per_page
|
|
47
|
+
OFFSET ((page_number - 1) * items_per_page);
|
|
48
|
+
$function$
|
|
@@ -1,38 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
s.
|
|
15
|
-
s.
|
|
16
|
-
s.
|
|
17
|
-
s.
|
|
18
|
-
s.
|
|
19
|
-
s.
|
|
20
|
-
s.
|
|
21
|
-
s.
|
|
22
|
-
s.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
1
|
+
DROP FUNCTION IF EXISTS public.get_top_scores_for_beatmap3(beatmap_hash text);
|
|
2
|
+
|
|
3
|
+
CREATE OR REPLACE FUNCTION public.get_top_scores_for_beatmap3(beatmap_hash text, score_limit integer DEFAULT 50)
|
|
4
|
+
RETURNS TABLE(id bigint, awarded_sp numeric, created_at timestamp without time zone, misses integer, mods json, passed boolean, "replayHwid" text, "songId" text, speed numeric, spin boolean, "userId" bigint, username text, avatar_url text, accuracy numeric)
|
|
5
|
+
LANGUAGE sql
|
|
6
|
+
STABLE
|
|
7
|
+
AS $function$
|
|
8
|
+
WITH bm AS (
|
|
9
|
+
SELECT "noteCount"
|
|
10
|
+
FROM beatmaps
|
|
11
|
+
WHERE "beatmapHash" = beatmap_hash
|
|
12
|
+
)
|
|
13
|
+
SELECT
|
|
14
|
+
s.id,
|
|
15
|
+
s.awarded_sp,
|
|
16
|
+
s.created_at,
|
|
17
|
+
s.misses,
|
|
18
|
+
s.mods,
|
|
19
|
+
s.passed,
|
|
20
|
+
s."replayHwid",
|
|
21
|
+
s."songId",
|
|
22
|
+
s.speed,
|
|
23
|
+
s.spin,
|
|
24
|
+
s."userId",
|
|
25
|
+
p.username,
|
|
26
|
+
p.avatar_url,
|
|
27
|
+
CASE
|
|
28
|
+
WHEN bm."noteCount" > 0 THEN
|
|
29
|
+
ROUND(((bm."noteCount" - s.misses) / bm."noteCount" * 100)::numeric, 2)
|
|
30
|
+
ELSE 0
|
|
31
|
+
END AS accuracy
|
|
32
|
+
FROM leaderboard_map_user l
|
|
33
|
+
JOIN scores s ON s.id = l.best_score_id
|
|
34
|
+
JOIN profiles p ON s."userId" = p.id
|
|
35
|
+
CROSS JOIN bm
|
|
36
|
+
WHERE l."beatmapHash" = beatmap_hash
|
|
37
|
+
AND s.passed = true
|
|
38
|
+
AND p.ban <> 'excluded'
|
|
39
|
+
AND EXISTS (
|
|
40
|
+
SELECT 1
|
|
41
|
+
FROM scores recent
|
|
42
|
+
WHERE recent."userId" = s."userId"
|
|
43
|
+
AND recent.created_at >= now() - interval '30 days'
|
|
44
|
+
)
|
|
45
|
+
ORDER BY l.best_sp DESC
|
|
46
|
+
LIMIT LEAST(GREATEST(COALESCE(score_limit, 50), 1), 50);
|
|
47
|
+
$function$
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.handle_profile_flag_change()
|
|
2
|
+
RETURNS trigger
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
AS $function$
|
|
5
|
+
begin
|
|
6
|
+
if new.flag is not distinct from old.flag then
|
|
7
|
+
return new;
|
|
8
|
+
end if;
|
|
9
|
+
|
|
10
|
+
if current_setting('app.bypass_profile_limits', true) = 'on' then
|
|
11
|
+
return new;
|
|
12
|
+
end if;
|
|
13
|
+
|
|
14
|
+
if exists (
|
|
15
|
+
select 1
|
|
16
|
+
from public."profileFlags" pf
|
|
17
|
+
where pf.profile_id = old.id
|
|
18
|
+
) then
|
|
19
|
+
raise exception 'Flag can only be changed once'
|
|
20
|
+
using errcode = 'P0001';
|
|
21
|
+
end if;
|
|
22
|
+
|
|
23
|
+
insert into public."profileFlags" (profile_id, flag, changed_at)
|
|
24
|
+
values (old.id, old.flag, now());
|
|
25
|
+
|
|
26
|
+
return new;
|
|
27
|
+
end;
|
|
28
|
+
$function$;
|
|
29
|
+
|
|
30
|
+
CREATE OR REPLACE FUNCTION public.handle_profile_username_change()
|
|
31
|
+
RETURNS trigger
|
|
32
|
+
LANGUAGE plpgsql
|
|
33
|
+
AS $function$
|
|
34
|
+
declare
|
|
35
|
+
latest_change_at timestamptz;
|
|
36
|
+
begin
|
|
37
|
+
if new.username is not distinct from old.username then
|
|
38
|
+
return new;
|
|
39
|
+
end if;
|
|
40
|
+
|
|
41
|
+
if current_setting('app.bypass_profile_limits', true) = 'on' then
|
|
42
|
+
return new;
|
|
43
|
+
end if;
|
|
44
|
+
|
|
45
|
+
select pu.changed_at
|
|
46
|
+
into latest_change_at
|
|
47
|
+
from public."profileUsernames" pu
|
|
48
|
+
where pu.profile_id = old.id
|
|
49
|
+
order by pu.changed_at desc
|
|
50
|
+
limit 1;
|
|
51
|
+
|
|
52
|
+
if latest_change_at is not null
|
|
53
|
+
and latest_change_at + interval '6 months' > now() then
|
|
54
|
+
raise exception 'Username can only be changed once every 6 months'
|
|
55
|
+
using errcode = 'P0001',
|
|
56
|
+
detail = 'next_username_change_at=' || (latest_change_at + interval '6 months');
|
|
57
|
+
end if;
|
|
58
|
+
|
|
59
|
+
if old.username is not null and btrim(old.username) <> '' then
|
|
60
|
+
insert into public."profileUsernames" (profile_id, username, changed_at)
|
|
61
|
+
values (old.id, old.username, now());
|
|
62
|
+
end if;
|
|
63
|
+
|
|
64
|
+
return new;
|
|
65
|
+
end;
|
|
66
|
+
$function$;
|