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/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
- const { data: usernameHistoryData } = await supabase
153
- .from("profileUsernames")
154
- .select("username,changed_at")
155
- .eq("profile_id", user.id)
156
- .order("changed_at", { ascending: false });
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
- const { data: flagHistoryData } = await supabase
159
- .from("profileFlags")
160
- .select("id")
161
- .eq("profile_id", user.id)
162
- .limit(1);
163
-
164
- //last 30 minutes
165
- if (activityData && activityData.last_activity) {
166
- isOnline = Date.now() - activityData.last_activity < 1800000;
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": "240.0.0",
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
@@ -0,0 +1,141 @@
1
+ export const COUNTRY_LIST = [
2
+ "US",
3
+ "AE",
4
+ "AM",
5
+ "AR",
6
+ "AT",
7
+ "AU",
8
+ "AZ",
9
+ "BA",
10
+ "BB",
11
+ "BD",
12
+ "BE",
13
+ "BG",
14
+ "BH",
15
+ "BN",
16
+ "BO",
17
+ "BR",
18
+ "BT",
19
+ "BW",
20
+ "BY",
21
+ "CA",
22
+ "CH",
23
+ "CI",
24
+ "CL",
25
+ "CN",
26
+ "CO",
27
+ "CR",
28
+ "CU",
29
+ "CW",
30
+ "CY",
31
+ "CZ",
32
+ "DE",
33
+ "DJ",
34
+ "DK",
35
+ "DO",
36
+ "DZ",
37
+ "EC",
38
+ "EE",
39
+ "EG",
40
+ "ES",
41
+ "ET",
42
+ "FI",
43
+ "FJ",
44
+ "FO",
45
+ "FR",
46
+ "GA",
47
+ "GB",
48
+ "GE",
49
+ "GH",
50
+ "GR",
51
+ "GT",
52
+ "GU",
53
+ "HK",
54
+ "HN",
55
+ "HR",
56
+ "HU",
57
+ "ID",
58
+ "IE",
59
+ "IL",
60
+ "IM",
61
+ "IN",
62
+ "IQ",
63
+ "IR",
64
+ "IS",
65
+ "IT",
66
+ "JE",
67
+ "JM",
68
+ "JO",
69
+ "JP",
70
+ "KE",
71
+ "KG",
72
+ "KH",
73
+ "KR",
74
+ "KW",
75
+ "LI",
76
+ "LK",
77
+ "LT",
78
+ "LU",
79
+ "LV",
80
+ "MA",
81
+ "MC",
82
+ "MD",
83
+ "MG",
84
+ "MK",
85
+ "MM",
86
+ "MN",
87
+ "MT",
88
+ "MU",
89
+ "MV",
90
+ "MX",
91
+ "MY",
92
+ "NA",
93
+ "NC",
94
+ "NG",
95
+ "NL",
96
+ "NO",
97
+ "NP",
98
+ "NZ",
99
+ "OM",
100
+ "PA",
101
+ "PE",
102
+ "PG",
103
+ "PH",
104
+ "PK",
105
+ "PL",
106
+ "PR",
107
+ "PS",
108
+ "PT",
109
+ "PY",
110
+ "QA",
111
+ "RE",
112
+ "RO",
113
+ "RU",
114
+ "SA",
115
+ "SD",
116
+ "SE",
117
+ "SG",
118
+ "SI",
119
+ "SK",
120
+ "SL",
121
+ "SN",
122
+ "SR",
123
+ "SV",
124
+ "SY",
125
+ "TG",
126
+ "TH",
127
+ "TN",
128
+ "TR",
129
+ "TT",
130
+ "TW",
131
+ "TZ",
132
+ "UA",
133
+ "UY",
134
+ "UZ",
135
+ "KZ",
136
+ "VE",
137
+ "VN",
138
+ "XK",
139
+ "ZA",
140
+ "ZW",
141
+ ] as const;