Add attachment util

pull/1198/head
Mikunj 5 years ago
parent 5a2de73639
commit b69ad7db16

@ -4,13 +4,21 @@ import {
Preview, Preview,
} from '../../ts/session/messages/outgoing'; } from '../../ts/session/messages/outgoing';
declare class LokiAppDotNetServerAPI { interface UploadResponse {
constructor(ourKey: string, url: string); url: string;
id?: number;
}
export interface LokiAppDotNetServerInterface {
findOrCreateChannel( findOrCreateChannel(
api: LokiPublicChatFactoryAPI, api: LokiPublicChatFactoryAPI,
channelId: number, channelId: number,
conversationId: string conversationId: string
): Promise<LokiPublicChannelAPI>; ): Promise<LokiPublicChannelAPI>;
uploadData(data: FormData): Promise<UploadResponse>;
uploadAvatar(data: FormData): Promise<UploadResponse>;
putAttachment(data: ArrayBuffer): Promise<UploadResponse>;
putAvatar(data: ArrayBuffer): Promise<UploadResponse>;
} }
export interface LokiPublicChannelAPI { export interface LokiPublicChannelAPI {
@ -25,4 +33,8 @@ export interface LokiPublicChannelAPI {
): Promise<boolean>; ): Promise<boolean>;
} }
declare class LokiAppDotNetServerAPI implements LokiAppDotNetServerInterface {
constructor(ourKey: string, url: string);
}
export default LokiAppDotNetServerAPI; export default LokiAppDotNetServerAPI;

@ -1166,8 +1166,7 @@ class LokiAppDotNetServerAPI {
); );
if (statusCode !== 200) { if (statusCode !== 200) {
log.warn('Failed to upload avatar to fileserver'); throw new Error(`Failed to upload avatar to ${this.baseServerUrl}`);
return null;
} }
const url = const url =
@ -1175,10 +1174,14 @@ class LokiAppDotNetServerAPI {
response.data.avatar_image && response.data.avatar_image &&
response.data.avatar_image.url; response.data.avatar_image.url;
if (!url) {
throw new Error(`Failed to upload data: Invalid url.`);
}
// We don't use the server id for avatars // We don't use the server id for avatars
return { return {
url, url,
id: null, id: undefined,
}; };
} }
@ -1195,12 +1198,16 @@ class LokiAppDotNetServerAPI {
options options
); );
if (statusCode !== 200) { if (statusCode !== 200) {
log.warn('Failed to upload data to server', this.baseServerUrl); throw new Error(`Failed to upload data to server: ${this.baseServerUrl}`);
return null;
} }
const url = response.data && response.data.url; const url = response.data && response.data.url;
const id = response.data && response.data.id; const id = response.data && response.data.id;
if (!url || !id) {
throw new Error(`Failed to upload data: Invalid url or id returned.`);
}
return { return {
url, url,
id, id,
@ -1221,6 +1228,17 @@ class LokiAppDotNetServerAPI {
return this.uploadData(formData); return this.uploadData(formData);
} }
putAvatar(buf) {
const formData = new FormData();
const buffer = Buffer.from(buf);
formData.append('avatar', buffer, {
contentType: 'application/octet-stream',
name: 'avatar',
filename: 'attachment',
});
return this.uploadAvatar(formData);
}
} }
// functions to a specific ADN channel on an ADN server // functions to a specific ADN channel on an ADN server

@ -1,5 +1,4 @@
declare class LokiMessageAPI { export interface LokiMessageInterface {
constructor(ourKey: string);
sendMessage( sendMessage(
pubKey: string, pubKey: string,
data: Uint8Array, data: Uint8Array,
@ -8,4 +7,8 @@ declare class LokiMessageAPI {
): Promise<void>; ): Promise<void>;
} }
declare class LokiMessageAPI implements LokiMessageInterface {
constructor(ourKey: string);
}
export default LokiMessageAPI; export default LokiMessageAPI;

@ -1,13 +1,22 @@
import { LokiPublicChannelAPI } from './loki_app_dot_net_api'; import {
LokiAppDotNetServerInterface,
LokiPublicChannelAPI,
} from './loki_app_dot_net_api';
declare class LokiPublicChatFactoryAPI { export interface LokiPublicChatFactoryInterface {
constructor(ourKey: string); ourKey: string;
findOrCreateServer(url: string): Promise<void>; findOrCreateServer(url: string): Promise<LokiAppDotNetServerInterface | null>;
findOrCreateChannel( findOrCreateChannel(
url: string, url: string,
channelId: number, channelId: number,
conversationId: string conversationId: string
): Promise<LokiPublicChannelAPI>; ): Promise<LokiPublicChannelAPI | null>;
getListOfMembers(): Promise<Array<{ authorPhoneNumber: string }>>;
}
declare class LokiPublicChatFactoryAPI
implements LokiPublicChatFactoryInterface {
constructor(ourKey: string);
} }
export default LokiPublicChatFactoryAPI; export default LokiPublicChatFactoryAPI;

@ -5,7 +5,6 @@ import classNames from 'classnames';
declare global { declare global {
interface Window { interface Window {
lokiPublicChatAPI: any;
shortenPubkey: any; shortenPubkey: any;
pubkeyPattern: any; pubkeyPattern: any;
getConversations: any; getConversations: any;

@ -10,7 +10,7 @@ export interface AttachmentPointer {
size?: number; size?: number;
thumbnail?: Uint8Array; thumbnail?: Uint8Array;
digest?: Uint8Array; digest?: Uint8Array;
filename?: string; fileName?: string;
flags?: number; flags?: number;
width?: number; width?: number;
height?: number; height?: number;

@ -107,12 +107,16 @@ export async function sendToOpenGroup(
group.conversationId group.conversationId
); );
if (!channelAPI) {
return false;
}
// Don't think returning true/false on `sendMessage` is a good way // Don't think returning true/false on `sendMessage` is a good way
return channelAPI.sendMessage( return channelAPI.sendMessage(
{ {
quote, quote,
attachments: attachments || [], attachments: attachments || [],
preview, preview: preview || [],
body, body,
}, },
timestamp timestamp

@ -0,0 +1,82 @@
import * as crypto from 'crypto';
import { Attachment } from '../../types/Attachment';
import { OpenGroup } from '../types';
import { AttachmentPointer } from '../messages/outgoing';
import { LokiAppDotNetServerInterface } from '../../../js/modules/loki_app_dot_net_api';
interface UploadParams {
attachment: Attachment;
openGroup?: OpenGroup;
isAvatar?: boolean;
isRaw?: boolean;
}
// tslint:disable-next-line: no-unnecessary-class
export class Attachments {
private constructor() {}
public static getDefaultServer(): LokiAppDotNetServerInterface {
return window.tokenlessFileServerAdnAPI;
}
public static async upload(params: UploadParams): Promise<AttachmentPointer> {
const { attachment, openGroup, isAvatar = false, isRaw = false } = 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\`; got: ${typeof attachment.data}`
);
}
let server = this.getDefaultServer();
if (openGroup) {
const openGroupServer = await window.lokiPublicChatAPI.findOrCreateServer(
openGroup.server
);
if (!openGroupServer) {
throw new Error(
`Failed to get open group server: ${openGroup.server}.`
);
}
server = openGroupServer;
}
const pointer: AttachmentPointer = {
contentType: attachment.contentType
? (attachment.contentType as string)
: undefined,
size: attachment.size,
fileName: attachment.fileName,
flags: attachment.flags,
};
let attachmentData: ArrayBuffer;
if (isRaw || openGroup) {
attachmentData = attachment.data;
} else {
server = this.getDefaultServer();
pointer.key = new Uint8Array(crypto.randomBytes(64));
const iv = new Uint8Array(crypto.randomBytes(16));
const data = await window.textsecure.crypto.encryptAttachment(
attachment.data,
pointer.key.buffer,
iv.buffer
);
pointer.digest = data.digest;
attachmentData = data.ciphertext;
}
const result = isAvatar
? await server.putAvatar(attachmentData)
: await server.putAttachment(attachmentData);
pointer.id = result.id;
pointer.url = result.url;
return pointer;
}
}

@ -64,7 +64,7 @@ describe('OpenGroupMessage', () => {
size: 10, size: 10,
thumbnail: new Uint8Array(2), thumbnail: new Uint8Array(2),
digest: new Uint8Array(3), digest: new Uint8Array(3),
filename: 'filename', fileName: 'filename',
flags: 0, flags: 0,
width: 10, width: 10,
height: 20, height: 20,

@ -8,9 +8,7 @@ import { TestUtils } from '../../test-utils';
import { UserUtil } from '../../../util'; import { UserUtil } from '../../../util';
import { MessageEncrypter } from '../../../session/crypto'; import { MessageEncrypter } from '../../../session/crypto';
import { SignalService } from '../../../protobuf'; import { SignalService } from '../../../protobuf';
import LokiPublicChatFactoryAPI from '../../../../js/modules/loki_public_chat_api';
import { OpenGroupMessage } from '../../../session/messages/outgoing'; import { OpenGroupMessage } from '../../../session/messages/outgoing';
import { LokiPublicChannelAPI } from '../../../../js/modules/loki_app_dot_net_api';
import { EncryptionType } from '../../../session/types/EncryptionType'; import { EncryptionType } from '../../../session/types/EncryptionType';
describe('MessageSender', () => { describe('MessageSender', () => {
@ -38,15 +36,21 @@ describe('MessageSender', () => {
describe('send', () => { describe('send', () => {
const ourNumber = 'ourNumber'; const ourNumber = 'ourNumber';
let lokiMessageAPIStub: sinon.SinonStubbedInstance<LokiMessageAPI>; let lokiMessageAPISendStub: sinon.SinonStub<
[string, Uint8Array, number, number],
Promise<void>
>;
let encryptStub: sinon.SinonStub<[string, Uint8Array, EncryptionType]>; let encryptStub: sinon.SinonStub<[string, Uint8Array, EncryptionType]>;
beforeEach(() => { beforeEach(() => {
// We can do this because LokiMessageAPI has a module export in it // We can do this because LokiMessageAPI has a module export in it
lokiMessageAPIStub = sandbox.createStubInstance(LokiMessageAPI, { lokiMessageAPISendStub = sandbox.stub<
sendMessage: sandbox.stub(), [string, Uint8Array, number, number],
Promise<void>
>();
TestUtils.stubWindow('lokiMessageAPI', {
sendMessage: lokiMessageAPISendStub,
}); });
TestUtils.stubWindow('lokiMessageAPI', lokiMessageAPIStub);
encryptStub = sandbox.stub(MessageEncrypter, 'encrypt').resolves({ encryptStub = sandbox.stub(MessageEncrypter, 'encrypt').resolves({
envelopeType: SignalService.Envelope.Type.CIPHERTEXT, envelopeType: SignalService.Envelope.Type.CIPHERTEXT,
@ -70,28 +74,26 @@ describe('MessageSender', () => {
encryptStub.throws(new Error('Failed to encrypt.')); encryptStub.throws(new Error('Failed to encrypt.'));
const promise = MessageSender.send(rawMessage); const promise = MessageSender.send(rawMessage);
await expect(promise).is.rejectedWith('Failed to encrypt.'); await expect(promise).is.rejectedWith('Failed to encrypt.');
expect(lokiMessageAPIStub.sendMessage.callCount).to.equal(0); expect(lokiMessageAPISendStub.callCount).to.equal(0);
}); });
it('should only call lokiMessageAPI once if no errors occured', async () => { it('should only call lokiMessageAPI once if no errors occured', async () => {
await MessageSender.send(rawMessage); await MessageSender.send(rawMessage);
expect(lokiMessageAPIStub.sendMessage.callCount).to.equal(1); expect(lokiMessageAPISendStub.callCount).to.equal(1);
}); });
it('should only retry the specified amount of times before throwing', async () => { it('should only retry the specified amount of times before throwing', async () => {
lokiMessageAPIStub.sendMessage.throws(new Error('API error')); lokiMessageAPISendStub.throws(new Error('API error'));
const attempts = 2; const attempts = 2;
const promise = MessageSender.send(rawMessage, attempts); const promise = MessageSender.send(rawMessage, attempts);
await expect(promise).is.rejectedWith('API error'); await expect(promise).is.rejectedWith('API error');
expect(lokiMessageAPIStub.sendMessage.callCount).to.equal(attempts); expect(lokiMessageAPISendStub.callCount).to.equal(attempts);
}); });
it('should not throw error if successful send occurs within the retry limit', async () => { it('should not throw error if successful send occurs within the retry limit', async () => {
lokiMessageAPIStub.sendMessage lokiMessageAPISendStub.onFirstCall().throws(new Error('API error'));
.onFirstCall()
.throws(new Error('API error'));
await MessageSender.send(rawMessage, 3); await MessageSender.send(rawMessage, 3);
expect(lokiMessageAPIStub.sendMessage.callCount).to.equal(2); expect(lokiMessageAPISendStub.callCount).to.equal(2);
}); });
}); });
@ -120,7 +122,7 @@ describe('MessageSender', () => {
ttl, ttl,
}); });
const args = lokiMessageAPIStub.sendMessage.getCall(0).args; const args = lokiMessageAPISendStub.getCall(0).args;
expect(args[0]).to.equal(device); expect(args[0]).to.equal(device);
expect(args[2]).to.equal(timestamp); expect(args[2]).to.equal(timestamp);
expect(args[3]).to.equal(ttl); expect(args[3]).to.equal(ttl);
@ -143,7 +145,7 @@ describe('MessageSender', () => {
ttl: 1, ttl: 1,
}); });
const data = lokiMessageAPIStub.sendMessage.getCall(0).args[1]; const data = lokiMessageAPISendStub.getCall(0).args[1];
const webSocketMessage = SignalService.WebSocketMessage.decode(data); const webSocketMessage = SignalService.WebSocketMessage.decode(data);
expect(webSocketMessage.request?.body).to.not.equal( expect(webSocketMessage.request?.body).to.not.equal(
undefined, undefined,
@ -182,7 +184,7 @@ describe('MessageSender', () => {
ttl: 1, ttl: 1,
}); });
const data = lokiMessageAPIStub.sendMessage.getCall(0).args[1]; const data = lokiMessageAPISendStub.getCall(0).args[1];
const webSocketMessage = SignalService.WebSocketMessage.decode(data); const webSocketMessage = SignalService.WebSocketMessage.decode(data);
expect(webSocketMessage.request?.body).to.not.equal( expect(webSocketMessage.request?.body).to.not.equal(
undefined, undefined,
@ -211,12 +213,13 @@ describe('MessageSender', () => {
describe('sendToOpenGroup', () => { describe('sendToOpenGroup', () => {
it('should send the message to the correct server and channel', async () => { it('should send the message to the correct server and channel', async () => {
// We can do this because LokiPublicChatFactoryAPI has a module export in it // We can do this because LokiPublicChatFactoryAPI has a module export in it
const stub = sandbox.createStubInstance(LokiPublicChatFactoryAPI, { const stub = sandbox.stub().resolves({
findOrCreateChannel: sandbox.stub().resolves({ sendMessage: sandbox.stub(),
sendMessage: sandbox.stub(), });
} as LokiPublicChannelAPI) as any,
TestUtils.stubWindow('lokiPublicChatAPI', {
findOrCreateChannel: stub,
}); });
TestUtils.stubWindow('lokiPublicChatAPI', stub);
const group = { const group = {
server: 'server', server: 'server',
@ -231,11 +234,7 @@ describe('MessageSender', () => {
await MessageSender.sendToOpenGroup(message); await MessageSender.sendToOpenGroup(message);
const [ const [server, channel, conversationId] = stub.getCall(0).args;
server,
channel,
conversationId,
] = stub.findOrCreateChannel.getCall(0).args;
expect(server).to.equal(group.server); expect(server).to.equal(group.server);
expect(channel).to.equal(group.channel); expect(channel).to.equal(group.channel);
expect(conversationId).to.equal(group.conversationId); expect(conversationId).to.equal(group.conversationId);

11
ts/window.d.ts vendored

@ -1,9 +1,11 @@
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import LokiMessageAPI from '../../js/modules/loki_message_api'; import { LokiMessageAPIInterface } from '../../js/modules/loki_message_api';
import LokiPublicChatFactoryAPI from '../../js/modules/loki_public_chat_api';
import { LibsignalProtocol } from '../../libtextsecure/libsignal-protocol'; import { LibsignalProtocol } from '../../libtextsecure/libsignal-protocol';
import { SignalInterface } from '../../js/modules/signal'; import { SignalInterface } from '../../js/modules/signal';
import { Libloki } from '../libloki'; import { Libloki } from '../libloki';
import { LokiPublicChatFactoryInterface } from '../js/modules/loki_public_chat_api';
import { LokiAppDotNetServerInterface } from '../js/modules/loki_app_dot_net_api';
import { LokiMessageInterface } from '../js/modules/loki_message_api';
/* /*
We declare window stuff here instead of global.d.ts because we are importing other declarations. We declare window stuff here instead of global.d.ts because we are importing other declarations.
@ -48,8 +50,8 @@ declare global {
log: any; log: any;
lokiFeatureFlags: any; lokiFeatureFlags: any;
lokiFileServerAPI: LokiFileServerInstance; lokiFileServerAPI: LokiFileServerInstance;
lokiMessageAPI: LokiMessageAPI; lokiMessageAPI: LokiMessageInterface;
lokiPublicChatAPI: LokiPublicChatFactoryAPI; lokiPublicChatAPI: LokiPublicChatFactoryInterface;
mnemonic: any; mnemonic: any;
onLogin: any; onLogin: any;
passwordUtil: any; passwordUtil: any;
@ -71,6 +73,7 @@ declare global {
toggleMenuBar: any; toggleMenuBar: any;
toggleSpellCheck: any; toggleSpellCheck: any;
toggleTheme: any; toggleTheme: any;
tokenlessFileServerAdnAPI: LokiAppDotNetServerInterface;
userConfig: any; userConfig: any;
versionInfo: any; versionInfo: any;
} }

Loading…
Cancel
Save