124 lines
3.6 KiB
JavaScript
124 lines
3.6 KiB
JavaScript
const tmi = require("tmi.js");
|
|
const { getSetting } = require("./settings");
|
|
const { incrementMessages } = require("./stats");
|
|
const { ensureUserForIdentity } = require("./users");
|
|
const { createLogger } = require("./logger");
|
|
const { publishOverlayChatMessage } = require("./overlay-chat");
|
|
const { resolveTwitchAvatar, resolveTwitchBadges } = require("./twitch-chat-assets");
|
|
|
|
const twitchLog = createLogger("platform:twitch", { category: "integration" });
|
|
|
|
let client = null;
|
|
|
|
function parseChannels(raw) {
|
|
return (raw || "")
|
|
.split(/[,\s]+/)
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
.map((entry) => (entry.startsWith("#") ? entry : `#${entry}`));
|
|
}
|
|
|
|
async function startTwitchBot({ commandRouter } = {}) {
|
|
const username = getSetting("twitch_bot_username");
|
|
const oauth = getSetting("twitch_bot_oauth");
|
|
const channels = parseChannels(getSetting("twitch_channels"));
|
|
if (!username || !oauth || !channels.length) {
|
|
return null;
|
|
}
|
|
|
|
const password = oauth.startsWith("oauth:") ? oauth : `oauth:${oauth}`;
|
|
client = new tmi.Client({
|
|
options: { debug: false },
|
|
identity: {
|
|
username,
|
|
password
|
|
},
|
|
channels
|
|
});
|
|
|
|
client.on("connected", (address, port) => {
|
|
twitchLog.info("Twitch bot connected", { address, port, channels }, { event: "platform_ready" });
|
|
});
|
|
|
|
client.on("message", async (channel, tags, message, self) => {
|
|
const userId = tags["user-id"] || (self ? `lumi:${username.toLowerCase()}` : "");
|
|
if (!userId) {
|
|
return;
|
|
}
|
|
const displayName = tags["display-name"] || tags.username || username;
|
|
const [badges, avatar] = await Promise.all([
|
|
resolveTwitchBadges(tags["room-id"], tags.badges || {}),
|
|
resolveTwitchAvatar(tags["user-id"], tags.username || username)
|
|
]);
|
|
const profile = self ? null : ensureUserForIdentity({
|
|
provider: "twitch",
|
|
providerUserId: userId,
|
|
displayName,
|
|
avatar
|
|
});
|
|
publishOverlayChatMessage({
|
|
id: tags.id,
|
|
platform: "twitch",
|
|
text: message,
|
|
timestamp: Number(tags["tmi-sent-ts"]) || Date.now(),
|
|
channel: { id: String(tags["room-id"] || ""), name: channel, key: channel },
|
|
author: {
|
|
id: userId,
|
|
name: displayName,
|
|
username: tags.username,
|
|
avatar,
|
|
color: tags.color,
|
|
badges,
|
|
lumi: profile ? { id: profile.id, username: profile.internal_username } : null
|
|
},
|
|
emotes: Object.entries(tags.emotes || {}).flatMap(([id, positions]) => (positions || []).map((position) => {
|
|
const [start, end] = String(position).split("-").map(Number);
|
|
return { start, end, image: `https://static-cdn.jtvnw.net/emoticons/v2/${encodeURIComponent(id)}/default/dark/3.0` };
|
|
}))
|
|
});
|
|
if (self) return;
|
|
incrementMessages(profile.id);
|
|
|
|
if (commandRouter) {
|
|
await commandRouter.handleMessage({
|
|
platform: "twitch",
|
|
raw: message,
|
|
user: profile,
|
|
platformUser: {
|
|
id: userId,
|
|
displayName,
|
|
username: tags.username
|
|
},
|
|
meta: { channel, tags, client },
|
|
reply: async (content) => {
|
|
try {
|
|
await client.say(channel, content);
|
|
} catch (error) {
|
|
twitchLog.error("Twitch command reply failed", error, { event: "reply_failed" });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
await client.connect();
|
|
return client;
|
|
}
|
|
|
|
async function stopTwitchBot() {
|
|
if (client) {
|
|
await client.disconnect();
|
|
client = null;
|
|
}
|
|
}
|
|
|
|
function getClient() {
|
|
return client;
|
|
}
|
|
|
|
module.exports = {
|
|
startTwitchBot,
|
|
stopTwitchBot,
|
|
getClient
|
|
};
|