Skip to content

Commit e25c6d6

Browse files
authored
Refactor server.js for WebSocket and API integration
Refactor server.js to improve WebSocket handling and integrate StreamElements API.
1 parent 29ce036 commit e25c6d6

File tree

1 file changed

+89
-100
lines changed

1 file changed

+89
-100
lines changed

server.js

Lines changed: 89 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,114 @@
11
import express from "express";
2-
import fetch from "node-fetch";
3-
import tmi from "tmi.js";
42
import { WebSocketServer } from "ws";
5-
import path from "path";
3+
import WebSocket from "ws";
64

75
const app = express();
86
const PORT = 3000;
9-
const __dirname = process.cwd();
10-
11-
const activeChannels = {}; // { channel: { emotes:[], users:{}, emoteUsage:{} } }
127

8+
// Serve static HTML (index.html, CSS, JS)
139
app.use(express.static("public"));
1410

15-
// 🔹 Get 7TV emotes for a channel
16-
async function get7TVEmotes(channel) {
17-
try {
18-
const res = await fetch(`https://7tv.io/v3/users/twitch/${channel}`);
19-
const data = await res.json();
20-
return data.emote_set?.emotes?.map(e => ({
21-
name: e.name,
22-
id: e.id,
23-
url: `https://cdn.7tv.app/emote/${e.id}/4x.webp`
24-
})) || [];
25-
} catch (e) {
26-
console.log("7TV fetch failed:", e);
27-
return [];
28-
}
29-
}
30-
31-
// 🔹 Get Twitch user info
32-
async function getTwitchUser(username) {
33-
try {
34-
const res = await fetch(`https://7tv.io/v3/users/twitch/${username}`);
35-
if (!res.ok) return null;
36-
const data = await res.json();
37-
return {
38-
id: data.id,
39-
avatar: data.avatar_url || "https://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_70x70.png",
40-
paint: data.style?.paint_id || null
41-
};
42-
} catch {
43-
return null;
44-
}
11+
// Local memory
12+
let clients = [];
13+
let emoteUsage = {}; // { emoteName: count }
14+
let userUsage = {}; // { username: count }
15+
16+
// Create HTTP + WebSocket server
17+
const server = app.listen(PORT, () =>
18+
console.log(`💜 Server running at http://localhost:${PORT}`)
19+
);
20+
const wss = new WebSocketServer({ server });
21+
22+
// Broadcast data to all connected clients
23+
function broadcast(data) {
24+
const message = JSON.stringify(data);
25+
clients.forEach((ws) => {
26+
if (ws.readyState === ws.OPEN) ws.send(message);
27+
});
4528
}
4629

47-
// 🔹 Connect and track emotes in a channel
48-
async function startTracking(channel) {
49-
if (activeChannels[channel]) return activeChannels[channel];
50-
console.log(`Starting tracking for ${channel}`);
51-
52-
const emotes = await get7TVEmotes(channel);
53-
const users = {};
54-
const emoteUsage = {};
30+
// Handle client dashboard connections
31+
wss.on("connection", (ws) => {
32+
clients.push(ws);
33+
console.log("🟣 New dashboard connected");
34+
ws.send(JSON.stringify({ type: "init", emoteUsage, userUsage }));
5535

56-
const client = new tmi.Client({
57-
connection: { reconnect: true },
58-
channels: [channel]
36+
ws.on("close", () => {
37+
clients = clients.filter((c) => c !== ws);
5938
});
39+
});
6040

61-
client.connect();
62-
63-
client.on("message", async (ch, tags, msg, self) => {
64-
if (self) return;
65-
const username = tags["display-name"] || tags.username;
66-
67-
if (!users[username]) {
68-
const info = await getTwitchUser(username);
69-
users[username] = {
70-
username,
71-
id: info?.id || username,
72-
avatar: info?.avatar,
73-
paint: info?.paint,
74-
count: 0
75-
};
76-
}
41+
// 🔹 Connect to StreamElements WebSocket
42+
const seSocket = new WebSocket("wss://realtime.streamelements.com/socket");
7743

78-
const words = msg.split(/\s+/);
79-
const used = emotes.filter(e => words.includes(e.name));
44+
// When StreamElements connects
45+
seSocket.on("open", () => {
46+
console.log("💫 Connected to StreamElements Realtime API");
47+
});
8048

81-
if (used.length > 0) {
82-
for (const e of used) {
83-
users[username].count++;
84-
emoteUsage[e.name] = (emoteUsage[e.name] || 0) + 1;
85-
}
86-
broadcast(channel, { type: "update", user: users[username], emoteUsage });
87-
}
88-
});
49+
// Handle StreamElements messages
50+
seSocket.on("message", (msg) => {
51+
try {
52+
const data = JSON.parse(msg);
8953

90-
activeChannels[channel] = { emotes, users, emoteUsage, client };
91-
return activeChannels[channel];
92-
}
54+
// Welcome event
55+
if (data.type === "welcome") {
56+
console.log("✅ Connected to StreamElements Realtime API (Session:", data.payload?.id, ")");
57+
return;
58+
}
9359

94-
// 🔹 WebSocket setup
95-
const wss = new WebSocketServer({ noServer: true });
96-
const sockets = new Set();
60+
// Ping-Pong keepalive
61+
if (data.type === "ping") {
62+
seSocket.send(JSON.stringify({ type: "pong" }));
63+
return;
64+
}
9765

98-
function broadcast(channel, data) {
99-
const msg = JSON.stringify({ channel, ...data });
100-
for (const ws of sockets) ws.send(msg);
101-
}
66+
// Handle chat messages
67+
if (data.type === "event" && data.event?.type === "message") {
68+
const message = data.event.data;
69+
const username = message.nick || "UnknownUser";
70+
const emotes = message.emotes || [];
71+
72+
// Count message as 1 for user
73+
userUsage[username] = (userUsage[username] || 0) + 1;
74+
75+
// Count emote usage
76+
emotes.forEach((em) => {
77+
const name = em.text || em.name || "unknown";
78+
emoteUsage[name] = (emoteUsage[name] || 0) + 1;
79+
});
80+
81+
// Broadcast updates
82+
broadcast({
83+
type: "update",
84+
userUsage,
85+
emoteUsage,
86+
});
87+
}
88+
} catch (err) {
89+
console.error("Error handling StreamElements message:", err);
90+
}
91+
});
10292

103-
const server = app.listen(PORT, () => {
104-
console.log(`✅ Running at http://localhost:${PORT}`);
93+
seSocket.on("close", () => {
94+
console.log("⚠️ Disconnected from StreamElements Realtime API. Reconnecting in 5s...");
95+
setTimeout(() => reconnectSE(), 5000);
10596
});
10697

107-
server.on("upgrade", (req, socket, head) => {
108-
wss.handleUpgrade(req, socket, head, ws => {
109-
sockets.add(ws);
110-
ws.on("close", () => sockets.delete(ws));
111-
});
98+
seSocket.on("error", (err) => {
99+
console.error("❌ StreamElements socket error:", err.message);
112100
});
113101

114-
// 🔹 API Routes
115-
app.get("/api/channel/:channel", async (req, res) => {
116-
const channel = req.params.channel.toLowerCase();
117-
const tracker = await startTracking(channel);
118-
res.json({
119-
emotes: tracker.emotes,
120-
users: Object.values(tracker.users),
121-
emoteUsage: tracker.emoteUsage
102+
// Auto-reconnect function
103+
function reconnectSE() {
104+
const newSocket = new WebSocket("wss://realtime.streamelements.com/socket");
105+
newSocket.on("open", () => {
106+
console.log("🔄 Reconnected to StreamElements API");
107+
seSocket = newSocket;
122108
});
123-
});
109+
}
124110

125-
app.use(express.static(path.join(__dirname, "public")));
111+
// Express API for debugging
112+
app.get("/api/data", (req, res) => {
113+
res.json({ users: userUsage, emotes: emoteUsage });
114+
});

0 commit comments

Comments
 (0)