feat: user following

This commit is contained in:
2025-06-30 18:09:40 +01:00
parent 7fa4454150
commit 40410b1412
7 changed files with 246 additions and 17 deletions

View File

@@ -1,10 +1,12 @@
// place files you want to import through the `$lib` alias in this folder.
type WLEDAPI = {
seg: Array<{ id: string, col: Array<[number, number, number]> }>
}
import { createTwitchClient } from "./twitch";
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 => {
// hex is not validated.
@@ -16,17 +18,25 @@ const buildReq = (hex: string): WLEDAPI => {
const b = hexValues & 255;
return {
seg: [{
id: "0",
col: [[r, g, b]],
}]
}
}
seg: [
{
id: "0",
col: [[r, g, b]],
},
],
};
};
export const createLedClient = (addr: string) => {
return {
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
View 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,
};
};

View File

@@ -1,7 +1,7 @@
import { createLedClient } from "$lib";
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 = {
async default({ request }) {
@@ -15,5 +15,5 @@ export const actions: Actions = {
// TODO: validate actual HEX
await client.setColor(color.toString());
}
}
},
};

View 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();
}
};