session-desktop/libtextsecure/sendmessage.js
sachaaaaa a0f67c22da Merge branch 'textsecure/master' into merge_1_21_0
* textsecure/master: (26 commits)
  v1.21.0
  v1.21.0-beta.4
  Dark Theme: Preserve blue background on app loading screen
  Localization updates
  Fix width of audio player when window is very narrow
  A number of small fixes for Link Previews
  Get rid of the white flash when the app starts up (#3083)
  v1.21.0-beta.3
  Lint fixes
  Introduce new language: NB
  Fail over to all numbers in retry if errors don't have numbers
  Use the proper method for pulling attachments off disk for retry
  Fix rendering bug with verified state in updateVerified()
  Update electron-builder and electron-updater
  Ensure that dialog pops up when permissions denied for voice note
  Lint fixes
  Large update to localization strings
  Link Previews
  Ensure that blocked messages are dropped even after sealed sender
  Don't linkify quoted message contents
  ...

# Conflicts:
#	.github/PULL_REQUEST_TEMPLATE.md
#	_locales/cs/messages.json
#	background.html
#	config/default.json
#	index.html
#	js/models/conversations.js
#	js/modules/web_api.js
#	js/settings_start.js
#	js/views/conversation_view.js
#	js/views/settings_view.js
#	package.json
#	protos/SignalService.proto
#	stylesheets/_index.scss
#	stylesheets/_settings.scss
2019-02-06 13:53:20 +11:00

1138 lines
32 KiB
JavaScript

/* global _, textsecure, WebAPI, libsignal, OutgoingMessage, window */
/* 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;
}
function Message(options) {
this.body = options.body;
this.attachments = options.attachments || [];
this.quote = options.quote;
this.preview = options.preview;
this.group = options.group;
this.flags = options.flags;
this.recipients = options.recipients;
this.timestamp = options.timestamp;
this.needsSync = options.needsSync;
this.expireTimer = options.expireTimer;
this.profileKey = options.profileKey;
this.profile = options.profile;
if (!(this.recipients instanceof Array) || this.recipients.length < 1) {
throw new Error('Invalid recipient list');
}
if (!this.group && this.recipients.length > 1) {
throw new Error('Invalid recipient list for non-group');
}
if (typeof this.timestamp !== 'number') {
throw new Error('Invalid timestamp');
}
if (this.expireTimer !== undefined && this.expireTimer !== null) {
if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) {
throw new Error('Invalid expireTimer');
}
}
if (this.attachments) {
if (!(this.attachments instanceof Array)) {
throw new Error('Invalid message attachments');
}
}
if (this.flags !== undefined) {
if (typeof this.flags !== 'number') {
throw new Error('Invalid message flags');
}
}
if (this.isEndSession()) {
if (
this.body !== null ||
this.group !== null ||
this.attachments.length !== 0
) {
throw new Error('Invalid end session message');
}
} else {
if (
typeof this.timestamp !== 'number' ||
(this.body && typeof this.body !== 'string')
) {
throw new Error('Invalid message body');
}
if (this.group) {
if (
typeof this.group.id !== 'string' ||
typeof this.group.type !== 'number'
) {
throw new Error('Invalid group context');
}
}
}
}
Message.prototype = {
constructor: Message,
isEndSession() {
return this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION;
},
toProto() {
if (this.dataMessage instanceof textsecure.protobuf.DataMessage) {
return this.dataMessage;
}
const proto = new textsecure.protobuf.DataMessage();
if (this.body) {
proto.body = this.body;
}
proto.attachments = this.attachmentPointers;
if (this.flags) {
proto.flags = this.flags;
}
if (this.group) {
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(this.group.id);
proto.group.type = this.group.type;
}
if (Array.isArray(this.preview)) {
proto.preview = this.preview.map(preview => {
const item = new textsecure.protobuf.DataMessage.Preview();
item.title = preview.title;
item.url = preview.url;
item.image = preview.image;
return item;
});
}
if (this.quote) {
const { QuotedAttachment } = textsecure.protobuf.DataMessage.Quote;
const { Quote } = textsecure.protobuf.DataMessage;
proto.quote = new Quote();
const { quote } = proto;
quote.id = this.quote.id;
quote.author = this.quote.author;
quote.text = this.quote.text;
quote.attachments = (this.quote.attachments || []).map(attachment => {
const quotedAttachment = new QuotedAttachment();
quotedAttachment.contentType = attachment.contentType;
quotedAttachment.fileName = attachment.fileName;
if (attachment.attachmentPointer) {
quotedAttachment.thumbnail = attachment.attachmentPointer;
}
return quotedAttachment;
});
}
if (this.expireTimer) {
proto.expireTimer = this.expireTimer;
}
if (this.profileKey) {
proto.profileKey = this.profileKey;
}
if (this.profile && this.profile.name) {
const contact = new textsecure.protobuf.DataMessage.Contact();
contact.name = this.profile.name;
proto.profile = contact;
}
this.dataMessage = proto;
return proto;
},
toArrayBuffer() {
return this.toProto().toArrayBuffer();
},
};
function MessageSender(username, password) {
this.server = WebAPI.connect({ username, password });
this.pendingMessages = {};
}
MessageSender.prototype = {
constructor: MessageSender,
// makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto
makeAttachmentPointer(attachment) {
if (typeof attachment !== 'object' || attachment == null) {
return Promise.resolve(undefined);
}
if (
!(attachment.data instanceof ArrayBuffer) &&
!ArrayBuffer.isView(attachment.data)
) {
return Promise.reject(
new TypeError(
`\`attachment.data\` must be an \`ArrayBuffer\` or \`ArrayBufferView\`; got: ${typeof attachment.data}`
)
);
}
const proto = new textsecure.protobuf.AttachmentPointer();
proto.key = libsignal.crypto.getRandomBytes(64);
const iv = libsignal.crypto.getRandomBytes(16);
return textsecure.crypto
.encryptAttachment(attachment.data, proto.key, iv)
.then(result =>
this.server.putAttachment(result.ciphertext).then(id => {
proto.id = id;
proto.contentType = attachment.contentType;
proto.digest = result.digest;
if (attachment.size) {
proto.size = attachment.size;
}
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;
})
);
},
queueJobForNumber(number, runJob) {
const taskWithTimeout = textsecure.createTaskWithTimeout(
runJob,
`queueJobForNumber ${number}`
);
const runPrevious = this.pendingMessages[number] || Promise.resolve();
this.pendingMessages[number] = runPrevious.then(
taskWithTimeout,
taskWithTimeout
);
const runCurrent = this.pendingMessages[number];
runCurrent.then(() => {
if (this.pendingMessages[number] === runCurrent) {
delete this.pendingMessages[number];
}
});
},
uploadAttachments(message) {
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 textsecure.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 textsecure.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 textsecure.MessageError(message, error);
} else {
throw error;
}
});
},
sendMessage(attrs, options) {
const message = new Message(attrs);
const silent = false;
return Promise.all([
this.uploadAttachments(message),
this.uploadThumbnails(message),
this.uploadLinkPreviews(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
);
})
);
},
sendMessageProto(
timestamp,
numbers,
message,
callback,
silent,
options = {}
) {
const rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
if (rejections > 5) {
throw new textsecure.SignedPreKeyRotationError(
numbers,
message.toArrayBuffer(),
timestamp
);
}
const outgoing = new OutgoingMessage(
this.server,
timestamp,
numbers,
message,
silent,
callback,
options
);
numbers.forEach(number => {
this.queueJobForNumber(number, () => outgoing.sendToNumber(number));
});
},
sendMessageProtoAndWait(timestamp, numbers, message, 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,
numbers,
message,
callback,
silent,
options
);
});
},
sendIndividualProto(number, 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,
[number],
proto,
callback,
silent,
options
);
});
},
createSyncMessage() {
const syncMessage = new textsecure.protobuf.SyncMessage();
// Generate a random int from 1 and 512
const buffer = libsignal.crypto.getRandomBytes(1);
const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;
// Generate a random padding buffer of the chosen size
syncMessage.padding = libsignal.crypto.getRandomBytes(paddingLength);
return syncMessage;
},
sendSyncMessage(
encodedDataMessage,
timestamp,
destination,
expirationStartTimestamp,
sentTo = [],
unidentifiedDeliveries = [],
options
) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') {
return Promise.resolve();
}
const dataMessage = textsecure.protobuf.DataMessage.decode(
encodedDataMessage
);
const sentMessage = new textsecure.protobuf.SyncMessage.Sent();
sentMessage.timestamp = timestamp;
sentMessage.message = dataMessage;
if (destination) {
sentMessage.destination = destination;
}
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)
);
// Though this field has 'unidenified' in the name, it should have entries for each
// number we sent to.
if (sentTo && sentTo.length) {
sentMessage.unidentifiedStatus = sentTo.map(number => {
const status = new textsecure.protobuf.SyncMessage.Sent.UnidentifiedDeliveryStatus();
status.destination = number;
status.unidentified = Boolean(unidentifiedLookup[number]);
return status;
});
}
const syncMessage = this.createSyncMessage();
syncMessage.sent = sentMessage;
const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
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);
},
sendRequestConfigurationSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request();
request.type = textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
},
sendRequestGroupSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request();
request.type = textsecure.protobuf.SyncMessage.Request.Type.GROUPS;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
},
sendRequestContactSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request();
request.type = textsecure.protobuf.SyncMessage.Request.Type.CONTACTS;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
},
async sendTypingMessage(options = {}, sendOptions = {}) {
const ACTION_ENUM = textsecure.protobuf.TypingMessage.Action;
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 = textsecure.storage.user.getNumber();
if (recipientId && myNumber === recipientId) {
return null;
}
if (!recipientId && !groupId) {
throw new Error('Need to provide either recipientId or groupId!');
}
const recipients = groupId
? _.without(await textsecure.storage.groups.getNumbers(groupId), myNumber)
: [recipientId];
const groupIdBuffer = groupId
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId)
: null;
const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED;
const finalTimestamp = timestamp || Date.now();
const typingMessage = new textsecure.protobuf.TypingMessage();
typingMessage.groupId = groupIdBuffer;
typingMessage.action = action;
typingMessage.timestamp = finalTimestamp;
const contentMessage = new textsecure.protobuf.Content();
contentMessage.typingMessage = typingMessage;
const silent = true;
const online = true;
return this.sendMessageProtoAndWait(
finalTimestamp,
recipients,
contentMessage,
silent,
{
...sendOptions,
online,
}
);
},
sendDeliveryReceipt(recipientId, timestamp, options) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId();
if (myNumber === recipientId && (myDevice === 1 || myDevice === '1')) {
return Promise.resolve();
}
const receiptMessage = new textsecure.protobuf.ReceiptMessage();
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.DELIVERY;
receiptMessage.timestamp = [timestamp];
const contentMessage = new textsecure.protobuf.Content();
contentMessage.receiptMessage = receiptMessage;
const silent = true;
return this.sendIndividualProto(
recipientId,
contentMessage,
Date.now(),
silent,
options
);
},
sendReadReceipts(sender, timestamps, options) {
const receiptMessage = new textsecure.protobuf.ReceiptMessage();
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
receiptMessage.timestamp = timestamps;
const contentMessage = new textsecure.protobuf.Content();
contentMessage.receiptMessage = receiptMessage;
const silent = true;
return this.sendIndividualProto(
sender,
contentMessage,
Date.now(),
silent,
options
);
},
syncReadMessages(reads, options) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const syncMessage = this.createSyncMessage();
syncMessage.read = [];
for (let i = 0; i < reads.length; i += 1) {
const read = new textsecure.protobuf.SyncMessage.Read();
read.timestamp = reads[i].timestamp;
read.sender = reads[i].sender;
syncMessage.read.push(read);
}
const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
},
syncVerification(destination, state, identityKey, options) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.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 = new textsecure.protobuf.NullMessage();
// Generate a random int from 1 and 512
const buffer = libsignal.crypto.getRandomBytes(1);
const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;
// Generate a random padding buffer of the chosen size
nullMessage.padding = libsignal.crypto.getRandomBytes(paddingLength);
const contentMessage = new textsecure.protobuf.Content();
contentMessage.nullMessage = nullMessage;
// We want the NullMessage to look like a normal outgoing message; not silent
const silent = false;
const promise = this.sendIndividualProto(
destination,
contentMessage,
now,
silent,
options
);
return promise.then(() => {
const verified = new textsecure.protobuf.Verified();
verified.state = state;
verified.destination = destination;
verified.identityKey = identityKey;
verified.nullMessage = nullMessage.padding;
const syncMessage = this.createSyncMessage();
syncMessage.verified = verified;
const secondMessage = new textsecure.protobuf.Content();
secondMessage.syncMessage = syncMessage;
const innerSilent = true;
return this.sendIndividualProto(
myNumber,
secondMessage,
now,
innerSilent,
options
);
});
},
sendGroupProto(providedNumbers, proto, timestamp = Date.now(), options = {}) {
const me = textsecure.storage.user.getNumber();
const numbers = providedNumbers.filter(number => number !== me);
if (numbers.length === 0) {
return Promise.reject(new Error('No other members in the group'));
}
return new Promise((resolve, reject) => {
const silent = true;
const callback = res => {
res.dataMessage = proto.toArrayBuffer();
if (res.errors.length > 0) {
reject(res);
} else {
resolve(res);
}
};
this.sendMessageProto(
timestamp,
numbers,
proto,
callback,
silent,
options
);
});
},
sendMessageToNumber(
number,
messageText,
attachments,
quote,
preview,
timestamp,
expireTimer,
profileKey,
options
) {
const profile = textsecure.storage.impl.getLocalProfile();
return this.sendMessage(
{
recipients: [number],
body: messageText,
timestamp,
attachments,
quote,
preview,
needsSync: true,
expireTimer,
profileKey,
profile,
},
options
);
},
resetSession(number, timestamp, options) {
window.log.info('resetting secure session');
const silent = false;
const proto = new textsecure.protobuf.DataMessage();
proto.body = 'TERMINATE';
proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION;
const logError = prefix => error => {
window.log.error(prefix, error && error.stack ? error.stack : error);
throw error;
};
// The actual deletion of the session now happens later
// as we need to ensure the other contact has successfully
// switch to a new session first.
return this.sendIndividualProto(
number,
proto,
timestamp,
silent,
options
).catch(logError('resetSession/sendToContact error:'));
/*
const deleteAllSessions = targetNumber =>
textsecure.storage.protocol.getDeviceIds(targetNumber).then(deviceIds =>
Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(
targetNumber,
deviceId
);
window.log.info('deleting sessions for', address.toString());
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
)
);
const sendToContact = deleteAllSessions(number)
.catch(logError('resetSession/deleteAllSessions1 error:'))
.then(() => {
window.log.info(
'finished closing local sessions, now sending to contact'
);
return this.sendIndividualProto(
number,
proto,
timestamp,
silent,
options
).catch(logError('resetSession/sendToContact error:'));
})
.then(() =>
deleteAllSessions(number).catch(
logError('resetSession/deleteAllSessions2 error:')
)
);
const buffer = proto.toArrayBuffer();
const sendSync = this.sendSyncMessage(
buffer,
timestamp,
number,
null,
[],
[],
options
).catch(logError('resetSession/sendSync error:'));
return Promise.all([sendToContact, sendSync]);
*/
},
sendMessageToGroup(
groupId,
messageText,
attachments,
quote,
preview,
timestamp,
expireTimer,
profileKey,
options
) {
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
if (targetNumbers === undefined) {
return Promise.reject(new Error('Unknown Group'));
}
const me = textsecure.storage.user.getNumber();
const numbers = targetNumbers.filter(number => number !== me);
if (numbers.length === 0) {
return Promise.reject(new Error('No other members in the group'));
}
return this.sendMessage(
{
recipients: numbers,
body: messageText,
timestamp,
attachments,
quote,
preview,
needsSync: true,
expireTimer,
profileKey,
group: {
id: groupId,
type: textsecure.protobuf.GroupContext.Type.DELIVER,
},
},
options
);
});
},
createGroup(targetNumbers, name, avatar, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
return textsecure.storage.groups
.createNewGroup(targetNumbers)
.then(group => {
proto.group.id = stringToArrayBuffer(group.id);
const { numbers } = group;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = numbers;
proto.group.name = name;
return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment;
return this.sendGroupProto(numbers, proto, Date.now(), options).then(
() => proto.group.id
);
});
});
},
updateGroup(groupId, name, avatar, targetNumbers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups
.addNumbers(groupId, targetNumbers)
.then(numbers => {
if (numbers === undefined) {
return Promise.reject(new Error('Unknown Group'));
}
proto.group.members = numbers;
return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment;
return this.sendGroupProto(numbers, proto, Date.now(), options).then(
() => proto.group.id
);
});
});
},
addNumberToGroup(groupId, number, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups
.addNumbers(groupId, [number])
.then(numbers => {
if (numbers === undefined)
return Promise.reject(new Error('Unknown Group'));
proto.group.members = numbers;
return this.sendGroupProto(numbers, proto, Date.now(), options);
});
},
setGroupName(groupId, name, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups.getNumbers(groupId).then(numbers => {
if (numbers === undefined)
return Promise.reject(new Error('Unknown Group'));
proto.group.members = numbers;
return this.sendGroupProto(numbers, proto, Date.now(), options);
});
},
setGroupAvatar(groupId, avatar, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.getNumbers(groupId).then(numbers => {
if (numbers === undefined)
return Promise.reject(new Error('Unknown Group'));
proto.group.members = numbers;
return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment;
return this.sendGroupProto(numbers, proto, Date.now(), options);
});
});
},
leaveGroup(groupId, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
return textsecure.storage.groups.getNumbers(groupId).then(numbers => {
if (numbers === undefined)
return Promise.reject(new Error('Unknown Group'));
return textsecure.storage.groups
.deleteGroup(groupId)
.then(() => this.sendGroupProto(numbers, proto, Date.now(), options));
});
},
sendExpirationTimerUpdateToGroup(
groupId,
expireTimer,
timestamp,
profileKey,
options
) {
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
if (targetNumbers === undefined)
return Promise.reject(new Error('Unknown Group'));
const me = textsecure.storage.user.getNumber();
const numbers = targetNumbers.filter(number => number !== me);
if (numbers.length === 0) {
return Promise.reject(new Error('No other members in the group'));
}
return this.sendMessage(
{
recipients: numbers,
timestamp,
needsSync: true,
expireTimer,
profileKey,
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
group: {
id: groupId,
type: textsecure.protobuf.GroupContext.Type.DELIVER,
},
},
options
);
});
},
sendExpirationTimerUpdateToNumber(
number,
expireTimer,
timestamp,
profileKey,
options
) {
return this.sendMessage(
{
recipients: [number],
timestamp,
needsSync: true,
expireTimer,
profileKey,
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
},
options
);
},
makeProxiedRequest(url, options) {
return this.server.makeProxiedRequest(url, options);
},
getProxiedSize(url) {
return this.server.getProxiedSize(url);
},
};
window.textsecure = window.textsecure || {};
textsecure.MessageSender = function MessageSenderWrapper(
url,
username,
password,
cdnUrl
) {
const sender = new MessageSender(url, username, password, cdnUrl);
this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(
sender
);
this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup.bind(
sender
);
this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage.bind(
sender
);
this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage.bind(
sender
);
this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(
sender
);
this.sendMessageToNumber = sender.sendMessageToNumber.bind(sender);
this.sendMessage = sender.sendMessage.bind(sender);
this.resetSession = sender.resetSession.bind(sender);
this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
this.sendTypingMessage = sender.sendTypingMessage.bind(sender);
this.createGroup = sender.createGroup.bind(sender);
this.updateGroup = sender.updateGroup.bind(sender);
this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
this.setGroupName = sender.setGroupName.bind(sender);
this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
this.leaveGroup = sender.leaveGroup.bind(sender);
this.sendSyncMessage = sender.sendSyncMessage.bind(sender);
this.getProfile = sender.getProfile.bind(sender);
this.getAvatar = sender.getAvatar.bind(sender);
this.syncReadMessages = sender.syncReadMessages.bind(sender);
this.syncVerification = sender.syncVerification.bind(sender);
this.sendDeliveryReceipt = sender.sendDeliveryReceipt.bind(sender);
this.sendReadReceipts = sender.sendReadReceipts.bind(sender);
this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender);
this.getProxiedSize = sender.getProxiedSize.bind(sender);
};
textsecure.MessageSender.prototype = {
constructor: textsecure.MessageSender,
};