Track more reactions, redo scoreboard page, rename lmstudio to openai

This commit is contained in:
james
2026-02-27 02:41:56 -08:00
parent 1106245fd5
commit f749f698f1
13 changed files with 1210 additions and 509 deletions

View File

@@ -1,5 +1,5 @@
TOKEN="sadkfl;jasdkl;fj"
REACTIONS="💀,💯,😭"
REACTIONS="💀,💯,😭,<:based:1178222955830968370>,<:this:1171632205924151387>"
CLIENT="123456789012345678"
GUILD="123456789012345678"
ADMIN="123456789012345678"
@@ -7,7 +7,7 @@ ADMIN="123456789012345678"
HF_TOKEN=""
LLM_HOST="http://127.0.0.1:8000"
LLM_TOKEN="dfsl;kjsdl;kfja"
LMSTUDIO_HOST="ws://localhost:1234"
OPENAI_HOST="http://localhost:1234/v1"
REPLY_CHANCE=0.2
ENABLE_MOTD=1

View File

@@ -60,8 +60,12 @@ client.commands = new Collection();
client.once(Events.ClientReady, async () => {
logInfo('[bot] Ready.');
for (let i = 0; i < reactionEmojis.length; ++i)
logInfo(`[bot] util: reaction_${i + 1} = ${reactionEmojis[i]}`);
for (let i = 0; i < reactionEmojis.length; ++i) {
// Extract emoji name from config (handle both unicode and custom emoji formats)
const emojiConfig = reactionEmojis[i];
const emojiName = emojiConfig.includes(':') ? emojiConfig.split(':')[1] : emojiConfig;
logInfo(`[bot] util: reaction_${i + 1} = ${emojiName}`);
}
});

View File

@@ -5,16 +5,16 @@ import {
import 'dotenv/config';
import { MikuAIProvider } from '../../provider/mikuai';
import { HuggingfaceProvider } from '../../provider/huggingface';
import { LMStudioProvider } from '../../provider/lmstudio';
import { OpenAIProvider } from '../../provider/openai';
import { OllamaProvider } from '../../provider/ollama';
const PROVIDERS = {
mikuai: new MikuAIProvider(),
huggingface: new HuggingfaceProvider(),
lmstudio: new LMStudioProvider(),
openai: new OpenAIProvider(),
ollama: new OllamaProvider()
};
let provider = PROVIDERS.lmstudio;
let provider = PROVIDERS.openai;
async function providerCommand(interaction: ChatInputCommandInteraction) {
if (interaction.user.id !== process.env.ADMIN) {

View File

@@ -0,0 +1,19 @@
--------------------------------------------------------------------------------
-- Up
--------------------------------------------------------------------------------
ALTER TABLE messages ADD COLUMN reaction_4_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE messages ADD COLUMN reaction_5_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN reaction_4_total INTEGER NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN reaction_5_total INTEGER NOT NULL DEFAULT 0;
--------------------------------------------------------------------------------
-- Down
--------------------------------------------------------------------------------
ALTER TABLE messages DROP COLUMN reaction_4_count;
ALTER TABLE messages DROP COLUMN reaction_5_count;
ALTER TABLE users DROP COLUMN reaction_4_total;
ALTER TABLE users DROP COLUMN reaction_5_total;

View File

@@ -9,8 +9,6 @@
"version": "1.0.0",
"dependencies": {
"@huggingface/inference": "^3.1.3",
"@lmstudio/immer-with-plugins": "^10.1.1",
"@lmstudio/sdk": "^1.5.0",
"discord.js": "^14.13.0",
"dotenv": "^16.3.1",
"emoji-unicode-map": "^1.1.11",
@@ -186,56 +184,6 @@
"node": ">=12"
}
},
"node_modules/@lmstudio/immer-with-plugins": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@lmstudio/immer-with-plugins/-/immer-with-plugins-10.1.1.tgz",
"integrity": "sha512-iV1biwfOQNYWXZEd/YJuUrdPWKvbL9WDvMSAcDR3+vEzgk3drdRWdcefoMGpggn0bJpxVyy7lCVwJJzI65Puiw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/@lmstudio/lms-isomorphic": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/@lmstudio/lms-isomorphic/-/lms-isomorphic-0.4.6.tgz",
"integrity": "sha512-v0LIjXKnDe3Ff3XZO5eQjlVxTjleUHXaom14MV7QU9bvwaoo3l5p71+xJ3mmSaqZq370CQ6pTKCn1Bb7Jf+VwQ==",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
}
},
"node_modules/@lmstudio/sdk": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@lmstudio/sdk/-/sdk-1.5.0.tgz",
"integrity": "sha512-fdY12x4hb14PEjYijh7YeCqT1ZDY5Ok6VR4l4+E/dI+F6NW8oB+P83Sxed5vqE4XgTzbgyPuSR2ZbMNxxF+6jA==",
"license": "Apache-2.0",
"dependencies": {
"@lmstudio/lms-isomorphic": "^0.4.6",
"chalk": "^4.1.2",
"jsonschema": "^1.5.0",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.5"
}
},
"node_modules/@lmstudio/sdk/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@lmstudio/sdk/node_modules/zod-to-json-schema": {
"version": "3.25.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.25 || ^4"
}
},
"node_modules/@npmcli/fs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
@@ -587,37 +535,6 @@
"node": ">=8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@@ -1103,15 +1020,6 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"optional": true
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -1355,15 +1263,6 @@
}
}
},
"node_modules/jsonschema": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz",
"integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -2555,18 +2454,6 @@
"node": ">=0.10.0"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",

View File

@@ -3,8 +3,6 @@
"version": "1.0.0",
"dependencies": {
"@huggingface/inference": "^3.1.3",
"@lmstudio/immer-with-plugins": "^10.1.1",
"@lmstudio/sdk": "^1.5.0",
"discord.js": "^14.13.0",
"dotenv": "^16.3.1",
"emoji-unicode-map": "^1.1.11",

View File

@@ -14,7 +14,7 @@ The conversation is as follows. The last line is the message you have to complet
`;
export class LMStudioProvider implements LLMProvider {
export class OpenAIProvider implements LLMProvider {
private client: OpenAI;
private model: string;
@@ -23,14 +23,14 @@ export class LMStudioProvider implements LLMProvider {
throw new TypeError("LLM token was not passed in, and environment variable LLM_TOKEN was unset!");
}
this.client = new OpenAI({
baseURL: process.env.LMSTUDIO_HOST,
baseURL: process.env.OPENAI_HOST,
apiKey: token,
});
this.model = model;
}
name() {
return 'LM Studio';
return 'OpenAI';
}
setModel(model: string) {
@@ -63,10 +63,9 @@ export class LMStudioProvider implements LLMProvider {
});
const messageHistoryTxt = messageList.map(msg => JSON.stringify(msg)).join('\n') + '\n' + templateMsgTxt;
logInfo(`[lmstudio] Requesting response for message history: ${messageHistoryTxt}`);
logInfo(`[openai] Requesting response for message history: ${messageHistoryTxt}`);
try {
// Get the currently loaded model from LM Studio
const response = await this.client.chat.completions.create({
model: this.model,
messages: [
@@ -82,15 +81,15 @@ export class LMStudioProvider implements LLMProvider {
if (content.lastIndexOf('</think>') > -1) {
content = content.slice(content.lastIndexOf('</think>') + 8);
}
logInfo(`[lmstudio] API response: ${content}`);
logInfo(`[openai] API response: ${content}`);
if (!content) {
throw new TypeError("LM Studio API returned no message.");
throw new TypeError("OpenAI API returned no message.");
}
return content;
} catch (err) {
logError(`[lmstudio] API Error: ` + err);
logError(`[openai] API Error: ` + err);
throw err;
}
}

View File

@@ -92,13 +92,37 @@ async function refreshUserReactionTotalCount(user: User, emoji_idx: number)
if (!existsSync(userAvatarPath(user))) {
await downloadUserAvatar(user);
}
logInfo(`[bot] Refreshed ${user.id}'s ${reactionEmojis[emoji_idx - 1]} count.`);
// Extract emoji name from config (handle both unicode and custom emoji formats)
const emojiConfig = reactionEmojis[emoji_idx - 1];
const emojiName = emojiConfig.includes(':') ? emojiConfig.split(':')[1] : emojiConfig;
logInfo(`[bot] Refreshed ${user.id}'s ${emojiName} count.`);
}
async function recordReaction(reaction: MessageReaction)
{
const emojiIdx = reactionEmojis.indexOf(reaction.emoji.name) + 1;
// Match emoji by name (unicode) or by ID (custom emoji)
let emojiIdx = 0;
const emojiName = reaction.emoji.name;
const emojiId = reaction.emoji.id;
if (emojiId) {
// Custom emoji - match by ID
emojiIdx = reactionEmojis.findIndex(e => e.includes(`:${emojiId}`)) + 1;
if (emojiIdx > 0) {
logInfo(`[bot] Custom emoji detected: ${emojiName} (ID: ${emojiId}), idx: ${emojiIdx}`);
}
}
if (emojiIdx === 0) {
// Unicode emoji - match by name
emojiIdx = reactionEmojis.indexOf(emojiName) + 1;
if (emojiIdx > 0) {
logInfo(`[bot] Unicode emoji detected: ${emojiName}, idx: ${emojiIdx}`);
}
}
if (emojiIdx === 0) {
logWarn(`[bot] Unknown emoji: ${emojiName} (ID: ${emojiId || 'none'})`);
return;
}
try {

View File

@@ -11,7 +11,9 @@ interface ScoreboardMessageRow {
content: string,
reaction_1_count: number,
reaction_2_count: number,
reaction_3_count: number
reaction_3_count: number,
reaction_4_count: number,
reaction_5_count: number
}
interface ScoreboardUserRow {
@@ -19,7 +21,9 @@ interface ScoreboardUserRow {
username: string,
reaction_1_total: number,
reaction_2_total: number,
reaction_3_total: number
reaction_3_total: number,
reaction_4_total: number,
reaction_5_total: number
}
export { ScoreboardMessageRow, ScoreboardUserRow };

827
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,8 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"pug": "^3.0.2",
"sqlite": "^5.0.1",
"sqlite3": "^5.1.6"
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"@types/express": "^4.17.18",

View File

@@ -24,16 +24,20 @@ async function openDb() {
}
app.get('/', async (req, res) => {
const msg1 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_1_count DESC LIMIT 10');
const msg2 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_2_count DESC LIMIT 10');
const msg3 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_3_count DESC LIMIT 10');
const bestMsg = await db.all<[ScoreboardMessageRow]>('SELECT *, SUM(reaction_1_count)+SUM(reaction_2_count)+SUM(reaction_3_count) AS all_reacts FROM messages GROUP BY id ORDER BY all_reacts DESC LIMIT 5');
const msg1 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_1_count DESC LIMIT 5');
const msg2 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_2_count DESC LIMIT 5');
const msg3 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_3_count DESC LIMIT 5');
const msg4 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_4_count DESC LIMIT 5');
const msg5 = await db.all<[ScoreboardMessageRow]>('SELECT * FROM messages ORDER BY reaction_5_count DESC LIMIT 5');
const bestMsg = await db.all<[ScoreboardMessageRow]>('SELECT *, SUM(reaction_1_count)+SUM(reaction_2_count)+SUM(reaction_3_count)+SUM(reaction_4_count)+SUM(reaction_5_count) AS all_reacts FROM messages GROUP BY id ORDER BY all_reacts DESC LIMIT 5');
const funniest = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_1_total DESC');
const realest = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_2_total DESC');
const cunniest = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_3_total DESC');
const funniest = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_1_total DESC LIMIT 15');
const realest = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_2_total DESC LIMIT 15');
const cunniest = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_3_total DESC LIMIT 15');
const based = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_4_total DESC LIMIT 15');
const agreeable = await db.all<[ScoreboardUserRow]>('SELECT * FROM users ORDER BY reaction_5_total DESC LIMIT 15');
res.render('index', { funniest, realest, cunniest, msg1, msg2, msg3, bestMsg });
res.render('index', { funniest, realest, cunniest, based, agreeable, msg1, msg2, msg3, msg4, msg5, bestMsg });
});
app.listen(port, async () => {

View File

@@ -2,138 +2,541 @@ doctype html
html
head
title FemScoreboard
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel="preconnect" href="https://fonts.googleapis.com")
link(rel="preconnect" href="https://fonts.gstatic.com" crossorigin)
link(href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet")
style.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #e4e4e4;
padding: 40px 20px;
}
.container {
max-width: 1800px;
margin: 0 auto;
}
h1 {
text-align: center;
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 40px;
background: linear-gradient(90deg, #e94560, #ff6b6b, #feca57);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.users-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin-bottom: 60px;
}
@media (max-width: 1400px) {
.users-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.users-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.users-grid {
grid-template-columns: 1fr;
}
}
.card {
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 16px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
}
.user-table {
width: 100%;
border-collapse: collapse;
}
.user-table th {
text-align: left;
padding: 12px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #888;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.user-table td {
padding: 12px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.user-table tr:last-child td {
border-bottom: none;
}
.user-table tr:hover td {
background: rgba(255, 255, 255, 0.02);
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.1);
}
.username {
font-weight: 500;
color: #fff;
font-size: 0.9rem;
}
.score {
font-weight: 700;
font-size: 1.1rem;
color: #e94560;
}
.rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
font-size: 0.75rem;
font-weight: 600;
}
.rank-1 { background: linear-gradient(135deg, #ffd700, #ffed4e); color: #1a1a2e; }
.rank-2 { background: linear-gradient(135deg, #c0c0c0, #e8e8e8); color: #1a1a2e; }
.rank-3 { background: linear-gradient(135deg, #cd7f32, #e09856); color: #1a1a2e; }
.messages-section {
margin-bottom: 60px;
}
.messages-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
}
@media (max-width: 1400px) {
.messages-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.messages-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.messages-grid {
grid-template-columns: 1fr;
}
}
.message-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.message-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.message-item {
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
padding: 16px;
transition: background 0.2s ease;
}
.message-item:hover {
background: rgba(255, 255, 255, 0.06);
}
.message-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.1);
}
.message-content {
color: #ccc;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 12px;
word-break: break-word;
}
.message-content i {
color: #888;
}
.message-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.message-link {
color: #e94560;
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
transition: color 0.2s ease;
}
.message-link:hover {
color: #ff6b6b;
text-decoration: underline;
}
.message-reaction {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
font-size: 0.9rem;
}
.best-section {
margin-top: 60px;
}
.best-card {
background: linear-gradient(135deg, rgba(233, 69, 96, 0.1), rgba(255, 107, 107, 0.1));
border: 1px solid rgba(233, 69, 96, 0.3);
}
.best-reactions {
display: flex;
gap: 12px;
font-size: 0.85rem;
}
.best-reaction {
display: flex;
align-items: center;
gap: 4px;
background: rgba(255, 255, 255, 0.1);
padding: 4px 10px;
border-radius: 20px;
}
@media (max-width: 768px) {
h1 {
font-size: 1.75rem;
}
.users-grid,
.messages-grid {
grid-template-columns: 1fr;
}
body {
padding: 20px 12px;
}
}
</style>
body
table
tbody
tr
td(style="width:33.3%")
h1(style="text-align:center") Funniest MFs 💀
table(style="margin:0 auto")
thead
.container
h1 FemScoreboard
h2.section-title
span 🏆 Top Users
.users-grid
.card
h3.section-title 💀 Funniest MFs
table.user-table
thead
tr
th Rank
th User
th Score
tbody
each row, idx in funniest
tr
th= "Name"
th= "Avatar"
th= "Score"
tbody
each row in funniest
tr
td= row.username
td
img(src="/avatars/" + row.id + ".webp", height="64")
td= row.reaction_1_total
td(style="width:33.3%")
h1(style="text-align:center") Realest MFs 💯
table(style="margin:0 auto")
thead
td
span.rank(class="rank-" + (idx + 1))= idx + 1
td
.user-info
img.user-avatar(src="/avatars/" + row.id + ".webp", alt=row.username)
span.username= row.username
td
span.score= row.reaction_1_total
.card
h3.section-title 💯 Realest MFs
table.user-table
thead
tr
th Rank
th User
th Score
tbody
each row, idx in realest
tr
th= "Name"
th= "Avatar"
th= "Score"
tbody
each row in realest
tr
td= row.username
td
img(src="/avatars/" + row.id + ".webp", height="64")
td= row.reaction_2_total
td(style="width:33.3%")
h1(style="text-align:center") Cunniest MFs 😭
table(style="margin:0 auto")
thead
td
span.rank(class="rank-" + (idx + 1))= idx + 1
td
.user-info
img.user-avatar(src="/avatars/" + row.id + ".webp", alt=row.username)
span.username= row.username
td
span.score= row.reaction_2_total
.card
h3.section-title 😭 Cunniest MFs
table.user-table
thead
tr
th Rank
th User
th Score
tbody
each row, idx in cunniest
tr
th= "Name"
th= "Avatar"
th= "Score"
tbody
each row in cunniest
tr
td
span= row.username
td
img(src="/avatars/" + row.id + ".webp", height="64")
td
span= row.reaction_3_total
tr
td(style="width:33.3%")
h1(style="text-align:center") Funniest Messages 💀
table(style="margin:0 auto")
tbody
each row in msg1
tr
td
img(src="/avatars/" + row.author + ".webp", height="48")
td
if row.content
span= row.content
else
span
i (media)
tr
td
a(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) Link
td
span= "💀 " + row.reaction_1_count
td(style="width:33.3%")
h1(style="text-align:center") Realest Messages 💯
table(style="margin:0 auto")
tbody
each row in msg2
tr
td
img(src="/avatars/" + row.author + ".webp", height="48")
td
if row.content
span= row.content
else
span
i (media)
tr
td
a(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) Link
td
span= "💯 " + row.reaction_2_count
td(style="width:33.3%")
h1(style="text-align:center") Cunniest Messages 😭
table(style="margin:0 auto")
tbody
each row in msg3
tr
td
img(src="/avatars/" + row.author + ".webp", height="48")
td
if row.content
span= row.content
else
span
i (media)
tr
td
a(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) Link
td
span= "😭 " + row.reaction_3_count
tr
td
td
h1(style="text-align:center") Best Messages 💀💯😭
table(style="margin:0 auto")
tbody
each row in bestMsg
tr
td
img(src="/avatars/" + row.author + ".webp", height="48")
td
if row.content
span= row.content
else
span
i (media)
tr
td
a(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) Link
td
if row.reaction_1_count
span= " 💀 " + row.reaction_1_count
if row.reaction_2_count
span= " 💯 " + row.reaction_2_count
if row.reaction_3_count
span= " 😭 " + row.reaction_3_count
td
span.rank(class="rank-" + (idx + 1))= idx + 1
td
.user-info
img.user-avatar(src="/avatars/" + row.id + ".webp", alt=row.username)
span.username= row.username
td
span.score= row.reaction_3_total
.card
h3.section-title 🅱ased MFs
table.user-table
thead
tr
th Rank
th User
th Score
tbody
each row, idx in based
tr
td
span.rank(class="rank-" + (idx + 1))= idx + 1
td
.user-info
img.user-avatar(src="/avatars/" + row.id + ".webp", alt=row.username)
span.username= row.username
td
span.score= row.reaction_4_total
.card
h3.section-title ⬆️ Agreeable MFs
table.user-table
thead
tr
th Rank
th User
th Score
tbody
each row, idx in agreeable
tr
td
span.rank(class="rank-" + (idx + 1))= idx + 1
td
.user-info
img.user-avatar(src="/avatars/" + row.id + ".webp", alt=row.username)
span.username= row.username
td
span.score= row.reaction_5_total
.messages-section
h2.section-title
span 💬 Top Messages
.messages-grid
.message-card
h3.section-title 💀 Funniest
.message-list
each row in msg1
.message-item
.message-header
img.message-avatar(src="/avatars/" + row.author + ".webp", alt="Avatar")
.message-content
if row.content
= row.content
else
i (media)
.message-footer
a.message-link(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) View Message
.message-reaction
span 💀
span= row.reaction_1_count
.message-card
h3.section-title 💯 Realest
.message-list
each row in msg2
.message-item
.message-header
img.message-avatar(src="/avatars/" + row.author + ".webp", alt="Avatar")
.message-content
if row.content
= row.content
else
i (media)
.message-footer
a.message-link(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) View Message
.message-reaction
span 💯
span= row.reaction_2_count
.message-card
h3.section-title 😭 Cunniest
.message-list
each row in msg3
.message-item
.message-header
img.message-avatar(src="/avatars/" + row.author + ".webp", alt="Avatar")
.message-content
if row.content
= row.content
else
i (media)
.message-footer
a.message-link(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) View Message
.message-reaction
span 😭
span= row.reaction_3_count
.message-card
h3.section-title 🔥 Based
.message-list
each row in msg4
.message-item
.message-header
img.message-avatar(src="/avatars/" + row.author + ".webp", alt="Avatar")
.message-content
if row.content
= row.content
else
i (media)
.message-footer
a.message-link(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) View Message
.message-reaction
span 🔥
span= row.reaction_4_count
.message-card
h3.section-title ⬆️ This
.message-list
each row in msg5
.message-item
.message-header
img.message-avatar(src="/avatars/" + row.author + ".webp", alt="Avatar")
.message-content
if row.content
= row.content
else
i (media)
.message-footer
a.message-link(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) View Message
.message-reaction
span ⬆️
span= row.reaction_5_count
.best-section
h2.section-title
span ⭐ Best Overall Messages
.message-card.best-card
.message-list
each row in bestMsg
.message-item
.message-header
img.message-avatar(src="/avatars/" + row.author + ".webp", alt="Avatar")
.message-content
if row.content
= row.content
else
i (media)
.message-footer
a.message-link(href="https://discord.com/channels/" + row.guild + "/" + row.channel + "/" + row.id) View Message
.best-reactions
if row.reaction_1_count
.best-reaction
span 💀
span= row.reaction_1_count
if row.reaction_2_count
.best-reaction
span 💯
span= row.reaction_2_count
if row.reaction_3_count
.best-reaction
span 😭
span= row.reaction_3_count
if row.reaction_4_count
.best-reaction
span 🔥
span= row.reaction_4_count
if row.reaction_5_count
.best-reaction
span 👁️
span= row.reaction_5_count