This commit is contained in:
2026-04-23 04:04:31 +02:00
parent 3cc89177f6
commit 30262c3b8d
417 changed files with 61615 additions and 33 deletions

View File

@@ -1,6 +1,8 @@
require("dotenv").config();
module.exports = {
clientId: process.env.CLIENT_ID,
token: process.env.APP_ACCESS_TOKEN,
clientSecret: process.env.CLIENT_SECRET,
broadcasterId: process.env.BROADCASTER_ID,
rewards: {

View File

@@ -1,3 +1,18 @@
const { startAuthServer } = require("./twitch/authServer");
const { connect } = require("./twitch/websocket");
connect();
const auth = startAuthServer();
// wait until token exists before starting twitch
const waitForToken = setInterval(() => {
const token = auth.getToken();
if (token) {
clearInterval(waitForToken);
process.env.APP_ACCESS_TOKEN = token;
console.log("🚀 Starting Twitch connection...");
connect();
}
}, 1000);

35
src/twitch/auth.js Normal file
View File

@@ -0,0 +1,35 @@
const axios = require("axios");
const config = require("../config");
let tokenCache = null;
let expiresAt = 0;
async function getAppToken() {
const now = Date.now();
// reuse token if still valid
if (tokenCache && now < expiresAt) {
return tokenCache;
}
const res = await axios.post(
"https://id.twitch.tv/oauth2/token",
null,
{
params: {
client_id: config.clientId,
client_secret: config.clientSecret,
grant_type: "client_credentials",
},
}
);
tokenCache = res.data.access_token;
expiresAt = now + res.data.expires_in * 1000 - 60000; // refresh 1 min early
console.log("🔐 New Twitch App Token generated");
return tokenCache;
}
module.exports = { getAppToken };

65
src/twitch/authServer.js Normal file
View File

@@ -0,0 +1,65 @@
const http = require("http");
const axios = require("axios");
const config = require("../config");
let accessToken = null;
function startAuthServer() {
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost:3000");
if (url.pathname === "/callback") {
const code = url.searchParams.get("code");
if (!code) {
res.end("Missing code");
return;
}
try {
const tokenRes = await axios.post(
"https://id.twitch.tv/oauth2/token",
null,
{
params: {
client_id: config.clientId,
client_secret: config.clientSecret,
code,
grant_type: "authorization_code",
redirect_uri: "http://localhost:3000/callback",
},
}
);
accessToken = tokenRes.data.access_token;
console.log("🔐 Twitch user token acquired!");
res.end("Auth successful! You can close this tab.");
} catch (err) {
console.error(err.response?.data || err.message);
res.end("Auth failed");
}
}
});
server.listen(3000, () => {
console.log("\n👉 Open this URL to login:\n");
const url =
`https://id.twitch.tv/oauth2/authorize` +
`?client_id=${config.clientId}` +
`&redirect_uri=http://localhost:3000/callback` +
`&response_type=code` +
`&scope=channel:read:redemptions`;
console.log(url + "\n");
});
return {
getToken: () => accessToken,
};
}
module.exports = { startAuthServer };

View File

@@ -8,16 +8,18 @@ const seen = new Set();
function handleEvent(payload) {
const { subscription, event } = payload;
if (subscription.type !== "channel.channel_points_custom_reward_redemption.add") {
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);
setTimeout(() => seen.delete(event.id), 60000);
console.log(`${event.user_name} redeemed ${event.reward.title}`);
console.log(`🎁 ${event.user_name} ${event.reward.title}`);
switch (event.reward.id) {
case config.rewards.HYDRATE:
@@ -29,7 +31,7 @@ function handleEvent(payload) {
break;
default:
console.log("No handler for reward:", event.reward.id);
console.log("Unknown reward:", event.reward.id);
}
}

View File

@@ -1,9 +1,12 @@
const axios = require("axios");
const config = require("../config");
const { getAppToken } = require("./auth");
async function createSubscription(sessionId) {
try {
const res = await axios.post(
const token = await getAppToken();
await axios.post(
"https://api.twitch.tv/helix/eventsub/subscriptions",
{
type: "channel.channel_points_custom_reward_redemption.add",
@@ -19,15 +22,16 @@ async function createSubscription(sessionId) {
{
headers: {
"Client-ID": config.clientId,
Authorization: `Bearer ${config.token}`,
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}
);
console.log("Subscription created:", res.data);
console.log("📡 Subscription created");
} catch (err) {
console.error(
"Subscription error:",
"Subscription error:",
err.response?.data || err.message
);
}

View File

@@ -8,43 +8,44 @@ function connect(url = reconnectUrl) {
const ws = new WebSocket(url);
ws.on("open", () => {
console.log("Connected to Twitch EventSub");
console.log("🔌 Connected to Twitch EventSub");
});
ws.on("message", async (data) => {
const message = JSON.parse(data);
const { metadata, payload } = message;
ws.on("message", async (raw) => {
const msg = JSON.parse(raw);
const { metadata, payload } = msg;
switch (metadata.message_type) {
case "session_welcome":
console.log("Session started:", payload.session.id);
case "session_welcome": {
const sessionId = payload.session.id;
reconnectUrl = payload.session.reconnect_url;
await createSubscription(payload.session.id);
console.log("🟢 Session:", sessionId);
reconnectUrl =
payload.session.reconnect_url ||
"wss://eventsub.wss.twitch.tv/ws";
await createSubscription(sessionId);
break;
}
case "notification":
handleEvent(payload);
break;
case "session_reconnect":
console.log("♻️ Reconnecting...");
ws.close();
connect(payload?.session?.reconnect_url || reconnectUrl);
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...");
console.log("Disconnected. Reconnecting...");
setTimeout(() => connect(), 3000);
});