Track more reactions, redo scoreboard page, rename lmstudio to openai
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
19
discord/migrations/003-add-reaction-columns.sql
Normal file
19
discord/migrations/003-add-reaction-columns.sql
Normal 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;
|
||||
113
discord/package-lock.json
generated
113
discord/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
827
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
20
server.ts
20
server.ts
@@ -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 () => {
|
||||
|
||||
665
views/index.pug
665
views/index.pug
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user