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.
		
		
		
		
		
			
		
			
				
	
	
		
			526 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			526 lines
		
	
	
		
			15 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() {
 | |
|     const convosToSync = await libsession.Utils.SyncMessageUtils.getSyncContacts();
 | |
| 
 | |
|     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.isBlocked() &&
 | |
|         !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(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) {
 | |
|       return Promise.resolve();
 | |
|     }
 | |
| 
 | |
|     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 audric currently not in used
 | |
|     if (myDevice !== 1 && myDevice !== '1') {
 | |
|       const syncReadMessages = new libsession.Messages.Outgoing.OpenGroupSyncMessage(
 | |
|         {
 | |
|           readMessages: reads,
 | |
|         }
 | |
|       );
 | |
|       return libsession.getMessageQueue().sendSyncMessage(syncReadMessages);
 | |
|     }
 | |
| 
 | |
|     return Promise.resolve();
 | |
|   },
 | |
|   async syncVerification(destination, state, identityKey) {
 | |
|     const myDevice = textsecure.storage.user.getDeviceId();
 | |
| 
 | |
|     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,
 | |
| };
 |