function stringToArrayBuffer(str) {
  if (typeof str !== 'string') {
    throw new Error('Passed non-string to stringToArrayBuffer');
  }
  var res = new ArrayBuffer(str.length);
  var uint = new Uint8Array(res);
  for (var i = 0; i < str.length; i++) {
    uint[i] = str.charCodeAt(i);
  }
  return res;
}

function Message(options) {
  this.body = options.body;
  this.attachments = options.attachments || [];
  this.quote = options.quote;
  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;

  if (!(this.recipients instanceof Array) || this.recipients.length < 1) {
    throw new Error('Invalid recipient list');
  }

  if (!this.group && this.recipients.length > 1) {
    throw new Error('Invalid recipient list for non-group');
  }

  if (typeof this.timestamp !== 'number') {
    throw new Error('Invalid timestamp');
  }

  if (this.expireTimer !== undefined && this.expireTimer !== null) {
    if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) {
      throw new Error('Invalid expireTimer');
    }
  }

  if (this.attachments) {
    if (!(this.attachments instanceof Array)) {
      throw new Error('Invalid message attachments');
    }
  }
  if (this.flags !== undefined) {
    if (typeof this.flags !== 'number') {
      throw new Error('Invalid message flags');
    }
  }
  if (this.isEndSession()) {
    if (
      this.body !== null ||
      this.group !== null ||
      this.attachments.length !== 0
    ) {
      throw new Error('Invalid end session message');
    }
  } else {
    if (
      typeof this.timestamp !== 'number' ||
      (this.body && typeof this.body !== 'string')
    ) {
      throw new Error('Invalid message body');
    }
    if (this.group) {
      if (
        typeof this.group.id !== 'string' ||
        typeof this.group.type !== 'number'
      ) {
        throw new Error('Invalid group context');
      }
    }
  }
}

Message.prototype = {
  constructor: Message,
  isEndSession: function() {
    return this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION;
  },
  toProto: function() {
    if (this.dataMessage instanceof textsecure.protobuf.DataMessage) {
      return this.dataMessage;
    }
    var 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 (this.quote) {
      var QuotedAttachment =
        textsecure.protobuf.DataMessage.Quote.QuotedAttachment;
      var Quote = textsecure.protobuf.DataMessage.Quote;

      proto.quote = new Quote();
      var quote = proto.quote;

      quote.id = this.quote.id;
      quote.author = this.quote.author;
      quote.text = this.quote.text;
      quote.attachments = (this.quote.attachments || []).map(function(
        attachment
      ) {
        var 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;
    }

    this.dataMessage = proto;
    return proto;
  },
  toArrayBuffer: function() {
    return this.toProto().toArrayBuffer();
  },
};

function MessageSender(url, username, password, cdn_url) {
  this.server = new TextSecureServer(url, username, password, cdn_url);
  this.pendingMessages = {};
}

MessageSender.prototype = {
  constructor: MessageSender,

  //  makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto
  makeAttachmentPointer: function(attachment) {
    if (typeof attachment !== 'object' || attachment == null) {
      return Promise.resolve(undefined);
    }

    if (
      !(attachment.data instanceof ArrayBuffer) &&
      !ArrayBuffer.isView(attachment.data)
    ) {
      return Promise.reject(
        new TypeError(
          '`attachment.data` must be an `ArrayBuffer` or `ArrayBufferView`; got: ' +
            typeof attachment.data
        )
      );
    }

    var proto = new textsecure.protobuf.AttachmentPointer();
    proto.key = libsignal.crypto.getRandomBytes(64);

    var iv = libsignal.crypto.getRandomBytes(16);
    return textsecure.crypto
      .encryptAttachment(attachment.data, proto.key, iv)
      .then(
        function(result) {
          return this.server
            .putAttachment(result.ciphertext)
            .then(function(id) {
              proto.id = id;
              proto.contentType = attachment.contentType;
              proto.digest = result.digest;
              if (attachment.fileName) {
                proto.fileName = attachment.fileName;
              }
              if (attachment.size) {
                proto.size = attachment.size;
              }
              if (attachment.flags) {
                proto.flags = attachment.flags;
              }
              return proto;
            });
        }.bind(this)
      );
  },

  retransmitMessage: function(number, jsonData, timestamp) {
    var outgoing = new OutgoingMessage(this.server);
    return outgoing.transmitMessage(number, jsonData, timestamp);
  },

  validateRetryContentMessage: function(content) {
    // We want at least one field set, but not more than one
    var count = 0;
    count += content.syncMessage ? 1 : 0;
    count += content.dataMessage ? 1 : 0;
    count += content.callMessage ? 1 : 0;
    count += content.nullMessage ? 1 : 0;
    if (count !== 1) {
      return false;
    }

    // It's most likely that dataMessage will be populated, so we look at it in detail
    var data = content.dataMessage;
    if (
      data &&
      !data.attachments.length &&
      !data.body &&
      !data.expireTimer &&
      !data.flags &&
      !data.group
    ) {
      return false;
    }

    return true;
  },

  getRetryProto: function(message, timestamp) {
    // If message was sent before v0.41.3 was released on Aug 7, then it was most certainly a DataMessage
    //
    // var d = new Date('2017-08-07T07:00:00.000Z');
    // d.getTime();
    var august7 = 1502089200000;
    if (timestamp < august7) {
      return textsecure.protobuf.DataMessage.decode(message);
    }

    // This is ugly. But we don't know what kind of proto we need to decode...
    try {
      // Simply decoding as a Content message may throw
      var proto = textsecure.protobuf.Content.decode(message);

      // But it might also result in an invalid object, so we try to detect that
      if (this.validateRetryContentMessage(proto)) {
        return proto;
      }

      return textsecure.protobuf.DataMessage.decode(message);
    } catch (e) {
      // If this call throws, something has really gone wrong, we'll fail to send
      return textsecure.protobuf.DataMessage.decode(message);
    }
  },

  tryMessageAgain: function(number, encodedMessage, timestamp) {
    var proto = this.getRetryProto(encodedMessage, timestamp);
    return this.sendIndividualProto(number, proto, timestamp);
  },

  queueJobForNumber: function(number, runJob) {
    var taskWithTimeout = textsecure.createTaskWithTimeout(
      runJob,
      'queueJobForNumber ' + number
    );

    var runPrevious = this.pendingMessages[number] || Promise.resolve();
    var runCurrent = (this.pendingMessages[number] = runPrevious.then(
      taskWithTimeout,
      taskWithTimeout
    ));
    runCurrent.then(
      function() {
        if (this.pendingMessages[number] === runCurrent) {
          delete this.pendingMessages[number];
        }
      }.bind(this)
    );
  },

  uploadAttachments: function(message) {
    return Promise.all(
      message.attachments.map(this.makeAttachmentPointer.bind(this))
    )
      .then(function(attachmentPointers) {
        message.attachmentPointers = attachmentPointers;
      })
      .catch(function(error) {
        if (error instanceof Error && error.name === 'HTTPError') {
          throw new textsecure.MessageError(message, error);
        } else {
          throw error;
        }
      });
  },

  uploadThumbnails: function(message) {
    var makePointer = this.makeAttachmentPointer.bind(this);
    var quote = message.quote;

    if (!quote || !quote.attachments || quote.attachments.length === 0) {
      return Promise.resolve();
    }

    return Promise.all(
      quote.attachments.map(function(attachment) {
        const thumbnail = attachment.thumbnail;
        if (!thumbnail) {
          return;
        }

        return makePointer(thumbnail).then(function(pointer) {
          attachment.attachmentPointer = pointer;
        });
      })
    ).catch(function(error) {
      if (error instanceof Error && error.name === 'HTTPError') {
        throw new textsecure.MessageError(message, error);
      } else {
        throw error;
      }
    });
  },

  sendMessage: function(attrs) {
    var message = new Message(attrs);
    return Promise.all([
      this.uploadAttachments(message),
      this.uploadThumbnails(message),
    ]).then(
      function() {
        return new Promise(
          function(resolve, reject) {
            this.sendMessageProto(
              message.timestamp,
              message.recipients,
              message.toProto(),
              function(res) {
                res.dataMessage = message.toArrayBuffer();
                if (res.errors.length > 0) {
                  reject(res);
                } else {
                  resolve(res);
                }
              }
            );
          }.bind(this)
        );
      }.bind(this)
    );
  },
  sendMessageProto: function(timestamp, numbers, message, callback, silent) {
    var rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
    if (rejections > 5) {
      throw new textsecure.SignedPreKeyRotationError(
        numbers,
        message.toArrayBuffer(),
        timestamp
      );
    }

    var outgoing = new OutgoingMessage(
      this.server,
      timestamp,
      numbers,
      message,
      silent,
      callback
    );

    numbers.forEach(
      function(number) {
        this.queueJobForNumber(number, function() {
          return outgoing.sendToNumber(number);
        });
      }.bind(this)
    );
  },

  retrySendMessageProto: function(numbers, encodedMessage, timestamp) {
    var proto = textsecure.protobuf.DataMessage.decode(encodedMessage);
    return new Promise(
      function(resolve, reject) {
        this.sendMessageProto(timestamp, numbers, proto, function(res) {
          if (res.errors.length > 0) {
            reject(res);
          } else {
            resolve(res);
          }
        });
      }.bind(this)
    );
  },

  sendIndividualProto: function(number, proto, timestamp, silent) {
    return new Promise(
      function(resolve, reject) {
        var callback = function(res) {
          if (res.errors.length > 0) {
            reject(res);
          } else {
            resolve(res);
          }
        };
        this.sendMessageProto(timestamp, [number], proto, callback, silent);
      }.bind(this)
    );
  },

  createSyncMessage: function() {
    var syncMessage = new textsecure.protobuf.SyncMessage();

    // Generate a random int from 1 and 512
    var buffer = libsignal.crypto.getRandomBytes(1);
    var paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;

    // Generate a random padding buffer of the chosen size
    syncMessage.padding = libsignal.crypto.getRandomBytes(paddingLength);

    return syncMessage;
  },

  sendSyncMessage: function(
    encodedDataMessage,
    timestamp,
    destination,
    expirationStartTimestamp
  ) {
    var myNumber = textsecure.storage.user.getNumber();
    var myDevice = textsecure.storage.user.getDeviceId();
    if (myDevice == 1) {
      return Promise.resolve();
    }

    var dataMessage = textsecure.protobuf.DataMessage.decode(
      encodedDataMessage
    );
    var sentMessage = new textsecure.protobuf.SyncMessage.Sent();
    sentMessage.timestamp = timestamp;
    sentMessage.message = dataMessage;
    if (destination) {
      sentMessage.destination = destination;
    }
    if (expirationStartTimestamp) {
      sentMessage.expirationStartTimestamp = expirationStartTimestamp;
    }
    var syncMessage = this.createSyncMessage();
    syncMessage.sent = sentMessage;
    var contentMessage = new textsecure.protobuf.Content();
    contentMessage.syncMessage = syncMessage;

    var silent = true;
    return this.sendIndividualProto(
      myNumber,
      contentMessage,
      Date.now(),
      silent
    );
  },

  getProfile: function(number) {
    return this.server.getProfile(number);
  },
  getAvatar: function(path) {
    return this.server.getAvatar(path);
  },

  sendRequestConfigurationSyncMessage: function() {
    var myNumber = textsecure.storage.user.getNumber();
    var myDevice = textsecure.storage.user.getDeviceId();
    if (myDevice != 1) {
      var request = new textsecure.protobuf.SyncMessage.Request();
      request.type = textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION;
      var syncMessage = this.createSyncMessage();
      syncMessage.request = request;
      var contentMessage = new textsecure.protobuf.Content();
      contentMessage.syncMessage = syncMessage;

      var silent = true;
      return this.sendIndividualProto(
        myNumber,
        contentMessage,
        Date.now(),
        silent
      );
    }

    return Promise.resolve();
  },
  sendRequestGroupSyncMessage: function() {
    var myNumber = textsecure.storage.user.getNumber();
    var myDevice = textsecure.storage.user.getDeviceId();
    if (myDevice != 1) {
      var request = new textsecure.protobuf.SyncMessage.Request();
      request.type = textsecure.protobuf.SyncMessage.Request.Type.GROUPS;
      var syncMessage = this.createSyncMessage();
      syncMessage.request = request;
      var contentMessage = new textsecure.protobuf.Content();
      contentMessage.syncMessage = syncMessage;

      var silent = true;
      return this.sendIndividualProto(
        myNumber,
        contentMessage,
        Date.now(),
        silent
      );
    }

    return Promise.resolve();
  },

  sendRequestContactSyncMessage: function() {
    var myNumber = textsecure.storage.user.getNumber();
    var myDevice = textsecure.storage.user.getDeviceId();
    if (myDevice != 1) {
      var request = new textsecure.protobuf.SyncMessage.Request();
      request.type = textsecure.protobuf.SyncMessage.Request.Type.CONTACTS;
      var syncMessage = this.createSyncMessage();
      syncMessage.request = request;
      var contentMessage = new textsecure.protobuf.Content();
      contentMessage.syncMessage = syncMessage;

      var silent = true;
      return this.sendIndividualProto(
        myNumber,
        contentMessage,
        Date.now(),
        silent
      );
    }

    return Promise.resolve();
  },
  sendReadReceipts: function(sender, timestamps) {
    var receiptMessage = new textsecure.protobuf.ReceiptMessage();
    receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
    receiptMessage.timestamp = timestamps;

    var contentMessage = new textsecure.protobuf.Content();
    contentMessage.receiptMessage = receiptMessage;

    var silent = true;
    return this.sendIndividualProto(sender, contentMessage, Date.now(), silent);
  },
  syncReadMessages: function(reads) {
    var myNumber = textsecure.storage.user.getNumber();
    var myDevice = textsecure.storage.user.getDeviceId();
    if (myDevice != 1) {
      var syncMessage = this.createSyncMessage();
      syncMessage.read = [];
      for (var i = 0; i < reads.length; ++i) {
        var read = new textsecure.protobuf.SyncMessage.Read();
        read.timestamp = reads[i].timestamp;
        read.sender = reads[i].sender;
        syncMessage.read.push(read);
      }
      var contentMessage = new textsecure.protobuf.Content();
      contentMessage.syncMessage = syncMessage;

      var silent = true;
      return this.sendIndividualProto(
        myNumber,
        contentMessage,
        Date.now(),
        silent
      );
    }

    return Promise.resolve();
  },
  syncVerification: function(destination, state, identityKey) {
    var myNumber = textsecure.storage.user.getNumber();
    var myDevice = textsecure.storage.user.getDeviceId();
    var now = Date.now();

    if (myDevice == 1) {
      return Promise.resolve();
    }

    // First send a null message to mask the sync message.
    var nullMessage = new textsecure.protobuf.NullMessage();

    // Generate a random int from 1 and 512
    var buffer = libsignal.crypto.getRandomBytes(1);
    var paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;

    // Generate a random padding buffer of the chosen size
    nullMessage.padding = libsignal.crypto.getRandomBytes(paddingLength);

    var contentMessage = new textsecure.protobuf.Content();
    contentMessage.nullMessage = nullMessage;

    // We want the NullMessage to look like a normal outgoing message; not silent
    const promise = this.sendIndividualProto(destination, contentMessage, now);

    return promise.then(
      function() {
        var verified = new textsecure.protobuf.Verified();
        verified.state = state;
        verified.destination = destination;
        verified.identityKey = identityKey;
        verified.nullMessage = nullMessage.padding;

        var syncMessage = this.createSyncMessage();
        syncMessage.verified = verified;

        var contentMessage = new textsecure.protobuf.Content();
        contentMessage.syncMessage = syncMessage;

        var silent = true;
        return this.sendIndividualProto(myNumber, contentMessage, now, silent);
      }.bind(this)
    );
  },

  sendGroupProto: function(numbers, proto, timestamp) {
    timestamp = timestamp || Date.now();
    var me = textsecure.storage.user.getNumber();
    numbers = numbers.filter(function(number) {
      return number != me;
    });
    if (numbers.length === 0) {
      return Promise.reject(new Error('No other members in the group'));
    }

    return new Promise(
      function(resolve, reject) {
        var silent = true;
        var callback = function(res) {
          res.dataMessage = proto.toArrayBuffer();
          if (res.errors.length > 0) {
            reject(res);
          } else {
            resolve(res);
          }
        }.bind(this);

        this.sendMessageProto(timestamp, numbers, proto, callback, silent);
      }.bind(this)
    );
  },

  sendMessageToNumber: function(
    number,
    messageText,
    attachments,
    quote,
    timestamp,
    expireTimer,
    profileKey
  ) {
    return this.sendMessage({
      recipients: [number],
      body: messageText,
      timestamp: timestamp,
      attachments: attachments,
      quote: quote,
      needsSync: true,
      expireTimer: expireTimer,
      profileKey: profileKey,
    });
  },

  resetSession: function(number, timestamp) {
    console.log('resetting secure session');
    var proto = new textsecure.protobuf.DataMessage();
    proto.body = 'TERMINATE';
    proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION;

    var logError = function(prefix) {
      return function(error) {
        console.log(prefix, error && error.stack ? error.stack : error);
        throw error;
      };
    };
    var deleteAllSessions = function(number) {
      return textsecure.storage.protocol
        .getDeviceIds(number)
        .then(function(deviceIds) {
          return Promise.all(
            deviceIds.map(function(deviceId) {
              var address = new libsignal.SignalProtocolAddress(
                number,
                deviceId
              );
              console.log('deleting sessions for', address.toString());
              var sessionCipher = new libsignal.SessionCipher(
                textsecure.storage.protocol,
                address
              );
              return sessionCipher.deleteAllSessionsForDevice();
            })
          );
        });
    };

    var sendToContact = deleteAllSessions(number)
      .catch(logError('resetSession/deleteAllSessions1 error:'))
      .then(
        function() {
          console.log(
            'finished closing local sessions, now sending to contact'
          );
          return this.sendIndividualProto(number, proto, timestamp).catch(
            logError('resetSession/sendToContact error:')
          );
        }.bind(this)
      )
      .then(function() {
        return deleteAllSessions(number).catch(
          logError('resetSession/deleteAllSessions2 error:')
        );
      });

    var buffer = proto.toArrayBuffer();
    var sendSync = this.sendSyncMessage(buffer, timestamp, number).catch(
      logError('resetSession/sendSync error:')
    );

    return Promise.all([sendToContact, sendSync]);
  },

  sendMessageToGroup: function(
    groupId,
    messageText,
    attachments,
    quote,
    timestamp,
    expireTimer,
    profileKey
  ) {
    return textsecure.storage.groups.getNumbers(groupId).then(
      function(numbers) {
        if (numbers === undefined)
          return Promise.reject(new Error('Unknown Group'));

        var me = textsecure.storage.user.getNumber();
        numbers = numbers.filter(function(number) {
          return number != me;
        });
        if (numbers.length === 0) {
          return Promise.reject(new Error('No other members in the group'));
        }

        return this.sendMessage({
          recipients: numbers,
          body: messageText,
          timestamp: timestamp,
          attachments: attachments,
          quote: quote,
          needsSync: true,
          expireTimer: expireTimer,
          profileKey: profileKey,
          group: {
            id: groupId,
            type: textsecure.protobuf.GroupContext.Type.DELIVER,
          },
        });
      }.bind(this)
    );
  },

  createGroup: function(numbers, name, avatar) {
    var proto = new textsecure.protobuf.DataMessage();
    proto.group = new textsecure.protobuf.GroupContext();

    return textsecure.storage.groups.createNewGroup(numbers).then(
      function(group) {
        proto.group.id = stringToArrayBuffer(group.id);
        var numbers = group.numbers;

        proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
        proto.group.members = numbers;
        proto.group.name = name;

        return this.makeAttachmentPointer(avatar).then(
          function(attachment) {
            proto.group.avatar = attachment;
            return this.sendGroupProto(numbers, proto).then(function() {
              return proto.group.id;
            });
          }.bind(this)
        );
      }.bind(this)
    );
  },

  updateGroup: function(groupId, name, avatar, numbers) {
    var proto = new textsecure.protobuf.DataMessage();
    proto.group = new textsecure.protobuf.GroupContext();

    proto.group.id = stringToArrayBuffer(groupId);
    proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
    proto.group.name = name;

    return textsecure.storage.groups.addNumbers(groupId, numbers).then(
      function(numbers) {
        if (numbers === undefined) {
          return Promise.reject(new Error('Unknown Group'));
        }
        proto.group.members = numbers;

        return this.makeAttachmentPointer(avatar).then(
          function(attachment) {
            proto.group.avatar = attachment;
            return this.sendGroupProto(numbers, proto).then(function() {
              return proto.group.id;
            });
          }.bind(this)
        );
      }.bind(this)
    );
  },

  addNumberToGroup: function(groupId, number) {
    var proto = new textsecure.protobuf.DataMessage();
    proto.group = new textsecure.protobuf.GroupContext();
    proto.group.id = stringToArrayBuffer(groupId);
    proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;

    return textsecure.storage.groups.addNumbers(groupId, [number]).then(
      function(numbers) {
        if (numbers === undefined)
          return Promise.reject(new Error('Unknown Group'));
        proto.group.members = numbers;

        return this.sendGroupProto(numbers, proto);
      }.bind(this)
    );
  },

  setGroupName: function(groupId, name) {
    var proto = new textsecure.protobuf.DataMessage();
    proto.group = new textsecure.protobuf.GroupContext();
    proto.group.id = stringToArrayBuffer(groupId);
    proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
    proto.group.name = name;

    return textsecure.storage.groups.getNumbers(groupId).then(
      function(numbers) {
        if (numbers === undefined)
          return Promise.reject(new Error('Unknown Group'));
        proto.group.members = numbers;

        return this.sendGroupProto(numbers, proto);
      }.bind(this)
    );
  },

  setGroupAvatar: function(groupId, avatar) {
    var proto = new textsecure.protobuf.DataMessage();
    proto.group = new textsecure.protobuf.GroupContext();
    proto.group.id = stringToArrayBuffer(groupId);
    proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;

    return textsecure.storage.groups.getNumbers(groupId).then(
      function(numbers) {
        if (numbers === undefined)
          return Promise.reject(new Error('Unknown Group'));
        proto.group.members = numbers;

        return this.makeAttachmentPointer(avatar).then(
          function(attachment) {
            proto.group.avatar = attachment;
            return this.sendGroupProto(numbers, proto);
          }.bind(this)
        );
      }.bind(this)
    );
  },

  leaveGroup: function(groupId) {
    var proto = new textsecure.protobuf.DataMessage();
    proto.group = new textsecure.protobuf.GroupContext();
    proto.group.id = stringToArrayBuffer(groupId);
    proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;

    return textsecure.storage.groups
      .getNumbers(groupId)
      .then(function(numbers) {
        if (numbers === undefined)
          return Promise.reject(new Error('Unknown Group'));
        return textsecure.storage.groups.deleteGroup(groupId).then(
          function() {
            return this.sendGroupProto(numbers, proto);
          }.bind(this)
        );
      });
  },
  sendExpirationTimerUpdateToGroup: function(
    groupId,
    expireTimer,
    timestamp,
    profileKey
  ) {
    return textsecure.storage.groups.getNumbers(groupId).then(
      function(numbers) {
        if (numbers === undefined)
          return Promise.reject(new Error('Unknown Group'));

        var me = textsecure.storage.user.getNumber();
        numbers = numbers.filter(function(number) {
          return number != me;
        });
        if (numbers.length === 0) {
          return Promise.reject(new Error('No other members in the group'));
        }
        return this.sendMessage({
          recipients: numbers,
          timestamp: timestamp,
          needsSync: true,
          expireTimer: expireTimer,
          profileKey: profileKey,
          flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
          group: {
            id: groupId,
            type: textsecure.protobuf.GroupContext.Type.DELIVER,
          },
        });
      }.bind(this)
    );
  },
  sendExpirationTimerUpdateToNumber: function(
    number,
    expireTimer,
    timestamp,
    profileKey
  ) {
    var proto = new textsecure.protobuf.DataMessage();
    return this.sendMessage({
      recipients: [number],
      timestamp: timestamp,
      needsSync: true,
      expireTimer: expireTimer,
      profileKey: profileKey,
      flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
    });
  },
};

window.textsecure = window.textsecure || {};

textsecure.MessageSender = function(url, username, password, cdn_url) {
  var sender = new MessageSender(url, username, password, cdn_url);
  textsecure.replay.registerFunction(
    sender.tryMessageAgain.bind(sender),
    textsecure.replay.Type.ENCRYPT_MESSAGE
  );
  textsecure.replay.registerFunction(
    sender.retransmitMessage.bind(sender),
    textsecure.replay.Type.TRANSMIT_MESSAGE
  );
  textsecure.replay.registerFunction(
    sender.sendMessage.bind(sender),
    textsecure.replay.Type.REBUILD_MESSAGE
  );
  textsecure.replay.registerFunction(
    sender.retrySendMessageProto.bind(sender),
    textsecure.replay.Type.RETRY_SEND_MESSAGE_PROTO
  );

  this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(
    sender
  );
  this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup.bind(
    sender
  );
  this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage.bind(
    sender
  );
  this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage.bind(
    sender
  );
  this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(
    sender
  );
  this.sendMessageToNumber = sender.sendMessageToNumber.bind(sender);
  this.resetSession = sender.resetSession.bind(sender);
  this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
  this.createGroup = sender.createGroup.bind(sender);
  this.updateGroup = sender.updateGroup.bind(sender);
  this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
  this.setGroupName = sender.setGroupName.bind(sender);
  this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
  this.leaveGroup = sender.leaveGroup.bind(sender);
  this.sendSyncMessage = sender.sendSyncMessage.bind(sender);
  this.getProfile = sender.getProfile.bind(sender);
  this.getAvatar = sender.getAvatar.bind(sender);
  this.syncReadMessages = sender.syncReadMessages.bind(sender);
  this.syncVerification = sender.syncVerification.bind(sender);
  this.sendReadReceipts = sender.sendReadReceipts.bind(sender);
};

textsecure.MessageSender.prototype = {
  constructor: textsecure.MessageSender,
};