@@ -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
2891interface 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
41109const TEXT_NORMAL = "var(--text-normal, var(--header-primary, #dcddde))" ;
42110const 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+
44124function 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+
52140function 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+
99217function 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; }) {
107225export 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}
0 commit comments