feat(codes): mark a code dead once the owner confirms it in game
Game8 still lists Y1INABQB6DEX, 2BJ64QRZ7RT8 and PLDXIB886Y1S as active Genshin codes after the game refused them. CODE_CHECKS records the owner's in-game check; build-codes stamps the matching code confirmedDead, and the Codes tab shows it expired: "Confirmed expired in game, 2026-10-20." Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7569aa6395
commit
b8e8d44816
12 files changed
+178
-12
No files matched your search
@@ -69,13 +69,15 @@ export function CodesPage({
|
||||
</button>
|
||||
{c.reward && <p>{c.reward}</p>}
|
||||
<small>
|
||||
{codeExpired(c, now)
|
||||
? "Expired"
|
||||
: c.expiresAt
|
||||
? `Expires ${formatInZone(c.expiresAt, true, typeof state.prefs.timezone === "string" ? state.prefs.timezone : null)}`
|
||||
: c.expiresOn
|
||||
? `Expires on ${c.expiresOn} (time not stated)`
|
||||
: "Expiry not announced"}{" "}
|
||||
{c.confirmedDead
|
||||
? `Confirmed expired in game, ${c.confirmedDead}.`
|
||||
: codeExpired(c, now)
|
||||
? "Expired"
|
||||
: c.expiresAt
|
||||
? `Expires ${formatInZone(c.expiresAt, true, typeof state.prefs.timezone === "string" ? state.prefs.timezone : null)}`
|
||||
: c.expiresOn
|
||||
? `Expires on ${c.expiresOn} (time not stated)`
|
||||
: "Expiry not announced"}{" "}
|
||||
·{" "}
|
||||
<a href={c.sourceUrl} target="_blank" rel="noreferrer">
|
||||
Source
|
||||
|
||||
@@ -11,9 +11,11 @@ export function newCodes(
|
||||
* Whether a code's stated expiry has passed. A date with no time ("Expires on
|
||||
* 2026-10-20") counts as passed only once that date has ended everywhere, at
|
||||
* 12:00 UTC the next day, so a code is never shown expired while it may
|
||||
* still redeem somewhere.
|
||||
* still redeem somewhere. A code the owner confirmed dead in game is expired
|
||||
* whatever it states.
|
||||
*/
|
||||
export function codeExpired(code: RedeemCode, now: number): boolean {
|
||||
if (code.confirmedDead) return true;
|
||||
if (code.expiresAt) return Date.parse(code.expiresAt) <= now;
|
||||
if (code.expiresOn)
|
||||
return Date.parse(`${code.expiresOn}T00:00:00Z`) + 36 * 3600_000 <= now;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CodesFeed, RedeemCode } from "../../shared/codes.ts";
|
||||
import type { CodeCheck } from "../../shared/codeChecks.ts";
|
||||
export function buildCodesFeed(
|
||||
previous: CodesFeed | null,
|
||||
parsed: readonly Omit<RedeemCode, "firstObservedAt">[],
|
||||
@@ -14,3 +15,28 @@ export function buildCodesFeed(
|
||||
sources: previous?.sources ?? [],
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Stamps each code the owner confirmed dead (`codeChecks.ts`) and returns the
|
||||
* checks that matched no code, for the build to report.
|
||||
*/
|
||||
export function applyCodeChecks(
|
||||
codes: readonly RedeemCode[],
|
||||
checks: readonly CodeCheck[],
|
||||
): { codes: RedeemCode[]; unmatched: CodeCheck[] } {
|
||||
const byId = new Map(
|
||||
checks.map((c) => [JSON.stringify([c.game, c.code]), c]),
|
||||
);
|
||||
const seen = new Set<string>();
|
||||
const stamped = codes.map((c) => {
|
||||
const check = byId.get(c.id);
|
||||
if (!check) return c;
|
||||
seen.add(c.id);
|
||||
return { ...c, confirmedDead: check.checked };
|
||||
});
|
||||
return {
|
||||
codes: stamped,
|
||||
unmatched: checks.filter(
|
||||
(c) => !seen.has(JSON.stringify([c.game, c.code])),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { GameId } from "./schema.ts";
|
||||
|
||||
/**
|
||||
* Codes the repository owner tried in game and found dead, while the source
|
||||
* still lists them as active (owner, 2026-10-20). The in-game check is the
|
||||
* evidence, as for a routine marked `Confirmed in game` (`routines.ts`): the
|
||||
* reader sees "Confirmed expired in game, YYYY-MM-DD." on the code.
|
||||
*
|
||||
* Only "dead" is recorded; there is no "still works". An entry whose code the
|
||||
* source has since dropped matches nothing and is logged by `build-codes`, so
|
||||
* prune it whenever convenient.
|
||||
*/
|
||||
export interface CodeCheck {
|
||||
game: GameId;
|
||||
/** Exactly as the source spells it. */
|
||||
code: string;
|
||||
/** The day it was tried and refused. */
|
||||
checked: string;
|
||||
}
|
||||
|
||||
export const CODE_CHECKS: readonly CodeCheck[] = [
|
||||
{ game: "genshin", code: "Y1INABQB6DEX", checked: "2026-10-20" },
|
||||
{ game: "genshin", code: "2BJ64QRZ7RT8", checked: "2026-10-20" },
|
||||
{ game: "genshin", code: "PLDXIB886Y1S", checked: "2026-10-20" },
|
||||
];
|
||||
@@ -12,6 +12,15 @@ export const RedeemCodeSchema = z.object({
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||
.nullable()
|
||||
.optional(),
|
||||
/**
|
||||
* The day the repository owner tried the code in game and it no longer
|
||||
* worked (`codeChecks.ts`). Only ever "dead": the source still supplies the
|
||||
* code, its reward and its link.
|
||||
*/
|
||||
confirmedDead: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||
.optional(),
|
||||
firstObservedAt: z.string().datetime(),
|
||||
sourceUrl: z.string().url(),
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user