|
|
|
import _ from 'lodash';
|
|
|
|
|
|
|
|
import { MessageModel } from '../models/message';
|
|
|
|
import { saveMessage } from '../../ts/data/data';
|
|
|
|
import { fromBase64ToArrayBuffer } from '../session/utils/String';
|
|
|
|
import { AttachmentUtils } from '../session/utils';
|
|
|
|
import { ConversationModel } from '../models/conversation';
|
|
|
|
|
|
|
|
export async function downloadAttachment(attachment: any) {
|
|
|
|
const serverUrl = new URL(attachment.url).origin;
|
|
|
|
|
|
|
|
// The fileserver adds the `-static` part for some reason
|
|
|
|
const defaultFileserver = _.includes(
|
|
|
|
['https://file-static.lokinet.org', 'https://file.getsession.org'],
|
|
|
|
serverUrl
|
|
|
|
);
|
|
|
|
|
|
|
|
let res: ArrayBuffer | null = null;
|
|
|
|
|
|
|
|
// TODO: we need attachments to remember which API should be used to retrieve them
|
|
|
|
if (!defaultFileserver) {
|
|
|
|
const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(serverUrl);
|
|
|
|
|
|
|
|
if (serverAPI) {
|
|
|
|
res = await serverAPI.downloadAttachment(attachment.url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fallback to using the default fileserver
|
|
|
|
if (defaultFileserver || !res || res.byteLength === 0) {
|
|
|
|
res = await window.lokiFileServerAPI.downloadAttachment(attachment.url);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (res.byteLength === 0) {
|
|
|
|
window.log.error('Failed to download attachment. Length is 0');
|
|
|
|
throw new Error(`Failed to download attachment. Length is 0 for ${attachment.url}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME "178" test to remove once this is fixed server side.
|
|
|
|
if (!window.lokiFeatureFlags.useFileOnionRequestsV2) {
|
|
|
|
if (res.byteLength === 178) {
|
|
|
|
window.log.error(
|
|
|
|
'Data of 178 length corresponds of a 404 returned as 200 by file.getsession.org.'
|
|
|
|
);
|
|
|
|
throw new Error(`downloadAttachment: invalid response for ${attachment.url}`);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// if useFileOnionRequestsV2 is true, we expect an ArrayBuffer not empty
|
|
|
|
}
|
|
|
|
|
|
|
|
// The attachment id is actually just the absolute url of the attachment
|
|
|
|
let data = res;
|
|
|
|
if (!attachment.isRaw) {
|
|
|
|
const { key, digest, size } = attachment;
|
|
|
|
|
|
|
|
if (!key || !digest) {
|
|
|
|
throw new Error('Attachment is not raw but we do not have a key to decode it');
|
|
|
|
}
|
|
|
|
|
|
|
|
data = await window.textsecure.crypto.decryptAttachment(
|
|
|
|
data,
|
|
|
|
fromBase64ToArrayBuffer(key),
|
|
|
|
fromBase64ToArrayBuffer(digest)
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!size || size !== data.byteLength) {
|
|
|
|
// we might have padding, check that all the remaining bytes are padding bytes
|
|
|
|
// otherwise we have an error.
|
|
|
|
if (AttachmentUtils.isLeftOfBufferPaddingOnly(data, size)) {
|
|
|
|
// we can safely remove the padding
|
|
|
|
data = data.slice(0, size);
|
|
|
|
} else {
|
|
|
|
throw new Error(
|
|
|
|
`downloadAttachment: Size ${size} did not match downloaded attachment size ${data.byteLength}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
..._.omit(attachment, 'digest', 'key'),
|
|
|
|
data,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function downloadAttachmentOpenGrouPV2(attachment: any) {
|
|
|
|
const serverUrl = new URL(attachment.url).origin;
|
|
|
|
|
|
|
|
// The fileserver adds the `-static` part for some reason
|
|
|
|
const defaultFileserver = _.includes(
|
|
|
|
['https://file-static.lokinet.org', 'https://file.getsession.org'],
|
|
|
|
serverUrl
|
|
|
|
);
|
|
|
|
|
|
|
|
let res: ArrayBuffer | null = null;
|
|
|
|
|
|
|
|
// TODO: we need attachments to remember which API should be used to retrieve them
|
|
|
|
if (!defaultFileserver) {
|
|
|
|
const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(serverUrl);
|
|
|
|
|
|
|
|
if (serverAPI) {
|
|
|
|
res = await serverAPI.downloadAttachment(attachment.url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fallback to using the default fileserver
|
|
|
|
if (defaultFileserver || !res || res.byteLength === 0) {
|
|
|
|
res = await window.lokiFileServerAPI.downloadAttachment(attachment.url);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (res.byteLength === 0) {
|
|
|
|
window.log.error('Failed to download attachment. Length is 0');
|
|
|
|
throw new Error(`Failed to download attachment. Length is 0 for ${attachment.url}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME "178" test to remove once this is fixed server side.
|
|
|
|
if (!window.lokiFeatureFlags.useFileOnionRequestsV2) {
|
|
|
|
if (res.byteLength === 178) {
|
|
|
|
window.log.error(
|
|
|
|
'Data of 178 length corresponds of a 404 returned as 200 by file.getsession.org.'
|
|
|
|
);
|
|
|
|
throw new Error(`downloadAttachment: invalid response for ${attachment.url}`);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// if useFileOnionRequestsV2 is true, we expect an ArrayBuffer not empty
|
|
|
|
}
|
|
|
|
|
|
|
|
// The attachment id is actually just the absolute url of the attachment
|
|
|
|
let data = res;
|
|
|
|
if (!attachment.isRaw) {
|
|
|
|
const { key, digest, size } = attachment;
|
|
|
|
|
|
|
|
if (!key || !digest) {
|
|
|
|
throw new Error('Attachment is not raw but we do not have a key to decode it');
|
|
|
|
}
|
|
|
|
|
|
|
|
data = await window.textsecure.crypto.decryptAttachment(
|
|
|
|
data,
|
|
|
|
fromBase64ToArrayBuffer(key),
|
|
|
|
fromBase64ToArrayBuffer(digest)
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!size || size !== data.byteLength) {
|
|
|
|
// we might have padding, check that all the remaining bytes are padding bytes
|
|
|
|
// otherwise we have an error.
|
|
|
|
if (AttachmentUtils.isLeftOfBufferPaddingOnly(data, size)) {
|
|
|
|
// we can safely remove the padding
|
|
|
|
data = data.slice(0, size);
|
|
|
|
} else {
|
|
|
|
throw new Error(
|
|
|
|
`downloadAttachment: Size ${size} did not match downloaded attachment size ${data.byteLength}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
..._.omit(attachment, 'digest', 'key'),
|
|
|
|
data,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processNormalAttachments(
|
|
|
|
message: MessageModel,
|
|
|
|
normalAttachments: Array<any>,
|
|
|
|
convo: ConversationModel
|
|
|
|
): Promise<number> {
|
|
|
|
const isOpenGroupV2 = convo.isOpenGroupV2();
|
|
|
|
const attachments = await Promise.all(
|
|
|
|
normalAttachments.map((attachment: any, index: any) => {
|
|
|
|
return window.Signal.AttachmentDownloads.addJob(attachment, {
|
|
|
|
messageId: message.id,
|
|
|
|
type: 'attachment',
|
|
|
|
index,
|
|
|
|
isOpenGroupV2,
|
|
|
|
});
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
message.set({ attachments });
|
|
|
|
|
|
|
|
return attachments.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processPreviews(message: MessageModel, convo: ConversationModel): Promise<number> {
|
|
|
|
let addedCount = 0;
|
|
|
|
const isOpenGroupV2 = convo.isOpenGroupV2();
|
|
|
|
|
|
|
|
const preview = await Promise.all(
|
|
|
|
(message.get('preview') || []).map(async (item: any, index: any) => {
|
|
|
|
if (!item.image) {
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
addedCount += 1;
|
|
|
|
|
|
|
|
const image = await window.Signal.AttachmentDownloads.addJob(item.image, {
|
|
|
|
messageId: message.id,
|
|
|
|
type: 'preview',
|
|
|
|
index,
|
|
|
|
isOpenGroupV2,
|
|
|
|
});
|
|
|
|
|
|
|
|
return { ...item, image };
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
message.set({ preview });
|
|
|
|
|
|
|
|
return addedCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processAvatars(message: MessageModel, convo: ConversationModel): Promise<number> {
|
|
|
|
let addedCount = 0;
|
|
|
|
const isOpenGroupV2 = convo.isOpenGroupV2();
|
|
|
|
|
|
|
|
const contacts = message.get('contact') || [];
|
|
|
|
|
|
|
|
const contact = await Promise.all(
|
|
|
|
contacts.map(async (item: any, index: any) => {
|
|
|
|
if (!item.avatar || !item.avatar.avatar) {
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
|
|
|
|
addedCount += 1;
|
|
|
|
|
|
|
|
const avatarJob = await window.Signal.AttachmentDownloads.addJob(item.avatar.avatar, {
|
|
|
|
messaeId: message.id,
|
|
|
|
type: 'contact',
|
|
|
|
index,
|
|
|
|
isOpenGroupV2,
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
avatar: {
|
|
|
|
...item.avatar,
|
|
|
|
avatar: avatarJob,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
message.set({ contact });
|
|
|
|
|
|
|
|
return addedCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processQuoteAttachments(
|
|
|
|
message: MessageModel,
|
|
|
|
convo: ConversationModel
|
|
|
|
): Promise<number> {
|
|
|
|
let addedCount = 0;
|
|
|
|
|
|
|
|
const quote = message.get('quote');
|
|
|
|
|
|
|
|
if (!quote || !quote.attachments || !quote.attachments.length) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
const isOpenGroupV2 = convo.isOpenGroupV2();
|
|
|
|
|
|
|
|
quote.attachments = await Promise.all(
|
|
|
|
quote.attachments.map(async (item: any, index: any) => {
|
|
|
|
// If we already have a path, then we copied this image from the quoted
|
|
|
|
// message and we don't need to download the attachment.
|
|
|
|
if (!item.thumbnail || item.thumbnail.path) {
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
|
|
|
|
addedCount += 1;
|
|
|
|
|
|
|
|
const thumbnail = await window.Signal.AttachmentDownloads.addJob(item.thumbnail, {
|
|
|
|
messageId: message.id,
|
|
|
|
type: 'quote',
|
|
|
|
index,
|
|
|
|
isOpenGroupV2,
|
|
|
|
});
|
|
|
|
|
|
|
|
return { ...item, thumbnail };
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
message.set({ quote });
|
|
|
|
|
|
|
|
return addedCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function processGroupAvatar(
|
|
|
|
message: MessageModel,
|
|
|
|
convo: ConversationModel
|
|
|
|
): Promise<boolean> {
|
|
|
|
let group = message.get('group');
|
|
|
|
|
|
|
|
if (!group || !group.avatar) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const isOpenGroupV2 = convo.isOpenGroupV2();
|
|
|
|
|
|
|
|
group = {
|
|
|
|
...group,
|
|
|
|
avatar: await window.Signal.AttachmentDownloads.addJob(group.avatar, {
|
|
|
|
messageId: message.id,
|
|
|
|
type: 'group-avatar',
|
|
|
|
index: 0,
|
|
|
|
isOpenGroupV2,
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
|
|
|
|
message.set({ group });
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function queueAttachmentDownloads(
|
|
|
|
message: MessageModel,
|
|
|
|
conversation: ConversationModel
|
|
|
|
): Promise<void> {
|
|
|
|
let count = 0;
|
|
|
|
|
|
|
|
count += await processNormalAttachments(message, message.get('attachments') || [], conversation);
|
|
|
|
|
|
|
|
count += await processPreviews(message, conversation);
|
|
|
|
|
|
|
|
count += await processAvatars(message, conversation);
|
|
|
|
|
|
|
|
count += await processQuoteAttachments(message, conversation);
|
|
|
|
|
|
|
|
if (await processGroupAvatar(message, conversation)) {
|
|
|
|
count += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (count > 0) {
|
|
|
|
await saveMessage(message.attributes);
|
|
|
|
}
|
|
|
|
}
|