Initial commit
This commit is contained in:
commit
ea7f81d9fe
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules/
|
||||||
|
/lib/
|
3
.npmignore
Normal file
3
.npmignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
src/
|
||||||
|
.prettierrc
|
||||||
|
.vscode
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"eslint.validate": [
|
||||||
|
"typescript",
|
||||||
|
],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
}
|
||||||
|
}
|
72
package-lock.json
generated
Normal file
72
package-lock.json
generated
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name": "@icynet/irclib",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@icynet/irclib",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.7.18",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"typescript": "^4.8.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "18.7.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz",
|
||||||
|
"integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin-prettier.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "4.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz",
|
||||||
|
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": {
|
||||||
|
"version": "18.7.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz",
|
||||||
|
"integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"version": "4.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz",
|
||||||
|
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@icynet/irclib",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "IRC library written in TypeScript",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "tsc",
|
||||||
|
"prepublish": "npm run build"
|
||||||
|
},
|
||||||
|
"author": "Evert Prants",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.7.18",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"typescript": "^4.8.3"
|
||||||
|
}
|
||||||
|
}
|
22
src/bot.ts
Normal file
22
src/bot.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { format } from 'util';
|
||||||
|
import { IRCSocketConnector } from './connector/net.connector';
|
||||||
|
import { IRCConnectionWrapper } from './irc';
|
||||||
|
import { IIRCOptions } from './types/irc.interfaces';
|
||||||
|
|
||||||
|
export class IRCBot extends IRCConnectionWrapper {
|
||||||
|
constructor(options: IIRCOptions) {
|
||||||
|
super(options, IRCSocketConnector);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(to: string, message: string, ...args: any[]) {
|
||||||
|
this.write('PRIVMSG %s :%s', to, format(message, ...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
notice(to: string, message: string, ...args: any[]) {
|
||||||
|
this.write('NOTICE %s :%s', to, format(message, ...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
nick(newNick: string) {
|
||||||
|
this.write('NICK %s', newNick);
|
||||||
|
}
|
||||||
|
}
|
83
src/connector/net.connector.ts
Normal file
83
src/connector/net.connector.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import net, { Socket } from 'net';
|
||||||
|
import tls, { TLSSocket } from 'tls';
|
||||||
|
import { formatWithOptions } from 'util';
|
||||||
|
import { IRCConnector } from '../types/impl.interface';
|
||||||
|
import { SimpleEventEmitter } from '../utility/simple-event-emitter';
|
||||||
|
|
||||||
|
export class IRCSocketConnector
|
||||||
|
extends SimpleEventEmitter
|
||||||
|
implements IRCConnector
|
||||||
|
{
|
||||||
|
connected = false;
|
||||||
|
private socket?: Socket | TLSSocket;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public secure: boolean,
|
||||||
|
public host: string,
|
||||||
|
public port?: number,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(): Promise<void> {
|
||||||
|
const opts = { host: this.host, port: this.port || 6667 };
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onConnect = () => {
|
||||||
|
this.connected = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.secure) {
|
||||||
|
this.socket = tls.connect(opts, onConnect);
|
||||||
|
} else {
|
||||||
|
this.socket = net.connect(opts, onConnect);
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return reject(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
this.connected = false;
|
||||||
|
this.socket?.destroy();
|
||||||
|
this.socket = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
write(format: string, ...args: any[]): void {
|
||||||
|
this.socket?.write(
|
||||||
|
formatWithOptions({ colors: false }, format, ...args) + '\r\n',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handle() {
|
||||||
|
this.socket?.setDefaultEncoding('utf-8');
|
||||||
|
|
||||||
|
let buffer: string = '';
|
||||||
|
this.socket?.on('data', (chunk: string) => {
|
||||||
|
buffer += chunk;
|
||||||
|
const data = buffer.split('\r\n');
|
||||||
|
buffer = data.pop() || '';
|
||||||
|
|
||||||
|
data.forEach((line: string) => {
|
||||||
|
if (line.indexOf('PING') === 0) {
|
||||||
|
this.socket?.write('PONG' + line.substring(4) + '\r\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('data', line);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket?.on('error', (err) => this.emit('error', err));
|
||||||
|
this.socket?.on('close', (err) => {
|
||||||
|
this.connected = false;
|
||||||
|
this.socket = undefined;
|
||||||
|
this.emit('close', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
79
src/connector/websocket.connector.ts
Normal file
79
src/connector/websocket.connector.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { formatWithOptions } from 'util';
|
||||||
|
import { IRCConnector } from '../types/impl.interface';
|
||||||
|
import { SimpleEventEmitter } from '../utility/simple-event-emitter';
|
||||||
|
|
||||||
|
export class IRCWebSocketConnector
|
||||||
|
extends SimpleEventEmitter
|
||||||
|
implements IRCConnector
|
||||||
|
{
|
||||||
|
connected = false;
|
||||||
|
private socket?: WebSocket;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public secure: boolean,
|
||||||
|
public host: string,
|
||||||
|
public port?: number,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(): Promise<void> {
|
||||||
|
const url = `ws${this.secure ? 's' : ''}://${this.host}:${
|
||||||
|
this.port || 6667
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onConnect = () => {
|
||||||
|
this.connected = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.socket = new WebSocket(url);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return reject(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket?.addEventListener('open', onConnect);
|
||||||
|
this.handle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
this.connected = false;
|
||||||
|
this.socket?.close();
|
||||||
|
this.socket = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
write(format: string, ...args: any[]): void {
|
||||||
|
this.socket?.send(
|
||||||
|
formatWithOptions({ colors: false }, format, ...args) + '\r\n',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handle() {
|
||||||
|
let buffer: string = '';
|
||||||
|
this.socket?.addEventListener('message', (event) => {
|
||||||
|
const chunk = event.data.toString();
|
||||||
|
buffer += chunk;
|
||||||
|
const data = buffer.split('\r\n');
|
||||||
|
buffer = data.pop() || '';
|
||||||
|
|
||||||
|
data.forEach((line: string) => {
|
||||||
|
if (line.indexOf('PING') === 0) {
|
||||||
|
this.socket?.send('PONG' + line.substring(4) + '\r\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('data', line);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket?.addEventListener('error', (err) => this.emit('error', err));
|
||||||
|
this.socket?.addEventListener('close', (err) => {
|
||||||
|
this.connected = false;
|
||||||
|
this.socket = undefined;
|
||||||
|
this.emit('close', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
56
src/examples/connection-test.ts
Normal file
56
src/examples/connection-test.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { IRCBot } from '../bot';
|
||||||
|
import { NickServValidator } from '../utility/nickserv-validator';
|
||||||
|
|
||||||
|
const bot = new IRCBot({
|
||||||
|
host: 'icynet.eu',
|
||||||
|
nick: 'MyTestBot',
|
||||||
|
channels: ['#squeebot'],
|
||||||
|
nickserv: {
|
||||||
|
enabled: true,
|
||||||
|
command: 'STATUS',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nickserv = new NickServValidator(bot);
|
||||||
|
|
||||||
|
bot.on('authenticated', () => {
|
||||||
|
console.log('Successful connection!');
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.on('server-supports', (supported) => {
|
||||||
|
console.log(supported);
|
||||||
|
});
|
||||||
|
|
||||||
|
// bot.on('line', console.log);
|
||||||
|
|
||||||
|
bot.on('message', ({ message, to, nickname }) => {
|
||||||
|
console.log(`[${to}] ${nickname}: ${message}`);
|
||||||
|
|
||||||
|
if (message.startsWith('!test')) {
|
||||||
|
nickserv
|
||||||
|
.getNickStatus(nickname)
|
||||||
|
.then((valid) =>
|
||||||
|
bot.send(
|
||||||
|
to,
|
||||||
|
`Hello, %s! ${
|
||||||
|
valid ? 'You are logged in.' : 'You are not logged in.'
|
||||||
|
}`,
|
||||||
|
nickname,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.startsWith('!whois')) {
|
||||||
|
bot.whois(nickname).then(console.log);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.startsWith('!ping')) {
|
||||||
|
bot.getPing().then((res) => bot.send(to, `Pong: ${res / 1000}s`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.on('disconnect', console.log);
|
||||||
|
|
||||||
|
bot.on('names', console.log);
|
||||||
|
|
||||||
|
bot.connect();
|
12
src/index.ts
Normal file
12
src/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export * from './types/irc.interfaces';
|
||||||
|
export * from './types/impl.interface';
|
||||||
|
|
||||||
|
export * from './utility/collector';
|
||||||
|
export * from './utility/nickserv-validator';
|
||||||
|
export * from './utility/parser';
|
||||||
|
export * from './utility/simple-event-emitter';
|
||||||
|
export * from './utility/truncate';
|
||||||
|
export * from './utility/user-mapper';
|
||||||
|
export * from './utility/whois-parser';
|
||||||
|
|
||||||
|
export * from './irc';
|
519
src/irc.ts
Normal file
519
src/irc.ts
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
import {
|
||||||
|
IRCCommunicator,
|
||||||
|
IRCConnector,
|
||||||
|
IRCConnectorConstructor,
|
||||||
|
} from './types/impl.interface';
|
||||||
|
import {
|
||||||
|
IIRCLine,
|
||||||
|
IIRCOptions,
|
||||||
|
INickStore,
|
||||||
|
IQueue,
|
||||||
|
} from './types/irc.interfaces';
|
||||||
|
import { Collector, WhoisCollector } from './utility/collector';
|
||||||
|
import { parse } from './utility/parser';
|
||||||
|
import { SimpleEventEmitter } from './utility/simple-event-emitter';
|
||||||
|
import { parseWhois, WhoisResponse } from './utility/whois-parser';
|
||||||
|
|
||||||
|
const encodeBase64 = (input: string) => {
|
||||||
|
if (window !== undefined && btoa !== undefined) {
|
||||||
|
return btoa(input);
|
||||||
|
} else if (Buffer !== undefined) {
|
||||||
|
return Buffer.from(input).toString('base64');
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class IRCConnectionWrapper
|
||||||
|
extends SimpleEventEmitter
|
||||||
|
implements IRCCommunicator
|
||||||
|
{
|
||||||
|
public channels: string[] = [];
|
||||||
|
public queue: IQueue[] = [];
|
||||||
|
public nickservStore: { [key: string]: INickStore } = {};
|
||||||
|
public authenticated = false;
|
||||||
|
public serverData: { [key: string]: any } = {
|
||||||
|
name: '',
|
||||||
|
supportedModes: {},
|
||||||
|
serverSupports: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
public connection?: IRCConnector;
|
||||||
|
|
||||||
|
private _supportsDone = false;
|
||||||
|
private _lastLineWasSupports = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public options: IIRCOptions,
|
||||||
|
public connector: IRCConnectorConstructor,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
if (!options.username) {
|
||||||
|
options.username = options.nick;
|
||||||
|
}
|
||||||
|
this.handlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
write(format: string, ...args: any[]): void {
|
||||||
|
this.connection?.write(format, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlers(): void {
|
||||||
|
this.on('authenticated', () => {
|
||||||
|
if (this.options.channels?.length) {
|
||||||
|
this.joinMissingChannels(this.options.channels);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private authenticate(): void {
|
||||||
|
if (this.options.sasl) {
|
||||||
|
this.write('CAP REQ :sasl');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.password && !this.options.sasl) {
|
||||||
|
this.write('PASS %s', this.options.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.write(
|
||||||
|
'USER %s 8 * :%s',
|
||||||
|
this.options.username,
|
||||||
|
this.options.realname || 'realname',
|
||||||
|
);
|
||||||
|
this.write('NICK %s', this.options.nick);
|
||||||
|
}
|
||||||
|
|
||||||
|
private joinMissingChannels(arr: string[]): void {
|
||||||
|
if (arr) {
|
||||||
|
for (const i in arr) {
|
||||||
|
let chan = arr[i];
|
||||||
|
if (chan.indexOf('#') !== 0) {
|
||||||
|
chan = '#' + chan;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.channels.indexOf(chan) === -1) {
|
||||||
|
this.write('JOIN %s', chan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleServerLine(line: IIRCLine): void {
|
||||||
|
if (this.queue.length) {
|
||||||
|
let skipHandling = false;
|
||||||
|
const afterModifyQueue: IQueue[] = [];
|
||||||
|
this.queue.forEach((entry) => {
|
||||||
|
if (!entry.untracked) {
|
||||||
|
if (entry.await && line.command === entry.await) {
|
||||||
|
if (
|
||||||
|
((entry.from &&
|
||||||
|
line.user.nickname.toLowerCase() ===
|
||||||
|
entry.from.toLowerCase()) ||
|
||||||
|
!entry.from) &&
|
||||||
|
entry.do
|
||||||
|
) {
|
||||||
|
skipHandling = true;
|
||||||
|
entry.do(line, entry.buffer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
entry.additional &&
|
||||||
|
entry.additional.includes(line.command) &&
|
||||||
|
entry.digest
|
||||||
|
) {
|
||||||
|
skipHandling = true;
|
||||||
|
entry.digest(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterModifyQueue.push(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.queue = afterModifyQueue;
|
||||||
|
|
||||||
|
if (skipHandling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._lastLineWasSupports &&
|
||||||
|
!this._supportsDone &&
|
||||||
|
line.command !== '005'
|
||||||
|
) {
|
||||||
|
this._lastLineWasSupports = false;
|
||||||
|
this._supportsDone = true;
|
||||||
|
this.emit('supported-modes', this.serverData.supportedModes);
|
||||||
|
this.emit('server-supports', this.serverData.serverSupports);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (line.command.toLowerCase()) {
|
||||||
|
case 'cap':
|
||||||
|
if (
|
||||||
|
line.trailing === 'sasl' &&
|
||||||
|
line.arguments?.[1] === 'ACK' &&
|
||||||
|
!this.authenticated
|
||||||
|
) {
|
||||||
|
this.write('AUTHENTICATE PLAIN');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '+':
|
||||||
|
case ':+': {
|
||||||
|
if (this.authenticated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authline = encodeBase64(
|
||||||
|
this.options.nick +
|
||||||
|
'\x00' +
|
||||||
|
this.options.username +
|
||||||
|
'\x00' +
|
||||||
|
this.options.password,
|
||||||
|
);
|
||||||
|
this.write('AUTHENTICATE %s', authline);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '353': {
|
||||||
|
const isUpQueued = this.queue.find(
|
||||||
|
(item) => item.await === '366' && item.from === line.arguments![2],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isUpQueued) {
|
||||||
|
isUpQueued.buffer.push(line);
|
||||||
|
} else {
|
||||||
|
this.queue.push({
|
||||||
|
untracked: true,
|
||||||
|
from: line.arguments![2],
|
||||||
|
await: '366',
|
||||||
|
additional: ['353'],
|
||||||
|
buffer: [line],
|
||||||
|
do: (bline, data) => {
|
||||||
|
this.emit('names', {
|
||||||
|
channel: bline.arguments![1],
|
||||||
|
list: [
|
||||||
|
...data.map((cline: IIRCLine) =>
|
||||||
|
(cline.trailing || '').split(' '),
|
||||||
|
),
|
||||||
|
].flat(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '366': {
|
||||||
|
const isUpQueued = this.queue.find(
|
||||||
|
(item) => item.await === '366' && item.from === line.arguments![1],
|
||||||
|
);
|
||||||
|
if (isUpQueued) {
|
||||||
|
isUpQueued.do(line, isUpQueued.buffer);
|
||||||
|
this.queue.splice(this.queue.indexOf(isUpQueued), 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '433':
|
||||||
|
const newNick = this.options.nick + '_';
|
||||||
|
this.write('NICK %s', newNick);
|
||||||
|
this.options.nick = newNick;
|
||||||
|
break;
|
||||||
|
case '904':
|
||||||
|
this.emit('error', {
|
||||||
|
error: new Error(line.trailing),
|
||||||
|
fatal: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case '903':
|
||||||
|
this.write('CAP END');
|
||||||
|
break;
|
||||||
|
case 'notice':
|
||||||
|
case 'privmsg':
|
||||||
|
if (!line.user.nickname || line.user.nickname === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.emit('message', {
|
||||||
|
type: line.command.toLowerCase(),
|
||||||
|
message: line.trailing,
|
||||||
|
to: line.arguments?.[0],
|
||||||
|
nickname: line.user.nickname,
|
||||||
|
raw: line,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case '001':
|
||||||
|
if (!this.authenticated) {
|
||||||
|
this.serverData.name = line.user.hostname;
|
||||||
|
this.authenticated = true;
|
||||||
|
|
||||||
|
// Set nick to what the server actually thinks is our nick
|
||||||
|
this.options.nick = line.arguments?.[0] || this.options.nick;
|
||||||
|
this.authenticated = true;
|
||||||
|
this.emit('authenticated', true);
|
||||||
|
|
||||||
|
// Send a whois request for self in order to reliably fetch hostname of self
|
||||||
|
this.write('WHOIS %s', this.options.nick);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '005': {
|
||||||
|
this._lastLineWasSupports = true;
|
||||||
|
if (!line.arguments) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const argv = line.arguments?.slice(1);
|
||||||
|
for (const entry of argv) {
|
||||||
|
if (entry.indexOf('=') !== -1) {
|
||||||
|
const t = entry.split('=') as string[];
|
||||||
|
if (t[0] === 'PREFIX') {
|
||||||
|
const d = t[1].match(/\((\w+)\)(.*)/);
|
||||||
|
if (d) {
|
||||||
|
const r = d[1].split('');
|
||||||
|
const aa = d[2].split('');
|
||||||
|
r.forEach((value, index) => {
|
||||||
|
this.serverData.supportedModes[value] = aa[index];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (t[0] === 'NETWORK') {
|
||||||
|
this.serverData.network = t[1];
|
||||||
|
} else if (t[0] === 'CHANNELLEN') {
|
||||||
|
this.serverData.maxChannelLength = parseInt(t[1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
let numeral: string | number = t[1];
|
||||||
|
if (!isNaN(parseInt(numeral, 10))) {
|
||||||
|
numeral = parseInt(numeral, 10);
|
||||||
|
}
|
||||||
|
this.serverData.serverSupports[t[0]] = numeral;
|
||||||
|
} else {
|
||||||
|
this.serverData.serverSupports[entry] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Set hostname from 396 (non-standard)
|
||||||
|
case '396':
|
||||||
|
this.options.hostname = line.arguments?.[1];
|
||||||
|
break;
|
||||||
|
// Set hostname from self-whois
|
||||||
|
case '311':
|
||||||
|
if (line.arguments?.[1] !== this.options.nick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.options.hostname = line.arguments?.[3];
|
||||||
|
break;
|
||||||
|
case '321':
|
||||||
|
this.emit('channel-list-item', {
|
||||||
|
channel: 'Channel',
|
||||||
|
users: 'Users',
|
||||||
|
topic: 'Topic',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case '322':
|
||||||
|
this.emit('channel-list-item', {
|
||||||
|
channel: line.arguments![1],
|
||||||
|
users: line.arguments![2],
|
||||||
|
topic: line.trailing,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'quit':
|
||||||
|
if (line.user.nickname !== this.options.nick) {
|
||||||
|
if (this.nickservStore[line.user.nickname]) {
|
||||||
|
delete this.nickservStore[line.user.nickname];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('leave', {
|
||||||
|
nickname: line.user.nickname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'nick':
|
||||||
|
if (line.user.nickname === this.options.nick) {
|
||||||
|
this.options.nick = line.arguments?.[0] || 'unknown';
|
||||||
|
} else if (this.nickservStore[line.user.nickname]) {
|
||||||
|
delete this.nickservStore[line.user.nickname];
|
||||||
|
}
|
||||||
|
this.emit('nick', {
|
||||||
|
oldNick: line.user.nickname,
|
||||||
|
newNick: line.arguments?.[0],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'join':
|
||||||
|
if (line.user.nickname === this.options.nick && line.trailing) {
|
||||||
|
this.channels.push(line.trailing);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('join', {
|
||||||
|
nickname: line.user.nickname,
|
||||||
|
channel: line.trailing,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'mode':
|
||||||
|
let isChannelMode = false;
|
||||||
|
let method = '+';
|
||||||
|
let lts = line.trailing ? line.trailing.split(' ') : [];
|
||||||
|
if (line.arguments![0].indexOf('#') !== -1) {
|
||||||
|
isChannelMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let modes: string | string[] = line.arguments![1];
|
||||||
|
|
||||||
|
if (!modes && line.trailing !== '') {
|
||||||
|
modes = line.trailing!;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sender = line.user.nickname;
|
||||||
|
if (sender === '') {
|
||||||
|
sender = line.user.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
method = modes.substring(0, 1);
|
||||||
|
modes = modes.substring(1).split('');
|
||||||
|
let pass = [];
|
||||||
|
|
||||||
|
if (isChannelMode) {
|
||||||
|
for (let i in modes) {
|
||||||
|
let mode = modes[i];
|
||||||
|
let modei = parseInt(i);
|
||||||
|
if (this.serverData.supportedModes[mode]) {
|
||||||
|
this.emit('channel-mode', {
|
||||||
|
type: method,
|
||||||
|
mode: mode,
|
||||||
|
modeTarget: line.arguments![2]
|
||||||
|
? line.arguments![2 + modei]
|
||||||
|
? line.arguments![2 + modei]
|
||||||
|
: lts[modei - 1]
|
||||||
|
: lts[modei],
|
||||||
|
...line,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pass.push(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pass = modes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pass.length > 0) {
|
||||||
|
this.emit('user-mode', {
|
||||||
|
type: method,
|
||||||
|
mode: pass.join(''),
|
||||||
|
modeTarget: line.arguments![0],
|
||||||
|
...line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'part':
|
||||||
|
case 'kick':
|
||||||
|
if (line.user.nickname === this.options.nick && line.arguments) {
|
||||||
|
const indexAt = this.channels.indexOf(line.arguments[0]);
|
||||||
|
if (indexAt !== -1) {
|
||||||
|
this.channels.splice(indexAt, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('leave', {
|
||||||
|
nickname: line.user.nickname,
|
||||||
|
channel: line.arguments?.[0],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
this.emit('error', { fatal: true, error: new Error(line.raw) });
|
||||||
|
if (!this.authenticated) {
|
||||||
|
this.connection?.destroy();
|
||||||
|
this.connection = undefined;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connect(): Promise<void> {
|
||||||
|
if (this.connection) {
|
||||||
|
await this.connection.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connection = new this.connector(
|
||||||
|
this.options.ssl ?? false,
|
||||||
|
this.options.host,
|
||||||
|
this.options.port,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.connection.on('close', (data) => {
|
||||||
|
this.emit('disconnect', {
|
||||||
|
type: 'sock_closed',
|
||||||
|
raw: data,
|
||||||
|
message: 'Connection closed.',
|
||||||
|
});
|
||||||
|
this.connection = undefined;
|
||||||
|
this.authenticated = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.connection.on('error', (data) => {
|
||||||
|
this.emit('error', { fatal: true, error: new Error(data) });
|
||||||
|
this.connection = undefined;
|
||||||
|
this.authenticated = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.connection.on('data', (line: string) => {
|
||||||
|
const parsedLine = parse(line);
|
||||||
|
this.emit('line', parsedLine);
|
||||||
|
this.handleServerLine(parsedLine);
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.connection.connect();
|
||||||
|
|
||||||
|
this.authenticate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnect(reason?: string): Promise<void> {
|
||||||
|
if (!this.connected) {
|
||||||
|
if (this.connection) {
|
||||||
|
await this.connection.destroy();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.write('QUIT :%s', reason || 'Client exiting');
|
||||||
|
|
||||||
|
// Wait for exit
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (!this.connected) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public get connected() {
|
||||||
|
return this.connection?.connected ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPing(): Promise<number> {
|
||||||
|
return new Promise<number>((resolve) => {
|
||||||
|
const sendTime = Date.now();
|
||||||
|
|
||||||
|
this.useCollector(
|
||||||
|
new Collector('PONG', (line) => {
|
||||||
|
resolve(Date.now() - sendTime);
|
||||||
|
}),
|
||||||
|
'PING :%s',
|
||||||
|
this.serverData.name,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async whois(nick: string): Promise<WhoisResponse> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.useCollector(
|
||||||
|
new WhoisCollector((data) => resolve(parseWhois(data))),
|
||||||
|
'WHOIS %s',
|
||||||
|
nick,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public useCollector(collector: IQueue, line: string, ...args: any[]) {
|
||||||
|
this.queue.push(collector);
|
||||||
|
this.write(line, ...args);
|
||||||
|
}
|
||||||
|
}
|
0
src/types/events.ts
Normal file
0
src/types/events.ts
Normal file
15
src/types/impl.interface.ts
Normal file
15
src/types/impl.interface.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export interface IRCCommunicator {
|
||||||
|
emit(event: string, ...args: any[]): void;
|
||||||
|
on(event: string, handler: (...args: any[]) => void): void;
|
||||||
|
write(format: string, ...args: any[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRCConnector extends IRCCommunicator {
|
||||||
|
connected: boolean;
|
||||||
|
connect(): Promise<void>;
|
||||||
|
destroy(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRCConnectorConstructor {
|
||||||
|
new (secure: boolean, host: string, port?: number): IRCConnector;
|
||||||
|
}
|
55
src/types/irc.interfaces.ts
Normal file
55
src/types/irc.interfaces.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
export interface IIRCUser {
|
||||||
|
nickname: string;
|
||||||
|
username: string;
|
||||||
|
hostname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIRCLine {
|
||||||
|
user: IIRCUser;
|
||||||
|
command: string;
|
||||||
|
arguments?: string[];
|
||||||
|
trailing?: string;
|
||||||
|
raw: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserLine {
|
||||||
|
command: string;
|
||||||
|
arguments?: string[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IQueue<T = any> {
|
||||||
|
untracked?: boolean;
|
||||||
|
await: string;
|
||||||
|
additional?: string[];
|
||||||
|
from?: string;
|
||||||
|
buffer?: T;
|
||||||
|
do(line: IIRCLine, data?: T): void;
|
||||||
|
digest?(line: IIRCLine): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INickServOptions {
|
||||||
|
enabled: boolean;
|
||||||
|
command: string;
|
||||||
|
nickservBot?: string;
|
||||||
|
responseCommand?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIRCOptions {
|
||||||
|
nick: string;
|
||||||
|
host: string;
|
||||||
|
username?: string;
|
||||||
|
hostname?: string;
|
||||||
|
realname?: string;
|
||||||
|
port?: number;
|
||||||
|
password?: string | null;
|
||||||
|
sasl?: boolean;
|
||||||
|
ssl?: boolean;
|
||||||
|
channels?: string[];
|
||||||
|
nickserv?: INickServOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INickStore {
|
||||||
|
checked: number;
|
||||||
|
result: boolean;
|
||||||
|
}
|
99
src/utility/collector.ts
Normal file
99
src/utility/collector.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { IIRCLine, IQueue } from '../types/irc.interfaces';
|
||||||
|
|
||||||
|
export class Collector implements IQueue<IIRCLine> {
|
||||||
|
constructor(
|
||||||
|
public await: string,
|
||||||
|
private resolve: (lines: IIRCLine) => void,
|
||||||
|
public from?: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
do(line: IIRCLine): void {
|
||||||
|
this.resolve(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MultiLineCollector implements IQueue<IIRCLine[]> {
|
||||||
|
public buffer: IIRCLine[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public await: string,
|
||||||
|
public additional: string[],
|
||||||
|
private resolve: (lines: IIRCLine[]) => void,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
do(line: IIRCLine, data: IIRCLine[]): void {
|
||||||
|
this.resolve([...data, line]);
|
||||||
|
}
|
||||||
|
|
||||||
|
digest(line: IIRCLine): void {
|
||||||
|
this.buffer.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a full WHOIS response from the server
|
||||||
|
*
|
||||||
|
* `311` - Start of WHOIS, Nickname, hostmask, realname
|
||||||
|
*
|
||||||
|
* `319` - Channels
|
||||||
|
*
|
||||||
|
* `378` - Connecting from
|
||||||
|
*
|
||||||
|
* `379` - User modes
|
||||||
|
*
|
||||||
|
* `312` - Server and server name
|
||||||
|
*
|
||||||
|
* `313` - User title
|
||||||
|
*
|
||||||
|
* `330` - Login time
|
||||||
|
*
|
||||||
|
* `335` - Is a bot
|
||||||
|
*
|
||||||
|
* `307` - Registered
|
||||||
|
*
|
||||||
|
* `671` - Secure connection
|
||||||
|
*
|
||||||
|
* `317` - Sign on time and idle time
|
||||||
|
*
|
||||||
|
* `318` - End of WHOIS
|
||||||
|
*/
|
||||||
|
export class WhoisCollector extends MultiLineCollector {
|
||||||
|
constructor(resolve: (lines: IIRCLine[]) => void) {
|
||||||
|
super(
|
||||||
|
'318', // End of WHOIS
|
||||||
|
[
|
||||||
|
'311', // Start of WHOIS, Nickname, hostmask, realname
|
||||||
|
'319', // Channels
|
||||||
|
'378', // Connecting from
|
||||||
|
'379', // User modes
|
||||||
|
'312', // Server and server name
|
||||||
|
'313', // User title
|
||||||
|
'330', // Login time
|
||||||
|
'335', // Is a bot
|
||||||
|
'307', // Registered
|
||||||
|
'671', // Secure connection
|
||||||
|
'317', // Sign on time and idle time
|
||||||
|
],
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a full WHO response from the server
|
||||||
|
*
|
||||||
|
* `352` - WHO line: `<channel> <user> <host> <server> <nick> <H|G>[*][@|+] :<hopcount> <real_name>`
|
||||||
|
*
|
||||||
|
* `315` - end of WHO
|
||||||
|
*/
|
||||||
|
export class WhoCollector extends MultiLineCollector {
|
||||||
|
constructor(resolve: (lines: IIRCLine[]) => void) {
|
||||||
|
super(
|
||||||
|
'315', // End of WHO
|
||||||
|
[
|
||||||
|
'352', // WHO line: <channel> <user> <host> <server> <nick> <H|G>[*][@|+] :<hopcount> <real_name>
|
||||||
|
],
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
72
src/utility/nickserv-validator.ts
Normal file
72
src/utility/nickserv-validator.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { IRCConnectionWrapper } from '../irc';
|
||||||
|
import { INickServOptions, INickStore } from '../types/irc.interfaces';
|
||||||
|
import { Collector } from './collector';
|
||||||
|
|
||||||
|
export class NickServCollector extends Collector {
|
||||||
|
constructor(
|
||||||
|
nickservOptions: INickServOptions,
|
||||||
|
resolve: (authed: boolean) => void,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
nickservOptions.responseCommand || 'NOTICE',
|
||||||
|
(line) => {
|
||||||
|
if (!line.trailing) {
|
||||||
|
return resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitline = line.trailing.trim().split(' ');
|
||||||
|
let result = false;
|
||||||
|
if (splitline[2] !== '0') {
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result);
|
||||||
|
},
|
||||||
|
nickservOptions.nickservBot || 'NickServ',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NickServValidator {
|
||||||
|
public nickservStore: { [key: string]: INickStore } = {};
|
||||||
|
|
||||||
|
constructor(public irc: IRCConnectionWrapper) {}
|
||||||
|
|
||||||
|
async getNickStatus(nickname: string): Promise<boolean> {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
if (this.nickservStore[nickname] != null) {
|
||||||
|
if (this.nickservStore[nickname].result === true) {
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (this.nickservStore[nickname].checked < Date.now() - 1800000) {
|
||||||
|
// 30 minutes
|
||||||
|
delete this.nickservStore[nickname];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.irc.options.nickserv &&
|
||||||
|
this.irc.options.nickserv.enabled &&
|
||||||
|
this.irc.options.nickserv.command
|
||||||
|
) {
|
||||||
|
this.irc.useCollector(
|
||||||
|
new NickServCollector(
|
||||||
|
this.irc.options.nickserv,
|
||||||
|
(result: boolean) => {
|
||||||
|
this.nickservStore[nickname] = {
|
||||||
|
result,
|
||||||
|
checked: Date.now(),
|
||||||
|
};
|
||||||
|
resolve(result);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
'PRIVMSG nickserv :%s %s',
|
||||||
|
this.irc.options.nickserv.command,
|
||||||
|
nickname,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
63
src/utility/parser.ts
Normal file
63
src/utility/parser.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
final.trailing = pass1;
|
||||||
|
|
||||||
|
return final;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse(rawline: string): IIRCLine {
|
||||||
|
const final: IIRCLine = {
|
||||||
|
user: {
|
||||||
|
nickname: '',
|
||||||
|
username: '',
|
||||||
|
hostname: '',
|
||||||
|
},
|
||||||
|
command: '',
|
||||||
|
arguments: [],
|
||||||
|
trailing: '',
|
||||||
|
raw: rawline,
|
||||||
|
};
|
||||||
|
|
||||||
|
const pass1 =
|
||||||
|
rawline.indexOf(':') === 0
|
||||||
|
? rawline.substring(1).split(' ')
|
||||||
|
: rawline.split(' ');
|
||||||
|
if (pass1[0] === 'ERROR') {
|
||||||
|
return parseERROR(pass1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pass1[0].indexOf('!') !== -1) {
|
||||||
|
const nickuser = pass1[0].split('!');
|
||||||
|
final.user.nickname = nickuser[0];
|
||||||
|
const userhost = nickuser[1].split('@');
|
||||||
|
final.user.username = userhost[0];
|
||||||
|
final.user.hostname = userhost[1];
|
||||||
|
} else {
|
||||||
|
final.user.hostname = pass1[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
final.command = pass1[1];
|
||||||
|
|
||||||
|
const pass2 = pass1.slice(2).join(' ');
|
||||||
|
if (pass2.indexOf(':') !== -1) {
|
||||||
|
final.arguments = pass2.substring(0, pass2.indexOf(' :')).split(' ');
|
||||||
|
final.trailing = pass2.substring(pass2.indexOf(':') + 1);
|
||||||
|
} else {
|
||||||
|
final.arguments = pass2.split(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return final;
|
||||||
|
}
|
38
src/utility/simple-event-emitter.ts
Normal file
38
src/utility/simple-event-emitter.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/utility/truncate.ts
Normal file
17
src/utility/truncate.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Chop message into pieces recursively, splitting them at lenoffset
|
||||||
|
export function truncate(msg: string, lenoffset: number): string[] {
|
||||||
|
let pieces: string[] = [];
|
||||||
|
if (msg.length <= lenoffset) {
|
||||||
|
pieces.push(msg);
|
||||||
|
} else {
|
||||||
|
const m1 = msg.substring(0, lenoffset);
|
||||||
|
const m2 = msg.substring(lenoffset);
|
||||||
|
pieces.push(m1);
|
||||||
|
if (m2.length > lenoffset) {
|
||||||
|
pieces = pieces.concat(truncate(m2, lenoffset));
|
||||||
|
} else {
|
||||||
|
pieces.push(m2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pieces;
|
||||||
|
}
|
63
src/utility/user-mapper.ts
Normal file
63
src/utility/user-mapper.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { IUserLine } from '../types/irc.interfaces';
|
||||||
|
import { truncate } from './truncate';
|
||||||
|
|
||||||
|
export function mapUserInput(data: IUserLine, msgMaxLength = 512): string[][] {
|
||||||
|
let output: string[][] = [];
|
||||||
|
switch (data.command) {
|
||||||
|
case 'topic':
|
||||||
|
output.push([
|
||||||
|
'TOPIC %s',
|
||||||
|
data.arguments![0],
|
||||||
|
data.message !== '' ? ' :' + data.message : '',
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
case 'kick':
|
||||||
|
output.push(['KICK %s :%s', data.arguments!.join(' '), data.message]);
|
||||||
|
break;
|
||||||
|
case 'part':
|
||||||
|
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]]);
|
||||||
|
break;
|
||||||
|
case 'quit':
|
||||||
|
output.push(['QUIT :%s', data.message]);
|
||||||
|
break;
|
||||||
|
case 'privmsg':
|
||||||
|
case 'notice':
|
||||||
|
const split = data.message.split('\n');
|
||||||
|
for (const splitMsg of split) {
|
||||||
|
const messages = truncate(splitMsg, msgMaxLength);
|
||||||
|
output.push(
|
||||||
|
...messages.map((msg) => [
|
||||||
|
'%s %s :%s',
|
||||||
|
data.command.toUpperCase(),
|
||||||
|
data.arguments![0],
|
||||||
|
msg,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'list':
|
||||||
|
output.push([data.command.toUpperCase()]);
|
||||||
|
break;
|
||||||
|
case 'ctcp':
|
||||||
|
let ctcpmsg = '';
|
||||||
|
|
||||||
|
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]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
output.push([data.command.toUpperCase(), data.message]);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
67
src/utility/whois-parser.ts
Normal file
67
src/utility/whois-parser.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { IIRCLine } from '../types/irc.interfaces';
|
||||||
|
|
||||||
|
export interface WhoisResponse {
|
||||||
|
nickname?: string;
|
||||||
|
hostmask?: string;
|
||||||
|
realname?: string;
|
||||||
|
channels?: string[];
|
||||||
|
connectingFrom?: string;
|
||||||
|
usingModes?: string;
|
||||||
|
server?: string;
|
||||||
|
serverName?: string;
|
||||||
|
title?: string;
|
||||||
|
loggedInAs?: string;
|
||||||
|
bot?: boolean;
|
||||||
|
registered?: boolean;
|
||||||
|
secure?: boolean;
|
||||||
|
signOnTime?: number;
|
||||||
|
idleSeconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWhois(lines: IIRCLine[]) {
|
||||||
|
const data: WhoisResponse = {};
|
||||||
|
|
||||||
|
lines.forEach((line) => {
|
||||||
|
switch (line.command) {
|
||||||
|
case '311':
|
||||||
|
data.nickname = line.arguments![1];
|
||||||
|
data.hostmask = `${line.arguments![2]}@${line.arguments![3]}`;
|
||||||
|
data.realname = line.trailing || '';
|
||||||
|
break;
|
||||||
|
case '319':
|
||||||
|
data.channels = line.trailing?.split(' ');
|
||||||
|
break;
|
||||||
|
case '378':
|
||||||
|
data.connectingFrom = line.trailing;
|
||||||
|
break;
|
||||||
|
case '379':
|
||||||
|
data.usingModes = line.trailing;
|
||||||
|
break;
|
||||||
|
case '312':
|
||||||
|
data.server = line.arguments![2];
|
||||||
|
data.serverName = line.trailing || '';
|
||||||
|
break;
|
||||||
|
case '313':
|
||||||
|
data.title = line.trailing;
|
||||||
|
break;
|
||||||
|
case '330':
|
||||||
|
data.loggedInAs = line.arguments![2];
|
||||||
|
break;
|
||||||
|
case '335':
|
||||||
|
data.bot = true;
|
||||||
|
break;
|
||||||
|
case '307':
|
||||||
|
data.registered = true;
|
||||||
|
break;
|
||||||
|
case '671':
|
||||||
|
data.secure = true;
|
||||||
|
break;
|
||||||
|
case '317':
|
||||||
|
data.signOnTime = parseInt(line.arguments![3], 10);
|
||||||
|
data.idleSeconds = parseInt(line.arguments![2], 10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||||
|
"module": "commonjs" /* Specify what module code is generated. */,
|
||||||
|
"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
|
||||||
|
"outDir": "./lib" /* Specify an output folder for all emitted files. */,
|
||||||
|
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||||
|
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user