rhythia-api 240.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 +103 -92
- 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/utils/countryList.ts +141 -0
- package/worker.ts +195 -191
package/api/editProfile.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { NextResponse } from "../utils/response";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
import { Database } from "../types/database";
|
|
1
|
+
import { NextResponse } from "../utils/response";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { Database } from "../types/database";
|
|
4
4
|
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
5
5
|
import { supabase } from "../utils/supabase";
|
|
6
6
|
import { getUserBySession } from "../utils/getUserBySession";
|
|
@@ -58,58 +58,69 @@ export const Schema = {
|
|
|
58
58
|
next_username_change_at: z.string().nullable().optional(),
|
|
59
59
|
}),
|
|
60
60
|
};
|
|
61
|
-
|
|
62
|
-
export async function POST(request: Request): Promise<NextResponse> {
|
|
63
|
-
return protectedApi({
|
|
64
|
-
request,
|
|
65
|
-
schema: Schema,
|
|
66
|
-
authorization: validUser,
|
|
67
|
-
activity: handler,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function handler(
|
|
72
|
-
data: (typeof Schema)["input"]["_type"]
|
|
73
|
-
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
74
|
-
if (data.data.username !== undefined) {
|
|
75
|
-
if (data.data.username.length < 3) {
|
|
76
|
-
return NextResponse.json(
|
|
77
|
-
{
|
|
78
|
-
error: "Username must be at least 3 characters long",
|
|
79
|
-
},
|
|
80
|
-
{ status: 404 }
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (data.data.username.length > 20) {
|
|
85
|
-
return NextResponse.json(
|
|
86
|
-
{
|
|
87
|
-
error: "Username too long.",
|
|
88
|
-
},
|
|
89
|
-
{ status: 404 }
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!/^[a-z0-9]+$/i.test(data.data.username)) {
|
|
94
|
-
return NextResponse.json(
|
|
95
|
-
{
|
|
96
|
-
error: "Username can only contain letters (a-z) and numbers (0-9)",
|
|
97
|
-
},
|
|
98
|
-
{ status: 404 }
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (validator.trim(data.data.username || "") !== (data.data.username || "")) {
|
|
104
|
-
return NextResponse.json(
|
|
105
|
-
{
|
|
106
|
-
error: "Username can't start or end with spaces.",
|
|
107
|
-
},
|
|
108
|
-
{ status: 404 }
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
61
|
|
|
112
|
-
|
|
62
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
63
|
+
return protectedApi({
|
|
64
|
+
request,
|
|
65
|
+
schema: Schema,
|
|
66
|
+
authorization: validUser,
|
|
67
|
+
activity: handler,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function handler(
|
|
72
|
+
data: (typeof Schema)["input"]["_type"]
|
|
73
|
+
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
74
|
+
if (data.data.username !== undefined) {
|
|
75
|
+
data.data.username = removeZeroWidth(data.data.username);
|
|
76
|
+
|
|
77
|
+
if (validator.trim(data.data.username) !== data.data.username) {
|
|
78
|
+
return NextResponse.json(
|
|
79
|
+
{
|
|
80
|
+
error: "Username can't start or end with spaces.",
|
|
81
|
+
},
|
|
82
|
+
{ status: 404 }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (data.data.username.length < 2) {
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{
|
|
89
|
+
error: "Username must be at least 2 characters long",
|
|
90
|
+
},
|
|
91
|
+
{ status: 404 }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (data.data.username.length > 16) {
|
|
96
|
+
return NextResponse.json(
|
|
97
|
+
{
|
|
98
|
+
error: "Username must be 16 characters or fewer.",
|
|
99
|
+
},
|
|
100
|
+
{ status: 404 }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
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
|
+
}
|
|
113
|
+
|
|
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();
|
|
@@ -127,38 +138,38 @@ export async function handler(
|
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
const user = (await getUserBySession(data.session)) as User;
|
|
130
|
-
|
|
131
|
-
let userData: Database["public"]["Tables"]["profiles"]["Update"];
|
|
132
|
-
|
|
133
|
-
// Find user's entry
|
|
134
|
-
{
|
|
135
|
-
let { data: queryUserData, error } = await supabase
|
|
136
|
-
.from("profiles")
|
|
137
|
-
.select("*")
|
|
138
|
-
.eq("uid", user.id);
|
|
139
|
-
|
|
140
|
-
if (!queryUserData?.length) {
|
|
141
|
-
return NextResponse.json(
|
|
142
|
-
{
|
|
143
|
-
error: "User cannot be retrieved from session",
|
|
144
|
-
},
|
|
145
|
-
{ status: 404 }
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
userData = queryUserData[0];
|
|
149
|
-
}
|
|
150
|
-
|
|
141
|
+
|
|
142
|
+
let userData: Database["public"]["Tables"]["profiles"]["Update"];
|
|
143
|
+
|
|
144
|
+
// Find user's entry
|
|
145
|
+
{
|
|
146
|
+
let { data: queryUserData, error } = await supabase
|
|
147
|
+
.from("profiles")
|
|
148
|
+
.select("*")
|
|
149
|
+
.eq("uid", user.id);
|
|
150
|
+
|
|
151
|
+
if (!queryUserData?.length) {
|
|
152
|
+
return NextResponse.json(
|
|
153
|
+
{
|
|
154
|
+
error: "User cannot be retrieved from session",
|
|
155
|
+
},
|
|
156
|
+
{ status: 404 }
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
userData = queryUserData[0];
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
if (
|
|
152
163
|
userData.ban == "excluded" ||
|
|
153
164
|
userData.ban == "restricted" ||
|
|
154
165
|
userData.ban == "silenced"
|
|
155
166
|
) {
|
|
156
|
-
return NextResponse.json(
|
|
157
|
-
{
|
|
158
|
-
error:
|
|
159
|
-
"Silenced, restricted or excluded players can't update their profile.",
|
|
160
|
-
},
|
|
161
|
-
{ status: 404 }
|
|
167
|
+
return NextResponse.json(
|
|
168
|
+
{
|
|
169
|
+
error:
|
|
170
|
+
"Silenced, restricted or excluded players can't update their profile.",
|
|
171
|
+
},
|
|
172
|
+
{ status: 404 }
|
|
162
173
|
);
|
|
163
174
|
}
|
|
164
175
|
|
|
@@ -236,13 +247,13 @@ export async function handler(
|
|
|
236
247
|
id: userData.id,
|
|
237
248
|
computedUsername: data.data.username?.toLowerCase(),
|
|
238
249
|
...data.data,
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
const upsertResult = await supabase
|
|
242
|
-
.from("profiles")
|
|
243
|
-
.upsert(upsertPayload)
|
|
244
|
-
.select();
|
|
245
|
-
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const upsertResult = await supabase
|
|
253
|
+
.from("profiles")
|
|
254
|
+
.upsert(upsertPayload)
|
|
255
|
+
.select();
|
|
256
|
+
|
|
246
257
|
if (upsertResult.error) {
|
|
247
258
|
if (
|
|
248
259
|
usernameHasChanged &&
|
|
@@ -287,10 +298,10 @@ export async function handler(
|
|
|
287
298
|
return NextResponse.json(
|
|
288
299
|
{
|
|
289
300
|
error: "Can't update, username might be used by someone else!",
|
|
290
|
-
},
|
|
291
|
-
{ status: 404 }
|
|
292
|
-
);
|
|
293
|
-
}
|
|
301
|
+
},
|
|
302
|
+
{ status: 404 }
|
|
303
|
+
);
|
|
304
|
+
}
|
|
294
305
|
|
|
295
306
|
return NextResponse.json({});
|
|
296
307
|
}
|
|
@@ -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
|
+
}
|