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.
		
		
		
		
		
			
		
			
				
	
	
		
			1355 lines
		
	
	
		
			39 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			1355 lines
		
	
	
		
			39 KiB
		
	
	
	
		
			JavaScript
		
	
| /* global _, textsecure, WebAPI, libsignal, OutgoingMessage, window, libloki */
 | |
| 
 | |
| /* 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(username, password) {
 | |
|   this.server = WebAPI.connect({ username, password });
 | |
|   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;
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   sendMessage(attrs, options) {
 | |
|     const message = new Message(attrs);
 | |
|     const silent = false;
 | |
|     const publicServer =
 | |
|       options.publicSendData && options.publicSendData.serverAPI;
 | |
| 
 | |
|     return Promise.all([
 | |
|       this.uploadAttachments(message, publicServer),
 | |
|       this.uploadThumbnails(message, publicServer),
 | |
|       this.uploadLinkPreviews(message, publicServer),
 | |
|     ]).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
 | |
|     );
 | |
| 
 | |
|     const ourNumber = textsecure.storage.user.getNumber();
 | |
| 
 | |
|     // Check wether we have the keys to start a session with the user
 | |
|     const hasKeys = async number => {
 | |
|       try {
 | |
|         const [preKey, signedPreKey] = await Promise.all([
 | |
|           textsecure.storage.protocol.loadContactPreKey(number),
 | |
|           textsecure.storage.protocol.loadContactSignedPreKey(number),
 | |
|         ]);
 | |
|         return preKey !== undefined && signedPreKey !== undefined;
 | |
|       } catch (e) {
 | |
|         return false;
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     // Note: Since we're just doing independant tasks,
 | |
|     // using `async` in the `forEach` loop should be fine.
 | |
|     // If however we want to use the results from forEach then
 | |
|     // we would need to convert this to a Promise.all(numbers.map(...))
 | |
|     numbers.forEach(async number => {
 | |
|       // Note: if we are sending a private group message, we do our best to
 | |
|       // ensure we have signal protocol sessions with every member, but if we
 | |
|       // fail, let's at least send messages to those members with which we do:
 | |
|       const haveSession = _.some(
 | |
|         textsecure.storage.protocol.sessions,
 | |
|         s => s.number === number
 | |
|       );
 | |
| 
 | |
|       let keysFound = false;
 | |
|       // If we don't have a session but we already have prekeys to
 | |
|       // start communication then we should use them
 | |
|       if (!haveSession && !options.isPublic) {
 | |
|         keysFound = await hasKeys(number);
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         number === ourNumber ||
 | |
|         haveSession ||
 | |
|         keysFound ||
 | |
|         options.isPublic ||
 | |
|         options.messageType === 'friend-request'
 | |
|       ) {
 | |
|         this.queueJobForNumber(number, () => outgoing.sendToNumber(number));
 | |
|       } else {
 | |
|         window.log.error(`No session for number: ${number}`);
 | |
|         // If it was a message to a group then we need to send a session request
 | |
|         if (outgoing.isGroup) {
 | |
|           this.sendMessageToNumber(
 | |
|             number,
 | |
|             '(If you see this message, you must be using an out-of-date client)',
 | |
|             [],
 | |
|             undefined,
 | |
|             [],
 | |
|             Date.now(),
 | |
|             undefined,
 | |
|             undefined,
 | |
|             { messageType: 'friend-request', sessionRequest: true }
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   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;
 | |
|   },
 | |
| 
 | |
|   async sendSyncMessage(
 | |
|     encodedDataMessage,
 | |
|     timestamp,
 | |
|     destination,
 | |
|     expirationStartTimestamp,
 | |
|     sentTo = [],
 | |
|     unidentifiedDeliveries = [],
 | |
|     options
 | |
|   ) {
 | |
|     const primaryDeviceKey =
 | |
|       window.storage.get('primaryDevicePubKey') ||
 | |
|       textsecure.storage.user.getNumber();
 | |
|     const allOurDevices = (await libloki.storage.getAllDevicePubKeysForPrimaryPubKey(
 | |
|       primaryDeviceKey
 | |
|     ))
 | |
|       // Don't send to ourselves
 | |
|       .filter(pubKey => pubKey !== textsecure.storage.user.getNumber());
 | |
|     if (allOurDevices.length === 0) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     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(
 | |
|       primaryDeviceKey,
 | |
|       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);
 | |
|   },
 | |
| 
 | |
|   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,
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   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();
 | |
|   },
 | |
| 
 | |
|   async sendContactSyncMessage(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();
 | |
|     }
 | |
| 
 | |
|     // We need to sync across 3 contacts at a time
 | |
|     // This is to avoid hitting storage server limit
 | |
|     const chunked = _.chunk(conversations, 3);
 | |
|     const syncMessages = await Promise.all(
 | |
|       chunked.map(c => libloki.api.createContactSyncProtoMessage(c))
 | |
|     );
 | |
|     const syncPromises = syncMessages
 | |
|       .filter(message => message != null)
 | |
|       .map(syncMessage => {
 | |
|         const contentMessage = new textsecure.protobuf.Content();
 | |
|         contentMessage.syncMessage = syncMessage;
 | |
| 
 | |
|         const silent = true;
 | |
|         return this.sendIndividualProto(
 | |
|           primaryDeviceKey,
 | |
|           contentMessage,
 | |
|           Date.now(),
 | |
|           silent,
 | |
|           {} // options
 | |
|         );
 | |
|       });
 | |
| 
 | |
|     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) {
 | |
|       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 = conversations
 | |
|       .map(c => libloki.api.createGroupSyncProtoMessage([c]))
 | |
|       .filter(message => message != null)
 | |
|       .map(syncMessage => {
 | |
|         const contentMessage = new textsecure.protobuf.Content();
 | |
|         contentMessage.syncMessage = syncMessage;
 | |
| 
 | |
|         const silent = true;
 | |
|         return this.sendIndividualProto(
 | |
|           primaryDeviceKey,
 | |
|           contentMessage,
 | |
|           Date.now(),
 | |
|           silent,
 | |
|           {} // options
 | |
|         );
 | |
|       });
 | |
| 
 | |
|     return Promise.all(syncPromises);
 | |
|   },
 | |
| 
 | |
|   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();
 | |
|     }
 | |
| 
 | |
|     // Send the whole list of open groups in a single message
 | |
| 
 | |
|     const openGroupsSyncMessage = libloki.api.createOpenGroupsSyncProtoMessage(
 | |
|       conversations
 | |
|     );
 | |
| 
 | |
|     if (!openGroupsSyncMessage) {
 | |
|       window.log.info('No open groups to sync');
 | |
|       return Promise.resolve();
 | |
|     }
 | |
| 
 | |
|     const contentMessage = new textsecure.protobuf.Content();
 | |
|     contentMessage.syncMessage = openGroupsSyncMessage;
 | |
| 
 | |
|     const silent = true;
 | |
|     return this.sendIndividualProto(
 | |
|       primaryDeviceKey,
 | |
|       contentMessage,
 | |
|       Date.now(),
 | |
|       silent,
 | |
|       {} // options
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   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, groupNumbers, 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();
 | |
|     const primaryDevicePubkey = window.storage.get('primaryDevicePubKey');
 | |
|     if (recipientId && primaryDevicePubkey === recipientId) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     if (!recipientId && !groupId) {
 | |
|       throw new Error('Need to provide either recipientId or groupId!');
 | |
|     }
 | |
| 
 | |
|     const recipients = groupId
 | |
|       ? _.without(groupNumbers, 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 = {}) {
 | |
|     // We always assume that only primary device is a member in the group
 | |
|     const primaryDeviceKey =
 | |
|       window.storage.get('primaryDevicePubKey') ||
 | |
|       textsecure.storage.user.getNumber();
 | |
|     const numbers = providedNumbers.filter(
 | |
|       number => number !== primaryDeviceKey
 | |
|     );
 | |
|     if (numbers.length === 0) {
 | |
|       return Promise.resolve({
 | |
|         successfulNumbers: [],
 | |
|         failoverNumbers: [],
 | |
|         errors: [],
 | |
|         unidentifiedDeliveries: [],
 | |
|         dataMessage: proto.toArrayBuffer(),
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     const sendPromise = 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
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     return sendPromise.then(result => {
 | |
|       // Sync the group message to our other devices
 | |
|       const encoded = textsecure.protobuf.DataMessage.encode(proto);
 | |
|       this.sendSyncMessage(encoded, timestamp, null, null, [], [], options);
 | |
|       return result;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   async getMessageProto(
 | |
|     number,
 | |
|     body,
 | |
|     attachments,
 | |
|     quote,
 | |
|     preview,
 | |
|     timestamp,
 | |
|     expireTimer,
 | |
|     profileKey,
 | |
|     flags
 | |
|   ) {
 | |
|     const attributes = {
 | |
|       recipients: [number],
 | |
|       body,
 | |
|       timestamp,
 | |
|       attachments,
 | |
|       quote,
 | |
|       preview,
 | |
|       expireTimer,
 | |
|       profileKey,
 | |
|       flags,
 | |
|     };
 | |
| 
 | |
|     return this.getMessageProtoObj(attributes);
 | |
|   },
 | |
| 
 | |
|   async getMessageProtoObj(attributes) {
 | |
|     const message = new Message(attributes);
 | |
|     await Promise.all([
 | |
|       this.uploadAttachments(message),
 | |
|       this.uploadThumbnails(message),
 | |
|       this.uploadLinkPreviews(message),
 | |
|     ]);
 | |
| 
 | |
|     return message.toArrayBuffer();
 | |
|   },
 | |
| 
 | |
|   getOurProfile() {
 | |
|     try {
 | |
|       // Secondary devices have their profile stored
 | |
|       // in their primary device's conversation
 | |
|       const ourNumber = window.storage.get('primaryDevicePubKey');
 | |
|       const conversation = window.ConversationController.get(ourNumber);
 | |
|       return conversation.getLokiProfile();
 | |
|     } catch (e) {
 | |
|       window.log.error(`Failed to get our profile: ${e}`);
 | |
|       return null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   async sendMessageToNumber(
 | |
|     number,
 | |
|     messageText,
 | |
|     attachments,
 | |
|     quote,
 | |
|     preview,
 | |
|     timestamp,
 | |
|     expireTimer,
 | |
|     profileKey,
 | |
|     options
 | |
|   ) {
 | |
|     const profile = this.getOurProfile();
 | |
| 
 | |
|     const flags = options.sessionRequest
 | |
|       ? textsecure.protobuf.DataMessage.Flags.SESSION_REQUEST
 | |
|       : undefined;
 | |
| 
 | |
|     const { groupInvitation, sessionRestoration } = options;
 | |
| 
 | |
|     return this.sendMessage(
 | |
|       {
 | |
|         recipients: [number],
 | |
|         body: messageText,
 | |
|         timestamp,
 | |
|         attachments,
 | |
|         quote,
 | |
|         preview,
 | |
|         needsSync: true,
 | |
|         expireTimer,
 | |
|         profileKey,
 | |
|         profile,
 | |
|         flags,
 | |
|         groupInvitation,
 | |
|         sessionRestoration,
 | |
|       },
 | |
|       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:'));
 | |
|   },
 | |
| 
 | |
|   async sendMessageToGroup(
 | |
|     groupId,
 | |
|     groupNumbers,
 | |
|     messageText,
 | |
|     attachments,
 | |
|     quote,
 | |
|     preview,
 | |
|     timestamp,
 | |
|     expireTimer,
 | |
|     profileKey,
 | |
|     options
 | |
|   ) {
 | |
|     // We always assume that only primary device is a member in the group
 | |
|     const primaryDeviceKey =
 | |
|       window.storage.get('primaryDevicePubKey') ||
 | |
|       textsecure.storage.user.getNumber();
 | |
|     let numbers = groupNumbers.filter(number => number !== primaryDeviceKey);
 | |
|     if (options.isPublic) {
 | |
|       numbers = [groupId];
 | |
|     }
 | |
|     const profile = this.getOurProfile();
 | |
|     const attrs = {
 | |
|       recipients: numbers,
 | |
|       body: messageText,
 | |
|       timestamp,
 | |
|       attachments,
 | |
|       quote,
 | |
|       preview,
 | |
|       needsSync: true,
 | |
|       expireTimer,
 | |
|       profileKey,
 | |
|       profile,
 | |
|       group: {
 | |
|         id: groupId,
 | |
|         type: textsecure.protobuf.GroupContext.Type.DELIVER,
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     if (numbers.length === 0) {
 | |
|       return Promise.resolve({
 | |
|         successfulNumbers: [],
 | |
|         failoverNumbers: [],
 | |
|         errors: [],
 | |
|         unidentifiedDeliveries: [],
 | |
|         dataMessage: await this.getMessageProtoObj(attrs),
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     return this.sendMessage(attrs, options);
 | |
|   },
 | |
| 
 | |
|   updateGroup(groupId, name, avatar, members, admins, recipients, 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;
 | |
|     proto.group.members = members;
 | |
| 
 | |
|     const primaryDeviceKey =
 | |
|       window.storage.get('primaryDevicePubKey') ||
 | |
|       textsecure.storage.user.getNumber();
 | |
|     proto.group.admins = [primaryDeviceKey];
 | |
| 
 | |
|     return this.makeAttachmentPointer(avatar).then(attachment => {
 | |
|       proto.group.avatar = attachment;
 | |
|       // TODO: re-enable this once we have attachments
 | |
|       proto.group.avatar = null;
 | |
|       return this.sendGroupProto(recipients, proto, Date.now(), options).then(
 | |
|         () => proto.group.id
 | |
|       );
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   addNumberToGroup(groupId, newNumbers, 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.members = newNumbers;
 | |
|     return this.sendGroupProto(newNumbers, proto, Date.now(), options);
 | |
|   },
 | |
| 
 | |
|   setGroupName(groupId, name, groupNumbers, 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;
 | |
|     proto.group.members = groupNumbers;
 | |
| 
 | |
|     return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
 | |
|   },
 | |
| 
 | |
|   setGroupAvatar(groupId, avatar, groupNumbers, 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.members = groupNumbers;
 | |
| 
 | |
|     return this.makeAttachmentPointer(avatar).then(attachment => {
 | |
|       proto.group.avatar = attachment;
 | |
|       return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   requestGroupInfo(groupId, groupNumbers, 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.REQUEST_INFO;
 | |
|     return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
 | |
|   },
 | |
| 
 | |
|   leaveGroup(groupId, groupNumbers, 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 this.sendGroupProto(groupNumbers, proto, Date.now(), options);
 | |
|   },
 | |
|   async sendExpirationTimerUpdateToGroup(
 | |
|     groupId,
 | |
|     groupNumbers,
 | |
|     expireTimer,
 | |
|     timestamp,
 | |
|     profileKey,
 | |
|     options
 | |
|   ) {
 | |
|     const me = textsecure.storage.user.getNumber();
 | |
|     const numbers = groupNumbers.filter(number => number !== me);
 | |
|     const attrs = {
 | |
|       recipients: numbers,
 | |
|       timestamp,
 | |
|       needsSync: true,
 | |
|       expireTimer,
 | |
|       profileKey,
 | |
|       flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
 | |
|       group: {
 | |
|         id: groupId,
 | |
|         type: textsecure.protobuf.GroupContext.Type.DELIVER,
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     if (numbers.length === 0) {
 | |
|       return Promise.resolve({
 | |
|         successfulNumbers: [],
 | |
|         failoverNumbers: [],
 | |
|         errors: [],
 | |
|         unidentifiedDeliveries: [],
 | |
|         dataMessage: await this.getMessageProtoObj(attrs),
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     return this.sendMessage(attrs, 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(username, password) {
 | |
|   const sender = new MessageSender(username, password);
 | |
| 
 | |
|   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.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender);
 | |
|   this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender);
 | |
|   this.sendOpenGroupsSyncMessage = sender.sendOpenGroupsSyncMessage.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.updateGroup = sender.updateGroup.bind(sender);
 | |
|   this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
 | |
|   this.setGroupName = sender.setGroupName.bind(sender);
 | |
|   this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
 | |
|   this.requestGroupInfo = sender.requestGroupInfo.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.uploadAvatar = sender.uploadAvatar.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);
 | |
|   this.getMessageProto = sender.getMessageProto.bind(sender);
 | |
| };
 | |
| 
 | |
| textsecure.MessageSender.prototype = {
 | |
|   constructor: textsecure.MessageSender,
 | |
| };
 |