import ByteBuffer from 'bytebuffer'; import { isEmpty } from 'lodash'; import { ContentMessage } from '..'; import { SignalService } from '../../../../protobuf'; import { LokiProfile } from '../../../../types/Message'; import { Reaction } from '../../../../types/Reaction'; import { DisappearingMessageType } from '../../../../util/expiringMessages'; import { DURATION, TTL_DEFAULT } from '../../../constants'; import { MessageParams } from '../Message'; interface AttachmentPointerCommon { contentType?: string; key?: Uint8Array; size?: number; thumbnail?: Uint8Array; digest?: Uint8Array; fileName?: string; flags?: number; width?: number; height?: number; caption?: string; } export interface AttachmentPointer extends AttachmentPointerCommon { url?: string; id?: number; } export interface AttachmentPointerWithUrl extends AttachmentPointerCommon { url: string; id: number; } export interface Preview { url: string; title?: string; image?: AttachmentPointer; } export interface PreviewWithAttachmentUrl { url: string; id: number; title?: string; image?: AttachmentPointerWithUrl; } interface QuotedAttachmentCommon { contentType?: string; fileName?: string; } export interface QuotedAttachment extends QuotedAttachmentCommon { thumbnail?: AttachmentPointer; } export interface QuotedAttachmentWithUrl extends QuotedAttachmentCommon { thumbnail?: AttachmentPointerWithUrl | QuotedAttachment; } export interface Quote { id: number; author: string; text?: string; attachments?: Array; } export interface VisibleMessageParams extends MessageParams { attachments?: Array; body?: string; quote?: Quote; expirationType?: DisappearingMessageType; expireTimer?: number; lokiProfile?: LokiProfile; preview?: Array; reaction?: Reaction; syncTarget?: string; // undefined means it is not a synced message } export class VisibleMessage extends ContentMessage { public readonly expirationType?: DisappearingMessageType; public readonly expireTimer?: number; public readonly reaction?: Reaction; private readonly attachments?: Array; private readonly body?: string; private readonly quote?: Quote; private readonly profileKey?: Uint8Array; private readonly profile?: SignalService.DataMessage.ILokiProfile; private readonly preview?: Array; /// In the case of a sync message, the public key of the person the message was targeted at. /// - Note: `null or undefined` if this isn't a sync message. private readonly syncTarget?: string; constructor(params: VisibleMessageParams) { super({ timestamp: params.timestamp, identifier: params.identifier }); this.attachments = params.attachments; this.body = params.body; this.quote = params.quote; this.expirationType = params.expirationType; this.expireTimer = params.expireTimer; const profile = buildProfileForOutgoingMessage(params); this.profile = profile.lokiProfile; this.profileKey = profile.profileKey; this.preview = params.preview; this.reaction = params.reaction; this.syncTarget = params.syncTarget; } public contentProto(): SignalService.Content { return new SignalService.Content({ dataMessage: this.dataProto(), expirationType: this.expirationType === 'deleteAfterSend' ? SignalService.Content.ExpirationType.DELETE_AFTER_SEND : SignalService.Content.ExpirationType.DELETE_AFTER_READ, expirationTimer: this.expireTimer, }); } public dataProto(): SignalService.DataMessage { const dataMessage = new SignalService.DataMessage(); if (this.body) { dataMessage.body = this.body; } dataMessage.attachments = this.attachments || []; if (this.expireTimer) { dataMessage.expireTimer = this.expireTimer; } if (this.preview) { dataMessage.preview = this.preview; } if (this.reaction) { dataMessage.reaction = this.reaction; } if (this.syncTarget) { dataMessage.syncTarget = this.syncTarget; } if (this.profile) { dataMessage.profile = this.profile; } if (this.profileKey && this.profileKey.length) { dataMessage.profileKey = this.profileKey; } if (this.quote) { dataMessage.quote = new SignalService.DataMessage.Quote(); dataMessage.quote.id = this.quote.id; dataMessage.quote.author = this.quote.author; dataMessage.quote.text = this.quote.text; if (this.quote.attachments) { dataMessage.quote.attachments = this.quote.attachments.map(attachment => { const quotedAttachment = new SignalService.DataMessage.Quote.QuotedAttachment(); if (attachment.contentType) { quotedAttachment.contentType = attachment.contentType; } if (attachment.fileName) { quotedAttachment.fileName = attachment.fileName; } if (attachment.thumbnail && (attachment.thumbnail as any).id) { quotedAttachment.thumbnail = attachment.thumbnail as any; // be sure to keep the typescript guard on id above } return quotedAttachment; }); } } if (Array.isArray(this.preview)) { dataMessage.preview = this.preview.map(preview => { const item = new SignalService.DataMessage.Preview(); if (preview.title) { item.title = preview.title; } if (preview.url) { item.url = preview.url; } item.image = preview.image || null; return item; }); } dataMessage.timestamp = this.timestamp; return dataMessage; } public isEqual(comparator: VisibleMessage): boolean { return this.identifier === comparator.identifier && this.timestamp === comparator.timestamp; } public ttl(): number { switch (this.expirationType) { case 'deleteAfterSend': return this.expireTimer ? this.expireTimer * DURATION.SECONDS : TTL_DEFAULT.TTL_MAX; case 'deleteAfterRead': return TTL_DEFAULT.TTL_MAX; default: return TTL_DEFAULT.TTL_MAX; } } } export function buildProfileForOutgoingMessage(params: { lokiProfile?: LokiProfile }) { let profileKey: Uint8Array | undefined; if (params.lokiProfile && params.lokiProfile.profileKey) { if ( params.lokiProfile.profileKey instanceof Uint8Array || (params.lokiProfile.profileKey as any) instanceof ByteBuffer ) { profileKey = new Uint8Array(params.lokiProfile.profileKey); } else { profileKey = new Uint8Array(ByteBuffer.wrap(params.lokiProfile.profileKey).toArrayBuffer()); } } const displayName = params.lokiProfile?.displayName; // no need to iclude the avatarPointer if there is no profileKey associated with it. const avatarPointer = params.lokiProfile?.avatarPointer && !isEmpty(profileKey) && params.lokiProfile.avatarPointer && !isEmpty(params.lokiProfile.avatarPointer) ? params.lokiProfile.avatarPointer : undefined; let lokiProfile: SignalService.DataMessage.ILokiProfile | undefined; if (avatarPointer || displayName) { lokiProfile = new SignalService.DataMessage.LokiProfile(); // we always need a profileKey tom decode an avatar pointer if (avatarPointer && avatarPointer.length && profileKey) { lokiProfile.profilePicture = avatarPointer; } if (displayName) { lokiProfile.displayName = displayName; } } return { lokiProfile, profileKey: lokiProfile?.profilePicture ? profileKey : undefined, }; }