generated from nhcarrigan/template
This commit is contained in:
+247
-17
@@ -30,13 +30,14 @@ interface Guild {
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
admin: Array<string>;
|
||||
community: Array<string>;
|
||||
moderating: Array<string>;
|
||||
owned: Array<string>;
|
||||
partnered: Array<string>;
|
||||
total: number;
|
||||
verified: Array<string>;
|
||||
admin: Array<string>;
|
||||
community: Array<string>;
|
||||
discoverable: Array<string>;
|
||||
moderating: Array<string>;
|
||||
owned: Array<string>;
|
||||
partnered: Array<string>;
|
||||
total: number;
|
||||
verified: Array<string>;
|
||||
}
|
||||
|
||||
// Permission Flags (BigInt)
|
||||
@@ -69,25 +70,249 @@ const printReport = (stats: Stats): void => {
|
||||
printList("Partnered Servers", stats.partnered, "🤝");
|
||||
printList("Verified Servers", stats.verified, "✅");
|
||||
printList("Community/Public Servers", stats.community, "🌍");
|
||||
printList("Discovery Enabled Servers", stats.discoverable, "🔍");
|
||||
|
||||
console.log(`======================================\n`);
|
||||
};
|
||||
|
||||
const escapeHtml = (value: string): string => {
|
||||
return value.
|
||||
replaceAll("&", "&").
|
||||
replaceAll("<", "<").
|
||||
replaceAll(">", ">").
|
||||
replaceAll("\"", """).
|
||||
replaceAll("'", "'");
|
||||
};
|
||||
|
||||
const buildDashboardHtml = (stats: Stats): string => {
|
||||
const tiles: Array<{ items: Array<string>; label: string; value: number }> = [
|
||||
{ items: stats.owned, label: "You Own", value: stats.owned.length },
|
||||
{ items: stats.admin, label: "You Admin", value: stats.admin.length },
|
||||
{
|
||||
items: stats.moderating,
|
||||
label: "You Moderate",
|
||||
value: stats.moderating.length,
|
||||
},
|
||||
{
|
||||
items: stats.community,
|
||||
label: "Community Enabled",
|
||||
value: stats.community.length,
|
||||
},
|
||||
{
|
||||
items: stats.discoverable,
|
||||
label: "Discovery Enabled",
|
||||
value: stats.discoverable.length,
|
||||
},
|
||||
{
|
||||
items: stats.partnered,
|
||||
label: "Partnered",
|
||||
value: stats.partnered.length,
|
||||
},
|
||||
{ items: stats.verified, label: "Verified", value: stats.verified.length },
|
||||
];
|
||||
|
||||
const cards = tiles.map((tile) => {
|
||||
const listItems
|
||||
= tile.items.length === 0
|
||||
? "<li class=\"empty\">No servers in this category.</li>"
|
||||
: tile.items.map((name) => {
|
||||
return `<li>${escapeHtml(name)}</li>`;
|
||||
}).join("");
|
||||
return `
|
||||
<div class="card">
|
||||
<p class="label">${escapeHtml(tile.label)}</p>
|
||||
<p class="count">${tile.value.toString()}</p>
|
||||
<ul class="server-list">
|
||||
${listItems}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Discord Server Counter</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root {
|
||||
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: #0f0f16;
|
||||
color: #111;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, #ff4ecd, #7873f5, #4ac7fa, #43e97b);
|
||||
}
|
||||
.wrapper {
|
||||
width: min(1100px, 100%);
|
||||
border-radius: 1.5rem;
|
||||
padding: 0.25rem;
|
||||
background: linear-gradient(120deg, #ff4ecd, #b388ff, #4ac7fa, #43e97b);
|
||||
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 1.25rem;
|
||||
padding: 2rem 2.5rem 3rem;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.75rem, 3vw, 2.5rem);
|
||||
}
|
||||
.subtitle {
|
||||
color: #6b7280;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.total {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.total span {
|
||||
display: block;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.total strong {
|
||||
font-size: clamp(2.5rem, 5vw, 3.5rem);
|
||||
color: #4c1d95;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
.card {
|
||||
background: #f9fafb;
|
||||
border-radius: 1rem;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.label {
|
||||
color: #7c3aed;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.count {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
color: #111827;
|
||||
}
|
||||
.server-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 180px;
|
||||
}
|
||||
.server-list li {
|
||||
padding: 0.35rem 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
font-size: 0.95rem;
|
||||
color: #374151;
|
||||
}
|
||||
.server-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.server-list .empty {
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
border-bottom: none;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
.content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<h1>Server Counter</h1>
|
||||
<p class="subtitle">View counts of servers you are in along with quick insights.</p>
|
||||
<div class="total">
|
||||
<span>You are in</span>
|
||||
<strong>${stats.total.toString()} servers</strong>
|
||||
</div>
|
||||
<div class="grid">
|
||||
${cards}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
|
||||
const serveStatsDashboard = async(stats: Stats): Promise<void> => {
|
||||
const html = buildDashboardHtml(stats);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const server = http.createServer((request, response) => {
|
||||
if (request.method !== "GET") {
|
||||
response.statusCode = 405;
|
||||
response.end("Method Not Allowed");
|
||||
return;
|
||||
}
|
||||
response.statusCode = 200;
|
||||
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
response.end(html);
|
||||
server.close();
|
||||
resolve();
|
||||
});
|
||||
|
||||
server.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (address !== null && typeof address === "object") {
|
||||
const url = `http://${address.address}:${address.port.toString()}/`;
|
||||
console.log(`Opening dashboard at ${url}`);
|
||||
void open(url, { wait: false }).catch(() => {
|
||||
console.log(
|
||||
`Failed to open browser automatically. Please visit ${url} manually.`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const checkForPermission = (perms: bigint, permission: bigint): boolean => {
|
||||
// eslint-disable-next-line no-bitwise -- Since Discord uses bit flags...
|
||||
return (perms & permission) === permission;
|
||||
};
|
||||
|
||||
const analyzeGuilds = (guilds: Array<Guild>): void => {
|
||||
const analyzeGuilds = (guilds: Array<Guild>): Stats => {
|
||||
// Arrays to store names instead of just counts
|
||||
const stats: Stats = {
|
||||
admin: [],
|
||||
community: [],
|
||||
moderating: [],
|
||||
owned: [],
|
||||
partnered: [],
|
||||
total: guilds.length,
|
||||
verified: [],
|
||||
admin: [],
|
||||
community: [],
|
||||
discoverable: [],
|
||||
moderating: [],
|
||||
owned: [],
|
||||
partnered: [],
|
||||
total: guilds.length,
|
||||
verified: [],
|
||||
};
|
||||
|
||||
for (const guild of guilds) {
|
||||
@@ -129,12 +354,16 @@ const analyzeGuilds = (guilds: Array<Guild>): void => {
|
||||
* "COMMUNITY" feature enables public facing screens (Welcome Screen, Rules, etc)
|
||||
* "DISCOVERABLE" means it appears in Server Discovery
|
||||
*/
|
||||
if (features.includes("COMMUNITY") || features.includes("DISCOVERABLE")) {
|
||||
if (features.includes("COMMUNITY")) {
|
||||
stats.community.push(name ?? id);
|
||||
}
|
||||
if (features.includes("DISCOVERABLE")) {
|
||||
stats.discoverable.push(name ?? id);
|
||||
}
|
||||
}
|
||||
|
||||
printReport(stats);
|
||||
return stats;
|
||||
};
|
||||
|
||||
const defaultRedirectUri = "http://127.0.0.1:8721/callback";
|
||||
@@ -434,7 +663,8 @@ async function getGuilds(): Promise<void> {
|
||||
}
|
||||
|
||||
const guilds: Array<Guild> = await response.json();
|
||||
analyzeGuilds(guilds);
|
||||
const stats = analyzeGuilds(guilds);
|
||||
await serveStatsDashboard(stats);
|
||||
}
|
||||
|
||||
await getGuilds();
|
||||
|
||||
Reference in New Issue
Block a user