mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-26 02:24:54 +00:00
core, libs: configurable queue size, library fixes (#7542)
* core: add chat_migrate_init_queue FFI export * bots: fix BadgeServiceErrorCode API type * nodejs: pass required command fields * nodejs: fix migration error types * nodejs: install libsimplex from SIMPLEX_LIBS_DIR * nodejs: add queue size option * nodejs: regenerate docs * python: add queue size option * bots: pass incognito in APIConnect * nodejs: accept documented success responses * python: accept documented success responses * nodejs: dispatch each bot message once * nodejs: fix startChat events loop lifecycle * nodejs, python: parse multi-line bot commands * nodejs: fix file buffer handling in addon * nodejs: keep events loop when chat stop fails * python: fix send_and_wait race, load lib off loop * python: make queue size export optional * nodejs: receive events on a dedicated thread * nodejs: release haskell thread after receive * python: receive on a dedicated thread per chat * python: test receive shutdown order * nodejs: one receive thread per chat controller * nodejs: harden receiver shutdown and tests * nodejs: stop chat before closing store * python: stop chat before closing store * nodejs, python: harden close regression and retry * python: free results with ucrt on windows * nodejs: enable c++ exceptions on mac and windows
This commit is contained in:
@@ -1537,15 +1537,15 @@ Connect via prepared SimpleX link. The link can be 1-time invitation link, conta
|
||||
**Syntax**:
|
||||
|
||||
```
|
||||
/_connect <userId>[ <str(preparedLink_)>]
|
||||
/_connect <userId>[ incognito=on][ <str(preparedLink_)>]
|
||||
```
|
||||
|
||||
```javascript
|
||||
'/_connect ' + userId + (preparedLink_ ? ' ' + CreatedConnLink.cmdString(preparedLink_) : '') // JavaScript
|
||||
'/_connect ' + userId + (incognito ? ' incognito=on' : '') + (preparedLink_ ? ' ' + CreatedConnLink.cmdString(preparedLink_) : '') // JavaScript
|
||||
```
|
||||
|
||||
```python
|
||||
'/_connect ' + str(userId) + ((' ' + CreatedConnLink_cmd_string(preparedLink_)) if preparedLink_ is not None else '') # Python
|
||||
'/_connect ' + str(userId) + (' incognito=on' if incognito else '') + ((' ' + CreatedConnLink_cmd_string(preparedLink_)) if preparedLink_ is not None else '') # Python
|
||||
```
|
||||
|
||||
**Responses**:
|
||||
|
||||
+18
-56
@@ -466,62 +466,24 @@ CredentialNotVerified:
|
||||
|
||||
## BadgeServiceErrorCode
|
||||
|
||||
**Discriminated union type**:
|
||||
|
||||
BadRequest:
|
||||
- type: "badRequest"
|
||||
|
||||
UnsupportedVersion:
|
||||
- type: "unsupportedVersion"
|
||||
|
||||
UnknownPurchaseKey:
|
||||
- type: "unknownPurchaseKey"
|
||||
|
||||
UnknownOfferId:
|
||||
- type: "unknownOfferId"
|
||||
|
||||
OfferDisabled:
|
||||
- type: "offerDisabled"
|
||||
|
||||
OfferMismatch:
|
||||
- type: "offerMismatch"
|
||||
|
||||
ProductUnavailable:
|
||||
- type: "productUnavailable"
|
||||
|
||||
PaymentNotEntitled:
|
||||
- type: "paymentNotEntitled"
|
||||
|
||||
PaymentPending:
|
||||
- type: "paymentPending"
|
||||
|
||||
ProviderUnavailable:
|
||||
- type: "providerUnavailable"
|
||||
|
||||
RateLimited:
|
||||
- type: "rateLimited"
|
||||
|
||||
CodeInvalid:
|
||||
- type: "codeInvalid"
|
||||
|
||||
CodeUsed:
|
||||
- type: "codeUsed"
|
||||
|
||||
CodeExpired:
|
||||
- type: "codeExpired"
|
||||
|
||||
ReceiptInvalid:
|
||||
- type: "receiptInvalid"
|
||||
|
||||
ReceiptUsed:
|
||||
- type: "receiptUsed"
|
||||
|
||||
Internal:
|
||||
- type: "internal"
|
||||
|
||||
Unknown:
|
||||
- type: "unknown"
|
||||
- : string
|
||||
**Enum type**:
|
||||
- "bad_request"
|
||||
- "unsupported_version"
|
||||
- "unknown_purchase_key"
|
||||
- "unknown_offer_id"
|
||||
- "offer_disabled"
|
||||
- "offer_mismatch"
|
||||
- "product_unavailable"
|
||||
- "payment_not_entitled"
|
||||
- "payment_pending"
|
||||
- "provider_unavailable"
|
||||
- "rate_limited"
|
||||
- "code_invalid"
|
||||
- "code_used"
|
||||
- "code_expired"
|
||||
- "receipt_invalid"
|
||||
- "receipt_used"
|
||||
- "internal"
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -140,7 +140,7 @@ chatCommandsDocsData =
|
||||
[ ("APIAddContact", [], "Create 1-time invitation link.", ["CRInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False)),
|
||||
-- `Maybe` in `connectTarget :: Maybe ConnectTarget` is used to signal parse failure to the runtime (the handler returns CEInvalidConnReq on Nothing); it is NOT API-level optionality. The parameter is required from callers.
|
||||
("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link or name.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectTarget"),
|
||||
("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"),
|
||||
("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> Optional "" (" " <> Param "$0") "preparedLink_"),
|
||||
("Connect", [], "Connect via SimpleX link or name as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRConnectionPlan", "CRSentInvitationToContact", "CRStartedConnectionToContact", "CRStartedConnectionToGroup", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connTarget_"),
|
||||
("APIAcceptContact", ["incognito"], "Accept contact request.", ["CRAcceptingContactRequest", "CRChatCmdError"], [], Just UNInteractive, "/_accept " <> Param "contactReqId"),
|
||||
("APIRejectContact", [], "Reject contact request. The user who sent the request is **not notified**.", ["CRContactRequestRejected", "CRChatCmdError"], [], Nothing, "/_reject " <> Param "contactReqId")
|
||||
|
||||
@@ -217,7 +217,7 @@ chatTypesDocsData =
|
||||
(sti @AutoAccept, STRecord, "", [], "", ""),
|
||||
(sti @BadgeProof, STRecord, "", [], "", ""),
|
||||
(sti @BadgeRedeemError, STUnion, "BRE", [], "", ""),
|
||||
(sti @BadgeServiceErrorCode, STUnion, "BSE", [], "", ""),
|
||||
(sti @BadgeServiceErrorCode, STEnum' (consSep "BSE" '_'), "", ["BSEUnknown"], "", ""),
|
||||
(sti @BlockingInfo, STRecord, "", [], "", ""),
|
||||
(sti @BlockingReason, STEnum, "BR", [], "", ""),
|
||||
(sti @BrokerErrorType, STUnion, "", [], "", ""),
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"chat_encrypt_file"
|
||||
"chat_encrypt_media"
|
||||
"chat_migrate_init"
|
||||
"chat_migrate_init_queue"
|
||||
"chat_parse_markdown"
|
||||
"chat_parse_server"
|
||||
"chat_parse_uri"
|
||||
@@ -515,6 +516,7 @@
|
||||
"chat_encrypt_file"
|
||||
"chat_encrypt_media"
|
||||
"chat_migrate_init"
|
||||
"chat_migrate_init_queue"
|
||||
"chat_parse_markdown"
|
||||
"chat_parse_server"
|
||||
"chat_parse_uri"
|
||||
|
||||
@@ -2,7 +2,9 @@ LIBRARY libsimplex
|
||||
EXPORTS
|
||||
hs_init
|
||||
hs_init_with_rtsopts
|
||||
hs_thread_done
|
||||
chat_migrate_init
|
||||
chat_migrate_init_queue
|
||||
chat_close_store
|
||||
chat_send_cmd
|
||||
chat_send_cmd_retry
|
||||
|
||||
@@ -570,7 +570,7 @@ export namespace APIConnect {
|
||||
export type Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError
|
||||
|
||||
export function cmdString(self: APIConnect): string {
|
||||
return '/_connect ' + self.userId + (self.preparedLink_ ? ' ' + T.CreatedConnLink.cmdString(self.preparedLink_) : '')
|
||||
return '/_connect ' + self.userId + (self.incognito ? ' incognito=on' : '') + (self.preparedLink_ ? ' ' + T.CreatedConnLink.cmdString(self.preparedLink_) : '')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -301,123 +301,24 @@ export namespace BadgeRedeemError {
|
||||
}
|
||||
}
|
||||
|
||||
export type BadgeServiceErrorCode =
|
||||
| BadgeServiceErrorCode.BadRequest
|
||||
| BadgeServiceErrorCode.UnsupportedVersion
|
||||
| BadgeServiceErrorCode.UnknownPurchaseKey
|
||||
| BadgeServiceErrorCode.UnknownOfferId
|
||||
| BadgeServiceErrorCode.OfferDisabled
|
||||
| BadgeServiceErrorCode.OfferMismatch
|
||||
| BadgeServiceErrorCode.ProductUnavailable
|
||||
| BadgeServiceErrorCode.PaymentNotEntitled
|
||||
| BadgeServiceErrorCode.PaymentPending
|
||||
| BadgeServiceErrorCode.ProviderUnavailable
|
||||
| BadgeServiceErrorCode.RateLimited
|
||||
| BadgeServiceErrorCode.CodeInvalid
|
||||
| BadgeServiceErrorCode.CodeUsed
|
||||
| BadgeServiceErrorCode.CodeExpired
|
||||
| BadgeServiceErrorCode.ReceiptInvalid
|
||||
| BadgeServiceErrorCode.ReceiptUsed
|
||||
| BadgeServiceErrorCode.Internal
|
||||
| BadgeServiceErrorCode.Unknown
|
||||
|
||||
export namespace BadgeServiceErrorCode {
|
||||
export type Tag =
|
||||
| "badRequest"
|
||||
| "unsupportedVersion"
|
||||
| "unknownPurchaseKey"
|
||||
| "unknownOfferId"
|
||||
| "offerDisabled"
|
||||
| "offerMismatch"
|
||||
| "productUnavailable"
|
||||
| "paymentNotEntitled"
|
||||
| "paymentPending"
|
||||
| "providerUnavailable"
|
||||
| "rateLimited"
|
||||
| "codeInvalid"
|
||||
| "codeUsed"
|
||||
| "codeExpired"
|
||||
| "receiptInvalid"
|
||||
| "receiptUsed"
|
||||
| "internal"
|
||||
| "unknown"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface BadRequest extends Interface {
|
||||
type: "badRequest"
|
||||
}
|
||||
|
||||
export interface UnsupportedVersion extends Interface {
|
||||
type: "unsupportedVersion"
|
||||
}
|
||||
|
||||
export interface UnknownPurchaseKey extends Interface {
|
||||
type: "unknownPurchaseKey"
|
||||
}
|
||||
|
||||
export interface UnknownOfferId extends Interface {
|
||||
type: "unknownOfferId"
|
||||
}
|
||||
|
||||
export interface OfferDisabled extends Interface {
|
||||
type: "offerDisabled"
|
||||
}
|
||||
|
||||
export interface OfferMismatch extends Interface {
|
||||
type: "offerMismatch"
|
||||
}
|
||||
|
||||
export interface ProductUnavailable extends Interface {
|
||||
type: "productUnavailable"
|
||||
}
|
||||
|
||||
export interface PaymentNotEntitled extends Interface {
|
||||
type: "paymentNotEntitled"
|
||||
}
|
||||
|
||||
export interface PaymentPending extends Interface {
|
||||
type: "paymentPending"
|
||||
}
|
||||
|
||||
export interface ProviderUnavailable extends Interface {
|
||||
type: "providerUnavailable"
|
||||
}
|
||||
|
||||
export interface RateLimited extends Interface {
|
||||
type: "rateLimited"
|
||||
}
|
||||
|
||||
export interface CodeInvalid extends Interface {
|
||||
type: "codeInvalid"
|
||||
}
|
||||
|
||||
export interface CodeUsed extends Interface {
|
||||
type: "codeUsed"
|
||||
}
|
||||
|
||||
export interface CodeExpired extends Interface {
|
||||
type: "codeExpired"
|
||||
}
|
||||
|
||||
export interface ReceiptInvalid extends Interface {
|
||||
type: "receiptInvalid"
|
||||
}
|
||||
|
||||
export interface ReceiptUsed extends Interface {
|
||||
type: "receiptUsed"
|
||||
}
|
||||
|
||||
export interface Internal extends Interface {
|
||||
type: "internal"
|
||||
}
|
||||
|
||||
export interface Unknown extends Interface {
|
||||
type: "unknown"
|
||||
: string
|
||||
}
|
||||
export enum BadgeServiceErrorCode {
|
||||
Bad_request = "bad_request",
|
||||
Unsupported_version = "unsupported_version",
|
||||
Unknown_purchase_key = "unknown_purchase_key",
|
||||
Unknown_offer_id = "unknown_offer_id",
|
||||
Offer_disabled = "offer_disabled",
|
||||
Offer_mismatch = "offer_mismatch",
|
||||
Product_unavailable = "product_unavailable",
|
||||
Payment_not_entitled = "payment_not_entitled",
|
||||
Payment_pending = "payment_pending",
|
||||
Provider_unavailable = "provider_unavailable",
|
||||
Rate_limited = "rate_limited",
|
||||
Code_invalid = "code_invalid",
|
||||
Code_used = "code_used",
|
||||
Code_expired = "code_expired",
|
||||
Receipt_invalid = "receipt_invalid",
|
||||
Receipt_used = "receipt_used",
|
||||
Internal = "internal",
|
||||
}
|
||||
|
||||
export enum BadgeStatus {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
],
|
||||
"cflags!": [ "-fno-exceptions" ],
|
||||
"cflags_cc!": [ "-fno-exceptions" ],
|
||||
"xcode_settings": { "GCC_ENABLE_CPP_EXCEPTIONS": "YES" },
|
||||
"msvs_settings": { "VCCLCompilerTool": { "ExceptionHandling": 1 } },
|
||||
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ],
|
||||
"conditions": [
|
||||
["OS=='mac'", {
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <cstdlib>
|
||||
#include <climits>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <system_error>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <unordered_map>
|
||||
#include "simplex.h"
|
||||
|
||||
namespace simplex {
|
||||
@@ -81,6 +89,11 @@ class ResultAsyncWorker : public AsyncWorker {
|
||||
return ctrl_;
|
||||
}
|
||||
|
||||
// the worker thread reads this object's memory until the worker completes
|
||||
void KeepAlive(Object obj) {
|
||||
keep_alive_ = Persistent(obj);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::string result_;
|
||||
uintptr_t ctrl_ = 0;
|
||||
@@ -88,6 +101,7 @@ class ResultAsyncWorker : public AsyncWorker {
|
||||
private:
|
||||
ExecuteFn execute_fn_;
|
||||
ResultProcessor result_processor_;
|
||||
ObjectReference keep_alive_;
|
||||
};
|
||||
|
||||
class BinaryAsyncWorker : public AsyncWorker {
|
||||
@@ -97,21 +111,25 @@ class BinaryAsyncWorker : public AsyncWorker {
|
||||
BinaryAsyncWorker(Function& callback, ExecuteFn execute_fn)
|
||||
: AsyncWorker(callback), execute_fn_(std::move(execute_fn)) {}
|
||||
|
||||
~BinaryAsyncWorker() {
|
||||
free(original_buf);
|
||||
}
|
||||
|
||||
void Execute() override {
|
||||
execute_fn_(this);
|
||||
}
|
||||
|
||||
void OnOK() override {
|
||||
HandleScope scope(Env());
|
||||
if (original_buf == nullptr || binary_len == 0) {
|
||||
Callback().Call({Env().Null(), Env().Undefined()});
|
||||
char* buf = original_buf;
|
||||
original_buf = nullptr;
|
||||
if (binary_len == 0) {
|
||||
free(buf);
|
||||
Callback().Call({Env().Null(), Buffer<char>::New(Env(), 0)});
|
||||
return;
|
||||
}
|
||||
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);
|
||||
// Copies when the runtime forbids external buffers (Electron); the finalizer then runs immediately.
|
||||
Buffer<char> buffer = Buffer<char>::NewOrCopy(Env(), buf + 5, binary_len, [](Napi::Env, char*, char* orig) { free(orig); }, buf);
|
||||
Callback().Call({Env().Null(), buffer});
|
||||
}
|
||||
|
||||
@@ -176,6 +194,159 @@ Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) {
|
||||
return deferred.Promise();
|
||||
}
|
||||
|
||||
const char* const RECEIVER_STOPPED = "chat receiver stopped";
|
||||
|
||||
struct RecvRequest {
|
||||
int wait = 0;
|
||||
std::shared_ptr<Promise::Deferred> deferred;
|
||||
};
|
||||
|
||||
// Holds the event loop open only while receives are pending; used only on the JS main thread.
|
||||
class PendingReceives {
|
||||
public:
|
||||
explicit PendingReceives(ThreadSafeFunction tsfn) : tsfn_(tsfn) {}
|
||||
|
||||
const ThreadSafeFunction& Tsfn() const {
|
||||
return tsfn_;
|
||||
}
|
||||
|
||||
void Add(Napi::Env env) {
|
||||
if (count_++ == 0) tsfn_.Ref(env);
|
||||
}
|
||||
|
||||
void Remove(Napi::Env env) {
|
||||
if (--count_ == 0) tsfn_.Unref(env);
|
||||
}
|
||||
|
||||
private:
|
||||
ThreadSafeFunction tsfn_;
|
||||
size_t count_ = 0;
|
||||
};
|
||||
|
||||
// A blocking receive would hold a libuv pool thread for up to `wait`, stalling fs, dns and crypto.
|
||||
class Receiver {
|
||||
public:
|
||||
// Returns nullptr with a pending JS exception if the TSFN cannot be created, throws std::system_error if the thread cannot start.
|
||||
static std::shared_ptr<Receiver> Start(Napi::Env env, chat_ctrl ctrl) {
|
||||
ThreadSafeFunction tsfn = ThreadSafeFunction::New(env, Function::New(env, [](const CallbackInfo&) {}), "chat_recv_msg_wait", 0, 1);
|
||||
if (env.IsExceptionPending()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto receiver = std::make_shared<Receiver>(ctrl, tsfn);
|
||||
try {
|
||||
receiver->thread_ = std::thread(&Receiver::Run, receiver.get());
|
||||
} catch (const std::system_error&) {
|
||||
tsfn.Release();
|
||||
throw;
|
||||
}
|
||||
return receiver;
|
||||
}
|
||||
|
||||
Receiver(chat_ctrl ctrl, ThreadSafeFunction tsfn) : ctrl_(ctrl), pending_(std::make_shared<PendingReceives>(tsfn)) {}
|
||||
|
||||
Receiver(const Receiver&) = delete;
|
||||
Receiver& operator=(const Receiver&) = delete;
|
||||
|
||||
~Receiver() {
|
||||
Stop();
|
||||
}
|
||||
|
||||
void Enqueue(Napi::Env env, RecvRequest request) {
|
||||
pending_->Add(env);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
queue_.push_back(std::move(request));
|
||||
}
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
void RequestStop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
stop_ = true;
|
||||
}
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
// Waits for the receive in progress, so it must not run on the JS main thread outside env teardown.
|
||||
void Stop() {
|
||||
RequestStop();
|
||||
if (thread_.joinable()) {
|
||||
thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void Run() {
|
||||
for (;;) {
|
||||
RecvRequest request;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
cv_.wait(lock, [this] { return stop_ || !queue_.empty(); });
|
||||
if (stop_) break;
|
||||
request = std::move(queue_.front());
|
||||
queue_.pop_front();
|
||||
}
|
||||
char* c_res = chat_recv_msg_wait(ctrl_, request.wait);
|
||||
napi_status status = Settle(request.deferred, [c_res](Napi::Env env, Promise::Deferred& deferred) {
|
||||
if (c_res == nullptr) {
|
||||
deferred.Reject(Error::New(env, "chat_recv_msg_wait failed").Value());
|
||||
} else {
|
||||
deferred.Resolve(String::New(env, c_res));
|
||||
free(c_res);
|
||||
}
|
||||
});
|
||||
if (status != napi_ok) {
|
||||
free(c_res);
|
||||
}
|
||||
}
|
||||
std::deque<RecvRequest> unserved;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
unserved.swap(queue_);
|
||||
}
|
||||
for (RecvRequest& request : unserved) {
|
||||
Settle(request.deferred, [](Napi::Env env, Promise::Deferred& deferred) {
|
||||
deferred.Reject(Error::New(env, RECEIVER_STOPPED).Value());
|
||||
});
|
||||
}
|
||||
pending_->Tsfn().Release();
|
||||
// Each OS thread that enters Haskell keeps an RTS task record until it calls hs_thread_done.
|
||||
hs_thread_done();
|
||||
}
|
||||
|
||||
template <typename SettleFn>
|
||||
napi_status Settle(std::shared_ptr<Promise::Deferred> deferred, SettleFn settle) {
|
||||
// The callback may run after this Receiver is destroyed, so it owns the pending count.
|
||||
std::shared_ptr<PendingReceives> pending = pending_;
|
||||
return pending->Tsfn().BlockingCall([pending, deferred, settle](Napi::Env env, Function) {
|
||||
settle(env, *deferred);
|
||||
pending->Remove(env);
|
||||
});
|
||||
}
|
||||
|
||||
const chat_ctrl ctrl_;
|
||||
const std::shared_ptr<PendingReceives> pending_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
std::deque<RecvRequest> queue_;
|
||||
bool stop_ = false;
|
||||
std::thread thread_;
|
||||
};
|
||||
|
||||
// Keyed by chat_ctrl, accessed only on the JS main thread.
|
||||
using Receivers = std::unordered_map<uintptr_t, std::shared_ptr<Receiver>>;
|
||||
|
||||
std::shared_ptr<Receiver> TakeReceiver(Receivers& receivers, chat_ctrl ctrl) {
|
||||
auto it = receivers.find(reinterpret_cast<uintptr_t>(ctrl));
|
||||
if (it == receivers.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
std::shared_ptr<Receiver> receiver = std::move(it->second);
|
||||
receivers.erase(it);
|
||||
return receiver;
|
||||
}
|
||||
|
||||
// Common result processors
|
||||
ResultAsyncWorker::ResultProcessor MigrateResultProcessor() {
|
||||
return [](ResultAsyncWorker* worker, Napi::Env env) {
|
||||
@@ -215,6 +386,39 @@ Value ChatMigrateInit(const CallbackInfo& args) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
Value ChatMigrateInitQueue(const CallbackInfo& args) {
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsNumber()) {
|
||||
TypeError::New(env, "Expected three string arguments and number").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
std::string path = args[0].As<String>().Utf8Value();
|
||||
std::string key = args[1].As<String>().Utf8Value();
|
||||
std::string confirm = args[2].As<String>().Utf8Value();
|
||||
Number queue_size_arg = args[3].As<Number>();
|
||||
int queue_size = queue_size_arg.Int32Value();
|
||||
if (static_cast<double>(queue_size) != queue_size_arg.DoubleValue()) {
|
||||
RangeError::New(env, "Expected 32-bit integer queue size").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [path, key, confirm, queue_size](ResultAsyncWorker* worker) {
|
||||
chat_ctrl ctrl = nullptr;
|
||||
char* c_res = chat_migrate_init_queue(path.c_str(), key.c_str(), confirm.c_str(), queue_size, &ctrl);
|
||||
worker->SetCtrl(reinterpret_cast<uintptr_t>(ctrl));
|
||||
HandleCResult(worker, c_res, "chat_migrate_init_queue");
|
||||
};
|
||||
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor());
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
Value ChatCloseStore(const CallbackInfo& args) {
|
||||
Env env = args.Env();
|
||||
if (args.Length() < 1 || !args[0].IsBigInt()) {
|
||||
@@ -223,11 +427,15 @@ Value ChatCloseStore(const CallbackInfo& args) {
|
||||
}
|
||||
|
||||
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
||||
std::shared_ptr<Receiver> receiver = TakeReceiver(*static_cast<Receivers*>(args.Data()), ctrl);
|
||||
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [ctrl](ResultAsyncWorker* worker) {
|
||||
auto execute_fn = [ctrl, receiver](ResultAsyncWorker* worker) {
|
||||
if (receiver) {
|
||||
receiver->Stop();
|
||||
}
|
||||
char* c_res = chat_close_store(ctrl);
|
||||
HandleCResult(worker, c_res, "chat_close_store");
|
||||
};
|
||||
@@ -271,44 +479,63 @@ Value ChatRecvMsgWait(const CallbackInfo& args) {
|
||||
|
||||
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
||||
int wait = static_cast<int>(args[1].As<Number>().Int32Value());
|
||||
Receivers& receivers = *static_cast<Receivers*>(args.Data());
|
||||
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
auto deferred = std::make_shared<Promise::Deferred>(Promise::Deferred::New(env));
|
||||
auto it = receivers.find(reinterpret_cast<uintptr_t>(ctrl));
|
||||
if (it == receivers.end()) {
|
||||
std::shared_ptr<Receiver> receiver;
|
||||
try {
|
||||
receiver = Receiver::Start(env, ctrl);
|
||||
} catch (const std::system_error& e) {
|
||||
deferred->Reject(Error::New(env, e.what()).Value());
|
||||
return deferred->Promise();
|
||||
}
|
||||
if (!receiver) {
|
||||
return env.Undefined();
|
||||
}
|
||||
it = receivers.emplace(reinterpret_cast<uintptr_t>(ctrl), std::move(receiver)).first;
|
||||
}
|
||||
it->second->Enqueue(env, {wait, deferred});
|
||||
|
||||
auto execute_fn = [ctrl, wait](ResultAsyncWorker* worker) {
|
||||
char* c_res = chat_recv_msg_wait(ctrl, wait);
|
||||
HandleCResult(worker, c_res, "chat_recv_msg_wait");
|
||||
};
|
||||
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
return deferred->Promise();
|
||||
}
|
||||
|
||||
Value ChatWriteFile(const CallbackInfo& args) {
|
||||
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();
|
||||
if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !(args[2].IsArrayBuffer() || args[2].IsTypedArray())) {
|
||||
TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer or Uint8Array").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
||||
std::string path = args[1].As<String>().Utf8Value();
|
||||
ArrayBuffer ab = args[2].As<ArrayBuffer>();
|
||||
char* data = static_cast<char*>(ab.Data());
|
||||
size_t len = ab.ByteLength();
|
||||
char* data;
|
||||
size_t len;
|
||||
if (args[2].IsArrayBuffer()) {
|
||||
ArrayBuffer ab = args[2].As<ArrayBuffer>();
|
||||
data = static_cast<char*>(ab.Data());
|
||||
len = ab.ByteLength();
|
||||
} else {
|
||||
TypedArray view = args[2].As<TypedArray>();
|
||||
data = static_cast<char*>(view.ArrayBuffer().Data()) + view.ByteOffset();
|
||||
len = view.ByteLength();
|
||||
}
|
||||
if (len > static_cast<size_t>(INT_MAX)) {
|
||||
RangeError::New(env, "Buffer is too large").ThrowAsJavaScriptException();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
Function cb;
|
||||
Promise promise = CreatePromiseAndCallback(env, cb);
|
||||
|
||||
auto execute_fn = [ctrl, path, ab, data, len](ResultAsyncWorker* worker) {
|
||||
(void)ab; // to keep ArrayBuffer alive
|
||||
auto execute_fn = [ctrl, path, data, len](ResultAsyncWorker* worker) {
|
||||
char* c_res = chat_write_file(ctrl, path.c_str(), data, static_cast<int>(len));
|
||||
HandleCResult(worker, c_res, "chat_write_file");
|
||||
};
|
||||
|
||||
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
||||
worker->KeepAlive(args[2].As<Object>());
|
||||
worker->Queue();
|
||||
|
||||
return promise;
|
||||
@@ -410,10 +637,19 @@ Value ChatDecryptFile(const CallbackInfo& args) {
|
||||
|
||||
Object Init(Env env, Object exports) {
|
||||
haskell_init();
|
||||
auto* receivers = new Receivers();
|
||||
// Stopping all receivers before joining any bounds teardown by the longest in-flight receive.
|
||||
env.AddCleanupHook([receivers]() {
|
||||
for (auto& entry : *receivers) {
|
||||
entry.second->RequestStop();
|
||||
}
|
||||
delete receivers;
|
||||
});
|
||||
exports.Set("chat_migrate_init", Function::New(env, ChatMigrateInit));
|
||||
exports.Set("chat_close_store", Function::New(env, ChatCloseStore));
|
||||
exports.Set("chat_migrate_init_queue", Function::New(env, ChatMigrateInitQueue));
|
||||
exports.Set("chat_close_store", Function::New(env, ChatCloseStore, "chat_close_store", receivers));
|
||||
exports.Set("chat_send_cmd", Function::New(env, ChatSendCmd));
|
||||
exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait));
|
||||
exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait, "chat_recv_msg_wait", receivers));
|
||||
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));
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
|
||||
extern "C" void hs_init(int argc, char **argv[]);
|
||||
extern "C" void hs_init_with_rtsopts(int * argc, char **argv[]);
|
||||
extern "C" void hs_thread_done(void);
|
||||
|
||||
typedef long* chat_ctrl;
|
||||
|
||||
// the last parameter is used to return the pointer to chat controller
|
||||
extern "C" char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
|
||||
extern "C" char *chat_migrate_init_queue(const char *path, const char *key, const char *confirm, const int queueSize, chat_ctrl *ctrl);
|
||||
extern "C" char *chat_close_store(chat_ctrl ctrl);
|
||||
extern "C" char *chat_reopen_store(chat_ctrl ctrl);
|
||||
extern "C" char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
|
||||
|
||||
@@ -21,3 +21,4 @@ It automates creating and updating of the bot profile, address and bot commands
|
||||
## Functions
|
||||
|
||||
- [run](bot.Function.run.md)
|
||||
- [subscribeChatItems](bot.Function.subscribeChatItems.md)
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [src/api.ts:103](../src/api.ts#L103)
|
||||
|
||||
> **get** **ctrl**(): `bigint`
|
||||
|
||||
Defined in: [src/api.ts:329](../src/api.ts#L329)
|
||||
Defined in: [src/api.ts:344](../src/api.ts#L344)
|
||||
|
||||
Chat controller reference
|
||||
|
||||
@@ -42,7 +42,7 @@ Chat controller reference
|
||||
|
||||
> **get** **initialized**(): `boolean`
|
||||
|
||||
Defined in: [src/api.ts:315](../src/api.ts#L315)
|
||||
Defined in: [src/api.ts:330](../src/api.ts#L330)
|
||||
|
||||
Chat controller is initialized
|
||||
|
||||
@@ -58,7 +58,7 @@ Chat controller is initialized
|
||||
|
||||
> **get** **started**(): `boolean`
|
||||
|
||||
Defined in: [src/api.ts:322](../src/api.ts#L322)
|
||||
Defined in: [src/api.ts:337](../src/api.ts#L337)
|
||||
|
||||
Chat controller is started
|
||||
|
||||
@@ -72,7 +72,7 @@ Chat controller is started
|
||||
|
||||
> **apiAcceptContactRequest**(`contactReqId`): `Promise`\<`Contact`\>
|
||||
|
||||
Defined in: [src/api.ts:731](../src/api.ts#L731)
|
||||
Defined in: [src/api.ts:750](../src/api.ts#L750)
|
||||
|
||||
Accept contact request.
|
||||
Network usage: interactive.
|
||||
@@ -93,7 +93,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiAcceptMember**(`groupId`, `groupMemberId`, `memberRole`): `Promise`\<`GroupMember`\>
|
||||
|
||||
Defined in: [src/api.ts:551](../src/api.ts#L551)
|
||||
Defined in: [src/api.ts:570](../src/api.ts#L570)
|
||||
|
||||
Accept group member. Requires Admin role.
|
||||
Network usage: background.
|
||||
@@ -122,7 +122,7 @@ Network usage: background.
|
||||
|
||||
> **apiAddMember**(`groupId`, `contactId`, `memberRole`): `Promise`\<`GroupMember`\>
|
||||
|
||||
Defined in: [src/api.ts:531](../src/api.ts#L531)
|
||||
Defined in: [src/api.ts:550](../src/api.ts#L550)
|
||||
|
||||
Add contact to group. Requires bot to have Admin role.
|
||||
Network usage: interactive.
|
||||
@@ -151,7 +151,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiBlockMembersForAll**(`groupId`, `groupMemberIds`, `blocked`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:571](../src/api.ts#L571)
|
||||
Defined in: [src/api.ts:590](../src/api.ts#L590)
|
||||
|
||||
Block members. Requires Moderator role.
|
||||
Network usage: background.
|
||||
@@ -180,7 +180,7 @@ Network usage: background.
|
||||
|
||||
> **apiCancelFile**(`fileId`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:521](../src/api.ts#L521)
|
||||
Defined in: [src/api.ts:540](../src/api.ts#L540)
|
||||
|
||||
Cancel file.
|
||||
Network usage: background.
|
||||
@@ -199,9 +199,9 @@ Network usage: background.
|
||||
|
||||
### apiChatItemReaction()
|
||||
|
||||
> **apiChatItemReaction**(`chatType`, `chatId`, `chatItemId`, `add`, `reaction`): `Promise`\<`ChatItemDeletion`[]\>
|
||||
> **apiChatItemReaction**(`chatType`, `chatId`, `chatItemId`, `add`, `reaction`): `Promise`\<`ACIReaction`\>
|
||||
|
||||
Defined in: [src/api.ts:495](../src/api.ts#L495)
|
||||
Defined in: [src/api.ts:513](../src/api.ts#L513)
|
||||
|
||||
Add/remove message reaction.
|
||||
Network usage: background.
|
||||
@@ -230,15 +230,15 @@ Network usage: background.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`ChatItemDeletion`[]\>
|
||||
`Promise`\<`ACIReaction`\>
|
||||
|
||||
***
|
||||
|
||||
### apiConnect()
|
||||
|
||||
> **apiConnect**(`userId`, `incognito`, `preparedLink?`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\>
|
||||
> **apiConnect**(`userId`, `incognito`, `preparedLink`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\>
|
||||
|
||||
Defined in: [src/api.ts:700](../src/api.ts#L700)
|
||||
Defined in: [src/api.ts:719](../src/api.ts#L719)
|
||||
|
||||
Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link
|
||||
Network usage: interactive.
|
||||
@@ -253,7 +253,7 @@ Network usage: interactive.
|
||||
|
||||
`boolean`
|
||||
|
||||
##### preparedLink?
|
||||
##### preparedLink
|
||||
|
||||
`CreatedConnLink`
|
||||
|
||||
@@ -267,7 +267,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiConnectActiveUser**(`connLink`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\>
|
||||
|
||||
Defined in: [src/api.ts:709](../src/api.ts#L709)
|
||||
Defined in: [src/api.ts:728](../src/api.ts#L728)
|
||||
|
||||
Connect via SimpleX link as string in the active user profile.
|
||||
Network usage: interactive.
|
||||
@@ -288,7 +288,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiConnectPlan**(`userId`, `connectionLink`): `Promise`\<\[`ConnectionPlan`, `CreatedConnLink`\]\>
|
||||
|
||||
Defined in: [src/api.ts:690](../src/api.ts#L690)
|
||||
Defined in: [src/api.ts:709](../src/api.ts#L709)
|
||||
|
||||
Determine SimpleX link type and if the bot is already connected via this link.
|
||||
Network usage: interactive.
|
||||
@@ -313,7 +313,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiCreateActiveUser**(`profile?`): `Promise`\<`User`\>
|
||||
|
||||
Defined in: [src/api.ts:849](../src/api.ts#L849)
|
||||
Defined in: [src/api.ts:887](../src/api.ts#L887)
|
||||
|
||||
Create new user profile
|
||||
Network usage: no.
|
||||
@@ -334,7 +334,7 @@ Network usage: no.
|
||||
|
||||
> **apiCreateGroupLink**(`groupId`, `memberRole`): `Promise`\<`string`\>
|
||||
|
||||
Defined in: [src/api.ts:631](../src/api.ts#L631)
|
||||
Defined in: [src/api.ts:650](../src/api.ts#L650)
|
||||
|
||||
Create group link.
|
||||
Network usage: interactive.
|
||||
@@ -359,7 +359,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiCreateLink**(`userId`): `Promise`\<`string`\>
|
||||
|
||||
Defined in: [src/api.ts:677](../src/api.ts#L677)
|
||||
Defined in: [src/api.ts:696](../src/api.ts#L696)
|
||||
|
||||
Create 1-time invitation link.
|
||||
Network usage: interactive.
|
||||
@@ -380,7 +380,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiCreateMemberContact**(`groupId`, `groupMemberId`): `Promise`\<`Contact`\>
|
||||
|
||||
Defined in: [src/api.ts:915](../src/api.ts#L915)
|
||||
Defined in: [src/api.ts:953](../src/api.ts#L953)
|
||||
|
||||
Create a direct message contact with a group member.
|
||||
Returns the created contact.
|
||||
@@ -406,7 +406,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiCreateUserAddress**(`userId`): `Promise`\<`CreatedConnLink`\>
|
||||
|
||||
Defined in: [src/api.ts:346](../src/api.ts#L346)
|
||||
Defined in: [src/api.ts:361](../src/api.ts#L361)
|
||||
|
||||
Create bot address.
|
||||
Network usage: interactive.
|
||||
@@ -427,7 +427,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiDeleteChat**(`chatType`, `chatId`, `deleteMode?`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:771](../src/api.ts#L771)
|
||||
Defined in: [src/api.ts:809](../src/api.ts#L809)
|
||||
|
||||
Delete chat.
|
||||
Network usage: background.
|
||||
@@ -456,7 +456,7 @@ Network usage: background.
|
||||
|
||||
> **apiDeleteChatItems**(`chatType`, `chatId`, `chatItemIds`, `deleteMode`): `Promise`\<`ChatItemDeletion`[]\>
|
||||
|
||||
Defined in: [src/api.ts:470](../src/api.ts#L470)
|
||||
Defined in: [src/api.ts:488](../src/api.ts#L488)
|
||||
|
||||
Delete message.
|
||||
Network usage: background.
|
||||
@@ -489,7 +489,7 @@ Network usage: background.
|
||||
|
||||
> **apiDeleteGroupLink**(`groupId`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:653](../src/api.ts#L653)
|
||||
Defined in: [src/api.ts:672](../src/api.ts#L672)
|
||||
|
||||
Delete group link.
|
||||
Network usage: background.
|
||||
@@ -510,7 +510,7 @@ Network usage: background.
|
||||
|
||||
> **apiDeleteMemberChatItem**(`groupId`, `chatItemIds`): `Promise`\<`ChatItemDeletion`[]\>
|
||||
|
||||
Defined in: [src/api.ts:485](../src/api.ts#L485)
|
||||
Defined in: [src/api.ts:503](../src/api.ts#L503)
|
||||
|
||||
Moderate message. Requires Moderator role (and higher than message author's).
|
||||
Network usage: background.
|
||||
@@ -535,7 +535,7 @@ Network usage: background.
|
||||
|
||||
> **apiDeleteUser**(`userId`, `delSMPQueues`, `viewPwd?`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:879](../src/api.ts#L879)
|
||||
Defined in: [src/api.ts:917](../src/api.ts#L917)
|
||||
|
||||
Delete user profile.
|
||||
Network usage: background.
|
||||
@@ -564,7 +564,7 @@ Network usage: background.
|
||||
|
||||
> **apiDeleteUserAddress**(`userId`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:356](../src/api.ts#L356)
|
||||
Defined in: [src/api.ts:371](../src/api.ts#L371)
|
||||
|
||||
Deletes a user address.
|
||||
Network usage: background.
|
||||
@@ -585,7 +585,7 @@ Network usage: background.
|
||||
|
||||
> **apiGetActiveUser**(): `Promise`\<`User` \| `undefined`\>
|
||||
|
||||
Defined in: [src/api.ts:829](../src/api.ts#L829)
|
||||
Defined in: [src/api.ts:867](../src/api.ts#L867)
|
||||
|
||||
Get active user profile
|
||||
Network usage: no.
|
||||
@@ -600,7 +600,7 @@ Network usage: no.
|
||||
|
||||
> **apiGetChat**(`chatType`, `chatId`, `count`): `Promise`\<`any`\>
|
||||
|
||||
Defined in: [src/api.ts:819](../src/api.ts#L819)
|
||||
Defined in: [src/api.ts:857](../src/api.ts#L857)
|
||||
|
||||
Get chat items.
|
||||
Network usage: no.
|
||||
@@ -625,11 +625,48 @@ Network usage: no.
|
||||
|
||||
***
|
||||
|
||||
### apiGetChats()
|
||||
|
||||
> **apiGetChats**(`userId`, `pagination`, `query?`, `pendingConnections?`): `Promise`\<`AChat`[]\>
|
||||
|
||||
Defined in: [src/api.ts:794](../src/api.ts#L794)
|
||||
|
||||
Get chat previews (paginated).
|
||||
Network usage: no.
|
||||
|
||||
Prefer this over apiListContacts / apiListGroups for any scan: those
|
||||
methods load every record into memory in a single response and will fail
|
||||
on large databases.
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### userId
|
||||
|
||||
`number`
|
||||
|
||||
##### pagination
|
||||
|
||||
`Last`
|
||||
|
||||
##### query?
|
||||
|
||||
`ChatListQuery` = `...`
|
||||
|
||||
##### pendingConnections?
|
||||
|
||||
`boolean` = `false`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`AChat`[]\>
|
||||
|
||||
***
|
||||
|
||||
### apiGetGroupLink()
|
||||
|
||||
> **apiGetGroupLink**(`groupId`): `Promise`\<`GroupLink`\>
|
||||
|
||||
Defined in: [src/api.ts:662](../src/api.ts#L662)
|
||||
Defined in: [src/api.ts:681](../src/api.ts#L681)
|
||||
|
||||
Get group link.
|
||||
Network usage: no.
|
||||
@@ -650,7 +687,7 @@ Network usage: no.
|
||||
|
||||
> **apiGetGroupLinkStr**(`groupId`): `Promise`\<`string`\>
|
||||
|
||||
Defined in: [src/api.ts:668](../src/api.ts#L668)
|
||||
Defined in: [src/api.ts:687](../src/api.ts#L687)
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -668,7 +705,7 @@ Defined in: [src/api.ts:668](../src/api.ts#L668)
|
||||
|
||||
> **apiGetUserAddress**(`userId`): `Promise`\<`UserContactLink` \| `undefined`\>
|
||||
|
||||
Defined in: [src/api.ts:366](../src/api.ts#L366)
|
||||
Defined in: [src/api.ts:381](../src/api.ts#L381)
|
||||
|
||||
Get bot address and settings.
|
||||
Network usage: no.
|
||||
@@ -689,7 +726,7 @@ Network usage: no.
|
||||
|
||||
> **apiJoinGroup**(`groupId`): `Promise`\<`GroupInfo`\>
|
||||
|
||||
Defined in: [src/api.ts:541](../src/api.ts#L541)
|
||||
Defined in: [src/api.ts:560](../src/api.ts#L560)
|
||||
|
||||
Join group.
|
||||
Network usage: interactive.
|
||||
@@ -710,7 +747,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiLeaveGroup**(`groupId`): `Promise`\<`GroupInfo`\>
|
||||
|
||||
Defined in: [src/api.ts:591](../src/api.ts#L591)
|
||||
Defined in: [src/api.ts:610](../src/api.ts#L610)
|
||||
|
||||
Leave group.
|
||||
Network usage: background.
|
||||
@@ -731,7 +768,7 @@ Network usage: background.
|
||||
|
||||
> **apiListContacts**(`userId`): `Promise`\<`Contact`[]\>
|
||||
|
||||
Defined in: [src/api.ts:751](../src/api.ts#L751)
|
||||
Defined in: [src/api.ts:770](../src/api.ts#L770)
|
||||
|
||||
Get contacts.
|
||||
Network usage: no.
|
||||
@@ -752,7 +789,7 @@ Network usage: no.
|
||||
|
||||
> **apiListGroups**(`userId`, `contactId?`, `search?`): `Promise`\<`GroupInfo`[]\>
|
||||
|
||||
Defined in: [src/api.ts:761](../src/api.ts#L761)
|
||||
Defined in: [src/api.ts:780](../src/api.ts#L780)
|
||||
|
||||
Get groups.
|
||||
Network usage: no.
|
||||
@@ -781,7 +818,7 @@ Network usage: no.
|
||||
|
||||
> **apiListMembers**(`groupId`): `Promise`\<`GroupMember`[]\>
|
||||
|
||||
Defined in: [src/api.ts:601](../src/api.ts#L601)
|
||||
Defined in: [src/api.ts:620](../src/api.ts#L620)
|
||||
|
||||
Get group members.
|
||||
Network usage: no.
|
||||
@@ -802,7 +839,7 @@ Network usage: no.
|
||||
|
||||
> **apiListUsers**(): `Promise`\<`UserInfo`[]\>
|
||||
|
||||
Defined in: [src/api.ts:859](../src/api.ts#L859)
|
||||
Defined in: [src/api.ts:897](../src/api.ts#L897)
|
||||
|
||||
Get all user profiles
|
||||
Network usage: no.
|
||||
@@ -817,7 +854,7 @@ Network usage: no.
|
||||
|
||||
> **apiNewGroup**(`userId`, `groupProfile`): `Promise`\<`GroupInfo`\>
|
||||
|
||||
Defined in: [src/api.ts:611](../src/api.ts#L611)
|
||||
Defined in: [src/api.ts:630](../src/api.ts#L630)
|
||||
|
||||
Create group.
|
||||
Network usage: no.
|
||||
@@ -842,7 +879,7 @@ Network usage: no.
|
||||
|
||||
> **apiReceiveFile**(`fileId`): `Promise`\<`AChatItem`\>
|
||||
|
||||
Defined in: [src/api.ts:511](../src/api.ts#L511)
|
||||
Defined in: [src/api.ts:529](../src/api.ts#L529)
|
||||
|
||||
Receive file.
|
||||
Network usage: no.
|
||||
@@ -863,7 +900,7 @@ Network usage: no.
|
||||
|
||||
> **apiRejectContactRequest**(`contactReqId`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:741](../src/api.ts#L741)
|
||||
Defined in: [src/api.ts:760](../src/api.ts#L760)
|
||||
|
||||
Reject contact request. The user who sent the request is **not notified**.
|
||||
Network usage: no.
|
||||
@@ -884,7 +921,7 @@ Network usage: no.
|
||||
|
||||
> **apiRemoveMembers**(`groupId`, `memberIds`, `withMessages?`): `Promise`\<`GroupMember`[]\>
|
||||
|
||||
Defined in: [src/api.ts:581](../src/api.ts#L581)
|
||||
Defined in: [src/api.ts:600](../src/api.ts#L600)
|
||||
|
||||
Remove members. Requires Admin role.
|
||||
Network usage: background.
|
||||
@@ -913,7 +950,7 @@ Network usage: background.
|
||||
|
||||
> **apiSendMemberContactInvitation**(`contactId`, `message?`): `Promise`\<`Contact`\>
|
||||
|
||||
Defined in: [src/api.ts:926](../src/api.ts#L926)
|
||||
Defined in: [src/api.ts:964](../src/api.ts#L964)
|
||||
|
||||
Send a direct message invitation to a group member contact.
|
||||
The contact must have been created with [apiCreateMemberContact](#apicreatemembercontact).
|
||||
@@ -939,7 +976,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiSendMessages**(`chat`, `messages`, `liveMessage?`): `Promise`\<`AChatItem`[]\>
|
||||
|
||||
Defined in: [src/api.ts:415](../src/api.ts#L415)
|
||||
Defined in: [src/api.ts:432](../src/api.ts#L432)
|
||||
|
||||
Send messages.
|
||||
Network usage: background.
|
||||
@@ -968,7 +1005,7 @@ Network usage: background.
|
||||
|
||||
> **apiSendTextMessage**(`chat`, `text`, `inReplyTo?`): `Promise`\<`AChatItem`[]\>
|
||||
|
||||
Defined in: [src/api.ts:437](../src/api.ts#L437)
|
||||
Defined in: [src/api.ts:455](../src/api.ts#L455)
|
||||
|
||||
Send text message.
|
||||
Network usage: background.
|
||||
@@ -997,7 +1034,7 @@ Network usage: background.
|
||||
|
||||
> **apiSendTextReply**(`chatItem`, `text`): `Promise`\<`AChatItem`[]\>
|
||||
|
||||
Defined in: [src/api.ts:445](../src/api.ts#L445)
|
||||
Defined in: [src/api.ts:463](../src/api.ts#L463)
|
||||
|
||||
Send text message in reply to received message.
|
||||
Network usage: background.
|
||||
@@ -1022,7 +1059,7 @@ Network usage: background.
|
||||
|
||||
> **apiSetActiveUser**(`userId`, `viewPwd?`): `Promise`\<`User`\>
|
||||
|
||||
Defined in: [src/api.ts:869](../src/api.ts#L869)
|
||||
Defined in: [src/api.ts:907](../src/api.ts#L907)
|
||||
|
||||
Set active user profile
|
||||
Network usage: no.
|
||||
@@ -1047,7 +1084,7 @@ Network usage: no.
|
||||
|
||||
> **apiSetAddressSettings**(`userId`, `__namedParameters`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:398](../src/api.ts#L398)
|
||||
Defined in: [src/api.ts:415](../src/api.ts#L415)
|
||||
|
||||
Set bot address settings.
|
||||
Network usage: interactive.
|
||||
@@ -1072,7 +1109,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiSetAutoAcceptMemberContacts**(`userId`, `onOff`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:808](../src/api.ts#L808)
|
||||
Defined in: [src/api.ts:846](../src/api.ts#L846)
|
||||
|
||||
Set auto-accept member contacts.
|
||||
Network usage: no.
|
||||
@@ -1097,7 +1134,7 @@ Network usage: no.
|
||||
|
||||
> **apiSetContactCustomData**(`contactId`, `customData?`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:798](../src/api.ts#L798)
|
||||
Defined in: [src/api.ts:836](../src/api.ts#L836)
|
||||
|
||||
Set contact custom data.
|
||||
Network usage: no.
|
||||
@@ -1122,7 +1159,7 @@ Network usage: no.
|
||||
|
||||
> **apiSetContactPrefs**(`contactId`, `preferences`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:905](../src/api.ts#L905)
|
||||
Defined in: [src/api.ts:943](../src/api.ts#L943)
|
||||
|
||||
Configure chat preference overrides for the contact.
|
||||
Network usage: background.
|
||||
@@ -1147,7 +1184,7 @@ Network usage: background.
|
||||
|
||||
> **apiSetGroupCustomData**(`groupId`, `customData?`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:788](../src/api.ts#L788)
|
||||
Defined in: [src/api.ts:826](../src/api.ts#L826)
|
||||
|
||||
Set group custom data.
|
||||
Network usage: no.
|
||||
@@ -1172,7 +1209,7 @@ Network usage: no.
|
||||
|
||||
> **apiSetGroupLinkMemberRole**(`groupId`, `memberRole`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:644](../src/api.ts#L644)
|
||||
Defined in: [src/api.ts:663](../src/api.ts#L663)
|
||||
|
||||
Set member role for group link.
|
||||
Network usage: no.
|
||||
@@ -1197,7 +1234,7 @@ Network usage: no.
|
||||
|
||||
> **apiSetMembersRole**(`groupId`, `groupMemberIds`, `memberRole`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:561](../src/api.ts#L561)
|
||||
Defined in: [src/api.ts:580](../src/api.ts#L580)
|
||||
|
||||
Set members role. Requires Admin role.
|
||||
Network usage: background.
|
||||
@@ -1226,7 +1263,7 @@ Network usage: background.
|
||||
|
||||
> **apiSetProfileAddress**(`userId`, `enable`): `Promise`\<`UserProfileUpdateSummary`\>
|
||||
|
||||
Defined in: [src/api.ts:384](../src/api.ts#L384)
|
||||
Defined in: [src/api.ts:399](../src/api.ts#L399)
|
||||
|
||||
Add address to bot profile.
|
||||
Network usage: interactive.
|
||||
@@ -1251,7 +1288,7 @@ Network usage: interactive.
|
||||
|
||||
> **apiUpdateChatItem**(`chatType`, `chatId`, `chatItemId`, `msgContent`, `liveMessage`): `Promise`\<`ChatItem`\>
|
||||
|
||||
Defined in: [src/api.ts:453](../src/api.ts#L453)
|
||||
Defined in: [src/api.ts:471](../src/api.ts#L471)
|
||||
|
||||
Update message.
|
||||
Network usage: background.
|
||||
@@ -1288,7 +1325,7 @@ Network usage: background.
|
||||
|
||||
> **apiUpdateGroupProfile**(`groupId`, `groupProfile`): `Promise`\<`GroupInfo`\>
|
||||
|
||||
Defined in: [src/api.ts:621](../src/api.ts#L621)
|
||||
Defined in: [src/api.ts:640](../src/api.ts#L640)
|
||||
|
||||
Update group profile.
|
||||
Network usage: background.
|
||||
@@ -1313,7 +1350,7 @@ Network usage: background.
|
||||
|
||||
> **apiUpdateProfile**(`userId`, `profile`): `Promise`\<`UserProfileUpdateSummary` \| `undefined`\>
|
||||
|
||||
Defined in: [src/api.ts:889](../src/api.ts#L889)
|
||||
Defined in: [src/api.ts:927](../src/api.ts#L927)
|
||||
|
||||
Update user profile.
|
||||
Network usage: background.
|
||||
@@ -1338,9 +1375,10 @@ Network usage: background.
|
||||
|
||||
> **close**(): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:148](../src/api.ts#L148)
|
||||
Defined in: [src/api.ts:158](../src/api.ts#L158)
|
||||
|
||||
Close chat database.
|
||||
Stop chat controller and close chat database.
|
||||
The database is not closed if stopping fails.
|
||||
Usually doesn't need to be called in chat bots.
|
||||
|
||||
#### Returns
|
||||
@@ -1353,7 +1391,7 @@ Usually doesn't need to be called in chat bots.
|
||||
|
||||
> **off**\<`K`\>(`event`, `subscriber?`): `void`
|
||||
|
||||
Defined in: [src/api.ts:287](../src/api.ts#L287)
|
||||
Defined in: [src/api.ts:302](../src/api.ts#L302)
|
||||
|
||||
Unsubscribe all or a specific handler from a specific event.
|
||||
|
||||
@@ -1387,7 +1425,7 @@ An optional subscriber function for the event.
|
||||
|
||||
> **offAny**(`receiver?`): `void`
|
||||
|
||||
Defined in: [src/api.ts:303](../src/api.ts#L303)
|
||||
Defined in: [src/api.ts:318](../src/api.ts#L318)
|
||||
|
||||
Unsubscribe all or a specific handler from any events.
|
||||
|
||||
@@ -1411,7 +1449,7 @@ An optional subscriber function for the event.
|
||||
|
||||
> **on**\<`K`\>(`subscribers`): `void`
|
||||
|
||||
Defined in: [src/api.ts:197](../src/api.ts#L197)
|
||||
Defined in: [src/api.ts:212](../src/api.ts#L212)
|
||||
|
||||
Subscribe multiple event handlers at once.
|
||||
|
||||
@@ -1441,7 +1479,7 @@ If the same function is subscribed to event.
|
||||
|
||||
> **on**\<`K`\>(`event`, `subscriber`): `void`
|
||||
|
||||
Defined in: [src/api.ts:205](../src/api.ts#L205)
|
||||
Defined in: [src/api.ts:220](../src/api.ts#L220)
|
||||
|
||||
Subscribe a handler to a specific event.
|
||||
|
||||
@@ -1479,7 +1517,7 @@ If the same function is subscribed to event.
|
||||
|
||||
> **onAny**(`receiver`): `void`
|
||||
|
||||
Defined in: [src/api.ts:228](../src/api.ts#L228)
|
||||
Defined in: [src/api.ts:243](../src/api.ts#L243)
|
||||
|
||||
Subscribe a handler to any event.
|
||||
|
||||
@@ -1505,7 +1543,7 @@ If the same function is subscribed to event.
|
||||
|
||||
> **once**\<`K`\>(`event`, `subscriber`): `void`
|
||||
|
||||
Defined in: [src/api.ts:239](../src/api.ts#L239)
|
||||
Defined in: [src/api.ts:254](../src/api.ts#L254)
|
||||
|
||||
Subscribe a handler to a specific event to be delivered one time.
|
||||
|
||||
@@ -1543,13 +1581,13 @@ If the same function is subscribed to event.
|
||||
|
||||
> **recvChatEvent**(`wait?`): `Promise`\<`ChatEvent` \| `undefined`\>
|
||||
|
||||
Defined in: [src/api.ts:338](../src/api.ts#L338)
|
||||
Defined in: [src/api.ts:353](../src/api.ts#L353)
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### wait?
|
||||
|
||||
`number` = `5_000_000`
|
||||
`number` = `500_000`
|
||||
|
||||
#### Returns
|
||||
|
||||
@@ -1561,7 +1599,7 @@ Defined in: [src/api.ts:338](../src/api.ts#L338)
|
||||
|
||||
> **sendChatCmd**(`cmd`): `Promise`\<`ChatResponse`\>
|
||||
|
||||
Defined in: [src/api.ts:334](../src/api.ts#L334)
|
||||
Defined in: [src/api.ts:349](../src/api.ts#L349)
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -1579,7 +1617,7 @@ Defined in: [src/api.ts:334](../src/api.ts#L334)
|
||||
|
||||
> **startChat**(): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:122](../src/api.ts#L122)
|
||||
Defined in: [src/api.ts:124](../src/api.ts#L124)
|
||||
|
||||
Start chat controller. Must be called with the existing user profile.
|
||||
|
||||
@@ -1593,10 +1631,10 @@ Start chat controller. Must be called with the existing user profile.
|
||||
|
||||
> **stopChat**(): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/api.ts:136](../src/api.ts#L136)
|
||||
Defined in: [src/api.ts:147](../src/api.ts#L147)
|
||||
|
||||
Stop chat controller.
|
||||
Must be called before closing the database.
|
||||
`close` calls it before closing the database.
|
||||
Usually doesn't need to be called in chat bots.
|
||||
|
||||
#### Returns
|
||||
@@ -1611,7 +1649,7 @@ Usually doesn't need to be called in chat bots.
|
||||
|
||||
> **wait**\<`K`\>(`event`): `Promise`\<`ChatEvent` & `object`\>
|
||||
|
||||
Defined in: [src/api.ts:247](../src/api.ts#L247)
|
||||
Defined in: [src/api.ts:262](../src/api.ts#L262)
|
||||
|
||||
Waits for specific event, with an optional predicate.
|
||||
Returns `undefined` on timeout if specified.
|
||||
@@ -1636,7 +1674,7 @@ Returns `undefined` on timeout if specified.
|
||||
|
||||
> **wait**\<`K`\>(`event`, `predicate`): `Promise`\<`ChatEvent` & `object`\>
|
||||
|
||||
Defined in: [src/api.ts:248](../src/api.ts#L248)
|
||||
Defined in: [src/api.ts:263](../src/api.ts#L263)
|
||||
|
||||
Waits for specific event, with an optional predicate.
|
||||
Returns `undefined` on timeout if specified.
|
||||
@@ -1665,7 +1703,7 @@ Returns `undefined` on timeout if specified.
|
||||
|
||||
> **wait**\<`K`\>(`event`, `timeout`): `Promise`\<ChatEvent & \{ type: K; \} \| `undefined`\>
|
||||
|
||||
Defined in: [src/api.ts:249](../src/api.ts#L249)
|
||||
Defined in: [src/api.ts:264](../src/api.ts#L264)
|
||||
|
||||
Waits for specific event, with an optional predicate.
|
||||
Returns `undefined` on timeout if specified.
|
||||
@@ -1694,7 +1732,7 @@ Returns `undefined` on timeout if specified.
|
||||
|
||||
> **wait**\<`K`\>(`event`, `predicate`, `timeout`): `Promise`\<ChatEvent & \{ type: K; \} \| `undefined`\>
|
||||
|
||||
Defined in: [src/api.ts:250](../src/api.ts#L250)
|
||||
Defined in: [src/api.ts:265](../src/api.ts#L265)
|
||||
|
||||
Waits for specific event, with an optional predicate.
|
||||
Returns `undefined` on timeout if specified.
|
||||
@@ -1727,9 +1765,9 @@ Returns `undefined` on timeout if specified.
|
||||
|
||||
### init()
|
||||
|
||||
> `static` **init**(`db`, `confirm?`): `Promise`\<`ChatApi`\>
|
||||
> `static` **init**(`db`, `confirm?`, `queueSize?`): `Promise`\<`ChatApi`\>
|
||||
|
||||
Defined in: [src/api.ts:110](../src/api.ts#L110)
|
||||
Defined in: [src/api.ts:111](../src/api.ts#L111)
|
||||
|
||||
Initializes the ChatApi.
|
||||
|
||||
@@ -1747,6 +1785,12 @@ Database configuration (sqlite or postgres).
|
||||
|
||||
Migration confirmation mode.
|
||||
|
||||
##### queueSize?
|
||||
|
||||
`number`
|
||||
|
||||
Size of internal queues, the core default is used when omitted.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`ChatApi`\>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> **run**(`__namedParameters`): `Promise`\<\[[`ChatApi`](api.Class.ChatApi.md), `User`, `UserContactLink` \| `undefined`\]\>
|
||||
|
||||
Defined in: [src/bot.ts:47](../src/bot.ts#L47)
|
||||
Defined in: [src/bot.ts:48](../src/bot.ts#L48)
|
||||
|
||||
## Parameters
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
[**simplex-chat**](README.md)
|
||||
|
||||
***
|
||||
|
||||
[simplex-chat](README.md) / [bot](Namespace.bot.md) / subscribeChatItems
|
||||
|
||||
# Function: subscribeChatItems()
|
||||
|
||||
> **subscribeChatItems**(`bot`, `onMessage`, `commands`): `void`
|
||||
|
||||
Defined in: [src/bot.ts:108](../src/bot.ts#L108)
|
||||
|
||||
## Parameters
|
||||
|
||||
### bot
|
||||
|
||||
[`ChatApi`](api.Class.ChatApi.md)
|
||||
|
||||
### onMessage
|
||||
|
||||
((`chatItem`, `content`) => `void` \| `Promise`\<`void`\>) \| `undefined`
|
||||
|
||||
### commands
|
||||
|
||||
## Returns
|
||||
|
||||
`void`
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: BotConfig
|
||||
|
||||
Defined in: [src/bot.ts:35](../src/bot.ts#L35)
|
||||
Defined in: [src/bot.ts:36](../src/bot.ts#L36)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -14,7 +14,7 @@ Defined in: [src/bot.ts:35](../src/bot.ts#L35)
|
||||
|
||||
> **dbOpts**: [`BotDbOpts`](bot.TypeAlias.BotDbOpts.md)
|
||||
|
||||
Defined in: [src/bot.ts:37](../src/bot.ts#L37)
|
||||
Defined in: [src/bot.ts:38](../src/bot.ts#L38)
|
||||
|
||||
***
|
||||
|
||||
@@ -22,7 +22,7 @@ Defined in: [src/bot.ts:37](../src/bot.ts#L37)
|
||||
|
||||
> `optional` **events?**: [`EventSubscribers`](api.TypeAlias.EventSubscribers.md)
|
||||
|
||||
Defined in: [src/bot.ts:44](../src/bot.ts#L44)
|
||||
Defined in: [src/bot.ts:45](../src/bot.ts#L45)
|
||||
|
||||
***
|
||||
|
||||
@@ -30,7 +30,7 @@ Defined in: [src/bot.ts:44](../src/bot.ts#L44)
|
||||
|
||||
> `optional` **onCommands?**: `object`
|
||||
|
||||
Defined in: [src/bot.ts:41](../src/bot.ts#L41)
|
||||
Defined in: [src/bot.ts:42](../src/bot.ts#L42)
|
||||
|
||||
#### Index Signature
|
||||
|
||||
@@ -42,7 +42,7 @@ Defined in: [src/bot.ts:41](../src/bot.ts#L41)
|
||||
|
||||
> `optional` **onMessage?**: (`chatItem`, `content`) => `void` \| `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/bot.ts:39](../src/bot.ts#L39)
|
||||
Defined in: [src/bot.ts:40](../src/bot.ts#L40)
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -64,7 +64,7 @@ Defined in: [src/bot.ts:39](../src/bot.ts#L39)
|
||||
|
||||
> **options**: [`BotOptions`](bot.Interface.BotOptions.md)
|
||||
|
||||
Defined in: [src/bot.ts:38](../src/bot.ts#L38)
|
||||
Defined in: [src/bot.ts:39](../src/bot.ts#L39)
|
||||
|
||||
***
|
||||
|
||||
@@ -72,4 +72,4 @@ Defined in: [src/bot.ts:38](../src/bot.ts#L38)
|
||||
|
||||
> **profile**: `Profile`
|
||||
|
||||
Defined in: [src/bot.ts:36](../src/bot.ts#L36)
|
||||
Defined in: [src/bot.ts:37](../src/bot.ts#L37)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: BotOptions
|
||||
|
||||
Defined in: [src/bot.ts:11](../src/bot.ts#L11)
|
||||
Defined in: [src/bot.ts:12](../src/bot.ts#L12)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -14,7 +14,7 @@ Defined in: [src/bot.ts:11](../src/bot.ts#L11)
|
||||
|
||||
> `optional` **addressSettings?**: [`BotAddressSettings`](api.Interface.BotAddressSettings.md)
|
||||
|
||||
Defined in: [src/bot.ts:15](../src/bot.ts#L15)
|
||||
Defined in: [src/bot.ts:16](../src/bot.ts#L16)
|
||||
|
||||
***
|
||||
|
||||
@@ -22,7 +22,7 @@ Defined in: [src/bot.ts:15](../src/bot.ts#L15)
|
||||
|
||||
> `optional` **allowFiles?**: `boolean`
|
||||
|
||||
Defined in: [src/bot.ts:16](../src/bot.ts#L16)
|
||||
Defined in: [src/bot.ts:17](../src/bot.ts#L17)
|
||||
|
||||
***
|
||||
|
||||
@@ -30,7 +30,7 @@ Defined in: [src/bot.ts:16](../src/bot.ts#L16)
|
||||
|
||||
> `optional` **commands?**: `ChatBotCommand`[]
|
||||
|
||||
Defined in: [src/bot.ts:17](../src/bot.ts#L17)
|
||||
Defined in: [src/bot.ts:18](../src/bot.ts#L18)
|
||||
|
||||
***
|
||||
|
||||
@@ -38,7 +38,7 @@ Defined in: [src/bot.ts:17](../src/bot.ts#L17)
|
||||
|
||||
> `optional` **createAddress?**: `boolean`
|
||||
|
||||
Defined in: [src/bot.ts:12](../src/bot.ts#L12)
|
||||
Defined in: [src/bot.ts:13](../src/bot.ts#L13)
|
||||
|
||||
***
|
||||
|
||||
@@ -46,7 +46,7 @@ Defined in: [src/bot.ts:12](../src/bot.ts#L12)
|
||||
|
||||
> `optional` **logContacts?**: `boolean`
|
||||
|
||||
Defined in: [src/bot.ts:19](../src/bot.ts#L19)
|
||||
Defined in: [src/bot.ts:20](../src/bot.ts#L20)
|
||||
|
||||
***
|
||||
|
||||
@@ -54,7 +54,7 @@ Defined in: [src/bot.ts:19](../src/bot.ts#L19)
|
||||
|
||||
> `optional` **logNetwork?**: `boolean`
|
||||
|
||||
Defined in: [src/bot.ts:20](../src/bot.ts#L20)
|
||||
Defined in: [src/bot.ts:21](../src/bot.ts#L21)
|
||||
|
||||
***
|
||||
|
||||
@@ -62,7 +62,7 @@ Defined in: [src/bot.ts:20](../src/bot.ts#L20)
|
||||
|
||||
> `optional` **updateAddress?**: `boolean`
|
||||
|
||||
Defined in: [src/bot.ts:13](../src/bot.ts#L13)
|
||||
Defined in: [src/bot.ts:14](../src/bot.ts#L14)
|
||||
|
||||
***
|
||||
|
||||
@@ -70,7 +70,7 @@ Defined in: [src/bot.ts:13](../src/bot.ts#L13)
|
||||
|
||||
> `optional` **updateProfile?**: `boolean`
|
||||
|
||||
Defined in: [src/bot.ts:14](../src/bot.ts#L14)
|
||||
Defined in: [src/bot.ts:15](../src/bot.ts#L15)
|
||||
|
||||
***
|
||||
|
||||
@@ -78,4 +78,4 @@ Defined in: [src/bot.ts:14](../src/bot.ts#L14)
|
||||
|
||||
> `optional` **useBotProfile?**: `boolean`
|
||||
|
||||
Defined in: [src/bot.ts:18](../src/bot.ts#L18)
|
||||
Defined in: [src/bot.ts:19](../src/bot.ts#L19)
|
||||
|
||||
@@ -15,3 +15,7 @@ Defined in: [src/bot.ts:7](../src/bot.ts#L7)
|
||||
### confirmMigrations?
|
||||
|
||||
> `optional` **confirmMigrations?**: [`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md)
|
||||
|
||||
### queueSize?
|
||||
|
||||
> `optional` **queueSize?**: `number`
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Class: ChatAPIError
|
||||
|
||||
Defined in: [src/core.ts:92](../src/core.ts#L92)
|
||||
Defined in: [src/core.ts:95](../src/core.ts#L95)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:92](../src/core.ts#L92)
|
||||
|
||||
> **new ChatAPIError**(`message`, `chatError?`): `ChatAPIError`
|
||||
|
||||
Defined in: [src/core.ts:93](../src/core.ts#L93)
|
||||
Defined in: [src/core.ts:96](../src/core.ts#L96)
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -44,7 +44,7 @@ Defined in: [src/core.ts:93](../src/core.ts#L93)
|
||||
|
||||
> **chatError**: `ChatError` \| `undefined` = `undefined`
|
||||
|
||||
Defined in: [src/core.ts:93](../src/core.ts#L93)
|
||||
Defined in: [src/core.ts:96](../src/core.ts#L96)
|
||||
|
||||
***
|
||||
|
||||
@@ -52,7 +52,7 @@ Defined in: [src/core.ts:93](../src/core.ts#L93)
|
||||
|
||||
> **message**: `string`
|
||||
|
||||
Defined in: [src/core.ts:93](../src/core.ts#L93)
|
||||
Defined in: [src/core.ts:96](../src/core.ts#L96)
|
||||
|
||||
#### Inherited from
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Class: ChatInitError
|
||||
|
||||
Defined in: [src/core.ts:116](../src/core.ts#L116)
|
||||
Defined in: [src/core.ts:119](../src/core.ts#L119)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:116](../src/core.ts#L116)
|
||||
|
||||
> **new ChatInitError**(`message`, `dbMigrationError`): `ChatInitError`
|
||||
|
||||
Defined in: [src/core.ts:117](../src/core.ts#L117)
|
||||
Defined in: [src/core.ts:120](../src/core.ts#L120)
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -44,7 +44,7 @@ Defined in: [src/core.ts:117](../src/core.ts#L117)
|
||||
|
||||
> **dbMigrationError**: [`DBMigrationError`](core.TypeAlias.DBMigrationError.md)
|
||||
|
||||
Defined in: [src/core.ts:117](../src/core.ts#L117)
|
||||
Defined in: [src/core.ts:120](../src/core.ts#L120)
|
||||
|
||||
***
|
||||
|
||||
@@ -52,7 +52,7 @@ Defined in: [src/core.ts:117](../src/core.ts#L117)
|
||||
|
||||
> **message**: `string`
|
||||
|
||||
Defined in: [src/core.ts:117](../src/core.ts#L117)
|
||||
Defined in: [src/core.ts:120](../src/core.ts#L120)
|
||||
|
||||
#### Inherited from
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: ErrorMigration
|
||||
|
||||
Defined in: [src/core.ts:144](../src/core.ts#L144)
|
||||
Defined in: [src/core.ts:152](../src/core.ts#L152)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:144](../src/core.ts#L144)
|
||||
|
||||
> **dbFile**: `string`
|
||||
|
||||
Defined in: [src/core.ts:146](../src/core.ts#L146)
|
||||
Defined in: [src/core.ts:154](../src/core.ts#L154)
|
||||
|
||||
***
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [src/core.ts:146](../src/core.ts#L146)
|
||||
|
||||
> **migrationError**: [`MigrationError`](core.TypeAlias.MigrationError.md)
|
||||
|
||||
Defined in: [src/core.ts:147](../src/core.ts#L147)
|
||||
Defined in: [src/core.ts:155](../src/core.ts#L155)
|
||||
|
||||
***
|
||||
|
||||
@@ -34,7 +34,7 @@ Defined in: [src/core.ts:147](../src/core.ts#L147)
|
||||
|
||||
> **type**: `"errorMigration"`
|
||||
|
||||
Defined in: [src/core.ts:145](../src/core.ts#L145)
|
||||
Defined in: [src/core.ts:153](../src/core.ts#L153)
|
||||
|
||||
#### Overrides
|
||||
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: ErrorNotADatabase
|
||||
|
||||
Defined in: [src/core.ts:139](../src/core.ts#L139)
|
||||
Defined in: [src/core.ts:147](../src/core.ts#L147)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:139](../src/core.ts#L139)
|
||||
|
||||
> **dbFile**: `string`
|
||||
|
||||
Defined in: [src/core.ts:141](../src/core.ts#L141)
|
||||
Defined in: [src/core.ts:149](../src/core.ts#L149)
|
||||
|
||||
***
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [src/core.ts:141](../src/core.ts#L141)
|
||||
|
||||
> **type**: `"errorNotADatabase"`
|
||||
|
||||
Defined in: [src/core.ts:140](../src/core.ts#L140)
|
||||
Defined in: [src/core.ts:148](../src/core.ts#L148)
|
||||
|
||||
#### Overrides
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: ErrorSQL
|
||||
|
||||
Defined in: [src/core.ts:150](../src/core.ts#L150)
|
||||
Defined in: [src/core.ts:158](../src/core.ts#L158)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:150](../src/core.ts#L150)
|
||||
|
||||
> **dbFile**: `string`
|
||||
|
||||
Defined in: [src/core.ts:152](../src/core.ts#L152)
|
||||
Defined in: [src/core.ts:160](../src/core.ts#L160)
|
||||
|
||||
***
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [src/core.ts:152](../src/core.ts#L152)
|
||||
|
||||
> **migrationSQLError**: `string`
|
||||
|
||||
Defined in: [src/core.ts:153](../src/core.ts#L153)
|
||||
Defined in: [src/core.ts:161](../src/core.ts#L161)
|
||||
|
||||
***
|
||||
|
||||
@@ -34,7 +34,7 @@ Defined in: [src/core.ts:153](../src/core.ts#L153)
|
||||
|
||||
> **type**: `"errorSQL"`
|
||||
|
||||
Defined in: [src/core.ts:151](../src/core.ts#L151)
|
||||
Defined in: [src/core.ts:159](../src/core.ts#L159)
|
||||
|
||||
#### Overrides
|
||||
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: InvalidConfirmation
|
||||
|
||||
Defined in: [src/core.ts:135](../src/core.ts#L135)
|
||||
Defined in: [src/core.ts:139](../src/core.ts#L139)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:135](../src/core.ts#L135)
|
||||
|
||||
> **type**: `"invalidConfirmation"`
|
||||
|
||||
Defined in: [src/core.ts:136](../src/core.ts#L136)
|
||||
Defined in: [src/core.ts:140](../src/core.ts#L140)
|
||||
|
||||
#### Overrides
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[**simplex-chat**](README.md)
|
||||
|
||||
***
|
||||
|
||||
[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / InvalidQueueSize
|
||||
|
||||
# Interface: InvalidQueueSize
|
||||
|
||||
Defined in: [src/core.ts:143](../src/core.ts#L143)
|
||||
|
||||
## Extends
|
||||
|
||||
- `Interface`
|
||||
|
||||
## Properties
|
||||
|
||||
### type
|
||||
|
||||
> **type**: `"invalidQueueSize"`
|
||||
|
||||
Defined in: [src/core.ts:144](../src/core.ts#L144)
|
||||
|
||||
#### Overrides
|
||||
|
||||
`Interface.type`
|
||||
@@ -6,6 +6,6 @@
|
||||
|
||||
# Type Alias: Tag
|
||||
|
||||
> **Tag** = `"invalidConfirmation"` \| `"errorNotADatabase"` \| `"errorMigration"` \| `"errorSQL"`
|
||||
> **Tag** = `"invalidConfirmation"` \| `"invalidQueueSize"` \| `"errorNotADatabase"` \| `"errorMigration"` \| `"errorSQL"`
|
||||
|
||||
Defined in: [src/core.ts:129](../src/core.ts#L129)
|
||||
Defined in: [src/core.ts:133](../src/core.ts#L133)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Enumeration: MigrationConfirmation
|
||||
|
||||
Defined in: [src/core.ts:101](../src/core.ts#L101)
|
||||
Defined in: [src/core.ts:104](../src/core.ts#L104)
|
||||
|
||||
Migration confirmation mode
|
||||
|
||||
@@ -16,7 +16,7 @@ Migration confirmation mode
|
||||
|
||||
> **Console**: `"console"`
|
||||
|
||||
Defined in: [src/core.ts:104](../src/core.ts#L104)
|
||||
Defined in: [src/core.ts:107](../src/core.ts#L107)
|
||||
|
||||
***
|
||||
|
||||
@@ -24,7 +24,7 @@ Defined in: [src/core.ts:104](../src/core.ts#L104)
|
||||
|
||||
> **Error**: `"error"`
|
||||
|
||||
Defined in: [src/core.ts:105](../src/core.ts#L105)
|
||||
Defined in: [src/core.ts:108](../src/core.ts#L108)
|
||||
|
||||
***
|
||||
|
||||
@@ -32,7 +32,7 @@ Defined in: [src/core.ts:105](../src/core.ts#L105)
|
||||
|
||||
> **YesUp**: `"yesUp"`
|
||||
|
||||
Defined in: [src/core.ts:102](../src/core.ts#L102)
|
||||
Defined in: [src/core.ts:105](../src/core.ts#L105)
|
||||
|
||||
***
|
||||
|
||||
@@ -40,4 +40,4 @@ Defined in: [src/core.ts:102](../src/core.ts#L102)
|
||||
|
||||
> **YesUpDown**: `"yesUpDown"`
|
||||
|
||||
Defined in: [src/core.ts:103](../src/core.ts#L103)
|
||||
Defined in: [src/core.ts:106](../src/core.ts#L106)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> **chatCloseStore**(`ctrl`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/core.ts:17](../src/core.ts#L17)
|
||||
Defined in: [src/core.ts:20](../src/core.ts#L20)
|
||||
|
||||
Close chat store
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> **chatDecryptFile**(`fromPath`, `__namedParameters`, `toPath`): `Promise`\<`void`\>
|
||||
|
||||
Defined in: [src/core.ts:73](../src/core.ts#L73)
|
||||
Defined in: [src/core.ts:76](../src/core.ts#L76)
|
||||
|
||||
Decrypt file
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> **chatEncryptFile**(`ctrl`, `fromPath`, `toPath`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\>
|
||||
|
||||
Defined in: [src/core.ts:65](../src/core.ts#L65)
|
||||
Defined in: [src/core.ts:68](../src/core.ts#L68)
|
||||
|
||||
Encrypt file
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
# Function: chatMigrateInit()
|
||||
|
||||
> **chatMigrateInit**(`dbPath`, `dbKey`, `confirm`): `Promise`\<`bigint`\>
|
||||
> **chatMigrateInit**(`dbPath`, `dbKey`, `confirm`, `queueSize?`): `Promise`\<`bigint`\>
|
||||
|
||||
Defined in: [src/core.ts:7](../src/core.ts#L7)
|
||||
Defined in: [src/core.ts:8](../src/core.ts#L8)
|
||||
|
||||
Initialize chat controller
|
||||
|
||||
@@ -26,6 +26,12 @@ Initialize chat controller
|
||||
|
||||
[`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md)
|
||||
|
||||
### queueSize?
|
||||
|
||||
`number`
|
||||
|
||||
Size of internal queues, the core default is used when omitted.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`bigint`\>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
# Function: chatReadFile()
|
||||
|
||||
> **chatReadFile**(`path`, `__namedParameters`): `Promise`\<`ArrayBuffer`\>
|
||||
> **chatReadFile**(`path`, `__namedParameters`): `Promise`\<`Buffer`\<`ArrayBufferLike`\>\>
|
||||
|
||||
Defined in: [src/core.ts:58](../src/core.ts#L58)
|
||||
Defined in: [src/core.ts:61](../src/core.ts#L61)
|
||||
|
||||
Read buffer from encrypted file
|
||||
|
||||
@@ -24,4 +24,4 @@ Read buffer from encrypted file
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`ArrayBuffer`\>
|
||||
`Promise`\<`Buffer`\<`ArrayBufferLike`\>\>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> **chatRecvMsgWait**(`ctrl`, `wait`): `Promise`\<`ChatEvent` \| `undefined`\>
|
||||
|
||||
Defined in: [src/core.ts:37](../src/core.ts#L37)
|
||||
Defined in: [src/core.ts:40](../src/core.ts#L40)
|
||||
|
||||
Receive chat event
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> **chatSendCmd**(`ctrl`, `cmd`): `Promise`\<`ChatResponse`\>
|
||||
|
||||
Defined in: [src/core.ts:25](../src/core.ts#L25)
|
||||
Defined in: [src/core.ts:28](../src/core.ts#L28)
|
||||
|
||||
Send chat command as string
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> **chatWriteFile**(`ctrl`, `path`, `buffer`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\>
|
||||
|
||||
Defined in: [src/core.ts:50](../src/core.ts#L50)
|
||||
Defined in: [src/core.ts:53](../src/core.ts#L53)
|
||||
|
||||
Write buffer to encrypted file
|
||||
|
||||
@@ -24,7 +24,7 @@ Write buffer to encrypted file
|
||||
|
||||
### buffer
|
||||
|
||||
`ArrayBuffer`
|
||||
`ArrayBuffer` \| `Uint8Array`\<`ArrayBufferLike`\>
|
||||
|
||||
## Returns
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: APIResult\<R\>
|
||||
|
||||
Defined in: [src/core.ts:87](../src/core.ts#L87)
|
||||
Defined in: [src/core.ts:90](../src/core.ts#L90)
|
||||
|
||||
## Type Parameters
|
||||
|
||||
@@ -20,7 +20,7 @@ Defined in: [src/core.ts:87](../src/core.ts#L87)
|
||||
|
||||
> `optional` **error?**: `ChatError`
|
||||
|
||||
Defined in: [src/core.ts:89](../src/core.ts#L89)
|
||||
Defined in: [src/core.ts:92](../src/core.ts#L92)
|
||||
|
||||
***
|
||||
|
||||
@@ -28,4 +28,4 @@ Defined in: [src/core.ts:89](../src/core.ts#L89)
|
||||
|
||||
> `optional` **result?**: `R`
|
||||
|
||||
Defined in: [src/core.ts:88](../src/core.ts#L88)
|
||||
Defined in: [src/core.ts:91](../src/core.ts#L91)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: CryptoArgs
|
||||
|
||||
Defined in: [src/core.ts:111](../src/core.ts#L111)
|
||||
Defined in: [src/core.ts:114](../src/core.ts#L114)
|
||||
|
||||
File encryption key and nonce
|
||||
|
||||
@@ -16,7 +16,7 @@ File encryption key and nonce
|
||||
|
||||
> **fileKey**: `string`
|
||||
|
||||
Defined in: [src/core.ts:112](../src/core.ts#L112)
|
||||
Defined in: [src/core.ts:115](../src/core.ts#L115)
|
||||
|
||||
***
|
||||
|
||||
@@ -24,4 +24,4 @@ Defined in: [src/core.ts:112](../src/core.ts#L112)
|
||||
|
||||
> **fileNonce**: `string`
|
||||
|
||||
Defined in: [src/core.ts:113](../src/core.ts#L113)
|
||||
Defined in: [src/core.ts:116](../src/core.ts#L116)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: UpMigration
|
||||
|
||||
Defined in: [src/core.ts:185](../src/core.ts#L185)
|
||||
Defined in: [src/core.ts:193](../src/core.ts#L193)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -14,7 +14,7 @@ Defined in: [src/core.ts:185](../src/core.ts#L185)
|
||||
|
||||
> **upName**: `string`
|
||||
|
||||
Defined in: [src/core.ts:186](../src/core.ts#L186)
|
||||
Defined in: [src/core.ts:194](../src/core.ts#L194)
|
||||
|
||||
***
|
||||
|
||||
@@ -22,4 +22,4 @@ Defined in: [src/core.ts:186](../src/core.ts#L186)
|
||||
|
||||
> **withDown**: `boolean`
|
||||
|
||||
Defined in: [src/core.ts:187](../src/core.ts#L187)
|
||||
Defined in: [src/core.ts:195](../src/core.ts#L195)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: MTREDifferent
|
||||
|
||||
Defined in: [src/core.ts:206](../src/core.ts#L206)
|
||||
Defined in: [src/core.ts:214](../src/core.ts#L214)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:206](../src/core.ts#L206)
|
||||
|
||||
> **downMigrations**: `string`[]
|
||||
|
||||
Defined in: [src/core.ts:208](../src/core.ts#L208)
|
||||
Defined in: [src/core.ts:216](../src/core.ts#L216)
|
||||
|
||||
***
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [src/core.ts:208](../src/core.ts#L208)
|
||||
|
||||
> **type**: `"different"`
|
||||
|
||||
Defined in: [src/core.ts:207](../src/core.ts#L207)
|
||||
Defined in: [src/core.ts:215](../src/core.ts#L215)
|
||||
|
||||
#### Overrides
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: MTRENoDown
|
||||
|
||||
Defined in: [src/core.ts:201](../src/core.ts#L201)
|
||||
Defined in: [src/core.ts:209](../src/core.ts#L209)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -14,20 +14,20 @@ Defined in: [src/core.ts:201](../src/core.ts#L201)
|
||||
|
||||
## Properties
|
||||
|
||||
### dbMigrations
|
||||
|
||||
> **dbMigrations**: `string`[]
|
||||
|
||||
Defined in: [src/core.ts:211](../src/core.ts#L211)
|
||||
|
||||
***
|
||||
|
||||
### type
|
||||
|
||||
> **type**: `"noDown"`
|
||||
|
||||
Defined in: [src/core.ts:202](../src/core.ts#L202)
|
||||
Defined in: [src/core.ts:210](../src/core.ts#L210)
|
||||
|
||||
#### Overrides
|
||||
|
||||
`Interface.type`
|
||||
|
||||
***
|
||||
|
||||
### upMigrations
|
||||
|
||||
> **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md)
|
||||
|
||||
Defined in: [src/core.ts:203](../src/core.ts#L203)
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
|
||||
> **Tag** = `"noDown"` \| `"different"`
|
||||
|
||||
Defined in: [src/core.ts:195](../src/core.ts#L195)
|
||||
Defined in: [src/core.ts:203](../src/core.ts#L203)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: MEDowngrade
|
||||
|
||||
Defined in: [src/core.ts:174](../src/core.ts#L174)
|
||||
Defined in: [src/core.ts:182](../src/core.ts#L182)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:174](../src/core.ts#L174)
|
||||
|
||||
> **downMigrations**: `string`[]
|
||||
|
||||
Defined in: [src/core.ts:176](../src/core.ts#L176)
|
||||
Defined in: [src/core.ts:184](../src/core.ts#L184)
|
||||
|
||||
***
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [src/core.ts:176](../src/core.ts#L176)
|
||||
|
||||
> **type**: `"downgrade"`
|
||||
|
||||
Defined in: [src/core.ts:175](../src/core.ts#L175)
|
||||
Defined in: [src/core.ts:183](../src/core.ts#L183)
|
||||
|
||||
#### Overrides
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: MEUpgrade
|
||||
|
||||
Defined in: [src/core.ts:169](../src/core.ts#L169)
|
||||
Defined in: [src/core.ts:177](../src/core.ts#L177)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:169](../src/core.ts#L169)
|
||||
|
||||
> **type**: `"upgrade"`
|
||||
|
||||
Defined in: [src/core.ts:170](../src/core.ts#L170)
|
||||
Defined in: [src/core.ts:178](../src/core.ts#L178)
|
||||
|
||||
#### Overrides
|
||||
|
||||
@@ -28,6 +28,6 @@ Defined in: [src/core.ts:170](../src/core.ts#L170)
|
||||
|
||||
### upMigrations
|
||||
|
||||
> **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md)
|
||||
> **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md)[]
|
||||
|
||||
Defined in: [src/core.ts:171](../src/core.ts#L171)
|
||||
Defined in: [src/core.ts:179](../src/core.ts#L179)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
# Interface: MigrationError
|
||||
|
||||
Defined in: [src/core.ts:179](../src/core.ts#L179)
|
||||
Defined in: [src/core.ts:187](../src/core.ts#L187)
|
||||
|
||||
## Extends
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [src/core.ts:179](../src/core.ts#L179)
|
||||
|
||||
> **mtrError**: [`MTRError`](core.TypeAlias.MTRError.md)
|
||||
|
||||
Defined in: [src/core.ts:181](../src/core.ts#L181)
|
||||
Defined in: [src/core.ts:189](../src/core.ts#L189)
|
||||
|
||||
***
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [src/core.ts:181](../src/core.ts#L181)
|
||||
|
||||
> **type**: `"migrationError"`
|
||||
|
||||
Defined in: [src/core.ts:180](../src/core.ts#L180)
|
||||
Defined in: [src/core.ts:188](../src/core.ts#L188)
|
||||
|
||||
#### Overrides
|
||||
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
|
||||
> **Tag** = `"upgrade"` \| `"downgrade"` \| `"migrationError"`
|
||||
|
||||
Defined in: [src/core.ts:163](../src/core.ts#L163)
|
||||
Defined in: [src/core.ts:171](../src/core.ts#L171)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- [ErrorNotADatabase](core.DBMigrationError.Interface.ErrorNotADatabase.md)
|
||||
- [ErrorSQL](core.DBMigrationError.Interface.ErrorSQL.md)
|
||||
- [InvalidConfirmation](core.DBMigrationError.Interface.InvalidConfirmation.md)
|
||||
- [InvalidQueueSize](core.DBMigrationError.Interface.InvalidQueueSize.md)
|
||||
|
||||
## Type Aliases
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
|
||||
# Type Alias: DBMigrationError
|
||||
|
||||
> **DBMigrationError** = [`InvalidConfirmation`](core.DBMigrationError.Interface.InvalidConfirmation.md) \| [`ErrorNotADatabase`](core.DBMigrationError.Interface.ErrorNotADatabase.md) \| [`ErrorMigration`](core.DBMigrationError.Interface.ErrorMigration.md) \| [`ErrorSQL`](core.DBMigrationError.Interface.ErrorSQL.md)
|
||||
> **DBMigrationError** = [`InvalidConfirmation`](core.DBMigrationError.Interface.InvalidConfirmation.md) \| [`InvalidQueueSize`](core.DBMigrationError.Interface.InvalidQueueSize.md) \| [`ErrorNotADatabase`](core.DBMigrationError.Interface.ErrorNotADatabase.md) \| [`ErrorMigration`](core.DBMigrationError.Interface.ErrorMigration.md) \| [`ErrorSQL`](core.DBMigrationError.Interface.ErrorSQL.md)
|
||||
|
||||
Defined in: [src/core.ts:122](../src/core.ts#L122)
|
||||
Defined in: [src/core.ts:125](../src/core.ts#L125)
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
|
||||
> **MTRError** = [`MTRENoDown`](core.MTRError.Interface.MTRENoDown.md) \| [`MTREDifferent`](core.MTRError.Interface.MTREDifferent.md)
|
||||
|
||||
Defined in: [src/core.ts:190](../src/core.ts#L190)
|
||||
Defined in: [src/core.ts:198](../src/core.ts#L198)
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
|
||||
> **MigrationError** = [`MEUpgrade`](core.MigrationError.Interface.MEUpgrade.md) \| [`MEDowngrade`](core.MigrationError.Interface.MEDowngrade.md) \| [`MigrationError`](core.MigrationError.Interface.MigrationError.md)
|
||||
|
||||
Defined in: [src/core.ts:157](../src/core.ts#L157)
|
||||
Defined in: [src/core.ts:165](../src/core.ts#L165)
|
||||
|
||||
@@ -106,13 +106,15 @@ export class ChatApi {
|
||||
* Initializes the ChatApi.
|
||||
* @param {DbConfig} db - Database configuration (sqlite or postgres).
|
||||
* @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode.
|
||||
* @param {number} [queueSize] - Size of internal queues, the core default is used when omitted.
|
||||
*/
|
||||
static async init(
|
||||
db: DbConfig,
|
||||
confirm = core.MigrationConfirmation.YesUp
|
||||
confirm = core.MigrationConfirmation.YesUp,
|
||||
queueSize?: number
|
||||
): Promise<ChatApi> {
|
||||
const [path, key] = dbConfigToMigrateArgs(db)
|
||||
const ctrl = await core.chatMigrateInit(path, key, confirm)
|
||||
const ctrl = await core.chatMigrateInit(path, key, confirm, queueSize)
|
||||
return new ChatApi(ctrl)
|
||||
}
|
||||
|
||||
@@ -120,39 +122,52 @@ export class ChatApi {
|
||||
* Start chat controller. Must be called with the existing user profile.
|
||||
*/
|
||||
async startChat(): Promise<void> {
|
||||
if (this.eventsLoop) throw new Error("chat already started")
|
||||
const ctrl = this.ctrl
|
||||
this.receiveEvents = true
|
||||
this.eventsLoop = this.runEventsLoop()
|
||||
const r = await this.sendChatCmd(CC.StartChat.cmdString({mainApp: true, enableSndFiles: true}))
|
||||
let r: ChatResponse
|
||||
try {
|
||||
r = await core.chatSendCmd(ctrl, CC.StartChat.cmdString({mainApp: true, enableSndFiles: true, serviceRequests: false}))
|
||||
} catch (e) {
|
||||
await this.stopEventsLoop()
|
||||
throw e
|
||||
}
|
||||
if (r.type !== "chatStarted" && r.type !== "chatRunning") {
|
||||
await this.stopEventsLoop()
|
||||
throw new ChatCommandError("error starting chat", r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stop chat controller.
|
||||
* Must be called before closing the database.
|
||||
* `close` calls it before closing the database.
|
||||
* Usually doesn't need to be called in chat bots.
|
||||
*/
|
||||
async stopChat(): Promise<void> {
|
||||
const r = await this.sendChatCmd("/_stop")
|
||||
if (r.type !== "chatStopped") throw new ChatCommandError("error starting chat", r)
|
||||
this.receiveEvents = false
|
||||
if (this.eventsLoop) await this.eventsLoop
|
||||
this.eventsLoop = undefined
|
||||
if (r.type !== "chatStopped") throw new ChatCommandError("error stopping chat", r)
|
||||
await this.stopEventsLoop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Close chat database.
|
||||
* Stop chat controller and close chat database.
|
||||
* The database is not closed if stopping fails.
|
||||
* Usually doesn't need to be called in chat bots.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
this.receiveEvents = false
|
||||
if (this.eventsLoop) await this.eventsLoop
|
||||
this.eventsLoop = undefined
|
||||
// a running controller keeps using the database connections that closing frees
|
||||
await this.stopChat()
|
||||
await core.chatCloseStore(this.ctrl)
|
||||
this.ctrl_ = undefined
|
||||
}
|
||||
|
||||
|
||||
private async stopEventsLoop(): Promise<void> {
|
||||
this.receiveEvents = false
|
||||
if (this.eventsLoop) await this.eventsLoop
|
||||
this.eventsLoop = undefined
|
||||
}
|
||||
|
||||
private async runEventsLoop(): Promise<void> {
|
||||
while (this.receiveEvents) {
|
||||
try {
|
||||
@@ -335,7 +350,7 @@ export class ChatApi {
|
||||
return await core.chatSendCmd(this.ctrl, cmd)
|
||||
}
|
||||
|
||||
async recvChatEvent(wait: number = 5_000_000): Promise<ChatEvent | undefined> {
|
||||
async recvChatEvent(wait: number = 500_000): Promise<ChatEvent | undefined> {
|
||||
return await core.chatRecvMsgWait(this.ctrl, wait)
|
||||
}
|
||||
|
||||
@@ -386,8 +401,10 @@ export class ChatApi {
|
||||
switch (r.type) {
|
||||
case "userProfileUpdated":
|
||||
return r.updateSummary
|
||||
case "userProfileNoChange":
|
||||
return {updateSuccesses: 0, updateFailures: 0, changedContacts: []}
|
||||
default:
|
||||
throw new ChatCommandError("error loading user address", r)
|
||||
throw new ChatCommandError("error setting profile address", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +477,7 @@ export class ChatApi {
|
||||
updatedMessage: {msgContent, mentions: {}},
|
||||
})
|
||||
)
|
||||
if (r.type === "chatItemUpdated") return r.chatItem.chatItem
|
||||
if (r.type === "chatItemUpdated" || r.type === "chatItemNotChanged") return r.chatItem.chatItem
|
||||
throw new ChatCommandError("error updating chat item", r)
|
||||
}
|
||||
|
||||
@@ -499,10 +516,10 @@ export class ChatApi {
|
||||
chatItemId: number,
|
||||
add: boolean,
|
||||
reaction: T.MsgReaction
|
||||
) {
|
||||
): Promise<T.ACIReaction> {
|
||||
const r = await this.sendChatCmd(CC.APIChatItemReaction.cmdString({chatRef: {chatType, chatId}, chatItemId, add, reaction}))
|
||||
if (r.type === "chatItemsDeleted") return r.chatItemDeletions
|
||||
throw new ChatCommandError("error setting item reaction", r)
|
||||
if (r.type === "chatItemReaction") return r.reaction
|
||||
throw new ChatCommandError("error setting item reaction", r)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -512,6 +529,7 @@ export class ChatApi {
|
||||
async apiReceiveFile(fileId: number): Promise<T.AChatItem> {
|
||||
const r = await this.sendChatCmd(CC.ReceiveFile.cmdString({fileId, userApprovedRelays: true}))
|
||||
if (r.type === "rcvFileAccepted") return r.chatItem
|
||||
if (r.type === "rcvFileAcceptedSndCancelled") throw new ChatCommandError("file cancelled by sender", r)
|
||||
throw new ChatCommandError("error receiving file", r)
|
||||
}
|
||||
|
||||
@@ -698,7 +716,7 @@ export class ChatApi {
|
||||
* Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link
|
||||
* Network usage: interactive.
|
||||
*/
|
||||
async apiConnect(userId: number, incognito: boolean, preparedLink?: T.CreatedConnLink): Promise<ConnReqType> {
|
||||
async apiConnect(userId: number, incognito: boolean, preparedLink: T.CreatedConnLink): Promise<ConnReqType> {
|
||||
const r = await this.sendChatCmd(CC.APIConnect.cmdString({userId, incognito, preparedLink_: preparedLink}))
|
||||
return this.handleConnectResult(r)
|
||||
}
|
||||
@@ -740,7 +758,7 @@ export class ChatApi {
|
||||
* Network usage: no.
|
||||
*/
|
||||
async apiRejectContactRequest(contactReqId: number): Promise<void> {
|
||||
const r = await this.sendChatCmd(CC.APIRejectContact.cmdString({contactReqId}))
|
||||
const r = await this.sendChatCmd(CC.APIRejectContact.cmdString({contactReqId, notify: false}))
|
||||
if (r.type === "contactRequestRejected") return
|
||||
throw new ChatCommandError("error rejecting contact request", r)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import equal = require("fast-deep-equal")
|
||||
|
||||
export type BotDbOpts = api.DbConfig & {
|
||||
confirmMigrations?: core.MigrationConfirmation
|
||||
queueSize?: number
|
||||
}
|
||||
|
||||
export interface BotOptions {
|
||||
@@ -45,10 +46,9 @@ export interface BotConfig {
|
||||
}
|
||||
|
||||
export async function run({profile, dbOpts, options = defaultOpts, onMessage, onCommands = {}, events = {}}: BotConfig): Promise<[api.ChatApi, T.User, T.UserContactLink | undefined]> {
|
||||
const bot = await api.ChatApi.init(dbOpts, dbOpts.confirmMigrations || core.MigrationConfirmation.YesUp)
|
||||
const bot = await api.ChatApi.init(dbOpts, dbOpts.confirmMigrations || core.MigrationConfirmation.YesUp, dbOpts.queueSize)
|
||||
const opts = fullOptions(options)
|
||||
if (onMessage) subscribeMessages(bot, onMessage)
|
||||
if (Object.keys(onCommands).length > 0) subscribeCommands(bot, onCommands)
|
||||
if (onMessage || Object.keys(onCommands).length > 0) subscribeChatItems(bot, onMessage, onCommands)
|
||||
if (Object.keys(events).length > 0) bot.on(events)
|
||||
subscribeLogEvents(bot, opts)
|
||||
const botProfile = mkBotProfile(profile, opts)
|
||||
@@ -105,40 +105,27 @@ function mkBotProfile(profile: T.Profile, opts: Required<BotOptions>): T.Profile
|
||||
return profile
|
||||
}
|
||||
|
||||
function subscribeMessages(bot: api.ChatApi, onMessage: (chatItem: T.AChatItem, content: T.MsgContent) => void | Promise<void>) {
|
||||
export function subscribeChatItems(
|
||||
bot: api.ChatApi,
|
||||
onMessage: ((chatItem: T.AChatItem, content: T.MsgContent) => void | Promise<void>) | undefined,
|
||||
commands: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise<void>)}
|
||||
) {
|
||||
bot.on("newChatItems", async ({chatItems}) => {
|
||||
for (const ci of chatItems) {
|
||||
if (ci.chatItem.content.type === "rcvMsgContent") {
|
||||
try {
|
||||
const p = onMessage(ci, ci.chatItem.content.msgContent)
|
||||
if (p instanceof Promise) await p
|
||||
} catch (e) {
|
||||
console.log("message processing error", e)
|
||||
}
|
||||
const content = ci.chatItem.content
|
||||
if (content.type !== "rcvMsgContent") continue
|
||||
const cmd = util.ciBotCommand(ci.chatItem)
|
||||
const cmdFunc = cmd && (commands[cmd.keyword] || commands[""])
|
||||
try {
|
||||
if (cmd && cmdFunc) await cmdFunc(ci, cmd)
|
||||
else if (onMessage) await onMessage(ci, content.msgContent)
|
||||
} catch (e) {
|
||||
console.log(cmd && cmdFunc ? `${cmd.keyword} command processing error` : "message processing error", e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function subscribeCommands(bot: api.ChatApi, commands: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise<void>)}) {
|
||||
bot.on("newChatItems", async (evt) => {
|
||||
for (const ci of evt.chatItems) {
|
||||
const cmd = util.ciBotCommand(ci.chatItem)
|
||||
if (cmd) {
|
||||
const cmdFunc = commands[cmd.keyword] || commands[""]
|
||||
if (cmdFunc) {
|
||||
try {
|
||||
const p = cmdFunc(ci, cmd)
|
||||
if (p instanceof Promise) await p
|
||||
} catch(e) {
|
||||
console.log(`${cmd} command processing error`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function subscribeLogEvents(bot: api.ChatApi, opts: Required<BotOptions>) {
|
||||
if (opts.logContacts) {
|
||||
bot.on({
|
||||
|
||||
@@ -3,9 +3,12 @@ import * as simplex from "./simplex"
|
||||
|
||||
/**
|
||||
* Initialize chat controller
|
||||
* @param {number} [queueSize] - Size of internal queues, the core default is used when omitted.
|
||||
*/
|
||||
export async function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation): Promise<bigint> {
|
||||
const [ctrl, res] = await simplex.chat_migrate_init(dbPath, dbKey, confirm)
|
||||
export async function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation, queueSize?: number): Promise<bigint> {
|
||||
const [ctrl, res] = queueSize === undefined
|
||||
? await simplex.chat_migrate_init(dbPath, dbKey, confirm)
|
||||
: await simplex.chat_migrate_init_queue(dbPath, dbKey, confirm, queueSize)
|
||||
const json = JSON.parse(res)
|
||||
if (json.type === 'ok') return ctrl
|
||||
throw new ChatInitError("Database or migration error (see dbMigrationError property)", json as DBMigrationError)
|
||||
@@ -47,7 +50,7 @@ export async function chatRecvMsgWait(ctrl: bigint, wait: number): Promise<ChatE
|
||||
/**
|
||||
* Write buffer to encrypted file
|
||||
*/
|
||||
export async function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise<CryptoArgs> {
|
||||
export async function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer | Uint8Array): Promise<CryptoArgs> {
|
||||
const res = await simplex.chat_write_file(ctrl, path, buffer)
|
||||
return cryptoArgsResult(res)
|
||||
}
|
||||
@@ -55,7 +58,7 @@ export async function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuf
|
||||
/**
|
||||
* Read buffer from encrypted file
|
||||
*/
|
||||
export async function chatReadFile(path: string, {fileKey, fileNonce}: CryptoArgs): Promise<ArrayBuffer> {
|
||||
export async function chatReadFile(path: string, {fileKey, fileNonce}: CryptoArgs): Promise<Buffer> {
|
||||
return await simplex.chat_read_file(path, fileKey, fileNonce)
|
||||
}
|
||||
|
||||
@@ -121,12 +124,13 @@ export class ChatInitError extends Error {
|
||||
|
||||
export type DBMigrationError =
|
||||
| DBMigrationError.InvalidConfirmation
|
||||
| DBMigrationError.InvalidQueueSize
|
||||
| DBMigrationError.ErrorNotADatabase // invalid/corrupt database file or incorrect encryption key
|
||||
| DBMigrationError.ErrorMigration
|
||||
| DBMigrationError.ErrorSQL
|
||||
|
||||
export namespace DBMigrationError {
|
||||
export type Tag = "invalidConfirmation" | "errorNotADatabase" | "errorMigration" | "errorSQL"
|
||||
export type Tag = "invalidConfirmation" | "invalidQueueSize" | "errorNotADatabase" | "errorMigration" | "errorSQL"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
@@ -136,6 +140,10 @@ export namespace DBMigrationError {
|
||||
type: "invalidConfirmation"
|
||||
}
|
||||
|
||||
export interface InvalidQueueSize extends Interface {
|
||||
type: "invalidQueueSize"
|
||||
}
|
||||
|
||||
export interface ErrorNotADatabase extends Interface {
|
||||
type: "errorNotADatabase"
|
||||
dbFile: string
|
||||
@@ -168,7 +176,7 @@ export namespace MigrationError {
|
||||
|
||||
export interface MEUpgrade extends Interface {
|
||||
type: "upgrade"
|
||||
upMigrations: UpMigration
|
||||
upMigrations: UpMigration[]
|
||||
}
|
||||
|
||||
export interface MEDowngrade extends Interface {
|
||||
@@ -200,7 +208,7 @@ export namespace MTRError {
|
||||
|
||||
export interface MTRENoDown extends Interface {
|
||||
type: "noDown"
|
||||
upMigrations: UpMigration
|
||||
dbMigrations: string[]
|
||||
}
|
||||
|
||||
export interface MTREDifferent extends Interface {
|
||||
|
||||
@@ -6,6 +6,10 @@ const extract = require('extract-zip');
|
||||
const GITHUB_REPO = 'simplex-chat/simplex-chat-libs';
|
||||
const RELEASE_TAG = 'v7.1.0-beta.3';
|
||||
const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase();
|
||||
// A locally built libsimplex, copied into libs/ rather than loaded in place:
|
||||
// the addon's RUNPATH is $ORIGIN/../../libs.
|
||||
const LIBS_DIR_OVERRIDE = process.env.SIMPLEX_LIBS_DIR || process.env.npm_config_simplex_libs_dir;
|
||||
const LIB_NAMES = ['libsimplex.so', 'libsimplex.dylib', 'libsimplex.dll'];
|
||||
|
||||
if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') {
|
||||
console.error(`✗ Invalid SIMPLEX_BACKEND: "${BACKEND}". Must be "sqlite" or "postgres".`);
|
||||
@@ -83,8 +87,31 @@ function isAlreadyInstalled() {
|
||||
}
|
||||
}
|
||||
|
||||
// No version check: the files behind SIMPLEX_LIBS_DIR change on every rebuild.
|
||||
function installFromOverride() {
|
||||
if (!fs.existsSync(LIBS_DIR_OVERRIDE)) {
|
||||
throw new Error(`SIMPLEX_LIBS_DIR does not exist: ${LIBS_DIR_OVERRIDE}`);
|
||||
}
|
||||
const lib = LIB_NAMES.find((name) => fs.existsSync(path.join(LIBS_DIR_OVERRIDE, name)));
|
||||
if (!lib) {
|
||||
throw new Error(`No ${LIB_NAMES.join(' / ')} in SIMPLEX_LIBS_DIR: ${LIBS_DIR_OVERRIDE}`);
|
||||
}
|
||||
console.log(`Using libraries from SIMPLEX_LIBS_DIR: ${LIBS_DIR_OVERRIDE}`);
|
||||
cleanLibsDirectory();
|
||||
copyDirSync(LIBS_DIR_OVERRIDE, LIBS_DIR);
|
||||
// Not a release tag: a later install without the variable sees a mismatch
|
||||
// and replaces these files with the released ones.
|
||||
fs.writeFileSync(INSTALLED_FILE, `${LIBS_DIR_OVERRIDE}:${BACKEND}`, 'utf-8');
|
||||
console.log(`✓ Installed ${lib} and its runtime libraries from ${LIBS_DIR_OVERRIDE}`);
|
||||
}
|
||||
|
||||
async function install() {
|
||||
try {
|
||||
if (LIBS_DIR_OVERRIDE) {
|
||||
installFromOverride();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
if (isAlreadyInstalled()) {
|
||||
return;
|
||||
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
// 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_migrate_init_queue(dbPath: string, dbKey: string, confirm: string, queueSize: number): 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_write_file(ctrl: bigint, path: string, buffer: ArrayBuffer | Uint8Array): Promise<string>
|
||||
export function chat_read_file(path: string, key: string, nonce: string): Promise<Buffer>
|
||||
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>
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface BotCommand {
|
||||
export function ciBotCommand(chatItem: T.ChatItem): BotCommand | undefined {
|
||||
const msg = ciContentText(chatItem)?.trim()
|
||||
if (msg) {
|
||||
const r = msg.match(/^\/([^\s]+)(.*)/)
|
||||
const r = msg.match(/^\/([^\s]+)([\s\S]*)/)
|
||||
if (r && r.length >= 3) {
|
||||
return {keyword: r[1], params: r[2].trim()}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ describe("API tests (use preset servers)", () => {
|
||||
await bob.stopChat()
|
||||
await alice.close()
|
||||
await bob.close()
|
||||
await expect(alice.startChat).rejects.toThrow()
|
||||
await expect(bob.startChat).rejects.toThrow()
|
||||
await expect(alice.startChat()).rejects.toThrow("chat api controller not initialized")
|
||||
await expect(bob.startChat()).rejects.toThrow("chat api controller not initialized")
|
||||
expect(servers.length).toBe(2)
|
||||
expect(servers[0] !== servers[1]).toBe(true)
|
||||
expect(eventCount > 0).toBe(true)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import {ChatResponse, T} from "@simplex-chat/types"
|
||||
import * as api from "../src/api"
|
||||
import * as core from "../src/core"
|
||||
|
||||
const user = {userId: 1} as T.User
|
||||
|
||||
async function chatWithResponse(response: object): Promise<api.ChatApi> {
|
||||
jest.spyOn(core, "chatMigrateInit").mockResolvedValue(BigInt(1))
|
||||
jest.spyOn(core, "chatSendCmd").mockResolvedValue(response as ChatResponse)
|
||||
return api.ChatApi.init({type: "sqlite", filePrefix: "unused"})
|
||||
}
|
||||
|
||||
afterEach(() => jest.restoreAllMocks())
|
||||
|
||||
describe("documented success responses", () => {
|
||||
it("apiChatItemReaction returns the reaction", async () => {
|
||||
const reaction = {chatReaction: {reaction: {type: "emoji", emoji: "👍"}}}
|
||||
const chat = await chatWithResponse({type: "chatItemReaction", user, added: true, reaction})
|
||||
await expect(chat.apiChatItemReaction(T.ChatType.Direct, 1, 2, true, {type: "emoji", emoji: "👍"})).resolves.toEqual(reaction)
|
||||
})
|
||||
|
||||
it("apiUpdateChatItem accepts chatItemNotChanged", async () => {
|
||||
const chatItem = {meta: {itemId: 2}}
|
||||
const chat = await chatWithResponse({type: "chatItemNotChanged", user, chatItem: {chatItem}})
|
||||
await expect(chat.apiUpdateChatItem(T.ChatType.Direct, 1, 2, {type: "text", text: "same"}, false)).resolves.toEqual(chatItem)
|
||||
})
|
||||
|
||||
it("apiSetProfileAddress accepts userProfileNoChange", async () => {
|
||||
const chat = await chatWithResponse({type: "userProfileNoChange", user})
|
||||
await expect(chat.apiSetProfileAddress(1, true)).resolves.toEqual({updateSuccesses: 0, updateFailures: 0, changedContacts: []})
|
||||
})
|
||||
|
||||
it("apiReceiveFile reports a file cancelled by sender", async () => {
|
||||
const chat = await chatWithResponse({type: "rcvFileAcceptedSndCancelled", user, rcvFileTransfer: {}})
|
||||
await expect(chat.apiReceiveFile(3)).rejects.toThrow("file cancelled by sender")
|
||||
})
|
||||
})
|
||||
|
||||
describe("startChat lifecycle", () => {
|
||||
function chatWithResponses(...responses: object[]): Promise<api.ChatApi> {
|
||||
jest.spyOn(core, "chatMigrateInit").mockResolvedValue(BigInt(1))
|
||||
jest.spyOn(core, "chatRecvMsgWait").mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(undefined), 10)))
|
||||
const send = jest.spyOn(core, "chatSendCmd")
|
||||
for (const r of responses) send.mockResolvedValueOnce(r as ChatResponse)
|
||||
return api.ChatApi.init({type: "sqlite", filePrefix: "unused"})
|
||||
}
|
||||
|
||||
it("rejects a second start", async () => {
|
||||
const chat = await chatWithResponses({type: "chatStarted"}, {type: "chatStopped"})
|
||||
await chat.startChat()
|
||||
await expect(chat.startChat()).rejects.toThrow("chat already started")
|
||||
await chat.stopChat()
|
||||
})
|
||||
|
||||
it("stops the events loop when start fails", async () => {
|
||||
const chat = await chatWithResponses({type: "chatCmdError"})
|
||||
await expect(chat.startChat()).rejects.toThrow("error starting chat")
|
||||
expect(chat.started).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects start after close", async () => {
|
||||
const chat = await chatWithResponses({type: "chatStopped"})
|
||||
jest.spyOn(core, "chatCloseStore").mockResolvedValue()
|
||||
await chat.close()
|
||||
await expect(chat.startChat()).rejects.toThrow("chat api controller not initialized")
|
||||
})
|
||||
|
||||
it("stops the chat before closing the store", async () => {
|
||||
const chat = await chatWithResponses()
|
||||
const calls: string[] = []
|
||||
jest.mocked(core.chatSendCmd).mockImplementation(async (_ctrl, cmd) => {
|
||||
calls.push(`send ${cmd}`)
|
||||
return {type: "chatStopped"} as ChatResponse
|
||||
})
|
||||
jest.spyOn(core, "chatCloseStore").mockImplementation(async () => { calls.push("closeStore") })
|
||||
await chat.close()
|
||||
expect(calls).toEqual(["send /_stop", "closeStore"])
|
||||
expect(chat.initialized).toBe(false)
|
||||
})
|
||||
|
||||
it("does not close the store when stopping fails", async () => {
|
||||
const chat = await chatWithResponses({type: "chatCmdError"})
|
||||
const closeStore = jest.spyOn(core, "chatCloseStore").mockResolvedValue()
|
||||
await expect(chat.close()).rejects.toThrow("error stopping chat")
|
||||
expect(closeStore).not.toHaveBeenCalled()
|
||||
expect(chat.initialized).toBe(true)
|
||||
})
|
||||
|
||||
it("reports stop failures as stop errors", async () => {
|
||||
const chat = await chatWithResponses({type: "chatStarted"}, {type: "chatCmdError"}, {type: "chatStopped"})
|
||||
await chat.startChat()
|
||||
await expect(chat.stopChat()).rejects.toThrow("error stopping chat")
|
||||
expect(chat.started).toBe(true)
|
||||
await chat.stopChat()
|
||||
})
|
||||
|
||||
it("receives with a 500 ms wait", async () => {
|
||||
const chat = await chatWithResponses()
|
||||
await chat.recvChatEvent()
|
||||
expect(core.chatRecvMsgWait).toHaveBeenCalledWith(BigInt(1), 500_000)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import {ChatEvent, T} from "@simplex-chat/types"
|
||||
import * as api from "../src/api"
|
||||
import {subscribeChatItems} from "../src/bot"
|
||||
|
||||
type Handler = (evt: ChatEvent) => Promise<void>
|
||||
|
||||
function fakeBot(): {bot: api.ChatApi, deliver: (items: T.AChatItem[]) => Promise<void>} {
|
||||
let handler: Handler | undefined
|
||||
const bot = {on: (_event: string, h: Handler) => { handler = h }} as unknown as api.ChatApi
|
||||
const deliver = (chatItems: T.AChatItem[]) => handler!({type: "newChatItems", chatItems} as unknown as ChatEvent)
|
||||
return {bot, deliver}
|
||||
}
|
||||
|
||||
function item(type: "rcvMsgContent" | "sndMsgContent", text: string): T.AChatItem {
|
||||
return {chatItem: {content: {type, msgContent: {type: "text", text}}}} as unknown as T.AChatItem
|
||||
}
|
||||
|
||||
describe("subscribeChatItems", () => {
|
||||
it("sends a known command only to its handler", async () => {
|
||||
const {bot, deliver} = fakeBot()
|
||||
const calls: string[] = []
|
||||
subscribeChatItems(bot, async () => { calls.push("message") }, {help: async () => { calls.push("help") }})
|
||||
await deliver([item("rcvMsgContent", "/help")])
|
||||
expect(calls).toEqual(["help"])
|
||||
})
|
||||
|
||||
it("sends an unknown command to the fallback handler", async () => {
|
||||
const {bot, deliver} = fakeBot()
|
||||
const calls: string[] = []
|
||||
subscribeChatItems(bot, async () => { calls.push("message") }, {"": async (_ci, cmd) => { calls.push(`fallback:${cmd.keyword}`) }})
|
||||
await deliver([item("rcvMsgContent", "/unknown")])
|
||||
expect(calls).toEqual(["fallback:unknown"])
|
||||
})
|
||||
|
||||
it("sends unhandled commands and text to onMessage", async () => {
|
||||
const {bot, deliver} = fakeBot()
|
||||
const calls: string[] = []
|
||||
subscribeChatItems(bot, async (_ci, content) => { calls.push(`message:${(content as T.MsgContent & {text: string}).text}`) }, {help: async () => { calls.push("help") }})
|
||||
await deliver([item("rcvMsgContent", "/unknown"), item("rcvMsgContent", "hello")])
|
||||
expect(calls).toEqual(["message:/unknown", "message:hello"])
|
||||
})
|
||||
|
||||
it("ignores sent items", async () => {
|
||||
const {bot, deliver} = fakeBot()
|
||||
const calls: string[] = []
|
||||
subscribeChatItems(bot, async () => { calls.push("message") }, {help: async () => { calls.push("help") }})
|
||||
await deliver([item("sndMsgContent", "/help"), item("sndMsgContent", "hello")])
|
||||
expect(calls).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import {CC} from "@simplex-chat/types"
|
||||
|
||||
describe("APIConnect.cmdString", () => {
|
||||
const preparedLink_ = {connFullLink: "L"}
|
||||
|
||||
it("renders incognito=on", () => {
|
||||
expect(CC.APIConnect.cmdString({userId: 1, incognito: true, preparedLink_})).toBe("/_connect 1 incognito=on L")
|
||||
})
|
||||
|
||||
it("omits incognito when off", () => {
|
||||
expect(CC.APIConnect.cmdString({userId: 1, incognito: false, preparedLink_})).toBe("/_connect 1 L")
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import {execFile, spawnSync} from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import {core} from "../src/index";
|
||||
@@ -9,17 +10,34 @@ describe("Core tests", () => {
|
||||
beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true}));
|
||||
afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true}));
|
||||
|
||||
async function stopAndClose(ctrl: bigint): Promise<void> {
|
||||
await expect(core.chatSendCmd(ctrl, "/_stop")).resolves.toMatchObject({type: "chatStopped"});
|
||||
await core.chatCloseStore(ctrl);
|
||||
}
|
||||
|
||||
it("should initialize chat controller", async () => {
|
||||
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(stopAndClose(ctrl)).resolves.toBe(undefined);
|
||||
|
||||
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"})
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should initialize chat controller with queue size", async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp, 65536);
|
||||
expect(typeof ctrl).toBe("bigint");
|
||||
await expect(stopAndClose(ctrl)).resolves.toBe(undefined);
|
||||
|
||||
await expect(core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp, 0)).rejects.toMatchObject({
|
||||
dbMigrationError: {type: "invalidQueueSize"}
|
||||
});
|
||||
await expect(core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp, 2 ** 31)).rejects.toThrow("Expected 32-bit integer queue size");
|
||||
await expect(core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp, 1.5)).rejects.toThrow("Expected 32-bit integer queue size");
|
||||
});
|
||||
|
||||
it("should send command and receive event", async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
|
||||
@@ -41,7 +59,7 @@ describe("Core tests", () => {
|
||||
chatError: expect.objectContaining({type: "error"})
|
||||
});
|
||||
|
||||
await core.chatCloseStore(ctrl);
|
||||
await stopAndClose(ctrl);
|
||||
});
|
||||
|
||||
it("should write/read encrypted file from/to buffer", async () => {
|
||||
@@ -59,7 +77,25 @@ describe("Core tests", () => {
|
||||
await expect(core.chatWriteFile(ctrl, path.join(tmpDir, "unknown", "unknown.txt"), buffer)).rejects.toThrow();
|
||||
await expect(core.chatReadFile(path.join(tmpDir, "unknown.txt"), cryptoArgs)).rejects.toThrow();
|
||||
|
||||
await core.chatCloseStore(ctrl);
|
||||
await stopAndClose(ctrl);
|
||||
});
|
||||
|
||||
it("should write the view of a Uint8Array and read an empty file", async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
|
||||
const viewPath = path.join(tmpDir, "view.txt");
|
||||
const viewArgs = await core.chatWriteFile(ctrl, viewPath, Buffer.from("xxabcxx").subarray(2, 5));
|
||||
const view = await core.chatReadFile(viewPath, viewArgs);
|
||||
expect(Buffer.isBuffer(view)).toBe(true);
|
||||
expect(view.toString()).toBe("abc");
|
||||
|
||||
const emptyPath = path.join(tmpDir, "empty.txt");
|
||||
const emptyArgs = await core.chatWriteFile(ctrl, emptyPath, new Uint8Array(0));
|
||||
const empty = await core.chatReadFile(emptyPath, emptyArgs);
|
||||
expect(Buffer.isBuffer(empty)).toBe(true);
|
||||
expect(empty.length).toBe(0);
|
||||
|
||||
await stopAndClose(ctrl);
|
||||
});
|
||||
|
||||
it("should encrypt/decrypt file", async () => {
|
||||
@@ -80,6 +116,118 @@ describe("Core tests", () => {
|
||||
await expect(core.chatEncryptFile(ctrl, path.join(tmpDir, "unknown.txt"), encryptedPath)).rejects.toThrow();
|
||||
await expect(core.chatDecryptFile(path.join(tmpDir, "unknown.txt"), cryptoArgs, decryptedPath)).rejects.toThrow();
|
||||
|
||||
await core.chatCloseStore(ctrl);
|
||||
await stopAndClose(ctrl);
|
||||
});
|
||||
|
||||
it("should not block the libuv pool while receiving", async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
const receives = [1, 2, 3, 4].map(() => core.chatRecvMsgWait(ctrl, 2_000_000));
|
||||
const start = Date.now();
|
||||
await fs.promises.stat(tmpDir);
|
||||
expect(Date.now() - start).toBeLessThan(200);
|
||||
await Promise.all(receives);
|
||||
await stopAndClose(ctrl);
|
||||
}, 10000);
|
||||
|
||||
const itOnLinux = process.platform === "linux" ? it : it.skip;
|
||||
|
||||
// Thread count is read from /proc/self/task, which only exists on Linux.
|
||||
itOnLinux("should keep the thread count constant while receiving", async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
for (let i = 0; i < 10; i++) await core.chatRecvMsgWait(ctrl, 1);
|
||||
const threadCount = () => fs.readdirSync("/proc/self/task").length;
|
||||
const warmCount = threadCount();
|
||||
const counts = new Set<number>();
|
||||
for (let i = 0; i < 200; i++) {
|
||||
await core.chatRecvMsgWait(ctrl, 1);
|
||||
counts.add(threadCount());
|
||||
}
|
||||
expect({warmCount, counts: [...counts]}).toEqual({warmCount, counts: [warmCount]});
|
||||
await stopAndClose(ctrl);
|
||||
}, 30000);
|
||||
|
||||
it("should receive on two controllers concurrently", async () => {
|
||||
const ctrlA = await core.chatMigrateInit(path.join(tmpDir, "simplex_a"), "key", core.MigrationConfirmation.YesUp);
|
||||
const ctrlB = await core.chatMigrateInit(path.join(tmpDir, "simplex_b"), "key", core.MigrationConfirmation.YesUp);
|
||||
const start = Date.now();
|
||||
await expect(Promise.all([core.chatRecvMsgWait(ctrlA, 1_000_000), core.chatRecvMsgWait(ctrlB, 1_000_000)]))
|
||||
.resolves.toEqual([undefined, undefined]);
|
||||
const elapsed = Date.now() - start;
|
||||
expect(elapsed).toBeGreaterThanOrEqual(900);
|
||||
expect(elapsed).toBeLessThan(1800);
|
||||
await stopAndClose(ctrlA);
|
||||
await stopAndClose(ctrlB);
|
||||
}, 10000);
|
||||
|
||||
it("should receive events of one controller in order", async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
for (const action of ["first", "second"]) {
|
||||
await expect(core.chatSendCmd(ctrl, `/debug event {"type": "timedAction", "action": "${action}", "durationMilliseconds": 1}`))
|
||||
.resolves.toMatchObject({type: "cmdOk"});
|
||||
}
|
||||
const events = await Promise.all([core.chatRecvMsgWait(ctrl, 500_000), core.chatRecvMsgWait(ctrl, 500_000)]);
|
||||
expect(events).toMatchObject([
|
||||
{type: "timedAction", action: "first"},
|
||||
{type: "timedAction", action: "second"}
|
||||
]);
|
||||
await stopAndClose(ctrl);
|
||||
}, 10000);
|
||||
|
||||
it("should close the store while a receive is in flight", async () => {
|
||||
const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
|
||||
// starts the receiver thread, so the next request only has to wake it
|
||||
await core.chatRecvMsgWait(ctrl, 1);
|
||||
const settle = (p: Promise<unknown>) => p.then((event) => ({event}), (e: Error) => ({error: e.message}));
|
||||
const receives = Promise.all([settle(core.chatRecvMsgWait(ctrl, 3_000_000)), settle(core.chatRecvMsgWait(ctrl, 3_000_000))]);
|
||||
// the thread enters the first receive within this margin even under load, so the second one is still queued at close
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await expect(core.chatSendCmd(ctrl, "/_stop")).resolves.toMatchObject({type: "chatStopped"});
|
||||
let closed = false;
|
||||
const close = core.chatCloseStore(ctrl).then(() => { closed = true; });
|
||||
const timerStart = Date.now();
|
||||
const timerDelay = await new Promise<number>((resolve) => setTimeout(() => resolve(Date.now() - timerStart), 10));
|
||||
expect({closed, timerDelayBelow100ms: timerDelay < 100}).toEqual({closed: false, timerDelayBelow100ms: true});
|
||||
await close;
|
||||
expect(await receives).toEqual([{event: undefined}, {error: "chat receiver stopped"}]);
|
||||
}, 10000);
|
||||
|
||||
it("should let the process exit while a receiver is idle", () => {
|
||||
const childDbPath = path.resolve(tmpDir, "simplex_child");
|
||||
const script = `
|
||||
const simplex = require("./build/Release/simplex.node");
|
||||
simplex.chat_migrate_init(${JSON.stringify(childDbPath)}, "key", "yesUp")
|
||||
.then(([ctrl]) => simplex.chat_recv_msg_wait(ctrl, 1))
|
||||
.then((res) => console.log("received " + JSON.stringify(res)));
|
||||
`;
|
||||
const child = spawnSync(process.execPath, ["-e", script], {cwd: path.join(__dirname, ".."), timeout: 10000, encoding: "utf8"});
|
||||
if (child.status !== 0 || child.signal !== null) console.log("child stderr:", child.stderr);
|
||||
expect({status: child.status, signal: child.signal, stdout: child.stdout.trim()})
|
||||
.toEqual({status: 0, signal: null, stdout: 'received ""'});
|
||||
}, 15000);
|
||||
|
||||
it("should not crash when closing stopped controllers repeatedly", async () => {
|
||||
const script = `
|
||||
const fs = require("fs"), path = require("path");
|
||||
const simplex = require("./build/Release/simplex.node");
|
||||
(async () => {
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const dir = fs.mkdtempSync(path.join(${JSON.stringify(path.resolve(tmpDir))}, "close-"));
|
||||
const [ctrl] = await simplex.chat_migrate_init(path.join(dir, "simplex"), "key", "yesUp");
|
||||
await simplex.chat_send_cmd(ctrl, "/v");
|
||||
await simplex.chat_send_cmd(ctrl, "/_stop");
|
||||
const res = await simplex.chat_close_store(ctrl);
|
||||
fs.rmSync(dir, {recursive: true, force: true});
|
||||
if (res !== "") throw new Error("close failed: " + res);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
const runChild = () => new Promise<{code: number | null, signal: NodeJS.Signals | null, stderr: string}>((resolve) => {
|
||||
const child = execFile(process.execPath, ["-e", script], {cwd: path.join(__dirname, ".."), timeout: 150000}, (_error, _stdout, stderr) =>
|
||||
resolve({code: child.exitCode, signal: child.signalCode, stderr}));
|
||||
});
|
||||
const childCount = 3;
|
||||
const results = await Promise.all(Array.from({length: childCount}, runChild));
|
||||
for (const r of results) if (r.code !== 0 || r.signal !== null) console.log("child stderr:", r.stderr);
|
||||
expect(results.map(({code, signal}) => ({code, signal}))).toEqual(Array(childCount).fill({code: 0, signal: null}));
|
||||
}, 180000);
|
||||
});
|
||||
|
||||
@@ -34,4 +34,8 @@ describe("ciBotCommand", () => {
|
||||
const ci = {content: {type: "rcvDeleted"}} as T.ChatItem
|
||||
expect(ciBotCommand(ci)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("keeps multi-line params", () => {
|
||||
expect(ciBotCommand(rcvText("/review line1\nline2"))).toEqual({keyword: "review", params: "line1\nline2"})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ import urllib.request
|
||||
import zipfile
|
||||
from ctypes import POINTER, c_char_p, c_int, c_uint8, c_void_p
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
from ._version import LIBS_VERSION
|
||||
|
||||
@@ -166,7 +166,8 @@ _backend: Backend | None = None
|
||||
|
||||
def _load_libc() -> ctypes.CDLL:
|
||||
if sys.platform == "win32":
|
||||
return ctypes.CDLL("msvcrt")
|
||||
# libsimplex.dll allocates results with UCRT malloc; msvcrt free would corrupt the heap.
|
||||
return ctypes.CDLL("ucrtbase")
|
||||
return ctypes.CDLL(None) # libc on POSIX is the process's own symbol table
|
||||
|
||||
|
||||
@@ -255,3 +256,19 @@ def lib() -> ctypes.CDLL:
|
||||
if _lib is None:
|
||||
raise RuntimeError("lib_for() must be called before lib()")
|
||||
return _lib
|
||||
|
||||
|
||||
QUEUE_SIZE_UNSUPPORTED = (
|
||||
"loaded libsimplex does not export chat_migrate_init_queue; queue size needs a newer libsimplex"
|
||||
)
|
||||
|
||||
|
||||
def migrate_init_queue() -> Any:
|
||||
"""`chat_migrate_init_queue`, which older libsimplex releases do not export."""
|
||||
try:
|
||||
fn = lib().chat_migrate_init_queue
|
||||
except AttributeError as e:
|
||||
raise RuntimeError(QUEUE_SIZE_UNSUPPORTED) from e
|
||||
fn.argtypes = [c_char_p, c_char_p, c_char_p, c_int, POINTER(c_void_p)]
|
||||
fn.restype = c_void_p
|
||||
return fn
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
|
||||
@@ -65,17 +67,20 @@ class ChatApi:
|
||||
def __init__(self, ctrl: int):
|
||||
self._ctrl: int | None = ctrl
|
||||
self._started = False
|
||||
self._recv_executor: ThreadPoolExecutor | None = None
|
||||
|
||||
@classmethod
|
||||
async def init(
|
||||
cls,
|
||||
db: Db,
|
||||
confirm: MigrationConfirmation = MigrationConfirmation.YES_UP,
|
||||
queue_size: int | None = None,
|
||||
) -> ChatApi:
|
||||
path_or_prefix, key_or_conn, backend = _db_to_migrate_args(db)
|
||||
# Trigger lazy lib load with the right backend BEFORE chat_migrate_init.
|
||||
_native.lib_for(backend)
|
||||
ctrl = await core.chat_migrate_init(path_or_prefix, key_or_conn, confirm)
|
||||
# It may download ~100 MB, so it must not block the event loop.
|
||||
await asyncio.to_thread(_native.lib_for, backend)
|
||||
ctrl = await core.chat_migrate_init(path_or_prefix, key_or_conn, confirm, queue_size)
|
||||
return cls(ctrl)
|
||||
|
||||
@property
|
||||
@@ -114,6 +119,14 @@ class ChatApi:
|
||||
self._started = False
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Stop the chat and close its store; the store stays open if stopping fails."""
|
||||
# a running controller keeps using the database connections that closing frees
|
||||
await self.stop_chat()
|
||||
if self._recv_executor is not None:
|
||||
# Waits for a receive already in flight (up to wait_us) so the store
|
||||
# never closes underneath one; run off-loop since shutdown blocks.
|
||||
await asyncio.to_thread(self._recv_executor.shutdown, wait=True)
|
||||
self._recv_executor = None
|
||||
await core.chat_close_store(self.ctrl)
|
||||
self._ctrl = None
|
||||
self._started = False
|
||||
@@ -122,7 +135,14 @@ class ChatApi:
|
||||
return await core.chat_send_cmd(self.ctrl, cmd)
|
||||
|
||||
async def recv_chat_event(self, wait_us: int = 500_000) -> CEvt.ChatEvent | None:
|
||||
return await core.chat_recv_msg_wait(self.ctrl, wait_us)
|
||||
ctrl = self.ctrl # raises before touching the executor if close() was called
|
||||
if self._recv_executor is None:
|
||||
# A receive blocks for up to wait_us almost back to back, so it would
|
||||
# otherwise pin one of the default executor's few worker threads.
|
||||
self._recv_executor = ThreadPoolExecutor(
|
||||
max_workers=1, thread_name_prefix="simplex-recv"
|
||||
)
|
||||
return await core.chat_recv_msg_wait(ctrl, wait_us, self._recv_executor)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Address commands
|
||||
@@ -158,6 +178,8 @@ class ChatApi:
|
||||
)
|
||||
if r["type"] == "userProfileUpdated":
|
||||
return r["updateSummary"]
|
||||
if r["type"] == "userProfileNoChange":
|
||||
return {"updateSuccesses": 0, "updateFailures": 0, "changedContacts": []}
|
||||
raise ChatCommandError("error setting profile address", r)
|
||||
|
||||
async def api_set_address_settings(self, user_id: int, settings: T.AddressSettings) -> None:
|
||||
@@ -236,6 +258,8 @@ class ChatApi:
|
||||
)
|
||||
if r["type"] == "chatItemUpdated":
|
||||
return r["chatItem"]["chatItem"]
|
||||
if r["type"] == "chatItemNotChanged":
|
||||
return r["chatItem"]["chatItem"]
|
||||
raise ChatCommandError("error updating chat item", r)
|
||||
|
||||
async def api_delete_chat_items(
|
||||
@@ -302,6 +326,8 @@ class ChatApi:
|
||||
)
|
||||
if r["type"] == "rcvFileAccepted":
|
||||
return r["chatItem"]
|
||||
if r["type"] == "rcvFileAcceptedSndCancelled":
|
||||
raise ChatCommandError("file cancelled by sender", r)
|
||||
raise ChatCommandError("error receiving file", r)
|
||||
|
||||
async def api_cancel_file(self, file_id: int) -> None:
|
||||
@@ -477,12 +503,13 @@ class ChatApi:
|
||||
self,
|
||||
user_id: int,
|
||||
incognito: bool,
|
||||
prepared_link: T.CreatedConnLink | None = None,
|
||||
prepared_link: T.CreatedConnLink,
|
||||
) -> ConnReqType:
|
||||
args: CC.APIConnect = {"userId": user_id, "incognito": incognito}
|
||||
if prepared_link is not None:
|
||||
args["preparedLink_"] = prepared_link
|
||||
r = await self.send_chat_cmd(CC.APIConnect_cmd_string(args))
|
||||
r = await self.send_chat_cmd(
|
||||
CC.APIConnect_cmd_string(
|
||||
{"userId": user_id, "incognito": incognito, "preparedLink_": prepared_link}
|
||||
)
|
||||
)
|
||||
return self._handle_connect_result(r)
|
||||
|
||||
async def api_connect_active_user(self, conn_link: str) -> ConnReqType:
|
||||
|
||||
@@ -90,6 +90,7 @@ class Bot(Client):
|
||||
welcome: str | T.MsgContent | None = None,
|
||||
commands: list[BotCommand] | None = None,
|
||||
confirm_migrations: MigrationConfirmation = MigrationConfirmation.YES_UP,
|
||||
queue_size: int | None = None,
|
||||
create_address: bool = True,
|
||||
update_address: bool = True,
|
||||
update_profile: bool = True,
|
||||
@@ -103,6 +104,7 @@ class Bot(Client):
|
||||
profile=profile,
|
||||
db=db,
|
||||
confirm_migrations=confirm_migrations,
|
||||
queue_size=queue_size,
|
||||
update_profile=update_profile,
|
||||
log_contacts=log_contacts,
|
||||
log_network=log_network,
|
||||
|
||||
@@ -149,6 +149,7 @@ class Client:
|
||||
profile: Profile,
|
||||
db: Db,
|
||||
confirm_migrations: MigrationConfirmation = MigrationConfirmation.YES_UP,
|
||||
queue_size: int | None = None,
|
||||
update_profile: bool = True,
|
||||
log_contacts: bool = False,
|
||||
log_network: bool = False,
|
||||
@@ -156,6 +157,7 @@ class Client:
|
||||
self._profile = profile
|
||||
self._db = db
|
||||
self._confirm_migrations = confirm_migrations
|
||||
self._queue_size = queue_size
|
||||
self._update_profile = update_profile
|
||||
self._log_contacts = log_contacts
|
||||
self._log_network = log_network
|
||||
@@ -343,7 +345,7 @@ class Client:
|
||||
# do post-start setup (profile sync; Bot adds address sync).
|
||||
# `_stop_event` is never cleared: a stop requested during startup has
|
||||
# to survive into the receive loop. A stopped client is spent.
|
||||
self._api = await ChatApi.init(self._db, self._confirm_migrations)
|
||||
self._api = await ChatApi.init(self._db, self._confirm_migrations, self._queue_size)
|
||||
try:
|
||||
user = await self._ensure_active_user()
|
||||
await self._api.start_chat()
|
||||
@@ -362,11 +364,6 @@ class Client:
|
||||
api = self._api
|
||||
if api is None:
|
||||
return
|
||||
if api.started:
|
||||
try:
|
||||
await api.stop_chat()
|
||||
except Exception:
|
||||
log.exception("stop_chat failed during init rollback")
|
||||
try:
|
||||
await api.close()
|
||||
except Exception:
|
||||
@@ -379,13 +376,16 @@ class Client:
|
||||
if api is None:
|
||||
return
|
||||
# Null out the reference up-front so the Client appears closed even
|
||||
# if stop_chat / close raise — otherwise `client.api` would still
|
||||
# if close raises — otherwise `client.api` would still
|
||||
# hand back a half-shutdown controller after `async with` exits.
|
||||
self._api = None
|
||||
try:
|
||||
await api.stop_chat()
|
||||
finally:
|
||||
await api.close()
|
||||
except BaseException:
|
||||
# A failed stop leaves the store open; keep it so the caller can retry.
|
||||
if api.initialized:
|
||||
self._api = api
|
||||
raise
|
||||
|
||||
async def _post_start(self, user: T.User) -> None:
|
||||
"""Hook for subclasses to add work between `start_chat` and serving.
|
||||
@@ -617,7 +617,7 @@ class Client:
|
||||
# message resolve a future no one is waiting on.
|
||||
if waiter in waiters:
|
||||
waiters.remove(waiter)
|
||||
if not waiters:
|
||||
if not waiters and self._reply_waiters.get(contact_id) is waiters:
|
||||
self._reply_waiters.pop(contact_id, None)
|
||||
|
||||
async def _receive_loop(self) -> None:
|
||||
|
||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import ctypes
|
||||
import json
|
||||
from concurrent.futures import Executor
|
||||
from enum import StrEnum
|
||||
from typing import Any, TypedDict
|
||||
|
||||
@@ -102,7 +103,9 @@ async def chat_send_cmd(ctrl: int, cmd: str) -> CR.ChatResponse:
|
||||
raise ChatAPIError(f"invalid chat command result: {raw[:200]}")
|
||||
|
||||
|
||||
async def chat_recv_msg_wait(ctrl: int, wait_us: int = 500_000) -> CEvt.ChatEvent | None:
|
||||
async def chat_recv_msg_wait(
|
||||
ctrl: int, wait_us: int = 500_000, executor: Executor | None = None
|
||||
) -> CEvt.ChatEvent | None:
|
||||
def _call() -> str:
|
||||
# On timeout, the C side returns a non-NULL pointer to a single NUL byte
|
||||
# (see Mobile.hs `fromMaybe ""`), so `_read_and_free` returns "" — no
|
||||
@@ -110,7 +113,10 @@ async def chat_recv_msg_wait(ctrl: int, wait_us: int = 500_000) -> CEvt.ChatEven
|
||||
ptr = _native.lib().chat_recv_msg_wait(ctrl, wait_us)
|
||||
return _read_and_free(ptr)
|
||||
|
||||
raw = await asyncio.to_thread(_call)
|
||||
if executor is None:
|
||||
raw = await asyncio.to_thread(_call)
|
||||
else:
|
||||
raw = await asyncio.get_running_loop().run_in_executor(executor, _call)
|
||||
if not raw:
|
||||
return None
|
||||
parsed = json.loads(raw)
|
||||
@@ -122,17 +128,29 @@ async def chat_recv_msg_wait(ctrl: int, wait_us: int = 500_000) -> CEvt.ChatEven
|
||||
raise ChatAPIError(f"invalid chat event: {raw[:200]}")
|
||||
|
||||
|
||||
async def chat_migrate_init(db_path: str, db_key: str, confirm: MigrationConfirmation) -> int:
|
||||
"""Initialize chat controller. Returns opaque ctrl pointer as Python int."""
|
||||
async def chat_migrate_init(
|
||||
db_path: str,
|
||||
db_key: str,
|
||||
confirm: MigrationConfirmation,
|
||||
queue_size: int | None = None,
|
||||
) -> int:
|
||||
"""Initialize chat controller. Returns opaque ctrl pointer as Python int.
|
||||
|
||||
`queue_size` is the size of internal queues; the core default is used when None.
|
||||
"""
|
||||
# ctypes silently wraps ints that do not fit C int.
|
||||
if queue_size is not None and ctypes.c_int(queue_size).value != queue_size:
|
||||
raise ValueError(f"queue_size {queue_size} does not fit C int")
|
||||
|
||||
init_queue = _native.migrate_init_queue() if queue_size is not None else None
|
||||
|
||||
def _call() -> tuple[int, str]:
|
||||
ctrl = ctypes.c_void_p()
|
||||
ptr = _native.lib().chat_migrate_init(
|
||||
db_path.encode("utf-8"),
|
||||
db_key.encode("utf-8"),
|
||||
confirm.encode("utf-8"),
|
||||
ctypes.byref(ctrl),
|
||||
)
|
||||
args = (db_path.encode("utf-8"), db_key.encode("utf-8"), confirm.encode("utf-8"))
|
||||
if init_queue is None:
|
||||
ptr = _native.lib().chat_migrate_init(*args, ctypes.byref(ctrl))
|
||||
else:
|
||||
ptr = init_queue(*args, queue_size, ctypes.byref(ctrl))
|
||||
return (ctrl.value or 0, _read_and_free(ptr))
|
||||
|
||||
ctrl_val, raw = await asyncio.to_thread(_call)
|
||||
|
||||
@@ -498,7 +498,7 @@ class APIConnect(TypedDict):
|
||||
|
||||
|
||||
def APIConnect_cmd_string(self: APIConnect) -> str:
|
||||
return '/_connect ' + str(self['userId']) + ((' ' + T.CreatedConnLink_cmd_string(self.get('preparedLink_'))) if self.get('preparedLink_') is not None else '')
|
||||
return '/_connect ' + str(self['userId']) + (' incognito=on' if self['incognito'] else '') + ((' ' + T.CreatedConnLink_cmd_string(self.get('preparedLink_'))) if self.get('preparedLink_') is not None else '')
|
||||
|
||||
APIConnect_Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError
|
||||
|
||||
|
||||
@@ -220,83 +220,7 @@ BadgeRedeemError = (
|
||||
|
||||
BadgeRedeemError_Tag = Literal["invalidCode", "serviceNotConfigured", "badgeActive", "serviceError", "invalidResponse", "unknownKeyIndex", "credentialNotVerified"]
|
||||
|
||||
class BadgeServiceErrorCode_badRequest(TypedDict):
|
||||
type: Literal["badRequest"]
|
||||
|
||||
class BadgeServiceErrorCode_unsupportedVersion(TypedDict):
|
||||
type: Literal["unsupportedVersion"]
|
||||
|
||||
class BadgeServiceErrorCode_unknownPurchaseKey(TypedDict):
|
||||
type: Literal["unknownPurchaseKey"]
|
||||
|
||||
class BadgeServiceErrorCode_unknownOfferId(TypedDict):
|
||||
type: Literal["unknownOfferId"]
|
||||
|
||||
class BadgeServiceErrorCode_offerDisabled(TypedDict):
|
||||
type: Literal["offerDisabled"]
|
||||
|
||||
class BadgeServiceErrorCode_offerMismatch(TypedDict):
|
||||
type: Literal["offerMismatch"]
|
||||
|
||||
class BadgeServiceErrorCode_productUnavailable(TypedDict):
|
||||
type: Literal["productUnavailable"]
|
||||
|
||||
class BadgeServiceErrorCode_paymentNotEntitled(TypedDict):
|
||||
type: Literal["paymentNotEntitled"]
|
||||
|
||||
class BadgeServiceErrorCode_paymentPending(TypedDict):
|
||||
type: Literal["paymentPending"]
|
||||
|
||||
class BadgeServiceErrorCode_providerUnavailable(TypedDict):
|
||||
type: Literal["providerUnavailable"]
|
||||
|
||||
class BadgeServiceErrorCode_rateLimited(TypedDict):
|
||||
type: Literal["rateLimited"]
|
||||
|
||||
class BadgeServiceErrorCode_codeInvalid(TypedDict):
|
||||
type: Literal["codeInvalid"]
|
||||
|
||||
class BadgeServiceErrorCode_codeUsed(TypedDict):
|
||||
type: Literal["codeUsed"]
|
||||
|
||||
class BadgeServiceErrorCode_codeExpired(TypedDict):
|
||||
type: Literal["codeExpired"]
|
||||
|
||||
class BadgeServiceErrorCode_receiptInvalid(TypedDict):
|
||||
type: Literal["receiptInvalid"]
|
||||
|
||||
class BadgeServiceErrorCode_receiptUsed(TypedDict):
|
||||
type: Literal["receiptUsed"]
|
||||
|
||||
class BadgeServiceErrorCode_internal(TypedDict):
|
||||
type: Literal["internal"]
|
||||
|
||||
class BadgeServiceErrorCode_unknown(TypedDict):
|
||||
type: Literal["unknown"]
|
||||
: str
|
||||
|
||||
BadgeServiceErrorCode = (
|
||||
BadgeServiceErrorCode_badRequest
|
||||
| BadgeServiceErrorCode_unsupportedVersion
|
||||
| BadgeServiceErrorCode_unknownPurchaseKey
|
||||
| BadgeServiceErrorCode_unknownOfferId
|
||||
| BadgeServiceErrorCode_offerDisabled
|
||||
| BadgeServiceErrorCode_offerMismatch
|
||||
| BadgeServiceErrorCode_productUnavailable
|
||||
| BadgeServiceErrorCode_paymentNotEntitled
|
||||
| BadgeServiceErrorCode_paymentPending
|
||||
| BadgeServiceErrorCode_providerUnavailable
|
||||
| BadgeServiceErrorCode_rateLimited
|
||||
| BadgeServiceErrorCode_codeInvalid
|
||||
| BadgeServiceErrorCode_codeUsed
|
||||
| BadgeServiceErrorCode_codeExpired
|
||||
| BadgeServiceErrorCode_receiptInvalid
|
||||
| BadgeServiceErrorCode_receiptUsed
|
||||
| BadgeServiceErrorCode_internal
|
||||
| BadgeServiceErrorCode_unknown
|
||||
)
|
||||
|
||||
BadgeServiceErrorCode_Tag = Literal["badRequest", "unsupportedVersion", "unknownPurchaseKey", "unknownOfferId", "offerDisabled", "offerMismatch", "productUnavailable", "paymentNotEntitled", "paymentPending", "providerUnavailable", "rateLimited", "codeInvalid", "codeUsed", "codeExpired", "receiptInvalid", "receiptUsed", "internal", "unknown"]
|
||||
BadgeServiceErrorCode = Literal["bad_request", "unsupported_version", "unknown_purchase_key", "unknown_offer_id", "offer_disabled", "offer_mismatch", "product_unavailable", "payment_not_entitled", "payment_pending", "provider_unavailable", "rate_limited", "code_invalid", "code_used", "code_expired", "receipt_invalid", "receipt_used", "internal"]
|
||||
|
||||
BadgeStatus = Literal["active", "expired", "expiredOld", "failed", "unknownKey"]
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ def ci_content_text(chat_item: T.ChatItem) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
_BOT_COMMAND_RE = re.compile(r"^/([^\s]+)(.*)$")
|
||||
_BOT_COMMAND_RE = re.compile(r"^/([^\s]+)(.*)$", re.DOTALL)
|
||||
|
||||
|
||||
def ci_bot_command(chat_item: T.ChatItem) -> tuple[str, str] | None:
|
||||
|
||||
@@ -7,6 +7,7 @@ shape it accepts.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
@@ -172,3 +173,46 @@ async def test_a_failed_custom_data_write_raises():
|
||||
api = FakeCtrl({"type": "chatCmdError"})
|
||||
with pytest.raises(ChatCommandError):
|
||||
await api.api_merge_group_custom_data({"groupId": 9}, "mine", 1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Documented success responses
|
||||
# ---------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_update_chat_item_accepts_not_changed():
|
||||
chat_item = {"meta": {"itemId": 2}}
|
||||
api = FakeCtrl({"type": "chatItemNotChanged", "chatItem": {"chatItem": chat_item}})
|
||||
msg_content = {"type": "text", "text": "same"}
|
||||
assert asyncio.run(api.api_update_chat_item("direct", 1, 2, msg_content)) == chat_item
|
||||
|
||||
|
||||
def test_set_profile_address_accepts_no_change():
|
||||
api = FakeCtrl({"type": "userProfileNoChange"})
|
||||
summary = asyncio.run(api.api_set_profile_address(1, True))
|
||||
assert summary == {"updateSuccesses": 0, "updateFailures": 0, "changedContacts": []}
|
||||
|
||||
|
||||
def test_receive_file_reports_cancelled_by_sender():
|
||||
api = FakeCtrl({"type": "rcvFileAcceptedSndCancelled", "rcvFileTransfer": {}})
|
||||
with pytest.raises(ChatCommandError, match="file cancelled by sender"):
|
||||
asyncio.run(api.api_receive_file(3))
|
||||
|
||||
|
||||
async def test_init_loads_library_off_the_event_loop(monkeypatch):
|
||||
import threading
|
||||
|
||||
from simplex_chat import _native, core
|
||||
from simplex_chat.api import SqliteDb
|
||||
|
||||
threads: list[int] = []
|
||||
monkeypatch.setattr(_native, "lib_for", lambda _backend: threads.append(threading.get_ident()))
|
||||
|
||||
async def fake_migrate_init(*_args):
|
||||
return 1
|
||||
|
||||
monkeypatch.setattr(core, "chat_migrate_init", fake_migrate_init)
|
||||
|
||||
loop_thread = threading.get_ident()
|
||||
await ChatApi.init(SqliteDb(file_prefix="/tmp/unused"))
|
||||
assert threads and threads[0] != loop_thread
|
||||
|
||||
@@ -14,11 +14,13 @@ import pytest
|
||||
from simplex_chat import (
|
||||
Bot,
|
||||
BotProfile,
|
||||
ChatCommandError,
|
||||
Client,
|
||||
ContactAlreadyExistsError,
|
||||
Profile,
|
||||
SqliteDb,
|
||||
)
|
||||
from simplex_chat.core import MigrationConfirmation
|
||||
|
||||
|
||||
class FakeApi:
|
||||
@@ -292,6 +294,31 @@ def test_send_and_wait_parallel_different_contacts():
|
||||
assert (a, b) == ("A", "B")
|
||||
|
||||
|
||||
def test_send_and_wait_keeps_waiter_registered_during_previous_cleanup():
|
||||
bot, _api = _bot_with_fake_api()
|
||||
|
||||
def reply(text: str) -> dict[str, Any]:
|
||||
return {"type": "newChatItems", "chatItems": [
|
||||
{
|
||||
"chatInfo": {"type": "direct", "contact": {"contactId": 42}},
|
||||
"chatItem": {"content": {"type": "rcvMsgContent", "msgContent": {"type": "text", "text": text}}},
|
||||
}
|
||||
]}
|
||||
|
||||
async def go() -> tuple[str, str]:
|
||||
first = asyncio.create_task(bot.send_and_wait(42, "a", timeout=2.0))
|
||||
await asyncio.sleep(0)
|
||||
# Created before the reply is dispatched, so it registers before `first` runs its cleanup.
|
||||
second = asyncio.create_task(bot.send_and_wait(42, "b", timeout=2.0))
|
||||
await bot._dispatch_event(reply("ra")) # type: ignore[arg-type]
|
||||
await asyncio.sleep(0)
|
||||
await asyncio.sleep(0)
|
||||
await bot._dispatch_event(reply("rb")) # type: ignore[arg-type]
|
||||
return (await first).text or "", (await second).text or ""
|
||||
|
||||
assert asyncio.run(go()) == ("ra", "rb")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# connect_to
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -466,6 +493,10 @@ def test_aexit_nulls_api_even_if_close_raises(monkeypatch):
|
||||
async def stop_chat(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def initialized(self):
|
||||
return False
|
||||
|
||||
async def close(self):
|
||||
raise RuntimeError("close failed")
|
||||
|
||||
@@ -498,6 +529,83 @@ def test_aexit_nulls_api_even_if_close_raises(monkeypatch):
|
||||
asyncio.run(go())
|
||||
|
||||
|
||||
def test_aexit_keeps_api_for_retry_when_stop_fails(monkeypatch):
|
||||
"""A failed stop leaves the store open, so the Client must keep the
|
||||
controller: dropping it would leak the store with no way to close it."""
|
||||
import simplex_chat.client as client_mod
|
||||
|
||||
stop_results = ["chatCmdError", "chatStopped"]
|
||||
closed: list[bool] = [False]
|
||||
|
||||
class _FailingStopApi:
|
||||
@classmethod
|
||||
async def init(cls, *_a, **_kw):
|
||||
return cls()
|
||||
|
||||
@property
|
||||
def initialized(self):
|
||||
return not closed[0]
|
||||
|
||||
async def start_chat(self):
|
||||
pass
|
||||
|
||||
async def close(self):
|
||||
response = stop_results.pop(0)
|
||||
if response != "chatStopped":
|
||||
raise ChatCommandError("error stopping chat", {"type": response})
|
||||
closed[0] = True
|
||||
|
||||
async def api_get_active_user(self):
|
||||
return {"userId": 1, "profile": {"displayName": "x"}}
|
||||
|
||||
async def send_chat_cmd(self, _cmd):
|
||||
return {"type": "cmdOk"}
|
||||
|
||||
monkeypatch.setattr(client_mod, "ChatApi", _FailingStopApi)
|
||||
|
||||
c = Client(profile=Profile(display_name="x"), db=SqliteDb(file_prefix="/tmp/test"))
|
||||
|
||||
async def go():
|
||||
with pytest.raises(ChatCommandError, match="error stopping chat"):
|
||||
async with c:
|
||||
pass
|
||||
assert c._api is not None, "controller dropped while its store is still open"
|
||||
assert closed == [False]
|
||||
await c.__aexit__(None, None, None)
|
||||
assert closed == [True]
|
||||
assert c._api is None
|
||||
|
||||
asyncio.run(go())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("queue_size", [None, 65536])
|
||||
def test_bot_passes_queue_size_to_chat_api_init(monkeypatch, queue_size):
|
||||
import simplex_chat.client as client_mod
|
||||
|
||||
init_args: list[tuple[Any, ...]] = []
|
||||
|
||||
class StopInit(RuntimeError):
|
||||
pass
|
||||
|
||||
class FakeChatApi:
|
||||
@classmethod
|
||||
async def init(cls, *args):
|
||||
init_args.append(args)
|
||||
raise StopInit
|
||||
|
||||
monkeypatch.setattr(client_mod, "ChatApi", FakeChatApi)
|
||||
db = SqliteDb(file_prefix="/tmp/test")
|
||||
bot = Bot(profile=BotProfile(display_name="x"), db=db, queue_size=queue_size)
|
||||
|
||||
async def go():
|
||||
with pytest.raises(StopInit):
|
||||
async with bot:
|
||||
pytest.fail("should not enter the with-block")
|
||||
|
||||
asyncio.run(go())
|
||||
assert init_args == [(db, MigrationConfirmation.YES_UP, queue_size)]
|
||||
|
||||
|
||||
def test_aenter_rolls_back_partial_init_on_post_start_failure(monkeypatch):
|
||||
"""If anything in __aenter__ raises after ChatApi.init succeeded — including
|
||||
_post_start — the controller must be closed. Otherwise the with-block isn't
|
||||
@@ -550,7 +658,7 @@ def test_aenter_rolls_back_partial_init_on_post_start_failure(monkeypatch):
|
||||
pytest.fail("should not enter the with-block")
|
||||
|
||||
asyncio.run(go())
|
||||
assert closed == ["stop", "close"], f"controller not cleaned up: {closed}"
|
||||
assert closed == ["close"], f"controller not cleaned up: {closed}"
|
||||
assert c._api is None, "Client._api should be reset to None after rollback"
|
||||
|
||||
|
||||
|
||||
@@ -39,3 +39,9 @@ def test_chat_ref_cmd_string_direct():
|
||||
"""Sanity check the codegen fix for ChatRef-bearing commands."""
|
||||
assert T.ChatRef_cmd_string({"chatType": "direct", "chatId": 7}) == "@7"
|
||||
assert T.ChatRef_cmd_string({"chatType": "group", "chatId": 42}) == "#42"
|
||||
|
||||
|
||||
def test_api_connect_cmd_string_renders_incognito():
|
||||
link = {"connFullLink": "L"}
|
||||
assert CC.APIConnect_cmd_string({"userId": 1, "incognito": True, "preparedLink_": link}) == "/_connect 1 incognito=on L"
|
||||
assert CC.APIConnect_cmd_string({"userId": 1, "incognito": False, "preparedLink_": link}) == "/_connect 1 L"
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""core.chat_migrate_init picks the FFI export by queue_size, with a fake libsimplex."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from simplex_chat import core
|
||||
from simplex_chat.core import ChatInitError, MigrationConfirmation
|
||||
|
||||
CTRL = 42
|
||||
|
||||
|
||||
class FakeLib:
|
||||
"""Records calls; each export writes CTRL to the out-param and returns the JSON result."""
|
||||
|
||||
def __init__(self, result: dict[str, Any]) -> None:
|
||||
self.result = json.dumps(result)
|
||||
self.calls: list[tuple[str, tuple[Any, ...]]] = []
|
||||
|
||||
def _init(self, name: str, args: tuple[Any, ...]) -> str:
|
||||
*call_args, ctrl_ref = args
|
||||
self.calls.append((name, tuple(call_args)))
|
||||
ctrl_ref._obj.value = CTRL
|
||||
return self.result
|
||||
|
||||
def chat_migrate_init(self, *args: Any) -> str:
|
||||
return self._init("chat_migrate_init", args)
|
||||
|
||||
@property
|
||||
def chat_migrate_init_queue(self) -> Any:
|
||||
lib = self
|
||||
|
||||
class Fn:
|
||||
argtypes: Any = None
|
||||
restype: Any = None
|
||||
|
||||
def __call__(self, *args: Any) -> str:
|
||||
return lib._init("chat_migrate_init_queue", args)
|
||||
|
||||
return Fn()
|
||||
|
||||
|
||||
class OldLib(FakeLib):
|
||||
"""A libsimplex released before chat_migrate_init_queue existed."""
|
||||
|
||||
def __getattribute__(self, name: str) -> Any:
|
||||
if name == "chat_migrate_init_queue":
|
||||
raise AttributeError(name)
|
||||
return super().__getattribute__(name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_lib(monkeypatch: pytest.MonkeyPatch):
|
||||
def install(result: dict[str, Any]) -> FakeLib:
|
||||
lib = FakeLib(result)
|
||||
monkeypatch.setattr(core._native, "lib", lambda: lib)
|
||||
monkeypatch.setattr(core, "_read_and_free", lambda ptr: ptr)
|
||||
return lib
|
||||
|
||||
return install
|
||||
|
||||
|
||||
def migrate_init(queue_size: int | None = None) -> int:
|
||||
return asyncio.run(
|
||||
core.chat_migrate_init("/tmp/db", "key", MigrationConfirmation.YES_UP, queue_size)
|
||||
)
|
||||
|
||||
|
||||
def test_without_queue_size_uses_chat_migrate_init(fake_lib):
|
||||
lib = fake_lib({"type": "ok"})
|
||||
assert migrate_init() == CTRL
|
||||
assert lib.calls == [("chat_migrate_init", (b"/tmp/db", b"key", b"yesUp"))]
|
||||
|
||||
|
||||
def test_with_queue_size_uses_chat_migrate_init_queue(fake_lib):
|
||||
lib = fake_lib({"type": "ok"})
|
||||
assert migrate_init(65536) == CTRL
|
||||
assert lib.calls == [("chat_migrate_init_queue", (b"/tmp/db", b"key", b"yesUp", 65536))]
|
||||
|
||||
|
||||
def test_invalid_queue_size_result_raises_init_error(fake_lib):
|
||||
fake_lib({"type": "invalidQueueSize"})
|
||||
with pytest.raises(ChatInitError) as e:
|
||||
migrate_init(0)
|
||||
assert e.value.db_migration_error == {"type": "invalidQueueSize"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("queue_size", [2**31, -(2**31) - 1])
|
||||
def test_queue_size_outside_c_int_is_rejected_before_ffi(fake_lib, queue_size):
|
||||
lib = fake_lib({"type": "ok"})
|
||||
with pytest.raises(ValueError, match="does not fit C int"):
|
||||
migrate_init(queue_size)
|
||||
assert lib.calls == []
|
||||
|
||||
|
||||
def test_queue_size_on_old_lib_raises_clear_error(monkeypatch):
|
||||
lib = OldLib({"type": "ok"})
|
||||
monkeypatch.setattr(core._native, "lib", lambda: lib)
|
||||
with pytest.raises(RuntimeError, match="does not export chat_migrate_init_queue"):
|
||||
migrate_init(65536)
|
||||
assert lib.calls == []
|
||||
|
||||
|
||||
def test_setup_signatures_accepts_old_lib():
|
||||
class Fn:
|
||||
argtypes: Any = None
|
||||
restype: Any = None
|
||||
|
||||
class Lib:
|
||||
def __getattr__(self, name: str) -> Fn:
|
||||
if name == "chat_migrate_init_queue":
|
||||
raise AttributeError(name)
|
||||
fn = Fn()
|
||||
setattr(self, name, fn)
|
||||
return fn
|
||||
|
||||
from simplex_chat import _native
|
||||
|
||||
_native._setup_signatures(Lib()) # type: ignore[arg-type]
|
||||
@@ -91,3 +91,13 @@ def test_atomic_install(tmp_path, monkeypatch):
|
||||
_download(target, "sqlite")
|
||||
assert (target / "libsimplex.so").read_text() == "fake-so"
|
||||
assert (target / "libHS-stub.so").read_text() == "fake-hs"
|
||||
|
||||
|
||||
def test_libc_on_windows_is_ucrt(monkeypatch):
|
||||
loaded: list[str | None] = []
|
||||
monkeypatch.setattr("sys.platform", "win32")
|
||||
monkeypatch.setattr("ctypes.CDLL", lambda name: loaded.append(name))
|
||||
from simplex_chat import _native
|
||||
|
||||
_native._load_libc()
|
||||
assert loaded == ["ucrtbase"]
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
"""ChatApi receives run on a dedicated per-instance thread, not the default pool.
|
||||
|
||||
Uses a fake libsimplex (see tests/test_core_migrate_init.py for the pattern):
|
||||
`core._native.lib` and `core._read_and_free` are monkeypatched so `chat_recv_msg_wait`
|
||||
sleeps for a controlled time and returns a scripted result.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from simplex_chat import ChatApi, ChatCommandError
|
||||
|
||||
RECV_SLEEP = 0.3
|
||||
|
||||
|
||||
class FakeRecvLib:
|
||||
"""Fake chat_recv_msg_wait: blocks for `sleep` seconds, then returns a scripted result.
|
||||
|
||||
`events` records "stop" / "recv_start" / "recv_end" / "close_store" in call order, across
|
||||
all ChatApi instances sharing this fake, so tests can assert ordering between stop, receive
|
||||
and store-close calls (not just that they happened).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sleep: float = RECV_SLEEP,
|
||||
results: list[str] | None = None,
|
||||
stop_response: str = "chatStopped",
|
||||
) -> None:
|
||||
self.sleep = sleep
|
||||
self._results = iter(results or [])
|
||||
self._stop_response = stop_response
|
||||
self.calls: list[tuple[int, int]] = [] # (ctrl, thread ident)
|
||||
self.events: list[str] = []
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def chat_recv_msg_wait(self, ctrl: int, wait_us: int) -> str:
|
||||
with self._lock:
|
||||
self.events.append("recv_start")
|
||||
time.sleep(self.sleep)
|
||||
with self._lock:
|
||||
self.events.append("recv_end")
|
||||
self.calls.append((ctrl, threading.get_ident()))
|
||||
return next(self._results, "")
|
||||
|
||||
def chat_send_cmd(self, ctrl: int, cmd: bytes) -> str:
|
||||
assert cmd == b"/_stop", f"unexpected command {cmd!r}"
|
||||
with self._lock:
|
||||
self.events.append("stop")
|
||||
return json.dumps({"result": {"type": self._stop_response}})
|
||||
|
||||
def chat_close_store(self, ctrl: int) -> str:
|
||||
with self._lock:
|
||||
self.events.append("close_store")
|
||||
return ""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_lib(monkeypatch: pytest.MonkeyPatch):
|
||||
def install(
|
||||
sleep: float = RECV_SLEEP,
|
||||
results: list[str] | None = None,
|
||||
stop_response: str = "chatStopped",
|
||||
) -> FakeRecvLib:
|
||||
lib = FakeRecvLib(sleep=sleep, results=results, stop_response=stop_response)
|
||||
monkeypatch.setattr("simplex_chat.core._native.lib", lambda: lib)
|
||||
monkeypatch.setattr("simplex_chat.core._read_and_free", lambda ptr: ptr)
|
||||
return lib
|
||||
|
||||
return install
|
||||
|
||||
|
||||
def _recv_thread_names() -> list[str]:
|
||||
return [t.name for t in threading.enumerate() if t.name.startswith("simplex-recv")]
|
||||
|
||||
|
||||
async def test_receives_do_not_use_the_default_executor(fake_lib):
|
||||
fake_lib(sleep=RECV_SLEEP)
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.set_default_executor(ThreadPoolExecutor(max_workers=1))
|
||||
|
||||
apis = [ChatApi(ctrl=i) for i in range(3)]
|
||||
recv_tasks = [asyncio.create_task(api.recv_chat_event()) for api in apis]
|
||||
try:
|
||||
await asyncio.sleep(0.05) # let all three receives claim their own thread
|
||||
|
||||
start = time.monotonic()
|
||||
await asyncio.to_thread(lambda: None)
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
await asyncio.gather(*recv_tasks)
|
||||
finally:
|
||||
for api in apis:
|
||||
await api.close()
|
||||
|
||||
assert elapsed < 0.1
|
||||
|
||||
|
||||
async def test_one_receive_thread_per_chatapi_reused(fake_lib):
|
||||
lib = fake_lib(sleep=0.02)
|
||||
api = ChatApi(ctrl=1)
|
||||
other_api = ChatApi(ctrl=2)
|
||||
try:
|
||||
for _ in range(3):
|
||||
await api.recv_chat_event()
|
||||
|
||||
idents = {ident for ctrl, ident in lib.calls if ctrl == 1}
|
||||
assert len(idents) == 1
|
||||
recv_ident = idents.pop()
|
||||
thread = next(t for t in threading.enumerate() if t.ident == recv_ident)
|
||||
assert thread.name.startswith("simplex-recv")
|
||||
assert thread.ident != threading.get_ident()
|
||||
|
||||
await other_api.recv_chat_event()
|
||||
other_idents = {ident for ctrl, ident in lib.calls if ctrl == 2}
|
||||
assert other_idents and other_idents != {thread.ident}
|
||||
finally:
|
||||
await api.close()
|
||||
await other_api.close()
|
||||
|
||||
|
||||
def test_no_thread_until_first_receive():
|
||||
# A bare ThreadPoolExecutor spawns no worker thread until the first submit,
|
||||
# so the real assertion is the attribute itself, not threading.enumerate().
|
||||
api = ChatApi(ctrl=1)
|
||||
assert api._recv_executor is None
|
||||
|
||||
|
||||
async def test_close_shuts_down_the_executor_without_blocking_the_loop(fake_lib):
|
||||
fake_lib(sleep=RECV_SLEEP)
|
||||
api = ChatApi(ctrl=1)
|
||||
recv_task = asyncio.create_task(api.recv_chat_event())
|
||||
await asyncio.sleep(0.05) # let the receive claim its executor thread
|
||||
assert _recv_thread_names() != []
|
||||
|
||||
sleep_task = asyncio.create_task(asyncio.sleep(0.01))
|
||||
close_task = asyncio.create_task(api.close())
|
||||
|
||||
await asyncio.wait_for(sleep_task, timeout=0.2)
|
||||
assert not close_task.done() # shutdown still waiting on the in-flight receive
|
||||
|
||||
await close_task
|
||||
await recv_task
|
||||
|
||||
assert _recv_thread_names() == []
|
||||
|
||||
|
||||
async def test_close_shuts_down_executor_before_closing_the_store(fake_lib):
|
||||
lib = fake_lib(sleep=RECV_SLEEP)
|
||||
api = ChatApi(ctrl=1)
|
||||
recv_task = asyncio.create_task(api.recv_chat_event())
|
||||
try:
|
||||
await asyncio.sleep(0.05) # ensure the receive is in flight before close() starts
|
||||
await api.close()
|
||||
await recv_task
|
||||
finally:
|
||||
if not recv_task.done():
|
||||
recv_task.cancel()
|
||||
|
||||
# recv_end (executor drained) must precede close_store: a receive must never
|
||||
# be in flight while the store closes underneath it.
|
||||
assert lib.events == ["recv_start", "stop", "recv_end", "close_store"]
|
||||
|
||||
|
||||
async def test_close_stops_the_chat_before_closing_the_store(fake_lib):
|
||||
lib = fake_lib()
|
||||
api = ChatApi(ctrl=1)
|
||||
await api.close()
|
||||
assert lib.events == ["stop", "close_store"]
|
||||
assert not api.initialized
|
||||
|
||||
|
||||
async def test_close_does_not_close_the_store_when_stop_fails(fake_lib):
|
||||
lib = fake_lib(stop_response="chatCmdError")
|
||||
api = ChatApi(ctrl=1)
|
||||
with pytest.raises(ChatCommandError, match="error stopping chat"):
|
||||
await api.close()
|
||||
assert lib.events == ["stop"]
|
||||
assert api.initialized
|
||||
|
||||
|
||||
async def test_recv_chat_event_after_close_raises_before_touching_executor(fake_lib):
|
||||
fake_lib()
|
||||
api = ChatApi(ctrl=1)
|
||||
await api.close()
|
||||
with pytest.raises(RuntimeError, match="controller not initialized"):
|
||||
await api.recv_chat_event()
|
||||
assert api._recv_executor is None
|
||||
|
||||
|
||||
async def test_receive_parses_event_json_and_none_on_timeout(fake_lib):
|
||||
event: dict[str, Any] = {"type": "chatItemUpdated", "chatItem": {}}
|
||||
fake_lib(sleep=0.01, results=[json.dumps({"result": event}), ""])
|
||||
api = ChatApi(ctrl=1)
|
||||
try:
|
||||
assert await api.recv_chat_event() == event
|
||||
assert await api.recv_chat_event() is None
|
||||
finally:
|
||||
await api.close()
|
||||
@@ -167,6 +167,11 @@ def test_ci_bot_command_no_text():
|
||||
assert util.ci_bot_command(ci) is None
|
||||
|
||||
|
||||
def test_ci_bot_command_multiline_params():
|
||||
ci = {"content": {"type": "rcvMsgContent", "msgContent": {"type": "text", "text": "/review line1\nline2"}}}
|
||||
assert util.ci_bot_command(ci) == ("review", "line1\nline2")
|
||||
|
||||
|
||||
def test_reaction_text_emoji():
|
||||
r = {"chatReaction": {"reaction": {"type": "emoji", "emoji": "🎉"}}}
|
||||
assert util.reaction_text(r) == "🎉"
|
||||
|
||||
@@ -12,6 +12,7 @@ module Simplex.Chat.Mobile where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (SomeException, catch)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import Data.Aeson (ToJSON (..))
|
||||
@@ -35,6 +36,7 @@ import Foreign.Ptr
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Storable (poke)
|
||||
import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEncoding)
|
||||
import Numeric.Natural (Natural)
|
||||
import Simplex.Chat
|
||||
import Simplex.Chat.Badges.Code (badgeCodeText, parseBadgeCode)
|
||||
import Simplex.Chat.Controller
|
||||
@@ -73,6 +75,7 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
data DBMigrationResult
|
||||
= DBMOk
|
||||
| DBMInvalidConfirmation
|
||||
| DBMInvalidQueueSize
|
||||
| DBMErrorNotADatabase {dbFile :: String}
|
||||
| DBMErrorMigration {dbFile :: String, migrationError :: MigrationError}
|
||||
| DBMErrorSQL {dbFile :: String, migrationSQLError :: String}
|
||||
@@ -113,6 +116,8 @@ foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString
|
||||
|
||||
foreign export ccall "chat_migrate_init_key" cChatMigrateInitKey :: CString -> CString -> CInt -> CString -> CInt -> Ptr (StablePtr ChatController) -> IO CJSONString
|
||||
|
||||
foreign export ccall "chat_migrate_init_queue" cChatMigrateInitQueue :: CString -> CString -> CString -> CInt -> Ptr (StablePtr ChatController) -> IO CJSONString
|
||||
|
||||
foreign export ccall "chat_close_store" cChatCloseStore :: StablePtr ChatController -> IO CString
|
||||
|
||||
foreign export ccall "chat_reopen_store" cChatReopenStore :: StablePtr ChatController -> IO CString
|
||||
@@ -166,7 +171,14 @@ cChatMigrateInit fp key conf = cChatMigrateInitKey fp key 0 conf 0
|
||||
|
||||
-- For postgres first param is schema prefix, second param is database connection string.
|
||||
cChatMigrateInitKey :: CString -> CString -> CInt -> CString -> CInt -> Ptr (StablePtr ChatController) -> IO CJSONString
|
||||
cChatMigrateInitKey fp key keepKey conf background ctrl = do
|
||||
cChatMigrateInitKey fp key keepKey conf background = cChatMigrateInit_ fp key (keepKey /= 0) conf (background /= 0) mobileQueueSize
|
||||
|
||||
-- | queueSize is the size of internal queues, same as terminal option --queue-size
|
||||
cChatMigrateInitQueue :: CString -> CString -> CString -> CInt -> Ptr (StablePtr ChatController) -> IO CJSONString
|
||||
cChatMigrateInitQueue fp key conf queueSize = cChatMigrateInit_ fp key False conf False (fromIntegral queueSize)
|
||||
|
||||
cChatMigrateInit_ :: CString -> CString -> Bool -> CString -> Bool -> Int -> Ptr (StablePtr ChatController) -> IO CJSONString
|
||||
cChatMigrateInit_ fp key keepKey conf background queueSize ctrl = do
|
||||
-- ensure we are set to UTF-8; iOS does not have locale, and will default to
|
||||
-- US-ASCII all the time.
|
||||
setLocaleEncoding utf8
|
||||
@@ -176,7 +188,7 @@ cChatMigrateInitKey fp key keepKey conf background ctrl = do
|
||||
chatDbOpts <- mobileDbOpts fp key
|
||||
confirm <- peekCAString conf
|
||||
r <-
|
||||
chatMigrateInitKey chatDbOpts (keepKey /= 0) confirm (background /= 0) >>= \case
|
||||
chatMigrateInitKey chatDbOpts keepKey confirm background queueSize >>= \case
|
||||
Right cc -> (newStablePtr cc >>= poke ctrl) $> DBMOk
|
||||
Left e -> pure e
|
||||
newCStringFromLazyBS $ J.encode r
|
||||
@@ -254,8 +266,11 @@ cChatParseBadgeCode cCode = do
|
||||
cChatJsonLength :: CString -> IO CInt
|
||||
cChatJsonLength s = fromIntegral . subtract 2 . LB.length . J.encode . safeDecodeUtf8 <$> B.packCString s
|
||||
|
||||
mobileChatOpts :: ChatDbOpts -> ChatOpts
|
||||
mobileChatOpts dbOptions =
|
||||
mobileQueueSize :: Int
|
||||
mobileQueueSize = 4096
|
||||
|
||||
mobileChatOpts :: ChatDbOpts -> Natural -> ChatOpts
|
||||
mobileChatOpts dbOptions tbqSize =
|
||||
ChatOpts
|
||||
{ coreOptions =
|
||||
CoreChatOpts
|
||||
@@ -268,7 +283,7 @@ mobileChatOpts dbOptions =
|
||||
logServerHosts = True,
|
||||
logAgent = Nothing,
|
||||
logFile = Nothing,
|
||||
tbqSize = 4096,
|
||||
tbqSize,
|
||||
maxChats = 5000,
|
||||
deviceName = Nothing,
|
||||
chatRelay = False,
|
||||
@@ -313,18 +328,19 @@ getActiveUser_ st = find activeUser <$> withTransaction st getUsers
|
||||
chatMigrateInit :: String -> ScrubbedBytes -> String -> IO (Either DBMigrationResult ChatController)
|
||||
chatMigrateInit dbFilePrefix dbKey confirm = do
|
||||
let chatDBOpts = ChatDbOpts {dbFilePrefix, dbKey, trackQueries = DB.TQSlow 5000, vacuumOnMigration = True}
|
||||
chatMigrateInitKey chatDBOpts False confirm False
|
||||
chatMigrateInitKey chatDBOpts False confirm False mobileQueueSize
|
||||
#endif
|
||||
|
||||
chatMigrateInitKey :: ChatDbOpts -> Bool -> String -> Bool -> IO (Either DBMigrationResult ChatController)
|
||||
chatMigrateInitKey chatDbOpts keepKey confirm backgroundMode = runExceptT $ do
|
||||
chatMigrateInitKey :: ChatDbOpts -> Bool -> String -> Bool -> Int -> IO (Either DBMigrationResult ChatController)
|
||||
chatMigrateInitKey chatDbOpts keepKey confirm backgroundMode queueSize = runExceptT $ do
|
||||
unless (queueSize > 0) $ throwError DBMInvalidQueueSize
|
||||
confirmMigrations <- liftEitherWith (const DBMInvalidConfirmation) $ strDecode $ B.pack confirm
|
||||
let migrationConfig = MigrationConfig confirmMigrations (Just "")
|
||||
chatStore <- migrate createChatStore (toDBOpts chatDbOpts chatSuffix keepKey chatDBFunctions) migrationConfig
|
||||
agentStore <- migrate createAgentStore (toDBOpts chatDbOpts agentSuffix keepKey []) migrationConfig
|
||||
ExceptT $ initialize chatStore ChatDatabase {chatStore, agentStore}
|
||||
where
|
||||
opts = mobileChatOpts $ removeDbKey chatDbOpts
|
||||
opts = mobileChatOpts (removeDbKey chatDbOpts) (fromIntegral queueSize)
|
||||
initialize st db = do
|
||||
user_ <- liftIO $ getActiveUser_ st
|
||||
first DBMAgentError <$> newChatController db user_ defaultMobileConfig opts backgroundMode
|
||||
|
||||
+21
-2
@@ -25,7 +25,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Word (Word8, Word32)
|
||||
import Foreign.C
|
||||
import Foreign.Marshal.Alloc (mallocBytes)
|
||||
import Foreign.Marshal.Alloc (alloca, mallocBytes)
|
||||
import Foreign.Marshal.Utils (copyBytes)
|
||||
import Foreign.Ptr
|
||||
import Foreign.StablePtr
|
||||
@@ -34,7 +34,7 @@ import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEnco
|
||||
import JSONFixtures
|
||||
import Simplex.Chat
|
||||
import Simplex.Chat.Badges (BadgeInfo (..), BadgeRequest (..), BadgeType (..), generateMasterKey, verifyCredential)
|
||||
import Simplex.Chat.Controller (ChatController (..), ChatDatabase (..))
|
||||
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatDatabase (..))
|
||||
import Simplex.Chat.Mobile hiding (error)
|
||||
import Simplex.Chat.Mobile.Badges hiding (error)
|
||||
import Simplex.Chat.Mobile.File
|
||||
@@ -44,6 +44,8 @@ import Simplex.Chat.Options.DB
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Store.Profiles
|
||||
import Simplex.Chat.Types (AgentUserId (..), Profile (..))
|
||||
import Simplex.Messaging.Agent.Client (AgentClient (..))
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Env (..))
|
||||
import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -65,6 +67,7 @@ mobileTests = do
|
||||
setForeignEncoding utf8
|
||||
it "start new chat without user" testChatApiNoUser
|
||||
it "start new chat with existing user" testChatApi
|
||||
it "should set queue size via C API" testChatMigrateInitQueueCApi
|
||||
it "should encrypt/decrypt WebRTC frames" testMediaApi
|
||||
it "should encrypt/decrypt WebRTC frames via C API" testMediaCApi
|
||||
describe "should read/write encrypted files via C API" $ do
|
||||
@@ -165,6 +168,22 @@ testChatApi ps = do
|
||||
chatParseMarkdown "hello" `shouldBe` "{}"
|
||||
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown
|
||||
|
||||
testChatMigrateInitQueueCApi :: TestParams -> IO ()
|
||||
testChatMigrateInitQueueCApi ps = do
|
||||
cPath <- newCString $ tmpPath ps </> "1"
|
||||
cKey <- newCString ""
|
||||
cConfirm <- newCString "yesUp"
|
||||
alloca $ \ctrlPtr -> do
|
||||
let migrateInit queueSize = peekCAString =<< cChatMigrateInitQueue cPath cKey cConfirm queueSize ctrlPtr
|
||||
migrateInit 0 `shouldReturn` jsonStr DBMInvalidQueueSize
|
||||
migrateInit (-1) `shouldReturn` jsonStr DBMInvalidQueueSize
|
||||
migrateInit 65536 `shouldReturn` jsonStr DBMOk
|
||||
ChatController {config = ChatConfig {tbqSize}, smpAgent = AgentClient {agentEnv = Env {config = AgentConfig {tbqSize = agentQSize}}}} <- deRefStablePtr =<< peek ctrlPtr
|
||||
tbqSize `shouldBe` 65536
|
||||
agentQSize `shouldBe` 65536
|
||||
where
|
||||
jsonStr = LB.unpack . J.encode
|
||||
|
||||
testMediaApi :: HasCallStack => TestParams -> IO ()
|
||||
testMediaApi ps = do
|
||||
let tmp = tmpPath ps
|
||||
|
||||
Reference in New Issue
Block a user