feat: user following
This commit is contained in:
34
biome.json
Normal file
34
biome.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": false,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": false
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "tab"
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": false,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "double"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assist": {
|
||||||
|
"enabled": true,
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"organizeImports": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
bun.lock
29
bun.lock
@ -3,11 +3,16 @@
|
|||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitchlight",
|
"name": "twitchlight",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.25.67",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "2.0.6",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@types/node": "^24.0.7",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
@ -19,6 +24,24 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||||
|
|
||||||
|
"@biomejs/biome": ["@biomejs/biome@2.0.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.0.6", "@biomejs/cli-darwin-x64": "2.0.6", "@biomejs/cli-linux-arm64": "2.0.6", "@biomejs/cli-linux-arm64-musl": "2.0.6", "@biomejs/cli-linux-x64": "2.0.6", "@biomejs/cli-linux-x64-musl": "2.0.6", "@biomejs/cli-win32-arm64": "2.0.6", "@biomejs/cli-win32-x64": "2.0.6" }, "bin": { "biome": "bin/biome" } }, "sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.0.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.0.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.0.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.0.6", "", { "os": "win32", "cpu": "x64" }, "sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
|
||||||
@ -165,6 +188,8 @@
|
|||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||||
@ -281,6 +306,8 @@
|
|||||||
|
|
||||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||||
|
|
||||||
"vitefu": ["vitefu@1.0.7", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-eRWXLBbJjW3X5z5P5IHcSm2yYbYRPb2kQuc+oqsbAl99WB5kVsPbiiox+cymo8twTzifA6itvhr2CmjnaZZp0Q=="],
|
"vitefu": ["vitefu@1.0.7", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-eRWXLBbJjW3X5z5P5IHcSm2yYbYRPb2kQuc+oqsbAl99WB5kVsPbiiox+cymo8twTzifA6itvhr2CmjnaZZp0Q=="],
|
||||||
@ -289,6 +316,8 @@
|
|||||||
|
|
||||||
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
|
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
|
||||||
|
|
||||||
|
"zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||||
|
@ -12,14 +12,19 @@
|
|||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "2.0.6",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@types/node": "^24.0.7",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^6.2.6"
|
"vite": "^6.2.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.25.67"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
|
|
||||||
type WLEDAPI = {
|
import { createTwitchClient } from "./twitch";
|
||||||
seg: Array<{ id: string, col: Array<[number, number, number]> }>
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAddr = (addr: string) => `http://${addr}/json/state`
|
type WLEDAPI = {
|
||||||
|
seg: Array<{ id: string; col: Array<[number, number, number]> }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAddr = (addr: string) => `http://${addr}/json/state`;
|
||||||
|
|
||||||
const buildReq = (hex: string): WLEDAPI => {
|
const buildReq = (hex: string): WLEDAPI => {
|
||||||
// hex is not validated.
|
// hex is not validated.
|
||||||
@ -16,17 +18,25 @@ const buildReq = (hex: string): WLEDAPI => {
|
|||||||
const b = hexValues & 255;
|
const b = hexValues & 255;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
seg: [{
|
seg: [
|
||||||
id: "0",
|
{
|
||||||
col: [[r, g, b]],
|
id: "0",
|
||||||
}]
|
col: [[r, g, b]],
|
||||||
}
|
},
|
||||||
}
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const createLedClient = (addr: string) => {
|
export const createLedClient = (addr: string) => {
|
||||||
return {
|
return {
|
||||||
async setColor(hex: string) {
|
async setColor(hex: string) {
|
||||||
await fetch(getAddr(addr), { method: "POST", body: JSON.stringify(buildReq(hex)) });
|
await fetch(getAddr(addr), {
|
||||||
}
|
method: "POST",
|
||||||
}
|
body: JSON.stringify(buildReq(hex)),
|
||||||
}
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const twitchClient = createTwitchClient();
|
||||||
|
// twitchClient.subscribeToFollow();
|
||||||
|
90
src/lib/twitch.ts
Normal file
90
src/lib/twitch.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { HASH_SECRET, TWITCH_APP_CLIENT_ID, TWITCH_APP_SECRET } from "$env/static/private";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const authBodyValidator = z.object({
|
||||||
|
access_token: z.string(),
|
||||||
|
expires_in: z.number(),
|
||||||
|
token_type: z.enum(["bearer"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
type TwitchSubscribeBody = {
|
||||||
|
type: "channel.follow";
|
||||||
|
version: 2;
|
||||||
|
transport: {
|
||||||
|
method: "webhook";
|
||||||
|
callback: string;
|
||||||
|
secret: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Not the complete event, but the information we need.
|
||||||
|
export const twitchFollowEvent = z.object({
|
||||||
|
subscription: z.object({
|
||||||
|
type: z.enum(['channel.follow']),
|
||||||
|
}),
|
||||||
|
event: z.object({
|
||||||
|
user_name: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
*curl -X POST 'https://api.twitch.tv/helix/eventsub/subscriptions' \
|
||||||
|
-H 'Authorization: Bearer 2gbdx6oar67tqtcmt49t3wpcgycthx' \
|
||||||
|
-H 'Client-Id: wbmytr93xzw8zbg0p1izqyzzc5mbiz' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"type":"channel.follow","version":"2","condition":{"broadcaster_user_id":"1234", "moderator_user_id": "1234"},"transport":{"method":"webhook","callback":"https://example.com/callback","secret":"s3cre77890ab"}}'
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Webhook -> Redeem of channel reward
|
||||||
|
export const createTwitchClient = () => {
|
||||||
|
const getAccessToken = async () => {
|
||||||
|
const twitchAuthResponse = await fetch(
|
||||||
|
`https://id.twitch.tv/oauth2/token?client_id=${TWITCH_APP_CLIENT_ID}&client_secret=${TWITCH_APP_SECRET}&grant_type=client_credentials`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { access_token } = authBodyValidator.parse(
|
||||||
|
await twitchAuthResponse.json(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return access_token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _access_token = getAccessToken();
|
||||||
|
|
||||||
|
const subscribeToFollow = async () => {
|
||||||
|
const twitchSubResponse = await fetch(
|
||||||
|
"https://api.twitch.tv/helix/eventsub/subscriptions",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${await _access_token}`,
|
||||||
|
"Client-Id": TWITCH_APP_CLIENT_ID,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: "channel.follow",
|
||||||
|
version: 2,
|
||||||
|
transport: {
|
||||||
|
callback: "http://localhost:5173/api/webhook",
|
||||||
|
method: "webhook",
|
||||||
|
secret: HASH_SECRET,
|
||||||
|
},
|
||||||
|
} satisfies TwitchSubscribeBody),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await twitchSubResponse.json();
|
||||||
|
|
||||||
|
console.log(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribeToFollow,
|
||||||
|
};
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { createLedClient } from "$lib";
|
import { createLedClient } from "$lib";
|
||||||
import { fail, type Actions } from "@sveltejs/kit";
|
import { fail, type Actions } from "@sveltejs/kit";
|
||||||
|
|
||||||
const client = createLedClient("192.168.1.215");
|
export const client = createLedClient("192.168.1.215");
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
async default({ request }) {
|
async default({ request }) {
|
||||||
@ -15,5 +15,5 @@ export const actions: Actions = {
|
|||||||
// TODO: validate actual HEX
|
// TODO: validate actual HEX
|
||||||
|
|
||||||
await client.setColor(color.toString());
|
await client.setColor(color.toString());
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
61
src/routes/api/webhook/+server.ts
Normal file
61
src/routes/api/webhook/+server.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||||
|
import type { RequestHandler } from "@sveltejs/kit";
|
||||||
|
import { HASH_SECRET } from "$env/static/private";
|
||||||
|
import { twitchFollowEvent } from "$lib/twitch";
|
||||||
|
import { create } from "node:domain";
|
||||||
|
import { createLedClient } from "$lib";
|
||||||
|
import { client } from "../../+page.server";
|
||||||
|
|
||||||
|
const getHmacMessage = (request: Request, body: string): string => {
|
||||||
|
return (
|
||||||
|
request.headers.get(TWITCH_MESSAGE_ID) +
|
||||||
|
request.headers.get(TWITCH_MESSAGE_TIMESTAMP) + body
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHmac = (secret: string, message: string): string => {
|
||||||
|
return createHmac("sha256", secret).update(message).digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyMessage = (hmac: string, verifySignature: string): boolean => {
|
||||||
|
return timingSafeEqual(Buffer.from(hmac), Buffer.from(verifySignature));
|
||||||
|
};
|
||||||
|
|
||||||
|
const HMAC_PREFIX = "sha256=";
|
||||||
|
|
||||||
|
const TWITCH_MESSAGE_ID = "Twitch-Eventsub-Message-Id".toLowerCase();
|
||||||
|
const TWITCH_MESSAGE_TIMESTAMP =
|
||||||
|
"Twitch-Eventsub-Message-Timestamp".toLowerCase();
|
||||||
|
const TWITCH_MESSAGE_SIGNATURE =
|
||||||
|
"Twitch-Eventsub-Message-Signature".toLowerCase();
|
||||||
|
|
||||||
|
export const fallback: RequestHandler = async ({ request }) => {
|
||||||
|
const body = await request.text();
|
||||||
|
|
||||||
|
const message = getHmacMessage(request, body);
|
||||||
|
const hmac = HMAC_PREFIX + getHmac(HASH_SECRET, message);
|
||||||
|
|
||||||
|
if (!verifyMessage(hmac, request.headers.get(TWITCH_MESSAGE_SIGNATURE)!)) {
|
||||||
|
console.log("Invalid request");
|
||||||
|
throw new Error("go away");
|
||||||
|
} else {
|
||||||
|
// Get JSON object from body, so you can process the message.
|
||||||
|
const jsonBody = JSON.parse(body);
|
||||||
|
|
||||||
|
console.log(jsonBody);
|
||||||
|
const event = twitchFollowEvent.safeParse(jsonBody);
|
||||||
|
if (!event.success) {
|
||||||
|
console.log("Not a follow event");
|
||||||
|
console.log(event.error);
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user_name } = event.data.event;
|
||||||
|
|
||||||
|
client.setColor("#FFFFFF");
|
||||||
|
|
||||||
|
// Handle notification...
|
||||||
|
console.log(`${user_name} has just subscribed`);
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
|
};
|
Reference in New Issue
Block a user