Browse Source

index.js: insert displayname for users too, add URL harvesting, catch some promise rejections

master
vampirefrog 1 year ago
parent
commit
73695e274b
  1. 678
      index.js

678
index.js

@ -3,29 +3,25 @@ const @@ -3,29 +3,25 @@ const
http = require('http'),
express = require('express'),
ExpressWS = require('express-ws'),
WebSocket = require('ws'),
WebSocket = require('isomorphic-ws'),
DLiveChat = require('./static/streamchat/DLiveChat'),
TwitchChat = require('./static/streamchat/TwitchChat'),
TrovoChat = require('./static/streamchat/TrovoChat'),
YouTube = require('./youtube'),
SoundCloud = require('./soundcloud'),
sqlite3 = require('sqlite3'),
say = require('say'),
child_process = require('child_process'),
fs = require('fs'),
tmp = require('tmp'),
sqlite3 = require('sqlite3'),
say = require('say'),
DLiveChat = require('./streamchat/DLiveChat'),
TwitchChat = require('./streamchat/TwitchChat'),
util = require('./static/util'),
YouTube = require('./youtube'),
SoundCloud = require('./soundcloud');
var localVoices = [];
say.getInstalledVoices((err, voices) => {
if(err) {
console.error(err);
return;
}
localVoices = voices;
});
util = require('./static/util');
var config = require('./config');
var voices = JSON.parse(fs.readFileSync('./voices.json'));
var songProviders = [
new YouTube(),
new SoundCloud()
];
const SQLITE_TEMPLATE = 'template.sqlite';
const SQLITE_TEMPLATE_PATH = __dirname+'/'+SQLITE_TEMPLATE;
@ -51,44 +47,85 @@ var db = new sqlite3.Database(SQLITE_FILE_PATH, (err) => { @@ -51,44 +47,85 @@ var db = new sqlite3.Database(SQLITE_FILE_PATH, (err) => {
}
});
var songProviders = [
new YouTube(),
new SoundCloud()
];
function getOrInsertUser(userData) {
return new Promise((resolve, reject) => {
if(!userData.platform) {
reject('Platform not specified in user data');
return;
}
var app = express();
if(!userData.username) {
reject('Username not specified in user data');
return;
}
app.use(express.static('static'));
let sql = `
INSERT INTO users (
platform,
username,
displayname
) VALUES (
?,
?,
?
) ON CONFLICT(platform, username) DO UPDATE SET displayname = excluded.displayname
`;
db.run(sql, [
userData.platform,
userData.username,
userData.displayName || userData.username
], (err) => {
if(err) {
reject(`Error inserting/updating user ${userData.platform}/${userData.username}: ${err}`);
return;
}
app.get('/speech', (req, res, next) => {
var url = 'https://code.responsivevoice.org/getvoice.php?'+util.buildQueryString(req.query);
var h = https.get(url, {
headers: { referer: 'https://responsivevoice.org/' }
}, (r) => {
if(r.statusCode !== 200) {
res.status(r.statusCode);
res.send('Error '+r.statusCode);
} else {
res.append('Content-type', r.headers['content-type']);
let sql = `SELECT * FROM users WHERE platform = ? AND username = ?`;
db.get(sql, [ userData.platform, userData.username ], (err, userRow) => {
if(err) {
reject(`Error selecting user ${userData.platform}/${userData.username}: ${err}`);
return;
}
r.on('data', (chunk) => {
res.write(chunk);
});
r.on('end', () => {
res.end();
resolve(userRow);
});
r.on('error', (err) => {
res.status(500);
res.end(err.message);
});
}
});
h.on('error', (err) => {
res.status(500);
res.end(err.errno);
});
});
}
var localVoices = [];
say.getInstalledVoices((err, voices) => {
if(err) {
console.error(err);
return;
}
localVoices = voices;
});
var voices = JSON.parse(fs.readFileSync('./voices.json'));
function getResponsiveVoiceData(voice) {
for(let i in voices.responsivevoices) {
var voiceData = voices.responsivevoices[i];
if(voiceData.name.toLowerCase() == voice.toLowerCase()) {
var voiceParams = null;
for(let j in voiceData.voiceIDs) {
var voiceId = voiceData.voiceIDs[j];
var vp = voices.voicecollection[voiceId];
if(vp.fallbackvoice) {
return vp;
}
}
break;
}
}
return null;
}
var app = express();
app.use(express.static('static'));
app.get('/stickerInfo/:id', (req, res, next) => {
var url = 'https://vampi.tech/wp-admin/admin-ajax.php?'+util.buildQueryString({ action: 'sticker_search', sticker_id: req.params.id});
var h = https.get(url, {
@ -122,87 +159,156 @@ app.get('/config.js', (req, res, next) => { @@ -122,87 +159,156 @@ app.get('/config.js', (req, res, next) => {
speechUrl: 'http://localhost:'+config.port+'/speech',
stickerInfoUrl: 'http://localhost:'+config.port+'/stickerInfo',
sounds: config.sounds,
streamers: config.streamers
streamers: config.streamers,
donations: config.donations
})+';');
});
var server = http.createServer({}, app);
server.listen(config.port);
app.delete('/urls', (req, res, next) => {
db.run('DELETE FROM chat_urls', [], (err) => {
if(err) {
res.status(500);
res.end(err);
return;
}
var expressWs = ExpressWS(app, server);
app.ws('/socket', function(ws, req) {
ws.on('message', function(msg) {
console.log(msg);
db.run('DELETE FROM urls', [], (err) => {
if(err) {
res.status(500);
res.end(err);
return;
}
res.end('OK');
});
});
console.log('socket', req.testing);
});
const serverWS = expressWs.getWss('/socket');
console.log('Listening on localhost:'+config.port);
function getOrInsertUser(platform, username, cb) {
let sql = `SELECT * FROM users WHERE platform = ? AND username = ?`;
db.get(sql, [ platform, username ], (err, user) => {
app.delete('/urls/:url', (req, res, next) => {
db.run('DELETE FROM chat_urls WHERE url = ?', [ req.params.url ], (err) => {
if(err) {
cb(err);
} else if(!user) {
db.run(`
INSERT INTO users (
platform,
username
) VALUES(
?,
?
)
`, [
platform,
username
], function(err) {
if(err) {
cb(`Error inserting user ${platform}/${username}: ${err}`);
} else {
let sql = `SELECT * FROM users WHERE id = ?`;
db.get(sql, [ this.lastID ], (err, user) => {
cb(err, user);
});
}
});
} else {
cb(null, user);
res.status(500);
res.end(err);
return;
}
});
}
function getResponsiveVoiceData(voice) {
for(let i in voices.responsivevoices) {
var voiceData = voices.responsivevoices[i];
if(voiceData.name.toLowerCase() == voice.toLowerCase()) {
var voiceParams = null;
for(let j in voiceData.voiceIDs) {
var voiceId = voiceData.voiceIDs[j];
var vp = voices.voicecollection[voiceId];
if(vp.fallbackvoice) {
return vp;
}
db.run('DELETE FROM urls WHERE url = ?', [ req.params.url ], (err) => {
if(err) {
res.status(500);
res.end(err);
return;
}
break;
res.end('OK');
});
});
});
app.post('/urls', (req, res, next) => {
console.log('POST urls');
});
app.get('/urls', (req, res, next) => {
res.append('Content-type', 'application/javascript; charset = utf8');
var date = new Date();
if('export' in req.query)
res.append('Content-Disposition', 'attachment; filename="video-urls-'+date.getFullYear()+'-'+(date.getMonth()+1)+'-'+date.getDate()+'_'+date.getHours()+'-'+date.getMinutes()+'.json"');
let q = `
SELECT
u.*
FROM urls u
ORDER BY u.timestamp
`;
db.all(q, [], (err, rows) => {
var ret = [];
if (err) {
res.end(JSON.stringify({
error: 'Error getting URLs: '+err
}));
return;
}
}
return null;
}
app.get('/speech/:platform/:username', (req, res, next) => {
var delay = Math.random() * 3000;
delay = 1;
var urls = {};
for(let r of rows) {
r.chats = [];
urls[r.url] = r;
}
setTimeout(() => {
getOrInsertUser(req.params.platform, req.params.username, (err, user) => {
if(err) {
res.status(500);
req.end('Error: '+err);
let q = `
SELECT
c.*,
ur.url AS url,
u.platform AS user_platform,
u.username AS user_username,
u.displayname AS user_displayname
FROM chat_urls cu
JOIN urls ur ON ur.url = cu.url
JOIN (
chats c
JOIN users u ON u.id = c.user_id
)
ON c.id = cu.chat_id
`;
db.all(q, [], (err, rows) => {
if (err) {
res.end(JSON.stringify({
error: 'Error getting URL chats: '+err
}));
return;
}
for(let r of rows) {
urls[r.url].chats.push({
user: {
username: r.user_username,
platform: r.user_platform,
displayName: r.user_displayname
},
timestamp: r.timestamp,
text: r.content,
donation: r.donation,
value: r.value,
currency: r.currency,
amount: r.amount
});
}
for(let u in urls) {
ret.push({
info: {
url: urls[u].url,
platform: urls[u].platform,
title: urls[u].title,
thumbnail: urls[u].thumbnail,
duration: urls[u].duration
},
chats: urls[u].chats,
});
}
res.end(JSON.stringify(ret));
});
});
});
var server = http.createServer(app);
var expressWs = ExpressWS(app, server);
app.ws('/socket', function(ws, req) {
ws.on('message', function(msg) {
console.log(msg);
});
console.log('socket', req.testing);
});
const serverWS = expressWs.getWss('/socket');
console.log('Listening on localhost:'+config.port);
server.listen(config.port);
app.get('/speech/:platform/:username', (req, res, next) => {
getOrInsertUser(req.params)
.then((user) => {
var rate = user.rate || 0.5;
if(rate < 0) rate = 0;
if(rate > 1) rate = 1;
@ -236,7 +342,6 @@ app.get('/speech/:platform/:username', (req, res, next) => { @@ -236,7 +342,6 @@ app.get('/speech/:platform/:username', (req, res, next) => {
},
(err) => {
if(err) {
console.log('error sending file', path, err);
res.status(err.status||500);
res.send('Error downloading file');
return;
@ -291,32 +396,27 @@ app.get('/speech/:platform/:username', (req, res, next) => { @@ -291,32 +396,27 @@ app.get('/speech/:platform/:username', (req, res, next) => {
res.status(500);
res.end('Error with HTTPS GET: ' + err.errno);
});
});
}, delay);
})
.catch((err) => {
console.error('speech', err);
res.status(500);
res.end('Error: '+err);
})
;
});
function handleDelete(msg) {
serverWS.clients.forEach(function (client) {
client.send(JSON.stringify({
type: 'delete',
platform: msg.client.getPlatformName(),
ids: msg.ids,
original: msg.original,
}));
});
}
function handleChat(client, chat) {
let text = chat.text || chat.message;
function handleChat(chat) {
getOrInsertUser(chat.client.getPlatformName(), chat.username, (err, user) => {
if(err) {
console.error(err);
return;
}
if(!chat.text)
return;
if(!text)
return;
var match = chat.text.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/, ' ').match(/([^\s]+)(\s+(.*))?/);
getOrInsertUser({
platform: client.getPlatformName(),
username: chat.username,
displayName: chat.displayName
}).then((user) => {
var match = text.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/, ' ').match(/([^\s]+)(\s+(.*))?/);
var cmd = match[1];
var params = match[3];
if(config.commands[cmd]) {
@ -326,10 +426,14 @@ function handleChat(chat) { @@ -326,10 +426,14 @@ function handleChat(chat) {
response = response[Math.floor(Math.random() * response.length)];
}
chat.client.sendStreamChatMessage(response.replace('%s', params), chat.data.username, (err, msg) => {
if(err)
client.sendStreamChatMessage(response.replace('%s', params), chat.source.data.username)
.then((msg) => {
console.log('Sent message', msg);
})
.catch((err) => {
console.error(err);
});
})
;
} else if(cmd == '!voice') {
if(params) {
var voice = params;
@ -337,9 +441,9 @@ function handleChat(chat) { @@ -337,9 +441,9 @@ function handleChat(chat) {
if(localVoices.indexOf(voice) !== -1) {
db.run(`UPDATE users SET voice = ? WHERE id = ?`, [voice, user.id], (err) => {
if(err) {
chat.client.sendStreamChatMessage(`Could not change voice for ${chat.displayName}: ${err}`, chat.data.username);
client.sendStreamChatMessage(`Could not change voice for ${chat.displayName}: ${err}`, chat.source.data.username);
} else {
chat.client.sendStreamChatMessage(`Voice for ${chat.displayName} changed to ${voice}, rate is ${user.rate}, pitch is ${user.pitch}.`, chat.data.username);
client.sendStreamChatMessage(`Voice for ${chat.displayName} changed to ${voice}, rate is ${user.rate}, pitch is ${user.pitch}.`, chat.source.data.username);
}
});
} else {
@ -347,51 +451,51 @@ function handleChat(chat) { @@ -347,51 +451,51 @@ function handleChat(chat) {
if(voiceData) {
db.run(`UPDATE users SET voice = ? WHERE id = ?`, [voice, user.id], (err) => {
if(err) {
chat.client.sendStreamChatMessage(`Could not change voice for ${chat.displayName}: ${err}`);
client.sendStreamChatMessage(`Could not change voice for ${chat.displayName}: ${err}`, chat.source.data.username);
} else {
chat.client.sendStreamChatMessage(`Voice for ${chat.displayName} changed to ${voice}, rate is ${user.rate}, pitch is ${user.pitch}.`, chat.data.username);
client.sendStreamChatMessage(`Voice for ${chat.displayName} changed to ${voice}, rate is ${user.rate}, pitch is ${user.pitch}.`, chat.source.data.username);
}
});
} else {
chat.client.sendStreamChatMessage(`Invalid voice ${voice}. Select a voice from the dropdown at https://responsivevoice.org/`, chat.data.username);
client.sendStreamChatMessage(`Invalid voice ${voice}. Select a voice from the dropdown at https://responsivevoice.org/`, chat.source.data.username);
}
}
} else {
chat.client.sendStreamChatMessage(`Voice for ${chat.displayName} is ${user.voice}, rate is ${user.rate}, pitch is ${user.pitch}. Select a voice from the dropdown at https://responsivevoice.org/`, chat.data.username);
client.sendStreamChatMessage(`Voice for ${chat.displayName} is ${user.voice}, rate is ${user.rate}, pitch is ${user.pitch}. Select a voice from the dropdown at https://responsivevoice.org/`, chat.source.data.username);
}
} else if(cmd == '!pitch') {
if(params) {
let pitch = parseFloat(params);
if(typeof pitch === 'NaN' || pitch < 0.0 || pitch > 1.0) {
chat.client.sendStreamChatMessage(`Could not change pitch for ${chat.displayName}: Invalid number. Specify a value between 0.0 and 1.0.`);
client.sendStreamChatMessage(`Could not change pitch for ${chat.displayName}: Invalid number. Specify a value between 0.0 and 1.0.`, chat.source.data.username);
} else {
db.run(`UPDATE users SET pitch = ? WHERE id = ?`, [pitch, user.id], (err) => {
if(err) {
chat.client.sendStreamChatMessage(`Could not change pitch for ${chat.displayName}: ${err}`);
client.sendStreamChatMessage(`Could not change pitch for ${chat.displayName}: ${err}`, chat.source.data.username);
} else {
chat.client.sendStreamChatMessage(`Pitch for ${chat.displayName} changed to ${pitch}`, chat.data.username);
client.sendStreamChatMessage(`Pitch for ${chat.displayName} changed to ${pitch}`, chat.source.data.username);
}
});
}
} else {
chat.client.sendStreamChatMessage(`Pitch for ${chat.displayName} is ${user.pitch}`, chat.data.username);
client.sendStreamChatMessage(`Pitch for ${chat.displayName} is ${user.pitch}`, chat.source.data.username);
}
} else if(cmd == '!rate') {
if(params) {
let rate = parseFloat(params);
if(typeof rate === 'NaN' || rate < 0.0 || rate > 1.0) {
chat.client.sendStreamChatMessage(`Could not change rate for ${chat.displayName}: Invalid number. Specify a value between 0.0 and 1.0.`);
client.sendStreamChatMessage(`Could not change rate for ${chat.displayName}: Invalid number. Specify a value between 0.0 and 1.0.`, chat.source.data.username);
} else {
db.run(`UPDATE users SET rate = ? WHERE id = ?`, [rate, user.id], (err) => {
if(err) {
chat.client.sendStreamChatMessage(`Could not change rate for ${chat.displayName}: ${err}`);
client.sendStreamChatMessage(`Could not change rate for ${chat.displayName}: ${err}`, chat.source.data.username);
} else {
chat.client.sendStreamChatMessage(`Pitch for ${chat.displayName} changed to ${rate}`, chat.data.username);
client.sendStreamChatMessage(`Pitch for ${chat.displayName} changed to ${rate}`, chat.source.data.username);
}
});
}
} else {
chat.client.sendStreamChatMessage(`Pitch for ${chat.displayName} is ${user.rate}`, chat.data.username);
client.sendStreamChatMessage(`Pitch for ${chat.displayName} is ${user.rate}`, chat.source.data.username);
}
} else if(cmd == '!sticker') {
var url = 'https://vampi.tech/wp-admin/admin-ajax.php?'+util.buildQueryString({ action: 'sticker_search', q: params||''});
@ -410,7 +514,7 @@ function handleChat(chat) { @@ -410,7 +514,7 @@ function handleChat(chat) {
try {
result = JSON.parse(resultJson);
if(result && result.results && result.results[0]) {
chat.client.sendStreamChatMessage(':emote/mine/dlive/'+result.results[0].id+':', chat.data.username);
client.sendStreamChatMessage(':emote/mine/dlive/'+result.results[0].id+':', chat.source.data.username);
}
} catch(e) {
}
@ -418,42 +522,140 @@ function handleChat(chat) { @@ -418,42 +522,140 @@ function handleChat(chat) {
}
});
} else {
var urls = chat.text.match(/(https?:\/\/[^\s]+)/g);
if(urls && urls[0]) {
var provider = null;
for(let p of songProviders) {
if(p.urlMatches(urls[0])) {
provider = p;
break;
}
handleUrls(client, user, chat);
}
}).catch((err) => {
console.error(err);
})
;
}
function handleUrls(client, user, chat) {
let text = chat.text || chat.message;
let urls = text.match(/(https?:\/\/[^\s]+)/g);
if(!urls)
return;
db.run(`
INSERT INTO chats (
user_id,
content,
donation,
value,
currency,
amount
) VALUES(
?,
?,
?,
?,
?,
?
)
`, [
user.id,
text,
chat.giftType||null,
chat.value||null,
chat.currency||null,
chat.amount||null
], function(err) {
if(err) {
console.error(`Could not insert chat: ${err}`);
return;
}
for(let u = 0; u < urls.length; u++) {
let url = urls[u];
let provider = null;
for(let p of songProviders) {
if(p.urlMatches(url)) {
provider = p;
break;
}
}
if(provider) {
var url = provider.getCanonicalUrl(urls[0]);
provider.getInfo(url, (err, info) => {
console.log(err, info);
if(!err && info && info.title) {
chat.client.sendStreamChatMessage(info.title + ' ' + util.secondsToTime(info.duration), chat.data.username);
if(!provider)
continue;
let canonicalUrl = provider.getCanonicalUrl(url);
provider.getInfo(url, (err, info) => {
if(!err && info && info.title) {
db.run(`
INSERT INTO urls (
url,
platform,
title,
thumbnail,
duration,
num_chats
) VALUES(
?,
?,
?,
?,
?,
1
) ON CONFLICT(url) DO UPDATE SET num_chats = num_chats + 1
`, [
canonicalUrl,
info.platform,
info.title,
info.thumbnail,
info.duration
], (err) => {
if(err) {
console.error(`Error inserting URL ${canonicalUrl}: ${err}`);
return;
}
db.run(`
INSERT INTO chat_urls (
chat_id,
url
) VALUES(
?,
?
)
`, [
this.lastID,
canonicalUrl
], function(err) {
if(err)
console.error('Error inserting chat URL chat_id=${this.lastID} url=${canonicalUrl}: ${err}');
});
console.log(`Harvested URL ${canonicalUrl} ${info.title}`, util.secondsToTime(info.duration));
serverWS.clients.forEach(function(client) {
client.send(JSON.stringify({
type: 'url',
data: {
info: info,
chat: {
user: {
username: chat.username,
platform: user.platform,
displayName: chat.displayName
},
text: text,
donation: chat.giftType,
value: chat.value,
currency: chat.currency,
amount: chat.amount,
timestamp: new Date()
}
}
}));
});
});
if(u == 0)
client.sendStreamChatMessage(info.title + ' ' + util.secondsToTime(info.duration), chat.source.data.username);
}
}
});
}
chat.user = user;
serverWS.clients.forEach(function (client) {
client.send(JSON.stringify({
type: 'chat',
platform: chat.client.getPlatformName(),
user: chat.user,
original: chat.original,
data: chat.data,
username: chat.username,
displayName: chat.displayName,
text: chat.text
}));
});
});
}
@ -463,64 +665,53 @@ for(let i in config.streamers) { @@ -463,64 +665,53 @@ for(let i in config.streamers) {
let client;
if(streamerConfig.platform == 'dlive') {
client = new DLiveChat();
client = new DLiveChat(streamerConfig.settings);
} else if(streamerConfig.platform == 'twitch') {
client = new TwitchChat();
client = new TwitchChat(streamerConfig.settings);
} else if(streamerConfig.platform == 'trovo') {
client = new TrovoChat(streamerConfig.settings);
}
client.config = streamerConfig;
if(!client)
continue;
chatClients.push(client);
client.listen(streamerConfig.username);
client.addListener('chat', handleChat);
client.addListener('delete', handleDelete);
if(streamerConfig.bot && streamerConfig.bot.username) {
if(streamerConfig.bot.authToken) {
client.loginWithAuthToken(streamerConfig.bot.username, streamerConfig.bot.authToken, function(err, result) {
if(err) {
console.error('Error authenticating with auth token:', err);
return;
}
console.log('Logged in with auth token', result);
if(streamerConfig.bot.hello) {
console.log('Sending hello', streamerConfig.bot.hello);
client.sendStreamChatMessage(streamerConfig.bot.hello, streamerConfig.username, function(err, result) {
if(err) {
console.error('Error sending stream chat message', err);
return;
}
console.log('Sent hello', result);
});
}
});
} else if(streamerConfig.bot.password) {
client.login(streamerConfig.bot.username, streamerConfig.bot.password, (err, result) => {
if(err) {
console.error('Error authenticating with username and password:', err);
return;
}
console.log('Logged in with username and password', result);
for(let j in streamerConfig.channels) {
var channel = streamerConfig.channels[j];
client.joinChannel(channel.username, channel);
}
if(streamerConfig.bot.hello) {
console.log('Sending hello', streamerConfig.bot.hello);
client.sendStreamChatMessage(streamerConfig.bot.hello, streamerConfig.username, function(err, result) {
if(err) {
console.error('Error sending stream chat message', err);
return;
}
client.addListener('joined', (joined) => {
for(let j in streamerConfig.channels) {
var channel = streamerConfig.channels[j];
if(channel.username != joined.channel)
continue;
if(channel.hello) {
console.log('sending hello', joined.channel);
client.sendStreamChatMessage(channel.hello, channel.username)
.then((result) => {
})
.catch((err) => {
console.error('Error sending hello', err);
})
;
}
}
});
console.log('Sent hello', result);
});
}
});
client.addListener('message', (data) => {
if(data.type == 'chat' || data.type == 'gift') {
handleChat(data.client, data);
}
}
data.client = null;
data.platform = streamerConfig.platform;
serverWS.clients.forEach(function(client) {
client.send(JSON.stringify(data));
});
});
}
if (process.platform === "win32") {
@ -534,21 +725,20 @@ if (process.platform === "win32") { @@ -534,21 +725,20 @@ if (process.platform === "win32") {
});
}
process.on("SIGINT", function () {
process.on("SIGINT", function() {
console.log('Caught interrupt');
for(let i in chatClients) {
if(chatClients[i].config.bot && chatClients[i].isConnected()) {
console.log('Sending goodbye message', chatClients[i].config.goodbye);
chatClients[i].sendStreamChatMessage(chatClients[i].config.goodbye, chatClients[i].config.username, (err, result) => {
if(err) {
console.error('Error sending goodbye message:', err);
process.exit();
}
console.log('Sent goodbye message', result);
process.exit();
});
var joinedChannels = chatClients[i].getJoinedChannels();
for(let j in joinedChannels) {
var channelData = chatClients[i].getChannelData(joinedChannels[j]);
console.log(channelData);
chatClients[i].sendStreamChatMessage(channelData.goodbye, joinedChannels[j]);
}
}
process.on("SIGINT", function() { process.exit(0); });
// wait for quit messages to be sent
setTimeout(() => { process.exit(0); }, 1000);
});

Loading…
Cancel
Save