mirror of
https://github.com/ChronosX88/wind-vue.git
synced 2024-12-05 00:32:22 +00:00
Start implementation of NNTP client
This commit is contained in:
parent
ea3defeeb0
commit
bad5f439b6
13
package-lock.json
generated
13
package-lock.json
generated
@ -9,7 +9,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^2.0.13",
|
"pinia": "^2.0.13",
|
||||||
"vue": "^3.2.33"
|
"vue": "^3.2.33",
|
||||||
|
"websocket-ts": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.1.0",
|
"@rushstack/eslint-patch": "^1.1.0",
|
||||||
@ -2821,6 +2822,11 @@
|
|||||||
"typescript": "*"
|
"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": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@ -4765,6 +4771,11 @@
|
|||||||
"@volar/vue-typescript": "0.34.12"
|
"@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": {
|
"which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^2.0.13",
|
"pinia": "^2.0.13",
|
||||||
"vue": "^3.2.33"
|
"vue": "^3.2.33",
|
||||||
|
"websocket-ts": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.1.0",
|
"@rushstack/eslint-patch": "^1.1.0",
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
|
import { NNTPClient } from "./nntp";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|
||||||
|
const nntp = new NNTPClient("wss://nntp.antiope.link");
|
||||||
|
109
src/nntp.ts
Normal file
109
src/nntp.ts
Normal 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
6
src/nntp/group_info.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface GroupInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
lowWater: number;
|
||||||
|
highWater: number;
|
||||||
|
}
|
16
src/nntp/nntp_command.ts
Normal file
16
src/nntp/nntp_command.ts
Normal 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
12
src/utils/completer.ts
Normal 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
24
src/utils/queue.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user