@throneless/libsignal-service
Version:
A fork of the the libtextsecure components of Signal-Desktop, adapted for use by nodejs.
1,448 lines (1,288 loc) • 40 kB
JavaScript
/*
* vim: ts=2:sw=2:expandtab
*/
const debug = require('debug')('libsignal-service:SendMessage');
const _ = require('lodash');
const libsignal = require('@throneless/libsignal-protocol');
const { default: PQueue } = require('p-queue');
const crypto = require('./crypto.js');
const errors = require('./errors.js');
const helpers = require('./helpers.js');
const OutgoingMessage = require('./OutgoingMessage.js');
const createTaskWithTimeout = require('./taskWithTimeout.js');
const Message = require('./Message.js');
const protobuf = require('./protobufs.js');
const AttachmentPointer = protobuf.lookupType(
'signalservice.AttachmentPointer'
);
const Content = protobuf.lookupType('signalservice.Content');
const DataMessage = protobuf.lookupType('signalservice.DataMessage');
const GroupContext = protobuf.lookupType('signalservice.GroupContext');
const NullMessage = protobuf.lookupType('signalservice.NullMessage');
const ReceiptMessage = protobuf.lookupType('signalservice.ReceiptMessage');
const SyncMessage = protobuf.lookupType('signalservice.SyncMessage');
const SyncMessageRead = protobuf.lookupType('signalservice.SyncMessage.Read');
const SyncMessageRequest = protobuf.lookupType(
'signalservice.SyncMessage.Request'
);
const SyncMessageSent = protobuf.lookupType('signalservice.SyncMessage.Sent');
const SyncMessageStickerPackOperation = protobuf.lookupType(
'signalservice.SyncMessage.StickerPackOperation'
);
const SyncMessageViewOnceOpen = protobuf.lookupType(
'signalservice.SyncMessage.ViewOnceOpen'
);
const TypingMessage = protobuf.lookupType('signalservice.TypingMessage');
const Verified = protobuf.lookupType('signalservice.Verified');
/* eslint-disable more/no-then, no-bitwise */
function stringToArrayBuffer(str) {
if (typeof str !== 'string') {
throw new Error('Passed non-string to stringToArrayBuffer');
}
const res = new ArrayBuffer(str.length);
const uint = new Uint8Array(res);
for (let i = 0; i < str.length; i += 1) {
uint[i] = str.charCodeAt(i);
}
return res;
}
class MessageSender {
constructor(store) {
this.pendingMessages = {};
this.store = store;
}
async connect() {
if (this.server === undefined) {
const username = await this.store.getNumber();
const password = await this.store.getPassword();
this.server = this.constructor.WebAPI.connect({ username, password });
}
}
_getAttachmentSizeBucket(size) {
return Math.max(
541,
Math.floor(1.05 ** Math.ceil(Math.log(size) / Math.log(1.05)))
);
}
getPaddedAttachment(data) {
const size = data.byteLength;
const paddedSize = this._getAttachmentSizeBucket(size);
const padding = crypto.getZeroes(paddedSize - size);
return crypto.concatenateBytes(data, padding);
}
async makeAttachmentPointer(attachment) {
if (typeof attachment !== 'object' || attachment == null) {
return Promise.resolve(undefined);
}
const { data, size } = attachment;
if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
throw new Error(
`makeAttachmentPointer: data was a '${typeof data}' instead of ArrayBuffer/ArrayBufferView`
);
}
if (data.byteLength !== size) {
throw new Error(
`makeAttachmentPointer: Size ${size} did not match data.byteLength ${data.byteLength}`
);
}
const padded = this.getPaddedAttachment(data);
const key = crypto.getRandomBytes(64);
const iv = crypto.getRandomBytes(16);
const result = await crypto.encryptAttachment(padded, key, iv);
const id = await this.server.putAttachment(result.ciphertext);
const proto = AttachmentPointer.create();
proto.cdnId = id;
proto.contentType = attachment.contentType;
proto.key = new Uint8Array(key);
proto.size = attachment.size;
proto.digest = new Uint8Array(result.digest);
if (attachment.fileName) {
proto.fileName = attachment.fileName;
}
if (attachment.flags) {
proto.flags = attachment.flags;
}
if (attachment.width) {
proto.width = attachment.width;
}
if (attachment.height) {
proto.height = attachment.height;
}
if (attachment.caption) {
proto.caption = attachment.caption;
}
return proto;
}
retransmitMessage(number, jsonData, timestamp) {
const outgoing = new OutgoingMessage(this.server);
return outgoing.transmitMessage(number, jsonData, timestamp);
}
validateRetryContentMessage(content) {
// We want at least one field set, but not more than one
let count = 0;
count += content.syncMessage ? 1 : 0;
count += content.dataMessage ? 1 : 0;
count += content.callMessage ? 1 : 0;
count += content.nullMessage ? 1 : 0;
if (count !== 1) {
return false;
}
// It's most likely that dataMessage will be populated, so we look at it in detail
const data = content.dataMessage;
if (
data
&& !data.attachments.length
&& !data.body
&& !data.expireTimer
&& !data.flags
&& !data.group
) {
return false;
}
return true;
}
getRetryProto(message, timestamp) {
// If message was sent before v0.41.3 was released on Aug 7, then it was most
// certainly a DataMessage
//
// var d = new Date('2017-08-07T07:00:00.000Z');
// d.getTime();
const august7 = 1502089200000;
if (timestamp < august7) {
return DataMessage.decode(message);
}
// This is ugly. But we don't know what kind of proto we need to decode...
try {
// Simply decoding as a Content message may throw
const proto = Content.decode(message);
// But it might also result in an invalid object, so we try to detect that
if (this.validateRetryContentMessage(proto)) {
return proto;
}
return DataMessage.decode(message);
} catch (e) {
// If this call throws, something has really gone wrong, we'll fail to send
return DataMessage.decode(message);
}
}
tryMessageAgain(number, encodedMessage, timestamp) {
const proto = this.getRetryProto(encodedMessage, timestamp);
return this.sendIndividualProto(number, proto, timestamp);
}
queueJobForIdentifier(identifier, runJob) {
// const { id } = await ConversationController.getOrCreateAndWait(
// identifier,
// 'private'
// );
const id = identifier;
this.pendingMessages[id] = this.pendingMessages[id] || new PQueue({ concurrency: 1 });
const queue = this.pendingMessages[id];
const taskWithTimeout = createTaskWithTimeout(
runJob,
`queueJobForIdentifier ${identifier} ${id}`
);
return queue.add(taskWithTimeout);
}
uploadAttachments(message) {
debug('Message attachments: ', message.attachments);
return Promise.all(
message.attachments.map(this.makeAttachmentPointer.bind(this))
)
.then(attachmentPointers => {
// eslint-disable-next-line no-param-reassign
message.attachmentPointers = attachmentPointers;
})
.catch(error => {
if (error instanceof Error && error.name === 'HTTPError') {
throw new errors.MessageError(message, error);
} else {
throw error;
}
});
}
async uploadLinkPreviews(message) {
try {
const preview = await Promise.all(
(message.preview || []).map(async item => ({
...item,
image: await this.makeAttachmentPointer(item.image),
}))
);
// eslint-disable-next-line no-param-reassign
message.preview = preview;
} catch (error) {
if (error instanceof Error && error.name === 'HTTPError') {
throw new errors.MessageError(message, error);
} else {
throw error;
}
}
}
async uploadSticker(message) {
try {
const { sticker } = message;
if (!sticker || !sticker.data) {
return;
}
// eslint-disable-next-line no-param-reassign
message.sticker = {
...sticker,
attachmentPointer: await this.makeAttachmentPointer(sticker.data),
};
} catch (error) {
if (error instanceof Error && error.name === 'HTTPError') {
throw new errors.MessageError(message, error);
} else {
throw error;
}
}
}
uploadThumbnails(message) {
const makePointer = this.makeAttachmentPointer.bind(this);
const { quote } = message;
if (!quote || !quote.attachments || quote.attachments.length === 0) {
return Promise.resolve();
}
return Promise.all(
quote.attachments.map(attachment => {
const { thumbnail } = attachment;
if (!thumbnail) {
return null;
}
return makePointer(thumbnail).then(pointer => {
// eslint-disable-next-line no-param-reassign
attachment.attachmentPointer = pointer;
});
})
).catch(error => {
if (error instanceof Error && error.name === 'HTTPError') {
throw new errors.MessageError(message, error);
} else {
throw error;
}
});
}
async sendMessage(attrs, options) {
if (!attrs.timestamp) {
// eslint-disable-next-line no-param-reassign
attrs.timestamp = Date.now();
}
// eslint-disable-next-line no-param-reassign
attrs.profileKey = await this.store.getProfileKey();
const message = new Message(attrs);
const silent = false;
return Promise.all([
this.uploadAttachments(message),
this.uploadThumbnails(message),
this.uploadLinkPreviews(message),
this.uploadSticker(message),
]).then(
() =>
new Promise((resolve, reject) => {
this.sendMessageProto(
message.timestamp,
message.recipients || [],
message.toProto(),
res => {
res.dataMessage = message.toArrayBuffer();
if (res.errors.length > 0) {
reject(res);
} else {
resolve(res);
}
},
silent,
options
);
})
);
}
async sendMessageProto(
timestamp,
recipients,
messageProto,
callback,
silent,
options = {}
) {
const rejections = await this.store.getSignedKeyRotationRejected();
if (rejections > 5) {
throw new errors.SignedPreKeyRotationError(
recipients,
messageProto.toArrayBuffer(),
timestamp
);
}
const outgoing = new OutgoingMessage(
this.server,
this.store,
timestamp,
recipients,
messageProto,
silent,
callback,
options
);
recipients.forEach(identifier => {
this.queueJobForIdentifier(identifier, () =>
outgoing.sendToIdentifier(identifier)
);
});
}
sendMessageProtoAndWait(
timestamp,
identifiers,
messageProto,
silent,
options = {}
) {
return new Promise((resolve, reject) => {
const callback = result => {
if (result && result.errors && result.errors.length > 0) {
return reject(result);
}
return resolve(result);
};
this.sendMessageProto(
timestamp,
identifiers,
messageProto,
callback,
silent,
options
);
});
}
sendIndividualProto(identifier, proto, timestamp, silent, options = {}) {
return new Promise((resolve, reject) => {
const callback = res => {
if (res && res.errors && res.errors.length > 0) {
reject(res);
} else {
resolve(res);
}
};
this.sendMessageProto(
timestamp,
[identifier],
proto,
callback,
silent,
options
);
});
}
createSyncMessage() {
const syncMessage = SyncMessage.create();
// Generate a random int from 1 and 512
const buffer = crypto.getRandomBytes(1);
const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;
// Generate a random padding buffer of the chosen size
syncMessage.padding = crypto.getRandomBytes(paddingLength);
return syncMessage;
}
async sendSyncMessage(
encodedDataMessage,
timestamp,
destination,
destinationUuid,
expirationStartTimestamp,
sentTo = [],
unidentifiedDeliveries = [],
isUpdate = false,
options
) {
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const myDevice = await this.store.getDeviceId();
if (myDevice === 1 || myDevice === '1') {
return Promise.resolve();
}
const dataMessage = DataMessage.decode(encodedDataMessage);
const sentMessage = SyncMessageSent.create();
sentMessage.timestamp = timestamp;
sentMessage.message = dataMessage;
if (destination) {
sentMessage.destination = destination;
}
if (destinationUuid) {
sentMessage.destinationUuid = destinationUuid;
}
if (expirationStartTimestamp) {
sentMessage.expirationStartTimestamp = expirationStartTimestamp;
}
const unidentifiedLookup = unidentifiedDeliveries.reduce(
(accumulator, item) => {
// eslint-disable-next-line no-param-reassign
accumulator[item] = true;
return accumulator;
},
Object.create(null)
);
if (isUpdate) {
syncMessage.isRecipientUpdate = true;
}
// Though this field has 'unidentified' in the name, it should have entries for each
// number we sent to.
if (sentTo && sentTo.length) {
sentMessage.unidentifiedStatus = sentTo.map(identifier => {
const status = SyncMessageSent.UnidentifiedDeliveryStatus.create();
// const conv = ConversationController.get(identifier);
// if (conv && conv.get("e164")) {
// status.destination = conv.get("e164");
// }
// if (conv && conv.get("uuid")) {
// status.destinationUuid = conv.get("uuid");
// }
status.destination = identifier;
status.unidentified = Boolean(unidentifiedLookup[identifier]);
return status;
});
}
const syncMessage = this.createSyncMessage();
syncMessage.sent = sentMessage;
const contentMessage = Content.create();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myUuid || myNumber,
contentMessage,
timestamp,
silent,
options
);
}
async getProfile(number, { accessKey } = {}) {
if (accessKey) {
return this.server.getProfileUnauth(number, { accessKey });
}
return this.server.getProfile(number);
}
getAvatar(path) {
return this.server.getAvatar(path);
}
getSticker(packId, stickerId) {
return this.server.getSticker(packId, stickerId);
}
getStickerPackManifest(packId) {
return this.server.getStickerPackManifest(packId);
}
async sendRequestBlockSyncMessage(options) {
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const myDevice = await this.store.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = SyncMessageRequest.create();
request.type = SyncMessageRequest.Type.BLOCKED;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = Content.create();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
}
async sendRequestConfigurationSyncMessage(options) {
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const myDevice = await this.store.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = SyncMessageRequest.create();
request.type = SyncMessageRequest.Type.CONFIGURATION;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = Content.create();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
}
async sendRequestGroupSyncMessage(options) {
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const myDevice = await this.store.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = SyncMessageRequest.create();
request.type = SyncMessageRequest.Type.GROUPS;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = Content.create();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
}
async sendRequestContactSyncMessage(options) {
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const myDevice = await this.store.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = SyncMessageRequest.create();
request.type = SyncMessageRequest.Type.CONTACTS;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = Content.create();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
}
async sendTypingMessage(options = {}, sendOptions = {}) {
const ACTION_ENUM = TypingMessage.Action;
let { groupNumbers } = options;
const { recipientId, groupId, isTyping, timestamp } = options;
// We don't want to send typing messages to our other devices, but we will
// in the group case.
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
if (recipientId && (myNumber === recipientId || myUuid === recipientId)) {
return null;
}
if (!recipientId && !groupId) {
throw new Error('Need to provide either recipientId or groupId!');
}
// If we have group support, retrieve from store
if (this.store.hasGroups()) {
groupNumbers = (await this.store.getGroupNumbers(groupId)) || [];
}
const recipients = groupId
? _.without(groupNumbers, myNumber, myUuid)
: [recipientId];
const groupIdBuffer = groupId
? crypto.fromEncodedBinaryToArrayBuffer(groupId)
: null;
const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED;
const finalTimestamp = timestamp || Date.now();
const typingMessage = TypingMessage.create();
typingMessage.groupId = groupIdBuffer;
typingMessage.action = action;
typingMessage.timestamp = finalTimestamp;
const contentMessage = Content.create();
contentMessage.typingMessage = typingMessage;
const silent = true;
const online = true;
return this.sendMessageProtoAndWait(
finalTimestamp,
recipients,
contentMessage,
silent,
{
...sendOptions,
online,
}
);
}
async sendDeliveryReceipt(recipientE164, recipientUuid, timestamp, options) {
const myNumber = await this.store.getNumber();
const myDevice = await this.store.getDeviceId();
const myUuid = await this.store.getUuid();
if (
myNumber === recipientE164
&& myUuid === recipientUuid
&& (myDevice === 1 || myDevice === '1')
) {
return Promise.resolve();
}
const receiptMessage = ReceiptMessage.create();
receiptMessage.type = ReceiptMessage.Type.DELIVERY;
receiptMessage.timestamp = [timestamp];
const contentMessage = Content.create();
contentMessage.receiptMessage = receiptMessage;
const silent = true;
return this.sendIndividualProto(
recipientUuid || recipientE164,
contentMessage,
Date.now(),
silent,
options
);
}
sendReadReceipts(senderE164, senderUuid, timestamps, options) {
const receiptMessage = ReceiptMessage.create();
receiptMessage.type = ReceiptMessage.Type.READ;
receiptMessage.timestamp = timestamps;
const contentMessage = Content.create();
contentMessage.receiptMessage = receiptMessage;
const silent = true;
return this.sendIndividualProto(
senderUuid || senderE164,
contentMessage,
Date.now(),
silent,
options
);
}
async syncReadMessages(reads, options) {
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const myDevice = await this.store.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const syncMessage = this.createSyncMessage();
syncMessage.read = [];
for (let i = 0; i < reads.length; i += 1) {
const read = SyncMessageRead.create();
read.timestamp = reads[i].timestamp;
read.sender = reads[i].sender;
syncMessage.read.push(read);
}
const contentMessage = Content.create();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
}
async syncViewOnceOpen(sender, senderUuid, timestamp, options) {
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const myDevice = await this.store.getDeviceId();
if (myDevice === 1 || myDevice === '1') {
return null;
}
const syncMessage = this.createSyncMessage();
const viewOnceOpen = SyncMessageViewOnceOpen.create();
viewOnceOpen.sender = sender;
viewOnceOpen.senderUuid = senderUuid;
viewOnceOpen.timestamp = timestamp;
syncMessage.viewOnceOpen = viewOnceOpen;
const contentMessage = Content.create();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
async sendStickerPackSync(operations, options) {
const myDevice = await this.store.getDeviceId();
if (myDevice === 1 || myDevice === '1') {
return null;
}
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const ENUM = SyncMessageStickerPackOperation.Type;
const packOperations = operations.map(item => {
const { packId, packKey, installed } = item;
const operation = SyncMessageStickerPackOperation.create();
operation.packId = helpers.hexStringToArrayBuffer(packId);
operation.packKey = helpers.base64ToArrayBuffer(packKey);
operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE;
return operation;
});
const syncMessage = this.createSyncMessage();
syncMessage.stickerPackOperation = packOperations;
const contentMessage = Content.create();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
async syncVerification(
destinationE164,
destinationUuid,
state,
identityKey,
options
) {
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const myDevice = await this.store.getDeviceId();
const now = Date.now();
if (myDevice === 1 || myDevice === '1') {
return Promise.resolve();
}
// First send a null message to mask the sync message.
const nullMessage = NullMessage.create();
// Generate a random int from 1 and 512
const buffer = crypto.getRandomBytes(1);
const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;
// Generate a random padding buffer of the chosen size
nullMessage.padding = crypto.getRandomBytes(paddingLength);
const contentMessage = Content.create();
contentMessage.nullMessage = nullMessage;
// We want the NullMessage to look like a normal outgoing message; not silent
const silent = false;
const promise = this.sendIndividualProto(
destinationUuid || destinationE164,
contentMessage,
now,
silent,
options
);
return promise.then(() => {
const verified = Verified.create();
verified.state = state;
if (destinationE164) {
verified.destination = destinationE164;
}
if (destinationUuid) {
verified.destinationUuid = destinationUuid;
}
verified.identityKey = identityKey;
verified.nullMessage = nullMessage.padding;
const syncMessage = this.createSyncMessage();
syncMessage.verified = verified;
const secondMessage = Content.create();
secondMessage.syncMessage = syncMessage;
const innerSilent = true;
return this.sendIndividualProto(
myUuid || myNumber,
secondMessage,
now,
innerSilent,
options
);
});
}
async sendGroupProto(
providedIdentifiers,
proto,
timestamp = Date.now(),
options = {}
) {
const myE164 = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const identifiers = providedIdentifiers.filter(
id => id !== myE164 && id !== myUuid
);
if (identifiers.length === 0) {
return Promise.resolve({
successfulIdentifiers: [],
failoverIdentifiers: [],
errors: [],
unidentifiedDeliveries: [],
dataMessage: helpers.convertToArrayBuffer(
DataMessage.encode(proto).finish()
),
});
}
return new Promise((resolve, reject) => {
const silent = true;
const callback = res => {
res.dataMessage = helpers.convertToArrayBuffer(
DataMessage.encode(proto).finish()
);
if (res.errors.length > 0) {
reject(res);
} else {
resolve(res);
}
};
this.sendMessageProto(
timestamp,
providedIdentifiers,
proto,
callback,
silent,
options
);
});
}
async getMessageProto(
destination,
body,
attachments,
quote,
preview,
sticker,
reaction,
timestamp,
expireTimer,
flags
) {
const attributes = {
recipients: [destination],
destination,
body,
timestamp,
attachments,
quote,
preview,
sticker,
reaction,
expireTimer,
flags,
};
return this.getMessageProtoObj(attributes);
}
async getMessageProtoObj(attrs) {
if (!attrs.timestamp) {
// eslint-disable-next-line no-param-reassign
attrs.timestamp = Date.now();
}
// eslint-disable-next-line no-param-reassign
attrs.profileKey = await this.store.getProfileKey();
const message = new Message(attrs);
await Promise.all([
this.uploadAttachments(message),
this.uploadThumbnails(message),
this.uploadLinkPreviews(message),
this.uploadSticker(message),
]);
return message.toArrayBuffer();
}
async sendMessageToIdentifier(
{
identifier,
body = '',
attachments = [],
quote,
preview,
sticker,
reaction,
timestamp,
expireTimer,
},
options = {}
) {
return this.sendMessage(
{
recipients: [identifier],
body,
timestamp,
attachments,
quote,
preview,
sticker,
reaction,
expireTimer,
},
options
);
}
// backwards compatibility
async sendMessageToNumber(
{
number,
body = '',
attachments = [],
quote,
preview,
sticker,
reaction,
timestamp,
expireTimer,
},
options = {}
) {
return this.sendMessage(
{
recipients: [number],
body,
timestamp,
attachments,
quote,
preview,
sticker,
reaction,
expireTimer,
},
options
);
}
async resetSession(identifier, timestamp, options) {
debug('resetting secure session');
const silent = false;
const proto = DataMessage.create();
proto.body = 'TERMINATE';
proto.flags = DataMessage.Flags.END_SESSION;
const logError = prefix => error => {
debug(prefix, error && error.stack ? error.stack : error);
throw error;
};
const deleteAllSessions = targetNumber =>
this.store.getDeviceIds(targetNumber).then(deviceIds =>
Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(
targetNumber,
deviceId
);
debug('deleting sessions for', address.toString());
const sessionCipher = new libsignal.SessionCipher(
this.store,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
)
);
const sendToContactPromise = deleteAllSessions(identifier)
.catch(logError('resetSession/deleteAllSessions1 error:'))
.then(() => {
debug('finished closing local sessions, now sending to contact');
return this.sendIndividualProto(
identifier,
proto,
timestamp,
silent,
options
).catch(logError('resetSession/sendToContact error:'));
})
.then(() =>
deleteAllSessions(identifier).catch(
logError('resetSession/deleteAllSessions2 error:')
)
);
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
// We already sent the reset session to our other devices in the code above!
if (identifier === myNumber || identifier === myUuid) {
return sendToContactPromise;
}
const buffer = proto.toArrayBuffer();
const sendSyncPromise = this.sendSyncMessage(
buffer,
timestamp,
identifier,
null,
[],
[],
options
).catch(logError('resetSession/sendSync error:'));
return Promise.all([sendToContactPromise, sendSyncPromise]);
}
async sendMessageToGroup(
{
groupId,
recipients = [],
body = '',
attachments = [],
quote,
preview,
sticker,
reaction,
timestamp,
expireTimer,
},
options = {}
) {
if (this.store.hasGroups()) {
const groupNumbers = await this.store.getGroupNumbers(groupId);
if (!groupNumbers) {
return Promise.reject(new Error('Unknown Group'));
}
}
const myE164 = await this.store.getNumber();
const myUuid = await this.store.getNumber();
const attrs = {
recipients: recipients.filter(r => r !== myE164 && r !== myUuid),
body,
timestamp,
attachments,
quote,
preview,
sticker,
reaction,
expireTimer,
group: {
id: groupId,
type: GroupContext.Type.DELIVER,
},
};
if (recipients.length === 0) {
return Promise.resolve({
successfulIdentifiers: [],
failoverIdentifiers: [],
errors: [],
unidentifiedDeliveries: [],
dataMessage: await this.getMessageProtoObj(attrs),
});
}
return this.sendMessage(attrs, options);
}
async createGroup(targetIdentifiers, id, name, avatar, options) {
debug('targetIdentifiers', targetIdentifiers);
const proto = DataMessage.create();
proto.group = GroupContext.create();
if (this.store.hasGroups()) {
await this.store.createNewGroup(id, targetIdentifiers).then(group => {
debug('group', group);
proto.group.id = stringToArrayBuffer(group.id);
const { identifiers } = group;
proto.group.members = identifiers;
});
} else {
proto.group.id = stringToArrayBuffer(id);
proto.group.members = targetIdentifiers;
}
proto.group.type = GroupContext.Type.UPDATE;
proto.group.name = name;
debug('group proto', proto);
return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment;
return this.sendGroupProto(
proto.group.members,
proto,
Date.now(),
options
).then(() => proto.group.id);
});
}
async updateGroup(groupId, name, avatar, targetIdentifiers, options) {
const proto = DataMessage.create();
proto.group = GroupContext.create();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = GroupContext.Type.UPDATE;
proto.group.name = name;
if (this.store.hasGroups()) {
await this.store
.updateGroupNumbers(groupId, targetIdentifiers)
.then(identifiers => {
if (!identifiers) {
return Promise.reject(new Error('Unknown Group'));
}
proto.group.members = identifiers;
return identifiers;
});
} else {
proto.group.members = targetIdentifiers;
}
return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment;
return this.sendGroupProto(
proto.group.members,
proto,
Date.now(),
options
).then(() => proto.group.id);
});
}
async addIdentifierToGroup(groupId, newIdentifiers, options) {
const proto = DataMessage.create();
proto.group = GroupContext.create();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = GroupContext.Type.UPDATE;
if (this.store.hasGroups()) {
if (typeof newIdentifiers === 'string') {
// eslint-disable-next-line no-param-reassign
newIdentifiers = [newIdentifiers];
}
await this.store
.addGroupNumbers(groupId, newIdentifiers)
.then(identifiers => {
if (!identifiers) {
return Promise.reject(new Error('Unknown Group'));
}
proto.group.members = identifiers;
return identifiers;
});
} else {
proto.group.members = newIdentifiers;
}
return this.sendGroupProto(proto.group.members, proto, Date.now(), options);
}
// backwards compatibility
async addNumberToGroup(groupId, newNumbers, options) {
const proto = DataMessage.create();
proto.group = GroupContext.create();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = GroupContext.Type.UPDATE;
if (this.store.hasGroups()) {
if (typeof newNumbers === 'string') {
// eslint-disable-next-line no-param-reassign
newNumbers = [newNumbers];
}
await this.store
.addGroupNumbers(groupId, newNumbers)
.then(identifiers => {
if (!identifiers) {
return Promise.reject(new Error('Unknown Group'));
}
proto.group.members = identifiers;
return identifiers;
});
} else {
proto.group.members = newNumbers;
}
return this.sendGroupProto(proto.group.members, proto, Date.now(), options);
}
async setGroupName(groupId, name, groupIdentifiers, options) {
const proto = DataMessage.create();
proto.group = GroupContext.create();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = GroupContext.Type.UPDATE;
proto.group.name = name;
if (this.store.hasGroups()) {
await this.store.getGroupNumbers(groupId).then(identifiers => {
if (!identifiers) {
return Promise.reject(new Error('Unknown Group'));
}
proto.group.members = identifiers;
return identifiers;
});
} else {
proto.group.members = groupIdentifiers;
}
return this.sendGroupProto(proto.group.members, proto, Date.now(), options);
}
async setGroupAvatar(groupId, avatar, groupIdentifiers, options) {
const proto = DataMessage.create();
proto.group = GroupContext.create();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = GroupContext.Type.UPDATE;
if (this.store.hasGroups()) {
await this.store.getGroupNumbers(groupId).then(identifiers => {
if (!identifiers) {
return Promise.reject(new Error('Unknown Group'));
}
proto.group.members = identifiers;
return identifiers;
});
} else {
proto.group.members = groupIdentifiers;
}
return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment;
return this.sendGroupProto(
proto.group.members,
proto,
Date.now(),
options
);
});
}
async leaveGroup(groupId, groupIdentifiers, options) {
const proto = DataMessage.create();
proto.group = GroupContext.create();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = GroupContext.Type.QUIT;
if (this.store.hasGroups()) {
await this.store.getGroupNumbers(groupId).then(identifiers => {
if (!identifiers) {
return Promise.reject(new Error('Unknown Group'));
}
proto.group.members = identifiers;
return this.store
.deleteGroup(groupId)
.then(() =>
this.sendGroupProto(proto.group.members, proto, Date.now(), options)
);
});
}
proto.group.members = groupIdentifiers;
return this.sendGroupProto(proto.group.members, proto, Date.now(), options);
}
async sendExpirationTimerUpdateToGroup(
groupId,
groupIdentifiers,
expireTimer,
timestamp,
options
) {
if (this.store.hasGroups()) {
// eslint-disable-next-line no-param-reassign
groupIdentifiers = await this.store.getGroupNumbers(groupId);
if (!groupIdentifiers) {
return Promise.reject(new Error('Unknown Group'));
}
}
const myNumber = await this.store.getNumber();
const myUuid = await this.store.getUuid();
const recipients = groupIdentifiers.filter(
identifier => identifier !== myNumber && identifier !== myUuid
);
const attrs = {
recipients,
timestamp,
expireTimer,
flags: DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
group: {
id: groupId,
type: GroupContext.Type.DELIVER,
},
};
if (recipients.length === 0) {
return Promise.resolve({
successfulIdentifiers: [],
failoverIdentifiers: [],
errors: [],
unidentifiedDeliveries: [],
dataMessage: await this.getMessageProtoObj(attrs),
});
}
return this.sendMessage(attrs, options);
}
sendExpirationTimerUpdateToIdentifier(
identifier,
expireTimer,
timestamp,
options = {}
) {
return this.sendMessage(
{
recipients: [identifier],
timestamp,
expireTimer,
flags: DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
},
options
);
}
// backwards compatibility
sendExpirationTimerUpdateToNumber(
number,
expireTimer,
timestamp,
options = {}
) {
return this.sendMessage(
{
recipients: [number],
timestamp,
expireTimer,
flags: DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
},
options
);
}
makeProxiedRequest(url, options) {
return this.server.makeProxiedRequest(url, options);
}
}
exports = module.exports = WebAPI => {
MessageSender.WebAPI = WebAPI;
return MessageSender;
};