You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			526 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			526 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
/* global textsecure, WebAPI, libsignal, window, libloki, _, libsession */
 | 
						|
 | 
						|
/* eslint-disable more/no-then, no-bitwise */
 | 
						|
 | 
						|
function stringToArrayBuffer(str) {
 | 
						|
  if (typeof str !== 'string') {
 | 
						|
    throw new Error('Passed non-string to stringToArrayBuffer');
 | 
						|
  }
 | 
						|
  const res = new ArrayBuffer(str.length);
 | 
						|
  const uint = new Uint8Array(res);
 | 
						|
  for (let i = 0; i < str.length; i += 1) {
 | 
						|
    uint[i] = str.charCodeAt(i);
 | 
						|
  }
 | 
						|
  return res;
 | 
						|
}
 | 
						|
 | 
						|
function Message(options) {
 | 
						|
  this.body = options.body;
 | 
						|
  this.attachments = options.attachments || [];
 | 
						|
  this.quote = options.quote;
 | 
						|
  this.preview = options.preview;
 | 
						|
  this.group = options.group;
 | 
						|
  this.flags = options.flags;
 | 
						|
  this.recipients = options.recipients;
 | 
						|
  this.timestamp = options.timestamp;
 | 
						|
  this.needsSync = options.needsSync;
 | 
						|
  this.expireTimer = options.expireTimer;
 | 
						|
  this.profileKey = options.profileKey;
 | 
						|
  this.profile = options.profile;
 | 
						|
  this.groupInvitation = options.groupInvitation;
 | 
						|
  this.sessionRestoration = options.sessionRestoration || false;
 | 
						|
 | 
						|
  if (!(this.recipients instanceof Array)) {
 | 
						|
    throw new Error('Invalid recipient list');
 | 
						|
  }
 | 
						|
 | 
						|
  if (!this.group && this.recipients.length !== 1) {
 | 
						|
    throw new Error('Invalid recipient list for non-group');
 | 
						|
  }
 | 
						|
 | 
						|
  if (typeof this.timestamp !== 'number') {
 | 
						|
    throw new Error('Invalid timestamp');
 | 
						|
  }
 | 
						|
 | 
						|
  if (this.expireTimer !== undefined && this.expireTimer !== null) {
 | 
						|
    if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) {
 | 
						|
      throw new Error('Invalid expireTimer');
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (this.attachments) {
 | 
						|
    if (!(this.attachments instanceof Array)) {
 | 
						|
      throw new Error('Invalid message attachments');
 | 
						|
    }
 | 
						|
  }
 | 
						|
  if (this.flags !== undefined) {
 | 
						|
    if (typeof this.flags !== 'number') {
 | 
						|
      throw new Error('Invalid message flags');
 | 
						|
    }
 | 
						|
  }
 | 
						|
  if (this.isEndSession()) {
 | 
						|
    if (
 | 
						|
      this.body !== null ||
 | 
						|
      this.group !== null ||
 | 
						|
      this.attachments.length !== 0
 | 
						|
    ) {
 | 
						|
      throw new Error('Invalid end session message');
 | 
						|
    }
 | 
						|
  } else {
 | 
						|
    if (
 | 
						|
      typeof this.timestamp !== 'number' ||
 | 
						|
      (this.body && typeof this.body !== 'string')
 | 
						|
    ) {
 | 
						|
      throw new Error('Invalid message body');
 | 
						|
    }
 | 
						|
    if (this.group) {
 | 
						|
      if (
 | 
						|
        typeof this.group.id !== 'string' ||
 | 
						|
        typeof this.group.type !== 'number'
 | 
						|
      ) {
 | 
						|
        throw new Error('Invalid group context');
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
Message.prototype = {
 | 
						|
  constructor: Message,
 | 
						|
  isEndSession() {
 | 
						|
    return this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION;
 | 
						|
  },
 | 
						|
  toProto() {
 | 
						|
    if (this.dataMessage instanceof textsecure.protobuf.DataMessage) {
 | 
						|
      return this.dataMessage;
 | 
						|
    }
 | 
						|
    const proto = new textsecure.protobuf.DataMessage();
 | 
						|
    if (this.body) {
 | 
						|
      proto.body = this.body;
 | 
						|
    }
 | 
						|
    proto.attachments = this.attachmentPointers;
 | 
						|
    if (this.flags) {
 | 
						|
      proto.flags = this.flags;
 | 
						|
    }
 | 
						|
    if (this.group) {
 | 
						|
      proto.group = new textsecure.protobuf.GroupContext();
 | 
						|
      proto.group.id = stringToArrayBuffer(this.group.id);
 | 
						|
      proto.group.type = this.group.type;
 | 
						|
    }
 | 
						|
    if (Array.isArray(this.preview)) {
 | 
						|
      proto.preview = this.preview.map(preview => {
 | 
						|
        const item = new textsecure.protobuf.DataMessage.Preview();
 | 
						|
        item.title = preview.title;
 | 
						|
        item.url = preview.url;
 | 
						|
        item.image = preview.image || null;
 | 
						|
        return item;
 | 
						|
      });
 | 
						|
    }
 | 
						|
    if (this.quote) {
 | 
						|
      const { QuotedAttachment } = textsecure.protobuf.DataMessage.Quote;
 | 
						|
      const { Quote } = textsecure.protobuf.DataMessage;
 | 
						|
 | 
						|
      proto.quote = new Quote();
 | 
						|
      const { quote } = proto;
 | 
						|
 | 
						|
      quote.id = this.quote.id;
 | 
						|
      quote.author = this.quote.author;
 | 
						|
      quote.text = this.quote.text;
 | 
						|
      quote.attachments = (this.quote.attachments || []).map(attachment => {
 | 
						|
        const quotedAttachment = new QuotedAttachment();
 | 
						|
 | 
						|
        quotedAttachment.contentType = attachment.contentType;
 | 
						|
        quotedAttachment.fileName = attachment.fileName;
 | 
						|
        if (attachment.attachmentPointer) {
 | 
						|
          quotedAttachment.thumbnail = attachment.attachmentPointer;
 | 
						|
        }
 | 
						|
 | 
						|
        return quotedAttachment;
 | 
						|
      });
 | 
						|
    }
 | 
						|
    if (this.expireTimer) {
 | 
						|
      proto.expireTimer = this.expireTimer;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.profileKey) {
 | 
						|
      proto.profileKey = this.profileKey;
 | 
						|
    }
 | 
						|
 | 
						|
    // Set the loki profile
 | 
						|
    if (this.profile) {
 | 
						|
      const profile = new textsecure.protobuf.DataMessage.LokiProfile();
 | 
						|
      if (this.profile.displayName) {
 | 
						|
        profile.displayName = this.profile.displayName;
 | 
						|
      }
 | 
						|
 | 
						|
      const conversation = window.ConversationController.get(
 | 
						|
        textsecure.storage.user.getNumber()
 | 
						|
      );
 | 
						|
      const avatarPointer = conversation.get('avatarPointer');
 | 
						|
      if (avatarPointer) {
 | 
						|
        profile.avatar = avatarPointer;
 | 
						|
      }
 | 
						|
      proto.profile = profile;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.groupInvitation) {
 | 
						|
      proto.groupInvitation = new textsecure.protobuf.DataMessage.GroupInvitation(
 | 
						|
        {
 | 
						|
          serverAddress: this.groupInvitation.serverAddress,
 | 
						|
          channelId: this.groupInvitation.channelId,
 | 
						|
          serverName: this.groupInvitation.serverName,
 | 
						|
        }
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.sessionRestoration) {
 | 
						|
      proto.flags = textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE;
 | 
						|
    }
 | 
						|
 | 
						|
    this.dataMessage = proto;
 | 
						|
    return proto;
 | 
						|
  },
 | 
						|
  toArrayBuffer() {
 | 
						|
    return this.toProto().toArrayBuffer();
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
function MessageSender() {
 | 
						|
  this.server = WebAPI.connect();
 | 
						|
  this.pendingMessages = {};
 | 
						|
}
 | 
						|
 | 
						|
MessageSender.prototype = {
 | 
						|
  constructor: MessageSender,
 | 
						|
 | 
						|
  //  makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto
 | 
						|
  async makeAttachmentPointer(attachment, publicServer = null, options = {}) {
 | 
						|
    const { isRaw = false, isAvatar = false } = options;
 | 
						|
    if (typeof attachment !== 'object' || attachment == null) {
 | 
						|
      return Promise.resolve(undefined);
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
      !(attachment.data instanceof ArrayBuffer) &&
 | 
						|
      !ArrayBuffer.isView(attachment.data)
 | 
						|
    ) {
 | 
						|
      return Promise.reject(
 | 
						|
        new TypeError(
 | 
						|
          `\`attachment.data\` must be an \`ArrayBuffer\` or \`ArrayBufferView\`; got: ${typeof attachment.data}`
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    const proto = new textsecure.protobuf.AttachmentPointer();
 | 
						|
    let attachmentData;
 | 
						|
    const server = publicServer || this.server;
 | 
						|
 | 
						|
    if (publicServer || isRaw) {
 | 
						|
      attachmentData = attachment.data;
 | 
						|
    } else {
 | 
						|
      proto.key = libsignal.crypto.getRandomBytes(64);
 | 
						|
      const iv = libsignal.crypto.getRandomBytes(16);
 | 
						|
      const result = await textsecure.crypto.encryptAttachment(
 | 
						|
        attachment.data,
 | 
						|
        proto.key,
 | 
						|
        iv
 | 
						|
      );
 | 
						|
      proto.digest = result.digest;
 | 
						|
      attachmentData = result.ciphertext;
 | 
						|
    }
 | 
						|
 | 
						|
    const result = isAvatar
 | 
						|
      ? await server.putAvatar(attachmentData)
 | 
						|
      : await server.putAttachment(attachmentData);
 | 
						|
 | 
						|
    if (!result) {
 | 
						|
      return Promise.reject(
 | 
						|
        new Error('Failed to upload data to attachment fileserver')
 | 
						|
      );
 | 
						|
    }
 | 
						|
    const { url, id } = result;
 | 
						|
    proto.id = id;
 | 
						|
    proto.url = url;
 | 
						|
    proto.contentType = attachment.contentType;
 | 
						|
 | 
						|
    if (attachment.size) {
 | 
						|
      proto.size = attachment.size;
 | 
						|
    }
 | 
						|
    if (attachment.fileName) {
 | 
						|
      proto.fileName = attachment.fileName;
 | 
						|
    }
 | 
						|
    if (attachment.flags) {
 | 
						|
      proto.flags = attachment.flags;
 | 
						|
    }
 | 
						|
    if (attachment.width) {
 | 
						|
      proto.width = attachment.width;
 | 
						|
    }
 | 
						|
    if (attachment.height) {
 | 
						|
      proto.height = attachment.height;
 | 
						|
    }
 | 
						|
    if (attachment.caption) {
 | 
						|
      proto.caption = attachment.caption;
 | 
						|
    }
 | 
						|
 | 
						|
    return proto;
 | 
						|
  },
 | 
						|
 | 
						|
  queueJobForNumber(number, runJob) {
 | 
						|
    const taskWithTimeout = textsecure.createTaskWithTimeout(
 | 
						|
      runJob,
 | 
						|
      `queueJobForNumber ${number}`
 | 
						|
    );
 | 
						|
 | 
						|
    const runPrevious = this.pendingMessages[number] || Promise.resolve();
 | 
						|
    this.pendingMessages[number] = runPrevious.then(
 | 
						|
      taskWithTimeout,
 | 
						|
      taskWithTimeout
 | 
						|
    );
 | 
						|
 | 
						|
    const runCurrent = this.pendingMessages[number];
 | 
						|
    runCurrent.then(() => {
 | 
						|
      if (this.pendingMessages[number] === runCurrent) {
 | 
						|
        delete this.pendingMessages[number];
 | 
						|
      }
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  uploadAttachments(message, publicServer) {
 | 
						|
    return Promise.all(
 | 
						|
      message.attachments.map(attachment =>
 | 
						|
        this.makeAttachmentPointer(attachment, publicServer)
 | 
						|
      )
 | 
						|
    )
 | 
						|
      .then(attachmentPointers => {
 | 
						|
        // eslint-disable-next-line no-param-reassign
 | 
						|
        message.attachmentPointers = attachmentPointers;
 | 
						|
      })
 | 
						|
      .catch(error => {
 | 
						|
        if (error instanceof Error && error.name === 'HTTPError') {
 | 
						|
          throw new textsecure.MessageError(message, error);
 | 
						|
        } else {
 | 
						|
          throw error;
 | 
						|
        }
 | 
						|
      });
 | 
						|
  },
 | 
						|
 | 
						|
  async uploadLinkPreviews(message, publicServer) {
 | 
						|
    try {
 | 
						|
      const preview = await Promise.all(
 | 
						|
        (message.preview || []).map(async item => ({
 | 
						|
          ...item,
 | 
						|
          image: await this.makeAttachmentPointer(item.image, publicServer),
 | 
						|
        }))
 | 
						|
      );
 | 
						|
      // eslint-disable-next-line no-param-reassign
 | 
						|
      message.preview = preview;
 | 
						|
    } catch (error) {
 | 
						|
      if (error instanceof Error && error.name === 'HTTPError') {
 | 
						|
        throw new textsecure.MessageError(message, error);
 | 
						|
      } else {
 | 
						|
        throw error;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  uploadThumbnails(message, publicServer) {
 | 
						|
    const makePointer = this.makeAttachmentPointer.bind(this);
 | 
						|
    const { quote } = message;
 | 
						|
 | 
						|
    if (!quote || !quote.attachments || quote.attachments.length === 0) {
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
 | 
						|
    return Promise.all(
 | 
						|
      quote.attachments.map(attachment => {
 | 
						|
        const { thumbnail } = attachment;
 | 
						|
        if (!thumbnail) {
 | 
						|
          return null;
 | 
						|
        }
 | 
						|
 | 
						|
        return makePointer(thumbnail, publicServer).then(pointer => {
 | 
						|
          // eslint-disable-next-line no-param-reassign
 | 
						|
          attachment.attachmentPointer = pointer;
 | 
						|
        });
 | 
						|
      })
 | 
						|
    ).catch(error => {
 | 
						|
      if (error instanceof Error && error.name === 'HTTPError') {
 | 
						|
        throw new textsecure.MessageError(message, error);
 | 
						|
      } else {
 | 
						|
        throw error;
 | 
						|
      }
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  uploadAvatar(attachment) {
 | 
						|
    // isRaw is true since the data is already encrypted
 | 
						|
    // and doesn't need to be encrypted again
 | 
						|
    return this.makeAttachmentPointer(attachment, null, {
 | 
						|
      isRaw: true,
 | 
						|
      isAvatar: true,
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  async sendContactSyncMessage() {
 | 
						|
    const convosToSync = await libsession.Utils.SyncMessageUtils.getSyncContacts();
 | 
						|
 | 
						|
    if (convosToSync.size === 0) {
 | 
						|
      window.console.info('No contacts to sync.');
 | 
						|
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
    libloki.api.debug.logContactSync(
 | 
						|
      'Triggering contact sync message with:',
 | 
						|
      convosToSync
 | 
						|
    );
 | 
						|
 | 
						|
    // We need to sync across 3 contacts at a time
 | 
						|
    // This is to avoid hitting storage server limit
 | 
						|
    const chunked = _.chunk(convosToSync, 3);
 | 
						|
    const syncMessages = await Promise.all(
 | 
						|
      chunked.map(c => libloki.api.createContactSyncMessage(c))
 | 
						|
    );
 | 
						|
 | 
						|
    const syncPromises = syncMessages.map(syncMessage =>
 | 
						|
      libsession.getMessageQueue().sendSyncMessage(syncMessage)
 | 
						|
    );
 | 
						|
 | 
						|
    return Promise.all(syncPromises);
 | 
						|
  },
 | 
						|
 | 
						|
  sendGroupSyncMessage(conversations) {
 | 
						|
    // If we havn't got a primaryDeviceKey then we are in the middle of pairing
 | 
						|
    // primaryDevicePubKey is set to our own number if we are the master device
 | 
						|
    const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
 | 
						|
    if (!primaryDeviceKey) {
 | 
						|
      window.console.debug('sendGroupSyncMessage: no primary device pubkey');
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
    // We only want to sync across closed groups that we haven't left
 | 
						|
    const sessionGroups = conversations.filter(
 | 
						|
      c =>
 | 
						|
        c.isClosedGroup() &&
 | 
						|
        !c.get('left') &&
 | 
						|
        !c.isBlocked() &&
 | 
						|
        !c.isMediumGroup()
 | 
						|
    );
 | 
						|
    if (sessionGroups.length === 0) {
 | 
						|
      window.console.info('No closed group to sync.');
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
 | 
						|
    // We need to sync across 1 group at a time
 | 
						|
    // This is because we could hit the storage server limit with one group
 | 
						|
    const syncPromises = sessionGroups
 | 
						|
      .map(c => libloki.api.createGroupSyncMessage(c))
 | 
						|
      .map(syncMessage =>
 | 
						|
        libsession.getMessageQueue().sendSyncMessage(syncMessage)
 | 
						|
      );
 | 
						|
 | 
						|
    return Promise.all(syncPromises);
 | 
						|
  },
 | 
						|
 | 
						|
  async sendOpenGroupsSyncMessage(conversations) {
 | 
						|
    // If we havn't got a primaryDeviceKey then we are in the middle of pairing
 | 
						|
    // primaryDevicePubKey is set to our own number if we are the master device
 | 
						|
    const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
 | 
						|
    if (!primaryDeviceKey) {
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
 | 
						|
    const openGroupsConvos = await libsession.Utils.SyncMessageUtils.filterOpenGroupsConvos(
 | 
						|
      conversations
 | 
						|
    );
 | 
						|
 | 
						|
    if (!openGroupsConvos.length) {
 | 
						|
      window.log.info('No open groups to sync');
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
 | 
						|
    // Send the whole list of open groups in a single message
 | 
						|
    const openGroupsDetails = openGroupsConvos.map(conversation => ({
 | 
						|
      url: conversation.id,
 | 
						|
      channelId: conversation.get('channelId'),
 | 
						|
    }));
 | 
						|
    const openGroupsSyncParams = {
 | 
						|
      timestamp: Date.now(),
 | 
						|
      openGroupsDetails,
 | 
						|
    };
 | 
						|
    const openGroupsSyncMessage = new libsession.Messages.Outgoing.OpenGroupSyncMessage(
 | 
						|
      openGroupsSyncParams
 | 
						|
    );
 | 
						|
 | 
						|
    return libsession.getMessageQueue().sendSyncMessage(openGroupsSyncMessage);
 | 
						|
  },
 | 
						|
  syncReadMessages(reads) {
 | 
						|
    const myDevice = textsecure.storage.user.getDeviceId();
 | 
						|
    // FIXME audric currently not in used
 | 
						|
    if (myDevice !== 1 && myDevice !== '1') {
 | 
						|
      const syncReadMessages = new libsession.Messages.Outgoing.OpenGroupSyncMessage(
 | 
						|
        {
 | 
						|
          readMessages: reads,
 | 
						|
        }
 | 
						|
      );
 | 
						|
      return libsession.getMessageQueue().sendSyncMessage(syncReadMessages);
 | 
						|
    }
 | 
						|
 | 
						|
    return Promise.resolve();
 | 
						|
  },
 | 
						|
  async syncVerification(destination, state, identityKey) {
 | 
						|
    const myDevice = textsecure.storage.user.getDeviceId();
 | 
						|
 | 
						|
    if (myDevice === 1 || myDevice === '1') {
 | 
						|
      return Promise.resolve();
 | 
						|
    }
 | 
						|
    // send a session established message (used as a nullMessage)
 | 
						|
    const destinationPubKey = new libsession.Types.PubKey(destination);
 | 
						|
 | 
						|
    const sessionEstablished = new window.libsession.Messages.Outgoing.SessionEstablishedMessage(
 | 
						|
      { timestamp: Date.now() }
 | 
						|
    );
 | 
						|
    const { padding } = sessionEstablished;
 | 
						|
    await libsession
 | 
						|
      .getMessageQueue()
 | 
						|
      .send(destinationPubKey, sessionEstablished);
 | 
						|
 | 
						|
    const verifiedSyncParams = {
 | 
						|
      state,
 | 
						|
      destination: destinationPubKey,
 | 
						|
      identityKey,
 | 
						|
      padding,
 | 
						|
      timestamp: Date.now(),
 | 
						|
    };
 | 
						|
    const verifiedSyncMessage = new window.libsession.Messages.Outgoing.VerifiedSyncMessage(
 | 
						|
      verifiedSyncParams
 | 
						|
    );
 | 
						|
 | 
						|
    return libsession.getMessageQueue().sendSyncMessage(verifiedSyncMessage);
 | 
						|
  },
 | 
						|
 | 
						|
  makeProxiedRequest(url, options) {
 | 
						|
    return this.server.makeProxiedRequest(url, options);
 | 
						|
  },
 | 
						|
  getProxiedSize(url) {
 | 
						|
    return this.server.getProxiedSize(url);
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
window.textsecure = window.textsecure || {};
 | 
						|
 | 
						|
textsecure.MessageSender = function MessageSenderWrapper(username, password) {
 | 
						|
  const sender = new MessageSender(username, password);
 | 
						|
  this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender);
 | 
						|
  this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender);
 | 
						|
  this.sendOpenGroupsSyncMessage = sender.sendOpenGroupsSyncMessage.bind(
 | 
						|
    sender
 | 
						|
  );
 | 
						|
  this.uploadAvatar = sender.uploadAvatar.bind(sender);
 | 
						|
  this.syncReadMessages = sender.syncReadMessages.bind(sender);
 | 
						|
  this.syncVerification = sender.syncVerification.bind(sender);
 | 
						|
  this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender);
 | 
						|
  this.getProxiedSize = sender.getProxiedSize.bind(sender);
 | 
						|
};
 | 
						|
 | 
						|
textsecure.MessageSender.prototype = {
 | 
						|
  constructor: textsecure.MessageSender,
 | 
						|
};
 |