Merge remote-tracking branch 'upstream/clearnet' into scoring-system

pull/1624/head
Audric Ackermann 4 years ago
commit a2ee3ac98f
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -14,6 +14,39 @@ Please search for any [existing issues](https://github.com/oxen-io/session-deskt
Build instructions can be found in [BUILDING.md](BUILDING.md). Build instructions can be found in [BUILDING.md](BUILDING.md).
## Verifing signatures
Get Kee's key and import it:
```
wget https://raw.githubusercontent.com/oxen-io/oxen-core/master/utils/gpg_keys/KeeJef.asc
gpg --import KeeJef.asc
```
Get the signed hash for this release, the SESSION_VERSION needs to be updated for the release you want to verify
```
export SESSION_VERSION=1.6.1
wget https://github.com/oxen-io/session-desktop/releases/download/v$SESSION_VERSION/signatures.asc
```
Verify the signature of the hashes of the files
```
gpg --verify signatures.asc 2>&1 |grep "Good signature from"
```
The command above should print "`Good signature from "Kee Jefferys...`"
If it does, the hashes are valid but we still have to make the sure the signed hashes matches the downloaded files.
Make sure the two commands below returns the same hash.
If they do, files are valid
```
sha256sum session-desktop-linux-amd64-$SESSION_VERSION.deb
grep .deb signatures.asc
```
## Debian repository ## Debian repository
Please visit https://deb.oxen.io/<br/> Please visit https://deb.oxen.io/<br/>

@ -10,7 +10,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; <meta http-equiv="Content-Security-Policy" content="default-src 'none';
child-src 'self'; child-src 'self';
connect-src 'self' https: wss:; connect-src 'self' https: wss: blob:;
font-src 'self'; font-src 'self';
form-action 'self'; form-action 'self';
frame-src 'none'; frame-src 'none';
@ -167,4 +167,4 @@
<script type='text/javascript' src='js/background.js'></script> <script type='text/javascript' src='js/background.js'></script>
</body> </body>
</html> </html>

@ -425,7 +425,7 @@
avatarPath, avatarPath,
onOk: async (newName, avatar) => { onOk: async (newName, avatar) => {
let newAvatarPath = ''; let newAvatarPath = '';
let url = null; let fileUrl = null;
let profileKey = null; let profileKey = null;
if (avatar) { if (avatar) {
const data = await readFile({ file: avatar }); const data = await readFile({ file: avatar });
@ -463,22 +463,18 @@
profileKey profileKey
); );
const avatarPointer = await libsession.Utils.AttachmentUtils.uploadAvatarV1({ const avatarPointer = await window.Fsv2.uploadFileToFsV2(encryptedData);
...dataResized,
data: encryptedData,
size: encryptedData.byteLength,
});
({ url } = avatarPointer); ({ fileUrl } = avatarPointer);
storage.put('profileKey', profileKey); storage.put('profileKey', profileKey);
conversation.set('avatarPointer', url); conversation.set('avatarPointer', fileUrl);
const upgraded = await Signal.Migrations.processNewAttachment({ const upgraded = await Signal.Migrations.processNewAttachment({
isRaw: true, isRaw: true,
data: data.data, data: data.data,
url, url: fileUrl,
}); });
newAvatarPath = upgraded.path; newAvatarPath = upgraded.path;
// Replace our temporary image with the attachment pointer from the server: // Replace our temporary image with the attachment pointer from the server:
@ -513,14 +509,6 @@
// so we could disable this here // so we could disable this here
// or least it enable for the quickest response // or least it enable for the quickest response
window.lokiPublicChatAPI.setProfileName(newName); window.lokiPublicChatAPI.setProfileName(newName);
if (avatar) {
window
.getConversationController()
.getConversations()
.filter(convo => convo.isPublic())
.forEach(convo => convo.trigger('ourAvatarChanged', { url, profileKey }));
}
}, },
}); });
} }

@ -13,4 +13,5 @@ export interface LibTextsecureCryptoInterface {
theirDigest: ArrayBuffer theirDigest: ArrayBuffer
): Promise<ArrayBuffer>; ): Promise<ArrayBuffer>;
decryptProfile(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer>; decryptProfile(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer>;
encryptProfile(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer>;
} }

@ -2,7 +2,7 @@
"name": "session-desktop", "name": "session-desktop",
"productName": "Session", "productName": "Session",
"description": "Private messaging from your desktop", "description": "Private messaging from your desktop",
"version": "1.6.2", "version": "1.6.4",
"license": "GPL-3.0", "license": "GPL-3.0",
"author": { "author": {
"name": "Loki Project", "name": "Loki Project",

@ -398,6 +398,7 @@ window.addEventListener('contextmenu', e => {
}); });
window.NewReceiver = require('./ts/receiver/receiver'); window.NewReceiver = require('./ts/receiver/receiver');
window.Fsv2 = require('./ts/fileserver/FileServerApiV2');
window.DataMessageReceiver = require('./ts/receiver/dataMessage'); window.DataMessageReceiver = require('./ts/receiver/dataMessage');
window.NewSnodeAPI = require('./ts/session/snode_api/SNodeAPI'); window.NewSnodeAPI = require('./ts/session/snode_api/SNodeAPI');
window.SnodePool = require('./ts/session/snode_api/snodePool'); window.SnodePool = require('./ts/session/snode_api/snodePool');

@ -91,7 +91,11 @@ export const Avatar = (props: Props) => {
// contentType is not important // contentType is not important
const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', ''); const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', '');
const handleImageError = () => { const handleImageError = () => {
window?.log?.warn('Avatar: Image failed to load; failing over to placeholder', urlToLoad); window.log.warn(
'Avatar: Image failed to load; failing over to placeholder',
urlToLoad,
avatarPath
);
setImageBroken(true); setImageBroken(true);
}; };

@ -229,7 +229,7 @@ class ConversationListItem extends React.PureComponent<Props> {
const displayName = isMe ? i18n('noteToSelf') : profileName; const displayName = isMe ? i18n('noteToSelf') : profileName;
let shouldShowPubkey = false; let shouldShowPubkey = false;
if (!name || name.length === 0) { if ((!name || name.length === 0) && (!displayName || displayName.length === 0)) {
shouldShowPubkey = true; shouldShowPubkey = true;
} }

@ -7,11 +7,13 @@ import { ConversationController } from '../../session/conversations';
import { UserUtils } from '../../session/utils'; import { UserUtils } from '../../session/utils';
import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils'; import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils';
import { DAYS, MINUTES } from '../../session/utils/Number'; import { DAYS, MINUTES } from '../../session/utils/Number';
import { import {
createOrUpdateItem,
generateAttachmentKeyIfEmpty, generateAttachmentKeyIfEmpty,
getItemById, getItemById,
hasSyncedInitialConfigurationItem, hasSyncedInitialConfigurationItem,
removeItemById, lastAvatarUploadTimestamp,
} from '../../data/data'; } from '../../data/data';
import { OnionPaths } from '../../session/onions'; import { OnionPaths } from '../../session/onions';
import { getMessageQueue } from '../../session/sending'; import { getMessageQueue } from '../../session/sending';
@ -29,11 +31,18 @@ import { useInterval } from '../../hooks/useInterval';
import { clearSearch } from '../../state/ducks/search'; import { clearSearch } from '../../state/ducks/search';
import { showLeftPaneSection } from '../../state/ducks/section'; import { showLeftPaneSection } from '../../state/ducks/section';
import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachmentsManager'; import {
cleanUpOldDecryptedMedias,
getDecryptedMediaUrl,
} from '../../session/crypto/DecryptedAttachmentsManager';
import { OpenGroupManagerV2 } from '../../opengroup/opengroupV2/OpenGroupManagerV2'; import { OpenGroupManagerV2 } from '../../opengroup/opengroupV2/OpenGroupManagerV2';
import { loadDefaultRooms } from '../../opengroup/opengroupV2/ApiUtil'; import { loadDefaultRooms } from '../../opengroup/opengroupV2/ApiUtil';
import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool'; import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool';
import { SwarmPolling } from '../../session/snode_api/swarmPolling'; import { SwarmPolling } from '../../session/snode_api/swarmPolling';
import { IMAGE_JPEG } from '../../types/MIME';
import { FSv2 } from '../../fileserver';
import { stringToArrayBuffer } from '../../session/utils/String';
import { debounce } from 'underscore';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports // tslint:disable-next-line: no-import-side-effect no-submodule-imports
export enum SectionType { export enum SectionType {
@ -45,6 +54,20 @@ export enum SectionType {
Moon, Moon,
} }
const showUnstableAttachmentsDialogIfNeeded = async () => {
const alreadyShown = (await getItemById('showUnstableAttachmentsDialog'))?.value;
if (!alreadyShown) {
window.confirmationDialog({
title: 'File server update',
message:
"We're upgrading the way files are stored. File transfer may be unstable for the next 24-48 hours.",
});
await createOrUpdateItem({ id: 'showUnstableAttachmentsDialog', value: true });
}
};
const Section = (props: { type: SectionType; avatarPath?: string }) => { const Section = (props: { type: SectionType; avatarPath?: string }) => {
const ourNumber = useSelector(getOurNumber); const ourNumber = useSelector(getOurNumber);
const unreadMessageCount = useSelector(getUnreadMessageCount); const unreadMessageCount = useSelector(getUnreadMessageCount);
@ -149,6 +172,81 @@ const triggerSyncIfNeeded = async () => {
} }
}; };
const triggerAvatarReUploadIfNeeded = async () => {
const lastTimeStampAvatarUpload = (await getItemById(lastAvatarUploadTimestamp))?.value || 0;
if (Date.now() - lastTimeStampAvatarUpload > DAYS * 14) {
window.log.info('Reuploading avatar...');
// reupload the avatar
const ourConvo = ConversationController.getInstance().get(UserUtils.getOurPubKeyStrFromCache());
if (!ourConvo) {
window.log.warn('ourConvo not found... This is not a valid case');
return;
}
const profileKey = window.textsecure.storage.get('profileKey');
if (!profileKey) {
window.log.warn('our profileKey not found... This is not a valid case');
return;
}
const currentAttachmentPath = ourConvo.getAvatarPath();
if (!currentAttachmentPath) {
window.log.warn('No attachment currently set for our convo.. Nothing to do.');
return;
}
const decryptedAvatarUrl = await getDecryptedMediaUrl(currentAttachmentPath, IMAGE_JPEG);
if (!decryptedAvatarUrl) {
window.log.warn('Could not decrypt avatar stored locally..');
return;
}
const response = await fetch(decryptedAvatarUrl);
const blob = await response.blob();
const decryptedAvatarData = await blob.arrayBuffer();
if (!decryptedAvatarData?.byteLength) {
window.log.warn('Could not read blob of avatar locally..');
return;
}
const encryptedData = await window.textsecure.crypto.encryptProfile(
decryptedAvatarData,
profileKey
);
const avatarPointer = await FSv2.uploadFileToFsV2(encryptedData);
let fileUrl;
if (!avatarPointer) {
window.log.warn('failed to reupload avatar to fsv2');
return;
}
({ fileUrl } = avatarPointer);
ourConvo.set('avatarPointer', fileUrl);
// this encrypts and save the new avatar and returns a new attachment path
const upgraded = await window.Signal.Migrations.processNewAttachment({
isRaw: true,
data: decryptedAvatarData,
url: fileUrl,
});
const newAvatarPath = upgraded.path;
// Replace our temporary image with the attachment pointer from the server:
ourConvo.set('avatar', null);
const existingHash = ourConvo.get('avatarHash');
const displayName = ourConvo.get('profileName');
// this commits already
await ourConvo.setLokiProfile({ avatar: newAvatarPath, displayName, avatarHash: existingHash });
const newTimestampReupload = Date.now();
await createOrUpdateItem({ id: lastAvatarUploadTimestamp, value: newTimestampReupload });
window.log.info(
`Reuploading avatar finished at ${newTimestampReupload}, newAttachmentPointer ${fileUrl}`
);
}
};
/** /**
* This function is called only once: on app startup with a logged in user * This function is called only once: on app startup with a logged in user
*/ */
@ -158,6 +256,7 @@ const doAppStartUp = (dispatch: Dispatch<any>) => {
void OnionPaths.buildNewOnionPathsOneAtATime(); void OnionPaths.buildNewOnionPathsOneAtATime();
} }
void showUnstableAttachmentsDialogIfNeeded();
// init the messageQueue. In the constructor, we add all not send messages // init the messageQueue. In the constructor, we add all not send messages
// this call does nothing except calling the constructor, which will continue sending message in the pipeline // this call does nothing except calling the constructor, which will continue sending message in the pipeline
void getMessageQueue().processAllPending(); void getMessageQueue().processAllPending();
@ -178,6 +277,8 @@ const doAppStartUp = (dispatch: Dispatch<any>) => {
void loadDefaultRooms(); void loadDefaultRooms();
debounce(triggerAvatarReUploadIfNeeded, 200);
// TODO: Investigate the case where we reconnect // TODO: Investigate the case where we reconnect
const ourKey = UserUtils.getOurPubKeyStrFromCache(); const ourKey = UserUtils.getOurPubKeyStrFromCache();
SwarmPolling.getInstance().addPubkey(ourKey); SwarmPolling.getInstance().addPubkey(ourKey);
@ -228,6 +329,11 @@ export const ActionsPanel = () => {
void forceRefreshRandomSnodePool(); void forceRefreshRandomSnodePool();
}, DAYS * 1); }, DAYS * 1);
useInterval(() => {
// this won't be run every days, but if the app stays open for more than 10 days
void triggerAvatarReUploadIfNeeded();
}, DAYS * 1);
return ( return (
<div className="module-left-pane__sections-container"> <div className="module-left-pane__sections-container">
<Section type={SectionType.Profile} avatarPath={ourPrimaryConversation.avatarPath} /> <Section type={SectionType.Profile} avatarPath={ourPrimaryConversation.avatarPath} />

@ -60,6 +60,7 @@ export type ServerToken = {
}; };
export const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem'; export const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem';
export const lastAvatarUploadTimestamp = 'lastAvatarUploadTimestamp';
const channelsToMake = { const channelsToMake = {
shutdown, shutdown,

@ -4,9 +4,14 @@ import { parseStatusCodeFromOnionRequest } from '../opengroup/opengroupV2/OpenGr
import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String'; import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String';
// tslint:disable-next-line: no-http-string // tslint:disable-next-line: no-http-string
export const fileServerV2URL = 'http://88.99.175.227'; export const oldFileServerV2URL = 'http://88.99.175.227';
export const fileServerV2PubKey = export const oldFileServerV2PubKey =
'7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69'; '7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69';
// tslint:disable-next-line: no-http-string
export const fileServerV2URL = 'http://filev2.getsession.org';
export const fileServerV2PubKey =
'da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59';
export type FileServerV2Request = { export type FileServerV2Request = {
method: 'GET' | 'POST' | 'DELETE' | 'PUT'; method: 'GET' | 'POST' | 'DELETE' | 'PUT';
@ -14,6 +19,7 @@ export type FileServerV2Request = {
// queryParams are used for post or get, but not the same way // queryParams are used for post or get, but not the same way
queryParams?: Record<string, any>; queryParams?: Record<string, any>;
headers?: Record<string, string>; headers?: Record<string, string>;
isOldV2server?: boolean; // to remove in a few days
}; };
const FILES_ENDPOINT = 'files'; const FILES_ENDPOINT = 'files';
@ -67,7 +73,8 @@ export const uploadFileToFsV2 = async (
* @returns the data as an Uint8Array or null * @returns the data as an Uint8Array or null
*/ */
export const downloadFileFromFSv2 = async ( export const downloadFileFromFSv2 = async (
fileIdOrCompleteUrl: string fileIdOrCompleteUrl: string,
isOldV2server: boolean
): Promise<ArrayBuffer | null> => { ): Promise<ArrayBuffer | null> => {
let fileId = fileIdOrCompleteUrl; let fileId = fileIdOrCompleteUrl;
if (!fileIdOrCompleteUrl) { if (!fileIdOrCompleteUrl) {
@ -75,13 +82,19 @@ export const downloadFileFromFSv2 = async (
return null; return null;
} }
const completeUrlPrefix = `${fileServerV2URL}/${FILES_ENDPOINT}/`; const oldCompleteUrlPrefix = `${oldFileServerV2URL}/${FILES_ENDPOINT}/`;
if (fileIdOrCompleteUrl.startsWith(completeUrlPrefix)) { const newCompleteUrlPrefix = `${fileServerV2URL}/${FILES_ENDPOINT}/`;
fileId = fileId.substr(completeUrlPrefix.length);
if (fileIdOrCompleteUrl.startsWith(newCompleteUrlPrefix)) {
fileId = fileId.substr(newCompleteUrlPrefix.length);
} else if (fileIdOrCompleteUrl.startsWith(oldCompleteUrlPrefix)) {
fileId = fileId.substr(oldCompleteUrlPrefix.length);
} }
const request: FileServerV2Request = { const request: FileServerV2Request = {
method: 'GET', method: 'GET',
endpoint: `${FILES_ENDPOINT}/${fileId}`, endpoint: `${FILES_ENDPOINT}/${fileId}`,
isOldV2server,
}; };
const result = await sendApiV2Request(request); const result = await sendApiV2Request(request);
@ -119,7 +132,11 @@ export const buildUrl = (request: FileServerV2Request | OpenGroupV2Request): URL
if (isOpenGroupV2Request(request)) { if (isOpenGroupV2Request(request)) {
rawURL = `${request.server}/${request.endpoint}`; rawURL = `${request.server}/${request.endpoint}`;
} else { } else {
rawURL = `${fileServerV2URL}/${request.endpoint}`; if (request.isOldV2server) {
rawURL = `${oldFileServerV2URL}/${request.endpoint}`;
} else {
rawURL = `${fileServerV2URL}/${request.endpoint}`;
}
} }
if (request.method === 'GET') { if (request.method === 'GET') {

@ -179,7 +179,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
//start right away the function is called, and wait 1sec before calling it again //start right away the function is called, and wait 1sec before calling it again
this.markRead = _.debounce(this.markReadBouncy, 1000, { leading: true }); this.markRead = _.debounce(this.markReadBouncy, 1000, { leading: true });
// Listening for out-of-band data updates // Listening for out-of-band data updates
this.on('ourAvatarChanged', avatar => this.updateAvatarOnPublicChat(avatar));
this.typingRefreshTimer = null; this.typingRefreshTimer = null;
this.typingPauseTimer = null; this.typingPauseTimer = null;
@ -783,23 +782,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return null; return null;
} }
public async updateAvatarOnPublicChat({ url, profileKey }: any) {
if (!this.isPublic()) {
return;
}
// Always share avatars on PublicChat
if (profileKey && typeof profileKey !== 'string') {
// eslint-disable-next-line no-param-reassign
// tslint:disable-next-line: no-parameter-reassignment
profileKey = fromArrayBufferToBase64(profileKey);
}
const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(this.get('server'));
if (!serverAPI) {
return;
}
await serverAPI.setAvatar(url, profileKey);
}
public async bouncyUpdateLastMessage() { public async bouncyUpdateLastMessage() {
if (!this.id) { if (!this.id) {
return; return;
@ -1227,11 +1209,11 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
// Not sure if we care about updating the database // Not sure if we care about updating the database
} }
public async setProfileAvatar(avatar: any, avatarHash?: string) { public async setProfileAvatar(avatar: null | { path: string }, avatarHash?: string) {
const profileAvatar = this.get('avatar'); const profileAvatar = this.get('avatar');
const existingHash = this.get('avatarHash'); const existingHash = this.get('avatarHash');
let shouldCommit = false; let shouldCommit = false;
if (profileAvatar !== avatar) { if (!_.isEqual(profileAvatar, avatar)) {
this.set({ avatar }); this.set({ avatar });
shouldCommit = true; shouldCommit = true;
} }

@ -58,7 +58,7 @@ const getDestinationPubKey = async (
} }
} else { } else {
// this is a fileServer call // this is a fileServer call
return FSv2.fileServerV2PubKey; return request.isOldV2server ? FSv2.oldFileServerV2PubKey : FSv2.fileServerV2PubKey;
} }
}; };

@ -23,18 +23,19 @@ export async function downloadAttachment(attachment: any) {
serverUrl serverUrl
); );
// is it an attachment hosted on the file server v2 ? // is it an attachment hosted on the file server v2 ?
const defaultFsOldV2 = _.startsWith(serverUrl, FSv2.oldFileServerV2URL);
const defaultFsV2 = _.startsWith(serverUrl, FSv2.fileServerV2URL); const defaultFsV2 = _.startsWith(serverUrl, FSv2.fileServerV2URL);
let res: ArrayBuffer | null = null; let res: ArrayBuffer | null = null;
if (defaultFsV2) { if (defaultFsV2 || defaultFsOldV2) {
let attachmentId = attachment.id; let attachmentId = attachment.id;
if (!attachmentId) { if (!attachmentId) {
// try to get the fileId from the end of the URL // try to get the fileId from the end of the URL
attachmentId = attachment.url; attachmentId = attachment.url;
} }
window?.log?.info('Download v2 file server attachment'); window?.log?.info('Download v2 file server attachment');
res = await FSv2.downloadFileFromFSv2(attachmentId); res = await FSv2.downloadFileFromFSv2(attachmentId, defaultFsOldV2);
} else { } else {
window.log.warn( window.log.warn(
'downloadAttachment attachment is neither opengroup attachment nor fsv2... Dropping it' 'downloadAttachment attachment is neither opengroup attachment nor fsv2... Dropping it'

@ -47,7 +47,7 @@ describe('Attachment', () => {
contentType: MIME.VIDEO_QUICKTIME, contentType: MIME.VIDEO_QUICKTIME,
}; };
const actual = Attachment.getSuggestedFilename({ attachment }); const actual = Attachment.getSuggestedFilename({ attachment });
const expected = 'session-attachment.mov'; const expected = 'funny-cat.mov';
assert.strictEqual(actual, expected); assert.strictEqual(actual, expected);
}); });
it('should generate a filename without timestamp but with an index', () => { it('should generate a filename without timestamp but with an index', () => {
@ -60,7 +60,7 @@ describe('Attachment', () => {
attachment, attachment,
index: 3, index: 3,
}); });
const expected = 'session-attachment_003.mov'; const expected = 'funny-cat.mov';
assert.strictEqual(actual, expected); assert.strictEqual(actual, expected);
}); });
it('should generate a filename with an extension if contentType is not setup', () => { it('should generate a filename with an extension if contentType is not setup', () => {
@ -73,7 +73,7 @@ describe('Attachment', () => {
attachment, attachment,
index: 3, index: 3,
}); });
const expected = 'session-attachment_003.ini'; const expected = 'funny-cat.ini';
assert.strictEqual(actual, expected); assert.strictEqual(actual, expected);
}); });
@ -87,7 +87,7 @@ describe('Attachment', () => {
attachment, attachment,
index: 3, index: 3,
}); });
const expected = 'session-attachment_003.txt'; const expected = 'funny-cat.txt';
assert.strictEqual(actual, expected); assert.strictEqual(actual, expected);
}); });
it('should generate a filename with an extension if contentType is json', () => { it('should generate a filename with an extension if contentType is json', () => {
@ -100,7 +100,7 @@ describe('Attachment', () => {
attachment, attachment,
index: 3, index: 3,
}); });
const expected = 'session-attachment_003.json'; const expected = 'funny-cat.json';
assert.strictEqual(actual, expected); assert.strictEqual(actual, expected);
}); });
}); });
@ -116,14 +116,14 @@ describe('Attachment', () => {
attachment, attachment,
timestamp, timestamp,
}); });
const expected = 'session-attachment-2000-01-01-000000.mov'; const expected = 'funny-cat.mov';
assert.strictEqual(actual, expected); assert.strictEqual(actual, expected);
}); });
}); });
context('for attachment with index', () => { context('for attachment with index', () => {
it('should generate a filename based on timestamp', () => { it('should generate a filename based on timestamp if filename is not set', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'funny-cat.mov', fileName: '',
url: 'funny-cat.mov', url: 'funny-cat.mov',
contentType: MIME.VIDEO_QUICKTIME, contentType: MIME.VIDEO_QUICKTIME,
}; };
@ -136,6 +136,22 @@ describe('Attachment', () => {
const expected = 'session-attachment-1970-01-01-000000_003.mov'; const expected = 'session-attachment-1970-01-01-000000_003.mov';
assert.strictEqual(actual, expected); assert.strictEqual(actual, expected);
}); });
it('should generate a filename based on filename if present', () => {
const attachment: Attachment.AttachmentType = {
fileName: 'funny-cat.mov',
url: 'funny-cat.mov',
contentType: MIME.VIDEO_QUICKTIME,
};
const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000);
const actual = Attachment.getSuggestedFilename({
attachment,
timestamp,
index: 3,
});
const expected = 'funny-cat.mov';
assert.strictEqual(actual, expected);
});
}); });
}); });

@ -334,6 +334,9 @@ export const getSuggestedFilename = ({
timestamp?: number | Date; timestamp?: number | Date;
index?: number; index?: number;
}): string => { }): string => {
if (attachment.fileName?.length > 3) {
return attachment.fileName;
}
const prefix = 'session-attachment'; const prefix = 'session-attachment';
const suffix = timestamp ? moment(timestamp).format('-YYYY-MM-DD-HHmmss') : ''; const suffix = timestamp ? moment(timestamp).format('-YYYY-MM-DD-HHmmss') : '';
const fileType = getFileExtension(attachment); const fileType = getFileExtension(attachment);

Loading…
Cancel
Save