diff --git a/package-lock.json b/package-lock.json index 4894ea0..1742309 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index dc2449b..7cc1828 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main.ts b/src/main.ts index af33052..490a828 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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"); diff --git a/src/nntp.ts b/src/nntp.ts new file mode 100644 index 0000000..0372a10 --- /dev/null +++ b/src/nntp.ts @@ -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(); + private tempBuffer: Array = []; + 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 { + const cmd = { + request: { command: command, args: args } as CommandRequest, + response: new Completer(), + } 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 { + const l: GroupInfo[] = []; + const groupMap: Record> = {}; + + 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; + } +} diff --git a/src/nntp/group_info.ts b/src/nntp/group_info.ts new file mode 100644 index 0000000..3307872 --- /dev/null +++ b/src/nntp/group_info.ts @@ -0,0 +1,6 @@ +export interface GroupInfo { + name: string; + description: string; + lowWater: number; + highWater: number; +} \ No newline at end of file diff --git a/src/nntp/nntp_command.ts b/src/nntp/nntp_command.ts new file mode 100644 index 0000000..358e548 --- /dev/null +++ b/src/nntp/nntp_command.ts @@ -0,0 +1,16 @@ +import type { Completer } from "@/utils/completer"; + +export interface NNTPCommand { + request: CommandRequest; + response: Completer; +} + +export interface CommandRequest { + command: string; + args: Array; +} + +export interface CommandResponse { + responseCode: number; + lines: Array; +} \ No newline at end of file diff --git a/src/utils/completer.ts b/src/utils/completer.ts new file mode 100644 index 0000000..e43cba1 --- /dev/null +++ b/src/utils/completer.ts @@ -0,0 +1,12 @@ +export class Completer { + public readonly promise: Promise; + public complete!: (value: (PromiseLike | T)) => void; + private reject!: (reason?: any) => void; + + public constructor() { + this.promise = new Promise((resolve, reject) => { + this.complete = resolve; + this.reject = reject; + }) + } +} \ No newline at end of file diff --git a/src/utils/queue.ts b/src/utils/queue.ts new file mode 100644 index 0000000..9c03d02 --- /dev/null +++ b/src/utils/queue.ts @@ -0,0 +1,24 @@ +export interface IQueue { + enqueue(item: T): void; + dequeue(): T | undefined; + size(): number; +} + +export class Queue implements IQueue { + 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; + } +} \ No newline at end of file