edit
This commit is contained in:
@@ -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: {
|
||||
|
||||
17
src/index.js
17
src/index.js
@@ -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
35
src/twitch/auth.js
Normal 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
65
src/twitch/authServer.js
Normal 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 };
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user