golos-lib-js
Version:
Golos-js the JavaScript library with API for GOLOS blockchain
734 lines (722 loc) • 31.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.MAX_TEXT_QUOTE_LENGTH = exports.MAX_PREVIEW_WIDTH = exports.MAX_PREVIEW_HEIGHT = exports.MAX_IMAGE_QUOTE_LENGTH = exports.DEFAULT_VERSION = exports.DEFAULT_APP = void 0;
exports.decode = decode;
exports.decodeMsgs = decodeMsgs;
exports.emptyPublicKey = void 0;
exports.encode = encode;
exports.encodeMsg = encodeMsg;
exports.makeDatedGroups = makeDatedGroups;
exports.makeQuoteMsg = makeQuoteMsg;
exports.newTextMsg = newTextMsg;
var _bytebuffer = _interopRequireDefault(require("bytebuffer"));
var _assert = _interopRequireDefault(require("assert"));
var _bs = _interopRequireDefault(require("bs58"));
var _debug = _interopRequireDefault(require("debug"));
var _truncate = _interopRequireDefault(require("lodash/truncate"));
var _ecc = require("./ecc");
var _serializer = require("./serializer");
var _api = _interopRequireDefault(require("../api"));
var _ = _interopRequireDefault(require("."));
var _utils = require("../utils");
var _promisify = require("../promisify");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const {
isInteger
} = Number;
/** @const {string} DEFAULT_APP
@default 'golos-messenger' */
const DEFAULT_APP = exports.DEFAULT_APP = 'golos-messenger';
/** @const {string} DEFAULT_VERSION
@default 1 */
const DEFAULT_VERSION = exports.DEFAULT_VERSION = 1;
/** @const {string} MAX_PREVIEW_WIDTH
@default 600 */
const MAX_PREVIEW_WIDTH = exports.MAX_PREVIEW_WIDTH = 600;
/** @const {string} MAX_PREVIEW_HEIGHT
@default 300 */
const MAX_PREVIEW_HEIGHT = exports.MAX_PREVIEW_HEIGHT = 300;
/** @const {string} MAX_TEXT_QUOTE_LENGTH
@default 200 */
const MAX_TEXT_QUOTE_LENGTH = exports.MAX_TEXT_QUOTE_LENGTH = 200;
/** @const {string} MAX_IMAGE_QUOTE_LENGTH
@default 2000 */
const MAX_IMAGE_QUOTE_LENGTH = exports.MAX_IMAGE_QUOTE_LENGTH = 2000;
const toPrivateObj = o => o ? o.d ? o : _ecc.PrivateKey.fromWif(o) : o /*null or undefined*/;
const toPublicObj = o => o ? o.Q ? o : _ecc.PublicKey.fromString(o) : o /*null or undefined*/;
const emptyPublicKey = exports.emptyPublicKey = 'GLS1111111111111111111111111111111114T1Anm';
const debugMsgs = (0, _debug.default)('golos:messages');
function validateAppVersion(app, version) {
(0, _assert.default)(typeof app === 'string' && app.length >= 1 && app.length <= 16, 'message.app should be a string, >= 1, <= 16');
(0, _assert.default)(isInteger(version) && version >= 1, 'message.version should be an integer, >= 1');
}
function validateBody(body) {
(0, _assert.default)(typeof body === 'string', 'message.body should be a string');
}
function validateImageMsg(msg) {
(0, _assert.default)(isInteger(msg.previewWidth) && msg.previewWidth >= 1 && msg.previewWidth <= MAX_PREVIEW_WIDTH, 'message.previewWidth (for image) should be an integer, >= 1, <= ' + MAX_PREVIEW_WIDTH);
(0, _assert.default)(isInteger(msg.previewHeight) && msg.previewHeight >= 1 && msg.previewHeight <= MAX_PREVIEW_HEIGHT, 'message.previewHeight (for image) should be an integer, >= 1, <= ' + MAX_PREVIEW_HEIGHT);
}
function validateMsgWithQuote(msg) {
if (msg.quote !== undefined) {
(0, _assert.default)(typeof msg.quote === 'object', 'message.quote should be an object or undefined at all');
(0, _assert.default)(typeof msg.quote.from === 'string' && msg.quote.from.length >= 1 && msg.quote.from.length <= 16, 'message.quote.from should be a valid nickname of sender of message, to which this reply is');
(0, _assert.default)(typeof msg.quote.nonce === 'string' && msg.quote.nonce.length, 'message.quote.nonce should be a valid nonce of message, to which this reply is');
if (msg.quote.type !== 'image') {
(0, _assert.default)(typeof msg.quote.body === 'string' && msg.quote.body.length <= MAX_TEXT_QUOTE_LENGTH, 'message.quote.body should be a truncated (length <= 200) text of message, to which this reply is');
} else {
(0, _assert.default)(typeof msg.quote.body === 'string' && msg.quote.body.length <= MAX_IMAGE_QUOTE_LENGTH, 'message.quote.body should be a truncated (length <= 2000) image URL from message, to which this reply is');
}
}
}
/**
Creates new message, which can be encoded by golos.messages.encode.
@arg {string} text - text ("body") of the message.
@arg {string} [app = DEFAULT_APP] - "app" field of the message. Should be a string with length from 1 to 16.
@arg {string} [version = DEFAULT_VERSION] - "version" field of the message. Should be an integer, starting of 1.
@throws {Exception} when supplied data is invalid.
@return {object} - result message object.
*/
function newTextMsg(text) {
let app = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_APP;
let version = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DEFAULT_VERSION;
validateBody(text);
validateAppVersion(app, version);
const msg = {
app,
version,
body: text
};
return msg;
}
/**
Creates new image message, which can be encoded by golos.messages.encode.
@arg {string} image_url - URL of the image in the Internet (please use https://, and store images in image hostings, to make them storing eternally). This URL will be a "body" of the message.
@arg {function} callback - callback. Params are <code>(err, message)</code>. Calling after message construction (err is null in this case), or if error occured (err is exception, message is null).
@arg {function} [on_progress = undefined] - progress callback. Params are <code>(percent, extra_data)</code>. Percent is integer from 1 to 100.
@arg {string} [app = DEFAULT_APP] - "app" field of the message. Should be a string with length from 1 to 16.
@arg {string} [version = DEFAULT_VERSION] - "version" field of the message. Should be an integer, starting of 1.
@throws {Exception} only if callback called due error, and callback also throws error. So callback shouldn't throw errors.
*/
exports.newImageMsg = function (image_url, callback) {
let on_progress = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
let app = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : DEFAULT_APP;
let version = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : DEFAULT_VERSION;
let reportProgress = function (progress) {
let error = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
if (on_progress) on_progress(progress, {
error
});
};
try {
(0, _assert.default)(typeof Image !== 'undefined', 'Current environment does not support Image()');
(0, _assert.default)(image_url, 'image_url is required');
(0, _assert.default)(callback, 'callback is required');
validateAppVersion(app, version);
reportProgress(0);
let reportLoadError = errorText => {
const err = new Error(errorText);
reportProgress(100, err);
callback(err, null);
};
let img = new Image();
let watchdog;
let clearWatchdog = () => {
if (watchdog) clearTimeout(watchdog);
};
img.onerror = img.onabort = () => {
clearWatchdog();
reportLoadError('Cannot load image');
};
img.onload = () => {
clearWatchdog();
reportProgress(100);
const previewSize = (0, _utils.fitImageToSize)(img.width, img.height, MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT);
const msg = {
app,
version,
body: image_url,
type: 'image',
previewWidth: previewSize.width,
previewHeight: previewSize.height
};
validateImageMsg(msg);
callback(null, msg);
};
watchdog = setTimeout(() => {
if (!img.complete) {
img.onload = img.onerror = img.onabort = {};
reportLoadError('Image load timed out, maybe it is too large');
}
}, 5000);
img.src = image_url;
} catch (err) {
reportProgress(100, err);
if (callback) callback(err, null);
}
};
/**
Creates new image message, which can be encoded by golos.messages.encode. Async version of function, should be called with await.
@arg {string} image_url - URL of the image in the Internet. It will be a "body" of the message.
@arg {function} [on_progress = undefined] - progress callback. Params are <code>(percent, extra_data)</code>. Percent is integer from 1 to 100.
@arg {string} [app = DEFAULT_APP] - "app" field of the message. Should be a string with length from 1 to 16.
@arg {string} [version = DEFAULT_VERSION] - "version" field of the message. Should be an integer, starting of 1.
@throws {Exception} if error occured.
@return {object} - result message object.
@function newImageMsgAsync
*/
exports.newImageMsgAsync = (0, _promisify.promisify)(function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
const callback = args[args.length - 1];
let [image_url, on_progress, app, version] = args.slice(0, args.length - 1);
return exports.newImageMsg(image_url, callback, on_progress, app, version);
});
/**
Makes your message the reply to another message_object. Call it after `newTextMsg`/`newImageMsg`/etc, next call `encode`, and finally send resulting message as usually.
@arg {object} msg = Your message, created by `newTextMsg`, `newImageMsg`, etc.
@arg {object} quoted_message_object = Message to which you want to reply. It should be one of messages returned by `golos.messages.decode`, and have "from", "nonce", and (warning!) "message". If message is invalid (cannot be decoded), it has no "message" field, and if you try reply it, it will throw.
@throws {Exception} if quoted_message_object is invalid.
@return {object} - result message object, which can be encoded and sent.
@function newImageMsgAsync
*/
function makeQuoteMsg(msg, quoted_message_object) {
(0, _assert.default)(quoted_message_object, 'quoted_message_object is required');
const assertPrefix = 'quoted_message_object should be one of VALID objects, returned by `golos.messages.decode`';
(0, _assert.default)(quoted_message_object.from, `${assertPrefix}, and have "from" field`);
(0, _assert.default)(quoted_message_object.nonce, `${assertPrefix}, and have "nonce" field`);
(0, _assert.default)(quoted_message_object.message, `${assertPrefix}, and have "message" field`);
(0, _assert.default)(quoted_message_object.message.body, `${assertPrefix}, and have "message" field with "body"`);
let type = quoted_message_object.message.type;
let body = quoted_message_object.message.body;
if (type !== 'image') {
body = (0, _truncate.default)(body, {
length: MAX_TEXT_QUOTE_LENGTH,
omission: '...'
});
} else {
if (body.length > MAX_IMAGE_QUOTE_LENGTH) {
body = (0, _truncate.default)(body, {
length: MAX_TEXT_QUOTE_LENGTH,
omission: '...'
});
type = undefined; // make it text
}
}
if (!msg) msg = {};
msg.quote = {
from: quoted_message_object.from,
nonce: quoted_message_object.nonce,
body
};
if (type) msg.quote.type = type;
return msg;
}
function forEachMessage(message_objects, begin_idx, end_idx, callback) {
if (begin_idx === undefined) begin_idx = 0;
if (end_idx === undefined) end_idx = message_objects.length;
const step = end_idx > begin_idx ? 1 : -1;
for (let i = begin_idx; i != end_idx; i += step) {
const message_object = message_objects[i];
// return true is `continue`
// return false is `break`
if (!callback(message_object, i)) break;
}
}
function msgFromBuf(buf) {
let lengthPrefixed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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) {
let lengthPrefixed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
const buf = _bytebuffer.default.fromHex(hex, _bytebuffer.default.LITTLE_ENDIAN);
return msgFromBuf(buf, lengthPrefixed);
}
const parseMsg = function (message, rawMsg) {
let raw_messages = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 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 => {
(0, _assert.default)(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).<br>
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 <code>on_error</code>).<br>
Processes whole incoming array, or only part of it.<br>
Can process in reversed order.
@arg {string|PrivateKey} private_memo_key - private memo key of "from" or "to".
@arg {string|PublicKey} second_user_public_memo_key - public memo key of second user.
@arg {array} message_objects - array of objects. Each object should contain nonce, checksum and encrypted_message (such object returns from private_message API).
@arg {function} [before_decode = undefined] - callback, calling on each message before processing. Params are <code>(message, idx, results)</code>. If returns true, message will not be processed. Also, you can push it to <code>results</code> manually.
@arg {function} [for_each = undefined] - callback, calling on each message, after message is decoded, parsed and validated, but before add it to result array. Params are <code>(message, idx)</code>. If returns true, message willn't be added to result array.
@arg {function} [on_error = undefined] - callback, calling on each message which can't be decrypted, parsed, validated, or if <code>for_each</code> throws. Params are <code>(message, idx, exception)</code>. If returns true, message willn't be added to result array.
@arg {int} [begin_idx = undefined] - if set, function will process messages only from this index (incl.). If begin_idx > end_idx, messages will be processed in reversed order.
@arg {int} [end_idx = undefined] - if set, function will process messages only before this index (excl.). If end_idx < begin_idx, messages will be processed in reversed order.
@arg {bool} [raw_messages = false] - if set, function will not parse messages as JSON and validate them.
@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.
*/
function decode(private_memo_key, second_user_public_memo_key, message_objects) {
let before_decode = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : undefined;
let for_each = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : undefined;
let on_error = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : undefined;
let begin_idx = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : undefined;
let end_idx = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : undefined;
let raw_messages = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : false;
warnDeprecation('decode', 'decodeMsgs');
(0, _assert.default)(private_memo_key, 'private_memo_key is required');
(0, _assert.default)(second_user_public_memo_key, 'second_user_public_memo_key is required');
(0, _assert.default)(message_objects, 'message_objects is required');
const private_key = toPrivateObj(private_memo_key);
const public_key = toPublicObj(second_user_public_memo_key);
let shared_secret;
let results = [];
forEachMessage(message_objects, begin_idx, end_idx, (message_object, i) => {
// Return true if for_each should not be called
let processOnError = exception => {
if (on_error) {
if (!on_error(message_object, i, exception)) {
results.push(message_object);
}
return true;
}
return false;
};
try {
message_object.raw_message = null; // Will be set if message will be successfully decoded
message_object.message = null; // Will be set if message will be also successfully parsed and validated
if (before_decode && before_decode(message_object, i, results)) {
return true;
}
// Most "heavy" line
if (!shared_secret) shared_secret = private_key.get_shared_secret(public_key);
let decrypted = _ecc.Aes.decrypt(shared_secret, null, message_object.nonce.toString(), Buffer.from(message_object.encrypted_message, 'hex'), message_object.checksum);
const mbuf = _bytebuffer.default.fromBinary(decrypted.toString('binary'), _bytebuffer.default.DEFAULT_CAPACITY, _bytebuffer.default.LITTLE_ENDIAN);
decrypted = msgFromBuf(mbuf, true);
parseMsg(message_object, decrypted, raw_messages);
} catch (exception) {
if (processOnError(exception)) return true;
}
try {
if (!for_each || !for_each(message_object, i)) {
results.push(message_object);
}
} catch (exception) {
processOnError(exception);
}
return true;
});
return results;
}
const ExceptionTypes = {
NodeError: 1,
Error: 2,
OuterError: 3,
RequestError: 4
};
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);
(0, _assert.default)(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 = function (exception) {
let exType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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 = _ecc.Aes.decrypt(shareds[pubKey], null, message.nonce.toString(), Buffer.from(message.encrypted_message, 'hex'), message.checksum);
const mbuf = _bytebuffer.default.fromBinary(decrypted.toString('binary'), _bytebuffer.default.DEFAULT_CAPACITY, _bytebuffer.default.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 = _api.default;
}
let decRes, decErr;
try {
debugMsgs('decryptMessagesAsync', entries.length + ' msgs');
const {
dgp,
account,
keys,
sessionName
} = login;
decRes = await _.default.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 = function (msg, idx, exception) {
let exType = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 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"
@arg {string|PublicKey} to_public_memo_key - private memo key of "to"
@arg {object} message - message to encode.
@arg {string|undefined} nonce - unique identifier of message. When editing message, set to its nonce. Otherwise keep undefined.
@return {object} - Object with fields: nonce, checksum and message.
*/
function encode(from_private_memo_key, to_public_memo_key, message) {
let nonce = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : undefined;
warnDeprecation('encode', 'encodeMsg');
(0, _assert.default)(from_private_memo_key, 'from_private_memo_key is required');
(0, _assert.default)(to_public_memo_key, 'to_public_memo_key is required');
(0, _assert.default)(message, 'message is required');
const fromKey = toPrivateObj(from_private_memo_key);
const toKey = toPublicObj(to_public_memo_key);
const mbuf = new _bytebuffer.default(_bytebuffer.default.DEFAULT_CAPACITY, _bytebuffer.default.LITTLE_ENDIAN);
mbuf.writeVString(JSON.stringify(message));
message = new Buffer(mbuf.copy(0, mbuf.offset).toBinary(), 'binary');
let data = _ecc.Aes.encrypt(fromKey, toKey, message, nonce);
return {
nonce: data.nonce.toString(),
encrypted_message: data.message.toString('hex'),
checksum: data.checksum
};
}
async function encodeMsg(_ref) {
let {
group,
private_memo,
to_public_memo,
msg,
nonce,
api
} = _ref;
(0, _assert.default)(msg, 'msg is required');
const msgToBuffer = msg => {
const mbuf = new _bytebuffer.default(_bytebuffer.default.DEFAULT_CAPACITY, _bytebuffer.default.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) {
(0, _assert.default)(private_memo, 'private_memo is required in private chats');
(0, _assert.default)(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 = _ecc.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
};
}
(0, _assert.default)(!private_memo, 'private_memo is for private messages, not groups');
(0, _assert.default)(!to_public_memo, 'to_public_memo is for private messages, not groups');
if (group.is_encrypted) {
if (!api) {
api = _api.default;
}
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: nonce || _ecc.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`.
@arg {function} condition - callback, calling on each message. Params are (message, idx). If returns true, message is adding to ranges. If returns false/undefined/null, message is skipping. If returns -1, processing loop breaks.
@arg {function} wrapper - callback, calling on each range, when adding it to result array. Allows to wrap range as an operation. Params are (range). Should return wrapped result. If returns false/undefined/null, range skipping.
@arg {int|undefined} begin_idx - if set, function will process messages only from it index (incl.). If begin_idx > end_idx, messages will be processed in reversed order.
@arg {int|undefined} end_idx - if set, function will process messages only before it index (excl.). If end_idx < begin_idx, messages will be processed in reversed order.
@return {array} - result array of operations, which can be sent in single transaction.
*/
function makeDatedGroups(message_objects, condition, wrapper, begin_idx, end_idx) {
(0, _assert.default)(message_objects, 'message_objects is required');
(0, _assert.default)(condition, 'condition is required');
(0, _assert.default)(wrapper, 'wrapper is required');
let results = [];
let group = null;
let before = null;
let fixStartDate = start_date => {
return new Date(new Date(start_date + 'Z').getTime() - 1000).toISOString().split('.')[0];
};
const time_point_min = '1970-01-01T00:00:00';
let pushResult = (nonces, msgs, start_date, stop_date) => {
if (nonces) {
for (const nonce of nonces) {
const msg = msgs[nonce];
const {
from,
to
} = msg;
let wrapped = wrapper({
start_date: time_point_min,
stop_date: time_point_min,
nonce,
from,
to
});
if (wrapped) results.push(wrapped);
}
} else {
let wrapped = wrapper({
start_date,
stop_date,
nonce: 0
});
if (wrapped) results.push(wrapped);
}
};
let pushGroup = nextCreateDate => {
if (!group) return;
let {
beforeNonces,
nonces,
afterNonces,
msgs
} = group;
if (!nextCreateDate || nextCreateDate !== group.start_date) {
nonces.push(...afterNonces);
afterNonces = [];
}
pushResult(beforeNonces, msgs);
if (nonces.length > 1) {
if (!afterNonces.length) group.start_date = fixStartDate(group.start_date);
pushResult(null, msgs, group.start_date, group.stop_date);
} else if (nonces[0]) {
pushResult([nonces[0]], msgs);
}
pushResult(afterNonces, msgs);
group = null;
};
forEachMessage(message_objects, begin_idx, end_idx, (message_object, i) => {
const cond = condition(message_object, i);
const {
create_date,
nonce
} = message_object;
if (cond === -1) {
return false;
} else if (cond) {
if (!group) {
group = {
stop_date: create_date,
beforeNonces: [],
afterNonces: [],
nonces: [],
msgs: {}
};
}
if (create_date === before) {
group.beforeNonces.push(nonce);
} else {
if (before) {
before = null;
group.stop_date = create_date;
}
if (create_date !== group.start_date) {
group.nonces.push(...group.afterNonces);
group.afterNonces = [];
}
group.afterNonces.push(nonce);
}
group.start_date = create_date;
group.msgs[nonce] = message_object;
} else if (cond === false) {
pushGroup(create_date);
before = create_date;
}
return true;
});
pushGroup();
return results;
}