0
0
Fork 0
deno_monorepo/net/irc/client.ts

286 lines
7.2 KiB
TypeScript

/**
* @file
* @copyright 2020, Valentin Anger
* @license ISC
*/
import { BufReader } from "https://deno.land/std@0.69.0/io/bufio.ts";
import IrcError from "./error.ts";
import Message from "./message.ts";
import { Replies } from "./replies.ts";
export interface ClientOptions {
nickname: string;
hostname: string;
port?: number;
use_tls?: boolean;
join_channels?: string[];
nickserv_password?: string;
}
export class Client {
readonly nickname: string;
readonly hostname: string;
readonly port: number;
readonly use_tls: boolean;
readonly join_channels: string[];
readonly nickserv_password?: string;
private conn?: Deno.Conn;
private buf_reader?: BufReader;
private encoder = new TextEncoder();
private message_buffer: Message[] = [];
/** Creates a new IRC Client
The default port is encrypted_port.
TLS is used by default.
*/
constructor(options: ClientOptions) {
this.nickname = options.nickname;
this.hostname = options.hostname;
this.port = options.port ?? Client.encrypted_port;
this.use_tls = options.use_tls ?? true;
this.join_channels = options.join_channels ?? [];
this.nickserv_password = options.nickserv_password;
}
async connect(): Promise<void> {
const connectFun = this.use_tls ? Deno.connectTls : Deno.connect;
const connection = await connectFun({
hostname: this.hostname,
port: this.port,
});
this.conn = connection;
this.buf_reader = new BufReader(connection, 512);
await this.sendMessage("NICK", [this.nickname]);
await this.sendMessage("USER", [this.nickname, "0", "*", "DenoIrc"]);
this.queueMessage(await this.getReply(Replies.RPL_WELCOME));
if (this.nickserv_password != undefined) {
this.nickserv(this.nickserv_password);
}
this.queueMessage(await this.getReply(Replies.RPL_ENDOFMOTD));
for (const channel of this.join_channels) {
await this.joinChannel(channel, true);
}
}
connected(): boolean {
return this.conn != undefined;
}
async disconnect(quit_message: string = ""): Promise<void> {
if (this.conn != undefined) {
await this.sendMessage("QUIT", [quit_message]);
}
await this.getReply("ERROR");
this.drop();
}
drop(): void {
if (this.conn != undefined) {
this.conn.close();
this.conn = undefined;
}
this.buf_reader = undefined;
}
async nickserv(password: string): Promise<void> {
return await this.sendPrivmsg("NickServ", `IDENTIFY ${password}`);
}
/** Constructs and sends a message as defined in RFC2812
Sections: 2.3
*/
async sendMessage(command: string, parameters: string[]): Promise<void> {
if (this.conn == undefined) {
throw new IrcError("Client is not connected");
}
let message = command;
let remaining = parameters.length;
for (const param of parameters) {
remaining -= 1;
// TODO Check parameter for validity
if (remaining > 0 || !message.match(/:| /)) {
// BNF middle
message += " " + param;
} else {
// BNF trailing
message += " :" + param;
}
}
// TODO Substitute all invalid chars with placeholders
message += "\r\n";
const encoded = this.encoder.encode(message);
if (encoded.length > 512) {
throw new IrcError("Resulting message is too long");
}
await this.conn.write(encoded).then((b) => {
if (b != encoded.length) {
throw new IrcError("Transmission failed");
}
});
}
/** Reads the next IRC message */
async readMessage(): Promise<Message> {
if (this.message_buffer.length > 0) {
return this.message_buffer.shift() as Message;
}
return this.readMessageStream();
}
private async readMessageStream() {
if (this.buf_reader == undefined) {
throw new IrcError("Client is not connected");
}
const message = await this.buf_reader.readString("\n");
if (message === null) {
return Promise.reject(null);
}
return new Message(this, message);
}
private queueMessage(message: Message): void {
this.message_buffer.push(message);
}
private async getReply(...replies: string[]): Promise<Message> {
while (true) {
const message = await this.readMessageStream();
if (replies.includes(message.command)) {
return message;
} else if (message.command[0] == "4") {
throw new IrcError("Received error code", message.command);
} else {
if (message.command == "PING") {
// Keep connection alive
await this.sendMessage("PONG", message.parameters);
} else {
this.queueMessage(message);
}
}
}
}
async sendPrivmsg(target: string, message: string): Promise<void> {
await this.sendMessage("PRIVMSG", [target, message]);
}
async sendNotice(target: string, message: string): Promise<void> {
await this.sendMessage("NOTICE", [target, message]);
}
async joinChannel(
name: string,
keep_message: boolean = false,
): Promise<void> {
await this.sendMessage("JOIN", [name]);
while (true) {
const join = await this.getReply("JOIN");
if (join.parameters[0] == name) {
if (keep_message) {
this.queueMessage(join);
}
break;
}
this.queueMessage(join);
}
}
async leaveChannel(name: string, part_message?: string): Promise<void> {
let parameters = [name];
if (part_message != undefined) {
parameters.push(part_message);
}
await this.sendMessage("PART", parameters);
}
/** Generates a stream of incomming messages
Pings and CTCP version requests are handled internally.
*/
messages(): AsyncIterable<Message> {
const client = this;
return {
async *[Symbol.asyncIterator](): AsyncIterator<Message> {
while (true) {
let message;
try {
message = await client.readMessage();
} catch {
return;
}
if (message.command[0] == "4") {
throw new IrcError("Recieved error code", message.command);
}
switch (message.command) {
case "ERROR":
client.drop();
yield message;
return;
case "PING":
await client.sendMessage("PONG", message.parameters);
break;
case "PRIVMSG":
// Respond to CTCP version requests
if (
message.sender != undefined &&
message.parameters[1] == "\u{1}VERSION\u{1}"
) {
await client.sendPrivmsg(
message.sender.nickname,
"\u{1}VERSION\u{1} " +
Client.client_name +
":" +
Client.client_version +
":" +
Client.client_environment,
);
break;
}
default:
yield message;
}
}
},
};
}
}
export namespace Client {
export const unencrypted_port = 6667;
export const encrypted_port = 6697;
export const client_name = "DenoIrc";
export const client_version = "0.1.0";
export const client_environment = "undefined";
}