first commit
This commit is contained in:
23
docker-compose.yaml
Normal file
23
docker-compose.yaml
Normal 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
13
package.json
Normal 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
10
src/config.js
Normal 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
3
src/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const { connect } = require("./twitch/websocket");
|
||||
|
||||
connect();
|
||||
3
src/rewards/hydrate.js
Normal file
3
src/rewards/hydrate.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = (event) => {
|
||||
console.log("💧 Hydrate triggered by", event.user_name);
|
||||
};
|
||||
5
src/rewards/sound.js
Normal file
5
src/rewards/sound.js
Normal 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
36
src/twitch/handler.js
Normal 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 };
|
||||
36
src/twitch/subscriptions.js
Normal file
36
src/twitch/subscriptions.js
Normal 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
56
src/twitch/websocket.js
Normal 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
12
temp.txt
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user