Add attachment util

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

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

@ -1166,8 +1166,7 @@ class LokiAppDotNetServerAPI {
);
if (statusCode !== 200) {
log.warn('Failed to upload avatar to fileserver');
return null;
throw new Error(`Failed to upload avatar to ${this.baseServerUrl}`);
}
const url =
@ -1175,10 +1174,14 @@ class LokiAppDotNetServerAPI {
response.data.avatar_image &&
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
return {
url,
id: null,
id: undefined,
};
}
@ -1195,12 +1198,16 @@ class LokiAppDotNetServerAPI {
options
);
if (statusCode !== 200) {
log.warn('Failed to upload data to server', this.baseServerUrl);
return null;
throw new Error(`Failed to upload data to server: ${this.baseServerUrl}`);
}
const url = response.data && response.data.url;
const id = response.data && response.data.id;
if (!url || !id) {
throw new Error(`Failed to upload data: Invalid url or id returned.`);
}
return {
url,
id,
@ -1221,6 +1228,17 @@ class LokiAppDotNetServerAPI {
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

@ -1,5 +1,4 @@
declare class LokiMessageAPI {
constructor(ourKey: string);
export interface LokiMessageInterface {
sendMessage(
pubKey: string,
data: Uint8Array,
@ -8,4 +7,8 @@ declare class LokiMessageAPI {
): Promise<void>;
}
declare class LokiMessageAPI implements LokiMessageInterface {
constructor(ourKey: string);
}
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 {
constructor(ourKey: string);
findOrCreateServer(url: string): Promise<void>;
export interface LokiPublicChatFactoryInterface {
ourKey: string;
findOrCreateServer(url: string): Promise<LokiAppDotNetServerInterface | null>;
findOrCreateChannel(
url: string,
channelId: number,
conversationId: string
): Promise<LokiPublicChannelAPI>;
): Promise<LokiPublicChannelAPI | null>;
getListOfMembers(): Promise<Array<{ authorPhoneNumber: string }>>;
}
declare class LokiPublicChatFactoryAPI
implements LokiPublicChatFactoryInterface {
constructor(ourKey: string);
}
export default LokiPublicChatFactoryAPI;

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

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

@ -107,12 +107,16 @@ export async function sendToOpenGroup(
group.conversationId
);
if (!channelAPI) {
return false;
}
// Don't think returning true/false on `sendMessage` is a good way
return channelAPI.sendMessage(
{
quote,
attachments: attachments || [],
preview,
preview: preview || [],
body,
},
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,
thumbnail: new Uint8Array(2),
digest: new Uint8Array(3),
filename: 'filename',
fileName: 'filename',
flags: 0,
width: 10,
height: 20,

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

11
ts/window.d.ts vendored

@ -1,9 +1,11 @@
import { LocalizerType } from '../types/Util';
import LokiMessageAPI from '../../js/modules/loki_message_api';
import LokiPublicChatFactoryAPI from '../../js/modules/loki_public_chat_api';
import { LokiMessageAPIInterface } from '../../js/modules/loki_message_api';
import { LibsignalProtocol } from '../../libtextsecure/libsignal-protocol';
import { SignalInterface } from '../../js/modules/signal';
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.
@ -48,8 +50,8 @@ declare global {
log: any;
lokiFeatureFlags: any;
lokiFileServerAPI: LokiFileServerInstance;
lokiMessageAPI: LokiMessageAPI;
lokiPublicChatAPI: LokiPublicChatFactoryAPI;
lokiMessageAPI: LokiMessageInterface;
lokiPublicChatAPI: LokiPublicChatFactoryInterface;
mnemonic: any;
onLogin: any;
passwordUtil: any;
@ -71,6 +73,7 @@ declare global {
toggleMenuBar: any;
toggleSpellCheck: any;
toggleTheme: any;
tokenlessFileServerAdnAPI: LokiAppDotNetServerInterface;
userConfig: any;
versionInfo: any;
}

Loading…
Cancel
Save