src/addjob.ts
/* eslint-disable prefer-destructuring */
/* eslint-disable no-param-reassign */
import type { MessageEventArgs } from 'irc-framework';
import * as net from 'net';
import Downloader from './downloader';
import type { Candidate } from './interfaces/candidate';
import { Job } from './interfaces/job';
import type { ParamsTimeout } from './timeouthandler';
import { TimeOut } from './timeouthandler';
export type Packets = string | string[] | number | number[]
export default class AddJob extends TimeOut {
candidates: Job[];
constructor(params: ParamsTimeout) {
super(params);
this.candidates = [];
this.onRequest();
this.onNext();
}
protected onRequest(): void {
this.on('request', (args: { target: string; packets: number[] }) => {
this.emit('debug', 'xdccJS:: DOWNLOAD_REGISTERED');
const candidate = this.getCandidate(args.target);
this.prepareCandidate(candidate);
});
}
private static constructCandidate(
target: string,
range: number[],
opts?: Partial<{ipv6:boolean, throttle: number }>,
): Candidate {
if(opts && opts.throttle) {
opts.throttle = Downloader.is({
name: 'throttle',
variable: opts.throttle,
type: 10,
condition: opts.throttle > 0,
conditionError: 'throttle cannot be equal or less than 0',
});
opts.throttle *= 1024;
}
return {
nick: target,
cancelNick: target,
queue: range,
retry: 0,
now: 0,
failures: [],
success: [],
opts,
timeout: {
clear: (): void => {
throw Error('calling clear too soon');
},
},
};
}
protected makeCancelable(candidate: Candidate, client?: net.Socket): () => void {
const fn = (): void => {
candidate.timeout.clear();
this.say(candidate.cancelNick, 'XDCC CANCEL');
if (client) {
const cancel = new Error('cancel');
client.destroy(cancel);
} else {
this.print('Cancelled by user', 4);
this.candidates = this.candidates.filter((x) => x.nick !== candidate.nick);
}
};
return fn;
}
public async download(
target: string,
packets: Packets,
opts?: Partial<{ ipv6:boolean, throttle: number }>,
): Promise<Job> {
const range = await AddJob.parsePackets(packets);
let candidate = this.getCandidate(target);
if (!candidate) {
const base = AddJob.constructCandidate(target, range, opts);
const cancelFn = this.makeCancelable(base);
const newCand = new Job(base, cancelFn);
AddJob.makeClearable(newCand);
this.candidates.push(newCand);
candidate = this.getCandidate(target);
} else {
const tmp: Job['queue'] = candidate.queue.concat(range);
candidate.queue = tmp.sort((a, b) => a - b);
if (opts) candidate.opts = { ...candidate.opts, ...opts };
}
if (this.candidates.length === 1 && candidate.now === 0) {
this.passMessage(candidate);
this.emit('request', { target, packets: candidate.queue });
}
return candidate;
}
private passMessage(job:Job) {
const listener = (event: MessageEventArgs) => {
if (typeof event.type === 'undefined') return;
const regexp = this.queue ? new RegExp(this.queue) : undefined;
const regex = regexp && regexp.test(event.message);
const toMe = event.target.toLocaleLowerCase() === this.nickname.toLocaleLowerCase();
if (event.nick === job.nick || event.nick === job.cancelNick) {
job.emit('message', { nick: event.nick, type: event.type, message: event.message });
if (!regex && toMe) {
this.print(
`%yellow%@${job.nick}%reset%: %cyan%${event.message}%reset%`,
8,
);
}
}
};
this.on('message', listener);
this.on('notice', listener);
job.on('done', () => {
this.removeListener('message', listener);
this.removeListener('notice', listener);
job.removeAllListeners();
});
}
private static async parsePackets(packets: string | string[] | number | number[])
: Promise<number[]> {
if (typeof packets === 'string') {
return AddJob.parsePacketString(packets);
} if (Array.isArray(packets)) {
return AddJob.parsePacketArray(packets);
} if (typeof packets === 'number') {
return [packets];
}
return [0];
}
private static async parsePacketArray(packets: string[] | number[]): Promise<number[]> {
const range: number[] = [];
const promises = packets.map(async (pack) => {
if (typeof pack === 'number') {
range.push(pack);
} else {
range.push(parseInt(pack, 10));
}
});
await Promise.all(promises);
return AddJob.sortPackets(range);
}
private static async parsePacketString(packet: string): Promise<number[]> {
const newPacket = packet.replace(/#/gi, '');
const splittedPackets = newPacket.split(',');
let range: number[] = [];
const promises = splittedPackets.map(async (p) => {
if (p.includes('-')) {
range = range.concat(AddJob.decomposeRange(p));
} else {
range.push(parseInt(p, 10));
}
});
await Promise.all(promises);
return AddJob.sortPackets(range);
}
private static decomposeRange(string: string): number[] {
const minmax = string.split('-');
const start = parseInt(minmax[0], 10);
const end = parseInt(minmax[1], 10);
return AddJob.range(start, end);
}
private static range(start: number, end: number): number[] {
return Array.from(Array(end + 1).keys()).slice(start);
}
private static sortPackets(range: number[]): number[] {
return range
.sort((a, b) => a - b)
.filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
}
protected prepareCandidate(candidate: Job): void {
candidate.retry = 0;
candidate.now = candidate.queue[0];
candidate.queue = candidate.queue.filter(
(pending) => pending.toString() !== candidate.now.toString(),
);
if (this.queue) {
const regex = this.queue;
this.DisableTimeOutOnQueue(candidate, regex);
}
this.SetupTimeout({
candidate,
eventType: 'error',
message: `timeout: no response from %yellow%${candidate.nick}`,
padding: 6,
delay: this.timeout,
});
this.emit('debug', 'xdccJS:: DOWNLOAD_REQUESTING');
this.print(
`%success% sending command: /MSG %yellow%${candidate.nick}%reset% xdcc send %yellow%${candidate.now}`,
4,
);
this.say(candidate.nick, `xdcc send ${candidate.now}`);
}
private getCandidate(target: string): Job {
return this.candidates.filter(
(candidates) => candidates.nick.localeCompare(target, 'en', { sensitivity: 'base' }) === 0,
)[0];
}
protected onNext(): void {
this.on('next', (candidate: Job, verbose:boolean) => {
this.emit('debug', 'xdccJS:: DOWNLOAD_NEXT');
if (candidate.queue.length) {
this.prepareCandidate(candidate);
} else {
this.candidates = this.candidates.filter((c) => c.nick !== candidate.nick);
candidate.emit('done', candidate.show());
this.emit('done', candidate.show());
if (verbose && candidate.failures.length) {
let message = `%danger% couldn't download pack: %yellow%${candidate.failures}%reset% from %yellow%${candidate.nick}%reset%`;
if (candidate.failures.length > 1) {
/**
* Credit to CertainPerformance on stackoverflow
* - Profile https://stackoverflow.com/users/9515207/certainperformance
* - Post: https://stackoverflow.com/questions/53879088/join-an-array-by-commas-and-and/53879103#53879103
* - Answer: https://stackoverflow.com/a/53879103
*/
const firsts = candidate.failures.slice(0, candidate.failures.length - 1);
const last = candidate.failures[candidate.failures.length - 1];
message = `%danger% couldn't download packs: %yellow%${`${firsts.join(', ')} and ${last}`}%reset% from %yellow%${candidate.nick}%reset%`;
}
this.print(message, 6);
}
if (!this.candidates.length) {
this.emit('debug', 'xdccJS:: EVENT_CAN_QUIT');
this.emit('can-quit');
} else {
const newcandidate = this.candidates[0];
this.emit('request', { target: newcandidate.nick, packets: newcandidate.queue });
}
}
});
}
}