fix: remove all of the ConfiguratioMessage (legacy) logic

pull/2963/head
Audric Ackermann 2 years ago
parent 0fbb0cc852
commit c9b2d69a73

@ -52,11 +52,12 @@ message SharedConfigMessage {
}
message Content {
reserved 7;
reserved "configurationMessage";
optional DataMessage dataMessage = 1;
optional CallMessage callMessage = 3;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
optional ConfigurationMessage configurationMessage = 7;
optional DataExtractionNotification dataExtractionNotification = 8;
optional Unsend unsendMessage = 9;
optional MessageRequestResponse messageRequestResponse = 10;
@ -212,36 +213,6 @@ message CallMessage {
}
message ConfigurationMessage {
message ClosedGroup {
optional bytes publicKey = 1;
optional string name = 2;
optional KeyPair encryptionKeyPair = 3;
repeated bytes members = 4;
repeated bytes admins = 5;
}
message Contact {
// @required
required bytes publicKey = 1;
// @required
required string name = 2;
optional string profilePicture = 3;
optional bytes profileKey = 4;
optional bool isApproved = 5;
optional bool isBlocked = 6;
optional bool didApproveMe = 7;
}
repeated ClosedGroup closedGroups = 1;
repeated string openGroups = 2;
optional string displayName = 3;
optional string profilePicture = 4;
optional bytes profileKey = 5;
repeated Contact contacts = 6;
}
message ReceiptMessage {
enum Type {

@ -9,7 +9,6 @@ import useTimeoutFn from 'react-use/lib/useTimeoutFn';
import { Data } from '../../data/data';
import { ConvoHub } from '../../session/conversations';
import { getMessageQueue } from '../../session/sending';
import { syncConfigurationIfNeeded } from '../../session/utils/sync/syncUtils';
import { clearSearch } from '../../state/ducks/search';
import { resetOverlayMode, SectionType, showLeftPaneSection } from '../../state/ducks/section';
@ -42,10 +41,11 @@ import {
forceRefreshRandomSnodePool,
getFreshSwarmFor,
} from '../../session/apis/snode_api/snodePool';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/sync/syncUtils';
import { isDarkTheme } from '../../state/selectors/theme';
import { ThemeStateType } from '../../themes/constants/colors';
import { switchThemeTo } from '../../themes/switchTheme';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
const Section = (props: { type: SectionType }) => {
const ourNumber = useSelector(getOurNumber);
@ -167,7 +167,7 @@ const triggerSyncIfNeeded = async () => {
const didWeHandleAConfigurationMessageAlready =
(await Data.getItemById(SettingsKey.hasSyncedInitialConfigurationItem))?.value || false;
if (didWeHandleAConfigurationMessageAlready) {
await syncConfigurationIfNeeded();
await forceSyncConfigurationNowIfNeeded();
}
};
@ -267,7 +267,7 @@ export const ActionsPanel = () => {
if (!ourPrimaryConversation) {
return;
}
void syncConfigurationIfNeeded();
void forceSyncConfigurationNowIfNeeded();
}, DURATION.DAYS * 2);
useInterval(() => {

@ -1,12 +1,12 @@
import React, { createContext, useEffect, useState } from 'react';
import { SignUpMode, SignUpTab } from './SignUpTab';
import { SignInMode, SignInTab } from './SignInTab';
import { Data } from '../../data/data';
import { SettingsKey } from '../../data/settings-key';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { ConvoHub } from '../../session/conversations';
import { mnDecode } from '../../session/crypto/mnemonic';
import { PromiseUtils, StringUtils, ToastUtils } from '../../session/utils';
import { TaskTimedOutError } from '../../session/utils/Promise';
import { fromHex } from '../../session/utils/String';
import { trigger } from '../../shims/events';
import {
generateMnemonic,
@ -14,9 +14,9 @@ import {
sessionGenerateKeyPair,
signInByLinkingDevice,
} from '../../util/accountManager';
import { fromHex } from '../../session/utils/String';
import { setSignInByLinking, setSignWithRecoveryPhrase, Storage } from '../../util/storage';
import { SettingsKey } from '../../data/settings-key';
import { Storage, setSignInByLinking, setSignWithRecoveryPhrase } from '../../util/storage';
import { SignInMode, SignInTab } from './SignInTab';
import { SignUpMode, SignUpTab } from './SignUpTab';
export async function resetRegistration() {
await Data.removeAll();
@ -71,8 +71,7 @@ export async function signUp(signUpDetails: {
/**
* Sign in/restore from seed.
* Ask for a display name, as we will drop incoming ConfigurationMessages if any are saved on the swarm.
* We will handle a ConfigurationMessage
* Ask for a display name, as we will drop incoming libsession updates if any are saved on the swarm.
*/
export async function signInWithRecovery(signInDetails: {
displayName: string;
@ -102,7 +101,7 @@ export async function signInWithRecovery(signInDetails: {
/**
* This is will try to sign in with the user recovery phrase.
* If no ConfigurationMessage is received in 60seconds, the loading will be canceled.
* If no libsession updates is received in 60seconds, the loading will be canceled.
*/
export async function signInWithLinking(signInDetails: { userRecoveryPhrase: string }) {
const { userRecoveryPhrase } = signInDetails;

@ -22,10 +22,10 @@ import { perfEnd, perfStart } from '../session/utils/Performance';
import { ReleasedFeatures } from '../util/releaseFeature';
import { Storage } from '../util/storage';
// eslint-disable-next-line import/no-unresolved, import/extensions
import { ConfigWrapperUser } from '../webworker/workers/browser/libsession_worker_functions';
import { getSettingsKeyFromLibsessionWrapper } from './configMessage';
import { ECKeyPair, HexKeyPair } from './keypairs';
import { queueAllCachedFromSource } from './receiver';
import { ConfigWrapperUser } from '../webworker/workers/browser/libsession_worker_functions';
export const distributingClosedGroupEncryptionKeyPairs = new Map<string, ECKeyPair>();
@ -101,7 +101,7 @@ export async function handleClosedGroupControlMessage(
return;
}
// We drop New closed group message from our other devices, as they will come as ConfigurationMessage instead
// We drop New closed group message from our other devices, as they will come through libsession instead
if (type === Type.ENCRYPTION_KEY_PAIR) {
const isComingFromGroupPubkey =
envelope.type === SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE;

@ -483,11 +483,7 @@ export async function innerHandleSwarmContentMessage(
perfEnd(`handleTypingMessage-${envelope.id}`, 'handleTypingMessage');
return;
}
if (content.configurationMessage) {
// we drop support for legacy configuration message
await removeFromCache(envelope);
return;
}
if (content.sharedConfigMessage) {
window.log.warn('content.sharedConfigMessage are handled outside of the receiving pipeline');
// this should never happen, but remove it from cache just in case something is messed up
@ -509,16 +505,23 @@ export async function innerHandleSwarmContentMessage(
}
if (content.unsendMessage) {
await handleUnsendMessage(envelope, content.unsendMessage as SignalService.Unsend);
return;
}
if (content.callMessage) {
await handleCallMessage(envelope, content.callMessage as SignalService.CallMessage);
return;
}
if (content.messageRequestResponse) {
await handleMessageRequestResponse(
envelope,
content.messageRequestResponse as SignalService.MessageRequestResponse
);
return;
}
// If we get here, we don't know how to handle that envelope. probably a very old type of message, or something we don't support.
// There is not much we can do expect drop it
await removeFromCache(envelope);
} catch (e) {
window?.log?.warn(e.message);
}

@ -12,13 +12,13 @@ import { ConversationTypeEnum, READ_MESSAGE_STATE } from '../models/conversation
import { MessageDirection } from '../models/messageType';
import { SignalService } from '../protobuf';
import { ProfileManager } from '../session/profile_manager/ProfileManager';
import { PubKey } from '../session/types';
import { PropsForMessageWithoutConvoProps, lookupQuote } from '../state/ducks/conversations';
import { showMessageRequestBannerOutsideRedux } from '../state/ducks/userConfig';
import { getHideMessageRequestBannerOutsideRedux } from '../state/selectors/userConfig';
import { GoogleChrome } from '../util';
import { LinkPreviews } from '../util/linkPreviews';
import { ReleasedFeatures } from '../util/releaseFeature';
import { PropsForMessageWithoutConvoProps, lookupQuote } from '../state/ducks/conversations';
import { PubKey } from '../session/types';
function contentTypeSupported(type: string): boolean {
const Chrome = GoogleChrome;
@ -436,7 +436,7 @@ export async function handleMessageJob(
void queueAttachmentDownloads(messageModel, conversation);
// Check if we need to update any profile names
// the only profile we don't update with what is coming here is ours,
// as our profile is shared across our devices with a ConfigurationMessage
// as our profile is shared across our devices with libsession
if (messageModel.isIncoming() && regularDataMessage.profile) {
await ProfileManager.updateProfileOfContact(
sendingDeviceConversation.id,

@ -6,7 +6,6 @@ type IncomingMessageAvailableTypes =
| SignalService.CallMessage
| SignalService.ReceiptMessage
| SignalService.TypingMessage
| SignalService.ConfigurationMessage
| SignalService.DataExtractionNotification
| SignalService.Unsend
| SignalService.MessageRequestResponse

@ -1,217 +0,0 @@
// 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 { SignalService } from '../../../../protobuf';
import { MessageParams } from '../Message';
import { ECKeyPair } from '../../../../receiver/keypairs';
import { fromHexToArray } from '../../../utils/String';
import { PubKey } from '../../../types';
import { ContentMessage } from '..';
interface ConfigurationMessageParams extends MessageParams {
activeClosedGroups: Array<ConfigurationMessageClosedGroup>;
activeOpenGroups: Array<string>;
displayName: string;
profilePicture?: string;
profileKey?: Uint8Array;
contacts: Array<ConfigurationMessageContact>;
}
export class ConfigurationMessage extends ContentMessage {
public readonly activeClosedGroups: Array<ConfigurationMessageClosedGroup>;
public readonly activeOpenGroups: Array<string>;
public readonly displayName: string;
public readonly profilePicture?: string;
public readonly profileKey?: Uint8Array;
public readonly contacts: Array<ConfigurationMessageContact>;
constructor(params: ConfigurationMessageParams) {
super({ timestamp: params.timestamp, identifier: params.identifier });
this.activeClosedGroups = params.activeClosedGroups;
this.activeOpenGroups = params.activeOpenGroups;
this.displayName = params.displayName;
this.profilePicture = params.profilePicture;
this.profileKey = params.profileKey;
this.contacts = params.contacts;
if (!this.activeClosedGroups) {
throw new Error('closed group must be set');
}
if (!this.activeOpenGroups) {
throw new Error('open group must be set');
}
if (!this.displayName || !this.displayName?.length) {
throw new Error('displayName must be set');
}
if (this.profilePicture && typeof this.profilePicture !== 'string') {
throw new Error('profilePicture set but not an Uin8Array');
}
if (this.profileKey && !(this.profileKey instanceof Uint8Array)) {
throw new Error('profileKey set but not an Uin8Array');
}
if (!this.contacts) {
throw new Error('contacts must be set');
}
}
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,
displayName: this.displayName,
profilePicture: this.profilePicture,
profileKey: this.profileKey,
contacts: this.mapContactsObjectToProto(this.contacts),
});
}
private mapClosedGroupsObjectToProto(
closedGroups: Array<ConfigurationMessageClosedGroup>
): Array<SignalService.ConfigurationMessage.ClosedGroup> {
return (closedGroups || []).map(m => m.toProto());
}
private mapContactsObjectToProto(
contacts: Array<ConfigurationMessageContact>
): Array<SignalService.ConfigurationMessage.Contact> {
return (contacts || []).map(m => m.toProto());
}
}
export class ConfigurationMessageContact {
public publicKey: string;
public displayName: string;
public profilePictureURL?: string;
public profileKey?: Uint8Array;
public isApproved?: boolean;
public isBlocked?: boolean;
public didApproveMe?: boolean;
public constructor({
publicKey,
displayName,
profilePictureURL,
profileKey,
isApproved,
isBlocked,
didApproveMe,
}: {
publicKey: string;
displayName: string;
profilePictureURL?: string;
profileKey?: Uint8Array;
isApproved?: boolean;
isBlocked?: boolean;
didApproveMe?: boolean;
}) {
this.publicKey = publicKey;
this.displayName = displayName;
this.profilePictureURL = profilePictureURL;
this.profileKey = profileKey;
this.isApproved = isApproved;
this.isBlocked = isBlocked;
this.didApproveMe = didApproveMe;
// will throw if public key is invalid
PubKey.cast(publicKey);
if (this.displayName?.length === 0) {
throw new Error('displayName must be set or undefined');
}
if (this.profilePictureURL !== undefined && this.profilePictureURL?.length === 0) {
throw new Error('profilePictureURL must either undefined or not empty');
}
if (this.profileKey !== undefined && this.profileKey?.length === 0) {
throw new Error('profileKey must either undefined or not empty');
}
}
public toProto(): SignalService.ConfigurationMessage.Contact {
return new SignalService.ConfigurationMessage.Contact({
publicKey: fromHexToArray(this.publicKey),
name: this.displayName,
profilePicture: this.profilePictureURL,
profileKey: this.profileKey,
isApproved: this.isApproved,
isBlocked: this.isBlocked,
didApproveMe: this.didApproveMe,
});
}
}
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,7 +1,6 @@
import { AbortController } from 'abort-controller';
import { MessageSender } from '.';
import { ConfigurationMessage } from '../messages/outgoing/controlMessage/ConfigurationMessage';
import { ClosedGroupMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMessage';
import { ClosedGroupNameChangeMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage';
import { PubKey, RawMessage } from '../types';
@ -62,7 +61,7 @@ export class MessageQueue {
sentCb?: (message: RawMessage) => Promise<void>,
isGroup = false
): Promise<void> {
if (message instanceof ConfigurationMessage || !!(message as any).syncTarget) {
if ((message as any).syncTarget) {
throw new Error('SyncMessage needs to be sent with sendSyncMessage');
}
await this.process(destinationPubKey, message, namespace, sentCb, isGroup);
@ -230,11 +229,7 @@ export class MessageQueue {
if (!message) {
return;
}
if (
!(message instanceof ConfigurationMessage) &&
!(message instanceof UnsendMessage) &&
!(message as any)?.syncTarget
) {
if (!(message instanceof UnsendMessage) && !(message as any)?.syncTarget) {
throw new Error('Invalid message given to sendSyncMessage');
}
@ -347,7 +342,7 @@ export class MessageQueue {
const us = UserUtils.getOurPubKeyFromCache();
let isSyncMessage = false;
if (us && destinationPk.isEqual(us)) {
// We allow a message for ourselves only if it's a ConfigurationMessage, a ClosedGroupNewMessage,
// We allow a message for ourselves only if it's a ClosedGroupNewMessage,
// or a message with a syncTarget set.
if (MessageSender.isSyncMessage(message)) {

@ -29,7 +29,6 @@ import { ConvoHub } from '../conversations';
import { MessageEncrypter } from '../crypto';
import { addMessagePadding } from '../crypto/BufferPadding';
import { ContentMessage } from '../messages/outgoing';
import { ConfigurationMessage } from '../messages/outgoing/controlMessage/ConfigurationMessage';
import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage';
import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
@ -79,7 +78,6 @@ function getMinRetryTimeout() {
function isSyncMessage(message: ContentMessage) {
if (
message instanceof ConfigurationMessage ||
message instanceof ClosedGroupNewMessage ||
message instanceof UnsendMessage ||
(message as any).syncTarget?.length > 0

@ -1,12 +1,12 @@
import _ from 'lodash';
import { UserUtils } from '.';
import { Data } from '../../data/data';
import { SessionKeyPair } from '../../receiver/keypairs';
import { LokiProfile } from '../../types/Message';
import { getOurPubKeyStrFromStorage } from '../../util/storage';
import { ConvoHub } from '../conversations';
import { PubKey } from '../types';
import { fromHexToArray, toHex } from './String';
import { ConvoHub } from '../conversations';
import { LokiProfile } from '../../types/Message';
import { getOurPubKeyStrFromStorage, Storage } from '../../util/storage';
import { SessionKeyPair } from '../../receiver/keypairs';
export type HexKeyPair = {
pubKey: string;
@ -100,9 +100,7 @@ export const getUserED25519KeyPairBytes = async (): Promise<ByteKeyPair | undefi
export function getOurProfile(): LokiProfile | undefined {
try {
// Secondary devices have their profile stored
// in their primary device's conversation
const ourNumber = Storage.get('primaryDevicePubKey') as string;
const ourNumber = UserUtils.getOurPubKeyStrFromCache();
const ourConversation = ConvoHub.use().get(ourNumber);
const ourProfileKeyHex = ourConversation.getProfileKey();
const profileKeyAsBytes = ourProfileKeyHex ? fromHexToArray(ourProfileKeyHex) : null;

@ -1,24 +1,8 @@
import _ from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { UserUtils } from '..';
import { getMessageQueue } from '../..';
import { Data } from '../../../data/data';
import { OpenGroupData } from '../../../data/opengroups';
import { ConversationModel } from '../../../models/conversation';
import { SignalService } from '../../../protobuf';
import { ECKeyPair } from '../../../receiver/keypairs';
import { UserSyncJobDone } from '../../../shims/events';
import { ReleasedFeatures } from '../../../util/releaseFeature';
import { Storage } from '../../../util/storage';
import { getCompleteUrlFromRoom } from '../../apis/open_group_api/utils/OpenGroupUtils';
import { SnodeNamespaces } from '../../apis/snode_api/namespaces';
import { DURATION } from '../../constants';
import { ConvoHub } from '../../conversations';
import {
ConfigurationMessage,
ConfigurationMessageClosedGroup,
ConfigurationMessageContact,
} from '../../messages/outgoing/controlMessage/ConfigurationMessage';
import { ExpirationTimerUpdateMessage } from '../../messages/outgoing/controlMessage/ExpirationTimerUpdateMessage';
import { MessageRequestResponse } from '../../messages/outgoing/controlMessage/MessageRequestResponse';
import { UnsendMessage } from '../../messages/outgoing/controlMessage/UnsendMessage';
@ -28,54 +12,8 @@ import {
Quote,
VisibleMessage,
} from '../../messages/outgoing/visibleMessage/VisibleMessage';
import { PubKey } from '../../types';
import { fromBase64ToArray, fromHexToArray } from '../String';
import { UserSync } from '../job_runners/jobs/UserSyncJob';
const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp';
const getLastSyncTimestampFromDb = async (): Promise<number | undefined> =>
(await Data.getItemById(ITEM_ID_LAST_SYNC_TIMESTAMP))?.value;
const writeLastSyncTimestampToDb = async (timestamp: number) =>
Storage.put(ITEM_ID_LAST_SYNC_TIMESTAMP, timestamp);
/**
* Conditionally Syncs user configuration with other devices linked.
*/
export const syncConfigurationIfNeeded = async () => {
await UserSync.queueNewJobIfNeeded();
const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased();
if (!userConfigLibsession) {
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) < DURATION.DAYS * 2) {
return;
}
const allConvos = ConvoHub.use().getConversations();
const configMessage = await getCurrentConfigurationMessage(allConvos);
try {
// window?.log?.info('syncConfigurationIfNeeded with', configMessage);
await getMessageQueue().sendSyncMessage({
namespace: SnodeNamespaces.Default,
message: 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) => {
await ReleasedFeatures.checkIsUserConfigFeatureReleased();
return new Promise(resolve => {
@ -91,197 +29,14 @@ export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = fal
e.message
);
});
if (ReleasedFeatures.isUserConfigFeatureReleasedCached()) {
if (waitForMessageSent) {
window.Whisper.events.once(UserSyncJobDone, () => {
resolve(true);
});
return;
}
resolve(true);
return;
}
const allConvos = ConvoHub.use().getConversations();
// eslint-disable-next-line more/no-then
void getCurrentConfigurationMessage(allConvos)
.then(configMessage => {
// this just adds the message to the sending queue.
// if waitForMessageSent is set, we need to effectively wait until then
const callback = waitForMessageSent
? () => {
resolve(true);
}
: undefined;
void getMessageQueue().sendSyncMessage({
namespace: SnodeNamespaces.Default,
message: configMessage,
sentCb: callback as any,
});
// either we resolve from the callback if we need to wait for it,
// or we don't want to wait, we resolve it here.
if (!waitForMessageSent) {
resolve(true);
}
})
.catch(e => {
window?.log?.warn('Caught an error while building our ConfigurationMessage:', e);
resolve(false);
if (waitForMessageSent) {
window.Whisper.events.once(UserSyncJobDone, () => {
resolve(true);
});
});
};
const getActiveOpenGroupV2CompleteUrls = async (
convos: Array<ConversationModel>
): Promise<Array<string>> => {
// Filter open groups v2
const openGroupsV2ConvoIds = convos
.filter(c => !!c.getActiveAt() && c.isOpenGroupV2() && !c.isLeft())
.map(c => c.id) as Array<string>;
const urls = await Promise.all(
openGroupsV2ConvoIds.map(async opengroupConvoId => {
const roomInfos = OpenGroupData.getV2OpenGroupRoom(opengroupConvoId);
if (roomInfos) {
return getCompleteUrlFromRoom(roomInfos);
}
return null;
})
);
return _.compact(urls) || [];
};
const getValidClosedGroups = async (convos: Array<ConversationModel>) => {
const ourPubKey = UserUtils.getOurPubKeyStrFromCache();
// Filter Closed/Medium groups
const closedGroupModels = convos.filter(
c =>
!!c.getActiveAt() &&
c.isClosedGroup() &&
c.getGroupMembers()?.includes(ourPubKey) &&
!c.isLeft() &&
!c.isKickedFromGroup() &&
!c.isBlocked() &&
c.getRealSessionUsername()
);
const closedGroups = await Promise.all(
closedGroupModels.map(async c => {
const groupPubKey = c.get('id');
const fetchEncryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair(groupPubKey);
if (!fetchEncryptionKeyPair) {
return null;
}
return new ConfigurationMessageClosedGroup({
publicKey: groupPubKey,
name: c.getRealSessionUsername() || '',
members: c.getGroupMembers() || [],
admins: c.getGroupAdmins(),
encryptionKeyPair: ECKeyPair.fromHexKeyPair(fetchEncryptionKeyPair),
});
})
);
const onlyValidClosedGroup = closedGroups.filter(
m => m !== null
) as Array<ConfigurationMessageClosedGroup>;
return onlyValidClosedGroup;
};
const getValidContacts = (convos: Array<ConversationModel>) => {
// Filter contacts
// blindedId are synced with the outbox logic.
const contactsModels = convos.filter(
c =>
!!c.getActiveAt() &&
c.getRealSessionUsername() &&
c.isPrivate() &&
c.isApproved() &&
!PubKey.isBlinded(c.get('id'))
);
const contacts = contactsModels.map(c => {
try {
const profileKey = c.getProfileKey();
let profileKeyForContact = null;
if (typeof profileKey === 'string') {
// this will throw if the profileKey is not in hex.
try {
// for some reason, at some point, the saved profileKey is a string in base64 format
// this hack is here to update existing conversations with a non-hex profileKey to a hex format and save them
if (!/^[0-9a-fA-F]+$/.test(profileKey)) {
throw new Error('Not Hex');
}
profileKeyForContact = fromHexToArray(profileKey);
} catch (e) {
// if not hex, try to decode it as base64
profileKeyForContact = fromBase64ToArray(profileKey);
// if the line above does not fail, update the stored profileKey for this convo
void c.setProfileKey(profileKeyForContact);
}
} else if (profileKey) {
window.log.warn(
'Got a profileKey for a contact in another format than string. Contact: ',
c.id
);
return null;
}
return new ConfigurationMessageContact({
publicKey: c.id as string,
displayName: c.getRealSessionUsername() || 'Anonymous',
profilePictureURL: c.getAvatarPointer(),
profileKey: !profileKeyForContact?.length ? undefined : profileKeyForContact,
isApproved: c.isApproved(),
isBlocked: c.isBlocked(),
didApproveMe: c.didApproveMe(),
});
} catch (e) {
window?.log.warn('getValidContacts', e);
return null;
return;
}
});
return _.compact(contacts);
};
export const getCurrentConfigurationMessage = async (
convos: Array<ConversationModel>
): Promise<ConfigurationMessage> => {
const ourPubKey = UserUtils.getOurPubKeyStrFromCache();
const ourConvo = convos.find(convo => convo.id === ourPubKey);
const opengroupV2CompleteUrls = await getActiveOpenGroupV2CompleteUrls(convos);
const onlyValidClosedGroup = await getValidClosedGroups(convos);
const validContacts = getValidContacts(convos);
if (!ourConvo) {
window?.log?.error('Could not find our convo while building a configuration message.');
}
const ourProfileKeyHex =
ConvoHub.use().get(UserUtils.getOurPubKeyStrFromCache())?.getProfileKey() || null;
const profileKey = ourProfileKeyHex ? fromHexToArray(ourProfileKeyHex) : undefined;
const profilePicture = ourConvo?.getAvatarPointer() || undefined;
const displayName = ourConvo?.getRealSessionUsername() || 'Anonymous'; // this should never be undefined, but well...
const activeOpenGroups = [...opengroupV2CompleteUrls];
return new ConfigurationMessage({
identifier: uuidv4(),
timestamp: Date.now(),
activeOpenGroups,
activeClosedGroups: onlyValidClosedGroup,
displayName,
profilePicture,
profileKey,
contacts: validContacts,
resolve(true);
});
};
@ -348,7 +103,6 @@ const buildSyncExpireTimerMessage = (
export type SyncMessageType =
| VisibleMessage
| ExpirationTimerUpdateMessage
| ConfigurationMessage
| MessageRequestResponse
| UnsendMessage;
@ -368,7 +122,7 @@ export const buildSyncMessage = (
if (!sentTimestamp || !_.isNumber(sentTimestamp)) {
throw new Error('Tried to build a sync message without a sentTimestamp');
}
// don't include our profileKey on syncing message. This is to be done by a ConfigurationMessage now
// don't include our profileKey on syncing message. This is to be done through libsession now
const timestamp = _.toNumber(sentTimestamp);
if (dataMessage.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE) {
return buildSyncExpireTimerMessage(identifier, dataMessage, timestamp, syncTarget);

@ -1,251 +0,0 @@
import { expect } from 'chai';
import { ECKeyPair } from '../../../../receiver/keypairs';
import { TTL_DEFAULT } from '../../../../session/constants';
import {
ConfigurationMessage,
ConfigurationMessageClosedGroup,
ConfigurationMessageContact,
} from '../../../../session/messages/outgoing/controlMessage/ConfigurationMessage';
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(),
displayName: 'displayName',
contacts: [],
};
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(),
displayName: 'displayName',
contacts: [],
};
expect(() => new ConfigurationMessage(params)).to.throw('open group must be set');
});
it('throw if display name is not set', () => {
const params = {
activeClosedGroups: [],
activeOpenGroups: [],
timestamp: Date.now(),
displayName: undefined as any,
contacts: [],
};
expect(() => new ConfigurationMessage(params)).to.throw('displayName must be set');
});
it('throw if display name is set but empty', () => {
const params = {
activeClosedGroups: [],
activeOpenGroups: [],
timestamp: Date.now(),
displayName: undefined as any,
contacts: [],
};
expect(() => new ConfigurationMessage(params)).to.throw('displayName must be set');
});
it('ttl is 4 days', () => {
const params = {
activeClosedGroups: [],
activeOpenGroups: [],
timestamp: Date.now(),
displayName: 'displayName',
contacts: [],
};
const configMessage = new ConfigurationMessage(params);
expect(configMessage.ttl()).to.be.equal(TTL_DEFAULT.TTL_MAX);
});
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'
);
});
});
describe('ConfigurationMessageContact', () => {
it('throws if contacts is not set', () => {
const params = {
activeClosedGroups: [],
activeOpenGroups: [],
timestamp: Date.now(),
displayName: 'displayName',
contacts: undefined as any,
};
expect(() => new ConfigurationMessage(params)).to.throw('contacts 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'
);
});
it('throw if the contact has not a valid pubkey', () => {
const params = {
publicKey: '05',
displayName: 'contactDisplayName',
};
expect(() => new ConfigurationMessageContact(params)).to.throw();
const params2 = {
publicKey: undefined as any,
displayName: 'contactDisplayName',
};
expect(() => new ConfigurationMessageContact(params2)).to.throw();
});
it('throw if the contact has an empty display name', () => {
// a display name cannot be empty nor undefined
expect(() => new ConfigurationMessageContact(params2)).to.throw();
const params2 = {
publicKey: TestUtils.generateFakePubKey().key,
displayName: '',
};
expect(() => new ConfigurationMessageContact(params2)).to.throw();
});
it('throw if the contact has a profilePictureURL set but empty', () => {
const params = {
publicKey: TestUtils.generateFakePubKey().key,
displayName: 'contactDisplayName',
profilePictureURL: '',
};
expect(() => new ConfigurationMessageContact(params)).to.throw(
'profilePictureURL must either undefined or not empty'
);
});
it('throw if the contact has a profileKey set but empty', () => {
const params = {
publicKey: TestUtils.generateFakePubKey().key,
displayName: 'contactDisplayName',
profileKey: new Uint8Array(),
};
expect(() => new ConfigurationMessageContact(params)).to.throw(
'profileKey must either undefined or not empty'
);
});
});
});

@ -1,29 +1,21 @@
/* eslint-disable no-unused-expressions */
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { beforeEach } from 'mocha';
import Sinon from 'sinon';
import { ConfigurationMessage } from '../../../../session/messages/outgoing/controlMessage/ConfigurationMessage';
import { ClosedGroupVisibleMessage } from '../../../../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage';
import { PubKey } from '../../../../session/types';
import { MessageUtils, UserUtils } from '../../../../session/utils';
import { MessageUtils } from '../../../../session/utils';
import { TestUtils } from '../../../test-utils';
import { OpenGroupData, OpenGroupV2Room } from '../../../../data/opengroups';
import { ConversationTypeEnum } from '../../../../models/conversationAttributes';
import { SignalService } from '../../../../protobuf';
import { getOpenGroupV2ConversationId } from '../../../../session/apis/open_group_api/utils/OpenGroupUtils';
import { SnodeNamespaces } from '../../../../session/apis/snode_api/namespaces';
import { ConvoHub } from '../../../../session/conversations';
import { ClosedGroupAddedMembersMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage';
import { ClosedGroupEncryptionPairMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage';
import { ClosedGroupEncryptionPairReplyMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairReplyMessage';
import { ClosedGroupNameChangeMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage';
import { ClosedGroupNewMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { ClosedGroupRemovedMembersMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage';
import { getCurrentConfigurationMessage } from '../../../../session/utils/sync/syncUtils';
import { stubData, stubOpenGroupData } from '../../../test-utils/utils';
chai.use(chaiAsPromised as any);
@ -205,80 +197,5 @@ describe('Message Utils', () => {
const rawMessage = await MessageUtils.toRawMessage(device, msg, SnodeNamespaces.Default);
expect(rawMessage.encryption).to.equal(SignalService.Envelope.Type.SESSION_MESSAGE);
});
it('passing a ConfigurationMessage returns Fallback', async () => {
const device = TestUtils.generateFakePubKey();
const msg = new ConfigurationMessage({
timestamp: Date.now(),
activeOpenGroups: [],
activeClosedGroups: [],
displayName: 'displayName',
contacts: [],
});
const rawMessage = await MessageUtils.toRawMessage(device, msg, SnodeNamespaces.Default);
expect(rawMessage.encryption).to.equal(SignalService.Envelope.Type.SESSION_MESSAGE);
});
});
describe('getCurrentConfigurationMessage', () => {
const ourNumber = TestUtils.generateFakePubKey().key;
beforeEach(async () => {
Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').resolves(ourNumber);
Sinon.stub(UserUtils, 'getOurPubKeyFromCache').resolves(PubKey.cast(ourNumber));
stubData('getAllConversations').resolves([]);
stubData('saveConversation').resolves();
stubOpenGroupData('getAllV2OpenGroupRooms').resolves();
TestUtils.stubData('getItemById').callsFake(async () => {
return { value: '[]' };
});
ConvoHub.use().reset();
await ConvoHub.use().load();
});
afterEach(() => {
Sinon.restore();
});
// open groups are actually removed when we leave them so this doesn't make much sense, but just in case we break something later
it('filter out non active open groups', async () => {
await ConvoHub.use().getOrCreateAndWait('05123456789', ConversationTypeEnum.PRIVATE);
await ConvoHub.use().getOrCreateAndWait('0512345678', ConversationTypeEnum.PRIVATE);
const convoId3 = getOpenGroupV2ConversationId('http://chat-dev2.lokinet.org', 'fish');
const convoId4 = getOpenGroupV2ConversationId('http://chat-dev3.lokinet.org', 'fish2');
const convoId5 = getOpenGroupV2ConversationId('http://chat-dev3.lokinet.org', 'fish3');
const convo3 = await ConvoHub.use().getOrCreateAndWait(convoId3, ConversationTypeEnum.GROUP);
convo3.set({ active_at: Date.now() });
stubOpenGroupData('getV2OpenGroupRoom')
.returns(null)
.withArgs(convoId3)
.returns({
serverUrl: 'http://chat-dev2.lokinet.org',
roomId: 'fish',
serverPublicKey: 'serverPublicKey',
} as OpenGroupV2Room);
const convo4 = await ConvoHub.use().getOrCreateAndWait(convoId4, ConversationTypeEnum.GROUP);
convo4.set({ active_at: undefined });
await OpenGroupData.opengroupRoomsLoad();
const convo5 = await ConvoHub.use().getOrCreateAndWait(convoId5, ConversationTypeEnum.GROUP);
convo5.set({ active_at: 0 });
await ConvoHub.use().getOrCreateAndWait('051234567', ConversationTypeEnum.PRIVATE);
const convos = ConvoHub.use().getConversations();
// convoID3 is active but 4 and 5 are not
const configMessage = await getCurrentConfigurationMessage(convos);
expect(configMessage.activeOpenGroups.length).to.equal(1);
expect(configMessage.activeOpenGroups[0]).to.equal(
'http://chat-dev2.lokinet.org/fish?public_key=serverPublicKey'
);
});
});
});

@ -1,20 +1,5 @@
// `missingCaseError` is useful for compile-time checking that all `case`s in
// a `switch` statement have been handled, e.g.
//
// type AttachmentType = 'media' | 'documents';
//
// const type: AttachmentType = selectedTab;
// switch (type) {
// case 'media':
// return <MediaGridItem/>;
// case 'documents':
// return <DocumentListItem/>;
// default:
// return missingCaseError(type);
// }
//
// If we extended `AttachmentType` to `'media' | 'documents' | 'links'` the code
// above would trigger a compiler error stating that `'links'` has not been
// handled by our `switch` / `case` statement which is useful for code
// maintenance and system evolution.
export const missingCaseError = (x: never): TypeError => new TypeError(`Unhandled case: ${x}`);

Loading…
Cancel
Save