Skip to content

Commit 44f3e80

Browse files
committed
{feat} CordCat new features, updated deprecated modals
1 parent 195d8e8 commit 44f3e80

5 files changed

Lines changed: 281 additions & 86 deletions

File tree

analyzers/CordCat/CordCatModal.tsx

Lines changed: 197 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,69 @@ interface BreachData {
2323
discordname?: string;
2424
categories?: string[];
2525
date?: string;
26+
fields?: Record<string, string>;
27+
}
28+
29+
interface IPGeoData {
30+
asn?: string;
31+
organization?: string;
32+
country?: string;
33+
}
34+
35+
interface FivemResult {
36+
id: number;
37+
license?: string | null;
38+
license2?: string | null;
39+
steam?: string | null;
40+
name?: string | null;
41+
ip?: string | null;
42+
discord_id?: string | null;
43+
discord_name?: string | null;
44+
email?: string | null;
45+
source_file?: string | null;
46+
ip_asn?: IPGeoData | null;
47+
}
48+
49+
interface ScoreSignal {
50+
key: string;
51+
label: string;
52+
detail?: string;
53+
points: number;
54+
kind: "good" | "risk";
55+
}
56+
57+
interface BotInfo {
58+
score: number;
59+
level: string;
60+
isBot: boolean;
61+
reasons?: string[];
62+
}
63+
64+
interface ScoreData {
65+
risk: number;
66+
level: string;
67+
signals?: ScoreSignal[];
68+
bot?: BotInfo;
69+
}
70+
71+
interface MetaData {
72+
cached?: boolean;
73+
fetchedAt?: string;
74+
lastChecked?: string | null;
75+
changes?: unknown[];
76+
}
77+
78+
interface BreachResponseData {
79+
results?: BreachData[];
80+
bySource?: Record<string, Record<string, string>[] | null>;
81+
ipGeo?: Record<string, IPGeoData>;
82+
}
83+
84+
interface GuildTag {
85+
tag: string;
86+
identity_guild_id: string;
87+
identity_enabled: boolean;
88+
badge?: string;
2689
}
2790

2891
interface UserInfo {
@@ -32,23 +95,48 @@ interface UserInfo {
3295
discriminator?: string;
3396
avatar?: string;
3497
banner?: string;
98+
banner_color?: string | null;
3599
public_flags?: number;
100+
flags?: number;
36101
accent_color?: number | null;
37-
clan?: { tag: string; identity_guild_id: string; identity_enabled: boolean; };
38-
primary_guild?: { tag: string; identity_guild_id: string; identity_enabled: boolean; };
102+
avatar_decoration_data?: unknown;
103+
collectibles?: unknown;
104+
display_name_styles?: unknown;
105+
clan?: GuildTag;
106+
primary_guild?: GuildTag;
39107
}
40108

41109
const TEXT_NORMAL = "var(--text-normal, var(--header-primary, #dcddde))";
42110
const TEXT_MUTED = "var(--text-muted, var(--header-secondary, #b5bac1))";
43111

112+
const RISK_COLORS: Record<string, string> = {
113+
low: "var(--status-positive)",
114+
medium: "var(--text-warning)",
115+
high: "var(--status-danger)",
116+
critical: "var(--status-danger)",
117+
};
118+
119+
const SIGNAL_COLORS: Record<ScoreSignal["kind"], string> = {
120+
risk: "var(--status-danger)",
121+
good: "var(--status-positive)",
122+
};
123+
44124
function fmtEnum(value: string | undefined, prefix: string) {
45125
return (value ?? "UNKNOWN").replace(prefix, "").replace(/_/g, " ");
46126
}
47127

48-
function fmtDate(value: string | undefined) {
128+
function fmtDate(value: string | undefined | null) {
49129
return value ? String(value).slice(0, 10) : "—";
50130
}
51131

132+
function fmtDateTime(value: string | undefined | null) {
133+
return value ? String(value).replace("T", " ").slice(0, 19) : "—";
134+
}
135+
136+
function riskColor(level: string | undefined) {
137+
return RISK_COLORS[(level ?? "").toLowerCase()] ?? TEXT_MUTED;
138+
}
139+
52140
function Tag({ children, color }: { children: React.ReactNode; color: string; }) {
53141
return (
54142
<span style={{ background: color, color: "#fff", borderRadius: 3, padding: "1px 6px", fontSize: 11, fontWeight: 700, textTransform: "uppercase" as const }}>
@@ -74,7 +162,7 @@ function SanctionCard({ s }: { s: StatementData; }) {
74162
<Tag color="var(--text-warning)">{fmtEnum(s.category, "STATEMENT_CATEGORY_")}</Tag>
75163
{s.incompatible_content_illegal === "Yes" && <Tag color="#7b0000">ILLEGAL</Tag>}
76164
</div>
77-
{/* {s.incompatible_content_ground && <Field label="Rule broken" value={s.incompatible_content_ground} />} */}
165+
{s.incompatible_content_ground && <Field label="Rule broken" value={s.incompatible_content_ground} />}
78166
{s.incompatible_content_explanation && <Field label="Explanation" value={s.incompatible_content_explanation} />}
79167
{s.category_specification_other && <Field label="Sub-category" value={s.category_specification_other} />}
80168
{s.decision_facts && <Field label="Facts" value={s.decision_facts} />}
@@ -96,6 +184,36 @@ function BreachCard({ b }: { b: BreachData; }) {
96184
);
97185
}
98186

187+
function FivemCard({ r }: { r: FivemResult; }) {
188+
return (
189+
<div style={{ borderLeft: "3px solid var(--brand-experiment, #5865f2)", background: "var(--background-secondary)", borderRadius: 4, padding: "8px 12px", marginBottom: 6 }}>
190+
<div style={{ fontWeight: 700, marginBottom: 4 }}>{r.source_file ?? `Result #${r.id}`}</div>
191+
{r.name && <Field label="Name" value={r.name} />}
192+
{r.license && <Field label="License" value={r.license} />}
193+
{r.steam && <Field label="Steam" value={r.steam} />}
194+
{r.ip && <Field label="IP" value={r.ip} />}
195+
{r.discord_name && <Field label="Discord name" value={r.discord_name} />}
196+
{r.email && <Field label="Email" value={r.email} />}
197+
{r.ip_asn && (
198+
<Field label="IP location" value={[r.ip_asn.organization, r.ip_asn.country].filter(Boolean).join(" — ")} />
199+
)}
200+
</div>
201+
);
202+
}
203+
204+
function SignalRow({ s }: { s: ScoreSignal; }) {
205+
const points = s.points >= 0 ? `+${s.points}` : String(s.points);
206+
return (
207+
<div style={{ display: "flex", gap: 8, alignItems: "baseline", fontSize: 13, marginBottom: 4 }}>
208+
<Tag color={SIGNAL_COLORS[s.kind]}>{points}</Tag>
209+
<div>
210+
<span style={{ color: TEXT_NORMAL, fontWeight: 600 }}>{s.label}</span>
211+
{s.detail && <span style={{ color: TEXT_MUTED }}>{s.detail}</span>}
212+
</div>
213+
</div>
214+
);
215+
}
216+
99217
function SectionTitle({ children }: { children: React.ReactNode; }) {
100218
return (
101219
<div style={{ fontSize: 11, fontWeight: 700, textTransform: "uppercase" as const, letterSpacing: "0.06em", color: TEXT_MUTED, borderBottom: "1px solid var(--background-modifier-accent)", paddingBottom: 3, marginBottom: 8, marginTop: 4 }}>
@@ -107,20 +225,27 @@ function SectionTitle({ children }: { children: React.ReactNode; }) {
107225
export function CordCatModal({ data }: { data: any; }) {
108226
const u: UserInfo = data.userInfo ?? {};
109227
const statements: StatementData[] = data.statements ?? [];
228+
const score: ScoreData | undefined = data.score;
229+
const meta: MetaData | undefined = data.meta;
110230

111-
// Handle breach data with fallbacks
112-
let breachResults: any[] = [];
231+
let breachResults: BreachData[] = [];
113232
let breachError: string | null = null;
233+
let ipGeo: Record<string, IPGeoData> = {};
114234
if (data.breach) {
115235
if (data.breach.success === false && data.breach.error) {
116236
breachError = `${data.breach.error.status}: ${data.breach.error.message}`;
117-
} else if (Array.isArray(data.breach.data?.results)) {
118-
breachResults = data.breach.data.results;
119-
} else if (Array.isArray(data.breach?.results)) {
120-
breachResults = data.breach.results;
237+
} else {
238+
const breachData: BreachResponseData | undefined = data.breach.data ?? data.breach;
239+
if (Array.isArray(breachData?.results)) {
240+
breachResults = breachData.results;
241+
ipGeo = breachData.ipGeo ?? {};
242+
}
121243
}
122244
}
123-
const breachCount: number = data.breach?.resultsCount ?? breachResults.length ?? 0;
245+
const breachCount: number = data.breach?.resultsCount ?? breachResults.length;
246+
247+
const fivemResults: FivemResult[] = Array.isArray(data.fivem?.data?.results) ? data.fivem.data.results : [];
248+
const fivemTotal: number = data.fivem?.data?.total ?? fivemResults.length;
124249

125250
const avatar = u.avatar
126251
? `https://cdn.discordapp.com/avatars/${u.id}/${u.avatar}.${u.avatar.startsWith("a_") ? "gif" : "png"}?size=80`
@@ -136,8 +261,8 @@ export function CordCatModal({ data }: { data: any; }) {
136261

137262
const guild = u.clan ?? u.primary_guild;
138263
const uniqueIPs = [...new Set(breachResults
139-
.map((b: any) => b.ip)
140-
.filter((ip: any) => typeof ip === "string" && ip.length > 0)
264+
.map(b => b.ip)
265+
.filter((ip): ip is string => typeof ip === "string" && ip.length > 0)
141266
)];
142267

143268
return (
@@ -163,8 +288,37 @@ export function CordCatModal({ data }: { data: any; }) {
163288
<Tag color={breachCount > 0 ? "var(--text-warning)" : "var(--status-positive)"}>
164289
{breachCount > 0 ? `${breachCount} breach${breachCount !== 1 ? "es" : ""}` : "No breaches"}
165290
</Tag>
291+
{score && (
292+
<Tag color={riskColor(score.level)}>Risk {score.risk} ({score.level})</Tag>
293+
)}
294+
{score?.bot?.isBot && (
295+
<Tag color="var(--status-danger)">Likely bot</Tag>
296+
)}
166297
</div>
167298

299+
{score && (<>
300+
<SectionTitle>Risk Score</SectionTitle>
301+
<div style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 8 }}>
302+
<div style={{ fontSize: 22, fontWeight: 800, color: riskColor(score.level) }}>{score.risk}</div>
303+
<div>
304+
<div style={{ fontSize: 13, fontWeight: 700, color: TEXT_NORMAL, textTransform: "capitalize" as const }}>{score.level} risk</div>
305+
{score.bot && (
306+
<div style={{ fontSize: 12, color: TEXT_MUTED }}>
307+
Bot likelihood: <span style={{ color: TEXT_NORMAL }}>{score.bot.level}</span> ({score.bot.score}/100)
308+
</div>
309+
)}
310+
</div>
311+
</div>
312+
{score.signals && score.signals.length > 0 && (
313+
<div style={{ marginBottom: 8 }}>
314+
{score.signals.map((s, i) => <SignalRow key={i} s={s} />)}
315+
</div>
316+
)}
317+
{score.bot?.reasons && score.bot.reasons.length > 0 && (
318+
<Field label="Bot reasons" value={score.bot.reasons.join(", ")} />
319+
)}
320+
</>)}
321+
168322
<SectionTitle>User Info</SectionTitle>
169323
{u.global_name && <Field label="Display name" value={u.global_name} />}
170324
<Field label="Username" value={u.username ?? "—"} />
@@ -192,15 +346,43 @@ export function CordCatModal({ data }: { data: any; }) {
192346
</div>
193347
) : uniqueIPs.length > 0 && (
194348
<div style={{ marginBottom: 8, fontSize: 13 }}>
195-
<span style={{ color: TEXT_MUTED }}>Leaked IPs: </span>
196-
{uniqueIPs.map((ip, i) => <code key={i} style={{ marginRight: 8 }}>{ip}</code>)}
349+
<div style={{ color: TEXT_MUTED, marginBottom: 2 }}>Leaked IPs:</div>
350+
{uniqueIPs.map((ip, i) => {
351+
const geo = ipGeo[ip];
352+
return (
353+
<div key={i} style={{ display: "flex", gap: 8, alignItems: "baseline", marginBottom: 2 }}>
354+
<code>{ip}</code>
355+
{geo && (
356+
<span style={{ color: TEXT_MUTED, fontSize: 12 }}>
357+
{[geo.organization, geo.asn, geo.country].filter(Boolean).join(" • ")}
358+
</span>
359+
)}
360+
</div>
361+
);
362+
})}
197363
</div>
198364
)}
199365
{breachError ? null : breachResults.length === 0
200366
? <div style={{ color: TEXT_MUTED, fontSize: 13 }}>No breach data found.</div>
201367
: breachResults.map((b, i) => <BreachCard key={i} b={b} />)
202368
}
203369

370+
{data.fivem && (<>
371+
<SectionTitle>FiveM Leaks ({fivemTotal})</SectionTitle>
372+
{fivemResults.length === 0
373+
? <div style={{ color: TEXT_MUTED, fontSize: 13 }}>No FiveM data found.</div>
374+
: fivemResults.map((r, i) => <FivemCard key={i} r={r} />)
375+
}
376+
</>)}
377+
378+
{meta && (
379+
<div style={{ marginTop: 14, paddingTop: 8, borderTop: "1px solid var(--background-modifier-accent)", display: "flex", flexWrap: "wrap" as const, gap: 12, fontSize: 11, color: TEXT_MUTED }}>
380+
<span>{meta.cached ? "Served from cache" : "Freshly fetched"}</span>
381+
{meta.fetchedAt && <span>Fetched: {fmtDateTime(meta.fetchedAt)}</span>}
382+
{meta.lastChecked && <span>Last checked: {fmtDateTime(meta.lastChecked)}</span>}
383+
</div>
384+
)}
385+
204386
</div>
205387
);
206388
}

analyzers/CordCat/index.tsx

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,25 @@
44
* SPDX-License-Identifier: GPL-3.0-or-later
55
*/
66

7-
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
87
import { PluginNative } from "@utils/types";
9-
import { Button, React, Toasts } from "@webpack/common";
8+
import { Modal, openModal, React, Toasts } from "@webpack/common";
109

1110
import { safeToast } from "../../utils";
1211
import { CordCatModal } from "./CordCatModal";
1312
import { settings } from "../../settings";
14-
import { result } from "lodash";
1513

1614
const Native = VencordNative.pluginHelpers.vAnalyzer as PluginNative<typeof import("./native")>;
1715

1816
export async function analyzeUserWithCordCat(userId: string, username: string): Promise<void> {
19-
safeToast(`Querying CordCat for ${username}...`);
20-
21-
const apiKey = settings.store.cordCatApiKey;
22-
let result;
23-
17+
const apiKey = settings.store.cordCatApiKey?.trim();
2418
if (!apiKey) {
25-
// sometimes cordcat returns data without an apikey, but is unreliable, for some reason the request works without the apikey
26-
result = await Native.queryCordCat(userId);
27-
} else {
28-
result = await Native.queryCordCat(userId, apiKey);
19+
safeToast("CordCat requires an API key. Set it in vAnalyzer settings.", Toasts.Type.FAILURE);
20+
return;
2921
}
3022

23+
safeToast(`Querying CordCat for ${username}...`);
24+
25+
const result = await Native.queryCordCat(userId, apiKey);
3126

3227
if (result.status !== 200) {
3328
safeToast(`CordCat lookup failed: HTTP ${result.status}`, Toasts.Type.FAILURE);
@@ -46,16 +41,13 @@ export async function analyzeUserWithCordCat(userId: string, username: string):
4641
const title = `CordCat: ${data.userInfo?.global_name || username}${suffix}`;
4742

4843
openModal(modalProps => (
49-
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
50-
<ModalHeader>
51-
<span style={{ fontWeight: 700, fontSize: 16, color: "var(--white-500, #fff)" }}>{title}</span>
52-
</ModalHeader>
53-
<ModalContent>
54-
<CordCatModal data={data} />
55-
</ModalContent>
56-
<ModalFooter>
57-
<Button onClick={modalProps.onClose}>Close</Button>
58-
</ModalFooter>
59-
</ModalRoot>
44+
<Modal
45+
{...modalProps}
46+
size="md"
47+
title={title}
48+
actions={[{ text: "Close", variant: "secondary", onClick: modalProps.onClose }]}
49+
>
50+
<CordCatModal data={data} />
51+
</Modal>
6052
));
6153
}

analyzers/CordCat/native.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function evictOldest() {
1818
}
1919
}
2020

21-
export async function queryCordCat(_: IpcMainInvokeEvent, userId: string, apiKey?: string): Promise<{ status: number; data: any; }> {
21+
export async function queryCordCat(_: IpcMainInvokeEvent, userId: string, apiKey: string): Promise<{ status: number; data: any; }> {
2222
const cached = resultCache.get(userId);
2323
if (cached) {
2424
// move to end
@@ -28,12 +28,11 @@ export async function queryCordCat(_: IpcMainInvokeEvent, userId: string, apiKey
2828
}
2929

3030
try {
31-
3231
const res = await fetch(`https://api.cord.cat/api/v2/query/${userId}`, {
3332
headers: {
3433
"accept": "application/json",
3534
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
36-
"authorization": apiKey ? `Bearer ${apiKey}` : "",
35+
"authorization": `Bearer ${apiKey}`,
3736
}
3837
});
3938

0 commit comments

Comments
 (0)