feat: implemented a basic store and emulatorjs

This commit is contained in:
Simeon Radivoev 2026-03-14 02:15:57 +02:00
parent 2f32cbc730
commit 7286541822
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
121 changed files with 5900 additions and 1092 deletions

View file

@ -0,0 +1,110 @@
import { IJob, JobContext } from "../task-queue";
import secrets from "../secrets";
import open from "open";
import z from "zod";
import { delay } from "@/shared/utils";
interface TwitchDevice
{
device_code: string,
expires_in: number,
expires_at: Date,
started_at: Date,
interval: number,
user_code: string,
verification_uri: string;
}
export default class TwitchLoginJob implements IJob
{
twitchScopes = "analytics:read:extensions analytics:read:games user:read:email";
device?: TwitchDevice;
clientId: string;
openInBrowser: boolean;
static id = 'twitch-login-job' as const;
static dataSchema = z.object({ expires_at: z.date(), started_at: z.date(), url: z.url(), user_code: z.string() }).or(z.undefined());
constructor(clientId: string, openInBrowser: boolean)
{
this.clientId = clientId;
this.openInBrowser = openInBrowser;
}
exposeData = (): z.infer<typeof TwitchLoginJob.dataSchema> => this.device ? ({
expires_at: this.device.expires_at,
started_at: this.device.started_at,
url: this.device.verification_uri,
user_code: this.device.user_code
}) : undefined;
async start (context: JobContext): Promise<any>
{
context.setProgress(0, "Retrieving Device");
let res = await fetch("https://id.twitch.tv/oauth2/device", {
method: "POST",
body: new URLSearchParams({
client_id: this.clientId,
scopes: this.twitchScopes
}),
signal: context.abortSignal
});
const device: TwitchDevice = await res.json();
const expiredTimeout = setTimeout(() => context.abort('expired'), device.expires_in * 1000);
device.expires_at = new Date(new Date().getTime() + device.expires_in * 1000);
device.started_at = new Date();
this.device = device;
try
{
if (this.openInBrowser)
open(device.verification_uri);
this.device = device;
context.setProgress(50, "Waiting For Authentication");
while (true)
{
if (context.abortSignal.aborted) break;
await delay(device.interval * 1000, context.abortSignal);
res = await fetch("https://id.twitch.tv/oauth2/token", {
method: "POST",
body: new URLSearchParams({
client_id: this.clientId,
scopes: this.twitchScopes,
device_code: this.device.device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
}),
signal: context.abortSignal
});
if (res.status === 200)
{
const data: {
access_token: string,
expires_in: number,
refresh_token: string,
scope: string[],
token_type: string;
} = await res.json();
secrets.set({ service: 'gamflow_twitch', name: 'access_token', value: data.access_token });
secrets.set({ service: 'gamflow_twitch', name: 'refresh_token', value: data.refresh_token });
secrets.set({ service: 'gamflow_twitch', name: 'expires_in', value: new Date(new Date().getTime() + data.expires_in).toString() });
break;
}
else if (res.status !== 400)
{
console.error(res.statusText);
break;
}
}
} finally
{
clearTimeout(expiredTimeout);
}
}
}