Merge pull request #566 from BeaudanBrown/group-attachments

Group attachments
pull/569/head
Beaudan Campbell-Brown 6 years ago committed by GitHub
commit b56dfbabaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -136,9 +136,9 @@
<div class='capture-audio hide'>
<button class='microphone' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
</div>
<div class='choose-file'>
<div id='choose-file' class='choose-file'>
<button class='paperclip thumbnail' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
<input type='file' class='file-input' multiple='multiple'>
<input type='file' class='file-input' multiple='multiple' accept='video/* image/*'>
</div>
</div>
</form>

@ -1,8 +1,9 @@
/* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController,
clearTimeout, MessageController, libsignal, StringView, window, _, dcodeIO */
clearTimeout, MessageController, libsignal, StringView, window, _, dcodeIO, Buffer */
const EventEmitter = require('events');
const nodeFetch = require('node-fetch');
const { URL, URLSearchParams } = require('url');
const FormData = require('form-data');
// Can't be less than 1200 if we have unauth'd requests
const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s
@ -11,6 +12,10 @@ const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s
const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
const ATTACHMENT_TYPE = 'net.app.core.oembed';
const LOKI_ATTACHMENT_TYPE = 'attachment';
const LOKI_PREVIEW_TYPE = 'preview';
class LokiAppDotNetAPI extends EventEmitter {
constructor(ourKey) {
super();
@ -344,6 +349,43 @@ class LokiAppDotNetServerAPI {
return false;
}
async uploadData(data) {
const endpoint = 'files';
const options = {
method: 'POST',
rawBody: data,
};
const { statusCode, response } = await this.serverRequest(
endpoint,
options
);
if (statusCode !== 200) {
log.warn('Failed to upload data to fileserver');
return null;
}
const url = response.data && response.data.url;
const id = response.data && response.data.id;
return {
url,
id,
};
}
putAttachment(attachmentBin) {
const formData = new FormData();
const buffer = Buffer.from(attachmentBin);
formData.append('type', 'network.loki');
formData.append('content', buffer, {
contentType: 'application/octet-stream',
name: 'content',
filename: 'attachment',
});
return this.uploadData(formData);
}
}
class LokiPublicChannelAPI {
@ -606,7 +648,13 @@ class LokiPublicChannelAPI {
}
}
static getSigData(sigVer, noteValue, adnMessage) {
static getSigData(
sigVer,
noteValue,
attachmentAnnotations,
previewAnnotations,
adnMessage
) {
let sigString = '';
sigString += adnMessage.text.trim();
sigString += noteValue.timestamp;
@ -618,6 +666,10 @@ class LokiPublicChannelAPI {
sigString += adnMessage.reply_to;
}
}
sigString += [...attachmentAnnotations, ...previewAnnotations]
.map(data => data.id || data.image.id)
.sort()
.join();
sigString += sigVer;
return dcodeIO.ByteBuffer.wrap(sigString, 'utf8').toArrayBuffer();
}
@ -640,17 +692,26 @@ class LokiPublicChannelAPI {
const { timestamp, quote } = noteValue;
if (quote) {
// TODO: Enable quote attachments again using proper ADN style
quote.attachments = [];
}
// try to verify signature
const { sig, sigver } = noteValue;
const annoCopy = [...adnMessage.annotations];
const attachments = annoCopy
.filter(anno => anno.value.lokiType === LOKI_ATTACHMENT_TYPE)
.map(attachment => ({ isRaw: true, ...attachment.value }));
const preview = annoCopy
.filter(anno => anno.value.lokiType === LOKI_PREVIEW_TYPE)
.map(LokiPublicChannelAPI.getPreviewFromAnnotation);
// strip out sig and sigver
annoCopy[0] = _.omit(annoCopy[0], ['value.sig', 'value.sigver']);
const sigData = LokiPublicChannelAPI.getSigData(
sigver,
noteValue,
attachments,
preview,
adnMessage
);
@ -688,6 +749,8 @@ class LokiPublicChannelAPI {
return {
timestamp,
attachments,
preview,
quote,
};
}
@ -747,7 +810,7 @@ class LokiPublicChannelAPI {
return;
}
const { timestamp, quote } = messengerData;
const { timestamp, quote, attachments, preview } = messengerData;
if (!timestamp) {
return; // Invalid message
}
@ -793,8 +856,9 @@ class LokiPublicChannelAPI {
receivedAt,
isPublic: true,
message: {
body: adnMessage.text,
attachments: [],
body:
adnMessage.text === timestamp.toString() ? '' : adnMessage.text,
attachments,
group: {
id: this.conversationId,
type: textsecure.protobuf.GroupContext.Type.DELIVER,
@ -807,7 +871,7 @@ class LokiPublicChannelAPI {
sent_at: timestamp,
quote,
contact: [],
preview: [],
preview,
profile: {
displayName: from,
},
@ -826,8 +890,85 @@ class LokiPublicChannelAPI {
}
}
static getPreviewFromAnnotation(annotation) {
const preview = {
title: annotation.value.linkPreviewTitle,
url: annotation.value.linkPreviewUrl,
image: {
isRaw: true,
caption: annotation.value.caption,
contentType: annotation.value.contentType,
digest: annotation.value.digest,
fileName: annotation.value.fileName,
flags: annotation.value.flags,
height: annotation.value.height,
id: annotation.value.id,
key: annotation.value.key,
size: annotation.value.size,
thumbnail: annotation.value.thumbnail,
url: annotation.value.url,
width: annotation.value.width,
},
};
return preview;
}
static getAnnotationFromPreview(preview) {
const annotation = {
type: ATTACHMENT_TYPE,
value: {
// Mandatory ADN fields
version: '1.0',
lokiType: LOKI_PREVIEW_TYPE,
// Signal stuff we actually care about
linkPreviewTitle: preview.title,
linkPreviewUrl: preview.url,
caption: preview.image.caption,
contentType: preview.image.contentType,
digest: preview.image.digest,
fileName: preview.image.fileName,
flags: preview.image.flags,
height: preview.image.height,
id: preview.image.id,
key: preview.image.key,
size: preview.image.size,
thumbnail: preview.image.thumbnail,
url: preview.image.url,
width: preview.image.width,
},
};
return annotation;
}
static getAnnotationFromAttachment(attachment) {
const type = attachment.contentType.match(/^image/) ? 'photo' : 'video';
const annotation = {
type: ATTACHMENT_TYPE,
value: {
// Mandatory ADN fields
version: '1.0',
type,
lokiType: LOKI_ATTACHMENT_TYPE,
// Signal stuff we actually care about
...attachment,
},
};
return annotation;
}
// create a message in the channel
async sendMessage(text, quote, messageTimeStamp) {
async sendMessage(data, messageTimeStamp) {
const { quote, attachments, preview } = data;
const text = data.body || messageTimeStamp.toString();
const attachmentAnnotations = attachments.map(
LokiPublicChannelAPI.getAnnotationFromAttachment
);
const previewAnnotations = preview.map(
LokiPublicChannelAPI.getAnnotationFromPreview
);
const payload = {
text,
annotations: [
@ -837,6 +978,8 @@ class LokiPublicChannelAPI {
timestamp: messageTimeStamp,
},
},
...attachmentAnnotations,
...previewAnnotations,
],
};
if (quote && quote.id) {
@ -869,6 +1012,8 @@ class LokiPublicChannelAPI {
const sigData = LokiPublicChannelAPI.getSigData(
sigVer,
payload.annotations[0].value,
attachmentAnnotations.map(anno => anno.value),
previewAnnotations.map(anno => anno.value),
mockAdnMessage
);
const sig = await libsignal.Curve.async.calculateSignature(

@ -2,8 +2,6 @@
const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
/* global log */
const DEVICE_MAPPING_ANNOTATION_KEY = 'network.loki.messenger.devicemapping';
class LokiFileServerAPI {
@ -38,28 +36,8 @@ class LokiFileServerAPI {
);
}
async uploadData(data) {
const endpoint = 'files';
const options = {
method: 'POST',
rawBody: data,
};
const { statusCode, response } = await this._server.serverRequest(
endpoint,
options
);
if (statusCode !== 200) {
log.warn('Failed to upload data to fileserver');
return null;
}
const url = response.data && response.data.url;
const id = response.data && response.data.id;
return {
url,
id,
};
uploadPrivateAttachment(data) {
return this._server.uploadData(data);
}
}

@ -93,11 +93,7 @@ class LokiMessageAPI {
'Missing public send data for public chat message'
);
}
const res = await publicSendData.sendMessage(
data.body,
data.quote,
messageTimeStamp
);
const res = await publicSendData.sendMessage(data, messageTimeStamp);
if (res === false) {
throw new window.textsecure.PublicChatError(
'Failed to send public chat message'

@ -864,7 +864,7 @@ function initialize({
filename: 'attachment',
});
return lokiFileServerAPI.uploadData(formData);
return lokiFileServerAPI.uploadPrivateAttachment(formData);
}
// eslint-disable-next-line no-shadow

@ -93,6 +93,7 @@
initialize(options) {
this.listenTo(this.model, 'destroy', this.stopListening);
this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
this.listenTo(this.model, 'change:friendRequestStatus', this.updateFileLock);
this.listenTo(this.model, 'newmessage', this.addMessage);
this.listenTo(this.model, 'opened', this.onOpened);
this.listenTo(this.model, 'prune', this.onPrune);
@ -160,6 +161,7 @@
);
this.render();
this.updateFileLock();
this.model.updateTextInputState();
@ -621,6 +623,14 @@
}
},
updateFileLock() {
if (this.model.isPrivate() && !this.model.isFriend()) {
this.$('#choose-file').hide()
} else {
this.$('#choose-file').show()
}
},
toggleMicrophone() {
// ALWAYS HIDE until we support audio
this.$('.capture-audio').hide();

@ -1438,21 +1438,23 @@ MessageReceiver.prototype.extend({
},
async downloadAttachment(attachment) {
// The attachment id is actually just the absolute url of the attachment
const encrypted = await this.server.getAttachment(attachment.url);
const { key, digest, size } = attachment;
const data = await textsecure.crypto.decryptAttachment(
encrypted,
window.Signal.Crypto.base64ToArrayBuffer(key),
window.Signal.Crypto.base64ToArrayBuffer(digest)
);
if (!size || size !== data.byteLength) {
throw new Error(
`downloadAttachment: Size ${size} did not match downloaded attachment size ${
data.byteLength
}`
let data = await this.server.getAttachment(attachment.url);
if (!attachment.isRaw) {
const { key, digest, size } = attachment;
data = await textsecure.crypto.decryptAttachment(
data,
window.Signal.Crypto.base64ToArrayBuffer(key),
window.Signal.Crypto.base64ToArrayBuffer(digest)
);
if (!size || size !== data.byteLength) {
throw new Error(
`downloadAttachment: Size ${size} did not match downloaded attachment size ${
data.byteLength
}`
);
}
}
return {

@ -168,7 +168,7 @@ MessageSender.prototype = {
constructor: MessageSender,
// makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto
makeAttachmentPointer(attachment) {
async makeAttachmentPointer(attachment, publicServer = null) {
if (typeof attachment !== 'object' || attachment == null) {
return Promise.resolve(undefined);
}
@ -185,42 +185,49 @@ MessageSender.prototype = {
}
const proto = new textsecure.protobuf.AttachmentPointer();
proto.key = libsignal.crypto.getRandomBytes(64);
const iv = libsignal.crypto.getRandomBytes(16);
return textsecure.crypto
.encryptAttachment(attachment.data, proto.key, iv)
.then(result =>
this.server
.putAttachment(result.ciphertext)
.then(async ({ url, id }) => {
proto.id = id;
proto.url = url;
proto.contentType = attachment.contentType;
proto.digest = result.digest;
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;
})
let attachmentData;
let server;
if (publicServer) {
attachmentData = attachment.data;
server = publicServer;
} 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;
({ server } = this);
}
const { url, id } = await server.putAttachment(attachmentData);
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) {
@ -243,9 +250,11 @@ MessageSender.prototype = {
});
},
uploadAttachments(message) {
uploadAttachments(message, publicServer) {
return Promise.all(
message.attachments.map(this.makeAttachmentPointer.bind(this))
message.attachments.map(attachment =>
this.makeAttachmentPointer(attachment, publicServer)
)
)
.then(attachmentPointers => {
// eslint-disable-next-line no-param-reassign
@ -260,12 +269,12 @@ MessageSender.prototype = {
});
},
async uploadLinkPreviews(message) {
async uploadLinkPreviews(message, publicServer) {
try {
const preview = await Promise.all(
(message.preview || []).map(async item => ({
...item,
image: await this.makeAttachmentPointer(item.image),
image: await this.makeAttachmentPointer(item.image, publicServer),
}))
);
// eslint-disable-next-line no-param-reassign
@ -279,7 +288,7 @@ MessageSender.prototype = {
}
},
uploadThumbnails(message) {
uploadThumbnails(message, publicServer) {
const makePointer = this.makeAttachmentPointer.bind(this);
const { quote } = message;
@ -294,7 +303,7 @@ MessageSender.prototype = {
return null;
}
return makePointer(thumbnail).then(pointer => {
return makePointer(thumbnail, publicServer).then(pointer => {
// eslint-disable-next-line no-param-reassign
attachment.attachmentPointer = pointer;
});
@ -311,11 +320,13 @@ MessageSender.prototype = {
sendMessage(attrs, options) {
const message = new Message(attrs);
const silent = false;
const publicServer =
options.publicSendData && options.publicSendData.serverAPI;
return Promise.all([
this.uploadAttachments(message),
this.uploadThumbnails(message),
this.uploadLinkPreviews(message),
this.uploadAttachments(message, publicServer),
this.uploadThumbnails(message, publicServer),
this.uploadLinkPreviews(message, publicServer),
]).then(
() =>
new Promise((resolve, reject) => {

Loading…
Cancel
Save