rhythia-api 242.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 -67
- 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/getMapUploadUrl.ts +90 -93
- 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 +121 -112
- package/package.json +4 -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 -1450
- 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/handleApi.ts
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
let
|
|
3
|
-
import { profanity, CensorType } from "@2toad/profanity";
|
|
4
|
-
export function setEnvironment(
|
|
5
|
-
stage: "development" | "testing" | "production"
|
|
6
|
-
) {
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
export function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
let endpoint = "https://development.rhythia.com";
|
|
3
|
+
import { profanity, CensorType } from "@2toad/profanity";
|
|
4
|
+
export function setEnvironment(
|
|
5
|
+
stage: "development" | "testing" | "production"
|
|
6
|
+
) {
|
|
7
|
+
endpoint = `https://${stage}.rhythia.com`;
|
|
8
|
+
}
|
|
9
|
+
export function setEndpoint(value: string) {
|
|
10
|
+
endpoint = value;
|
|
11
|
+
}
|
|
12
|
+
export function handleApi<
|
|
13
|
+
T extends { url: string; input: z.ZodObject<any>; output: z.ZodObject<any> }
|
|
14
|
+
>(apiSchema: T) {
|
|
15
|
+
profanity.whitelist.addWords(["willy"]);
|
|
16
|
+
return async (input: T["input"]["_type"]): Promise<T["output"]["_type"]> => {
|
|
17
|
+
const response = await fetch(`${endpoint}${apiSchema.url}`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
body: JSON.stringify(input),
|
|
20
|
+
});
|
|
21
|
+
const output = await response.text();
|
|
22
|
+
return JSON.parse(profanity.censor(output));
|
|
23
|
+
};
|
|
24
|
+
}
|
package/index.ts
CHANGED
|
@@ -392,18 +392,18 @@ export const executeAdminOperation = handleApi({url:"/api/executeAdminOperation"
|
|
|
392
392
|
// ./api/getAvatarUploadUrl.ts API
|
|
393
393
|
|
|
394
394
|
/*
|
|
395
|
-
export const Schema = {
|
|
396
|
-
input: z.strictObject({
|
|
397
|
-
session: z.string(),
|
|
398
|
-
contentLength: z.number(),
|
|
399
|
-
contentType: z.string(),
|
|
400
|
-
intrinsicToken: z.string(),
|
|
401
|
-
}),
|
|
402
|
-
output: z.strictObject({
|
|
403
|
-
error: z.string().optional(),
|
|
404
|
-
url: z.string().optional(),
|
|
405
|
-
objectKey: z.string().optional(),
|
|
406
|
-
}),
|
|
395
|
+
export const Schema = {
|
|
396
|
+
input: z.strictObject({
|
|
397
|
+
session: z.string(),
|
|
398
|
+
contentLength: z.number(),
|
|
399
|
+
contentType: z.string(),
|
|
400
|
+
intrinsicToken: z.string(),
|
|
401
|
+
}),
|
|
402
|
+
output: z.strictObject({
|
|
403
|
+
error: z.string().optional(),
|
|
404
|
+
url: z.string().optional(),
|
|
405
|
+
objectKey: z.string().optional(),
|
|
406
|
+
}),
|
|
407
407
|
};*/
|
|
408
408
|
import { Schema as GetAvatarUploadUrl } from "./api/getAvatarUploadUrl"
|
|
409
409
|
export { Schema as SchemaGetAvatarUploadUrl } from "./api/getAvatarUploadUrl"
|
|
@@ -541,6 +541,7 @@ export const Schema = {
|
|
|
541
541
|
description: z.string().nullable().optional(),
|
|
542
542
|
tags: z.string().nullable().optional(),
|
|
543
543
|
videoUrl: z.string().nullable().optional(),
|
|
544
|
+
mapHash: z.string().nullable().optional(),
|
|
544
545
|
vetos: z
|
|
545
546
|
.array(
|
|
546
547
|
z.object({
|
|
@@ -613,6 +614,7 @@ export const Schema = {
|
|
|
613
614
|
qualified: z.boolean().nullable().optional(),
|
|
614
615
|
qualifiedAt: z.string().nullable().optional(),
|
|
615
616
|
videoUrl: z.string().nullable(),
|
|
617
|
+
mapHash: z.string().nullable().optional(),
|
|
616
618
|
vetos: z
|
|
617
619
|
.array(
|
|
618
620
|
z.object({
|
|
@@ -636,50 +638,51 @@ export const getBeatmapPageById = handleApi({url:"/api/getBeatmapPageById",...Ge
|
|
|
636
638
|
// ./api/getBeatmaps.ts API
|
|
637
639
|
|
|
638
640
|
/*
|
|
639
|
-
export const Schema = {
|
|
640
|
-
input: z.strictObject({
|
|
641
|
-
session: z.string(),
|
|
642
|
-
textFilter: z.string().optional(),
|
|
643
|
-
authorFilter: z.string().optional(),
|
|
644
|
-
tagsFilter: z.string().optional(),
|
|
645
|
-
page: z.number().default(1),
|
|
646
|
-
maxStars: z.number().optional(),
|
|
647
|
-
minLength: z.number().optional(),
|
|
648
|
-
maxLength: z.number().optional(),
|
|
649
|
-
minStars: z.number().optional(),
|
|
650
|
-
creator: z.number().optional(),
|
|
651
|
-
status: z.string().optional(),
|
|
652
|
-
}),
|
|
653
|
-
output: z.object({
|
|
654
|
-
error: z.string().optional(),
|
|
655
|
-
total: z.number(),
|
|
656
|
-
viewPerPage: z.number(),
|
|
657
|
-
currentPage: z.number(),
|
|
658
|
-
beatmaps: z
|
|
659
|
-
.array(
|
|
660
|
-
z.object({
|
|
661
|
-
id: z.number(),
|
|
662
|
-
playcount: z.number().nullable().optional(),
|
|
663
|
-
created_at: z.string().nullable().optional(),
|
|
664
|
-
difficulty: z.number().nullable().optional(),
|
|
665
|
-
noteCount: z.number().nullable().optional(),
|
|
666
|
-
length: z.number().nullable().optional(),
|
|
667
|
-
title: z.string().nullable().optional(),
|
|
668
|
-
ranked: z.boolean().nullable().optional(),
|
|
669
|
-
beatmapFile: z.string().nullable().optional(),
|
|
670
|
-
image: z.string().nullable().optional(),
|
|
671
|
-
starRating: z.number().nullable().optional(),
|
|
672
|
-
owner: z.number().nullable().optional(),
|
|
673
|
-
ownerUsername: z.string().nullable().optional(),
|
|
674
|
-
ownerAvatar: z.string().nullable().optional(),
|
|
675
|
-
status: z.string().nullable().optional(),
|
|
676
|
-
qualified: z.boolean().nullable().optional(),
|
|
677
|
-
tags: z.string().nullable().optional(),
|
|
678
|
-
videoUrl: z.string().nullable().optional(),
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
641
|
+
export const Schema = {
|
|
642
|
+
input: z.strictObject({
|
|
643
|
+
session: z.string(),
|
|
644
|
+
textFilter: z.string().optional(),
|
|
645
|
+
authorFilter: z.string().optional(),
|
|
646
|
+
tagsFilter: z.string().optional(),
|
|
647
|
+
page: z.number().default(1),
|
|
648
|
+
maxStars: z.number().optional(),
|
|
649
|
+
minLength: z.number().optional(),
|
|
650
|
+
maxLength: z.number().optional(),
|
|
651
|
+
minStars: z.number().optional(),
|
|
652
|
+
creator: z.number().optional(),
|
|
653
|
+
status: z.string().optional(),
|
|
654
|
+
}),
|
|
655
|
+
output: z.object({
|
|
656
|
+
error: z.string().optional(),
|
|
657
|
+
total: z.number(),
|
|
658
|
+
viewPerPage: z.number(),
|
|
659
|
+
currentPage: z.number(),
|
|
660
|
+
beatmaps: z
|
|
661
|
+
.array(
|
|
662
|
+
z.object({
|
|
663
|
+
id: z.number(),
|
|
664
|
+
playcount: z.number().nullable().optional(),
|
|
665
|
+
created_at: z.string().nullable().optional(),
|
|
666
|
+
difficulty: z.number().nullable().optional(),
|
|
667
|
+
noteCount: z.number().nullable().optional(),
|
|
668
|
+
length: z.number().nullable().optional(),
|
|
669
|
+
title: z.string().nullable().optional(),
|
|
670
|
+
ranked: z.boolean().nullable().optional(),
|
|
671
|
+
beatmapFile: z.string().nullable().optional(),
|
|
672
|
+
image: z.string().nullable().optional(),
|
|
673
|
+
starRating: z.number().nullable().optional(),
|
|
674
|
+
owner: z.number().nullable().optional(),
|
|
675
|
+
ownerUsername: z.string().nullable().optional(),
|
|
676
|
+
ownerAvatar: z.string().nullable().optional(),
|
|
677
|
+
status: z.string().nullable().optional(),
|
|
678
|
+
qualified: z.boolean().nullable().optional(),
|
|
679
|
+
tags: z.string().nullable().optional(),
|
|
680
|
+
videoUrl: z.string().nullable().optional(),
|
|
681
|
+
mapHash: z.string().nullable().optional(),
|
|
682
|
+
})
|
|
683
|
+
)
|
|
684
|
+
.optional(),
|
|
685
|
+
}),
|
|
683
686
|
};*/
|
|
684
687
|
import { Schema as GetBeatmaps } from "./api/getBeatmaps"
|
|
685
688
|
export { Schema as SchemaGetBeatmaps } from "./api/getBeatmaps"
|
|
@@ -783,19 +786,22 @@ export const Schema = {
|
|
|
783
786
|
input: z.strictObject({
|
|
784
787
|
session: z.string(),
|
|
785
788
|
collection: z.number(),
|
|
789
|
+
page: z.number().optional().default(1),
|
|
790
|
+
itemsPerPage: z.number().optional().default(30),
|
|
786
791
|
}),
|
|
787
792
|
output: z.object({
|
|
788
|
-
collection: z.object({
|
|
789
|
-
title: z.string(),
|
|
790
|
-
description: z.string(),
|
|
791
|
-
owner: z.object({
|
|
792
|
-
id: z.number(),
|
|
793
|
-
username: z.string(),
|
|
794
|
-
avatar_url: z.string().nullable(),
|
|
795
|
-
}),
|
|
796
|
-
isList: z.boolean(),
|
|
797
|
-
|
|
798
|
-
|
|
793
|
+
collection: z.object({
|
|
794
|
+
title: z.string(),
|
|
795
|
+
description: z.string(),
|
|
796
|
+
owner: z.object({
|
|
797
|
+
id: z.number(),
|
|
798
|
+
username: z.string(),
|
|
799
|
+
avatar_url: z.string().nullable(),
|
|
800
|
+
}),
|
|
801
|
+
isList: z.boolean(),
|
|
802
|
+
beatmapCount: z.number(),
|
|
803
|
+
beatmaps: z.array(
|
|
804
|
+
z.object({
|
|
799
805
|
id: z.number(),
|
|
800
806
|
playcount: z.number().nullable().optional(),
|
|
801
807
|
created_at: z.string().nullable().optional(),
|
|
@@ -813,6 +819,7 @@ export const Schema = {
|
|
|
813
819
|
})
|
|
814
820
|
),
|
|
815
821
|
}),
|
|
822
|
+
totalPages: z.number(),
|
|
816
823
|
error: z.string().optional(),
|
|
817
824
|
}),
|
|
818
825
|
};*/
|
|
@@ -939,19 +946,19 @@ export const getLeaderboard = handleApi({url:"/api/getLeaderboard",...GetLeaderb
|
|
|
939
946
|
// ./api/getMapUploadUrl.ts API
|
|
940
947
|
|
|
941
948
|
/*
|
|
942
|
-
export const Schema = {
|
|
943
|
-
input: z.strictObject({
|
|
944
|
-
mapName: z.string().optional(),
|
|
945
|
-
session: z.string(),
|
|
946
|
-
contentLength: z.number(),
|
|
947
|
-
contentType: z.string(),
|
|
948
|
-
intrinsicToken: z.string(),
|
|
949
|
-
}),
|
|
950
|
-
output: z.strictObject({
|
|
951
|
-
error: z.string().optional(),
|
|
952
|
-
url: z.string().optional(),
|
|
953
|
-
objectKey: z.string().optional(),
|
|
954
|
-
}),
|
|
949
|
+
export const Schema = {
|
|
950
|
+
input: z.strictObject({
|
|
951
|
+
mapName: z.string().optional(),
|
|
952
|
+
session: z.string(),
|
|
953
|
+
contentLength: z.number(),
|
|
954
|
+
contentType: z.string(),
|
|
955
|
+
intrinsicToken: z.string(),
|
|
956
|
+
}),
|
|
957
|
+
output: z.strictObject({
|
|
958
|
+
error: z.string().optional(),
|
|
959
|
+
url: z.string().optional(),
|
|
960
|
+
objectKey: z.string().optional(),
|
|
961
|
+
}),
|
|
955
962
|
};*/
|
|
956
963
|
import { Schema as GetMapUploadUrl } from "./api/getMapUploadUrl"
|
|
957
964
|
export { Schema as SchemaGetMapUploadUrl } from "./api/getMapUploadUrl"
|
|
@@ -1161,6 +1168,7 @@ export const Schema = {
|
|
|
1161
1168
|
username: z.string().optional().nullable(),
|
|
1162
1169
|
speed: z.number().optional().nullable(),
|
|
1163
1170
|
spin: z.boolean().optional().nullable(),
|
|
1171
|
+
replay_url: z.string().optional().nullable(),
|
|
1164
1172
|
})
|
|
1165
1173
|
.optional(),
|
|
1166
1174
|
}),
|
|
@@ -1321,18 +1329,18 @@ export const getVerified = handleApi({url:"/api/getVerified",...GetVerified})
|
|
|
1321
1329
|
// ./api/getVideoUploadUrl.ts API
|
|
1322
1330
|
|
|
1323
1331
|
/*
|
|
1324
|
-
export const Schema = {
|
|
1325
|
-
input: z.strictObject({
|
|
1326
|
-
session: z.string(),
|
|
1327
|
-
contentLength: z.number(),
|
|
1328
|
-
contentType: z.string(),
|
|
1329
|
-
intrinsicToken: z.string(),
|
|
1330
|
-
}),
|
|
1331
|
-
output: z.strictObject({
|
|
1332
|
-
error: z.string().optional(),
|
|
1333
|
-
url: z.string().optional(),
|
|
1334
|
-
objectKey: z.string().optional(),
|
|
1335
|
-
}),
|
|
1332
|
+
export const Schema = {
|
|
1333
|
+
input: z.strictObject({
|
|
1334
|
+
session: z.string(),
|
|
1335
|
+
contentLength: z.number(),
|
|
1336
|
+
contentType: z.string(),
|
|
1337
|
+
intrinsicToken: z.string(),
|
|
1338
|
+
}),
|
|
1339
|
+
output: z.strictObject({
|
|
1340
|
+
error: z.string().optional(),
|
|
1341
|
+
url: z.string().optional(),
|
|
1342
|
+
objectKey: z.string().optional(),
|
|
1343
|
+
}),
|
|
1336
1344
|
};*/
|
|
1337
1345
|
import { Schema as GetVideoUploadUrl } from "./api/getVideoUploadUrl"
|
|
1338
1346
|
export { Schema as SchemaGetVideoUploadUrl } from "./api/getVideoUploadUrl"
|
|
@@ -1479,26 +1487,27 @@ export const submitScore = handleApi({url:"/api/submitScore",...SubmitScore})
|
|
|
1479
1487
|
// ./api/submitScoreInternal.ts API
|
|
1480
1488
|
|
|
1481
1489
|
/*
|
|
1482
|
-
export const Schema = {
|
|
1483
|
-
input: z.strictObject({
|
|
1484
|
-
session: z.string(),
|
|
1485
|
-
secret: z.string(),
|
|
1486
|
-
token: z.string(),
|
|
1487
|
-
data: z.strictObject({
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1490
|
+
export const Schema = {
|
|
1491
|
+
input: z.strictObject({
|
|
1492
|
+
session: z.string(),
|
|
1493
|
+
secret: z.string(),
|
|
1494
|
+
token: z.string(),
|
|
1495
|
+
data: z.strictObject({
|
|
1496
|
+
beatmapHash: z.string().optional().nullable(),
|
|
1497
|
+
onlineMapId: z.number(),
|
|
1498
|
+
misses: z.number(),
|
|
1499
|
+
hits: z.number(),
|
|
1500
|
+
speed: z.number(),
|
|
1501
|
+
mods: z.array(z.string()),
|
|
1502
|
+
spin: z.boolean(),
|
|
1503
|
+
replayBytes: z.string().nullable().optional(),
|
|
1504
|
+
pauses: z.number().nullable().optional(),
|
|
1505
|
+
failTime: z.number().nullable().optional(),
|
|
1506
|
+
}),
|
|
1507
|
+
}),
|
|
1508
|
+
output: z.object({
|
|
1509
|
+
error: z.string().optional(),
|
|
1510
|
+
}),
|
|
1502
1511
|
};*/
|
|
1503
1512
|
import { Schema as SubmitScoreInternal } from "./api/submitScoreInternal"
|
|
1504
1513
|
export { Schema as SchemaSubmitScoreInternal } from "./api/submitScoreInternal"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhythia-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "243.0.0",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"author": "online-contributors-cunev",
|
|
6
6
|
"scripts": {
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"test": "tsx ./scripts/test.ts",
|
|
11
11
|
"cache:clear-user-scores": "bun run scripts/clear-user-score-cache.ts",
|
|
12
12
|
"db:optimize-user-scores": "bun run scripts/optimize-user-scores-indexes.ts",
|
|
13
|
+
"db:add-beatmap-page-map-hash": "node --experimental-transform-types scripts/add-beatmap-page-map-hash.ts",
|
|
14
|
+
"db:backfill-beatmap-page-map-hashes": "node --experimental-transform-types scripts/backfill-beatmap-page-map-hashes.ts",
|
|
13
15
|
"db:create-profile-flags": "node scripts/create-profile-flags-table.ts",
|
|
14
16
|
"db:create-profile-friends": "node scripts/create-profile-friends-table.ts",
|
|
15
17
|
"db:create-profile-reports": "node scripts/create-profile-reports-table.ts",
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
"@supabase/supabase-js": "^2.45.1",
|
|
29
31
|
"@types/bun": "^1.1.6",
|
|
30
32
|
"@types/lodash": "^4.17.7",
|
|
31
|
-
"@types/node": "^
|
|
33
|
+
"@types/node": "^25.6.0",
|
|
32
34
|
"@types/pg": "^8.15.5",
|
|
33
35
|
"@types/validator": "^13.12.2",
|
|
34
36
|
"bad-words": "^4.0.0",
|
|
@@ -1,39 +1,42 @@
|
|
|
1
|
-
CREATE OR REPLACE FUNCTION public.admin_delete_user(user_id integer)
|
|
2
|
-
RETURNS boolean
|
|
3
|
-
LANGUAGE plpgsql
|
|
4
|
-
SECURITY DEFINER
|
|
5
|
-
AS $function$
|
|
6
|
-
DECLARE
|
|
7
|
-
success BOOLEAN;
|
|
8
|
-
BEGIN
|
|
9
|
-
-- Delete scores first due to foreign key constraints
|
|
10
|
-
DELETE FROM public.scores WHERE "userId" = user_id;
|
|
11
|
-
|
|
12
|
-
-- Delete beatmapPageComments
|
|
13
|
-
DELETE FROM public."beatmapPageComments" WHERE owner = user_id;
|
|
14
|
-
|
|
15
|
-
-- Delete beatmapPages owned by user
|
|
16
|
-
DELETE FROM public."beatmapPages" WHERE owner = user_id;
|
|
17
|
-
|
|
18
|
-
-- Delete collections owned by user
|
|
19
|
-
DELETE FROM public."beatmapCollections" WHERE owner = user_id;
|
|
20
|
-
|
|
21
|
-
-- Delete collection relations related to user's collections
|
|
22
|
-
-- (this is handled by cascade if you have it set up)
|
|
23
|
-
|
|
24
|
-
-- Delete passkeys
|
|
25
|
-
DELETE FROM public.passkeys WHERE id = user_id;
|
|
26
|
-
|
|
27
|
-
-- Delete
|
|
28
|
-
DELETE FROM public.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
--
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.admin_delete_user(user_id integer)
|
|
2
|
+
RETURNS boolean
|
|
3
|
+
LANGUAGE plpgsql
|
|
4
|
+
SECURITY DEFINER
|
|
5
|
+
AS $function$
|
|
6
|
+
DECLARE
|
|
7
|
+
success BOOLEAN;
|
|
8
|
+
BEGIN
|
|
9
|
+
-- Delete scores first due to foreign key constraints
|
|
10
|
+
DELETE FROM public.scores WHERE "userId" = user_id;
|
|
11
|
+
|
|
12
|
+
-- Delete beatmapPageComments
|
|
13
|
+
DELETE FROM public."beatmapPageComments" WHERE owner = user_id;
|
|
14
|
+
|
|
15
|
+
-- Delete beatmapPages owned by user
|
|
16
|
+
DELETE FROM public."beatmapPages" WHERE owner = user_id;
|
|
17
|
+
|
|
18
|
+
-- Delete collections owned by user
|
|
19
|
+
DELETE FROM public."beatmapCollections" WHERE owner = user_id;
|
|
20
|
+
|
|
21
|
+
-- Delete collection relations related to user's collections
|
|
22
|
+
-- (this is handled by cascade if you have it set up)
|
|
23
|
+
|
|
24
|
+
-- Delete passkeys
|
|
25
|
+
DELETE FROM public.passkeys WHERE id = user_id;
|
|
26
|
+
|
|
27
|
+
-- Delete user HWID records
|
|
28
|
+
DELETE FROM public.user_hwids WHERE id = user_id;
|
|
29
|
+
|
|
30
|
+
-- Delete profile activity
|
|
31
|
+
DELETE FROM public."profileActivities"
|
|
32
|
+
WHERE uid = (SELECT uid FROM public.profiles WHERE id = user_id);
|
|
33
|
+
|
|
34
|
+
-- Finally, delete the profile
|
|
35
|
+
DELETE FROM public.profiles WHERE id = user_id;
|
|
36
|
+
|
|
37
|
+
-- Check if deletion was successful
|
|
38
|
+
success := NOT EXISTS (SELECT 1 FROM public.profiles WHERE id = user_id);
|
|
39
|
+
|
|
40
|
+
RETURN success;
|
|
41
|
+
END;
|
|
42
|
+
$function$
|
|
@@ -5,9 +5,12 @@ CREATE OR REPLACE FUNCTION public.admin_remove_all_scores(user_id integer)
|
|
|
5
5
|
AS $function$
|
|
6
6
|
DECLARE
|
|
7
7
|
deleted_count INTEGER;
|
|
8
|
-
BEGIN
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
BEGIN
|
|
9
|
+
DELETE FROM public.leaderboard_map_user
|
|
10
|
+
WHERE "userId" = user_id;
|
|
11
|
+
|
|
12
|
+
-- Delete all scores for this user and count them
|
|
13
|
+
WITH deleted AS (
|
|
11
14
|
DELETE FROM public.scores
|
|
12
15
|
WHERE "userId" = user_id
|
|
13
16
|
RETURNING *
|
|
@@ -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$
|