Compare commits
2 Commits
34cbea2753
...
7cf8d68d39
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cf8d68d39 | ||
|
|
83cd33e439 |
@@ -15,6 +15,7 @@
|
|||||||
"generate": "docker compose run --rm app drizzle-kit generate",
|
"generate": "docker compose run --rm app drizzle-kit generate",
|
||||||
"migrate": "docker compose run --rm app drizzle-kit migrate",
|
"migrate": "docker compose run --rm app drizzle-kit migrate",
|
||||||
"db:push": "docker compose run --rm app drizzle-kit push",
|
"db:push": "docker compose run --rm app drizzle-kit push",
|
||||||
|
"db:push:local": "drizzle-kit push",
|
||||||
"dev": "bun --watch src/index.ts",
|
"dev": "bun --watch src/index.ts",
|
||||||
"db:studio": "drizzle-kit studio --host 0.0.0.0"
|
"db:studio": "drizzle-kit studio --host 0.0.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,9 +76,27 @@ export const update = createCommand({
|
|||||||
|
|
||||||
const { stdout } = await execAsync(`git reset --hard origin/${branch}`);
|
const { stdout } = await execAsync(`git reset --hard origin/${branch}`);
|
||||||
|
|
||||||
|
// Run DB Migrations
|
||||||
|
await confirmation.editReply({
|
||||||
|
embeds: [createWarningEmbed("Updating database schema...", "Running Migrations")],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
|
||||||
|
let migrationOutput = "";
|
||||||
|
try {
|
||||||
|
const { stdout: dbOut } = await execAsync("bun run db:push:local");
|
||||||
|
migrationOutput = dbOut;
|
||||||
|
} catch (dbErr: any) {
|
||||||
|
migrationOutput = `Migration Failed: ${dbErr.message}`;
|
||||||
|
console.error("Migration Error:", dbErr);
|
||||||
|
// We continue with restart even if migration fails?
|
||||||
|
// Probably safer to try, or should we abort?
|
||||||
|
// For now, let's log it and proceed, as code might depend on it but maybe it was a no-op partial fail.
|
||||||
|
}
|
||||||
|
|
||||||
await interaction.followUp({
|
await interaction.followUp({
|
||||||
flags: MessageFlags.Ephemeral,
|
flags: MessageFlags.Ephemeral,
|
||||||
embeds: [createSuccessEmbed(`Git Reset Output:\n\`\`\`\n${stdout}\n\`\`\`\nRestarting process...`, "Update Successful")]
|
embeds: [createSuccessEmbed(`Git Reset Output:\n\`\`\`\n${stdout}\n\`\`\`\nDB Migration Output:\n\`\`\`\n${migrationOutput}\n\`\`\`\nRestarting process...`, "Update Successful")]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Write context for post-restart notification
|
// Write context for post-restart notification
|
||||||
|
|||||||
@@ -60,20 +60,24 @@ export const use = createCommand({
|
|||||||
const focusedValue = interaction.options.getFocused();
|
const focusedValue = interaction.options.getFocused();
|
||||||
const userId = interaction.user.id;
|
const userId = interaction.user.id;
|
||||||
|
|
||||||
// Fetch owned items that are usable
|
// Fetch owned items that match the search query
|
||||||
const userInventory = await DrizzleClient.query.inventory.findMany({
|
// We join with items table to filter by name directly in the database
|
||||||
where: eq(inventory.userId, BigInt(userId)),
|
const entries = await DrizzleClient.select({
|
||||||
with: {
|
quantity: inventory.quantity,
|
||||||
item: true
|
item: items
|
||||||
},
|
})
|
||||||
limit: 10
|
.from(inventory)
|
||||||
});
|
.innerJoin(items, eq(inventory.itemId, items.id))
|
||||||
|
.where(and(
|
||||||
|
eq(inventory.userId, BigInt(userId)),
|
||||||
|
like(items.name, `%${focusedValue}%`)
|
||||||
|
))
|
||||||
|
.limit(20); // Fetch up to 20 matching items
|
||||||
|
|
||||||
const filtered = userInventory.filter(entry => {
|
const filtered = entries.filter(entry => {
|
||||||
const matchName = entry.item.name.toLowerCase().includes(focusedValue.toLowerCase());
|
|
||||||
const usageData = entry.item.usageData as ItemUsageData | null;
|
const usageData = entry.item.usageData as ItemUsageData | null;
|
||||||
const isUsable = usageData && usageData.effects && usageData.effects.length > 0;
|
const isUsable = usageData && usageData.effects && usageData.effects.length > 0;
|
||||||
return matchName && isUsable;
|
return isUsable;
|
||||||
});
|
});
|
||||||
|
|
||||||
await interaction.respond(
|
await interaction.respond(
|
||||||
|
|||||||
@@ -130,8 +130,17 @@ export const userTimers = pgTable('user_timers', {
|
|||||||
}, (table) => [
|
}, (table) => [
|
||||||
primaryKey({ columns: [table.userId, table.type, table.key] })
|
primaryKey({ columns: [table.userId, table.type, table.key] })
|
||||||
]);
|
]);
|
||||||
|
// 9. Lootdrops
|
||||||
|
export const lootdrops = pgTable('lootdrops', {
|
||||||
|
messageId: varchar('message_id', { length: 255 }).primaryKey(),
|
||||||
|
channelId: varchar('channel_id', { length: 255 }).notNull(),
|
||||||
|
rewardAmount: integer('reward_amount').notNull(),
|
||||||
|
currency: varchar('currency', { length: 50 }).notNull(),
|
||||||
|
claimedBy: bigint('claimed_by', { mode: 'bigint' }).references(() => users.id, { onDelete: 'set null' }),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
||||||
|
});
|
||||||
|
|
||||||
// --- RELATIONS ---
|
|
||||||
|
|
||||||
export const classesRelations = relations(classes, ({ many }) => ({
|
export const classesRelations = relations(classes, ({ many }) => ({
|
||||||
users: many(users),
|
users: many(users),
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { Message, TextChannel, EmbedBuilder, ActionRowBuilder, ButtonBuilder, Bu
|
|||||||
import { config } from "@/lib/config";
|
import { config } from "@/lib/config";
|
||||||
import { economyService } from "./economy.service";
|
import { economyService } from "./economy.service";
|
||||||
|
|
||||||
|
import { lootdrops } from "@/db/schema";
|
||||||
|
import { DrizzleClient } from "@/lib/DrizzleClient";
|
||||||
|
import { eq, and, isNull, lt } from "drizzle-orm";
|
||||||
|
|
||||||
interface Lootdrop {
|
interface Lootdrop {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
@@ -15,11 +19,13 @@ interface Lootdrop {
|
|||||||
class LootdropService {
|
class LootdropService {
|
||||||
private channelActivity: Map<string, number[]> = new Map();
|
private channelActivity: Map<string, number[]> = new Map();
|
||||||
private channelCooldowns: Map<string, number> = new Map();
|
private channelCooldowns: Map<string, number> = new Map();
|
||||||
private activeLootdrops: Map<string, Lootdrop> = new Map(); // key: messageId
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Cleanup interval for activity tracking
|
// Cleanup interval for activity tracking and expired lootdrops
|
||||||
setInterval(() => this.cleanupActivity(), 60000);
|
setInterval(() => {
|
||||||
|
this.cleanupActivity();
|
||||||
|
this.cleanupExpiredLootdrops();
|
||||||
|
}, 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanupActivity() {
|
private cleanupActivity() {
|
||||||
@@ -36,6 +42,19 @@ class LootdropService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async cleanupExpiredLootdrops() {
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
await DrizzleClient.delete(lootdrops)
|
||||||
|
.where(and(
|
||||||
|
isNull(lootdrops.claimedBy),
|
||||||
|
lt(lootdrops.expiresAt, now)
|
||||||
|
));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to cleanup lootdrops:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async processMessage(message: Message) {
|
public async processMessage(message: Message) {
|
||||||
if (message.author.bot || !message.guild) return;
|
if (message.author.bot || !message.guild) return;
|
||||||
|
|
||||||
@@ -61,8 +80,6 @@ class LootdropService {
|
|||||||
await this.spawnLootdrop(message.channel as TextChannel);
|
await this.spawnLootdrop(message.channel as TextChannel);
|
||||||
// Set cooldown
|
// Set cooldown
|
||||||
this.channelCooldowns.set(channelId, now + config.lootdrop.cooldownMs);
|
this.channelCooldowns.set(channelId, now + config.lootdrop.cooldownMs);
|
||||||
// Reset activity for this channel to prevent immediate double spawn?
|
|
||||||
// Maybe not strictly necessary if cooldown handles it, but good practice.
|
|
||||||
this.channelActivity.set(channelId, []);
|
this.channelActivity.set(channelId, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,44 +109,45 @@ class LootdropService {
|
|||||||
try {
|
try {
|
||||||
const message = await channel.send({ embeds: [embed], components: [row] });
|
const message = await channel.send({ embeds: [embed], components: [row] });
|
||||||
|
|
||||||
this.activeLootdrops.set(message.id, {
|
// Persist to DB
|
||||||
|
await DrizzleClient.insert(lootdrops).values({
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
rewardAmount: reward,
|
rewardAmount: reward,
|
||||||
currency: currency,
|
currency: currency,
|
||||||
createdAt: new Date()
|
createdAt: new Date(),
|
||||||
|
// Expire after 10 mins
|
||||||
|
expiresAt: new Date(Date.now() + 600000)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-cleanup unclaimable drops after 10 minutes (optional, prevents memory leak)
|
|
||||||
setTimeout(() => {
|
|
||||||
const drop = this.activeLootdrops.get(message.id);
|
|
||||||
if (drop && !drop.claimedBy) {
|
|
||||||
this.activeLootdrops.delete(message.id);
|
|
||||||
// Optionally edit message to say expired
|
|
||||||
}
|
|
||||||
}, 600000);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to spawn lootdrop:", error);
|
console.error("Failed to spawn lootdrop:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async tryClaim(messageId: string, userId: string, username: string): Promise<{ success: boolean; amount?: number; currency?: string; error?: string }> {
|
public async tryClaim(messageId: string, userId: string, username: string): Promise<{ success: boolean; amount?: number; currency?: string; error?: string }> {
|
||||||
const drop = this.activeLootdrops.get(messageId);
|
try {
|
||||||
|
// Atomic update: Try to set claimedBy where it is currently null
|
||||||
|
// This acts as a lock and check in one query
|
||||||
|
const result = await DrizzleClient.update(lootdrops)
|
||||||
|
.set({ claimedBy: BigInt(userId) })
|
||||||
|
.where(and(
|
||||||
|
eq(lootdrops.messageId, messageId),
|
||||||
|
isNull(lootdrops.claimedBy)
|
||||||
|
))
|
||||||
|
.returning();
|
||||||
|
|
||||||
if (!drop) {
|
if (result.length === 0 || !result[0]) {
|
||||||
return { success: false, error: "This lootdrop has expired or already been fully processed." };
|
// If update affected 0 rows, check if it was because it doesn't exist or is already claimed
|
||||||
|
const check = await DrizzleClient.select().from(lootdrops).where(eq(lootdrops.messageId, messageId));
|
||||||
|
if (check.length === 0) {
|
||||||
|
return { success: false, error: "This lootdrop has expired." };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (drop.claimedBy) {
|
|
||||||
return { success: false, error: "This lootdrop has already been claimed." };
|
return { success: false, error: "This lootdrop has already been claimed." };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock it
|
const drop = result[0];
|
||||||
drop.claimedBy = userId;
|
|
||||||
this.activeLootdrops.set(messageId, drop);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await economyService.modifyUserBalance(
|
await economyService.modifyUserBalance(
|
||||||
userId,
|
userId,
|
||||||
BigInt(drop.rewardAmount),
|
BigInt(drop.rewardAmount),
|
||||||
@@ -137,15 +155,10 @@ class LootdropService {
|
|||||||
`Claimed lootdrop in channel ${drop.channelId}`
|
`Claimed lootdrop in channel ${drop.channelId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clean up from memory
|
|
||||||
this.activeLootdrops.delete(messageId);
|
|
||||||
|
|
||||||
return { success: true, amount: drop.rewardAmount, currency: drop.currency };
|
return { success: true, amount: drop.rewardAmount, currency: drop.currency };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error claiming lootdrop:", error);
|
console.error("Error claiming lootdrop:", error);
|
||||||
// Unlock if failed?
|
|
||||||
drop.claimedBy = undefined;
|
|
||||||
this.activeLootdrops.set(messageId, drop);
|
|
||||||
return { success: false, error: "An error occurred while processing the reward." };
|
return { success: false, error: "An error occurred while processing the reward." };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user