diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fc6eee7..52d63c0 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,6 +15,7 @@ concurrency:
jobs:
build:
runs-on: ${{ matrix.os }}
+
strategy:
fail-fast: false
matrix:
@@ -49,6 +50,13 @@ jobs:
run: |
yarn global add node-gyp@latest
+ # - name: sed it
+ # if: runner.os == 'Windows'
+ # shell: bash
+ # run: |
+ # sed -i "s/target_compile_options(oxen-logging-warnings INTERFACE/#target_compile_options(oxen-logging-warnings INTERFACE/" libsession-util/external/oxen-libquic/external/oxen-logging/CMakeLists.txt
+ # cat libsession-util/external/oxen-libquic/external/oxen-logging/CMakeLists.txt
+
- name: build libsession-util-nodejs
shell: bash
- run: yarn install --frozen-lockfile --network-timeout 600000
+ run: yarn install --frozen-lockfile
diff --git a/.gitignore b/.gitignore
index 0061e0c..31dd01f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,7 @@
/package-lock.json
/compile_commands.json
/.cache
+.yarn/
+*.cjs
+*.mjs
+
diff --git a/.gitmodules b/.gitmodules
index 8720b3c..d6ca3b1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "libsession-util"]
path = libsession-util
- url = https://github.com/oxen-io/libsession-util.git
+ url = https://github.com/session-foundation/libsession-util.git
diff --git a/.yarnrc b/.yarnrc
deleted file mode 100644
index b162bd8..0000000
--- a/.yarnrc
+++ /dev/null
@@ -1 +0,0 @@
---install.frozen-lockfile true
diff --git a/.yarnrc.yml b/.yarnrc.yml
new file mode 100644
index 0000000..5fdaec4
--- /dev/null
+++ b/.yarnrc.yml
@@ -0,0 +1,3 @@
+nodeLinker: node-modules
+
+patchFolder: patches
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2f885bc..0f540ec 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -6,6 +6,19 @@ set(CMAKE_POLICY_DEFAULT_CMP0069 NEW)
set(VERBOSE ON)
+# Detect the number of processors
+include(ProcessorCount)
+ProcessorCount(N)
+
+# Set a default value in case the detection fails
+if(NOT N EQUAL 0)
+ set(CMAKE_BUILD_PARALLEL_LEVEL ${N})
+else()
+ set(CMAKE_BUILD_PARALLEL_LEVEL 4) # Fallback to 16 if detection fails
+endif()
+message(STATUS "Number of processors detected: ${N}")
+
+
add_definitions(-DNAPI_VERSION=8)
set(CMAKE_CONFIGURATION_TYPES Release)
@@ -15,11 +28,15 @@ SET(CMAKE_EXPORT_COMPILE_COMMANDS ON)
SET(CMAKE_BUILD_TYPE Release)
SET(WITH_TESTS OFF)
-set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
-file(GLOB SOURCE_FILES src/*.cpp)
+# when building from a release of libsession on desktop, it complains that ios-cmake is not up to date
+# as it is not part of the archive. We actually don't care about it on session-desktop
+set(SUBMODULE_CHECK OFF)
+
+file(GLOB SOURCE_FILES src/*.cpp src/groups/*.cpp src/multi_encrypt/*.cpp)
add_subdirectory(libsession-util)
@@ -33,7 +50,7 @@ add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC} "node_modules/node-addon-api" "../../node_modules/node-addon-api" "node_modules/node-api-headers/include" "../../node_modules/node-api-headers/include")
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
-target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_LIB} libsession::config libsession::crypto)
+target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_LIB} ${LIBSESSION_STATIC_BUNDLE_LIBS})
if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)
# Generate node.lib
diff --git a/README.md b/README.md
index d57b09f..e45c109 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Clone this project to somewhere **not** part of `session-desktop` node_modules:
```
cd [FOLDER_NOT_IN_SESSION_DESKTOP]
-git clone --recursive git@github.com:oxen-io/libsession-util-nodejs.git
+git clone --recursive git@github.com:session-foundation/libsession-util-nodejs.git
```
Always do your changes in `[FOLDER_NOT_IN_SESSION_DESKTOP]/libsession-util-nodejs`, never in the one under session-desktop's `node_modules` as you might override your local changes.
@@ -31,7 +31,7 @@ Replace `[SESSION_DESKTOP_PATH]` with the full path to your `session-desktop` fo
Every part of this command is needed and might need to be updated using your paths. Also, the `worker:libsession` needs to be recompiled too to include the just created .node file in itself. This is done by the `yarn build:workers` command.
-Note: The `electron` property in the `config` object will need to be updated in the `package.json` every time we update `electron` package in [session-desktop](https://github.com/oxen-io/session-desktop/) so that the versions match. It is a node version, but not part of the official node docs. If you compiled the node module for an incorrect electron/node version you will get an error on `session-desktop` start.
+Note: The `electron` property in the `config` object will need to be updated in the `package.json` every time we update `electron` package in [session-desktop](https://github.com/session-foundation/session-desktop/) so that the versions match. It is a node version, but not part of the official node docs. If you compiled the node module for an incorrect electron/node version you will get an error on `session-desktop` start.
### Making a Release and updating Session-desktop
@@ -71,7 +71,7 @@ Once this is done, update the dependency on `session-desktop`.
Make sure to remove the existing one first (with the include `yarn remove` below) as you might have messed up your `node_modules` doing the dev instructions.
```
-yarn remove libsession_util_nodejs && yarn add https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz
+yarn remove libsession_util_nodejs && yarn add https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz
```
Keep in mind that you need to update the two version numbers (e.g. `0.1.15`) to the just created release version of this project.
diff --git a/index.d.ts b/index.d.ts
index 3b670f4..60dddc8 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,5 +1,4 @@
-///
-///
+
///
declare module 'libsession_util_nodejs' {
@@ -10,7 +9,7 @@ declare module 'libsession_util_nodejs' {
* - one side are calls made by the webworker directly to the wrapper
* - the other side are calls made by the renderer to the webworker (which should forward them to the wrapper)
*
- * We cannot pass unserializable data between those two, so we need to have a serializable way of calling one
+ * We cannot pass non serializable data between those two, so we need to have a serializable way of calling one
* method of a wrapper with the required arguments.
* Those serializable data, are `UserConfigActionsType` or just any of the `*ActionsType`. They are defined with a tuple of what each methods accepts on which wrapper with which argument.
*
diff --git a/libsession-util b/libsession-util
index 0193c36..c29c934 160000
--- a/libsession-util
+++ b/libsession-util
@@ -1 +1 @@
-Subproject commit 0193c36e0dad461385d6407a00f33b7314e6d740
+Subproject commit c29c93457eb6abfdb9e13af378cc67a2dc68115d
diff --git a/package.json b/package.json
index 200ab01..9c3b46f 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"main": "index.js",
"name": "libsession_util_nodejs",
"description": "Wrappers for the Session Util Library",
- "version": "0.3.23",
+ "version": "0.4.4",
"license": "GPL-3.0",
"author": {
"name": "Oxen Project",
@@ -10,15 +10,16 @@
},
"scripts": {
"clean": "rimraf .cache build",
- "install": "cmake-js compile --runtime=electron --runtime-version=25.8.4 -p16 --CDSUBMODULE_CHECK=OFF --CDLOCAL_MIRROR=https://oxen.rocks/deps --CDENABLE_ONIONREQ=OFF"
+ "install": "cmake-js compile --runtime=electron --runtime-version=25.8.4 --CDSUBMODULE_CHECK=OFF --CDLOCAL_MIRROR=https://oxen.rocks/deps --CDENABLE_ONIONREQ=OFF --CDWITH_TESTS=OFF"
},
"devDependencies": {
"clang-format": "^1.8.0",
"rimraf": "2.6.2"
},
"dependencies": {
- "cmake-js": "^7.2.1",
+ "cmake-js": "7.2.1",
"node-addon-api": "^6.1.0"
},
- "typings": "index.d.ts"
-}
\ No newline at end of file
+ "typings": "index.d.ts",
+ "packageManager": "yarn@1.22.19"
+}
diff --git a/shared.d.ts b/shared.d.ts
deleted file mode 100644
index ef47215..0000000
--- a/shared.d.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-declare module 'libsession_util_nodejs' {
- /**
- *
- * Utilities
- *
- */
-
- type AsyncWrapper any> = (
- ...args: Parameters
- ) => Promise>;
-
- export type RecordOfFunctions = Record any>;
-
- type MakeWrapperActionCalls = {
- [Property in keyof Type]: AsyncWrapper;
- };
-
- export type ProfilePicture = {
- url: string | null;
- key: Uint8Array | null;
- };
-
- export type PushConfigResult = { data: Uint8Array; seqno: number; hashes: Array };
- export type MergeSingle = { hash: string; data: Uint8Array };
-
- type MakeActionCall = [B, ...Parameters];
-
- /**
- *
- * Base Config wrapper logic
- *
- */
-
- type BaseConfigWrapper = {
- needsDump: () => boolean;
- needsPush: () => boolean;
- push: () => PushConfigResult;
- dump: () => Uint8Array;
- confirmPushed: (seqno: number, hash: string) => void;
- merge: (toMerge: Array) => Array; // merge returns the array of hashes that merged correctly
- storageNamespace: () => number;
- currentHashes: () => Array;
- };
-
- export type BaseConfigActions =
- | MakeActionCall
- | MakeActionCall
- | MakeActionCall
- | MakeActionCall
- | MakeActionCall
- | MakeActionCall
- | MakeActionCall
- | MakeActionCall;
-
- export abstract class BaseConfigWrapperNode {
- public needsDump: BaseConfigWrapper['needsDump'];
- public needsPush: BaseConfigWrapper['needsPush'];
- public push: BaseConfigWrapper['push'];
- public dump: BaseConfigWrapper['dump'];
- public confirmPushed: BaseConfigWrapper['confirmPushed'];
- public merge: BaseConfigWrapper['merge'];
- public storageNamespace: BaseConfigWrapper['storageNamespace'];
- public currentHashes: BaseConfigWrapper['currentHashes'];
- }
-
- export type BaseWrapperActionsCalls = MakeWrapperActionCalls;
-
- export type ConstantsType = {
- /** 100 bytes */
- CONTACT_MAX_NAME_LENGTH: number;
- /** 100 bytes - for legacy groups and communities */
- BASE_GROUP_MAX_NAME_LENGTH: number;
- /** 100 bytes */
- GROUP_INFO_MAX_NAME_LENGTH: number;
- /** 411 bytes
- *
- * BASE_URL_MAX_LENGTH + '/r/' + ROOM_MAX_LENGTH + qs_pubkey.size() + hex pubkey + null terminator
- */
- COMMUNITY_FULL_URL_MAX_LENGTH: number;
- };
-
- export const CONSTANTS: ConstantsType;
-}
diff --git a/src/addon.cpp b/src/addon.cpp
index 776a304..e7fe92c 100644
--- a/src/addon.cpp
+++ b/src/addon.cpp
@@ -4,19 +4,26 @@
#include "constants.hpp"
#include "contacts_config.hpp"
#include "convo_info_volatile_config.hpp"
+#include "groups/meta_group_wrapper.hpp"
+#include "multi_encrypt/multi_encrypt.hpp"
#include "user_config.hpp"
#include "user_groups_config.hpp"
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
using namespace session::nodeapi;
-
ConstantsWrapper::Init(env, exports);
+
+ // Group wrappers init
+ MetaGroupWrapper::Init(env, exports);
+
+ // User wrappers init
UserConfigWrapper::Init(env, exports);
ContactsConfigWrapper::Init(env, exports);
UserGroupsWrapper::Init(env, exports);
ConvoInfoVolatileWrapper::Init(env, exports);
// Fully static wrappers init
+ MultiEncryptWrapper::Init(env, exports);
BlindingWrapper::Init(env, exports);
return exports;
diff --git a/src/base_config.cpp b/src/base_config.cpp
index ce4976e..57163f8 100644
--- a/src/base_config.cpp
+++ b/src/base_config.cpp
@@ -15,12 +15,6 @@ Napi::Value ConfigBaseImpl::needsPush(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] { return get_config().needs_push(); });
}
-Napi::Value ConfigBaseImpl::storageNamespace(const Napi::CallbackInfo& info) {
- return wrapResult(info, [&] {
- return static_cast(get_config().storage_namespace());
- });
-}
-
Napi::Value ConfigBaseImpl::currentHashes(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] { return (get_config().current_hashes()); });
}
@@ -28,15 +22,10 @@ Napi::Value ConfigBaseImpl::currentHashes(const Napi::CallbackInfo& info) {
Napi::Value ConfigBaseImpl::push(const Napi::CallbackInfo& info) {
return wrapResult(info, [&]() {
assertInfoLength(info, 0);
- auto [seqno, to_push, hashes] = get_config().push();
+ auto& conf = get_config();
+ auto to_push = conf.push();
- auto env = info.Env();
- Napi::Object result = Napi::Object::New(env);
- result["data"] = toJs(env, to_push);
- result["seqno"] = toJs(env, seqno);
- result["hashes"] = toJs(env, hashes);
-
- return result;
+ return push_result_to_JS(info.Env(), to_push, conf.storage_namespace());
});
}
@@ -47,10 +36,17 @@ Napi::Value ConfigBaseImpl::dump(const Napi::CallbackInfo& info) {
});
}
+Napi::Value ConfigBaseImpl::makeDump(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&]() {
+ assertInfoLength(info, 0);
+ return get_config().make_dump();
+ });
+}
+
void ConfigBaseImpl::confirmPushed(const Napi::CallbackInfo& info) {
return wrapResult(info, [&]() {
assertInfoLength(info, 2);
- assertIsNumber(info[0]);
+ assertIsNumber(info[0], "confirmPushed");
assertIsString(info[1]);
get_config().confirm_pushed(
diff --git a/src/base_config.hpp b/src/base_config.hpp
index 4363520..d99250e 100644
--- a/src/base_config.hpp
+++ b/src/base_config.hpp
@@ -1,6 +1,7 @@
#pragma once
#include
+#include
#include
#include
@@ -11,6 +12,8 @@
#include "session/types.hpp"
#include "utilities.hpp"
+using ustring_view = std::basic_string_view;
+
namespace session::nodeapi {
class ConfigBaseImpl;
@@ -30,11 +33,11 @@ class ConfigBaseImpl {
// These are exposed as read-only accessors rather than methods:
Napi::Value needsDump(const Napi::CallbackInfo& info);
Napi::Value needsPush(const Napi::CallbackInfo& info);
- Napi::Value storageNamespace(const Napi::CallbackInfo& info);
Napi::Value currentHashes(const Napi::CallbackInfo& info);
Napi::Value push(const Napi::CallbackInfo& info);
Napi::Value dump(const Napi::CallbackInfo& info);
+ Napi::Value makeDump(const Napi::CallbackInfo& info);
void confirmPushed(const Napi::CallbackInfo& info);
Napi::Value merge(const Napi::CallbackInfo& info);
@@ -50,11 +53,10 @@ class ConfigBaseImpl {
properties.push_back(T::InstanceMethod("needsDump", &T::needsDump));
properties.push_back(T::InstanceMethod("needsPush", &T::needsPush));
- properties.push_back(T::InstanceMethod("storageNamespace", &T::storageNamespace));
properties.push_back(T::InstanceMethod("currentHashes", &T::currentHashes));
-
properties.push_back(T::InstanceMethod("push", &T::push));
properties.push_back(T::InstanceMethod("dump", &T::dump));
+ properties.push_back(T::InstanceMethod("makeDump", &T::makeDump));
properties.push_back(T::InstanceMethod("confirmPushed", &T::confirmPushed));
properties.push_back(T::InstanceMethod("merge", &T::merge));
@@ -90,7 +92,7 @@ class ConfigBaseImpl {
assertInfoLength(info, 2);
// we should get secret key as first arg and optional dumped as second argument
- assertIsUInt8Array(info[0]);
+ assertIsUInt8Array(info[0], "base construct");
assertIsUInt8ArrayOrNull(info[1]);
ustring_view secretKey = toCppBufferView(info[0], class_name + ".new");
@@ -104,18 +106,6 @@ class ConfigBaseImpl {
Napi::Env env = info.Env();
- config->logger = [env, class_name](session::config::LogLevel, std::string_view x) {
- std::string toLog =
- "libsession-util:" + std::string(class_name) + ": " + std::string(x) + "\n";
-
- Napi::Function consoleLog = env.Global()
- .Get("console")
- .As()
- .Get("log")
- .As();
- consoleLog.Call({Napi::String::New(env, toLog)});
- };
-
return config;
});
}
@@ -135,18 +125,6 @@ class ConfigBaseImpl {
"Error retrieving config: config instance is not of the requested type"};
}
- // Same as above, but return a shared ptr.
- template <
- typename T = config::ConfigBase,
- std::enable_if_t, int> = 0>
- std::shared_ptr config_ptr() {
- assert(conf_); // should not be possible to construct without this set
- if (auto t = std::dynamic_pointer_cast(conf_))
- return t;
- throw std::invalid_argument{
- "Error retrieving config: config instance is not of the requested type"};
- }
-
// Helper function for doing the subtype napi Init call. This sets up the class registration,
// sets it in the exports, and appends the base methods and properties (needsDump, etc.) to the
// given methods/properties list.
diff --git a/src/blinding/blinding.hpp b/src/blinding/blinding.hpp
index 88d4f62..d244696 100644
--- a/src/blinding/blinding.hpp
+++ b/src/blinding/blinding.hpp
@@ -19,7 +19,7 @@ class BlindingWrapper : public Napi::ObjectWrap {
public:
BlindingWrapper(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} {
throw std::invalid_argument(
- "BlindingWrapper is all static and don't need to be constructed");
+ "BlindingWrapper is static and doesn't need to be constructed");
}
static void Init(Napi::Env env, Napi::Object exports) {
@@ -50,7 +50,7 @@ class BlindingWrapper : public Napi::ObjectWrap {
if (obj.IsEmpty())
throw std::invalid_argument("blindVersionPubkey received empty");
- assertIsUInt8Array(obj.Get("ed25519SecretKey"));
+ assertIsUInt8Array(obj.Get("ed25519SecretKey"), "BlindingWrapper::blindVersionPubkey");
auto ed25519_secret_key =
toCppBuffer(obj.Get("ed25519SecretKey"), "blindVersionPubkey.ed25519SecretKey");
@@ -76,11 +76,11 @@ class BlindingWrapper : public Napi::ObjectWrap {
if (obj.IsEmpty())
throw std::invalid_argument("blindVersionSign received empty");
- assertIsUInt8Array(obj.Get("ed25519SecretKey"));
+ assertIsUInt8Array(obj.Get("ed25519SecretKey"), "BlindingWrapper::blindVersionSign");
auto ed25519_secret_key =
toCppBuffer(obj.Get("ed25519SecretKey"), "blindVersionSign.ed25519SecretKey");
- assertIsNumber(obj.Get("sigTimestampSeconds"));
+ assertIsNumber(obj.Get("sigTimestampSeconds"), "BlindingWrapper::blindVersionSign");
auto sig_timestamp = toCppInteger(
obj.Get("sigTimestampSeconds"), "blindVersionSign.sigTimestampSeconds", false);
diff --git a/src/contacts_config.cpp b/src/contacts_config.cpp
index a1a0739..19c9593 100644
--- a/src/contacts_config.cpp
+++ b/src/contacts_config.cpp
@@ -49,7 +49,7 @@ struct toJs_impl {
obj["createdAtSeconds"] = toJs(env, contact.created);
obj["expirationMode"] = toJs(env, expiration_mode_string(contact.exp_mode));
obj["expirationTimerSeconds"] = toJs(env, contact.exp_timer.count());
- obj["profilePicture"] = object_from_profile_pic(env, contact.profile_picture);
+ obj["profilePicture"] = toJs(env, contact.profile_picture);
return obj;
}
diff --git a/src/convo_info_volatile_config.cpp b/src/convo_info_volatile_config.cpp
index ddf3991..9cf82dd 100644
--- a/src/convo_info_volatile_config.cpp
+++ b/src/convo_info_volatile_config.cpp
@@ -50,6 +50,19 @@ struct toJs_impl : toJs_impl {
}
};
+template <>
+struct toJs_impl {
+ Napi::Object operator()(const Napi::Env& env, const convo::group group_info) {
+ auto obj = Napi::Object::New(env);
+
+ obj["pubkeyHex"] = toJs(env, group_info.id);
+ obj["unread"] = toJs(env, group_info.unread);
+ obj["lastRead"] = toJs(env, group_info.last_read);
+
+ return obj;
+ }
+};
+
void ConvoInfoVolatileWrapper::Init(Napi::Env env, Napi::Object exports) {
InitHelper(
env,
@@ -69,6 +82,12 @@ void ConvoInfoVolatileWrapper::Init(Napi::Env env, Napi::Object exports) {
InstanceMethod("setLegacyGroup", &ConvoInfoVolatileWrapper::setLegacyGroup),
InstanceMethod("eraseLegacyGroup", &ConvoInfoVolatileWrapper::eraseLegacyGroup),
+ // group related methods
+ InstanceMethod("getGroup", &ConvoInfoVolatileWrapper::getGroup),
+ InstanceMethod("getAllGroups", &ConvoInfoVolatileWrapper::getAllGroups),
+ InstanceMethod("setGroup", &ConvoInfoVolatileWrapper::setGroup),
+ InstanceMethod("eraseGroup", &ConvoInfoVolatileWrapper::eraseGroup),
+
// communities related methods
InstanceMethod("getCommunity", &ConvoInfoVolatileWrapper::getCommunity),
InstanceMethod(
@@ -107,7 +126,7 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) {
assertIsString(first);
auto second = info[1];
- assertIsNumber(second);
+ assertIsNumber(second, "set1o1");
auto third = info[2];
assertIsBoolean(third);
@@ -123,6 +142,10 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) {
});
}
+Napi::Value ConvoInfoVolatileWrapper::erase1o1(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] { return config.erase_1to1(getStringArgs<1>(info)); });
+}
+
/**
* =================================================
* ================= Legacy groups =================
@@ -144,7 +167,7 @@ void ConvoInfoVolatileWrapper::setLegacyGroup(const Napi::CallbackInfo& info) {
auto first = info[0];
assertIsString(first);
auto second = info[1];
- assertIsNumber(second);
+ assertIsNumber(second, "setLegacyGroup");
auto third = info[2];
assertIsBoolean(third);
@@ -166,8 +189,45 @@ Napi::Value ConvoInfoVolatileWrapper::eraseLegacyGroup(const Napi::CallbackInfo&
return wrapResult(info, [&] { return config.erase_legacy_group(getStringArgs<1>(info)); });
}
-Napi::Value ConvoInfoVolatileWrapper::erase1o1(const Napi::CallbackInfo& info) {
- return wrapResult(info, [&] { return config.erase_1to1(getStringArgs<1>(info)); });
+/**
+ * =================================================
+ * ===================== Groups ====================
+ * =================================================
+ */
+
+Napi::Value ConvoInfoVolatileWrapper::getGroup(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] { return config.get_group(getStringArgs<1>(info)); });
+}
+
+Napi::Value ConvoInfoVolatileWrapper::getAllGroups(const Napi::CallbackInfo& info) {
+ return get_all_impl(info, config.size_groups(), config.begin_groups(), config.end());
+}
+
+void ConvoInfoVolatileWrapper::setGroup(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertInfoLength(info, 3);
+ auto first = info[0];
+ assertIsString(first);
+ auto second = info[1];
+ assertIsNumber(second, "setGroup");
+
+ auto third = info[2];
+ assertIsBoolean(third);
+
+ auto convo = config.get_or_construct_group(toCppString(first, "convoInfo.setGroup1"));
+
+ if (auto last_read = toCppInteger(second, "convoInfo.setGroup2");
+ last_read > convo.last_read)
+ convo.last_read = last_read;
+
+ convo.unread = toCppBoolean(third, "convoInfo.setGroup3");
+
+ config.set(convo);
+ });
+}
+
+Napi::Value ConvoInfoVolatileWrapper::eraseGroup(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] { return config.erase_group(getStringArgs<1>(info)); });
}
/**
@@ -194,7 +254,7 @@ void ConvoInfoVolatileWrapper::setCommunityByFullUrl(const Napi::CallbackInfo& i
assertIsString(first);
auto second = info[1];
- assertIsNumber(second);
+ assertIsNumber(second, "setCommunityByFullUrl");
auto third = info[2];
assertIsBoolean(third);
diff --git a/src/convo_info_volatile_config.hpp b/src/convo_info_volatile_config.hpp
index 2c491fc..57191ff 100644
--- a/src/convo_info_volatile_config.hpp
+++ b/src/convo_info_volatile_config.hpp
@@ -29,6 +29,12 @@ class ConvoInfoVolatileWrapper : public ConfigBaseImpl,
void setLegacyGroup(const Napi::CallbackInfo& info);
Napi::Value eraseLegacyGroup(const Napi::CallbackInfo& info);
+ // group related methods
+ Napi::Value getGroup(const Napi::CallbackInfo& info);
+ Napi::Value getAllGroups(const Napi::CallbackInfo& info);
+ void setGroup(const Napi::CallbackInfo& info);
+ Napi::Value eraseGroup(const Napi::CallbackInfo& info);
+
// communities related methods
Napi::Value getCommunity(const Napi::CallbackInfo& info);
Napi::Value getAllCommunities(const Napi::CallbackInfo& info);
diff --git a/src/groups/meta_group.hpp b/src/groups/meta_group.hpp
new file mode 100644
index 0000000..a11d7be
--- /dev/null
+++ b/src/groups/meta_group.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include
+
+#include "session/config/groups/info.hpp"
+#include "session/config/groups/keys.hpp"
+#include "session/config/groups/members.hpp"
+
+namespace session::nodeapi {
+
+using config::groups::Info;
+using config::groups::Keys;
+using config::groups::Members;
+using session::config::profile_pic;
+using std::pair;
+using std::string;
+using std::tuple;
+using std::vector;
+
+using std::make_shared;
+using std::shared_ptr;
+
+class MetaGroup {
+ public:
+ shared_ptr info;
+ shared_ptr members;
+ shared_ptr keys;
+ string edGroupPubKey;
+ std::optional edGroupSecKey;
+
+ MetaGroup(
+ shared_ptr info,
+ shared_ptr members,
+ shared_ptr keys,
+ session::ustring edGroupPubKey,
+ std::optional edGroupSecKey) :
+ info{info}, members{members}, keys{keys}, edGroupPubKey{oxenc::to_hex(edGroupPubKey)} {
+
+ if (edGroupSecKey.has_value()) {
+ this->edGroupSecKey = oxenc::to_hex(*edGroupSecKey);
+ } else {
+ this->edGroupSecKey = std::nullopt;
+ }
+ }
+};
+} // namespace session::nodeapi
\ No newline at end of file
diff --git a/src/groups/meta_group_wrapper.cpp b/src/groups/meta_group_wrapper.cpp
new file mode 100644
index 0000000..c6e2752
--- /dev/null
+++ b/src/groups/meta_group_wrapper.cpp
@@ -0,0 +1,775 @@
+#include "meta_group_wrapper.hpp"
+
+#include
+
+#include
+
+#include "oxenc/bt_producer.h"
+#include "session/types.hpp"
+
+namespace session::nodeapi {
+
+MetaGroupWrapper::MetaGroupWrapper(const Napi::CallbackInfo& info) :
+ meta_group{std::move(MetaBaseWrapper::constructGroupWrapper(info, "MetaGroupWrapper"))},
+ Napi::ObjectWrap{info} {}
+
+void MetaGroupWrapper::Init(Napi::Env env, Napi::Object exports) {
+ MetaBaseWrapper::NoBaseClassInitHelper(
+ env,
+ exports,
+ "MetaGroupWrapperNode",
+ {
+ // shared exposed functions
+ InstanceMethod("needsPush", &MetaGroupWrapper::needsPush),
+ InstanceMethod("push", &MetaGroupWrapper::push),
+ InstanceMethod("needsDump", &MetaGroupWrapper::needsDump),
+ InstanceMethod("metaDump", &MetaGroupWrapper::metaDump),
+ InstanceMethod("metaMakeDump", &MetaGroupWrapper::metaMakeDump),
+ InstanceMethod("metaConfirmPushed", &MetaGroupWrapper::metaConfirmPushed),
+ InstanceMethod("metaMerge", &MetaGroupWrapper::metaMerge),
+
+ // infos exposed functions
+ InstanceMethod("infoGet", &MetaGroupWrapper::infoGet),
+ InstanceMethod("infoSet", &MetaGroupWrapper::infoSet),
+ InstanceMethod("infoDestroy", &MetaGroupWrapper::infoDestroy),
+
+ // members exposed functions
+ InstanceMethod("memberGet", &MetaGroupWrapper::memberGet),
+ InstanceMethod("memberGetOrConstruct", &MetaGroupWrapper::memberGetOrConstruct),
+ InstanceMethod(
+ "memberConstructAndSet", &MetaGroupWrapper::memberConstructAndSet),
+ InstanceMethod("memberGetAll", &MetaGroupWrapper::memberGetAll),
+ InstanceMethod(
+ "memberGetAllPendingRemovals",
+ &MetaGroupWrapper::memberGetAllPendingRemovals),
+ InstanceMethod(
+ "membersMarkPendingRemoval",
+ &MetaGroupWrapper::membersMarkPendingRemoval),
+ InstanceMethod(
+ "memberSetNameTruncated", &MetaGroupWrapper::memberSetNameTruncated),
+ InstanceMethod("memberSetInvited", &MetaGroupWrapper::memberSetInvited),
+ InstanceMethod("memberSetAccepted", &MetaGroupWrapper::memberSetAccepted),
+ InstanceMethod("memberSetPromoted", &MetaGroupWrapper::memberSetPromoted),
+ InstanceMethod(
+ "memberSetPromotionSent", &MetaGroupWrapper::memberSetPromotionSent),
+ InstanceMethod(
+ "memberSetPromotionFailed",
+ &MetaGroupWrapper::memberSetPromotionFailed),
+ InstanceMethod(
+ "memberSetPromotionAccepted",
+ &MetaGroupWrapper::memberSetPromotionAccepted),
+ InstanceMethod(
+ "memberSetProfilePicture", &MetaGroupWrapper::memberSetProfilePicture),
+ InstanceMethod("memberEraseAndRekey", &MetaGroupWrapper::memberEraseAndRekey),
+
+ // keys exposed functions
+ InstanceMethod("keysNeedsRekey", &MetaGroupWrapper::keysNeedsRekey),
+ InstanceMethod("keyRekey", &MetaGroupWrapper::keyRekey),
+ InstanceMethod("keyGetAll", &MetaGroupWrapper::keyGetAll),
+ InstanceMethod("currentHashes", &MetaGroupWrapper::currentHashes),
+ InstanceMethod("loadKeyMessage", &MetaGroupWrapper::loadKeyMessage),
+ InstanceMethod("keyGetCurrentGen", &MetaGroupWrapper::keyGetCurrentGen),
+ InstanceMethod("encryptMessages", &MetaGroupWrapper::encryptMessages),
+ InstanceMethod("decryptMessage", &MetaGroupWrapper::decryptMessage),
+ InstanceMethod("makeSwarmSubAccount", &MetaGroupWrapper::makeSwarmSubAccount),
+ InstanceMethod("swarmSubAccountToken", &MetaGroupWrapper::swarmSubAccountToken),
+ InstanceMethod(
+ "swarmVerifySubAccount", &MetaGroupWrapper::swarmVerifySubAccount),
+ InstanceMethod("loadAdminKeys", &MetaGroupWrapper::loadAdminKeys),
+ InstanceMethod("keysAdmin", &MetaGroupWrapper::keysAdmin),
+ InstanceMethod("swarmSubaccountSign", &MetaGroupWrapper::swarmSubaccountSign),
+ InstanceMethod(
+ "generateSupplementKeys", &MetaGroupWrapper::generateSupplementKeys),
+ });
+}
+
+/* #region SHARED ACTIONS */
+
+Napi::Value MetaGroupWrapper::needsPush(const Napi::CallbackInfo& info) {
+
+ return wrapResult(info, [&] {
+ return this->meta_group->members->needs_push() || this->meta_group->info->needs_push() ||
+ this->meta_group->keys->pending_config();
+ });
+}
+
+Napi::Value MetaGroupWrapper::push(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ auto env = info.Env();
+ auto to_push = Napi::Object::New(env);
+
+ if (this->meta_group->members->needs_push())
+ to_push["groupMember"s] = push_result_to_JS(
+ env,
+ this->meta_group->members->push(),
+ this->meta_group->members->storage_namespace());
+ else
+ to_push["groupMember"s] = env.Null();
+
+ if (this->meta_group->info->needs_push())
+ to_push["groupInfo"s] = push_result_to_JS(
+ env,
+ this->meta_group->info->push(),
+ this->meta_group->info->storage_namespace());
+ else
+ to_push["groupInfo"s] = env.Null();
+
+ if (auto pending_config = this->meta_group->keys->pending_config())
+ to_push["groupKeys"s] = push_key_entry_to_JS(
+ env, *(pending_config), this->meta_group->keys->storage_namespace());
+ else
+ to_push["groupKeys"s] = env.Null();
+
+ return to_push;
+ });
+}
+
+Napi::Value MetaGroupWrapper::needsDump(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ return this->meta_group->members->needs_dump() || this->meta_group->info->needs_dump() ||
+ this->meta_group->keys->needs_dump();
+ });
+}
+
+Napi::Value MetaGroupWrapper::metaDump(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ oxenc::bt_dict_producer combined;
+
+ // NOTE: the keys have to be in ascii-sorted order:
+ combined.append("info", session::from_unsigned_sv(this->meta_group->info->dump()));
+ combined.append("keys", session::from_unsigned_sv(this->meta_group->keys->dump()));
+ combined.append("members", session::from_unsigned_sv(this->meta_group->members->dump()));
+ auto to_dump = std::move(combined).str();
+
+ return session::ustring{to_unsigned_sv(to_dump)};
+ });
+}
+
+Napi::Value MetaGroupWrapper::metaMakeDump(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ oxenc::bt_dict_producer combined;
+
+ // NOTE: the keys have to be in ascii-sorted order:
+ combined.append("info", session::from_unsigned_sv(this->meta_group->info->make_dump()));
+ combined.append("keys", session::from_unsigned_sv(this->meta_group->keys->make_dump()));
+ combined.append(
+ "members", session::from_unsigned_sv(this->meta_group->members->make_dump()));
+ auto to_dump = std::move(combined).str();
+
+ return ustring{to_unsigned_sv(to_dump)};
+ });
+}
+
+void MetaGroupWrapper::metaConfirmPushed(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&]() {
+ assertInfoLength(info, 1);
+ auto arg = info[0];
+ assertIsObject(arg);
+ auto obj = arg.As();
+
+ auto groupInfo = obj.Get("groupInfo");
+ auto groupMember = obj.Get("groupMember");
+
+ if (!groupInfo.IsNull() && !groupInfo.IsUndefined()) {
+ assertIsArray(groupInfo);
+ auto groupInfoArr = groupInfo.As();
+ if (groupInfoArr.Length() != 2) {
+ throw std::invalid_argument("groupInfoArr length was not 2");
+ }
+
+ auto seqno = maybeNonemptyInt(
+ groupInfoArr.Get("0"), "MetaGroupWrapper::metaConfirmPushed groupInfo seqno");
+ auto hash = maybeNonemptyString(
+ groupInfoArr.Get("1"), "MetaGroupWrapper::metaConfirmPushed groupInfo hash");
+ if (seqno && hash)
+ this->meta_group->info->confirm_pushed(*seqno, *hash);
+ }
+
+ if (!groupMember.IsNull() && !groupMember.IsUndefined()) {
+ assertIsArray(groupMember);
+ auto groupMemberArr = groupMember.As();
+ if (groupMemberArr.Length() != 2) {
+ throw std::invalid_argument("groupMemberArr length was not 2");
+ }
+
+ auto seqno = maybeNonemptyInt(
+ groupMemberArr.Get("0"),
+ "MetaGroupWrapper::metaConfirmPushed groupMemberArr seqno");
+ auto hash = maybeNonemptyString(
+ groupMemberArr.Get("1"),
+ "MetaGroupWrapper::metaConfirmPushed groupMemberArr hash");
+ if (seqno && hash)
+ this->meta_group->members->confirm_pushed(*seqno, *hash);
+ }
+ });
+};
+
+Napi::Value MetaGroupWrapper::metaMerge(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ auto arg = info[0];
+ assertIsObject(arg);
+ auto obj = arg.As();
+
+ auto groupInfo = obj.Get("groupInfo");
+ auto groupMember = obj.Get("groupMember");
+ auto groupKeys = obj.Get("groupKeys");
+
+ auto count_merged = 0;
+
+ // Note: we need to process keys first as they might allow us the incoming info+members
+ // details
+ if (!groupKeys.IsNull() && !groupKeys.IsUndefined()) {
+ assertIsArray(groupKeys);
+ auto asArr = groupKeys.As();
+
+ for (uint32_t i = 0; i < asArr.Length(); i++) {
+ Napi::Value item = asArr[i];
+ assertIsObject(item);
+ if (item.IsEmpty())
+ throw std::invalid_argument("MetaMerge.item groupKeys received empty");
+
+ Napi::Object itemObject = item.As();
+ assertIsString(itemObject.Get("hash"));
+ assertIsUInt8Array(itemObject.Get("data"), "groupKeys merge");
+ assertIsNumber(itemObject.Get("timestampMs"), "timestampMs groupKeys");
+
+ auto hash = toCppString(itemObject.Get("hash"), "meta.merge keys hash");
+ auto data = toCppBuffer(itemObject.Get("data"), "meta.merge keys data");
+ auto timestamp_ms = toCppInteger(
+ itemObject.Get("timestampMs"), "meta.merge keys timestampMs", false);
+
+ this->meta_group->keys->load_key_message(
+ hash,
+ data,
+ timestamp_ms,
+ *(this->meta_group->info),
+ *(this->meta_group->members));
+ count_merged++; // load_key_message doesn't necessarely merge something as not
+ // all keys are for us.
+ }
+ }
+
+ if (!groupInfo.IsNull() && !groupInfo.IsUndefined()) {
+ assertIsArray(groupInfo);
+ auto asArr = groupInfo.As();
+
+ std::vector> conf_strs;
+ conf_strs.reserve(asArr.Length());
+
+ for (uint32_t i = 0; i < asArr.Length(); i++) {
+ Napi::Value item = asArr[i];
+ assertIsObject(item);
+ if (item.IsEmpty())
+ throw std::invalid_argument("MetaMerge.item groupInfo received empty");
+
+ Napi::Object itemObject = item.As();
+ assertIsString(itemObject.Get("hash"));
+ assertIsUInt8Array(itemObject.Get("data"), "groupInfo merge");
+ conf_strs.emplace_back(
+ toCppString(itemObject.Get("hash"), "meta.merge"),
+ toCppBufferView(itemObject.Get("data"), "meta.merge"));
+ }
+
+ auto info_merged = this->meta_group->info->merge(conf_strs);
+ count_merged += info_merged.size();
+ }
+
+ if (!groupMember.IsNull() && !groupMember.IsUndefined()) {
+ assertIsArray(groupMember);
+ auto asArr = groupMember.As();
+
+ std::vector> conf_strs;
+ conf_strs.reserve(asArr.Length());
+
+ for (uint32_t i = 0; i < asArr.Length(); i++) {
+ Napi::Value item = asArr[i];
+ assertIsObject(item);
+ if (item.IsEmpty())
+ throw std::invalid_argument("MetaMerge.item groupMember received empty");
+
+ Napi::Object itemObject = item.As();
+ assertIsString(itemObject.Get("hash"));
+ assertIsUInt8Array(itemObject.Get("data"), "groupMember merge");
+ conf_strs.emplace_back(
+ toCppString(itemObject.Get("hash"), "meta.merge"),
+ toCppBufferView(itemObject.Get("data"), "meta.merge"));
+ }
+
+ auto member_merged = this->meta_group->members->merge(conf_strs);
+
+ count_merged += member_merged.size();
+ }
+
+ if (this->meta_group->keys->needs_rekey()) {
+ this->meta_group->keys->rekey(*(this->meta_group->info), *(this->meta_group->members));
+ }
+ return count_merged;
+ });
+}
+
+/* #endregion */
+
+/* #region INFO ACTIONS */
+
+Napi::Value MetaGroupWrapper::infoGet(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ auto env = info.Env();
+ auto obj = Napi::Object::New(env);
+
+ obj["name"] = toJs(env, this->meta_group->info->get_name());
+ obj["createdAtSeconds"] = toJs(env, this->meta_group->info->get_created());
+ obj["deleteAttachBeforeSeconds"] =
+ toJs(env, this->meta_group->info->get_delete_attach_before());
+ obj["deleteBeforeSeconds"] = toJs(env, this->meta_group->info->get_delete_before());
+
+ if (auto expiry = this->meta_group->info->get_expiry_timer(); expiry)
+ obj["expirySeconds"] = toJs(env, expiry->count());
+ else
+ obj["expirySeconds"] = env.Null();
+
+ obj["isDestroyed"] = toJs(env, this->meta_group->info->is_destroyed());
+ obj["profilePicture"] = toJs(env, this->meta_group->info->get_profile_pic());
+
+ return obj;
+ });
+}
+
+Napi::Value MetaGroupWrapper::infoSet(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ auto arg = info[0];
+ assertIsObject(arg);
+ auto obj = arg.As();
+
+ if (auto name = maybeNonemptyString(obj.Get("name"), "MetaGroupWrapper::setInfo name"))
+ this->meta_group->info->set_name(*name);
+
+ if (auto created = maybeNonemptyInt(
+ obj.Get("createdAtSeconds"), "MetaGroupWrapper::setInfo set_created"))
+ this->meta_group->info->set_created(std::move(*created));
+
+ if (auto expiry = maybeNonemptyInt(
+ obj.Get("expirySeconds"), "MetaGroupWrapper::setInfo set_expiry_timer"))
+ this->meta_group->info->set_expiry_timer(std::chrono::seconds{*expiry});
+
+ if (auto deleteBefore = maybeNonemptyInt(
+ obj.Get("deleteBeforeSeconds"), "MetaGroupWrapper::setInfo set_delete_before"))
+ this->meta_group->info->set_delete_before(std::move(*deleteBefore));
+
+ if (auto deleteAttachBefore = maybeNonemptyInt(
+ obj.Get("deleteAttachBeforeSeconds"),
+ "MetaGroupWrapper::setInfo set_delete_attach_before"))
+ this->meta_group->info->set_delete_attach_before(std::move(*deleteAttachBefore));
+
+ if (auto profilePicture = obj.Get("profilePicture")) {
+ auto profilePic = profile_pic_from_object(profilePicture);
+ this->meta_group->info->set_profile_pic(profilePic);
+ }
+
+ return this->infoGet(info);
+ });
+}
+
+Napi::Value MetaGroupWrapper::infoDestroy(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ meta_group->info->destroy_group();
+ return this->infoGet(info);
+ });
+}
+
+/* #endregion */
+
+/* #region MEMBERS ACTIONS */
+
+Napi::Value MetaGroupWrapper::memberGetAll(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ std::vector allMembers;
+ for (auto& member : *this->meta_group->members) {
+ allMembers.push_back(member);
+ }
+ return allMembers;
+ });
+}
+
+Napi::Value MetaGroupWrapper::memberGetAllPendingRemovals(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ std::vector allMembersRemoved;
+ for (auto& member : *this->meta_group->members) {
+ if (member.is_removed()) {
+ allMembersRemoved.push_back(member);
+ }
+ }
+ return allMembersRemoved;
+ });
+}
+
+Napi::Value MetaGroupWrapper::memberGet(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+
+ auto pubkeyHex = toCppString(info[0], "memberGet");
+ return meta_group->members->get(pubkeyHex);
+ });
+}
+
+Napi::Value MetaGroupWrapper::memberGetOrConstruct(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+
+ auto pubkeyHex = toCppString(info[0], "memberGetOrConstruct");
+ return meta_group->members->get_or_construct(pubkeyHex);
+ });
+}
+
+Napi::Value MetaGroupWrapper::memberConstructAndSet(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+
+ auto pubkeyHex = toCppString(info[0], "memberConstructAndSet");
+ auto created = meta_group->members->get_or_construct(pubkeyHex);
+ meta_group->members->set(created);
+ return created;
+ });
+}
+
+void MetaGroupWrapper::memberSetNameTruncated(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertIsString(info[0]);
+ assertIsString(info[1]);
+
+ auto pubkeyHex = toCppString(info[0], "memberSetNameTruncated pubkeyHex");
+ auto newName = toCppString(info[1], "memberSetNameTruncated newName");
+ auto m = this->meta_group->members->get(pubkeyHex);
+ if (m) {
+ m->set_name(newName);
+ this->meta_group->members->set(*m);
+ }
+ });
+}
+
+void MetaGroupWrapper::memberSetInvited(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertIsString(info[0]);
+ assertIsBoolean(info[1]);
+ auto pubkeyHex = toCppString(info[0], "memberSetInvited");
+ auto failed = toCppBoolean(info[1], "memberSetInvited");
+
+ auto m = this->meta_group->members->get(pubkeyHex);
+ if (m) {
+ m->set_invited(failed);
+ this->meta_group->members->set(*m);
+ }
+ });
+}
+
+void MetaGroupWrapper::memberSetAccepted(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+
+ auto pubkeyHex = toCppString(info[0], "memberSetAccepted");
+ auto m = this->meta_group->members->get(pubkeyHex);
+ if (m) {
+ m->set_accepted();
+ this->meta_group->members->set(*m);
+ }
+ });
+}
+
+void MetaGroupWrapper::memberSetPromoted(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+ auto pubkeyHex = toCppString(info[0], "memberSetPromoted");
+ auto m = this->meta_group->members->get(pubkeyHex);
+ if (m) {
+ m->set_promoted();
+ this->meta_group->members->set(*m);
+ }
+ });
+}
+
+void MetaGroupWrapper::memberSetPromotionSent(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+ auto pubkeyHex = toCppString(info[0], "memberSetPromotionSent");
+ auto m = this->meta_group->members->get(pubkeyHex);
+ if (m) {
+ m->set_promotion_sent();
+ this->meta_group->members->set(*m);
+ }
+ });
+}
+
+void MetaGroupWrapper::memberSetPromotionFailed(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+ auto pubkeyHex = toCppString(info[0], "memberSetPromotionFailed");
+ auto m = this->meta_group->members->get(pubkeyHex);
+ if (m) {
+ m->set_promotion_failed();
+ this->meta_group->members->set(*m);
+ }
+ });
+}
+
+void MetaGroupWrapper::memberSetPromotionAccepted(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+ auto pubkeyHex = toCppString(info[0], "memberSetPromotionAccepted");
+ auto m = this->meta_group->members->get(pubkeyHex);
+ if (m) {
+ m->set_promotion_accepted();
+ this->meta_group->members->set(*m);
+ }
+ });
+}
+
+void MetaGroupWrapper::memberSetProfilePicture(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertInfoLength(info, 2);
+ assertIsString(info[0]);
+ assertIsObject(info[1]);
+
+ auto pubkeyHex = toCppString(info[0], "memberSetProfilePicture");
+ auto profilePicture = profile_pic_from_object(info[1]);
+
+ auto m = this->meta_group->members->get(pubkeyHex);
+ if (m) {
+ m->profile_picture = profilePicture;
+ this->meta_group->members->set(*m);
+ }
+ });
+}
+
+void MetaGroupWrapper::membersMarkPendingRemoval(const Napi::CallbackInfo& info) {
+ wrapExceptions(info, [&] {
+ assertInfoLength(info, 2);
+ auto toUpdateJSValue = info[0];
+ auto withMessageJSValue = info[1];
+
+ assertIsArray(toUpdateJSValue);
+ assertIsBoolean(withMessageJSValue);
+ bool withMessages = toCppBoolean(withMessageJSValue, "membersMarkPendingRemoval");
+
+ auto toUpdateJS = toUpdateJSValue.As();
+ for (uint32_t i = 0; i < toUpdateJS.Length(); i++) {
+ auto pubkeyHex = toCppString(toUpdateJS[i], "membersMarkPendingRemoval");
+ auto existing = this->meta_group->members->get(pubkeyHex);
+ if (existing) {
+ existing->set_removed(withMessages);
+ this->meta_group->members->set(*existing);
+ }
+ }
+ });
+}
+
+Napi::Value MetaGroupWrapper::memberEraseAndRekey(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ auto toRemoveJSValue = info[0];
+
+ assertIsArray(toRemoveJSValue);
+
+ auto toRemoveJS = toRemoveJSValue.As();
+ auto rekeyed = false;
+ for (uint32_t i = 0; i < toRemoveJS.Length(); i++) {
+ auto pubkeyHex = toCppString(toRemoveJS[i], "memberEraseAndRekey");
+ rekeyed |= this->meta_group->members->erase(pubkeyHex);
+ }
+
+ if (rekeyed) {
+ meta_group->keys->rekey(*(this->meta_group->info), *(this->meta_group->members));
+ }
+
+ return rekeyed;
+ });
+}
+
+/* #endregion */
+
+/* #region KEYS ACTIONS */
+Napi::Value MetaGroupWrapper::keysNeedsRekey(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] { return meta_group->keys->needs_rekey(); });
+}
+
+Napi::Value MetaGroupWrapper::keyRekey(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ return meta_group->keys->rekey(*(meta_group->info), *(meta_group->members));
+ });
+}
+
+Napi::Value MetaGroupWrapper::keyGetAll(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] { return meta_group->keys->group_keys(); });
+}
+
+Napi::Value MetaGroupWrapper::loadKeyMessage(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 3);
+ assertIsString(info[0]);
+ assertIsUInt8Array(info[1], "loadKeyMessage");
+ assertIsNumber(info[2], "loadKeyMessage");
+
+ auto hash = toCppString(info[0], "loadKeyMessage");
+ auto data = toCppBuffer(info[1], "loadKeyMessage");
+ auto timestamp_ms = toCppInteger(info[2], "loadKeyMessage");
+
+ return meta_group->keys->load_key_message(
+ hash, data, timestamp_ms, *(this->meta_group->info), *(this->meta_group->members));
+ });
+}
+
+Napi::Value MetaGroupWrapper::keyGetCurrentGen(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 0);
+ return meta_group->keys->current_generation();
+ });
+}
+
+Napi::Value MetaGroupWrapper::currentHashes(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ auto keysHashes = meta_group->keys->current_hashes();
+ auto infoHashes = meta_group->info->current_hashes();
+ auto memberHashes = meta_group->members->current_hashes();
+ std::vector merged;
+ std::copy(std::begin(keysHashes), std::end(keysHashes), std::back_inserter(merged));
+ std::copy(std::begin(infoHashes), std::end(infoHashes), std::back_inserter(merged));
+ std::copy(std::begin(memberHashes), std::end(memberHashes), std::back_inserter(merged));
+ return merged;
+ });
+}
+
+Napi::Value MetaGroupWrapper::encryptMessages(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsArray(info[0]);
+
+ auto plaintextsJS = info[0].As();
+ uint32_t arrayLength = plaintextsJS.Length();
+ std::vector encryptedMessages;
+ encryptedMessages.reserve(arrayLength);
+
+ for (uint32_t i = 0; i < plaintextsJS.Length(); i++) {
+ auto plaintext = toCppBuffer(plaintextsJS[i], "encryptMessages");
+
+ encryptedMessages.push_back(this->meta_group->keys->encrypt_message(plaintext));
+ }
+ return encryptedMessages;
+ });
+}
+
+Napi::Value MetaGroupWrapper::decryptMessage(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsUInt8Array(info[0], "decryptMessage");
+
+ auto ciphertext = toCppBuffer(info[0], "decryptMessage");
+ auto decrypted = this->meta_group->keys->decrypt_message(ciphertext);
+
+ return decrypt_result_to_JS(info.Env(), decrypted);
+ });
+}
+
+Napi::Value MetaGroupWrapper::makeSwarmSubAccount(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+
+ auto memberPk = toCppString(info[0], "makeSwarmSubAccount");
+ ustring subaccount = this->meta_group->keys->swarm_make_subaccount(memberPk);
+
+ session::nodeapi::checkOrThrow(
+ subaccount.length() == 100, "expected subaccount to be 100 bytes long");
+
+ return subaccount;
+ });
+}
+
+Napi::Value MetaGroupWrapper::swarmSubAccountToken(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsString(info[0]);
+
+ auto memberPk = toCppString(info[0], "swarmSubAccountToken");
+ ustring subaccount = this->meta_group->keys->swarm_subaccount_token(memberPk);
+
+ session::nodeapi::checkOrThrow(
+ subaccount.length() == 36, "expected subaccount token to be 36 bytes long");
+
+ return oxenc::to_hex(subaccount);
+ });
+}
+
+Napi::Value MetaGroupWrapper::swarmVerifySubAccount(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsUInt8Array(info[0], "swarmVerifySubAccount");
+
+ auto signingValue = toCppBuffer(info[0], "swarmVerifySubAccount");
+ return this->meta_group->keys->swarm_verify_subaccount(signingValue);
+ });
+}
+
+Napi::Value MetaGroupWrapper::loadAdminKeys(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsUInt8Array(info[0], "loadAdminKeys");
+
+ auto secret = toCppBuffer(info[0], "loadAdminKeys");
+ this->meta_group->keys->load_admin_key(
+ secret, *(this->meta_group->info), *(this->meta_group->members));
+ return info.Env().Null();
+ });
+}
+
+Napi::Value MetaGroupWrapper::keysAdmin(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 0);
+ return this->meta_group->keys->admin();
+ });
+}
+
+Napi::Value MetaGroupWrapper::generateSupplementKeys(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ auto membersJSValue = info[0];
+ assertIsArray(membersJSValue);
+
+ auto membersJS = membersJSValue.As();
+ uint32_t arrayLength = membersJS.Length();
+ std::vector membersToAdd;
+ membersToAdd.reserve(arrayLength);
+ std::vector membersCpp;
+ membersCpp.reserve(arrayLength);
+
+ for (uint32_t i = 0; i < membersJS.Length(); i++) {
+ auto memberPk = toCppString(membersJS[i], "generateSupplementKeys");
+ membersCpp.push_back(memberPk);
+ }
+ return this->meta_group->keys->key_supplement(membersCpp);
+ });
+}
+
+Napi::Value MetaGroupWrapper::swarmSubaccountSign(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 2);
+ assertIsUInt8Array(info[0], "swarmSubaccountSign 0");
+ assertIsUInt8Array(info[1], "swarmSubaccountSign 1");
+
+ auto message = toCppBuffer(info[0], "swarmSubaccountSign message");
+ auto authdata = toCppBuffer(info[1], "swarmSubaccountSign authdata");
+ auto subaccountSign = this->meta_group->keys->swarm_subaccount_sign(message, authdata);
+
+ return subaccountSign;
+ });
+}
+/* #endregion */
+
+} // namespace session::nodeapi
diff --git a/src/groups/meta_group_wrapper.hpp b/src/groups/meta_group_wrapper.hpp
new file mode 100644
index 0000000..2b2562c
--- /dev/null
+++ b/src/groups/meta_group_wrapper.hpp
@@ -0,0 +1,140 @@
+#pragma once
+
+#include
+
+#include "../meta/meta_base_wrapper.hpp"
+#include "../profile_pic.hpp"
+#include "../utilities.hpp"
+#include "./meta_group.hpp"
+#include "oxenc/bt_producer.h"
+#include "session/config/user_groups.hpp"
+
+namespace session::nodeapi {
+using config::groups::Members;
+using session::config::GROUP_DESTROYED;
+using session::config::KICKED_FROM_GROUP;
+using session::config::NOT_REMOVED;
+using session::config::groups::member;
+using session::nodeapi::MetaGroup;
+
+template <>
+struct toJs_impl {
+ Napi::Object operator()(const Napi::Env& env, const member& info) {
+ auto obj = Napi::Object::New(env);
+
+ obj["pubkeyHex"] = toJs(env, info.session_id);
+ obj["name"] = toJs(env, info.name);
+ obj["profilePicture"] = toJs(env, info.profile_picture);
+ obj["removedStatus"] = toJs(env, info.removed_status);
+
+ // promoted() is true as soon as the member is scheduled to be promoted
+ // Note: this should be part of `libsession-util`, not `libsession-util-nodejs`
+ if (info.promoted() && !info.promotion_pending()) {
+ obj["memberStatus"] = toJs(env, "PROMOTION_ACCEPTED");
+ } else if (info.promotion_failed()) {
+ obj["memberStatus"] = toJs(env, "PROMOTION_FAILED");
+ } else if (info.promotion_pending()) {
+ obj["memberStatus"] = toJs(env, "PROMOTION_SENT");
+ } else if (info.admin) {
+ obj["memberStatus"] = toJs(env, "PROMOTION_NOT_SENT");
+ } else if (info.invite_status == 0) {
+ obj["memberStatus"] = toJs(env, "INVITE_ACCEPTED");
+ } else if (info.invite_not_sent()) {
+ obj["memberStatus"] = toJs(env, "INVITE_NOT_SENT");
+ } else if (info.invite_failed()) {
+ obj["memberStatus"] = toJs(env, "INVITE_FAILED");
+ } else if (info.invite_pending()) {
+ // Note: INVITE_NOT_SENT is 3, which makes invite_pending() return true, so be sure to
+ // check for invite_not_sent() above.
+ obj["memberStatus"] = toJs(env, "INVITE_SENT");
+ } else {
+ obj["memberStatus"] = toJs(env, "UNKNOWN");
+ }
+
+ obj["nominatedAdmin"] = toJs(env, info.admin);
+
+ if (!info.is_removed()) {
+ obj["removedStatus"] = toJs(env, "NOT_REMOVED");
+ } else {
+ if (info.should_remove_messages()) {
+ obj["removedStatus"] = toJs(env, "REMOVED_MEMBER_AND_MESSAGES");
+ } else {
+ obj["removedStatus"] = toJs(env, "REMOVED_MEMBER");
+ }
+ }
+ return obj;
+ }
+};
+
+template <>
+struct toJs_impl {
+ Napi::Object operator()(const Napi::Env& env, const Keys::swarm_auth& auth) {
+ auto obj = Napi::Object::New(env);
+
+ obj["subaccount"] = toJs(env, auth.subaccount);
+ obj["subaccount_sig"] = toJs(env, auth.subaccount_sig);
+ obj["signature"] = toJs(env, auth.signature);
+ return obj;
+ }
+};
+
+class MetaGroupWrapper : public Napi::ObjectWrap {
+ public:
+ static void Init(Napi::Env env, Napi::Object exports);
+
+ explicit MetaGroupWrapper(const Napi::CallbackInfo& info);
+
+ private:
+ std::unique_ptr meta_group;
+
+ /* Shared Actions */
+ Napi::Value needsPush(const Napi::CallbackInfo& info);
+ Napi::Value push(const Napi::CallbackInfo& info);
+ Napi::Value needsDump(const Napi::CallbackInfo& info);
+ Napi::Value metaDump(const Napi::CallbackInfo& info);
+ Napi::Value metaMakeDump(const Napi::CallbackInfo& info);
+ void metaConfirmPushed(const Napi::CallbackInfo& info);
+ Napi::Value metaMerge(const Napi::CallbackInfo& info);
+
+ /** Info Actions */
+ Napi::Value infoGet(const Napi::CallbackInfo& info);
+ Napi::Value infoSet(const Napi::CallbackInfo& info);
+ Napi::Value infoDestroy(const Napi::CallbackInfo& info);
+
+ /** Members Actions */
+ Napi::Value memberGetAll(const Napi::CallbackInfo& info);
+ Napi::Value memberGetAllPendingRemovals(const Napi::CallbackInfo& info);
+ Napi::Value memberGet(const Napi::CallbackInfo& info);
+ Napi::Value memberGetOrConstruct(const Napi::CallbackInfo& info);
+ Napi::Value memberConstructAndSet(const Napi::CallbackInfo& info);
+
+ void memberSetNameTruncated(const Napi::CallbackInfo& info);
+ void memberSetInvited(const Napi::CallbackInfo& info);
+ void memberSetAccepted(const Napi::CallbackInfo& info);
+ void memberSetPromoted(const Napi::CallbackInfo& info);
+ void memberSetPromotionSent(const Napi::CallbackInfo& info);
+ void memberSetPromotionFailed(const Napi::CallbackInfo& info);
+ void memberSetPromotionAccepted(const Napi::CallbackInfo& info);
+ void memberSetProfilePicture(const Napi::CallbackInfo& info);
+ Napi::Value memberEraseAndRekey(const Napi::CallbackInfo& info);
+ void membersMarkPendingRemoval(const Napi::CallbackInfo& info);
+
+ /** Keys Actions */
+ Napi::Value keysNeedsRekey(const Napi::CallbackInfo& info);
+ Napi::Value keyRekey(const Napi::CallbackInfo& info);
+ Napi::Value keyGetAll(const Napi::CallbackInfo& info);
+ Napi::Value loadKeyMessage(const Napi::CallbackInfo& info);
+ Napi::Value keyGetCurrentGen(const Napi::CallbackInfo& info);
+ Napi::Value currentHashes(const Napi::CallbackInfo& info);
+ Napi::Value encryptMessages(const Napi::CallbackInfo& info);
+ Napi::Value decryptMessage(const Napi::CallbackInfo& info);
+ Napi::Value makeSwarmSubAccount(const Napi::CallbackInfo& info);
+ Napi::Value swarmSubAccountToken(const Napi::CallbackInfo& info);
+ Napi::Value generateSupplementKeys(const Napi::CallbackInfo& info);
+ Napi::Value swarmSubaccountSign(const Napi::CallbackInfo& info);
+ Napi::Value swarmVerifySubAccount(const Napi::CallbackInfo& info);
+ Napi::Value loadAdminKeys(const Napi::CallbackInfo& info);
+ Napi::Value keysAdmin(const Napi::CallbackInfo& info);
+};
+
+} // namespace session::nodeapi
diff --git a/src/meta/meta_base_wrapper.hpp b/src/meta/meta_base_wrapper.hpp
index 95a94a6..75e71e9 100644
--- a/src/meta/meta_base_wrapper.hpp
+++ b/src/meta/meta_base_wrapper.hpp
@@ -3,6 +3,7 @@
#include
#include "../base_config.hpp"
+#include "../groups/meta_group.hpp"
namespace session::nodeapi {
@@ -29,6 +30,81 @@ class MetaBaseWrapper {
exports.Set(class_name, cls);
}
+
+ static std::unique_ptr constructGroupWrapper(
+ const Napi::CallbackInfo& info, const std::string& class_name) {
+ return wrapExceptions(info, [&] {
+ if (!info.IsConstructCall())
+ throw std::invalid_argument{
+ "You need to call the constructor with the `new` syntax"};
+
+ assertInfoLength(info, 1);
+ auto arg = info[0];
+ assertIsObject(arg);
+ auto obj = arg.As();
+
+ if (obj.IsEmpty())
+ throw std::invalid_argument("constructGroupWrapper received empty");
+
+ assertIsUInt8Array(obj.Get("userEd25519Secretkey"), "constructGroupWrapper userEd");
+ auto user_ed25519_secretkey = toCppBuffer(
+ obj.Get("userEd25519Secretkey"),
+ class_name + ":constructGroupWrapper.userEd25519Secretkey");
+
+ assertIsUInt8Array(obj.Get("groupEd25519Pubkey"), "constructGroupWrapper groupEd");
+ auto group_ed25519_pubkey = toCppBuffer(
+ obj.Get("groupEd25519Pubkey"),
+ class_name + ":constructGroupWrapper.groupEd25519Pubkey");
+
+ std::optional group_ed25519_secretkey = maybeNonemptyBuffer(
+ obj.Get("groupEd25519Secretkey"),
+ class_name + ":constructGroupWrapper.groupEd25519Secretkey");
+
+ std::optional dumped_meta = maybeNonemptyBuffer(
+ obj.Get("metaDumped"), class_name + ":constructGroupWrapper.metaDumped");
+
+ std::optional dumped_info;
+ std::optional dumped_members;
+ std::optional dumped_keys;
+
+ if (dumped_meta) {
+ auto dumped_meta_str = from_unsigned_sv(*dumped_meta);
+
+ oxenc::bt_dict_consumer combined{dumped_meta_str};
+ // NB: must read in ascii-sorted order:
+ if (!combined.skip_until("info"))
+ throw std::runtime_error{"info dump not found in combined dump!"};
+ dumped_info = session::to_unsigned_sv(combined.consume_string_view());
+
+ if (!combined.skip_until("keys"))
+ throw std::runtime_error{"keys dump not found in combined dump!"};
+ dumped_keys = session::to_unsigned_sv(combined.consume_string_view());
+
+ if (!combined.skip_until("members"))
+ throw std::runtime_error{"members dump not found in combined dump!"};
+ dumped_members = session::to_unsigned_sv(combined.consume_string_view());
+ }
+
+ // Note, we keep shared_ptr for those as the Keys one need a reference to Members and
+ // Info on its own currently.
+ auto info = std::make_shared(
+ group_ed25519_pubkey, group_ed25519_secretkey, dumped_info);
+
+ auto members = std::make_shared(
+ group_ed25519_pubkey, group_ed25519_secretkey, dumped_members);
+
+ auto keys = std::make_shared(
+ user_ed25519_secretkey,
+ group_ed25519_pubkey,
+ group_ed25519_secretkey,
+ dumped_keys,
+ *info,
+ *members);
+
+ return std::make_unique(
+ info, members, keys, group_ed25519_pubkey, group_ed25519_secretkey);
+ });
+ }
};
} // namespace session::nodeapi
diff --git a/src/multi_encrypt/multi_encrypt.hpp b/src/multi_encrypt/multi_encrypt.hpp
new file mode 100644
index 0000000..6816fda
--- /dev/null
+++ b/src/multi_encrypt/multi_encrypt.hpp
@@ -0,0 +1,124 @@
+#pragma once
+
+#include
+
+#include
+
+#include "../utilities.hpp"
+#include "session/config/user_profile.hpp"
+#include "session/multi_encrypt.hpp"
+#include "session/random.hpp"
+
+namespace session::nodeapi {
+
+class MultiEncryptWrapper : public Napi::ObjectWrap {
+ public:
+ MultiEncryptWrapper(const Napi::CallbackInfo& info) :
+ Napi::ObjectWrap{info} {
+ throw std::invalid_argument(
+ "MultiEncryptWrapper is static and doesn't need to be constructed");
+ }
+
+ static void Init(Napi::Env env, Napi::Object exports) {
+ MetaBaseWrapper::NoBaseClassInitHelper(
+ env,
+ exports,
+ "MultiEncryptWrapperNode",
+ {
+ StaticMethod<&MultiEncryptWrapper::multiEncrypt>(
+ "multiEncrypt",
+ static_cast(
+ napi_writable | napi_configurable)),
+ StaticMethod<&MultiEncryptWrapper::multiDecryptEd25519>(
+ "multiDecryptEd25519",
+ static_cast(
+ napi_writable | napi_configurable)),
+
+ });
+ }
+
+ private:
+ static Napi::Value multiEncrypt(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsObject(info[0]);
+ auto obj = info[0].As();
+
+ if (obj.IsEmpty())
+ throw std::invalid_argument("multiEncrypt received empty");
+
+ assertIsUInt8Array(obj.Get("ed25519SecretKey"), "multiEncrypt.ed25519SecretKey");
+ auto ed25519SecretKey =
+ toCppBuffer(obj.Get("ed25519SecretKey"), "multiEncrypt.ed25519SecretKey");
+
+ assertIsString(obj.Get("domain"));
+ auto domain = toCppString(obj.Get("domain"), "multiEncrypt.domain");
+
+ // handle the messages conversion
+ auto messagesJSValue = obj.Get("messages");
+ assertIsArray(messagesJSValue);
+ auto messagesJS = messagesJSValue.As();
+ std::vector messages;
+ messages.reserve(messagesJS.Length());
+ for (uint32_t i = 0; i < messagesJS.Length(); i++) {
+ auto itemValue = messagesJS.Get(i);
+ assertIsUInt8Array(itemValue, "multiEncrypt.itemValue.message");
+ auto item = toCppBuffer(itemValue, "multiEncrypt.itemValue.message");
+ messages.push_back(item);
+ }
+
+ // handle the recipients conversion
+ auto recipientsJSValue = obj.Get("recipients");
+ assertIsArray(recipientsJSValue);
+ auto recipientsJS = recipientsJSValue.As();
+ std::vector recipients;
+ recipients.reserve(recipientsJS.Length());
+ for (uint32_t i = 0; i < recipientsJS.Length(); i++) {
+ auto itemValue = recipientsJS.Get(i);
+ assertIsUInt8Array(itemValue, "multiEncrypt.itemValue.recipient");
+ auto item = toCppBuffer(itemValue, "multiEncrypt.itemValue.recipient");
+ recipients.push_back(item);
+ }
+ ustring random_nonce = session::random::random(24);
+
+ std::vector messages_sv(messages.begin(), messages.end());
+ std::vector recipients_sv(recipients.begin(), recipients.end());
+
+ // Note: this function needs the first 2 args to be vector of sv explicitly
+ return session::encrypt_for_multiple_simple(
+ messages_sv, recipients_sv, ed25519SecretKey, domain, random_nonce);
+ });
+ };
+
+ static Napi::Value multiDecryptEd25519(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsObject(info[0]);
+ auto obj = info[0].As();
+
+ if (obj.IsEmpty())
+ throw std::invalid_argument("multiDecryptEd25519 received empty");
+
+ assertIsUInt8Array(obj.Get("encoded"), "multiDecryptEd25519.encoded");
+ auto encoded = toCppBuffer(obj.Get("encoded"), "multiDecryptEd25519.encoded");
+
+ assertIsUInt8Array(
+ obj.Get("userEd25519SecretKey"), "multiDecryptEd25519.userEd25519SecretKey");
+ auto ed25519_secret_key = toCppBuffer(
+ obj.Get("userEd25519SecretKey"), "multiDecryptEd25519.userEd25519SecretKey");
+
+ assertIsUInt8Array(
+ obj.Get("senderEd25519Pubkey"), "multiDecryptEd25519.senderEd25519Pubkey");
+ auto sender_ed25519_pubkey = toCppBuffer(
+ obj.Get("senderEd25519Pubkey"), "multiDecryptEd25519.senderEd25519Pubkey");
+
+ assertIsString(obj.Get("domain"));
+ auto domain = toCppString(obj.Get("domain"), "multiDecryptEd25519.domain");
+
+ return session::decrypt_for_multiple_simple_ed25519(
+ encoded, ed25519_secret_key, sender_ed25519_pubkey, domain);
+ });
+ };
+};
+
+} // namespace session::nodeapi
diff --git a/src/profile_pic.cpp b/src/profile_pic.cpp
index e513112..14d2040 100644
--- a/src/profile_pic.cpp
+++ b/src/profile_pic.cpp
@@ -5,18 +5,6 @@
namespace session::nodeapi {
-Napi::Object object_from_profile_pic(const Napi::Env& env, const config::profile_pic& pic) {
- auto obj = Napi::Object::New(env);
- if (pic) {
- obj["url"] = toJs(env, pic.url);
- obj["key"] = toJs(env, pic.key);
- } else {
- obj["url"] = env.Null();
- obj["key"] = env.Null();
- }
- return obj;
-}
-
config::profile_pic profile_pic_from_object(Napi::Value val) {
if (val.IsNull() || val.IsUndefined())
return {};
@@ -34,7 +22,7 @@ config::profile_pic profile_pic_from_object(Napi::Value val) {
return {};
assertIsString(url);
- assertIsUInt8Array(key);
+ assertIsUInt8Array(key, "profile_pic_from_object");
auto url_str = toCppString(url, "profile_pic_from_object");
if (url_str.size() > config::profile_pic::MAX_URL_LENGTH)
diff --git a/src/profile_pic.hpp b/src/profile_pic.hpp
index d6c3aed..c959571 100644
--- a/src/profile_pic.hpp
+++ b/src/profile_pic.hpp
@@ -1,16 +1,31 @@
#include
#include "session/config/profile_pic.hpp"
+#include "utilities.hpp"
namespace session::nodeapi {
// Returns {"url": "...", "key": buffer} object; both values will be Null if the pic is not set.
-Napi::Object object_from_profile_pic(const Napi::Env& env, const config::profile_pic& pic);
-// Constructs a profile_pic from a Napi::Value which must be either Null or an Object; if an Object
-// then it *must* contain "url" (string or null) and "key" (uint8array of size 32 or null) keys; if
-// either is empty or Null then you get back an empty (i.e. clearing) profile_pic. Throws on
-// invalid input.
+template <>
+struct toJs_impl {
+ Napi::Object operator()(const Napi::Env& env, const config::profile_pic& pic) {
+ auto obj = Napi::Object::New(env);
+ if (pic) {
+ obj["url"] = toJs(env, pic.url);
+ obj["key"] = toJs(env, pic.key);
+ } else {
+ obj["url"] = env.Null();
+ obj["key"] = env.Null();
+ }
+ return obj;
+ }
+};
+
+// Constructs a profile_pic from a Napi::Value which must be either Null or an Object; if an
+// Object then it *must* contain "url" (string or null) and "key" (uint8array of size 32 or
+// null) keys; if either is empty or Null then you get back an empty (i.e. clearing)
+// profile_pic. Throws on invalid input.
config::profile_pic profile_pic_from_object(Napi::Value val);
} // namespace session::nodeapi
diff --git a/src/user_config.cpp b/src/user_config.cpp
index 60b8b4b..4f1937c 100644
--- a/src/user_config.cpp
+++ b/src/user_config.cpp
@@ -1,7 +1,10 @@
#include "user_config.hpp"
+#include
+
#include "base_config.hpp"
#include "profile_pic.hpp"
+#include "session/config/base.hpp"
#include "session/config/user_profile.hpp"
namespace session::nodeapi {
@@ -36,7 +39,6 @@ UserConfigWrapper::UserConfigWrapper(const Napi::CallbackInfo& info) :
ConfigBaseImpl{construct(info, "UserConfig")},
Napi::ObjectWrap{info} {}
-
Napi::Value UserConfigWrapper::getPriority(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
auto env = info.Env();
@@ -54,7 +56,16 @@ Napi::Value UserConfigWrapper::getName(const Napi::CallbackInfo& info) {
Napi::Value UserConfigWrapper::getProfilePic(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
auto env = info.Env();
- return object_from_profile_pic(env, config.get_profile_pic());
+ auto pic = config.get_profile_pic();
+ auto obj = Napi::Object::New(env);
+ if (pic) {
+ obj["url"] = toJs(env, pic.url);
+ obj["key"] = toJs(env, pic.key);
+ } else {
+ obj["url"] = env.Null();
+ obj["key"] = env.Null();
+ }
+ return obj;
});
}
@@ -63,7 +74,7 @@ void UserConfigWrapper::setPriority(const Napi::CallbackInfo& info) {
auto env = info.Env();
assertInfoLength(info, 1);
auto priority = info[0];
- assertIsNumber(priority);
+ assertIsNumber(priority, "UserConfigWrapper::setPriority");
auto new_priority = toPriority(priority, config.get_nts_priority());
config.set_nts_priority(new_priority);
@@ -145,7 +156,7 @@ void UserConfigWrapper::setNoteToSelfExpiry(const Napi::CallbackInfo& info) {
assertInfoLength(info, 1);
auto expirySeconds = info[0];
- assertIsNumber(expirySeconds);
+ assertIsNumber(expirySeconds, "setNoteToSelfExpiry");
auto expiryCppSeconds = toCppInteger(expirySeconds, "set_nts_expiry", false);
config.set_nts_expiry(std::chrono::seconds{expiryCppSeconds});
diff --git a/src/user_groups_config.cpp b/src/user_groups_config.cpp
index ed46edb..1b29ffc 100644
--- a/src/user_groups_config.cpp
+++ b/src/user_groups_config.cpp
@@ -13,6 +13,7 @@
namespace session::nodeapi {
using config::community_info;
+using config::group_info;
using config::legacy_group_info;
using config::UserGroups;
@@ -56,6 +57,25 @@ struct toJs_impl {
}
};
+template <>
+struct toJs_impl {
+ Napi::Object operator()(const Napi::Env& env, const group_info& info) {
+ auto obj = Napi::Object::New(env);
+
+ obj["pubkeyHex"] = toJs(env, info.id);
+ obj["secretKey"] = toJs(env, info.secretkey);
+ obj["priority"] = toJs(env, info.priority);
+ obj["joinedAtSeconds"] = toJs(env, info.joined_at);
+ obj["name"] = toJs(env, info.name);
+ obj["authData"] = toJs(env, info.auth_data);
+ obj["invitePending"] = toJs(env, info.invited);
+ obj["kicked"] = toJs(env, info.kicked());
+ obj["destroyed"] = toJs(env, info.isDestroyed());
+
+ return obj;
+ }
+};
+
void UserGroupsWrapper::Init(Napi::Env env, Napi::Object exports) {
InitHelper(
env,
@@ -78,6 +98,17 @@ void UserGroupsWrapper::Init(Napi::Env env, Napi::Object exports) {
InstanceMethod("getAllLegacyGroups", &UserGroupsWrapper::getAllLegacyGroups),
InstanceMethod("setLegacyGroup", &UserGroupsWrapper::setLegacyGroup),
InstanceMethod("eraseLegacyGroup", &UserGroupsWrapper::eraseLegacyGroup),
+
+ // Groups related methods
+ InstanceMethod("createGroup", &UserGroupsWrapper::createGroup),
+ InstanceMethod("getGroup", &UserGroupsWrapper::getGroup),
+ InstanceMethod("getAllGroups", &UserGroupsWrapper::getAllGroups),
+ InstanceMethod("setGroup", &UserGroupsWrapper::setGroup),
+ InstanceMethod("markGroupKicked", &UserGroupsWrapper::markGroupKicked),
+ InstanceMethod("markGroupInvited", &UserGroupsWrapper::markGroupInvited),
+ InstanceMethod("markGroupDestroyed", &UserGroupsWrapper::markGroupDestroyed),
+ InstanceMethod("eraseGroup", &UserGroupsWrapper::eraseGroup),
+
});
}
@@ -104,7 +135,7 @@ void UserGroupsWrapper::setCommunityByFullUrl(const Napi::CallbackInfo& info) {
toCppString(first, "group.SetCommunityByFullUrl"));
auto second = info[1];
- assertIsNumber(second);
+ assertIsNumber(second, "setCommunityByFullUrl");
createdOrFound.priority = toPriority(second, createdOrFound.priority);
config.set(createdOrFound);
@@ -177,7 +208,8 @@ void UserGroupsWrapper::setLegacyGroup(const Napi::CallbackInfo& info) {
group.disappearing_timer = std::chrono::seconds{toCppInteger(
obj.Get("disappearingTimerSeconds"),
- "legacyGroup.set disappearingTimerSeconds", true)};
+ "legacyGroup.set disappearingTimerSeconds",
+ true)};
auto membersJSValue = obj.Get("members");
assertIsArray(membersJSValue);
@@ -236,4 +268,119 @@ Napi::Value UserGroupsWrapper::eraseLegacyGroup(const Napi::CallbackInfo& info)
return wrapResult(info, [&] { return config.erase_legacy_group(getStringArgs<1>(info)); });
}
+/**
+ * =================================================
+ * ===================== GROUPS ====================
+ * =================================================
+ */
+
+Napi::Value UserGroupsWrapper::createGroup(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] { return config.create_group(); });
+}
+
+Napi::Value UserGroupsWrapper::getGroup(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] { return config.get_group(getStringArgs<1>(info)); });
+}
+
+Napi::Value UserGroupsWrapper::getAllGroups(const Napi::CallbackInfo& info) {
+
+ return get_all_impl(info, config.size_groups(), config.begin_groups(), config.end());
+}
+
+Napi::Value UserGroupsWrapper::setGroup(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ assertInfoLength(info, 1);
+ assertIsObject(info[0]);
+ auto obj = info[0].As();
+
+ if (obj.IsEmpty())
+ throw std::invalid_argument("setGroup received empty");
+
+ assertIsString(obj.Get("pubkeyHex"));
+ auto groupPk = toCppString(obj.Get("pubkeyHex"), "legacyGroup.set");
+
+ // we should get a `UserGroupsSet` object. If any fields are null, skip updating them.
+ // Otherwise, use the corresponding value to update what we got from the
+ // `get_or_construct_group` below
+
+ auto group_info = config.get_or_construct_group(groupPk);
+
+ if (auto priority =
+ maybeNonemptyInt(obj.Get("priority"), "UserGroupsWrapper::setGroup priority")) {
+ group_info.priority = toPriority(obj.Get("priority"), group_info.priority);
+ }
+
+ if (auto joinedAtSeconds = maybeNonemptyInt(
+ obj.Get("joinedAtSeconds"), "UserGroupsWrapper::setGroup joinedAtSeconds")) {
+ group_info.joined_at = *joinedAtSeconds;
+ }
+
+ if (auto invited = maybeNonemptyBoolean(
+ obj.Get("invitePending"), "UserGroupsWrapper::setGroup invitePending")) {
+ group_info.invited = *invited;
+ }
+
+ if (auto secretKey = maybeNonemptyBuffer(
+ obj.Get("secretKey"), "UserGroupsWrapper::setGroup secretKey")) {
+ group_info.secretkey = *secretKey;
+ }
+
+ if (auto authData = maybeNonemptyBuffer(
+ obj.Get("authData"), "UserGroupsWrapper::setGroup authData")) {
+ group_info.auth_data = *authData;
+ }
+
+ if (auto name = maybeNonemptyString(obj.Get("name"), "UserGroupsWrapper::setGroup name")) {
+ group_info.name = *name;
+ }
+
+ config.set(group_info);
+
+ return config.get_or_construct_group(groupPk);
+ });
+}
+
+Napi::Value UserGroupsWrapper::markGroupKicked(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ auto groupPk = getStringArgs<1>(info);
+
+ auto group = config.get_group(groupPk);
+ if (group) {
+ group->markKicked();
+ config.set(*group);
+ }
+ return config.get_or_construct_group(groupPk);
+ });
+}
+
+Napi::Value UserGroupsWrapper::markGroupInvited(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ auto groupPk = getStringArgs<1>(info);
+
+ auto group = config.get_group(groupPk);
+ if (group) {
+ group->markInvited();
+ config.set(*group);
+ }
+ return config.get_or_construct_group(groupPk);
+ });
+}
+
+Napi::Value UserGroupsWrapper::markGroupDestroyed(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] {
+ auto groupPk = getStringArgs<1>(info);
+
+ auto group = config.get_group(groupPk);
+ if (group) {
+ group->markDestroyed();
+ config.set(*group);
+ }
+ return config.get_or_construct_group(groupPk);
+ });
+}
+
+Napi::Value UserGroupsWrapper::eraseGroup(const Napi::CallbackInfo& info) {
+ return wrapResult(info, [&] { return config.erase_group(getStringArgs<1>(info)); });
+}
+
} // namespace session::nodeapi
diff --git a/src/user_groups_config.hpp b/src/user_groups_config.hpp
index 7af3372..be8bcf1 100644
--- a/src/user_groups_config.hpp
+++ b/src/user_groups_config.hpp
@@ -28,6 +28,16 @@ class UserGroupsWrapper : public ConfigBaseImpl, public Napi::ObjectWrap
-namespace session::nodeapi {
+#include "session/config/namespaces.hpp"
-static void checkOrThrow(bool condition, const char* msg) {
- if (!condition)
- throw std::invalid_argument{msg};
-}
+namespace session::nodeapi {
void assertInfoLength(const Napi::CallbackInfo& info, const int expected) {
checkOrThrow(info.Length() == expected, "Invalid number of arguments");
@@ -21,10 +18,10 @@ void assertIsStringOrNull(const Napi::Value& val) {
checkOrThrow(val.IsString() || val.IsNull(), "Wrong arguments: expected string or null");
}
-void assertIsNumber(const Napi::Value& val) {
+void assertIsNumber(const Napi::Value& val, const std::string& identifier) {
checkOrThrow(
val.IsNumber() && !val.IsEmpty() && !val.IsNull() && !val.IsUndefined(),
- "Wrong arguments: expected number");
+ std::string("Wrong arguments: expected number" + identifier).c_str());
}
void assertIsArray(const Napi::Value& val) {
@@ -45,8 +42,10 @@ void assertIsUInt8ArrayOrNull(const Napi::Value& val) {
checkOrThrow(val.IsNull() || IsUint8Array(val), "Wrong arguments: expected uint8Array or null");
}
-void assertIsUInt8Array(const Napi::Value& val) {
- checkOrThrow(IsUint8Array(val), "Wrong arguments: expected Buffer");
+void assertIsUInt8Array(const Napi::Value& val, const std::string& identifier) {
+ checkOrThrow(
+ IsUint8Array(val),
+ std::string("Wrong arguments: expected uint8Array" + identifier).c_str());
}
void assertIsString(const Napi::Value& val) {
@@ -123,6 +122,29 @@ int64_t toCppInteger(Napi::Value x, const std::string& identifier, bool allowUnd
throw std::invalid_argument{"Unsupported type for "s + identifier + ": expected a number"};
}
+std::optional maybeNonemptyInt(Napi::Value x, const std::string& identifier) {
+ if (x.IsNull() || x.IsUndefined())
+ return std::nullopt;
+ if (x.IsNumber()) {
+ auto num = x.As().Int64Value();
+ return num;
+ }
+
+ throw std::invalid_argument{"maybeNonemptyInt with invalid type, called from " + identifier};
+}
+
+std::optional maybeNonemptyBoolean(Napi::Value x, const std::string& identifier) {
+ if (x.IsNull() || x.IsUndefined())
+ return std::nullopt;
+ if (x.IsBoolean()) {
+
+ return x.As().Value();
+ }
+
+ throw std::invalid_argument{
+ "maybeNonemptyBoolean with invalid type, called from " + identifier};
+}
+
bool toCppBoolean(Napi::Value x, const std::string& identifier) {
if (x.IsNull() || x.IsUndefined())
return false;
@@ -168,4 +190,40 @@ int64_t unix_timestamp_now() {
return duration_cast(system_clock::now().time_since_epoch()).count();
}
+Napi::Object push_result_to_JS(
+ const Napi::Env& env,
+ const push_entry_t& push_entry,
+ const session::config::Namespace& push_namespace) {
+ auto obj = Napi::Object::New(env);
+
+ obj["seqno"] = toJs(env, std::get<0>(push_entry));
+ obj["data"] = toJs(env, std::get<1>(push_entry));
+ obj["hashes"] = toJs(env, std::get<2>(push_entry));
+ obj["namespace"] = toJs(env, push_namespace);
+
+ return obj;
+};
+
+Napi::Object push_key_entry_to_JS(
+ const Napi::Env& env,
+ const session::ustring_view& key_data,
+ const session::config::Namespace& push_namespace) {
+ auto obj = Napi::Object::New(env);
+
+ obj["data"] = toJs(env, key_data);
+ obj["namespace"] = toJs(env, push_namespace);
+
+ return obj;
+};
+
+Napi::Object decrypt_result_to_JS(
+ const Napi::Env& env, const std::pair decrypted) {
+ auto obj = Napi::Object::New(env);
+
+ obj["pubkeyHex"] = toJs(env, decrypted.first);
+ obj["plaintext"] = toJs(env, decrypted.second);
+
+ return obj;
+}
+
} // namespace session::nodeapi
diff --git a/src/utilities.hpp b/src/utilities.hpp
index 7cdc428..1a0d8bd 100644
--- a/src/utilities.hpp
+++ b/src/utilities.hpp
@@ -3,31 +3,35 @@
#include
#include
+#include
#include
#include
+#include
#include
+#include "session/config/namespaces.hpp"
#include "session/types.hpp"
+#include "utilities.hpp"
namespace session::nodeapi {
using namespace std::literals;
-// template
-// T object_wrap_impl(const Napi::ObjectWrap&);
-// template
-// using object_wrap_t = decltype(object_wrap_impl(std::declval()));
+static void checkOrThrow(bool condition, const char* msg) {
+ if (!condition)
+ throw std::invalid_argument{msg};
+}
void assertInfoLength(const Napi::CallbackInfo& info, const int expected);
void assertInfoMinLength(const Napi::CallbackInfo& info, const int minLength);
void assertIsStringOrNull(const Napi::Value& value);
-void assertIsNumber(const Napi::Value& value);
+void assertIsNumber(const Napi::Value& value, const std::string& identifier);
void assertIsArray(const Napi::Value& value);
void assertIsObject(const Napi::Value& value);
void assertIsUInt8ArrayOrNull(const Napi::Value& value);
-void assertIsUInt8Array(const Napi::Value& value);
+void assertIsUInt8Array(const Napi::Value& value, const std::string& identifier);
void assertIsString(const Napi::Value& value);
void assertIsBoolean(const Napi::Value& value);
@@ -48,11 +52,13 @@ auto getStringArgs(const Napi::CallbackInfo& info) {
return args;
}
-
std::string toCppString(Napi::Value x, const std::string& identifier);
ustring toCppBuffer(Napi::Value x, const std::string& identifier);
ustring_view toCppBufferView(Napi::Value x, const std::string& identifier);
int64_t toCppInteger(Napi::Value x, const std::string& identifier, bool allowUndefined = false);
+std::optional maybeNonemptyInt(Napi::Value x, const std::string& identifier);
+std::optional maybeNonemptyBoolean(Napi::Value x, const std::string& identifier);
+
bool toCppBoolean(Napi::Value x, const std::string& identifier);
// If the object is null/undef/empty returns nullopt, otherwise if a String returns a std::string of
@@ -93,26 +99,38 @@ template <>
struct toJs_impl {
auto operator()(const Napi::Env& env, bool b) const { return Napi::Boolean::New(env, b); }
};
+
+template <>
+struct toJs_impl {
+ auto operator()(const Napi::Env& env, session::config::Namespace b) const {
+ return Napi::Number::New(env, static_cast(b));
+ }
+};
+
template
struct toJs_impl>> {
auto operator()(const Napi::Env& env, T n) const { return Napi::Number::New(env, n); }
};
+
template
struct toJs_impl>> {
auto operator()(const Napi::Env& env, std::string_view s) const {
return Napi::String::New(env, s.data(), s.size());
}
};
+
template
struct toJs_impl>> {
auto operator()(const Napi::Env& env, ustring_view b) const {
return Napi::Buffer::Copy(env, b.data(), b.size());
}
};
+
template
struct toJs_impl>> {
auto operator()(const Napi::Env& env, const T& val) { return val; }
};
+
template
struct toJs_impl> {
auto operator()(const Napi::Env& env, const std::vector& val) {
@@ -122,6 +140,19 @@ struct toJs_impl> {
return arr;
}
};
+
+template
+struct toJs_impl> {
+ auto operator()(const Napi::Env& env, const std::unordered_set& set) {
+ std::vector as_array(set.begin(), set.end());
+
+ auto arr = Napi::Array::New(env, as_array.size());
+ for (size_t i = 0; i < as_array.size(); i++)
+ arr[i] = toJs(env, as_array[i]);
+ return arr;
+ }
+};
+
template
struct toJs_impl> {
Napi::Value operator()(const Napi::Env& env, const std::optional& val) {
@@ -161,7 +192,8 @@ inline std::optional maybe_string(std::string_view val) {
// - The return value will be returned as-is if it is already a Napi::Value (or subtype)
// - The return will be void if void
// - Otherwise the return value will be passed through toJs() to convert it to a Napi::Value.
-// See toJs below, but generally this supports numeric types, bools, strings, ustrings, and vectors of any of those primitives.
+// See toJs below, but generally this supports numeric types, bools, strings, ustrings, and vectors
+// of any of those primitives.
//
// General use is:
//
@@ -219,4 +251,22 @@ int64_t toPriority(Napi::Value x, int64_t currentPriority);
int64_t unix_timestamp_now();
+using push_entry_t = std::tuple<
+ session::config::seqno_t,
+ session::ustring,
+ std::vector>>;
+
+Napi::Object push_result_to_JS(
+ const Napi::Env& env,
+ const push_entry_t& push_entry,
+ const session::config::Namespace& push_namespace);
+
+Napi::Object push_key_entry_to_JS(
+ const Napi::Env& env,
+ const session::ustring_view& key_data,
+ const session::config::Namespace& push_namespace);
+
+Napi::Object decrypt_result_to_JS(
+ const Napi::Env& env, const std::pair decrypted);
+
} // namespace session::nodeapi
diff --git a/types/blinding/blinding.d.ts b/types/blinding/blinding.d.ts
index 5c0ad23..d15774e 100644
--- a/types/blinding/blinding.d.ts
+++ b/types/blinding/blinding.d.ts
@@ -20,7 +20,7 @@ declare module 'libsession_util_nodejs' {
export type BlindingActionsCalls = MakeWrapperActionCalls;
/**
- * To be used inside the web worker only (calls are synchronous and won't work asynchrously)
+ * To be used inside the web worker only (calls are synchronous and won't work asynchronously)
*/
export class BlindingWrapperNode {
public static blindVersionPubkey: BlindingWrapper['blindVersionPubkey'];
diff --git a/types/groups/groupinfo.d.ts b/types/groups/groupinfo.d.ts
new file mode 100644
index 0000000..bf9cb95
--- /dev/null
+++ b/types/groups/groupinfo.d.ts
@@ -0,0 +1,10 @@
+///
+
+declare module 'libsession_util_nodejs' {
+ export type GroupInfoWrapper = {
+ // GroupInfo related methods
+ infoGet: () => GroupInfoGet;
+ infoSet: (info: GroupInfoSet) => GroupInfoGet;
+ infoDestroy: () => void;
+ };
+}
diff --git a/types/groups/groupkeys.d.ts b/types/groups/groupkeys.d.ts
new file mode 100644
index 0000000..41d92e1
--- /dev/null
+++ b/types/groups/groupkeys.d.ts
@@ -0,0 +1,27 @@
+///
+
+declare module 'libsession_util_nodejs' {
+ export type GroupKeysWrapper = {
+ // GroupKeys related methods
+ keysNeedsRekey: () => boolean;
+ keyRekey: () => Uint8Array;
+ keyGetAll: () => Array;
+ loadKeyMessage: (hash: string, data: Uint8Array, timestampMs: number) => boolean;
+ keysAdmin: () => boolean;
+ keyGetCurrentGen: () => number;
+
+ currentHashes: () => Array;
+ encryptMessages: (plaintexts: Array) => Array;
+ decryptMessage: (ciphertext: Uint8Array) => { pubkeyHex: string; plaintext: Uint8Array };
+ makeSwarmSubAccount: (memberPubkeyHex: PubkeyType) => Uint8ArrayLen100;
+ generateSupplementKeys: (membersPubkeyHex: Array) => Uint8Array;
+ swarmSubaccountSign: (
+ message: Uint8Array,
+ authData: Uint8ArrayLen100
+ ) => SwarmSubAccountSignResult;
+
+ swarmSubAccountToken: (memberPk: PubkeyType) => string; // hex encoded
+ swarmVerifySubAccount: (signingValue: Uint8ArrayLen100) => boolean;
+ loadAdminKeys: (secret: Uint8ArrayLen64) => void;
+ };
+}
diff --git a/types/groups/groupmembers.d.ts b/types/groups/groupmembers.d.ts
new file mode 100644
index 0000000..b414105
--- /dev/null
+++ b/types/groups/groupmembers.d.ts
@@ -0,0 +1,78 @@
+///
+
+declare module 'libsession_util_nodejs' {
+ /**
+ *
+ * GroupMembers wrapper logic
+ *
+ */
+ type GroupMemberShared = {
+ pubkeyHex: PubkeyType;
+ name: string | null;
+ profilePicture: ProfilePicture | null;
+ };
+
+ type MemberStateGroupV2 =
+ | 'INVITE_NOT_SENT' // as soon as we've scheduled that guy to be invited, but before we've tried sending the invite message
+ | 'INVITE_FAILED'
+ | 'INVITE_SENT'
+ | 'INVITE_ACCEPTED' // regular member
+ | 'PROMOTION_NOT_SENT' // as soon as we've scheduled that guy to be an admin, but before we've tried sending the promotion message
+ | 'PROMOTION_FAILED'
+ | 'PROMOTION_SENT'
+ | 'PROMOTION_ACCEPTED' // regular admin
+ | 'UNKNOWN';
+
+ export type GroupMemberGet = GroupMemberShared & {
+ memberStatus: MemberStateGroupV2;
+
+ /**
+ * NOT_REMOVED = 0:
+ * REMOVED_MEMBER = 1,
+ * REMOVED_MEMBER_AND_MESSAGES = 2;
+ */
+ removedStatus: 'NOT_REMOVED' | 'REMOVED_MEMBER' | 'REMOVED_MEMBER_AND_MESSAGES' | 'UNKNOWN';
+ /**
+ * True if the member is scheduled to get the keys (.admin field of libsession).
+ * This is equivalent of memberStatus being one of:
+ * - PROMOTION_NOT_SENT
+ * - PROMOTION_FAILED
+ * - PROMOTION_SENT
+ * - PROMOTION_ACCEPTED
+ */
+ nominatedAdmin: boolean;
+ };
+
+ type GroupMemberWrapper = {
+ // GroupMember related methods
+ memberGet: (pubkeyHex: PubkeyType) => GroupMemberGet | null;
+ memberGetOrConstruct: (pubkeyHex: PubkeyType) => GroupMemberGet;
+ memberConstructAndSet: (pubkeyHex: PubkeyType) => void;
+
+ memberGetAll: () => Array;
+ memberGetAllPendingRemovals: () => Array;
+
+ // setters
+ memberSetNameTruncated: (pubkeyHex: PubkeyType, newName: string) => void;
+
+ /** A member's invite state defaults to invite-not-sent. Use this function to mark that you've sent one, or at least tried (failed: boolean)*/
+ memberSetInvited: (pubkeyHex: PubkeyType, failed: boolean) => void;
+ /** User has accepted an invitation and is now a regular member of the group */
+ memberSetAccepted: (pubkeyHex: PubkeyType) => void;
+
+ /** Mark the member as waiting a promotion to be sent to them */
+ memberSetPromoted: (pubkeyHex: PubkeyType) => void;
+ /** Called when we did send the promotion to the member */
+ memberSetPromotionSent: (pubkeyHex: PubkeyType) => void;
+ /** Called when we did send the promotion to the member, but failed */
+ memberSetPromotionFailed: (pubkeyHex: PubkeyType) => void;
+ /** Called when the member accepted the promotion */
+ memberSetPromotionAccepted: (pubkeyHex: PubkeyType) => void;
+
+ memberSetProfilePicture: (pubkeyHex: PubkeyType, profilePicture: ProfilePicture) => void;
+ membersMarkPendingRemoval: (members: Array, withMessages: boolean) => void;
+
+ // eraser
+ memberEraseAndRekey: (members: Array) => boolean;
+ };
+}
diff --git a/types/groups/index.d.ts b/types/groups/index.d.ts
new file mode 100644
index 0000000..311fdbb
--- /dev/null
+++ b/types/groups/index.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file
diff --git a/types/groups/metagroup.d.ts b/types/groups/metagroup.d.ts
new file mode 100644
index 0000000..f812070
--- /dev/null
+++ b/types/groups/metagroup.d.ts
@@ -0,0 +1,169 @@
+///
+///
+///
+///
+
+declare module 'libsession_util_nodejs' {
+ export type ConfirmKeysPush = [data: Uint8Array, hash: string, timestampMs: number];
+
+ export type GroupWrapperConstructor = {
+ /**
+ * The user's ed25519 secret key, length 64.
+ */
+ userEd25519Secretkey: Uint8Array;
+ /**
+ * The group ed25519 pubkey without the 03 prefix, length 32.
+ */
+ groupEd25519Pubkey: Uint8Array;
+ /**
+ * The group ed25519 priv key if we have it (len 64). Having this means we have admin rights in the group.
+ * This usually comes from the user group wrapper if we have it.
+ */
+ groupEd25519Secretkey: Uint8Array | null;
+ /**
+ * The unified dumps (as saved in the db) for this group. i.e. Keys, Members and Info concatenated, see MetaGroupWrapper::metaDump for details.
+ */
+ metaDumped: Uint8Array | null;
+ };
+
+ type MetaGroupWrapper = GroupInfoWrapper &
+ GroupMemberWrapper &
+ GroupKeysWrapper & {
+ // shared actions
+ init: (options: GroupWrapperConstructor) => void;
+ free: () => void;
+ needsPush: () => boolean;
+ push: () => {
+ groupInfo: PushConfigResult | null;
+ groupMember: PushConfigResult | null;
+ groupKeys: PushKeyConfigResult | null;
+ };
+ needsDump: () => boolean;
+ metaDump: () => Uint8Array;
+ metaMakeDump: () => Uint8Array;
+ metaConfirmPushed: ({
+ groupInfo,
+ groupMember,
+ }: {
+ groupInfo: ConfirmPush | null;
+ groupMember: ConfirmPush | null;
+ }) => void;
+ metaMerge: ({
+ groupInfo,
+ groupKeys,
+ groupMember,
+ }: {
+ groupInfo: Array | null;
+ groupMember: Array | null;
+ groupKeys: Array | null;
+ }) => void;
+ };
+
+ // this just adds an argument of type GroupPubkeyType in front of the parameters of that function
+ type AddGroupPkToFunction any> = (
+ ...args: [GroupPubkeyType, ...Parameters]
+ ) => ReturnType;
+
+ export type MetaGroupWrapperActionsCalls = MakeWrapperActionCalls<{
+ [key in keyof MetaGroupWrapper]: AddGroupPkToFunction;
+ }>;
+
+ export class MetaGroupWrapperNode {
+ constructor(options: GroupWrapperConstructor);
+
+ // shared actions
+ public needsPush: MetaGroupWrapper['needsPush'];
+ public push: MetaGroupWrapper['push'];
+ public needsDump: MetaGroupWrapper['needsDump'];
+ public metaDump: MetaGroupWrapper['metaDump'];
+ public metaMakeDump: MetaGroupWrapper['metaMakeDump'];
+ public metaConfirmPushed: MetaGroupWrapper['metaConfirmPushed'];
+ public metaMerge: MetaGroupWrapper['metaMerge'];
+ public currentHashes: MetaGroupWrapper['currentHashes'];
+
+ // info
+ public infoGet: MetaGroupWrapper['infoGet'];
+ public infoSet: MetaGroupWrapper['infoSet'];
+ public infoDestroy: MetaGroupWrapper['infoDestroy'];
+
+ // members
+ public memberGet: MetaGroupWrapper['memberGet'];
+ public memberGetOrConstruct: MetaGroupWrapper['memberGetOrConstruct'];
+ public memberConstructAndSet: MetaGroupWrapper['memberConstructAndSet'];
+ public memberGetAll: MetaGroupWrapper['memberGetAll'];
+ public memberGetAllPendingRemovals: MetaGroupWrapper['memberGetAllPendingRemovals'];
+ public memberSetAccepted: MetaGroupWrapper['memberSetAccepted'];
+ public memberSetNameTruncated: MetaGroupWrapper['memberSetNameTruncated'];
+ public memberSetPromoted: MetaGroupWrapper['memberSetPromoted'];
+ public memberSetPromotionAccepted: MetaGroupWrapper['memberSetPromotionAccepted'];
+ public memberSetPromotionSent: MetaGroupWrapper['memberSetPromotionSent'];
+ public memberSetPromotionFailed: MetaGroupWrapper['memberSetPromotionFailed'];
+ public memberSetInvited: MetaGroupWrapper['memberSetInvited'];
+ public memberEraseAndRekey: MetaGroupWrapper['memberEraseAndRekey'];
+ public membersMarkPendingRemoval: MetaGroupWrapper['membersMarkPendingRemoval'];
+ public memberSetProfilePicture: MetaGroupWrapper['memberSetProfilePicture'];
+
+ // keys
+ public keysNeedsRekey: MetaGroupWrapper['keysNeedsRekey'];
+ public keyRekey: MetaGroupWrapper['keyRekey'];
+ public loadKeyMessage: MetaGroupWrapper['loadKeyMessage'];
+ public keysAdmin: MetaGroupWrapper['keysAdmin'];
+ public keyGetCurrentGen: MetaGroupWrapper['keyGetCurrentGen'];
+ public encryptMessages: MetaGroupWrapper['encryptMessages'];
+ public decryptMessage: MetaGroupWrapper['decryptMessage'];
+ public makeSwarmSubAccount: MetaGroupWrapper['makeSwarmSubAccount'];
+ public swarmSubaccountSign: MetaGroupWrapper['swarmSubaccountSign'];
+ }
+
+ export type MetaGroupActionsType =
+ | ['init', GroupWrapperConstructor]
+
+ // shared actions
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+
+ // info actions
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+
+ // member actions
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+
+ // keys actions
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall;
+}
diff --git a/types/index.d.ts b/types/index.d.ts
index d766c8c..0693f7e 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1 +1,5 @@
///
+///
+///
+///
+///
diff --git a/types/multi_encrypt/index.d.ts b/types/multi_encrypt/index.d.ts
new file mode 100644
index 0000000..8183bb7
--- /dev/null
+++ b/types/multi_encrypt/index.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/types/multi_encrypt/multi_encrypt.d.ts b/types/multi_encrypt/multi_encrypt.d.ts
new file mode 100644
index 0000000..499075d
--- /dev/null
+++ b/types/multi_encrypt/multi_encrypt.d.ts
@@ -0,0 +1,43 @@
+///
+
+declare module 'libsession_util_nodejs' {
+ type MultiEncryptWrapper = {
+ multiEncrypt: (opts: {
+ /**
+ * len 64: ed25519 secretKey with pubkey
+ */
+ ed25519SecretKey: Uint8ArrayLen64;
+ domain: EncryptionDomain;
+ messages: Array;
+ recipients: Array;
+ }) => Uint8Array;
+ multiDecryptEd25519: (opts: {
+ encoded: Uint8Array;
+ /**
+ * len 64: ed25519 secretKey with pubkey
+ */
+ userEd25519SecretKey: Uint8ArrayLen64;
+ senderEd25519Pubkey: Uint8Array;
+ domain: EncryptionDomain;
+ }) => Uint8Array | null;
+ };
+
+ export type MultiEncryptActionsCalls = MakeWrapperActionCalls;
+
+ /**
+ * To be used inside the web worker only (calls are synchronous and won't work asynchronously)
+ */
+ export class MultiEncryptWrapperNode {
+ public static multiEncrypt: MultiEncryptWrapper['multiEncrypt'];
+ public static multiDecryptEd25519: MultiEncryptWrapper['multiDecryptEd25519'];
+ }
+
+ /**
+ * Those actions are used internally for the web worker communication.
+ * You should never need to import them in Session directly
+ * You will need to add an entry here if you add a new function
+ */
+ export type MultiEncryptActionsType =
+ | MakeActionCall
+ | MakeActionCall;
+}
diff --git a/types/shared.d.ts b/types/shared.d.ts
new file mode 100644
index 0000000..69fbd47
--- /dev/null
+++ b/types/shared.d.ts
@@ -0,0 +1,162 @@
+declare module 'libsession_util_nodejs' {
+ type Uint8ArrayFixedLength = {
+ buffer: Uint8Array;
+ length: T;
+ };
+ /**
+ * Allow a single type to be Nullable. i.e. string => string | null
+ */
+ export type Nullable = T | null;
+
+ /**
+ * Allow all the fields of a type to be -themselves- nullable.
+ * i.e. {field1: string, field2: number} => {field1: string | null, field2: number | null}
+ */
+ type AllFieldsNullable = {
+ [P in keyof T]: Nullable;
+ };
+
+ type AsyncWrapper any> = (
+ ...args: Parameters
+ ) => Promise>;
+
+ export type RecordOfFunctions = Record any>;
+
+ type MakeWrapperActionCalls = {
+ [Property in keyof Type]: AsyncWrapper;
+ };
+
+ export type ProfilePicture = {
+ url: string | null;
+ key: Uint8Array | null;
+ };
+
+ export type PushConfigResult = {
+ data: Uint8Array;
+ seqno: number;
+ hashes: Array;
+ namespace: number;
+ };
+
+ export type PushKeyConfigResult = Pick;
+
+ export type ConfirmPush = [seqno: number, hash: string];
+ export type MergeSingle = { hash: string; data: Uint8Array };
+
+ type MakeActionCall = [B, ...Parameters];
+
+ /**
+ *
+ * Base Config wrapper logic
+ *
+ */
+
+ export type BaseConfigWrapper = {
+ needsDump: () => boolean;
+ needsPush: () => boolean;
+ push: () => PushConfigResult;
+ dump: () => Uint8Array;
+ makeDump: () => Uint8Array;
+ confirmPushed: (seqno: number, hash: string) => void;
+ merge: (toMerge: Array) => Array; // merge returns the array of hashes that merged correctly
+ storageNamespace: () => number;
+ currentHashes: () => Array;
+ };
+
+ export type GenericWrapperActionsCall = (
+ wrapperId: A,
+ ...args: Parameters
+ ) => Promise>;
+
+ export type BaseConfigActions =
+ | MakeActionCall
+ | MakeActionCall
+ | MakeActionCall