rhythia-api 241.0.0 → 242.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/editProfile.ts +27 -16
- package/api/editProfileFriend.ts +122 -0
- package/api/getFriends.ts +154 -0
- package/api/getProfile.ts +195 -127
- package/index.ts +62 -25
- package/package.json +2 -1
- package/types/database.ts +36 -0
- package/worker.ts +195 -191
package/api/editProfile.ts
CHANGED
|
@@ -72,44 +72,55 @@ export async function handler(
|
|
|
72
72
|
data: (typeof Schema)["input"]["_type"]
|
|
73
73
|
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
74
74
|
if (data.data.username !== undefined) {
|
|
75
|
-
|
|
75
|
+
data.data.username = removeZeroWidth(data.data.username);
|
|
76
|
+
|
|
77
|
+
if (validator.trim(data.data.username) !== data.data.username) {
|
|
76
78
|
return NextResponse.json(
|
|
77
79
|
{
|
|
78
|
-
error: "Username
|
|
80
|
+
error: "Username can't start or end with spaces.",
|
|
79
81
|
},
|
|
80
82
|
{ status: 404 }
|
|
81
83
|
);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
if (data.data.username.length
|
|
86
|
+
if (data.data.username.length < 2) {
|
|
85
87
|
return NextResponse.json(
|
|
86
88
|
{
|
|
87
|
-
error: "Username
|
|
89
|
+
error: "Username must be at least 2 characters long",
|
|
88
90
|
},
|
|
89
91
|
{ status: 404 }
|
|
90
92
|
);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
if (
|
|
95
|
+
if (data.data.username.length > 16) {
|
|
94
96
|
return NextResponse.json(
|
|
95
97
|
{
|
|
96
|
-
error: "Username
|
|
98
|
+
error: "Username must be 16 characters or fewer.",
|
|
97
99
|
},
|
|
98
100
|
{ status: 404 }
|
|
99
101
|
);
|
|
100
102
|
}
|
|
101
|
-
}
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
if (!/^[A-Za-z0-9._]+$/.test(data.data.username)) {
|
|
105
|
+
return NextResponse.json(
|
|
106
|
+
{
|
|
107
|
+
error:
|
|
108
|
+
"Username can only contain English letters (a-z), numbers (0-9), and up to one '_' or '.'",
|
|
109
|
+
},
|
|
110
|
+
{ status: 404 }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
const specialCharacters = data.data.username.match(/[._]/g) ?? [];
|
|
115
|
+
if (specialCharacters.length > 1) {
|
|
116
|
+
return NextResponse.json(
|
|
117
|
+
{
|
|
118
|
+
error: "Username can contain at most one '_' or one '.'",
|
|
119
|
+
},
|
|
120
|
+
{ status: 404 }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
113
124
|
|
|
114
125
|
if (data.data.flag !== undefined) {
|
|
115
126
|
const normalizedFlag = validator.trim(data.data.flag).toUpperCase();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { NextResponse } from "../utils/response";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
+
import { supabase } from "../utils/supabase";
|
|
5
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
6
|
+
import { User } from "@supabase/supabase-js";
|
|
7
|
+
|
|
8
|
+
const friendStateSchema = z.enum(["none", "added", "mutual"]);
|
|
9
|
+
|
|
10
|
+
export const Schema = {
|
|
11
|
+
input: z.strictObject({
|
|
12
|
+
session: z.string(),
|
|
13
|
+
profileId: z.number(),
|
|
14
|
+
action: z.enum(["add", "remove"]),
|
|
15
|
+
}),
|
|
16
|
+
output: z.strictObject({
|
|
17
|
+
error: z.string().optional(),
|
|
18
|
+
friend_count: z.number().optional(),
|
|
19
|
+
friend_state: friendStateSchema.optional(),
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function getFriendState(viewerProfileId: number, profileId: number) {
|
|
24
|
+
const [{ count: addedCount }, { count: mutualCount }] = await Promise.all([
|
|
25
|
+
supabase
|
|
26
|
+
.from("profileFriends")
|
|
27
|
+
.select("*", { count: "exact", head: true })
|
|
28
|
+
.eq("profile_id", viewerProfileId)
|
|
29
|
+
.eq("friend_id", profileId),
|
|
30
|
+
supabase
|
|
31
|
+
.from("profileFriends")
|
|
32
|
+
.select("*", { count: "exact", head: true })
|
|
33
|
+
.eq("profile_id", profileId)
|
|
34
|
+
.eq("friend_id", viewerProfileId),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
if (!addedCount) {
|
|
38
|
+
return "none";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return mutualCount ? "mutual" : "added";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
45
|
+
return protectedApi({
|
|
46
|
+
request,
|
|
47
|
+
schema: Schema,
|
|
48
|
+
authorization: validUser,
|
|
49
|
+
activity: handler,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function handler({
|
|
54
|
+
session,
|
|
55
|
+
profileId,
|
|
56
|
+
action,
|
|
57
|
+
}: (typeof Schema)["input"]["_type"]): Promise<
|
|
58
|
+
NextResponse<(typeof Schema)["output"]["_type"]>
|
|
59
|
+
> {
|
|
60
|
+
const user = (await getUserBySession(session)) as User;
|
|
61
|
+
|
|
62
|
+
const { data: viewerProfile } = await supabase
|
|
63
|
+
.from("profiles")
|
|
64
|
+
.select("id")
|
|
65
|
+
.eq("uid", user.id)
|
|
66
|
+
.single();
|
|
67
|
+
|
|
68
|
+
if (!viewerProfile) {
|
|
69
|
+
return NextResponse.json({ error: "Can't find user" });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (viewerProfile.id === profileId) {
|
|
73
|
+
return NextResponse.json({ error: "You can't friend yourself." });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { data: targetProfile } = await supabase
|
|
77
|
+
.from("profiles")
|
|
78
|
+
.select("id")
|
|
79
|
+
.eq("id", profileId)
|
|
80
|
+
.single();
|
|
81
|
+
|
|
82
|
+
if (!targetProfile) {
|
|
83
|
+
return NextResponse.json({ error: "Player not found." });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const relation = {
|
|
87
|
+
profile_id: viewerProfile.id,
|
|
88
|
+
friend_id: targetProfile.id,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const mutation =
|
|
92
|
+
action === "add"
|
|
93
|
+
? await supabase
|
|
94
|
+
.from("profileFriends")
|
|
95
|
+
.upsert(relation, {
|
|
96
|
+
onConflict: "profile_id,friend_id",
|
|
97
|
+
ignoreDuplicates: true,
|
|
98
|
+
})
|
|
99
|
+
: await supabase
|
|
100
|
+
.from("profileFriends")
|
|
101
|
+
.delete()
|
|
102
|
+
.eq("profile_id", relation.profile_id)
|
|
103
|
+
.eq("friend_id", relation.friend_id);
|
|
104
|
+
|
|
105
|
+
if (mutation.error) {
|
|
106
|
+
return NextResponse.json({ error: mutation.error.message });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const [friendCount, friendState] = await Promise.all([
|
|
110
|
+
supabase
|
|
111
|
+
.from("profileFriends")
|
|
112
|
+
.select("*", { count: "exact", head: true })
|
|
113
|
+
.eq("friend_id", targetProfile.id)
|
|
114
|
+
.then((result) => result.count || 0),
|
|
115
|
+
getFriendState(viewerProfile.id, targetProfile.id),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
return NextResponse.json({
|
|
119
|
+
friend_count: friendCount,
|
|
120
|
+
friend_state: friendState,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { NextResponse } from "../utils/response";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
+
import { supabase } from "../utils/supabase";
|
|
5
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
6
|
+
import { User } from "@supabase/supabase-js";
|
|
7
|
+
|
|
8
|
+
const friendUserSchema = z.object({
|
|
9
|
+
id: z.number(),
|
|
10
|
+
username: z.string().nullable(),
|
|
11
|
+
avatar_url: z.string().nullable(),
|
|
12
|
+
flag: z.string().nullable(),
|
|
13
|
+
profile_image: z.string().nullable(),
|
|
14
|
+
is_online: z.boolean(),
|
|
15
|
+
last_active_timestamp: z.number().nullable(),
|
|
16
|
+
is_mutual: z.boolean(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const Schema = {
|
|
20
|
+
input: z.strictObject({
|
|
21
|
+
session: z.string(),
|
|
22
|
+
}),
|
|
23
|
+
output: z.strictObject({
|
|
24
|
+
error: z.string().optional(),
|
|
25
|
+
friends: z.array(friendUserSchema),
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
30
|
+
return protectedApi({
|
|
31
|
+
request,
|
|
32
|
+
schema: Schema,
|
|
33
|
+
authorization: validUser,
|
|
34
|
+
activity: handler,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function handler({
|
|
39
|
+
session,
|
|
40
|
+
}: (typeof Schema)["input"]["_type"]): Promise<
|
|
41
|
+
NextResponse<(typeof Schema)["output"]["_type"]>
|
|
42
|
+
> {
|
|
43
|
+
const user = (await getUserBySession(session)) as User;
|
|
44
|
+
|
|
45
|
+
const { data: viewerProfile } = await supabase
|
|
46
|
+
.from("profiles")
|
|
47
|
+
.select("id")
|
|
48
|
+
.eq("uid", user.id)
|
|
49
|
+
.single();
|
|
50
|
+
|
|
51
|
+
if (!viewerProfile) {
|
|
52
|
+
return NextResponse.json({ error: "Can't find user", friends: [] });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { data: outgoingRows, error: outgoingError } = await supabase
|
|
56
|
+
.from("profileFriends")
|
|
57
|
+
.select("friend_id,created_at")
|
|
58
|
+
.eq("profile_id", viewerProfile.id)
|
|
59
|
+
.order("created_at", { ascending: false });
|
|
60
|
+
|
|
61
|
+
if (outgoingError) {
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
error: outgoingError.message,
|
|
64
|
+
friends: [],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const outgoingIds = (outgoingRows || []).map((row) => row.friend_id);
|
|
69
|
+
|
|
70
|
+
if (!outgoingIds.length) {
|
|
71
|
+
return NextResponse.json({ friends: [] });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const [{ data: reverseRows, error: reverseError }, { data: profiles, error: profilesError }] =
|
|
75
|
+
await Promise.all([
|
|
76
|
+
supabase
|
|
77
|
+
.from("profileFriends")
|
|
78
|
+
.select("profile_id")
|
|
79
|
+
.in("profile_id", outgoingIds)
|
|
80
|
+
.eq("friend_id", viewerProfile.id),
|
|
81
|
+
supabase
|
|
82
|
+
.from("profiles")
|
|
83
|
+
.select("id,uid,username,avatar_url,flag,profile_image")
|
|
84
|
+
.in("id", outgoingIds)
|
|
85
|
+
.neq("ban", "excluded"),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
if (reverseError) {
|
|
89
|
+
return NextResponse.json({
|
|
90
|
+
error: reverseError.message,
|
|
91
|
+
friends: [],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (profilesError) {
|
|
96
|
+
return NextResponse.json({
|
|
97
|
+
error: profilesError.message,
|
|
98
|
+
friends: [],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const activityUids = (profiles || [])
|
|
103
|
+
.map((profile) => profile.uid)
|
|
104
|
+
.filter((uid): uid is string => !!uid);
|
|
105
|
+
|
|
106
|
+
const { data: activities, error: activitiesError } = activityUids.length
|
|
107
|
+
? await supabase
|
|
108
|
+
.from("profileActivities")
|
|
109
|
+
.select("uid,last_activity")
|
|
110
|
+
.in("uid", activityUids)
|
|
111
|
+
: { data: [], error: null };
|
|
112
|
+
|
|
113
|
+
if (activitiesError) {
|
|
114
|
+
return NextResponse.json({
|
|
115
|
+
error: activitiesError.message,
|
|
116
|
+
friends: [],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const profileMap = new Map((profiles || []).map((profile) => [profile.id, profile]));
|
|
121
|
+
const mutualIds = new Set((reverseRows || []).map((row) => row.profile_id));
|
|
122
|
+
const activityMap = new Map(
|
|
123
|
+
(activities || []).map((activity) => [activity.uid, activity.last_activity])
|
|
124
|
+
);
|
|
125
|
+
const addedUsers = outgoingIds
|
|
126
|
+
.map((friendId) => profileMap.get(friendId))
|
|
127
|
+
.filter(
|
|
128
|
+
(
|
|
129
|
+
profile
|
|
130
|
+
): profile is {
|
|
131
|
+
id: number;
|
|
132
|
+
uid: string | null;
|
|
133
|
+
username: string | null;
|
|
134
|
+
avatar_url: string | null;
|
|
135
|
+
flag: string | null;
|
|
136
|
+
profile_image: string | null;
|
|
137
|
+
} => !!profile
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return NextResponse.json({
|
|
141
|
+
friends: addedUsers.map((profile) => ({
|
|
142
|
+
id: profile.id,
|
|
143
|
+
username: profile.username,
|
|
144
|
+
avatar_url: profile.avatar_url,
|
|
145
|
+
flag: profile.flag,
|
|
146
|
+
profile_image: profile.profile_image,
|
|
147
|
+
is_online:
|
|
148
|
+
typeof activityMap.get(profile.uid || "") === "number" &&
|
|
149
|
+
Date.now() - (activityMap.get(profile.uid || "") || 0) < 1800000,
|
|
150
|
+
last_active_timestamp: activityMap.get(profile.uid || "") ?? null,
|
|
151
|
+
is_mutual: mutualIds.has(profile.id),
|
|
152
|
+
})),
|
|
153
|
+
});
|
|
154
|
+
}
|
package/api/getProfile.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { geolocation } from "../utils/requestGeo";
|
|
2
|
-
import { NextResponse } from "../utils/response";
|
|
3
|
-
import z from "zod";
|
|
4
|
-
import { Database } from "../types/database";
|
|
5
|
-
import { protectedApi } from "../utils/requestUtils";
|
|
6
|
-
import { supabase } from "../utils/supabase";
|
|
7
|
-
import { getUserBySession } from "../utils/getUserBySession";
|
|
1
|
+
import { geolocation } from "../utils/requestGeo";
|
|
2
|
+
import { NextResponse } from "../utils/response";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { Database } from "../types/database";
|
|
5
|
+
import { protectedApi } from "../utils/requestUtils";
|
|
6
|
+
import { supabase } from "../utils/supabase";
|
|
7
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
8
8
|
import { User } from "@supabase/supabase-js";
|
|
9
9
|
import {
|
|
10
10
|
getActivityStatusForUserId,
|
|
11
11
|
getScoreActivityCutoffIso,
|
|
12
12
|
} from "../utils/activityStatus";
|
|
13
13
|
|
|
14
|
+
const friendStateSchema = z.enum(["none", "added", "mutual"]);
|
|
15
|
+
|
|
14
16
|
function getNextUsernameChangeAt(
|
|
15
17
|
lastChangedAt: string | null | undefined
|
|
16
18
|
): string | null {
|
|
@@ -23,28 +25,73 @@ function getNextUsernameChangeAt(
|
|
|
23
25
|
return nextAllowedAt.toISOString();
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
async function getViewerProfileId(session: string) {
|
|
29
|
+
if (!session) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const viewer = await getUserBySession(session);
|
|
34
|
+
|
|
35
|
+
if (!viewer) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { data: viewerProfile } = await supabase
|
|
40
|
+
.from("profiles")
|
|
41
|
+
.select("id")
|
|
42
|
+
.eq("uid", viewer.id)
|
|
43
|
+
.single();
|
|
44
|
+
|
|
45
|
+
return viewerProfile?.id ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getFriendState(viewerProfileId: number | null, profileId: number) {
|
|
49
|
+
if (viewerProfileId === null || viewerProfileId === profileId) {
|
|
50
|
+
return "none" as const;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const [{ count: addedCount }, { count: mutualCount }] = await Promise.all([
|
|
54
|
+
supabase
|
|
55
|
+
.from("profileFriends")
|
|
56
|
+
.select("*", { count: "exact", head: true })
|
|
57
|
+
.eq("profile_id", viewerProfileId)
|
|
58
|
+
.eq("friend_id", profileId),
|
|
59
|
+
supabase
|
|
60
|
+
.from("profileFriends")
|
|
61
|
+
.select("*", { count: "exact", head: true })
|
|
62
|
+
.eq("profile_id", profileId)
|
|
63
|
+
.eq("friend_id", viewerProfileId),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
if (!addedCount) {
|
|
67
|
+
return "none" as const;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return mutualCount ? ("mutual" as const) : ("added" as const);
|
|
71
|
+
}
|
|
72
|
+
|
|
26
73
|
export const Schema = {
|
|
27
74
|
input: z.strictObject({
|
|
28
|
-
session: z.string(),
|
|
29
|
-
id: z.number().nullable().optional(),
|
|
30
|
-
}),
|
|
31
|
-
output: z.object({
|
|
32
|
-
error: z.string().optional(),
|
|
33
|
-
user: z
|
|
34
|
-
.object({
|
|
35
|
-
about_me: z.string().nullable(),
|
|
36
|
-
avatar_url: z.string().nullable(),
|
|
37
|
-
profile_image: z.string().nullable(),
|
|
38
|
-
badges: z.any().nullable(),
|
|
39
|
-
created_at: z.number().nullable(),
|
|
40
|
-
flag: z.string().nullable(),
|
|
41
|
-
id: z.number(),
|
|
42
|
-
uid: z.string().nullable(),
|
|
43
|
-
ban: z.string().nullable(),
|
|
44
|
-
username: z.string().nullable(),
|
|
45
|
-
verified: z.boolean().nullable(),
|
|
46
|
-
verificationDeadline: z.number().nullable(),
|
|
47
|
-
play_count: z.number().nullable(),
|
|
75
|
+
session: z.string(),
|
|
76
|
+
id: z.number().nullable().optional(),
|
|
77
|
+
}),
|
|
78
|
+
output: z.object({
|
|
79
|
+
error: z.string().optional(),
|
|
80
|
+
user: z
|
|
81
|
+
.object({
|
|
82
|
+
about_me: z.string().nullable(),
|
|
83
|
+
avatar_url: z.string().nullable(),
|
|
84
|
+
profile_image: z.string().nullable(),
|
|
85
|
+
badges: z.any().nullable(),
|
|
86
|
+
created_at: z.number().nullable(),
|
|
87
|
+
flag: z.string().nullable(),
|
|
88
|
+
id: z.number(),
|
|
89
|
+
uid: z.string().nullable(),
|
|
90
|
+
ban: z.string().nullable(),
|
|
91
|
+
username: z.string().nullable(),
|
|
92
|
+
verified: z.boolean().nullable(),
|
|
93
|
+
verificationDeadline: z.number().nullable(),
|
|
94
|
+
play_count: z.number().nullable(),
|
|
48
95
|
skill_points: z.number().nullable(),
|
|
49
96
|
squares_hit: z.number().nullable(),
|
|
50
97
|
total_score: z.number().nullable(),
|
|
@@ -53,6 +100,8 @@ export const Schema = {
|
|
|
53
100
|
activity_status: z.enum(["active", "inactive"]),
|
|
54
101
|
is_online: z.boolean(),
|
|
55
102
|
last_active_timestamp: z.number().nullable(),
|
|
103
|
+
friend_count: z.number(),
|
|
104
|
+
friend_state: friendStateSchema,
|
|
56
105
|
can_change_flag: z.boolean(),
|
|
57
106
|
next_username_change_at: z.string().nullable(),
|
|
58
107
|
previous_usernames: z.array(
|
|
@@ -66,105 +115,122 @@ export const Schema = {
|
|
|
66
115
|
id: z.number(),
|
|
67
116
|
acronym: z.string(),
|
|
68
117
|
})
|
|
69
|
-
.optional()
|
|
70
|
-
.nullable(),
|
|
71
|
-
})
|
|
72
|
-
.optional(),
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export async function POST(request: Request): Promise<NextResponse> {
|
|
77
|
-
return protectedApi({
|
|
78
|
-
request,
|
|
79
|
-
schema: Schema,
|
|
80
|
-
authorization: () => {},
|
|
81
|
-
activity: handler,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function handler(
|
|
86
|
-
data: (typeof Schema)["input"]["_type"],
|
|
87
|
-
req: Request
|
|
88
|
-
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
89
|
-
let profiles: Database["public"]["Tables"]["profiles"]["Row"][] = [];
|
|
90
|
-
let isOnline = false;
|
|
91
|
-
// Fetch by id
|
|
92
|
-
if (data.id !== undefined && data.id !== null) {
|
|
93
|
-
let { data: queryData, error } = await supabase
|
|
94
|
-
.from("profiles")
|
|
95
|
-
.select(`*,clans:clan(id,acronym)`)
|
|
96
|
-
.eq("id", data.id);
|
|
97
|
-
|
|
98
|
-
console.log(profiles, error);
|
|
99
|
-
|
|
100
|
-
if (!queryData?.length) {
|
|
101
|
-
return NextResponse.json(
|
|
102
|
-
{
|
|
103
|
-
error: "User not found",
|
|
104
|
-
},
|
|
105
|
-
{ status: 404 }
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
profiles = queryData;
|
|
110
|
-
} else {
|
|
111
|
-
// Fetch by session id
|
|
112
|
-
const user = (await getUserBySession(data.session)) as User;
|
|
113
|
-
|
|
114
|
-
if (user) {
|
|
115
|
-
let { data: queryData, error } = await supabase
|
|
116
|
-
.from("profiles")
|
|
117
|
-
.select("*")
|
|
118
|
-
.eq("uid", user.id);
|
|
119
|
-
|
|
120
|
-
if (!queryData?.length) {
|
|
121
|
-
const geo = geolocation(req);
|
|
122
|
-
const data = await supabase.from("profiles").upsert({
|
|
123
|
-
uid: user.id,
|
|
124
|
-
about_me: "",
|
|
125
|
-
avatar_url:
|
|
126
|
-
"https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/user-avatar-1725309193296-72002e6b-321c-4f60-a692-568e0e75147d",
|
|
127
|
-
badges: [],
|
|
128
|
-
username: `user${Math.round(Math.random() * 900000 + 100000)}`,
|
|
129
|
-
computedUsername: `user${Math.round(Math.random() * 900000 + 100000)}`.toLowerCase(),
|
|
130
|
-
flag: (geo.country || "US").toUpperCase(),
|
|
131
|
-
created_at: Date.now(),
|
|
132
|
-
}).select(`
|
|
133
|
-
*,clans:clan(id,acronym)`);
|
|
134
|
-
|
|
135
|
-
profiles = data.data!;
|
|
136
|
-
} else {
|
|
137
|
-
profiles = queryData;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const user = profiles[0];
|
|
143
|
-
|
|
144
|
-
const activityStatus = await getActivityStatusForUserId(user.id);
|
|
145
|
-
|
|
146
|
-
const { data: activityData } = await supabase
|
|
147
|
-
.from("profileActivities")
|
|
148
|
-
.select("last_activity")
|
|
149
|
-
.eq("uid", user.uid || "")
|
|
150
|
-
.single();
|
|
118
|
+
.optional()
|
|
119
|
+
.nullable(),
|
|
120
|
+
})
|
|
121
|
+
.optional(),
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
151
124
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
125
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
126
|
+
return protectedApi({
|
|
127
|
+
request,
|
|
128
|
+
schema: Schema,
|
|
129
|
+
authorization: () => {},
|
|
130
|
+
activity: handler,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
157
133
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
134
|
+
export async function handler(
|
|
135
|
+
data: (typeof Schema)["input"]["_type"],
|
|
136
|
+
req: Request
|
|
137
|
+
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
138
|
+
let profiles: Database["public"]["Tables"]["profiles"]["Row"][] = [];
|
|
139
|
+
let isOnline = false;
|
|
140
|
+
let viewerProfileId =
|
|
141
|
+
data.id !== undefined && data.id !== null
|
|
142
|
+
? await getViewerProfileId(data.session)
|
|
143
|
+
: null;
|
|
144
|
+
// Fetch by id
|
|
145
|
+
if (data.id !== undefined && data.id !== null) {
|
|
146
|
+
let { data: queryData, error } = await supabase
|
|
147
|
+
.from("profiles")
|
|
148
|
+
.select(`*,clans:clan(id,acronym)`)
|
|
149
|
+
.eq("id", data.id);
|
|
150
|
+
|
|
151
|
+
console.log(profiles, error);
|
|
152
|
+
|
|
153
|
+
if (!queryData?.length) {
|
|
154
|
+
return NextResponse.json(
|
|
155
|
+
{
|
|
156
|
+
error: "User not found",
|
|
157
|
+
},
|
|
158
|
+
{ status: 404 }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
profiles = queryData;
|
|
163
|
+
} else {
|
|
164
|
+
// Fetch by session id
|
|
165
|
+
const user = (await getUserBySession(data.session)) as User;
|
|
166
|
+
|
|
167
|
+
if (user) {
|
|
168
|
+
let { data: queryData, error } = await supabase
|
|
169
|
+
.from("profiles")
|
|
170
|
+
.select("*")
|
|
171
|
+
.eq("uid", user.id);
|
|
172
|
+
|
|
173
|
+
if (!queryData?.length) {
|
|
174
|
+
const geo = geolocation(req);
|
|
175
|
+
const data = await supabase.from("profiles").upsert({
|
|
176
|
+
uid: user.id,
|
|
177
|
+
about_me: "",
|
|
178
|
+
avatar_url:
|
|
179
|
+
"https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/user-avatar-1725309193296-72002e6b-321c-4f60-a692-568e0e75147d",
|
|
180
|
+
badges: [],
|
|
181
|
+
username: `user${Math.round(Math.random() * 900000 + 100000)}`,
|
|
182
|
+
computedUsername: `user${Math.round(Math.random() * 900000 + 100000)}`.toLowerCase(),
|
|
183
|
+
flag: (geo.country || "US").toUpperCase(),
|
|
184
|
+
created_at: Date.now(),
|
|
185
|
+
}).select(`
|
|
186
|
+
*,clans:clan(id,acronym)`);
|
|
187
|
+
|
|
188
|
+
profiles = data.data!;
|
|
189
|
+
} else {
|
|
190
|
+
profiles = queryData;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
viewerProfileId = profiles[0]?.id ?? null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const user = profiles[0];
|
|
198
|
+
|
|
199
|
+
const activityStatus = await getActivityStatusForUserId(user.id);
|
|
200
|
+
|
|
201
|
+
const [
|
|
202
|
+
{ data: activityData },
|
|
203
|
+
{ data: usernameHistoryData },
|
|
204
|
+
{ data: flagHistoryData },
|
|
205
|
+
{ count: friendCount },
|
|
206
|
+
friendState,
|
|
207
|
+
] = await Promise.all([
|
|
208
|
+
supabase
|
|
209
|
+
.from("profileActivities")
|
|
210
|
+
.select("last_activity")
|
|
211
|
+
.eq("uid", user.uid || "")
|
|
212
|
+
.single(),
|
|
213
|
+
supabase
|
|
214
|
+
.from("profileUsernames")
|
|
215
|
+
.select("username,changed_at")
|
|
216
|
+
.eq("profile_id", user.id)
|
|
217
|
+
.order("changed_at", { ascending: false }),
|
|
218
|
+
supabase
|
|
219
|
+
.from("profileFlags")
|
|
220
|
+
.select("id")
|
|
221
|
+
.eq("profile_id", user.id)
|
|
222
|
+
.limit(1),
|
|
223
|
+
supabase
|
|
224
|
+
.from("profileFriends")
|
|
225
|
+
.select("*", { count: "exact", head: true })
|
|
226
|
+
.eq("friend_id", user.id),
|
|
227
|
+
getFriendState(viewerProfileId, user.id),
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
//last 30 minutes
|
|
231
|
+
if (activityData && activityData.last_activity) {
|
|
232
|
+
isOnline = Date.now() - activityData.last_activity < 1800000;
|
|
233
|
+
}
|
|
168
234
|
|
|
169
235
|
let position: number | null = null;
|
|
170
236
|
let countryPosition: number | null = null;
|
|
@@ -196,13 +262,13 @@ export async function handler(
|
|
|
196
262
|
? (countryPlayersWithMorePoints || 0) + 1
|
|
197
263
|
: null;
|
|
198
264
|
}
|
|
199
|
-
|
|
265
|
+
|
|
200
266
|
if (user.verificationDeadline < Date.now()) {
|
|
201
267
|
await supabase
|
|
202
268
|
.from("profiles")
|
|
203
269
|
.upsert({
|
|
204
|
-
id: user.id,
|
|
205
|
-
verified: false,
|
|
270
|
+
id: user.id,
|
|
271
|
+
verified: false,
|
|
206
272
|
})
|
|
207
273
|
.select();
|
|
208
274
|
}
|
|
@@ -221,6 +287,8 @@ export async function handler(
|
|
|
221
287
|
activity_status: activityStatus,
|
|
222
288
|
is_online: isOnline,
|
|
223
289
|
last_active_timestamp: activityData?.last_activity ?? null,
|
|
290
|
+
friend_count: friendCount || 0,
|
|
291
|
+
friend_state: friendState,
|
|
224
292
|
can_change_flag: !flagHistoryData?.length,
|
|
225
293
|
next_username_change_at: getNextUsernameChangeAt(latestUsernameChangeAt),
|
|
226
294
|
previous_usernames: previousUsernames,
|
package/index.ts
CHANGED
|
@@ -321,6 +321,25 @@ import { Schema as EditProfile } from "./api/editProfile"
|
|
|
321
321
|
export { Schema as SchemaEditProfile } from "./api/editProfile"
|
|
322
322
|
export const editProfile = handleApi({url:"/api/editProfile",...EditProfile})
|
|
323
323
|
|
|
324
|
+
// ./api/editProfileFriend.ts API
|
|
325
|
+
|
|
326
|
+
/*
|
|
327
|
+
export const Schema = {
|
|
328
|
+
input: z.strictObject({
|
|
329
|
+
session: z.string(),
|
|
330
|
+
profileId: z.number(),
|
|
331
|
+
action: z.enum(["add", "remove"]),
|
|
332
|
+
}),
|
|
333
|
+
output: z.strictObject({
|
|
334
|
+
error: z.string().optional(),
|
|
335
|
+
friend_count: z.number().optional(),
|
|
336
|
+
friend_state: friendStateSchema.optional(),
|
|
337
|
+
}),
|
|
338
|
+
};*/
|
|
339
|
+
import { Schema as EditProfileFriend } from "./api/editProfileFriend"
|
|
340
|
+
export { Schema as SchemaEditProfileFriend } from "./api/editProfileFriend"
|
|
341
|
+
export const editProfileFriend = handleApi({url:"/api/editProfileFriend",...EditProfileFriend})
|
|
342
|
+
|
|
324
343
|
// ./api/enhancedSearch.ts API
|
|
325
344
|
|
|
326
345
|
/*
|
|
@@ -840,6 +859,22 @@ import { Schema as GetCollections } from "./api/getCollections"
|
|
|
840
859
|
export { Schema as SchemaGetCollections } from "./api/getCollections"
|
|
841
860
|
export const getCollections = handleApi({url:"/api/getCollections",...GetCollections})
|
|
842
861
|
|
|
862
|
+
// ./api/getFriends.ts API
|
|
863
|
+
|
|
864
|
+
/*
|
|
865
|
+
export const Schema = {
|
|
866
|
+
input: z.strictObject({
|
|
867
|
+
session: z.string(),
|
|
868
|
+
}),
|
|
869
|
+
output: z.strictObject({
|
|
870
|
+
error: z.string().optional(),
|
|
871
|
+
friends: z.array(friendUserSchema),
|
|
872
|
+
}),
|
|
873
|
+
};*/
|
|
874
|
+
import { Schema as GetFriends } from "./api/getFriends"
|
|
875
|
+
export { Schema as SchemaGetFriends } from "./api/getFriends"
|
|
876
|
+
export const getFriends = handleApi({url:"/api/getFriends",...GetFriends})
|
|
877
|
+
|
|
843
878
|
// ./api/getInventory.ts API
|
|
844
879
|
|
|
845
880
|
/*
|
|
@@ -967,26 +1002,26 @@ export const getPassToken = handleApi({url:"/api/getPassToken",...GetPassToken})
|
|
|
967
1002
|
/*
|
|
968
1003
|
export const Schema = {
|
|
969
1004
|
input: z.strictObject({
|
|
970
|
-
session: z.string(),
|
|
971
|
-
id: z.number().nullable().optional(),
|
|
972
|
-
}),
|
|
973
|
-
output: z.object({
|
|
974
|
-
error: z.string().optional(),
|
|
975
|
-
user: z
|
|
976
|
-
.object({
|
|
977
|
-
about_me: z.string().nullable(),
|
|
978
|
-
avatar_url: z.string().nullable(),
|
|
979
|
-
profile_image: z.string().nullable(),
|
|
980
|
-
badges: z.any().nullable(),
|
|
981
|
-
created_at: z.number().nullable(),
|
|
982
|
-
flag: z.string().nullable(),
|
|
983
|
-
id: z.number(),
|
|
984
|
-
uid: z.string().nullable(),
|
|
985
|
-
ban: z.string().nullable(),
|
|
986
|
-
username: z.string().nullable(),
|
|
987
|
-
verified: z.boolean().nullable(),
|
|
988
|
-
verificationDeadline: z.number().nullable(),
|
|
989
|
-
play_count: z.number().nullable(),
|
|
1005
|
+
session: z.string(),
|
|
1006
|
+
id: z.number().nullable().optional(),
|
|
1007
|
+
}),
|
|
1008
|
+
output: z.object({
|
|
1009
|
+
error: z.string().optional(),
|
|
1010
|
+
user: z
|
|
1011
|
+
.object({
|
|
1012
|
+
about_me: z.string().nullable(),
|
|
1013
|
+
avatar_url: z.string().nullable(),
|
|
1014
|
+
profile_image: z.string().nullable(),
|
|
1015
|
+
badges: z.any().nullable(),
|
|
1016
|
+
created_at: z.number().nullable(),
|
|
1017
|
+
flag: z.string().nullable(),
|
|
1018
|
+
id: z.number(),
|
|
1019
|
+
uid: z.string().nullable(),
|
|
1020
|
+
ban: z.string().nullable(),
|
|
1021
|
+
username: z.string().nullable(),
|
|
1022
|
+
verified: z.boolean().nullable(),
|
|
1023
|
+
verificationDeadline: z.number().nullable(),
|
|
1024
|
+
play_count: z.number().nullable(),
|
|
990
1025
|
skill_points: z.number().nullable(),
|
|
991
1026
|
squares_hit: z.number().nullable(),
|
|
992
1027
|
total_score: z.number().nullable(),
|
|
@@ -995,6 +1030,8 @@ export const Schema = {
|
|
|
995
1030
|
activity_status: z.enum(["active", "inactive"]),
|
|
996
1031
|
is_online: z.boolean(),
|
|
997
1032
|
last_active_timestamp: z.number().nullable(),
|
|
1033
|
+
friend_count: z.number(),
|
|
1034
|
+
friend_state: friendStateSchema,
|
|
998
1035
|
can_change_flag: z.boolean(),
|
|
999
1036
|
next_username_change_at: z.string().nullable(),
|
|
1000
1037
|
previous_usernames: z.array(
|
|
@@ -1008,11 +1045,11 @@ export const Schema = {
|
|
|
1008
1045
|
id: z.number(),
|
|
1009
1046
|
acronym: z.string(),
|
|
1010
1047
|
})
|
|
1011
|
-
.optional()
|
|
1012
|
-
.nullable(),
|
|
1013
|
-
})
|
|
1014
|
-
.optional(),
|
|
1015
|
-
}),
|
|
1048
|
+
.optional()
|
|
1049
|
+
.nullable(),
|
|
1050
|
+
})
|
|
1051
|
+
.optional(),
|
|
1052
|
+
}),
|
|
1016
1053
|
};*/
|
|
1017
1054
|
import { Schema as GetProfile } from "./api/getProfile"
|
|
1018
1055
|
export { Schema as SchemaGetProfile } from "./api/getProfile"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhythia-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "242.0.0",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"author": "online-contributors-cunev",
|
|
6
6
|
"scripts": {
|
|
@@ -11,6 +11,7 @@
|
|
|
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
13
|
"db:create-profile-flags": "node scripts/create-profile-flags-table.ts",
|
|
14
|
+
"db:create-profile-friends": "node scripts/create-profile-friends-table.ts",
|
|
14
15
|
"db:create-profile-reports": "node scripts/create-profile-reports-table.ts",
|
|
15
16
|
"query-pull": "bun run scripts/pull-queries.ts",
|
|
16
17
|
"query-push": "bun run scripts/deploy-queries.ts",
|
package/types/database.ts
CHANGED
|
@@ -558,6 +558,42 @@ export type Database = {
|
|
|
558
558
|
},
|
|
559
559
|
]
|
|
560
560
|
}
|
|
561
|
+
profileFriends: {
|
|
562
|
+
Row: {
|
|
563
|
+
created_at: string
|
|
564
|
+
friend_id: number
|
|
565
|
+
id: number
|
|
566
|
+
profile_id: number
|
|
567
|
+
}
|
|
568
|
+
Insert: {
|
|
569
|
+
created_at?: string
|
|
570
|
+
friend_id: number
|
|
571
|
+
id?: number
|
|
572
|
+
profile_id: number
|
|
573
|
+
}
|
|
574
|
+
Update: {
|
|
575
|
+
created_at?: string
|
|
576
|
+
friend_id?: number
|
|
577
|
+
id?: number
|
|
578
|
+
profile_id?: number
|
|
579
|
+
}
|
|
580
|
+
Relationships: [
|
|
581
|
+
{
|
|
582
|
+
foreignKeyName: "profileFriends_friend_id_fkey"
|
|
583
|
+
columns: ["friend_id"]
|
|
584
|
+
isOneToOne: false
|
|
585
|
+
referencedRelation: "profiles"
|
|
586
|
+
referencedColumns: ["id"]
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
foreignKeyName: "profileFriends_profile_id_fkey"
|
|
590
|
+
columns: ["profile_id"]
|
|
591
|
+
isOneToOne: false
|
|
592
|
+
referencedRelation: "profiles"
|
|
593
|
+
referencedColumns: ["id"]
|
|
594
|
+
},
|
|
595
|
+
]
|
|
596
|
+
}
|
|
561
597
|
profileFlags: {
|
|
562
598
|
Row: {
|
|
563
599
|
changed_at: string
|
package/worker.ts
CHANGED
|
@@ -1,191 +1,195 @@
|
|
|
1
|
-
import { POST as acceptInvite } from "./api/acceptInvite";
|
|
2
|
-
import { POST as addCollectionMap } from "./api/addCollectionMap";
|
|
3
|
-
import { POST as chartPublicStats } from "./api/chartPublicStats";
|
|
4
|
-
import { POST as checkQualified } from "./api/checkQualified";
|
|
5
|
-
import { POST as createBeatmap } from "./api/createBeatmap";
|
|
6
|
-
import { POST as createBeatmapPage } from "./api/createBeatmapPage";
|
|
7
|
-
import { POST as createClan } from "./api/createClan";
|
|
8
|
-
import { POST as createCollection } from "./api/createCollection";
|
|
9
|
-
import { POST as createInvite } from "./api/createInvite";
|
|
10
|
-
import { POST as createSupporter } from "./api/createSupporter";
|
|
11
|
-
import { POST as deleteBeatmapPage } from "./api/deleteBeatmapPage";
|
|
12
|
-
import { POST as deleteCollection } from "./api/deleteCollection";
|
|
13
|
-
import { POST as deleteCollectionMap } from "./api/deleteCollectionMap";
|
|
14
|
-
import { POST as editAboutMe } from "./api/editAboutMe";
|
|
15
|
-
import { POST as editClan } from "./api/editClan";
|
|
16
|
-
import { POST as editCollection } from "./api/editCollection";
|
|
17
|
-
import { POST as editProfile } from "./api/editProfile";
|
|
18
|
-
import { POST as
|
|
19
|
-
import { POST as
|
|
20
|
-
import { POST as
|
|
21
|
-
import { POST as
|
|
22
|
-
import { POST as
|
|
23
|
-
import { POST as
|
|
24
|
-
import { POST as
|
|
25
|
-
import { POST as
|
|
26
|
-
import { POST as
|
|
27
|
-
import { POST as
|
|
28
|
-
import { POST as
|
|
29
|
-
import { POST as
|
|
30
|
-
import { POST as
|
|
31
|
-
import { POST as
|
|
32
|
-
import { POST as
|
|
33
|
-
import { POST as
|
|
34
|
-
import { POST as
|
|
35
|
-
import { POST as
|
|
36
|
-
import { POST as
|
|
37
|
-
import { POST as
|
|
38
|
-
import { POST as
|
|
39
|
-
import { POST as
|
|
40
|
-
import { POST as
|
|
41
|
-
import { POST as
|
|
42
|
-
import { POST as
|
|
43
|
-
import { POST as
|
|
44
|
-
import { POST as
|
|
45
|
-
import { POST as
|
|
46
|
-
import { POST as
|
|
47
|
-
import { POST as
|
|
48
|
-
import { POST as
|
|
49
|
-
import { POST as
|
|
50
|
-
import { POST as
|
|
51
|
-
import { POST as
|
|
52
|
-
import { POST as
|
|
53
|
-
import { POST as
|
|
54
|
-
import { POST as
|
|
55
|
-
import { POST as
|
|
56
|
-
import {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"Access-Control-Allow-
|
|
64
|
-
"Access-Control-Allow-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"/api/
|
|
72
|
-
"/api/
|
|
73
|
-
"/api/
|
|
74
|
-
"/api/
|
|
75
|
-
"/api/
|
|
76
|
-
"/api/
|
|
77
|
-
"/api/
|
|
78
|
-
"/api/
|
|
79
|
-
"/api/
|
|
80
|
-
"/api/
|
|
81
|
-
"/api/
|
|
82
|
-
"/api/
|
|
83
|
-
"/api/
|
|
84
|
-
"/api/
|
|
85
|
-
"/api/
|
|
86
|
-
"/api/
|
|
87
|
-
"/api/
|
|
88
|
-
"/api/
|
|
89
|
-
"/api/
|
|
90
|
-
"/api/
|
|
91
|
-
"/api/
|
|
92
|
-
"/api/
|
|
93
|
-
"/api/
|
|
94
|
-
"/api/
|
|
95
|
-
"/api/
|
|
96
|
-
"/api/
|
|
97
|
-
"/api/
|
|
98
|
-
"/api/
|
|
99
|
-
"/api/
|
|
100
|
-
"/api/
|
|
101
|
-
"/api/
|
|
102
|
-
"/api/
|
|
103
|
-
"/api/
|
|
104
|
-
"/api/
|
|
105
|
-
"/api/
|
|
106
|
-
"/api/
|
|
107
|
-
"/api/
|
|
108
|
-
"/api/
|
|
109
|
-
"/api/
|
|
110
|
-
"/api/
|
|
111
|
-
"/api/
|
|
112
|
-
"/api/
|
|
113
|
-
"/api/
|
|
114
|
-
"/api/
|
|
115
|
-
"/api/
|
|
116
|
-
"/api/
|
|
117
|
-
"/api/
|
|
118
|
-
"/api/
|
|
119
|
-
"/api/
|
|
120
|
-
"/api/
|
|
121
|
-
"/api/
|
|
122
|
-
"/api/
|
|
123
|
-
"/api/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
return withCors(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
return withCors(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
1
|
+
import { POST as acceptInvite } from "./api/acceptInvite";
|
|
2
|
+
import { POST as addCollectionMap } from "./api/addCollectionMap";
|
|
3
|
+
import { POST as chartPublicStats } from "./api/chartPublicStats";
|
|
4
|
+
import { POST as checkQualified } from "./api/checkQualified";
|
|
5
|
+
import { POST as createBeatmap } from "./api/createBeatmap";
|
|
6
|
+
import { POST as createBeatmapPage } from "./api/createBeatmapPage";
|
|
7
|
+
import { POST as createClan } from "./api/createClan";
|
|
8
|
+
import { POST as createCollection } from "./api/createCollection";
|
|
9
|
+
import { POST as createInvite } from "./api/createInvite";
|
|
10
|
+
import { POST as createSupporter } from "./api/createSupporter";
|
|
11
|
+
import { POST as deleteBeatmapPage } from "./api/deleteBeatmapPage";
|
|
12
|
+
import { POST as deleteCollection } from "./api/deleteCollection";
|
|
13
|
+
import { POST as deleteCollectionMap } from "./api/deleteCollectionMap";
|
|
14
|
+
import { POST as editAboutMe } from "./api/editAboutMe";
|
|
15
|
+
import { POST as editClan } from "./api/editClan";
|
|
16
|
+
import { POST as editCollection } from "./api/editCollection";
|
|
17
|
+
import { POST as editProfile } from "./api/editProfile";
|
|
18
|
+
import { POST as editProfileFriend } from "./api/editProfileFriend";
|
|
19
|
+
import { POST as enhancedSearch } from "./api/enhancedSearch";
|
|
20
|
+
import { POST as executeAdminOperation } from "./api/executeAdminOperation";
|
|
21
|
+
import { POST as getAvatarUploadUrl } from "./api/getAvatarUploadUrl";
|
|
22
|
+
import { POST as getBadgeLeaders } from "./api/getBadgeLeaders";
|
|
23
|
+
import { POST as getBadgedUsers } from "./api/getBadgedUsers";
|
|
24
|
+
import { POST as getBeatmapComments } from "./api/getBeatmapComments";
|
|
25
|
+
import { POST as getBeatmapPage } from "./api/getBeatmapPage";
|
|
26
|
+
import { POST as getBeatmapPageById } from "./api/getBeatmapPageById";
|
|
27
|
+
import { POST as getBeatmapStarRating } from "./api/getBeatmapStarRating";
|
|
28
|
+
import { POST as getBeatmaps } from "./api/getBeatmaps";
|
|
29
|
+
import { POST as getClan } from "./api/getClan";
|
|
30
|
+
import { POST as getClans } from "./api/getClans";
|
|
31
|
+
import { POST as getCollection } from "./api/getCollection";
|
|
32
|
+
import { POST as getCollections } from "./api/getCollections";
|
|
33
|
+
import { POST as getFriends } from "./api/getFriends";
|
|
34
|
+
import { POST as getInventory } from "./api/getInventory";
|
|
35
|
+
import { POST as getLeaderboard } from "./api/getLeaderboard";
|
|
36
|
+
import { POST as getMapUploadUrl } from "./api/getMapUploadUrl";
|
|
37
|
+
import { POST as getOnlinePlayers } from "./api/getOnlinePlayers";
|
|
38
|
+
import { POST as getPassToken } from "./api/getPassToken";
|
|
39
|
+
import { POST as getProfile } from "./api/getProfile";
|
|
40
|
+
import { POST as getPublicStats } from "./api/getPublicStats";
|
|
41
|
+
import { POST as getRawStarRating } from "./api/getRawStarRating";
|
|
42
|
+
import { POST as getScore } from "./api/getScore";
|
|
43
|
+
import { POST as getStoryBeatmaps } from "./api/getStoryBeatmaps";
|
|
44
|
+
import { POST as getTimestamp } from "./api/getTimestamp";
|
|
45
|
+
import { POST as getUserScores } from "./api/getUserScores";
|
|
46
|
+
import { POST as getVerified } from "./api/getVerified";
|
|
47
|
+
import { POST as getVideoUploadUrl } from "./api/getVideoUploadUrl";
|
|
48
|
+
import { POST as postBeatmapComment } from "./api/postBeatmapComment";
|
|
49
|
+
import { POST as qualifyMap } from "./api/qualifyMap";
|
|
50
|
+
import { POST as rankMapsArchive } from "./api/rankMapsArchive";
|
|
51
|
+
import { POST as reportProfile } from "./api/reportProfile";
|
|
52
|
+
import { POST as searchUsers } from "./api/searchUsers";
|
|
53
|
+
import { POST as setPasskey } from "./api/setPasskey";
|
|
54
|
+
import { POST as submitScore } from "./api/submitScore";
|
|
55
|
+
import { POST as submitScoreInternal } from "./api/submitScoreInternal";
|
|
56
|
+
import { POST as updateBeatmapPage } from "./api/updateBeatmapPage";
|
|
57
|
+
import { POST as vetoMap } from "./api/vetoMap";
|
|
58
|
+
import { NextResponse } from "./utils/response";
|
|
59
|
+
|
|
60
|
+
type RouteHandler = (request: Request) => Promise<Response> | Response;
|
|
61
|
+
|
|
62
|
+
const corsHeaders = {
|
|
63
|
+
"Access-Control-Allow-Credentials": "true",
|
|
64
|
+
"Access-Control-Allow-Origin": "*",
|
|
65
|
+
"Access-Control-Allow-Methods": "GET,OPTIONS,PATCH,DELETE,POST,PUT",
|
|
66
|
+
"Access-Control-Allow-Headers":
|
|
67
|
+
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const apiRoutes: Record<string, RouteHandler> = {
|
|
71
|
+
"/api/acceptInvite": acceptInvite,
|
|
72
|
+
"/api/addCollectionMap": addCollectionMap,
|
|
73
|
+
"/api/chartPublicStats": chartPublicStats,
|
|
74
|
+
"/api/checkQualified": checkQualified,
|
|
75
|
+
"/api/createBeatmap": createBeatmap,
|
|
76
|
+
"/api/createBeatmapPage": createBeatmapPage,
|
|
77
|
+
"/api/createClan": createClan,
|
|
78
|
+
"/api/createCollection": createCollection,
|
|
79
|
+
"/api/createInvite": createInvite,
|
|
80
|
+
"/api/createSupporter": createSupporter,
|
|
81
|
+
"/api/deleteBeatmapPage": deleteBeatmapPage,
|
|
82
|
+
"/api/deleteCollection": deleteCollection,
|
|
83
|
+
"/api/deleteCollectionMap": deleteCollectionMap,
|
|
84
|
+
"/api/editAboutMe": editAboutMe,
|
|
85
|
+
"/api/editClan": editClan,
|
|
86
|
+
"/api/editCollection": editCollection,
|
|
87
|
+
"/api/editProfile": editProfile,
|
|
88
|
+
"/api/editProfileFriend": editProfileFriend,
|
|
89
|
+
"/api/enhancedSearch": enhancedSearch,
|
|
90
|
+
"/api/executeAdminOperation": executeAdminOperation,
|
|
91
|
+
"/api/getAvatarUploadUrl": getAvatarUploadUrl,
|
|
92
|
+
"/api/getBadgeLeaders": getBadgeLeaders,
|
|
93
|
+
"/api/getBadgedUsers": getBadgedUsers,
|
|
94
|
+
"/api/getBeatmapComments": getBeatmapComments,
|
|
95
|
+
"/api/getBeatmapPage": getBeatmapPage,
|
|
96
|
+
"/api/getBeatmapPageById": getBeatmapPageById,
|
|
97
|
+
"/api/getBeatmapStarRating": getBeatmapStarRating,
|
|
98
|
+
"/api/getBeatmaps": getBeatmaps,
|
|
99
|
+
"/api/getClan": getClan,
|
|
100
|
+
"/api/getClans": getClans,
|
|
101
|
+
"/api/getCollection": getCollection,
|
|
102
|
+
"/api/getCollections": getCollections,
|
|
103
|
+
"/api/getFriends": getFriends,
|
|
104
|
+
"/api/getInventory": getInventory,
|
|
105
|
+
"/api/getLeaderboard": getLeaderboard,
|
|
106
|
+
"/api/getMapUploadUrl": getMapUploadUrl,
|
|
107
|
+
"/api/getOnlinePlayers": getOnlinePlayers,
|
|
108
|
+
"/api/getPassToken": getPassToken,
|
|
109
|
+
"/api/getProfile": getProfile,
|
|
110
|
+
"/api/getPublicStats": getPublicStats,
|
|
111
|
+
"/api/getRawStarRating": getRawStarRating,
|
|
112
|
+
"/api/getScore": getScore,
|
|
113
|
+
"/api/getStoryBeatmaps": getStoryBeatmaps,
|
|
114
|
+
"/api/getTimestamp": getTimestamp,
|
|
115
|
+
"/api/getUserScores": getUserScores,
|
|
116
|
+
"/api/getVerified": getVerified,
|
|
117
|
+
"/api/getVideoUploadUrl": getVideoUploadUrl,
|
|
118
|
+
"/api/postBeatmapComment": postBeatmapComment,
|
|
119
|
+
"/api/qualifyMap": qualifyMap,
|
|
120
|
+
"/api/rankMapsArchive": rankMapsArchive,
|
|
121
|
+
"/api/reportProfile": reportProfile,
|
|
122
|
+
"/api/searchUsers": searchUsers,
|
|
123
|
+
"/api/setPasskey": setPasskey,
|
|
124
|
+
"/api/submitScore": submitScore,
|
|
125
|
+
"/api/submitScoreInternal": submitScoreInternal,
|
|
126
|
+
"/api/updateBeatmapPage": updateBeatmapPage,
|
|
127
|
+
"/api/vetoMap": vetoMap,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function withCors(response: Response) {
|
|
131
|
+
const headers = new Headers(response.headers);
|
|
132
|
+
|
|
133
|
+
for (const [key, value] of Object.entries(corsHeaders)) {
|
|
134
|
+
headers.set(key, value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return new Response(response.body, {
|
|
138
|
+
status: response.status,
|
|
139
|
+
statusText: response.statusText,
|
|
140
|
+
headers,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function notFound() {
|
|
145
|
+
return NextResponse.json({ error: "Not Found" }, { status: 404 });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default {
|
|
149
|
+
async fetch(request: Request) {
|
|
150
|
+
const url = new URL(request.url);
|
|
151
|
+
const pathname =
|
|
152
|
+
url.pathname.endsWith("/") && url.pathname.length > 1
|
|
153
|
+
? url.pathname.slice(0, -1)
|
|
154
|
+
: url.pathname;
|
|
155
|
+
|
|
156
|
+
if (request.method === "OPTIONS") {
|
|
157
|
+
return withCors(new Response(null, { status: 204 }));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (pathname === "/") {
|
|
161
|
+
return withCors(
|
|
162
|
+
new Response("<html><div>Rhythia API</div></html>", {
|
|
163
|
+
headers: {
|
|
164
|
+
"content-type": "text/html; charset=utf-8",
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const handler = apiRoutes[pathname];
|
|
171
|
+
if (!handler) {
|
|
172
|
+
return withCors(notFound());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (request.method !== "POST") {
|
|
176
|
+
return withCors(
|
|
177
|
+
NextResponse.json({ error: "Method Not Allowed" }, { status: 405 })
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
return withCors(await handler(request));
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(error);
|
|
185
|
+
return withCors(
|
|
186
|
+
NextResponse.json(
|
|
187
|
+
{
|
|
188
|
+
error: "Internal Server Error",
|
|
189
|
+
},
|
|
190
|
+
{ status: 500 }
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
};
|