rhythia-api 243.0.0 → 244.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 +13 -7
- package/api/executeAdminOperation.ts +1291 -1030
- package/api/getChangelog.ts +46 -0
- package/api/getProfile.ts +297 -297
- package/handleApi.ts +24 -24
- package/index.ts +72 -50
- package/package.json +4 -2
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/linked-project.json +1 -0
- package/types/database.ts +178 -1
- package/utils/beatmapHash.ts +239 -336
- package/utils/moderation.ts +101 -0
- package/utils/requestUtils.ts +2 -2
- package/utils/star-calc/formatSingle.ts +107 -0
- package/utils/star-calc/rhmParser.ts +214 -0
- package/worker.ts +197 -195
- package/.env +0 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { NextResponse } from "../utils/response";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi } from "../utils/requestUtils";
|
|
4
|
+
import { supabase } from "../utils/supabase";
|
|
5
|
+
|
|
6
|
+
const changelogType = z.enum(["public", "testing", "web"]);
|
|
7
|
+
|
|
8
|
+
export const Schema = {
|
|
9
|
+
input: z.strictObject({
|
|
10
|
+
type: changelogType,
|
|
11
|
+
skip: z.number().default(0),
|
|
12
|
+
limit: z.number().default(10),
|
|
13
|
+
}),
|
|
14
|
+
output: z.array(
|
|
15
|
+
z.object({
|
|
16
|
+
id: z.number(),
|
|
17
|
+
type: z.string(),
|
|
18
|
+
date: z.string(),
|
|
19
|
+
markdown: z.string(),
|
|
20
|
+
})
|
|
21
|
+
),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
25
|
+
return protectedApi({
|
|
26
|
+
request,
|
|
27
|
+
schema: Schema,
|
|
28
|
+
authorization: () => { },
|
|
29
|
+
activity: handler,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function handler(data: (typeof Schema)["input"]["_type"]) {
|
|
34
|
+
const { data: changelog, error } = await supabase
|
|
35
|
+
.from("changelogs")
|
|
36
|
+
.select("id,type,date,markdown")
|
|
37
|
+
.eq("type", data.type)
|
|
38
|
+
.order("date", { ascending: false })
|
|
39
|
+
.range(data.skip, data.skip + data.limit - 1);
|
|
40
|
+
|
|
41
|
+
if (error) {
|
|
42
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return NextResponse.json(changelog || []);
|
|
46
|
+
}
|
package/api/getProfile.ts
CHANGED
|
@@ -1,297 +1,297 @@
|
|
|
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
|
-
import { User } from "@supabase/supabase-js";
|
|
9
|
-
import {
|
|
10
|
-
getActivityStatusForUserId,
|
|
11
|
-
getScoreActivityCutoffIso,
|
|
12
|
-
} from "../utils/activityStatus";
|
|
13
|
-
|
|
14
|
-
const friendStateSchema = z.enum(["none", "added", "mutual"]);
|
|
15
|
-
|
|
16
|
-
function getNextUsernameChangeAt(
|
|
17
|
-
lastChangedAt: string | null | undefined
|
|
18
|
-
): string | null {
|
|
19
|
-
if (!lastChangedAt) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const nextAllowedAt = new Date(lastChangedAt);
|
|
24
|
-
nextAllowedAt.setUTCMonth(nextAllowedAt.getUTCMonth() + 6);
|
|
25
|
-
return nextAllowedAt.toISOString();
|
|
26
|
-
}
|
|
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
|
-
|
|
73
|
-
export const Schema = {
|
|
74
|
-
input: z.strictObject({
|
|
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(),
|
|
95
|
-
skill_points: z.number().nullable(),
|
|
96
|
-
squares_hit: z.number().nullable(),
|
|
97
|
-
total_score: z.number().nullable(),
|
|
98
|
-
position: z.number().nullable(),
|
|
99
|
-
country_position: z.number().nullable(),
|
|
100
|
-
activity_status: z.enum(["active", "inactive"]),
|
|
101
|
-
is_online: z.boolean(),
|
|
102
|
-
last_active_timestamp: z.number().nullable(),
|
|
103
|
-
friend_count: z.number(),
|
|
104
|
-
friend_state: friendStateSchema,
|
|
105
|
-
can_change_flag: z.boolean(),
|
|
106
|
-
next_username_change_at: z.string().nullable(),
|
|
107
|
-
previous_usernames: z.array(
|
|
108
|
-
z.object({
|
|
109
|
-
username: z.string(),
|
|
110
|
-
changed_at: z.string(),
|
|
111
|
-
})
|
|
112
|
-
),
|
|
113
|
-
clans: z
|
|
114
|
-
.object({
|
|
115
|
-
id: z.number(),
|
|
116
|
-
acronym: z.string(),
|
|
117
|
-
})
|
|
118
|
-
.optional()
|
|
119
|
-
.nullable(),
|
|
120
|
-
})
|
|
121
|
-
.optional(),
|
|
122
|
-
}),
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
export async function POST(request: Request): Promise<NextResponse> {
|
|
126
|
-
return protectedApi({
|
|
127
|
-
request,
|
|
128
|
-
schema: Schema,
|
|
129
|
-
authorization: () => {},
|
|
130
|
-
activity: handler,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
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
|
-
}
|
|
234
|
-
|
|
235
|
-
let position: number | null = null;
|
|
236
|
-
let countryPosition: number | null = null;
|
|
237
|
-
if (activityStatus === "active") {
|
|
238
|
-
const cutoffIso = getScoreActivityCutoffIso();
|
|
239
|
-
const globalPositionQuery = supabase
|
|
240
|
-
.from("profiles")
|
|
241
|
-
.select("id,scores!inner(id)", { count: "exact", head: true })
|
|
242
|
-
.neq("ban", "excluded")
|
|
243
|
-
.gte("scores.created_at", cutoffIso)
|
|
244
|
-
.gt("skill_points", user.skill_points);
|
|
245
|
-
|
|
246
|
-
const countryPositionQuery = user.flag
|
|
247
|
-
? supabase
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
: Promise.resolve({ count: null });
|
|
255
|
-
|
|
256
|
-
const [{ count: playersWithMorePoints }, { count: countryPlayersWithMorePoints }] =
|
|
257
|
-
await Promise.all([globalPositionQuery, countryPositionQuery]);
|
|
258
|
-
|
|
259
|
-
position = (playersWithMorePoints || 0) + 1;
|
|
260
|
-
countryPosition =
|
|
261
|
-
user.flag && countryPlayersWithMorePoints !== null
|
|
262
|
-
? (countryPlayersWithMorePoints || 0) + 1
|
|
263
|
-
: null;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (user.verificationDeadline < Date.now()) {
|
|
267
|
-
await supabase
|
|
268
|
-
.from("profiles")
|
|
269
|
-
.upsert({
|
|
270
|
-
id: user.id,
|
|
271
|
-
verified: false,
|
|
272
|
-
})
|
|
273
|
-
.select();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const previousUsernames = (usernameHistoryData || []).filter((entry) => {
|
|
277
|
-
return entry.username.toLowerCase() !== (user.username || "").toLowerCase();
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const latestUsernameChangeAt = usernameHistoryData?.[0]?.changed_at ?? null;
|
|
281
|
-
|
|
282
|
-
return NextResponse.json({
|
|
283
|
-
user: {
|
|
284
|
-
...user,
|
|
285
|
-
position,
|
|
286
|
-
country_position: countryPosition,
|
|
287
|
-
activity_status: activityStatus,
|
|
288
|
-
is_online: isOnline,
|
|
289
|
-
last_active_timestamp: activityData?.last_activity ?? null,
|
|
290
|
-
friend_count: friendCount || 0,
|
|
291
|
-
friend_state: friendState,
|
|
292
|
-
can_change_flag: !flagHistoryData?.length,
|
|
293
|
-
next_username_change_at: getNextUsernameChangeAt(latestUsernameChangeAt),
|
|
294
|
-
previous_usernames: previousUsernames,
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
|
-
}
|
|
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
|
+
import { User } from "@supabase/supabase-js";
|
|
9
|
+
import {
|
|
10
|
+
getActivityStatusForUserId,
|
|
11
|
+
getScoreActivityCutoffIso,
|
|
12
|
+
} from "../utils/activityStatus";
|
|
13
|
+
|
|
14
|
+
const friendStateSchema = z.enum(["none", "added", "mutual"]);
|
|
15
|
+
|
|
16
|
+
function getNextUsernameChangeAt(
|
|
17
|
+
lastChangedAt: string | null | undefined
|
|
18
|
+
): string | null {
|
|
19
|
+
if (!lastChangedAt) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nextAllowedAt = new Date(lastChangedAt);
|
|
24
|
+
nextAllowedAt.setUTCMonth(nextAllowedAt.getUTCMonth() + 6);
|
|
25
|
+
return nextAllowedAt.toISOString();
|
|
26
|
+
}
|
|
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
|
+
|
|
73
|
+
export const Schema = {
|
|
74
|
+
input: z.strictObject({
|
|
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(),
|
|
95
|
+
skill_points: z.number().nullable(),
|
|
96
|
+
squares_hit: z.number().nullable(),
|
|
97
|
+
total_score: z.number().nullable(),
|
|
98
|
+
position: z.number().nullable(),
|
|
99
|
+
country_position: z.number().nullable(),
|
|
100
|
+
activity_status: z.enum(["active", "inactive"]),
|
|
101
|
+
is_online: z.boolean(),
|
|
102
|
+
last_active_timestamp: z.number().nullable(),
|
|
103
|
+
friend_count: z.number(),
|
|
104
|
+
friend_state: friendStateSchema,
|
|
105
|
+
can_change_flag: z.boolean(),
|
|
106
|
+
next_username_change_at: z.string().nullable(),
|
|
107
|
+
previous_usernames: z.array(
|
|
108
|
+
z.object({
|
|
109
|
+
username: z.string(),
|
|
110
|
+
changed_at: z.string(),
|
|
111
|
+
})
|
|
112
|
+
),
|
|
113
|
+
clans: z
|
|
114
|
+
.object({
|
|
115
|
+
id: z.number(),
|
|
116
|
+
acronym: z.string(),
|
|
117
|
+
})
|
|
118
|
+
.optional()
|
|
119
|
+
.nullable(),
|
|
120
|
+
})
|
|
121
|
+
.optional(),
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
126
|
+
return protectedApi({
|
|
127
|
+
request,
|
|
128
|
+
schema: Schema,
|
|
129
|
+
authorization: () => { },
|
|
130
|
+
activity: handler,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
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
|
+
}
|
|
234
|
+
|
|
235
|
+
let position: number | null = null;
|
|
236
|
+
let countryPosition: number | null = null;
|
|
237
|
+
if (activityStatus === "active") {
|
|
238
|
+
const cutoffIso = getScoreActivityCutoffIso();
|
|
239
|
+
const globalPositionQuery = supabase
|
|
240
|
+
.from("profiles")
|
|
241
|
+
.select("id,scores!inner(id)", { count: "exact", head: true })
|
|
242
|
+
.neq("ban", "excluded")
|
|
243
|
+
.gte("scores.created_at", cutoffIso)
|
|
244
|
+
.gt("skill_points", user.skill_points);
|
|
245
|
+
|
|
246
|
+
const countryPositionQuery = user.flag
|
|
247
|
+
? supabase
|
|
248
|
+
.from("profiles")
|
|
249
|
+
.select("id,scores!inner(id)", { count: "exact", head: true })
|
|
250
|
+
.neq("ban", "excluded")
|
|
251
|
+
.eq("flag", user.flag)
|
|
252
|
+
.gte("scores.created_at", cutoffIso)
|
|
253
|
+
.gt("skill_points", user.skill_points)
|
|
254
|
+
: Promise.resolve({ count: null });
|
|
255
|
+
|
|
256
|
+
const [{ count: playersWithMorePoints }, { count: countryPlayersWithMorePoints }] =
|
|
257
|
+
await Promise.all([globalPositionQuery, countryPositionQuery]);
|
|
258
|
+
|
|
259
|
+
position = (playersWithMorePoints || 0) + 1;
|
|
260
|
+
countryPosition =
|
|
261
|
+
user.flag && countryPlayersWithMorePoints !== null
|
|
262
|
+
? (countryPlayersWithMorePoints || 0) + 1
|
|
263
|
+
: null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (user.verificationDeadline < Date.now()) {
|
|
267
|
+
await supabase
|
|
268
|
+
.from("profiles")
|
|
269
|
+
.upsert({
|
|
270
|
+
id: user.id,
|
|
271
|
+
verified: false,
|
|
272
|
+
})
|
|
273
|
+
.select();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const previousUsernames = (usernameHistoryData || []).filter((entry) => {
|
|
277
|
+
return entry.username.toLowerCase() !== (user.username || "").toLowerCase();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const latestUsernameChangeAt = usernameHistoryData?.[0]?.changed_at ?? null;
|
|
281
|
+
|
|
282
|
+
return NextResponse.json({
|
|
283
|
+
user: {
|
|
284
|
+
...user,
|
|
285
|
+
position,
|
|
286
|
+
country_position: countryPosition,
|
|
287
|
+
activity_status: activityStatus,
|
|
288
|
+
is_online: isOnline,
|
|
289
|
+
last_active_timestamp: activityData?.last_activity ?? null,
|
|
290
|
+
friend_count: friendCount || 0,
|
|
291
|
+
friend_state: friendState,
|
|
292
|
+
can_change_flag: !flagHistoryData?.length,
|
|
293
|
+
next_username_change_at: getNextUsernameChangeAt(latestUsernameChangeAt),
|
|
294
|
+
previous_usernames: previousUsernames,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|