forked from syntaxbullet/AuroraBot-discord
refactor: initial moves
This commit is contained in:
171
shared/modules/economy/lootdrop.service.ts
Normal file
171
shared/modules/economy/lootdrop.service.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
|
||||
import { Message, TextChannel } from "discord.js";
|
||||
import { getLootdropMessage } from "./lootdrop.view";
|
||||
import { config } from "@/lib/config";
|
||||
import { economyService } from "./economy.service";
|
||||
import { terminalService } from "@shared/modules/terminal/terminal.service";
|
||||
|
||||
|
||||
import { lootdrops } from "@db/schema";
|
||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||
import { eq, and, isNull, lt } from "drizzle-orm";
|
||||
|
||||
interface Lootdrop {
|
||||
messageId: string;
|
||||
channelId: string;
|
||||
rewardAmount: number;
|
||||
currency: string;
|
||||
claimedBy?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
class LootdropService {
|
||||
private channelActivity: Map<string, number[]> = new Map();
|
||||
private channelCooldowns: Map<string, number> = new Map();
|
||||
|
||||
constructor() {
|
||||
// Cleanup interval for activity tracking and expired lootdrops
|
||||
setInterval(() => {
|
||||
this.cleanupActivity();
|
||||
this.cleanupExpiredLootdrops(true);
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
private cleanupActivity() {
|
||||
const now = Date.now();
|
||||
const window = config.lootdrop.activityWindowMs;
|
||||
|
||||
for (const [channelId, timestamps] of this.channelActivity.entries()) {
|
||||
const validTimestamps = timestamps.filter(t => now - t < window);
|
||||
if (validTimestamps.length === 0) {
|
||||
this.channelActivity.delete(channelId);
|
||||
} else {
|
||||
this.channelActivity.set(channelId, validTimestamps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async cleanupExpiredLootdrops(includeClaimed: boolean = false): Promise<number> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const whereClause = includeClaimed
|
||||
? lt(lootdrops.expiresAt, now)
|
||||
: and(isNull(lootdrops.claimedBy), lt(lootdrops.expiresAt, now));
|
||||
|
||||
const result = await DrizzleClient.delete(lootdrops)
|
||||
.where(whereClause)
|
||||
.returning();
|
||||
|
||||
if (result.length > 0) {
|
||||
console.log(`[LootdropService] Cleaned up ${result.length} expired lootdrops.`);
|
||||
}
|
||||
return result.length;
|
||||
} catch (error) {
|
||||
console.error("Failed to cleanup lootdrops:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async processMessage(message: Message) {
|
||||
if (message.author.bot || !message.guild) return;
|
||||
|
||||
const channelId = message.channel.id;
|
||||
const now = Date.now();
|
||||
|
||||
// Check cooldown
|
||||
const cooldown = this.channelCooldowns.get(channelId);
|
||||
if (cooldown && now < cooldown) return;
|
||||
|
||||
// Track activity
|
||||
const timestamps = this.channelActivity.get(channelId) || [];
|
||||
timestamps.push(now);
|
||||
this.channelActivity.set(channelId, timestamps);
|
||||
|
||||
// Filter for window
|
||||
const window = config.lootdrop.activityWindowMs;
|
||||
const recentActivity = timestamps.filter(t => now - t < window);
|
||||
|
||||
if (recentActivity.length >= config.lootdrop.minMessages) {
|
||||
// Chance to spawn
|
||||
if (Math.random() < config.lootdrop.spawnChance) {
|
||||
await this.spawnLootdrop(message.channel as TextChannel);
|
||||
// Set cooldown
|
||||
this.channelCooldowns.set(channelId, now + config.lootdrop.cooldownMs);
|
||||
this.channelActivity.set(channelId, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async spawnLootdrop(channel: TextChannel) {
|
||||
const min = config.lootdrop.reward.min;
|
||||
const max = config.lootdrop.reward.max;
|
||||
const reward = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
const currency = config.lootdrop.reward.currency;
|
||||
|
||||
const { content, files, components } = await getLootdropMessage(reward, currency);
|
||||
|
||||
try {
|
||||
const message = await channel.send({ content, files, components });
|
||||
|
||||
// Persist to DB
|
||||
await DrizzleClient.insert(lootdrops).values({
|
||||
messageId: message.id,
|
||||
channelId: channel.id,
|
||||
rewardAmount: reward,
|
||||
currency: currency,
|
||||
createdAt: new Date(),
|
||||
// Expire after 10 mins
|
||||
expiresAt: new Date(Date.now() + 600000)
|
||||
});
|
||||
|
||||
// Trigger Terminal Update
|
||||
terminalService.update();
|
||||
|
||||
} catch (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 }> {
|
||||
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 (result.length === 0 || !result[0]) {
|
||||
// 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." };
|
||||
}
|
||||
return { success: false, error: "This lootdrop has already been claimed." };
|
||||
}
|
||||
|
||||
const drop = result[0];
|
||||
|
||||
await economyService.modifyUserBalance(
|
||||
userId,
|
||||
BigInt(drop.rewardAmount),
|
||||
"LOOTDROP_CLAIM",
|
||||
`Claimed lootdrop in channel ${drop.channelId}`
|
||||
);
|
||||
|
||||
// Trigger Terminal Update
|
||||
terminalService.update();
|
||||
|
||||
return { success: true, amount: drop.rewardAmount, currency: drop.currency };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error claiming lootdrop:", error);
|
||||
return { success: false, error: "An error occurred while processing the reward." };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const lootdropService = new LootdropService();
|
||||
Reference in New Issue
Block a user