forked from syntaxbullet/AuroraBot-discord
Compare commits
5 Commits
049725c384
...
209340c06e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
209340c06e | ||
|
|
9250057574 | ||
|
|
90a1861416 | ||
|
|
bcfd254071 | ||
|
|
4d553ddc91 |
25
README.md
25
README.md
@@ -39,27 +39,4 @@ This project uses a modular architecture:
|
||||
```bash
|
||||
bun run db:push # Apply schema changes
|
||||
bun run generate # Generate migrations
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Manual Command Registration
|
||||
Since command registration is decoupled from startup, you must run this manually when you add or change commands.
|
||||
|
||||
**Option 1: Using Docker (Recommended)**
|
||||
Uses the credentials configured in `docker-compose.yml`.
|
||||
```bash
|
||||
docker compose run --rm app bun run deploy
|
||||
```
|
||||
|
||||
**Option 2: Running Locally**
|
||||
Requires valid `.env` file with `DISCORD_CLIENT_ID`.
|
||||
```bash
|
||||
bun run deploy
|
||||
```
|
||||
|
||||
## Development Features
|
||||
|
||||
- **Type Safety**: Full TypeScript support.
|
||||
- **Env Validation**: `zod` ensures all required env vars are present.
|
||||
- **Hot Reloading**: `bun --watch` for fast development.
|
||||
```
|
||||
23
bun.lock
23
bun.lock
@@ -5,6 +5,7 @@
|
||||
"": {
|
||||
"name": "app",
|
||||
"dependencies": {
|
||||
"@napi-rs/canvas": "^0.1.84",
|
||||
"discord.js": "^14.25.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
@@ -91,6 +92,28 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@napi-rs/canvas": ["@napi-rs/canvas@0.1.84", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.84", "@napi-rs/canvas-darwin-arm64": "0.1.84", "@napi-rs/canvas-darwin-x64": "0.1.84", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.84", "@napi-rs/canvas-linux-arm64-gnu": "0.1.84", "@napi-rs/canvas-linux-arm64-musl": "0.1.84", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.84", "@napi-rs/canvas-linux-x64-gnu": "0.1.84", "@napi-rs/canvas-linux-x64-musl": "0.1.84", "@napi-rs/canvas-win32-x64-msvc": "0.1.84" } }, "sha512-88FTNFs4uuiFKP0tUrPsEXhpe9dg7za9ILZJE08pGdUveMIDeana1zwfVkqRHJDPJFAmGY3dXmJ99dzsy57YnA=="],
|
||||
|
||||
"@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.84", "", { "os": "android", "cpu": "arm64" }, "sha512-pdvuqvj3qtwVryqgpAGornJLV6Ezpk39V6wT4JCnRVGy8I3Tk1au8qOalFGrx/r0Ig87hWslysPpHBxVpBMIww=="],
|
||||
|
||||
"@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.84", "", { "os": "darwin", "cpu": "arm64" }, "sha512-A8IND3Hnv0R6abc6qCcCaOCujTLMmGxtucMTZ5vbQUrEN/scxi378MyTLtyWg+MRr6bwQJ6v/orqMS9datIcww=="],
|
||||
|
||||
"@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.84", "", { "os": "darwin", "cpu": "x64" }, "sha512-AUW45lJhYWwnA74LaNeqhvqYKK/2hNnBBBl03KRdqeCD4tKneUSrxUqIv8d22CBweOvrAASyKN3W87WO2zEr/A=="],
|
||||
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.84", "", { "os": "linux", "cpu": "arm" }, "sha512-8zs5ZqOrdgs4FioTxSBrkl/wHZB56bJNBqaIsfPL4ZkEQCinOkrFF7xIcXiHiKp93J3wUtbIzeVrhTIaWwqk+A=="],
|
||||
|
||||
"@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.84", "", { "os": "linux", "cpu": "arm64" }, "sha512-i204vtowOglJUpbAFWU5mqsJgH0lVpNk/Ml4mQtB4Lndd86oF+Otr6Mr5KQnZHqYGhlSIKiU2SYnUbhO28zGQA=="],
|
||||
|
||||
"@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.84", "", { "os": "linux", "cpu": "arm64" }, "sha512-VyZq0EEw+OILnWk7G3ZgLLPaz1ERaPP++jLjeyLMbFOF+Tr4zHzWKiKDsEV/cT7btLPZbVoR3VX+T9/QubnURQ=="],
|
||||
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.84", "", { "os": "linux", "cpu": "none" }, "sha512-PSMTh8DiThvLRsbtc/a065I/ceZk17EXAATv9uNvHgkgo7wdEfTh2C3aveNkBMGByVO3tvnvD5v/YFtZL07cIg=="],
|
||||
|
||||
"@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.84", "", { "os": "linux", "cpu": "x64" }, "sha512-N1GY3noO1oqgEo3rYQIwY44kfM11vA0lDbN0orTOHfCSUZTUyiYCY0nZ197QMahZBm1aR/vYgsWpV74MMMDuNA=="],
|
||||
|
||||
"@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.84", "", { "os": "linux", "cpu": "x64" }, "sha512-vUZmua6ADqTWyHyei81aXIt9wp0yjeNwTH0KdhdeoBb6azHmFR8uKTukZMXfLCC3bnsW0t4lW7K78KNMknmtjg=="],
|
||||
|
||||
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.84", "", { "os": "win32", "cpu": "x64" }, "sha512-YSs8ncurc1xzegUMNnQUTYrdrAuaXdPMOa+iYYyAxydOtg0ppV386hyYMsy00Yip1NlTgLCseRG4sHSnjQx6og=="],
|
||||
|
||||
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
|
||||
|
||||
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"db:studio": "drizzle-kit studio --host 0.0.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@napi-rs/canvas": "^0.1.84",
|
||||
"discord.js": "^14.25.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
|
||||
BIN
src/assets/fonts/IBMPlexMono-Bold.ttf
Normal file
BIN
src/assets/fonts/IBMPlexMono-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/IBMPlexSansCondensed-SemiBold.ttf
Normal file
BIN
src/assets/fonts/IBMPlexSansCondensed-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/assets/student-id-class-badge.png
Normal file
BIN
src/assets/student-id-class-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/student-id-template.png
Normal file
BIN
src/assets/student-id-template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -1,6 +1,8 @@
|
||||
import { createCommand } from "@/lib/utils";
|
||||
import { SlashCommandBuilder, EmbedBuilder } from "discord.js";
|
||||
import { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } from "discord.js";
|
||||
import { userService } from "@/modules/user/user.service";
|
||||
import { classService } from "@/modules/class/class.service";
|
||||
import { generateStudentIdCard } from "@/graphics/studentID";
|
||||
|
||||
export const profile = createCommand({
|
||||
data: new SlashCommandBuilder()
|
||||
@@ -16,24 +18,25 @@ export const profile = createCommand({
|
||||
|
||||
const targetUser = interaction.options.getUser("user") || interaction.user;
|
||||
const targetMember = await interaction.guild?.members.fetch(targetUser.id).catch(() => null);
|
||||
const targetUserClass = await userService.getUserClass(targetUser.id);
|
||||
const classBalance = await classService.getClassBalance(targetUserClass?.id || BigInt(0));
|
||||
|
||||
const user = await userService.getOrCreateUser(targetUser.id, targetUser.username);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`${targetUser.username}'s Profile`)
|
||||
.setThumbnail(targetUser.displayAvatarURL({ size: 512 }))
|
||||
.setColor(targetMember?.displayHexColor || "Random")
|
||||
.addFields(
|
||||
{ name: "Level", value: `${user!.level || 1}`, inline: true },
|
||||
{ name: "XP", value: `${user!.xp || 0}`, inline: true },
|
||||
{ name: "Balance", value: `${user!.balance || 0}`, inline: true },
|
||||
{ name: "Class", value: user!.class?.name || "None", inline: true },
|
||||
{ name: "Joined Discord", value: `<t:${Math.floor(targetUser.createdTimestamp / 1000)}:R>`, inline: true },
|
||||
{ name: "Joined Server", value: targetMember ? `<t:${Math.floor(targetMember.joinedTimestamp! / 1000)}:R>` : "Unknown", inline: true }
|
||||
)
|
||||
.setFooter({ text: `ID: ${targetUser.id}` })
|
||||
.setTimestamp();
|
||||
const cardBuffer = await generateStudentIdCard({
|
||||
username: targetUser.username,
|
||||
avatarUrl: targetUser.displayAvatarURL({ extension: 'png', size: 256 }),
|
||||
id: targetUser.id,
|
||||
level: user!.level || 1,
|
||||
xp: user!.xp || 0n,
|
||||
au: user!.balance || 0n,
|
||||
cu: classBalance || 0n,
|
||||
className: user!.class?.name || "D"
|
||||
});
|
||||
|
||||
const attachment = new AttachmentBuilder(cardBuffer, { name: 'student-id.png' });
|
||||
|
||||
await interaction.editReply({ files: [attachment] }); // Send mostly just the image as requested, or maybe both? User said "show that user's student id". Image is primary.
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
});
|
||||
|
||||
159
src/graphics/studentID.ts
Normal file
159
src/graphics/studentID.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { GlobalFonts, createCanvas, loadImage } from '@napi-rs/canvas';
|
||||
import { levelingService } from '@/modules/leveling/leveling.service';
|
||||
import path from 'path';
|
||||
|
||||
// Register Fonts
|
||||
const fontDir = path.join(process.cwd(), 'src', 'assets', 'fonts');
|
||||
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexSansCondensed-SemiBold.ttf'), 'IBMPlexSansCondensed-SemiBold');
|
||||
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexMono-Bold.ttf'), 'IBMPlexMono-Bold');
|
||||
|
||||
const textColor = '#FFE7C5';
|
||||
const secondaryTextColor = '#9C7D53';
|
||||
const accentColor = '#3F2923';
|
||||
|
||||
interface StudentCardData {
|
||||
username: string;
|
||||
avatarUrl: string;
|
||||
id: string;
|
||||
level: number;
|
||||
au: bigint; // Astral Units
|
||||
cu: bigint; // Constellation Units
|
||||
xp: bigint; // Experience
|
||||
className?: string | null;
|
||||
}
|
||||
|
||||
export async function generateStudentIdCard(data: StudentCardData): Promise<Buffer> {
|
||||
const templatePath = path.join(process.cwd(), 'src', 'assets', 'student-id-template.png');
|
||||
const template = await loadImage(templatePath);
|
||||
|
||||
const canvas = createCanvas(template.width, template.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Draw Template
|
||||
ctx.drawImage(template, 0, 0);
|
||||
|
||||
// Draw Avatar
|
||||
const avatarSize = 111;
|
||||
const avatarX = 18;
|
||||
const avatarY = 64;
|
||||
|
||||
try {
|
||||
const avatar = await loadImage(data.avatarUrl);
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
// Square avatar
|
||||
ctx.rect(avatarX, avatarY, avatarSize, avatarSize);
|
||||
ctx.clip();
|
||||
ctx.drawImage(avatar, avatarX, avatarY, avatarSize, avatarSize);
|
||||
ctx.restore();
|
||||
} catch (e) {
|
||||
console.error("Failed to load avatar", e);
|
||||
}
|
||||
|
||||
// Draw ID
|
||||
ctx.save();
|
||||
ctx.font = '12px IBMPlexMono-Bold';
|
||||
ctx.fillStyle = secondaryTextColor;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`ID: ${data.id}`, 302, 30);
|
||||
ctx.restore();
|
||||
|
||||
// Draw Username
|
||||
ctx.save();
|
||||
ctx.font = '32px IBMPlexSansCondensed-SemiBold';
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(data.username, 140, 107);
|
||||
ctx.restore();
|
||||
|
||||
// Draw Class
|
||||
try {
|
||||
const classBadge = await loadImage(path.join(process.cwd(), 'src', 'assets', 'student-id-class-badge.png'));
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.drawImage(classBadge, 100, 126, 58, 58);
|
||||
ctx.font = '32px IBMPlexMono-Bold';
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(data.className || 'Unknown', 120, 162);
|
||||
ctx.restore();
|
||||
} catch (e) {
|
||||
console.error("Failed to load class badge", e);
|
||||
}
|
||||
|
||||
// Draw Astral Units
|
||||
ctx.save();
|
||||
ctx.font = '32px IBMPlexMono-Bold';
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(data.au.toString(), 163, 218);
|
||||
ctx.restore();
|
||||
|
||||
// Draw Constellation Units
|
||||
ctx.save();
|
||||
ctx.font = '32px IBMPlexMono-Bold';
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(data.cu.toString(), 163, 153);
|
||||
ctx.restore();
|
||||
|
||||
// Draw Level
|
||||
ctx.save();
|
||||
// check how many digits the level has and adjust the position accordingly
|
||||
const levelDigits = data.level.toString().length;
|
||||
ctx.font = '12px IBMPlexMono-Bold';
|
||||
ctx.fillStyle = secondaryTextColor;
|
||||
ctx.textAlign = 'left';
|
||||
if (levelDigits === 1) {
|
||||
ctx.fillText(data.level.toString(), 406, 265);
|
||||
} else {
|
||||
ctx.fillText(data.level.toString(), 400, 265);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Draw Next Level
|
||||
ctx.save();
|
||||
// check how many digits the next level has and adjust the position accordingly
|
||||
const nextLevelDigits = (data.level + 1).toString().length;
|
||||
ctx.font = '24px IBMPlexMono-Bold';
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textAlign = 'left';
|
||||
if (nextLevelDigits === 1) {
|
||||
ctx.fillText((data.level + 1).toString(), 438, 255);
|
||||
} else {
|
||||
ctx.fillText((data.level + 1).toString(), 431, 255);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Draw xp bar
|
||||
const xpbarX = 2;
|
||||
const xpbarY = 268;
|
||||
const xpbarMaxWidth = 400; // in pixels
|
||||
const xpbarHeight = 4;
|
||||
|
||||
const xp = data.xp;
|
||||
const requiredXp = BigInt(levelingService.getXpForLevel(data.level));
|
||||
|
||||
// Check to avoid division by zero, though requiredXp should be >= 100
|
||||
let percentage = 0;
|
||||
if (requiredXp > 0n) {
|
||||
percentage = Number(xp) / Number(requiredXp);
|
||||
}
|
||||
|
||||
const xpFilledWidth = Math.min(Math.max(percentage * xpbarMaxWidth, 0), xpbarMaxWidth);
|
||||
|
||||
const gradient = ctx.createLinearGradient(xpbarX, xpbarY, xpbarX + xpFilledWidth, xpbarY);
|
||||
gradient.addColorStop(0, '#FFFFFF');
|
||||
gradient.addColorStop(1, '#D9B178');
|
||||
ctx.save();
|
||||
ctx.fillStyle = accentColor;
|
||||
ctx.fillRect(xpbarX, xpbarY, xpbarMaxWidth, xpbarHeight);
|
||||
|
||||
if (xpFilledWidth > 0) {
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(xpbarX, xpbarY, xpFilledWidth, xpbarHeight);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
return canvas.toBuffer('image/png');
|
||||
}
|
||||
13
src/index.ts
13
src/index.ts
@@ -2,6 +2,7 @@ import { Events } from "discord.js";
|
||||
import { KyokoClient } from "@lib/KyokoClient";
|
||||
import { env } from "@lib/env";
|
||||
import { userService } from "@/modules/user/user.service";
|
||||
import { levelingService } from "@/modules/leveling/leveling.service";
|
||||
|
||||
// Load commands
|
||||
await KyokoClient.loadCommands();
|
||||
@@ -11,6 +12,18 @@ KyokoClient.once(Events.ClientReady, async c => {
|
||||
console.log(`Ready! Logged in as ${c.user.tag}`);
|
||||
});
|
||||
|
||||
// process xp on message
|
||||
KyokoClient.on(Events.MessageCreate, async message => {
|
||||
if (message.author.bot) return;
|
||||
if (!message.guild) return;
|
||||
|
||||
const user = await userService.getUserById(message.author.id);
|
||||
if (!user) return;
|
||||
|
||||
levelingService.processChatXp(message.author.id);
|
||||
});
|
||||
|
||||
// handle commands
|
||||
KyokoClient.on(Events.InteractionCreate, async interaction => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
|
||||
@@ -25,7 +25,12 @@ export const classService = {
|
||||
};
|
||||
return tx ? await execute(tx) : await DrizzleClient.transaction(execute);
|
||||
},
|
||||
|
||||
getClassBalance: async (classId: bigint) => {
|
||||
const cls = await DrizzleClient.query.classes.findFirst({
|
||||
where: eq(classes.id, classId),
|
||||
});
|
||||
return cls?.balance || 0n;
|
||||
},
|
||||
modifyClassBalance: async (classId: bigint, amount: bigint, tx?: any) => {
|
||||
const execute = async (txFn: any) => {
|
||||
const cls = await txFn.query.classes.findFirst({
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { users } from "@/db/schema";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { users, cooldowns } from "@/db/schema";
|
||||
import { eq, sql, and } from "drizzle-orm";
|
||||
import { DrizzleClient } from "@/lib/DrizzleClient";
|
||||
|
||||
// Simple configurable curve: Base * (Level ^ Exponent)
|
||||
const XP_BASE = 1000;
|
||||
const XP_EXPONENT = 1.5;
|
||||
const XP_BASE = 100;
|
||||
const XP_EXPONENT = 2.5;
|
||||
const CHAT_XP_COOLDOWN_MS = 60000; // 1 minute
|
||||
const MIN_CHAT_XP = 15;
|
||||
const MAX_CHAT_XP = 25;
|
||||
|
||||
export const levelingService = {
|
||||
// Calculate XP required for a specific level
|
||||
@@ -12,6 +15,7 @@ export const levelingService = {
|
||||
return Math.floor(XP_BASE * Math.pow(level, XP_EXPONENT));
|
||||
},
|
||||
|
||||
// Pure XP addition - No cooldown checks
|
||||
addXp: async (id: string, amount: bigint, tx?: any) => {
|
||||
const execute = async (txFn: any) => {
|
||||
// Get current state
|
||||
@@ -45,7 +49,7 @@ export const levelingService = {
|
||||
.returning();
|
||||
|
||||
return { user: updatedUser, levelUp, currentLevel };
|
||||
}
|
||||
};
|
||||
|
||||
if (tx) {
|
||||
return await execute(tx);
|
||||
@@ -55,4 +59,52 @@ export const levelingService = {
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Handle chat XP with cooldowns
|
||||
processChatXp: async (id: string, tx?: any) => {
|
||||
const execute = async (txFn: any) => {
|
||||
// check if an xp cooldown is in place
|
||||
const cooldown = await txFn.query.cooldowns.findFirst({
|
||||
where: and(
|
||||
eq(cooldowns.userId, BigInt(id)),
|
||||
eq(cooldowns.actionKey, 'xp')
|
||||
),
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
if (cooldown && cooldown.readyAt > now) {
|
||||
return { awarded: false, reason: 'cooldown' };
|
||||
}
|
||||
|
||||
// Calculate random XP
|
||||
const amount = BigInt(Math.floor(Math.random() * (MAX_CHAT_XP - MIN_CHAT_XP + 1)) + MIN_CHAT_XP);
|
||||
|
||||
// Add XP
|
||||
const result = await levelingService.addXp(id, amount, txFn);
|
||||
|
||||
// Update/Set Cooldown
|
||||
const nextReadyAt = new Date(now.getTime() + CHAT_XP_COOLDOWN_MS);
|
||||
|
||||
await txFn.insert(cooldowns)
|
||||
.values({
|
||||
userId: BigInt(id),
|
||||
actionKey: 'xp',
|
||||
readyAt: nextReadyAt,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [cooldowns.userId, cooldowns.actionKey],
|
||||
set: { readyAt: nextReadyAt },
|
||||
});
|
||||
|
||||
return { awarded: true, amount, ...result };
|
||||
};
|
||||
|
||||
if (tx) {
|
||||
return await execute(tx);
|
||||
} else {
|
||||
return await DrizzleClient.transaction(async (t) => {
|
||||
return await execute(t);
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,6 +32,13 @@ export const userService = {
|
||||
};
|
||||
return tx ? await execute(tx) : await DrizzleClient.transaction(execute);
|
||||
},
|
||||
getUserClass: async (id: string) => {
|
||||
const user = await DrizzleClient.query.users.findFirst({
|
||||
where: eq(users.id, BigInt(id)),
|
||||
with: { class: true }
|
||||
});
|
||||
return user?.class;
|
||||
},
|
||||
createUser: async (id: string | bigint, username: string, classId?: bigint, tx?: any) => {
|
||||
const execute = async (txFn: any) => {
|
||||
const [user] = await txFn.insert(users).values({
|
||||
|
||||
61
src/scripts/test-student-id.ts
Normal file
61
src/scripts/test-student-id.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
import { DrizzleClient } from "@/lib/DrizzleClient";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { userService } from "@/modules/user/user.service";
|
||||
import { classService } from "@/modules/class/class.service";
|
||||
import { generateStudentIdCard } from "@/graphics/studentID";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function main() {
|
||||
console.log("Fetching first user from database...");
|
||||
|
||||
// Get the first user
|
||||
const user = await DrizzleClient.query.users.findFirst({ where: (users) => eq(users.id, BigInt(109998942841765888)) });
|
||||
|
||||
if (!user) {
|
||||
console.error("No users found in database. Please ensure the database is seeded or has at least one user.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Found user: ${user.username} (${user.id})`);
|
||||
|
||||
// Get user class
|
||||
const userClass = await userService.getUserClass(user.id.toString());
|
||||
const className = userClass?.name || "Unknown";
|
||||
console.log(`User Class: ${className}`);
|
||||
|
||||
// Get class balance
|
||||
const classBalance = await classService.getClassBalance(userClass?.id || BigInt(0));
|
||||
console.log(`Class Balance: ${classBalance}`);
|
||||
|
||||
// Placeholder avatar (default discord avatar)
|
||||
const avatarUrl = "https://cdn.discordapp.com/embed/avatars/0.png";
|
||||
|
||||
console.log("Generating Student ID Card...");
|
||||
|
||||
try {
|
||||
const cardBuffer = await generateStudentIdCard({
|
||||
username: user.username,
|
||||
avatarUrl: avatarUrl,
|
||||
id: user.id.toString(),
|
||||
level: user.level || 1,
|
||||
xp: user.xp || 0n,
|
||||
au: user.balance || 0n,
|
||||
cu: classBalance || 0n,
|
||||
className: 'D'
|
||||
});
|
||||
|
||||
const outputPath = path.join(process.cwd(), 'test-student-id.png');
|
||||
fs.writeFileSync(outputPath, cardBuffer);
|
||||
|
||||
console.log(`Student ID card generated successfully: ${outputPath}`);
|
||||
} catch (error) {
|
||||
console.error("Error generating student ID card:", error);
|
||||
} finally {
|
||||
// Exit cleanly
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user