Send edited messages support
Co-authored-by: Fedor Indutnyy <indutny@signal.org>
This commit is contained in:
parent
d380817a44
commit
1f2cde6d04
79 changed files with 2507 additions and 1175 deletions
|
@ -13,7 +13,6 @@ import {
|
|||
SenderKeyDistributionMessage,
|
||||
} from '@signalapp/libsignal-client';
|
||||
|
||||
import type { QuotedMessageType } from '../model-types.d';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import { GLOBAL_ZONE } from '../SignalProtocolStore';
|
||||
import { assertDev, strictAssert } from '../util/assert';
|
||||
|
@ -21,9 +20,10 @@ import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
|||
import { Address } from '../types/Address';
|
||||
import { QualifiedAddress } from '../types/QualifiedAddress';
|
||||
import { SenderKeys } from '../LibSignalStores';
|
||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import { MIMETypeToString } from '../types/MIME';
|
||||
import type * as Attachment from '../types/Attachment';
|
||||
import type {
|
||||
TextAttachmentType,
|
||||
UploadedAttachmentType,
|
||||
} from '../types/Attachment';
|
||||
import type { UUID } from '../types/UUID';
|
||||
import type {
|
||||
ChallengeType,
|
||||
|
@ -49,7 +49,7 @@ import type {
|
|||
} from './OutgoingMessage';
|
||||
import OutgoingMessage from './OutgoingMessage';
|
||||
import * as Bytes from '../Bytes';
|
||||
import { getRandomBytes, getZeroes, encryptAttachment } from '../Crypto';
|
||||
import { getRandomBytes } from '../Crypto';
|
||||
import {
|
||||
MessageError,
|
||||
SignedPreKeyRotationError,
|
||||
|
@ -57,8 +57,8 @@ import {
|
|||
HTTPError,
|
||||
NoSenderKeyError,
|
||||
} from './Errors';
|
||||
import type { RawBodyRange } from '../types/BodyRange';
|
||||
import { BodyRange } from '../types/BodyRange';
|
||||
import type { RawBodyRange } from '../types/BodyRange';
|
||||
import type { StoryContextType } from '../types/Util';
|
||||
import type {
|
||||
LinkPreviewImage,
|
||||
|
@ -71,13 +71,12 @@ import { uuidToBytes } from '../util/uuidToBytes';
|
|||
import type { DurationInSeconds } from '../util/durations';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import * as log from '../logging/log';
|
||||
import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact';
|
||||
import type { EmbeddedContactWithUploadedAvatar } from '../types/EmbeddedContact';
|
||||
import {
|
||||
numberToPhoneType,
|
||||
numberToEmailType,
|
||||
numberToAddressType,
|
||||
} from '../types/EmbeddedContact';
|
||||
import type { StickerWithHydratedData } from '../types/Stickers';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
export type SendMetadataType = {
|
||||
|
@ -92,9 +91,33 @@ export type SendOptionsType = {
|
|||
online?: boolean;
|
||||
};
|
||||
|
||||
type QuoteAttachmentType = {
|
||||
thumbnail?: AttachmentType;
|
||||
attachmentPointer?: Proto.IAttachmentPointer;
|
||||
export type OutgoingQuoteAttachmentType = Readonly<{
|
||||
contentType: string;
|
||||
fileName?: string;
|
||||
thumbnail: UploadedAttachmentType;
|
||||
}>;
|
||||
|
||||
export type OutgoingQuoteType = Readonly<{
|
||||
isGiftBadge?: boolean;
|
||||
id?: number;
|
||||
authorUuid?: string;
|
||||
text?: string;
|
||||
attachments: ReadonlyArray<OutgoingQuoteAttachmentType>;
|
||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
}>;
|
||||
|
||||
export type OutgoingLinkPreviewType = Readonly<{
|
||||
title?: string;
|
||||
description?: string;
|
||||
domain?: string;
|
||||
url: string;
|
||||
isStickerPack?: boolean;
|
||||
image?: Readonly<UploadedAttachmentType>;
|
||||
date?: number;
|
||||
}>;
|
||||
|
||||
export type OutgoingTextAttachmentType = Omit<TextAttachmentType, 'preview'> & {
|
||||
preview?: OutgoingLinkPreviewType;
|
||||
};
|
||||
|
||||
export type GroupV2InfoType = {
|
||||
|
@ -108,9 +131,13 @@ type GroupCallUpdateType = {
|
|||
eraId: string;
|
||||
};
|
||||
|
||||
export type StickerType = StickerWithHydratedData & {
|
||||
attachmentPointer?: Proto.IAttachmentPointer;
|
||||
};
|
||||
export type OutgoingStickerType = Readonly<{
|
||||
packId: string;
|
||||
packKey: string;
|
||||
stickerId: number;
|
||||
emoji?: string;
|
||||
data: Readonly<UploadedAttachmentType>;
|
||||
}>;
|
||||
|
||||
export type ReactionType = {
|
||||
emoji?: string;
|
||||
|
@ -119,22 +146,6 @@ export type ReactionType = {
|
|||
targetTimestamp?: number;
|
||||
};
|
||||
|
||||
export type AttachmentType = {
|
||||
size: number;
|
||||
data: Uint8Array;
|
||||
contentType: string;
|
||||
|
||||
fileName?: string;
|
||||
flags?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
caption?: string;
|
||||
|
||||
attachmentPointer?: Proto.IAttachmentPointer;
|
||||
|
||||
blurHash?: string;
|
||||
};
|
||||
|
||||
export const singleProtoJobDataSchema = z.object({
|
||||
contentHint: z.number(),
|
||||
identifier: z.string(),
|
||||
|
@ -147,35 +158,12 @@ export const singleProtoJobDataSchema = z.object({
|
|||
|
||||
export type SingleProtoJobData = z.infer<typeof singleProtoJobDataSchema>;
|
||||
|
||||
function makeAttachmentSendReady(
|
||||
attachment: Attachment.AttachmentType
|
||||
): AttachmentType | undefined {
|
||||
const { data } = attachment;
|
||||
|
||||
if (!data) {
|
||||
throw new Error(
|
||||
'makeAttachmentSendReady: Missing data, returning undefined'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
contentType: MIMETypeToString(attachment.contentType),
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export type ContactWithHydratedAvatar = EmbeddedContactType & {
|
||||
avatar?: Avatar & {
|
||||
attachmentPointer?: Proto.IAttachmentPointer;
|
||||
};
|
||||
};
|
||||
|
||||
export type MessageOptionsType = {
|
||||
attachments?: ReadonlyArray<AttachmentType> | null;
|
||||
attachments?: ReadonlyArray<UploadedAttachmentType>;
|
||||
body?: string;
|
||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
contact?: Array<ContactWithHydratedAvatar>;
|
||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||
editedMessageTimestamp?: number;
|
||||
expireTimer?: DurationInSeconds;
|
||||
flags?: number;
|
||||
group?: {
|
||||
|
@ -184,11 +172,11 @@ export type MessageOptionsType = {
|
|||
};
|
||||
groupV2?: GroupV2InfoType;
|
||||
needsSync?: boolean;
|
||||
preview?: ReadonlyArray<LinkPreviewType>;
|
||||
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
|
||||
profileKey?: Uint8Array;
|
||||
quote?: QuotedMessageType | null;
|
||||
quote?: OutgoingQuoteType;
|
||||
recipients: ReadonlyArray<string>;
|
||||
sticker?: StickerWithHydratedData;
|
||||
sticker?: OutgoingStickerType;
|
||||
reaction?: ReactionType;
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
timestamp: number;
|
||||
|
@ -196,32 +184,33 @@ export type MessageOptionsType = {
|
|||
storyContext?: StoryContextType;
|
||||
};
|
||||
export type GroupSendOptionsType = {
|
||||
attachments?: Array<AttachmentType>;
|
||||
attachments?: ReadonlyArray<UploadedAttachmentType>;
|
||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
contact?: Array<ContactWithHydratedAvatar>;
|
||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
editedMessageTimestamp?: number;
|
||||
expireTimer?: DurationInSeconds;
|
||||
flags?: number;
|
||||
groupCallUpdate?: GroupCallUpdateType;
|
||||
groupV2?: GroupV2InfoType;
|
||||
messageText?: string;
|
||||
preview?: ReadonlyArray<LinkPreviewType>;
|
||||
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
|
||||
profileKey?: Uint8Array;
|
||||
quote?: QuotedMessageType | null;
|
||||
quote?: OutgoingQuoteType;
|
||||
reaction?: ReactionType;
|
||||
sticker?: StickerWithHydratedData;
|
||||
sticker?: OutgoingStickerType;
|
||||
storyContext?: StoryContextType;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
class Message {
|
||||
attachments: ReadonlyArray<AttachmentType>;
|
||||
attachments: ReadonlyArray<UploadedAttachmentType>;
|
||||
|
||||
body?: string;
|
||||
|
||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
|
||||
contact?: Array<ContactWithHydratedAvatar>;
|
||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||
|
||||
expireTimer?: DurationInSeconds;
|
||||
|
||||
|
@ -236,15 +225,15 @@ class Message {
|
|||
|
||||
needsSync?: boolean;
|
||||
|
||||
preview?: ReadonlyArray<LinkPreviewType>;
|
||||
preview?: ReadonlyArray<OutgoingLinkPreviewType>;
|
||||
|
||||
profileKey?: Uint8Array;
|
||||
|
||||
quote?: QuotedMessageType | null;
|
||||
quote?: OutgoingQuoteType;
|
||||
|
||||
recipients: ReadonlyArray<string>;
|
||||
|
||||
sticker?: StickerType;
|
||||
sticker?: OutgoingStickerType;
|
||||
|
||||
reaction?: ReactionType;
|
||||
|
||||
|
@ -252,8 +241,6 @@ class Message {
|
|||
|
||||
dataMessage?: Proto.DataMessage;
|
||||
|
||||
attachmentPointers: Array<Proto.IAttachmentPointer> = [];
|
||||
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
|
||||
groupCallUpdate?: GroupCallUpdateType;
|
||||
|
@ -346,7 +333,7 @@ class Message {
|
|||
const proto = new Proto.DataMessage();
|
||||
|
||||
proto.timestamp = Long.fromNumber(this.timestamp);
|
||||
proto.attachments = this.attachmentPointers;
|
||||
proto.attachments = this.attachments.slice();
|
||||
|
||||
if (this.body) {
|
||||
proto.body = this.body;
|
||||
|
@ -383,10 +370,7 @@ class Message {
|
|||
proto.sticker.packKey = Bytes.fromBase64(this.sticker.packKey);
|
||||
proto.sticker.stickerId = this.sticker.stickerId;
|
||||
proto.sticker.emoji = this.sticker.emoji;
|
||||
|
||||
if (this.sticker.attachmentPointer) {
|
||||
proto.sticker.data = this.sticker.attachmentPointer;
|
||||
}
|
||||
proto.sticker.data = this.sticker.data;
|
||||
}
|
||||
if (this.reaction) {
|
||||
proto.reaction = new Proto.DataMessage.Reaction();
|
||||
|
@ -406,82 +390,83 @@ class Message {
|
|||
item.url = preview.url;
|
||||
item.description = preview.description || null;
|
||||
item.date = preview.date || null;
|
||||
if (preview.attachmentPointer) {
|
||||
item.image = preview.attachmentPointer;
|
||||
if (preview.image) {
|
||||
item.image = preview.image;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
if (Array.isArray(this.contact)) {
|
||||
proto.contact = this.contact.map(contact => {
|
||||
const contactProto = new Proto.DataMessage.Contact();
|
||||
if (contact.name) {
|
||||
const nameProto: Proto.DataMessage.Contact.IName = {
|
||||
givenName: contact.name.givenName,
|
||||
familyName: contact.name.familyName,
|
||||
prefix: contact.name.prefix,
|
||||
suffix: contact.name.suffix,
|
||||
middleName: contact.name.middleName,
|
||||
displayName: contact.name.displayName,
|
||||
};
|
||||
contactProto.name = new Proto.DataMessage.Contact.Name(nameProto);
|
||||
}
|
||||
if (Array.isArray(contact.number)) {
|
||||
contactProto.number = contact.number.map(number => {
|
||||
const numberProto: Proto.DataMessage.Contact.IPhone = {
|
||||
value: number.value,
|
||||
type: numberToPhoneType(number.type),
|
||||
label: number.label,
|
||||
proto.contact = this.contact.map(
|
||||
(contact: EmbeddedContactWithUploadedAvatar) => {
|
||||
const contactProto = new Proto.DataMessage.Contact();
|
||||
if (contact.name) {
|
||||
const nameProto: Proto.DataMessage.Contact.IName = {
|
||||
givenName: contact.name.givenName,
|
||||
familyName: contact.name.familyName,
|
||||
prefix: contact.name.prefix,
|
||||
suffix: contact.name.suffix,
|
||||
middleName: contact.name.middleName,
|
||||
displayName: contact.name.displayName,
|
||||
};
|
||||
contactProto.name = new Proto.DataMessage.Contact.Name(nameProto);
|
||||
}
|
||||
if (Array.isArray(contact.number)) {
|
||||
contactProto.number = contact.number.map(number => {
|
||||
const numberProto: Proto.DataMessage.Contact.IPhone = {
|
||||
value: number.value,
|
||||
type: numberToPhoneType(number.type),
|
||||
label: number.label,
|
||||
};
|
||||
|
||||
return new Proto.DataMessage.Contact.Phone(numberProto);
|
||||
});
|
||||
}
|
||||
if (Array.isArray(contact.email)) {
|
||||
contactProto.email = contact.email.map(email => {
|
||||
const emailProto: Proto.DataMessage.Contact.IEmail = {
|
||||
value: email.value,
|
||||
type: numberToEmailType(email.type),
|
||||
label: email.label,
|
||||
};
|
||||
return new Proto.DataMessage.Contact.Phone(numberProto);
|
||||
});
|
||||
}
|
||||
if (Array.isArray(contact.email)) {
|
||||
contactProto.email = contact.email.map(email => {
|
||||
const emailProto: Proto.DataMessage.Contact.IEmail = {
|
||||
value: email.value,
|
||||
type: numberToEmailType(email.type),
|
||||
label: email.label,
|
||||
};
|
||||
|
||||
return new Proto.DataMessage.Contact.Email(emailProto);
|
||||
});
|
||||
}
|
||||
if (Array.isArray(contact.address)) {
|
||||
contactProto.address = contact.address.map(address => {
|
||||
const addressProto: Proto.DataMessage.Contact.IPostalAddress = {
|
||||
type: numberToAddressType(address.type),
|
||||
label: address.label,
|
||||
street: address.street,
|
||||
pobox: address.pobox,
|
||||
neighborhood: address.neighborhood,
|
||||
city: address.city,
|
||||
region: address.region,
|
||||
postcode: address.postcode,
|
||||
country: address.country,
|
||||
};
|
||||
return new Proto.DataMessage.Contact.Email(emailProto);
|
||||
});
|
||||
}
|
||||
if (Array.isArray(contact.address)) {
|
||||
contactProto.address = contact.address.map(address => {
|
||||
const addressProto: Proto.DataMessage.Contact.IPostalAddress = {
|
||||
type: numberToAddressType(address.type),
|
||||
label: address.label,
|
||||
street: address.street,
|
||||
pobox: address.pobox,
|
||||
neighborhood: address.neighborhood,
|
||||
city: address.city,
|
||||
region: address.region,
|
||||
postcode: address.postcode,
|
||||
country: address.country,
|
||||
};
|
||||
|
||||
return new Proto.DataMessage.Contact.PostalAddress(addressProto);
|
||||
});
|
||||
}
|
||||
if (contact.avatar && contact.avatar.attachmentPointer) {
|
||||
const avatarProto = new Proto.DataMessage.Contact.Avatar();
|
||||
avatarProto.avatar = contact.avatar.attachmentPointer;
|
||||
avatarProto.isProfile = Boolean(contact.avatar.isProfile);
|
||||
contactProto.avatar = avatarProto;
|
||||
}
|
||||
return new Proto.DataMessage.Contact.PostalAddress(addressProto);
|
||||
});
|
||||
}
|
||||
if (contact.avatar?.avatar) {
|
||||
const avatarProto = new Proto.DataMessage.Contact.Avatar();
|
||||
avatarProto.avatar = contact.avatar.avatar;
|
||||
avatarProto.isProfile = Boolean(contact.avatar.isProfile);
|
||||
contactProto.avatar = avatarProto;
|
||||
}
|
||||
|
||||
if (contact.organization) {
|
||||
contactProto.organization = contact.organization;
|
||||
}
|
||||
if (contact.organization) {
|
||||
contactProto.organization = contact.organization;
|
||||
}
|
||||
|
||||
return contactProto;
|
||||
});
|
||||
return contactProto;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (this.quote) {
|
||||
const { QuotedAttachment } = Proto.DataMessage.Quote;
|
||||
const { BodyRange: ProtoBodyRange, Quote } = Proto.DataMessage;
|
||||
|
||||
proto.quote = new Quote();
|
||||
|
@ -497,21 +482,7 @@ class Message {
|
|||
this.quote.id === undefined ? null : Long.fromNumber(this.quote.id);
|
||||
quote.authorUuid = this.quote.authorUuid || null;
|
||||
quote.text = this.quote.text || null;
|
||||
quote.attachments = (this.quote.attachments || []).map(
|
||||
(attachment: AttachmentType) => {
|
||||
const quotedAttachment = new QuotedAttachment();
|
||||
|
||||
quotedAttachment.contentType = attachment.contentType;
|
||||
if (attachment.fileName) {
|
||||
quotedAttachment.fileName = attachment.fileName;
|
||||
}
|
||||
if (attachment.attachmentPointer) {
|
||||
quotedAttachment.thumbnail = attachment.attachmentPointer;
|
||||
}
|
||||
|
||||
return quotedAttachment;
|
||||
}
|
||||
);
|
||||
quote.attachments = this.quote.attachments.slice() || [];
|
||||
const bodyRanges = this.quote.bodyRanges || [];
|
||||
quote.bodyRanges = bodyRanges.map(range => {
|
||||
const bodyRange = new ProtoBodyRange();
|
||||
|
@ -665,13 +636,6 @@ export default class MessageSender {
|
|||
|
||||
// Attachment upload functions
|
||||
|
||||
_getAttachmentSizeBucket(size: number): number {
|
||||
return Math.max(
|
||||
541,
|
||||
Math.floor(1.05 ** Math.ceil(Math.log(size) / Math.log(1.05)))
|
||||
);
|
||||
}
|
||||
|
||||
static getRandomPadding(): Uint8Array {
|
||||
// Generate a random int from 1 and 512
|
||||
const buffer = getRandomBytes(2);
|
||||
|
@ -681,216 +645,11 @@ export default class MessageSender {
|
|||
return getRandomBytes(paddingLength);
|
||||
}
|
||||
|
||||
getPaddedAttachment(data: Readonly<Uint8Array>): Uint8Array {
|
||||
const size = data.byteLength;
|
||||
const paddedSize = this._getAttachmentSizeBucket(size);
|
||||
const padding = getZeroes(paddedSize - size);
|
||||
|
||||
return Bytes.concatenate([data, padding]);
|
||||
}
|
||||
|
||||
async makeAttachmentPointer(
|
||||
attachment: Readonly<
|
||||
Partial<AttachmentType> &
|
||||
Pick<AttachmentType, 'data' | 'size' | 'contentType'>
|
||||
>
|
||||
): Promise<Proto.IAttachmentPointer> {
|
||||
assertDev(
|
||||
typeof attachment === 'object' && attachment != null,
|
||||
'Got null attachment in `makeAttachmentPointer`'
|
||||
);
|
||||
|
||||
const { data, size, contentType } = attachment;
|
||||
if (!(data instanceof Uint8Array)) {
|
||||
throw new Error(
|
||||
`makeAttachmentPointer: data was a '${typeof data}' instead of Uint8Array`
|
||||
);
|
||||
}
|
||||
if (data.byteLength !== size) {
|
||||
throw new Error(
|
||||
`makeAttachmentPointer: Size ${size} did not match data.byteLength ${data.byteLength}`
|
||||
);
|
||||
}
|
||||
if (typeof contentType !== 'string') {
|
||||
throw new Error(
|
||||
`makeAttachmentPointer: contentType ${contentType} was not a string`
|
||||
);
|
||||
}
|
||||
|
||||
const padded = this.getPaddedAttachment(data);
|
||||
const key = getRandomBytes(64);
|
||||
|
||||
const result = encryptAttachment(padded, key);
|
||||
const id = await this.server.putAttachment(result.ciphertext);
|
||||
|
||||
const proto = new Proto.AttachmentPointer();
|
||||
proto.cdnId = Long.fromString(id);
|
||||
proto.contentType = attachment.contentType;
|
||||
proto.key = key;
|
||||
proto.size = data.byteLength;
|
||||
proto.digest = result.digest;
|
||||
|
||||
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;
|
||||
}
|
||||
if (attachment.blurHash) {
|
||||
proto.blurHash = attachment.blurHash;
|
||||
}
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
async uploadAttachments(message: Message): Promise<void> {
|
||||
try {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.attachmentPointers = await Promise.all(
|
||||
message.attachments.map(attachment =>
|
||||
this.makeAttachmentPointer(attachment)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
throw new MessageError(message, error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadLinkPreviews(message: Message): Promise<void> {
|
||||
try {
|
||||
const preview = await Promise.all(
|
||||
(message.preview || []).map(async (item: Readonly<LinkPreviewType>) => {
|
||||
if (!item.image) {
|
||||
return item;
|
||||
}
|
||||
const attachment = makeAttachmentSendReady(item.image);
|
||||
if (!attachment) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
attachmentPointer: await this.makeAttachmentPointer(attachment),
|
||||
};
|
||||
})
|
||||
);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.preview = preview;
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
throw new MessageError(message, error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadSticker(message: Message): Promise<void> {
|
||||
try {
|
||||
const { sticker } = message;
|
||||
|
||||
if (!sticker) {
|
||||
return;
|
||||
}
|
||||
if (!sticker.data) {
|
||||
throw new Error('uploadSticker: No sticker data to upload!');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.sticker = {
|
||||
...sticker,
|
||||
attachmentPointer: await this.makeAttachmentPointer(sticker.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
throw new MessageError(message, error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadContactAvatar(message: Message): Promise<void> {
|
||||
const { contact } = message;
|
||||
if (!contact || contact.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
contact.map(async (item: ContactWithHydratedAvatar) => {
|
||||
const itemAvatar = item?.avatar;
|
||||
const avatar = itemAvatar?.avatar;
|
||||
|
||||
if (!itemAvatar || !avatar || !avatar.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachment = makeAttachmentSendReady(avatar);
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
itemAvatar.attachmentPointer = await this.makeAttachmentPointer(
|
||||
attachment
|
||||
);
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
throw new MessageError(message, error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadThumbnails(message: Message): Promise<void> {
|
||||
const { quote } = message;
|
||||
if (!quote || !quote.attachments || quote.attachments.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
quote.attachments.map(async (attachment: QuoteAttachmentType) => {
|
||||
if (!attachment.thumbnail) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
attachment.attachmentPointer = await this.makeAttachmentPointer(
|
||||
attachment.thumbnail
|
||||
);
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
throw new MessageError(message, error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proto assembly
|
||||
|
||||
async getTextAttachmentProto(
|
||||
attachmentAttrs: Attachment.TextAttachmentType
|
||||
): Promise<Proto.TextAttachment> {
|
||||
getTextAttachmentProto(
|
||||
attachmentAttrs: OutgoingTextAttachmentType
|
||||
): Proto.TextAttachment {
|
||||
const textAttachment = new Proto.TextAttachment();
|
||||
|
||||
if (attachmentAttrs.text) {
|
||||
|
@ -910,15 +669,8 @@ export default class MessageSender {
|
|||
}
|
||||
|
||||
if (attachmentAttrs.preview) {
|
||||
const previewImage = attachmentAttrs.preview.image;
|
||||
// This cast is OK because we're ensuring that previewImage.data is truthy
|
||||
const image =
|
||||
previewImage && previewImage.data
|
||||
? await this.makeAttachmentPointer(previewImage as AttachmentType)
|
||||
: undefined;
|
||||
|
||||
textAttachment.preview = {
|
||||
image,
|
||||
image: attachmentAttrs.preview.image,
|
||||
title: attachmentAttrs.preview.title,
|
||||
url: attachmentAttrs.preview.url,
|
||||
};
|
||||
|
@ -950,20 +702,17 @@ export default class MessageSender {
|
|||
textAttachment,
|
||||
}: {
|
||||
allowsReplies?: boolean;
|
||||
fileAttachment?: AttachmentType;
|
||||
fileAttachment?: UploadedAttachmentType;
|
||||
groupV2?: GroupV2InfoType;
|
||||
profileKey: Uint8Array;
|
||||
textAttachment?: Attachment.TextAttachmentType;
|
||||
textAttachment?: OutgoingTextAttachmentType;
|
||||
}): Promise<Proto.StoryMessage> {
|
||||
const storyMessage = new Proto.StoryMessage();
|
||||
storyMessage.profileKey = profileKey;
|
||||
|
||||
if (fileAttachment) {
|
||||
try {
|
||||
const attachmentPointer = await this.makeAttachmentPointer(
|
||||
fileAttachment
|
||||
);
|
||||
storyMessage.fileAttachment = attachmentPointer;
|
||||
storyMessage.fileAttachment = fileAttachment;
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
throw new MessageError(message, error);
|
||||
|
@ -974,9 +723,7 @@ export default class MessageSender {
|
|||
}
|
||||
|
||||
if (textAttachment) {
|
||||
storyMessage.textAttachment = await this.getTextAttachmentProto(
|
||||
textAttachment
|
||||
);
|
||||
storyMessage.textAttachment = this.getTextAttachmentProto(textAttachment);
|
||||
}
|
||||
|
||||
if (groupV2) {
|
||||
|
@ -1006,7 +753,16 @@ export default class MessageSender {
|
|||
const dataMessage = message.toProto();
|
||||
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.dataMessage = dataMessage;
|
||||
if (options.editedMessageTimestamp) {
|
||||
const editMessage = new Proto.EditMessage();
|
||||
editMessage.dataMessage = dataMessage;
|
||||
editMessage.targetSentTimestamp = Long.fromNumber(
|
||||
options.editedMessageTimestamp
|
||||
);
|
||||
contentMessage.editMessage = editMessage;
|
||||
} else {
|
||||
contentMessage.dataMessage = dataMessage;
|
||||
}
|
||||
|
||||
const { includePniSignatureMessage } = options;
|
||||
if (includePniSignatureMessage) {
|
||||
|
@ -1033,13 +789,6 @@ export default class MessageSender {
|
|||
attributes: Readonly<MessageOptionsType>
|
||||
): Promise<Message> {
|
||||
const message = new Message(attributes);
|
||||
await Promise.all([
|
||||
this.uploadAttachments(message),
|
||||
this.uploadContactAvatar(message),
|
||||
this.uploadThumbnails(message),
|
||||
this.uploadLinkPreviews(message),
|
||||
this.uploadSticker(message),
|
||||
]);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
@ -1094,6 +843,7 @@ export default class MessageSender {
|
|||
bodyRanges,
|
||||
contact,
|
||||
deletedForEveryoneTimestamp,
|
||||
editedMessageTimestamp,
|
||||
expireTimer,
|
||||
flags,
|
||||
groupCallUpdate,
|
||||
|
@ -1144,6 +894,7 @@ export default class MessageSender {
|
|||
body: messageText,
|
||||
contact,
|
||||
deletedForEveryoneTimestamp,
|
||||
editedMessageTimestamp,
|
||||
expireTimer,
|
||||
flags,
|
||||
groupCallUpdate,
|
||||
|
@ -1353,6 +1104,7 @@ export default class MessageSender {
|
|||
contact,
|
||||
contentHint,
|
||||
deletedForEveryoneTimestamp,
|
||||
editedMessageTimestamp,
|
||||
expireTimer,
|
||||
groupId,
|
||||
identifier,
|
||||
|
@ -1369,21 +1121,22 @@ export default class MessageSender {
|
|||
urgent,
|
||||
includePniSignatureMessage,
|
||||
}: Readonly<{
|
||||
attachments: ReadonlyArray<AttachmentType> | undefined;
|
||||
attachments: ReadonlyArray<UploadedAttachmentType> | undefined;
|
||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
contact?: Array<ContactWithHydratedAvatar>;
|
||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||
contentHint: number;
|
||||
deletedForEveryoneTimestamp: number | undefined;
|
||||
editedMessageTimestamp?: number;
|
||||
expireTimer: DurationInSeconds | undefined;
|
||||
groupId: string | undefined;
|
||||
identifier: string;
|
||||
messageText: string | undefined;
|
||||
options?: SendOptionsType;
|
||||
preview?: ReadonlyArray<LinkPreviewType> | undefined;
|
||||
preview?: ReadonlyArray<OutgoingLinkPreviewType> | undefined;
|
||||
profileKey?: Uint8Array;
|
||||
quote?: QuotedMessageType | null;
|
||||
quote?: OutgoingQuoteType;
|
||||
reaction?: ReactionType;
|
||||
sticker?: StickerWithHydratedData;
|
||||
sticker?: OutgoingStickerType;
|
||||
storyContext?: StoryContextType;
|
||||
story?: boolean;
|
||||
timestamp: number;
|
||||
|
@ -1397,6 +1150,7 @@ export default class MessageSender {
|
|||
body: messageText,
|
||||
contact,
|
||||
deletedForEveryoneTimestamp,
|
||||
editedMessageTimestamp,
|
||||
expireTimer,
|
||||
preview,
|
||||
profileKey,
|
||||
|
@ -1421,6 +1175,7 @@ export default class MessageSender {
|
|||
// Note: this is used for sending real messages to your other devices after sending a
|
||||
// message to others.
|
||||
async sendSyncMessage({
|
||||
editedMessageTimestamp,
|
||||
encodedDataMessage,
|
||||
timestamp,
|
||||
destination,
|
||||
|
@ -1434,6 +1189,7 @@ export default class MessageSender {
|
|||
storyMessage,
|
||||
storyMessageRecipients,
|
||||
}: Readonly<{
|
||||
editedMessageTimestamp?: number;
|
||||
encodedDataMessage?: Uint8Array;
|
||||
timestamp: number;
|
||||
destination: string | undefined;
|
||||
|
@ -1452,7 +1208,13 @@ export default class MessageSender {
|
|||
const sentMessage = new Proto.SyncMessage.Sent();
|
||||
sentMessage.timestamp = Long.fromNumber(timestamp);
|
||||
|
||||
if (encodedDataMessage) {
|
||||
if (editedMessageTimestamp && encodedDataMessage) {
|
||||
const dataMessage = Proto.DataMessage.decode(encodedDataMessage);
|
||||
const editMessage = new Proto.EditMessage();
|
||||
editMessage.dataMessage = dataMessage;
|
||||
editMessage.targetSentTimestamp = Long.fromNumber(editedMessageTimestamp);
|
||||
sentMessage.editMessage = editMessage;
|
||||
} else if (encodedDataMessage) {
|
||||
const dataMessage = Proto.DataMessage.decode(encodedDataMessage);
|
||||
sentMessage.message = dataMessage;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue