feat(discord-bot): drop files to use postgres and redis

This commit is contained in:
Pihkaal
2025-12-04 17:37:57 +01:00
parent f41072ffe1
commit 622b881d89
6 changed files with 83 additions and 99 deletions

View File

@@ -9,3 +9,9 @@ QUEST_EXCLUDE=
DISCORD_WEBHOOK_URL= DISCORD_WEBHOOK_URL=
DISCORD_MENTION= DISCORD_MENTION=
DISCORD_REWARDS_GIVER= DISCORD_REWARDS_GIVER=
# Postgres database url
DATABASE_URL=""
# Redis url
REDIS_URL=""

View File

@@ -14,6 +14,7 @@
"typescript": "^5.7.2" "typescript": "^5.7.2"
}, },
"dependencies": { "dependencies": {
"@lbf-bot/database": "workspace:*",
"discord.js": "^14.21.0", "discord.js": "^14.21.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"zod": "^3.24.4" "zod": "^3.24.4"

View File

@@ -1,10 +1,5 @@
import { initAccounts } from "~/services/account";
import { env } from "~/env"; import { env } from "~/env";
import { import { listTrackedPlayers, trackWovPlayer } from "~/services/tracking";
initTracking,
listTrackedPlayers,
trackWovPlayer,
} from "~/services/tracking";
import { checkForNewQuest } from "~/services/wov"; import { checkForNewQuest } from "~/services/wov";
import { createInfoEmbed } from "~/utils/discord"; import { createInfoEmbed } from "~/utils/discord";
import { askForGrinders } from "~/utils/quest"; import { askForGrinders } from "~/utils/quest";
@@ -93,9 +88,6 @@ client.on("ready", async (client) => {
process.exit(0); process.exit(0);
}); });
} else { } else {
await initAccounts();
await initTracking();
await questCheckCron(); await questCheckCron();
setInterval(questCheckCron, env.WOV_FETCH_INTERVAL); setInterval(questCheckCron, env.WOV_FETCH_INTERVAL);

View File

@@ -1,23 +1,16 @@
import { readFile, writeFile, access } from "node:fs/promises"; import { db, tables, eq } from "@lbf-bot/database";
import { constants } from "node:fs";
const ACCOUNTS_FILE = "./.cache/accounts.json";
export const initAccounts = async (): Promise<void> => {
try {
await access(ACCOUNTS_FILE, constants.F_OK);
} catch {
await writeFile(ACCOUNTS_FILE, "{}");
}
};
export const getAccountBalance = async (playerId: string): Promise<number> => { export const getAccountBalance = async (playerId: string): Promise<number> => {
const content = await readFile(ACCOUNTS_FILE, "utf-8"); const account = await db.query.accounts.findFirst({
const accounts: Record<string, number> = JSON.parse(content); where: eq(tables.accounts.playerId, playerId),
if (accounts[playerId]) return accounts[playerId]; });
accounts[playerId] = 0; if (account) return account.balance;
await writeFile(ACCOUNTS_FILE, JSON.stringify(accounts));
await db.insert(tables.accounts).values({
playerId,
balance: 0,
});
return 0; return 0;
}; };
@@ -26,9 +19,14 @@ export const setAccountBalance = async (
playerId: string, playerId: string,
balance: number, balance: number,
): Promise<void> => { ): Promise<void> => {
const content = await readFile(ACCOUNTS_FILE, "utf-8"); await db
const accounts: Record<string, number> = JSON.parse(content); .insert(tables.accounts)
accounts[playerId] = balance; .values({
playerId,
await writeFile(ACCOUNTS_FILE, JSON.stringify(accounts)); balance,
})
.onConflictDoUpdate({
target: tables.accounts.playerId,
set: { balance, updatedAt: new Date() },
});
}; };

View File

@@ -1,35 +1,28 @@
import { getPlayer } from "~/services/wov"; import { getPlayer } from "~/services/wov";
import { readFile, writeFile, access } from "node:fs/promises"; import { db, tables, eq } from "@lbf-bot/database";
import { constants } from "node:fs";
import type { TrackedPlayers } from "~/types";
const TRACKED_PLAYER_FILE = "./.cache/tracked.json";
export async function initTracking(): Promise<void> {
try {
await access(TRACKED_PLAYER_FILE, constants.F_OK);
} catch {
await writeFile(TRACKED_PLAYER_FILE, "{}");
}
}
export async function listTrackedPlayers(): Promise<string[]> { export async function listTrackedPlayers(): Promise<string[]> {
const content = await readFile(TRACKED_PLAYER_FILE, "utf-8"); const players = await db.query.trackedPlayers.findMany({
const trackedPlayers: TrackedPlayers = JSON.parse(content); columns: {
playerId: true,
},
});
return Object.keys(trackedPlayers); return players.map((p) => p.playerId);
} }
export async function untrackWovPlayer( export async function untrackWovPlayer(
playerId: string, playerId: string,
): Promise<{ event: "notTracked" } | { event: "trackerRemoved" }> { ): Promise<{ event: "notTracked" } | { event: "trackerRemoved" }> {
const content = await readFile(TRACKED_PLAYER_FILE, "utf-8"); const player = await db.query.trackedPlayers.findFirst({
const trackedPlayers: TrackedPlayers = JSON.parse(content); where: eq(tables.trackedPlayers.playerId, playerId),
});
if (!trackedPlayers[playerId]) return { event: "notTracked" }; if (!player) return { event: "notTracked" };
delete trackedPlayers[playerId]; await db
await writeFile(TRACKED_PLAYER_FILE, JSON.stringify(trackedPlayers)); .delete(tables.trackedPlayers)
.where(eq(tables.trackedPlayers.playerId, playerId));
return { event: "trackerRemoved" }; return { event: "trackerRemoved" };
} }
@@ -42,23 +35,35 @@ export async function trackWovPlayer(playerId: string): Promise<
| { event: "changed"; oldUsernames: string[]; newUsername: string } | { event: "changed"; oldUsernames: string[]; newUsername: string }
| { event: "none" } | { event: "none" }
> { > {
const content = await readFile(TRACKED_PLAYER_FILE, "utf-8");
const trackedPlayers: TrackedPlayers = JSON.parse(content);
const player = await getPlayer(playerId); const player = await getPlayer(playerId);
if (!player) return { event: "notFound" }; if (!player) return { event: "notFound" };
const currentUsernames = trackedPlayers[playerId]; const tracked = await db.query.trackedPlayers.findFirst({
if (currentUsernames) { where: eq(tables.trackedPlayers.playerId, playerId),
const oldUsernames = [...currentUsernames]; with: {
if (!currentUsernames.includes(player.username)) { usernameHistory: {
currentUsernames.push(player.username); orderBy: (history, { asc }) => [asc(history.firstSeenAt)],
},
},
});
await writeFile(TRACKED_PLAYER_FILE, JSON.stringify(trackedPlayers)); if (tracked) {
const currentUsernames = tracked.usernameHistory.map((h) => h.username);
if (!currentUsernames.includes(player.username)) {
await db.insert(tables.usernameHistory).values({
playerId,
username: player.username,
});
await db
.update(tables.trackedPlayers)
.set({ updatedAt: new Date() })
.where(eq(tables.trackedPlayers.playerId, playerId));
return { return {
event: "changed", event: "changed",
oldUsernames, oldUsernames: currentUsernames,
newUsername: player.username, newUsername: player.username,
}; };
} else { } else {
@@ -67,8 +72,15 @@ export async function trackWovPlayer(playerId: string): Promise<
}; };
} }
} else { } else {
trackedPlayers[playerId] = [player.username]; await db.insert(tables.trackedPlayers).values({
await writeFile(TRACKED_PLAYER_FILE, JSON.stringify(trackedPlayers)); playerId,
});
await db.insert(tables.usernameHistory).values({
playerId,
username: player.username,
});
return { event: "registered" }; return { event: "registered" };
} }
} }

View File

@@ -1,6 +1,5 @@
import { env } from "~/env"; import { env } from "~/env";
import { mkdir, readFile, writeFile, access } from "node:fs/promises"; import { redis } from "@lbf-bot/database";
import { constants } from "node:fs";
import type { QuestResult } from "~/types"; import type { QuestResult } from "~/types";
export const getLatestQuest = async (): Promise<QuestResult> => { export const getLatestQuest = async (): Promise<QuestResult> => {
@@ -17,45 +16,23 @@ export const getLatestQuest = async (): Promise<QuestResult> => {
export const checkForNewQuest = async (): Promise<QuestResult | null> => { export const checkForNewQuest = async (): Promise<QuestResult | null> => {
const lastQuest = await getLatestQuest(); const lastQuest = await getLatestQuest();
const lastId = lastQuest.quest.id; const lastId = lastQuest.quest.id;
const cacheFilePath = ".cache/.quest_cache";
await mkdir(".cache", { recursive: true });
try { const cachedQuestId = await redis.get("quest:last_id");
await access(cacheFilePath, constants.F_OK); if (cachedQuestId === lastId || cachedQuestId === "IGNORE") {
const cachedQuestId = await readFile(cacheFilePath, "utf-8"); return null;
if (cachedQuestId === lastId || cachedQuestId === "IGNORE") {
return null;
}
} catch {
// File doesn't exist, continue
} }
await writeFile(cacheFilePath, lastId); await redis.set("quest:last_id", lastId);
return lastQuest; return lastQuest;
}; };
export const getClanMembers = async (): Promise< export const getClanMembers = async (): Promise<
Array<{ playerId: string; username: string }> Array<{ playerId: string; username: string }>
> => { > => {
const cacheFilePath = ".clan_members_cache"; const cached = await redis.get("clan:members");
await mkdir(".cache", { recursive: true }); if (cached) {
return JSON.parse(cached);
let cached: {
timestamp: number;
data: Array<{ playerId: string; username: string }>;
} | null = null;
try {
await access(cacheFilePath, constants.F_OK);
const content = await readFile(cacheFilePath, "utf-8");
cached = JSON.parse(content);
if (cached && Date.now() - cached.timestamp < 60 * 60 * 1000) {
return cached.data;
}
} catch {
// File doesn't exist or is invalid, continue
} }
const response = await fetch( const response = await fetch(
@@ -69,10 +46,8 @@ export const getClanMembers = async (): Promise<
playerId: string; playerId: string;
username: string; username: string;
}>; }>;
await writeFile(
cacheFilePath, await redis.set("clan:members", JSON.stringify(data), "EX", 60 * 60);
JSON.stringify({ timestamp: Date.now(), data }),
);
return data; return data;
}; };