initial files
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…
Reference in New Issue