rhythia-api 237.0.0 → 238.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.
@@ -7,6 +7,56 @@ import { getUserBySession } from "../utils/getUserBySession";
7
7
  import { User } from "@supabase/supabase-js";
8
8
  import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
9
9
 
10
+ type AdminProfileSummary = Pick<
11
+ Database["public"]["Tables"]["profiles"]["Row"],
12
+ "id" | "username" | "avatar_url" | "flag" | "badges" | "ban"
13
+ >;
14
+
15
+ type InvestigationDateRange = {
16
+ firstSeenAt: string | null;
17
+ lastSeenAt: string | null;
18
+ };
19
+
20
+ type LinkedAccountAggregate = AdminProfileSummary &
21
+ InvestigationDateRange & {
22
+ occurrences: number;
23
+ sharedHwids: Set<string>;
24
+ };
25
+
26
+ type LinkedProfileAggregate = AdminProfileSummary &
27
+ InvestigationDateRange & {
28
+ occurrences: number;
29
+ };
30
+
31
+ const MULTIACCOUNT_PROFILE_SELECT =
32
+ "id, username, avatar_url, flag, badges, ban";
33
+
34
+ const SCORE_CACHE_READ_OPERATIONS = new Set([
35
+ "getScoresPaginated",
36
+ "getMultiaccountInvestigation",
37
+ ]);
38
+
39
+ function timestampOrZero(value: string | null) {
40
+ return value ? new Date(value).getTime() : 0;
41
+ }
42
+
43
+ function updateDateRange(
44
+ range: InvestigationDateRange,
45
+ createdAt: string | null
46
+ ) {
47
+ if (!createdAt) {
48
+ return;
49
+ }
50
+
51
+ if (!range.firstSeenAt || createdAt < range.firstSeenAt) {
52
+ range.firstSeenAt = createdAt;
53
+ }
54
+
55
+ if (!range.lastSeenAt || createdAt > range.lastSeenAt) {
56
+ range.lastSeenAt = createdAt;
57
+ }
58
+ }
59
+
10
60
  // Define supported admin operations and their parameter types
11
61
  const adminOperations = {
12
62
  deleteUser: z.object({ userId: z.number() }),
@@ -28,6 +78,7 @@ const adminOperations = {
28
78
  userId: z.number().optional(),
29
79
  includeAdditionalData: z.boolean().default(true),
30
80
  }),
81
+ getMultiaccountInvestigation: z.object({ userId: z.number() }),
31
82
  } as const;
32
83
 
33
84
  // Create a discriminated union type for operation parameters
@@ -88,6 +139,10 @@ const OperationParam = z.discriminatedUnion("operation", [
88
139
  operation: z.literal("getScoresPaginated"),
89
140
  params: adminOperations.getScoresPaginated,
90
141
  }),
142
+ z.object({
143
+ operation: z.literal("getMultiaccountInvestigation"),
144
+ params: adminOperations.getMultiaccountInvestigation,
145
+ }),
91
146
  ]);
92
147
 
93
148
  export const Schema = {
@@ -346,6 +401,242 @@ export async function handler(
346
401
  };
347
402
  }
348
403
  break;
404
+
405
+ case "getMultiaccountInvestigation":
406
+ const { data: investigatedUser, error: investigatedUserError } =
407
+ await supabase
408
+ .from("profiles")
409
+ .select(MULTIACCOUNT_PROFILE_SELECT)
410
+ .eq("id", params.userId)
411
+ .maybeSingle();
412
+
413
+ if (investigatedUserError) {
414
+ result = { error: investigatedUserError, data: null };
415
+ break;
416
+ }
417
+
418
+ if (!investigatedUser) {
419
+ result = {
420
+ error: { message: "User not found" },
421
+ data: null,
422
+ };
423
+ break;
424
+ }
425
+
426
+ const { data: investigatedUserHwids, error: investigatedUserHwidsError } =
427
+ await supabase
428
+ .from("user_hwids")
429
+ .select("id, hwid, created_at")
430
+ .eq("id", params.userId)
431
+ .order("created_at", { ascending: false });
432
+
433
+ if (investigatedUserHwidsError) {
434
+ result = { error: investigatedUserHwidsError, data: null };
435
+ break;
436
+ }
437
+
438
+ const targetRows = investigatedUserHwids || [];
439
+ const uniqueHwids = Array.from(
440
+ new Set(targetRows.map((row) => row.hwid).filter(Boolean))
441
+ );
442
+
443
+ const dateRange: InvestigationDateRange = {
444
+ firstSeenAt: null,
445
+ lastSeenAt: null,
446
+ };
447
+
448
+ const hwidAggregates = new Map<
449
+ string,
450
+ InvestigationDateRange & {
451
+ hwid: string;
452
+ occurrences: number;
453
+ linkedProfiles: Map<number, LinkedProfileAggregate>;
454
+ }
455
+ >();
456
+
457
+ for (const row of targetRows) {
458
+ updateDateRange(dateRange, row.created_at);
459
+
460
+ const existing = hwidAggregates.get(row.hwid) || {
461
+ hwid: row.hwid,
462
+ occurrences: 0,
463
+ firstSeenAt: null,
464
+ lastSeenAt: null,
465
+ linkedProfiles: new Map<number, LinkedProfileAggregate>(),
466
+ };
467
+
468
+ existing.occurrences += 1;
469
+ updateDateRange(existing, row.created_at);
470
+ hwidAggregates.set(row.hwid, existing);
471
+ }
472
+
473
+ if (uniqueHwids.length === 0) {
474
+ result = {
475
+ data: {
476
+ user: investigatedUser,
477
+ summary: {
478
+ totalRows: 0,
479
+ uniqueHwids: 0,
480
+ sharedHwids: 0,
481
+ linkedAccounts: 0,
482
+ firstSeenAt: null,
483
+ lastSeenAt: null,
484
+ },
485
+ accounts: [],
486
+ hwids: [],
487
+ },
488
+ error: null,
489
+ };
490
+ break;
491
+ }
492
+
493
+ const { data: relatedRows, error: relatedRowsError } = await supabase
494
+ .from("user_hwids")
495
+ .select("id, hwid, created_at")
496
+ .in("hwid", uniqueHwids)
497
+ .order("created_at", { ascending: false });
498
+
499
+ if (relatedRowsError) {
500
+ result = { error: relatedRowsError, data: null };
501
+ break;
502
+ }
503
+
504
+ const relatedProfileIds = Array.from(
505
+ new Set(
506
+ (relatedRows || [])
507
+ .map((row) => row.id)
508
+ .filter((id) => id !== params.userId)
509
+ )
510
+ );
511
+
512
+ const { data: relatedProfiles, error: relatedProfilesError } =
513
+ relatedProfileIds.length > 0
514
+ ? await supabase
515
+ .from("profiles")
516
+ .select(MULTIACCOUNT_PROFILE_SELECT)
517
+ .in("id", relatedProfileIds)
518
+ : { data: [], error: null };
519
+
520
+ if (relatedProfilesError) {
521
+ result = { error: relatedProfilesError, data: null };
522
+ break;
523
+ }
524
+
525
+ const profileMap = new Map<number, AdminProfileSummary>([
526
+ [investigatedUser.id, investigatedUser],
527
+ ...((relatedProfiles || []) as AdminProfileSummary[]).map((profile) => [
528
+ profile.id,
529
+ profile,
530
+ ]),
531
+ ]);
532
+
533
+ const linkedAccountAggregates = new Map<number, LinkedAccountAggregate>();
534
+
535
+ for (const row of relatedRows || []) {
536
+ if (row.id === params.userId) {
537
+ continue;
538
+ }
539
+
540
+ const hwidAggregate = hwidAggregates.get(row.hwid);
541
+ const relatedProfile = profileMap.get(row.id);
542
+
543
+ if (!hwidAggregate || !relatedProfile) {
544
+ continue;
545
+ }
546
+
547
+ const linkedProfile =
548
+ hwidAggregate.linkedProfiles.get(row.id) || {
549
+ ...relatedProfile,
550
+ occurrences: 0,
551
+ firstSeenAt: null,
552
+ lastSeenAt: null,
553
+ };
554
+
555
+ linkedProfile.occurrences += 1;
556
+ updateDateRange(linkedProfile, row.created_at);
557
+ hwidAggregate.linkedProfiles.set(row.id, linkedProfile);
558
+
559
+ const linkedAccount = linkedAccountAggregates.get(row.id) || {
560
+ ...relatedProfile,
561
+ occurrences: 0,
562
+ sharedHwids: new Set<string>(),
563
+ firstSeenAt: null,
564
+ lastSeenAt: null,
565
+ };
566
+
567
+ linkedAccount.occurrences += 1;
568
+ linkedAccount.sharedHwids.add(row.hwid);
569
+ updateDateRange(linkedAccount, row.created_at);
570
+ linkedAccountAggregates.set(row.id, linkedAccount);
571
+ }
572
+
573
+ const accounts = Array.from(linkedAccountAggregates.values())
574
+ .map((account) => ({
575
+ ...account,
576
+ sharedHwids: Array.from(account.sharedHwids).sort(),
577
+ sharedHwidCount: account.sharedHwids.size,
578
+ }))
579
+ .sort((a, b) => {
580
+ if (b.sharedHwidCount !== a.sharedHwidCount) {
581
+ return b.sharedHwidCount - a.sharedHwidCount;
582
+ }
583
+
584
+ if (b.occurrences !== a.occurrences) {
585
+ return b.occurrences - a.occurrences;
586
+ }
587
+
588
+ return timestampOrZero(b.lastSeenAt) - timestampOrZero(a.lastSeenAt);
589
+ });
590
+
591
+ const hwids = Array.from(hwidAggregates.values())
592
+ .map((aggregate) => ({
593
+ hwid: aggregate.hwid,
594
+ occurrences: aggregate.occurrences,
595
+ firstSeenAt: aggregate.firstSeenAt,
596
+ lastSeenAt: aggregate.lastSeenAt,
597
+ linkedAccountCount: aggregate.linkedProfiles.size,
598
+ linkedProfiles: Array.from(aggregate.linkedProfiles.values()).sort(
599
+ (a, b) => {
600
+ if (b.occurrences !== a.occurrences) {
601
+ return b.occurrences - a.occurrences;
602
+ }
603
+
604
+ return (
605
+ timestampOrZero(b.lastSeenAt) - timestampOrZero(a.lastSeenAt)
606
+ );
607
+ }
608
+ ),
609
+ }))
610
+ .sort((a, b) => {
611
+ if (b.linkedAccountCount !== a.linkedAccountCount) {
612
+ return b.linkedAccountCount - a.linkedAccountCount;
613
+ }
614
+
615
+ if (b.occurrences !== a.occurrences) {
616
+ return b.occurrences - a.occurrences;
617
+ }
618
+
619
+ return timestampOrZero(b.lastSeenAt) - timestampOrZero(a.lastSeenAt);
620
+ });
621
+
622
+ result = {
623
+ data: {
624
+ user: investigatedUser,
625
+ summary: {
626
+ totalRows: targetRows.length,
627
+ uniqueHwids: uniqueHwids.length,
628
+ sharedHwids: hwids.filter((hwid) => hwid.linkedAccountCount > 0)
629
+ .length,
630
+ linkedAccounts: accounts.length,
631
+ firstSeenAt: dateRange.firstSeenAt,
632
+ lastSeenAt: dateRange.lastSeenAt,
633
+ },
634
+ accounts,
635
+ hwids,
636
+ },
637
+ error: null,
638
+ };
639
+ break;
349
640
  }
350
641
 
351
642
  // Log the admin action
@@ -366,7 +657,11 @@ export async function handler(
366
657
  );
367
658
  }
368
659
 
369
- if (targetUserId !== null && !result?.error) {
660
+ if (
661
+ targetUserId !== null &&
662
+ !result?.error &&
663
+ !SCORE_CACHE_READ_OPERATIONS.has(operation)
664
+ ) {
370
665
  await invalidateCachePrefix(`userscore:${targetUserId}`);
371
666
  }
372
667
 
package/api/getProfile.ts CHANGED
@@ -40,11 +40,12 @@ export const Schema = {
40
40
  country_position: z.number().nullable(),
41
41
  activity_status: z.enum(["active", "inactive"]),
42
42
  is_online: z.boolean(),
43
+ last_active_timestamp: z.number().nullable(),
43
44
  clans: z
44
45
  .object({
45
46
  id: z.number(),
46
- acronym: z.string(),
47
- })
47
+ acronym: z.string(),
48
+ })
48
49
  .optional()
49
50
  .nullable(),
50
51
  })
@@ -122,11 +123,11 @@ export async function handler(
122
123
 
123
124
  const activityStatus = await getActivityStatusForUserId(user.id);
124
125
 
125
- const { data: activityData } = await supabase
126
- .from("profileActivities")
127
- .select("*")
128
- .eq("uid", user.uid || "")
129
- .single();
126
+ const { data: activityData } = await supabase
127
+ .from("profileActivities")
128
+ .select("last_activity")
129
+ .eq("uid", user.uid || "")
130
+ .single();
130
131
 
131
132
  //last 30 minutes
132
133
  if (activityData && activityData.last_activity) {
@@ -181,6 +182,7 @@ export async function handler(
181
182
  country_position: countryPosition,
182
183
  activity_status: activityStatus,
183
184
  is_online: isOnline,
185
+ last_active_timestamp: activityData?.last_activity ?? null,
184
186
  },
185
187
  });
186
188
  }
package/index.ts CHANGED
@@ -362,17 +362,7 @@ export const enhancedSearch = handleApi({url:"/api/enhancedSearch",...EnhancedSe
362
362
  // ./api/executeAdminOperation.ts API
363
363
 
364
364
  /*
365
- export const Schema = {
366
- input: z.strictObject({
367
- session: z.string(),
368
- data: OperationParam,
369
- }),
370
- output: z.object({
371
- success: z.boolean(),
372
- result: z.any().optional(),
373
- error: z.string().optional(),
374
- }),
375
- };*/
365
+ */
376
366
  import { Schema as ExecuteAdminOperation } from "./api/executeAdminOperation"
377
367
  export { Schema as SchemaExecuteAdminOperation } from "./api/executeAdminOperation"
378
368
  export const executeAdminOperation = handleApi({url:"/api/executeAdminOperation",...ExecuteAdminOperation})
@@ -1001,11 +991,12 @@ export const Schema = {
1001
991
  country_position: z.number().nullable(),
1002
992
  activity_status: z.enum(["active", "inactive"]),
1003
993
  is_online: z.boolean(),
994
+ last_active_timestamp: z.number().nullable(),
1004
995
  clans: z
1005
996
  .object({
1006
997
  id: z.number(),
1007
- acronym: z.string(),
1008
- })
998
+ acronym: z.string(),
999
+ })
1009
1000
  .optional()
1010
1001
  .nullable(),
1011
1002
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "237.0.0",
3
+ "version": "238.0.0",
4
4
  "main": "index.ts",
5
5
  "author": "online-contributors-cunev",
6
6
  "scripts": {