mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-25 09:05:02 +00:00
simplify cpp add-on - move logic to JS, fix API
This commit is contained in:
@@ -22,19 +22,12 @@ void haskell_init() {
|
||||
hs_init_with_rtsopts(&argc, &pargv);
|
||||
}
|
||||
|
||||
Napi::Value ParseJson(Env env, const std::string& json_str) {
|
||||
Object global = env.Global();
|
||||
Object json = global.Get("JSON").As<Object>();
|
||||
Function parse = json.Get("parse").As<Function>();
|
||||
return parse.Call(json, {String::New(env, json_str)});
|
||||
}
|
||||
|
||||
class JsonAsyncWorker : public AsyncWorker {
|
||||
class ResultAsyncWorker : public AsyncWorker {
|
||||
public:
|
||||
using ExecuteFn = std::function<void(JsonAsyncWorker*)>;
|
||||
using ResultProcessor = std::function<void(JsonAsyncWorker*, Napi::Env)>;
|
||||
using ExecuteFn = std::function<void(ResultAsyncWorker*)>;
|
||||
using ResultProcessor = std::function<void(ResultAsyncWorker*, Napi::Env)>;
|
||||
|
||||
JsonAsyncWorker(Function& callback, ExecuteFn execute_fn, ResultProcessor result_processor = nullptr)
|
||||
ResultAsyncWorker(Function& callback, ExecuteFn execute_fn, ResultProcessor result_processor = nullptr)
|
||||
: AsyncWorker(callback), execute_fn_(std::move(execute_fn)), result_processor_(std::move(result_processor)) {}
|
||||
|
||||
void Execute() override {
|
||||
@@ -67,21 +60,6 @@ class JsonAsyncWorker : public AsyncWorker {
|
||||
return result_;
|
||||
}
|
||||
|
||||
Value ParseAndHandleJson(Napi::Env env, bool allow_empty = false) {
|
||||
if (result_.empty() && allow_empty) return env.Undefined();
|
||||
if (result_.empty()) {
|
||||
Callback().Call({Error::New(env, "Empty result").Value(), env.Undefined()});
|
||||
return env.Undefined();
|
||||
}
|
||||
Value parsed = ParseJson(env, result_);
|
||||
if (env.IsExceptionPending()) {
|
||||
Error exception = env.GetAndClearPendingException();
|
||||
Callback().Call({exception.Value(), env.Undefined()});
|
||||
return env.Undefined();
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
void SetCtrl(uintptr_t ctrl) {
|
||||
ctrl_ = ctrl;
|
||||
}
|
||||
@@ -162,18 +140,14 @@ chat_ctrl FromChatCtrlBigInt(const Napi::Value& value) {
|
||||
return reinterpret_cast<chat_ctrl>(val);
|
||||
}
|
||||
|
||||
// Helper for handling common C result patterns (where empty res is error)
|
||||
void HandleCResult(JsonAsyncWorker* worker, char* c_res, const std::string& func_name) {
|
||||
// Helper for handling common C result patterns (no empty check)
|
||||
void HandleCResult(ResultAsyncWorker* worker, char* c_res, const std::string& func_name) {
|
||||
if (c_res == nullptr) {
|
||||
worker->SetWorkerError(func_name + " failed");
|
||||
return;
|
||||
}
|
||||
std::string res = c_res;
|
||||
free(c_res);
|
||||
if (res.empty()) {
|
||||
worker->SetWorkerError(func_name + " failed");
|
||||
return;
|
||||
}
|
||||
worker->SetResult(res);
|
||||
}
|
||||
|
||||
@@ -190,75 +164,12 @@ Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) {
|
||||
}
|
||||
|
||||
// Common result processors
|
||||
JsonAsyncWorker::ResultProcessor JsonResultProcessor() {
|
||||
return [](JsonAsyncWorker* worker, Napi::Env env) {
|
||||
Value parsed = worker->ParseAndHandleJson(env);
|
||||
if (parsed.IsUndefined()) return;
|
||||
worker->Callback().Call({env.Null(), parsed});
|
||||
};
|
||||
}
|
||||
|
||||
JsonAsyncWorker::ResultProcessor RecvResultProcessor() {
|
||||
return [](JsonAsyncWorker* worker, Napi::Env env) {
|
||||
Value parsed = worker->ParseAndHandleJson(env, true); // Allow empty
|
||||
if (parsed.IsUndefined() && !worker->GetStringResult().empty()) return;
|
||||
worker->Callback().Call({env.Null(), parsed});
|
||||
};
|
||||
}
|
||||
|
||||
JsonAsyncWorker::ResultProcessor WriteResultProcessor() {
|
||||
return [](JsonAsyncWorker* worker, Napi::Env env) {
|
||||
Value parsed = worker->ParseAndHandleJson(env);
|
||||
if (parsed.IsUndefined()) return;
|
||||
Object parsed_obj = parsed.As<Object>();
|
||||
Value type_val = parsed_obj.Get("type");
|
||||
if (!type_val.IsString()) {
|
||||
Error err = Error::New(env, "Invalid response type");
|
||||
worker->Callback().Call({err.Value(), env.Undefined()});
|
||||
return;
|
||||
}
|
||||
std::string type = type_val.As<String>().Utf8Value();
|
||||
if (type == "error") {
|
||||
Value err_val = parsed_obj.Get("writeError");
|
||||
std::string err_msg = err_val.IsString() ? err_val.As<String>().Utf8Value() : "Unknown error";
|
||||
Error err = Error::New(env, err_msg);
|
||||
worker->Callback().Call({err.Value(), env.Undefined()});
|
||||
} else {
|
||||
Value cryptoArgs = parsed_obj.Get("cryptoArgs");
|
||||
if (cryptoArgs.IsUndefined()) {
|
||||
Error err = Error::New(env, "Missing cryptoArgs");
|
||||
worker->Callback().Call({err.Value(), env.Undefined()});
|
||||
return;
|
||||
}
|
||||
worker->Callback().Call({env.Null(), cryptoArgs});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
JsonAsyncWorker::ResultProcessor MigrateResultProcessor() {
|
||||
return [](JsonAsyncWorker* worker, Napi::Env env) {
|
||||
Value parsed = worker->ParseAndHandleJson(env);
|
||||
if (parsed.IsUndefined()) return;
|
||||
Object parsed_obj = parsed.As<Object>();
|
||||
Value type_val = parsed_obj.Get("type");
|
||||
if (type_val.IsString() && type_val.As<String>().Utf8Value() == "ok") {
|
||||
worker->Callback().Call({env.Null(), ToChatCtrlBigInt(env, worker->GetCtrl())});
|
||||
} else {
|
||||
Error err = Error::New(env, "Database or migration error (see dbMigrationError property)");
|
||||
err.Set("dbMigrationError", parsed_obj);
|
||||
worker->Callback().Call({err.Value(), env.Undefined()});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
JsonAsyncWorker::ResultProcessor ErrorResultProcessor() {
|
||||
return [](JsonAsyncWorker* worker, Napi::Env env) {
|
||||
if (worker->GetStringResult().empty()) {
|
||||
worker->Callback().Call({env.Null(), env.Undefined()});
|
||||
} else {
|
||||
Error err = Error::New(env, worker->GetStringResult());
|
||||
worker->Callback().Call({err.Value(), env.Undefined()});
|
||||
}
|
||||
ResultAsyncWorker::ResultProcessor MigrateResultProcessor() {
|
||||
return [](ResultAsyncWorker* worker, Napi::Env env) {
|
||||
Napi::Array arr = Napi::Array::New(env, 2);
|
||||
arr.Set(0u, ToChatCtrlBigInt(env, worker->GetCtrl()));
|
||||
arr.Set(1u, Napi::String::New(env, worker->GetStringResult()));
|
||||
worker->Callback().Call({env.Null(), arr});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -278,20 +189,14 @@ Value ChatMigrateInit(const CallbackInfo& args) {
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [path, key, confirm](JsonAsyncWorker* worker) {
|
||||
auto execute_fn = [path, key, confirm](ResultAsyncWorker* worker) {
|
||||
chat_ctrl ctrl = nullptr;
|
||||
char* c_res = chat_migrate_init(path.c_str(), key.c_str(), confirm.c_str(), &ctrl);
|
||||
if (c_res == nullptr) {
|
||||
worker->SetWorkerError("chat_migrate_init failed");
|
||||
return;
|
||||
}
|
||||
std::string res = c_res;
|
||||
free(c_res);
|
||||
worker->SetCtrl(reinterpret_cast<uintptr_t>(ctrl));
|
||||
worker->SetResult(res);
|
||||
HandleCResult(worker, c_res, "chat_migrate_init");
|
||||
};
|
||||
|
||||
JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor());
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor());
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
@@ -309,18 +214,12 @@ Value ChatCloseStore(const CallbackInfo& args) {
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [ctrl](JsonAsyncWorker* worker) {
|
||||
auto execute_fn = [ctrl](ResultAsyncWorker* worker) {
|
||||
char* c_res = chat_close_store(ctrl);
|
||||
if (c_res == nullptr) {
|
||||
worker->SetWorkerError("chat_close_store failed");
|
||||
return;
|
||||
}
|
||||
std::string res = c_res;
|
||||
free(c_res);
|
||||
worker->SetResult(res);
|
||||
HandleCResult(worker, c_res, "chat_close_store");
|
||||
};
|
||||
|
||||
JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), ErrorResultProcessor());
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
@@ -339,12 +238,12 @@ Value ChatSendCmd(const CallbackInfo& args) {
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [ctrl, cmd](JsonAsyncWorker* worker) {
|
||||
auto execute_fn = [ctrl, cmd](ResultAsyncWorker* worker) {
|
||||
char* c_res = chat_send_cmd(ctrl, cmd.c_str());
|
||||
HandleCResult(worker, c_res, "chat_send_cmd");
|
||||
};
|
||||
|
||||
JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), JsonResultProcessor());
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
@@ -363,18 +262,12 @@ Value ChatRecvMsgWait(const CallbackInfo& args) {
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [ctrl, wait](JsonAsyncWorker* worker) {
|
||||
auto execute_fn = [ctrl, wait](ResultAsyncWorker* worker) {
|
||||
char* c_res = chat_recv_msg_wait(ctrl, wait);
|
||||
if (c_res == nullptr) {
|
||||
worker->SetWorkerError("chat_recv_msg_wait failed");
|
||||
return;
|
||||
}
|
||||
std::string res = c_res;
|
||||
free(c_res);
|
||||
worker->SetResult(res);
|
||||
HandleCResult(worker, c_res, "chat_recv_msg_wait");
|
||||
};
|
||||
|
||||
JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), RecvResultProcessor());
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
@@ -391,19 +284,18 @@ Value ChatWriteFile(const CallbackInfo& args) {
|
||||
std::string path = args[1].As<String>().Utf8Value();
|
||||
ArrayBuffer ab = args[2].As<ArrayBuffer>();
|
||||
char* data = static_cast<char*>(ab.Data());
|
||||
int len = static_cast<int>(ab.ByteLength());
|
||||
size_t len = ab.ByteLength();
|
||||
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [ctrl, path, data, len](JsonAsyncWorker* worker) {
|
||||
char* c_res = chat_write_file(ctrl, path.c_str(), data, len);
|
||||
auto execute_fn = [ctrl, path, ab, data, len](ResultAsyncWorker* worker) {
|
||||
(void)ab; // to keep ArrayBuffer alive
|
||||
char* c_res = chat_write_file(ctrl, path.c_str(), data, static_cast<int>(len));
|
||||
HandleCResult(worker, c_res, "chat_write_file");
|
||||
};
|
||||
|
||||
// Note: To keep ab alive, we can use a Reference, but since data is used in lambda capture by value, it's fine as long as the worker lives.
|
||||
// If needed, add Reference<ArrayBuffer> to JsonAsyncWorker for this case.
|
||||
JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), WriteResultProcessor());
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
@@ -466,12 +358,12 @@ Value ChatEncryptFile(const CallbackInfo& args) {
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [ctrl, fromPath, toPath](JsonAsyncWorker* worker) {
|
||||
auto execute_fn = [ctrl, fromPath, toPath](ResultAsyncWorker* worker) {
|
||||
char* c_res = chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str());
|
||||
HandleCResult(worker, c_res, "chat_encrypt_file");
|
||||
};
|
||||
|
||||
JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), WriteResultProcessor());
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
@@ -492,18 +384,12 @@ Value ChatDecryptFile(const CallbackInfo& args) {
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [fromPath, key, nonce, toPath](JsonAsyncWorker* worker) {
|
||||
auto execute_fn = [fromPath, key, nonce, toPath](ResultAsyncWorker* worker) {
|
||||
char* c_res = chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str());
|
||||
if (c_res == nullptr) {
|
||||
worker->SetWorkerError("chat_decrypt_file failed");
|
||||
return;
|
||||
}
|
||||
std::string res = c_res;
|
||||
free(c_res);
|
||||
worker->SetResult(res);
|
||||
HandleCResult(worker, c_res, "chat_decrypt_file");
|
||||
};
|
||||
|
||||
JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_fn), ErrorResultProcessor());
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
@@ -511,14 +397,14 @@ Value ChatDecryptFile(const CallbackInfo& args) {
|
||||
|
||||
Object Init(Env env, Object exports) {
|
||||
haskell_init();
|
||||
exports.Set("chatMigrateInit", Function::New(env, ChatMigrateInit));
|
||||
exports.Set("chatCloseStore", Function::New(env, ChatCloseStore));
|
||||
exports.Set("chatSendCmd", Function::New(env, ChatSendCmd));
|
||||
exports.Set("chatRecvMsgWait", Function::New(env, ChatRecvMsgWait));
|
||||
exports.Set("chatWriteFile", Function::New(env, ChatWriteFile));
|
||||
exports.Set("chatReadFile", Function::New(env, ChatReadFile));
|
||||
exports.Set("chatEncryptFile", Function::New(env, ChatEncryptFile));
|
||||
exports.Set("chatDecryptFile", Function::New(env, ChatDecryptFile));
|
||||
exports.Set("chat_migrate_init", Function::New(env, ChatMigrateInit));
|
||||
exports.Set("chat_close_store", Function::New(env, ChatCloseStore));
|
||||
exports.Set("chat_send_cmd", Function::New(env, ChatSendCmd));
|
||||
exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait));
|
||||
exports.Set("chat_write_file", Function::New(env, ChatWriteFile));
|
||||
exports.Set("chat_read_file", Function::New(env, ChatReadFile));
|
||||
exports.Set("chat_encrypt_file", Function::New(env, ChatEncryptFile));
|
||||
exports.Set("chat_decrypt_file", Function::New(env, ChatDecryptFile));
|
||||
return exports;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {CC, ChatResponse, T} from "@simplex-chat/types"
|
||||
import * as core from "./core"
|
||||
import {MigrationConfirmation} from "./types"
|
||||
|
||||
export class ChatCommandError extends Error {
|
||||
constructor(public message: string, public response: ChatResponse) {
|
||||
@@ -16,7 +15,7 @@ export enum ConnReqType {
|
||||
export class ChatApi {
|
||||
private constructor(protected ctrl_: bigint | undefined) {}
|
||||
|
||||
static async init(dbPath: string, dbKey: string, confirm = MigrationConfirmation.YesUp): Promise<ChatApi> {
|
||||
static async init(dbPath: string, dbKey: string, confirm = core.MigrationConfirmation.YesUp): Promise<ChatApi> {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, dbKey, confirm)
|
||||
return new ChatApi(ctrl)
|
||||
}
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import {ChatEvent, ChatResponse} from "@simplex-chat/types"
|
||||
import {CryptoArgs, MigrationConfirmation} from "./types"
|
||||
|
||||
// initialize chat controller
|
||||
export function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation): Promise<bigint>
|
||||
|
||||
// close chat store
|
||||
export function chatCloseStore(ctrl: bigint): Promise<void>
|
||||
|
||||
// send chat command as string
|
||||
export function chatSendCmd(ctrl: bigint, cmd: string): Promise<ChatResponse>
|
||||
|
||||
// receive chat event
|
||||
export function chatRecvMsgWait(ctrl: bigint, wait: number): Promise<ChatEvent | undefined>
|
||||
|
||||
// write buffer to encrypted file
|
||||
export function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise<CryptoArgs>
|
||||
|
||||
// read buffer from encrypted file
|
||||
export function chatReadFile(path: string, key: string, nonce: string): Promise<ArrayBuffer>
|
||||
|
||||
// encrypt file
|
||||
export function chatEncryptFile(ctrl: bigint, fromPath: string, toPath: string): Promise<CryptoArgs>
|
||||
|
||||
// decrypt file
|
||||
export function chatDecryptFile(fromPath: string, key: string, nonce: string, toPath: string): Promise<void>
|
||||
@@ -0,0 +1,186 @@
|
||||
import {ChatEvent, ChatResponse, T} from "@simplex-chat/types"
|
||||
import * as simplex from "./simplex"
|
||||
|
||||
// initialize chat controller
|
||||
export async function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation): Promise<bigint> {
|
||||
const [ctrl, res] = await simplex.chat_migrate_init(dbPath, dbKey, confirm)
|
||||
const json = JSON.parse(res)
|
||||
if (json.type === 'ok') return ctrl
|
||||
throw new ChatInitError("Database or migration error (see dbMigrationError property)", json as DBMigrationError)
|
||||
}
|
||||
|
||||
// close chat store
|
||||
export async function chatCloseStore(ctrl: bigint): Promise<void> {
|
||||
const res = await simplex.chat_close_store(ctrl)
|
||||
if (res !== "") throw new Error(res)
|
||||
}
|
||||
|
||||
// send chat command as string
|
||||
export async function chatSendCmd(ctrl: bigint, cmd: string): Promise<ChatResponse> {
|
||||
const res = await simplex.chat_send_cmd(ctrl, cmd)
|
||||
const json = JSON.parse(res) as APIResult<ChatResponse>
|
||||
if (typeof json.result === 'object') return json.result
|
||||
if (typeof json.error === 'object') throw new ChatAPIError("Chat command error (see chatError property)", json.error as T.ChatError)
|
||||
throw new ChatAPIError("Invalid chat command result")
|
||||
}
|
||||
|
||||
// receive chat event
|
||||
export async function chatRecvMsgWait(ctrl: bigint, wait: number): Promise<ChatEvent | undefined> {
|
||||
const res = await simplex.chat_recv_msg_wait(ctrl, wait)
|
||||
if (res === "") return undefined
|
||||
const json = JSON.parse(res) as APIResult<ChatEvent>
|
||||
if (typeof json.result === 'object') return json.result
|
||||
if (typeof json.error === 'object') throw new ChatAPIError("Chat event error (see chatError property)", json.error as T.ChatError)
|
||||
throw new ChatAPIError("Invalid chat event")
|
||||
}
|
||||
|
||||
// write buffer to encrypted file
|
||||
export async function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise<CryptoArgs> {
|
||||
const res = await simplex.chat_write_file(ctrl, path, buffer)
|
||||
return cryptoArgsResult(res)
|
||||
}
|
||||
|
||||
// read buffer from encrypted file
|
||||
export async function chatReadFile(path: string, {fileKey, fileNonce}: CryptoArgs): Promise<ArrayBuffer> {
|
||||
return await simplex.chat_read_file(path, fileKey, fileNonce)
|
||||
}
|
||||
|
||||
// encrypt file
|
||||
export async function chatEncryptFile(ctrl: bigint, fromPath: string, toPath: string): Promise<CryptoArgs> {
|
||||
const res = await simplex.chat_encrypt_file(ctrl, fromPath, toPath)
|
||||
return cryptoArgsResult(res)
|
||||
}
|
||||
|
||||
// decrypt file
|
||||
export async function chatDecryptFile(fromPath: string, {fileKey, fileNonce}: CryptoArgs, toPath: string): Promise<void> {
|
||||
const res = await simplex.chat_decrypt_file(fromPath, fileKey, fileNonce, toPath)
|
||||
if (res !== "") throw new Error(res)
|
||||
}
|
||||
|
||||
function cryptoArgsResult(res: string): CryptoArgs {
|
||||
const json = JSON.parse(res)
|
||||
switch (json.type) {
|
||||
case "result": return json.cryptoArgs as CryptoArgs
|
||||
case "error": throw Error(json.writeError)
|
||||
default: throw Error("unexpected chat_write_file result: " + res)
|
||||
}
|
||||
}
|
||||
|
||||
export interface APIResult<R> {
|
||||
result?: R
|
||||
error?: T.ChatError
|
||||
}
|
||||
|
||||
export class ChatAPIError extends Error {
|
||||
constructor(public message: string, public chatError: T.ChatError | undefined = undefined) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
export enum MigrationConfirmation {
|
||||
YesUp = "yesUp",
|
||||
YesUpDown = "yesUpDown",
|
||||
Console = "console",
|
||||
Error = "error"
|
||||
}
|
||||
|
||||
export interface CryptoArgs {
|
||||
fileKey: string
|
||||
fileNonce: string
|
||||
}
|
||||
|
||||
export class ChatInitError extends Error {
|
||||
constructor(public message: string, public dbMigrationError: DBMigrationError) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
export type DBMigrationError =
|
||||
| DBMigrationError.InvalidConfirmation
|
||||
| DBMigrationError.ErrorNotADatabase // invalid/corrupt database file or incorrect encryption key
|
||||
| DBMigrationError.ErrorMigration
|
||||
| DBMigrationError.ErrorSQL
|
||||
|
||||
export namespace DBMigrationError {
|
||||
export type Tag = "invalidConfirmation" | "errorNotADatabase" | "errorMigration" | "errorSQL"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface InvalidConfirmation extends Interface {
|
||||
type: "invalidConfirmation"
|
||||
}
|
||||
|
||||
export interface ErrorNotADatabase extends Interface {
|
||||
type: "errorNotADatabase"
|
||||
dbFile: string
|
||||
}
|
||||
|
||||
export interface ErrorMigration extends Interface {
|
||||
type: "errorMigration"
|
||||
dbFile: string
|
||||
migrationError: MigrationError
|
||||
}
|
||||
|
||||
export interface ErrorSQL extends Interface {
|
||||
type: "errorSQL"
|
||||
dbFile: string
|
||||
migrationSQLError: string
|
||||
}
|
||||
}
|
||||
|
||||
export type MigrationError =
|
||||
| MigrationError.MEUpgrade
|
||||
| MigrationError.MEDowngrade
|
||||
| MigrationError.MigrationError
|
||||
|
||||
export namespace MigrationError {
|
||||
export type Tag = "upgrade" | "downgrade" | "migrationError"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface MEUpgrade extends Interface {
|
||||
type: "upgrade"
|
||||
upMigrations: UpMigration
|
||||
}
|
||||
|
||||
export interface MEDowngrade extends Interface {
|
||||
type: "downgrade"
|
||||
downMigrations: [string]
|
||||
}
|
||||
|
||||
export interface MigrationError extends Interface {
|
||||
type: "migrationError"
|
||||
mtrError: MTRError
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpMigration {
|
||||
upName: string
|
||||
withDown: boolean
|
||||
}
|
||||
|
||||
export type MTRError =
|
||||
| MTRError.MTRENoDown
|
||||
| MTRError.MTREDifferent
|
||||
|
||||
export namespace MTRError {
|
||||
export type Tag = "noDown" | "different"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface MTRENoDown extends Interface {
|
||||
type: "noDown"
|
||||
upMigrations: UpMigration
|
||||
}
|
||||
|
||||
export interface MTREDifferent extends Interface {
|
||||
type: "different"
|
||||
downMigrations: [string]
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import {ChatEvent, ChatResponse} from "@simplex-chat/types";
|
||||
|
||||
// These functions are defined in CPP add-on ../cpp/simplex.cc
|
||||
|
||||
export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]>
|
||||
export function chat_close_store(ctrl: bigint): Promise<string>
|
||||
export function chat_send_cmd(ctrl: bigint, cmd: string): Promise<string>
|
||||
export function chat_recv_msg_wait(ctrl: bigint, wait: number): Promise<string>
|
||||
export function chat_write_file(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise<string>
|
||||
export function chat_read_file(path: string, key: string, nonce: string): Promise<ArrayBuffer>
|
||||
export function chat_encrypt_file(ctrl: bigint, fromPath: string, toPath: string): Promise<string>
|
||||
export function chat_decrypt_file(fromPath: string, key: string, nonce: string, toPath: string): Promise<string>
|
||||
@@ -1,101 +0,0 @@
|
||||
export enum MigrationConfirmation {
|
||||
YesUp = "yesUp",
|
||||
YesUpDown = "yesUpDown",
|
||||
Console = "console",
|
||||
Error = "error"
|
||||
}
|
||||
|
||||
export interface CryptoArgs {
|
||||
fileKey: string
|
||||
fileNonce: string
|
||||
}
|
||||
|
||||
export type DBMigrationError =
|
||||
| DBMigrationError.InvalidConfirmation
|
||||
| DBMigrationError.ErrorNotADatabase // invalid/corrupt database file or incorrect encryption key
|
||||
| DBMigrationError.ErrorMigration
|
||||
| DBMigrationError.ErrorSQL
|
||||
|
||||
export namespace DBMigrationError {
|
||||
export type Tag = "invalidConfirmation" | "errorNotADatabase" | "errorMigration" | "errorSQL"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface InvalidConfirmation extends Interface {
|
||||
type: "invalidConfirmation"
|
||||
}
|
||||
|
||||
export interface ErrorNotADatabase extends Interface {
|
||||
type: "errorNotADatabase"
|
||||
dbFile: string
|
||||
}
|
||||
|
||||
export interface ErrorMigration extends Interface {
|
||||
type: "errorMigration"
|
||||
dbFile: string
|
||||
migrationError: MigrationError
|
||||
}
|
||||
|
||||
export interface ErrorSQL extends Interface {
|
||||
type: "errorSQL"
|
||||
dbFile: string
|
||||
migrationSQLError: string
|
||||
}
|
||||
}
|
||||
|
||||
export type MigrationError =
|
||||
| MigrationError.MEUpgrade
|
||||
| MigrationError.MEDowngrade
|
||||
| MigrationError.MigrationError
|
||||
|
||||
export namespace MigrationError {
|
||||
export type Tag = "upgrade" | "downgrade" | "migrationError"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface MEUpgrade extends Interface {
|
||||
type: "upgrade"
|
||||
upMigrations: UpMigration
|
||||
}
|
||||
|
||||
export interface MEDowngrade extends Interface {
|
||||
type: "downgrade"
|
||||
downMigrations: [string]
|
||||
}
|
||||
|
||||
export interface MigrationError extends Interface {
|
||||
type: "migrationError"
|
||||
mtrError: MTRError
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpMigration {
|
||||
upName: string
|
||||
withDown: boolean
|
||||
}
|
||||
|
||||
export type MTRError =
|
||||
| MTRError.MTRENoDown
|
||||
| MTRError.MTREDifferent
|
||||
|
||||
export namespace MTRError {
|
||||
export type Tag = "noDown" | "different"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface MTRENoDown extends Interface {
|
||||
type: "noDown"
|
||||
upMigrations: UpMigration
|
||||
}
|
||||
|
||||
export interface MTREDifferent extends Interface {
|
||||
type: "different"
|
||||
downMigrations: [string]
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,85 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import {core} from "../src/index";
|
||||
import {MigrationConfirmation} from "../src/types";
|
||||
|
||||
describe("Core tests", () => {
|
||||
const tmpDir: string = "./tests/tmp";
|
||||
const dbPath: string = path.join(tmpDir, "simplex_v1");
|
||||
const tmpDir = "./tests/tmp";
|
||||
const dbPath = path.join(tmpDir, "simplex_v1");
|
||||
|
||||
beforeEach(() => fs.mkdirSync(tmpDir, { recursive: true }));
|
||||
|
||||
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
||||
|
||||
it("should initialize chat controller", async () => {
|
||||
const ctrl: bigint = await core.chatMigrateInit(dbPath, "key", MigrationConfirmation.YesUp);
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
expect(typeof ctrl).toBe("bigint");
|
||||
await expect(core.chatCloseStore(ctrl)).resolves.toBe(undefined);
|
||||
|
||||
await expect(core.chatMigrateInit(dbPath, "wrong_key", MigrationConfirmation.YesUp)).rejects.toMatchObject({
|
||||
await expect(core.chatMigrateInit(dbPath, "wrong_key", core.MigrationConfirmation.YesUp)).rejects.toMatchObject({
|
||||
message: "Database or migration error (see dbMigrationError property)",
|
||||
dbMigrationError: expect.objectContaining({ type: "errorNotADatabase" })
|
||||
dbMigrationError: expect.objectContaining({type: "errorNotADatabase"})
|
||||
});
|
||||
});
|
||||
|
||||
it("should send command and receive event", async () => {
|
||||
const ctrl: bigint = await core.chatMigrateInit(dbPath, "key", MigrationConfirmation.YesUp);
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
|
||||
await expect(core.chatSendCmd(ctrl, "/v")).resolves.toHaveProperty("result");
|
||||
await expect(core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}')).resolves.toMatchObject({ result: { type: "cmdOk" } });
|
||||
await expect(core.chatSendCmd(ctrl, "/v")).resolves.toMatchObject({
|
||||
type: "versionInfo"
|
||||
});
|
||||
await expect(core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}')).resolves.toMatchObject({
|
||||
type: "cmdOk"
|
||||
});
|
||||
|
||||
const wait: number = 500_000;
|
||||
await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toMatchObject({ result: { type: "chatSuspended" } });
|
||||
const wait = 500_000;
|
||||
await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toMatchObject({
|
||||
type: "chatSuspended"
|
||||
});
|
||||
await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toBe(undefined);
|
||||
|
||||
await expect(core.chatSendCmd(ctrl, "/unknown")).resolves.toHaveProperty("error");
|
||||
await expect(core.chatSendCmd(ctrl, "/unknown")).rejects.toMatchObject({
|
||||
message: "Chat command error (see chatError property)",
|
||||
chatError: expect.objectContaining({type: "error"})
|
||||
});
|
||||
|
||||
await core.chatCloseStore(ctrl);
|
||||
});
|
||||
|
||||
it("should write/read encrypted file from/to buffer", async () => {
|
||||
const ctrl: bigint = await core.chatMigrateInit(dbPath, "key", MigrationConfirmation.YesUp);
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
|
||||
const filePath: string = path.join(tmpDir, "write_file.txt");
|
||||
const buffer: ArrayBuffer = new Uint8Array([0, 1, 2]).buffer;
|
||||
const encRes: { fileKey: string; fileNonce: string } = await core.chatWriteFile(ctrl, filePath, buffer);
|
||||
const key: string = encRes.fileKey;
|
||||
const nonce: string = encRes.fileNonce;
|
||||
expect(typeof key).toBe("string");
|
||||
expect(typeof nonce).toBe("string");
|
||||
const filePath = path.join(tmpDir, "write_file.txt");
|
||||
const buffer = new Uint8Array([0, 1, 2]).buffer;
|
||||
const cryptoArgs = await core.chatWriteFile(ctrl, filePath, buffer);
|
||||
expect(typeof cryptoArgs.fileKey).toBe("string");
|
||||
expect(typeof cryptoArgs.fileNonce).toBe("string");
|
||||
|
||||
const buffer2: ArrayBuffer = await core.chatReadFile(filePath, key, nonce);
|
||||
const buffer2 = await core.chatReadFile(filePath, cryptoArgs);
|
||||
expect(Buffer.from(buffer2).equals(Buffer.from(buffer))).toBe(true);
|
||||
|
||||
await expect(core.chatWriteFile(ctrl, path.join(tmpDir, "unknown", "unknown.txt"), buffer)).rejects.toThrow();
|
||||
await expect(core.chatReadFile(path.join(tmpDir, "unknown.txt"), key, nonce)).rejects.toThrow();
|
||||
await expect(core.chatReadFile(path.join(tmpDir, "unknown.txt"), cryptoArgs)).rejects.toThrow();
|
||||
|
||||
await core.chatCloseStore(ctrl);
|
||||
});
|
||||
|
||||
it("should encrypt/decrypt file", async () => {
|
||||
const ctrl: bigint = await core.chatMigrateInit(dbPath, "key", MigrationConfirmation.YesUp);
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
|
||||
const unencryptedPath: string = path.join(tmpDir, "file_unencrypted.txt");
|
||||
const unencryptedPath = path.join(tmpDir, "file_unencrypted.txt");
|
||||
fs.writeFileSync(unencryptedPath, "unencrypted\n");
|
||||
const encryptedPath: string = path.join(tmpDir, "file_encrypted.txt");
|
||||
const encRes: { fileKey: string; fileNonce: string } = await core.chatEncryptFile(ctrl, unencryptedPath, encryptedPath);
|
||||
const key: string = encRes.fileKey;
|
||||
const nonce: string = encRes.fileNonce;
|
||||
expect(typeof key).toBe("string");
|
||||
expect(typeof nonce).toBe("string");
|
||||
const encryptedPath = path.join(tmpDir, "file_encrypted.txt");
|
||||
const cryptoArgs = await core.chatEncryptFile(ctrl, unencryptedPath, encryptedPath);
|
||||
expect(typeof cryptoArgs.fileKey).toBe("string");
|
||||
expect(typeof cryptoArgs.fileNonce).toBe("string");
|
||||
|
||||
const decryptedPath: string = path.join(tmpDir, "file_decrypted.txt");
|
||||
await expect(core.chatDecryptFile(encryptedPath, key, nonce, decryptedPath)).resolves.toBe(undefined);
|
||||
await expect(core.chatDecryptFile(encryptedPath, cryptoArgs, decryptedPath)).resolves.toBe(undefined);
|
||||
|
||||
expect(fs.readFileSync(decryptedPath, "utf8")).toBe("unencrypted\n");
|
||||
|
||||
await expect(core.chatEncryptFile(ctrl, path.join(tmpDir, "unknown.txt"), encryptedPath)).rejects.toThrow();
|
||||
await expect(core.chatDecryptFile(path.join(tmpDir, "unknown.txt"), key, nonce, decryptedPath)).rejects.toThrow();
|
||||
await expect(core.chatDecryptFile(path.join(tmpDir, "unknown.txt"), cryptoArgs, decryptedPath)).rejects.toThrow();
|
||||
|
||||
await core.chatCloseStore(ctrl);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user