irclib/src/utility/nicklist/nicklist.ts

379 lines
9.1 KiB
TypeScript

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<NickListEvents> {
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<string[]>((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<string>(
(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);
});
}
}