You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
529 lines
16 KiB
JavaScript
529 lines
16 KiB
JavaScript
/* global textsecure, WebAPI, libsignal, window, libloki, _, libsession */
|
|
|
|
/* 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;
|
|
this.groupInvitation = options.groupInvitation;
|
|
this.sessionRestoration = options.sessionRestoration || false;
|
|
|
|
if (!(this.recipients instanceof Array)) {
|
|
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 || null;
|
|
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;
|
|
}
|
|
|
|
// Set the loki profile
|
|
if (this.profile) {
|
|
const profile = new textsecure.protobuf.DataMessage.LokiProfile();
|
|
if (this.profile.displayName) {
|
|
profile.displayName = this.profile.displayName;
|
|
}
|
|
|
|
const conversation = window.ConversationController.get(
|
|
textsecure.storage.user.getNumber()
|
|
);
|
|
const avatarPointer = conversation.get('avatarPointer');
|
|
if (avatarPointer) {
|
|
profile.avatar = avatarPointer;
|
|
}
|
|
proto.profile = profile;
|
|
}
|
|
|
|
if (this.groupInvitation) {
|
|
proto.groupInvitation = new textsecure.protobuf.DataMessage.GroupInvitation(
|
|
{
|
|
serverAddress: this.groupInvitation.serverAddress,
|
|
channelId: this.groupInvitation.channelId,
|
|
serverName: this.groupInvitation.serverName,
|
|
}
|
|
);
|
|
}
|
|
|
|
if (this.sessionRestoration) {
|
|
proto.flags = textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE;
|
|
}
|
|
|
|
this.dataMessage = proto;
|
|
return proto;
|
|
},
|
|
toArrayBuffer() {
|
|
return this.toProto().toArrayBuffer();
|
|
},
|
|
};
|
|
|
|
function MessageSender() {
|
|
this.server = WebAPI.connect();
|
|
this.pendingMessages = {};
|
|
}
|
|
|
|
MessageSender.prototype = {
|
|
constructor: MessageSender,
|
|
|
|
// makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto
|
|
async makeAttachmentPointer(attachment, publicServer = null, options = {}) {
|
|
const { isRaw = false, isAvatar = false } = options;
|
|
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();
|
|
let attachmentData;
|
|
const server = publicServer || this.server;
|
|
|
|
if (publicServer || isRaw) {
|
|
attachmentData = attachment.data;
|
|
} else {
|
|
proto.key = libsignal.crypto.getRandomBytes(64);
|
|
const iv = libsignal.crypto.getRandomBytes(16);
|
|
const result = await textsecure.crypto.encryptAttachment(
|
|
attachment.data,
|
|
proto.key,
|
|
iv
|
|
);
|
|
proto.digest = result.digest;
|
|
attachmentData = result.ciphertext;
|
|
}
|
|
|
|
const result = isAvatar
|
|
? await server.putAvatar(attachmentData)
|
|
: await server.putAttachment(attachmentData);
|
|
|
|
if (!result) {
|
|
return Promise.reject(
|
|
new Error('Failed to upload data to attachment fileserver')
|
|
);
|
|
}
|
|
const { url, id } = result;
|
|
proto.id = id;
|
|
proto.url = url;
|
|
proto.contentType = attachment.contentType;
|
|
|
|
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, publicServer) {
|
|
return Promise.all(
|
|
message.attachments.map(attachment =>
|
|
this.makeAttachmentPointer(attachment, publicServer)
|
|
)
|
|
)
|
|
.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, publicServer) {
|
|
try {
|
|
const preview = await Promise.all(
|
|
(message.preview || []).map(async item => ({
|
|
...item,
|
|
image: await this.makeAttachmentPointer(item.image, publicServer),
|
|
}))
|
|
);
|
|
// 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, publicServer) {
|
|
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, publicServer).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;
|
|
}
|
|
});
|
|
},
|
|
|
|
uploadAvatar(attachment) {
|
|
// isRaw is true since the data is already encrypted
|
|
// and doesn't need to be encrypted again
|
|
return this.makeAttachmentPointer(attachment, null, {
|
|
isRaw: true,
|
|
isAvatar: true,
|
|
});
|
|
},
|
|
|
|
async sendContactSyncMessage(convos) {
|
|
let convosToSync;
|
|
if (!convos) {
|
|
convosToSync = await libsession.Utils.SyncMessageUtils.getSyncContacts();
|
|
} else {
|
|
convosToSync = convos;
|
|
}
|
|
|
|
if (convosToSync.size === 0) {
|
|
window.console.info('No contacts to sync.');
|
|
|
|
return Promise.resolve();
|
|
}
|
|
libloki.api.debug.logContactSync(
|
|
'Triggering contact sync message with:',
|
|
convosToSync
|
|
);
|
|
|
|
// We need to sync across 3 contacts at a time
|
|
// This is to avoid hitting storage server limit
|
|
const chunked = _.chunk(convosToSync, 3);
|
|
const syncMessages = await Promise.all(
|
|
chunked.map(c => libloki.api.createContactSyncMessage(c))
|
|
);
|
|
|
|
const syncPromises = syncMessages.map(syncMessage =>
|
|
libsession.getMessageQueue().sendSyncMessage(syncMessage)
|
|
);
|
|
|
|
return Promise.all(syncPromises);
|
|
},
|
|
|
|
sendGroupSyncMessage(conversations) {
|
|
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
|
|
// primaryDevicePubKey is set to our own number if we are the master device
|
|
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
|
|
if (!primaryDeviceKey) {
|
|
window.console.debug('sendGroupSyncMessage: no primary device pubkey');
|
|
return Promise.resolve();
|
|
}
|
|
// We only want to sync across closed groups that we haven't left
|
|
const sessionGroups = conversations.filter(
|
|
c => c.isClosedGroup() && !c.get('left') && !c.isMediumGroup()
|
|
);
|
|
if (sessionGroups.length === 0) {
|
|
window.console.info('No closed group to sync.');
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// We need to sync across 1 group at a time
|
|
// This is because we could hit the storage server limit with one group
|
|
const syncPromises = sessionGroups
|
|
.map(c => libloki.api.createGroupSyncMessage(c))
|
|
.map(syncMessage =>
|
|
libsession.getMessageQueue().sendSyncMessage(syncMessage)
|
|
);
|
|
|
|
return Promise.all(syncPromises);
|
|
},
|
|
|
|
async sendOpenGroupsSyncMessage(convos) {
|
|
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
|
|
// primaryDevicePubKey is set to our own number if we are the master device
|
|
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
|
|
if (!primaryDeviceKey) {
|
|
return Promise.resolve();
|
|
}
|
|
const conversations = Array.isArray(convos) ? convos : [convos];
|
|
|
|
const openGroupsConvos = await libsession.Utils.SyncMessageUtils.filterOpenGroupsConvos(
|
|
conversations
|
|
);
|
|
|
|
if (!openGroupsConvos.length) {
|
|
window.log.info('No open groups to sync');
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// Send the whole list of open groups in a single message
|
|
const openGroupsDetails = openGroupsConvos.map(conversation => ({
|
|
url: conversation.id,
|
|
channelId: conversation.get('channelId'),
|
|
}));
|
|
const openGroupsSyncParams = {
|
|
timestamp: Date.now(),
|
|
openGroupsDetails,
|
|
};
|
|
const openGroupsSyncMessage = new libsession.Messages.Outgoing.OpenGroupSyncMessage(
|
|
openGroupsSyncParams
|
|
);
|
|
|
|
return libsession.getMessageQueue().sendSyncMessage(openGroupsSyncMessage);
|
|
},
|
|
syncReadMessages(reads) {
|
|
const myDevice = textsecure.storage.user.getDeviceId();
|
|
// FIXME currently not in used
|
|
if (myDevice !== 1 && myDevice !== '1') {
|
|
const syncReadMessages = new libsession.Messages.Outgoing.SyncReadMessage(
|
|
{
|
|
timestamp: Date.now(),
|
|
readMessages: reads,
|
|
}
|
|
);
|
|
return libsession.getMessageQueue().sendSyncMessage(syncReadMessages);
|
|
}
|
|
|
|
return Promise.resolve();
|
|
},
|
|
async syncVerification(destination, state, identityKey) {
|
|
const myDevice = textsecure.storage.user.getDeviceId();
|
|
// FIXME currently not in used
|
|
if (myDevice === 1 || myDevice === '1') {
|
|
return Promise.resolve();
|
|
}
|
|
// send a session established message (used as a nullMessage)
|
|
const destinationPubKey = new libsession.Types.PubKey(destination);
|
|
|
|
const sessionEstablished = new window.libsession.Messages.Outgoing.SessionEstablishedMessage(
|
|
{ timestamp: Date.now() }
|
|
);
|
|
const { padding } = sessionEstablished;
|
|
await libsession
|
|
.getMessageQueue()
|
|
.send(destinationPubKey, sessionEstablished);
|
|
|
|
const verifiedSyncParams = {
|
|
state,
|
|
destination: destinationPubKey,
|
|
identityKey,
|
|
padding,
|
|
timestamp: Date.now(),
|
|
};
|
|
const verifiedSyncMessage = new window.libsession.Messages.Outgoing.VerifiedSyncMessage(
|
|
verifiedSyncParams
|
|
);
|
|
|
|
return libsession.getMessageQueue().sendSyncMessage(verifiedSyncMessage);
|
|
},
|
|
|
|
makeProxiedRequest(url, options) {
|
|
return this.server.makeProxiedRequest(url, options);
|
|
},
|
|
getProxiedSize(url) {
|
|
return this.server.getProxiedSize(url);
|
|
},
|
|
};
|
|
|
|
window.textsecure = window.textsecure || {};
|
|
|
|
textsecure.MessageSender = function MessageSenderWrapper(username, password) {
|
|
const sender = new MessageSender(username, password);
|
|
this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender);
|
|
this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender);
|
|
this.sendOpenGroupsSyncMessage = sender.sendOpenGroupsSyncMessage.bind(
|
|
sender
|
|
);
|
|
this.uploadAvatar = sender.uploadAvatar.bind(sender);
|
|
this.syncReadMessages = sender.syncReadMessages.bind(sender);
|
|
this.syncVerification = sender.syncVerification.bind(sender);
|
|
this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender);
|
|
this.getProxiedSize = sender.getProxiedSize.bind(sender);
|
|
};
|
|
|
|
textsecure.MessageSender.prototype = {
|
|
constructor: textsecure.MessageSender,
|
|
};
|