initial files

master
vampirefrog 3 years ago
commit af560610ee

@ -0,0 +1,9 @@
const EventEmitter = require('events');
class Chat extends EventEmitter {
constructor() {
super();
}
}
module.exports = Chat;

@ -0,0 +1,297 @@
const Chat = require('./Chat');
const WebSocket = require('ws');
class DLiveChat extends Chat {
constructor() {
super();
this.state = 'none';
this.users = {};
this.id = 1;
// Keepalive
this.expectedPingInterval = 20000; // miliseconds
this.reconnectDelay = 2000;
this.pingTimeout = null;
this.connect();
}
clearPingTimeout() {
if(this.pingTimeout)
clearTimeout(this.pingTimeout);
}
setPingTimeout() {
this.clearPingTimeout();
this.pingTimeout = setTimeout(() => { this.connect(); }, this.expectedPingInterval + this.reconnectDelay);
}
send(data) {
if(this.webSocket && this.webSocket.readyState == 1) {
var json = JSON.stringify(data);
this.webSocket.send(json);
console.log(new Date(), 'DLIVE SEND', json);
}
}
connect() {
if(this.webSocket) {
this.clearPingTimeout();
this.webSocket.removeAllListeners('message');
this.state = 'none';
this.webSocket = null;
}
var wsUrl = 'wss://graphigostream.prd.dlive.tv/';
console.log('Opening DLive WebSocket', wsUrl);
this.webSocket = new WebSocket(wsUrl, 'graphql-ws');
this.webSocket.once('open', () => {
console.log(new Date(), 'DLive websocket open');
this.send({type:'connection_init',payload:{}});
this.emit('open');
});
this.webSocket.once('error', (err) => {
console.error(new Date(), 'DLive websocket error', err.toString());
});
this.webSocket.once('close', (code, reason) => {
console.error(new Date(), 'DLive websocket close', code, reason);
this.clearPingTimeout();
setTimeout(() => { this.connect(); }, this.reconnectDelay);
});
this.webSocket.on('message', (json) => {
console.log(new Date(), 'DLIVE RECV', json.replace(/\s*$/, ''));
var data = JSON.parse(json);
if(data && data.type) {
if(data.type == 'connection_ack') {
this.state = 'connected';
this.emit('connected');
for(let id in this.users) {
this.connectUser(this.users[id]);
}
this.setPingTimeout();
} else if(data.type == 'ka') {
this.setPingTimeout();
} else if(data.type == 'complete') {
this.emit('complete', { id: data.id });
} else if(data.type == 'data') {
if(data.payload && data.payload.data) {
if(data.payload.data.streamMessageReceived) {
for(let i in data.payload.data.streamMessageReceived) {
var msg = data.payload.data.streamMessageReceived[i];
if(msg && msg.sender && msg.sender.displayname) {
this.emit('chat', {
data: this.users[data.id],
username: msg.sender.username,
displayName: msg.sender.displayname,
text: msg.content,
original: msg
});
}
}
}
}
}
}
});
}
listen(username, data) {
var id = this.id.toString();
this.users[id] = {
username: username,
id: id,
data: data
};
if(this.state == 'connected')
this.connectUser(this.users[id]);
this.id++;
}
stopListening(username, cb) {
for(let id in this.users) {
if(this.users[id].username == username) {
if(this.state == 'connected') {
this.disconnectUser(this.users[id]);
}
delete this.users[id];
}
};
}
connectUser(user) {
var data = {
id: user.id,
type:'start',
payload: {
variables: {
streamer: user.username
},
extensions: {
persistedQuery: {
version: 1,
sha256Hash: "feb450b243f3dc91f7672129876b5c700b6594b9ce334bc71f574653181625d5"
}
},
operationName: "StreamMessageSubscription",
query:
"subscription StreamMessageSubscription($streamer: String!) {\n"+
" streamMessageReceived(streamer: $streamer) {\n"+
" type\n"+
" ... on ChatGift {\n"+
" id\n"+
" gift\n"+
" amount\n"+
" message\n"+
" recentCount\n"+
" expireDuration\n"+
" ...VStreamChatSenderInfoFrag\n"+
" __typename\n"+
" }\n"+
" ... on ChatHost {\n"+
" id\n"+
" viewer\n"+
" ...VStreamChatSenderInfoFrag\n"+
" __typename\n"+
" }\n"+
" ... on ChatSubscription {\n"+
" id\n"+
" month\n"+
" ...VStreamChatSenderInfoFrag\n"+
" __typename\n"+
" }\n"+
" ... on ChatExtendSub {\n"+
" id\n"+
" month\n"+
" length\n"+
" ...VStreamChatSenderInfoFrag\n"+
" __typename\n"+
" }\n"+
" ... on ChatChangeMode {\n"+
" mode\n"+
" __typename\n"+
" }\n"+
" ... on ChatText {\n"+
" id\n"+
" content\n"+
" subLength\n"+
" ...VStreamChatSenderInfoFrag\n"+
" __typename\n"+
" }\n"+
" ... on ChatSubStreak {\n"+
" id\n"+
" ...VStreamChatSenderInfoFrag\n"+
" length\n"+
" __typename\n"+
" }\n"+
" ... on ChatClip {\n"+
" id\n"+
" url\n"+
" ...VStreamChatSenderInfoFrag\n"+
" __typename\n"+
" }\n"+
" ... on ChatFollow {\n"+
" id\n"+
" ...VStreamChatSenderInfoFrag\n"+
" __typename\n"+
" }\n"+
" ... on ChatDelete {\n"+
" ids\n"+
" __typename\n"+
" }\n"+
" ... on ChatBan {\n"+
" id\n"+
" ...VStreamChatSenderInfoFrag\n"+
" bannedBy {\n"+
" id\n"+
" displayname\n"+
" __typename\n"+
" }\n"+
" bannedByRoomRole\n"+
" __typename\n"+
" }\n"+
" ... on ChatModerator {\n"+
" id\n"+
" ...VStreamChatSenderInfoFrag\n"+
" add\n"+
" __typename\n"+
" }\n"+
" ... on ChatEmoteAdd {\n"+
" id\n"+
" ...VStreamChatSenderInfoFrag\n"+
" emote\n"+
" __typename\n"+
" }\n"+
" ... on ChatTimeout {\n"+
" id\n"+
" ...VStreamChatSenderInfoFrag\n"+
" minute\n"+
" bannedBy {\n"+
" id\n"+
" displayname\n"+
" __typename\n"+
" }\n"+
" bannedByRoomRole\n"+
" __typename\n"+
" }\n"+
" ... on ChatTCValueAdd {\n"+
" id\n"+
" ...VStreamChatSenderInfoFrag\n"+
" amount\n"+
" totalAmount\n"+
" __typename\n"+
" }\n"+
" ... on ChatGiftSub {\n"+
" id\n"+
" ...VStreamChatSenderInfoFrag\n"+
" count\n"+
" receiver\n"+
" __typename\n"+
" }\n"+
" ... on ChatGiftSubReceive {\n"+
" id\n"+
" ...VStreamChatSenderInfoFrag\n"+
" gifter\n"+
" __typename\n"+
" }\n"+
" __typename\n"+
" }\n"+
"}\n"+
"\n"+
"fragment VStreamChatSenderInfoFrag on SenderInfo {\n"+
" subscribing\n"+
" role\n"+
" roomRole\n"+
" sender {\n"+
" id\n"+
" username\n"+
" displayname\n"+
" avatar\n"+
" partnerStatus\n"+
" badges\n"+
" effect\n"+
" __typename\n"+
" }\n"+
" __typename\n"+
"}"
}
};
this.send(data);
}
disconnectUser(user) {
var data = {
id: user.id,
type:'stop'
};
this.send(data);
}
}
module.exports = DLiveChat;

@ -0,0 +1,168 @@
const Chat = require('./Chat');
const WebSocket = require('ws');
class TwitchChat extends Chat {
constructor() {
super();
this.state = 'none';
this.users = {};
// Keepalive
this.pingTime = 20000; // miliseconds
this.pingInterval = null;
this.pongTimeout = null;
this.reconnectDelay = 2000;
this.connect();
}
clearPingInterval() {
if(this.pingInterval)
clearInterval(this.pingInterval);
}
setPingInterval() {
this.clearPingInterval();
this.pingInterval = setInterval(() => {
this.send('PING');
this.setPongTimeout();
}, this.pingTime);
}
clearPongTimeout() {
if(this.pongTimeout)
clearTimeout(this.pongTimeout);
}
setPongTimeout() {
this.clearPongTimeout();
this.pongTimeout = setTimeout(() => { this.connect(); }, this.reconnectDelay);
}
send(str) {
if(this.webSocket && this.webSocket.readyState == 1) {
this.webSocket.send(str);
console.log(new Date(), 'TWITCH SEND', str);
}
}
connect() {
if(this.webSocket) {
this.clearPongTimeout();
this.clearPingInterval();
this.webSocket.removeAllListeners('message');
this.state = 'none';
this.webSocket = null;
}
var wsUrl = 'wss://irc-ws.chat.twitch.tv/';
console.log('Opening Twitch WebSocket', wsUrl);
this.webSocket = new WebSocket(wsUrl);
this.webSocket.once('open', () => {
console.error(new Date(), 'Twitch websocket open');
this.send('CAP REQ :twitch.tv/tags twitch.tv/commands');
this.send('PASS SCHMOOPIIE');
this.send('NICK justinfan5555');
this.send('USER justinfan5555 8 * :justinfan5555');
this.emit('open');
});
this.webSocket.once('error', (err) => {
console.error(new Date(), 'Twitch websocket error', err.toString());
});
this.webSocket.on('close', (err, message) => {
console.error(new Date(), 'Twitch websocket close', err, message);
this.clearPongTimeout();
this.clearPingInterval();
setTimeout(() => { this.connect(); }, this.reconnectDelay);
});
this.webSocket.on('message', (text) => {
var lines = text.replace(/\s+$/, '').split('\r\n');
for(var l of lines) {
if(!l || typeof l == 'undefined')
continue;
let match = l.match(/^((@([^ ]+) )?:([^ ]+) )?([^ ]+) ([^ ]+)( (([^ ]+) )?:(.*))?$/);
if(match) {
console.log(new Date(), 'TWITCH RECV', l);
let vars = {};
if(match[3]) {
let pairs = match[3].split(';');
for(let v in pairs) {
let m = pairs[v].match(/^([^=]+)=(.*)$/);
vars[m[1]] = m[2];
}
}
let senderMatch = match[4] && match[4].match(/^(.*?)!(.*?)@(.*?)$/);
let msg = {
varString: match[3],
vars: vars,
sender: senderMatch ? { nick: senderMatch[1], user: senderMatch[2], server: senderMatch[3] } : null,
senderStr: match[4],
code: match[5],
target: match[6],
cmd: match[8],
text: match[10]
};
this.emit('message', msg);
switch(msg.code) {
case '001':
this.serverName = msg.sender;
this.state = 'connected';
this.emit('connected');
for(let username in this.users) {
this.connectUser(username);
}
this.setPingInterval();
break;
case 'PRIVMSG':
this.emit('chat', {
data: this.users[msg.target.replace(/^#/, '')],
username: msg.sender.user,
displayName: msg.vars['display-name'] || msg.sender.nick || msg.sender.user,
text: msg.text
});
break;
case 'PONG':
this.clearPongTimeout();
break;
default:
// console.log("unhandled code", msg);
}
} else {
// console.error('unmatched message', l);
}
}
});
}
listen(username, data, cb) {
this.users[username] = { username: username, data: data };
if(this.state == 'connected') {
this.connectUser(username);
}
}
stopListening(username, data, cb) {
if(this.state == 'connected' && this.users[username]) {
this.disconnectUser(username);
}
delete this.users[username];
}
connectUser(username) {
this.send('JOIN #' + username);
}
disconnectUser(username) {
this.send('PART #' + username);
}
}
module.exports = TwitchChat;

@ -0,0 +1,3 @@
'use strict'
exports.DLiveChat = require('./DLiveChat');
exports.TwitchChat = require('./TwitchChat');

@ -0,0 +1,19 @@
{
"name": "streamchat",
"version": "0.0.1",
"description": "Stream Chat Clients for DLive and Twitch.",
"main": "index.js",
"dependencies": {
"ws": "^7.2.3"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "http://git.vampi.tech/vampi/streamchat"
},
"author": "vampirefrog",
"license": "ISC"
}
Loading…
Cancel
Save