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.
		
		
		
		
		
			
		
			
				
	
	
		
			1297 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			1297 lines
		
	
	
		
			37 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();
 | 
						|
 | 
						|
    numbers.forEach(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
 | 
						|
      );
 | 
						|
 | 
						|
      if (
 | 
						|
        number === ourNumber ||
 | 
						|
        haveSession ||
 | 
						|
        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(contactConversation) {
 | 
						|
    if (!contactConversation.isPrivate()) {
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
 | 
						|
    const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
 | 
						|
    const allOurDevices = (await libloki.storage.getAllDevicePubKeysForPrimaryPubKey(
 | 
						|
      primaryDeviceKey
 | 
						|
    ))
 | 
						|
      // Don't send to ourselves
 | 
						|
      .filter(pubKey => pubKey !== textsecure.storage.user.getNumber());
 | 
						|
    if (
 | 
						|
      allOurDevices.includes(contactConversation.id) ||
 | 
						|
      !primaryDeviceKey ||
 | 
						|
      allOurDevices.length === 0
 | 
						|
    ) {
 | 
						|
      // If we havn't got a primaryDeviceKey then we are in the middle of pairing
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
 | 
						|
    const syncMessage = await libloki.api.createContactSyncProtoMessage([
 | 
						|
      contactConversation,
 | 
						|
    ]);
 | 
						|
    const contentMessage = new textsecure.protobuf.Content();
 | 
						|
    contentMessage.syncMessage = syncMessage;
 | 
						|
 | 
						|
    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 = {}) {
 | 
						|
    if (providedNumbers.length === 0) {
 | 
						|
      return Promise.resolve({
 | 
						|
        successfulNumbers: [],
 | 
						|
        failoverNumbers: [],
 | 
						|
        errors: [],
 | 
						|
        unidentifiedDeliveries: [],
 | 
						|
        dataMessage: proto.toArrayBuffer(),
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    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,
 | 
						|
        providedNumbers,
 | 
						|
        proto,
 | 
						|
        callback,
 | 
						|
        silent,
 | 
						|
        options
 | 
						|
      );
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  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:'));
 | 
						|
    /*
 | 
						|
    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 sendToContactPromise = 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 myNumber = textsecure.storage.user.getNumber();
 | 
						|
    // We already sent the reset session to our other devices in the code above!
 | 
						|
    if (number === myNumber) {
 | 
						|
      return sendToContactPromise;
 | 
						|
    }
 | 
						|
 | 
						|
    const buffer = proto.toArrayBuffer();
 | 
						|
    const sendSyncPromise = this.sendSyncMessage(
 | 
						|
      buffer,
 | 
						|
      timestamp,
 | 
						|
      number,
 | 
						|
      null,
 | 
						|
      [],
 | 
						|
      [],
 | 
						|
      options
 | 
						|
    ).catch(logError('resetSession/sendSync error:'));
 | 
						|
 | 
						|
    return Promise.all([sendToContact, sendSyncPromise]);
 | 
						|
    */
 | 
						|
  },
 | 
						|
 | 
						|
  async sendMessageToGroup(
 | 
						|
    groupId,
 | 
						|
    groupNumbers,
 | 
						|
    messageText,
 | 
						|
    attachments,
 | 
						|
    quote,
 | 
						|
    preview,
 | 
						|
    timestamp,
 | 
						|
    expireTimer,
 | 
						|
    profileKey,
 | 
						|
    options
 | 
						|
  ) {
 | 
						|
    const me = textsecure.storage.user.getNumber();
 | 
						|
    let numbers = groupNumbers.filter(number => number !== me);
 | 
						|
    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, 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 ourPK = textsecure.storage.user.getNumber();
 | 
						|
    proto.group.admins = [ourPK];
 | 
						|
 | 
						|
    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);
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  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.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.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,
 | 
						|
};
 |