Start implementation of NNTP client

This commit is contained in:
ChronosX88 2023-01-08 04:50:40 +03:00
parent ea3defeeb0
commit bad5f439b6
8 changed files with 184 additions and 2 deletions

13
package-lock.json generated
View File

@ -9,7 +9,8 @@
"version": "0.0.0",
"dependencies": {
"pinia": "^2.0.13",
"vue": "^3.2.33"
"vue": "^3.2.33",
"websocket-ts": "^1.1.1"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.0",
@ -2821,6 +2822,11 @@
"typescript": "*"
}
},
"node_modules/websocket-ts": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/websocket-ts/-/websocket-ts-1.1.1.tgz",
"integrity": "sha512-rm+S60J74Ckw5iizzgID12ju+OfaHAa6dhXhULIOrXkl0e05RzxfY42/vMStpz5jWL3iz9mkyjPcFUY1IgI0fw=="
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -4765,6 +4771,11 @@
"@volar/vue-typescript": "0.34.12"
}
},
"websocket-ts": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/websocket-ts/-/websocket-ts-1.1.1.tgz",
"integrity": "sha512-rm+S60J74Ckw5iizzgID12ju+OfaHAa6dhXhULIOrXkl0e05RzxfY42/vMStpz5jWL3iz9mkyjPcFUY1IgI0fw=="
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -10,7 +10,8 @@
},
"dependencies": {
"pinia": "^2.0.13",
"vue": "^3.2.33"
"vue": "^3.2.33",
"websocket-ts": "^1.1.1"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.0",

View File

@ -1,9 +1,12 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import { NNTPClient } from "./nntp";
const app = createApp(App);
app.use(createPinia());
app.mount("#app");
const nntp = new NNTPClient("wss://nntp.antiope.link");

109
src/nntp.ts Normal file
View File

@ -0,0 +1,109 @@
import { Websocket, WebsocketBuilder } from "websocket-ts";
import type { GroupInfo } from "./nntp/group_info";
import type {
CommandRequest,
CommandResponse,
NNTPCommand,
} from "./nntp/nntp_command";
import { Completer } from "./utils/completer";
import { Queue } from "./utils/queue";
export class NNTPClient {
private commandQueue = new Queue<NNTPCommand>();
private tempBuffer: Array<string> = [];
private ws: Websocket;
constructor(url: string) {
this.ws = new WebsocketBuilder(url)
.onMessage((ins, evt) => {
if ((evt.data as string).startsWith("201")) {
console.debug("skipping welcome message");
}
let data = evt.data as string;
let responseLines = data.split("\r\n");
if (
(responseLines.length > 1 || this.tempBuffer.length != 0) &&
responseLines[responseLines.length - 1] != "."
) {
// if it's multiline response and it doesn't contain dot in the end
// then looks like we need to wait for next message to concatenate with current msg
this.tempBuffer.push(data);
}
if (this.tempBuffer.length != 0) {
this.tempBuffer.push(data);
data = this.tempBuffer.join();
responseLines = data.split("\r\n");
responseLines.pop();
this.tempBuffer = [];
}
const command = this.commandQueue.dequeue();
const respCode = parseInt(responseLines[0].split(" ")[0]);
command?.response.complete({
responseCode: respCode,
lines: responseLines,
} as CommandResponse);
})
.build();
}
private async sendCommand(
command: string,
args: string[]
): Promise<CommandResponse> {
const cmd = {
request: { command: command, args: args } as CommandRequest,
response: new Completer<CommandResponse>(),
} as NNTPCommand;
this.commandQueue.enqueue(cmd);
if (args.length > 0) {
this.ws.send(`${command} ${args.join(" ")}\r\n`);
} else {
this.ws.send(`${command}`);
}
const result = await cmd.response.promise;
return result;
}
private async getNewsGroupList(): Promise<GroupInfo[]> {
const l: GroupInfo[] = [];
const groupMap: Record<string, Partial<GroupInfo>> = {};
await this.sendCommand("LIST", ["NEWSGROUPS"]).then((value) => {
value.lines.shift();
value.lines.pop();
value.lines.forEach((elem) => {
const firstSpace = elem.indexOf(" ");
const name = elem.substring(0, firstSpace);
groupMap[name] = { description: elem.substring(firstSpace + 1) };
});
});
await this.sendCommand("LIST", ["ACTIVE"]).then((value) => {
value.lines.shift();
value.lines.pop();
value.lines.forEach((elem) => {
const splitted = elem.split(" ");
const [name, high, low] = splitted;
groupMap[name].highWater = Number(high);
groupMap[name].lowWater = Number(low);
});
});
Object.keys(groupMap).forEach((key) => {
l.push({
name: groupMap[key].name!,
description: groupMap[key].description!,
lowWater: groupMap[key].lowWater!,
highWater: groupMap[key].highWater!,
});
});
return l;
}
}

6
src/nntp/group_info.ts Normal file
View File

@ -0,0 +1,6 @@
export interface GroupInfo {
name: string;
description: string;
lowWater: number;
highWater: number;
}

16
src/nntp/nntp_command.ts Normal file
View File

@ -0,0 +1,16 @@
import type { Completer } from "@/utils/completer";
export interface NNTPCommand {
request: CommandRequest;
response: Completer<CommandResponse>;
}
export interface CommandRequest {
command: string;
args: Array<string>;
}
export interface CommandResponse {
responseCode: number;
lines: Array<string>;
}

12
src/utils/completer.ts Normal file
View File

@ -0,0 +1,12 @@
export class Completer<T> {
public readonly promise: Promise<T>;
public complete!: (value: (PromiseLike<T> | T)) => void;
private reject!: (reason?: any) => void;
public constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.complete = resolve;
this.reject = reject;
})
}
}

24
src/utils/queue.ts Normal file
View File

@ -0,0 +1,24 @@
export interface IQueue<T> {
enqueue(item: T): void;
dequeue(): T | undefined;
size(): number;
}
export class Queue<T> implements IQueue<T> {
private storage: T[] = [];
constructor(private capacity: number = Infinity) {}
enqueue(item: T): void {
if (this.size() === this.capacity) {
throw Error("Queue has reached max capacity, you cannot add more items");
}
this.storage.push(item);
}
dequeue(): T | undefined {
return this.storage.shift();
}
size(): number {
return this.storage.length;
}
}