fix: add mgmt of members in user groups wrapper

pull/2620/head
Audric Ackermann 3 years ago
parent bc905bdba1
commit c4e17d0825

@ -1,5 +1,6 @@
import React from 'react';
import { useMembersAvatars } from '../../../hooks/useMembersAvatars';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
import { Avatar, AvatarSize } from '../Avatar';
type Props = {
@ -11,6 +12,8 @@ type Props = {
function getClosedGroupAvatarsSize(size: AvatarSize): AvatarSize {
// Always use the size directly under the one requested
switch (size) {
case AvatarSize.XS:
throw new Error('AvatarSize.XS is not supported for closed group avatar sizes');
case AvatarSize.S:
return AvatarSize.XS;
case AvatarSize.M:
@ -22,7 +25,7 @@ function getClosedGroupAvatarsSize(size: AvatarSize): AvatarSize {
case AvatarSize.HUGE:
return AvatarSize.XL;
default:
throw new Error(`Invalid size request for closed group avatar: ${size}`);
assertUnreachable(size, `Invalid size request for closed group avatar "${size}"`);
}
}

@ -1,9 +1,9 @@
import React from 'react';
import { missingCaseError } from '../../util/missingCaseError';
import { PropsForExpirationTimer } from '../../state/ducks/conversations';
import { NotificationBubble } from './message/message-item/notification-bubble/NotificationBubble';
import { ReadableMessage } from './message/message-item/ReadableMessage';
import { assertUnreachable } from '../../types/sqlSharedTypes';
export const TimerNotification = (props: PropsForExpirationTimer) => {
const { messageId, receivedAt, isUnread, pubkey, profileName, timespan, type, disabled } = props;
@ -28,7 +28,7 @@ export const TimerNotification = (props: PropsForExpirationTimer) => {
: window.i18n('timerSetOnSync', [timespan]);
break;
default:
throw missingCaseError(type);
assertUnreachable(type, `TimerNotification: Missing case error "${type}"`);
}
if (!textToRender || textToRender.length === 0) {

@ -8,6 +8,7 @@ import { NotificationBubble } from './notification-bubble/NotificationBubble';
import { ReadableMessage } from './ReadableMessage';
import { arrayContainsUsOnly } from '../../../../models/message';
import { useConversationsUsernameWithQuoteOrFullPubkey } from '../../../../hooks/useParamSelector';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
// This component is used to display group updates in the conversation view.
@ -51,7 +52,8 @@ const ChangeItemLeft = (left: Array<string>): string => {
// tslint:disable-next-line: cyclomatic-complexity
const ChangeItem = (change: PropsForGroupUpdateType): string => {
switch (change.type) {
const type = change.type;
switch (type) {
case 'name':
return window.i18n('titleIsNow', [change.newName || '']);
case 'add':
@ -66,7 +68,7 @@ const ChangeItem = (change: PropsForGroupUpdateType): string => {
case 'general':
return window.i18n('updatedTheGroup');
default:
throw new Error('Missing case error');
assertUnreachable(type, `ChangeItem: Missing case error "${type}"`);
}
};

@ -1,10 +1,11 @@
import React, { useState } from 'react';
import { SpacerLG } from '../basic/Text';
import React from 'react';
import styled from 'styled-components';
import { getConversationController } from '../../session/conversations';
import { leaveClosedGroup } from '../../session/group/closed-group';
import { adminLeaveClosedGroup } from '../../state/ducks/modalDialog';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SpacerLG } from '../basic/Text';
import { SessionWrapperModal } from '../SessionWrapperModal';
import styled from 'styled-components';
type Props = {
conversationId: string;
@ -21,15 +22,9 @@ export const AdminLeaveClosedGroupDialog = (props: Props) => {
const warningAsAdmin = `${window.i18n('leaveGroupConfirmationAdmin')}`;
const okText = window.i18n('leaveAndRemoveForEveryone');
const cancelText = window.i18n('cancel');
const [_isLoading, setIsLoading] = useState(false);
const onClickOK = async () => {
setIsLoading(true);
await getConversationController()
.get(props.conversationId)
.leaveClosedGroup();
setIsLoading(false);
await leaveClosedGroup(props.conversationId);
closeDialog();
};

@ -1,6 +1,5 @@
import React from 'react';
import { missingCaseError } from '../../util';
import { ToastUtils } from '../../session/utils';
import { Data } from '../../data/data';
import { SpacerSM } from '../basic/Text';
@ -10,6 +9,7 @@ import { LocalizerKeys } from '../../types/LocalizerKeys';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { matchesHash, validatePassword } from '../../util/passwordUtils';
import { assertUnreachable } from '../../types/sqlSharedTypes';
export type PasswordAction = 'set' | 'change' | 'remove' | 'enter';
@ -329,7 +329,7 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
return;
}
default:
throw missingCaseError(passwordAction);
assertUnreachable(passwordAction, 'passwordAction');
}
}

@ -15,6 +15,7 @@ import { SessionIconButton, SessionIconType } from '../icon';
import * as MIME from '../../types/MIME';
import { isUndefined } from 'lodash';
import styled from 'styled-components';
import { assertUnreachable } from '../../types/sqlSharedTypes';
const colorSVG = (url: string, color: string) => {
return {
@ -166,7 +167,7 @@ const IconButton = ({ onClick, type }: IconButtonProps) => {
break;
default:
throw new TypeError(`Invalid button type: ${type}`);
assertUnreachable(type, `Invalid button type: ${type}`);
}
return (

@ -225,7 +225,7 @@ export const DeleteContactMenuItem = () => {
onClickClose,
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
await getConversationController().deleteContact(convoId);
await getConversationController().deleteContact(convoId, false);
},
})
);

@ -1,6 +1,6 @@
import React from 'react';
import styled from 'styled-components';
import { missingCaseError } from '../../util';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { SessionSettingCategory, SettingsViewProps } from './SessionSettings';
type Props = Pick<SettingsViewProps, 'category'>;
@ -44,8 +44,13 @@ export const SettingsHeader = (props: Props) => {
case SessionSettingCategory.Privacy:
categoryTitle = window.i18n('privacySettingsTitle');
break;
case SessionSettingCategory.ClearData:
case SessionSettingCategory.MessageRequests:
case SessionSettingCategory.RecoveryPhrase:
throw new Error(`no header for should be tried to be rendered for "${category}"`);
default:
throw missingCaseError('SettingsHeader' as never);
assertUnreachable(category, `SettingsHeader "${category}"`);
}
return (

@ -36,6 +36,7 @@ import { encryptProfile } from '../util/crypto/profileEncrypter';
import { setLastProfileUpdateTimestamp } from '../util/storage';
import { OpenGroupUtils } from '../session/apis/open_group_api/utils';
import { SessionUtilUserGroups } from '../session/utils/libsession/libsession_utils_user_groups';
import { leaveClosedGroup } from '../session/group/closed-group';
export function copyPublicKeyByConvoId(convoId: string) {
if (OpenGroupUtils.isOpenGroupV2(convoId)) {
@ -203,7 +204,7 @@ export function showLeaveGroupByConvoId(conversationId: string) {
title,
message,
onClickOk: async () => {
await conversation.leaveClosedGroup();
await leaveClosedGroup(conversation.id);
onClickClose();
},
onClickClose,

@ -188,10 +188,14 @@ Storage.onready(async () => {
await window.Events.setThemeSetting(newThemeSetting);
try {
try {
await LibSessionUtil.initializeLibSessionUtilWrappers();
} catch (e) {
window.log.warn('LibSessionUtil.initializeLibSessionUtilWrappers failed with', e.message);
if (Registration.isDone()) {
try {
await LibSessionUtil.initializeLibSessionUtilWrappers();
} catch (e) {
window.log.warn('LibSessionUtil.initializeLibSessionUtilWrappers failed with', e.message);
// TODO what should we do if this happens?
throw e;
}
}
await initialiseEmojiData(nativeEmojiData);
await AttachmentDownloads.initAttachmentPaths();

@ -18,7 +18,6 @@ import {
import { SignalService } from '../protobuf';
import { getMessageQueue } from '../session';
import { getConversationController } from '../session/conversations';
import { leaveClosedGroup } from '../session/group/closed-group';
import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage';
import { PubKey } from '../session/types';
import { ToastUtils, UserUtils } from '../session/utils';
@ -103,6 +102,7 @@ import {
isOpenOrClosedGroup,
} from './conversationAttributes';
import { SessionUtilUserGroups } from '../session/utils/libsession/libsession_utils_user_groups';
import { Registration } from '../util/registration';
export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public updateLastMessage: () => any;
@ -343,10 +343,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
toRet.isTyping = true;
}
if (isTyping) {
toRet.isTyping = true;
}
if (avatarPath) {
toRet.avatarPath = avatarPath;
}
@ -1279,16 +1275,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
});
}
public async leaveClosedGroup() {
if (this.isMediumGroup()) {
await leaveClosedGroup(this.id);
} else {
throw new Error(
'Legacy group are not supported anymore. You need to create this group again.'
);
}
}
/**
* Mark everything as read efficiently if possible.
*
@ -2241,31 +2227,31 @@ export async function commitConversationAndRefreshWrapper(id: string) {
}
// write to DB
// TODO remove duplicates between db and wrapper (except nickname&name as we need them for search)
// TODO when deleting a contact from the ConversationController, we still need to keep it in the wrapper but mark it as hidden (and we might need to add an hidden convo model field for it)
await Data.saveConversation(convo.attributes);
const shouldBeSavedToContactsWrapper = SessionUtilContact.filterContactsToStoreInContactsWrapper(
const shouldBeSavedToContactsWrapper = SessionUtilContact.isContactToStoreInContactsWrapper(
convo
);
const shouldBeSavedToUserGroupsWrapper = SessionUtilUserGroups.filterUserGroupsToStoreInWrapper(
convo
const shouldBeSavedToUserGroupsWrapper = SessionUtilUserGroups.isUserGroupToStoreInWrapper(convo);
console.warn(
`should be saved to wrapper ${id}: contacts:${shouldBeSavedToContactsWrapper}; usergroups:${shouldBeSavedToUserGroupsWrapper}`
);
console.warn(`should be saved to contacts wrapper ${id}: ${shouldBeSavedToContactsWrapper}`);
console.warn(`should be saved to usergroups wrapper ${id}: ${shouldBeSavedToUserGroupsWrapper}`);
if (shouldBeSavedToContactsWrapper) {
await SessionUtilContact.insertContactFromDBIntoWrapperAndRefresh(convo.id);
} else if (shouldBeSavedToUserGroupsWrapper) {
await SessionUtilUserGroups.insertGroupsFromDBIntoWrapperAndRefresh(convo.id);
}
// save the new dump if needed to the DB asap
// this call throttled so we do not run this too often (and not for every .commit())
await ConfigurationDumpSync.queueNewJobIfNeeded();
if (Registration.isDone()) {
// save the new dump if needed to the DB asap
// this call throttled so we do not run this too often (and not for every .commit())
await ConfigurationDumpSync.queueNewJobIfNeeded();
// if we need to sync the dump, also send add a job for syncing
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
// if we need to sync the dump, also send add a job for syncing
await ConfigurationSync.queueNewJobIfNeeded();
}
convo.triggerUIRefresh();

@ -7,11 +7,13 @@ import {
UserGroupsWrapperInsideWorker,
} from 'session_util_wrapper';
import { ConversationAttributes } from '../../models/conversationAttributes';
import { HexKeyPair } from '../../receiver/keypairs';
import { fromHexToArray } from '../../session/utils/String';
import {
CONFIG_DUMP_TABLE,
getCommunityInfoFromDBValues,
getContactInfoFromDBValues,
getLegacyGroupInfoFromDBValues,
} from '../../types/sqlSharedTypes';
import {
CLOSED_GROUP_V2_KEY_PAIRS_TABLE,
@ -1361,6 +1363,51 @@ function insertCommunityIntoWrapper(
}
}
function insertLegacyGroupIntoWrapper(
legacyGroup: Pick<
ConversationAttributes,
'hidden' | 'id' | 'isPinned' | 'expireTimer' | 'displayNameInProfile'
> & { members: string; groupAdmins: string }, // members and groupAdmins are still stringified here
userGroupConfigWrapper: UserGroupsWrapperInsideWorker,
db: BetterSqlite3.Database
) {
const {
isPinned,
id,
hidden,
expireTimer,
groupAdmins,
members,
displayNameInProfile,
} = legacyGroup;
const latestEncryptionKeyPairHex = sqlNode.getLatestClosedGroupEncryptionKeyPair(
legacyGroup.id,
db
) as HexKeyPair | undefined;
const wrapperLegacyGroup = getLegacyGroupInfoFromDBValues({
id,
hidden,
isPinned,
expireTimer,
groupAdmins,
members,
displayNameInProfile,
encPubkeyHex: latestEncryptionKeyPairHex?.publicHex || '',
encSeckeyHex: latestEncryptionKeyPairHex?.privateHex || '',
});
try {
console.info('Inserting legacy group into wrapper: ', wrapperLegacyGroup);
userGroupConfigWrapper.setLegacyGroup(wrapperLegacyGroup);
} catch (e) {
console.error(
`userGroupConfigWrapper.set during migration failed with ${e.message} for legacyGroup.id: "${legacyGroup.id}". Skipping that legacy group entirely`
);
}
}
function getBlockedNumbersDuringMigration(db: BetterSqlite3.Database) {
try {
const blockedItem = sqlNode.getItemById('blocked', db);
@ -1368,13 +1415,13 @@ function getBlockedNumbersDuringMigration(db: BetterSqlite3.Database) {
throw new Error('no blocked contacts at all');
}
const foundBlocked = blockedItem?.value;
console.warn('foundBlockedNumbers during migration', foundBlocked);
console.info('foundBlockedNumbers during migration', foundBlocked);
if (isArray(foundBlocked)) {
return foundBlocked;
}
return [];
} catch (e) {
console.warn('failed to read blocked numbers. Considering no blocked numbers', e.stack);
console.info('failed to read blocked numbers. Considering no blocked numbers', e.stack);
return [];
}
}
@ -1411,7 +1458,7 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
WHERE type <> 'private' AND active_at > 0;`
).run({});
db.exec(`CREATE TABLE configDump(
db.exec(`CREATE TABLE ${CONFIG_DUMP_TABLE}(
variant TEXT NOT NULL,
publicKey TEXT NOT NULL,
data BLOB,
@ -1419,10 +1466,58 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
);
`);
const allOpengroupsConvo = db
.prepare(
`SELECT id FROM ${CONVERSATIONS_TABLE} WHERE
type = 'group' AND
id LIKE 'publicChat:%'
ORDER BY id ASC;`
)
.all();
const allValidOpengroupsDetails = allOpengroupsConvo
.filter(m => isString(m.id) && m.id.indexOf('@') > 0)
.map(row => {
const roomNameStart = (row.id.indexOf(':') as number) + 1;
const roomNameEnd = row.id.indexOf('@');
const roomName = row.id.substring(roomNameStart, roomNameEnd);
const baseUrl = row.id.substring((roomNameEnd as number) + 1);
return { roomName, baseUrl, oldConvoId: row.id };
});
allValidOpengroupsDetails.forEach(convoDetails => {
const newId = `${convoDetails.baseUrl}/${convoDetails.roomName}`;
db.prepare(
`UPDATE ${CONVERSATIONS_TABLE} SET
id = $newId
WHERE id = $oldId;`
).run({
newId,
oldId: convoDetails.oldConvoId,
});
// do the same for messages and where else?
db.prepare(
`UPDATE ${MESSAGES_TABLE} SET
conversationId = $newId,
json = json_set(json,'$.conversationId', $newId)
WHERE conversationId = $oldConvoId;`
).run({ oldConvoId: convoDetails.oldConvoId, newId });
db.prepare(
`UPDATE ${OPEN_GROUP_ROOMS_V2_TABLE} SET
conversationId = $newId,
json = json_set(json, '$.conversationId', $newId);`
).run({ newId });
});
try {
const keys = getIdentityKeysDuringMigration(db);
if (!keys || !keys.privateEd25519 || isEmpty(keys.privateEd25519)) {
const userAlreadyCreated = !!keys && !isEmpty(keys.privateEd25519);
if (!userAlreadyCreated) {
throw new Error('privateEd25519 was empty. Considering no users are logged in');
}
const { privateEd25519, publicKeyHex } = keys;
@ -1475,17 +1570,17 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
*/
const blockedNumbers = getBlockedNumbersDuringMigration(db);
// this filter is based on the `filterContactsToStoreInContactsWrapper` function.
// this filter is based on the `isContactToStoreInContactsWrapper` function.
const contactsToWriteInWrapper = db
.prepare(
`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE type = 'private' AND active_at > 0 AND NOT hidden AND (didApproveMe OR isApproved) AND id <> $us AND id NOT LIKE '15%' ;`
`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE type = 'private' AND active_at > 0 AND NOT hidden AND (didApproveMe OR isApproved) AND id <> '$us' AND id NOT LIKE '15%' ;`
)
.all({
us: publicKeyHex,
});
if (isArray(contactsToWriteInWrapper) && contactsToWriteInWrapper.length) {
console.warn(
console.info(
'===================== Starting contact inserting into wrapper ======================='
);
@ -1498,7 +1593,7 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
insertContactIntoWrapper(contact, blockedNumbers, contactsConfigWrapper);
});
console.warn('===================== Done with contact inserting =======================');
console.info('===================== Done with contact inserting =======================');
}
const contactsDump = contactsConfigWrapper.dump();
@ -1518,80 +1613,64 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
data: contactsDump,
});
const allOpengroupsConvo = db
/**
* Setup up the UserGroups Wrapper with all the comunities details which needs to be stored in it.
*/
// this filter is based on the `isCommunityToStoreInWrapper` function.
const communitiesToWriteInWrapper = db
.prepare(
`SELECT id FROM ${CONVERSATIONS_TABLE} WHERE
type = 'group' AND
id LIKE 'publicChat:%'
ORDER BY id ASC;`
`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE type = 'group' AND active_at > 0 AND id LIKE 'http%' ;`
)
.all();
.all({});
const allValidOpengroupsDetails = allOpengroupsConvo
.filter(m => isString(m.id) && m.id.indexOf('@') > 0)
.map(row => {
const roomNameStart = (row.id.indexOf(':') as number) + 1;
const roomNameEnd = row.id.indexOf('@');
const roomName = row.id.substring(roomNameStart, roomNameEnd);
const baseUrl = row.id.substring((roomNameEnd as number) + 1);
if (isArray(communitiesToWriteInWrapper) && communitiesToWriteInWrapper.length) {
console.info(
`===================== Starting communities inserting into wrapper ${communitiesToWriteInWrapper?.length} =======================`
);
return { roomName, baseUrl, oldConvoId: row.id };
communitiesToWriteInWrapper.forEach(community => {
try {
console.info('Writing community: ', JSON.stringify(community));
insertCommunityIntoWrapper(community, userGroupsConfigWrapper, db);
} catch (e) {
console.info(`failed to insert community with ${e.message}`, community);
}
});
allValidOpengroupsDetails.forEach(convoDetails => {
const newId = `${convoDetails.baseUrl}/${convoDetails.roomName}`;
db.prepare(
`UPDATE ${CONVERSATIONS_TABLE} SET
id = $newId
WHERE id = $oldId;`
).run({
newId,
oldId: convoDetails.oldConvoId,
});
// do the same for messages and where else?
db.prepare(
`UPDATE ${MESSAGES_TABLE} SET
conversationId = $newId,
json = json_set(json,'$.conversationId', $newId)
WHERE conversationId = $oldConvoId;`
).run({ oldConvoId: convoDetails.oldConvoId, newId });
db.prepare(
`UPDATE ${OPEN_GROUP_ROOMS_V2_TABLE} SET
conversationId = $newId,
json = json_set(json, '$.conversationId', $newId);`
).run({ newId });
});
console.info(
'===================== Done with communinities inserting ======================='
);
}
// this filter is based on the `filterUserGroupsToStoreInWrapper` function.
const communitiesToWriteInWrapper = db
// this filter is based on the `isLegacyGroupToStoreInWrapper` function.
const legacyGroupsToWriteInWrapper = db
.prepare(
`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE type = 'group' AND active_at > 0 AND id LIKE 'http%' ;`
`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE type = 'group' AND active_at > 0 AND id LIKE '05%' AND NOT isKickedFromGroup AND NOT left ;`
)
.all({});
if (isArray(communitiesToWriteInWrapper) && communitiesToWriteInWrapper.length) {
console.warn(
'===================== Starting communities inserting into wrapper ======================='
);
if (isArray(legacyGroupsToWriteInWrapper) && legacyGroupsToWriteInWrapper.length) {
console.info(
'Writing communities to wrapper during migration. length: ',
communitiesToWriteInWrapper?.length
`===================== Starting legacy group inserting into wrapper length: ${legacyGroupsToWriteInWrapper?.length} =======================`
);
communitiesToWriteInWrapper.forEach(community => {
insertCommunityIntoWrapper(community, userGroupsConfigWrapper, db);
legacyGroupsToWriteInWrapper.forEach(legacyGroup => {
try {
console.info('Writing legacy group: ', JSON.stringify(legacyGroup));
console.info('Writing community: ', JSON.stringify(community));
insertLegacyGroupIntoWrapper(legacyGroup, userGroupsConfigWrapper, db);
} catch (e) {
console.info(`failed to insert legacy group with ${e.message}`, legacyGroup);
}
});
console.warn(
'===================== Done with communinities inserting ======================='
console.info(
'===================== Done with legacy group inserting ======================='
);
// TODO we need to do the same for closed groups
}
// TODO we need to do the same for new groups once they are available
const userGroupsDump = userGroupsConfigWrapper.dump();
db.prepare(
@ -1627,8 +1706,6 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
// Didn't find any reference to this serverTimestamp in the unprocessed table needed.
db.exec(`ALTER TABLE unprocessed DROP COLUMN serverTimestamp;`);
// we need to populate those fields with the current state of the conversation so let's throw null until this is done
// throw new Error('update me');
writeSessionSchemaVersion(targetVersion, db);
})();

@ -1809,17 +1809,23 @@ function getMessagesCountByConversation(
* The returned array is ordered based on the timestamp, the latest is at the end.
* @param groupPublicKey string | PubKey
*/
function getAllEncryptionKeyPairsForGroup(groupPublicKey: string | PubKey) {
const rows = getAllEncryptionKeyPairsForGroupRaw(groupPublicKey);
function getAllEncryptionKeyPairsForGroup(
groupPublicKey: string | PubKey,
db?: BetterSqlite3.Database
) {
const rows = getAllEncryptionKeyPairsForGroupRaw(groupPublicKey, db);
return map(rows, row => jsonToObject(row.json));
}
function getAllEncryptionKeyPairsForGroupRaw(groupPublicKey: string | PubKey) {
function getAllEncryptionKeyPairsForGroupRaw(
groupPublicKey: string | PubKey,
db?: BetterSqlite3.Database
) {
const pubkeyAsString = (groupPublicKey as PubKey).key
? (groupPublicKey as PubKey).key
: groupPublicKey;
const rows = assertGlobalInstance()
const rows = assertGlobalInstanceOrInstance(db)
.prepare(
`SELECT * FROM ${CLOSED_GROUP_V2_KEY_PAIRS_TABLE} WHERE groupPublicKey = $groupPublicKey ORDER BY timestamp ASC;`
)
@ -1830,8 +1836,11 @@ function getAllEncryptionKeyPairsForGroupRaw(groupPublicKey: string | PubKey) {
return rows;
}
function getLatestClosedGroupEncryptionKeyPair(groupPublicKey: string) {
const rows = getAllEncryptionKeyPairsForGroup(groupPublicKey);
function getLatestClosedGroupEncryptionKeyPair(
groupPublicKey: string,
db?: BetterSqlite3.Database
) {
const rows = getAllEncryptionKeyPairsForGroup(groupPublicKey, db);
if (!rows || rows.length === 0) {
return undefined;
}

@ -4,7 +4,7 @@ import _ from 'lodash';
import { Data } from '../data/data';
import { UnprocessedParameter } from '../types/sqlSharedTypes';
export async function removeFromCache(envelope: EnvelopePlus) {
export async function removeFromCache(envelope: Pick<EnvelopePlus, 'id'>) {
return Data.removeUnprocessed(envelope.id);
}
@ -91,10 +91,10 @@ export async function getAllFromCacheForSource(source: string) {
}
export async function updateCacheWithDecryptedContent(
envelope: EnvelopePlus,
envelope: Pick<EnvelopePlus, 'id' | 'senderIdentity' | 'source'>,
plaintext: ArrayBuffer
): Promise<void> {
const { id } = envelope;
const { id, senderIdentity, source } = envelope;
const item = await Data.getUnprocessedById(id);
if (!item) {
window?.log?.error(
@ -103,11 +103,11 @@ export async function updateCacheWithDecryptedContent(
return;
}
item.source = envelope.source;
item.source = source;
// For medium-size closed groups
if (envelope.senderIdentity) {
item.senderIdentity = envelope.senderIdentity;
item.senderIdentity = senderIdentity;
}
item.decrypted = StringUtils.decode(plaintext, 'base64');

@ -319,7 +319,6 @@ export async function handleNewClosedGroup(
convo.updateLastMessage();
await convo.commit();
// sanity checks validate this
// tslint:disable: no-non-null-assertion
const ecKeyPair = new ECKeyPair(encryptionKeyPair!.publicKey, encryptionKeyPair!.privateKey);
window?.log?.info(`Received the encryptionKeyPair for new group ${groupId}`);

@ -4,6 +4,7 @@ import { Data, hasSyncedInitialConfigurationItem } from '../data/data';
import { ConversationInteraction } from '../interactions';
import { ConversationTypeEnum } from '../models/conversationAttributes';
import { SignalService } from '../protobuf';
import { ClosedGroup } from '../session';
import {
joinOpenGroupV2WithUIEvents,
parseOpenGroupV2,
@ -20,8 +21,10 @@ import { IncomingConfResult, LibSessionUtil } from '../session/utils/libsession/
import { SessionUtilUserGroups } from '../session/utils/libsession/libsession_utils_user_groups';
import { toHex } from '../session/utils/String';
import { configurationMessageReceived, trigger } from '../shims/events';
import { assertUnreachable } from '../types/sqlSharedTypes';
import { BlockedNumberController } from '../util';
import { getLastProfileUpdateTimestamp, setLastProfileUpdateTimestamp } from '../util/storage';
import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions';
import {
ContactsWrapperActions,
GenericWrapperActions,
@ -29,33 +32,75 @@ import {
UserGroupsWrapperActions,
} from '../webworker/workers/browser/libsession_worker_interface';
import { removeFromCache } from './cache';
import { handleNewClosedGroup } from './closedGroups';
import { addKeyPairToCacheAndDBIfNeeded, handleNewClosedGroup } from './closedGroups';
import { HexKeyPair } from './keypairs';
import { EnvelopePlus } from './types';
function groupByVariant(
incomingConfigs: Array<IncomingMessage<SignalService.ISharedConfigMessage>>
) {
const groupedByVariant: Map<
ConfigWrapperObjectTypes,
Array<IncomingMessage<SignalService.ISharedConfigMessage>>
> = new Map();
incomingConfigs.forEach(incomingConfig => {
const { kind } = incomingConfig.message;
const wrapperId = LibSessionUtil.kindToVariant(kind);
if (!groupedByVariant.has(wrapperId)) {
groupedByVariant.set(wrapperId, []);
}
groupedByVariant.get(wrapperId)?.push(incomingConfig);
});
return groupedByVariant;
}
async function mergeConfigsWithIncomingUpdates(
incomingConfig: IncomingMessage<SignalService.ISharedConfigMessage>
): Promise<{ kind: SignalService.SharedConfigMessage.Kind; result: IncomingConfResult }> {
const { kind } = incomingConfig.message;
incomingConfigs: Array<IncomingMessage<SignalService.ISharedConfigMessage>>
): Promise<Map<ConfigWrapperObjectTypes, IncomingConfResult>> {
// first, group by variant so we do a single merge call
const groupedByVariant = groupByVariant(incomingConfigs);
const groupedResults: Map<ConfigWrapperObjectTypes, IncomingConfResult> = new Map();
// TODO currently we only poll for user config messages, so this can be hardcoded
const publicKey = UserUtils.getOurPubKeyStrFromCache();
const toMerge = [{ data: incomingConfig.message.data, hash: incomingConfig.messageHash }];
const wrapperId = LibSessionUtil.kindToVariant(kind);
try {
await GenericWrapperActions.merge(wrapperId, toMerge);
const needsPush = await GenericWrapperActions.needsPush(wrapperId);
const needsDump = await GenericWrapperActions.needsDump(wrapperId);
window.log.info(`${wrapperId} needsPush:${needsPush} needsDump:${needsDump} `);
const messageHashes = [incomingConfig.messageHash];
const latestSentTimestamp = incomingConfig.envelopeTimestamp;
const incomingConfResult: IncomingConfResult = {
latestSentTimestamp,
messageHashes,
needsDump,
needsPush,
};
for (let index = 0; index < groupedByVariant.size; index++) {
const variant = [...groupedByVariant.keys()][index];
const sameVariant = groupedByVariant.get(variant);
if (!sameVariant?.length) {
continue;
}
const toMerge = sameVariant.map(msg => ({
data: msg.message.data,
hash: msg.messageHash,
}));
await GenericWrapperActions.merge(variant, toMerge);
const needsPush = await GenericWrapperActions.needsPush(variant);
const needsDump = await GenericWrapperActions.needsDump(variant);
window.log.info(`${variant}: "${publicKey}" needsPush:${needsPush} needsDump:${needsDump} `);
const messageHashes = toMerge.map(m => m.hash);
const latestEnvelopeTimestamp = Math.max(...sameVariant.map(m => m.envelopeTimestamp));
const incomingConfResult: IncomingConfResult = {
messageHashes,
needsDump,
needsPush,
kind: LibSessionUtil.variantToKind(variant),
publicKey,
latestEnvelopeTimestamp: latestEnvelopeTimestamp ? latestEnvelopeTimestamp : Date.now(),
};
groupedResults.set(variant, incomingConfResult);
}
return { kind, result: incomingConfResult };
return groupedResults;
} catch (e) {
window.log.error('mergeConfigsWithIncomingUpdates failed with', e);
throw e;
@ -155,16 +200,13 @@ async function handleContactsUpdate(result: IncomingConfResult): Promise<Incomin
return result;
}
async function handleUserGroupsUpdate(result: IncomingConfResult): Promise<IncomingConfResult> {
if (!result.needsDump) {
return result;
}
async function handleCommunitiesUpdate() {
// first let's check which communities needs to be joined or left by doing a diff of what is in the wrapper and what is in the DB
const allCommunitiesInWrapper = await UserGroupsWrapperActions.getAllCommunities();
const allCommunitiesConversation = getConversationController()
.getConversations()
.filter(SessionUtilUserGroups.filterUserCommunitiesToStoreInWrapper);
.filter(SessionUtilUserGroups.isCommunityToStoreInWrapper);
const allCommunitiesIdsInDB = allCommunitiesConversation.map(m => m.id as string);
@ -189,11 +231,7 @@ async function handleUserGroupsUpdate(result: IncomingConfResult): Promise<Incom
m.baseUrl,
m.roomCasePreserved
);
if (allCommunitiesIdsInDB.includes(builtConvoId)) {
return null;
}
console.warn('builtConvoId', builtConvoId, allCommunitiesIdsInDB);
return m;
return allCommunitiesIdsInDB.includes(builtConvoId) ? null : m;
} catch (e) {
return null;
}
@ -206,15 +244,10 @@ async function handleUserGroupsUpdate(result: IncomingConfResult): Promise<Incom
})
);
console.warn(
'communitiesToJoinInDB',
communitiesToJoinInDB.map(m => `${m.fullUrl}`)
);
for (let index = 0; index < communitiesToLeaveInDB.length; index++) {
const toLeave = communitiesToLeaveInDB[index];
console.warn('leaving community with convoId ', toLeave.id);
await getConversationController().deleteContact(toLeave.id);
await getConversationController().deleteContact(toLeave.id, true);
}
// this call can take quite a long time and should not cause issues to not be awaited
@ -254,86 +287,255 @@ async function handleUserGroupsUpdate(result: IncomingConfResult): Promise<Incom
}
}
}
return result;
}
async function processMergingResults(
envelope: EnvelopePlus,
result: { kind: SignalService.SharedConfigMessage.Kind; result: IncomingConfResult }
) {
const pubkey = envelope.source;
async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
// first let's check which closed groups needs to be joined or left by doing a diff of what is in the wrapper and what is in the DB
const { kind, result: incomingResult } = result;
const allLegacyGroupsInWrapper = await UserGroupsWrapperActions.getAllLegacyGroups();
if (allLegacyGroupsInWrapper.some(m => m.members.length === 0)) {
debugger;
}
const allLegacyGroupsInDb = getConversationController()
.getConversations()
.filter(SessionUtilUserGroups.isLegacyGroupToStoreInWrapper);
if (!incomingResult) {
await removeFromCache(envelope);
return;
const allLegacyGroupsIdsInDB = allLegacyGroupsInDb.map(m => m.id as string);
const allLegacyGroupsIdsInWrapper = allLegacyGroupsInWrapper.map(m => m.pubkeyHex);
const legacyGroupsToJoinInDB = allLegacyGroupsInWrapper.filter(m => {
return !allLegacyGroupsIdsInDB.includes(m.pubkeyHex);
});
const legacyGroupsToLeaveInDB = allLegacyGroupsInDb.filter(m => {
return !allLegacyGroupsIdsInWrapper.includes(m.id);
});
window.log.info(
`we have to join ${legacyGroupsToJoinInDB.length} legacy groups in DB compared to what is in the wrapper`
);
window.log.info(
`we have to leave ${legacyGroupsToLeaveInDB.length} legacy groups in DB compared to what is in the wrapper`
);
for (let index = 0; index < legacyGroupsToLeaveInDB.length; index++) {
const toLeave = legacyGroupsToLeaveInDB[index];
console.warn('leaving legacy group from configuration sync message with convoId ', toLeave.id);
await getConversationController().deleteContact(toLeave.id, true);
}
try {
let finalResult = incomingResult;
for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) {
const toJoin = legacyGroupsToJoinInDB[index];
console.warn(
'joining legacy group from configuration sync message with convoId ',
toJoin.pubkeyHex
);
switch (kind) {
case SignalService.SharedConfigMessage.Kind.USER_PROFILE:
finalResult = await handleUserProfileUpdate(incomingResult);
break;
case SignalService.SharedConfigMessage.Kind.CONTACTS:
finalResult = await handleContactsUpdate(incomingResult);
// let's just create the required convo here, as we update the fields right below
await getConversationController().getOrCreateAndWait(
toJoin.pubkeyHex,
ConversationTypeEnum.GROUP
);
}
for (let index = 0; index < allLegacyGroupsInWrapper.length; index++) {
const fromWrapper = allLegacyGroupsInWrapper[index];
if (fromWrapper.members.length === 0) {
debugger;
}
const convo = getConversationController().get(fromWrapper.pubkeyHex);
if (!convo) {
// this should not happen as we made sure to create them before
window.log.warn(
'could not find legacy group which should already be there:',
fromWrapper.pubkeyHex
);
continue;
}
const members = fromWrapper.members.map(m => m.pubkeyHex);
const admins = fromWrapper.members.filter(m => m.isAdmin).map(m => m.pubkeyHex);
// then for all the existing legacy group in the wrapper, we need to override the field of what we have in the DB with what is in the wrapper
// We only set group admins on group creation
const groupDetails: ClosedGroup.GroupInfo = {
id: fromWrapper.pubkeyHex,
name: fromWrapper.name,
members,
admins,
activeAt:
!!convo.get('active_at') && convo.get('active_at') < latestEnvelopeTimestamp
? convo.get('active_at')
: latestEnvelopeTimestamp,
weWereJustAdded: false, // TODO to remove
};
await ClosedGroup.updateOrCreateClosedGroup(groupDetails);
let changes = false;
if (convo.isPinned() !== fromWrapper.priority > 0) {
await convo.setIsPinned(fromWrapper.priority > 0, false);
changes = true;
}
if (!!convo.isHidden() !== !!fromWrapper.hidden) {
convo.set({ hidden: !!fromWrapper.hidden });
changes = true;
}
if (convo.get('expireTimer') !== fromWrapper.disappearingTimerSeconds) {
await convo.updateExpireTimer(
fromWrapper.disappearingTimerSeconds,
undefined,
latestEnvelopeTimestamp,
{
fromSync: true,
}
);
changes = true;
}
if (changes) {
await convo.commit();
}
// save the encryption keypair if needed
if (!isEmpty(fromWrapper.encPubkey) && !isEmpty(fromWrapper.encSeckey)) {
const inWrapperKeypair: HexKeyPair = {
publicHex: toHex(fromWrapper.encPubkey),
privateHex: toHex(fromWrapper.encSeckey),
};
await addKeyPairToCacheAndDBIfNeeded(fromWrapper.pubkeyHex, inWrapperKeypair);
}
}
// // if the convos already exists, make sure to update the fields if needed
// for (let index = 0; index < allCommunitiesInWrapper.length; index++) {
// const fromWrapper = allCommunitiesInWrapper[index];
// const convoId = OpenGroupUtils.getOpenGroupV2ConversationId(
// fromWrapper.baseUrl,
// fromWrapper.roomCasePreserved
// );
// const existingConvo = getConversationController().get(convoId);
// if (fromWrapper && existingConvo) {
// let changes = false;
// //TODO priority means more than just isPinned but has an order logic in it too
// const shouldBePinned = fromWrapper.priority > 0;
// if (shouldBePinned !== Boolean(existingConvo.isPinned())) {
// await existingConvo.setIsPinned(shouldBePinned, false);
// changes = true;
// }
// // make sure to write the changes to the database now as the `AvatarDownloadJob` below might take some time before getting run
// if (changes) {
// await existingConvo.commit();
// }
// }
// }
}
async function handleUserGroupsUpdate(result: IncomingConfResult): Promise<IncomingConfResult> {
if (!result.needsDump) {
return result;
}
const toHandle = SessionUtilUserGroups.getUserGroupTypes();
for (let index = 0; index < toHandle.length; index++) {
const typeToHandle = toHandle[index];
switch (typeToHandle) {
case 'Community':
await handleCommunitiesUpdate();
break;
case SignalService.SharedConfigMessage.Kind.USER_GROUPS:
finalResult = await handleUserGroupsUpdate(incomingResult);
case 'LegacyGroup':
await handleLegacyGroupUpdate(result.latestEnvelopeTimestamp);
break;
default:
throw new Error(`processMergingResults unknown kind of contact : ${kind}`);
assertUnreachable(typeToHandle, `handleUserGroupsUpdate unhandled type "${typeToHandle}"`);
}
const variant = LibSessionUtil.kindToVariant(kind);
if (finalResult.needsDump) {
// The config data had changes so regenerate the dump and save it
}
const dump = await GenericWrapperActions.dump(variant);
await ConfigDumpData.saveConfigDump({
data: dump,
publicKey: pubkey,
variant,
});
}
return result;
}
await removeFromCache(envelope);
} catch (e) {
window.log.error(`processMergingResults failed with ${e.message}`);
await removeFromCache(envelope);
async function processMergingResults(results: Map<ConfigWrapperObjectTypes, IncomingConfResult>) {
if (!results || !results.size) {
return;
}
const keys = [...results.keys()];
let anyNeedsPush = false;
for (let index = 0; index < keys.length; index++) {
const wrapperType = keys[index];
const incomingResult = results.get(wrapperType);
if (!incomingResult) {
continue;
}
try {
const { kind } = incomingResult;
switch (kind) {
case SignalService.SharedConfigMessage.Kind.USER_PROFILE:
await handleUserProfileUpdate(incomingResult);
break;
case SignalService.SharedConfigMessage.Kind.CONTACTS:
await handleContactsUpdate(incomingResult);
break;
case SignalService.SharedConfigMessage.Kind.USER_GROUPS:
await handleUserGroupsUpdate(incomingResult);
break;
default:
assertUnreachable(kind, `processMergingResults unsupported kind: "${kind}"`);
}
const variant = LibSessionUtil.kindToVariant(kind);
if (incomingResult.needsDump) {
// The config data had changes so regenerate the dump and save it
const dump = await GenericWrapperActions.dump(variant);
await ConfigDumpData.saveConfigDump({
data: dump,
publicKey: incomingResult.publicKey,
variant,
});
}
if (incomingResult.needsPush) {
anyNeedsPush = true;
}
} catch (e) {
window.log.error(`processMergingResults failed with ${e.message}`);
return;
}
}
// Now that the local state has been updated, trigger a config sync (this will push any
// pending updates and properly update the state)
if (result.result.needsPush) {
if (anyNeedsPush) {
await ConfigurationSync.queueNewJobIfNeeded();
}
}
async function handleConfigMessageViaLibSession(
envelope: EnvelopePlus,
configMessage: IncomingMessage<SignalService.ISharedConfigMessage>
async function handleConfigMessagesViaLibSession(
configMessages: Array<IncomingMessage<SignalService.ISharedConfigMessage>>
) {
// TODO: Remove this once `useSharedUtilForUserConfig` is permanent
if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) {
await removeFromCache(envelope);
return;
}
if (!configMessage) {
await removeFromCache(envelope);
if (isEmpty(configMessages)) {
return;
}
window?.log?.info('Handling our sharedConfig message via libsession_util.');
window?.log?.info(
`Handling our sharedConfig message via libsession_util: ${configMessages.length}`
);
const incomingMergeResult = await mergeConfigsWithIncomingUpdates(configMessage);
const incomingMergeResult = await mergeConfigsWithIncomingUpdates(configMessages);
await processMergingResults(envelope, incomingMergeResult);
await processMergingResults(incomingMergeResult);
}
async function handleOurProfileUpdateLegacy(
@ -564,5 +766,5 @@ async function handleConfigurationMessageLegacy(
export const ConfigMessageHandler = {
handleConfigurationMessageLegacy,
handleConfigMessageViaLibSession,
handleConfigMessagesViaLibSession,
};

@ -17,22 +17,22 @@ import { findCachedBlindedMatchOrLookupOnAllServers } from '../session/apis/open
import { getConversationController } from '../session/conversations';
import { concatUInt8Array, getSodiumRenderer } from '../session/crypto';
import { removeMessagePadding } from '../session/crypto/BufferPadding';
import { IncomingMessage } from '../session/messages/incoming/IncomingMessage';
import { ProfileManager } from '../session/profile_manager/ProfileManager';
import { GroupUtils, UserUtils } from '../session/utils';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { fromHexToArray, toHex } from '../session/utils/String';
import { assertUnreachable } from '../types/sqlSharedTypes';
import { BlockedNumberController } from '../util';
import { ReadReceipts } from '../util/readReceipts';
import { Storage } from '../util/storage';
import { handleCallMessage } from './callMessage';
import { getAllCachedECKeyPair } from './closedGroups';
import { ConfigMessageHandler } from './configMessage';
import { ECKeyPair } from './keypairs';
import { BlockedNumberController } from '../util';
export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) {
try {
const plaintext = await decrypt(envelope, envelope.content);
const plaintext = await decrypt(envelope);
if (!plaintext) {
return;
@ -48,7 +48,7 @@ export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageH
}
}
async function decryptForClosedGroup(envelope: EnvelopePlus, ciphertext: ArrayBuffer) {
async function decryptForClosedGroup(envelope: EnvelopePlus) {
// case .closedGroupCiphertext: for ios
window?.log?.info('received closed group message');
try {
@ -82,7 +82,7 @@ async function decryptForClosedGroup(envelope: EnvelopePlus, ciphertext: ArrayBu
decryptedContent = await decryptWithSessionProtocol(
envelope,
ciphertext,
envelope.content,
encryptionKeyPair,
true
);
@ -205,11 +205,15 @@ export async function decryptWithSessionProtocol(
return plaintext;
}
async function decryptUnidentifiedSender(
envelope: EnvelopePlus,
ciphertext: ArrayBuffer
/**
* This function is used to decrypt any messages send to our own pubkey.
* Either messages deposited into our swarm by other people, or messages we sent to ourselves, or config messages stored on the user namespaces.
* @param envelope the envelope contaning an encrypted .content field to decrypt
* @returns the decrypted content, or null
*/
export async function decryptEnvelopeWithOurKey(
envelope: EnvelopePlus
): Promise<ArrayBuffer | null> {
// window?.log?.info('received unidentified sender message');
try {
const userX25519KeyPair = await UserUtils.getIdentityKeyPair();
@ -225,7 +229,11 @@ async function decryptUnidentifiedSender(
// keep the await so the try catch works as expected
perfStart(`decryptUnidentifiedSender-${envelope.id}`);
const retSessionProtocol = await decryptWithSessionProtocol(envelope, ciphertext, ecKeyPair);
const retSessionProtocol = await decryptWithSessionProtocol(
envelope,
envelope.content,
ecKeyPair
);
const ret = removeMessagePadding(retSessionProtocol);
perfEnd(`decryptUnidentifiedSender-${envelope.id}`, 'decryptUnidentifiedSender');
@ -237,32 +245,28 @@ async function decryptUnidentifiedSender(
}
}
async function doDecrypt(
envelope: EnvelopePlus,
ciphertext: ArrayBuffer
): Promise<ArrayBuffer | null> {
if (ciphertext.byteLength === 0) {
throw new Error('Received an empty envelope.'); // Error.noData
}
switch (envelope.type) {
// Only SESSION_MESSAGE and CLOSED_GROUP_MESSAGE are supported
case SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE:
return decryptForClosedGroup(envelope, ciphertext);
case SignalService.Envelope.Type.SESSION_MESSAGE: {
return decryptUnidentifiedSender(envelope, ciphertext);
}
default:
throw new Error(`Unknown message type:${envelope.type}`);
}
}
// tslint:disable-next-line: max-func-body-length
async function decrypt(envelope: EnvelopePlus, ciphertext: ArrayBuffer): Promise<any> {
async function decrypt(envelope: EnvelopePlus): Promise<any> {
try {
const plaintext = await doDecrypt(envelope, ciphertext);
if (envelope.content.byteLength === 0) {
throw new Error('Received an empty envelope.');
}
let plaintext: ArrayBuffer | null = null;
switch (envelope.type) {
// Only SESSION_MESSAGE and CLOSED_GROUP_MESSAGE are supported
case SignalService.Envelope.Type.SESSION_MESSAGE:
plaintext = await decryptEnvelopeWithOurKey(envelope);
break;
case SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE:
plaintext = await decryptForClosedGroup(envelope);
break;
default:
assertUnreachable(envelope.type, `Unknown message type:${envelope.type}`);
}
if (!plaintext) {
// content could not be decrypted.
await removeFromCache(envelope);
return null;
}
@ -431,20 +435,10 @@ export async function innerHandleSwarmContentMessage(
return;
}
if (content.sharedConfigMessage) {
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
const asIncomingMsg: IncomingMessage<SignalService.ISharedConfigMessage> = {
envelopeTimestamp: sentAtTimestamp,
message: content.sharedConfigMessage,
messageHash: messageHash,
authorOrGroupPubkey: envelope.source,
authorInGroup: envelope.senderIdentity,
};
await ConfigMessageHandler.handleConfigMessageViaLibSession(envelope, asIncomingMsg);
return;
} else {
await removeFromCache(envelope);
return;
}
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
await removeFromCache(envelope);
return;
}
if (content.dataExtractionNotification) {
perfStart(`handleDataExtractionNotification-${envelope.id}`);

@ -16,10 +16,6 @@ import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { UnprocessedParameter } from '../types/sqlSharedTypes';
export type ReqOptions = {
conversationId: string;
};
const incomingMessagePromises: Array<Promise<any>> = [];
async function handleSwarmEnvelope(envelope: EnvelopePlus, messageHash: string) {
@ -73,7 +69,7 @@ function queueSwarmEnvelope(envelope: EnvelopePlus, messageHash: string) {
async function handleRequestDetail(
plaintext: Uint8Array,
options: ReqOptions,
inConversation: string | null,
lastPromise: Promise<any>,
messageHash: string
): Promise<void> {
@ -83,7 +79,7 @@ async function handleRequestDetail(
// fault, and we should handle them gracefully and tell the
// user they received an invalid message
// The message is for a medium size group
if (options.conversationId) {
if (inConversation) {
const ourNumber = UserUtils.getOurPubKeyStrFromCache();
const senderIdentity = envelope.source;
@ -93,7 +89,7 @@ async function handleRequestDetail(
// Sender identity will be lost if we load from cache, because
// plaintext (and protobuf.Envelope) does not have that field...
envelope.source = options.conversationId;
envelope.source = inConversation;
// tslint:disable-next-line no-parameter-reassignment
plaintext = SignalService.Envelope.encode(envelope).finish();
envelope.senderIdentity = senderIdentity;
@ -124,17 +120,23 @@ async function handleRequestDetail(
}
}
/**
*
* @param inConversation if the request is related to a group, this will be set to the group pubkey. Otherwise, it is set to null
*/
export function handleRequest(
plaintext: Uint8Array,
options: ReqOptions,
inConversation: string | null,
messageHash: string
): void {
// tslint:disable-next-line no-promise-as-boolean
const lastPromise = _.last(incomingMessagePromises) || Promise.resolve();
const promise = handleRequestDetail(plaintext, options, lastPromise, messageHash).catch(e => {
window?.log?.error('Error handling incoming message:', e && e.stack ? e.stack : e);
});
const promise = handleRequestDetail(plaintext, inConversation, lastPromise, messageHash).catch(
e => {
window?.log?.error('Error handling incoming message:', e && e.stack ? e.stack : e);
}
);
incomingMessagePromises.push(promise);
}

@ -81,7 +81,8 @@ async function joinOpenGroupV2(
} else if (existingConvo) {
// we already have a convo associated with it. Remove everything related to it so we start fresh
window?.log?.warn('leaving before rejoining open group v2 room', conversationId);
await getConversationController().deleteContact(conversationId);
await getConversationController().deleteContact(conversationId, true);
}
// Try to connect to server

@ -115,14 +115,15 @@ export class OpenGroupManagerV2 {
}
const inWrapperCommunities = SessionUtilUserGroups.getAllCommunities();
const inWrapperIds = inWrapperCommunities.map(m =>
getOpenGroupV2ConversationId(m.baseUrl, m.roomCasePreserved)
);
let allRoomInfos = OpenGroupData.getAllV2OpenGroupRoomsMap();
// this is time for some cleanup!
// We consider the wrapper is our source-of-truth,
// Itis time for some cleanup!
// We consider the wrapper to be our source-of-truth,
// so if there is a roomInfos without an associated entry in the wrapper, we remove it from the map of opengroups rooms
if (allRoomInfos?.size) {
const roomInfosAsArray = [...allRoomInfos.values()];
@ -135,7 +136,7 @@ export class OpenGroupManagerV2 {
await OpenGroupData.removeV2OpenGroupRoom(roomConvoId);
getOpenGroupManager().removeRoomFromPolledRooms(infos);
await getConversationController().deleteContact(roomConvoId);
await getConversationController().deleteContact(roomConvoId, false);
}
} catch (e) {
window?.log?.warn('cleanup roomInfos error', e);

@ -37,6 +37,7 @@ import { Data } from '../../../../data/data';
import { processMessagesUsingCache } from './sogsV3MutationCache';
import { destroyMessagesAndUpdateRedux } from '../../../../util/expiringMessages';
import { sogsRollingDeletions } from './sogsRollingDeletions';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
/**
* Get the convo matching those criteria and make sure it is an opengroup convo, or return null.
@ -550,8 +551,21 @@ export const handleBatchPollResults = async (
case 'outbox':
await handleInboxOutboxMessages(subResponse.body, serverUrl, true);
break;
case 'addRemoveModerators':
case 'deleteMessage':
case 'banUnbanUser':
case 'deleteAllPosts':
case 'updateRoom':
case 'deleteReaction':
// we do nothing for all of those, but let's make sure if we ever add something batch polled for, we include it's handling here.
// the assertUnreachable will fail to compile everytime we add a new batch poll endpoint without taking care of it.
break;
default:
window.log.error('No matching subrequest response body for type: ', responseType);
assertUnreachable(
responseType,
`No matching subrequest response body for type: "${responseType}"`
);
}
}
}

@ -1,14 +1,15 @@
import { AbortSignal } from 'abort-controller';
import { flatten, isEmpty, isNumber, isObject } from 'lodash';
import { OpenGroupData } from '../../../../data/opengroups';
import _, { flatten, isEmpty, isNumber, isObject } from 'lodash';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { Reactions } from '../../../../util/reactions';
import { OnionSending, OnionV4JSONSnodeResponse } from '../../../onions/onionSend';
import {
OpenGroupPollingUtils,
OpenGroupRequestHeaders,
} from '../opengroupV2/OpenGroupPollingUtils';
import { addJsonContentTypeToHeaders } from './sogsV3SendMessage';
import { AbortSignal } from 'abort-controller';
import { roomHasBlindEnabled } from './sogsV3Capabilities';
import { Reactions } from '../../../../util/reactions';
import { addJsonContentTypeToHeaders } from './sogsV3SendMessage';
type BatchFetchRequestOptions = {
method: 'POST' | 'PUT' | 'GET' | 'DELETE';
@ -228,7 +229,8 @@ export type OpenGroupBatchRow =
const makeBatchRequestPayload = (
options: OpenGroupBatchRow
): BatchSubRequest | Array<BatchSubRequest> | null => {
switch (options.type) {
const type = options.type;
switch (type) {
case 'capabilities':
return {
method: 'GET',
@ -323,7 +325,7 @@ const makeBatchRequestPayload = (
path: `/room/${options.deleteReaction.roomId}/reactions/${options.deleteReaction.messageId}/${options.deleteReaction.reaction}`,
};
default:
throw new Error('Invalid batch request row');
assertUnreachable(type, 'Invalid batch request row');
}
return null;

@ -1,31 +1,33 @@
import { PubKey } from '../../types';
import * as snodePool from './snodePool';
import { ERROR_CODE_NO_CONNECT } from './SNodeAPI';
import { compact, concat, difference, flatten, last, sample, toNumber, uniqBy } from 'lodash';
import { Data, Snode } from '../../../data/data';
import { SignalService } from '../../../protobuf';
import * as Receiver from '../../../receiver/receiver';
import _, { compact, concat, difference, flatten, last, sample, uniqBy } from 'lodash';
import { Data, Snode } from '../../../data/data';
import { PubKey } from '../../types';
import { ERROR_CODE_NO_CONNECT } from './SNodeAPI';
import * as snodePool from './snodePool';
import { StringUtils, UserUtils } from '../../utils';
import pRetry from 'p-retry';
import { ConversationModel } from '../../../models/conversation';
import { ConfigMessageHandler } from '../../../receiver/configMessage';
import { decryptEnvelopeWithOurKey } from '../../../receiver/contentMessage';
import { EnvelopePlus } from '../../../receiver/types';
import { updateIsOnline } from '../../../state/ducks/onion';
import { DURATION, SWARM_POLLING_TIMEOUT } from '../../constants';
import { getConversationController } from '../../conversations';
import { perfEnd, perfStart } from '../../utils/Performance';
import { IncomingMessage } from '../../messages/incoming/IncomingMessage';
import { ed25519Str } from '../../onions/onionPath';
import { updateIsOnline } from '../../../state/ducks/onion';
import pRetry from 'p-retry';
import { SnodeAPIRetrieve } from './retrieveRequest';
import { StringUtils, UserUtils } from '../../utils';
import { perfEnd, perfStart } from '../../utils/Performance';
import { SnodeNamespace, SnodeNamespaces } from './namespaces';
import { SnodeAPIRetrieve } from './retrieveRequest';
import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types';
export function extractWebSocketContent(
message: string,
options: any = {},
messageHash: string
): null | {
body: Uint8Array;
messageHash: string;
options: any;
} {
try {
const dataPlaintext = new Uint8Array(StringUtils.encode(message, 'base64'));
@ -37,12 +39,11 @@ export function extractWebSocketContent(
return {
body: messageBuf.request.body,
messageHash,
options,
};
}
return null;
} catch (error) {
window?.log?.warn('processMessage Failed to handle message:', error.message);
window?.log?.warn('extractWebSocketContent from message failed with:', error.message);
return null;
}
}
@ -196,9 +197,9 @@ export class SwarmPolling {
isGroup: boolean,
namespaces: Array<SnodeNamespaces>
) {
const pkStr = pubkey.key;
const polledPubkey = pubkey.key;
const swarmSnodes = await snodePool.getSwarmFor(pkStr);
const swarmSnodes = await snodePool.getSwarmFor(polledPubkey);
// Select nodes for which we already have lastHashes
const alreadyPolled = swarmSnodes.filter((n: Snode) => this.lastHashes[n.pubkey_ed25519]);
@ -220,7 +221,6 @@ export class SwarmPolling {
resultsFromAllNamespaces = null;
}
let userConfigMessagesMerged: Array<RetrieveMessageItem> = [];
let allNamespacesWithoutUserConfigIfNeeded: Array<RetrieveMessageItem> = [];
// check if we just fetched the details from the config namespaces.
@ -237,14 +237,29 @@ export class SwarmPolling {
.map(r => r.messages.messages)
)
);
userConfigMessagesMerged = flatten(compact(userConfigMessages));
if (!isGroup) {
const userConfigMessagesMerged = flatten(compact(userConfigMessages));
window.log.info(
`received userConfigMessagesMerged: ${userConfigMessagesMerged.length} for key ${pubkey.key}`
);
try {
await this.handleSharedConfigMessages(userConfigMessagesMerged);
} catch (e) {
window.log.warn(
`handleSharedConfigMessages of ${userConfigMessagesMerged.length} failed with ${e.message}`
);
// not rethrowing
}
}
// first make sure to handle the shared user config message first
} else {
allNamespacesWithoutUserConfigIfNeeded = flatten(
compact(resultsFromAllNamespaces?.map(m => m.messages.messages))
);
}
window.log.info(`received userConfigMessagesMerged: ${userConfigMessagesMerged.length}`);
window.log.info(
`received allNamespacesWithoutUserConfigIfNeeded: ${allNamespacesWithoutUserConfigIfNeeded.length}`
);
@ -274,40 +289,76 @@ export class SwarmPolling {
}
return group;
});
} else if (isGroup) {
window?.log?.info(
`Polled for group(${ed25519Str(
pubkey.key
)}):, but no snode returned something else than null.`
);
}
perfStart(`handleSeenMessages-${pkStr}`);
perfStart(`handleSeenMessages-${polledPubkey}`);
const newMessages = await this.handleSeenMessages(messages);
perfEnd(`handleSeenMessages-${pkStr}`, 'handleSeenMessages');
perfEnd(`handleSeenMessages-${polledPubkey}`, 'handleSeenMessages');
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
const extractedUserConfigMessage = compact(
userConfigMessagesMerged.map((m: RetrieveMessageItem) => {
return extractWebSocketContent(m.data, {}, m.hash);
})
);
// trigger the handling of all the other messages, not shared config related
newMessages.forEach(m => {
const content = extractWebSocketContent(m.data, m.hash);
if (!content) {
return;
}
extractedUserConfigMessage.forEach(m => {
Receiver.handleRequest(m.body, m.options, m.messageHash);
});
}
Receiver.handleRequest(content.body, isGroup ? polledPubkey : null, content.messageHash);
});
}
const extractedContentMessage = compact(
newMessages.map((m: RetrieveMessageItem) => {
const options = isGroup ? { conversationId: pkStr } : {};
return extractWebSocketContent(m.data, options, m.hash);
private async handleSharedConfigMessages(userConfigMessagesMerged: Array<RetrieveMessageItem>) {
const extractedUserConfigMessage = compact(
userConfigMessagesMerged.map((m: RetrieveMessageItem) => {
return extractWebSocketContent(m.data, m.hash);
})
);
extractedContentMessage.forEach(m => {
Receiver.handleRequest(m.body, m.options, m.messageHash);
});
const allDecryptedConfigMessages: Array<IncomingMessage<
SignalService.ISharedConfigMessage
>> = [];
for (let index = 0; index < extractedUserConfigMessage.length; index++) {
const userConfigMessage = extractedUserConfigMessage[index];
try {
const envelope: EnvelopePlus = SignalService.Envelope.decode(userConfigMessage.body) as any;
const decryptedEnvelope = await decryptEnvelopeWithOurKey(envelope);
if (!decryptedEnvelope?.byteLength) {
continue;
}
const content = SignalService.Content.decode(new Uint8Array(decryptedEnvelope));
if (content.sharedConfigMessage) {
const asIncomingMsg: IncomingMessage<SignalService.ISharedConfigMessage> = {
envelopeTimestamp: toNumber(envelope.timestamp),
message: content.sharedConfigMessage,
messageHash: userConfigMessage.messageHash,
authorOrGroupPubkey: envelope.source,
authorInGroup: envelope.senderIdentity,
};
allDecryptedConfigMessages.push(asIncomingMsg);
} else {
throw new Error(
'received a message to a namespace reserved for user config but not containign a sharedConfigMessage'
);
}
} catch (e) {
window.log.warn(
`failed to decrypt message with hash "${userConfigMessage.messageHash}": ${e.message}`
);
}
}
try {
window.log.info(
`handleConfigMessagesViaLibSession of "${allDecryptedConfigMessages.length}" messages with libsession`
);
await ConfigMessageHandler.handleConfigMessagesViaLibSession(allDecryptedConfigMessages);
} catch (e) {
const allMessageHases = allDecryptedConfigMessages.map(m => m.messageHash).join(',');
window.log.warn(
`failed to handle messages hashes "${allMessageHases}" with libsession. Error: "${e.message}"`
);
}
}
// Fetches messages for `pubkey` from `node` potentially updating
@ -458,9 +509,9 @@ export class SwarmPolling {
hash: string;
expiration: number;
}): Promise<void> {
if (!SnodeNamespace.isNamespaceAlwaysPolled(namespace)) {
return;
}
// if (!SnodeNamespace.isNamespaceAlwaysPolled(namespace)) {
// return;
// }
const pkStr = pubkey.key;
const cached = await this.getLastHash(edkey, pubkey.key, namespace);
@ -484,9 +535,9 @@ export class SwarmPolling {
}
private async getLastHash(nodeEdKey: string, pubkey: string, namespace: number): Promise<string> {
if (!SnodeNamespace.isNamespaceAlwaysPolled(namespace)) {
return '';
}
// if (!SnodeNamespace.isNamespaceAlwaysPolled(namespace)) {
// return '';
// }
if (!this.lastHashes[nodeEdKey]?.[pubkey]?.[namespace]) {
const lastHash = await Data.getLastHashBySnode(pubkey, nodeEdKey, namespace);

@ -11,6 +11,8 @@ import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/con
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { SessionUtilContact } from '../utils/libsession/libsession_utils_contacts';
import { SessionUtilUserGroups } from '../utils/libsession/libsession_utils_user_groups';
import { leaveClosedGroup } from '../group/closed-group';
import { ConfigurationSync } from '../utils/job_runners/jobs/ConfigurationSyncJob';
let instance: ConversationController | null;
@ -197,7 +199,7 @@ export class ConversationController {
await conversation.commit();
}
public async deleteContact(id: string) {
public async deleteContact(id: string, fromSyncMessage: boolean) {
if (!this._initialFetchComplete) {
throw new Error('getConversationController().deleteContact() needs complete initial fetch');
}
@ -210,12 +212,17 @@ export class ConversationController {
return;
}
// those are the stuff to do for all conversation types
window.log.info(`deleteContact destroyingMessages: ${id}`);
await deleteAllMessagesByConvoIdNoConfirmation(id);
window.log.info(`deleteContact messages destroyed: ${id}`);
// Closed/Medium group leaving
if (conversation.isClosedGroup()) {
window.log.info(`deleteContact ClosedGroup case: ${id}`);
await conversation.leaveClosedGroup();
await leaveClosedGroup(conversation.id);
// open group v2
} else if (conversation.isOpenGroupV2()) {
} else if (conversation.isPublic()) {
window?.log?.info('leaving open group v2', conversation.id);
const roomInfos = OpenGroupData.getV2OpenGroupRoom(conversation.id);
if (roomInfos) {
@ -229,24 +236,17 @@ export class ConversationController {
window?.log?.info('removeV2OpenGroupRoom failed:', e);
}
try {
console.error(' plop1', SessionUtilUserGroups.getAllCommunities());
await SessionUtilUserGroups.removeCommunityFromWrapper(conversation.id, conversation.id);
console.error(' plop2', SessionUtilUserGroups.getAllCommunities());
} catch (e) {
window?.log?.info('SessionUtilUserGroups.removeCommunityFromWrapper failed:', e);
}
}
// those are the stuff to do for all contact types
window.log.info(`deleteContact destroyingMessages: ${id}`);
await deleteAllMessagesByConvoIdNoConfirmation(conversation.id);
window.log.info(`deleteContact messages destroyed: ${id}`);
// if this conversation is a private conversation it's in fact a `contact` for desktop.
// we just want to remove everything related to it, set the active_at to undefined
// so conversation still exists (useful for medium groups members or opengroups) but is not shown on the UI
if (conversation.isPrivate()) {
window.log.info(`deleteContact isPrivate, marking as inactive: ${id}`);
// if this conversation is a private conversation it's in fact a `contact` for desktop.
// we just want to remove everything related to it and set the hidden field to true
// so the conversation still exists (needed for that user's profile in groups) but is not shown on the list of conversation
window.log.info(`deleteContact isPrivate, marking as hidden: ${id}`);
conversation.set({
hidden: true,
@ -256,11 +256,11 @@ export class ConversationController {
await conversation.commit();
// TODO the call above won't mark the conversation as hidden in the wrapper, it will just stop being updated (which is a bad thing)
} else {
window.log.info(`deleteContact !isPrivate, removing convo from DB: ${id}`);
window.log.info(`deleteContact NOT private, removing convo from DB: ${id}`);
// not a private conversation, so not a contact for the ContactWrapper
await Data.removeConversation(id);
window.log.info(`deleteContact !isPrivate, convo removed from DB: ${id}`);
window.log.info(`deleteContact NOT private, convo removed from DB: ${id}`);
// TODO remove group related entries from their corresponding wrappers here
this.conversations.remove(conversation);
@ -273,7 +273,11 @@ export class ConversationController {
);
window.inboxStore?.dispatch(conversationActions.conversationRemoved(conversation.id));
window.log.info(`deleteContact !isPrivate, convo removed from store: ${id}`);
window.log.info(`deleteContact NOT private, convo removed from store: ${id}`);
}
if (!fromSyncMessage) {
await ConfigurationSync.queueNewJobIfNeeded();
}
}
@ -306,18 +310,22 @@ export class ConversationController {
console.time('refreshAllWrapperContactsData');
for (let index = 0; index < collection.models.length; index++) {
const convo = collection.models[index];
if (SessionUtilContact.filterContactsToStoreInContactsWrapper(convo)) {
if (SessionUtilContact.isContactToStoreInContactsWrapper(convo)) {
await SessionUtilContact.refreshMappedValue(convo.id, true);
}
if (SessionUtilUserGroups.filterUserCommunitiesToStoreInWrapper(convo)) {
await SessionUtilUserGroups.refreshCommunityMappedValue(convo.id, true);
if (SessionUtilUserGroups.isUserGroupToStoreInWrapper(convo)) {
await SessionUtilUserGroups.refreshMappedValue(convo.id, true);
}
}
console.timeEnd('refreshAllWrapperContactsData');
this._initialFetchComplete = true;
this.conversations.forEach((conversation: ConversationModel) => {
if (conversation.isActive() && !conversation.get('lastMessage')) {
if (
conversation.isActive() &&
!conversation.isHidden() &&
!conversation.get('lastMessage')
) {
conversation.updateLastMessage();
}
});

@ -106,11 +106,7 @@ export async function createClosedGroup(groupName: string, members: Array<string
if (allInvitesSent) {
const newHexKeypair = encryptionKeyPair.toHexKeyPair();
const isHexKeyPairSaved = await addKeyPairToCacheAndDBIfNeeded(groupPublicKey, newHexKeypair);
if (isHexKeyPairSaved) {
window?.log?.info('Dropping already saved keypair for group', groupPublicKey);
}
await addKeyPairToCacheAndDBIfNeeded(groupPublicKey, newHexKeypair);
// Subscribe to this group id
getSwarmPollingInstance().addGroupId(new PubKey(groupPublicKey));

@ -235,7 +235,7 @@ function isV3(details: GroupInfo | GroupInfoV3): details is GroupInfoV3 {
}
export async function updateOrCreateClosedGroup(details: GroupInfo | GroupInfoV3) {
const { id, weWereJustAdded } = details;
const { id, weWereJustAdded, expireTimer } = details;
const conversation = await getConversationController().getOrCreateAndWait(
id,
@ -279,8 +279,6 @@ export async function updateOrCreateClosedGroup(details: GroupInfo | GroupInfoV3
await conversation.commit();
const { expireTimer } = details;
if (expireTimer === undefined || typeof expireTimer !== 'number') {
return;
}
@ -297,10 +295,11 @@ export async function updateOrCreateClosedGroup(details: GroupInfo | GroupInfoV3
export async function leaveClosedGroup(groupId: string) {
const convo = getConversationController().get(groupId);
if (!convo) {
if (!convo || !convo.isMediumGroup()) {
window?.log?.error('Cannot leave non-existing group');
return;
}
const ourNumber = UserUtils.getOurPubKeyFromCache();
const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourNumber.key);

@ -258,7 +258,6 @@ export class MessageQueue {
*/
public async processPending(device: PubKey, isSyncMessage: boolean = false) {
const messages = await this.pendingMessageCache.getForDevice(device);
console.warn('processPending', messages);
const jobQueue = this.getJobQueue(device);
messages.forEach(async message => {

@ -223,7 +223,6 @@ async function sendMessagesDataToSnode(
})
: null;
console.error('oldMessageHashes', oldMessageHashes);
const snode = sample(swarm);
if (!snode) {
throw new EmptySwarmError(destination, 'Ran out of swarm nodes to query');

@ -2,7 +2,7 @@ import { isNumber } from 'lodash';
import { v4 } from 'uuid';
import { UserUtils } from '../..';
import { ConfigDumpData } from '../../../../data/configDump/configDump';
import { ConfigWrapperObjectTypes } from '../../../../webworker/workers/browser/libsession_worker_functions';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { GenericWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { DURATION } from '../../../constants';
import { getConversationController } from '../../../conversations';
@ -25,16 +25,10 @@ const defaultMaxAttempts = 2;
*/
let lastRunConfigSyncJobDumpTimestamp: number | null = null;
const variantsToSaveRegularly: Array<ConfigWrapperObjectTypes> = [
'UserConfig',
'ContactsConfig',
'UserGroupsConfig',
];
async function saveDumpsNeededToDB(): Promise<boolean> {
let savedAtLeastOne = false;
for (let i = 0; i < variantsToSaveRegularly.length; i++) {
const variant = variantsToSaveRegularly[i];
for (let i = 0; i < LibSessionUtil.userVariants.length; i++) {
const variant = LibSessionUtil.userVariants[i];
const needsDump = await GenericWrapperActions.needsDump(variant);
if (!needsDump) {
@ -100,9 +94,24 @@ class ConfigurationSyncDumpJob extends PersistedJob<ConfigurationSyncDumpPersist
// refresh all the data stored by the wrappers we need to store.
// so when we call needsDump(), we know for sure that we are up to date
console.time('ConfigurationSyncDumpJob insertAll');
await LibSessionUtil.insertUserProfileIntoWrapper();
await LibSessionUtil.insertAllContactsIntoContactsWrapper();
await LibSessionUtil.insertAllUserGroupsIntoWrapper();
for (let index = 0; index < LibSessionUtil.userVariants.length; index++) {
const variant = LibSessionUtil.userVariants[index];
switch (variant) {
case 'UserConfig':
await LibSessionUtil.insertUserProfileIntoWrapper();
break;
case 'ContactsConfig':
await LibSessionUtil.insertAllContactsIntoContactsWrapper();
break;
case 'UserGroupsConfig':
await LibSessionUtil.insertAllUserGroupsIntoWrapper();
break;
default:
assertUnreachable(variant, `ConfigurationSyncDumpJob unhandled variant: "${variant}"`);
}
}
console.timeEnd('ConfigurationSyncDumpJob insertAll');
await saveDumpsNeededToDB();
return RunJobResult.Success;
@ -148,6 +157,9 @@ class ConfigurationSyncDumpJob extends PersistedJob<ConfigurationSyncDumpPersist
* A ConfigurationSyncJob can only be added if there is none of the same type queued already.
*/
async function queueNewJobIfNeeded() {
if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) {
return;
}
if (
!lastRunConfigSyncJobDumpTimestamp ||
lastRunConfigSyncJobDumpTimestamp < Date.now() - defaultMsBetweenRetries

@ -2,6 +2,7 @@ import { compact, groupBy, isArray, isEmpty, isNumber, isString } from 'lodash';
import { v4 } from 'uuid';
import { UserUtils } from '../..';
import { ConfigDumpData } from '../../../../data/configDump/configDump';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { GenericWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { NotEmptyArrayOfBatchResults } from '../../../apis/snode_api/SnodeRequestTypes';
import { getConversationController } from '../../../conversations';
@ -129,7 +130,11 @@ async function buildAndSaveDumpsToDB(changes: Array<SuccessfulChange>): Promise<
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
const variant = LibSessionUtil.kindToVariant(change.message.kind);
console.warn('buildAndSaveDumpsToDB: change.updatedHash: ', change.updatedHash);
console.warn(
`ConfigurationSyncJob.saveDumpToDB: "${variant}" updatedHash: "${
change.updatedHash
}:${change.message.seqno.toNumber()}"`
);
const needsDump = await LibSessionUtil.markAsPushed(
variant,
change.publicKey,
@ -186,9 +191,23 @@ class ConfigurationSyncJob extends PersistedJob<ConfigurationSyncPersistedData>
window.log.warn('did not find our own conversation');
return RunJobResult.PermanentFailure;
}
await LibSessionUtil.insertUserProfileIntoWrapper();
await LibSessionUtil.insertAllContactsIntoContactsWrapper();
await LibSessionUtil.insertAllUserGroupsIntoWrapper();
for (let index = 0; index < LibSessionUtil.userVariants.length; index++) {
const variant = LibSessionUtil.userVariants[index];
switch (variant) {
case 'UserConfig':
await LibSessionUtil.insertUserProfileIntoWrapper();
break;
case 'ContactsConfig':
await LibSessionUtil.insertAllContactsIntoContactsWrapper();
break;
case 'UserGroupsConfig':
await LibSessionUtil.insertAllUserGroupsIntoWrapper();
break;
default:
assertUnreachable(variant, `ConfigurationSyncDumpJob unhandled variant: "${variant}"`);
}
}
const singleDestChanges = await retrieveSingleDestinationChanges();
@ -211,7 +230,6 @@ class ConfigurationSyncJob extends PersistedJob<ConfigurationSyncPersistedData>
};
});
const asSet = new Set(dest.allOldHashes);
console.warn('asSet', [...asSet]);
return MessageSender.sendMessagesToSnode(msgs, dest.destination, asSet);
})
);
@ -271,6 +289,9 @@ class ConfigurationSyncJob extends PersistedJob<ConfigurationSyncPersistedData>
* A ConfigurationSyncJob can only be added if there is none of the same type queued already.
*/
async function queueNewJobIfNeeded() {
if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) {
return;
}
if (
!lastRunConfigSyncJobTimestamp ||
lastRunConfigSyncJobTimestamp < Date.now() - defaultMsBetweenRetries

@ -3,6 +3,7 @@ import Long from 'long';
import { UserUtils } from '..';
import { ConfigDumpData } from '../../../data/configDump/configDump';
import { SignalService } from '../../../protobuf';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
import { ConfigWrapperObjectTypes } from '../../../webworker/workers/browser/libsession_worker_functions';
import { GenericWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
import { GetNetworkTime } from '../../apis/snode_api/getNetworkTime';
@ -14,7 +15,7 @@ import { SessionUtilUserGroups } from './libsession_utils_user_groups';
import { SessionUtilUserProfile } from './libsession_utils_user_profile';
// TODO complete this list
const requiredUserDumpVariants: Array<ConfigWrapperObjectTypes> = [
const userVariants: Array<ConfigWrapperObjectTypes> = [
'UserConfig',
'ContactsConfig',
'UserGroupsConfig',
@ -24,7 +25,9 @@ export type IncomingConfResult = {
needsPush: boolean;
needsDump: boolean;
messageHashes: Array<string>;
latestSentTimestamp: number;
kind: SignalService.SharedConfigMessage.Kind;
publicKey: string;
latestEnvelopeTimestamp: number;
};
export type OutgoingConfResult = {
@ -42,7 +45,7 @@ async function initializeLibSessionUtilWrappers() {
const privateKeyEd25519 = keypair.privKeyBytes;
// let's plan a sync on start for now
// TODO is this causing any issues?
setTimeout(() => ConfigurationSync.queueNewJobIfNeeded, 3000);
setTimeout(() => ConfigurationSync.queueNewJobIfNeeded, 20000);
// fetch the dumps we already have from the database
const dumps = await ConfigDumpData.getAllDumpsWithData();
@ -75,13 +78,13 @@ async function initializeLibSessionUtilWrappers() {
console.warn('requiredVariants: FIXME add conversation volatile wrapper as required ');
const missingRequiredVariants: Array<ConfigWrapperObjectTypes> = difference(
requiredUserDumpVariants,
LibSessionUtil.userVariants,
[...userVariantsBuildWithoutErrors.values()]
);
if (missingRequiredVariants.length) {
// TODO this is only needed for debugging. Should be removed as we force them created right below with an empty dump
throw new Error(`missingRequiredVariants: ${JSON.stringify(missingRequiredVariants)}`);
// throw new Error(`missingRequiredVariants: ${JSON.stringify(missingRequiredVariants)}`);
}
for (let index = 0; index < missingRequiredVariants.length; index++) {
@ -97,7 +100,7 @@ async function pendingChangesForPubkey(pubkey: string): Promise<Array<OutgoingCo
// Ensure we always check the required user config types for changes even if there is no dump
// data yet (to deal with first launch cases)
if (pubkey === us) {
LibSessionUtil.requiredUserDumpVariants.forEach(requiredVariant => {
LibSessionUtil.userVariants.forEach(requiredVariant => {
if (!dumps.find(m => m.publicKey === us && m.variant === requiredVariant)) {
dumps.push({
publicKey: us,
@ -146,7 +149,7 @@ function kindToVariant(kind: SignalService.SharedConfigMessage.Kind): ConfigWrap
case SignalService.SharedConfigMessage.Kind.USER_GROUPS:
return 'UserGroupsConfig';
default:
throw new Error(`kindToVariant: Unsupported variant: "${kind}"`);
assertUnreachable(kind, `kindToVariant: Unsupported variant: "${kind}"`);
}
}
@ -159,7 +162,7 @@ function variantToKind(variant: ConfigWrapperObjectTypes): SignalService.SharedC
case 'UserGroupsConfig':
return SignalService.SharedConfigMessage.Kind.USER_GROUPS;
default:
throw new Error(`variantToKind: Unsupported kind: "${variant}"`);
assertUnreachable(variant, `variantToKind: Unsupported kind: "${variant}"`);
}
}
@ -181,12 +184,13 @@ async function markAsPushed(
export const LibSessionUtil = {
initializeLibSessionUtilWrappers,
requiredUserDumpVariants,
userVariants,
pendingChangesForPubkey,
insertUserProfileIntoWrapper: SessionUtilUserProfile.insertUserProfileIntoWrapper,
insertAllContactsIntoContactsWrapper: SessionUtilContact.insertAllContactsIntoContactsWrapper,
insertAllUserGroupsIntoWrapper: SessionUtilUserGroups.insertAllUserGroupsIntoWrapper,
removeCommunityFromWrapper: SessionUtilUserGroups.removeCommunityFromWrapper,
kindToVariant,
variantToKind,
markAsPushed,
};

@ -31,7 +31,7 @@ const mappedContactWrapperValues = new Map<string, ContactInfo>();
async function insertAllContactsIntoContactsWrapper() {
const idsToInsert = getConversationController()
.getConversations()
.filter(filterContactsToStoreInContactsWrapper)
.filter(isContactToStoreInContactsWrapper)
.map(m => m.id);
window.log.debug(`ContactsWrapper keep tracks of ${idsToInsert.length} contacts`);
@ -48,7 +48,7 @@ async function insertAllContactsIntoContactsWrapper() {
* `isApproved` or `didApproveMe` field set.
* So that would be all the private conversations we either sent or receive a message from, not blinded
*/
function filterContactsToStoreInContactsWrapper(convo: ConversationModel): boolean {
function isContactToStoreInContactsWrapper(convo: ConversationModel): boolean {
return (
!convo.isMe() &&
convo.isPrivate() &&
@ -69,7 +69,7 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise<voi
return;
}
if (!filterContactsToStoreInContactsWrapper(foundConvo)) {
if (!isContactToStoreInContactsWrapper(foundConvo)) {
// window.log.info(`insertContactFromDBIntoWrapperAndRefresh: convo ${id} should not be saved. Skipping`);
return;
}
@ -133,7 +133,7 @@ function getMappedValue(id: string) {
}
export const SessionUtilContact = {
filterContactsToStoreInContactsWrapper,
isContactToStoreInContactsWrapper,
insertAllContactsIntoContactsWrapper,
insertContactFromDBIntoWrapperAndRefresh,
getMappedValue,

@ -1,18 +1,26 @@
import { isEmpty, uniq } from 'lodash';
import { CommunityInfo } from 'session_util_wrapper';
import { uniq } from 'lodash';
import { CommunityInfo, LegacyGroupInfo, UserGroupsType } from 'session_util_wrapper';
import { Data } from '../../../data/data';
import { OpenGroupData } from '../../../data/opengroups';
import { ConversationModel } from '../../../models/conversation';
import { getCommunityInfoFromDBValues } from '../../../types/sqlSharedTypes';
import {
getCommunityInfoFromDBValues,
getLegacyGroupInfoFromDBValues,
} from '../../../types/sqlSharedTypes';
import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
import { OpenGroupUtils } from '../../apis/open_group_api/utils';
import { getConversationController } from '../../conversations';
/**
* The key of this map is the convoId as stored in the database.
* Note: the wrapper clean
*/
const mappedCommunityWrapperValues = new Map<string, CommunityInfo>();
/**
* The key of this map is the convoId as stored in the database. So the legacy group 05 sessionID
*/
const mappedLegacyGroupWrapperValues = new Map<string, LegacyGroupInfo>();
/**
* Update the UserGroupsWrapper with all the data is cares about from the database.
*/
@ -20,19 +28,12 @@ async function insertAllUserGroupsIntoWrapper() {
const convoIdsToInsert = uniq(
getConversationController()
.getConversations()
.filter(filterUserGroupsToStoreInWrapper)
.map(m => m.id)
);
const communitiesIdsToInsert = uniq(
getConversationController()
.getConversations()
.filter(filterUserCommunitiesToStoreInWrapper)
.filter(isUserGroupToStoreInWrapper)
.map(m => m.id)
);
window.log.debug(
`UserGroupsWrapper keep tracks of ${convoIdsToInsert.length} usergroups of which ${communitiesIdsToInsert.length} are communities`
`UserGroupsWrapper keep tracks of ${convoIdsToInsert.length} usergroups including groups and communities`
);
for (let index = 0; index < convoIdsToInsert.length; index++) {
@ -45,17 +46,29 @@ async function insertAllUserGroupsIntoWrapper() {
/**
* Returns true if that conversation is an active group
*/
function filterUserGroupsToStoreInWrapper(convo: ConversationModel): boolean {
return convo.isGroup() && convo.isActive();
function isUserGroupToStoreInWrapper(convo: ConversationModel): boolean {
return isCommunityToStoreInWrapper(convo) || isLegacyGroupToStoreInWrapper(convo);
}
function isCommunityToStoreInWrapper(convo: ConversationModel): boolean {
return convo.isGroup() && convo.isPublic() && convo.isActive();
}
function filterUserCommunitiesToStoreInWrapper(convo: ConversationModel): boolean {
return convo.isPublic() && convo.isActive();
function isLegacyGroupToStoreInWrapper(convo: ConversationModel): boolean {
return (
convo.isGroup() &&
!convo.isPublic() &&
convo.id.startsWith('05') && // new closed groups won't start with 05
convo.isActive() &&
!convo.get('isKickedFromGroup') &&
!convo.get('left')
);
}
/**
* Fetches the specified convo and updates the required field in the wrapper.
* If that community does not exist in the wrapper, it is created before being updated.
* Same applies for a legacy group.
*/
async function insertGroupsFromDBIntoWrapperAndRefresh(convoId: string): Promise<void> {
const foundConvo = getConversationController().get(convoId);
@ -63,13 +76,12 @@ async function insertGroupsFromDBIntoWrapperAndRefresh(convoId: string): Promise
return;
}
if (!filterUserGroupsToStoreInWrapper(foundConvo)) {
if (!isUserGroupToStoreInWrapper(foundConvo)) {
return;
}
if (foundConvo.isOpenGroupV2()) {
if (isCommunityToStoreInWrapper(foundConvo)) {
const asOpengroup = foundConvo.toOpenGroupV2();
const isPinned = !!foundConvo.get('isPinned');
const roomDetails = OpenGroupData.getV2OpenGroupRoomByRoomId(asOpengroup);
if (!roomDetails) {
@ -84,64 +96,85 @@ async function insertGroupsFromDBIntoWrapperAndRefresh(convoId: string): Promise
);
const wrapperComm = getCommunityInfoFromDBValues({
isPinned,
isPinned: !!foundConvo.get('isPinned'),
fullUrl,
});
try {
console.info(`inserting into usergroup wrapper ${convoId}...`);
console.info(`inserting into usergroup wrapper "${wrapperComm.fullUrl}"...`);
// this does the create or the update of the matching existing community
await UserGroupsWrapperActions.setCommunityByFullUrl(
wrapperComm.fullUrl,
wrapperComm.priority
);
await refreshMappedValue(convoId);
} catch (e) {
window.log.warn(`UserGroupsWrapperActions.set of ${convoId} failed with ${e.message}`);
// we still let this go through
}
} else if (isLegacyGroupToStoreInWrapper(foundConvo)) {
const encryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair(convoId);
const wrapperLegacyGroup = getLegacyGroupInfoFromDBValues({
id: foundConvo.id,
isPinned: !!foundConvo.get('isPinned'),
members: foundConvo.get('members') || [],
groupAdmins: foundConvo.get('groupAdmins') || [],
expireTimer: foundConvo.get('expireTimer'),
displayNameInProfile: foundConvo.get('displayNameInProfile'),
hidden: false,
encPubkeyHex: encryptionKeyPair?.publicHex || '',
encSeckeyHex: encryptionKeyPair?.privateHex || '',
});
if (wrapperLegacyGroup.members.length === 0) {
debugger;
}
try {
console.info(`inserting into usergroup wrapper "${foundConvo.id}"...`);
// this does the create or the update of the matching existing legacy group
await refreshCommunityMappedValue(convoId);
} else {
// TODO
// throw new Error('insertGroupsFromDBIntoWrapperAndRefresh group and legacy todo');
await UserGroupsWrapperActions.setLegacyGroup(wrapperLegacyGroup);
await refreshMappedValue(convoId);
} catch (e) {
window.log.warn(`UserGroupsWrapperActions.set of ${convoId} failed with ${e.message}`);
// we still let this go through
}
}
}
/**
* refreshMappedValue is used to query the UserGroups Wrapper for the details of that group and update the cached in-memory entry representing its content.
* @param id the pubkey to re fresh the cached value from
* @param id the pubkey to re fresh the cached value from1
* @param duringAppStart set this to true if we should just fetch the cached value but not trigger a UI refresh of the corresponding conversation
*/
async function refreshCommunityMappedValue(convoId: string, duringAppStart = false) {
async function refreshMappedValue(convoId: string, duringAppStart = false) {
try {
if (!OpenGroupUtils.isOpenGroupV2(convoId)) {
throw new Error(`Not an opengroupv2: "${convoId}"`);
}
const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(convoId);
if (fromWrapper) {
SessionUtilUserGroups.setCommunityMappedValue(convoId, fromWrapper);
if (!duringAppStart) {
getConversationController()
.get(convoId)
?.triggerUIRefresh();
let refreshed = false;
if (OpenGroupUtils.isOpenGroupV2(convoId)) {
const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(convoId);
if (fromWrapper && fromWrapper.fullUrl) {
mappedCommunityWrapperValues.set(convoId, fromWrapper);
}
refreshed = true;
} else if (convoId.startsWith('05')) {
// currently this should only be a legacy group here
const fromWrapper = await UserGroupsWrapperActions.getLegacyGroup(convoId);
if (fromWrapper) {
mappedLegacyGroupWrapperValues.set(convoId, fromWrapper);
}
refreshed = true;
}
return;
} catch (e) {
window.log.info(`refreshCommunityMappedValue: not an opengroup convoID: ${convoId}`, e);
}
// TODO
// throw new Error('refreshMappedValue group and legacy todo');
}
function setCommunityMappedValue(convoId: string, info: CommunityInfo) {
if (isEmpty(info.fullUrl)) {
throw new Error(`setCommunityMappedValue needs a valid info.fullUrl ${info.fullUrl}`);
if (refreshed && !duringAppStart) {
getConversationController()
.get(convoId)
?.triggerUIRefresh();
}
} catch (e) {
window.log.info(`refreshMappedValue: not an opengroup convoID: ${convoId}`, e);
}
// this has the pubkey associated with it
mappedCommunityWrapperValues.set(convoId, info);
// TODO handle the new closed groups once we got them ready
}
function getCommunityMappedValueByConvoId(convoId: string) {
@ -153,7 +186,7 @@ function getAllCommunities(): Array<CommunityInfo> {
}
/**
* Remove the matching community from the wrapper and from the cached list of communities
* Removes the matching community from the wrapper and from the cached list of communities
*/
async function removeCommunityFromWrapper(convoId: string, fullUrlWithOrWithoutPubkey: string) {
const fromWrapper = await UserGroupsWrapperActions.getCommunityByFullUrl(
@ -163,18 +196,58 @@ async function removeCommunityFromWrapper(convoId: string, fullUrlWithOrWithoutP
if (fromWrapper) {
await UserGroupsWrapperActions.eraseCommunityByFullUrl(fromWrapper.fullUrl);
}
// might not be there but better make sure
mappedCommunityWrapperValues.delete(convoId);
}
function getLegacyGroupMappedValueByConvoId(convoId: string) {
return mappedLegacyGroupWrapperValues.get(convoId);
}
function getAllLegacyGroups(): Array<LegacyGroupInfo> {
return [...mappedLegacyGroupWrapperValues.values()];
}
/**
* Remove the matching legacy group from the wrapper and from the cached list of legacy groups
*/
async function removeLegacyGroupFromWrapper(groupPk: string) {
const fromWrapper = await UserGroupsWrapperActions.getLegacyGroup(groupPk);
if (fromWrapper) {
await UserGroupsWrapperActions.eraseLegacyGroup(groupPk);
}
mappedLegacyGroupWrapperValues.delete(groupPk);
}
/**
* This function can be used where there are things to do for all the types handled by this wrapper.
* You can do a loop on all the types handled by this wrapper and have a switch using assertUnreachable to get errors when not every case is handled.
*
*
* Note: Ideally, we'd like to have this type in the wrapper index.d.ts, but it would require it to be a index.ts instead, which causes a whole other bunch of issues because it is a native node module.
*/
function getUserGroupTypes(): Array<UserGroupsType> {
return ['Community', 'LegacyGroup'];
}
export const SessionUtilUserGroups = {
filterUserGroupsToStoreInWrapper,
filterUserCommunitiesToStoreInWrapper,
getAllCommunities,
// shared
isUserGroupToStoreInWrapper,
insertAllUserGroupsIntoWrapper,
insertGroupsFromDBIntoWrapperAndRefresh,
setCommunityMappedValue,
refreshMappedValue,
getUserGroupTypes,
// communities
isCommunityToStoreInWrapper,
getAllCommunities,
getCommunityMappedValueByConvoId,
refreshCommunityMappedValue,
removeCommunityFromWrapper,
// legacy group
isLegacyGroupToStoreInWrapper,
getLegacyGroupMappedValueByConvoId,
getAllLegacyGroups,
removeLegacyGroupFromWrapper, // a group can be removed but also just marked hidden, so only call this function when the group is completely removed // TODO
};

@ -76,6 +76,7 @@ export const syncConfigurationIfNeeded = async () => {
export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = false) =>
new Promise(resolve => {
// TODO this should check for feature flag and queue a ConfigurationSyncJob
const allConvos = getConversationController().getConversations();
// if we hang for more than 10sec, force resolve this promise.
setTimeout(() => {

@ -309,6 +309,7 @@ describe('libsession_contacts', () => {
id: '051111567890acbdef',
type: ConversationTypeEnum.PRIVATE,
isApproved: true,
active_at: 123,
didApproveMe: true,
};
beforeEach(() => {
@ -320,7 +321,7 @@ describe('libsession_contacts', () => {
it('excludes ourselves', () => {
expect(
SessionUtilContact.filterContactsToStoreInContactsWrapper(
SessionUtilContact.isContactToStoreInContactsWrapper(
new ConversationModel({ ...validArgs, id: ourNumber } as any)
)
).to.be.eq(false);
@ -328,7 +329,7 @@ describe('libsession_contacts', () => {
it('excludes non private', () => {
expect(
SessionUtilContact.filterContactsToStoreInContactsWrapper(
SessionUtilContact.isContactToStoreInContactsWrapper(
new ConversationModel({ ...validArgs, type: ConversationTypeEnum.GROUP } as any)
)
).to.be.eq(false);
@ -336,15 +337,27 @@ describe('libsession_contacts', () => {
it('includes private', () => {
expect(
SessionUtilContact.filterContactsToStoreInContactsWrapper(
SessionUtilContact.isContactToStoreInContactsWrapper(
new ConversationModel({ ...validArgs, type: ConversationTypeEnum.PRIVATE } as any)
)
).to.be.eq(true);
});
it('includes hidden private', () => {
expect(
SessionUtilContact.isContactToStoreInContactsWrapper(
new ConversationModel({
...validArgs,
type: ConversationTypeEnum.PRIVATE,
hidden: true,
} as any)
)
).to.be.eq(true);
});
it('excludes blinded', () => {
expect(
SessionUtilContact.filterContactsToStoreInContactsWrapper(
SessionUtilContact.isContactToStoreInContactsWrapper(
new ConversationModel({
...validArgs,
type: ConversationTypeEnum.PRIVATE,
@ -356,7 +369,7 @@ describe('libsession_contacts', () => {
it('excludes non approved by us nor did approveme', () => {
expect(
SessionUtilContact.filterContactsToStoreInContactsWrapper(
SessionUtilContact.isContactToStoreInContactsWrapper(
new ConversationModel({
...validArgs,
didApproveMe: false,
@ -368,7 +381,7 @@ describe('libsession_contacts', () => {
it('includes approved only by us ', () => {
expect(
SessionUtilContact.filterContactsToStoreInContactsWrapper(
SessionUtilContact.isContactToStoreInContactsWrapper(
new ConversationModel({
...validArgs,
didApproveMe: false,
@ -377,9 +390,23 @@ describe('libsession_contacts', () => {
)
).to.be.eq(true);
});
it('excludes not active ', () => {
expect(
SessionUtilContact.isContactToStoreInContactsWrapper(
new ConversationModel({
...validArgs,
didApproveMe: false,
isApproved: true,
active_at: undefined,
} as any)
)
).to.be.eq(false);
});
it('includes approved only by them ', () => {
expect(
SessionUtilContact.filterContactsToStoreInContactsWrapper(
SessionUtilContact.isContactToStoreInContactsWrapper(
new ConversationModel({
...validArgs,
didApproveMe: true,

@ -25,49 +25,120 @@ describe('libsession_groups', () => {
Sinon.restore();
});
it('includes public group/community', () => {
expect(
SessionUtilUserGroups.filterUserGroupsToStoreInWrapper(
new ConversationModel({ ...validArgs } as any)
)
).to.be.eq(true);
describe('communities', () => {
it('includes public group/community', () => {
expect(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({ ...validArgs } as any)
)
).to.be.eq(true);
});
it('excludes public group/community inactive', () => {
expect(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({ ...validArgs, active_at: undefined } as any)
)
).to.be.eq(false);
});
});
describe('legacy closed groups', () => {
const validLegacyGroupArgs = {
...validArgs,
type: ConversationTypeEnum.GROUP,
id: '05123456564',
} as any;
it('includes legacy group', () => {
expect(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({
...validLegacyGroupArgs,
})
)
).to.be.eq(true);
});
it('exclude legacy group left', () => {
expect(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({
...validLegacyGroupArgs,
left: true,
})
)
).to.be.eq(false);
});
it('exclude legacy group kicked', () => {
expect(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({
...validLegacyGroupArgs,
isKickedFromGroup: true,
})
)
).to.be.eq(false);
});
it('exclude legacy group not active', () => {
expect(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({
...validLegacyGroupArgs,
active_at: undefined,
})
)
).to.be.eq(false);
});
it('include hidden legacy group', () => {
expect(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({
...validLegacyGroupArgs,
hidden: true,
})
)
).to.be.eq(true);
});
});
it('excludes public group/community inactive', () => {
it('excludes closed group v3 (for now)', () => {
expect(
SessionUtilUserGroups.filterUserGroupsToStoreInWrapper(
new ConversationModel({ ...validArgs, active_at: undefined } as any)
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({
...validArgs,
type: ConversationTypeEnum.GROUPV3,
id: '03123456564',
} as any)
)
).to.be.eq(false);
});
it('includes closed group', () => {
it('excludes empty id', () => {
expect(
SessionUtilUserGroups.filterUserGroupsToStoreInWrapper(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({
...validArgs,
type: ConversationTypeEnum.GROUP,
id: '05123456564',
id: '',
} as any)
)
).to.be.eq(true);
});
).to.be.eq(false);
it('includes closed group v3', () => {
expect(
SessionUtilUserGroups.filterUserGroupsToStoreInWrapper(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({
...validArgs,
type: ConversationTypeEnum.GROUPV3,
id: '03123456564',
id: '9871',
} as any)
)
).to.be.eq(true);
).to.be.eq(false);
});
it('excludes private', () => {
expect(
SessionUtilUserGroups.filterUserGroupsToStoreInWrapper(
SessionUtilUserGroups.isUserGroupToStoreInWrapper(
new ConversationModel({
...validArgs,
id: '0511111',

@ -1,22 +0,0 @@
// tslint:disable: no-implicit-dependencies
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import Sinon from 'sinon';
chai.use(chaiAsPromised as any);
describe('SyncUtils', () => {
afterEach(() => {
Sinon.restore();
});
describe('syncConfigurationIfNeeded', () => {
it.skip('sync if last sync undefined', () => {
// TestUtils.stubData('getItemById').resolves(undefined);
// sandbox.stub(ConversationController, 'getConversations').returns([]);
// const getCurrentConfigurationMessageSpy = sandbox.spy(MessageUtils, 'getCurrentConfigurationMessage');
// await syncConfigurationIfNeeded();
// expect(getCurrentConfigurationMessageSpy.callCount).equal(1);
});
});
});

@ -1,5 +1,7 @@
import { isEmpty, isEqual } from 'lodash';
import { ContactInfo } from 'session_util_wrapper';
import { from_hex } from 'libsodium-wrappers-sumo';
import { isArray, isEmpty, isEqual } from 'lodash';
import { ContactInfo, LegacyGroupInfo, LegacyGroupMemberInfo } from 'session_util_wrapper';
import { ConversationAttributes } from '../models/conversationAttributes';
import { OpenGroupRequestCommonType } from '../session/apis/open_group_api/opengroupV2/ApiUtil';
import { fromHexToArray } from '../session/utils/String';
import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions';
@ -162,3 +164,71 @@ export function getCommunityInfoFromDBValues({
return community;
}
function maybeArrayJSONtoArray(arr: string | Array<string>): Array<string> {
try {
if (isArray(arr)) {
return arr;
}
const parsed = JSON.parse(arr);
if (isArray(parsed)) {
return parsed;
}
return [];
} catch (e) {
return [];
}
}
export function getLegacyGroupInfoFromDBValues({
id,
hidden,
isPinned,
members: maybeMembers,
displayNameInProfile,
expireTimer,
encPubkeyHex,
encSeckeyHex,
groupAdmins: maybeAdmins,
}: Pick<
ConversationAttributes,
'hidden' | 'id' | 'isPinned' | 'displayNameInProfile' | 'expireTimer'
> & {
encPubkeyHex: string;
encSeckeyHex: string;
members: string | Array<string>;
groupAdmins: string | Array<string>;
}) {
const admins: Array<string> = maybeArrayJSONtoArray(maybeAdmins);
const members: Array<string> = maybeArrayJSONtoArray(maybeMembers);
const wrappedMembers: Array<LegacyGroupMemberInfo> = (members || []).map(m => {
return {
isAdmin: admins.includes(m),
pubkeyHex: m,
};
});
const legacyGroup: LegacyGroupInfo = {
pubkeyHex: id,
disappearingTimerSeconds: !expireTimer ? 0 : expireTimer,
hidden: !!hidden,
name: displayNameInProfile || '',
priority: !!isPinned ? 1 : 0, // TODO the priority handling is not that simple
members: wrappedMembers,
encPubkey: !isEmpty(encPubkeyHex) ? from_hex(encPubkeyHex) : new Uint8Array(),
encSeckey: !isEmpty(encSeckeyHex) ? from_hex(encSeckeyHex) : new Uint8Array(),
};
console.warn('legacyGroup', legacyGroup);
return legacyGroup;
}
/**
* This function can be used to make sure all the possible values as input of a switch as taken care off, without having a default case.
*
*/
export function assertUnreachable(_x: never, message: string): never {
console.info(`assertUnreachable: Didn't expect to get here with "${message}"`);
throw new Error("Didn't expect to get here");
}

@ -7,6 +7,7 @@ import {
BaseWrapperActionsCalls,
ContactInfo,
ContactsWrapperActionsCalls,
LegacyGroupInfo,
UserConfigWrapperActionsCalls,
UserGroupsWrapperActionsCalls,
} from 'session_util_wrapper';
@ -222,6 +223,25 @@ export const UserGroupsWrapperActions: UserGroupsWrapperActionsCalls = {
roomId,
pubkeyHex,
]) as Promise<ReturnType<UserGroupsWrapperActionsCalls['buildFullUrlFromDetails']>>,
getLegacyGroup: async (pubkeyHex: string) =>
callLibSessionWorker(['UserGroupsConfig', 'getLegacyGroup', pubkeyHex]) as Promise<
ReturnType<UserGroupsWrapperActionsCalls['getLegacyGroup']>
>,
getAllLegacyGroups: async () =>
callLibSessionWorker(['UserGroupsConfig', 'getAllLegacyGroups']) as Promise<
ReturnType<UserGroupsWrapperActionsCalls['getAllLegacyGroups']>
>,
setLegacyGroup: async (info: LegacyGroupInfo) =>
callLibSessionWorker(['UserGroupsConfig', 'setLegacyGroup', info]) as Promise<
ReturnType<UserGroupsWrapperActionsCalls['setLegacyGroup']>
>,
eraseLegacyGroup: async (pubkeyHex: string) =>
callLibSessionWorker(['UserGroupsConfig', 'eraseLegacyGroup', pubkeyHex]) as Promise<
ReturnType<UserGroupsWrapperActionsCalls['eraseLegacyGroup']>
>,
};
const callLibSessionWorker = async (callToMake: LibSessionWorkerFunctions): Promise<unknown> => {

@ -1,4 +1,4 @@
import _, { isEmpty, isNull } from 'lodash';
import { isEmpty, isNull } from 'lodash';
import {
BaseConfigWrapper,
BaseConfigWrapperInsideWorker,
@ -64,7 +64,7 @@ function assertUserWrapperType(wrapperType: ConfigWrapperObjectTypes): UserWrapp
*/
function initUserWrapper(options: Array<any>, wrapperType: UserWrapperType): BaseConfigWrapper {
const wrapper = getUserWrapper(wrapperType);
console.warn('initUserWrapper: ', wrapperType);
console.warn('initUserWrapper: ', wrapperType, options);
if (wrapper) {
throw new Error(`${wrapperType} already init`);
}

Loading…
Cancel
Save