From 9076b1817ba37a28a36d286eccf2fe7da6f59b86 Mon Sep 17 00:00:00 2001 From: knotteye Date: Tue, 5 Nov 2019 17:22:52 -0600 Subject: [PATCH] Add contributors file Add irc.js for peering with IRC Change default to reflect that Add basic peering. Implemented: register/unregister a user messages join/part Not implemented/TODO: Kill, kick, change nick It is likely that the web chat will not have full feature parity with clients connecting directly to IRC. --- CONTRIBUTORS | 2 + config/default.toml | 6 +- src/controller.ts | 2 +- src/http.ts | 22 ++++- src/irc.js | 213 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTORS create mode 100644 src/irc.js diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..dac1314 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,2 @@ +knotteye +crushv \ No newline at end of file diff --git a/config/default.toml b/config/default.toml index cda1845..c7ff14d 100644 --- a/config/default.toml +++ b/config/default.toml @@ -12,9 +12,11 @@ rootredirect = '/users/live' [ircd] enable = false -port = 7000 -user = '' +port = 6667 +sid = '' +server = '' pass = '' +vhost = 'web.satyr.net' [database] host = 'localhost' diff --git a/src/controller.ts b/src/controller.ts index 593679e..8ed3e1d 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -56,7 +56,7 @@ async function run() { db.init(dbcfg, bcryptcfg); await cleanup.init(config.server.http.directory); api.init(satyr); - http.init(satyr, config.server.http.port); + http.init(satyr, config.server.http.port, config.ircd); mediaserver.init(nms, satyr); } run(); diff --git a/src/http.ts b/src/http.ts index 02e7e17..4593f4d 100644 --- a/src/http.ts +++ b/src/http.ts @@ -7,6 +7,7 @@ import * as http from "http"; import * as dirty from "dirty"; import * as api from "./api"; import * as db from "./database"; +import * as irc from "./irc"; const app = express(); const server = http.createServer(app); @@ -14,7 +15,7 @@ const io = socketio(server); const store = dirty(); var njkconf; -function init(satyr: any, port: number){ +async function init(satyr: any, port: number, ircconf: any){ njk.configure('templates', { autoescape: true, express : app, @@ -119,26 +120,44 @@ function init(satyr: any, port: number){ app.use(function (req, res, next) { res.status(404).render('404.njk', njkconf); }); + //irc peering + if(ircconf.enable){ + await irc.connect({ + port: ircconf.port, + sid: ircconf.sid, + pass: ircconf.pass, + server: ircconf.server, + vhost: ircconf.vhost + }); + irc.events.on('message', (nick, channel, msg) => { + io.to(channel).emit('MSG', {nick: nick, msg: msg}); + }); + } //socket.io chat logic io.on('connection', async (socket) => { socket.nick = await newNick(socket); + if(ircconf.enable) irc.registerUser(socket.nick); socket.on('JOINROOM', async (data) => { let t: any = await db.query('select username from users where username='+db.raw.escape(data)); if(t[0]){ socket.join(data); io.to(data).emit('JOINED', {nick: socket.nick}); + if(ircconf.enable) irc.join(socket.nick, data); } else socket.emit('ALERT', 'Room does not exist'); }); socket.on('LEAVEROOM', (data) => { socket.leave(data); + if(ircconf.enable) irc.part(socket.nick, data); io.to(data).emit('LEFT', {nick: socket.nick}); }); socket.on('disconnecting', (reason) => { let rooms = Object.keys(socket.rooms); for(let i=1;i { @@ -164,6 +183,7 @@ function init(satyr: any, port: number){ }); socket.on('MSG', (data) => { io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg}); + if(ircconf.enable) irc.send(socket.nick, data.room, data.msg); }); socket.on('KICK', (data) => { if(socket.nick === data.room){ diff --git a/src/irc.js b/src/irc.js new file mode 100644 index 0000000..4427303 --- /dev/null +++ b/src/irc.js @@ -0,0 +1,213 @@ +// written by crushv +// thanks nikki + +const net = require('net') +const EventEmitter = require('events') + +const socket = new net.Socket() +const emitter = new EventEmitter() + +socket.setEncoding('utf8') + +socket.on('error', console.error) + +function m (text) { + console.log('> ' + text) + socket.write(text + '\r\n') +} + +var config + +socket.once('connect', async () => { + console.log('Connected') + m(`PASS ${config.pass} TS 6 :${config.sid}`) + m('CAPAB QS ENCAP EX IE SAVE EUID') + m(`SERVER ${config.server} 1 satyr`) +}) + +function parseLine (l) { + const colIndex = l.lastIndexOf(':') + if (colIndex > -1) { + return { + params: l.substring(0, colIndex - 1).split(' '), + query: l.substring(colIndex + 1) + } + } else return { params: l.split(' ') } +} + +const servers = [] +const users = {} +const channels = {} + +const globalCommands = { + // PING :42X + // params: SID + PING: l => { + const { query } = parseLine(l) + m(`PONG :${query}`) + emitter.emit('ping') + }, + // PASS hunter2 TS 6 :42X + // params: password, 'TS', TS version, SID + PASS: l => { + const { query } = parseLine(l) + // adds a server + servers.push(query) + } +} + +const serverCommands = { + // EUID nik 1 1569146316 +i ~nik localhost6.attlocal.net 0::1 42XAAAAAB * * :nik + // params: nickname, hopcount, nickTS, umodes, username, visible hostname, IP address, UID, real hostname, account name, gecos + EUID: l => { + const { params } = parseLine(l) + const user = { + nick: params[0], + nickTS: params[2], + modes: params[3], + username: params[4], + vhost: params[5], + ip: params[6], + uid: params[7] + } + users[user.uid] = user + }, + // SJOIN 1569142987 #test +nt :42XAAAAAB + // params: channelTS, channel, simple modes, opt. mode parameters..., nicklist + SJOIN: l => { + const { params, query } = parseLine(l) + const channel = { + timestamp: params[0], + name: params[1], + modes: params.slice(2).join(' '), + nicklist: query.split(' ').map(uid => { + if (/[^0-9a-zA-Z]/.test(uid[0])) return { uid: uid.slice(1), mode: uid[0] } + else return { uid: uid, mode: '' } + }) + } + channels[channel.name] = channel + } +} + +const userCommands = { + // :42XAAAAAC PRIVMSG #test :asd + // params: target, msg + PRIVMSG: (l, source) => { + const { params, query } = parseLine(l) + emitter.emit('message', users[source].nick, params[0], query) + }, + // :42XAAAAAC JOIN 1569149395 #test + + JOIN: (l, source) => { + const { params } = parseLine(l) + channels[params[1]].nicklist.push({ + uid: source + }) + }, + // :42XAAAAAC PART #test :WeeChat 2.6 + PART: (l, source) => { + const { params } = parseLine(l) + for (let i = 0; i < channels[params[0]].nicklist.length; i++) { + if (channels[params[0]].nicklist[i].uid === source) { + channels[params[0]].nicklist.splice(i, 1) + return + } + } + }, + QUIT: (_l, source) => { + delete users[source] + } +} + +function parser (l) { + const split = l.split(' ') + const cmd = split[0] + const args = split.slice(1).join(' ') + if (globalCommands[cmd]) return globalCommands[cmd](args) + if (cmd[0] === ':') { + const source = cmd.slice(1) + const subcmd = split[1] + const subargs = split.slice(2).join(' ') + if (servers.indexOf(source) > -1 && serverCommands[subcmd]) serverCommands[subcmd](subargs) + if (users[source] && userCommands[subcmd]) userCommands[subcmd](subargs, source) + } +} + +socket.on('data', data => { + data.split('\r\n') + .filter(l => l !== '') + .forEach(l => { + console.log('< ' + l) + parser(l) + }) +}) + +process.on('SIGINT', () => { + socket.write('QUIT\r\n') + process.exit() +}) + +module.exports.connect = conf => new Promise((resolve, reject) => { + emitter.once('ping', resolve) + config = conf + socket.connect(config.port) +}) +module.exports.events = emitter + +const genTS = () => Math.trunc((new Date()).getTime() / 1000) +const genUID = () => { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + var uid = '' + for (let i = 0; i < 6; i++) uid += chars.charAt(Math.floor(Math.random() * chars.length)) + if (users[uid]) return genUID() + return config.sid + uid +} +const getUID = nick => { + for (const key in users) if (users[key].nick === nick) return key +} + +module.exports.registerUser = nick => { + const user = { + nick: nick, + nickTS: genTS(), + modes: '+i', + username: '~' + nick, + vhost: config.vhost, + ip: '0::1', + uid: genUID() + } + users[user.uid] = user + m(`EUID ${user.nick} 1 ${user.nickTS} ${user.modes} ~${user.nick} ${user.vhost} 0::1 ${user.uid} * * :${user.nick}`) +} +module.exports.unregisterUser = nick => { + const uid = getUID(nick) + m(`:${uid} QUIT :Quit: satyr`) + delete users[uid] +} +module.exports.join = (nick, channelName) => { + const uid = getUID(nick) + if (!channels[channelName]) { + const channel = { + timestamp: genTS(), + name: channelName, + modes: '+nt', + nicklist: [{ uid: uid, mode: '' }] + } + channels[channel.name] = channel + } + m(`:${uid} JOIN ${channels[channelName].timestamp} ${channelName} +`) +} +module.exports.part = (nick, channelName) => { + const uid = getUID(nick) + m(`:${uid} PART ${channelName} :satyr`) + for (let i = 0; i < channels[channelName].nicklist.length; i++) { + if (channels[channelName].nicklist[i].uid === uid) { + channels[channelName].nicklist.splice(i, 1) + return + } + } +} +module.exports.send = (nick, channelName, message) => { + const uid = getUID(nick) + m(`:${uid} PRIVMSG ${channelName} :${message}`) + emitter.emit('message', nick, channelName, message) +}