mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-17 12:27:03 +00:00
async addon, tests
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
This TypeScript library provides auto-generated types for bots API: commands and responses, events and all types they use.
|
||||
|
||||
It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) library that uses WebSockets interface of SimpleX Chat CLI.
|
||||
It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) Node.js library.
|
||||
|
||||
[API reference](https://github.com/simplex-chat/simplex-chat/tree/stable/bots).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@simplex-chat/types",
|
||||
"version": "0.1.0",
|
||||
"version": "6.5.0-beta.3",
|
||||
"description": "TypeScript types for SimpleX Chat bot libraries",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -35,7 +35,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/simplex-chat/simplex-chat/issues"
|
||||
},
|
||||
"homepage": "https://github.com/simplex-chat/simplex-chat#readme",
|
||||
"homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/types/typescript#readme",
|
||||
"dependencies": {
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# SimpleX Chat JavaScript client
|
||||
|
||||
**THIS PACKAGE IS DEPRECATED**
|
||||
|
||||
Use [SimpleX Chat Node.js library](https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-nodejs#readme) instead of this package.
|
||||
|
||||
This is a TypeScript library that defines WebSocket API client for [SimpleX Chat terminal CLI](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/CLI.md) that should be run as a WebSockets server on any port:
|
||||
|
||||
```bash
|
||||
@@ -24,7 +28,7 @@ Please share your use cases and implementations.
|
||||
## Quick start
|
||||
|
||||
```
|
||||
npm i simplex-chat
|
||||
npm i @simplex-chat/webrtc-client
|
||||
npm run build
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simplex-chat",
|
||||
"version": "0.3.0",
|
||||
"name": "@simplex-chat/webrtc-client",
|
||||
"version": "6.5.0-beta.3",
|
||||
"description": "SimpleX Chat client",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -38,7 +38,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/simplex-chat/simplex-chat/issues"
|
||||
},
|
||||
"homepage": "https://github.com/simplex-chat/simplex-chat/packages/simplex-chat-client/typescript#readme",
|
||||
"homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript#readme",
|
||||
"dependencies": {
|
||||
"isomorphic-ws": "^4.0.1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
dist
|
||||
coverage
|
||||
@@ -1,12 +1,21 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "simplex_shim",
|
||||
"target_name": "simplex",
|
||||
"sources": [ "cpp/simplex.cc" ],
|
||||
"include_dirs": [
|
||||
"<!@(node -p \"require('node-addon-api').include\")"
|
||||
],
|
||||
"dependencies": [
|
||||
"<!(node -p \"require('node-addon-api').gyp\")"
|
||||
],
|
||||
"libraries": [
|
||||
"-L<(module_root_dir)/libs",
|
||||
"-lsimplex"
|
||||
],
|
||||
"cflags!": [ "-fno-exceptions" ],
|
||||
"cflags_cc!": [ "-fno-exceptions" ],
|
||||
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ],
|
||||
"conditions": [
|
||||
["OS=='mac'", {
|
||||
"xcode_settings": {
|
||||
|
||||
@@ -1,188 +1,568 @@
|
||||
#include <napi.h>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include <node.h>
|
||||
#include <functional>
|
||||
#include <cstdlib>
|
||||
#include "simplex.h"
|
||||
|
||||
namespace simplex
|
||||
{
|
||||
|
||||
using v8::Array;
|
||||
using v8::ArrayBuffer;
|
||||
using v8::Context;
|
||||
using v8::FunctionCallbackInfo;
|
||||
using v8::Isolate;
|
||||
using v8::Local;
|
||||
using v8::Maybe;
|
||||
using v8::Number;
|
||||
using v8::Object;
|
||||
using v8::String;
|
||||
using v8::Value;
|
||||
using namespace Napi;
|
||||
|
||||
void haskell_init()
|
||||
{
|
||||
int argc = 5;
|
||||
char *argv[] = {
|
||||
const char *argv[] = {
|
||||
"simplex",
|
||||
"+RTS", // requires `hs_init_with_rtsopts`
|
||||
"-A64m", // chunk size for new allocations
|
||||
"-H64m", // initial heap size
|
||||
"-xn", // non-moving GC
|
||||
0};
|
||||
char **pargv = argv;
|
||||
nullptr};
|
||||
char **pargv = const_cast<char **>(argv);
|
||||
hs_init_with_rtsopts(&argc, &pargv);
|
||||
}
|
||||
|
||||
void ChatMigrateInit(const FunctionCallbackInfo<Value> &args)
|
||||
{
|
||||
Isolate *isolate = args.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
|
||||
std::string path(*(v8::String::Utf8Value(isolate, args[0])));
|
||||
std::string key(*(v8::String::Utf8Value(isolate, args[1])));
|
||||
std::string confirm(*(v8::String::Utf8Value(isolate, args[2])));
|
||||
|
||||
long *ctrl(0);
|
||||
|
||||
std::string res = chat_migrate_init(path.c_str(), key.c_str(), confirm.c_str(), &ctrl);
|
||||
std::stringstream ss;
|
||||
ss << ctrl
|
||||
<< "\n"
|
||||
<< res;
|
||||
// printf("ChatCtrl after init %d", cChatCtrl);
|
||||
// v8::Local<v8::String> ctrl = v8::String::NewFromUtf8(isolate, cppChatCtrl.c_str(), v8::String::kNormalString);
|
||||
|
||||
args.GetReturnValue().Set(
|
||||
String::NewFromUtf8(
|
||||
isolate, ss.str().c_str())
|
||||
.ToLocalChecked());
|
||||
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)});
|
||||
}
|
||||
|
||||
void ChatCloseStore(const FunctionCallbackInfo<Value> &args)
|
||||
class GenericAsyncWorker : public AsyncWorker
|
||||
{
|
||||
Isolate *isolate = args.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
public:
|
||||
GenericAsyncWorker(Function& callback, std::function<void(GenericAsyncWorker*)> execute_lambda)
|
||||
: AsyncWorker(callback), execute_lambda_(std::move(execute_lambda)) {}
|
||||
|
||||
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
|
||||
void Execute() override {
|
||||
execute_lambda_(this);
|
||||
}
|
||||
|
||||
args.GetReturnValue().Set(
|
||||
String::NewFromUtf8(
|
||||
isolate, chat_close_store(ctrl))
|
||||
.ToLocalChecked());
|
||||
void OnOK() override {
|
||||
HandleScope scope(Env());
|
||||
std::vector<napi_value> args = { Env().Null() };
|
||||
auto res = GetResult(Env());
|
||||
args.insert(args.end(), res.begin(), res.end());
|
||||
Callback().Call(args);
|
||||
}
|
||||
|
||||
void OnError(const Error& e) override {
|
||||
HandleScope scope(Env());
|
||||
Callback().Call({e.Value(), Env().Undefined()});
|
||||
}
|
||||
|
||||
virtual std::vector<napi_value> GetResult(Napi::Env env) override {
|
||||
return { String::New(env, result_) };
|
||||
}
|
||||
|
||||
void SetResult(std::string result) {
|
||||
result_ = std::move(result);
|
||||
}
|
||||
|
||||
void SetWorkerError(const std::string& msg) {
|
||||
SetError(msg);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::string result_;
|
||||
|
||||
private:
|
||||
std::function<void(GenericAsyncWorker*)> execute_lambda_;
|
||||
};
|
||||
|
||||
class JsonAsyncWorker : public GenericAsyncWorker
|
||||
{
|
||||
public:
|
||||
JsonAsyncWorker(Function& callback, std::function<void(GenericAsyncWorker*)> execute_lambda)
|
||||
: GenericAsyncWorker(callback, std::move(execute_lambda)) {}
|
||||
|
||||
void OnOK() override {
|
||||
HandleScope scope(Env());
|
||||
Value parsed = ParseJson(Env(), result_);
|
||||
if (Env().IsExceptionPending()) {
|
||||
Error exception = Env().GetAndClearPendingException();
|
||||
Callback().Call({exception.Value(), Env().Undefined()});
|
||||
return;
|
||||
}
|
||||
Callback().Call({Env().Null(), parsed});
|
||||
}
|
||||
};
|
||||
|
||||
class ResultUnwrapAsyncWorker : public GenericAsyncWorker
|
||||
{
|
||||
public:
|
||||
ResultUnwrapAsyncWorker(Function& callback, std::function<void(GenericAsyncWorker*)> execute_lambda)
|
||||
: GenericAsyncWorker(callback, std::move(execute_lambda)) {}
|
||||
|
||||
void OnOK() override {
|
||||
HandleScope scope(Env());
|
||||
Value parsed = ParseJson(Env(), result_);
|
||||
if (Env().IsExceptionPending()) {
|
||||
Error exception = Env().GetAndClearPendingException();
|
||||
Callback().Call({exception.Value(), Env().Undefined()});
|
||||
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");
|
||||
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);
|
||||
Callback().Call({err.Value(), Env().Undefined()});
|
||||
} else {
|
||||
Value cryptoArgs = parsed_obj.Get("cryptoArgs");
|
||||
if (cryptoArgs.IsUndefined()) {
|
||||
Error err = Error::New(Env(), "Missing cryptoArgs");
|
||||
Callback().Call({err.Value(), Env().Undefined()});
|
||||
return;
|
||||
}
|
||||
Callback().Call({Env().Null(), cryptoArgs});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class MigrateAsyncWorker : public GenericAsyncWorker
|
||||
{
|
||||
public:
|
||||
MigrateAsyncWorker(Function& callback, std::function<void(GenericAsyncWorker*)> execute_lambda)
|
||||
: GenericAsyncWorker(callback, execute_lambda) {}
|
||||
|
||||
void SetCtrl(uintptr_t ctrl) {
|
||||
ctrl_ = ctrl;
|
||||
}
|
||||
|
||||
void OnOK() override {
|
||||
HandleScope scope(Env());
|
||||
Value parsed = ParseJson(Env(), result_);
|
||||
if (Env().IsExceptionPending()) {
|
||||
Error exception = Env().GetAndClearPendingException();
|
||||
Callback().Call({exception.Value(), Env().Undefined()});
|
||||
return;
|
||||
}
|
||||
Object parsed_obj = parsed.As<Object>();
|
||||
Value type_val = parsed_obj.Get("type");
|
||||
if (type_val.IsString() && type_val.As<String>().Utf8Value() == "ok") {
|
||||
Callback().Call({Env().Null(), BigInt::New(Env(), static_cast<uint64_t>(ctrl_))});
|
||||
} else {
|
||||
Error err = Error::New(Env(), "Database or migration error (see dbMigrationError property)");
|
||||
err.Set("dbMigrationError", parsed_obj);
|
||||
Callback().Call({err.Value(), Env().Undefined()});
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
uintptr_t ctrl_ = 0;
|
||||
};
|
||||
|
||||
class BinaryAsyncWorker : public AsyncWorker
|
||||
{
|
||||
public:
|
||||
BinaryAsyncWorker(Function& callback, std::function<void(BinaryAsyncWorker*)> execute_lambda)
|
||||
: AsyncWorker(callback), execute_lambda_(std::move(execute_lambda)) {}
|
||||
|
||||
void Execute() override {
|
||||
execute_lambda_(this);
|
||||
}
|
||||
|
||||
void OnOK() override {
|
||||
HandleScope scope(Env());
|
||||
char* data_ptr = original_buf + 5;
|
||||
auto finalizer = [](Napi::Env env, char* finalize_data, char* orig) {
|
||||
free(orig);
|
||||
};
|
||||
Napi::Buffer<char> buffer = Napi::Buffer<char>::New(Env(), data_ptr, binary_len, finalizer, original_buf);
|
||||
Callback().Call({Env().Null(), buffer});
|
||||
}
|
||||
|
||||
void OnError(const Error& e) override {
|
||||
HandleScope scope(Env());
|
||||
Callback().Call({e.Value(), Env().Undefined()});
|
||||
}
|
||||
|
||||
void SetWorkerError(const std::string& msg) {
|
||||
SetError(msg);
|
||||
}
|
||||
|
||||
public:
|
||||
char* original_buf = nullptr;
|
||||
size_t binary_len = 0;
|
||||
|
||||
private:
|
||||
std::function<void(BinaryAsyncWorker*)> execute_lambda_;
|
||||
};
|
||||
|
||||
class WriteAsyncWorker : public ResultUnwrapAsyncWorker
|
||||
{
|
||||
public:
|
||||
WriteAsyncWorker(Function& callback, std::function<void(GenericAsyncWorker*)> execute_lambda, ArrayBuffer ab)
|
||||
: ResultUnwrapAsyncWorker(callback, std::move(execute_lambda)), ab_ref(Reference<ArrayBuffer>::New(ab, 1)) {}
|
||||
|
||||
Reference<ArrayBuffer> ab_ref;
|
||||
};
|
||||
|
||||
Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) {
|
||||
Promise::Deferred deferred = Promise::Deferred::New(env);
|
||||
cb_out = Function::New(env, [deferred](const CallbackInfo& args) {
|
||||
if (!args[0].IsNull() && !args[0].IsUndefined()) {
|
||||
deferred.Reject(args[0]);
|
||||
} else {
|
||||
deferred.Resolve(args[1]);
|
||||
}
|
||||
});
|
||||
return deferred.Promise();
|
||||
}
|
||||
|
||||
void ChatSendCmd(const FunctionCallbackInfo<Value> &args)
|
||||
Value ChatMigrateInit(const CallbackInfo& args)
|
||||
{
|
||||
Isolate *isolate = args.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) {
|
||||
TypeError::New(env, "Expected three string arguments").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
|
||||
std::string cmd(*(v8::String::Utf8Value(isolate, args[1])));
|
||||
std::string path = args[0].As<String>().Utf8Value();
|
||||
std::string key = args[1].As<String>().Utf8Value();
|
||||
std::string confirm = args[2].As<String>().Utf8Value();
|
||||
|
||||
args.GetReturnValue().Set(
|
||||
String::NewFromUtf8(
|
||||
isolate, chat_send_cmd(ctrl, cmd.c_str()))
|
||||
.ToLocalChecked());
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_lambda = [path, key, confirm](GenericAsyncWorker* 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);
|
||||
auto mworker = static_cast<MigrateAsyncWorker*>(worker);
|
||||
mworker->SetCtrl(reinterpret_cast<uintptr_t>(ctrl));
|
||||
worker->SetResult(res);
|
||||
};
|
||||
|
||||
MigrateAsyncWorker* worker = new MigrateAsyncWorker(cb, std::move(execute_lambda));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
void ChatRecvMsgWait(const FunctionCallbackInfo<Value> &args)
|
||||
class CloseStoreAsyncWorker : public GenericAsyncWorker
|
||||
{
|
||||
Isolate *isolate = args.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
public:
|
||||
CloseStoreAsyncWorker(Function& callback, std::function<void(GenericAsyncWorker*)> execute_lambda)
|
||||
: GenericAsyncWorker(callback, std::move(execute_lambda)) {}
|
||||
|
||||
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
|
||||
long wait = ((long)args[1].As<Number>()->Value());
|
||||
void OnOK() override {
|
||||
HandleScope scope(Env());
|
||||
if (result_.empty()) {
|
||||
Callback().Call({Env().Null(), Env().Undefined()});
|
||||
} else {
|
||||
Error err = Error::New(Env(), result_);
|
||||
Callback().Call({err.Value(), Env().Undefined()});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
args.GetReturnValue().Set(
|
||||
String::NewFromUtf8(
|
||||
isolate, chat_recv_msg_wait(ctrl, wait))
|
||||
.ToLocalChecked());
|
||||
Value ChatCloseStore(const CallbackInfo& args)
|
||||
{
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 1 || !args[0].IsBigInt()) {
|
||||
TypeError::New(env, "Expected bigint (ctrl)").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
bool lossless;
|
||||
chat_ctrl ctrl = reinterpret_cast<chat_ctrl>(args[0].As<BigInt>().Int64Value(&lossless));
|
||||
|
||||
if (!lossless) {
|
||||
TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_lambda = [ctrl](GenericAsyncWorker* 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);
|
||||
};
|
||||
|
||||
CloseStoreAsyncWorker* worker = new CloseStoreAsyncWorker(cb, std::move(execute_lambda));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
void ChatWriteFile(const FunctionCallbackInfo<Value> &args)
|
||||
Value ChatSendCmd(const CallbackInfo& args)
|
||||
{
|
||||
Isolate *isolate = args.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsString()) {
|
||||
TypeError::New(env, "Expected bigint (ctrl) and string (cmd)").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
|
||||
std::string path(*(v8::String::Utf8Value(isolate, args[1])));
|
||||
bool lossless;
|
||||
chat_ctrl ctrl = reinterpret_cast<chat_ctrl>(args[0].As<BigInt>().Int64Value(&lossless));
|
||||
if (!lossless) {
|
||||
TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
std::string cmd = args[1].As<String>().Utf8Value();
|
||||
|
||||
Local<v8::ArrayBuffer> arr = args[2].As<ArrayBuffer>();
|
||||
char *buffer = (char *)arr->Data();
|
||||
int len(arr->ByteLength());
|
||||
// std::array address(*arr);
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
// v8::Array a = *buffer;
|
||||
// std::string data(*((isolate, args[1])));
|
||||
auto execute_lambda = [ctrl, cmd](GenericAsyncWorker* worker) {
|
||||
char* c_res = chat_send_cmd(ctrl, cmd.c_str());
|
||||
if (c_res == nullptr) {
|
||||
worker->SetWorkerError("chat_send_cmd failed");
|
||||
return;
|
||||
}
|
||||
std::string res = c_res;
|
||||
free(c_res);
|
||||
if (res.empty()) {
|
||||
worker->SetWorkerError("chat_send_cmd failed");
|
||||
return;
|
||||
}
|
||||
worker->SetResult(res);
|
||||
};
|
||||
|
||||
args.GetReturnValue().Set(
|
||||
String::NewFromUtf8(
|
||||
isolate, chat_write_file(ctrl, path.c_str(), buffer, len))
|
||||
.ToLocalChecked());
|
||||
JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_lambda));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
void ChatReadFile(const FunctionCallbackInfo<Value> &args)
|
||||
Value ChatRecvMsgWait(const CallbackInfo& args)
|
||||
{
|
||||
Isolate *isolate = args.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsNumber()) {
|
||||
TypeError::New(env, "Expected bigint (ctrl), number (wait)").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
bool lossless;
|
||||
chat_ctrl ctrl = reinterpret_cast<chat_ctrl>(args[0].As<BigInt>().Int64Value(&lossless));
|
||||
if (!lossless) {
|
||||
TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
int wait = static_cast<int>(args[1].As<Number>().Int32Value());
|
||||
|
||||
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
|
||||
std::string path(*(v8::String::Utf8Value(isolate, args[1])));
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
Local<v8::ArrayBuffer> arr = args[2].As<ArrayBuffer>();
|
||||
char *buffer = (char *)arr->Data();
|
||||
int len(arr->ByteLength());
|
||||
auto execute_lambda = [ctrl, wait](GenericAsyncWorker* 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);
|
||||
if (res.empty()) {
|
||||
worker->SetWorkerError("chat_recv_msg_wait failed");
|
||||
return;
|
||||
}
|
||||
worker->SetResult(res);
|
||||
};
|
||||
|
||||
args.GetReturnValue().Set(
|
||||
String::NewFromUtf8(
|
||||
isolate, chat_write_file(ctrl, path.c_str(), buffer, len))
|
||||
.ToLocalChecked());
|
||||
JsonAsyncWorker* worker = new JsonAsyncWorker(cb, std::move(execute_lambda));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
void ChatEncryptFile(const FunctionCallbackInfo<Value> &args)
|
||||
Value ChatWriteFile(const CallbackInfo& args)
|
||||
{
|
||||
Isolate *isolate = args.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsArrayBuffer()) {
|
||||
TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
|
||||
std::string fromPath(*(v8::String::Utf8Value(isolate, args[1])));
|
||||
std::string toPath(*(v8::String::Utf8Value(isolate, args[2])));
|
||||
bool lossless;
|
||||
chat_ctrl ctrl = reinterpret_cast<chat_ctrl>(args[0].As<BigInt>().Int64Value(&lossless));
|
||||
if (!lossless) {
|
||||
TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
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());
|
||||
|
||||
args.GetReturnValue().Set(
|
||||
String::NewFromUtf8(
|
||||
isolate, chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str()))
|
||||
.ToLocalChecked());
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_lambda = [ctrl, path, data, len](GenericAsyncWorker* worker) {
|
||||
char* c_res = chat_write_file(ctrl, path.c_str(), data, len);
|
||||
if (c_res == nullptr) {
|
||||
worker->SetWorkerError("chat_write_file failed");
|
||||
return;
|
||||
}
|
||||
std::string res = c_res;
|
||||
free(c_res);
|
||||
if (res.empty()) {
|
||||
worker->SetWorkerError("chat_write_file failed");
|
||||
return;
|
||||
}
|
||||
worker->SetResult(res);
|
||||
};
|
||||
|
||||
WriteAsyncWorker* worker = new WriteAsyncWorker(cb, std::move(execute_lambda), ab);
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
void ChatDecryptFile(const FunctionCallbackInfo<Value> &args)
|
||||
Value ChatReadFile(const CallbackInfo& args)
|
||||
{
|
||||
Isolate *isolate = args.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) {
|
||||
TypeError::New(env, "Expected three strings (path, key, nonce)").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
std::string fromPath(*(v8::String::Utf8Value(isolate, args[0])));
|
||||
std::string key(*(v8::String::Utf8Value(isolate, args[1])));
|
||||
std::string nonce(*(v8::String::Utf8Value(isolate, args[2])));
|
||||
std::string toPath(*(v8::String::Utf8Value(isolate, args[3])));
|
||||
std::string path = args[0].As<String>().Utf8Value();
|
||||
std::string key = args[1].As<String>().Utf8Value();
|
||||
std::string nonce = args[2].As<String>().Utf8Value();
|
||||
|
||||
args.GetReturnValue().Set(
|
||||
String::NewFromUtf8(
|
||||
isolate, chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str()))
|
||||
.ToLocalChecked());
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_lambda = [path, key, nonce](BinaryAsyncWorker* worker) {
|
||||
char* buf = chat_read_file(path.c_str(), key.c_str(), nonce.c_str());
|
||||
if (buf == nullptr) {
|
||||
worker->SetWorkerError("chat_read_file failed");
|
||||
return;
|
||||
}
|
||||
char status = buf[0];
|
||||
if (status == 1) {
|
||||
std::string err = buf + 1;
|
||||
free(buf);
|
||||
worker->SetWorkerError(err);
|
||||
return;
|
||||
} else if (status == 0) {
|
||||
uint32_t len = *(uint32_t*)(buf + 1);
|
||||
worker->original_buf = buf;
|
||||
worker->binary_len = len;
|
||||
} else {
|
||||
free(buf);
|
||||
worker->SetWorkerError("Unexpected status from chat_read_file");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
BinaryAsyncWorker* worker = new BinaryAsyncWorker(cb, std::move(execute_lambda));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
void Initialize(Local<Object> exports)
|
||||
Value ChatEncryptFile(const CallbackInfo& args)
|
||||
{
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsString()) {
|
||||
TypeError::New(env, "Expected bigint (ctrl), two strings (fromPath, toPath)").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
bool lossless;
|
||||
chat_ctrl ctrl = reinterpret_cast<chat_ctrl>(args[0].As<BigInt>().Int64Value(&lossless));
|
||||
if (!lossless) {
|
||||
TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
std::string fromPath = args[1].As<String>().Utf8Value();
|
||||
std::string toPath = args[2].As<String>().Utf8Value();
|
||||
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_lambda = [ctrl, fromPath, toPath](GenericAsyncWorker* worker) {
|
||||
char* c_res = chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str());
|
||||
if (c_res == nullptr) {
|
||||
worker->SetWorkerError("chat_encrypt_file failed");
|
||||
return;
|
||||
}
|
||||
std::string res = c_res;
|
||||
free(c_res);
|
||||
if (res.empty()) {
|
||||
worker->SetWorkerError("chat_encrypt_file failed");
|
||||
return;
|
||||
}
|
||||
worker->SetResult(res);
|
||||
};
|
||||
|
||||
ResultUnwrapAsyncWorker* worker = new ResultUnwrapAsyncWorker(cb, std::move(execute_lambda));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
Value ChatDecryptFile(const CallbackInfo& args)
|
||||
{
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsString()) {
|
||||
TypeError::New(env, "Expected four strings (fromPath, key, nonce, toPath)").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
std::string fromPath = args[0].As<String>().Utf8Value();
|
||||
std::string key = args[1].As<String>().Utf8Value();
|
||||
std::string nonce = args[2].As<String>().Utf8Value();
|
||||
std::string toPath = args[3].As<String>().Utf8Value();
|
||||
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_lambda = [fromPath, key, nonce, toPath](GenericAsyncWorker* worker) {
|
||||
char* c_res = chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str());
|
||||
std::string res = c_res ? c_res : "";
|
||||
free(c_res);
|
||||
if (!res.empty()) {
|
||||
worker->SetWorkerError(res);
|
||||
return;
|
||||
}
|
||||
worker->SetResult("ok");
|
||||
};
|
||||
|
||||
GenericAsyncWorker* worker = new GenericAsyncWorker(cb, std::move(execute_lambda));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
Object Init(Env env, Object exports) {
|
||||
haskell_init();
|
||||
NODE_SET_METHOD(exports, "chat_migrate_init", ChatMigrateInit);
|
||||
NODE_SET_METHOD(exports, "chat_close_store", ChatCloseStore);
|
||||
NODE_SET_METHOD(exports, "chat_send_cmd", ChatSendCmd);
|
||||
NODE_SET_METHOD(exports, "chat_recv_msg_wait", ChatRecvMsgWait);
|
||||
NODE_SET_METHOD(exports, "chat_write_file", ChatWriteFile);
|
||||
NODE_SET_METHOD(exports, "chat_read_file", ChatReadFile);
|
||||
NODE_SET_METHOD(exports, "chat_encrypt_file", ChatEncryptFile);
|
||||
NODE_SET_METHOD(exports, "chat_decrypt_file", ChatDecryptFile);
|
||||
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));
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
|
||||
NODE_API_MODULE(simplex, Init)
|
||||
|
||||
} // namespace simplex
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simplexjs",
|
||||
"version": "6.5.3",
|
||||
"name": "simplex-chat",
|
||||
"version": "6.5.0-beta.3",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"preinstall": "node src/download-libs.js",
|
||||
@@ -11,10 +11,14 @@
|
||||
"build": "node-gyp build",
|
||||
"run": "node src/index.js",
|
||||
"build-run": "node-gyp build && node src/index.js",
|
||||
"test": "node src/tests.js"
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"extract-zip": "^2.0.1"
|
||||
"extract-zip": "^2.0.1",
|
||||
"node-addon-api": "^8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^30.2.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -31,6 +35,6 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/simplex-chat/simplex-chat/issues"
|
||||
},
|
||||
"homepage": "https://github.com/simplex-chat/simplex-chat/packages/simplex-chat-nodejs#readme",
|
||||
"homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-nodejs#readme",
|
||||
"description": ""
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = require('../build/Release/simplex_shim');
|
||||
module.exports = require('../build/Release/simplex');
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
const addon = require('../build/Release/simplex_shim');
|
||||
const fs = require('fs');
|
||||
|
||||
const ctrlAndRes = addon.chat_migrate_init("db", "a", "yesUp")
|
||||
const ctrl = Number(ctrlAndRes.split("\n")[0])
|
||||
const res = ctrlAndRes.split("\n")[1]
|
||||
console.log("Migrate ctrl:", ctrl, "res:", res)
|
||||
console.log(addon.chat_send_cmd(ctrl, "/v"))
|
||||
// const wait = 15_000_000
|
||||
// console.log(ctrl, addon.chat_recv_msg_wait(ctrl, wait))
|
||||
|
||||
const path = "/tmp/write_file.txt"
|
||||
const data = [0, 1, 2]
|
||||
console.log(addon.chat_write_file(ctrl, path, new ArrayBuffer(data)))
|
||||
|
||||
|
||||
fs.writeFileSync("/tmp/file_unencrypted.txt", "unencrypted")
|
||||
const encRes = JSON.parse(addon.chat_encrypt_file(ctrl, "/tmp/file_unencrypted.txt", "/tmp/file_encrypted.txt"))
|
||||
const key = encRes.cryptoArgs.fileKey
|
||||
const nonce = encRes.cryptoArgs.fileNonce
|
||||
console.log(encRes)
|
||||
console.log(addon.chat_decrypt_file("/tmp/file_encrypted.txt", key, nonce, "/tmp/file_decrypted.txt"))
|
||||
@@ -0,0 +1,101 @@
|
||||
const core = require('../src/index');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('Addon Tests', () => {
|
||||
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 controller', async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", "yesUp");
|
||||
expect(typeof ctrl).toBe("bigint");
|
||||
await expect(core.chatCloseStore(ctrl)).resolves.toBe(undefined);
|
||||
|
||||
await expect(core.chatMigrateInit(dbPath, "wrong_key", "yesUp")).rejects.toMatchObject({
|
||||
message: 'Database or migration error (see dbMigrationError property)',
|
||||
dbMigrationError: expect.objectContaining({type: 'errorNotADatabase'})
|
||||
});
|
||||
await expect(core.chatMigrateInit(dbPath, "key", "invalidMigrationMode")).rejects.toMatchObject({
|
||||
message: 'Database or migration error (see dbMigrationError property)',
|
||||
dbMigrationError: expect.objectContaining({type: 'invalidConfirmation'})
|
||||
});
|
||||
});
|
||||
|
||||
it('should send command and receive event', async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", "yesUp");
|
||||
|
||||
const sendRes1 = await core.chatSendCmd(ctrl, "/v");
|
||||
expect(typeof sendRes1).toBe('object');
|
||||
expect(sendRes1).toHaveProperty('result')
|
||||
|
||||
const sendRes2 = await core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}');
|
||||
expect(sendRes2).toMatchObject({result: {type: 'cmdOk'}});
|
||||
|
||||
const wait = 15_000_000;
|
||||
const recvRes = await core.chatRecvMsgWait(ctrl, wait);
|
||||
expect(recvRes).toMatchObject({result: {type: 'chatSuspended'}});
|
||||
|
||||
await core.chatCloseStore(ctrl);
|
||||
});
|
||||
|
||||
it('should write/read encrypted file from/to buffer', async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", "yesUp");
|
||||
|
||||
const filePath = path.join(tmpDir, 'write_file.txt');
|
||||
const buffer = new Uint8Array([0, 1, 2]).buffer;
|
||||
const encRes1 = await core.chatWriteFile(ctrl, filePath, buffer);
|
||||
const key1 = encRes1.fileKey;
|
||||
const nonce1 = encRes1.fileNonce;
|
||||
expect(typeof key1).toBe('string');
|
||||
expect(typeof nonce1).toBe('string');
|
||||
|
||||
const buffer2 = await core.chatReadFile(filePath, key1, nonce1);
|
||||
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'), key1, nonce1)).rejects.toThrow();
|
||||
|
||||
core.chatCloseStore(ctrl);
|
||||
});
|
||||
|
||||
it('should encrypt/decrypt file', async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", "yesUp");
|
||||
|
||||
const unencryptedPath = path.join(tmpDir, 'file_unencrypted.txt');
|
||||
fs.writeFileSync(unencryptedPath, "unencrypted\n");
|
||||
const encryptedPath = path.join(tmpDir, 'file_encrypted.txt');
|
||||
const encRes = await core.chatEncryptFile(ctrl, unencryptedPath, encryptedPath);
|
||||
const key = encRes.fileKey;
|
||||
const nonce = encRes.fileNonce;
|
||||
expect(typeof key).toBe('string');
|
||||
expect(typeof nonce).toBe('string');
|
||||
|
||||
const decryptedPath = path.join(tmpDir, 'file_decrypted.txt');
|
||||
const decryptRes = await core.chatDecryptFile(encryptedPath, key, nonce, decryptedPath);
|
||||
expect(decryptRes).toBeDefined(); // Assuming it returns something like 'ok'
|
||||
|
||||
const decryptedContent = fs.readFileSync(decryptedPath, 'utf8');
|
||||
expect(decryptedContent).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();
|
||||
|
||||
core.chatCloseStore(ctrl);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// data DBMigrationResult
|
||||
// = DBMOk
|
||||
// | DBMInvalidConfirmation
|
||||
// | DBMErrorNotADatabase {dbFile :: String}
|
||||
// | DBMErrorMigration {dbFile :: String, migrationError :: MigrationError}
|
||||
// | DBMErrorSQL {dbFile :: String, migrationSQLError :: String}
|
||||
Reference in New Issue
Block a user