diff --git a/src/gateway.ts b/src/gateway.ts index 2221a16..72cc395 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -224,7 +224,13 @@ export class GatewayManager implements DurableObject { token: this.env.DISCORD_BOT_TOKEN, intents: INTENTS, properties: { os: "linux", browser: "dough-restful", device: "dough-restful" }, - presence: { status: "invisible", activities: [], since: 0, afk: false }, + presence: { + status: "idle", + afk: false, + since: 0, + // Custom status (type 4): the text shown is the `state` field. + activities: [{ name: "Custom Status", type: 4, state: "meow meow mrrp meow" }], + }, }, }); } diff --git a/src/index.ts b/src/index.ts index 59cfebb..016d333 100644 --- a/src/index.ts +++ b/src/index.ts @@ -91,7 +91,7 @@ export default { } if (sub === "profile") { - const profile = await getProfile(env, id); + const profile = await getProfile(env, id, ctx); if (!profile) { return json({ success: false, error: { code: "not_found", message: "User not found." } }, 404); } @@ -99,7 +99,7 @@ export default { } // Unified record: profile (REST) + presence (gateway), in parallel. - const [profile, presence] = await Promise.all([getProfile(env, id), fetchPresence(env, id)]); + const [profile, presence] = await Promise.all([getProfile(env, id, ctx), fetchPresence(env, id)]); if (!profile) { return json({ success: false, error: { code: "not_found", message: "User not found." } }, 404); } diff --git a/src/profile.ts b/src/profile.ts index cadd901..2d9b9b4 100644 --- a/src/profile.ts +++ b/src/profile.ts @@ -84,18 +84,43 @@ function cacheKey(id: string): string { return `profile:${id}`; } -/** Build profile from Discord, with a KV read-through cache. */ -export async function getProfile(env: Env, id: string): Promise { +/** + * Build profile from Discord — LIVE-FIRST. + * + * Always fetches fresh from Discord so profile/badges/connections are current, + * and only falls back to the KV copy if the REST call fails (e.g. Discord is + * rate-limiting us). The KV copy is refreshed in the background, throttled to + * at most one write per TTL window so we don't blow KV write limits. + */ +export async function getProfile( + env: Env, + id: string, + ctx?: ExecutionContext +): Promise { + const fresh = await buildFreshProfile(env, id); + if (fresh) { + const refresh = maybeRefreshCache(env, id, fresh); + if (ctx) ctx.waitUntil(refresh); + else await refresh; + return fresh; + } + + // Discord REST failed — serve last-known-good from KV if we have it. const cached = await env.PROFILE_CACHE.get(cacheKey(id), "json"); if (cached) { const c = cached as Omit; return { ...c, source: "cache" }; } + return null; +} - const result = await buildFreshProfile(env, id); - if (!result) return null; - +/** Write the fallback copy to KV, but at most once per TTL window. */ +async function maybeRefreshCache(env: Env, id: string, result: ProfileResult): Promise { const ttl = Math.max(60, Number(env.PROFILE_CACHE_TTL_SECONDS || "300")); + const { metadata } = await env.PROFILE_CACHE.getWithMetadata(cacheKey(id)); + const lastWrite = (metadata as { t?: number } | null)?.t ?? 0; + if (Date.now() - lastWrite < ttl * 1000) return; // throttled + await env.PROFILE_CACHE.put( cacheKey(id), JSON.stringify({ @@ -103,9 +128,8 @@ export async function getProfile(env: Env, id: string): Promise {