rhythia-api 229.0.0 → 231.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/checkQualified.ts +83 -0
- package/api/getBeatmapPage.ts +65 -28
- package/api/getBeatmapPageById.ts +61 -24
- package/api/getUserScores.ts +18 -15
- package/api/{nominateMap.ts → qualifyMap.ts} +86 -82
- package/api/submitScoreInternal.ts +449 -426
- package/api/{approveMap.ts → vetoMap.ts} +94 -78
- package/handleApi.ts +3 -7
- package/index.ts +134 -85
- package/package.json +10 -2
- package/queries/admin_delete_user.sql +39 -0
- package/queries/admin_exclude_user.sql +21 -0
- package/queries/admin_invalidate_ranked_scores.sql +18 -0
- package/queries/admin_log_action.sql +10 -0
- package/queries/admin_profanity_clear.sql +29 -0
- package/queries/admin_remove_all_scores.sql +29 -0
- package/queries/admin_restrict_user.sql +21 -0
- package/queries/admin_search_users.sql +24 -0
- package/queries/admin_silence_user.sql +21 -0
- package/queries/admin_unban_user.sql +21 -0
- package/queries/get_badge_leaderboard.sql +50 -0
- package/queries/get_clan_leaderboard.sql +68 -0
- package/queries/get_collections_v4.sql +109 -0
- package/queries/get_top_scores_for_beatmap.sql +44 -0
- package/queries/get_user_by_email.sql +32 -0
- package/queries/get_user_scores_lastday.sql +47 -0
- package/queries/get_user_scores_reign.sql +31 -0
- package/queries/get_user_scores_top_and_stats.sql +84 -0
- package/queries/grant_special_badges.sql +69 -0
- package/types/database.ts +1224 -1179
- package/utils/getUserBySession.ts +1 -1
- package/utils/requestUtils.ts +127 -88
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.admin_remove_all_scores(user_id integer)
|
|
2
|
+
RETURNS integer
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
DECLARE
|
|
7
|
+
deleted_count INTEGER;
|
|
8
|
+
BEGIN
|
|
9
|
+
-- Delete all scores for this user and count them
|
|
10
|
+
WITH deleted AS (
|
|
11
|
+
DELETE FROM public.scores
|
|
12
|
+
WHERE "userId" = user_id
|
|
13
|
+
RETURNING *
|
|
14
|
+
)
|
|
15
|
+
SELECT COUNT(*) INTO deleted_count FROM deleted;
|
|
16
|
+
|
|
17
|
+
-- Update user stats to reflect score removal
|
|
18
|
+
UPDATE public.profiles
|
|
19
|
+
SET
|
|
20
|
+
play_count = 0,
|
|
21
|
+
squares_hit = 0,
|
|
22
|
+
total_score = 0,
|
|
23
|
+
skill_points = 0,
|
|
24
|
+
spin_skill_points = 0
|
|
25
|
+
WHERE id = user_id;
|
|
26
|
+
|
|
27
|
+
RETURN deleted_count;
|
|
28
|
+
END;
|
|
29
|
+
$function$
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.admin_restrict_user(user_id integer)
|
|
2
|
+
RETURNS boolean
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
DECLARE
|
|
7
|
+
success BOOLEAN;
|
|
8
|
+
BEGIN
|
|
9
|
+
UPDATE public.profiles
|
|
10
|
+
SET ban = 'restricted'::"banTypes",
|
|
11
|
+
"bannedAt" = EXTRACT(EPOCH FROM NOW())::bigint
|
|
12
|
+
WHERE id = user_id;
|
|
13
|
+
|
|
14
|
+
success := EXISTS (
|
|
15
|
+
SELECT 1 FROM public.profiles
|
|
16
|
+
WHERE id = user_id AND ban = 'restricted'::"banTypes"
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
RETURN success;
|
|
20
|
+
END;
|
|
21
|
+
$function$
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.admin_search_users(search_text text)
|
|
2
|
+
RETURNS SETOF profiles
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
BEGIN
|
|
7
|
+
RETURN QUERY
|
|
8
|
+
SELECT *
|
|
9
|
+
FROM public.profiles
|
|
10
|
+
WHERE
|
|
11
|
+
username ILIKE '%' || search_text || '%'
|
|
12
|
+
OR "computedUsername" ILIKE '%' || search_text || '%'
|
|
13
|
+
OR CAST(uid AS TEXT) ILIKE '%' || search_text || '%'
|
|
14
|
+
OR CAST(id AS TEXT) = search_text
|
|
15
|
+
OR about_me ILIKE '%' || search_text || '%'
|
|
16
|
+
OR flag ILIKE '%' || search_text || '%'
|
|
17
|
+
OR EXISTS (
|
|
18
|
+
SELECT 1
|
|
19
|
+
FROM public.passkeys
|
|
20
|
+
WHERE passkeys.id = profiles.id AND passkeys.email ILIKE '%' || search_text || '%'
|
|
21
|
+
)
|
|
22
|
+
ORDER BY id;
|
|
23
|
+
END;
|
|
24
|
+
$function$
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.admin_silence_user(user_id integer)
|
|
2
|
+
RETURNS boolean
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
DECLARE
|
|
7
|
+
success BOOLEAN;
|
|
8
|
+
BEGIN
|
|
9
|
+
UPDATE public.profiles
|
|
10
|
+
SET ban = 'silenced'::"banTypes",
|
|
11
|
+
"bannedAt" = EXTRACT(EPOCH FROM NOW())::bigint
|
|
12
|
+
WHERE id = user_id;
|
|
13
|
+
|
|
14
|
+
success := EXISTS (
|
|
15
|
+
SELECT 1 FROM public.profiles
|
|
16
|
+
WHERE id = user_id AND ban = 'silenced'::"banTypes"
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
RETURN success;
|
|
20
|
+
END;
|
|
21
|
+
$function$
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.admin_unban_user(user_id integer)
|
|
2
|
+
RETURNS boolean
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
DECLARE
|
|
7
|
+
success BOOLEAN;
|
|
8
|
+
BEGIN
|
|
9
|
+
UPDATE public.profiles
|
|
10
|
+
SET ban = 'cool',
|
|
11
|
+
"bannedAt" = NULL
|
|
12
|
+
WHERE id = user_id;
|
|
13
|
+
|
|
14
|
+
success := EXISTS (
|
|
15
|
+
SELECT 1 FROM public.profiles
|
|
16
|
+
WHERE id = user_id AND ban IS NULL
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
RETURN success;
|
|
20
|
+
END;
|
|
21
|
+
$function$
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.get_badge_leaderboard(p_limit integer DEFAULT 100)
|
|
2
|
+
RETURNS TABLE(id integer, display_name text, avatar_url text, special_badge_count integer)
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
BEGIN
|
|
7
|
+
RETURN QUERY
|
|
8
|
+
WITH special_badges AS (
|
|
9
|
+
SELECT unnest(ARRAY[
|
|
10
|
+
'The Start of an Era',
|
|
11
|
+
'New Farm',
|
|
12
|
+
'Spinnin',
|
|
13
|
+
'Old Farm',
|
|
14
|
+
'Birb',
|
|
15
|
+
'Flamingos',
|
|
16
|
+
'Cats!'
|
|
17
|
+
]) AS badge_name
|
|
18
|
+
),
|
|
19
|
+
user_badge_data AS (
|
|
20
|
+
SELECT
|
|
21
|
+
p.id::INTEGER as id,
|
|
22
|
+
p.username,
|
|
23
|
+
p.avatar_url,
|
|
24
|
+
p."computedUsername",
|
|
25
|
+
json_array_elements_text(COALESCE(p.badges, '[]'::json)) as user_badge
|
|
26
|
+
FROM profiles p
|
|
27
|
+
WHERE p.username IS NOT NULL
|
|
28
|
+
AND (p.ban IS NULL OR p.ban != 'excluded')
|
|
29
|
+
),
|
|
30
|
+
user_special_badge_counts AS (
|
|
31
|
+
SELECT
|
|
32
|
+
ubd.id,
|
|
33
|
+
ubd.username,
|
|
34
|
+
ubd.avatar_url,
|
|
35
|
+
ubd."computedUsername",
|
|
36
|
+
COUNT(DISTINCT sb.badge_name)::INTEGER as special_badge_count
|
|
37
|
+
FROM user_badge_data ubd
|
|
38
|
+
INNER JOIN special_badges sb ON ubd.user_badge = sb.badge_name
|
|
39
|
+
GROUP BY ubd.id, ubd.username, ubd.avatar_url, ubd."computedUsername"
|
|
40
|
+
)
|
|
41
|
+
SELECT
|
|
42
|
+
usbc.id,
|
|
43
|
+
COALESCE(usbc."computedUsername", usbc.username) as display_name,
|
|
44
|
+
usbc.avatar_url,
|
|
45
|
+
usbc.special_badge_count
|
|
46
|
+
FROM user_special_badge_counts usbc
|
|
47
|
+
ORDER BY usbc.special_badge_count DESC, display_name ASC
|
|
48
|
+
LIMIT p_limit;
|
|
49
|
+
END;
|
|
50
|
+
$function$
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.get_clan_leaderboard(page_number integer DEFAULT 1, items_per_page integer DEFAULT 10)
|
|
2
|
+
RETURNS TABLE(id integer, name text, acronym text, avatar_url text, description text, member_count bigint, total_skill_points numeric, total_pages integer)
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
SET search_path TO 'public'
|
|
6
|
+
AS $function$
|
|
7
|
+
DECLARE
|
|
8
|
+
total_clan_pages integer;
|
|
9
|
+
total_clan_count integer;
|
|
10
|
+
BEGIN
|
|
11
|
+
-- Calculate total clans
|
|
12
|
+
SELECT COUNT(*) INTO total_clan_count FROM clans;
|
|
13
|
+
|
|
14
|
+
-- Calculate total pages, ensuring at least 1 page even if no clans exist
|
|
15
|
+
SELECT GREATEST(1, CEIL(total_clan_count::numeric / items_per_page)) INTO total_clan_pages;
|
|
16
|
+
|
|
17
|
+
-- Set page_number to 1 if it's out of bounds
|
|
18
|
+
IF page_number <= 0 OR page_number > total_clan_pages THEN
|
|
19
|
+
page_number := 1;
|
|
20
|
+
END IF;
|
|
21
|
+
|
|
22
|
+
RETURN QUERY
|
|
23
|
+
WITH clan_stats AS (
|
|
24
|
+
SELECT
|
|
25
|
+
c.id,
|
|
26
|
+
c.name,
|
|
27
|
+
c.acronym,
|
|
28
|
+
c.avatar_url,
|
|
29
|
+
c.description,
|
|
30
|
+
COUNT(p.id) FILTER (WHERE p.ban != 'excluded' OR p.ban IS NULL) AS member_count,
|
|
31
|
+
COALESCE(SUM(p.skill_points) FILTER (WHERE p.ban != 'excluded' OR p.ban IS NULL), 0)::numeric AS total_skill_points
|
|
32
|
+
FROM
|
|
33
|
+
clans c
|
|
34
|
+
LEFT JOIN
|
|
35
|
+
profiles p ON p.clan = c.id
|
|
36
|
+
GROUP BY
|
|
37
|
+
c.id, c.name, c.acronym, c.avatar_url, c.description
|
|
38
|
+
),
|
|
39
|
+
clan_ranking AS (
|
|
40
|
+
SELECT
|
|
41
|
+
cs.id::integer,
|
|
42
|
+
cs.name,
|
|
43
|
+
cs.acronym,
|
|
44
|
+
cs.avatar_url,
|
|
45
|
+
cs.description,
|
|
46
|
+
cs.member_count,
|
|
47
|
+
cs.total_skill_points,
|
|
48
|
+
ROW_NUMBER() OVER (ORDER BY cs.total_skill_points DESC, cs.member_count DESC, cs.name ASC) AS rank
|
|
49
|
+
FROM
|
|
50
|
+
clan_stats cs
|
|
51
|
+
)
|
|
52
|
+
SELECT
|
|
53
|
+
cr.id,
|
|
54
|
+
cr.name,
|
|
55
|
+
cr.acronym,
|
|
56
|
+
cr.avatar_url,
|
|
57
|
+
cr.description,
|
|
58
|
+
cr.member_count,
|
|
59
|
+
cr.total_skill_points,
|
|
60
|
+
total_clan_pages
|
|
61
|
+
FROM
|
|
62
|
+
clan_ranking cr
|
|
63
|
+
WHERE
|
|
64
|
+
(total_clan_count = 0 OR (cr.rank > (page_number - 1) * items_per_page AND cr.rank <= page_number * items_per_page))
|
|
65
|
+
ORDER BY
|
|
66
|
+
cr.total_skill_points DESC, cr.member_count DESC, cr.name ASC;
|
|
67
|
+
END;
|
|
68
|
+
$function$
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.get_collections_v4(page_number integer DEFAULT 1, items_per_page integer DEFAULT 10, owner_filter bigint DEFAULT NULL::bigint, search_query text DEFAULT NULL::text, author_filter text DEFAULT NULL::text, min_beatmaps integer DEFAULT NULL::integer)
|
|
2
|
+
RETURNS TABLE(id bigint, title text, description text, created_at timestamp with time zone, owner bigint, owner_username text, owner_avatar_url text, beatmap_count bigint, star1 bigint, star2 bigint, star3 bigint, star4 bigint, star5 bigint, star6 bigint, star7 bigint, star8 bigint, star9 bigint, star10 bigint, star11 bigint, star12 bigint, star13 bigint, star14 bigint, star15 bigint, star16 bigint, star17 bigint, star18 bigint, total_pages bigint)
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
AS $function$
|
|
5
|
+
DECLARE
|
|
6
|
+
total_count bigint;
|
|
7
|
+
BEGIN
|
|
8
|
+
-- Get total count with beatmap count filter
|
|
9
|
+
WITH collection_counts AS (
|
|
10
|
+
SELECT
|
|
11
|
+
bc.id,
|
|
12
|
+
COUNT(DISTINCT cr.id) as map_count
|
|
13
|
+
FROM "beatmapCollections" bc
|
|
14
|
+
LEFT JOIN "collectionRelations" cr ON cr.collection = bc.id
|
|
15
|
+
LEFT JOIN "profiles" p ON p."id" = bc."owner"
|
|
16
|
+
WHERE (owner_filter IS NULL OR bc.owner = owner_filter)
|
|
17
|
+
AND (author_filter IS NULL OR p.username ILIKE '%' || author_filter || '%')
|
|
18
|
+
AND (
|
|
19
|
+
search_query IS NULL
|
|
20
|
+
OR bc.title ILIKE '%' || search_query || '%'
|
|
21
|
+
OR bc.description ILIKE '%' || search_query || '%'
|
|
22
|
+
OR p.username ILIKE '%' || search_query || '%'
|
|
23
|
+
)
|
|
24
|
+
GROUP BY bc.id
|
|
25
|
+
)
|
|
26
|
+
SELECT COUNT(*)
|
|
27
|
+
INTO total_count
|
|
28
|
+
FROM collection_counts
|
|
29
|
+
WHERE (min_beatmaps IS NULL OR map_count >= min_beatmaps);
|
|
30
|
+
|
|
31
|
+
RETURN QUERY
|
|
32
|
+
WITH collection_counts AS (
|
|
33
|
+
SELECT
|
|
34
|
+
bc.id,
|
|
35
|
+
bc.title,
|
|
36
|
+
bc.description,
|
|
37
|
+
bc.created_at,
|
|
38
|
+
bc.owner,
|
|
39
|
+
p.username,
|
|
40
|
+
p."avatar_url",
|
|
41
|
+
COUNT(DISTINCT cr.id) as map_count,
|
|
42
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 1) as star1,
|
|
43
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 2) as star2,
|
|
44
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 3) as star3,
|
|
45
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 4) as star4,
|
|
46
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 5) as star5,
|
|
47
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 6) as star6,
|
|
48
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 7) as star7,
|
|
49
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 8) as star8,
|
|
50
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 9) as star9,
|
|
51
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 10) as star10,
|
|
52
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 11) as star11,
|
|
53
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 12) as star12,
|
|
54
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 13) as star13,
|
|
55
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 14) as star14,
|
|
56
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 15) as star15,
|
|
57
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 16) as star16,
|
|
58
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 17) as star17,
|
|
59
|
+
COUNT(*) FILTER (WHERE round(b."starRating") = 18) as star18
|
|
60
|
+
FROM "beatmapCollections" bc
|
|
61
|
+
LEFT JOIN "collectionRelations" cr ON cr.collection = bc.id
|
|
62
|
+
LEFT JOIN "beatmapPages" bp ON bp.id = cr."beatmapPage"
|
|
63
|
+
LEFT JOIN "beatmaps" b ON b."beatmapHash" = bp."latestBeatmapHash"
|
|
64
|
+
LEFT JOIN "profiles" p ON p."id" = bc."owner"
|
|
65
|
+
WHERE (owner_filter IS NULL OR bc.owner = owner_filter)
|
|
66
|
+
AND (author_filter IS NULL OR p.username ILIKE '%' || author_filter || '%')
|
|
67
|
+
AND (
|
|
68
|
+
search_query IS NULL
|
|
69
|
+
OR bc.title ILIKE '%' || search_query || '%'
|
|
70
|
+
OR bc.description ILIKE '%' || search_query || '%'
|
|
71
|
+
OR p.username ILIKE '%' || search_query || '%'
|
|
72
|
+
)
|
|
73
|
+
GROUP BY bc.id, bc.title, bc.description, bc.created_at, bc.owner, p.username, p."avatar_url"
|
|
74
|
+
HAVING (min_beatmaps IS NULL OR COUNT(DISTINCT cr.id) >= min_beatmaps)
|
|
75
|
+
)
|
|
76
|
+
SELECT
|
|
77
|
+
cc.id,
|
|
78
|
+
cc.title,
|
|
79
|
+
cc.description,
|
|
80
|
+
cc.created_at,
|
|
81
|
+
cc.owner,
|
|
82
|
+
cc.username as owner_username,
|
|
83
|
+
cc."avatar_url" as owner_avatar_url,
|
|
84
|
+
cc.map_count as beatmap_count,
|
|
85
|
+
cc.star1,
|
|
86
|
+
cc.star2,
|
|
87
|
+
cc.star3,
|
|
88
|
+
cc.star4,
|
|
89
|
+
cc.star5,
|
|
90
|
+
cc.star6,
|
|
91
|
+
cc.star7,
|
|
92
|
+
cc.star8,
|
|
93
|
+
cc.star9,
|
|
94
|
+
cc.star10,
|
|
95
|
+
cc.star11,
|
|
96
|
+
cc.star12,
|
|
97
|
+
cc.star13,
|
|
98
|
+
cc.star14,
|
|
99
|
+
cc.star15,
|
|
100
|
+
cc.star16,
|
|
101
|
+
cc.star17,
|
|
102
|
+
cc.star18,
|
|
103
|
+
CEILING(total_count::numeric / items_per_page)::bigint as total_pages
|
|
104
|
+
FROM collection_counts cc
|
|
105
|
+
ORDER BY cc.created_at DESC
|
|
106
|
+
LIMIT items_per_page
|
|
107
|
+
OFFSET ((page_number - 1) * items_per_page);
|
|
108
|
+
END;
|
|
109
|
+
$function$
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.get_top_scores_for_beatmap(beatmap_hash text)
|
|
2
|
+
RETURNS TABLE(id bigint, awarded_sp numeric, created_at timestamp with time zone, misses numeric, mods json, passed boolean, replayhwid text, songid text, speed numeric, spin boolean, userid bigint, username text, avatar_url text, accuracy numeric)
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
AS $function$
|
|
5
|
+
BEGIN
|
|
6
|
+
RETURN QUERY
|
|
7
|
+
WITH user_top_scores AS (
|
|
8
|
+
SELECT DISTINCT ON (s."userId")
|
|
9
|
+
s.id,
|
|
10
|
+
s.awarded_sp,
|
|
11
|
+
s.created_at,
|
|
12
|
+
s.misses,
|
|
13
|
+
s.mods,
|
|
14
|
+
s.passed,
|
|
15
|
+
s."replayHwid",
|
|
16
|
+
s."songId",
|
|
17
|
+
s.speed,
|
|
18
|
+
s.spin,
|
|
19
|
+
s."userId",
|
|
20
|
+
p.username,
|
|
21
|
+
p.avatar_url,
|
|
22
|
+
CASE
|
|
23
|
+
WHEN b."noteCount" > 0 THEN
|
|
24
|
+
ROUND(((b."noteCount" - s.misses) / b."noteCount" * 100)::numeric, 2)
|
|
25
|
+
ELSE 0
|
|
26
|
+
END AS accuracy
|
|
27
|
+
FROM
|
|
28
|
+
scores s
|
|
29
|
+
JOIN
|
|
30
|
+
profiles p ON s."userId" = p.id
|
|
31
|
+
JOIN
|
|
32
|
+
beatmaps b ON s."beatmapHash" = b."beatmapHash"
|
|
33
|
+
WHERE
|
|
34
|
+
s."beatmapHash" = beatmap_hash
|
|
35
|
+
AND p.ban <> 'excluded'
|
|
36
|
+
ORDER BY
|
|
37
|
+
s."userId", s.awarded_sp DESC
|
|
38
|
+
)
|
|
39
|
+
SELECT *
|
|
40
|
+
FROM user_top_scores
|
|
41
|
+
ORDER BY awarded_sp DESC
|
|
42
|
+
LIMIT 10;
|
|
43
|
+
END;
|
|
44
|
+
$function$
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.get_user_by_email(email_address text)
|
|
2
|
+
RETURNS json
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
SET search_path TO 'public'
|
|
6
|
+
AS $function$
|
|
7
|
+
DECLARE
|
|
8
|
+
user_data json;
|
|
9
|
+
BEGIN
|
|
10
|
+
-- Check that the executing user has permission to access user data
|
|
11
|
+
-- You can add more permission checks here as needed
|
|
12
|
+
|
|
13
|
+
-- Query the auth.users table to find the user with the matching email
|
|
14
|
+
SELECT json_build_object(
|
|
15
|
+
'id', id,
|
|
16
|
+
'email', email,
|
|
17
|
+
'created_at', created_at,
|
|
18
|
+
'last_sign_in_at', last_sign_in_at,
|
|
19
|
+
'user_metadata', raw_user_meta_data
|
|
20
|
+
-- Add or remove fields as needed
|
|
21
|
+
) INTO user_data
|
|
22
|
+
FROM auth.users
|
|
23
|
+
WHERE email = email_address;
|
|
24
|
+
|
|
25
|
+
-- Return NULL if no user was found
|
|
26
|
+
IF user_data IS NULL THEN
|
|
27
|
+
RETURN NULL;
|
|
28
|
+
END IF;
|
|
29
|
+
|
|
30
|
+
RETURN user_data;
|
|
31
|
+
END;
|
|
32
|
+
$function$
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.get_user_scores_lastday(userid integer, limit_param integer)
|
|
2
|
+
RETURNS jsonb
|
|
3
|
+
LANGUAGE sql
|
|
4
|
+
AS $function$
|
|
5
|
+
with s as (
|
|
6
|
+
select
|
|
7
|
+
sc.created_at,
|
|
8
|
+
sc.id,
|
|
9
|
+
sc.passed,
|
|
10
|
+
sc."userId",
|
|
11
|
+
sc.awarded_sp,
|
|
12
|
+
sc."beatmapHash",
|
|
13
|
+
sc.misses,
|
|
14
|
+
sc.replay_url,
|
|
15
|
+
sc."songId",
|
|
16
|
+
sc.speed,
|
|
17
|
+
sc.spin
|
|
18
|
+
from public.scores sc
|
|
19
|
+
where sc."userId" = userid
|
|
20
|
+
and sc.passed = true
|
|
21
|
+
order by sc.created_at desc
|
|
22
|
+
limit least(coalesce(limit_param, 100), 100)
|
|
23
|
+
)
|
|
24
|
+
select coalesce(
|
|
25
|
+
jsonb_agg(
|
|
26
|
+
jsonb_build_object(
|
|
27
|
+
'created_at', s.created_at,
|
|
28
|
+
'id', s.id,
|
|
29
|
+
'passed', s.passed,
|
|
30
|
+
'userId', s."userId",
|
|
31
|
+
'awarded_sp', s.awarded_sp,
|
|
32
|
+
'beatmapHash', s."beatmapHash",
|
|
33
|
+
'misses', s.misses,
|
|
34
|
+
'replay_url', s.replay_url,
|
|
35
|
+
'songId', s."songId",
|
|
36
|
+
'beatmapDifficulty', b.difficulty,
|
|
37
|
+
'beatmapNotes', b."noteCount",
|
|
38
|
+
'beatmapTitle', b.title,
|
|
39
|
+
'speed', s.speed,
|
|
40
|
+
'spin', s.spin
|
|
41
|
+
)
|
|
42
|
+
),
|
|
43
|
+
'[]'::jsonb
|
|
44
|
+
)
|
|
45
|
+
from s
|
|
46
|
+
left join public.beatmaps b on b."beatmapHash" = s."beatmapHash";
|
|
47
|
+
$function$
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.get_user_scores_reign(userid integer)
|
|
2
|
+
RETURNS jsonb
|
|
3
|
+
LANGUAGE sql
|
|
4
|
+
AS $function$
|
|
5
|
+
with r as (
|
|
6
|
+
select * from public.get_user_reigning_scores(userid, 10)
|
|
7
|
+
)
|
|
8
|
+
select coalesce(
|
|
9
|
+
jsonb_agg(
|
|
10
|
+
jsonb_build_object(
|
|
11
|
+
'id', r.id,
|
|
12
|
+
'awarded_sp', r.awarded_sp,
|
|
13
|
+
'created_at', r.created_at,
|
|
14
|
+
'misses', r.misses,
|
|
15
|
+
'mods', r.mods,
|
|
16
|
+
'passed', r.passed,
|
|
17
|
+
'replay_url', s.replay_url,
|
|
18
|
+
'songId', r.songid,
|
|
19
|
+
'speed', r.speed,
|
|
20
|
+
'spin', r.spin,
|
|
21
|
+
'beatmapHash', r.beatmaphash,
|
|
22
|
+
'beatmapTitle', r.beatmaptitle,
|
|
23
|
+
'beatmapNotes', r.notes,
|
|
24
|
+
'difficulty', r.difficulty
|
|
25
|
+
)
|
|
26
|
+
),
|
|
27
|
+
'[]'::jsonb
|
|
28
|
+
)
|
|
29
|
+
from r
|
|
30
|
+
left join public.scores s on s.id = r.id;
|
|
31
|
+
$function$
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.get_user_scores_top_and_stats(userid integer, limit_param integer)
|
|
2
|
+
RETURNS jsonb
|
|
3
|
+
LANGUAGE sql
|
|
4
|
+
AS $function$
|
|
5
|
+
with filtered as materialized (
|
|
6
|
+
select
|
|
7
|
+
s.created_at,
|
|
8
|
+
s.id,
|
|
9
|
+
s.passed,
|
|
10
|
+
s."userId",
|
|
11
|
+
s.awarded_sp,
|
|
12
|
+
s."beatmapHash",
|
|
13
|
+
s.misses,
|
|
14
|
+
s.replay_url,
|
|
15
|
+
s."songId",
|
|
16
|
+
s.speed,
|
|
17
|
+
s.spin
|
|
18
|
+
from public.scores s
|
|
19
|
+
where s."userId" = userid
|
|
20
|
+
and s.passed = true
|
|
21
|
+
and s.awarded_sp is not null
|
|
22
|
+
and s.awarded_sp <> 0
|
|
23
|
+
),
|
|
24
|
+
stats as (
|
|
25
|
+
select
|
|
26
|
+
count(*)::int as total_scores,
|
|
27
|
+
count(*) filter (where spin = true)::int as spin_scores
|
|
28
|
+
from filtered
|
|
29
|
+
),
|
|
30
|
+
top_rows as (
|
|
31
|
+
select *
|
|
32
|
+
from (
|
|
33
|
+
select distinct on (f."beatmapHash")
|
|
34
|
+
f.created_at,
|
|
35
|
+
f.id,
|
|
36
|
+
f.passed,
|
|
37
|
+
f."userId",
|
|
38
|
+
f.awarded_sp,
|
|
39
|
+
f."beatmapHash",
|
|
40
|
+
f.misses,
|
|
41
|
+
f.replay_url,
|
|
42
|
+
f."songId",
|
|
43
|
+
f.speed,
|
|
44
|
+
f.spin
|
|
45
|
+
from filtered f
|
|
46
|
+
order by f."beatmapHash", f.awarded_sp desc
|
|
47
|
+
) best_per_hash
|
|
48
|
+
order by awarded_sp desc
|
|
49
|
+
limit least(coalesce(limit_param, 100), 100)
|
|
50
|
+
),
|
|
51
|
+
top_json as (
|
|
52
|
+
select coalesce(
|
|
53
|
+
jsonb_agg(
|
|
54
|
+
jsonb_build_object(
|
|
55
|
+
'created_at', t.created_at,
|
|
56
|
+
'id', t.id,
|
|
57
|
+
'passed', t.passed,
|
|
58
|
+
'userId', t."userId",
|
|
59
|
+
'awarded_sp', t.awarded_sp,
|
|
60
|
+
'beatmapHash', t."beatmapHash",
|
|
61
|
+
'misses', t.misses,
|
|
62
|
+
'replay_url', t.replay_url,
|
|
63
|
+
'rank', null, -- keep key for Zod compatibility
|
|
64
|
+
'songId', t."songId",
|
|
65
|
+
'beatmapDifficulty', b.difficulty,
|
|
66
|
+
'beatmapNotes', b."noteCount",
|
|
67
|
+
'beatmapTitle', b.title,
|
|
68
|
+
'speed', t.speed,
|
|
69
|
+
'spin', t.spin
|
|
70
|
+
)
|
|
71
|
+
),
|
|
72
|
+
'[]'::jsonb
|
|
73
|
+
) as value
|
|
74
|
+
from top_rows t
|
|
75
|
+
left join public.beatmaps b on b."beatmapHash" = t."beatmapHash"
|
|
76
|
+
)
|
|
77
|
+
select jsonb_build_object(
|
|
78
|
+
'top', (select value from top_json),
|
|
79
|
+
'stats', jsonb_build_object(
|
|
80
|
+
'totalScores', coalesce((select total_scores from stats), 0),
|
|
81
|
+
'spinScores', coalesce((select spin_scores from stats), 0)
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
$function$
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.grant_special_badges(p_user_id integer, p_beatmap_id integer, p_spin boolean DEFAULT false, p_passed boolean DEFAULT true)
|
|
2
|
+
RETURNS json
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
DECLARE
|
|
7
|
+
badge_granted JSON := '{"granted": false, "badge": null}';
|
|
8
|
+
current_badges JSONB;
|
|
9
|
+
new_badge_name TEXT := NULL;
|
|
10
|
+
BEGIN
|
|
11
|
+
-- Only proceed if the score was a pass
|
|
12
|
+
IF NOT p_passed THEN
|
|
13
|
+
RETURN badge_granted;
|
|
14
|
+
END IF;
|
|
15
|
+
|
|
16
|
+
-- Get current badges from user profile (cast to JSONB)
|
|
17
|
+
SELECT COALESCE(badges::jsonb, '[]'::jsonb) INTO current_badges
|
|
18
|
+
FROM profiles
|
|
19
|
+
WHERE id = p_user_id;
|
|
20
|
+
|
|
21
|
+
-- Check each special map and determine badge to grant
|
|
22
|
+
CASE p_beatmap_id
|
|
23
|
+
WHEN 5368 THEN
|
|
24
|
+
new_badge_name := 'The Start of an Era';
|
|
25
|
+
WHEN 6178 THEN
|
|
26
|
+
new_badge_name := 'New Farm';
|
|
27
|
+
WHEN 4701 THEN
|
|
28
|
+
-- Spinnin (only if using spin gamestyle)
|
|
29
|
+
IF p_spin THEN
|
|
30
|
+
new_badge_name := 'Spinnin';
|
|
31
|
+
END IF;
|
|
32
|
+
WHEN 5216 THEN
|
|
33
|
+
new_badge_name := 'Old Farm';
|
|
34
|
+
WHEN 4305 THEN
|
|
35
|
+
new_badge_name := 'Birb';
|
|
36
|
+
WHEN 5094 THEN
|
|
37
|
+
new_badge_name := 'Flamingos';
|
|
38
|
+
WHEN 4285 THEN
|
|
39
|
+
new_badge_name := 'Cats!';
|
|
40
|
+
ELSE
|
|
41
|
+
-- No special badge for this beatmap
|
|
42
|
+
RETURN badge_granted;
|
|
43
|
+
END CASE;
|
|
44
|
+
|
|
45
|
+
-- If no badge should be granted (e.g., spin condition not met), return
|
|
46
|
+
IF new_badge_name IS NULL THEN
|
|
47
|
+
RETURN badge_granted;
|
|
48
|
+
END IF;
|
|
49
|
+
|
|
50
|
+
-- Check if user already has this badge
|
|
51
|
+
IF current_badges ? new_badge_name THEN
|
|
52
|
+
-- Badge already exists (using JSONB containment operator)
|
|
53
|
+
RETURN badge_granted;
|
|
54
|
+
END IF;
|
|
55
|
+
|
|
56
|
+
-- Append new badge to the existing array (both operands are JSONB now)
|
|
57
|
+
UPDATE profiles
|
|
58
|
+
SET badges = (badges::jsonb || jsonb_build_array(new_badge_name))::json
|
|
59
|
+
WHERE id = p_user_id;
|
|
60
|
+
|
|
61
|
+
-- Return success response
|
|
62
|
+
badge_granted := json_build_object(
|
|
63
|
+
'granted', true,
|
|
64
|
+
'badge', new_badge_name
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
RETURN badge_granted;
|
|
68
|
+
END;
|
|
69
|
+
$function$
|