├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── channel.js ├── commands ├── index.js ├── ison.js ├── join.js ├── kick.js ├── kill.js ├── list.js ├── mode.js ├── names.js ├── nick.js ├── notice.js ├── part.js ├── pass.js ├── ping.js ├── privmsg.js ├── quit.js ├── stats.js ├── topic.js ├── user.js ├── version.js ├── who.js └── whois.js ├── example ├── index.js └── server.js ├── index.js ├── message.js ├── modes.js ├── package.json ├── parser.js ├── replies.js ├── server.js ├── test └── index.js └── user.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.log 3 | yarn.lock 4 | 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | *.log 3 | 4 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | Copyright 2018 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, 11 | OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, 12 | DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 13 | ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## IRC 2 | 3 | > IRC server implementation in Node.js 4 | 5 | ### Installation 6 | 7 | ```bash 8 | $ npm install irc-server 9 | ``` 10 | 11 | ### Example 12 | 13 | ```js 14 | const IRC = require('irc-server'); 15 | 16 | const server = IRC.createServer(); 17 | 18 | server.listen(6667); 19 | ``` 20 | 21 | ### SPEC 22 | 23 | + https://tools.ietf.org/html/rfc1459 24 | 25 | ### Contributing 26 | - Fork this Repo first 27 | - Clone your Repo 28 | - Install dependencies by `$ npm install` 29 | - Checkout a feature branch 30 | - Feel free to add your features 31 | - Make sure your features are fully tested 32 | - Publish your local branch, Open a pull request 33 | - Enjoy hacking <3 34 | 35 | ### MIT 36 | 37 | This work is licensed under the [MIT license](./LICENSE). 38 | 39 | --- -------------------------------------------------------------------------------- /channel.js: -------------------------------------------------------------------------------- 1 | const Modes = require('./modes'); 2 | const Message = require('./message'); 3 | const { debuglog } = require('util'); 4 | 5 | const debug = debuglog('ircs:Channel'); 6 | 7 | /** 8 | * Represents an IRC Channel on the server. 9 | */ 10 | class Channel { 11 | /** 12 | * Create a new channel. 13 | * 14 | * @param {string} name Channel name. (Starting with # or &, preferably.) 15 | */ 16 | constructor(name) { 17 | this.name = name 18 | this.users = [] 19 | this.topic = null 20 | 21 | this.modes = new Modes(this) 22 | } 23 | 24 | static isValidChannelName(name) { 25 | // https://tools.ietf.org/html/rfc1459#section-1.3 26 | return name.length <= 200 && 27 | (name[0] === '#' || name[0] === '&') && 28 | name.indexOf(' ') === -1 && 29 | name.indexOf(',') === -1 && 30 | name.indexOf('\x07') === -1 // ^G 31 | } 32 | 33 | /** 34 | * Joins a user into this channel. 35 | * 36 | * @param {User} user Joining user. 37 | */ 38 | join(user) { 39 | if (this.hasUser(user)) { 40 | console.log(thsi.toString()); 41 | throw new Error(`User ${user.nickname} has already join this channel ${this.name}`); 42 | } else { 43 | user.join(this); 44 | this.users.push(user); 45 | } 46 | if (this.users.length === 1) { 47 | this.addOp(user); 48 | } 49 | return this; 50 | } 51 | 52 | /** 53 | * Parts a user from this channel. 54 | * 55 | * @param {User} user Parting user. 56 | */ 57 | part(user) { 58 | let i = this.users.indexOf(user) 59 | if (i !== -1) { 60 | this.users.splice(i, 1) 61 | } 62 | i = user.channels.indexOf(this) 63 | if (i !== -1) { 64 | user.channels.splice(i, 1) 65 | } 66 | } 67 | 68 | /** 69 | * Checks if a user is in this channel. 70 | * 71 | * @param {User} user User to look for. 72 | * 73 | * @return boolean Whether the user is here. 74 | */ 75 | hasUser(user) { 76 | return this.users.indexOf(user) !== -1 77 | } 78 | 79 | /** 80 | * Sends a message to all users in a channel, including the sender. 81 | * 82 | * @param {Message} message Message to send. 83 | */ 84 | send(message) { 85 | if (!(message instanceof Message)) { 86 | message = new Message(...arguments); 87 | } 88 | debug(this.name, 'send', message.toString()); 89 | this.users.forEach(user => user.send(message)); 90 | } 91 | 92 | /** 93 | * Broadcasts a message to all users in a channel, except the sender. 94 | * 95 | * @param {Message} message Message to send. 96 | */ 97 | broadcast(message) { 98 | if (!(message instanceof Message)) { 99 | message = new Message(...arguments) 100 | } 101 | this.users.forEach((u) => { 102 | if (!u.matchesMask(message.prefix)) u.send(message) 103 | }) 104 | } 105 | 106 | addOp(user) { 107 | if (!this.hasOp(user)) { 108 | this.modes.add('o', user.nickname) 109 | } 110 | } 111 | 112 | removeOp(user) { 113 | this.modes.remove('o', user.nickname) 114 | } 115 | 116 | hasOp(user) { 117 | return this.modes.has('o', user.nickname) 118 | } 119 | 120 | addVoice(user) { 121 | if (!this.hasVoice(user)) { 122 | this.modes.add('v', user.nickname) 123 | } 124 | } 125 | 126 | removeVoice(user) { 127 | this.modes.remove('v', user.nickname) 128 | } 129 | 130 | hasVoice(user) { 131 | return this.modes.has('v', user.nickname) 132 | } 133 | 134 | addFlag(flag) { 135 | this.modes.add(flag) 136 | } 137 | 138 | removeFlag(flag) { 139 | this.modes.remove(flag) 140 | } 141 | 142 | get isPrivate() { 143 | return this.modes.has('p') 144 | } 145 | 146 | get isSecret() { 147 | return this.modes.has('s') 148 | } 149 | 150 | get isInviteOnly() { 151 | return this.modes.has('i') 152 | } 153 | 154 | get isModerated() { 155 | return this.modes.has('m') 156 | } 157 | 158 | inspect() { 159 | return this.toString(); 160 | } 161 | toString() { 162 | return ` 163 | channel name: ${this.name} 164 | topic: ${this.topic} 165 | users: ${this.users.length} 166 | ${this.users.map(u => `- ${u.nickname}`).join('\n')} 167 | `; 168 | }; 169 | } 170 | 171 | module.exports = Channel; -------------------------------------------------------------------------------- /commands/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NICK: require('./nick'), 3 | // Specifies username, hostname, servername and real name for a user. 4 | // Currently also sends a welcome message back to the user (should change) 5 | USER: require('./user'), 6 | // ISON 7 | ISON: require('./ison'), 8 | // Shows a list of known channels. 9 | LIST: require('./list'), 10 | // Joins a channel. 11 | JOIN: require('./join'), 12 | // Parts a channel. 13 | PART: require('./part'), 14 | // Sets channel modes. 15 | MODE: require('./mode'), 16 | // Sets channel topics. 17 | TOPIC: require('./topic'), 18 | // Replies with the names of all users in a channel. 19 | NAMES: require('./names'), 20 | // Replies with more info about users in a channel. 21 | WHO: require('./who'), 22 | // IRC /WHOIS command. 23 | WHOIS: require('./whois'), 24 | // Sends a message to a user or channel. 25 | PRIVMSG: require('./privmsg'), 26 | // Sends a notice to a user or channel. 27 | NOTICE: require('./notice'), 28 | // ping 29 | PING: require('./ping'), 30 | // Disconnects. 31 | QUIT: require('./quit'), 32 | }; -------------------------------------------------------------------------------- /commands/ison.js: -------------------------------------------------------------------------------- 1 | const { 2 | RPL_ISON, 3 | ERR_NEEDMOREPARAMS, 4 | } = require('../replies'); 5 | 6 | /** 7 | * Command: ISON 8 | * Parameters: {} 9 | * @docs https://tools.ietf.org/html/rfc1459#section-5.8 10 | */ 11 | const ison = ({ user, server, parameters }) => { 12 | if(parameters.length < 1) { 13 | return user.send(server, ERR_NEEDMOREPARAMS, [ 'ISON', ':Not enough parameters' ]); 14 | } 15 | const users = parameters.filter(nickname => server.findUser(nickname)); 16 | user.send(server, RPL_ISON, [user.nickname].concat(users)); 17 | }; 18 | 19 | module.exports = ison; -------------------------------------------------------------------------------- /commands/join.js: -------------------------------------------------------------------------------- 1 | const { 2 | RPL_TOPIC, 3 | RPL_NOTOPIC, 4 | ERR_NEEDMOREPARAMS 5 | } = require('../replies') 6 | 7 | const names = require('./names') 8 | 9 | function join (opts) { 10 | const { user, server, parameters: [ channelNames ] } = opts 11 | 12 | if (!channelNames) { 13 | return user.send(server, ERR_NEEDMOREPARAMS, [ 'JOIN', ':Not enough parameters' ]) 14 | } 15 | 16 | for (const channelName of channelNames.split(',')) { 17 | const channel = server.getChannel(channelName) 18 | channel.join(user) 19 | 20 | channel.send(user, 'JOIN', [ channel.name, user.username, `:${user.realname}` ]) 21 | 22 | names(Object.assign( 23 | {}, 24 | opts, 25 | { parameters: [ channelName ] } 26 | )) 27 | 28 | // Topic 29 | if (channel.topic) { 30 | user.send(server, RPL_TOPIC, [ user.nickname, channel.name, `:${channel.topic}` ]) 31 | } else { 32 | user.send(server, RPL_NOTOPIC, [ user.nickname, channel.name, ':No topic is set.' ]) 33 | } 34 | } 35 | } 36 | 37 | module.exports = join; -------------------------------------------------------------------------------- /commands/kick.js: -------------------------------------------------------------------------------- 1 | const { 2 | ERR_NEEDMOREPARAMS, 3 | ERR_NOSUCHCHANNEL, 4 | ERR_CHANOPRIVSNEEDED, 5 | ERR_BADCHANMASK, 6 | ERR_NOTONCHANNEL, 7 | } = require('../replies'); 8 | 9 | /** 10 | * @docs https://tools.ietf.org/html/rfc1459#section-4.2.8 11 | * Parameters: [] 12 | */ 13 | const kick = ({ server, user, parameters }) => { 14 | let [ channelName, user, comment ] = parameters; 15 | 16 | if (!channelName || !user) { 17 | user.send(server, ERR_NEEDMOREPARAMS, [ 'KICK', ':Not enough parameters' ]) 18 | return 19 | } 20 | 21 | const channel = server.findChannel(channelName) 22 | if (!channel) { 23 | user.send(user, ERR_NOSUCHCHANNEL, [ channelName, ':No such channel.' ]) 24 | return 25 | } 26 | if (!channel.hasUser(user)) { 27 | user.send(user, ERR_NOTONCHANNEL, [ channelName, ':No such user.' ]) 28 | return 29 | } 30 | 31 | channel.part(user) 32 | }; 33 | 34 | module.exports = kick; -------------------------------------------------------------------------------- /commands/kill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Command: KILL 3 | * Parameters: 4 | * @docs https://tools.ietf.org/html/rfc1459#section-4.6.1 5 | */ 6 | const kill = ({ server, user, parameters }) => { 7 | 8 | }; 9 | 10 | module.exports = kill; -------------------------------------------------------------------------------- /commands/list.js: -------------------------------------------------------------------------------- 1 | const { 2 | RPL_LISTSTART, 3 | RPL_LIST, 4 | RPL_LISTEND 5 | } = require('../replies') 6 | 7 | function list({ user, server, parameters: [channels] }) { 8 | channels = channels ? channels.split(',') : null 9 | 10 | user.send(server, RPL_LISTSTART, [user.nickname, 'Channel', ':Users Name']) 11 | 12 | server.channels.forEach((channel, name) => { 13 | if (channels && channels.indexOf(name) === -1) { 14 | return 15 | } 16 | 17 | if (channel.isSecret && !channel.hasUser(user)) { 18 | return 19 | } 20 | 21 | let response = [user.nickname, channel.name, channel.users.length, channel.topic || ''] 22 | if (channel.isPrivate && !channel.hasUser(user)) { 23 | response = [user.nickname, 'Prv', channel.users.length, ''] 24 | } 25 | user.send(server, RPL_LIST, response) 26 | }) 27 | 28 | user.send(server, RPL_LISTEND, [user.nickname, ':End of /LIST']) 29 | } 30 | 31 | module.exports = list; -------------------------------------------------------------------------------- /commands/mode.js: -------------------------------------------------------------------------------- 1 | const { 2 | ERR_CHANOPRIVSNEEDED, 3 | ERR_NOSUCHCHANNEL, 4 | RPL_CHANNELMODEIS 5 | } = require('../replies') 6 | 7 | module.exports = function mode ({ user, server, parameters: [ target, modes = '', ...params ] }) { 8 | const channel = server.findChannel(target) 9 | if (!channel) { 10 | user.send(server, ERR_NOSUCHCHANNEL, 11 | [ user.nickname, target, ':No such channel' ]) 12 | return 13 | } 14 | 15 | if (!modes) { 16 | // bare /MODE: return current modes 17 | const modeString = channel.modes.toString() 18 | user.send(server, RPL_CHANNELMODEIS, 19 | [ user.nickname, target, ...modeString.split(' ') ]) 20 | return 21 | } 22 | 23 | const action = modes[0] 24 | const modeChars = modes.slice(1).split('') 25 | 26 | if (!channel.hasOp(user)) { 27 | user.send(server, ERR_CHANOPRIVSNEEDED, 28 | [ user.nickname, channel.name, ':You\'re not channel operator' ]) 29 | return 30 | } 31 | 32 | modeChars.forEach((mode) => { 33 | if (action === '+') { 34 | channel.modes.add(mode, params) 35 | } else if (action === '-') { 36 | channel.modes.remove(mode, params) 37 | } 38 | }) 39 | 40 | channel.send(user, 'MODE', [ target, modes, ...params ]) 41 | } 42 | -------------------------------------------------------------------------------- /commands/names.js: -------------------------------------------------------------------------------- 1 | const { 2 | RPL_NAMREPLY, 3 | RPL_ENDOFNAMES 4 | } = require('../replies') 5 | 6 | module.exports = function names ({ user, server, parameters: [ channelName ] }) { 7 | let channel = server.findChannel(channelName) 8 | if (channel) { 9 | let names = channel.users.map((u) => { 10 | let mode = '' 11 | if (channel.hasOp(u)) mode += '@' 12 | else if (channel.hasVoice(u)) mode += '+' 13 | return mode + u.nickname 14 | }) 15 | 16 | const myMode = channel.hasOp(user) ? '@' 17 | : channel.hasVoice(user) ? '+' : '=' 18 | 19 | user.send(server, RPL_NAMREPLY, [ user.nickname, myMode, channel.name, ...names ]) 20 | user.send(server, RPL_ENDOFNAMES, [ user.nickname, channel.name, ':End of /NAMES list.' ]) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /commands/nick.js: -------------------------------------------------------------------------------- 1 | const { 2 | ERR_NONICKNAMEGIVEN, 3 | ERR_NICKNAMEINUSE 4 | } = require('../replies') 5 | const {debuglog} = require('util'); 6 | const debug = debuglog('ircs:commands:nick') 7 | 8 | module.exports = function nick ({ user, server, parameters: [ nickname ] }) { 9 | nickname = nickname.trim() 10 | 11 | debug('NICK', user.mask(), nickname) 12 | 13 | if (!nickname || nickname.length === 0) { 14 | return user.send(server, ERR_NONICKNAMEGIVEN, [ 'No nickname given' ]); 15 | } 16 | 17 | if (nickname === user.nickname) { 18 | // ignore 19 | return 20 | } 21 | 22 | const lnick = nickname.toLowerCase() 23 | if (server.users.some((us) => us.nickname && 24 | us.nickname.toLowerCase() === lnick && 25 | us !== user)) { 26 | return user.send(server, ERR_NICKNAMEINUSE, 27 | [ user.nickname, nickname, ':Nickname is already in use' ]) 28 | } 29 | user.nickname = nickname; 30 | user.send(user, 'NICK', [ nickname ]) 31 | user.channels.forEach(chan => chan.broadcast(user, 'NICK', [ nickname ])); 32 | } 33 | -------------------------------------------------------------------------------- /commands/notice.js: -------------------------------------------------------------------------------- 1 | module.exports = function notice ({ user, server, parameters: [ targetName, content ] }) { 2 | let target 3 | if (targetName[0] === '#' || targetName[0] === '&') { 4 | target = server.findChannel(targetName) 5 | if (target) { 6 | target.broadcast(user, 'NOTICE', [ target.name, `:${content}` ]) 7 | } 8 | } else { 9 | target = server.findUser(targetName) 10 | if (target) { 11 | target.send(user, 'NOTICE', [ target.nickname, `:${content}` ]) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /commands/part.js: -------------------------------------------------------------------------------- 1 | const { 2 | ERR_NOSUCHCHANNEL, 3 | ERR_NOTONCHANNEL, 4 | ERR_NEEDMOREPARAMS 5 | } = require('../replies') 6 | 7 | module.exports = function part ({ user, server, parameters: [ channelName, message ] }) { 8 | if (!channelName) { 9 | user.send(server, ERR_NEEDMOREPARAMS, [ 'PART', ':Not enough parameters' ]) 10 | return 11 | } 12 | 13 | const channel = server.findChannel(channelName) 14 | if (!channel) { 15 | user.send(user, ERR_NOSUCHCHANNEL, [ channelName, ':No such channel.' ]) 16 | return 17 | } 18 | if (!channel.hasUser(user)) { 19 | user.send(user, ERR_NOTONCHANNEL, [ channelName, ':You\'re not on that channel.' ]) 20 | return 21 | } 22 | 23 | channel.part(user) 24 | 25 | channel.send(user, 'PART', [ channel.name ]) 26 | user.send(user, 'PART', [ channel.name ]) 27 | } 28 | -------------------------------------------------------------------------------- /commands/pass.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/node-irc/9f51a0f1ac45691462df974f155e3cb2538f66d2/commands/pass.js -------------------------------------------------------------------------------- /commands/ping.js: -------------------------------------------------------------------------------- 1 | const { debuglog } = require('util'); 2 | const debug = debuglog('ircs:Command:PING'); 3 | /** 4 | * Command: PONG 5 | * Parameters: [] 6 | * @docs https://tools.ietf.org/html/rfc1459#section-4.6.3 7 | */ 8 | function PING({ server, user, parameters }) { 9 | debug('received ping', parameters); 10 | user.send(server, 'PONG', parameters); 11 | } 12 | 13 | module.exports = PING; -------------------------------------------------------------------------------- /commands/privmsg.js: -------------------------------------------------------------------------------- 1 | const { 2 | ERR_NOSUCHNICK 3 | } = require('../replies') 4 | 5 | module.exports = function privmsg ({ user, server, parameters: [ targetName, content ] }) { 6 | let target 7 | if (targetName[0] === '#' || targetName[0] === '&') { 8 | target = server.findChannel(targetName) 9 | if (target) { 10 | target.broadcast(user, 'PRIVMSG', [ target.name, `:${content}` ]); 11 | } 12 | } else { 13 | target = server.findUser(targetName) 14 | if (target) { 15 | target.send(user, 'PRIVMSG', [ target.nickname, `:${content}` ]); 16 | } 17 | } 18 | 19 | if (!target) { 20 | user.send(server, ERR_NOSUCHNICK, [ user.nickname, targetName, ':No such nick/channel' ]) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /commands/quit.js: -------------------------------------------------------------------------------- 1 | const { debuglog } = require('util'); 2 | 3 | const debug = debuglog('ircs:Command:QUIT'); 4 | 5 | function QUIT({ user, server, parameters: [ message ] }) { 6 | message = message || user.nickname 7 | debug('user quit', message); 8 | const index = server.users.indexOf(user); 9 | if(index === -1) return new Error(`No such user ${user.nickname} from this server`); 10 | server.users.splice(index, 1); 11 | user.channels.forEach(channel => { 12 | channel.part(user); 13 | channel.send(user, 'PART', [ channel.name, `:${message}` ]) 14 | }) 15 | user.end(); 16 | } 17 | 18 | module.exports = QUIT; -------------------------------------------------------------------------------- /commands/stats.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/node-irc/9f51a0f1ac45691462df974f155e3cb2538f66d2/commands/stats.js -------------------------------------------------------------------------------- /commands/topic.js: -------------------------------------------------------------------------------- 1 | const { 2 | RPL_TOPIC, 3 | RPL_NOTOPIC, 4 | ERR_NOTONCHANNEL, 5 | ERR_CHANOPRIVSNEEDED 6 | } = require('../replies') 7 | 8 | function topic({ user, server, parameters: [channelName, topic] }) { 9 | let channel = server.findChannel(channelName) 10 | if (channel) { 11 | // no new topic given, → check 12 | if (topic === undefined) { 13 | if (channel.topic) { 14 | user.send(server, RPL_TOPIC, [user.nickname, channel.name, `:${channel.topic}`]) 15 | } else { 16 | user.send(server, RPL_NOTOPIC, [user.nickname, channel.name, ':No topic is set.']) 17 | } 18 | return 19 | } 20 | 21 | if (!channel.hasUser(user)) { 22 | user.send(server, ERR_NOTONCHANNEL, [user.nickname, channel.name, ':You\'re not on that channel.']) 23 | return 24 | } 25 | if (!channel.hasOp(user)) { 26 | user.send(server, ERR_CHANOPRIVSNEEDED, [user.nickname, channel.name, ':You\'re not channel operator']) 27 | return 28 | } 29 | // empty string for topic, → clear 30 | channel.topic = topic === '' ? null : topic 31 | channel.send(user, 'TOPIC', [channel.name, topic === '' ? ':' : `:${topic}`]) 32 | } 33 | } 34 | 35 | module.exports = topic; -------------------------------------------------------------------------------- /commands/user.js: -------------------------------------------------------------------------------- 1 | const { debuglog } = require('util'); 2 | const pkg = require('../package.json') 3 | const { 4 | ERR_NEEDMOREPARAMS 5 | } = require('../replies'); 6 | 7 | const debug = debuglog('ircs:commands:user') 8 | /** 9 | * Command: USER 10 | Parameters: 11 | */ 12 | function USER({ user, server, parameters }) { 13 | 14 | if(parameters.length !== 4){ 15 | return user.send(server, ERR_NEEDMOREPARAMS, [ 'USER', ':Not enough parameters' ]); 16 | } 17 | 18 | const [ username, hostname, servername, realname ] = parameters; 19 | debug('USER', user.mask(), username, hostname, servername, realname); 20 | 21 | user.username = username; 22 | user.realname = realname; 23 | user.hostname = hostname; 24 | user.servername = servername; 25 | 26 | user.send(server, '001', [ user.nickname, ':Welcome' ]); 27 | user.send(server, '002', [ user.nickname, `:Your host is ${server.hostname} running version ${pkg.version}` ]); 28 | user.send(server, '003', [ user.nickname, `:This server was created ${server.created}` ]); 29 | user.send(server, '004', [ user.nickname, pkg.name, pkg.version ]); 30 | user.send(server, 'MODE', [ user.nickname, '+w' ]); 31 | } 32 | 33 | module.exports = USER; -------------------------------------------------------------------------------- /commands/version.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/node-irc/9f51a0f1ac45691462df974f155e3cb2538f66d2/commands/version.js -------------------------------------------------------------------------------- /commands/who.js: -------------------------------------------------------------------------------- 1 | const { 2 | RPL_WHOREPLY, 3 | RPL_ENDOFWHO 4 | } = require('../replies') 5 | 6 | function who({ user, server, parameters: [channelName] }) { 7 | let channel = server.findChannel(channelName) 8 | if (channel) { 9 | channel.users.forEach((u) => { 10 | let mode = 'H' 11 | if (channel.hasOp(u)) mode += '@' 12 | else if (channel.hasVoice(u)) mode += '+' 13 | user.send(server, RPL_WHOREPLY, [ 14 | user.nickname, channel.name, u.username, 15 | u.hostname, u.servername, u.nickname, 16 | mode, ':0', u.realname 17 | ]) 18 | }) 19 | user.send(server, RPL_ENDOFWHO, [user.nickname, channelName, ':End of /WHO list.']) 20 | } 21 | } 22 | 23 | module.exports = who; -------------------------------------------------------------------------------- /commands/whois.js: -------------------------------------------------------------------------------- 1 | const { 2 | RPL_WHOISUSER, 3 | RPL_WHOISSERVER, 4 | RPL_ENDOFWHOIS, 5 | ERR_NOSUCHNICK 6 | } = require('../replies'); 7 | /** 8 | * https://tools.ietf.org/html/rfc1459#section-4.5.2 9 | */ 10 | function whois ({ user, server, parameters: [ nickmask ] }) { 11 | const target = server.findUser(nickmask) 12 | if (target) { 13 | user.send(server, RPL_WHOISUSER, [ user.nickname, target.username, target.hostname, '*', `:${user.realname}` ]) 14 | user.send(server, RPL_WHOISSERVER, [ user.nickname, target.username, target.servername, target.servername ]) 15 | user.send(server, RPL_ENDOFWHOIS, [ user.nickname, target.username, ':End of /WHOIS list.' ]) 16 | } else { 17 | user.send(server, ERR_NOSUCHNICK, [ user.nickname, nickmask, ':No such nick/channel.' ]) 18 | user.send(server, RPL_ENDOFWHOIS, [ user.nickname, nickmask, ':End of /WHOIS list.' ]) 19 | } 20 | } 21 | 22 | module.exports = whois; -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const IRC = require('..'); 2 | 3 | const client = new IRC({ 4 | port: 6668, 5 | host: 'lsong.me', 6 | username: 'lsong', 7 | nickname: 'lsong', 8 | realname: 'Liu song' 9 | }); 10 | 11 | client.on('message', message => { 12 | console.log(message); 13 | }); 14 | 15 | client.on('PRIVMSG', ([ from, message ]) => { 16 | console.log(from, message); 17 | }); 18 | 19 | client.on('PING', x => { 20 | client.pong(x); 21 | }); 22 | 23 | setInterval(() => { 24 | client.ping(); 25 | }, 30000); 26 | 27 | (async () => { 28 | 29 | await client.connect(); 30 | await client.nick(); 31 | await client.user(); 32 | await client.join('#nodejs'); 33 | await client.send('hello world', '#nodejs'); 34 | 35 | })(); -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | const IRC = require('..'); 2 | 3 | const server = IRC.createServer(); 4 | 5 | server.listen(6667); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const tcp = require('net'); 2 | const Parser = require('./parser'); 3 | const EventEmitter = require('events'); 4 | const to = require('flush-write-stream'); 5 | 6 | class IRC extends EventEmitter { 7 | constructor(options) { 8 | super(); 9 | Object.assign(this, options); 10 | this.socket = new tcp.Socket(); 11 | this.socket.pipe(Parser()).pipe(to.obj((message, enc, cb) => { 12 | const { prefix, command, parameters } = message; 13 | this.emit(command, parameters); 14 | cb() 15 | })); 16 | } 17 | connect() { 18 | const { host, port } = this; 19 | return new Promise(resolve => { 20 | this.socket.connect(port, host, resolve); 21 | }); 22 | } 23 | write(command, parameters) { 24 | this.socket.write([command].concat(parameters).join(' ') + '\r\n'); 25 | return this; 26 | } 27 | nick() { 28 | const user = this; 29 | return this.write('NICK', [user.nickname]); 30 | } 31 | user() { 32 | const user = this; 33 | const { username, hostname = '0', servername = '*', realname } = user; 34 | return this.write('USER', [username, hostname, servername, `:${realname}`]); 35 | } 36 | join(channel) { 37 | return this.write('JOIN', [channel]); 38 | } 39 | send(message, to) { 40 | return this.write('PRIVMSG', [to, `:${message}`]); 41 | } 42 | ping() { 43 | const now = Date.now(); 44 | return this.write('PING', [now]); 45 | } 46 | pong(x) { 47 | return this.write('PING', x); 48 | } 49 | notice(message, to) { 50 | return this.write('NOTICE', [to, `:${message}`]); 51 | } 52 | part(channel, message) { 53 | return this.write('PART', [channel, message]); 54 | } 55 | quit(message) { 56 | return this.write('QUIT', [`:${message}`]); 57 | } 58 | } 59 | 60 | IRC.Server = require('./server'); 61 | IRC.createServer = options => { 62 | return new IRC.Server(options); 63 | }; 64 | 65 | module.exports = IRC; -------------------------------------------------------------------------------- /message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an IRC message. 3 | */ 4 | class Message { 5 | /** 6 | * @param {string|Object|null} prefix Message prefix. (Optional.) 7 | * @param {string} command Command name. 8 | * @param {Array.} parameters IRC Command parameters. 9 | */ 10 | constructor (prefix, command, parameters) { 11 | if (prefix && typeof prefix.mask === 'function') { 12 | prefix = prefix.mask() 13 | } 14 | 15 | /** 16 | * Message Prefix. Basically just the sender nickmask. 17 | * @member {string} 18 | */ 19 | this.prefix = prefix 20 | /** 21 | * Command, i.e. what this message actually means to us! 22 | * @member {string} 23 | */ 24 | this.command = command 25 | /** 26 | * Parameters given to this command. 27 | * @member {Array.} 28 | */ 29 | this.parameters = parameters 30 | } 31 | 32 | /** 33 | * Compiles the message back down into an IRC command string. 34 | * 35 | * @return {string} IRC command. 36 | */ 37 | toString () { 38 | return (this.prefix ? `:${this.prefix} ` : '') + 39 | this.command + 40 | (this.parameters.length ? ` ${this.parameters.join(' ')}` : '') 41 | } 42 | } 43 | 44 | module.exports = Message; -------------------------------------------------------------------------------- /modes.js: -------------------------------------------------------------------------------- 1 | const flagModeChars = [ 'p', 's', 'i', 't', 'n', 'm' ] 2 | const paramModeChars = [ 'l', 'k' ] 3 | const listModeChars = [ 'o', 'v' ] 4 | 5 | const isFlagMode = (mode) => flagModeChars.indexOf(mode) !== -1 6 | const isParamMode = (mode) => paramModeChars.indexOf(mode) !== -1 7 | const isListMode = (mode) => listModeChars.indexOf(mode) !== -1 8 | 9 | class Modes { 10 | constructor () { 11 | this.flagModes = {} 12 | this.paramModes = {} 13 | this.listModes = {} 14 | } 15 | 16 | add (mode, params = []) { 17 | if (isFlagMode(mode)) { 18 | this.flagModes[mode] = true 19 | } else if (isParamMode(mode)) { 20 | this.paramModes[mode] = params[0] 21 | } else if (isListMode(mode)) { 22 | this.listModes[mode] = (this.listModes[mode] || []).concat(params) 23 | } 24 | } 25 | 26 | remove (mode, params = []) { 27 | if (isFlagMode(mode)) { 28 | delete this.flagModes[mode] 29 | } else if (isParamMode(mode)) { 30 | delete this.paramModes[mode] 31 | } else if (isListMode(mode)) { 32 | const shouldKeep = (param) => params.every((remove) => param !== remove) 33 | this.listModes[mode] = (this.listModes[mode] || []).filter(shouldKeep) 34 | } 35 | } 36 | 37 | get (mode) { 38 | if (isFlagMode(mode)) { 39 | return !!this.flagModes[mode] 40 | } else if (isParamMode(mode)) { 41 | return this.paramModes[mode] 42 | } else if (isListMode(mode)) { 43 | return this.listModes[mode] 44 | } 45 | } 46 | 47 | has (mode, param) { 48 | if (isFlagMode(mode)) { 49 | return this.get(mode) 50 | } else if (isParamMode(mode)) { 51 | return this.paramModes[mode] != null 52 | } else if (isListMode(mode) && param) { 53 | const list = this.listModes[mode] 54 | return list && list.indexOf(param) !== -1 55 | } 56 | return false 57 | } 58 | 59 | flags () { 60 | return Object.keys(this.flagModes) 61 | } 62 | 63 | toString () { 64 | let str = '+' + this.flags().join('') 65 | let params = [] 66 | paramModeChars.forEach((mode) => { 67 | if (this.has(mode)) { 68 | str += mode 69 | params.push(this.get(mode)) 70 | } 71 | }) 72 | if (params.length > 0) { 73 | str += ' ' + params.join(' ') 74 | } 75 | return str 76 | } 77 | } 78 | 79 | Modes.isFlagMode = isFlagMode 80 | Modes.isParamMode = isParamMode 81 | Modes.isListMode = isListMode 82 | 83 | module.exports = Modes; 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "irc-server", 3 | "version": "0.0.4", 4 | "description": "IRC server implementation in Node.js", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "node test", 11 | "start": "node example" 12 | }, 13 | "keywords": [ 14 | "irc", 15 | "irc-server" 16 | ], 17 | "author": "Lsong", 18 | "license": "MIT", 19 | "dependencies": { 20 | "split2": "^2.1.1", 21 | "each-async": "^1.1.1", 22 | "stream-combiner": "^0.2.2", 23 | "flush-write-stream": "^2.0.0" 24 | }, 25 | "devDependencies": {}, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/song940/node-irc.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/song940/node-irc/issues" 32 | }, 33 | "homepage": "https://github.com/song940/node-irc#readme" 34 | } 35 | -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | const split = require('split2'); 2 | const { debuglog } = require('util'); 3 | const through = require('through2'); 4 | const combine = require('stream-combiner'); 5 | const Message = require('./message'); 6 | 7 | const debug = debuglog('ircs:MessageParser'); 8 | 9 | function MessageParser() { 10 | return combine( 11 | split('\r\n'), 12 | through.obj(parse) 13 | ) 14 | 15 | /** 16 | * Parses an individual IRC command. 17 | * 18 | * @param {string} line IRC command string. 19 | * @return {Message} 20 | */ 21 | function parse(line, enc, cb) { 22 | debug('parsing', line) 23 | 24 | let prefix 25 | let command 26 | let params 27 | 28 | if (line[0] === ':') { 29 | let prefixEnd = line.indexOf(' ') 30 | prefix = line.slice(1, prefixEnd) 31 | line = line.slice(prefixEnd + 1) 32 | } 33 | 34 | let colon = line.indexOf(' :') 35 | if (colon !== -1) { 36 | let append = line.slice(colon + 2) 37 | line = line.slice(0, colon) 38 | params = line.split(/ +/g).concat([append]) 39 | } else { 40 | params = line.split(/ +/g) 41 | } 42 | 43 | command = params.shift() 44 | cb(null, new Message(prefix, command, params)) 45 | } 46 | } 47 | 48 | module.exports = MessageParser; -------------------------------------------------------------------------------- /replies.js: -------------------------------------------------------------------------------- 1 | exports.ERR_NOSUCHNICK = 401 2 | exports.ERR_NOSUCHSERVER = 402 3 | exports.ERR_NOSUCHCHANNEL = 403 4 | exports.ERR_CANNOTSENDTOCHAN = 404 5 | exports.ERR_TOOMANYCHANNELS = 405 6 | exports.ERR_WASNOSUCKNICK = 406 7 | exports.ERR_TOOMANYTARGETS = 407 8 | exports.ERR_NOORIGIN = 409 9 | exports.ERR_NORECIPIENT = 411 10 | exports.ERR_NOTEXTTOSEND = 412 11 | exports.ERR_NOTOPLEVEL = 413 12 | exports.ERR_WILDTOPLEVEL = 414 13 | exports.ERR_UNKNOWNCOMMAND = 421 14 | exports.ERR_NOMOTD = 422 15 | exports.ERR_NOADMININFO = 423 16 | exports.ERR_FILEERROR = 424 17 | exports.ERR_NONICKNAMEGIVEN = 431 18 | exports.ERR_ERRONEUSNICKNAME = 432 19 | exports.ERR_NICKNAMEINUSE = 433 20 | exports.ERR_NICKCOLLISION = 436 21 | exports.ERR_USERNOTINCHANNEL = 441 22 | exports.ERR_NOTONCHANNEL = 442 23 | exports.ERR_USERONCHANNEL = 443 24 | exports.ERR_NOLOGIN = 444 25 | exports.ERR_SUMMONDISABLED = 445 26 | exports.ERR_USERSDISABLED = 446 27 | exports.ERR_NOTREGISTERED = 451 28 | exports.ERR_NEEDMOREPARAMS = 461 29 | exports.ERR_ALREADYREGISTRED = 462 30 | exports.ERR_NOPERMFORHOST = 463 31 | exports.ERR_PASSWDMISMATCH = 464 32 | exports.ERR_YOUREBANNEDCREEP = 465 33 | exports.ERR_KEYSET = 467 34 | exports.ERR_CHANNELISFULL = 471 35 | exports.ERR_UNKNOWNMODE = 472 36 | exports.ERR_INVITEONLYCHAN = 473 37 | exports.ERR_BANNEDFROMCHAN = 474 38 | exports.ERR_BADCHANNELKEY = 475 39 | exports.ERR_NOPRIVILEGES = 481 40 | exports.ERR_CHANOPRIVSNEEDED = 482 41 | exports.ERR_CANTKILLSERVER = 483 42 | exports.ERR_NOOPERHOST = 491 43 | exports.ERR_UMODEUNKNOWNFLAG = 501 44 | exports.ERR_USERSDONTMATCH = 502 45 | 46 | exports.RPL_NONE = 300 47 | exports.RPL_USERHOST = 302 48 | exports.RPL_ISON = 303 49 | exports.RPL_AWAY = 301 50 | exports.RPL_UNAWAY = 305 51 | exports.RPL_NOWAWAY = 306 52 | exports.RPL_WHOISUSER = 311 53 | exports.RPL_WHOISSERVER = 312 54 | exports.RPL_WHOISOPERATOR = 313 55 | exports.RPL_WHOISIDLE = 317 56 | exports.RPL_ENDOFWHOIS = 318 57 | exports.RPL_WHOISCHANNELS = 319 58 | exports.RPL_WHOWASUSER = 314 59 | exports.RPL_ENDOFWHOWAS = 369 60 | exports.RPL_LISTSTART = 321 61 | exports.RPL_LIST = 322 62 | exports.RPL_LISTEND = 323 63 | exports.RPL_CHANNELMODEIS = 324 64 | exports.RPL_NOTOPIC = 331 65 | exports.RPL_TOPIC = 332 66 | exports.RPL_INVITING = 341 67 | exports.RPL_SUMMONING = 342 68 | exports.RPL_VERSION = 351 69 | exports.RPL_WHOREPLY = 352 70 | exports.RPL_ENDOFWHO = 315 71 | exports.RPL_NAMREPLY = 353 72 | exports.RPL_ENDOFNAMES = 366 73 | exports.RPL_LINKS = 364 74 | exports.RPL_ENDOFLINKS = 365 75 | exports.RPL_BANLIST = 367 76 | exports.RPL_ENDOFBANLIST = 368 77 | exports.RPL_INFO = 371 78 | exports.RPL_ENDOFINFO = 374 79 | exports.RPL_MOTDSTART = 375 80 | exports.RPL_MOTD = 372 81 | exports.RPL_ENDOFMOTD = 376 82 | exports.RPL_YOUREOPER = 381 83 | exports.RPL_REHASHING = 382 84 | exports.RPL_TIME = 391 85 | exports.RPL_USERSSTART = 392 86 | exports.RPL_USERS = 393 87 | exports.RPL_ENDOFUSERS = 394 88 | exports.RPL_NOUSERS = 395 89 | exports.RPL_TRACELINK = 200 90 | exports.RPL_TRACECONNECTING = 201 91 | exports.RPL_TRACEHANDSHAKE = 202 92 | exports.RPL_TRACEUNKNOWN = 203 93 | exports.RPL_TRACEOPERATOR = 204 94 | exports.RPL_TRACEUSER = 205 95 | exports.RPL_TRACESERVER = 206 96 | exports.RPL_TRACENEWTYPE = 208 97 | exports.RPL_TRACELOG = 261 98 | exports.RPL_STATSLINKINFO = 211 99 | exports.RPL_STATSCOMMANDS = 212 100 | exports.RPL_STATSCLINE = 213 101 | exports.RPL_STATSNLINE = 214 102 | exports.RPL_STATSILINE = 215 103 | exports.RPL_STATSKLINE = 216 104 | exports.RPL_STATSYLINE = 218 105 | exports.RPL_ENDOFSTATS = 219 106 | exports.RPL_STATSLLINE = 241 107 | exports.RPL_STATSUPTIME = 242 108 | exports.RPL_STATSOLINE = 243 109 | exports.RPL_STATSHLINE = 244 110 | exports.RPL_UMODEIS = 221 111 | exports.RPL_LUSERCLIENT = 251 112 | exports.RPL_LUSEROP = 252 113 | exports.RPL_LUSERUNKNOWN = 253 114 | exports.RPL_LUSERCHANNELS = 254 115 | exports.RPL_LUSERME = 255 116 | exports.RPL_ADMINME = 256 117 | exports.RPL_ADMINLOC1 = 257 118 | exports.RPL_ADMINLOC2 = 258 119 | exports.RPL_ADMINEMAIL = 259 120 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const { debuglog } = require('util'); 3 | const each = require('each-async'); 4 | const writer = require('flush-write-stream'); 5 | const User = require('./user'); 6 | const Channel = require('./channel'); 7 | const Message = require('./message'); 8 | const commands = require('./commands'); 9 | 10 | const debug = debuglog('ircs:Server') 11 | 12 | /** 13 | * Represents a single IRC server. 14 | */ 15 | class Server extends net.Server { 16 | /** 17 | * Creates a server instance. 18 | * 19 | * @see Server 20 | * @return {Server} 21 | */ 22 | static createServer(options, messageHandler) { 23 | return new Server(options, messageHandler) 24 | } 25 | 26 | /** 27 | * Create an IRC server. 28 | * 29 | * @param {Object} options `net.Server` options. 30 | * @param {function()} messageHandler `net.Server` connection listener. 31 | */ 32 | constructor(options = {}, messageHandler) { 33 | super(options) 34 | this.users = []; 35 | this.middleware = []; 36 | this.created = new Date(); 37 | this.channels = new Map(); 38 | this.hostname = options.hostname || 'localhost'; 39 | this.on('connection', sock => { 40 | const user = new User(sock); 41 | this.users.push(user); 42 | this.emit('user', user); 43 | }); 44 | 45 | this.on('user', user => { 46 | user.pipe(writer.obj((message, enc, cb) => { 47 | this.emit('message', message, user); 48 | cb(); 49 | })); 50 | }); 51 | 52 | this.on('message', message => { 53 | this.execute(message); 54 | debug('message', message + '') 55 | }); 56 | 57 | for (const command in commands) { 58 | const fn = commands[command]; 59 | this.use(command, fn); 60 | } 61 | 62 | if (messageHandler) { 63 | this.on('message', messageHandler); 64 | } 65 | 66 | debug('server started') 67 | } 68 | 69 | /** 70 | * Finds a user by their nickname. 71 | * 72 | * @param {string} nickname Nickname to look for. 73 | * 74 | * @return {User|undefined} Relevant User object if found, `undefined` if not found. 75 | */ 76 | findUser(nickname) { 77 | nickname = normalize(nickname) 78 | return this.users.find(user => normalize(user.nickname) === nickname); 79 | } 80 | 81 | /** 82 | * Finds a channel on the server. 83 | * 84 | * @param {string} channelName Channel name. 85 | * 86 | * @return {Channel|undefined} Relevant Channel object if found, `undefined` if not found. 87 | */ 88 | findChannel(channelName) { 89 | return this.channels.get(normalize(channelName)) 90 | } 91 | 92 | /** 93 | * Creates a new channel with the given name. 94 | * 95 | * @param {string} channelName Channel name. 96 | * 97 | * @return {Channel} The new Channel. 98 | */ 99 | createChannel(channelName) { 100 | channelName = normalize(channelName) 101 | if (!Channel.isValidChannelName(channelName)) { 102 | throw new Error('Invalid channel name') 103 | } 104 | 105 | if (!this.channels.has(channelName)) { 106 | this.channels.set(channelName, new Channel(channelName)) 107 | } 108 | 109 | return this.channels.get(channelName) 110 | } 111 | 112 | /** 113 | * Gets a channel by name, creating a new one if it does not yet exist. 114 | * 115 | * @param {string} channelName Channel name. 116 | * 117 | * @return {Channel} The Channel. 118 | */ 119 | getChannel(channelName) { 120 | if (!Channel.isValidChannelName(channelName)) return; 121 | return this.findChannel(channelName) || this.createChannel(channelName); 122 | } 123 | 124 | /** 125 | * Checks if there is a channel of a given name. 126 | * 127 | * @param {string} channelName Channel name. 128 | * 129 | * @return {boolean} True if the channel exists, false if not. 130 | */ 131 | hasChannel(channelName) { 132 | return this.channels.has(normalize(channelName)) 133 | } 134 | 135 | use(command, fn) { 136 | if (!fn) { 137 | [command, fn] = ['', command] 138 | } 139 | debug('register middleware', command) 140 | this.middleware.push({ command, fn }) 141 | } 142 | 143 | execute(message, cb) { 144 | debug('exec', message + '') 145 | message.server = this 146 | each(this.middleware, (mw, idx, next) => { 147 | if (mw.command === '' || mw.command === message.command) { 148 | debug('executing', mw.command, message.parameters) 149 | if (mw.fn.length < 2) { 150 | mw.fn(message) 151 | next(null) 152 | } else { 153 | mw.fn(message, next) 154 | } 155 | } 156 | }, cb) 157 | } 158 | 159 | /** 160 | * Send a message to every user on the server, including the sender. 161 | * 162 | * That sounds dangerous. 163 | * 164 | * @param {Message} message Message to send. 165 | */ 166 | send(message) { 167 | if (!(message instanceof Message)) { 168 | message = new Message(...arguments) 169 | } 170 | this.users.forEach(u => { u.send(message) }) 171 | } 172 | 173 | /** 174 | * Gives the server mask. 175 | * 176 | * @return {string} Mask. 177 | */ 178 | mask() { 179 | return this.hostname; 180 | } 181 | } 182 | 183 | function normalize(str) { 184 | return str.toLowerCase().trim() 185 | // {, } and | are uppercase variants of [, ] and \ respectively 186 | .replace(/{/g, '[') 187 | .replace(/}/g, ']') 188 | .replace(/\|/g, '\\') 189 | } 190 | 191 | module.exports = Server; -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/node-irc/9f51a0f1ac45691462df974f155e3cb2538f66d2/test/index.js -------------------------------------------------------------------------------- /user.js: -------------------------------------------------------------------------------- 1 | const { debuglog } = require('util'); 2 | const to = require('flush-write-stream'); 3 | const Parser = require('./parser'); 4 | const Message = require('./message'); 5 | const { Duplex } = require('stream'); 6 | 7 | const debug = debuglog('ircs:User'); 8 | /** 9 | * Represents a User on the server. 10 | */ 11 | class User extends Duplex { 12 | /** 13 | * @param {stream.Duplex} sock Duplex Stream to read & write commands from & to. 14 | */ 15 | constructor(sock) { 16 | super({ 17 | readableObjectMode: true, 18 | writableObjectMode: true 19 | }); 20 | this.socket = sock; 21 | this.channels = []; 22 | this.nickname = null; 23 | this.hostname = sock.remoteAddress; 24 | this.on('end', () => { 25 | const message = new Message(null, 'QUIT', []); 26 | this.onReceive(message); 27 | }); 28 | sock.pipe(Parser()).pipe(to.obj((message, enc, cb) => { 29 | this.onReceive(message) 30 | cb() 31 | })); 32 | sock.on('error', e => { 33 | debug('error', e); 34 | }); 35 | sock.on('end', e => { 36 | this.emit('end', e); 37 | }); 38 | } 39 | 40 | onReceive(message) { 41 | debug('receive', message + '') 42 | message.user = this 43 | message.prefix = this.mask() 44 | this.push(message) 45 | } 46 | 47 | _read() { 48 | // 49 | } 50 | 51 | _write(message, enc, cb) { 52 | debug('write', message + ''); 53 | if (this.socket.destroyed) { 54 | debug('user socket destroyed', this.nickname); 55 | this.socket.emit('error'); 56 | return cb(); 57 | } 58 | this.socket.write(`${message}\r\n`); 59 | cb() 60 | } 61 | 62 | join(channel) { 63 | if (-1 === this.channels.indexOf(channel)) { 64 | this.channels.push(channel); 65 | } else { 66 | throw new Error(`Already join channel: ${channel.name}`); 67 | } 68 | return this; 69 | } 70 | 71 | /** 72 | * Send a message to this user. 73 | * 74 | * @param {Message} message Message to send. 75 | */ 76 | send(message) { 77 | if (!(message instanceof Message)) { 78 | message = new Message(...arguments) 79 | } 80 | debug('send', message + '') 81 | this.write(message); 82 | } 83 | 84 | /** 85 | * Check if this user is matched by a given mask. 86 | * 87 | * @param {string} mask Mask to match. 88 | * 89 | * @return {boolean} Whether the user is matched by the mask. 90 | */ 91 | matchesMask(mask) { 92 | // simple & temporary 93 | return mask === this.mask(); 94 | } 95 | 96 | /** 97 | * Gives this user's mask. 98 | * 99 | * @return {string|boolean} Mask or false if this user isn't really known yet. 100 | * @todo Just use a temporary nick or something, so we don't have to deal with `false` everywhere… 101 | */ 102 | mask() { 103 | var mask = '' 104 | if (this.nickname) { 105 | mask += this.nickname; 106 | if (this.username) { 107 | mask += `!${this.username}`; 108 | } 109 | if (this.hostname) { 110 | mask += `@${this.hostname}`; 111 | } 112 | } 113 | return mask || false; 114 | } 115 | /** 116 | * end socket 117 | */ 118 | end() { 119 | this.socket.end(); 120 | return this; 121 | } 122 | 123 | toString() { 124 | return this.mask(); 125 | } 126 | 127 | inspect() { 128 | return this.toString(); 129 | } 130 | } 131 | 132 | module.exports = User; --------------------------------------------------------------------------------