Add RCON client + SendCurrency service (AtomCMS-faithful)
- rcon.ts: Node net.Socket transport speaking AtomCMS's exact protocol (raw
JSON {"key","data"} over rcon_ip:rcon_port, fire-and-forget) with a bounded
timeout + backoff retry; typed commands matching RconService EXACTLY
(sendgift `itemid`, disconnect `username`, no-data => data:null, givepoints
numeric `type`, forwarduser, setmotto/setrank/alertuser, give*). Injectable
transport keeps command shapes unit-testable.
- send-currency.ts: mirrors the SendCurrency action — RCON deliver, else DB
fallback (credits -> users.credits; duckets/diamonds/points -> users_currency
by type 0/5/101).
- env: RCON_HOST/PORT/TIMEOUT/RETRIES. vitest: SKIP_ENV_VALIDATION for unit tests.
Verified: tsc exit 0, vitest 39/39, next build exit 0. Live TCP test deferred
(needs a running emulator).
This commit is contained in:
1 parent
443d908909
commit
8fca407f0f
6 files changed
+350
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import type { RconClient } from "@/lib/services/rcon";
|
||||
|
||||
export type CurrencyName = "credits" | "duckets" | "diamonds" | "points";
|
||||
|
||||
// users_currency.type values for the non-credits currencies.
|
||||
const TYPE_VALUE: Record<Exclude<CurrencyName, "credits">, number> = {
|
||||
duckets: 0,
|
||||
diamonds: 5,
|
||||
points: 101,
|
||||
};
|
||||
|
||||
export interface CurrencyDb {
|
||||
user: {
|
||||
update(args: {
|
||||
where: { id: number };
|
||||
data: { credits: { increment: number } };
|
||||
}): Promise<unknown>;
|
||||
};
|
||||
usersCurrency: {
|
||||
upsert(args: {
|
||||
where: { userId_type: { userId: number; type: number } };
|
||||
update: { amount: { increment: number } };
|
||||
create: { userId: number; type: number; amount: number };
|
||||
}): Promise<unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SendCurrencyDeps {
|
||||
rcon: Pick<RconClient, "giveCredits" | "giveDuckets" | "giveDiamonds" | "givePointsGotw">;
|
||||
db: CurrencyDb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors AtomCMS's SendCurrency action: deliver via RCON, falling back to a
|
||||
* direct DB write when RCON delivery fails (the PHP version checks
|
||||
* rcon->isConnected up front; trying-then-falling-back is equivalent and more
|
||||
* robust). Credits live on users.credits; duckets/diamonds/points live in
|
||||
* users_currency keyed by (user_id, type).
|
||||
*/
|
||||
export async function sendCurrency(
|
||||
deps: SendCurrencyDeps,
|
||||
userId: number,
|
||||
type: CurrencyName,
|
||||
amount: number | null | undefined,
|
||||
): Promise<boolean> {
|
||||
if (!amount || amount <= 0) return false;
|
||||
|
||||
if (await rconDeliver(deps.rcon, userId, type, amount)) return true;
|
||||
|
||||
if (type === "credits") {
|
||||
await deps.db.user.update({
|
||||
where: { id: userId },
|
||||
data: { credits: { increment: amount } },
|
||||
});
|
||||
} else {
|
||||
const t = TYPE_VALUE[type];
|
||||
await deps.db.usersCurrency.upsert({
|
||||
where: { userId_type: { userId, type: t } },
|
||||
update: { amount: { increment: amount } },
|
||||
create: { userId, type: t, amount },
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function rconDeliver(
|
||||
rcon: SendCurrencyDeps["rcon"],
|
||||
userId: number,
|
||||
type: CurrencyName,
|
||||
amount: number,
|
||||
): Promise<boolean> {
|
||||
switch (type) {
|
||||
case "credits":
|
||||
return rcon.giveCredits(userId, amount);
|
||||
case "duckets":
|
||||
return rcon.giveDuckets(userId, amount);
|
||||
case "diamonds":
|
||||
return rcon.giveDiamonds(userId, amount);
|
||||
case "points":
|
||||
return rcon.givePointsGotw(userId, amount);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user