Merge remote-tracking branch 'upstream/clearnet' into move-models-to-ts-2

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

@ -916,13 +916,13 @@
({ identifier, pubKey, timestamp, serverId, serverTimestamp }) => {
try {
const conversation = window.getConversationController().get(pubKey);
conversation.onPublicMessageSent(
conversation.onPublicMessageSent({
identifier,
pubKey,
timestamp,
serverId,
serverTimestamp
);
serverTimestamp,
});
} catch (e) {
window.log.error('Error setting public on message');
}

@ -173,14 +173,18 @@ export function removeAllSessions(): Promise<void>;
export function getConversationCount(): Promise<number>;
export function saveConversation(data: ConversationType): Promise<void>;
export function saveConversations(data: Array<ConversationType>): Promise<void>;
export function updateConversation(data: ConversationType): Promise<void>;
export function removeConversation(id: string): Promise<void>;
export function updateConversation(
id: string,
data: ConversationType,
{ Conversation }
): Promise<void>;
export function removeConversation(id: string, { Conversation }): Promise<void>;
export function getAllConversations({
ConversationCollection,
}: {
ConversationCollection: any;
}): Promise<Array<ConversationCollection>>;
}): Promise<ConversationCollection>;
export function getAllConversationIds(): Promise<Array<string>>;
export function getPublicConversationsByServer(
@ -197,7 +201,7 @@ export function getPublicServerTokenByServerUrl(
export function getAllGroupsInvolvingId(
id: string,
{ ConversationCollection }: { ConversationCollection: any }
): Promise<Array<ConversationCollection>>;
): Promise<ConversationCollection>;
// Returns conversation row
// TODO: Make strict return types for search
@ -237,6 +241,10 @@ export function getUnreadByConversation(
conversationId: string,
{ MessageCollection }?: any
): Promise<any>;
export function getUnreadCountByConversation(
conversationId: string,
{ MessageCollection }?: any
): Promise<any>;
export function removeAllMessagesInConversation(
conversationId: string,
{ MessageCollection }?: any

@ -35,7 +35,7 @@
"test-electron": "yarn grunt test",
"test-integration": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --full-trace --timeout 10000 ts/test/session/integration/integration_itest.js",
"test-node": "mocha --recursive --exit --timeout 10000 test/app test/modules \"./ts/test/**/*_test.js\" libloki/test/node ",
"test-audric": "mocha --recursive --exit --timeout 10000 ts/test/session/unit/receiving/",
"test-audric": "mocha --recursive --exit --timeout 10000 ts/test/session/unit/",
"eslint": "eslint --cache .",
"eslint-fix": "eslint --fix .",
"eslint-full": "eslint .",

@ -34,12 +34,6 @@ window.Signal = {
},
};
window.CONSTANTS = {
MAX_LOGIN_TRIES: 3,
MAX_PASSWORD_LENGTH: 64,
MAX_USERNAME_LENGTH: 20,
};
window.Signal.Logs = require('./js/modules/logs');
window.resetDatabase = () => {

@ -86,9 +86,6 @@ window.isBeforeVersion = (toCheck, baseVersion) => {
// eslint-disable-next-line func-names
window.CONSTANTS = new (function() {
this.MAX_LOGIN_TRIES = 3;
this.MAX_PASSWORD_LENGTH = 64;
this.MAX_USERNAME_LENGTH = 20;
this.MAX_GROUP_NAME_LENGTH = 64;
this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer');
this.MAX_LINKED_DEVICES = 1;
@ -501,9 +498,10 @@ const {
window.BlockedNumberController = BlockedNumberController;
window.deleteAccount = async reason => {
try {
window.log.info('Deleting everything!');
const syncedMessageSent = async () => {
window.log.info(
'configuration message sent successfully. Deleting everything'
);
await window.Signal.Logs.deleteAll();
await window.Signal.Data.removeAll();
await window.Signal.Data.close();
@ -511,11 +509,24 @@ window.deleteAccount = async reason => {
await window.Signal.Data.removeOtherData();
// 'unlink' => toast will be shown on app restart
window.localStorage.setItem('restart-reason', reason);
};
try {
window.log.info('DeleteAccount => Sending a last SyncConfiguration');
// be sure to wait for the message being effectively sent. Otherwise we won't be able to encrypt it for our devices !
await window.libsession.Utils.SyncUtils.forceSyncConfigurationNowIfNeeded(
true
);
await syncedMessageSent();
} catch (error) {
window.log.error(
'Something went wrong deleting all data:',
error && error.stack ? error.stack : error
);
try {
await syncedMessageSent();
} catch (e) {
window.log.error(e);
}
}
window.restart();
};

@ -33,9 +33,17 @@ message TypingMessage {
message Content {
optional DataMessage dataMessage = 1;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
optional DataMessage dataMessage = 1;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
optional ConfigurationMessage configurationMessage = 7;
}
message KeyPair {
// @required
required bytes publicKey = 1;
// @required
required bytes privateKey = 2;
}
@ -144,6 +152,7 @@ message DataMessage {
optional string profilePicture = 2;
}
message ClosedGroupControlMessage {
enum Type {
@ -156,12 +165,7 @@ message DataMessage {
MEMBER_LEFT = 7;
}
message KeyPair {
// @required
required bytes publicKey = 1;
// @required
required bytes privateKey = 2;
}
message KeyPairWrapper {
// @required
@ -186,20 +190,34 @@ message DataMessage {
optional string serverName = 3;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
optional Quote quote = 8;
repeated Contact contact = 9;
repeated Preview preview = 10;
optional LokiProfile profile = 101;
optional GroupInvitation groupInvitation = 102;
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
optional Quote quote = 8;
repeated Contact contact = 9;
repeated Preview preview = 10;
optional LokiProfile profile = 101;
optional GroupInvitation groupInvitation = 102;
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105;
}
message ConfigurationMessage {
message ClosedGroup {
optional bytes publicKey = 1;
optional string name = 2;
optional KeyPair encryptionKeyPair = 3;
repeated bytes members = 4;
repeated bytes admins = 5;
}
repeated ClosedGroup closedGroups = 1;
repeated string openGroups = 2;
}
message ReceiptMessage {

@ -19,6 +19,7 @@ import { SessionModal } from './session/SessionModal';
import { PillDivider } from './session/PillDivider';
import { ToastUtils, UserUtils } from '../session/utils';
import { DefaultTheme } from 'styled-components';
import { MAX_USERNAME_LENGTH } from './session/RegistrationTabs';
interface Props {
i18n: any;
@ -213,7 +214,7 @@ export class EditProfileDialog extends React.Component<Props, State> {
value={this.state.profileName}
placeholder={placeholderText}
onChange={this.onNameEdited}
maxLength={window.CONSTANTS.MAX_USERNAME_LENGTH}
maxLength={MAX_USERNAME_LENGTH}
tabIndex={0}
required={true}
aria-required={true}
@ -292,10 +293,7 @@ export class EditProfileDialog extends React.Component<Props, State> {
private onClickOK() {
const newName = this.state.profileName.trim();
if (
newName.length === 0 ||
newName.length > window.CONSTANTS.MAX_USERNAME_LENGTH
) {
if (newName.length === 0 || newName.length > MAX_USERNAME_LENGTH) {
return;
}

@ -14,6 +14,8 @@ import { getFocusedSection } from '../../state/selectors/section';
import { getTheme } from '../../state/selectors/theme';
import { getOurNumber } from '../../state/selectors/user';
import { UserUtils } from '../../session/utils';
import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils';
import { DAYS } from '../../session/utils/Number';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
export enum SectionType {
@ -36,6 +38,8 @@ interface Props {
}
class ActionsPanelPrivate extends React.Component<Props> {
private syncInterval: NodeJS.Timeout | null = null;
constructor(props: Props) {
super(props);
@ -57,6 +61,20 @@ class ActionsPanelPrivate extends React.Component<Props> {
// remove existing prekeys, sign prekeys and sessions
void window.getAccountManager().clearSessionsAndPreKeys();
// trigger a sync message if needed for our other devices
void syncConfigurationIfNeeded();
this.syncInterval = global.setInterval(() => {
void syncConfigurationIfNeeded();
}, DAYS * 2);
}
public componentWillUnmount() {
if (this.syncInterval) {
clearInterval(this.syncInterval);
this.syncInterval = null;
}
}
public Section = ({

@ -10,11 +10,13 @@ import {
import { trigger } from '../../shims/events';
import { SessionHtmlRenderer } from './SessionHTMLRenderer';
import { SessionIdEditable } from './SessionIdEditable';
import { SessionSpinner } from './SessionSpinner';
import { StringUtils, ToastUtils } from '../../session/utils';
import { lightTheme } from '../../state/ducks/SessionTheme';
import { ConversationController } from '../../session/conversations';
import { PasswordUtil } from '../../util';
import { removeAll } from '../../../js/modules/data';
export const MAX_USERNAME_LENGTH = 20;
enum SignInMode {
Default,
@ -440,7 +442,7 @@ export class RegistrationTabs extends React.Component<any, State> {
type="text"
placeholder={window.i18n('enterDisplayName')}
value={this.state.displayName}
maxLength={window.CONSTANTS.MAX_USERNAME_LENGTH}
maxLength={MAX_USERNAME_LENGTH}
onValueChanged={(val: string) => {
this.onDisplayNameChanged(val);
}}
@ -628,7 +630,7 @@ export class RegistrationTabs extends React.Component<any, State> {
}
private async resetRegistration() {
await window.Signal.Data.removeAll();
await removeAll();
await window.storage.fetch();
ConversationController.getInstance().reset();
await ConversationController.getInstance().load();

@ -8,6 +8,7 @@ import { toast } from 'react-toastify';
import { SessionToast, SessionToastType } from './SessionToast';
import { SessionIconType } from './icon';
import { DefaultTheme, withTheme } from 'styled-components';
import { getPasswordHash } from '../../../js/modules/data';
export enum PasswordAction {
Set = 'set',
Change = 'change',
@ -116,7 +117,7 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
public async validatePasswordHash(password: string | null) {
// Check if the password matches the hash we have stored
const hash = await window.Signal.Data.getPasswordHash();
const hash = await getPasswordHash();
if (hash && !PasswordUtil.matchesHash(password, hash)) {
return false;
}

@ -16,6 +16,8 @@ interface State {
clearDataView: boolean;
}
export const MAX_LOGIN_TRIES = 3;
class SessionPasswordPromptInner extends React.PureComponent<
{ theme: DefaultTheme },
State
@ -44,8 +46,7 @@ class SessionPasswordPromptInner extends React.PureComponent<
}
public render() {
const showResetElements =
this.state.errorCount >= window.CONSTANTS.MAX_LOGIN_TRIES;
const showResetElements = this.state.errorCount >= MAX_LOGIN_TRIES;
const wrapperClass = this.state.clearDataView
? 'clear-data-wrapper'
@ -163,8 +164,7 @@ class SessionPasswordPromptInner extends React.PureComponent<
}
private renderPasswordViewButtons(): JSX.Element {
const showResetElements =
this.state.errorCount >= window.CONSTANTS.MAX_LOGIN_TRIES;
const showResetElements = this.state.errorCount >= MAX_LOGIN_TRIES;
return (
<div className={classNames(showResetElements && 'button-group')}>

@ -5,6 +5,7 @@ import { SessionButton } from './SessionButton';
import { ToastUtils } from '../../session/utils';
import { DefaultTheme, withTheme } from 'styled-components';
import { PasswordUtil } from '../../util';
import { getPasswordHash } from '../../../js/modules/data';
interface Props {
onClose: any;
@ -173,19 +174,16 @@ class SessionSeedModalInner extends React.Component<Props, State> {
return true;
}
private checkHasPassword() {
private async checkHasPassword() {
if (!this.state.loadingPassword) {
return;
}
const hashPromise = window.Signal.Data.getPasswordHash();
hashPromise.then((hash: any) => {
this.setState({
hasPassword: !!hash,
passwordHash: hash,
loadingPassword: false,
});
const hash = await getPasswordHash();
this.setState({
hasPassword: !!hash,
passwordHash: hash || '',
loadingPassword: false,
});
}

@ -26,7 +26,10 @@ import * as MIME from '../../../types/MIME';
import { SessionFileDropzone } from './SessionFileDropzone';
import { ConversationType } from '../../../state/ducks/conversations';
import { MessageView } from '../../MainViewController';
import { getMessageById } from '../../../../js/modules/data';
import {
getMessageById,
getPubkeysInPublicConversation,
} from '../../../../js/modules/data';
import { pushUnblockToSend } from '../../../session/utils/Toast';
import { MessageDetail } from '../../conversation/MessageDetail';
import { ConversationController } from '../../../session/conversations';
@ -1196,7 +1199,7 @@ export class SessionConversation extends React.Component<Props, State> {
}
private async updateMemberList() {
const allPubKeys = await window.Signal.Data.getPubkeysInPublicConversation(
const allPubKeys = await getPubkeysInPublicConversation(
this.props.selectedConversationKey
);

@ -17,6 +17,7 @@ import { TypingBubble } from '../../conversation/TypingBubble';
import { ConversationController } from '../../../session/conversations';
import { MessageCollection, MessageModel } from '../../../models/message';
import { MessageRegularProps } from '../../../models/messageType';
import { getMessagesBySentAt } from '../../../../js/modules/data';
interface State {
showScrollButton: boolean;
@ -554,7 +555,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
// If there's no message already in memory, we won't be scrolling. So we'll gather
// some more information then show an informative toast to the user.
if (!targetMessage) {
const collection = await window.Signal.Data.getMessagesBySentAt(quoteId, {
const collection = await getMessagesBySentAt(quoteId, {
MessageCollection,
});
const found = Boolean(

@ -17,7 +17,10 @@ import {
} from '../usingClosedConversationDetails';
import { save } from '../../../types/Attachment';
import { DefaultTheme, withTheme } from 'styled-components';
import { MessageCollection } from '../../../models/message';
import {
getMessagesWithFileAttachments,
getMessagesWithVisualMediaAttachments,
} from '../../../../js/modules/data';
interface Props {
id: string;
@ -107,20 +110,15 @@ class SessionRightPanel extends React.Component<Props, State> {
// We fetch more documents than media as they dont require to be loaded
// into memory right away. Revisit this once we have infinite scrolling:
const conversationId = this.props.id;
const rawMedia = await window.Signal.Data.getMessagesWithVisualMediaAttachments(
const rawMedia = await getMessagesWithVisualMediaAttachments(
conversationId,
{
limit: Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT,
MessageCollection,
}
);
const rawDocuments = await window.Signal.Data.getMessagesWithFileAttachments(
conversationId,
{
limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT,
MessageCollection,
}
);
const rawDocuments = await getMessagesWithFileAttachments(conversationId, {
limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT,
});
// First we upgrade these messages to ensure that they have thumbnails
const max = rawMedia.length;

@ -17,6 +17,7 @@ import {
getConversations,
} from '../../../state/selectors/conversations';
import { connect } from 'react-redux';
import { getPasswordHash } from '../../../../js/modules/data';
export enum SessionSettingCategory {
Appearance = 'appearance',
@ -80,7 +81,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
this.onPasswordUpdated = this.onPasswordUpdated.bind(this);
this.validatePasswordLock = this.validatePasswordLock.bind(this);
this.hasPassword();
void this.hasPassword();
this.onKeyUp = this.onKeyUp.bind(this);
window.addEventListener('keyup', this.onKeyUp);
@ -209,7 +210,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
}
// Check if the password matches the hash we have stored
const hash = await window.Signal.Data.getPasswordHash();
const hash = await getPasswordHash();
if (hash && !PasswordUtil.matchesHash(enteredPassword, hash)) {
this.setState({
pwdLockError: window.i18n('invalidPassword'),
@ -269,13 +270,11 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
window.setSettingValue(settingID, selectedValue);
}
public hasPassword() {
const hashPromise = window.Signal.Data.getPasswordHash();
public async hasPassword() {
const hash = await getPasswordHash();
hashPromise.then((hash: any) => {
this.setState({
hasPassword: !!hash,
});
this.setState({
hasPassword: !!hash,
});
}

@ -5,6 +5,7 @@ import { getMessageQueue } from '../session';
import { ConversationController } from '../session/conversations';
import {
ChatMessage,
ChatMessageParams,
ExpirationTimerUpdateMessage,
GroupInvitationMessage,
OpenGroupMessage,
@ -19,9 +20,16 @@ import { MessageController } from '../session/messages';
import { leaveClosedGroup } from '../session/group';
import { SignalService } from '../protobuf';
import { MessageCollection, MessageModel } from './message';
import * as Data from '../../js/modules/data';
import { MessageAttributesOptionals, MessageModelType } from './messageType';
import autoBind from 'auto-bind';
import {
getMessagesByConversation,
getUnreadByConversation,
getUnreadCountByConversation,
removeAllMessagesInConversation,
removeMessage as dataRemoveMessage,
updateConversation,
} from '../../js/modules/data';
export interface OurLokiProfile {
displayName: string;
@ -400,17 +408,21 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
await Promise.all(messages.map((m: any) => m.setCalculatingPoW()));
}
public async onPublicMessageSent(
identifier: any,
serverId: any,
serverTimestamp: any
) {
public async onPublicMessageSent({
identifier,
serverId,
serverTimestamp,
}: {
identifier: string;
serverId: number;
serverTimestamp: number;
}) {
const registeredMessage = MessageController.getInstance().get(identifier);
if (!registeredMessage || !registeredMessage.message) {
return null;
}
const model = registeredMessage.message;
const model = registeredMessage.message as MessageModel;
await model.setIsPublic(true);
await model.setServerId(serverId);
await model.setServerTimestamp(serverTimestamp);
@ -508,13 +520,13 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
public async getUnread() {
return window.Signal.Data.getUnreadByConversation(this.id, {
return getUnreadByConversation(this.id, {
MessageCollection: MessageCollection,
});
}
public async getUnreadCount() {
return window.Signal.Data.getUnreadCountByConversation(this.id);
return getUnreadCountByConversation(this.id);
}
public queueJob(callback: any) {
@ -654,17 +666,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
throw new Error('sendMessageJob() sent_at must be set.');
}
const chatMessage = new ChatMessage({
body: uploads.body,
identifier: id,
timestamp: sentAt,
attachments: uploads.attachments,
expireTimer,
preview: uploads.preview,
quote: uploads.quote,
lokiProfile: this.getOurProfile(),
});
if (this.isPublic()) {
const openGroup = this.toOpenGroup();
@ -682,9 +683,26 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
await getMessageQueue().sendToGroup(openGroupMessage);
return;
}
const chatMessageParams: ChatMessageParams = {
body: uploads.body,
identifier: id,
timestamp: sentAt,
attachments: uploads.attachments,
expireTimer,
preview: uploads.preview,
quote: uploads.quote,
lokiProfile: this.getOurProfile(),
};
const destinationPubkey = new PubKey(destination);
if (this.isPrivate()) {
if (this.isMe()) {
chatMessageParams.syncTarget = this.id;
const chatMessageMe = new ChatMessage(chatMessageParams);
await getMessageQueue().sendSyncMessage(chatMessageMe);
return;
}
// Handle Group Invitation Message
if (message.get('groupInvitation')) {
const groupInvitation = message.get('groupInvitation');
@ -703,14 +721,19 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
);
return;
}
// we need the return await so that errors are caught in the catch {}
await getMessageQueue().sendToPubKey(destinationPubkey, chatMessage);
const chatMessagePrivate = new ChatMessage(chatMessageParams);
await getMessageQueue().sendToPubKey(
destinationPubkey,
chatMessagePrivate
);
return;
}
if (this.isMediumGroup()) {
const chatMessageMediumGroup = new ChatMessage(chatMessageParams);
const closedGroupChatMessage = new ClosedGroupChatMessage({
chatMessage,
chatMessage: chatMessageMediumGroup,
groupId: destination,
});
@ -772,13 +795,11 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
recipients,
});
if (this.isPublic()) {
// Public chats require this data to detect duplicates
messageWithSchema.source = UserUtils.getOurPubKeyStrFromCache();
messageWithSchema.sourceDevice = 1;
} else {
if (!this.isPublic()) {
messageWithSchema.destination = destination;
}
messageWithSchema.source = UserUtils.getOurPubKeyStrFromCache();
messageWithSchema.sourceDevice = 1;
const attributes: MessageAttributesOptionals = {
...messageWithSchema,
@ -844,10 +865,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
window.log.info('Skipping update last message as active_at is falsy');
return;
}
const messages = await window.Signal.Data.getMessagesByConversation(
this.id,
{ limit: 1, MessageCollection: MessageCollection }
);
const messages = await getMessagesByConversation(this.id, {
limit: 1,
MessageCollection: MessageCollection,
});
const lastMessageModel = messages.at(0);
const lastMessageJSON = lastMessageModel ? lastMessageModel.toJSON() : null;
const lastMessageStatusModel = lastMessageModel
@ -988,7 +1009,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
public async commit() {
await window.Signal.Data.updateConversation(this.id, this.attributes, {
await updateConversation(this.id, this.attributes, {
Conversation: ConversationModel,
});
this.trigger('change', this);
@ -1443,7 +1464,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
public async removeMessage(messageId: any) {
await Data.removeMessage(messageId, {
await dataRemoveMessage(messageId, {
Message: MessageModel,
});
window.Whisper.events.trigger('messageDeleted', {
@ -1470,7 +1491,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
public async destroyMessages() {
await window.Signal.Data.removeAllMessagesInConversation(this.id, {
await removeAllMessagesInConversation(this.id, {
MessageCollection,
});

@ -21,6 +21,8 @@ import {
} from './messageType';
import autoBind from 'auto-bind';
import { saveMessage } from '../../js/modules/data';
import { ConversationModel } from './conversation';
export class MessageModel extends Backbone.Model<MessageAttributes> {
public propsForTimerNotification: any;
public propsForGroupNotification: any;
@ -837,7 +839,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
};
}
// One caller today: event handler for the 'Retry Send' entry in triple-dot menu
// One caller today: event handler for the 'Retry Send' entry on right click of a failed send message
public async retrySend() {
if (!window.textsecure.messaging) {
window.log.error('retrySend: Cannot retry since we are offline!');
@ -847,7 +849,9 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
this.set({ errors: null });
await this.commit();
try {
const conversation = this.getConversation();
const conversation:
| ConversationModel
| undefined = this.getConversation();
if (!conversation) {
window.log.info(
'cannot retry send message, the corresponding conversation was not found.'
@ -1019,7 +1023,10 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
// Handle the sync logic here
if (shouldTriggerSyncMessage) {
if (dataMessage) {
await this.sendSyncMessage(dataMessage as DataMessage);
await this.sendSyncMessage(
dataMessage as SignalService.DataMessage,
sentMessage.timestamp
);
}
} else if (shouldMarkMessageAsSynced) {
this.set({ synced: true });
@ -1032,6 +1039,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
sent_to: sentTo,
sent: true,
expirationStartTimestamp: Date.now(),
sent_at: sentMessage.timestamp,
});
await this.commit();
@ -1065,7 +1073,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
this.getConversation()?.updateLastMessage();
}
public getConversation() {
public getConversation(): ConversationModel | undefined {
// This needs to be an unsafe call, because this method is called during
// initial module setup. We may be in the middle of the initial fetch to
// the database.
@ -1179,10 +1187,11 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
}
public async sendSyncMessageOnly(dataMessage: any) {
const now = Date.now();
this.set({
sent_to: [UserUtils.getOurPubKeyStrFromCache()],
sent: true,
expirationStartTimestamp: Date.now(),
expirationStartTimestamp: now,
});
await this.commit();
@ -1191,33 +1200,33 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
dataMessage instanceof DataMessage
? dataMessage.dataProto()
: dataMessage;
await this.sendSyncMessage(data);
await this.sendSyncMessage(data, now);
}
public async sendSyncMessage(dataMessage: DataMessage) {
public async sendSyncMessage(
dataMessage: SignalService.DataMessage,
sentTimestamp: number
) {
if (this.get('synced') || this.get('sentSync')) {
return;
}
window.log.error('sendSyncMessage to upgrade to multi device protocol v2');
// const data =
// dataMessage instanceof DataMessage
// ? dataMessage.dataProto()
// : dataMessage;
// const syncMessage = new SentSyncMessage({
// timestamp: this.get('sent_at'),
// identifier: this.id,
// dataMessage: data,
// destination: this.get('destination'),
// expirationStartTimestamp: this.get('expirationStartTimestamp'),
// sent_to: this.get('sent_to'),
// unidentifiedDeliveries: this.get('unidentifiedDeliveries'),
// });
// await sendSyncMessage(syncMessage);
// if this message needs to be synced
if (
(dataMessage.body && dataMessage.body.length) ||
dataMessage.attachments.length
) {
const conversation = this.getConversation();
if (!conversation) {
throw new Error('Cannot trigger syncMessage with unknown convo.');
}
const syncMessage = ChatMessage.buildSyncMessage(
dataMessage,
conversation.id,
sentTimestamp
);
await getMessageQueue().sendSyncMessage(syncMessage);
}
this.set({ sentSync: true });
await this.commit();
}
@ -1265,7 +1274,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
public async commit(forceSave = false) {
// TODO investigate the meaning of the forceSave
const id = await window.Signal.Data.saveMessage(this.attributes, {
const id = await saveMessage(this.attributes, {
forceSave,
Message: MessageModel,
});

@ -187,29 +187,3 @@ export interface MessageRegularProps {
markRead: (readAt: number) => Promise<void>;
theme: DefaultTheme;
}
// export interface MessageModel extends Backbone.Model<MessageAttributes> {
// setServerTimestamp(serverTimestamp: any);
// setServerId(serverId: any);
// setIsPublic(arg0: boolean);
// idForLogging: () => string;
// isGroupUpdate: () => boolean;
// isExpirationTimerUpdate: () => boolean;
// getNotificationText: () => string;
// markRead: (readAt: number) => Promise<void>;
// merge: (other: MessageModel) => void;
// saveErrors: (error: any) => promise<void>;
// sendSyncMessageOnly: (message: any) => void;
// isUnread: () => boolean;
// commit: () => Promise<number>;
// getPropsForMessageDetail: () => any;
// getConversation: () => ConversationModel;
// handleMessageSentSuccess: (sentMessage: any, wrappedEnvelope: any) => any;
// handleMessageSentFailure: (sentMessage: any, error: any) => any;
// propsForMessage?: MessageRegularProps;
// propsForTimerNotification?: any;
// propsForGroupInvitation?: any;
// propsForGroupNotification?: any;
// firstMessageOfSeries: boolean;
// }

@ -1,7 +1,7 @@
import _ from 'lodash';
import * as Data from '../../js/modules/data';
import { MessageModel } from '../models/message';
import { saveMessage } from '../../js/modules/data';
export async function downloadAttachment(attachment: any) {
const serverUrl = new URL(attachment.url).origin;
@ -56,6 +56,12 @@ export async function downloadAttachment(attachment: any) {
if (!attachment.isRaw) {
const { key, digest, size } = attachment;
if (!key || !digest) {
throw new Error(
'Attachment is not raw but we do not have a key to decode it'
);
}
data = await window.textsecure.crypto.decryptAttachment(
data,
window.Signal.Crypto.base64ToArrayBuffer(key),
@ -234,8 +240,8 @@ export async function queueAttachmentDownloads(
}
if (count > 0) {
await Data.saveMessage(message.attributes, {
Message: MessageModel,
await saveMessage(message.attributes, {
Message: Whisper.Message,
});
return true;

@ -12,7 +12,10 @@ import {
} from '../session/crypto';
import { getMessageQueue } from '../session';
import { decryptWithSessionProtocol } from './contentMessage';
import * as Data from '../../js/modules/data';
import {
addClosedGroupEncryptionKeyPair,
removeAllClosedGroupEncryptionKeyPairs,
} from '../../js/modules/data';
import {
ClosedGroupNewMessage,
ClosedGroupNewMessageParams,
@ -22,6 +25,7 @@ import { ECKeyPair } from './keypairs';
import { UserUtils } from '../session/utils';
import { ConversationModel } from '../models/conversation';
import _ from 'lodash';
import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils';
import { MessageController } from '../session/messages';
export async function handleClosedGroupControlMessage(
@ -30,13 +34,16 @@ export async function handleClosedGroupControlMessage(
) {
const { type } = groupUpdate;
const { Type } = SignalService.DataMessage.ClosedGroupControlMessage;
window.log.info(
` handle closed group update from ${envelope.senderIdentity} about group ${envelope.source}`
);
if (BlockedNumberController.isGroupBlocked(PubKey.cast(envelope.source))) {
window.log.warn('Message ignored; destined for blocked group');
await removeFromCache(envelope);
return;
}
// We drop New closed group message from our other devices, as they will come as ConfigurationMessage instead
if (type === Type.ENCRYPTION_KEY_PAIR) {
await handleClosedGroupEncryptionKeyPair(envelope, groupUpdate);
} else if (type === Type.NEW) {
@ -117,7 +124,7 @@ function sanityCheckNewGroup(
return true;
}
async function handleNewClosedGroup(
export async function handleNewClosedGroup(
envelope: EnvelopePlus,
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage
) {
@ -134,6 +141,14 @@ async function handleNewClosedGroup(
await removeFromCache(envelope);
return;
}
const ourPrimary = UserUtils.getOurPubKeyFromCache();
if (envelope.senderIdentity === ourPrimary.key) {
window.log.warn(
'Dropping new closed group updatemessage from our other device.'
);
return removeFromCache(envelope);
}
const {
name,
@ -147,7 +162,6 @@ async function handleNewClosedGroup(
const members = membersAsData.map(toHex);
const admins = adminsAsData.map(toHex);
const ourPrimary = UserUtils.getOurPubKeyFromCache();
if (!members.includes(ourPrimary.key)) {
log.info(
'Got a new group message but apparently we are not a member of it. Dropping it.'
@ -219,7 +233,7 @@ async function handleNewClosedGroup(
);
window.log.info(`Received a the encryptionKeyPair for new group ${groupId}`);
await Data.addClosedGroupEncryptionKeyPair(groupId, ecKeyPair.toHexKeyPair());
await addClosedGroupEncryptionKeyPair(groupId, ecKeyPair.toHexKeyPair());
// start polling for this new group
window.SwarmPolling.addGroupId(PubKey.cast(groupId));
@ -258,9 +272,7 @@ async function handleUpdateClosedGroup(
await removeFromCache(envelope);
return;
}
await window.Signal.Data.removeAllClosedGroupEncryptionKeyPairs(
groupPublicKey
);
await removeAllClosedGroupEncryptionKeyPairs(groupPublicKey);
// Disable typing:
convo.set('isKickedFromGroup', true);
window.SwarmPolling.removePubkey(groupPublicKey);
@ -385,11 +397,9 @@ async function handleClosedGroupEncryptionKeyPair(
}
// Parse it
let proto: SignalService.DataMessage.ClosedGroupControlMessage.KeyPair;
let proto: SignalService.KeyPair;
try {
proto = SignalService.DataMessage.ClosedGroupControlMessage.KeyPair.decode(
plaintext
);
proto = SignalService.KeyPair.decode(plaintext);
if (
!proto ||
proto.privateKey.length === 0 ||
@ -416,10 +426,7 @@ async function handleClosedGroupEncryptionKeyPair(
);
// Store it
await Data.addClosedGroupEncryptionKeyPair(
groupPublicKey,
keyPair.toHexKeyPair()
);
await addClosedGroupEncryptionKeyPair(groupPublicKey, keyPair.toHexKeyPair());
await removeFromCache(envelope);
}
@ -525,7 +532,6 @@ async function handleClosedGroupMembersAdded(
const membersNotAlreadyPresent = addedMembers.filter(
m => !oldMembers.includes(m)
);
console.warn('membersNotAlreadyPresent', membersNotAlreadyPresent);
window.log.info(
`Got a group update for group ${envelope.source}, type: MEMBERS_ADDED`
);
@ -592,9 +598,7 @@ async function handleClosedGroupMembersRemoved(
const ourPubKey = UserUtils.getOurPubKeyFromCache();
const wasCurrentUserRemoved = !membersAfterUpdate.includes(ourPubKey.key);
if (wasCurrentUserRemoved) {
await window.Signal.Data.removeAllClosedGroupEncryptionKeyPairs(
groupPubKey
);
await removeAllClosedGroupEncryptionKeyPairs(groupPubKey);
// Disable typing:
convo.set('isKickedFromGroup', true);
window.SwarmPolling.removePubkey(groupPubKey);
@ -663,9 +667,7 @@ async function handleClosedGroupMemberLeft(
}
if (didAdminLeave) {
await window.Signal.Data.removeAllClosedGroupEncryptionKeyPairs(
groupPublicKey
);
await removeAllClosedGroupEncryptionKeyPairs(groupPublicKey);
// Disable typing:
convo.set('isKickedFromGroup', true);
window.SwarmPolling.removePubkey(groupPublicKey);
@ -759,7 +761,7 @@ export async function createClosedGroup(
`Creating a new group and an encryptionKeyPair for group ${groupPublicKey}`
);
// tslint:disable-next-line: no-non-null-assertion
await Data.addClosedGroupEncryptionKeyPair(
await addClosedGroupEncryptionKeyPair(
groupPublicKey,
encryptionKeyPair.toHexKeyPair()
);
@ -771,6 +773,8 @@ export async function createClosedGroup(
await Promise.all(promises);
await forceSyncConfigurationNowIfNeeded();
window.inboxStore.dispatch(
window.actionsCreators.openConversationExternal(groupPublicKey)
);

@ -4,15 +4,16 @@ import { handleDataMessage } from './dataMessage';
import { removeFromCache, updateCache } from './cache';
import { SignalService } from '../protobuf';
import * as Lodash from 'lodash';
import { PubKey } from '../session/types';
import { OpenGroup, PubKey } from '../session/types';
import { BlockedNumberController } from '../util/blockedNumberController';
import { GroupUtils, UserUtils } from '../session/utils';
import { fromHexToArray, toHex } from '../session/utils/String';
import { concatUInt8Array, getSodium } from '../session/crypto';
import { ConversationController } from '../session/conversations';
import * as Data from '../../js/modules/data';
import { getAllEncryptionKeyPairsForGroup } from '../../js/modules/data';
import { ECKeyPair } from './keypairs';
import { handleNewClosedGroup } from './closedGroups';
export async function handleContentMessage(envelope: EnvelopePlus) {
try {
@ -44,7 +45,7 @@ async function decryptForClosedGroup(
);
throw new Error('Invalid group public key'); // invalidGroupPublicKey
}
const encryptionKeyPairs = await Data.getAllEncryptionKeyPairsForGroup(
const encryptionKeyPairs = await getAllEncryptionKeyPairsForGroup(
hexEncodedGroupPublicKey
);
const encryptionKeyPairsCount = encryptionKeyPairs?.length;
@ -100,18 +101,6 @@ async function decryptForClosedGroup(
'ClosedGroup Message decrypted successfully with keyIndex:',
keyIndex
);
const ourDevicePubKey = UserUtils.getOurPubKeyStrFromCache();
if (
envelope.senderIdentity &&
envelope.senderIdentity === ourDevicePubKey
) {
await removeFromCache(envelope);
window.log.info(
'Dropping message from our current device after decrypt for closed group'
);
return null;
}
return unpad(decryptedContent);
} catch (e) {
@ -390,6 +379,14 @@ export async function innerHandleContentMessage(
await handleTypingMessage(envelope, content.typingMessage);
return;
}
if (content.configurationMessage) {
await handleConfigurationMessage(
envelope,
content.configurationMessage as SignalService.ConfigurationMessage
);
return;
}
} catch (e) {
window.log.warn(e);
}
@ -499,3 +496,57 @@ async function handleTypingMessage(
});
}
}
async function handleConfigurationMessage(
envelope: EnvelopePlus,
configurationMessage: SignalService.ConfigurationMessage
): Promise<void> {
const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
if (!ourPubkey) {
return;
}
if (envelope.source !== ourPubkey) {
window.log.info('dropping configuration change from someone else than us.');
return removeFromCache(envelope);
}
const numberClosedGroup = configurationMessage.closedGroups?.length || 0;
window.log.warn(
`Received ${numberClosedGroup} closed group on configuration. Creating them... `
);
await Promise.all(
configurationMessage.closedGroups.map(async c => {
const groupUpdate = new SignalService.DataMessage.ClosedGroupControlMessage(
{
type: SignalService.DataMessage.ClosedGroupControlMessage.Type.NEW,
encryptionKeyPair: c.encryptionKeyPair,
name: c.name,
admins: c.admins,
members: c.members,
publicKey: c.publicKey,
}
);
await handleNewClosedGroup(envelope, groupUpdate);
})
);
const allOpenGroups = OpenGroup.getAllAlreadyJoinedOpenGroupsUrl();
const numberOpenGroup = configurationMessage.openGroups?.length || 0;
// Trigger a join for all open groups we are not already in.
// Currently, if you left an open group but kept the conversation, you won't rejoin it here.
for (let i = 0; i < numberOpenGroup; i++) {
const current = configurationMessage.openGroups[i];
if (!allOpenGroups.includes(current)) {
window.log.info(
`triggering join of public chat '${current}' from ConfigurationMessage`
);
void OpenGroup.join(current);
}
}
await removeFromCache(envelope);
}

@ -13,8 +13,8 @@ import { getMessageQueue } from '../session';
import { ConversationController } from '../session/conversations';
import { handleClosedGroupControlMessage } from './closedGroups';
import { MessageModel } from '../models/message';
import { isUsFromCache } from '../session/utils/User';
import { MessageModelType } from '../models/messageType';
import { getMessageBySender } from '../../js/modules/data';
export async function updateProfile(
conversation: any,
@ -152,7 +152,10 @@ function cleanAttachments(decrypted: any) {
}
}
export async function processDecrypted(envelope: EnvelopePlus, decrypted: any) {
export async function processDecrypted(
envelope: EnvelopePlus,
decrypted: SignalService.IDataMessage
) {
/* tslint:disable:no-bitwise */
const FLAGS = SignalService.DataMessage.Flags;
@ -175,7 +178,7 @@ export async function processDecrypted(envelope: EnvelopePlus, decrypted: any) {
}
if (decrypted.group) {
decrypted.group.id = new TextDecoder('utf-8').decode(decrypted.group.id);
// decrypted.group.id = new TextDecoder('utf-8').decode(decrypted.group.id);
switch (decrypted.group.type) {
case SignalService.GroupContext.Type.UPDATE:
@ -201,7 +204,7 @@ export async function processDecrypted(envelope: EnvelopePlus, decrypted: any) {
}
}
const attachmentCount = decrypted.attachments.length;
const attachmentCount = decrypted?.attachments?.length || 0;
const ATTACHMENT_MAX = 32;
if (attachmentCount > ATTACHMENT_MAX) {
await removeFromCache(envelope);
@ -212,7 +215,7 @@ export async function processDecrypted(envelope: EnvelopePlus, decrypted: any) {
cleanAttachments(decrypted);
return decrypted;
return decrypted as SignalService.DataMessage;
/* tslint:disable:no-bitwise */
}
@ -245,12 +248,22 @@ function isBodyEmpty(body: string) {
return _.isEmpty(body);
}
/**
* We have a few origins possible
* - if the message is from a private conversation with a friend and he wrote to us,
* the conversation to add the message to is our friend pubkey, so envelope.source
* - if the message is from a medium group conversation
* * envelope.source is the medium group pubkey
* * envelope.senderIdentity is the author pubkey (the one who sent the message)
* - at last, if the message is a syncMessage,
* * envelope.source is our pubkey (our other device has the same pubkey as us)
* * dataMessage.syncTarget is either the group public key OR the private conversation this message is about.
*/
export async function handleDataMessage(
envelope: EnvelopePlus,
dataMessage: SignalService.IDataMessage
): Promise<void> {
window.log.info('data message from', getEnvelopeId(envelope));
// we handle group updates from our other devices in handleClosedGroupControlMessage()
if (dataMessage.closedGroupControlMessage) {
await handleClosedGroupControlMessage(
envelope,
@ -260,10 +273,23 @@ export async function handleDataMessage(
}
const message = await processDecrypted(envelope, dataMessage);
const ourPubKey = UserUtils.getOurPubKeyStrFromCache();
const source = envelope.source;
const source = dataMessage.syncTarget || envelope.source;
const senderPubKey = envelope.senderIdentity || envelope.source;
const isMe = senderPubKey === ourPubKey;
const isMe = UserUtils.isUsFromCache(senderPubKey);
const isSyncMessage = Boolean(dataMessage.syncTarget?.length);
window.log.info(`Handle dataMessage from ${source} `);
if (isSyncMessage && !isMe) {
window.log.warn(
'Got a sync message from someone else than me. Dropping it.'
);
return removeFromCache(envelope);
} else if (isSyncMessage && dataMessage.syncTarget) {
// override the envelope source
envelope.source = dataMessage.syncTarget;
}
const senderConversation = await ConversationController.getInstance().getOrCreateAndWait(
senderPubKey,
'private'
@ -282,13 +308,8 @@ export async function handleDataMessage(
return removeFromCache(envelope);
}
const ownDevice = isUsFromCache(senderPubKey);
const sourceConversation = ConversationController.getInstance().get(source);
const ownMessage = sourceConversation?.isMediumGroup() && ownDevice;
const ev: any = {};
if (ownMessage) {
if (isMe) {
// Data messages for medium groups don't arrive as sync messages. Instead,
// linked devices poll for group messages independently, thus they need
// to recognise some of those messages at their own.
@ -299,13 +320,14 @@ export async function handleDataMessage(
if (envelope.senderIdentity) {
message.group = {
id: envelope.source,
id: envelope.source as any, // FIXME Uint8Array vs string
};
}
ev.confirm = () => removeFromCache(envelope);
ev.data = {
source: senderPubKey,
destination: isMe ? message.syncTarget : undefined,
sourceDevice: 1,
timestamp: _.toNumber(envelope.timestamp),
receivedAt: envelope.receivedAt,
@ -332,12 +354,13 @@ async function isMessageDuplicate({
const { Errors } = window.Signal.Types;
try {
const result = await window.Signal.Data.getMessageBySender(
const result = await getMessageBySender(
{ source, sourceDevice, sent_at: timestamp },
{
Message: MessageModel,
}
);
if (!result) {
return false;
}
@ -412,7 +435,7 @@ interface MessageCreationData {
receivedAt: number;
sourceDevice: number; // always 1 isn't it?
source: boolean;
serverId: string;
serverId: number;
message: any;
serverTimestamp: any;
@ -467,6 +490,7 @@ function createSentMessage(data: MessageCreationData): MessageModel {
const {
timestamp,
serverTimestamp,
serverId,
isPublic,
receivedAt,
sourceDevice,
@ -500,8 +524,10 @@ function createSentMessage(data: MessageCreationData): MessageModel {
source: UserUtils.getOurPubKeyStrFromCache(),
sourceDevice,
serverTimestamp,
serverId,
sent_at: timestamp,
received_at: isPublic ? receivedAt : now,
isPublic,
conversationId: destination, // conversation ID will might change later (if it is a group)
type: 'outgoing' as MessageModelType,
...sentSpecificFields,
@ -530,7 +556,7 @@ function sendDeliveryReceipt(source: string, timestamp: any) {
void getMessageQueue().sendToPubKey(device, receiptMessage);
}
interface MessageEvent {
export interface MessageEvent {
data: any;
type: string;
confirm: () => void;
@ -558,7 +584,12 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
? ConversationType.GROUP
: ConversationType.PRIVATE;
let conversationId = isIncoming ? source : destination;
let conversationId = isIncoming ? source : destination || source; // for synced message
if (!conversationId) {
window.log.error('We cannot handle a message without a conversationId');
confirm();
return;
}
if (message.profileKey?.length) {
await handleProfileUpdate(
message.profileKey,
@ -574,12 +605,11 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
source = source || msg.get('source');
if (await isMessageDuplicate(data)) {
window.log.info('Received duplicate message. Dropping it.');
confirm();
return;
}
// TODO: this shouldn't be called when source is not a pubkey!!!
const isOurDevice = UserUtils.isUsFromCache(source);
const shouldSendReceipt = isIncoming && !isGroupMessage && !isOurDevice;
@ -614,9 +644,10 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
conversationId = source;
}
// the conversation with the primary device of that source (can be the same as conversationOrigin)
const conversation = ConversationController.getInstance().get(conversationId);
const conversation = await ConversationController.getInstance().getOrCreateAndWait(
conversationId,
isGroupMessage ? 'group' : 'private'
);
if (!conversation) {
window.log.warn('Skipping handleJob for unknown convo: ', conversationId);

@ -9,7 +9,7 @@ import { ConversationController } from '../session/conversations';
import { ConversationModel } from '../models/conversation';
import { MessageCollection, MessageModel } from '../models/message';
import { MessageController } from '../session/messages';
import { getMessageById } from '../../js/modules/data';
import { getMessageById, getMessagesBySentAt } from '../../js/modules/data';
async function handleGroups(
conversation: ConversationModel,
@ -99,7 +99,7 @@ async function copyFromQuotedMessage(
const { attachments, id, author } = quote;
const firstAttachment = attachments[0];
const collection = await window.Signal.Data.getMessagesBySentAt(id, {
const collection = await getMessagesBySentAt(id, {
MessageCollection,
});
const found = collection.find((item: any) => {
@ -370,7 +370,7 @@ async function handleRegularMessage(
const now = new Date().getTime();
// Medium grups might have `group` set even if with group chat messages...
// Medium groups might have `group` set even if with group chat messages...
if (dataMessage.group && !conversation.isMediumGroup()) {
// This is not necessarily a group update message, it could also be a regular group message
const groupUpdate = await handleGroups(

@ -302,5 +302,5 @@ export async function handlePublicMessage(messageData: any) {
},
};
await handleMessageEvent(ev);
await handleMessageEvent(ev); // open groups
}

@ -1,3 +1,9 @@
import {
getAllConversations,
getAllGroupsInvolvingId,
removeConversation,
saveConversation,
} from '../../../js/modules/data';
import {
ConversationAttributes,
ConversationCollection,
@ -100,9 +106,7 @@ export class ConversationController {
}
try {
await window.Signal.Data.saveConversation(conversation.attributes, {
Conversation: ConversationModel,
});
await saveConversation(conversation.attributes);
} catch (error) {
window.log.error(
'Conversation save failed! ',
@ -191,8 +195,8 @@ export class ConversationController {
});
}
public async getAllGroupsInvolvingId(id: String) {
const groups = await window.Signal.Data.getAllGroupsInvolvingId(id, {
public async getAllGroupsInvolvingId(id: string) {
const groups = await getAllGroupsInvolvingId(id, {
ConversationCollection,
});
return groups.map((group: any) => this.conversations.add(group));
@ -228,7 +232,7 @@ export class ConversationController {
await conversation.destroyMessages();
await window.Signal.Data.removeConversation(id, {
await removeConversation(id, {
Conversation: ConversationModel,
});
conversation.off('change', this.updateReduxConvoChanged);
@ -253,7 +257,7 @@ export class ConversationController {
const load = async () => {
try {
const collection = await window.Signal.Data.getAllConversations({
const collection = await getAllConversations({
ConversationCollection,
});

@ -1,5 +1,5 @@
import { PubKey } from '../types';
import * as Data from '../../../js/modules/data';
import _ from 'lodash';
import { fromHex, fromHexToArray, toHex } from '../utils/String';
@ -7,7 +7,12 @@ import { BlockedNumberController } from '../../util/blockedNumberController';
import { ConversationController } from '../conversations';
import { updateOpenGroup } from '../../receiver/openGroups';
import { getMessageQueue } from '../instance';
import { ExpirationTimerUpdateMessage } from '../messages/outgoing';
import {
addClosedGroupEncryptionKeyPair,
getIdentityKeyById,
getLatestClosedGroupEncryptionKeyPair,
removeAllClosedGroupEncryptionKeyPairs,
} from '../../../js/modules/data';
import uuid from 'uuid';
import { SignalService } from '../../protobuf';
import { generateCurve25519KeyPairWithoutPrefix } from '../crypto';
@ -55,7 +60,7 @@ export interface MemberChanges {
}
export async function getGroupSecretKey(groupId: string): Promise<Uint8Array> {
const groupIdentity = await Data.getIdentityKeyById(groupId);
const groupIdentity = await getIdentityKeyById(groupId);
if (!groupIdentity) {
throw new Error(`Could not load secret key for group ${groupId}`);
}
@ -71,12 +76,6 @@ export async function getGroupSecretKey(groupId: string): Promise<Uint8Array> {
return new Uint8Array(fromHex(secretKey));
}
// Secondary devices are not expected to already have the group, so
// we send messages of type NEW
export async function syncMediumGroups(groups: Array<ConversationModel>) {
// await Promise.all(groups.map(syncMediumGroup));
}
// tslint:disable: max-func-body-length
// tslint:disable: cyclomatic-complexity
export async function initiateGroupUpdate(
@ -311,7 +310,9 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) {
}
export async function leaveClosedGroup(groupId: string) {
const convo = ConversationController.getInstance().get(groupId);
const convo = ConversationController.getInstance().get(
groupId
) as ConversationModel;
if (!convo) {
window.log.error('Cannot leave non-existing group');
@ -367,7 +368,7 @@ export async function leaveClosedGroup(groupId: string) {
window.log.info(
`Leaving message sent ${groupId}. Removing everything related to this group.`
);
await Data.removeAllClosedGroupEncryptionKeyPairs(groupId);
await removeAllClosedGroupEncryptionKeyPairs(groupId);
});
}
@ -409,7 +410,7 @@ async function sendAddedMembers(
const admins = groupUpdate.admins || [];
// Check preconditions
const hexEncryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair(
const hexEncryptionKeyPair = await getLatestClosedGroupEncryptionKeyPair(
groupId
);
if (!hexEncryptionKeyPair) {
@ -540,15 +541,11 @@ export async function generateAndSendNewEncryptionKeyPair(
);
return;
}
const proto = new SignalService.DataMessage.ClosedGroupControlMessage.KeyPair(
{
privateKey: newKeyPair?.privateKeyData,
publicKey: newKeyPair?.publicKeyData,
}
);
const plaintext = SignalService.DataMessage.ClosedGroupControlMessage.KeyPair.encode(
proto
).finish();
const proto = new SignalService.KeyPair({
privateKey: newKeyPair?.privateKeyData,
publicKey: newKeyPair?.publicKeyData,
});
const plaintext = SignalService.KeyPair.encode(proto).finish();
// Distribute it
const wrappers = await Promise.all(
@ -580,7 +577,7 @@ export async function generateAndSendNewEncryptionKeyPair(
`KeyPairMessage for ClosedGroup ${groupPublicKey} is sent. Saving the new encryptionKeyPair.`
);
await Data.addClosedGroupEncryptionKeyPair(
await addClosedGroupEncryptionKeyPair(
toHex(groupId),
newKeyPair.toHexKeyPair()
);

@ -1,6 +1,7 @@
// You can see MessageController for in memory registered messages.
// Ee register messages to it everytime we send one, so that when an event happens we can find which message it was based on this id.
import { getMessagesByConversation } from '../../../js/modules/data';
import { ConversationModel } from '../../models/conversation';
import { MessageCollection, MessageModel } from '../../models/message';
@ -75,7 +76,7 @@ export class MessageController {
// loadLive gets messages live, not from the database which can lag behind.
let messages = [];
const messageSet = await window.Signal.Data.getMessagesByConversation(key, {
const messageSet = await getMessagesByConversation(key, {
limit: 100,
MessageCollection,
});

@ -14,6 +14,9 @@ export abstract class Message {
if (identifier && identifier.length === 0) {
throw new Error('Cannot set empty identifier');
}
if (!timestamp) {
throw new Error('Cannot set undefined timestamp');
}
this.identifier = identifier || uuid();
}
}

@ -0,0 +1,124 @@
// this is not a very good name, but a configuration message is a message sent to our other devices so sync our current public and closed groups
import { ContentMessage } from './ContentMessage';
import { SignalService } from '../../../../protobuf';
import { MessageParams } from '../Message';
import { Constants } from '../../..';
import { ECKeyPair } from '../../../../receiver/keypairs';
import { fromHexToArray } from '../../../utils/String';
import { PubKey } from '../../../types';
interface ConfigurationMessageParams extends MessageParams {
activeClosedGroups: Array<ConfigurationMessageClosedGroup>;
activeOpenGroups: Array<string>;
}
export class ConfigurationMessage extends ContentMessage {
public readonly activeClosedGroups: Array<ConfigurationMessageClosedGroup>;
public readonly activeOpenGroups: Array<string>;
constructor(params: ConfigurationMessageParams) {
super({ timestamp: params.timestamp, identifier: params.identifier });
this.activeClosedGroups = params.activeClosedGroups;
this.activeOpenGroups = params.activeOpenGroups;
if (!this.activeClosedGroups) {
throw new Error('closed group must be set');
}
if (!this.activeOpenGroups) {
throw new Error('open group must be set');
}
}
public ttl(): number {
return Constants.TTL_DEFAULT.TYPING_MESSAGE;
}
public contentProto(): SignalService.Content {
return new SignalService.Content({
configurationMessage: this.configurationProto(),
});
}
protected configurationProto(): SignalService.ConfigurationMessage {
return new SignalService.ConfigurationMessage({
closedGroups: this.mapClosedGroupsObjectToProto(this.activeClosedGroups),
openGroups: this.activeOpenGroups,
});
}
private mapClosedGroupsObjectToProto(
closedGroups: Array<ConfigurationMessageClosedGroup>
): Array<SignalService.ConfigurationMessage.ClosedGroup> {
return (closedGroups || []).map(m =>
new ConfigurationMessageClosedGroup(m).toProto()
);
}
}
export class ConfigurationMessageClosedGroup {
public publicKey: string;
public name: string;
public encryptionKeyPair: ECKeyPair;
public members: Array<string>;
public admins: Array<string>;
public constructor({
publicKey,
name,
encryptionKeyPair,
members,
admins,
}: {
publicKey: string;
name: string;
encryptionKeyPair: ECKeyPair;
members: Array<string>;
admins: Array<string>;
}) {
this.publicKey = publicKey;
this.name = name;
this.encryptionKeyPair = encryptionKeyPair;
this.members = members;
this.admins = admins;
// will throw if publik key is invalid
PubKey.cast(publicKey);
if (
!encryptionKeyPair?.privateKeyData?.byteLength ||
!encryptionKeyPair?.publicKeyData?.byteLength
) {
throw new Error('Encryption key pair looks invalid');
}
if (!this.name?.length) {
throw new Error('name must be set');
}
if (!this.members?.length) {
throw new Error('members must be set');
}
if (!this.admins?.length) {
throw new Error('admins must be set');
}
if (this.admins.some(a => !this.members.includes(a))) {
throw new Error('some admins are not members');
}
}
public toProto(): SignalService.ConfigurationMessage.ClosedGroup {
return new SignalService.ConfigurationMessage.ClosedGroup({
publicKey: fromHexToArray(this.publicKey),
name: this.name,
encryptionKeyPair: {
publicKey: this.encryptionKeyPair.publicKeyData,
privateKey: this.encryptionKeyPair.privateKeyData,
},
members: this.members.map(fromHexToArray),
admins: this.admins.map(fromHexToArray),
});
}
}

@ -1,6 +1,5 @@
import { Message } from '../Message';
import { SignalService } from '../../../../protobuf';
import { Constants } from '../../..';
export abstract class ContentMessage extends Message {
public plainTextBuffer(): Uint8Array {

@ -4,6 +4,7 @@ import { MessageParams } from '../../Message';
import { LokiProfile } from '../../../../../types/Message';
import ByteBuffer from 'bytebuffer';
import { Constants } from '../../../..';
import { isNumber, toNumber } from 'lodash';
export interface AttachmentPointer {
id?: number;
@ -46,6 +47,7 @@ export interface ChatMessageParams extends MessageParams {
expireTimer?: number;
lokiProfile?: LokiProfile;
preview?: Array<Preview>;
syncTarget?: string; // null means it is not a synced message
}
export class ChatMessage extends DataMessage {
@ -59,6 +61,10 @@ export class ChatMessage extends DataMessage {
private readonly avatarPointer?: string;
private readonly preview?: Array<Preview>;
/// In the case of a sync message, the public key of the person the message was targeted at.
/// - Note: `null or undefined` if this isn't a sync message.
private readonly syncTarget?: string;
constructor(params: ChatMessageParams) {
super({ timestamp: params.timestamp, identifier: params.identifier });
this.attachments = params.attachments;
@ -74,6 +80,62 @@ export class ChatMessage extends DataMessage {
this.displayName = params.lokiProfile && params.lokiProfile.displayName;
this.avatarPointer = params.lokiProfile && params.lokiProfile.avatarPointer;
this.preview = params.preview;
this.syncTarget = params.syncTarget;
}
public static buildSyncMessage(
dataMessage: SignalService.IDataMessage,
syncTarget: string,
sentTimestamp: number
) {
const lokiProfile: any = {
profileKey: dataMessage.profileKey,
};
if ((dataMessage as any)?.$type?.name !== 'DataMessage') {
throw new Error(
'Tried to build a sync message from something else than a DataMessage'
);
}
if (!sentTimestamp || !isNumber(sentTimestamp)) {
throw new Error('Tried to build a sync message without a sentTimestamp');
}
if (dataMessage.profile) {
if (dataMessage.profile?.displayName) {
lokiProfile.displayName = dataMessage.profile.displayName;
}
if (dataMessage.profile?.profilePicture) {
lokiProfile.avatarPointer = dataMessage.profile.profilePicture;
}
}
const timestamp = toNumber(sentTimestamp);
const body = dataMessage.body || undefined;
const attachments = (dataMessage.attachments || []).map(attachment => {
return {
...attachment,
key: attachment.key
? new Uint8Array((attachment.key as any).toArrayBuffer())
: undefined,
digest: attachment.digest
? new Uint8Array((attachment.digest as any).toArrayBuffer())
: undefined,
};
}) as Array<AttachmentPointer>;
const quote = (dataMessage.quote as Quote) || undefined;
const preview = (dataMessage.preview as Array<Preview>) || [];
return new ChatMessage({
timestamp,
attachments,
body,
quote,
lokiProfile,
preview,
syncTarget,
});
}
public ttl(): number {
@ -96,6 +158,9 @@ export class ChatMessage extends DataMessage {
if (this.preview) {
dataMessage.preview = this.preview;
}
if (this.syncTarget) {
dataMessage.syncTarget = this.syncTarget;
}
if (this.avatarPointer || this.displayName) {
const profile = new SignalService.DataMessage.LokiProfile();

@ -73,7 +73,7 @@ export class ClosedGroupNewMessage extends ClosedGroupMessage {
fromHexToArray
);
try {
dataMessage.closedGroupControlMessage.encryptionKeyPair = new SignalService.DataMessage.ClosedGroupControlMessage.KeyPair();
dataMessage.closedGroupControlMessage.encryptionKeyPair = new SignalService.KeyPair();
dataMessage.closedGroupControlMessage.encryptionKeyPair.privateKey = new Uint8Array(
this.keypair.privateKeyData
);

@ -1,5 +1,5 @@
import { allowOnlyOneAtATime } from '../../../js/modules/loki_primitives';
import * as Data from '../../../js/modules/data';
import { getGuardNodes } from '../../../js/modules/data';
import * as SnodePool from '../snode_api/snodePool';
import _ from 'lodash';
import fetch from 'node-fetch';
@ -235,7 +235,7 @@ class OnionPaths {
if (this.guardNodes.length === 0) {
// Not cached, load from DB
const nodes = await Data.getGuardNodes();
const nodes = await getGuardNodes();
if (nodes.length === 0) {
log.warn(

@ -5,7 +5,9 @@ import {
MessageQueueInterfaceEvents,
} from './MessageQueueInterface';
import {
ChatMessage,
ContentMessage,
DataMessage,
ExpirationTimerUpdateMessage,
OpenGroupMessage,
} from '../messages/outgoing';
@ -14,6 +16,7 @@ import { JobQueue, TypedEventEmitter, UserUtils } from '../utils';
import { PubKey, RawMessage } from '../types';
import { MessageSender } from '.';
import { ClosedGroupMessage } from '../messages/outgoing/content/data/group/ClosedGroupMessage';
import { ConfigurationMessage } from '../messages/outgoing/content/ConfigurationMessage';
export class MessageQueue implements MessageQueueInterface {
public readonly events: TypedEventEmitter<MessageQueueInterfaceEvents>;
@ -31,9 +34,12 @@ export class MessageQueue implements MessageQueueInterface {
message: ContentMessage,
sentCb?: (message: RawMessage) => Promise<void>
): Promise<void> {
// if (message instanceof SyncMessage) {
// return this.sendSyncMessage(message);
// }
if (
message instanceof ConfigurationMessage ||
!!(message as any).syncTarget
) {
throw new Error('SyncMessage needs to be sent with sendSyncMessage');
}
await this.sendMessageToDevices([user], message);
}
@ -43,9 +49,12 @@ export class MessageQueue implements MessageQueueInterface {
message: ContentMessage,
sentCb?: (message: RawMessage) => Promise<void>
): Promise<void> {
// if (message instanceof SyncMessage) {
// return this.sendSyncMessage(message);
// }
if (
message instanceof ConfigurationMessage ||
!!(message as any).syncTarget
) {
throw new Error('SyncMessage needs to be sent with sendSyncMessage');
}
await this.sendMessageToDevices([device], message, sentCb);
}
@ -107,15 +116,25 @@ export class MessageQueue implements MessageQueueInterface {
}
public async sendSyncMessage(
message: any | undefined,
message?: ContentMessage,
sentCb?: (message: RawMessage) => Promise<void>
): Promise<void> {
if (!message) {
return;
}
if (
!(message instanceof ConfigurationMessage) &&
!(message as any)?.syncTarget
) {
throw new Error('Invalid message given to sendSyncMessage');
}
const ourPubKey = UserUtils.getOurPubKeyStrFromCache();
window.log.warn('sendSyncMessage TODO with syncTarget');
if (!ourPubKey) {
throw new Error('ourNumber is not set');
}
await this.sendMessageToDevices([PubKey.cast(ourPubKey)], message, sentCb);
}
@ -179,7 +198,16 @@ export class MessageQueue implements MessageQueueInterface {
// Don't send to ourselves
const currentDevice = UserUtils.getOurPubKeyFromCache();
if (currentDevice && device.isEqual(currentDevice)) {
return;
// We allow a message for ourselve only if it's a ConfigurationMessage or a message with a syncTarget set
if (
message instanceof ConfigurationMessage ||
(message as any).syncTarget?.length > 0
) {
window.log.warn('Processing sync message');
} else {
window.log.warn('Dropping message in process() to be sent to ourself');
return;
}
}
await this.pendingMessageCache.add(device, message, sentCb);

@ -30,6 +30,9 @@ export interface MessageQueueInterface {
message: GroupMessageType,
sentCb?: (message?: RawMessage) => Promise<void>
): Promise<void>;
sendSyncMessage(message: any): Promise<void>;
sendSyncMessage(
message: any,
sentCb?: (message?: RawMessage) => Promise<void>
): Promise<void>;
processPending(device: PubKey): Promise<void>;
}

@ -9,7 +9,10 @@ import {
requestSnodesForPubkey,
} from './serviceNodeAPI';
import * as Data from '../../../js/modules/data';
import {
getSwarmNodesForPubkey,
updateSwarmNodesForPubkey,
} from '../../../js/modules/data';
import semver from 'semver';
import _ from 'lodash';
@ -329,7 +332,7 @@ export async function updateSnodesFor(
async function internalUpdateSnodesFor(pubkey: string, edkeys: Array<string>) {
nodesForPubkey.set(pubkey, edkeys);
await Data.updateSwarmNodesForPubkey(pubkey, edkeys);
await updateSwarmNodesForPubkey(pubkey, edkeys);
}
export async function getSnodesFor(pubkey: string): Promise<Array<Snode>> {
@ -339,7 +342,7 @@ export async function getSnodesFor(pubkey: string): Promise<Array<Snode>> {
// NOTE: important that maybeNodes is not [] here
if (maybeNodes === undefined) {
// First time access, try the database:
nodes = await Data.getSwarmNodesForPubkey(pubkey);
nodes = await getSwarmNodesForPubkey(pubkey);
nodesForPubkey.set(pubkey, nodes);
} else {
nodes = maybeNodes;

@ -4,7 +4,12 @@ import { retrieveNextMessages } from './serviceNodeAPI';
import { SignalService } from '../../protobuf';
import * as Receiver from '../../receiver/receiver';
import _ from 'lodash';
import * as Data from '../../../js/modules/data';
import {
getLastHashBySnode,
getSeenMessagesByHashList,
saveSeenMessageHashes,
updateLastHash,
} from '../../../js/modules/data';
import { StringUtils } from '../../session/utils';
import { ConversationController } from '../conversations';
@ -180,7 +185,7 @@ export class SwarmPolling {
const incomingHashes = messages.map((m: Message) => m.hash);
const dupHashes = await Data.getSeenMessagesByHashList(incomingHashes);
const dupHashes = await getSeenMessagesByHashList(incomingHashes);
const newMessages = messages.filter(
(m: Message) => !dupHashes.includes(m.hash)
);
@ -190,7 +195,7 @@ export class SwarmPolling {
expiresAt: m.expiration,
hash: m.hash,
}));
await Data.saveSeenMessageHashes(newHashes);
await saveSeenMessageHashes(newHashes);
}
return newMessages;
}
@ -220,7 +225,7 @@ export class SwarmPolling {
): Promise<void> {
const pkStr = pubkey.key;
await Data.updateLastHash({
await updateLastHash({
convoId: pkStr,
snode: edkey,
hash,
@ -243,7 +248,7 @@ export class SwarmPolling {
const nodeRecords = this.lastHashes[nodeEdKey];
if (!nodeRecords || !nodeRecords[pubkey]) {
const lastHash = await Data.getLastHashBySnode(pubkey, nodeEdKey);
const lastHash = await getLastHashBySnode(pubkey, nodeEdKey);
return lastHash || '';
} else {

@ -1,6 +1,7 @@
import { ConversationModel } from '../../models/conversation';
import { ConversationController } from '../conversations';
import { PromiseUtils } from '../utils';
import { forceSyncConfigurationNowIfNeeded } from '../utils/syncUtils';
interface OpenGroupParams {
server: string;
@ -52,6 +53,15 @@ export class OpenGroup {
return this.serverRegex.test(serverUrl);
}
public static getAllAlreadyJoinedOpenGroupsUrl(): Array<string> {
const convos = ConversationController.getInstance().getConversations();
return convos
.filter(c => !!c.get('active_at') && c.isPublic() && !c.get('left'))
.map(c => c.id.substring((c.id as string).lastIndexOf('@') + 1)) as Array<
string
>;
}
/**
* Try to make a new instance of `OpenGroup`.
* This does NOT respect `ConversationController` and does not guarentee the conversation's existence.
@ -95,7 +105,10 @@ export class OpenGroup {
* @param onLoading Callback function to be called once server begins connecting
* @returns `OpenGroup` if connection success or if already connected
*/
public static async join(server: string): Promise<OpenGroup | undefined> {
public static async join(
server: string,
fromSyncMessage: boolean = false
): Promise<OpenGroup | undefined> {
const prefixedServer = OpenGroup.prefixify(server);
if (!OpenGroup.validate(server)) {
return;
@ -130,6 +143,12 @@ export class OpenGroup {
throw new Error(window.i18n('connectToServerFail'));
}
conversationId = (conversation as any)?.cid;
// here we managed to connect to the group.
// if this is not a Sync Message, we should trigger one
if (!fromSyncMessage) {
await forceSyncConfigurationNowIfNeeded();
}
} catch (e) {
throw new Error(e);
}

@ -2,11 +2,20 @@ import { RawMessage } from '../types/RawMessage';
import {
ContentMessage,
ExpirationTimerUpdateMessage,
TypingMessage,
} from '../messages/outgoing';
import { EncryptionType, PubKey } from '../types';
import { ClosedGroupMessage } from '../messages/outgoing/content/data/group/ClosedGroupMessage';
import { ClosedGroupNewMessage } from '../messages/outgoing/content/data/group/ClosedGroupNewMessage';
import {
ConfigurationMessage,
ConfigurationMessageClosedGroup,
} from '../messages/outgoing/content/ConfigurationMessage';
import uuid from 'uuid';
import { getLatestClosedGroupEncryptionKeyPair } from '../../../js/modules/data';
import { UserUtils } from '.';
import { ECKeyPair } from '../../receiver/keypairs';
import _ from 'lodash';
import { ConversationModel } from '../../models/conversation';
export function getEncryptionTypeFromMessageType(
message: ContentMessage
@ -51,3 +60,53 @@ export async function toRawMessage(
return rawMessage;
}
export const getCurrentConfigurationMessage = async (
convos: Array<ConversationModel>
) => {
const ourPubKey = UserUtils.getOurPubKeyStrFromCache();
const openGroupsIds = convos
.filter(c => !!c.get('active_at') && c.isPublic() && !c.get('left'))
.map(c => c.id.substring((c.id as string).lastIndexOf('@') + 1)) as Array<
string
>;
const closedGroupModels = convos.filter(
c =>
!!c.get('active_at') &&
c.isMediumGroup() &&
c.get('members').includes(ourPubKey) &&
!c.get('left') &&
!c.get('isKickedFromGroup') &&
!c.isBlocked()
);
const closedGroups = await Promise.all(
closedGroupModels.map(async c => {
const groupPubKey = c.get('id');
const fetchEncryptionKeyPair = await getLatestClosedGroupEncryptionKeyPair(
groupPubKey
);
if (!fetchEncryptionKeyPair) {
return null;
}
return new ConfigurationMessageClosedGroup({
publicKey: groupPubKey,
name: c.get('name') || '',
members: c.get('members') || [],
admins: c.get('groupAdmins') || [],
encryptionKeyPair: ECKeyPair.fromHexKeyPair(fetchEncryptionKeyPair),
});
})
);
const onlyValidClosedGroup = closedGroups.filter(m => m !== null) as Array<
ConfigurationMessageClosedGroup
>;
return new ConfigurationMessage({
identifier: uuid(),
timestamp: Date.now(),
activeOpenGroups: openGroupsIds,
activeClosedGroups: onlyValidClosedGroup,
});
};

@ -7,6 +7,7 @@ import * as ProtobufUtils from './Protobuf';
import * as MenuUtils from '../../components/session/menu/Menu';
import * as ToastUtils from './Toast';
import * as UserUtils from './User';
import * as SyncUtils from './syncUtils';
export * from './Attachments';
export * from './TypedEmitter';
@ -22,4 +23,5 @@ export {
MenuUtils,
ToastUtils,
UserUtils,
SyncUtils,
};

@ -0,0 +1,80 @@
import { createOrUpdateItem, getItemById } from '../../../js/modules/data';
import { getMessageQueue } from '..';
import { ConversationController } from '../conversations';
import { getCurrentConfigurationMessage } from './Messages';
import { RawMessage } from '../types';
import { DAYS } from './Number';
const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp';
const getLastSyncTimestampFromDb = async (): Promise<number | undefined> =>
(await getItemById(ITEM_ID_LAST_SYNC_TIMESTAMP))?.value;
const writeLastSyncTimestampToDb = async (timestamp: number) =>
createOrUpdateItem({ id: ITEM_ID_LAST_SYNC_TIMESTAMP, value: timestamp });
export const syncConfigurationIfNeeded = async () => {
const lastSyncedTimestamp = (await getLastSyncTimestampFromDb()) || 0;
const now = Date.now();
// if the last sync was less than 2 days before, return early.
if (Math.abs(now - lastSyncedTimestamp) < DAYS * 2) {
return;
}
const allConvos = ConversationController.getInstance().getConversations();
const configMessage = await getCurrentConfigurationMessage(allConvos);
try {
window.log.info('syncConfigurationIfNeeded with', configMessage);
await getMessageQueue().sendSyncMessage(configMessage);
} catch (e) {
window.log.warn(
'Caught an error while sending our ConfigurationMessage:',
e
);
// we do return early so that next time we use the old timestamp again
// and so try again to trigger a sync
return;
}
await writeLastSyncTimestampToDb(now);
};
export const forceSyncConfigurationNowIfNeeded = async (
waitForMessageSent = false
) => {
const allConvos = ConversationController.getInstance().getConversations();
const configMessage = await getCurrentConfigurationMessage(allConvos);
window.log.info('forceSyncConfigurationNowIfNeeded with', configMessage);
const waitForMessageSentEvent = new Promise(resolve => {
const ourResolver = (message: any) => {
if (message.identifier === configMessage.identifier) {
getMessageQueue().events.off('sendSuccess', ourResolver);
getMessageQueue().events.off('sendFail', ourResolver);
resolve(true);
}
};
getMessageQueue().events.on('sendSuccess', ourResolver);
getMessageQueue().events.on('sendFail', ourResolver);
});
try {
// this just adds the message to the sending queue.
// if waitForMessageSent is set, we need to effectively wait until then
await Promise.all([
getMessageQueue().sendSyncMessage(configMessage),
waitForMessageSentEvent,
]);
} catch (e) {
window.log.warn(
'Caught an error while sending our ConfigurationMessage:',
e
);
}
if (!waitForMessageSent) {
return;
}
return waitForMessageSentEvent;
};

@ -1,6 +1,7 @@
import { getPasswordHash } from '../../js/modules/data';
export async function hasPassword() {
// @ts-ignore
const hash = await window.Signal.Data.getPasswordHash();
const hash = await getPasswordHash();
return !!hash;
}

@ -4,6 +4,7 @@ import { Constants } from '../../session';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { ConversationController } from '../../session/conversations';
import { MessageCollection, MessageModel } from '../../models/message';
import { getMessagesByConversation } from '../../../js/modules/data';
// State
@ -112,10 +113,10 @@ async function getMessages(
msgCount = Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT;
}
const messageSet = await window.Signal.Data.getMessagesByConversation(
conversationKey,
{ limit: msgCount, MessageCollection }
);
const messageSet = await getMessagesByConversation(conversationKey, {
limit: msgCount,
MessageCollection,
});
// Set first member of series here.
const messageModels = messageSet.models;

@ -0,0 +1,141 @@
import { expect } from 'chai';
import { ECKeyPair } from '../../../../receiver/keypairs';
import {
ConfigurationMessage,
ConfigurationMessageClosedGroup,
} from '../../../../session/messages/outgoing/content/ConfigurationMessage';
import { PubKey } from '../../../../session/types';
import { TestUtils } from '../../../test-utils';
describe('ConfigurationMessage', () => {
it('throw if closed group is not set', () => {
const activeClosedGroups = null as any;
const params = {
activeClosedGroups,
activeOpenGroups: [],
timestamp: Date.now(),
};
expect(() => new ConfigurationMessage(params)).to.throw(
'closed group must be set'
);
});
it('throw if open group is not set', () => {
const activeOpenGroups = null as any;
const params = {
activeClosedGroups: [],
activeOpenGroups,
timestamp: Date.now(),
};
expect(() => new ConfigurationMessage(params)).to.throw(
'open group must be set'
);
});
describe('ConfigurationMessageClosedGroup', () => {
it('throw if closed group has no encryptionkeypair', () => {
const member = TestUtils.generateFakePubKey().key;
const params = {
publicKey: TestUtils.generateFakePubKey().key,
name: 'groupname',
members: [member],
admins: [member],
encryptionKeyPair: undefined as any,
};
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
'Encryption key pair looks invalid'
);
});
it('throw if closed group has invalid encryptionkeypair', () => {
const member = TestUtils.generateFakePubKey().key;
const params = {
publicKey: TestUtils.generateFakePubKey().key,
name: 'groupname',
members: [member],
admins: [member],
encryptionKeyPair: new ECKeyPair(new Uint8Array(), new Uint8Array()),
};
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
'Encryption key pair looks invalid'
);
});
it('throw if closed group has invalid pubkey', () => {
const member = TestUtils.generateFakePubKey().key;
const params = {
publicKey: 'invalidpubkey',
name: 'groupname',
members: [member],
admins: [member],
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
};
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw();
});
it('throw if closed group has invalid name', () => {
const member = TestUtils.generateFakePubKey().key;
const params = {
publicKey: TestUtils.generateFakePubKey().key,
name: '',
members: [member],
admins: [member],
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
};
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
'name must be set'
);
});
it('throw if members is empty', () => {
const member = TestUtils.generateFakePubKey().key;
const params = {
publicKey: TestUtils.generateFakePubKey().key,
name: 'groupname',
members: [],
admins: [member],
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
};
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
'members must be set'
);
});
it('throw if admins is empty', () => {
const member = TestUtils.generateFakePubKey().key;
const params = {
publicKey: TestUtils.generateFakePubKey().key,
name: 'groupname',
members: [member],
admins: [],
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
};
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
'admins must be set'
);
});
it('throw if some admins are not members', () => {
const member = TestUtils.generateFakePubKey().key;
const admin = TestUtils.generateFakePubKey().key;
const params = {
publicKey: TestUtils.generateFakePubKey().key,
name: 'groupname',
members: [member],
admins: [admin],
encryptionKeyPair: TestUtils.generateFakeECKeyPair(),
};
expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(
'some admins are not members'
);
});
});
});

@ -1,7 +1,7 @@
import chai from 'chai';
import * as sinon from 'sinon';
import { TestUtils } from '../../../test-utils';
import { MessageUtils } from '../../../../session/utils';
import { MessageUtils, UserUtils } from '../../../../session/utils';
import { EncryptionType, PubKey } from '../../../../session/types';
import { ClosedGroupChatMessage } from '../../../../session/messages/outgoing/content/data/group/ClosedGroupChatMessage';
import {
@ -14,6 +14,9 @@ import {
ClosedGroupNameChangeMessage,
ClosedGroupRemovedMembersMessage,
} from '../../../../session/messages/outgoing/content/data/group';
import { MockConversation } from '../../../test-utils/utils';
import { ConfigurationMessage } from '../../../../session/messages/outgoing/content/ConfigurationMessage';
import { ConversationModel } from '../../../../models/conversation';
// tslint:disable-next-line: no-require-imports no-var-requires
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
@ -203,5 +206,72 @@ describe('Message Utils', () => {
const rawMessage = await MessageUtils.toRawMessage(device, msg);
expect(rawMessage.encryption).to.equal(EncryptionType.ClosedGroup);
});
it('passing a ConfigurationMessage returns Fallback', async () => {
const device = TestUtils.generateFakePubKey();
const msg = new ConfigurationMessage({
timestamp: Date.now(),
activeOpenGroups: [],
activeClosedGroups: [],
});
const rawMessage = await MessageUtils.toRawMessage(device, msg);
expect(rawMessage.encryption).to.equal(EncryptionType.Fallback);
});
});
describe('getCurrentConfigurationMessage', () => {
const ourNumber = TestUtils.generateFakePubKey().key;
let convos: Array<ConversationModel>;
const mockValidOpenGroup = new MockConversation({
type: 'public',
id: 'publicChat:1@chat-dev.lokinet.org',
});
const mockValidOpenGroup2 = new MockConversation({
type: 'public',
id: 'publicChat:1@chat-dev2.lokinet.org',
});
const mockValidClosedGroup = new MockConversation({
type: 'group',
});
const mockValidPrivate = {
id: TestUtils.generateFakePubKey(),
isMediumGroup: () => false,
isPublic: () => false,
};
beforeEach(() => {
convos = [];
sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').resolves(ourNumber);
sandbox
.stub(UserUtils, 'getOurPubKeyFromCache')
.resolves(PubKey.cast(ourNumber));
});
beforeEach(() => {
convos = [];
sandbox.restore();
});
// it('filter out non active open groups', async () => {
// // override the first open group and make it inactive
// (mockValidOpenGroup as any).attributes.active_at = undefined;
// convos.push(
// mockValidOpenGroup as any,
// mockValidOpenGroup as any,
// mockValidPrivate as any,
// mockValidClosedGroup as any,
// mockValidOpenGroup2 as any
// );
// const configMessage = await getCurrentConfigurationMessage(convos);
// expect(configMessage.activeOpenGroups.length).to.equal(1);
// expect(configMessage.activeOpenGroups[0]).to.equal('chat-dev2.lokinet.org');
// });
});
});

@ -58,7 +58,7 @@ describe('Password Util', () => {
});
it('should return an error if password is not between 6 and 64 characters', () => {
const invalid = ['a', 'abcde', '#'.repeat(51), '#'.repeat(100)];
const invalid = ['a', 'abcde', '#'.repeat(65), '#'.repeat(100)];
invalid.forEach(pass => {
assert.strictEqual(
PasswordUtil.validatePassword(pass),

@ -0,0 +1,31 @@
import chai from 'chai';
import * as sinon from 'sinon';
import { ConversationController } from '../../../../session/conversations';
import * as MessageUtils from '../../../../session/utils/Messages';
import { syncConfigurationIfNeeded } from '../../../../session/utils/syncUtils';
import { TestUtils } from '../../../test-utils';
import { restoreStubs } from '../../../test-utils/utils';
// tslint:disable-next-line: no-require-imports no-var-requires
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const { expect } = chai;
describe('SyncUtils', () => {
const sandbox = sinon.createSandbox();
afterEach(() => {
sandbox.restore();
restoreStubs();
});
describe('syncConfigurationIfNeeded', () => {
it('sync if last sync undefined', async () => {
// TestUtils.stubData('getItemById').resolves(undefined);
// sandbox.stub(ConversationController.getInstance(), 'getConversations').returns([]);
// const getCurrentConfigurationMessageSpy = sandbox.spy(MessageUtils, 'getCurrentConfigurationMessage');
// await syncConfigurationIfNeeded();
// expect(getCurrentConfigurationMessageSpy.callCount).equal(1);
});
});
});

@ -50,44 +50,36 @@ export function generateClosedGroupMessage(
interface MockConversationParams {
id?: string;
type: MockConversationType;
members?: Array<string>;
}
export enum MockConversationType {
Primary = 'primary',
Secondary = 'secondary',
Group = 'group',
type: 'private' | 'group' | 'public';
isMediumGroup?: boolean;
}
export class MockConversation {
public id: string;
public type: MockConversationType;
public type: 'private' | 'group' | 'public';
public attributes: ConversationAttributes;
public isPrimary?: boolean;
constructor(params: MockConversationParams) {
const dayInSeconds = 86400;
this.type = params.type;
this.id = params.id ?? generateFakePubKey().key;
this.isPrimary = this.type === MockConversationType.Primary;
const members =
this.type === MockConversationType.Group
? params.members ?? generateFakePubKeys(10).map(m => m.key)
: [];
const members = params.isMediumGroup
? params.members ?? generateFakePubKeys(10).map(m => m.key)
: [];
this.type = params.type;
this.attributes = {
id: this.id,
name: '',
type: '',
type: params.type === 'public' ? 'group' : params.type,
members,
left: false,
expireTimer: dayInSeconds,
expireTimer: 0,
profileSharing: true,
mentionedUs: false,
unreadCount: 99,
unreadCount: 5,
isKickedFromGroup: false,
active_at: Date.now(),
lastJoinedTimestamp: Date.now(),
lastMessageStatus: null,
@ -95,13 +87,21 @@ export class MockConversation {
}
public isPrivate() {
return true;
return this.type === 'private';
}
public isBlocked() {
return false;
}
public isPublic() {
return this.id.match(/^publicChat:/);
}
public isMediumGroup() {
return this.type === 'group';
}
public get(obj: string) {
return (this.attributes as any)[obj];
}

@ -13,6 +13,8 @@ const sha512 = (text: string) => {
return hash.digest('hex');
};
export const MAX_PASSWORD_LENGTH = 64;
export const generateHash = (phrase: string) => phrase && sha512(phrase.trim());
export const matchesHash = (phrase: string | null, hash: string) =>
phrase && sha512(phrase.trim()) === hash.trim();
@ -27,10 +29,7 @@ export const validatePassword = (phrase: string, i18n?: LocalizerType) => {
return i18n ? i18n('noGivenPassword') : ERRORS.LENGTH;
}
if (
trimmed.length < 6 ||
trimmed.length > window.CONSTANTS.MAX_PASSWORD_LENGTH
) {
if (trimmed.length < 6 || trimmed.length > MAX_PASSWORD_LENGTH) {
return i18n ? i18n('passwordLengthError') : ERRORS.LENGTH;
}

Loading…
Cancel
Save