From f7e163c14201e11250e7db6196cacad629d181ea Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 26 Apr 2021 11:56:00 +1000 Subject: [PATCH] add sending of message for opengroupv2` --- ts/models/conversation.ts | 35 ++++- ts/models/message.ts | 36 ++++- ts/opengroup/opengroupV2/ApiUtil.ts | 10 +- ts/opengroup/opengroupV2/OpenGroupAPIV2.ts | 54 ++++--- .../opengroupV2/OpenGroupAPIV2CompactPoll.ts | 1 - ts/opengroup/utils/OpenGroupUtils.ts | 19 +++ .../messages/outgoing/OpenGroupMessage.ts | 3 + .../visibleMessage/OpenGroupVisibleMessage.ts | 3 + ts/session/sending/MessageQueue.ts | 33 ++++- ts/session/sending/MessageSender.ts | 30 +++- ts/session/sending/MessageSentHandler.ts | 15 +- ts/session/types/RawMessage.ts | 2 - ts/session/utils/Attachments.ts | 31 ++-- ts/session/utils/AttachmentsV2.ts | 135 ++++++++++++++++++ ts/session/utils/Messages.ts | 1 - ts/session/utils/index.ts | 2 + 16 files changed, 335 insertions(+), 75 deletions(-) create mode 100644 ts/session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage.ts create mode 100644 ts/session/utils/AttachmentsV2.ts diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 0f2fad665..05aa92400 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -39,6 +39,10 @@ import { ReadReceiptMessage } from '../session/messages/outgoing/controlMessage/ import { OpenGroup } from '../opengroup/opengroupV1/OpenGroup'; import { OpenGroupUtils } from '../opengroup/utils'; import { ConversationInteraction } from '../interactions'; +import { getV2OpenGroupRoom } from '../data/opengroups'; +import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; +import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil'; +import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils'; export enum ConversationType { GROUP = 'group', @@ -573,9 +577,16 @@ export class ConversationModel extends Backbone.Model { }; } - public toOpenGroup() { - if (!this.isPublic()) { - throw new Error('tried to run toOpenGroup for not public group'); + public toOpenGroupV2(): OpenGroupRequestCommonType { + if (!this.isOpenGroupV2()) { + throw new Error('tried to run toOpenGroup for not public group v2'); + } + return getOpenGroupV2FromConversationId(this.id); + } + + public toOpenGroupV1(): OpenGroup { + if (!this.isOpenGroupV1()) { + throw new Error('tried to run toOpenGroup for not public group v1'); } return new OpenGroup({ @@ -596,8 +607,8 @@ export class ConversationModel extends Backbone.Model { throw new Error('sendMessageJob() sent_at must be set.'); } - if (this.isPublic()) { - const openGroup = this.toOpenGroup(); + if (this.isPublic() && !this.isOpenGroupV2()) { + const openGroup = this.toOpenGroupV1(); const openGroupParams = { body: uploads.body, @@ -611,8 +622,10 @@ export class ConversationModel extends Backbone.Model { const openGroupMessage = new OpenGroupMessage(openGroupParams); // we need the return await so that errors are caught in the catch {} await getMessageQueue().sendToOpenGroup(openGroupMessage); + return; } + // an OpenGroupV2 message is just a visible message const chatMessageParams: VisibleMessageParams = { body: uploads.body, identifier: id, @@ -624,6 +637,18 @@ export class ConversationModel extends Backbone.Model { lokiProfile: UserUtils.getOurProfile(), }; + if (this.isOpenGroupV2()) { + const chatMessageOpenGroupV2 = new OpenGroupVisibleMessage(chatMessageParams); + const roomInfos = this.toOpenGroupV2(); + if (!roomInfos) { + throw new Error('Could not find this room in db'); + } + + // we need the return await so that errors are caught in the catch {} + await getMessageQueue().sendToOpenGroupV2(chatMessageOpenGroupV2, roomInfos); + return; + } + const destinationPubkey = new PubKey(destination); if (this.isPrivate()) { if (this.isMe()) { diff --git a/ts/models/message.ts b/ts/models/message.ts index 422eda4ce..27e57efcb 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -27,6 +27,11 @@ import { isOpenGroupV2 } from '../opengroup/utils/OpenGroupUtils'; import { banUser } from '../opengroup/opengroupV2/OpenGroupAPIV2'; import { getV2OpenGroupRoom } from '../data/opengroups'; import { MessageInteraction } from '../interactions'; +import { + uploadAttachmentsV2, + uploadLinkPreviewsV2, + uploadQuoteThumbnailsV2, +} from '../session/utils/AttachmentsV2'; export class MessageModel extends Backbone.Model { public propsForTimerNotification: any; public propsForGroupNotification: any; @@ -736,14 +741,35 @@ export class MessageModel extends Backbone.Model { const previewWithData = await window.Signal.Migrations.loadPreviewData(this.get('preview')); const conversation = this.getConversation(); - const openGroup = - (conversation && conversation.isPublic() && conversation.toOpenGroup()) || undefined; + let attachmentPromise; + let linkPreviewPromise; + let quotePromise; const { AttachmentUtils } = Utils; + + // we want to go for the v1, if this is an OpenGroupV1 or not an open group at all + if (conversation?.isOpenGroupV2()) { + const openGroupV2 = conversation.toOpenGroupV2(); + attachmentPromise = uploadAttachmentsV2(filenameOverridenAttachments, openGroupV2); + linkPreviewPromise = uploadLinkPreviewsV2(previewWithData, openGroupV2); + quotePromise = uploadQuoteThumbnailsV2(openGroupV2, quoteWithData); + } else { + // NOTE: we want to go for the v1 if this is an OpenGroupV1 or not an open group at all + // because there is a fallback invoked on uploadV1() for attachments for not open groups attachments + + const openGroupV1 = conversation?.toOpenGroupV1(); + attachmentPromise = AttachmentUtils.uploadAttachmentsV1( + filenameOverridenAttachments, + openGroupV1 + ); + linkPreviewPromise = AttachmentUtils.uploadLinkPreviewsV1(previewWithData, openGroupV1); + quotePromise = AttachmentUtils.uploadQuoteThumbnailsV1(quoteWithData, openGroupV1); + } + const [attachments, preview, quote] = await Promise.all([ - AttachmentUtils.uploadAttachments(filenameOverridenAttachments, openGroup), - AttachmentUtils.uploadLinkPreviews(previewWithData, openGroup), - AttachmentUtils.uploadQuoteThumbnails(quoteWithData, openGroup), + attachmentPromise, + linkPreviewPromise, + quotePromise, ]); return { diff --git a/ts/opengroup/opengroupV2/ApiUtil.ts b/ts/opengroup/opengroupV2/ApiUtil.ts index a6ab72bc3..69f8f41b2 100644 --- a/ts/opengroup/opengroupV2/ApiUtil.ts +++ b/ts/opengroup/opengroupV2/ApiUtil.ts @@ -1,13 +1,6 @@ import _ from 'underscore'; -import { getV2OpenGroupRoomByRoomId } from '../../data/opengroups'; -import { getSodium } from '../../session/crypto'; import { PubKey } from '../../session/types'; -import { - fromBase64ToArray, - fromBase64ToArrayBuffer, - fromHex, - fromHexToArray, -} from '../../session/utils/String'; +import { fromBase64ToArrayBuffer, fromHex } from '../../session/utils/String'; import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; export const defaultServer = 'https://sessionopengroup.com'; @@ -80,6 +73,7 @@ export const setCachedModerators = ( cachedModerators.set(serverUrl, new Map()); allRoomsMods = cachedModerators.get(serverUrl); } + // tslint:disable: no-non-null-assertion if (!allRoomsMods!.get(roomId)) { allRoomsMods!.set(roomId, new Set()); } diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts index 66864b313..69b10fa94 100644 --- a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts @@ -335,40 +335,38 @@ export const getMessages = async ({ return validMessages; }; +/** + * Send the specified message to the specified room. + * If an error happens, this function throws it + * + */ export const postMessage = async ( message: OpenGroupMessageV2, room: OpenGroupRequestCommonType ) => { - try { - const signedMessage = await message.sign(); - const json = signedMessage.toJson(); - - const request: OpenGroupV2Request = { - method: 'POST', - room: room.roomId, - server: room.serverUrl, - queryParams: json, - isAuthRequired: true, - endpoint: 'messages', - }; - const result = await sendOpenGroupV2Request(request); - const statusCode = parseStatusCodeFromOnionRequest(result); + const signedMessage = await message.sign(); + const json = signedMessage.toJson(); - if (statusCode !== 200) { - window.log.warn(`Could not postMessage, status code: ${statusCode}`); - return null; - } - const rawMessage = (result as any)?.result?.message; - if (!rawMessage) { - window.log.warn('postMessage parsing failed'); - return null; - } - // this will throw if the json is not valid - return OpenGroupMessageV2.fromJson(rawMessage); - } catch (e) { - window.log.error('Failed to post message to open group v2', e); - return null; + const request: OpenGroupV2Request = { + method: 'POST', + room: room.roomId, + server: room.serverUrl, + queryParams: json, + isAuthRequired: true, + endpoint: 'messages', + }; + const result = await sendOpenGroupV2Request(request); + const statusCode = parseStatusCodeFromOnionRequest(result); + + if (statusCode !== 200) { + throw new Error(`Could not postMessage, status code: ${statusCode}`); + } + const rawMessage = (result as any)?.result?.message; + if (!rawMessage) { + throw new Error('postMessage parsing failed'); } + // this will throw if the json is not valid + return OpenGroupMessageV2.fromJson(rawMessage); }; /** Those functions are related to moderators management */ diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts index 04f0436c6..d767e40e8 100644 --- a/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts +++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts @@ -145,7 +145,6 @@ async function sendOpenGroupV2RequestCompactPoll( const roomPollValidResults = results.filter(ret => ret.statusCode === 200); if (roomWithTokensToRefresh) { - console.warn('roomWithTokensToRefresh', roomWithTokensToRefresh); await Promise.all( roomWithTokensToRefresh.map(async roomId => { const roomDetails = await getV2OpenGroupRoomByRoomId({ diff --git a/ts/opengroup/utils/OpenGroupUtils.ts b/ts/opengroup/utils/OpenGroupUtils.ts index 744257275..3881bb741 100644 --- a/ts/opengroup/utils/OpenGroupUtils.ts +++ b/ts/opengroup/utils/OpenGroupUtils.ts @@ -1,6 +1,7 @@ import { default as insecureNodeFetch } from 'node-fetch'; import { OpenGroupV2Room } from '../../data/opengroups'; import { sendViaOnion } from '../../session/onions/onionSend'; +import { OpenGroupRequestCommonType } from '../opengroupV2/ApiUtil'; const protocolRegex = new RegExp('(https?://)?'); @@ -163,6 +164,24 @@ export function getOpenGroupV2ConversationId(serverUrl: string, roomId: string) return `${openGroupPrefix}${roomId}@${serverUrl}`; } +/** + * No sql access. Just plain string logic + */ +export function getOpenGroupV2FromConversationId( + conversationId: string +): OpenGroupRequestCommonType { + if (isOpenGroupV2(conversationId)) { + const atIndex = conversationId.indexOf('@'); + const roomId = conversationId.slice(openGroupPrefix.length, atIndex); + const serverUrl = conversationId.slice(atIndex + 1); + return { + serverUrl, + roomId, + }; + } + throw new Error('Not a v2 open group convo id'); +} + /** * Check if this conversation id corresponds to an OpenGroupV1 conversation. * No access to database are made. Only regex matches diff --git a/ts/session/messages/outgoing/OpenGroupMessage.ts b/ts/session/messages/outgoing/OpenGroupMessage.ts index 125444aaa..355e9c3b4 100644 --- a/ts/session/messages/outgoing/OpenGroupMessage.ts +++ b/ts/session/messages/outgoing/OpenGroupMessage.ts @@ -10,6 +10,9 @@ interface OpenGroupMessageParams extends MessageParams { quote?: Quote; } +/** + * This class is only used for OpenGroup v1 (deprecated) + */ export class OpenGroupMessage extends Message { public readonly group: OpenGroup; public readonly body?: string; diff --git a/ts/session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage.ts b/ts/session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage.ts new file mode 100644 index 000000000..13ff6ba37 --- /dev/null +++ b/ts/session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage.ts @@ -0,0 +1,3 @@ +import { VisibleMessage } from './VisibleMessage'; + +export class OpenGroupVisibleMessage extends VisibleMessage {} diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index ee418c49a..b8ef41d7e 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -18,6 +18,9 @@ import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMe import { ClosedGroupVisibleMessage } from '../messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; import { SyncMessageType } from '../utils/syncUtils'; +import { OpenGroupRequestCommonType } from '../../opengroup/opengroupV2/ApiUtil'; +import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; + type ClosedGroupMessageType = | ClosedGroupVisibleMessage | ClosedGroupAddedMembersMessage @@ -51,7 +54,7 @@ export class MessageQueue { } /** - * This function is synced. It will wait for the message to be delivered to the open + * DEPRECATED This function is synced. It will wait for the message to be delivered to the open * group to return. * So there is no need for a sendCb callback * @@ -79,6 +82,34 @@ export class MessageQueue { } } + /** + * This function is synced. It will wait for the message to be delivered to the open + * group to return. + * So there is no need for a sendCb callback + * + */ + public async sendToOpenGroupV2( + message: OpenGroupVisibleMessage, + roomInfos: OpenGroupRequestCommonType + ) { + // No queue needed for Open Groups v2; send directly + const error = new Error('Failed to send message to open group.'); + + try { + const { sentTimestamp, serverId } = await MessageSender.sendToOpenGroupV2(message, roomInfos); + if (!serverId) { + throw new Error(`Invalid serverId returned by server: ${serverId}`); + } + void MessageSentHandler.handlePublicMessageSentSuccess(message, { + serverId: serverId, + serverTimestamp: sentTimestamp, + }); + } catch (e) { + window?.log?.warn(`Failed to send message to open group: ${roomInfos}`, e); + void MessageSentHandler.handleMessageSentFailure(message, error); + } + } + /** * * @param sentCb currently only called for medium groups sent message diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 36c2bdc6c..ede87ce60 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -7,6 +7,13 @@ import { MessageEncrypter } from '../crypto'; import pRetry from 'p-retry'; import { PubKey } from '../types'; import { UserUtils } from '../utils'; +import { VisibleMessage } from '../messages/outgoing/visibleMessage/VisibleMessage'; +import { OpenGroupRequestCommonType } from '../../opengroup/opengroupV2/ApiUtil'; +import { postMessage } from '../../opengroup/opengroupV2/OpenGroupAPIV2'; +import { OpenGroupMessageV2 } from '../../opengroup/opengroupV2/OpenGroupMessageV2'; +import { padPlainTextBuffer } from '../crypto/MessageEncrypter'; +import { fromUInt8ArrayToBase64 } from '../utils/String'; +import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; // ================ Regular ================ @@ -99,7 +106,7 @@ function wrapEnvelope(envelope: SignalService.Envelope): Uint8Array { // ================ Open Group ================ /** - * Send a message to an open group. + * Deprecated Send a message to an open group v2. * @param message The open group message. */ export async function sendToOpenGroup( @@ -132,3 +139,24 @@ export async function sendToOpenGroup( timestamp ); } + +/** + * Deprecated Send a message to an open group v2. + * @param message The open group message. + */ +export async function sendToOpenGroupV2( + rawMessage: OpenGroupVisibleMessage, + roomInfos: OpenGroupRequestCommonType +): Promise { + const paddedBody = padPlainTextBuffer(rawMessage.plainTextBuffer()); + const v2Message = new OpenGroupMessageV2({ + sentTimestamp: Date.now(), + sender: UserUtils.getOurPubKeyStrFromCache(), + base64EncodedData: fromUInt8ArrayToBase64(paddedBody), + // the signature is added in the postMessage()) + }); + + // postMessage throws + const sentMessage = await postMessage(v2Message, roomInfos); + return sentMessage; +} diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index b42290eed..2e4f9b15a 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -1,16 +1,16 @@ import _ from 'lodash'; import { getMessageById } from '../../data/data'; import { SignalService } from '../../protobuf'; -import { ConversationController } from '../conversations'; import { MessageController } from '../messages'; import { OpenGroupMessage } from '../messages/outgoing'; +import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; import { EncryptionType, RawMessage } from '../types'; import { UserUtils } from '../utils'; // tslint:disable-next-line no-unnecessary-class export class MessageSentHandler { public static async handlePublicMessageSentSuccess( - sentMessage: OpenGroupMessage, + sentMessage: OpenGroupMessage | OpenGroupVisibleMessage, result: { serverId: number; serverTimestamp: number } ) { const { serverId, serverTimestamp } = result; @@ -131,7 +131,7 @@ export class MessageSentHandler { } public static async handleMessageSentFailure( - sentMessage: RawMessage | OpenGroupMessage, + sentMessage: RawMessage | OpenGroupMessage | OpenGroupVisibleMessage, error: any ) { const fetchedMessage = await MessageSentHandler.fetchHandleMessageSentData(sentMessage); @@ -143,7 +143,10 @@ export class MessageSentHandler { await fetchedMessage.saveErrors(error); } - if (!(sentMessage instanceof OpenGroupMessage)) { + if ( + !(sentMessage instanceof OpenGroupMessage) && + !(sentMessage instanceof OpenGroupVisibleMessage) + ) { const isOurDevice = UserUtils.isUsFromCache(sentMessage.device); // if this message was for ourself, and it was not already synced, // it means that we failed to sync it. @@ -176,7 +179,9 @@ export class MessageSentHandler { * In this case, this function will look for it in the database and return it. * If the message is found on the db, it will also register it to the MessageController so our subsequent calls are quicker. */ - private static async fetchHandleMessageSentData(m: RawMessage | OpenGroupMessage) { + private static async fetchHandleMessageSentData( + m: RawMessage | OpenGroupMessage | OpenGroupVisibleMessage + ) { // if a message was sent and this message was sent after the last app restart, // this message is still in memory in the MessageController const msg = MessageController.getInstance().get(m.identifier); diff --git a/ts/session/types/RawMessage.ts b/ts/session/types/RawMessage.ts index d3a8ed4c9..ee0996dde 100644 --- a/ts/session/types/RawMessage.ts +++ b/ts/session/types/RawMessage.ts @@ -1,7 +1,5 @@ import { EncryptionType } from './EncryptionType'; -// TODO: Should we store failure count on raw messages?? -// Might be better to have a seperate interface which takes in a raw message aswell as a failure count export type RawMessage = { identifier: string; plainTextBuffer: Uint8Array; diff --git a/ts/session/utils/Attachments.ts b/ts/session/utils/Attachments.ts index d7504198a..dd98e73eb 100644 --- a/ts/session/utils/Attachments.ts +++ b/ts/session/utils/Attachments.ts @@ -1,7 +1,6 @@ import * as crypto from 'crypto'; import { Attachment } from '../../types/Attachment'; -import { LokiAppDotNetServerInterface } from '../../../js/modules/loki_app_dot_net_api'; import { AttachmentPointer, Preview, @@ -43,11 +42,7 @@ export class AttachmentUtils { private constructor() {} - public static getDefaultServer(): LokiAppDotNetServerInterface { - return window.tokenlessFileServerAdnAPI; - } - - public static async upload(params: UploadParams): Promise { + public static async uploadV1(params: UploadParams): Promise { const { attachment, openGroup, isAvatar = false, isRaw = false, shouldPad = false } = params; if (typeof attachment !== 'object' || attachment == null) { throw new Error('Invalid attachment passed.'); @@ -59,7 +54,7 @@ export class AttachmentUtils { ); } - let server = this.getDefaultServer(); + let server = window.tokenlessFileServerAdnAPI; if (openGroup) { const openGroupServer = await window.lokiPublicChatAPI.findOrCreateServer(openGroup.server); if (!openGroupServer) { @@ -68,7 +63,7 @@ export class AttachmentUtils { server = openGroupServer; } const pointer: AttachmentPointer = { - contentType: attachment.contentType ? attachment.contentType : undefined, + contentType: attachment.contentType || undefined, size: attachment.size, fileName: attachment.fileName, flags: attachment.flags, @@ -80,7 +75,7 @@ export class AttachmentUtils { if (isRaw || openGroup) { attachmentData = attachment.data; } else { - server = this.getDefaultServer(); + server = window.tokenlessFileServerAdnAPI; pointer.key = new Uint8Array(crypto.randomBytes(64)); const iv = new Uint8Array(crypto.randomBytes(16)); @@ -107,7 +102,7 @@ export class AttachmentUtils { return pointer; } - public static async uploadAvatar( + public static async uploadAvatarV1( attachment?: Attachment ): Promise { if (!attachment) { @@ -116,19 +111,19 @@ export class AttachmentUtils { // isRaw is true since the data is already encrypted // and doesn't need to be encrypted again - return this.upload({ + return this.uploadV1({ attachment, isAvatar: true, isRaw: true, }); } - public static async uploadAttachments( + public static async uploadAttachmentsV1( attachments: Array, openGroup?: OpenGroup ): Promise> { const promises = (attachments || []).map(async attachment => - this.upload({ + this.uploadV1({ attachment, openGroup, shouldPad: true, @@ -138,7 +133,7 @@ export class AttachmentUtils { return Promise.all(promises); } - public static async uploadLinkPreviews( + public static async uploadLinkPreviewsV1( previews: Array, openGroup?: OpenGroup ): Promise> { @@ -149,7 +144,7 @@ export class AttachmentUtils { } return { ...item, - image: await this.upload({ + image: await this.uploadV1({ attachment: item.image, openGroup, }), @@ -158,7 +153,7 @@ export class AttachmentUtils { return Promise.all(promises); } - public static async uploadQuoteThumbnails( + public static async uploadQuoteThumbnailsV1( quote?: RawQuote, openGroup?: OpenGroup ): Promise { @@ -169,7 +164,7 @@ export class AttachmentUtils { const promises = (quote.attachments ?? []).map(async attachment => { let thumbnail: AttachmentPointer | undefined; if (attachment.thumbnail) { - thumbnail = await this.upload({ + thumbnail = await this.uploadV1({ attachment: attachment.thumbnail, openGroup, }); @@ -206,7 +201,7 @@ export class AttachmentUtils { return true; } - private static addAttachmentPadding(data: ArrayBuffer): ArrayBuffer { + public static addAttachmentPadding(data: ArrayBuffer): ArrayBuffer { const originalUInt = new Uint8Array(data); const paddedSize = Math.max( diff --git a/ts/session/utils/AttachmentsV2.ts b/ts/session/utils/AttachmentsV2.ts new file mode 100644 index 000000000..aee0d6984 --- /dev/null +++ b/ts/session/utils/AttachmentsV2.ts @@ -0,0 +1,135 @@ +import * as crypto from 'crypto'; +import { Attachment } from '../../types/Attachment'; + +import { OpenGroupRequestCommonType } from '../../opengroup/opengroupV2/ApiUtil'; +import { + AttachmentPointer, + Preview, + Quote, + QuotedAttachment, +} from '../messages/outgoing/visibleMessage/VisibleMessage'; +import { AttachmentUtils } from './Attachments'; +import { uploadFileOpenGroupV2 } from '../../opengroup/opengroupV2/OpenGroupAPIV2'; + +interface UploadParamsV2 { + attachment: Attachment; + openGroup: OpenGroupRequestCommonType; +} + +interface RawPreview { + url?: string; + title?: string; + image: Attachment; +} + +interface RawQuoteAttachment { + contentType?: string; + fileName?: string; + thumbnail?: Attachment; +} + +interface RawQuote { + id?: number; + author?: string; + text?: string; + attachments?: Array; +} + +const PADDING_BYTE = 0; + +export async function uploadV2(params: UploadParamsV2): Promise { + const { attachment, openGroup } = params; + if (typeof attachment !== 'object' || attachment == null) { + throw new Error('Invalid attachment passed.'); + } + + if (!(attachment.data instanceof ArrayBuffer)) { + throw new TypeError( + `attachment.data must be an ArrayBuffer but got: ${typeof attachment.data}` + ); + } + + const pointer: AttachmentPointer = { + contentType: attachment.contentType || undefined, + size: attachment.size, + fileName: attachment.fileName, + flags: attachment.flags, + caption: attachment.caption, + }; + + const paddedAttachment: ArrayBuffer = + (window.lokiFeatureFlags.padOutgoingAttachments && + AttachmentUtils.addAttachmentPadding(attachment.data)) || + attachment.data; + + const fileId = await uploadFileOpenGroupV2(new Uint8Array(paddedAttachment), openGroup); + + pointer.id = fileId || undefined; + console.warn('should we set the URL too here for that v2?'); + + return pointer; +} + +export async function uploadAttachmentsV2( + attachments: Array, + openGroup: OpenGroupRequestCommonType +): Promise> { + const promises = (attachments || []).map(async attachment => + exports.uploadV2({ + attachment, + openGroup, + }) + ); + + return Promise.all(promises); +} + +export async function uploadLinkPreviewsV2( + previews: Array, + openGroup: OpenGroupRequestCommonType +): Promise> { + const promises = (previews || []).map(async item => { + // some links does not have an image associated, and it makes the whole message fail to send + if (!item.image) { + return item; + } + return { + ...item, + image: await exports.uploadV2({ + attachment: item.image, + openGroup, + }), + }; + }); + return Promise.all(promises); +} + +export async function uploadQuoteThumbnailsV2( + openGroup: OpenGroupRequestCommonType, + quote?: RawQuote +): Promise { + if (!quote) { + return undefined; + } + + const promises = (quote.attachments ?? []).map(async attachment => { + let thumbnail: AttachmentPointer | undefined; + if (attachment.thumbnail) { + thumbnail = await exports.uploadV2({ + attachment: attachment.thumbnail, + openGroup, + }); + } + return { + ...attachment, + thumbnail, + } as QuotedAttachment; + }); + + const attachments = await Promise.all(promises); + + return { + ...quote, + attachments, + }; +} diff --git a/ts/session/utils/Messages.ts b/ts/session/utils/Messages.ts index 89d774828..b4cad8f5b 100644 --- a/ts/session/utils/Messages.ts +++ b/ts/session/utils/Messages.ts @@ -32,7 +32,6 @@ function getEncryptionTypeFromMessageType(message: ContentMessage): EncryptionTy export async function toRawMessage(device: PubKey, message: ContentMessage): Promise { const timestamp = message.timestamp; const ttl = message.ttl(); - // window?.log?.debug('toRawMessage proto:', message.contentProto()); const plainTextBuffer = message.plainTextBuffer(); const encryption = getEncryptionTypeFromMessageType(message); diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts index 85a377513..f60efb67e 100644 --- a/ts/session/utils/index.ts +++ b/ts/session/utils/index.ts @@ -8,6 +8,7 @@ import * as MenuUtils from '../../components/session/menu/Menu'; import * as ToastUtils from './Toast'; import * as UserUtils from './User'; import * as SyncUtils from './syncUtils'; +import * as AttachmentsV2Utils from './AttachmentsV2'; export * from './Attachments'; export * from './TypedEmitter'; @@ -24,4 +25,5 @@ export { ToastUtils, UserUtils, SyncUtils, + AttachmentsV2Utils, };