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