tons of useful additions
This commit is contained in:
parent
7f28f41ca9
commit
146c7ba900
@ -1,7 +1,7 @@
|
||||
import { format } from 'util';
|
||||
import { IRCSocketConnector } from './connector/net.connector';
|
||||
import { IRCConnectionWrapper } from './irc';
|
||||
import { IIRCOptions } from './types/irc.interfaces';
|
||||
import { formatstr } from './utility/formatstr';
|
||||
|
||||
export class IRCBot extends IRCConnectionWrapper {
|
||||
constructor(options: IIRCOptions) {
|
||||
@ -9,11 +9,11 @@ export class IRCBot extends IRCConnectionWrapper {
|
||||
}
|
||||
|
||||
send(to: string, message: string, ...args: any[]) {
|
||||
this.write('PRIVMSG %s :%s', to, format(message, ...args));
|
||||
this.write('PRIVMSG %s :%s', to, formatstr(message, ...args));
|
||||
}
|
||||
|
||||
notice(to: string, message: string, ...args: any[]) {
|
||||
this.write('NOTICE %s :%s', to, format(message, ...args));
|
||||
this.write('NOTICE %s :%s', to, formatstr(message, ...args));
|
||||
}
|
||||
|
||||
nick(newNick: string) {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import net, { Socket } from 'net';
|
||||
import tls, { TLSSocket } from 'tls';
|
||||
import { formatWithOptions } from 'util';
|
||||
import { ConnectorEvents } from '../types/events';
|
||||
import { IRCConnector } from '../types/impl.interface';
|
||||
import { SimpleEventEmitter } from '../utility/simple-event-emitter';
|
||||
import { formatstr } from '../utility/formatstr';
|
||||
import { TypedEventEmitter } from '../utility/typed-event-emitter';
|
||||
|
||||
export class IRCSocketConnector
|
||||
extends SimpleEventEmitter
|
||||
extends TypedEventEmitter<ConnectorEvents>
|
||||
implements IRCConnector
|
||||
{
|
||||
connected = false;
|
||||
@ -15,12 +16,13 @@ export class IRCSocketConnector
|
||||
public secure: boolean,
|
||||
public host: string,
|
||||
public port?: number,
|
||||
public connOpts?: Record<string, unknown>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Promise<void> {
|
||||
const opts = { host: this.host, port: this.port || 6667 };
|
||||
const opts = { host: this.host, port: this.port || 6667, ...this.connOpts };
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const onConnect = () => {
|
||||
@ -49,9 +51,7 @@ export class IRCSocketConnector
|
||||
}
|
||||
|
||||
write(format: string, ...args: any[]): void {
|
||||
this.socket?.write(
|
||||
formatWithOptions({ colors: false }, format, ...args) + '\r\n',
|
||||
);
|
||||
this.socket?.write(formatstr(format, ...args) + '\r\n');
|
||||
}
|
||||
|
||||
private handle() {
|
||||
@ -64,7 +64,7 @@ export class IRCSocketConnector
|
||||
buffer = data.pop() || '';
|
||||
|
||||
data.forEach((line: string) => {
|
||||
if (line.indexOf('PING') === 0) {
|
||||
if (line.indexOf('PING') === 0 && !this.connOpts?.skipPings) {
|
||||
this.socket?.write('PONG' + line.substring(4) + '\r\n');
|
||||
return;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { formatWithOptions } from 'util';
|
||||
import { ConnectorEvents } from '../types/events';
|
||||
import { IRCConnector } from '../types/impl.interface';
|
||||
import { SimpleEventEmitter } from '../utility/simple-event-emitter';
|
||||
import { formatstr } from '../utility/formatstr';
|
||||
import { TypedEventEmitter } from '../utility/typed-event-emitter';
|
||||
|
||||
export class IRCWebSocketConnector
|
||||
extends SimpleEventEmitter
|
||||
extends TypedEventEmitter<ConnectorEvents>
|
||||
implements IRCConnector
|
||||
{
|
||||
connected = false;
|
||||
@ -13,14 +14,17 @@ export class IRCWebSocketConnector
|
||||
public secure: boolean,
|
||||
public host: string,
|
||||
public port?: number,
|
||||
public connOpts?: Record<string, unknown>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Promise<void> {
|
||||
const url = `ws${this.secure ? 's' : ''}://${this.host}:${
|
||||
this.port || 6667
|
||||
}`;
|
||||
let url = `ws${this.secure ? 's' : ''}://${this.host}:${this.port || 6667}`;
|
||||
|
||||
if (this.connOpts?.path) {
|
||||
url += ('/' + this.connOpts.path) as string;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const onConnect = () => {
|
||||
@ -46,9 +50,7 @@ export class IRCWebSocketConnector
|
||||
}
|
||||
|
||||
write(format: string, ...args: any[]): void {
|
||||
this.socket?.send(
|
||||
formatWithOptions({ colors: false }, format, ...args) + '\r\n',
|
||||
);
|
||||
this.socket?.send(formatstr(format, ...args) + '\r\n');
|
||||
}
|
||||
|
||||
private handle() {
|
||||
@ -60,7 +62,7 @@ export class IRCWebSocketConnector
|
||||
buffer = data.pop() || '';
|
||||
|
||||
data.forEach((line: string) => {
|
||||
if (line.indexOf('PING') === 0) {
|
||||
if (line.indexOf('PING') === 0 && !this.connOpts?.skipPings) {
|
||||
this.socket?.send('PONG' + line.substring(4) + '\r\n');
|
||||
return;
|
||||
}
|
||||
@ -73,7 +75,7 @@ export class IRCWebSocketConnector
|
||||
this.socket?.addEventListener('close', (err) => {
|
||||
this.connected = false;
|
||||
this.socket = undefined;
|
||||
this.emit('close', err);
|
||||
this.emit('close', err.reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,10 @@ bot.on('server-supports', (supported) => {
|
||||
console.log(supported);
|
||||
});
|
||||
|
||||
bot.on('supported-modes', (supported) => {
|
||||
console.log(supported);
|
||||
});
|
||||
|
||||
// bot.on('line', console.log);
|
||||
|
||||
bot.on('message', ({ message, to, nickname }) => {
|
||||
@ -38,14 +42,32 @@ bot.on('message', ({ message, to, nickname }) => {
|
||||
nickname,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.startsWith('!whois')) {
|
||||
bot.whois(nickname).then(console.log);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.startsWith('!who')) {
|
||||
bot.who(to).then(console.log);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.startsWith('!names')) {
|
||||
bot.names(to).then(console.log);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.startsWith('!list')) {
|
||||
bot.list().then(console.log);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.startsWith('!ping')) {
|
||||
bot.getPing().then((res) => bot.send(to, `Pong: ${res / 1000}s`));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,19 @@
|
||||
export * from './types/events';
|
||||
export * from './types/irc.interfaces';
|
||||
export * from './types/impl.interface';
|
||||
|
||||
export * from './utility/collector';
|
||||
export * from './utility/estimate-prefix';
|
||||
export * from './utility/formatstr';
|
||||
export * from './utility/mode-from-prefix';
|
||||
export * from './utility/nickserv-validator';
|
||||
export * from './utility/parser';
|
||||
export * from './utility/simple-event-emitter';
|
||||
export * from './utility/typed-event-emitter';
|
||||
export * from './utility/truncate';
|
||||
export * from './utility/user-mapper';
|
||||
export * from './utility/whois-parser';
|
||||
|
||||
export * from './spec/command-replies';
|
||||
export * from './spec/error-replies';
|
||||
|
||||
export * from './irc';
|
||||
|
143
src/irc.ts
143
src/irc.ts
@ -1,12 +1,19 @@
|
||||
import { IRCCommunicatorEvents } from './types/events';
|
||||
import {
|
||||
IRCCommunicator,
|
||||
IRCConnector,
|
||||
IRCConnectorConstructor,
|
||||
} from './types/impl.interface';
|
||||
import { IIRCLine, IIRCOptions, IQueue } from './types/irc.interfaces';
|
||||
import { Collector, WhoisCollector } from './utility/collector';
|
||||
import {
|
||||
Collector,
|
||||
MultiLineCollector,
|
||||
WhoCollector,
|
||||
WhoisCollector,
|
||||
} from './utility/collector';
|
||||
import { parse } from './utility/parser';
|
||||
import { SimpleEventEmitter } from './utility/simple-event-emitter';
|
||||
import { TypedEventEmitter } from './utility/typed-event-emitter';
|
||||
import { parseWho, WhoResponse } from './utility/who-parser';
|
||||
import { parseWhois, WhoisResponse } from './utility/whois-parser';
|
||||
|
||||
const encodeBase64 = (input: string) => {
|
||||
@ -18,8 +25,9 @@ const encodeBase64 = (input: string) => {
|
||||
return input;
|
||||
};
|
||||
|
||||
// TODO: typed emitter
|
||||
export class IRCConnectionWrapper
|
||||
extends SimpleEventEmitter
|
||||
extends TypedEventEmitter<IRCCommunicatorEvents>
|
||||
implements IRCCommunicator
|
||||
{
|
||||
public channels: string[] = [];
|
||||
@ -91,9 +99,9 @@ export class IRCConnectionWrapper
|
||||
}
|
||||
}
|
||||
|
||||
private handleServerLine(line: IIRCLine): void {
|
||||
if (this.queue.length) {
|
||||
private pumpQueue(line: IIRCLine): boolean {
|
||||
let skipHandling = false;
|
||||
if (this.queue.length) {
|
||||
const afterModifyQueue: IQueue[] = [];
|
||||
this.queue.forEach((entry) => {
|
||||
if (!entry.untracked) {
|
||||
@ -103,6 +111,7 @@ export class IRCConnectionWrapper
|
||||
line.user.nickname.toLowerCase() ===
|
||||
entry.from.toLowerCase()) ||
|
||||
!entry.from) &&
|
||||
(!entry.match || (entry.match && entry.match(line))) &&
|
||||
entry.do
|
||||
) {
|
||||
skipHandling = true;
|
||||
@ -125,10 +134,14 @@ export class IRCConnectionWrapper
|
||||
});
|
||||
|
||||
this.queue = afterModifyQueue;
|
||||
|
||||
if (skipHandling) {
|
||||
return;
|
||||
}
|
||||
|
||||
return skipHandling;
|
||||
}
|
||||
|
||||
private handleServerLine(line: IIRCLine): void {
|
||||
if (this.pumpQueue(line)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
@ -169,8 +182,9 @@ export class IRCConnectionWrapper
|
||||
break;
|
||||
}
|
||||
case '353': {
|
||||
// RPL_NAMEREPLY
|
||||
const isUpQueued = this.queue.find(
|
||||
(item) => item.await === '366' && item.from === line.arguments![2],
|
||||
(item) => item.await === '366' && item.from === line.arguments[2],
|
||||
);
|
||||
|
||||
if (isUpQueued) {
|
||||
@ -178,13 +192,13 @@ export class IRCConnectionWrapper
|
||||
} else {
|
||||
this.queue.push({
|
||||
untracked: true,
|
||||
from: line.arguments![2],
|
||||
from: line.arguments[2],
|
||||
await: '366',
|
||||
additional: ['353'],
|
||||
buffer: [line],
|
||||
do: (bline, data) => {
|
||||
this.emit('names', {
|
||||
channel: bline.arguments![1],
|
||||
channel: bline.arguments[1],
|
||||
list: [
|
||||
...data.map((cline: IIRCLine) =>
|
||||
(cline.trailing || '').split(' '),
|
||||
@ -197,8 +211,9 @@ export class IRCConnectionWrapper
|
||||
break;
|
||||
}
|
||||
case '366': {
|
||||
// RPL_ENDOFNAMES
|
||||
const isUpQueued = this.queue.find(
|
||||
(item) => item.await === '366' && item.from === line.arguments![1],
|
||||
(item) => item.await === '366' && item.from === line.arguments[1],
|
||||
);
|
||||
if (isUpQueued) {
|
||||
isUpQueued.do(line, isUpQueued.buffer);
|
||||
@ -206,18 +221,18 @@ export class IRCConnectionWrapper
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '433':
|
||||
case '433': // ERR_NICKNAMEINUSE
|
||||
const newNick = this.options.nick + '_';
|
||||
this.write('NICK %s', newNick);
|
||||
this.options.nick = newNick;
|
||||
break;
|
||||
case '904':
|
||||
case '904': // SASL fail
|
||||
this.emit('error', {
|
||||
error: new Error(line.trailing),
|
||||
fatal: true,
|
||||
});
|
||||
break;
|
||||
case '903':
|
||||
case '903': // SASL success
|
||||
this.write('CAP END');
|
||||
break;
|
||||
case 'notice':
|
||||
@ -233,7 +248,7 @@ export class IRCConnectionWrapper
|
||||
raw: line,
|
||||
});
|
||||
break;
|
||||
case '001':
|
||||
case '001': // RPL_WELCOME
|
||||
if (!this.authenticated) {
|
||||
this.serverData.name = line.user.hostname;
|
||||
this.authenticated = true;
|
||||
@ -248,6 +263,7 @@ export class IRCConnectionWrapper
|
||||
}
|
||||
break;
|
||||
case '005': {
|
||||
// RPL_ISUPPORT
|
||||
this._lastLineWasSupports = true;
|
||||
if (!line.arguments) {
|
||||
break;
|
||||
@ -288,30 +304,32 @@ export class IRCConnectionWrapper
|
||||
this.options.hostname = line.arguments?.[1];
|
||||
break;
|
||||
// Set hostname from self-whois
|
||||
case '311':
|
||||
case '311': // RPL_WHOISUSER
|
||||
if (line.arguments?.[1] !== this.options.nick) {
|
||||
return;
|
||||
}
|
||||
this.options.hostname = line.arguments?.[3];
|
||||
break;
|
||||
case '321':
|
||||
case '321': // RPL_LISTSTART
|
||||
this.emit('channel-list-item', {
|
||||
channel: 'Channel',
|
||||
users: 'Users',
|
||||
topic: 'Topic',
|
||||
});
|
||||
break;
|
||||
case '322':
|
||||
case '322': // RPL_LIST
|
||||
this.emit('channel-list-item', {
|
||||
channel: line.arguments![1],
|
||||
users: line.arguments![2],
|
||||
channel: line.arguments[1],
|
||||
users: line.arguments[2],
|
||||
topic: line.trailing,
|
||||
});
|
||||
break;
|
||||
case 'quit':
|
||||
if (line.user.nickname !== this.options.nick) {
|
||||
this.emit('leave', {
|
||||
type: 'quit',
|
||||
nickname: line.user.nickname,
|
||||
reason: line.trailing,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@ -338,14 +356,14 @@ export class IRCConnectionWrapper
|
||||
let isChannelMode = false;
|
||||
let method = '+';
|
||||
let lts = line.trailing ? line.trailing.split(' ') : [];
|
||||
if (line.arguments![0].indexOf('#') !== -1) {
|
||||
if (line.arguments[0].indexOf('#') !== -1) {
|
||||
isChannelMode = true;
|
||||
}
|
||||
|
||||
let modes: string | string[] = line.arguments![1];
|
||||
let modes: string | string[] = line.arguments[1];
|
||||
|
||||
if (!modes && line.trailing !== '') {
|
||||
modes = line.trailing!;
|
||||
modes = line.trailing;
|
||||
}
|
||||
|
||||
let sender = line.user.nickname;
|
||||
@ -365,9 +383,9 @@ export class IRCConnectionWrapper
|
||||
this.emit('channel-mode', {
|
||||
type: method,
|
||||
mode: mode,
|
||||
modeTarget: line.arguments![2]
|
||||
? line.arguments![2 + modei]
|
||||
? line.arguments![2 + modei]
|
||||
modeTarget: line.arguments[2]
|
||||
? line.arguments[2 + modei]
|
||||
? line.arguments[2 + modei]
|
||||
: lts[modei - 1]
|
||||
: lts[modei],
|
||||
...line,
|
||||
@ -384,7 +402,7 @@ export class IRCConnectionWrapper
|
||||
this.emit('user-mode', {
|
||||
type: method,
|
||||
mode: pass.join(''),
|
||||
modeTarget: line.arguments![0],
|
||||
modeTarget: line.arguments[0],
|
||||
...line,
|
||||
});
|
||||
}
|
||||
@ -399,8 +417,10 @@ export class IRCConnectionWrapper
|
||||
}
|
||||
|
||||
this.emit('leave', {
|
||||
type: line.command.toLowerCase(),
|
||||
nickname: line.user.nickname,
|
||||
channel: line.arguments?.[0],
|
||||
reason: line.trailing,
|
||||
});
|
||||
break;
|
||||
case 'error':
|
||||
@ -422,6 +442,7 @@ export class IRCConnectionWrapper
|
||||
this.options.ssl ?? false,
|
||||
this.options.host,
|
||||
this.options.port,
|
||||
this.options.connOpts,
|
||||
);
|
||||
|
||||
this.connection.on('close', (data) => {
|
||||
@ -490,6 +511,11 @@ export class IRCConnectionWrapper
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous WHOIS query on `nick`
|
||||
* @param nick Nick to query
|
||||
* @returns Parsed WHOIS response object
|
||||
*/
|
||||
public async whois(nick: string): Promise<WhoisResponse> {
|
||||
return new Promise((resolve) => {
|
||||
this.useCollector(
|
||||
@ -500,6 +526,67 @@ export class IRCConnectionWrapper
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous WHO query on `target`
|
||||
* @param target Channel or nick to query
|
||||
* @returns Parsed WHO response object array
|
||||
*/
|
||||
public async who(target: string): Promise<WhoResponse[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.useCollector(
|
||||
new WhoCollector((lines) => resolve(parseWho(lines))),
|
||||
'WHO %s',
|
||||
target,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of names in a channel asynchronously
|
||||
* @param channel Channel to query
|
||||
* @returns String list of nicks (with mode prefixes preserved)
|
||||
*/
|
||||
public async names(channel: string): Promise<string[]> {
|
||||
return new Promise((resolve) => {
|
||||
this.useCollector(
|
||||
new MultiLineCollector('366', ['353'], (lines) => {
|
||||
resolve(
|
||||
[
|
||||
...lines
|
||||
.filter(({ command }) => command === '353')
|
||||
.map((cline: IIRCLine) => (cline.trailing || '').split(' ')),
|
||||
].flat(),
|
||||
);
|
||||
}),
|
||||
'NAMES %s',
|
||||
channel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of channels asynchronously
|
||||
* @returns Channel list in `[<channel>, <# visible>, <topic>][]` format
|
||||
*/
|
||||
public async list(): Promise<string[][]> {
|
||||
return new Promise((resolve) => {
|
||||
this.useCollector(
|
||||
new MultiLineCollector('323', ['322'], (lines) => {
|
||||
resolve(
|
||||
lines
|
||||
.filter(({ command }) => command === '322')
|
||||
.map((line) => [
|
||||
line.arguments[1],
|
||||
line.arguments[2],
|
||||
line.trailing,
|
||||
]),
|
||||
);
|
||||
}),
|
||||
'LIST',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public useCollector(collector: IQueue, line: string, ...args: any[]) {
|
||||
this.queue.push(collector);
|
||||
this.write(line, ...args);
|
||||
|
110
src/spec/command-replies.ts
Normal file
110
src/spec/command-replies.ts
Normal file
@ -0,0 +1,110 @@
|
||||
export const RPL_COMMAND = [
|
||||
['300', 'RPL_NONE', ''],
|
||||
['302', 'RPL_USERHOST', ':[<reply>{<space><reply>}]'],
|
||||
['303', 'RPL_ISON', ':[<nick> {<space><nick>}]'],
|
||||
['301', 'RPL_AWAY', '<nick> :<away message>'],
|
||||
['305', 'RPL_UNAWAY', ':You are no longer marked as being away'],
|
||||
['306', 'RPL_NOWAWAY', ':You have been marked as being away'],
|
||||
['307', 'RPL_WHOISREGNICK', '<nick> :is a registered nick'],
|
||||
['311', 'RPL_WHOISUSER', '<nick> <user> <host> * :<real name>'],
|
||||
['312', 'RPL_WHOISSERVER', '<nick> <server> :<server info>'],
|
||||
['313', 'RPL_WHOISOPERATOR', '<nick> :is an IRC operator'],
|
||||
[
|
||||
'317',
|
||||
'RPL_WHOISIDLE',
|
||||
'<nick> <integer> [<integer>] :seconds idle[, signon at]',
|
||||
],
|
||||
['318', 'RPL_ENDOFWHOIS', '<nick> :End of /WHOIS list'],
|
||||
['319', 'RPL_WHOISCHANNELS', '<nick> :{[@|+]<channel><space>}'],
|
||||
['314', 'RPL_WHOWASUSER', '<nick> <user> <host> * :<real name>'],
|
||||
['369', 'RPL_ENDOFWHOWAS', '<nick> :End of WHOWAS'],
|
||||
['321', 'RPL_LISTSTART', 'Channel :Users Name'],
|
||||
['322', 'RPL_LIST', '<channel> <# visible> :<topic>'],
|
||||
['323', 'RPL_LISTEND', ':End of /LIST'],
|
||||
['324', 'RPL_CHANNELMODEIS', '<channel> <mode> <mode params>'],
|
||||
['324', 'RPL_WHOISACCOUNT', '<nick> <authname> :is logged in as'],
|
||||
['331', 'RPL_NOTOPIC', '<channel> :No topic is set'],
|
||||
['332', 'RPL_TOPIC', '<channel> :<topic>'],
|
||||
['333', 'RPL_TOPICWHOTIME', '<nick> <user> <host> :<integer>'],
|
||||
['335', 'RPL_WHOISBOT', ':is a bot on network'],
|
||||
['341', 'RPL_INVITING', '<channel> <nick>'],
|
||||
['342', 'RPL_SUMMONING', '<user> :Summoning user to IRC'],
|
||||
['351', 'RPL_VERSION', '<version>.<debuglevel> <server> :<comments>'],
|
||||
[
|
||||
'352',
|
||||
'RPL_WHOREPLY',
|
||||
'<channel> <user> <host> <server> <nick> <H|G>[*][@|+] :<hopcount> <real name>',
|
||||
],
|
||||
['315', 'RPL_ENDOFWHO', '<name> :End of /WHO list'],
|
||||
['353', 'RPL_NAMREPLY', '<channel> :[[@|+]<nick> [[@|+]<nick> [...]]]'],
|
||||
['366', 'RPL_ENDOFNAMES', '<channel> :End of /NAMES list'],
|
||||
['364', 'RPL_LINKS', '<mask> <server> :<hopcount> <server info>'],
|
||||
['365', 'RPL_ENDOFLINKS', '<mask> :End of /LINKS list'],
|
||||
['367', 'RPL_BANLIST', '<channel> <banid>'],
|
||||
['368', 'RPL_ENDOFBANLIST', '<channel> :End of channel ban list'],
|
||||
['371', 'RPL_INFO', ':<string>'],
|
||||
['374', 'RPL_ENDOFINFO', ':End of /INFO list'],
|
||||
['375', 'RPL_MOTDSTART', ':- <server> Message of the day - '],
|
||||
['372', 'RPL_MOTD', ':- <text>'],
|
||||
['376', 'RPL_ENDOFMOTD', ':End of /MOTD command'],
|
||||
['379', 'RPL_WHOISMODES', '<nick> <modes> :is using modes'],
|
||||
['381', 'RPL_YOUREOPER', ':You are now an IRC operator'],
|
||||
['382', 'RPL_REHASHING', '<config file> :Rehashing'],
|
||||
['391', 'RPL_TIME', "<server> :<string showing server's local time>"],
|
||||
['392', 'RPL_USERSSTART', ':UserID Terminal Host'],
|
||||
['393', 'RPL_USERS', ':%-8s %-9s %-8s'],
|
||||
['394', 'RPL_ENDOFUSERS', ':End of users'],
|
||||
['395', 'RPL_NOUSERS', ':Nobody logged in'],
|
||||
[
|
||||
'200',
|
||||
'RPL_TRACELINK',
|
||||
'Link <version & debug level> <destination> <next server>',
|
||||
],
|
||||
['201', 'RPL_TRACECONNECTING', 'Try. <class> <server>'],
|
||||
['202', 'RPL_TRACEHANDSHAKE', 'H.S. <class> <server>'],
|
||||
['203', 'RPL_TRACEUNKNOWN', '???? <class> [<client IP address in dot form>]'],
|
||||
['204', 'RPL_TRACEOPERATOR', 'Oper <class> <nick>'],
|
||||
['205', 'RPL_TRACEUSER', 'User <class> <nick>'],
|
||||
[
|
||||
'206',
|
||||
'RPL_TRACESERVER',
|
||||
'Serv <class> <int>S <int>C <server> <nick!user|*!*>@<host|server>',
|
||||
],
|
||||
['208', 'RPL_TRACENEWTYPE', '<newtype> 0 <client name>'],
|
||||
['261', 'RPL_TRACELOG', 'File <logfile> <debug level>'],
|
||||
[
|
||||
'211',
|
||||
'RPL_STATSLINKINFO',
|
||||
'<linkname> <sendq> <sent messages> <sent bytes> <received messages> <received bytes> <time open>',
|
||||
],
|
||||
['212', 'RPL_STATSCOMMANDS', '<command> <count>'],
|
||||
['213', 'RPL_STATSCLINE', 'C <host> * <name> <port> <class>'],
|
||||
['214', 'RPL_STATSNLINE', 'N <host> * <name> <port> <class>'],
|
||||
['215', 'RPL_STATSILINE', 'I <host> * <host> <port> <class>'],
|
||||
['216', 'RPL_STATSKLINE', 'K <host> * <username> <port> <class>'],
|
||||
[
|
||||
'218',
|
||||
'RPL_STATSYLINE',
|
||||
'Y <class> <ping frequency> <connect frequency> <max sendq>',
|
||||
],
|
||||
['219', 'RPL_ENDOFSTATS', '<stats letter> :End of /STATS report'],
|
||||
['241', 'RPL_STATSLLINE', 'L <hostmask> * <servername> <maxdepth>'],
|
||||
['242', 'RPL_STATSUPTIME', ':Server Up %d days %d:%02d:%02d'],
|
||||
['243', 'RPL_STATSOLINE', 'O <hostmask> * <name>'],
|
||||
['244', 'RPL_STATSHLINE', 'H <hostmask> * <servername>'],
|
||||
['221', 'RPL_UMODEIS', '<user mode string>'],
|
||||
[
|
||||
'251',
|
||||
'RPL_LUSERCLIENT',
|
||||
':There are <integer> users and <integer> invisible on <integer> servers',
|
||||
],
|
||||
['252', 'RPL_LUSEROP', '<integer> :operator(s) online'],
|
||||
['253', 'RPL_LUSERUNKNOWN', '<integer> :unknown connection(s)'],
|
||||
['254', 'RPL_LUSERCHANNELS', '<integer> :channels formed'],
|
||||
['255', 'RPL_LUSERME', ':I have <integer> clients and <integer> servers'],
|
||||
['256', 'RPL_ADMINME', '<server> :Administrative info'],
|
||||
['257', 'RPL_ADMINLOC1', ':<admin info>'],
|
||||
['258', 'RPL_ADMINLOC2', ':<admin info>'],
|
||||
['259', 'RPL_ADMINEMAIL', ':<admin info>'],
|
||||
['671', 'RPL_WHOISSECURE', '<nick> [<type>] :is using a secure connection'],
|
||||
];
|
59
src/spec/error-replies.ts
Normal file
59
src/spec/error-replies.ts
Normal file
@ -0,0 +1,59 @@
|
||||
export const RPL_ERROR = [
|
||||
['401', 'ERR_NOSUCHNICK', '<nickname> :No such nick/channel'],
|
||||
['402', 'ERR_NOSUCHSERVER', '<server name> :No such server'],
|
||||
['403', 'ERR_NOSUCHCHANNEL', '<channel name> :No such channel'],
|
||||
['404', 'ERR_CANNOTSENDTOCHAN', '<channel name> :Cannot send to channel'],
|
||||
[
|
||||
'405',
|
||||
'ERR_TOOMANYCHANNELS',
|
||||
'<channel name> :You have joined too many channels',
|
||||
],
|
||||
['406', 'ERR_WASNOSUCHNICK', '<nickname> :There was no such nickname'],
|
||||
[
|
||||
'407',
|
||||
'ERR_TOOMANYTARGETS',
|
||||
'<target> :Duplicate recipients. No message delivered',
|
||||
],
|
||||
['409', 'ERR_NOORIGIN', ':No origin specified'],
|
||||
['411', 'ERR_NORECIPIENT', ':No recipient given (<command>)'],
|
||||
['412', 'ERR_NOTEXTTOSEND', ':No text to send'],
|
||||
['413', 'ERR_NOTOPLEVEL', '<mask> :No toplevel domain specified'],
|
||||
['414', 'ERR_WILDTOPLEVEL', '<mask> :Wildcard in toplevel domain'],
|
||||
['421', 'ERR_UNKNOWNCOMMAND', '<command> :Unknown command'],
|
||||
['422', 'ERR_NOMOTD', ':MOTD File is missing'],
|
||||
['423', 'ERR_NOADMININFO', '<server> :No administrative info available'],
|
||||
['424', 'ERR_FILEERROR', ':File error doing <file op> on <file>'],
|
||||
['431', 'ERR_NONICKNAMEGIVEN', ':No nickname given'],
|
||||
['432', 'ERR_ERRONEUSNICKNAME', '<nick> :Erroneus nickname'],
|
||||
['433', 'ERR_NICKNAMEINUSE', '<nick> :Nickname is already in use'],
|
||||
['436', 'ERR_NICKCOLLISION', '<nick> :Nickname collision KILL'],
|
||||
[
|
||||
'441',
|
||||
'ERR_USERNOTINCHANNEL',
|
||||
"<nick> <channel> :They aren't on that channel",
|
||||
],
|
||||
['442', 'ERR_NOTONCHANNEL', "<channel> :You're not on that channel"],
|
||||
['443', 'ERR_USERONCHANNEL', '<user> <channel> :is already on channel'],
|
||||
['444', 'ERR_NOLOGIN', '<user> :User not logged in'],
|
||||
['445', 'ERR_SUMMONDISABLED', ':SUMMON has been disabled'],
|
||||
['446', 'ERR_USERSDISABLED', ':USERS has been disabled'],
|
||||
['451', 'ERR_NOTREGISTERED', ':You have not registered'],
|
||||
['461', 'ERR_NEEDMOREPARAMS', '<command> :Not enough parameters'],
|
||||
['462', 'ERR_ALREADYREGISTRED', ':You may not reregister'],
|
||||
['463', 'ERR_NOPERMFORHOST', ":Your host isn't among the privileged"],
|
||||
['464', 'ERR_PASSWDMISMATCH', ':Password incorrect'],
|
||||
['465', 'ERR_YOUREBANNEDCREEP', ':You are banned from this server'],
|
||||
['467', 'ERR_KEYSET', '<channel> :Channel key already set'],
|
||||
['471', 'ERR_CHANNELISFULL', '<channel> :Cannot join channel (+l)'],
|
||||
['472', 'ERR_UNKNOWNMODE', '<char> :is unknown mode char to me'],
|
||||
['473', 'ERR_INVITEONLYCHAN', '<channel> :Cannot join channel (+i)'],
|
||||
['474', 'ERR_BANNEDFROMCHAN', '<channel> :Cannot join channel (+b)'],
|
||||
['475', 'ERR_BADCHANNELKEY', '<channel> :Cannot join channel (+k)'],
|
||||
['481', 'ERR_NOPRIVILEGES', ":Permission Denied- You're not an IRC operator"],
|
||||
['482', 'ERR_CHANOPRIVSNEEDED', "<channel> :You're not channel operator"],
|
||||
['483', 'ERR_CANTKILLSERVER', ':You cant kill a server!'],
|
||||
['489', 'ERR_SECUREONLYCHAN', '<channel> :Secure connection required'],
|
||||
['491', 'ERR_NOOPERHOST', ':No O-lines for your host'],
|
||||
['501', 'ERR_UMODEUNKNOWNFLAG', ':Unknown MODE flag'],
|
||||
['502', 'ERR_USERSDONTMATCH', ':Cant change mode for other users'],
|
||||
];
|
@ -0,0 +1,92 @@
|
||||
import { IIRCLine } from './irc.interfaces';
|
||||
|
||||
export type ConnectorEvents = {
|
||||
error: (error: any) => void;
|
||||
data: (data: string) => void;
|
||||
close: (reason: string) => void;
|
||||
};
|
||||
|
||||
export type IRCCommunicatorEvents = {
|
||||
/**
|
||||
* Parsed line from the IRC server.
|
||||
*/
|
||||
line: (line: IIRCLine) => void;
|
||||
/**
|
||||
* Supported channel user modes from the server (e.g. `ohv: @%+`)
|
||||
*/
|
||||
'supported-modes': (modes: Record<string, unknown>) => void;
|
||||
/**
|
||||
* Everything this server supports. See IRC documentation for command `005` or `RPL_ISUPPORT` for more info.
|
||||
*/
|
||||
'server-supports': (supports: Record<string, unknown>) => void;
|
||||
/**
|
||||
* Nicks in a channel.
|
||||
*/
|
||||
names: (data: { channel: string; list: string[] }) => void;
|
||||
/**
|
||||
* Error event from the server.
|
||||
*/
|
||||
error: (data: { error: Error; fatal: boolean }) => void;
|
||||
/**
|
||||
* Message event from the server.
|
||||
*/
|
||||
message: (data: {
|
||||
type: string;
|
||||
message: string;
|
||||
to: string;
|
||||
nickname: string;
|
||||
raw: IIRCLine;
|
||||
}) => void;
|
||||
/**
|
||||
* Emitted when the login to the server was successful.
|
||||
*
|
||||
* **Note:** "login" in this case doesn't account for NickServ or SASL success.
|
||||
*/
|
||||
authenticated: (authed: boolean) => void;
|
||||
'channel-list-item': (data: {
|
||||
channel: string;
|
||||
users: string;
|
||||
topic: string;
|
||||
}) => void;
|
||||
/**
|
||||
* User leaving event. Type can be `quit`, `kick` or `part`.
|
||||
*/
|
||||
leave: (data: {
|
||||
type: string;
|
||||
nickname: string;
|
||||
channel?: string;
|
||||
reason?: string;
|
||||
}) => void;
|
||||
/**
|
||||
* User changed their nickname.
|
||||
*/
|
||||
nick: (data: { oldNick: string; newNick: string }) => void;
|
||||
/**
|
||||
* User joined a channel you are in.
|
||||
*/
|
||||
join: (data: { nickname: string; channel: string }) => void;
|
||||
/**
|
||||
* A mode was set on a user in a channel you're in.
|
||||
*/
|
||||
'channel-mode': (
|
||||
data: {
|
||||
type: string;
|
||||
mode: string;
|
||||
modeTarget: string;
|
||||
} & IIRCLine,
|
||||
) => void;
|
||||
/**
|
||||
* A mode was set on a user, usually you.
|
||||
*/
|
||||
'user-mode': (
|
||||
data: {
|
||||
type: string;
|
||||
mode: string;
|
||||
modeTarget: string;
|
||||
} & IIRCLine,
|
||||
) => void;
|
||||
/**
|
||||
* Disconnected from the server.
|
||||
*/
|
||||
disconnect: (data: { type: string; raw: any; message: string }) => void;
|
||||
};
|
@ -11,5 +11,10 @@ export interface IRCConnector extends IRCCommunicator {
|
||||
}
|
||||
|
||||
export interface IRCConnectorConstructor {
|
||||
new (secure: boolean, host: string, port?: number): IRCConnector;
|
||||
new (
|
||||
secure: boolean,
|
||||
host: string,
|
||||
port?: number,
|
||||
opts?: Record<string, unknown>,
|
||||
): IRCConnector;
|
||||
}
|
||||
|
@ -5,46 +5,145 @@ export interface IIRCUser {
|
||||
}
|
||||
|
||||
export interface IIRCLine {
|
||||
/**
|
||||
* Sender information
|
||||
*/
|
||||
user: IIRCUser;
|
||||
/**
|
||||
* Raw IRC command. You may need to map this to `RPL_ERROR` or `RPL_COMMAND` depending on your needs.
|
||||
*/
|
||||
command: string;
|
||||
arguments?: string[];
|
||||
trailing?: string;
|
||||
/**
|
||||
* Arguments of the IRC command. They appear after the command and before the trailing (` :`)
|
||||
*/
|
||||
arguments: string[];
|
||||
/**
|
||||
* Trailing of the IRC command. This is the text after ` :` (excluded)
|
||||
*/
|
||||
trailing: string;
|
||||
/**
|
||||
* Raw line from the server.
|
||||
*/
|
||||
raw: string;
|
||||
}
|
||||
|
||||
export interface IUserLine {
|
||||
command: string;
|
||||
arguments?: string[];
|
||||
arguments: string[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IQueue<T = any> {
|
||||
untracked?: boolean;
|
||||
/**
|
||||
* Wait for this command from the server.
|
||||
*/
|
||||
await: string;
|
||||
/**
|
||||
* Append lines with these commands to buffer.
|
||||
*/
|
||||
additional?: string[];
|
||||
/**
|
||||
* From nickname, used for NOTICE and PRIVMSG collectors.
|
||||
*/
|
||||
from?: string;
|
||||
/**
|
||||
* Buffer list of additional data.
|
||||
*/
|
||||
buffer?: T;
|
||||
/**
|
||||
* Resolve the collector.
|
||||
* @param line The line resolving to `await`
|
||||
* @param data Additional data in `buffer`, usually only present when using `additional`
|
||||
*/
|
||||
do(line: IIRCLine, data?: T): void;
|
||||
/**
|
||||
* Match the lines you're looking for.
|
||||
* @param line Server line
|
||||
*/
|
||||
match?(line: IIRCLine): boolean;
|
||||
/**
|
||||
* Line matching commands in `additional` will call this function.
|
||||
* Use it to populate the `buffer`.
|
||||
* @param line Server line
|
||||
*/
|
||||
digest?(line: IIRCLine): void;
|
||||
}
|
||||
|
||||
export interface INickServOptions {
|
||||
enabled: boolean;
|
||||
/**
|
||||
* NickServ login status command.
|
||||
*/
|
||||
command: string;
|
||||
/**
|
||||
* NickServ bot name, defaults to NickServ.
|
||||
*/
|
||||
nickservBot?: string;
|
||||
/**
|
||||
* Response command to wait from NickServ, defaults to NOTICE.
|
||||
*/
|
||||
responseCommand?: string;
|
||||
}
|
||||
|
||||
export interface IIRCOptions {
|
||||
/**
|
||||
* IRC nickname
|
||||
*/
|
||||
nick: string;
|
||||
/**
|
||||
* IRC server hostname
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* IRC username
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* Your user's hostname, this will be set automatically on connect.
|
||||
* Setting it manually has no effect.
|
||||
*/
|
||||
hostname?: string;
|
||||
/**
|
||||
* IRC realname
|
||||
*/
|
||||
realname?: string;
|
||||
/**
|
||||
* IRC server port
|
||||
*/
|
||||
port?: number;
|
||||
password?: string | null;
|
||||
/**
|
||||
* IRC server password.
|
||||
* Sometimes also used as nickserv password.
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* Enable SASL authentication.
|
||||
*/
|
||||
sasl?: boolean;
|
||||
/**
|
||||
* Enable SSL
|
||||
*/
|
||||
ssl?: boolean;
|
||||
/**
|
||||
* Set +B mode on self (servers may not all support this!)
|
||||
*/
|
||||
bot?: boolean;
|
||||
/**
|
||||
* List of channels to join on connect
|
||||
*/
|
||||
channels?: string[];
|
||||
/**
|
||||
* Additional NickServ options
|
||||
*/
|
||||
nickserv?: INickServOptions;
|
||||
/**
|
||||
* Additional options for connections, usually passed right along to socket
|
||||
* without additional alterations.
|
||||
*
|
||||
* Special cases for included connections:
|
||||
* - `path` - `IRCWebSocketConnector` will append this to the WebSocket URL.
|
||||
* - `skipPings` - Included connectors will not respond to PINGs if set.
|
||||
*/
|
||||
connOpts?: Record<string, unknown>;
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { IIRCLine, IQueue } from '../types/irc.interfaces';
|
||||
|
||||
/**
|
||||
* Collect a single line from the server.
|
||||
*/
|
||||
export class Collector implements IQueue<IIRCLine> {
|
||||
constructor(
|
||||
public await: string,
|
||||
private resolve: (lines: IIRCLine) => void,
|
||||
public from?: string,
|
||||
public match?: (line: IIRCLine) => boolean,
|
||||
) {}
|
||||
|
||||
do(line: IIRCLine): void {
|
||||
@ -12,6 +16,9 @@ export class Collector implements IQueue<IIRCLine> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect lines from the server.
|
||||
*/
|
||||
export class MultiLineCollector implements IQueue<IIRCLine[]> {
|
||||
public buffer: IIRCLine[] = [];
|
||||
|
||||
@ -19,6 +26,7 @@ export class MultiLineCollector implements IQueue<IIRCLine[]> {
|
||||
public await: string,
|
||||
public additional: string[],
|
||||
private resolve: (lines: IIRCLine[]) => void,
|
||||
public match?: (line: IIRCLine) => boolean,
|
||||
) {}
|
||||
|
||||
do(line: IIRCLine, data: IIRCLine[]): void {
|
||||
@ -58,7 +66,10 @@ export class MultiLineCollector implements IQueue<IIRCLine[]> {
|
||||
* `318` - End of WHOIS
|
||||
*/
|
||||
export class WhoisCollector extends MultiLineCollector {
|
||||
constructor(resolve: (lines: IIRCLine[]) => void) {
|
||||
constructor(
|
||||
resolve: (lines: IIRCLine[]) => void,
|
||||
match?: (line: IIRCLine) => boolean,
|
||||
) {
|
||||
super(
|
||||
'318', // End of WHOIS
|
||||
[
|
||||
@ -75,6 +86,7 @@ export class WhoisCollector extends MultiLineCollector {
|
||||
'317', // Sign on time and idle time
|
||||
],
|
||||
resolve,
|
||||
match,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -87,13 +99,17 @@ export class WhoisCollector extends MultiLineCollector {
|
||||
* `315` - end of WHO
|
||||
*/
|
||||
export class WhoCollector extends MultiLineCollector {
|
||||
constructor(resolve: (lines: IIRCLine[]) => void) {
|
||||
constructor(
|
||||
resolve: (lines: IIRCLine[]) => void,
|
||||
match?: (line: IIRCLine) => boolean,
|
||||
) {
|
||||
super(
|
||||
'315', // End of WHO
|
||||
[
|
||||
'352', // WHO line: <channel> <user> <host> <server> <nick> <H|G>[*][@|+] :<hopcount> <real_name>
|
||||
],
|
||||
resolve,
|
||||
match,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
25
src/utility/estimate-prefix.ts
Normal file
25
src/utility/estimate-prefix.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { IIRCOptions } from '../types/irc.interfaces';
|
||||
|
||||
/**
|
||||
* Estimate the length of the message the actual server will send to clients,
|
||||
* you can use this information to truncate/split your messages.
|
||||
*
|
||||
* `:nickname!username@hostname command args :trailing\r\n`
|
||||
* @param options IRC options (nick, hostname, username)
|
||||
* @param command Command to be sent
|
||||
* @param args Arguments for the command
|
||||
* @returns Predictive length of the message without the trailing
|
||||
*/
|
||||
export const estimateMessagePrefixLength = (
|
||||
options: IIRCOptions,
|
||||
command: string,
|
||||
args: string[],
|
||||
) => {
|
||||
const header =
|
||||
options.nick.length +
|
||||
(options.hostname || '').length +
|
||||
(options.username || '').length +
|
||||
4 +
|
||||
2;
|
||||
return command.length + args.join(' ').length + 3 + header;
|
||||
};
|
47
src/utility/formatstr.ts
Normal file
47
src/utility/formatstr.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Basic string formatting, akin to `sprintf` or `util.format`, but limited.
|
||||
* Only supports `%o` for arrays, `%j` for objects, `%s` for strings and `%d` for numbers.
|
||||
*
|
||||
* You should really be using template literals instead (developer's note to self, lol).
|
||||
* @param fmt Input
|
||||
* @param args Arguments
|
||||
* @returns Formatted string
|
||||
*/
|
||||
export function formatstr(fmt: string, ...args: any[]) {
|
||||
const re = /(%?)(%([ojds]))/g;
|
||||
if (args.length) {
|
||||
fmt = fmt.replace(re, function (match, escaped, ptn, flag) {
|
||||
let arg = args.shift();
|
||||
switch (flag) {
|
||||
case 'o':
|
||||
if (Array.isArray(arg)) {
|
||||
arg = JSON.stringify(arg);
|
||||
break;
|
||||
}
|
||||
case 's':
|
||||
arg = '' + arg;
|
||||
break;
|
||||
case 'd':
|
||||
arg = Number(arg);
|
||||
break;
|
||||
case 'j':
|
||||
arg = JSON.stringify(arg);
|
||||
break;
|
||||
}
|
||||
if (!escaped) {
|
||||
return arg;
|
||||
}
|
||||
args.unshift(arg);
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
// arguments remain after formatting
|
||||
if (args.length) {
|
||||
fmt += ' ' + args.join(' ');
|
||||
}
|
||||
|
||||
fmt = fmt.replace('%%', '%');
|
||||
|
||||
return '' + fmt;
|
||||
}
|
15
src/utility/mode-from-prefix.ts
Normal file
15
src/utility/mode-from-prefix.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Get a channel user mode from their prefix.
|
||||
* @param prefixOrNick Prefix on its own (`@`, `+`, etc) or a nick `@Diamond`
|
||||
* @param supportedModes Object of channel modes `{ 'o': '@' }`
|
||||
* @returns Mode for prefix symbol
|
||||
*/
|
||||
export const modeFromPrefix = (
|
||||
prefixOrNick: string,
|
||||
supportedModes: Record<string, string>,
|
||||
) => {
|
||||
const pfx = prefixOrNick.substring(0, 1);
|
||||
return Object.keys(supportedModes)[
|
||||
Object.values(supportedModes).indexOf(pfx)
|
||||
];
|
||||
};
|
@ -1,21 +1,11 @@
|
||||
import { IIRCLine } from '../types/irc.interfaces';
|
||||
|
||||
function parseERROR(line: string[]): IIRCLine {
|
||||
const final: IIRCLine = {
|
||||
user: { nickname: '', username: '', hostname: '' },
|
||||
command: 'ERROR',
|
||||
trailing: '',
|
||||
raw: line.join(' '),
|
||||
};
|
||||
|
||||
let pass1 = line.slice(1).join(' ');
|
||||
if (pass1.indexOf(':') === 0) {
|
||||
pass1 = pass1.substring(1);
|
||||
function skipAndLine(line: string[]): string {
|
||||
let result = line.slice(1).join(' ');
|
||||
if (result.indexOf(':') === 0) {
|
||||
result = result.substring(1);
|
||||
}
|
||||
|
||||
final.trailing = pass1;
|
||||
|
||||
return final;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parse(rawline: string): IIRCLine {
|
||||
@ -35,8 +25,11 @@ export function parse(rawline: string): IIRCLine {
|
||||
rawline.indexOf(':') === 0
|
||||
? rawline.substring(1).split(' ')
|
||||
: rawline.split(' ');
|
||||
if (pass1[0] === 'ERROR') {
|
||||
return parseERROR(pass1);
|
||||
|
||||
if (pass1[0] === 'PING' || pass1[0] === 'ERROR') {
|
||||
final.command = pass1[0];
|
||||
final.trailing = skipAndLine(pass1);
|
||||
return final;
|
||||
}
|
||||
|
||||
if (pass1[0].indexOf('!') !== -1) {
|
||||
|
@ -1,38 +0,0 @@
|
||||
export type EventHandler = (...args: any[]) => void;
|
||||
|
||||
export class SimpleEventEmitter {
|
||||
private _handlers: { [x: string]: EventHandler[] } = {};
|
||||
|
||||
on(event: string, fn: EventHandler) {
|
||||
if (typeof fn !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._handlers[event]) {
|
||||
this._handlers[event] = [];
|
||||
}
|
||||
|
||||
this._handlers[event].push(fn);
|
||||
}
|
||||
|
||||
emit(event: string, ...args: any[]): void {
|
||||
if (!this._handlers[event]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._handlers[event]
|
||||
.filter((fn) => fn && typeof fn === 'function')
|
||||
.forEach((fn) => fn(...args));
|
||||
}
|
||||
|
||||
removeEventListener(event: string, fn: EventHandler): void {
|
||||
if (!this._handlers[event] || typeof fn !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexOf = this._handlers[event].indexOf(fn);
|
||||
if (indexOf > -1) {
|
||||
this._handlers[event].splice(indexOf, 1);
|
||||
}
|
||||
}
|
||||
}
|
65
src/utility/typed-event-emitter.ts
Normal file
65
src/utility/typed-event-emitter.ts
Normal file
@ -0,0 +1,65 @@
|
||||
export type EventMap = {
|
||||
[key: string]: (...args: any) => void;
|
||||
};
|
||||
|
||||
export class TypedEventEmitter<Events extends EventMap> {
|
||||
private _handlers: {
|
||||
[x: string]: { fn: (...args: any) => void; once: boolean }[];
|
||||
} = {};
|
||||
|
||||
addListener<E extends keyof Events>(event: E, fn: Events[E], once = false) {
|
||||
if (typeof fn !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._handlers[event as string]) {
|
||||
this._handlers[event as string] = [];
|
||||
}
|
||||
|
||||
this._handlers[event as string].push({ fn, once });
|
||||
}
|
||||
|
||||
on<E extends keyof Events>(event: E, fn: Events[E]) {
|
||||
this.addListener<E>(event, fn, false);
|
||||
}
|
||||
|
||||
once<E extends keyof Events>(event: E, fn: Events[E]) {
|
||||
this.addListener<E>(event, fn, true);
|
||||
}
|
||||
|
||||
emit<E extends keyof Events>(event: E, ...args: Parameters<Events[E]>): void {
|
||||
if (!this._handlers[event as string]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._handlers[event as string]
|
||||
.filter((handler) => handler && typeof handler.fn === 'function')
|
||||
.forEach((handler) => {
|
||||
handler.fn(...(args as []));
|
||||
if (handler.once) {
|
||||
this.removeEventListener(event, handler.fn as Events[E]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeEventListener<E extends keyof Events>(event: E, fn: Events[E]): void {
|
||||
if (!this._handlers[event as string] || typeof fn !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexOf = this._handlers[event as string].findIndex(
|
||||
(entry) => entry.fn === fn,
|
||||
);
|
||||
if (indexOf > -1) {
|
||||
this._handlers[event as string].splice(indexOf, 1);
|
||||
}
|
||||
}
|
||||
|
||||
removeAllListeners<E extends keyof Events>(event: E): void {
|
||||
if (!this._handlers[event as string]) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete this._handlers[event as string];
|
||||
}
|
||||
}
|
@ -7,22 +7,22 @@ export function mapUserInput(data: IUserLine, msgMaxLength = 512): string[][] {
|
||||
case 'topic':
|
||||
output.push([
|
||||
'TOPIC %s',
|
||||
data.arguments![0],
|
||||
data.arguments[0],
|
||||
data.message !== '' ? ' :' + data.message : '',
|
||||
]);
|
||||
break;
|
||||
case 'kick':
|
||||
output.push(['KICK %s :%s', data.arguments!.join(' '), data.message]);
|
||||
output.push(['KICK %s :%s', data.arguments.join(' '), data.message]);
|
||||
break;
|
||||
case 'part':
|
||||
output.push(['PART %s :%s', data.arguments![0], data.message]);
|
||||
output.push(['PART %s :%s', data.arguments[0], data.message]);
|
||||
break;
|
||||
case 'nick':
|
||||
case 'whois':
|
||||
case 'who':
|
||||
case 'names':
|
||||
case 'join':
|
||||
output.push(['JOIN %s', data.arguments![0]]);
|
||||
output.push(['JOIN %s', data.arguments[0]]);
|
||||
break;
|
||||
case 'quit':
|
||||
output.push(['QUIT :%s', data.message]);
|
||||
@ -36,7 +36,7 @@ export function mapUserInput(data: IUserLine, msgMaxLength = 512): string[][] {
|
||||
...messages.map((msg) => [
|
||||
'%s %s :%s',
|
||||
data.command.toUpperCase(),
|
||||
data.arguments![0],
|
||||
data.arguments[0],
|
||||
msg,
|
||||
]),
|
||||
);
|
||||
@ -48,13 +48,13 @@ export function mapUserInput(data: IUserLine, msgMaxLength = 512): string[][] {
|
||||
case 'ctcp':
|
||||
let ctcpmsg = '';
|
||||
|
||||
if (data.arguments![1].toLowerCase() === 'ping') {
|
||||
if (data.arguments[1].toLowerCase() === 'ping') {
|
||||
ctcpmsg = 'PING ' + Math.floor(Date.now() / 1000);
|
||||
} else {
|
||||
ctcpmsg = data.message;
|
||||
}
|
||||
|
||||
output.push(['PRIVMSG %s :\x01%s\x01', data.arguments![0], ctcpmsg]);
|
||||
output.push(['PRIVMSG %s :\x01%s\x01', data.arguments[0], ctcpmsg]);
|
||||
break;
|
||||
default:
|
||||
output.push([data.command.toUpperCase(), data.message]);
|
||||
|
29
src/utility/who-parser.ts
Normal file
29
src/utility/who-parser.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { IIRCLine } from '../types/irc.interfaces';
|
||||
|
||||
export interface WhoResponse {
|
||||
channel?: string;
|
||||
username?: string;
|
||||
hostname?: string;
|
||||
server?: string;
|
||||
nickname?: string;
|
||||
modes?: string[];
|
||||
oper?: boolean;
|
||||
hops?: number;
|
||||
realname?: string;
|
||||
}
|
||||
|
||||
export const parseWho = (lines: IIRCLine[]): WhoResponse[] => {
|
||||
return lines
|
||||
.filter(({ command }) => command === '352')
|
||||
.map((line) => ({
|
||||
channel: line.arguments[1],
|
||||
username: line.arguments[2],
|
||||
hostname: line.arguments[3],
|
||||
server: line.arguments[4],
|
||||
nickname: line.arguments[5],
|
||||
modes: line.arguments[6].split(''),
|
||||
oper: line.arguments[6].includes('*'),
|
||||
hops: parseInt(line.trailing.split(' ')[0], 10),
|
||||
realname: line.trailing.split(' ').slice(1).join(' '),
|
||||
}));
|
||||
};
|
@ -24,8 +24,8 @@ export function parseWhois(lines: IIRCLine[]) {
|
||||
lines.forEach((line) => {
|
||||
switch (line.command) {
|
||||
case '311':
|
||||
data.nickname = line.arguments![1];
|
||||
data.hostmask = `${line.arguments![2]}@${line.arguments![3]}`;
|
||||
data.nickname = line.arguments[1];
|
||||
data.hostmask = `${line.arguments[2]}@${line.arguments[3]}`;
|
||||
data.realname = line.trailing || '';
|
||||
break;
|
||||
case '319':
|
||||
@ -38,14 +38,14 @@ export function parseWhois(lines: IIRCLine[]) {
|
||||
data.usingModes = line.trailing;
|
||||
break;
|
||||
case '312':
|
||||
data.server = line.arguments![2];
|
||||
data.server = line.arguments[2];
|
||||
data.serverName = line.trailing || '';
|
||||
break;
|
||||
case '313':
|
||||
data.title = line.trailing;
|
||||
break;
|
||||
case '330':
|
||||
data.loggedInAs = line.arguments![2];
|
||||
data.loggedInAs = line.arguments[2];
|
||||
break;
|
||||
case '335':
|
||||
data.bot = true;
|
||||
@ -57,8 +57,8 @@ export function parseWhois(lines: IIRCLine[]) {
|
||||
data.secure = true;
|
||||
break;
|
||||
case '317':
|
||||
data.signOnTime = parseInt(line.arguments![3], 10);
|
||||
data.idleSeconds = parseInt(line.arguments![2], 10);
|
||||
data.signOnTime = parseInt(line.arguments[3], 10);
|
||||
data.idleSeconds = parseInt(line.arguments[2], 10);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user