From 280c8a5e2f615621cb2069f81783f93ce5163d35 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sat, 24 Sep 2022 14:00:12 +0300 Subject: [PATCH] add a nicklist manager class --- .gitignore | 1 + package-lock.json | 173 +++++++++ package.json | 1 + src/examples/connection-test.ts | 4 + src/index.ts | 1 + src/irc.ts | 4 +- src/utility/collector.ts | 14 +- src/utility/nicklist/index.ts | 3 + src/utility/nicklist/nicklist.events.ts | 4 + src/utility/nicklist/nicklist.interfaces.ts | 11 + src/utility/nicklist/nicklist.ts | 378 ++++++++++++++++++++ 11 files changed, 582 insertions(+), 12 deletions(-) create mode 100644 src/utility/nicklist/index.ts create mode 100644 src/utility/nicklist/nicklist.events.ts create mode 100644 src/utility/nicklist/nicklist.interfaces.ts create mode 100644 src/utility/nicklist/nicklist.ts diff --git a/.gitignore b/.gitignore index 8f028e4..d62bec7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules/ /lib/ +/docs diff --git a/package-lock.json b/package-lock.json index c9a301b..279f141 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@types/node": "^18.7.18", "prettier": "^2.7.1", + "typedoc": "^0.23.15", "typescript": "^4.8.3" } }, @@ -20,6 +21,57 @@ "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==", "dev": true }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "node_modules/marked": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.1.0.tgz", + "integrity": "sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prettier": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", @@ -35,6 +87,38 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/shiki": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.11.1.tgz", + "integrity": "sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-oniguruma": "^1.6.1", + "vscode-textmate": "^6.0.0" + } + }, + "node_modules/typedoc": { + "version": "0.23.15", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.15.tgz", + "integrity": "sha512-x9Zu+tTnwxb9YdVr+zvX7LYzyBl1nieOr6lrSHbHsA22/RJK2m4Y525WIg5Mj4jWCmfL47v6f4hUzY7EIuwS5w==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.0.19", + "minimatch": "^5.1.0", + "shiki": "^0.11.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 14.14" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x" + } + }, "node_modules/typescript": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", @@ -47,6 +131,18 @@ "engines": { "node": ">=4.2.0" } + }, + "node_modules/vscode-oniguruma": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz", + "integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-6.0.0.tgz", + "integrity": "sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==", + "dev": true } }, "dependencies": { @@ -56,17 +152,94 @@ "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==", "dev": true }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "marked": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.1.0.tgz", + "integrity": "sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA==", + "dev": true + }, + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, "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 }, + "shiki": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.11.1.tgz", + "integrity": "sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==", + "dev": true, + "requires": { + "jsonc-parser": "^3.0.0", + "vscode-oniguruma": "^1.6.1", + "vscode-textmate": "^6.0.0" + } + }, + "typedoc": { + "version": "0.23.15", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.15.tgz", + "integrity": "sha512-x9Zu+tTnwxb9YdVr+zvX7LYzyBl1nieOr6lrSHbHsA22/RJK2m4Y525WIg5Mj4jWCmfL47v6f4hUzY7EIuwS5w==", + "dev": true, + "requires": { + "lunr": "^2.3.9", + "marked": "^4.0.19", + "minimatch": "^5.1.0", + "shiki": "^0.11.1" + } + }, "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 + }, + "vscode-oniguruma": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz", + "integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==", + "dev": true + }, + "vscode-textmate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-6.0.0.tgz", + "integrity": "sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==", + "dev": true } } } diff --git a/package.json b/package.json index a136aa6..cf5beae 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@types/node": "^18.7.18", "prettier": "^2.7.1", + "typedoc": "^0.23.15", "typescript": "^4.8.3" } } diff --git a/src/examples/connection-test.ts b/src/examples/connection-test.ts index 588e217..1f13c97 100644 --- a/src/examples/connection-test.ts +++ b/src/examples/connection-test.ts @@ -4,6 +4,7 @@ import { applyTextColor, wrapFormattedText, } from '../utility/message-formatting'; +import { IRCNickList } from '../utility/nicklist'; import { NickServValidator } from '../utility/nickserv-validator'; const bot = new IRCBot({ @@ -18,6 +19,7 @@ const bot = new IRCBot({ }); const nickserv = new NickServValidator(bot); +const nicklist = new IRCNickList(bot); bot.on('authenticated', () => { console.log('Successful connection!'); @@ -91,4 +93,6 @@ bot.on('disconnect', console.log); bot.on('names', console.log); +nicklist.on('update', () => console.log(JSON.stringify(nicklist.channels))); + bot.connect(); diff --git a/src/index.ts b/src/index.ts index 0050ea5..0bbd32b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from './types/events'; export * from './types/irc.interfaces'; export * from './types/impl.interface'; +export * from './utility/nicklist'; export * from './utility/collector'; export * from './utility/estimate-prefix'; export * from './utility/formatstr'; diff --git a/src/irc.ts b/src/irc.ts index 7e14312..4ebf4fa 100644 --- a/src/irc.ts +++ b/src/irc.ts @@ -560,7 +560,7 @@ export class IRCConnectionWrapper public async whois(nick: string): Promise { return new Promise((resolve) => { this.useCollector( - new WhoisCollector((data) => resolve(parseWhois(data))), + new WhoisCollector((data) => resolve(parseWhois(data)), nick), 'WHOIS %s', nick, ); @@ -575,7 +575,7 @@ export class IRCConnectionWrapper public async who(target: string): Promise { return new Promise((resolve) => { this.useCollector( - new WhoCollector((lines) => resolve(parseWho(lines))), + new WhoCollector((lines) => resolve(parseWho(lines)), target), 'WHO %s', target, ); diff --git a/src/utility/collector.ts b/src/utility/collector.ts index 650309d..389fcac 100644 --- a/src/utility/collector.ts +++ b/src/utility/collector.ts @@ -66,10 +66,7 @@ export class MultiLineCollector implements IQueue { * `318` - End of WHOIS */ export class WhoisCollector extends MultiLineCollector { - constructor( - resolve: (lines: IIRCLine[]) => void, - match?: (line: IIRCLine) => boolean, - ) { + constructor(resolve: (lines: IIRCLine[]) => void, target: string) { super( '318', // End of WHOIS [ @@ -86,7 +83,7 @@ export class WhoisCollector extends MultiLineCollector { '317', // Sign on time and idle time ], resolve, - match, + (line) => target === line.arguments[1], ); } } @@ -99,17 +96,14 @@ export class WhoisCollector extends MultiLineCollector { * `315` - end of WHO */ export class WhoCollector extends MultiLineCollector { - constructor( - resolve: (lines: IIRCLine[]) => void, - match?: (line: IIRCLine) => boolean, - ) { + constructor(resolve: (lines: IIRCLine[]) => void, target: string) { super( '315', // End of WHO [ '352', // WHO line: [*][@|+] : ], resolve, - match, + (line) => line.arguments[1] === target || line.arguments[2] === target, ); } } diff --git a/src/utility/nicklist/index.ts b/src/utility/nicklist/index.ts new file mode 100644 index 0000000..71673c1 --- /dev/null +++ b/src/utility/nicklist/index.ts @@ -0,0 +1,3 @@ +export * from './nicklist.interfaces'; +export * from './nicklist.events'; +export * from './nicklist'; diff --git a/src/utility/nicklist/nicklist.events.ts b/src/utility/nicklist/nicklist.events.ts new file mode 100644 index 0000000..23a3ac3 --- /dev/null +++ b/src/utility/nicklist/nicklist.events.ts @@ -0,0 +1,4 @@ +export type NickListEvents = { + update: (channels?: string[]) => void; + channelRemoved: (channel: string) => void; +}; diff --git a/src/utility/nicklist/nicklist.interfaces.ts b/src/utility/nicklist/nicklist.interfaces.ts new file mode 100644 index 0000000..4c1904f --- /dev/null +++ b/src/utility/nicklist/nicklist.interfaces.ts @@ -0,0 +1,11 @@ +export interface INicklistNick { + nickname: string; + modes: string[]; + prefix?: string; +} + +export interface INicklistChannel { + channel: string; + topic?: string; + nicks: INicklistNick[]; +} diff --git a/src/utility/nicklist/nicklist.ts b/src/utility/nicklist/nicklist.ts new file mode 100644 index 0000000..8be6049 --- /dev/null +++ b/src/utility/nicklist/nicklist.ts @@ -0,0 +1,378 @@ +import { IRCConnectionWrapper } from '../../irc'; +import { modeFromPrefix } from '../mode-from-prefix'; +import { TypedEventEmitter } from '../typed-event-emitter'; +import { WhoResponse } from '../who-parser'; +import { NickListEvents } from './nicklist.events'; +import { INicklistChannel } from './nicklist.interfaces'; + +/** + * Manages a nicklist for your IRC connection, complete with modes and prefixes. + */ +export class IRCNickList extends TypedEventEmitter { + public channels: INicklistChannel[] = []; + + constructor(public irc: IRCConnectionWrapper) { + super(); + + if (this.irc.authenticated) { + this.handlers(); + } + + this.irc.on('authenticated', () => this.handlers()); + } + + /** + * Get my nickname + */ + public get nickname() { + return this.irc.options.nick; + } + + /** + * Available user prefixes + */ + public get prefixes() { + return Object.values(this.irc.serverData.supportedModes); + } + + /** + * Available user modes with prefixes + */ + public get modes() { + return Object.keys(this.irc.serverData.supportedModes); + } + + /** + * List of supported prefix modes + */ + public get supportedModes() { + return this.irc.serverData.supportedModes; + } + + /** + * Does the provided nickname currently have a supported prefix. + * @param nick Nickname with a prefix + * @returns boolean + */ + public nickHasPrefix(nick: string): boolean { + return this.prefixes.some((prefix) => nick.startsWith(prefix)); + } + + /** + * Strip prefix from nickname, if exists + * @param nick Nickname with a prefix + * @returns Nickname without prefix + */ + public stripPrefix(nick: string) { + if (this.nickHasPrefix(nick)) { + return nick.substring(1); + } + return nick; + } + + /** + * Find a channel by name + * @param channel Channel name + * @returns Channel object or undefined + */ + public getChannelByName(channel: string) { + return this.channels.find((item) => item.channel === channel); + } + + /** + * Handle a user join event. + * @param channel Channel + * @param nickname Nickname + */ + public handleJoin(channel: string, nickname: string): void { + const channelObject = this.getChannelByName(channel); + if (nickname === this.nickname) { + if (!channelObject) { + this.channels.push({ + channel, + nicks: [], + }); + } + + this.irc.who(channel).then((response) => this.handleWho(response)); + this.emit('update', [channel]); + return; + } + + channelObject?.nicks.push({ + nickname, + modes: [], + }); + this.emit('update', [channel]); + } + + /** + * Handle `NAMES` reply. + * @param channel Channel + * @param names Nick list + */ + public handleNames(channel: string, names: string[]): void { + const channelObject = this.getChannelByName(channel); + if (!channelObject) { + return; + } + + for (const nick of names) { + const prefixed = this.nickHasPrefix(nick); + const stripped = this.stripPrefix(nick); + const umode = modeFromPrefix(nick, this.supportedModes); + const existsNick = channelObject.nicks.find( + (entry) => entry.nickname === stripped, + ); + + if (!existsNick) { + channelObject.nicks.push({ + modes: [umode].filter((x) => x), + nickname: stripped, + prefix: prefixed ? nick.substring(0, 1) : undefined, + }); + } + } + this.emit('update', [channel]); + } + + /** + * Handle a WHO response list, sets hidden modes for nicks + * @param whoList WHO reply + */ + public handleWho(whoList: WhoResponse[]): void { + let channels = []; + for (const who of whoList) { + if (!who.channel || !who.nickname) { + continue; + } + + const channelObject = this.getChannelByName(who.channel); + if (!channelObject) { + continue; + } + + channels.push(who.channel); + + const providedModeList = who.modes || []; + const userModes = this.prefixes.reduce((list, current) => { + const mode = providedModeList.includes(current) + ? modeFromPrefix(current, this.supportedModes) + : undefined; + return mode ? [...list, mode] : list; + }, []); + + const inListNick = channelObject.nicks.find( + (item) => item.nickname === who.nickname, + ); + if (inListNick) { + inListNick.modes = userModes; + inListNick.prefix = this.getPrefixFromModes(inListNick.modes); + continue; + } + + channelObject.nicks.push({ + nickname: who.nickname, + modes: userModes, + prefix: this.getPrefixFromModes(userModes), + }); + } + this.emit('update', channels); + } + + /** + * Handles `QUIT`, `PART` and `KICK` events (user left channel). + * @param nickname Nickname + * @param channel Optional channel, if missing, removes from all + */ + public handleLeave(nickname: string, channel?: string): void { + if (nickname === this.nickname) { + if (channel) { + this.channels.splice( + this.channels.findIndex((chan) => chan.channel === channel), + 1, + ); + + this.emit('channelRemoved', channel); + this.emit('update', [channel]); + return; + } + + const allMap = this.channels.map((chan) => chan.channel); + this.channels = []; + + allMap.forEach((x) => this.emit('channelRemoved', x)); + this.emit('update', allMap); + return; + } + + if (!channel) { + let channels = []; + for (const chan of this.channels) { + if (this.removeNickIfExists(chan, nickname)) { + channels.push(chan.channel); + } + } + this.emit('update', channels); + return; + } + + const channelObject = this.getChannelByName(channel); + if (!channelObject) { + return; + } + + this.removeNickIfExists(channelObject, nickname); + this.emit('update', [channel]); + } + + /** + * Handle channel modes. + * @param channel Channel + * @param mode Mode + * @param modeTarget User + * @param method Add or remove (`+` or `-`) + */ + public handleMode( + channel: string, + mode: string, + modeTarget: string, + method: string, + ): void { + if (!this.modes.includes(mode)) { + return; + } + + const chan = this.getChannelByName(channel); + if (!chan) { + return; + } + + const nick = this.getNickFromChannel(chan, modeTarget); + if (!nick) { + return; + } + + if (method === '+') { + if (nick.modes.includes(mode)) { + return; + } + nick.modes.push(mode); + nick.prefix = this.getPrefixFromModes(nick.modes); + } else { + if (!nick.modes.includes(mode)) { + return; + } + nick.modes.splice(nick.modes.indexOf(mode), 1); + nick.prefix = this.getPrefixFromModes(nick.modes); + } + this.emit('update', [channel]); + } + + /** + * Handle nickname changes. + * @param oldNick Old nickname + * @param newNick New nickname + */ + public handleNick(oldNick: string, newNick: string) { + let channels = []; + + for (const channel of this.channels) { + const nick = this.getNickFromChannel(channel, oldNick); + + if (!nick) { + continue; + } + + channels.push(channel.channel); + nick.nickname = newNick; + } + + this.emit('update', channels); + } + + /** + * Get nick object from channel, if exists. + * @param channel Channel object + * @param nickname Nickname + * @returns Nick object + */ + public getNickFromChannel(channel: INicklistChannel, nickname: string) { + return channel.nicks.find((item) => item.nickname === nickname); + } + + /** + * Remove a nickname from channel, if exists. + * @param channel Channel object + * @param nickname Nickname + */ + public removeNickIfExists( + channel: INicklistChannel, + nickname: string, + ): boolean { + const inListNick = channel.nicks.findIndex( + (item) => item.nickname === nickname, + ); + + if (inListNick > -1) { + channel.nicks.splice(inListNick, 1); + return true; + } + + return false; + } + + /** + * Get the applicable prefix for a list of user channel modes. + * @param modes List of modes + * @returns Prefix or empty string + */ + public getPrefixFromModes(modes: string[]): string { + if (!modes?.length) { + return ''; + } + + const highestModeWeight = this.modes.reduce( + (last, current, _, array) => + modes.includes(current) + ? last + ? array.indexOf(current) < array.indexOf(last) + ? current + : last + : current + : last, + '', + ); + + if (!highestModeWeight) { + return ''; + } + + return this.prefixes[this.modes.indexOf(highestModeWeight)]; + } + + private handlers() { + this.irc.on('join', ({ nickname, channel }) => { + this.handleJoin(channel, nickname); + }); + + this.irc.on( + 'channel-mode', + ({ type, mode, modeTarget, arguments: [channel] }) => { + this.handleMode(channel, mode, modeTarget, type); + }, + ); + + this.irc.on('leave', ({ nickname, channel }) => { + this.handleLeave(nickname, channel); + }); + + this.irc.on('nick', ({ oldNick, newNick }) => { + this.handleNick(oldNick, newNick); + }); + + this.irc.on('names', ({ channel, list }) => { + this.handleNames(channel, list); + }); + } +}