mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
Merge remote-tracking branch 'upstream/clearnet' into scoring-system
This commit is contained in:
commit
a2ee3ac98f
33
README.md
33
README.md
|
@ -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).
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
Please visit https://deb.oxen.io/<br/>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none';
|
||||
child-src 'self';
|
||||
connect-src 'self' https: wss:;
|
||||
connect-src 'self' https: wss: blob:;
|
||||
font-src 'self';
|
||||
form-action 'self';
|
||||
frame-src 'none';
|
||||
|
@ -167,4 +167,4 @@
|
|||
<script type='text/javascript' src='js/background.js'></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -425,7 +425,7 @@
|
|||
avatarPath,
|
||||
onOk: async (newName, avatar) => {
|
||||
let newAvatarPath = '';
|
||||
let url = null;
|
||||
let fileUrl = null;
|
||||
let profileKey = null;
|
||||
if (avatar) {
|
||||
const data = await readFile({ file: avatar });
|
||||
|
@ -463,22 +463,18 @@
|
|||
profileKey
|
||||
);
|
||||
|
||||
const avatarPointer = await libsession.Utils.AttachmentUtils.uploadAvatarV1({
|
||||
...dataResized,
|
||||
data: encryptedData,
|
||||
size: encryptedData.byteLength,
|
||||
});
|
||||
const avatarPointer = await window.Fsv2.uploadFileToFsV2(encryptedData);
|
||||
|
||||
({ url } = avatarPointer);
|
||||
({ fileUrl } = avatarPointer);
|
||||
|
||||
storage.put('profileKey', profileKey);
|
||||
|
||||
conversation.set('avatarPointer', url);
|
||||
conversation.set('avatarPointer', fileUrl);
|
||||
|
||||
const upgraded = await Signal.Migrations.processNewAttachment({
|
||||
isRaw: true,
|
||||
data: data.data,
|
||||
url,
|
||||
url: fileUrl,
|
||||
});
|
||||
newAvatarPath = upgraded.path;
|
||||
// Replace our temporary image with the attachment pointer from the server:
|
||||
|
@ -513,14 +509,6 @@
|
|||
// so we could disable this here
|
||||
// or least it enable for the quickest response
|
||||
window.lokiPublicChatAPI.setProfileName(newName);
|
||||
|
||||
if (avatar) {
|
||||
window
|
||||
.getConversationController()
|
||||
.getConversations()
|
||||
.filter(convo => convo.isPublic())
|
||||
.forEach(convo => convo.trigger('ourAvatarChanged', { url, profileKey }));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
1
libtextsecure/crypto.d.ts
vendored
1
libtextsecure/crypto.d.ts
vendored
|
@ -13,4 +13,5 @@ export interface LibTextsecureCryptoInterface {
|
|||
theirDigest: ArrayBuffer
|
||||
): Promise<ArrayBuffer>;
|
||||
decryptProfile(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer>;
|
||||
encryptProfile(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer>;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "session-desktop",
|
||||
"productName": "Session",
|
||||
"description": "Private messaging from your desktop",
|
||||
"version": "1.6.2",
|
||||
"version": "1.6.4",
|
||||
"license": "GPL-3.0",
|
||||
"author": {
|
||||
"name": "Loki Project",
|
||||
|
|
|
@ -398,6 +398,7 @@ window.addEventListener('contextmenu', e => {
|
|||
});
|
||||
|
||||
window.NewReceiver = require('./ts/receiver/receiver');
|
||||
window.Fsv2 = require('./ts/fileserver/FileServerApiV2');
|
||||
window.DataMessageReceiver = require('./ts/receiver/dataMessage');
|
||||
window.NewSnodeAPI = require('./ts/session/snode_api/SNodeAPI');
|
||||
window.SnodePool = require('./ts/session/snode_api/snodePool');
|
||||
|
|
|
@ -91,7 +91,11 @@ export const Avatar = (props: Props) => {
|
|||
// contentType is not important
|
||||
const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', '');
|
||||
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);
|
||||
};
|
||||
|
||||
|
|
|
@ -229,7 +229,7 @@ class ConversationListItem extends React.PureComponent<Props> {
|
|||
const displayName = isMe ? i18n('noteToSelf') : profileName;
|
||||
|
||||
let shouldShowPubkey = false;
|
||||
if (!name || name.length === 0) {
|
||||
if ((!name || name.length === 0) && (!displayName || displayName.length === 0)) {
|
||||
shouldShowPubkey = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,11 +7,13 @@ import { ConversationController } from '../../session/conversations';
|
|||
import { UserUtils } from '../../session/utils';
|
||||
import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils';
|
||||
import { DAYS, MINUTES } from '../../session/utils/Number';
|
||||
|
||||
import {
|
||||
createOrUpdateItem,
|
||||
generateAttachmentKeyIfEmpty,
|
||||
getItemById,
|
||||
hasSyncedInitialConfigurationItem,
|
||||
removeItemById,
|
||||
lastAvatarUploadTimestamp,
|
||||
} from '../../data/data';
|
||||
import { OnionPaths } from '../../session/onions';
|
||||
import { getMessageQueue } from '../../session/sending';
|
||||
|
@ -29,11 +31,18 @@ import { useInterval } from '../../hooks/useInterval';
|
|||
import { clearSearch } from '../../state/ducks/search';
|
||||
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 { loadDefaultRooms } from '../../opengroup/opengroupV2/ApiUtil';
|
||||
import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool';
|
||||
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
|
||||
|
||||
export enum SectionType {
|
||||
|
@ -45,6 +54,20 @@ export enum SectionType {
|
|||
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 ourNumber = useSelector(getOurNumber);
|
||||
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
|
||||
*/
|
||||
|
@ -158,6 +256,7 @@ const doAppStartUp = (dispatch: Dispatch<any>) => {
|
|||
void OnionPaths.buildNewOnionPathsOneAtATime();
|
||||
}
|
||||
|
||||
void showUnstableAttachmentsDialogIfNeeded();
|
||||
// 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
|
||||
void getMessageQueue().processAllPending();
|
||||
|
@ -178,6 +277,8 @@ const doAppStartUp = (dispatch: Dispatch<any>) => {
|
|||
|
||||
void loadDefaultRooms();
|
||||
|
||||
debounce(triggerAvatarReUploadIfNeeded, 200);
|
||||
|
||||
// TODO: Investigate the case where we reconnect
|
||||
const ourKey = UserUtils.getOurPubKeyStrFromCache();
|
||||
SwarmPolling.getInstance().addPubkey(ourKey);
|
||||
|
@ -228,6 +329,11 @@ export const ActionsPanel = () => {
|
|||
void forceRefreshRandomSnodePool();
|
||||
}, 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 (
|
||||
<div className="module-left-pane__sections-container">
|
||||
<Section type={SectionType.Profile} avatarPath={ourPrimaryConversation.avatarPath} />
|
||||
|
|
|
@ -60,6 +60,7 @@ export type ServerToken = {
|
|||
};
|
||||
|
||||
export const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem';
|
||||
export const lastAvatarUploadTimestamp = 'lastAvatarUploadTimestamp';
|
||||
|
||||
const channelsToMake = {
|
||||
shutdown,
|
||||
|
|
|
@ -4,9 +4,14 @@ import { parseStatusCodeFromOnionRequest } from '../opengroup/opengroupV2/OpenGr
|
|||
import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String';
|
||||
|
||||
// tslint:disable-next-line: no-http-string
|
||||
export const fileServerV2URL = 'http://88.99.175.227';
|
||||
export const fileServerV2PubKey =
|
||||
export const oldFileServerV2URL = 'http://88.99.175.227';
|
||||
export const oldFileServerV2PubKey =
|
||||
'7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69';
|
||||
// tslint:disable-next-line: no-http-string
|
||||
export const fileServerV2URL = 'http://filev2.getsession.org';
|
||||
|
||||
export const fileServerV2PubKey =
|
||||
'da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59';
|
||||
|
||||
export type FileServerV2Request = {
|
||||
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?: Record<string, any>;
|
||||
headers?: Record<string, string>;
|
||||
isOldV2server?: boolean; // to remove in a few days
|
||||
};
|
||||
|
||||
const FILES_ENDPOINT = 'files';
|
||||
|
@ -67,7 +73,8 @@ export const uploadFileToFsV2 = async (
|
|||
* @returns the data as an Uint8Array or null
|
||||
*/
|
||||
export const downloadFileFromFSv2 = async (
|
||||
fileIdOrCompleteUrl: string
|
||||
fileIdOrCompleteUrl: string,
|
||||
isOldV2server: boolean
|
||||
): Promise<ArrayBuffer | null> => {
|
||||
let fileId = fileIdOrCompleteUrl;
|
||||
if (!fileIdOrCompleteUrl) {
|
||||
|
@ -75,13 +82,19 @@ export const downloadFileFromFSv2 = async (
|
|||
return null;
|
||||
}
|
||||
|
||||
const completeUrlPrefix = `${fileServerV2URL}/${FILES_ENDPOINT}/`;
|
||||
if (fileIdOrCompleteUrl.startsWith(completeUrlPrefix)) {
|
||||
fileId = fileId.substr(completeUrlPrefix.length);
|
||||
const oldCompleteUrlPrefix = `${oldFileServerV2URL}/${FILES_ENDPOINT}/`;
|
||||
const newCompleteUrlPrefix = `${fileServerV2URL}/${FILES_ENDPOINT}/`;
|
||||
|
||||
if (fileIdOrCompleteUrl.startsWith(newCompleteUrlPrefix)) {
|
||||
fileId = fileId.substr(newCompleteUrlPrefix.length);
|
||||
} else if (fileIdOrCompleteUrl.startsWith(oldCompleteUrlPrefix)) {
|
||||
fileId = fileId.substr(oldCompleteUrlPrefix.length);
|
||||
}
|
||||
|
||||
const request: FileServerV2Request = {
|
||||
method: 'GET',
|
||||
endpoint: `${FILES_ENDPOINT}/${fileId}`,
|
||||
isOldV2server,
|
||||
};
|
||||
|
||||
const result = await sendApiV2Request(request);
|
||||
|
@ -119,7 +132,11 @@ export const buildUrl = (request: FileServerV2Request | OpenGroupV2Request): URL
|
|||
if (isOpenGroupV2Request(request)) {
|
||||
rawURL = `${request.server}/${request.endpoint}`;
|
||||
} else {
|
||||
rawURL = `${fileServerV2URL}/${request.endpoint}`;
|
||||
if (request.isOldV2server) {
|
||||
rawURL = `${oldFileServerV2URL}/${request.endpoint}`;
|
||||
} else {
|
||||
rawURL = `${fileServerV2URL}/${request.endpoint}`;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
this.markRead = _.debounce(this.markReadBouncy, 1000, { leading: true });
|
||||
// Listening for out-of-band data updates
|
||||
this.on('ourAvatarChanged', avatar => this.updateAvatarOnPublicChat(avatar));
|
||||
|
||||
this.typingRefreshTimer = null;
|
||||
this.typingPauseTimer = null;
|
||||
|
@ -783,23 +782,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
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() {
|
||||
if (!this.id) {
|
||||
return;
|
||||
|
@ -1227,11 +1209,11 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
// 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 existingHash = this.get('avatarHash');
|
||||
let shouldCommit = false;
|
||||
if (profileAvatar !== avatar) {
|
||||
if (!_.isEqual(profileAvatar, avatar)) {
|
||||
this.set({ avatar });
|
||||
shouldCommit = true;
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ const getDestinationPubKey = async (
|
|||
}
|
||||
} else {
|
||||
// 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
|
||||
);
|
||||
// is it an attachment hosted on the file server v2 ?
|
||||
const defaultFsOldV2 = _.startsWith(serverUrl, FSv2.oldFileServerV2URL);
|
||||
const defaultFsV2 = _.startsWith(serverUrl, FSv2.fileServerV2URL);
|
||||
|
||||
let res: ArrayBuffer | null = null;
|
||||
|
||||
if (defaultFsV2) {
|
||||
if (defaultFsV2 || defaultFsOldV2) {
|
||||
let attachmentId = attachment.id;
|
||||
if (!attachmentId) {
|
||||
// try to get the fileId from the end of the URL
|
||||
attachmentId = attachment.url;
|
||||
}
|
||||
window?.log?.info('Download v2 file server attachment');
|
||||
res = await FSv2.downloadFileFromFSv2(attachmentId);
|
||||
res = await FSv2.downloadFileFromFSv2(attachmentId, defaultFsOldV2);
|
||||
} else {
|
||||
window.log.warn(
|
||||
'downloadAttachment attachment is neither opengroup attachment nor fsv2... Dropping it'
|
||||
|
|
|
@ -47,7 +47,7 @@ describe('Attachment', () => {
|
|||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
};
|
||||
const actual = Attachment.getSuggestedFilename({ attachment });
|
||||
const expected = 'session-attachment.mov';
|
||||
const expected = 'funny-cat.mov';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
it('should generate a filename without timestamp but with an index', () => {
|
||||
|
@ -60,7 +60,7 @@ describe('Attachment', () => {
|
|||
attachment,
|
||||
index: 3,
|
||||
});
|
||||
const expected = 'session-attachment_003.mov';
|
||||
const expected = 'funny-cat.mov';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
it('should generate a filename with an extension if contentType is not setup', () => {
|
||||
|
@ -73,7 +73,7 @@ describe('Attachment', () => {
|
|||
attachment,
|
||||
index: 3,
|
||||
});
|
||||
const expected = 'session-attachment_003.ini';
|
||||
const expected = 'funny-cat.ini';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
|
||||
|
@ -87,7 +87,7 @@ describe('Attachment', () => {
|
|||
attachment,
|
||||
index: 3,
|
||||
});
|
||||
const expected = 'session-attachment_003.txt';
|
||||
const expected = 'funny-cat.txt';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
it('should generate a filename with an extension if contentType is json', () => {
|
||||
|
@ -100,7 +100,7 @@ describe('Attachment', () => {
|
|||
attachment,
|
||||
index: 3,
|
||||
});
|
||||
const expected = 'session-attachment_003.json';
|
||||
const expected = 'funny-cat.json';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
@ -116,12 +116,28 @@ describe('Attachment', () => {
|
|||
attachment,
|
||||
timestamp,
|
||||
});
|
||||
const expected = 'session-attachment-2000-01-01-000000.mov';
|
||||
const expected = 'funny-cat.mov';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
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 = {
|
||||
fileName: '',
|
||||
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 = 'session-attachment-1970-01-01-000000_003.mov';
|
||||
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',
|
||||
|
@ -133,7 +149,7 @@ describe('Attachment', () => {
|
|||
timestamp,
|
||||
index: 3,
|
||||
});
|
||||
const expected = 'session-attachment-1970-01-01-000000_003.mov';
|
||||
const expected = 'funny-cat.mov';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -334,6 +334,9 @@ export const getSuggestedFilename = ({
|
|||
timestamp?: number | Date;
|
||||
index?: number;
|
||||
}): string => {
|
||||
if (attachment.fileName?.length > 3) {
|
||||
return attachment.fileName;
|
||||
}
|
||||
const prefix = 'session-attachment';
|
||||
const suffix = timestamp ? moment(timestamp).format('-YYYY-MM-DD-HHmmss') : '';
|
||||
const fileType = getFileExtension(attachment);
|
||||
|
|
Loading…
Reference in a new issue