/ support / ebsSupport / irc.mjs
irc.mjs
  1  import * as util from "./util.mjs"
  2  import * as twitch from "./twitch.mjs"
  3  import * as events from  "node:events"
  4  
  5  const ERROR_FAILED_TO_AUTHENTICATE = new Error("Failed to Authenticate Twitch IRC WebSocket")
  6  const ERROR_CONNECTION_CLOSED = new Error("Connection Closed by Remote Target")
  7  
  8  export function ValidateQuestion(questionText) {
  9    const failedTests = new Array()
 10    ,     questionMarks = questionText.match(/\?+/g)
 11  
 12    if (questionText.length == 0) {
 13      failedTests.push("isEmpty")
 14    }
 15    if (!(/^[\w,'"\.\-\@\!\?\$\&\\\s]+$/ig).test(questionText)) {
 16      failedTests.push("isNotAlphaNumeric")
 17    }
 18    if (questionMarks && questionMarks.length > 1) {
 19      failedTests.push("isManyQuestions")
 20    }
 21    if (!(/\?/g).test(questionText)) {
 22      failedTests.push("isNotQuestion")
 23    }
 24    if (questionText.length > 240) {
 25      failedTests.push("isTooLong")
 26    }
 27    return failedTests.length == 0
 28  }
 29  
 30  export function FilterMessage(parsed) {
 31    const { user, message } = parsed
 32  	
 33    const      isBot = user == "nightbot" || user == "streamelements"
 34  	,     isQuestion = message.includes("?")
 35  	,        hasLink = message.includes("http")
 36  	,  isSignificant = message.length > 15
 37  
 38  	return !isBot && !hasLink && isQuestion && isSignificant 
 39  }
 40  
 41  export function ParseMessage(raw) {
 42    const atCharacter = raw.indexOf("@")
 43    ,      firstSpace = raw.indexOf(" ")
 44    ,      firstColon = raw.indexOf(":", firstSpace+1)
 45    ,     secondColon = raw.indexOf(":", firstColon+1)
 46  
 47    const tagsRaw = raw.substring(atCharacter+1, firstColon) 
 48    ,   sourceRaw = raw.substring(firstColon+1, secondColon)
 49    ,     message = raw.substring(secondColon+1, raw.length-2) // remove trailing carriage return + newline
 50  
 51    const tags = {}
 52  
 53    for (const tag of tagsRaw.split(";")) {
 54      const [key, value] = tag.split("=")
 55      tags[key] = value
 56    }
 57  
 58    const nickname = sourceRaw.substring(sourceRaw.indexOf("!")+1,sourceRaw.indexOf("@"))
 59  
 60    return { user: nickname, id: +tags["user-id"], bits: +tags.bits || 0, message }
 61  }
 62  
 63  export function MakeChatReader() {
 64    const ChatReader = {}
 65  
 66    ChatReader.Ongoing = new Map() // userName => connection
 67    ChatReader.emitter = new events.EventEmitter()
 68  
 69    ChatReader.emitter.on("start", (userName, consumeMessage) => {
 70      const messageHandler = (e) => {
 71        const rawMessage = e.data 
 72        if (rawMessage.includes("PRIVMSG")) { 
 73          consumeMessage(rawMessage)
 74        }
 75      }
 76     
 77      ChatReader.makeConnection().then(connection => {
 78        connection.addEventListener("message", messageHandler)
 79        ChatReader.Ongoing.set(userName, connection)
 80        connection.send(`JOIN #${userName}`)
 81      }).catch(console.error)
 82    })
 83    
 84    ChatReader.emitter.on("end", (userName) => {
 85      if (!ChatReader.Ongoing.has(userName)) return 
 86  
 87      const connection = ChatReader.Ongoing.get(userName)
 88  
 89      connection.send(`PART #${userName}`)
 90      connection.close(1000)
 91  
 92      ChatReader.Ongoing.delete(userName)
 93    })
 94  
 95    ChatReader.makeConnection = () => {
 96      return new Promise((resolve, reject) => {
 97        let connection = new WebSocket("wss://irc-ws.chat.twitch.tv:443")
 98  
 99        connection.addEventListener("open", (e) => {
100          connection.send(`CAP REQ :twitch.tv/tags`)
101          connection.send(`PASS oauth:${Bun.env.EBS_SUPPORT_APP_TOKEN}`)
102          connection.send(`NICK offbeatlive`)
103          resolve(connection)
104        })
105  
106        connection.addEventListener("message", (e) => {
107          const rawMessage = e.data
108          const { user, message }= ParseMessage(rawMessage)
109  
110          if (message.indexOf("JOIN #")+message.indexOf("PART #") > -2) {
111            console.log(`[IRC] ${message}`)
112          }
113  
114          if (rawMessage.includes("Login unsuccessful")) {
115            connection = null
116            reject(ERROR_FAILED_TO_AUTHENTICATE)
117          }
118  
119          if (rawMessage.includes("PING")) { connection.send("PONG :tmi.twitch.tv") }
120        }) 
121  
122        connection.addEventListener("close", () => {
123          console.log(`[IRC] Connection Closed!`)
124          reject(ERROR_CONNECTION_CLOSED)
125        }) 
126        connection.addEventListener("error", reject)
127      })
128    }
129   
130    return ChatReader
131  }