Merge branch 'clearnet' into scoring-system

This commit is contained in:
Audric Ackermann 2021-05-19 10:43:49 +10:00
commit c2298c4c30
No known key found for this signature in database
GPG key ID: 999F434D76324AD4
16 changed files with 469 additions and 83 deletions

View file

@ -5,9 +5,6 @@ on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:

View file

@ -1387,6 +1387,38 @@
"description": "Button action that the user can click to edit a group name (open)",
"androidKey": "conversation__menu_edit_group"
},
"closedGroupInviteFailTitle": {
"message": "Group Invitation Failed",
"description": "Title for the dialog of a failed group invite modal"
},
"closedGroupInviteFailTitlePlural": {
"message": "Group Invitations Failed",
"description": "Title for the dialog of a failed group invite modal plural"
},
"closedGroupInviteFailMessage": {
"message": "Unable to successfully invite a group member",
"description": "Message for the dialog of a failed group invite modal"
},
"closedGroupInviteFailMessagePlural": {
"message": "Unable to successfully invite all group members",
"description": "Message for the dialog of a failed group invite modal plural"
},
"closedGroupInviteOkText": {
"message": "Retry invitations",
"description": "Text for the OK button of a closed group invite failure"
},
"closedGroupInviteSuccessTitlePlural": {
"message": "Group Invitations Completed",
"description": "The title for the modal dialog when a closed group invite retry succeeds"
},
"closedGroupInviteSuccessTitle": {
"message": "Group Invitation Succeeded",
"description": "The title for the modal dialog when a closed group invite retry succeeds"
},
"closedGroupInviteSuccessMessage": {
"message": "Successfully invited closed group members",
"description": "The message for the modal dialog when a closed group invite retry succeeds"
},
"editGroupName": {
"message": "Edit group name",
"description": "Button action that the user can click to edit a group name (closed)"

View file

@ -1,10 +1,10 @@
{
"copyErrorAndQuit": {
"message": "Copy error and quit",
"message": "エラーの文章をコピーして終了",
"description": "Shown in the top-level error popup, allowing user to copy the error text and close the app"
},
"databaseError": {
"message": "Database Error",
"message": "データベースエラー",
"description": "Shown in a popup if the database cannot start up properly"
},
"mainMenuFile": {
@ -270,7 +270,7 @@
"description": "Shown in toast when user attempts to send .exe file, for example"
},
"stagedPreviewThumbnail": {
"message": "Draft thumbnail link preview for $domain$",
"message": "$domain$ のサムネイルリンクプレビュー(下書き)",
"description": "Shown while Session Desktop is fetching metadata for a url in composition area",
"placeholders": {
"path": {
@ -280,7 +280,7 @@
}
},
"previewThumbnail": {
"message": "Thumbnail link preview for $domain$",
"message": "$domain$ のサムネイルリンクプレビュー",
"description": "Shown while Session Desktop is fetching metadata for a url in composition area",
"placeholders": {
"path": {
@ -290,7 +290,7 @@
}
},
"stagedImageAttachment": {
"message": "Draft image attachment: $path$",
"message": "添付画像(下書き): $path$",
"description": "Alt text for staged attachments",
"placeholders": {
"path": {
@ -300,15 +300,15 @@
}
},
"oneNonImageAtATimeToast": {
"message": "When including a non-image attachment, the limit is one attachment per message.",
"message": "画像でないファイルを添付する場合、メッセージに添付できるファイルは1つのみです。",
"description": "An error popup when the user has attempted to add an attachment"
},
"cannotMixImageAndNonImageAttachments": {
"message": "You cannot mix non-image and image attachments in one message.",
"message": "画像ファイルと画像でないファイルを合わせてメッセージに添付できません。",
"description": "An error popup when the user has attempted to add an attachment"
},
"maximumAttachments": {
"message": "You cannot add any more attachments to this message.",
"message": "メッセージに添付できるファイルの数の上限に達しています。",
"description": "An error popup when the user has attempted to add an attachment"
},
"fileSizeWarning": {
@ -446,7 +446,7 @@
"description": "Shown in a quotation of a message containing a photo if no text was originally provided with that image"
},
"cannotUpdate": {
"message": "Cannot Update",
"message": "更新できませんでした",
"description": "Shown as the title of our update error dialogs on windows"
},
"ok": {
@ -466,7 +466,7 @@
"description": ""
},
"deleteWarning": {
"message": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.",
"message": "「削除」を選択したら自分の端末からのみメッセージが永久削除されます。よろしいですか?",
"description": ""
},
"deleteThisMessage": {
@ -562,7 +562,7 @@
"description": "Explain the purpose of the notification settings"
},
"disableNotifications": {
"message": "通知をミュート",
"message": "通知を無効にする",
"description": "Label for disabling notifications"
},
"nameAndMessage": {
@ -985,7 +985,7 @@
"message": "削除"
},
"invalidSessionId": {
"message": "Session ID が不正です"
"message": "Session ID が正しくありません"
},
"emptyGroupNameError": {
"message": "グループ名を入力してください"
@ -1024,7 +1024,7 @@
"message": "アカウントを復元する"
},
"newSession": {
"message": "新しい Session"
"message": "新しいセッション"
},
"searchFor...": {
"message": "会話やメッセージ、連絡先を検索します。"
@ -1088,5 +1088,142 @@
},
"noBlockedContacts": {
"message": "ブロックしている連絡先はありません"
},
"add": {
"message": "追加",
"androidKey": "fragment_add_public_chat_add_button_title_1"
},
"addContact": {
"message": "連絡先を追加する"
},
"autoUpdateSettingTitle": {
"message": "自動更新"
},
"autoUpdateSettingDescription": {
"message": "起動時に自動的に更新の有無を確認する"
},
"ByUsingThisService...": {
"comment": "By the way the terms of use and privacy policy are presented and the implied consent, there is a risk the terms of use and privacy policy may not legally apply to users in Japan. https://topcourt-law.com/terms_of_service/how-to-get-consent",
"message": "本サービスを利用する場合、<a href=\"https://getsession.org/legal/#tos\">利用規約</a>および<a href=\"https://getsession.org/privacy-policy/\" target=\"_blank\">プライバシーポリシー</a>に同意するものとします"
},
"copySessionID": {
"message": "Session ID をコピー",
"description": "Copy to clipboard session ID",
"androidKey": "activity_conversation_menu_copy_session_id"
},
"createAccount": {
"message": "アカウントを作成"
},
"deleted": {
"message": "メッセージが削除されました",
"description": "Toast validation when a single or several messages were deleted"
},
"deleteForEveryone": {
"message": "全員から削除",
"description": "Menu item for deleting messages, title case."
},
"deleteMessageForEveryone": {
"message": "全員からメッセージを削除",
"description": "Menu item for deleting messages, title case."
},
"deleteMessagesForEveryone": {
"comment": "Same content as in \"deleteMessageForEveryone\".",
"message": "全員からメッセージを削除",
"description": "Menu item for deleting messages, title case."
},
"deleteMultiplePublicWarning": {
"comment": "Same content as in \"deletePublicWarning\".",
"message": "「削除」を選択したら公開グループからメッセージが永久削除されます。よろしいですか?"
},
"deleteMultipleWarning": {
"comment": "Same content as in \"deleteWarning\".",
"message": "「削除」を選択したら自分の端末からのみメッセージが永久削除されます。よろしいですか?"
},
"deletePublicConversationConfirmation": {
"message": "「削除」を選択したら自分の端末からのみ公開グループのメッセージが永久削除されます。よろしいですか?",
"description": "Confirmation dialog text that asks the user if they really wish to delete the open group messages locally. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone."
},
"deletePublicWarning": {
"comment": "Does deletion apply to just the open group or does it extend to all members' devices?",
"message": "「削除」を選択したら公開グループからメッセージが永久削除されます。よろしいですか?"
},
"editProfileModalTitle": {
"message": "プロフィール",
"description": "Title for the Edit Profile modal"
},
"enterOptionalPassword": {
"message": "パスワード入力(任意)"
},
"hideMenuBarDescription": {
"message": "メニューバーの表示を切り替える",
"description": "Label text for menu bar visibility setting"
},
"hideMenuBarTitle": {
"message": "メニューバーを隠す",
"description": "Label text for menu bar visibility setting"
},
"mediaPermissionsTitle": {
"message": "マイクとカメラ"
},
"password": {
"message": "パスワード",
"description": "Placeholder for password input"
},
"passwordCharacterError": {
"message": "パスワードには英数字と記号の文字しか使えません",
"description": "Error string shown to the user when password contains an invalid character"
},
"passwordLengthError": {
"message": "パスワードの長さを6文字から64文字にしてください",
"description": "Error string shown to the user when password doesn't meet length criteria"
},
"passwordsDoNotMatch": {
"message": "パスワードが一致しません"
},
"passwordTypeError": {
"message": "パスワードは文字列でなければいけません",
"description": "Error string shown to the user when password is not a string"
},
"passwordViewTitle": {
"message": "パスワード入力",
"description": "The title shown when user needs to type in a password to unlock the messenger"
},
"readReceiptSettingDescription": {
"message": "メッセージが読まれた状態の表示と送信をする(すべてのセッションに既読通知を有功にする)。",
"description": "Description of the read receipts setting"
},
"replyingToMessage": {
"message": "このメッセージへの返信:"
},
"selectMessage": {
"message": "メッセージを選択",
"description": "Button action that the user can click to select the message"
},
"setAccountPasswordDescription": {
"message": "Session のスクリーンのロック解除にパスワードを要求する。スクリーンロック中にもメッセージの通知が受信できます。Session の通知設定でメッセージの通知に表示される情報を調整できます。",
"description": "Description for set account password setting view"
},
"setAccountPasswordTitle": {
"message": "アカウントのパスワード設定",
"description": "Prompt for user to set account password in settings view"
},
"signIn": {
"message": "ログイン"
},
"spellCheckTitle": {
"message": "スペルチェック",
"description": "Description of the media permission description"
},
"typingIndicatorsSettingDescription": {
"message": "メッセージが入力中である状態の表示と送信をする(すべてのセッションに適用される)。",
"description": "Description of the typing indicators setting"
},
"unpairDeviceWarning": {
"message": "この端末をリンク解除してもよろしいですか?",
"description": "Warning for device unlinking in settings view"
},
"zoomFactorSettingTitle": {
"message": "ズーム",
"description": "Title of the Zoom Factor setting"
}
}
}

View file

@ -383,15 +383,8 @@
};
window.showNicknameDialog = params => {
const options = {
title: params.title || undefined,
message: params.message,
placeholder: params.placeholder,
convoId: params.convoId || undefined,
};
if (appView) {
appView.showNicknameDialog(options);
appView.showNicknameDialog(params);
}
};

View file

@ -33,32 +33,39 @@
unregisterEvents() {
document.removeEventListener('keyup', this.props.onClickClose, false);
if (this.confirmView && this.confirmView.el) {
window.ReactDOM.unmountComponentAtNode(this.confirmView.el);
}
this.$('.session-confirm-wrapper').remove();
},
render() {
this.$('.session-confirm-wrapper').remove();
this.registerEvents();
this.confirmView = new Whisper.ReactWrapperView({
className: 'loki-dialog modal session-confirm-wrapper',
Component: window.Signal.Components.SessionConfirm,
props: this.props,
});
this.registerEvents();
this.$el.prepend(this.confirmView.el);
},
ok() {
this.$('.session-confirm-wrapper').remove();
this.unregisterEvents();
this.$('.session-confirm-wrapper').remove();
if (this.props.resolve) {
this.props.resolve();
}
},
cancel() {
this.$('.session-confirm-wrapper').remove();
this.unregisterEvents();
this.$('.session-confirm-wrapper').remove();
if (this.props.reject) {
this.props.reject();
}

View file

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

View file

@ -1,26 +1,20 @@
import React, { useState } from 'react';
import { ConversationController } from '../../session/conversations/ConversationController';
import { SessionModal } from './SessionModal';
import { SessionButton, SessionButtonColor } from './SessionButton';
import { SessionButton } from './SessionButton';
import { DefaultTheme, withTheme } from 'styled-components';
import _ from 'lodash';
type Props = {
message: string;
title: string;
placeholder?: string;
onOk?: any;
onClose?: any;
onClickOk: any;
onClickClose: any;
hideCancel: boolean;
okTheme: SessionButtonColor;
theme: DefaultTheme;
convoId?: string;
convoId: string;
};
const SessionNicknameInner = (props: Props) => {
const { title = '', message, onClickOk, onClickClose, convoId, placeholder } = props;
const showHeader = true;
const { onClickOk, onClickClose, convoId, theme } = props;
const [nickname, setNickname] = useState('');
/**
@ -30,9 +24,10 @@ const SessionNicknameInner = (props: Props) => {
const onNicknameInput = async (event: any) => {
if (event.key === 'Enter') {
await saveNickname();
} else {
const currentNicknameEntered = event.target.value;
setNickname(currentNicknameEntered);
}
const currentNicknameEntered = event.target.value;
setNickname(currentNicknameEntered);
};
/**
@ -49,24 +44,24 @@ const SessionNicknameInner = (props: Props) => {
return (
<SessionModal
title={title}
title={window.i18n('changeNickname')}
onClose={onClickClose}
showExitIcon={false}
showHeader={showHeader}
theme={props.theme}
showHeader={true}
theme={theme}
>
{!showHeader && <div className="spacer-lg" />}
<div className="session-modal__centered">
<span className="subtle">{message}</span>
<span className="subtle">{window.i18n('changeNicknameMessage')}</span>
<div className="spacer-lg" />
</div>
<input
type="nickname"
id="nickname-modal-input"
placeholder={placeholder}
onKeyUp={onNicknameInput}
placeholder={window.i18n('nicknamePlaceholder')}
onKeyUp={e => {
void onNicknameInput(_.cloneDeep(e));
}}
/>
<div className="session-modal__button-group">

View file

@ -1312,9 +1312,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
);
}
window.showNicknameDialog({
title: window.i18n('changeNickname') || 'Change Nickname',
message: window.i18n('changeNicknameMessage') || '',
placeholder: window.i18n('nicknamePlaceholder') || '',
convoId: this.id,
});
}

View file

@ -34,6 +34,7 @@ import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgo
import { queueAllCachedFromSource } from './receiver';
import { actions as conversationActions } from '../state/ducks/conversations';
import { SwarmPolling } from '../session/snode_api/swarmPolling';
import { MessageModel } from '../models/message';
export const distributingClosedGroupEncryptionKeyPairs = new Map<string, ECKeyPair>();
@ -885,7 +886,122 @@ export async function createClosedGroup(groupName: string, members: Array<string
convo.updateLastMessage();
// Send a closed group update message to all members individually
const promises = listOfMembers.map(async m => {
const allInvitesSent = await sendToGroupMembers(
listOfMembers,
groupPublicKey,
groupName,
admins,
encryptionKeyPair,
dbMessage
);
if (allInvitesSent) {
const newHexKeypair = encryptionKeyPair.toHexKeyPair();
const isHexKeyPairSaved = await isKeyPairAlreadySaved(groupPublicKey, newHexKeypair);
if (!isHexKeyPairSaved) {
// tslint:disable-next-line: no-non-null-assertion
await addClosedGroupEncryptionKeyPair(groupPublicKey, encryptionKeyPair.toHexKeyPair());
} else {
window.log.info('Dropping already saved keypair for group', groupPublicKey);
}
// Subscribe to this group id
SwarmPolling.getInstance().addGroupId(new PubKey(groupPublicKey));
}
await forceSyncConfigurationNowIfNeeded();
window.inboxStore?.dispatch(conversationActions.openConversationExternal(groupPublicKey));
}
/**
* Sends a group invite message to each member of the group.
* @returns Array of promises for group invite messages sent to group members
*/
async function sendToGroupMembers(
listOfMembers: Array<string>,
groupPublicKey: string,
groupName: string,
admins: Array<string>,
encryptionKeyPair: ECKeyPair,
dbMessage: MessageModel,
isRetry: boolean = false
): Promise<any> {
const promises = createInvitePromises(
listOfMembers,
groupPublicKey,
groupName,
admins,
encryptionKeyPair,
dbMessage
);
window.log.info(`Creating a new group and an encryptionKeyPair for group ${groupPublicKey}`);
// evaluating if all invites sent, if failed give the option to retry failed invites via modal dialog
const inviteResults = await Promise.all(promises);
const allInvitesSent = _.every(inviteResults, Boolean);
if (allInvitesSent) {
if (isRetry) {
const invitesTitle =
inviteResults.length > 1
? window.i18n('closedGroupInviteSuccessTitlePlural')
: window.i18n('closedGroupInviteSuccessTitle');
window.confirmationDialog({
title: invitesTitle,
message: window.i18n('closedGroupInviteSuccessMessage'),
});
}
return allInvitesSent;
} else {
// Confirmation dialog that recursively calls sendToGroupMembers on resolve
window.confirmationDialog({
title:
inviteResults.length > 1
? window.i18n('closedGroupInviteFailTitlePlural')
: window.i18n('closedGroupInviteFailTitle'),
message:
inviteResults.length > 1
? window.i18n('closedGroupInviteFailMessagePlural')
: window.i18n('closedGroupInviteFailMessage'),
okText: window.i18n('closedGroupInviteOkText'),
resolve: async () => {
const membersToResend: Array<string> = new Array<string>();
inviteResults.forEach((result, index) => {
const member = listOfMembers[index];
// group invite must always contain the admin member.
if (result !== true || admins.includes(member)) {
membersToResend.push(member);
}
});
if (membersToResend.length > 0) {
const isRetrySend = true;
await sendToGroupMembers(
membersToResend,
groupPublicKey,
groupName,
admins,
encryptionKeyPair,
dbMessage,
isRetrySend
);
}
},
});
}
return allInvitesSent;
}
function createInvitePromises(
listOfMembers: Array<string>,
groupPublicKey: string,
groupName: string,
admins: Array<string>,
encryptionKeyPair: ECKeyPair,
dbMessage: MessageModel
) {
return listOfMembers.map(async m => {
const messageParams: ClosedGroupNewMessageParams = {
groupId: groupPublicKey,
name: groupName,
@ -897,19 +1013,6 @@ export async function createClosedGroup(groupName: string, members: Array<string
expireTimer: 0,
};
const message = new ClosedGroupNewMessage(messageParams);
return getMessageQueue().sendToPubKey(PubKey.cast(m), message);
return getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(m), message);
});
window.log.info(`Creating a new group and an encryptionKeyPair for group ${groupPublicKey}`);
// tslint:disable-next-line: no-non-null-assertion
await addClosedGroupEncryptionKeyPair(groupPublicKey, encryptionKeyPair.toHexKeyPair());
// Subscribe to this group id
SwarmPolling.getInstance().addGroupId(new PubKey(groupPublicKey));
await Promise.all(promises);
await forceSyncConfigurationNowIfNeeded();
window.inboxStore?.dispatch(conversationActions.openConversationExternal(groupPublicKey));
}

View file

@ -12,14 +12,15 @@ const PADDING_BYTE = 0x00;
*/
export function removeMessagePadding(paddedData: ArrayBuffer): ArrayBuffer {
const paddedPlaintext = new Uint8Array(paddedData);
window.log.info('Removing message padding...');
window?.log.info('Removing message padding...');
for (let i = paddedPlaintext.length - 1; i >= 0; i -= 1) {
if (paddedPlaintext[i] === 0x80) {
const plaintext = new Uint8Array(i);
plaintext.set(paddedPlaintext.subarray(0, i));
return plaintext.buffer;
} else if (paddedPlaintext[i] !== PADDING_BYTE) {
throw new Error('Invalid padding');
window?.log.warn('got a message without padding... Letting it through for now');
return paddedPlaintext;
}
}
@ -31,7 +32,7 @@ export function removeMessagePadding(paddedData: ArrayBuffer): ArrayBuffer {
* @param messageBuffer The buffer to add padding to.
*/
export function addMessagePadding(messageBuffer: Uint8Array): Uint8Array {
window.log?.info('Adding message padding...');
window?.log?.info('Adding message padding...');
const plaintext = new Uint8Array(getPaddedMessageLength(messageBuffer.byteLength + 1) - 1);
plaintext.set(new Uint8Array(messageBuffer));
@ -58,25 +59,19 @@ export function getUnpaddedAttachment(
data: ArrayBuffer,
unpaddedExpectedSize: number
): ArrayBuffer | null {
window.log?.info('Removing attachment padding...');
window?.log?.info('Removing attachment padding...');
// to have a padding we must have a strictly longer length expected
if (data.byteLength <= unpaddedExpectedSize) {
return null;
}
const dataUint = new Uint8Array(data);
for (let i = unpaddedExpectedSize; i < data.byteLength; i++) {
if (dataUint[i] !== PADDING_BYTE) {
return null;
}
}
// we now consider that anything coming after the expected size is padding, no matter what there is there
return data.slice(0, unpaddedExpectedSize);
}
export function addAttachmentPadding(data: ArrayBuffer): ArrayBuffer {
const originalUInt = new Uint8Array(data);
window.log.info('Adding attchment padding...');
window?.log.info('Adding attchment padding...');
const paddedSize = Math.max(
541,

View file

@ -1,5 +1,5 @@
import { PendingMessageCache } from './PendingMessageCache';
import { JobQueue, UserUtils } from '../utils';
import { JobQueue, MessageUtils, UserUtils } from '../utils';
import { PubKey, RawMessage } from '../types';
import { MessageSender } from '.';
import { ClosedGroupMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMessage';
@ -146,6 +146,29 @@ export class MessageQueue {
await this.process(PubKey.cast(ourPubKey), message, sentCb);
}
/**
* Sends a message that awaits until the message is completed sending
* @param user user pub key to send to
* @param message Message to be sent
*/
public async sendToPubKeyNonDurably(
user: PubKey,
message: ClosedGroupNewMessage
): Promise<boolean> {
let rawMessage;
try {
rawMessage = await MessageUtils.toRawMessage(user, message);
const wrappedEnvelope = await MessageSender.send(rawMessage);
await MessageSentHandler.handleMessageSentSuccess(rawMessage, wrappedEnvelope);
return !!wrappedEnvelope;
} catch (error) {
if (rawMessage) {
await MessageSentHandler.handleMessageSentFailure(rawMessage, error);
}
return false;
}
}
public async processPending(device: PubKey) {
const messages = await this.pendingMessageCache.getForDevice(device);

View file

@ -0,0 +1,92 @@
// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression
import chai from 'chai';
import * as sinon from 'sinon';
import _ from 'lodash';
import { describe } from 'mocha';
import chaiAsPromised from 'chai-as-promised';
import {
addAttachmentPadding,
addMessagePadding,
getUnpaddedAttachment,
removeMessagePadding,
} from '../../../../session/crypto/BufferPadding';
chai.use(chaiAsPromised as any);
chai.should();
const { expect } = chai;
// tslint:disable-next-line: max-func-body-length
describe('Padding', () => {
describe('Attachment padding', () => {
it('add padding', () => {
const bufferIn = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
const paddedBuffer = addAttachmentPadding(bufferIn);
expect(paddedBuffer.byteLength).to.equal(541);
expect(new Uint8Array(paddedBuffer.slice(0, bufferIn.length))).to.equalBytes(bufferIn);
// this makes sure that the padding is just the 0 bytes
expect(new Uint8Array(paddedBuffer.slice(bufferIn.length))).to.equalBytes(
new Uint8Array(541 - bufferIn.length)
);
});
it('remove padding', () => {
// padding can be anything after the expected size
const expectedSize = 10;
const paddedBuffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 5]);
const paddingRemoveBuffer = getUnpaddedAttachment(paddedBuffer, expectedSize);
expect(paddingRemoveBuffer?.byteLength).to.equal(expectedSize);
expect(paddingRemoveBuffer).to.equalBytes(paddedBuffer.slice(0, expectedSize));
});
});
describe('Message padding', () => {
it('add padding', () => {
const bufferIn = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
const paddedMessage = addMessagePadding(bufferIn);
expect(paddedMessage.byteLength).to.equal(159);
// for message padding, we have [bufferIn, 0x80, 0x00, 0x00, 0x00, ...]
expect(new Uint8Array(paddedMessage.slice(0, bufferIn.length))).to.equalBytes(bufferIn);
expect(paddedMessage[bufferIn.length]).to.equal(0x80);
// this makes sure that the padding is just the 0 bytes
expect(new Uint8Array(paddedMessage.slice(bufferIn.length + 1))).to.equalBytes(
new Uint8Array(159 - bufferIn.length - 1)
);
});
it('remove padding', () => {
const expectedSize = 10;
const paddedBuffer = new Uint8Array([
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
128,
0,
0,
0,
0,
0,
0,
0,
0,
]);
const unpaddedMessage = removeMessagePadding(paddedBuffer);
// for message padding, we have [paddedBuffer, 0x80, 0x00, 0x00, 0x00, ...]
expect(unpaddedMessage?.byteLength).to.equal(expectedSize);
expect(new Uint8Array(unpaddedMessage)).to.equalBytes(paddedBuffer.slice(0, expectedSize));
});
});
});

View file

@ -27,6 +27,15 @@ describe('Attachment', () => {
};
assert.strictEqual(Attachment.getFileExtension(input), 'mov');
});
it('should return file extension for application files', () => {
const input: Attachment.AttachmentType = {
fileName: 'funny-cat.odt',
url: 'funny-cat.odt',
contentType: MIME.ODT,
};
assert.strictEqual(Attachment.getFileExtension(input), 'odt');
});
});
describe('getSuggestedFilename', () => {

View file

@ -361,7 +361,12 @@ export const getSuggestedFilenameSending = ({
export const getFileExtension = (attachment: AttachmentType): string | undefined => {
// we override textplain to the extension of the file
if (!attachment.contentType || attachment.contentType === 'text/plain') {
// for contenttype starting with application, the mimetype is probably wrong so just use the extension of the file instead
if (
!attachment.contentType ||
attachment.contentType === 'text/plain' ||
attachment.contentType.startsWith('application')
) {
if (attachment.fileName?.length) {
const dotLastIndex = attachment.fileName.lastIndexOf('.');
if (dotLastIndex !== -1) {

View file

@ -14,6 +14,7 @@ export const IMAGE_WEBP = 'image/webp' as MIMEType;
export const IMAGE_PNG = 'image/png' as MIMEType;
export const VIDEO_MP4 = 'video/mp4' as MIMEType;
export const VIDEO_QUICKTIME = 'video/quicktime' as MIMEType;
export const ODT = 'application/vnd.oasis.opendocument.spreadsheet' as MIMEType;
export const isJPEG = (value: MIMEType): boolean => value === 'image/jpeg';
export const isImage = (value: MIMEType): boolean =>

2
ts/window.d.ts vendored
View file

@ -68,7 +68,7 @@ declare global {
setPassword: any;
setSettingValue: any;
showEditProfileDialog: any;
showNicknameDialog: any;
showNicknameDialog: (options: { convoId: string }) => void;
showResetSessionIdDialog: any;
storage: any;
textsecure: LibTextsecure;