379 lines
9.1 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
}
|