diff --git a/background.html b/background.html
index 04040201f..a2c80e74b 100644
--- a/background.html
+++ b/background.html
@@ -136,9 +136,9 @@
-
diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js
index eb113d860..c0670aedd 100644
--- a/js/modules/loki_app_dot_net_api.js
+++ b/js/modules/loki_app_dot_net_api.js
@@ -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(
diff --git a/js/modules/loki_file_server_api.js b/js/modules/loki_file_server_api.js
index b70984ccc..e6f1ec518 100644
--- a/js/modules/loki_file_server_api.js
+++ b/js/modules/loki_file_server_api.js
@@ -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);
}
}
diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js
index 2c52ea4a3..d3f10a9f1 100644
--- a/js/modules/loki_message_api.js
+++ b/js/modules/loki_message_api.js
@@ -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'
diff --git a/js/modules/web_api.js b/js/modules/web_api.js
index 92bdf937b..7de88a727 100644
--- a/js/modules/web_api.js
+++ b/js/modules/web_api.js
@@ -864,7 +864,7 @@ function initialize({
filename: 'attachment',
});
- return lokiFileServerAPI.uploadData(formData);
+ return lokiFileServerAPI.uploadPrivateAttachment(formData);
}
// eslint-disable-next-line no-shadow
diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js
index 8826a89c1..482e15e29 100644
--- a/js/views/conversation_view.js
+++ b/js/views/conversation_view.js
@@ -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();
diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js
index 4a987d6b5..ea2539c8c 100644
--- a/libtextsecure/message_receiver.js
+++ b/libtextsecure/message_receiver.js
@@ -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 {
diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js
index 4ce31886d..613954c56 100644
--- a/libtextsecure/sendmessage.js
+++ b/libtextsecure/sendmessage.js
@@ -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) => {