diff --git a/golos-lib-js/docs/files/msgs.md b/golos-lib-js/docs/files/msgs.md index 2cfc7d5..0ad9ace 100644 --- a/golos-lib-js/docs/files/msgs.md +++ b/golos-lib-js/docs/files/msgs.md @@ -1,10 +1,15 @@ # Личные сообщения -Блокчейн Golos предоставляет подсистему мгновенных сообщений, которая позволяет создать полноценный мессенджер с приватными, шифрованными сообщениями. Сообщения шифруются на стороне клиента (с использованием закрытого мемо-ключа отправителя и открытого мемо-ключа получателя), отправляются в блокчейн через `private_message_operation`, а затем могут быть получены из БД с помощью API `private_message` и расшифрованы на стороне клиента (с использованием закрытого мемо-ключа из from / to и публичный мемо-ключ другого пользователя). +Блокчейн Golos предоставляет подсистему мгновенных сообщений, которая позволяет создать полноценный мессенджер с приватными, шифрованными сообщениями, а также с группами с поддержкой шифрования сообщений. + +Сообщения в приватных чатах шифруются на стороне клиента (приватные: с использованием закрытого мемо-ключа отправителя и открытого мемо-ключа получателя). +Сообщения в группах, поддерживающих шифрование, шифруются на ноде блокчейна, при этом оригиналы **не** сохраняются ни в какую-либо базу данных, ни в block-log. + +Зашифрованные сообщения отправляются в блокчейн через `private_message_operation`, а затем могут быть получены из БД с помощью API `private_message` и расшифрованы на стороне клиента. ### Шифрование и отправка -Сообщения - это объекты JSON. Если вы хотите, чтобы отправленные вами сообщения отображались в Golos Messenger (в блогах, на форумах), вы должны использовать объекты JSON с полем `body`, содержащим строку с текстом сообщения, а также с полями `app` и `version`, описывающими ваше приложение. Также вы можете добавлять любые ваши собственные поля. Но если вы используете только `body`, мы рекомендуем вам установить `app` - `'golos-messenger'`, а `version` - `1`. +Сообщения - это объекты JSON. Если вы хотите, чтобы отправленные вами сообщения отображались в Golos Messenger, вы должны использовать объекты JSON с полем `body`, содержащим строку с текстом сообщения, а также с полями `app` и `version`, описывающими ваше приложение. Также вы можете добавлять любые ваши собственные поля. Но если вы используете только `body`, мы рекомендуем вам установить `app` - `'golos-messenger'`, а `version` - `1`. Для создания объекта-сообщения используйте функцию `golos.messages.newTextMsg`. @@ -13,22 +18,59 @@ - `version` должно быть целым числом, начиная с 1; - `body` должно быть строкой. -Полсе создания объекта-сообщения, следует преобразовать его в JSON-строку, зашифровать (используется SHA-512 с nonce, который является уникальным идентифиатором на основе UNIX timestamp, и затем AES), и преобразовать результат в HEX-строку (`encrypted_message`). Все это автоматически делается с помощью функции `golos.messages.encode`. +Полсе создания объекта-сообщения, следует преобразовать его в JSON-строку, а затем зашифровать. +- В приватных чатах используется SHA-512 с nonce, который является уникальным идентификатором на основе UNIX timestamp, и затем AES, после этого результат преобразуется в HEX-строку (`encrypted_message`). +- В группах, поддерживающих шифрование, JSON-строку надо отправить в `golos.api.encryptBodyAsync`, затем обернуть результат в JSON-строку формата `{"t":"em","c":"результат шифрования"}` и преобразовать ее в HEX-строку. +- В группах без шифрования, JSON-строку надо сразу преобразовать в HEX-строку. +Все это можно сделать автоматически, просто вызвав функцию `golos.messages.encodeMsg`. + +Пример для приватного чата: + +```js +let data = await golos.messages.encodeMsg({ private_memo: 'alice private memo key', + to_public_memo: 'bob public memo key', + msg: golos.messages.newTextMsg('Hello world', 'golos-messenger', 1) +}) + +const json = JSON.stringify(['private_message', { + from: 'alice', + to: 'bob', + nonce: data.nonce, + from_memo_key: data.from_memo_key, + to_memo_key: data.to_memo_key, + checksum: data.checksum, + update: false, + encrypted_message: data.encrypted_message, +}]); +golos.broadcast.customJson('alice private posting key', [], ['alice'], 'private_message', json, (err, result) => { + alert(err); + alert(JSON.stringify(result)); +}); +``` -Полный пример: +Пример для группы: ```js -let data = golos.messages.encode('alice private memo key', 'bob public memo key', golos.messages.newTextMsg('Hello world', 'golos-messenger', 1)); +// group - это объект группы, полученный через `golos.api.getGroupsAsync`, +// содержащий по меньшей мере поля `name` и `is_encrypted`, необходимые для +// правильного формирования сообщения + +let data = await golos.messages.encodeMsg({ group, + msg: golos.messages.newTextMsg('Hello world', 'golos-messenger', 1) +}) const json = JSON.stringify(['private_message', { from: 'alice', to: 'bob', nonce: data.nonce, - from_memo_key: 'alice PUBLIC memo key', - to_memo_key: 'bob public memo key', + from_memo_key: data.from_memo_key, + to_memo_key: data.to_memo_key, checksum: data.checksum, update: false, encrypted_message: data.encrypted_message, + extensions: [[0, { + group: group.name + }]], }]); golos.broadcast.customJson('alice private posting key', [], ['alice'], 'private_message', json, (err, result) => { alert(err); @@ -38,10 +80,13 @@ golos.broadcast.customJson('alice private posting key', [], ['alice'], 'private_ ### Редактирование сообщений -Сообщения идентифицируются с помощью from+to+nonce, поэтому при обновлении сообщения вы должны кодировать его тем же значением nonce, что и в предыдущей версии. +Сообщения идентифицируются с помощью group+from+to+nonce, поэтому при обновлении сообщения вы должны кодировать его тем же значением nonce, что и в предыдущей версии. ```js -data = golos.messages.encode('alice private memo key', 'bob public memo key', golos.messages.newTextMsg('Goodbye world', 'golos-messenger', 1), data.nonce); +data = await golos.messages.encodeMsg({ private_memo: 'alice private memo key', + to_public_memo: 'bob public memo key', msg: golos.messages.newTextMsg('Goodbye world', 'golos-messenger', 1), + nonce: data.nonce +}) ``` Затем эти данные должны быть отправлены с помощью операции `private_message`, как и в предыдущем случае, но с `update` = `true`. @@ -73,7 +118,7 @@ try { console.error(err); } if (msg) { - let data = golos.messages.encode('alice private memo key', 'bob public memo key', msg); + let data = await golos.messages.encodeMsg({ private_memo: 'alice private memo key', to_public_memo: 'bob public memo key', msg }) // ...и отправьте, так же, как обычное текстовое сообщение } ``` @@ -86,8 +131,9 @@ golos.messages.newImageMsg('https://site.com/https-is-recommended.jpg', (err, ms alert(err); console.error(err); } else { - let data = golos.messages.encode('alice private memo key', 'bob public memo key', msg); - // ...и отправьте, так же, как обычное текстовое сообщение + golos.messages.encodeMsg({ private_memo: 'alice private memo key', to_public_memo: 'bob public memo key', msg}).then((data) => { + // ...и отправьте, так же, как обычное текстовое сообщение + }) } }, (progress, extra_data) => { console.log('Progress: %i%', progress); @@ -97,27 +143,50 @@ golos.messages.newImageMsg('https://site.com/https-is-recommended.jpg', (err, ms ### Получение сообщений при открытии мессенджера. Расшифровка сообщений -Сообщение можно получить с помощью `golos.api.getThread`, каждое сообщение является объектом с полями `from_memo_key`, `to_memo_key`, `nonce`, `checksum`, `encrypted_message` и другими полями. Затем сообщение можно расшифровать с помощью `golos.messages.decode`, который поддерживает пакетную обработку (может расшифровать несколько сообщений одновременно) и обеспечивает высокую производительность. +Сообщение можно получить с помощью `golos.api.getThread`, каждое сообщение является объектом с полями `from_memo_key`, `to_memo_key`, `nonce`, `checksum`, `encrypted_message` и другими полями. Затем сообщение можно расшифровать с помощью `golos.messages.decodeMsgs`, который поддерживает пакетную обработку (может расшифровать несколько сообщений одновременно) и обеспечивает высокую производительность. Метод позволяет за один вызов расшифровывать сообщения даже из разных групп и разных приватных чатов (актуально для получения "последних сообщений" в списке Контактов). -:electron: `golos.messages.decode` использует WebAssembly. Перед первым действием вызовите `await golos.importNativeLib()`. [Подробнее](./wasm.md). +:electron: `golos.messages.decodeMsgs` использует WebAssembly. -```js -await golos.importNativeLib(); +Пример для приватного чата: -golos.api.getThread('alice', 'bob', {}, (err, results) => { - results = golos.messages.decode('alice private key', 'bob public memo key', results); +```js +golos.api.getThread({ from: 'alice', to: 'bob', }, (err, results) => { + results = await golos.messages.decodeMsgs({ private_memo: 'alice private key', msgs: results }) alert(results[0].message.body); }); ``` -**Примечание:** это также проверяет сообщения на соответствие следующим правилам: +В случае с группой, getThread позволяет **сразу** расшифровать сообщения. +От Алисы для этого нужен не memo-, а posting-ключ. + +```js +const results = await golos.auth.withNodeLogin({ account: 'alice', keys: { + posting: 'alice POSTING key', +}, call: async (loginData) => { + const th = await golos.api.getThreadAsync({ + ...loginData, + group: 'test-group', + }) + return th +}}) +alert(results) +``` + +Однако, перед рендерингом сообщений в uI вам все равно нужно вызвать `decodeMsgs`, чтобы: +- проверить сообщения на соответствие правилам (об этом ниже) и по результатам проверки либо исключить, либо пометить ошибочные сообщения; +- можно было вызывать `getThread` только при открытии пользователем группы, как более тяжелый, а при получении новых сообщений через Golos Notify Service вызывать лишь `decodeMsgs`. + +При этом, `decodeMsgs` расшифрует лишь те сообщения в массиве, которые не были расшифрованы до этого (встроенной расшифровкой в `getThread`, или прошлым вызовом `decodeMsgs`). +Для еще большей оптимизации вы можете кешировать сообщения, используя `before_decode` и `for_each`, как это делаем мы в коде Golos Messenger. + +**Примечание:** `decodeMsgs` также проверяет сообщения на соответствие следующим правилам: - сообщение должно быть правильным объектом JSON с полями, соответствующими следующим правилам; - поле `app` должно быть строкой длиной от 1 до 16; - поле `версия` должно быть целым числом, начиная с 1; - body должно быть строкой; - для сообщений-изображений: previewWidth и previewHeight должны быть целыми числами, которые являются результатом подгонки изображения к области 600x300 пикселей. -**Примечание:** если сообщение не может быть расшифровано, распарсено как JSON и/или проверено, оно все равно добавляется к результату, но имеет `message: null` (если не может быть распарсено как JSON или проверено) и `raw_message: null` (если вообще не может быть расшифровано). Такое поведение позволяет клиенту пометить это сообщение как прочитанное в блокчейне, но не отображать его пользователю. Если вы хотите изменить это поведение, вы можете переопределить параметр `on_error` в `golos.messages.decode` (подробнее см. в коде). +**Примечание:** если сообщение не может быть расшифровано, распарсено как JSON и/или проверено, оно все равно добавляется к результату, но имеет `message: null` (если не может быть распарсено как JSON или проверено) и `raw_message: null` (если вообще не может быть расшифровано). Такое поведение позволяет клиенту пометить это сообщение как прочитанное в блокчейне, но не отображать его пользователю. Если вы хотите изменить это поведение, вы можете переопределить параметр `on_error` в `golos.messages.decodeMsgs` (подробнее см. в коде). ### Мгновенное получение сообщений @@ -135,7 +204,7 @@ golos.api.getThread('alice', 'bob', {}, (err, results) => { Для создания диапазонов вы можете использовать `golos.messages.makeDatedGroups`, который строит такие диапазоны по условию и может превращать их в реальные операции "на лету". -Он принимает декодированные сообщения от `golos.messages.decode`. +Он принимает декодированные сообщения от `golos.messages.decodeMsgs`. **Примечание: функция должна перебирать сообщения от start к end.** @@ -211,7 +280,7 @@ msg = {...msg, ...quote}; // добавляем цитату #### Отображение сообщений с цитатами -`golos.messages.decode` поддерживает сообщения с цитатами. Каждое такое сообщение имеет поле `quote` в своем поле `сообщение`. Но, если `quote` сообщения неверна (сообщение составлено с некорректным пользовательским интерфейсом, который не использует `makeQuoteMsg` и неправильно составляет цитаты), **весь объект сообщения будет считаться некорректным**, то есть поле `message` будет `null`. +`golos.messages.decodeMsgs` поддерживает сообщения с цитатами. Каждое такое сообщение имеет поле `quote` в своем поле `сообщение`. Но, если `quote` сообщения неверна (сообщение составлено с некорректным пользовательским интерфейсом, который не использует `makeQuoteMsg` и неправильно составляет цитаты), **весь объект сообщения будет считаться некорректным**, то есть поле `message` будет `null`. #### Редактирование сообщений с цитатами diff --git a/golos-lib-js/package.json b/golos-lib-js/package.json index f5e6075..a1422d2 100644 --- a/golos-lib-js/package.json +++ b/golos-lib-js/package.json @@ -1,6 +1,6 @@ { "name": "golos-lib-js", - "version": "0.9.73", + "version": "0.9.74", "description": "Golos-js the JavaScript library with API for GOLOS blockchain", "main": "lib/index.js", "scripts": { diff --git a/golos-lib-js/src/api/methods.js b/golos-lib-js/src/api/methods.js index 84cf652..8e4adad 100644 --- a/golos-lib-js/src/api/methods.js +++ b/golos-lib-js/src/api/methods.js @@ -421,7 +421,8 @@ module.exports = [ { "api": "database_api", "method": "get_accounts", - "params": ["accountNames"] + "has_default_values": true, + "params": ["accountNames", `query=${EMPTY_OBJECT}`] }, { "api": "database_api", @@ -679,7 +680,12 @@ module.exports = [ { "api": "private_message", "method": "get_thread", - "params": ["from", "to", "query"] + "has_default_values": true, + "params": [ + "from_or_query", + `to=${EMPTY_STRING}`, + `opts=${EMPTY_OBJECT}`, + ] }, { "api": "private_message", @@ -695,12 +701,22 @@ module.exports = [ { "api": "private_message", "method": "get_contact_info", - "params": ["owner", "contact"] + "has_default_values": true, + "params": [ + "owner_or_query", + `contact=${EMPTY_STRING}`, + ] }, { "api": "private_message", "method": "get_contacts", - "params": ["owner", "type", "limit", "offset"] + "has_default_values": true, + "params": [ + "owner_or_query", + `type="unknown"`, + `limit=20`, + `offset=0`, + ] }, { "api": "private_message", @@ -793,4 +809,9 @@ module.exports = [ "method": "decrypt_comments", "params": ["query={}"] }, + { + "api": "cryptor", + "method": "decrypt_messages", + "params": ["query={}"] + }, ] diff --git a/golos-lib-js/src/auth/ecc/src/aes.js b/golos-lib-js/src/auth/ecc/src/aes.js index 470d5a3..0e2fe31 100644 --- a/golos-lib-js/src/auth/ecc/src/aes.js +++ b/golos-lib-js/src/auth/ecc/src/aes.js @@ -123,7 +123,7 @@ function cryptoJsEncrypt(message, key, iv) { /** @return {string} unique 64 bit unsigned number string. Being time based, this is careful to never choose the same nonce twice. This value could be recorded in the blockchain for a long time. */ -function uniqueNonce() { +export function uniqueNonce() { if(unique_nonce_entropy === null) { const b = secureRandom.randomUint8Array(2) unique_nonce_entropy = parseInt(b[0] << 8 | b[1], 10) diff --git a/golos-lib-js/src/auth/index.js b/golos-lib-js/src/auth/index.js index 85772db..f8b3c4d 100644 --- a/golos-lib-js/src/auth/index.js +++ b/golos-lib-js/src/auth/index.js @@ -10,7 +10,7 @@ var bigi = require('bigi'), PublicKey = require('./ecc/src/key_public'), session = require('./session'), multiSession = require('./multiSession'), - api = require('../api'), + golosApi = require('../api'), hash = require('./ecc/src/hash'); var Auth = {}; @@ -97,7 +97,7 @@ Auth.loginAsync = function (name, password, callback) { privateKeys[role] = PrivateKey.fromSeed(`${name}${role}${password}`).toString() ); } - api.getAccountsAsync([name], (err, res) => { + golosApi.getAccounts([name], (err, res) => { if (err) { callback(err, null); return; @@ -128,6 +128,60 @@ Auth.loginAsync = function (name, password, callback) { Auth.login = promisify(Auth.loginAsync); +// `keys` is object like: +// { posting: "private posting key" } +Auth.withNodeLogin = async function ({ account, keys, + call, dgp, api, sessionName }) { + if (!sessionName) sessionName = 'node_login' + if (!api) { + api = golosApi + } + + if (!dgp) { + dgp = await api.getDynamicGlobalPropertiesAsync() + } + + let resp + + const { MultiSession } = multiSession + const ms = new MultiSession(sessionName) + const sessionData = ms.load() + let loginData = sessionData.getVal(account, 'login_data') + if (loginData) { + let resp = await call(loginData) + if (!resp.login_error) { + return resp + } + } + + const { head_block_number, witness } = dgp + + console.time('withNodeLogin - signData') + const signed = this.signData(head_block_number.toString(), keys) + console.timeEnd('withNodeLogin - signData') + + // TODO: only 1st, because node supports only 1 key + const signature = Object.values(signed)[0] + + loginData = { + account, + signed_data: { + head_block_number, + witness, + }, + signature, + } + + resp = await call(loginData) + if (resp.login_error) { + throw resp.login_error + } + + sessionData.setVal(account, 'login_data', loginData).save() + + return resp +} + Auth.toWif = function (name, password, role) { var seed = name + role + password; var brainKey = seed.trim().split(/[\t\n\v\f\r ]+/).join(' '); diff --git a/golos-lib-js/src/auth/messages.js b/golos-lib-js/src/auth/messages.js index 099bdff..523a52d 100644 --- a/golos-lib-js/src/auth/messages.js +++ b/golos-lib-js/src/auth/messages.js @@ -2,11 +2,14 @@ import ByteBuffer from 'bytebuffer' import assert from 'assert' import base58 from 'bs58' +import newDebug from 'debug' import truncate from 'lodash/truncate'; import {Aes, PrivateKey, PublicKey} from './ecc' import {ops} from './serializer' +import golosApi from '../api' +import auth from '.' import {fitImageToSize} from '../utils'; -import { promisify, } from '../promisify'; +import { promisify, } from '../promisify' const {isInteger} = Number /** @const {string} DEFAULT_APP @@ -31,6 +34,10 @@ export const MAX_IMAGE_QUOTE_LENGTH = 2000; const toPrivateObj = o => (o ? o.d ? o : PrivateKey.fromWif(o) : o/*null or undefined*/) const toPublicObj = o => (o ? o.Q ? o : PublicKey.fromString(o) : o/*null or undefined*/) +export const emptyPublicKey = 'GLS1111111111111111111111111111111114T1Anm' + +const debugMsgs = newDebug('golos:messages') + function validateAppVersion(app, version) { assert(typeof app === 'string' && app.length >= 1 && app.length <= 16, 'message.app should be a string, >= 1, <= 16'); @@ -217,6 +224,58 @@ function forEachMessage(message_objects, begin_idx, end_idx, callback) { } } +function msgFromBuf(buf, lengthPrefixed = false) { + const toUTF8String = () => { + return new Buffer(buf.toString('binary'), 'binary').toString('utf-8') + } + if (!lengthPrefixed) { // Used in groups. Prefixed used in private chats + buf.mark() + return toUTF8String() + } + let rawMsg + try { + buf.mark() + rawMsg = buf.readVString() + } catch(e) { + buf.reset() + // Sender did not length-prefix the message + rawMsg = toUTF8String() + } + return rawMsg +} + +function msgFromHex(hex, lengthPrefixed = false) { + const buf = ByteBuffer.fromHex(hex, ByteBuffer.LITTLE_ENDIAN) + return msgFromBuf(buf, lengthPrefixed) +} + +const parseMsg = (message, rawMsg, raw_messages = false) => { + message.raw_message = rawMsg + message.decrypt_date = message.receive_date + if (!raw_messages) { + let msg = JSON.parse(message.raw_message) + msg.type = msg.type || 'text' + validateBody(msg.body) + if (msg.type === 'image') + validateImageMsg(msg) + validateAppVersion(msg.app, msg.version) + validateMsgWithQuote(msg) + message.message = msg + } +} + +const parseQuery = (query) => { + assert(query && typeof(query) === 'object' && !Array.isArray(query), + 'argument should be an object with argument-field. See golos-lib-js documentation.') + return query +} + +const warnDeprecation = (oldMethod, newMethod) => { + console.warn(`golos.messages.${oldMethod} deprecated. ` + + `It is not async, not supports groups, etc. ` + + `Will be removed in future. Migrate to golos.messages.${newMethod}.`) +} + /** Decodes messages of format used by golos.messages.encode(), which are length-prefixed, and also messages sent by another way (not length-prefixed).
Also, parses (JSON) and validates each message (app, version...). (Invalid messages are also added to result, it is need to mark them as read. To change it, use on_error).
@@ -234,14 +293,15 @@ function forEachMessage(message_objects, begin_idx, end_idx, callback) { @return {array} - result array of message_objects. Each object has "message" and "raw_message" fields. If message is invalid, it has only "raw_message" field. And if message cannot be decoded at all, it hasn't any of these fields. */ export function decode(private_memo_key, second_user_public_memo_key, message_objects, before_decode = undefined, for_each = undefined, on_error = undefined, begin_idx = undefined, end_idx = undefined, raw_messages = false) { + warnDeprecation('decode', 'decodeMsgs') + assert(private_memo_key, 'private_memo_key is required'); assert(second_user_public_memo_key, 'second_user_public_memo_key is required'); assert(message_objects, 'message_objects is required'); - // Most "heavy" lines const private_key = toPrivateObj(private_memo_key); const public_key = toPublicObj(second_user_public_memo_key); - let shared_secret = private_key.get_shared_secret(public_key); + let shared_secret let results = []; forEachMessage(message_objects, begin_idx, end_idx, (message_object, i) => { @@ -264,33 +324,18 @@ export function decode(private_memo_key, second_user_public_memo_key, message_ob return true; } + // Most "heavy" line + if (!shared_secret) shared_secret = private_key.get_shared_secret(public_key) + let decrypted = Aes.decrypt(shared_secret, null, message_object.nonce.toString(), Buffer.from(message_object.encrypted_message, 'hex'), message_object.checksum); const mbuf = ByteBuffer.fromBinary(decrypted.toString('binary'), ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN) - try { - mbuf.mark() - decrypted = mbuf.readVString() - } catch(e) { - mbuf.reset() - // Sender did not length-prefix the memo - decrypted = new Buffer(mbuf.toString('binary'), 'binary').toString('utf-8') - } + decrypted = msgFromBuf(mbuf, true) - decrypted = decrypted.toString(); - message_object.raw_message = decrypted; - if (!raw_messages) { - let msg = JSON.parse(message_object.raw_message); - msg.type = msg.type || 'text'; - validateBody(msg.body); - if (msg.type === 'image') - validateImageMsg(msg); - validateAppVersion(msg.app, msg.version); - validateMsgWithQuote(msg); - message_object.message = msg; - } + parseMsg(message_object, decrypted, raw_messages) } catch (exception) { if (processOnError(exception)) return true; @@ -307,6 +352,191 @@ export function decode(private_memo_key, second_user_public_memo_key, message_ob return results; } +const ExceptionTypes = { + NodeError: 1, + Error: 2, + OuterError: 3, + RequestError: 4, +} + +export async function decodeMsgs(query) { + let { msgs, before_decode, for_each, on_error, + raw_messages, + private_memo, // chats + api, login, // groups + begin_idx, end_idx, + } = parseQuery(query) + + assert(msgs, 'msgs is required') + + let private_key = private_memo && toPrivateObj(private_memo) + const myPublic = private_key && private_key.toPublic().toString() + let shareds = {} + + let results = [] + let entriesDec = {} + + forEachMessage(msgs, begin_idx, end_idx, (message, i) => { + // Return true if for_each should not be called + let processOnError = (exception, exType = ExceptionTypes.Error) => { + if (on_error) { + if (!on_error(message, i, exception, exType)) { + results.push(message) + } + return true + } + console.warn('golos.messages.decodeMsgs', i, exception) + return false + } + + try { + if (!message.group || message.decrypt_date !== message.receive_date) { + message.raw_message = null // Will be set if message will be successfully decoded + message.message = null // Will be set if message will be also successfully parsed and validated + } + + if (before_decode && before_decode(message, i, results)) { + return true + } + + if (!message.group) { + const pubKey = message.from_memo_key === myPublic ? + message.to_memo_key : message.from_memo_key + // Most "heavy" line (in private chats) + if (!shareds[pubKey]) { + shareds[pubKey] = private_key.get_shared_secret(toPublicObj(pubKey)) + } + + let decrypted = Aes.decrypt(shareds[pubKey], null, + message.nonce.toString(), + Buffer.from(message.encrypted_message, 'hex'), + message.checksum) + + const mbuf = ByteBuffer.fromBinary(decrypted.toString('binary'), ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN) + decrypted = msgFromBuf(mbuf, true) + + parseMsg(message, decrypted, raw_messages) + message.decryptor = 'golos-lib' + } else { + let rawMsg = msgFromHex(message.encrypted_message) + const isEncrypted = rawMsg.startsWith('{"t":"em"') + + if (isEncrypted) { + if (!message.decrypted || message.decrypt_date !== message.receive_date) { + entriesDec[i] = { group: message.group, + encrypted_message: message.encrypted_message, } + results.push(message) + return true + } + rawMsg = msgFromHex(message.decrypted) + } + + parseMsg(message, rawMsg, raw_messages) + } + } catch (exception) { + if (processOnError(exception)) + return true + } + try { + if (!for_each || !for_each(message, i)) { + results.push(message); + } + } catch (exception) { + console.error(exception) + processOnError(exception, ExceptionTypes.OuterError) + } + + return true + }) + + const entries = Object.values(entriesDec) + if (entries.length) { + if (!api) { + api = golosApi + } + let decRes, decErr + try { + debugMsgs('decryptMessagesAsync', entries.length + ' msgs') + + const { dgp, account, keys, sessionName } = login + decRes = await auth.withNodeLogin({ account, keys, sessionName, dgp, + call: async (loginData) => { + const decRes = await api.decryptMessagesAsync({ + ...loginData, + entries + }) + return decRes + } + }) + } catch (err) { + decErr = err + } + debugMsgs('decryptMessagesAsync', entries.length + ' msgs') + + // Return true if for_each should not be called + let processOnError = (msg, idx, exception, exType = ExceptionTypes.NodeError) => { + if (on_error) { + if (on_error(msg, idx, exception, exType)) { + msg.ignore = true + } + return true + } + console.warn('golos.messages.decodeMsgs', i, exception) + return false + } + + const idxs = Object.keys(entriesDec) + for (let j = 0; j < entries.length; ++j) { + let i = idxs[j] + let msg = msgs[i] + + try { + let proceedId = true + if (!decRes) { + proceedId = false + const ex = new Error((decErr && decErr.message) || 'Unknown error') + if (processOnError(msg, i, decErr, ExceptionTypes.RequestError)) + continue + } else if (decRes.status !== 'ok' || !decRes.results) { + proceedId = false + const ex = new Error(decRes.err || 'Unknown error') + if (processOnError(msg, i, ex)) + continue + } + + if (proceedId) { + const dec = decRes.results[j] + if (dec && dec.body) { + dec.decrypted = dec.body // TODO: but `decrypted` should be hex + parseMsg(msg, dec.decrypted, raw_messages) + msg.decryptor = 'node/decrypt_messages' + } else { + const ex = new Error(dec.err || (!dec ? ' No decrypt result' : 'No body') + ', unknown error') + if (processOnError(msg, i, ex)) + continue + } + } + } catch (err) { + if (processOnError(msg, i, exception, ExceptionTypes.Error)) + continue + } + + try { + if (!msg.ignore && for_each && for_each(msg, i)) { + msg.ignore = true + } + } catch (exception) { + console.error(exception) + processOnError(exception, ExceptionTypes.OuterError) + } + } + } + + results = results.filter(res => !res.ignore) + + return results +} + /** Encodes message to send with private_message_operation. Converts object to JSON string. Uses writeVString, so format of data to encode is string length + string. @arg {string|PrivateKey} from_private_memo_key - private memo key of "from" @@ -316,6 +546,8 @@ export function decode(private_memo_key, second_user_public_memo_key, message_ob @return {object} - Object with fields: nonce, checksum and message. */ export function encode(from_private_memo_key, to_public_memo_key, message, nonce = undefined) { + warnDeprecation('encode', 'encodeMsg') + assert(from_private_memo_key, 'from_private_memo_key is required'); assert(to_public_memo_key, 'to_public_memo_key is required'); assert(message, 'message is required'); @@ -339,6 +571,85 @@ export function encode(from_private_memo_key, to_public_memo_key, message, nonce }; } +export async function encodeMsg({ group, + private_memo, to_public_memo, + msg, nonce, + api +}) { + assert(msg, 'msg is required') + + const msgToBuffer = (msg) => { + const mbuf = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN) + if (group) { + mbuf.writeUTF8String(JSON.stringify(msg)) + } else { + mbuf.writeVString(JSON.stringify(msg)) + } + msg = new Buffer(mbuf.copy(0, mbuf.offset).toBinary(), 'binary') + return msg + } + + if (!group) { + assert(private_memo, 'private_memo is required in private chats') + assert(to_public_memo, 'to_public_memo is required in private chats') + + const fromKey = toPrivateObj(private_memo) + const toKey = toPublicObj(to_public_memo) + + msg = msgToBuffer(msg) + + let data = Aes.encrypt(fromKey, + toKey, + msg, + nonce) + + return { + nonce: data.nonce.toString(), + encrypted_message: data.message.toString('hex'), + checksum: data.checksum, + from_memo_key: fromKey.toPublic().toString(), + to_memo_key: to_public_memo, + } + } + + assert(!private_memo, 'private_memo is for private messages, not groups') + assert(!to_public_memo, 'to_public_memo is for private messages, not groups') + + if (group.is_encrypted) { + if (!api) { + api = golosApi + } + const body = JSON.stringify(msg) + + let res + { + debugMsgs('encryptBodyAsync') + res = await api.encryptBodyAsync({ group: group.name, body }) + debugMsgs('encryptBodyAsync') + } + + if (res.error) { + throw new Error(res.error) + } + + let newBody = {} + newBody.t = 'em' + newBody.c = res.encrypted + msg = msgToBuffer(newBody) + } else { + msg = msgToBuffer(msg) + } + + const encrypted_message = msg.toString('hex') + return { + nonce: Aes.uniqueNonce().toString(), + encrypted_message, + checksum: 0, + from_memo_key: emptyPublicKey, + to_memo_key: emptyPublicKey, + } +} + /** Selects messages by condition (e.g unread, or selected by user), and groups them into ranges with `nonce` (if range has 1 message) or `start_date`+`stop_date` (if range has few messages). Can wrap these ranges into operations: `private_mark_message` and `private_delete_message`. @arg {array} message_objects - array of message objects. It can be result array from `golos.messages.decode`. diff --git a/golos-lib-js/test/messages.test.js b/golos-lib-js/test/messages.test.js index 0c8e12a..c2625d9 100644 --- a/golos-lib-js/test/messages.test.js +++ b/golos-lib-js/test/messages.test.js @@ -1,6 +1,6 @@ import { assert } from 'chai'; import cloneDeep from 'lodash/cloneDeep'; -import { encode, decode, newTextMsg, newImageMsgAsync, makeQuoteMsg, +import { encode, encodeMsg, decode, decodeMsgs, newTextMsg, newImageMsgAsync, makeQuoteMsg, DEFAULT_APP, DEFAULT_VERSION, MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT, MAX_TEXT_QUOTE_LENGTH, MAX_IMAGE_QUOTE_LENGTH } from '../src/auth/messages'; import th from './test_helper'; @@ -17,22 +17,24 @@ const bob = { memo_pub: 'GLS67bFM2GtnEcrayTquHXdA8QdgRUgmGtUTQK24ez3uz4XLDShzc', }; -describe('golos.messages: encode()', function() { +describe('golos.messages: encodeMsg()', function() { beforeEach(async function() { await importNativeLib(); }); - it('input arguments', function() { - assert.throws(() => encode(), 'from_private_memo_key is required'); - assert.throws(() => encode(alice.memo), 'to_public_memo_key is required'); - assert.throws(() => encode(alice.memo, bob.memo_pub), 'message is required'); + it('input arguments', async function() { + await assert.isRejected(encodeMsg({}), 'msg is required'); + await assert.isRejected(encodeMsg({ msg: 1 }), 'private_memo is required in private chats'); + await assert.isRejected(encodeMsg({ msg: 1, private_memo: alice.memo }), + 'to_public_memo is required in private chats'); - assert.throws(() => encode(1, alice.memo, bob.memo_pub)); + await assert.isRejected(encodeMsg({ msg: 1, private_memo: 1, to_public_memo: alice.memo })) }) - it('normal case', function() { + it('normal case', async function() { var msg = newTextMsg('Привет'); - var res = encode(alice.memo, bob.memo_pub, msg); + var res = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) assert.isString(res.nonce); assert.isNotEmpty(res.nonce); @@ -43,7 +45,7 @@ describe('golos.messages: encode()', function() { assert.isNotEmpty(res.encrypted_message); }) - it('cyrillic, emoji, etc', function() { + it('cyrillic, emoji, etc', async function() { var veryLong = 'Очень'; for (let i = 0; i < 100; ++i) { veryLong += ' длинный текст. Длинный текст.\nОчень'; @@ -63,76 +65,114 @@ describe('golos.messages: encode()', function() { veryLong, ]) { var msg = newTextMsg(str); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) msg.type = 'text'; - var res = decode(bob.memo, alice.memo_pub, [enc]); + var res = await decodeMsgs({ private_memo: bob.memo, msgs: [enc] }) assert.lengthOf(res, 1); assert.deepStrictEqual(res[0].message, msg); } }) - it('alice -> alice, bob', function() { + it('alice -> alice, bob', async function() { var msg = newTextMsg('Привет'); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) msg.type = 'text'; - var res = decode(alice.memo, bob.memo_pub, [enc]); + var res = await decodeMsgs({ private_memo: alice.memo, + msgs: [enc] }); assert.lengthOf(res, 1); + console.error(res[0].raw_message) + console.error(res[0].message) assert.deepStrictEqual(res[0].message, msg); - var res = decode(bob.memo, alice.memo_pub, [enc]); + var res = await decodeMsgs({ private_memo: bob.memo, msgs: [enc] }) assert.lengthOf(res, 1); assert.deepStrictEqual(res[0].message, msg); }) - it('alice -> alice', function() { + it('alice -> alice', async function() { var msg = newTextMsg('Привет'); - var enc = encode(alice.memo, alice.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: alice.memo_pub, msg }) msg.type = 'text'; - var res = decode(alice.memo, alice.memo_pub, [enc]); + var res = await decodeMsgs({ private_memo: alice.memo, msgs: [enc] }) assert.lengthOf(res, 1); assert.deepStrictEqual(res[0].message, msg); - var res = decode(bob.memo, alice.memo_pub, [enc]); - assert.lengthOf(res, 1); - assert.isNull(res[0].message); - - var res = decode(alice.memo, bob.memo_pub, [enc]); + var res = await decodeMsgs({ private_memo: bob.memo, msgs: [enc] }) assert.lengthOf(res, 1); assert.isNull(res[0].message); }) - it('edit case', function() { + it('edit case', async function() { var msg = newTextMsg('Привет'); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) msg.type = 'text'; - var res = decode(bob.memo, alice.memo_pub, [enc]); + var res = await decodeMsgs({ private_memo: bob.memo, msgs: [enc] }) assert.lengthOf(res, 1); assert.deepStrictEqual(res[0].message, msg); var msg = newTextMsg('Приветик'); - var enc = encode(alice.memo, bob.memo_pub, msg, enc.nonce); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg, nonce: enc.nonce }) msg.type = 'text'; - var res2 = decode(bob.memo, alice.memo_pub, [enc]); + var res2 = await decodeMsgs({ private_memo: bob.memo, msgs: [enc] }) assert.lengthOf(res2, 1); assert.deepStrictEqual(res2[0].message, msg); assert.strictEqual(res2[0].nonce, res[0].nonce); assert.strictEqual(res2[0].checksum, res[0].checksum); }) + + it('normal + edit case, with legacy encode() and decode()', async function() { + var msg = newTextMsg('Привет') + var enc = encode(alice.memo, bob.memo_pub, msg) + msg.type = 'text' + + assert.isString(enc.nonce) + assert.isNotEmpty(enc.nonce) + + assert.isTrue(Number.isInteger(enc.checksum)) + + assert.isString(enc.encrypted_message) + assert.isNotEmpty(enc.encrypted_message) + + // these are need for compatibility with decodeMsgs() + enc.from_memo_key = alice.memo_pub + enc.to_memo_key = bob.memo_pub + + var res = await decodeMsgs({ private_memo: bob.memo, msgs: [enc] }) + assert.lengthOf(res, 1) + assert.deepStrictEqual(res[0].message, msg) + + var msg = newTextMsg('Приветик') + var enc = encode(alice.memo, bob.memo_pub, msg, enc.nonce) + msg.type = 'text' + + var res2 = decode(bob.memo, alice.memo_pub, [enc]) + assert.lengthOf(res2, 1) + assert.deepStrictEqual(res2[0].message, msg) + assert.strictEqual(res2[0].nonce, res[0].nonce) + assert.strictEqual(res2[0].checksum, res[0].checksum) + }) }) -describe('golos.messages: decode()', function() { +describe('golos.messages: decodeMsgs()', function() { before(async function () { var msg = newTextMsg('Привет'); - var msgEnc = encode(alice.memo, bob.memo_pub, msg); + var msgEnc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) msg.type = 'text'; // for comparison var msg2 = await newImageMsgAsync(correctImageURL); - var msgEnc2 = encode(alice.memo, bob.memo_pub, msg2); + var msgEnc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) this._msgs = [ msg, @@ -143,11 +183,13 @@ describe('golos.messages: decode()', function() { this._msgObjs = [ msgEnc, msgEnc2, - encode(alice.memo, alice.memo_pub, {}), - encode(alice.memo, bob.memo_pub, {}), + await encodeMsg({ private_memo: alice.memo, + to_public_memo: alice.memo_pub, msg: {} }), + await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: {} }), ]; - this._decoded = decode(bob.memo, alice.memo_pub, this._msgObjs); + this._decoded = await decodeMsgs({ private_memo: bob.memo, msgs: this._msgObjs }) }) beforeEach(function() { // encode/decode are slow, so we just cloning instead of recreating @@ -157,14 +199,9 @@ describe('golos.messages: decode()', function() { }) it('input arguments', async function() { - assert.throws(() => decode(), 'private_memo_key is required'); - assert.throws(() => decode(null), 'private_memo_key is required'); - assert.throws(() => decode(alice.memo), 'second_user_public_memo_key is required'); - assert.throws(() => decode(alice.memo, null), 'second_user_public_memo_key is required'); - assert.throws(() => decode(alice.memo, bob.memo_pub), 'message_objects is required'); - - assert.throws(() => decode('wrong key', bob.memo_pub, []), 'Non-base58 character'); - assert.throws(() => decode(alice.memo, 'wrong key', [])); + await assert.isRejected(decodeMsgs(), 'argument should be an object with argument-field. See golos-lib-js documentation.'); + await assert.isRejected(decodeMsgs({}), 'msgs is required') + await assert.isRejected(decodeMsgs({ msgs: [], private_memo: 'wrong key' }), 'Non-base58 character') }) it('validation', async function() { @@ -178,9 +215,12 @@ describe('golos.messages: decode()', function() { // non-decodable { - let msg = encode(alice.memo, alice.memo_pub, normalText); + let msg = await encodeMsg({ private_memo: alice.memo, + to_public_memo: alice.memo_pub, msg: normalText }) messages.push({ nonce: msg.nonce, + from_memo_key: msg.from_memo_key, + to_memo_key: msg.to_memo_key, checksum: 1, encrypted_message: 'not encrypted', }); @@ -191,16 +231,20 @@ describe('golos.messages: decode()', function() { return data; // as it is }); - messages.push(encode(alice.memo, bob.memo_pub, 'не json')); + messages.push(await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: 'не json' })); JSON.stringify.restore(); } // JSON, but not object - messages.push(encode(alice.memo, bob.memo_pub, 'Привет')); + messages.push(await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: 'Привет' })) // no body - messages.push(encode(alice.memo, bob.memo_pub, {})); + messages.push(await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: {} })); // no app, version - messages.push(encode(alice.memo, bob.memo_pub, {body: 'Привет'})); + messages.push(await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: {body: 'Привет'} })); // normal text msgs for (let i = 0; i < 50 - 5; ++i) { @@ -212,7 +256,7 @@ describe('golos.messages: decode()', function() { messages.push(normalImageEnc); } - var res = decode(bob.memo, alice.memo_pub, messages); + var res = await decodeMsgs({ private_memo: bob.memo, msgs: messages }) assert.lengthOf(res, 100); // non-decodable @@ -242,10 +286,11 @@ describe('golos.messages: decode()', function() { // With on_error var on_error = sandbox.spy(); - var res2 = decode(bob.memo, alice.memo_pub, cloneDeep(messages), - undefined, undefined, (msg, idx, err) => { + var res2 = await decodeMsgs({ private_memo: bob.memo, msgs: cloneDeep(messages), + on_error: (msg, idx, err) => { on_error(msg, idx, err.message); - }); + } + }) on_error = on_error.getCalls(); assert.lengthOf(on_error, 5); @@ -265,10 +310,10 @@ describe('golos.messages: decode()', function() { // With raw_messages var on_error_raw = sandbox.spy(); - var res3 = decode(bob.memo, alice.memo_pub, cloneDeep(messages), - undefined, undefined, (msg, idx, err) => { - on_error_raw(msg, idx, err.message); - }, undefined, undefined, true); + var res3 = await decodeMsgs({ private_memo: bob.memo, msgs: cloneDeep(messages), + on_error: (msg, idx, err) => { + on_error_raw(msg, idx, err.message) + }, raw_messages: true}) on_error_raw = on_error_raw.getCalls(); assert.lengthOf(on_error_raw, 1); @@ -308,31 +353,33 @@ describe('golos.messages: decode()', function() { // normal messages.push(normalImageEnc); - var addMsg = ((breaker) => { + var addMsg = async (breaker) => { var msg = Object.assign({}, normalImage); breaker(msg); - msg = encode(alice.memo, bob.memo_pub, msg); + msg = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) messages.push(msg); - }); + }; // no body - addMsg(msg => delete msg.body); + await addMsg(msg => delete msg.body); // no app - addMsg(msg => delete msg.app); + await addMsg(msg => delete msg.app); // previewWidth, previewHeight problems - addMsg(msg => delete msg.previewWidth); - addMsg(msg => msg.previewWidth = '12px'); - addMsg(msg => msg.previewWidth = 0); - addMsg(msg => msg.previewWidth = MAX_PREVIEW_WIDTH + 1); - addMsg(msg => delete msg.previewHeight); - addMsg(msg => msg.previewHeight = '12px'); - addMsg(msg => msg.previewHeight = 0); - addMsg(msg => msg.previewHeight = MAX_PREVIEW_HEIGHT + 1); + await addMsg(msg => delete msg.previewWidth); + await addMsg(msg => msg.previewWidth = '12px'); + await addMsg(msg => msg.previewWidth = 0); + await addMsg(msg => msg.previewWidth = MAX_PREVIEW_WIDTH + 1); + await addMsg(msg => delete msg.previewHeight); + await addMsg(msg => msg.previewHeight = '12px'); + await addMsg(msg => msg.previewHeight = 0); + await addMsg(msg => msg.previewHeight = MAX_PREVIEW_HEIGHT + 1); var on_error = sandbox.spy(); - var res = decode(bob.memo, alice.memo_pub, messages, - undefined, undefined, (msg, idx, err) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: messages, + on_error: (msg, idx, err) => { on_error(msg, idx, err.message); - }); + } + }) on_error = on_error.getCalls(); assert.lengthOf(on_error, 10); @@ -357,10 +404,11 @@ describe('golos.messages: decode()', function() { it('on_error without return', async function() { var on_error = sandbox.spy(); - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, undefined, undefined, - (msg, i, ex) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + on_error: (msg, i, ex) => { on_error(msg, i, ex.message); - }); + } + }) assert.deepStrictEqual(res, this.decoded); on_error = on_error.getCalls(); @@ -370,18 +418,20 @@ describe('golos.messages: decode()', function() { }) it('on_error with return false', async function() { - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, undefined, undefined, - (msg, i, ex) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + on_error: (msg, i, ex) => { return false; - }); + } + }) assert.deepStrictEqual(res, this.decoded); }) it('on_error with return true', async function() { - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, undefined, undefined, - (msg, i, ex) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + on_error: (msg, i, ex) => { return true; - }); + } + }) assert.lengthOf(res, 2); assert.deepStrictEqual(res[0].message, this.msgs[0]); assert.deepStrictEqual(res[1].message, this.msgs[1]); @@ -389,10 +439,11 @@ describe('golos.messages: decode()', function() { it('for_each without return', async function() { var for_each = sandbox.spy(); - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, undefined, - (msg, i) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + for_each: (msg, i) => { for_each(msg, i); - }); + } + }) assert.deepStrictEqual(res, this.decoded); for_each = for_each.getCalls(); @@ -403,18 +454,20 @@ describe('golos.messages: decode()', function() { }) it('for_each with return false', async function() { - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, undefined, - (msg, i) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + for_each: (msg, i) => { return false; - }); + } + }) assert.deepStrictEqual(res, this.decoded); }) it('for_each with return true', async function() { - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, undefined, - (msg, i) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + for_each: (msg, i) => { if (i % 2 != 0) return true; - }); + } + }) assert.deepStrictEqual(res[0], this.decoded[0]); assert.deepStrictEqual(res[1], this.decoded[2]); }) @@ -422,12 +475,14 @@ describe('golos.messages: decode()', function() { it('for_each with on_error shouldn\'t be called if error occured', async function() { var for_each = sandbox.spy(); var on_error = sandbox.spy(); - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, undefined, - (msg, i) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + for_each: (msg, i) => { for_each(msg, i); - }, (msg, i, exception) => { + }, + on_error: (msg, i, exception) => { on_error(msg, i, exception.message); - }); + } + }) assert.lengthOf(res, this.msgObjs.length); for_each = for_each.getCalls(); @@ -443,13 +498,15 @@ describe('golos.messages: decode()', function() { it('for_each throws, on_error decides push result or not', async function() { var on_error = sandbox.spy(); - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, undefined, - (msg, i) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + for_each: (msg, i) => { throw new Error('for_each fail'); - }, (msg, i, exception) => { + }, + on_error: (msg, i, exception) => { on_error(msg, i, exception.message); if (i === 2) return true; // do not push - }); + } + }) assert.lengthOf(res, 3); @@ -466,11 +523,12 @@ describe('golos.messages: decode()', function() { for (let obj of this.decoded) obj.field = 'test'; var before_decode = sandbox.spy(); - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, - (msg, i, results) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + before_decode: (msg, i, results) => { msg.field = 'test'; before_decode(msg, i, results); - }); + } + }) assert.deepStrictEqual(res, this.decoded); @@ -487,11 +545,12 @@ describe('golos.messages: decode()', function() { for (let obj of this.decoded) obj.field = 'test'; // before_decode with return false - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, - (msg, i, results) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + before_decode: (msg, i, results) => { msg.field = 'test'; return false; - }); + } + }) assert.deepStrictEqual(res, this.decoded); }) @@ -500,11 +559,12 @@ describe('golos.messages: decode()', function() { // for comparison for (let obj of this.decoded) obj.field = 'test'; - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, - (msg, i, results) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + before_decode: (msg, i, results) => { msg.field = 'test'; return i % 2 != 0; - }); + } + }) assert.lengthOf(res, 2); assert.deepStrictEqual(res[0], this.decoded[0]); @@ -513,11 +573,12 @@ describe('golos.messages: decode()', function() { it('before_decode throws without on_error', async function() { var before_decode = sandbox.spy(); - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, - (msg, i, results) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + before_decode: (msg, i, results) => { before_decode(msg, i, results); throw new Error('before_decode fail'); - }); + } + }) before_decode = before_decode.getCalls(); @@ -531,13 +592,15 @@ describe('golos.messages: decode()', function() { }) it('before_decode throws with on_error', async function() { - var res = decode(bob.memo, alice.memo_pub, this.msgObjs, - (msg, i, results) => { + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + before_decode: (msg, i, results) => { throw new Error('before_decode fail'); - }, undefined, (msg, i, err) => { + }, + on_error: (msg, i, err) => { assert.equal(err.message, 'before_decode fail'); msg.message = 'alt msg'; - }); + } + }) assert.lengthOf(res, this.msgObjs.length); for (let i = 0; i < res.length; ++i) { @@ -548,25 +611,25 @@ describe('golos.messages: decode()', function() { it('ordering + slicing', async function() { // default case - var res = decode(bob.memo, alice.memo_pub, this.msgObjs); + var res = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs }) // reversed order case - var resRev = decode(bob.memo, alice.memo_pub, this.msgObjs, - undefined, undefined, undefined, this.msgObjs.length - 1, -1); + var resRev = await decodeMsgs({ private_memo: bob.memo, msgs: this.msgObjs, + begin_idx: this.msgObjs.length - 1, end_idx: -1 }) assert.deepStrictEqual([...resRev].reverse(), res); // reversed + slicing - var resSl = decode(bob.memo, alice.memo_pub, cloneDeep(this.msgObjs), - undefined, undefined, undefined, this.msgObjs.length - 2, 0); + var resSl = await decodeMsgs({ private_memo: bob.memo, msgs: cloneDeep(this.msgObjs), + begin_idx: this.msgObjs.length - 2, end_idx: 0 }) assert.lengthOf(resSl, 2); assert.deepStrictEqual(resSl[0], resRev[1]); assert.deepStrictEqual(resSl[1], resRev[2]); // default + slicing by begin_idx only - var resSl = decode(bob.memo, alice.memo_pub, cloneDeep(this.msgObjs), - undefined, undefined, undefined, 2); + var resSl = await decodeMsgs({ private_memo: bob.memo, msgs: cloneDeep(this.msgObjs), + begin_idx: 2 }) assert.lengthOf(resSl, 2); assert.deepStrictEqual(resSl[0], res[2]); assert.deepStrictEqual(resSl[1], res[3]); @@ -574,23 +637,25 @@ describe('golos.messages: decode()', function() { }) describe('golos.messages: decode() replies', function() { - it('message', function() { + it('message', async function() { var msg = newTextMsg('Привет'); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) const orig = Object.assign({ from: 'alice', }, enc); - var origDecoded = decode(alice.memo, bob.memo_pub, [orig]); + var origDecoded = await decodeMsgs({ private_memo: alice.memo, msgs: [orig] }) var msg2 = newTextMsg('Hi'); msg2 = makeQuoteMsg(msg2, origDecoded[0]); - var enc2 = encode(alice.memo, bob.memo_pub, msg2); + var enc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) const reply = Object.assign({ from: 'bob', }, enc2); - var bothDecoded = decode(alice.memo, bob.memo_pub, [orig, enc2]); + var bothDecoded = await decodeMsgs({ private_memo: alice.memo, msgs: [orig, enc2] }) assert.lengthOf(bothDecoded, 2); assert.strictEqual(bothDecoded[1].message.quote.from, bothDecoded[0].from); assert.strictEqual(bothDecoded[1].message.quote.nonce, bothDecoded[0].nonce); @@ -598,25 +663,27 @@ describe('golos.messages: decode() replies', function() { assert.strictEqual(bothDecoded[1].message.quote.type, bothDecoded[0].message.type); }) - it('too long message', function() { + it('too long message', async function() { var msg = newTextMsg('a'.repeat(MAX_TEXT_QUOTE_LENGTH + 1)); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) const orig = Object.assign({ from: 'alice', }, enc); - var origDecoded = decode(alice.memo, bob.memo_pub, [orig]); + var origDecoded = await decodeMsgs({ private_memo: alice.memo, msgs: [orig] }) var msg2 = newTextMsg('Hi'); msg2 = makeQuoteMsg(msg2, origDecoded[0]); // keep its original length to make message invalid msg2.quote.body = 'a'.repeat(MAX_TEXT_QUOTE_LENGTH + 1); - var enc2 = encode(alice.memo, bob.memo_pub, msg2); + var enc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) const reply = Object.assign({ from: 'bob', }, enc2); - var bothDecoded = decode(alice.memo, bob.memo_pub, [orig, enc2]); + var bothDecoded = await decodeMsgs({ private_memo: alice.memo, msgs: [orig, enc2] }) assert.lengthOf(bothDecoded, 2); assert.isNotNull(bothDecoded[1].raw_message); assert.isNull(bothDecoded[1].message); @@ -626,21 +693,23 @@ describe('golos.messages: decode() replies', function() { assert.isTrue(correctImageURL.length > MAX_TEXT_QUOTE_LENGTH, 'too short correctImageURL for this test'); var msg = await newImageMsgAsync(correctImageURL); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) const orig = Object.assign({ from: 'alice', }, enc); - var origDecoded = decode(alice.memo, bob.memo_pub, [orig]); + var origDecoded = await decodeMsgs({ private_memo: alice.memo, msgs: [orig] }) var msg2 = newTextMsg('Hi'); msg2 = makeQuoteMsg(msg2, origDecoded[0]); - var enc2 = encode(alice.memo, bob.memo_pub, msg2); + var enc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) const reply = Object.assign({ from: 'bob', }, enc2); - var bothDecoded = decode(alice.memo, bob.memo_pub, [orig, enc2]); + var bothDecoded = await decodeMsgs({ private_memo: alice.memo, msgs: [orig, enc2] }) assert.lengthOf(bothDecoded, 2); assert.strictEqual(bothDecoded[1].message.quote.from, bothDecoded[0].from); assert.strictEqual(bothDecoded[1].message.quote.nonce, bothDecoded[0].nonce); @@ -657,21 +726,23 @@ describe('golos.messages: decode() replies', function() { previewWidth: MAX_PREVIEW_WIDTH, previewHeight: MAX_PREVIEW_HEIGHT, }; - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) const orig = Object.assign({ from: 'alice', }, enc); - var origDecoded = decode(alice.memo, bob.memo_pub, [orig]); + var origDecoded = await decodeMsgs({ private_memo: alice.memo, msgs: [orig] }) var msg2 = newTextMsg('Hi'); msg2 = makeQuoteMsg(msg2, origDecoded[0]); - var enc2 = encode(alice.memo, bob.memo_pub, msg2); + var enc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) const reply = Object.assign({ from: 'bob', }, enc2); - var bothDecoded = decode(alice.memo, bob.memo_pub, [orig, enc2]); + var bothDecoded = await decodeMsgs({ private_memo: alice.memo, msgs: [orig, enc2] }) assert.lengthOf(bothDecoded, 2); assert.strictEqual(bothDecoded[1].message.quote.from, bothDecoded[0].from); assert.strictEqual(bothDecoded[1].message.quote.nonce, bothDecoded[0].nonce); diff --git a/golos-lib-js/test/methods_by_version.js b/golos-lib-js/test/methods_by_version.js index dc4eef0..fb279fd 100644 --- a/golos-lib-js/test/methods_by_version.js +++ b/golos-lib-js/test/methods_by_version.js @@ -135,5 +135,6 @@ export const methods_0_25_3 = [ "get_nft_orders", "get_nft_bets", "encrypt_body", - "decrypt_comments" + "decrypt_comments", + "decrypt_messages" ]