Twitch Stream Notifications Bot
Twitch Stream Notifications Bot
I denne guiden skal vi bygge en komplett Discord bot som automatisk sender meldinger når Twitch-streamere går live. Boten vil kunne overvåke flere streamere samtidig på tvers av flere Discord-servere og sende pene embeds med live-informasjon.
Hva du lærer
- Koble sammen Discord og Twitch APIer
- Jobbe med MySQL database for å lagre konfigurasjoner
- Bygge et system som sjekker live-status regelmessig
- Lage pene Discord embeds med knapper
- Håndtere flere servere samtidig med samme bot
Oversikt over systemet
Vår Twitch bot består av følgende hovedkomponenter:
- Hovedbot-fil (
bot.js) - Håndterer Discord-tilkobling og kommandoer - Twitch-modul (
twitch.js) - Kommuniserer med Twitch API - Database-tabeller - Lagrer streamer-konfigurasjoner og aktive streams
- Kommandosystem - Lar administratorer legge til/fjerne streamere
Del 1: Prosjektoppsett
Lag ny mappe og installer pakker
Åpne VS Code og lag en ny mappe for prosjektet:
mkdir twitch-discord-bot
cd twitch-discord-bot
Opprett package.json filen:
{
"name": "twitch-discord-bot",
"version": "2.0.0",
"type": "commonjs",
"main": "bot.js",
"scripts": {
"start": "node bot.js"
},
"dependencies": {
"discord.js": "^14.15.3",
"dotenv": "^16.4.5",
"mysql2": "^3.10.0"
}
}
Installer pakkene:
npm install
Forklaring av pakker:
discord.js- Discord API bibliotekdotenv- For å laste miljøvariabler fra .env filmysql2- MySQL database tilkobling med Promise støtte
Opprett .env fil
Lag en .env fil for å lagre API-nøkler og database-info:
DISCORD_TOKEN=
TWITCH_CLIENT_ID=
TWITCH_CLIENT_SECRET=
MYSQL_HOST=
MYSQL_PORT=3306
MYSQL_USER=
MYSQL_PASSWORD=
MYSQL_DATABASE=
Del 2: Skaff API-tilganger
Discord Bot Token
- Gå til Discord Developer Portal
- Lag en ny applikasjon
- Gå til "Bot" seksjonen
- Kopier token og lim inn i
.envfilen
Twitch API-nøkler
- Gå til Twitch Developer Console
- Lag en ny applikasjon
- Kopier Client ID og Client Secret
- Lim inn begge i
.envfilen
Viktigt: Twitch bruker OAuth2 for autentisering, så vi trenger begge nøklene.
MySQL Database
Du trenger en MySQL database. Dette kan være:
- Lokal database (XAMPP/MAMP)
- Cloud database (PlanetScale, AWS RDS, etc.)
- VPS med MySQL installert
Fyll inn tilkoblingsdetaljene i .env filen.
Del 3: Database-struktur
Opprett database.sql fil med tabellene vi trenger:
CREATE TABLE IF NOT EXISTS guild_streamers (
id INT AUTO_INCREMENT PRIMARY KEY,
guild_id VARCHAR(32) NOT NULL,
channel_id VARCHAR(32) NOT NULL,
streamer_login VARCHAR(100) NOT NULL,
UNIQUE KEY uniq_cfg (guild_id, streamer_login)
);
CREATE TABLE IF NOT EXISTS live_streams (
id INT AUTO_INCREMENT PRIMARY KEY,
guild_id VARCHAR(32) NOT NULL,
channel_id VARCHAR(32) NOT NULL,
message_id VARCHAR(32) NOT NULL,
streamer_login VARCHAR(100) NOT NULL,
last_title VARCHAR(255) DEFAULT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uniq_guild_streamer (guild_id, streamer_login)
);
Forklaring av tabeller:
guild_streamers tabell
guild_id- Hvilken Discord serverchannel_id- Hvilken kanal meldinger sendes tilstreamer_login- Twitch brukernavnet til streamerenUNIQUE KEY- Forhindrer duplikater av samme streamer per server
live_streams tabell
message_id- ID til Discord-meldingen som viser live-statuslast_title- Siste stream-tittel (for å oppdage endringer)updated_at- Når meldingen sist ble oppdatertUNIQUE KEY- Én aktiv stream per streamer per server
Kjør denne SQL-koden i din MySQL database for å opprette tabellene.
Del 4: Hovedbot-fil (bot.js)
Opprett bot.js som er hjerte av vår bot:
require("dotenv").config();
const {
Client,
GatewayIntentBits,
Partials,
PermissionsBitField,
} = require("discord.js");
const mysql = require("mysql2/promise");
const checkTwitch = require("./twitch");
// Feilhåndtering
process.on("unhandledRejection", (e) => console.error("Unhandled rejection:", e));
process.on("uncaughtException", (e) => console.error("Uncaught exception:", e));
require("events").defaultMaxListeners = 50;
// Discord client oppsett
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Channel, Partials.Message, Partials.GuildMember],
});
let db;
Forklaring av koden:
require("dotenv").config()- Laster .env filenGatewayIntentBits- Definerer hvilke Discord-events boten skal lytte tilPartials- Lar boten håndtere ufullstendige objekterPermissionsBitField- For å sjekke brukerrettighetermysql2/promise- Promise-basert MySQL tilkobling- Error handlers - Fanger opp uventet feil
Database initialisering
async function initDB() {
db = await mysql.createPool({
host: process.env.MYSQL_HOST,
port: process.env.MYSQL_PORT ? Number(process.env.MYSQL_PORT) : 3306,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
waitForConnections: true,
connectionLimit: 10,
});
// Test tilkobling
await db.query("SELECT 1");
// Opprett tabeller hvis de ikke eksisterer
await db.query(
`CREATE TABLE IF NOT EXISTS guild_streamers(
id INT AUTO_INCREMENT PRIMARY KEY,
guild_id VARCHAR(32) NOT NULL,
channel_id VARCHAR(32) NOT NULL,
streamer_login VARCHAR(100) NOT NULL,
UNIQUE KEY uniq_cfg (guild_id, streamer_login)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
);
await db.query(
`CREATE TABLE IF NOT EXISTS live_streams(
id INT AUTO_INCREMENT PRIMARY KEY,
guild_id VARCHAR(32) NOT NULL,
channel_id VARCHAR(32) NOT NULL,
message_id VARCHAR(32) NOT NULL,
streamer_login VARCHAR(100) NOT NULL,
last_title VARCHAR(255) DEFAULT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uniq_guild_streamer (guild_id, streamer_login)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
);
console.log(
`✅ MySQL tilkoblet: ${process.env.MYSQL_HOST}:${process.env.MYSQL_PORT || 3306}`
);
}
Forklaring:
createPool()- Lager en pool av tilkoblinger for bedre ytelseconnectionLimit: 10- Maksimalt 10 samtidige tilkoblingerENGINE=InnoDB- Bruker InnoDB for ACID-compliance og foreign keysutf8mb4- Støtter alle Unicode-tegn (inkludert emojis)
Bot ready event
client.once("clientReady", () => {
console.log(`✅ Logget inn som ${client.user.tag}`);
// Start periodisk Twitch-sjekk hver 60. sekund
setInterval(async () => {
try {
await checkTwitch(client, db);
} catch (e) {
console.error("Feil i periodisk Twitch-sjekk:", e);
}
}, 60 * 1000);
});
Forklaring:
clientReady- Kjøres når boten er logget inn og klarsetInterval()- Kjører Twitch-sjekk hver 60. sekund- Try-catch - Forhindrer at feilen stopper hele boten
Del 5: Kommandosystem
La oss bygge kommandosystemet som lar administratorer styre boten:
client.on("messageCreate", async (msg) => {
// Ignorer meldinger fra andre bots og DMs
if (!msg.guild || msg.author.bot) return;
// Kun !twitch kommandoer
if (!msg.content.startsWith("!twitch")) return;
// Sjekk administrator-rettigheter
const isAdmin = msg.member.permissions.has(
PermissionsBitField.Flags.Administrator
);
// Del opp kommandoen i deler
const parts = msg.content.trim().split(/\s+/);
const sub = parts[1]?.toLowerCase(); // add/remove/list
const arg1 = parts[2]; // streamer navn
// Hjelpefunksjon for admin-sjekk
const requireAdmin = () => {
if (!isAdmin) {
msg.reply("Du må ha **Administrator** for å bruke denne kommandoen.");
return true;
}
return false;
};
// Rens streamer-navn
const sanitizeLogin = (s) =>
String(s || "")
.trim()
.toLowerCase()
.replace(/^@/, "") // Fjern @ i starten
.replace(/[^a-z0-9_]/g, ""); // Kun tillatte tegn
Forklaring:
messageCreate- Kjøres for hver melding i serverenmsg.guild- Sjekker at det ikke er en DMmsg.author.bot- Ignorerer andre botssplit(/\s+/)- Deler opp meldingen på whitespacesanitizeLogin()- Renser Twitch-brukernavnet for ugyldige tegn
Add-kommando
try {
if (sub === "add") {
if (requireAdmin()) return;
const login = sanitizeLogin(arg1 || "");
// Finn kanal fra mention eller navn
let channel = msg.mentions.channels.first();
if (!channel) {
const arg2 = parts[3] || parts[2];
if (arg2) {
const wanted = arg2.replace(/^#/, "").toLowerCase();
channel = msg.guild.channels.cache.find(
(c) => c.name.toLowerCase() === wanted
);
}
}
// Sjekk at vi har både streamer og kanal
if (!login || !channel)
return msg.reply(
"Bruk: `!twitch add <streamer_login> #kanal` eller `!twitch add <streamer_login> kanalnavn`"
);
// Legg til i database (eller oppdater eksisterende)
await db.query(
"INSERT INTO guild_streamers (guild_id, channel_id, streamer_login) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE channel_id = VALUES(channel_id)",
[msg.guild.id, channel.id, login]
);
return msg.reply(`✅ Lagt til **${login}** → ${channel}`);
}
Forklaring:
msg.mentions.channels.first()- Finner første nevnte kanal (#kanal)msg.guild.channels.cache.find()- Søker etter kanal på navnON DUPLICATE KEY UPDATE- Oppdaterer kanal hvis streameren allerede eksisterer
Remove og List kommandoer
if (sub === "remove") {
if (requireAdmin()) return;
const login = sanitizeLogin(arg1 || "");
if (!login) return msg.reply("Bruk: `!twitch remove <streamer_login>`");
await db.query(
"DELETE FROM guild_streamers WHERE guild_id = ? AND streamer_login = ?",
[msg.guild.id, login]
);
return msg.reply(`🗑️ Fjernet **${login}** fra denne serveren.`);
}
if (sub === "list") {
if (requireAdmin()) return;
const [rows] = await db.query(
"SELECT streamer_login, channel_id FROM guild_streamers WHERE guild_id = ? ORDER BY streamer_login",
[msg.guild.id]
);
if (!rows.length) return msg.reply("Ingen streamere konfigurert.");
return msg.reply(
"Konfigurerte streamere:\n" +
rows
.map((r) => `• **${r.streamer_login}** → <#${r.channel_id}>`)
.join("\n")
);
}
// Ukjent kommando
return msg.reply(
"Tilgjengelig: `!twitch add <login> #kanal`, `!twitch remove <login>`, `!twitch list` (admin only)"
);
} catch (e) {
console.error(e);
return msg.reply("Noe gikk galt. Sjekk loggene.");
}
});
Forklaring:
<#${r.channel_id}>- Discord formatering for kanal-mentions.map().join()- Konverterer array til formatert streng- Try-catch fanger database-feil
Oppstart av bot
// Start database og bot
initDB()
.then(() => client.login(process.env.DISCORD_TOKEN))
.catch((e) => {
console.error("DB-init feilet:", e);
process.exit(1);
});
module.exports = { client };
Del 6: Twitch API-modul (twitch.js)
Opprett twitch.js som håndterer all Twitch API-kommunikasjon:
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
// Token cache for å unngå unødvendige API-kall
let tokenCache = { access_token: null, expires_at: 0 };
const gameNameCache = new Map();
// Rens Twitch login (samme som i bot.js)
const cleanLogin = (s) =>
String(s || "")
.trim()
.toLowerCase()
.replace(/^@/, "")
.replace(/[^a-z0-9_]/g, "");
Twitch OAuth2 Token
async function getTwitchToken() {
const now = Date.now();
// Bruk cached token hvis den ikke er utløpt
if (tokenCache.access_token && now < tokenCache.expires_at)
return tokenCache.access_token;
// Hent ny token
const url = new URL("https://id.twitch.tv/oauth2/token");
url.searchParams.set("client_id", process.env.TWITCH_CLIENT_ID);
url.searchParams.set("client_secret", process.env.TWITCH_CLIENT_SECRET);
url.searchParams.set("grant_type", "client_credentials");
const res = await fetch(url, { method: "POST" });
const data = await res.json();
if (!res.ok)
throw new Error(`Twitch token error: ${res.status} ${JSON.stringify(data)}`);
// Cache token med 1 minutt buffer før utløp
tokenCache.access_token = data.access_token;
tokenCache.expires_at = Date.now() + ((data.expires_in || 3600) - 60) * 1000;
return tokenCache.access_token;
}
Forklaring:
- OAuth2 Client Credentials flow for app-til-app autentisering
- Token caching reduserer API-kall betydelig
- 60 sekunder buffer sikrer at vi ikke bruker utløpte tokens
Twitch Helix API
async function helix(endpoint, params = {}) {
const token = await getTwitchToken();
const url = new URL(`https://api.twitch.tv/helix/${endpoint}`);
// Legg til query parameters
for (const [k, v] of Object.entries(params))
url.searchParams.append(k, v);
const res = await fetch(url, {
headers: {
"Client-ID": process.env.TWITCH_CLIENT_ID,
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
if (!res.ok)
throw new Error(
`Helix ${endpoint} error: ${res.status} ${JSON.stringify(data)}`
);
return data.data;
}
Forklaring:
- Helix er Twitch sin moderne API
- Krever både Client-ID og Bearer token
- Returnerer
data.dataarray fra responsen
Stream og Game funksjoner
async function getStream(loginRaw) {
const login = cleanLogin(loginRaw);
if (!login) return null;
const rows = await helix("streams", { user_login: login });
return rows[0] || null; // Returnerer første stream eller null
}
async function getGameName(game_id) {
if (!game_id) return "Ukjent";
// Sjekk cache først
if (gameNameCache.has(game_id)) return gameNameCache.get(game_id);
// Hent fra API og cache
const rows = await helix("games", { id: game_id });
const name = rows[0]?.name || "Ukjent";
gameNameCache.set(game_id, name);
return name;
}
Forklaring:
getStream()returnerer stream-objektet hvis live, ellers nullgetGameName()bruker cache for å unngå gjentatte API-kall for samme spill?.optional chaining for sikker tilgang til objektegenskaper
Discord Embed Builder
function buildEmbed(loginRaw, stream, gameName) {
const login = cleanLogin(loginRaw);
const embed = new EmbedBuilder()
.setAuthor({
name: `${login} er nå live!`,
iconURL: "https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png",
})
.setTitle(stream.title || "Live nå")
.addFields(
{ name: "Spill", value: gameName || "Ukjent", inline: true },
{ name: "Seere", value: String(stream.viewer_count ?? "0"), inline: true }
)
.setImage(
(stream.thumbnail_url || "")
.replace("{width}", "1280")
.replace("{height}", "720")
)
.setURL(`https://twitch.tv/${login}`)
.setColor("#9146FF"); // Twitch sin lilla farge
// Knapp for å åpne stream
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel("Se stream")
.setStyle(ButtonStyle.Link)
.setURL(`https://twitch.tv/${login}`)
);
return { embed, row };
}
Forklaring:
EmbedBuilderlager rike meldinger med bilder og felt- Twitch thumbnail URLs har placeholder som vi erstatter
ActionRowBuilderogButtonBuilderlager klikkbare knapperButtonStyle.Linklager en URL-knapp
Del 7: Hovedfunksjonen - checkTwitch
Dette er hjertet av systemet som sjekker live-status og håndterer meldinger:
module.exports = async function checkTwitch(client, db) {
// Hent alle konfigurerte streamere
const [cfg] = await db.query(
"SELECT guild_id, channel_id, streamer_login FROM guild_streamers"
);
if (!cfg.length) return; // Ingen streamere konfigurert
// Gå gjennom hver streamer
for (const { guild_id, channel_id, streamer_login } of cfg) {
const loginClean = cleanLogin(streamer_login);
if (!loginClean) {
console.warn("Hopper over tom/ugyldig login:", streamer_login);
continue;
}
try {
// Sjekk om streameren er live
const stream = await getStream(loginClean);
// Sjekk om vi allerede tracker denne streamen
const [existingRows] = await db.query(
"SELECT * FROM live_streams WHERE guild_id = ? AND streamer_login = ?",
[guild_id, loginClean]
);
const isTracked = existingRows.length > 0;
Håndtering av nye streams
if (stream) {
// Streameren er live
const gameName = await getGameName(stream.game_id);
if (!isTracked) {
// Ny stream - send melding
const channel = await client.channels.fetch(channel_id).catch(() => null);
if (!channel || !channel.send) continue;
const { embed, row } = buildEmbed(loginClean, stream, gameName);
const msg = await channel.send({ embeds: [embed], components: [row] });
// Lagre i database
await db.query(
"INSERT INTO live_streams (guild_id, channel_id, message_id, streamer_login, last_title) VALUES (?, ?, ?, ?, ?)",
[guild_id, channel_id, msg.id, loginClean, stream.title || null]
);
} else {
// Eksisterende stream - oppdater melding
for (const rowDB of existingRows) {
const channel = await client.channels
.fetch(rowDB.channel_id)
.catch(() => null);
if (!channel) continue;
const message = await channel.messages
.fetch(rowDB.message_id)
.catch(() => null);
if (!message) continue;
// Oppdater embed med ny info
const { embed, row } = buildEmbed(loginClean, stream, gameName);
await message.edit({ embeds: [embed], components: [row] });
// Oppdater database
await db.query(
"UPDATE live_streams SET last_title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[stream.title || null, rowDB.id]
);
}
}
Håndtering av streams som stopper
} else if (!stream && isTracked) {
// Streameren er ikke lenger live - slett meldinger
for (const rowDB of existingRows) {
try {
const channel = await client.channels
.fetch(rowDB.channel_id)
.catch(() => null);
if (channel) {
const message = await channel.messages
.fetch(rowDB.message_id)
.catch(() => null);
if (message) await message.delete();
}
} catch {
// Ignore deletion errors
} finally {
// Fjern fra database uansett
await db.query("DELETE FROM live_streams WHERE id = ?", [rowDB.id]);
}
}
}
} catch (e) {
console.error(`Twitch-sjekk feilet for ${loginClean}:`, e.message);
}
}
};
Forklaring av logikken:
- Stream starter: Send ny melding og lagre i
live_streams - Stream fortsetter: Oppdater eksisterende melding med ny info
- Stream stopper: Slett Discord-melding og database-entry
- Feilhåndtering: Continue med neste streamer hvis én feiler
Del 8: Testing av boten
Start boten
npm start
Du burde se:
✅ MySQL tilkoblet: din-host:3306
✅ Logget inn som DinBot#1234
Test kommandoer
I en Discord-server hvor boten har tilgang:
!twitch add twitchNavn #stream-notifications
!twitch list
!twitch remove twitchNavn
Debug tips
- Sjekk konsollen for feilmeldinger
- Kontroller at .env filen har riktige verdier
- Verifiser at databasen er tilgjengelig
- Test Twitch streamere som faktisk er live
Del 9: Avanserte features
Error handling og logging
Legg til bedre logging:
// I bot.js
const logMessage = (level, message, data = null) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${level}: ${message}`, data || '');
};
// Bruk slik:
logMessage('INFO', 'Bot started successfully');
logMessage('ERROR', 'Twitch API error', error);
Rate limiting
Twitch API har rate limits. Legg til delays:
// I twitch.js
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// Mellom API-kall:
await sleep(100); // 100ms delay
Graceful shutdown
Håndter bot shutdown:
// I bot.js
process.on('SIGINT', async () => {
console.log('Shutting down...');
await db.end();
client.destroy();
process.exit(0);
});
Del 10: Deployment
Environment setup
For produksjon, sett opp:
- Robust MySQL database (ikke lokal)
- Process manager som PM2
- Proper logging system
- Health checks
PM2 deployment
npm install -g pm2
pm2 start bot.js --name "twitch-bot"
pm2 startup
pm2 save
Docker container
Opprett Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "bot.js"]
Konklusjon
Du har nå bygget en fullstendig Twitch-integrasjon for Discord! Boten kan:
✅ Overvåke flere streamere på tvers av flere servere
✅ Sende pene embeds når noen går live
✅ Oppdatere meldinger med ny stream-info
✅ Slette meldinger når streams stopper
✅ Håndtere administrator-kommandoer
✅ Cache API-data for bedre ytelse
Dette er et solid fundament du kan bygge videre på! 🎉