From f589dc0bcf2974aa773e7a2a0146f27031f9ddbc Mon Sep 17 00:00:00 2001 From: Reaper Date: Thu, 23 Apr 2026 03:13:52 +0200 Subject: [PATCH] first commit --- README.md | 0 docker-compose.yaml | 23 +++++++++++++++ package.json | 13 +++++++++ src/config.js | 10 +++++++ src/index.js | 3 ++ src/rewards/hydrate.js | 3 ++ src/rewards/sound.js | 5 ++++ src/twitch/handler.js | 36 ++++++++++++++++++++++++ src/twitch/subscriptions.js | 36 ++++++++++++++++++++++++ src/twitch/websocket.js | 56 +++++++++++++++++++++++++++++++++++++ temp.txt | 12 ++++++++ 11 files changed, 197 insertions(+) create mode 100644 README.md create mode 100644 docker-compose.yaml create mode 100644 package.json create mode 100644 src/config.js create mode 100644 src/index.js create mode 100644 src/rewards/hydrate.js create mode 100644 src/rewards/sound.js create mode 100644 src/twitch/handler.js create mode 100644 src/twitch/subscriptions.js create mode 100644 src/twitch/websocket.js create mode 100644 temp.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..3c9fc21 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,23 @@ +name: TwitchEventSub + +services: + twitch-eventsub: + image: node:24-alpine + working_dir: /app + + environment: + CLIENT_ID: 1d55t6jkkl2l0triq2irmog3lo3ver + APP_ACCESS_TOKEN: 7k8q1pvj3bqclkh1qxm9bdcmmb3w05 + BROADCASTER_ID: "86025329" + + REWARD_HYDRATE_ID: "abc123" + REWARD_SOUND_ID: "def456" + + volumes: + - .:/app + - /app/node_modules + + command: > + sh -c "npm install --omit=dev && node main.js" + + restart: unless-stopped \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0de3b71 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "twitch-eventsub", + "version": "1.0.0", + "main": "src/index.js", + "type": "commonjs", + "scripts": { + "start": "node src/index.js" + }, + "dependencies": { + "axios": "^1.6.0", + "ws": "^8.14.2" + } +} \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..d9bda57 --- /dev/null +++ b/src/config.js @@ -0,0 +1,10 @@ +module.exports = { + clientId: process.env.CLIENT_ID, + token: process.env.APP_ACCESS_TOKEN, + broadcasterId: process.env.BROADCASTER_ID, + + rewards: { + HYDRATE: process.env.REWARD_HYDRATE_ID, + SOUND: process.env.REWARD_SOUND_ID, + }, +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..533c85f --- /dev/null +++ b/src/index.js @@ -0,0 +1,3 @@ +const { connect } = require("./twitch/websocket"); + +connect(); \ No newline at end of file diff --git a/src/rewards/hydrate.js b/src/rewards/hydrate.js new file mode 100644 index 0000000..4541154 --- /dev/null +++ b/src/rewards/hydrate.js @@ -0,0 +1,3 @@ +module.exports = (event) => { + console.log("💧 Hydrate triggered by", event.user_name); +}; \ No newline at end of file diff --git a/src/rewards/sound.js b/src/rewards/sound.js new file mode 100644 index 0000000..324da4b --- /dev/null +++ b/src/rewards/sound.js @@ -0,0 +1,5 @@ +module.exports = (event) => { + console.log("🔊 Sound triggered by", event.user_name); + + // later: play sound, trigger OBS, etc. +}; \ No newline at end of file diff --git a/src/twitch/handler.js b/src/twitch/handler.js new file mode 100644 index 0000000..cd62a89 --- /dev/null +++ b/src/twitch/handler.js @@ -0,0 +1,36 @@ +const config = require("../config"); + +const hydrate = require("../rewards/hydrate"); +const sound = require("../rewards/sound"); + +const seen = new Set(); + +function handleEvent(payload) { + const { subscription, event } = payload; + + if (subscription.type !== "channel.channel_points_custom_reward_redemption.add") { + return; + } + + // Deduplication + if (seen.has(event.id)) return; + seen.add(event.id); + setTimeout(() => seen.delete(event.id), 60_000); + + console.log(`${event.user_name} redeemed ${event.reward.title}`); + + switch (event.reward.id) { + case config.rewards.HYDRATE: + hydrate(event); + break; + + case config.rewards.SOUND: + sound(event); + break; + + default: + console.log("No handler for reward:", event.reward.id); + } +} + +module.exports = { handleEvent }; \ No newline at end of file diff --git a/src/twitch/subscriptions.js b/src/twitch/subscriptions.js new file mode 100644 index 0000000..4048473 --- /dev/null +++ b/src/twitch/subscriptions.js @@ -0,0 +1,36 @@ +const axios = require("axios"); +const config = require("../config"); + +async function createSubscription(sessionId) { + try { + const res = await axios.post( + "https://api.twitch.tv/helix/eventsub/subscriptions", + { + type: "channel.channel_points_custom_reward_redemption.add", + version: "1", + condition: { + broadcaster_user_id: config.broadcasterId, + }, + transport: { + method: "websocket", + session_id: sessionId, + }, + }, + { + headers: { + "Client-ID": config.clientId, + Authorization: `Bearer ${config.token}`, + }, + } + ); + + console.log("Subscription created:", res.data); + } catch (err) { + console.error( + "Subscription error:", + err.response?.data || err.message + ); + } +} + +module.exports = { createSubscription }; \ No newline at end of file diff --git a/src/twitch/websocket.js b/src/twitch/websocket.js new file mode 100644 index 0000000..fb6c152 --- /dev/null +++ b/src/twitch/websocket.js @@ -0,0 +1,56 @@ +const WebSocket = require("ws"); +const { createSubscription } = require("./subscriptions"); +const { handleEvent } = require("./handler"); + +let reconnectUrl = "wss://eventsub.wss.twitch.tv/ws"; + +function connect(url = reconnectUrl) { + const ws = new WebSocket(url); + + ws.on("open", () => { + console.log("Connected to Twitch EventSub"); + }); + + ws.on("message", async (data) => { + const message = JSON.parse(data); + const { metadata, payload } = message; + + switch (metadata.message_type) { + case "session_welcome": + console.log("Session started:", payload.session.id); + + reconnectUrl = payload.session.reconnect_url; + await createSubscription(payload.session.id); + break; + + case "notification": + handleEvent(payload); + break; + + case "session_keepalive": + break; + + case "session_reconnect": + console.log("Reconnect requested"); + + ws.close(); + reconnectUrl = payload.session.reconnect_url; + connect(reconnectUrl); + break; + + default: + console.log("Unhandled message:", message); + } + }); + + ws.on("close", () => { + console.log("Disconnected. Reconnecting in 3s..."); + setTimeout(() => connect(), 3000); + }); + + ws.on("error", (err) => { + console.error("WebSocket error:", err.message); + }); +} + +module.exports = { connect }; \ No newline at end of file diff --git a/temp.txt b/temp.txt new file mode 100644 index 0000000..9c4d5e7 --- /dev/null +++ b/temp.txt @@ -0,0 +1,12 @@ +curl -X POST "https://id.twitch.tv/oauth2/token" \ +-d "client_id=1d55t6jkkl2l0triq2irmog3lo3ver" \ +-d "client_secret=ekas9hxc1jmhk1mh7dvw7fw7vixbxw" \ +-d "grant_type=client_credentials" + + + +{ + "access_token":"7k8q1pvj3bqclkh1qxm9bdcmmb3w05", + "expires_in":4915881, + "token_type":"bearer" +} \ No newline at end of file