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.
@@ -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
- .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
- }
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
+ }