first commit

This commit is contained in:
2026-04-23 03:13:52 +02:00
commit f589dc0bcf
11 changed files with 197 additions and 0 deletions

0
README.md Normal file
View File

23
docker-compose.yaml Normal file
View File

@@ -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

13
package.json Normal file
View File

@@ -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"
}
}

10
src/config.js Normal file
View File

@@ -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,
},
};

3
src/index.js Normal file
View File

@@ -0,0 +1,3 @@
const { connect } = require("./twitch/websocket");
connect();

3
src/rewards/hydrate.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = (event) => {
console.log("💧 Hydrate triggered by", event.user_name);
};

5
src/rewards/sound.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = (event) => {
console.log("🔊 Sound triggered by", event.user_name);
// later: play sound, trigger OBS, etc.
};

36
src/twitch/handler.js Normal file
View File

@@ -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 };

View File

@@ -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 };

56
src/twitch/websocket.js Normal file
View File

@@ -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 };

12
temp.txt Normal file
View File

@@ -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"
}