Bring in node-signald
This commit is contained in:
parent
26bd446368
commit
41971732d0
13 changed files with 3474 additions and 0 deletions
2
packages/node-signald/.gitignore
vendored
Normal file
2
packages/node-signald/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
9
packages/node-signald/CHANGELOG.md
Normal file
9
packages/node-signald/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [0.0.3](https://gitlab.com/digiresilience/link/node-signald/compare/v0.0.2...v0.0.3) (2022-01-03)
|
||||||
|
|
||||||
|
### [0.0.2](https://gitlab.com/digiresilience/link/node-signald/compare/v0.0.1...v0.0.2) (2021-10-08)
|
||||||
|
|
||||||
|
### 0.0.1 (2021-10-08)
|
||||||
73
packages/node-signald/example/example.ts
Normal file
73
packages/node-signald/example/example.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { SignaldAPI, CaptchaRequiredException } from "node-signald";
|
||||||
|
import * as process from "process";
|
||||||
|
import * as prompt from "prompt";
|
||||||
|
|
||||||
|
const SOCKETFILE =
|
||||||
|
process.env.SIGNALD_SOCKET || "/run/user/1000/signald/signald.sock";
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
if (!process.env.NUMBER)
|
||||||
|
throw new Error(
|
||||||
|
"Please set the NUMBER env var to the number you want to test with."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const main = async () => {
|
||||||
|
validate();
|
||||||
|
const signald = new SignaldAPI();
|
||||||
|
await signald.connectAsync(SOCKETFILE);
|
||||||
|
try {
|
||||||
|
await signald.register(process.env.NUMBER);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("GOT A error", e.name);
|
||||||
|
if (e.name === "CaptchaRequiredException") {
|
||||||
|
console.log(`
|
||||||
|
|
||||||
|
CAPTCHA REQUIRED
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
1. Visit https://signalcaptchas.org/registration/generate.html
|
||||||
|
2. Appease the machine
|
||||||
|
3. Bring back your captcha gobbly-gook here
|
||||||
|
`);
|
||||||
|
console.log("captcha required");
|
||||||
|
|
||||||
|
prompt.start();
|
||||||
|
const { captcha } = await prompt.get(["captcha"]);
|
||||||
|
await signald.register(process.env.NUMBER, false, captcha);
|
||||||
|
} else {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code } = await prompt.get(["code"]);
|
||||||
|
await signald.verify(process.env.NUMBER, code);
|
||||||
|
};
|
||||||
|
|
||||||
|
const main2 = async () => {
|
||||||
|
validate();
|
||||||
|
const signald = new SignaldAPI();
|
||||||
|
await signald.connectAsync(SOCKETFILE);
|
||||||
|
let result = await signald.listAccounts();
|
||||||
|
console.log(JSON.stringify(result));
|
||||||
|
signald.on("messagev0", (envelope) => {
|
||||||
|
const source = envelope.source.number;
|
||||||
|
const body = envelope.dataMessage.body;
|
||||||
|
const when = new Date(envelope.timestamp).toDateString();
|
||||||
|
console.log(`${when} [${source}]: ${body}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
result.accounts.map(
|
||||||
|
async (account: any) => await signald.requestSync(account.address.uuid)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
result.accounts.map(
|
||||||
|
async (account: any) => await signald.subscribev0(account.address.uuid)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// main();
|
||||||
|
main2();
|
||||||
20
packages/node-signald/example/package.json
Normal file
20
packages/node-signald/example/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "node-signald-example",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "example usage",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"private": false,
|
||||||
|
"scripts": {
|
||||||
|
"example": "node --unhandled-rejections=strict -r ts-node/register --unhandled-rejections=strict example.ts",
|
||||||
|
"linklib": "cd ../ && yarn build && yarn link && cd example && yarn link node-signald"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ts-node": "^10.0.0",
|
||||||
|
"typescript": "^4.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"prompt": "^1.1.0",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/node-signald/example/tsconfig.json
Normal file
27
packages/node-signald/example/tsconfig.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2019",
|
||||||
|
"lib": ["es2020"],
|
||||||
|
"declaration": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"types": ["node", "jest"],
|
||||||
|
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"traceResolution": false,
|
||||||
|
"listEmittedFiles": false,
|
||||||
|
"listFiles": false,
|
||||||
|
"pretty": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
|
},
|
||||||
|
"include": ["example.ts"]
|
||||||
|
}
|
||||||
44
packages/node-signald/package.json
Normal file
44
packages/node-signald/package.json
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "@digiresilience/node-signald",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "signald bindings for node.js",
|
||||||
|
"author": "Abel Luck <abel@guardianproject.info>",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"private": false,
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"/dist"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.9.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build --verbose",
|
||||||
|
"watch": "tsc --build --verbose --watch",
|
||||||
|
"generate": "node util/generate.js && prettier src/generated.ts -w --loglevel error && yarn build",
|
||||||
|
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --out dist/docs",
|
||||||
|
"fix": "echo n/a",
|
||||||
|
"lint": "echo n/a",
|
||||||
|
"fix:lint": "echo n/a",
|
||||||
|
"fmt": "prettier \"src/**/*.ts\" --write",
|
||||||
|
"test": "echo n/a"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/backoff": "^2.5.2",
|
||||||
|
"camelcase": "^6.2.0",
|
||||||
|
"typedoc": "^0.22.5",
|
||||||
|
"tsconfig-link": "*",
|
||||||
|
"eslint-config-link": "*",
|
||||||
|
"jest-config-link": "*",
|
||||||
|
"babel-preset-link": "*"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"backoff": "^2.5.0",
|
||||||
|
"camelcase-keys": "^7.0.1",
|
||||||
|
"eventemitter3": "^4.0.7",
|
||||||
|
"snakecase-keys": "^5.1.0",
|
||||||
|
"ts-custom-error": "^3.2.0",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/node-signald/src/api.ts
Normal file
21
packages/node-signald/src/api.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { SignaldGeneratedApi, JsonMessageEnvelopev1 } from "./generated";
|
||||||
|
|
||||||
|
export class SignaldAPI extends SignaldGeneratedApi {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async subscribev0(account: string): Promise<void> {
|
||||||
|
return this.getResponse({
|
||||||
|
type: "subscribe",
|
||||||
|
username: account,
|
||||||
|
}) as Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unsubscribev0(account: string): Promise<void> {
|
||||||
|
return this.getResponse({
|
||||||
|
type: "unsubscribe",
|
||||||
|
username: account,
|
||||||
|
}) as Promise<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
packages/node-signald/src/error.ts
Normal file
52
packages/node-signald/src/error.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { CustomError } from "ts-custom-error";
|
||||||
|
|
||||||
|
export class SignaldError extends CustomError {
|
||||||
|
public msg: string;
|
||||||
|
constructor(public errorType: string, public message: string) {
|
||||||
|
super(`[${errorType}]: ${message}`);
|
||||||
|
this.errorType = errorType;
|
||||||
|
this.message = `[${errorType}]: ${message}`;
|
||||||
|
this.msg = `${message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CaptchaRequiredException extends SignaldError {
|
||||||
|
constructor(errorType: string, message: string) {
|
||||||
|
super(errorType, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBoolean = (v) => "boolean" === typeof v;
|
||||||
|
const isString = (v) => typeof v === "string" || v instanceof String;
|
||||||
|
|
||||||
|
export const throwOnError = (response: any) => {
|
||||||
|
if (response.type === "profile_not_available")
|
||||||
|
throw new SignaldError("profile_not_available", response.data);
|
||||||
|
|
||||||
|
const { data } = response;
|
||||||
|
let error;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
error = response.error || false;
|
||||||
|
} else if (isString(data)) {
|
||||||
|
error = false;
|
||||||
|
} else {
|
||||||
|
error = response.error || data?.error || false;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
let type_, msg;
|
||||||
|
if (isBoolean(error)) {
|
||||||
|
type_ = response.type;
|
||||||
|
msg = data.message || "";
|
||||||
|
let req = data.request || "";
|
||||||
|
msg += req.toString();
|
||||||
|
} else {
|
||||||
|
type_ = error.type;
|
||||||
|
msg = error.message || "";
|
||||||
|
msg += (error.validationResults || [""]).join("");
|
||||||
|
}
|
||||||
|
if (!type_) type_ = response.error_type;
|
||||||
|
if (type_ === "CaptchaRequired")
|
||||||
|
throw new CaptchaRequiredException(type_, msg);
|
||||||
|
throw new SignaldError(type_, msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
2703
packages/node-signald/src/generated.ts
Normal file
2703
packages/node-signald/src/generated.ts
Normal file
File diff suppressed because it is too large
Load diff
5
packages/node-signald/src/index.ts
Normal file
5
packages/node-signald/src/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export { SignaldAPI } from "./api";
|
||||||
|
export { JSONTransport, EventTypes } from "./util";
|
||||||
|
export { SignaldError, CaptchaRequiredException } from "./error";
|
||||||
|
import "./util";
|
||||||
|
export * from "./generated";
|
||||||
277
packages/node-signald/src/util.ts
Normal file
277
packages/node-signald/src/util.ts
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
import EventEmitter from "eventemitter3";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import * as backoff from "backoff";
|
||||||
|
import * as net from "net";
|
||||||
|
import { throwOnError } from "./error";
|
||||||
|
|
||||||
|
export interface EventTypes {
|
||||||
|
/**
|
||||||
|
* Emitted when the signald connection closes.
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
transport_disconnected(): void;
|
||||||
|
/**
|
||||||
|
* Emitted when a new connection is established.
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
transport_connected(): void;
|
||||||
|
/**
|
||||||
|
* Emitted when a transport level error occurs. This is errors around the socket and connection, errors from signald itself will not be emitted here.
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
transport_error(error: any): void;
|
||||||
|
/**
|
||||||
|
* Every full JSON payload sent to signald will be emitted on this event (after sending).
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
transport_sent_payload(payload: object): void;
|
||||||
|
/**
|
||||||
|
* Every full JSON payload received from signald will be emitted on this event (before processing)
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
transport_received_payload(payload: object): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSONTransport handles the low level connection and over-the-wire handling.
|
||||||
|
*
|
||||||
|
* You probably do not need to instantiate this class directly.
|
||||||
|
*/
|
||||||
|
export class JSONTransport extends EventEmitter<EventTypes> {
|
||||||
|
#socketfile: string;
|
||||||
|
#client;
|
||||||
|
#connected: boolean = false;
|
||||||
|
#buffer: string;
|
||||||
|
#callbacks: Record<string, Function>;
|
||||||
|
#logger: Logger = consoleLogger;
|
||||||
|
#backoff: backoff.Backoff;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.#buffer = "";
|
||||||
|
this.#callbacks = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to signald via the unix domain socket, with a fibonacci backoff
|
||||||
|
* @param socketfile the path to the signald socket
|
||||||
|
* @param maxDelay maximum delay (in ms) of the backoff
|
||||||
|
* @param initialDelay how long (in ms) the initial delay before the first retry is
|
||||||
|
* @return a promise that resolves on a successful connection
|
||||||
|
*/
|
||||||
|
public connectWithBackoff(
|
||||||
|
socketfile: string,
|
||||||
|
maxDelay: number = 90000,
|
||||||
|
initialDelay: number = 10
|
||||||
|
) {
|
||||||
|
this.#backoff = backoff.fibonacci({
|
||||||
|
initialDelay: 10,
|
||||||
|
maxDelay: 90000,
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastLogNotify = 0;
|
||||||
|
let backoffInProgress = true;
|
||||||
|
this.#backoff.on("backoff", (number, delay) => {});
|
||||||
|
|
||||||
|
this.#backoff.on("ready", async (number, delay) => {
|
||||||
|
const now = +new Date();
|
||||||
|
if (backoffInProgress && now - lastLogNotify > 10000) {
|
||||||
|
lastLogNotify = +new Date();
|
||||||
|
this.log(`reconnecting. attempt=${number} delay=${delay}`);
|
||||||
|
}
|
||||||
|
if (!this.isConnected()) {
|
||||||
|
this.connect(socketfile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on("transport_disconnected", async () => {
|
||||||
|
if (!backoffInProgress) this.log("disconnected. attemping to reconnect.");
|
||||||
|
this.#backoff.backoff();
|
||||||
|
backoffInProgress = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on("transport_connected", async () => {
|
||||||
|
this.log("connected");
|
||||||
|
this.#backoff.reset();
|
||||||
|
backoffInProgress = false;
|
||||||
|
lastLogNotify = 0;
|
||||||
|
});
|
||||||
|
this.connect(socketfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to signald via the unix domain socket.
|
||||||
|
* @param socketfile the path to the signald socket
|
||||||
|
* @return a promise that resolves on a successful connection
|
||||||
|
*/
|
||||||
|
public connect(socketfile: string) {
|
||||||
|
let connectedOnce = false;
|
||||||
|
this.#socketfile = socketfile;
|
||||||
|
this.#client = net.createConnection(socketfile);
|
||||||
|
this.#client.setEncoding("utf8");
|
||||||
|
|
||||||
|
this.#client.on("connect", () => {
|
||||||
|
this.#connected = true;
|
||||||
|
connectedOnce = true;
|
||||||
|
this.#buffer = "";
|
||||||
|
this.emit("transport_connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#client.on("close", () => {
|
||||||
|
this.#connected = false;
|
||||||
|
this.emit("transport_disconnected");
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#client.on("data", (frame) => {
|
||||||
|
this.#buffer += frame;
|
||||||
|
if (!this.#buffer.endsWith("\n")) return;
|
||||||
|
const data = this.#buffer;
|
||||||
|
this.#buffer = "";
|
||||||
|
data.split("\n").forEach((line) => {
|
||||||
|
if (!line) return;
|
||||||
|
const payload = JSON.parse(line);
|
||||||
|
this.emit("transport_received_payload", payload);
|
||||||
|
this.receivePayload(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#client.on("error", (error) => {
|
||||||
|
this.emit("transport_error", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to signald asynchronously. Does not include backoff and auto-reconnect.
|
||||||
|
*/
|
||||||
|
public async connectAsync(socketfile: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onconnected = () => {
|
||||||
|
this.removeListener("transport_connected", onconnected);
|
||||||
|
this.removeListener("transport_error", onerror);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const onerror = (error) => {
|
||||||
|
this.removeListener("transport_error", onerror);
|
||||||
|
this.removeListener("transport_connected", onconnected);
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
this.on("transport_connected", onconnected);
|
||||||
|
this.on("transport_error", onerror);
|
||||||
|
this.connect(socketfile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the signald socket.
|
||||||
|
*/
|
||||||
|
public disconnect() {
|
||||||
|
this.#client.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the socket is connected
|
||||||
|
*/
|
||||||
|
public isConnected(): boolean {
|
||||||
|
return this.#connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private receivePayload(payload: any) {
|
||||||
|
const { id, type, version = "v0" } = payload;
|
||||||
|
if (!type) {
|
||||||
|
this.debug("no type in payload.");
|
||||||
|
this.debug(
|
||||||
|
"found following keys: ",
|
||||||
|
JSON.stringify(Object.keys(payload))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.emit(`${type}${version}` as keyof EventTypes, payload.data);
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
const callback = this.#callbacks[id];
|
||||||
|
if (!callback) {
|
||||||
|
this.error(
|
||||||
|
`Payload received for an id but no callbacks were registered. id=${id}`
|
||||||
|
);
|
||||||
|
} else callback(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async sendRequest(
|
||||||
|
payload: any,
|
||||||
|
id: string = uuid(),
|
||||||
|
callback?
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.#connected) {
|
||||||
|
reject(new Error("send failed. not connected"));
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
this.#callbacks[id] = callback;
|
||||||
|
}
|
||||||
|
payload["id"] = id;
|
||||||
|
const serialized = JSON.stringify(payload) + "\n";
|
||||||
|
this.#client.write(serialized, "utf8", () => {
|
||||||
|
this.emit("transport_sent_payload", payload);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getResponse(payload: any): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const callback = (responsePayload: any) => {
|
||||||
|
try {
|
||||||
|
throwOnError(responsePayload);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
resolve(responsePayload.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sendRequest(payload, uuid(), callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the logger used to log events, debug, and error messages
|
||||||
|
*
|
||||||
|
* @param logger the logger implementation to use
|
||||||
|
*/
|
||||||
|
public setLogger(logger: Logger) {
|
||||||
|
this.#logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log(level: LogLevel, message?: any, ...extra: any[]) {
|
||||||
|
if (!this.#logger) return;
|
||||||
|
this.#logger(level, `[signald] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected log(message?: any, ...extra: any[]) {
|
||||||
|
this._log("info", message, ...extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected debug(message?: any, ...extra: any[]) {
|
||||||
|
this._log("debug", message, ...extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected warn(message?: any, ...extra: any[]) {
|
||||||
|
this._log("warn", message, ...extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected error(message?: any, ...extra: any[]) {
|
||||||
|
this._log("error", message, ...extra);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
|
export type Logger = (level: LogLevel, message?: any, ...extra: any[]) => void;
|
||||||
|
export const nullLogger = (
|
||||||
|
level: LogLevel,
|
||||||
|
message?: any,
|
||||||
|
...extra: any[]
|
||||||
|
) => {};
|
||||||
|
export const consoleLogger = (
|
||||||
|
level: LogLevel,
|
||||||
|
message?: any,
|
||||||
|
...extra: any[]
|
||||||
|
) => console[level](message, ...extra);
|
||||||
12
packages/node-signald/tsconfig.json
Normal file
12
packages/node-signald/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"extends": "tsconfig-link",
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true,
|
||||||
|
"outDir": "build/main",
|
||||||
|
"rootDir": "src",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules/**"]
|
||||||
|
}
|
||||||
229
packages/node-signald/util/generate.js
Normal file
229
packages/node-signald/util/generate.js
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
const net = require("net");
|
||||||
|
const fs = require("fs");
|
||||||
|
const process = require("process");
|
||||||
|
const camelCase = require("camelcase");
|
||||||
|
|
||||||
|
const SOCKETFILE =
|
||||||
|
process.env.SIGNALD_SOCKET || "/run/user/1000/signald/signald.sock";
|
||||||
|
const GENERATEDFILE = "./src/generated.ts";
|
||||||
|
const VERSION = "v1";
|
||||||
|
let globalVersion;
|
||||||
|
|
||||||
|
const toJs = (name) => name.replace("-", "_");
|
||||||
|
|
||||||
|
const TYPES = {
|
||||||
|
String: "string",
|
||||||
|
long: "number",
|
||||||
|
int: "number",
|
||||||
|
Integer: "number",
|
||||||
|
Map: "Record<string, string>",
|
||||||
|
boolean: "boolean",
|
||||||
|
Boolean: "boolean",
|
||||||
|
Long: "number",
|
||||||
|
UUID: "string",
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertType = (field) => {
|
||||||
|
let name = "";
|
||||||
|
if (field.version) {
|
||||||
|
name = `${field.type}${field.version}`;
|
||||||
|
} else {
|
||||||
|
if (!TYPES[field.type]) {
|
||||||
|
throw new Error(
|
||||||
|
`unknown type ${field.type} inside ${JSON.stringify(field)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
name = TYPES[field.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.list) {
|
||||||
|
return `${name}[]`;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
const fieldToData = ([rawName, field]) => {
|
||||||
|
const name = toJs(rawName);
|
||||||
|
const type = convertType(field);
|
||||||
|
const requiredStr = field.required ? "" : "?";
|
||||||
|
const example = field.example ? `\n Example: ${field.example}\n` : "\n";
|
||||||
|
const doc = field.doc ? `\n/**\n ${field.doc}.${example} */\n` : "";
|
||||||
|
let code = `${doc}${name}${requiredStr}: ${type};`;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
rawName,
|
||||||
|
type,
|
||||||
|
code,
|
||||||
|
doc: field.doc,
|
||||||
|
example: field.example,
|
||||||
|
required: field.required,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeToData = (name, version, data) => {
|
||||||
|
const typeName = `${name}${version}`;
|
||||||
|
const doc = data.doc
|
||||||
|
? `\n/**
|
||||||
|
${data.doc}
|
||||||
|
*/\n`
|
||||||
|
: "\n";
|
||||||
|
|
||||||
|
const header = `export type ${typeName} = {
|
||||||
|
`;
|
||||||
|
const dataFields = Object.entries(data.fields).map(fieldToData);
|
||||||
|
const codeFields = dataFields.map((field) => field.code);
|
||||||
|
const body = codeFields.join("\n");
|
||||||
|
const footer = "\n}\n";
|
||||||
|
// ensure required fields are sorted before non-required fields
|
||||||
|
dataFields.sort((a, b) =>
|
||||||
|
a.required === b.required ? 0 : a.required ? -1 : 1
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
typeName,
|
||||||
|
fields: dataFields,
|
||||||
|
code: doc + header + body + footer,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionToCode = (structures, rawName, version, data) => {
|
||||||
|
const name = camelCase(rawName);
|
||||||
|
const { request, response } = data;
|
||||||
|
const requestFields = structures[request].fields;
|
||||||
|
const returnTypeStr = response
|
||||||
|
? `Promise<${response}${version}>`
|
||||||
|
: "Promise<void>";
|
||||||
|
let code = ` async ${name}(`;
|
||||||
|
requestFields.map((field) => {
|
||||||
|
const requiredStr = field.required ? "" : "?";
|
||||||
|
code += `${field.name}${requiredStr}: ${field.type},\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
code += `): ${returnTypeStr} {\n`;
|
||||||
|
const params = requestFields
|
||||||
|
.map((field) => {
|
||||||
|
return `"${field.rawName}": ${field.name}`;
|
||||||
|
})
|
||||||
|
.join(",\n");
|
||||||
|
const requestStr = `{"type": "${rawName}","version": "${version}", ${params}}`;
|
||||||
|
const responseStr = `this.getResponse(${requestStr})`;
|
||||||
|
if (response) {
|
||||||
|
code += `return ${responseStr} as Promise<${response}${version}>`;
|
||||||
|
} else {
|
||||||
|
code += `await ${responseStr}; return;`;
|
||||||
|
}
|
||||||
|
code += "\n}\n";
|
||||||
|
return code;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveFile = (code) => {
|
||||||
|
fs.writeFileSync(GENERATEDFILE, code);
|
||||||
|
console.log("generated code at ", GENERATEDFILE);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processProtocolDefinition = (client, def, version) => {
|
||||||
|
console.log("processing protocol definition");
|
||||||
|
const when = new Date(new Date().toUTCString());
|
||||||
|
let contents = `
|
||||||
|
/* Generated by the output of the 'protocol' command of signald
|
||||||
|
Date: ${when}
|
||||||
|
Version: ${globalVersion}
|
||||||
|
*/
|
||||||
|
import {JSONTransport} from "./util";
|
||||||
|
`;
|
||||||
|
|
||||||
|
const structures = {};
|
||||||
|
// generate type definitions
|
||||||
|
for (version in def.types) {
|
||||||
|
Object.entries(def.types[version]).map(([name, data]) => {
|
||||||
|
structures[name] = typeToData(name, version, data);
|
||||||
|
contents += structures[name].code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO this should be improved once v0 is totally deprecated
|
||||||
|
const typeNames = ["version", "message", "subscribe", "unsubscribe"]
|
||||||
|
.map((name) => `"${name}"`)
|
||||||
|
.join(" | ");
|
||||||
|
contents += `\ndeclare module "./util" {
|
||||||
|
interface EventTypes {
|
||||||
|
/**
|
||||||
|
* The v0 event emitted when a signal message is received
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
messagev0(message: JsonMessageEnvelopev1): void;
|
||||||
|
/**
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
subscribev0(): void;
|
||||||
|
/**
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
unsubscribev0(): void;
|
||||||
|
}
|
||||||
|
}\n`;
|
||||||
|
|
||||||
|
// generate action functions
|
||||||
|
for (version in def.actions) {
|
||||||
|
if (version != VERSION) continue;
|
||||||
|
contents += "export class SignaldGeneratedApi extends JSONTransport{\n";
|
||||||
|
Object.entries(def.actions[version]).map(([name, data]) => {
|
||||||
|
contents += actionToCode(structures, name, version, data);
|
||||||
|
});
|
||||||
|
contents += "\n}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFile(contents);
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
// := Driver
|
||||||
|
|
||||||
|
const getProtocolDefinition = (client) => {
|
||||||
|
const msg = {
|
||||||
|
type: "protocol",
|
||||||
|
};
|
||||||
|
console.log("requesting protocol definition");
|
||||||
|
client.write(JSON.stringify(msg) + "\n");
|
||||||
|
};
|
||||||
|
const client = net.createConnection(SOCKETFILE);
|
||||||
|
let buffer = "";
|
||||||
|
client.setEncoding("utf8");
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
client.end();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
client.on("connect", () => {
|
||||||
|
console.log("connected");
|
||||||
|
buffer = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("data", (payload) => {
|
||||||
|
buffer += payload;
|
||||||
|
if (!buffer.endsWith("\n")) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const data = buffer;
|
||||||
|
buffer = "";
|
||||||
|
data.split("\n").forEach((line) => {
|
||||||
|
if (!line) return;
|
||||||
|
|
||||||
|
const msg = JSON.parse(line);
|
||||||
|
if (msg.type === "version") {
|
||||||
|
getProtocolDefinition(client);
|
||||||
|
globalVersion = msg.data.version;
|
||||||
|
}
|
||||||
|
if (msg.type === "protocol") {
|
||||||
|
// console.log(JSON.stringify(msg.data, null, 2));
|
||||||
|
processProtocolDefinition(client, msg.data, globalVersion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", (err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", cleanup);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue