diff --git a/libs/twitch-bot/index.js b/libs/twitch-bot/index.js new file mode 100644 index 0000000..7a446fe --- /dev/null +++ b/libs/twitch-bot/index.js @@ -0,0 +1,83 @@ +const WebSocket = require('ws') +const config = require('@config/index') +const DB = require('@libs/database') +const { + msgSplit +} = require('./parser') + +class WS { + constructor () { + /** @type {WebSocket} */ + this.ws = null + } + + runBot () { + if (!config.twitch.chat_host || !config.twitch.bot_oauth) return + if (this.ws !== null) return + this.ws = new WebSocket(`wss://${config.twitch.chat_host}:443`, 'irc') + + this.ws.on('open', this.handleOpen.bind(this)) + + this.ws.on('message', this.handleMessage.bind(this)) + + this.ws.on('error', this.handleError.bind(this)) + + this.ws.on('close', this.handleExit.bind(this)) + } + + handleError (err) { + console.log(err) + this.ws = null + this.runBot() + } + handleExit (code) { + this.ws = null + this.runBot() + } + handleMessage (data) { + // socket message convert to string + let d = data.toString() + // split message with \n + // filter array remove empty element + // replace \r to empty + let darr = d.split(/\n/).filter(t => t).map(t => t.replace(/\r$/, '')) + + for (let i in darr) { + // parse message + msgSplit(this.ws, darr[i]).then(() => { /* pass */ }) + } + } + async handleOpen () { + // from pool get db connection + let db = await DB.connect() + + // login to twitch irc + this.ws.send('PASS ' + config.twitch.bot_oauth) + this.ws.send('NICK mtfosbot') + this.ws.send('CAP REQ :twitch.tv/membership :twitch.tv/commands') + + // 取得要加入的頻道列表 + let text = `select "name" from "public"."twitch_channel where "join" = true` + let result = await db.query({ + text + }) + + // release pool connection + await db.replace() + + if (result !== null && result.rowCount > 0) { + for (let row of result.rows) { + if ('name' in row) { + this.ws.send('JOIN #' + row.name) + } + } + } + } + + joinChannel (channel = null) { + if (channel === null || typeof channel !== 'string' || channel.trim().length === 0) return null + this.ws.send(`JOIN #${channel.trim()}`) + } +} + +module.exports = new WS() diff --git a/libs/twitch-bot/parser.js b/libs/twitch-bot/parser.js new file mode 100644 index 0000000..c885362 --- /dev/null +++ b/libs/twitch-bot/parser.js @@ -0,0 +1,44 @@ +const DB = require('@libs/database') // eslint-disable-line +const config = require('@config/index') +/** + * @param {WebSocket} ws + * @param {string} msg + */ +const msgSplit = async function (ws, msg) { + if (!msg || typeof msg !== 'string') return null + let txtarr = msg.split(' ') + + if (txtarr.length > 2) { + // server heartbeat message + if (/^ping$/i.test(txtarr[0])) { + // reply server heartbeat + ws.send(`PONG ${txtarr[1]}`) + return + } + // chat message + if (/^privmsg$/i.test(txtarr[1])) { + // message format + // :user!user@user.tmi.twitch.tv PRIVMSG #channel :message + let user = txtarr[0].split('!')[0].substr(1) + if (user === config.twitch.bot_account) return + let channel = txtarr[2] // eslint-disable-line + + // get message + txtarr = txtarr.slice(3, txtarr.length) + // remove message first : + let m = txtarr.join(' ').substr(1) // eslint-disable-line + } + } +} + +const sendMsg = async (ws, channel = null, msg = null) => { + if (!ws || !('send' in ws) || typeof ws.send !== 'function') return null + if (channel === null || typeof channel !== 'string' || channel.trim().length === 0) return null + if (msg === null || typeof msg !== 'string' || msg.trim().length === 0) return null + ws.send(`PRIVMSG #${channel} :${msg}`) +} + +module.exports = { + msgSplit, + sendMsg +} diff --git a/schema/20180716-1.sql b/schema/20180716-1.sql new file mode 100644 index 0000000..a04e770 --- /dev/null +++ b/schema/20180716-1.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.twitch_channel ADD "join" boolean DEFAULT false NOT NULL; +ALTER TABLE public.twitch_channel ADD opayid varchar(150) DEFAULT '' NOT NULL; \ No newline at end of file