Skip to content

Instantly share code, notes, and snippets.

@giuseppe998e
Last active August 4, 2022 07:34
Show Gist options
  • Save giuseppe998e/c3237b88a50e887a90e912605a68b7a9 to your computer and use it in GitHub Desktop.
Save giuseppe998e/c3237b88a50e887a90e912605a68b7a9 to your computer and use it in GitHub Desktop.
An IRC WebSocket client that reads Twitch channel chat without the need to authenticate
/**
* MIT License
*
* Copyright (c) 2022 Giuseppe Eletto <[email protected]>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
var TwitchChatReader = (function () {
// Constants
var IRC_WEBSOCKET = "wss://irc-ws.chat.twitch.tv";
var MSG_REGEX = /^(?:@(\S+)\ +)?(?::(?:(?:(\w+)!(\S+))|(\S+))\ +)?(?:(\w+)\ *)(?:(\w+)[ =]*)?(?:\#(\w+)\ *)?(?::(.+))?$/m;
var TAG_REGEX = /([\w\+][^\=]+)\=(?:(\d+)|([^\;\s]+))[\;\s]/g;
// Private functions
var parseTags = (tags) => {
if (!tags) return null;
var tagDict = {};
for (var match; (match = TAG_REGEX.exec(tags)) !== null;) {
var name = match[1].replace(/-(\w)/g, (_, w) => w.toUpperCase());
if (match[3]) {
if (match[3].indexOf(',') > 0)
tagDict[name] = match[3].split(',');
else if ("true" == match[3]) tagDict[name] = 1;
else if ("false" == match[3]) tagDict[name] = 0;
else tagDict[name] = match[3];
}
else tagDict[name] = parseInt(match[2]);
}
return tagDict;
}
var parseMessage = (msg) => {
var rexMsg = MSG_REGEX.exec(msg);
if (!rexMsg) return { raw: msg };
return {
tags: parseTags(rexMsg[1]),
host: rexMsg[3] || rexMsg[4] || null,
user: rexMsg[6] || rexMsg[2] || null,
command: rexMsg[5] || null,
channel: rexMsg[7] || null,
payload: rexMsg[8] || null
};
}
// -------------------
function TwitchChatReader(
channel,
onMessageFn = null,
onOpenFn = null,
onCloseFn = null
) {
this.channel = channel;
this.onOpenFn = onOpenFn;
this.onCloseFn = onCloseFn;
this.onMessageFn = onMessageFn;
this.socket = null;
}
TwitchChatReader.prototype.connect = function () {
if (this.socket != null) return;
this.socket = new WebSocket(IRC_WEBSOCKET);
// Socket onOpenConnection function
this.socket.onopen = () => {
var randId = Math.floor(9e6 * Math.random() + 1e6);
var botName = "justinfan" + randId;
this.socket.send("NICK " + botName);
this.socket.send("CAP REQ :twitch.tv/commands twitch.tv/tags");
this.socket.send("JOIN #" + this.channel);
if (this.onOpenFn) this.onOpenFn(botName);
};
// Socket onCloseConnection function
this.socket.onclose = (event) => {
if (this.onCloseFn)
this.onCloseFn(event.code, event.reason, event.wasClean);
};
// Socket onMessageReceived function
this.socket.onmessage = (event) => {
event.data
.trim()
.split("\r\n")
.forEach((str) => {
var data = parseMessage(str);
if (data.command == "PING")
this.socket.send("PONG :" + data.payload);
else if (this.onMessageFn)
this.onMessageFn(data, data.command == "PRIVMSG");
else if (data.command == undefined) console.log(data);
});
};
};
TwitchChatReader.prototype.close = function () {
if (this.socket == null) return;
this.socket.close(1000);
this.socket = null;
};
TwitchChatReader.prototype.send = function (msg) {
if (this.socket == null) return;
this.socket.send(msg);
};
return (TwitchChatReader.prototype.constructor = TwitchChatReader);
})();
/**
* MIT License
* Copyright (c) 2022 Giuseppe Eletto <[email protected]>
*/var TwitchChatReader=function(){var t=/^(?:@(\S+)\ +)?(?::(?:(?:(\w+)!(\S+))|(\S+))\ +)?(?:(\w+)\ *)(?:(\w+)[ =]*)?(?:\#(\w+)\ *)?(?::(.+))?$/m,n=/([\w\+][^\=]+)\=(?:(\d+)|([^\;\s]+))[\;\s]/g,s=t=>{if(!t)return null;for(var s,e={};null!==(s=n.exec(t));){var o=s[1].replace(/-(\w)/g,((t,n)=>n.toUpperCase()));s[3]?s[3].indexOf(",")>0?e[o]=s[3].split(","):"true"==s[3]?e[o]=1:"false"==s[3]?e[o]=0:e[o]=s[3]:e[o]=parseInt(s[2])}return e};function e(t,n=null,s=null,e=null){this.channel=t,this.onOpenFn=s,this.onCloseFn=e,this.onMessageFn=n,this.socket=null}return e.prototype.connect=function(){null==this.socket&&(this.socket=new WebSocket("wss://irc-ws.chat.twitch.tv"),this.socket.onopen=()=>{var t="justinfan"+Math.floor(9e6*Math.random()+1e6);this.socket.send("NICK "+t),this.socket.send("CAP REQ :twitch.tv/commands twitch.tv/tags"),this.socket.send("JOIN #"+this.channel),this.onOpenFn&&this.onOpenFn(t)},this.socket.onclose=t=>{this.onCloseFn&&this.onCloseFn(t.code,t.reason,t.wasClean)},this.socket.onmessage=n=>{n.data.trim().split("\r\n").forEach((n=>{var e,o,l=(e=n,(o=t.exec(e))?{tags:s(o[1]),host:o[3]||o[4]||null,user:o[6]||o[2]||null,command:o[5]||null,channel:o[7]||null,payload:o[8]||null}:{raw:e});"PING"==l.command?this.socket.send("PONG :"+l.payload):this.onMessageFn?this.onMessageFn(l,"PRIVMSG"==l.command):null==l.command&&console.log(l)}))})},e.prototype.close=function(){null!=this.socket&&(this.socket.close(1e3),this.socket=null)},e.prototype.send=function(t){null!=this.socket&&this.socket.send(t)},e.prototype.constructor=e}();
@giuseppe998e
Copy link
Author

giuseppe998e commented Jun 28, 2022

TODO

  • Better IRCv3 message parser
  • Create an asynchronous version
  • Keep it lightweight (~1.6Kb minimized )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment