diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx index bf164ced8..3252852a5 100644 --- a/ts/components/MemberListItem.tsx +++ b/ts/components/MemberListItem.tsx @@ -1,10 +1,12 @@ import styled from 'styled-components'; import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; +import { useNicknameOrProfileNameOrShortenedPubkey } from '../hooks/useParamSelector'; +import { promoteUsersInGroup } from '../interactions/conversationInteractions'; import { PubKey } from '../session/types'; import { UserUtils } from '../session/utils'; import { GroupInvite } from '../session/utils/job_runners/jobs/GroupInviteJob'; -import { GroupPromote } from '../session/utils/job_runners/jobs/GroupPromoteJob'; +import { hasClosedGroupV2QAButtons } from '../shared/env_vars'; import { useMemberInviteFailed, useMemberInviteSending, @@ -13,6 +15,7 @@ import { useMemberPromotionFailed, useMemberPromotionSent, } from '../state/selectors/groups'; +import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar'; import { Flex } from './basic/Flex'; import { SessionButton, @@ -20,10 +23,7 @@ import { SessionButtonShape, SessionButtonType, } from './basic/SessionButton'; -import { useNicknameOrProfileNameOrShortenedPubkey } from '../hooks/useParamSelector'; -import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar'; import { SessionRadio } from './basic/SessionRadio'; -import { hasClosedGroupV2QAButtons } from '../shared/env_vars'; const AvatarContainer = styled.div` position: relative; @@ -252,7 +252,10 @@ const ResendPromoteButton = ({ buttonColor={SessionButtonColor.Danger} text="PrOmOtE" onClick={() => { - void GroupPromote.addJob({ groupPk, member: pubkey }); + void promoteUsersInGroup({ + groupPk, + toPromote: [pubkey], + }); }} /> ); diff --git a/ts/components/dialog/InviteContactsDialog.tsx b/ts/components/dialog/InviteContactsDialog.tsx index 5c25a99eb..5f7b95ecf 100644 --- a/ts/components/dialog/InviteContactsDialog.tsx +++ b/ts/components/dialog/InviteContactsDialog.tsx @@ -187,7 +187,7 @@ const InviteContactsDialogInner = (props: Props) => { - {/* TODO: localize those strings once out releasing those buttons for real */} + {/* TODO: localize those strings once out releasing those buttons for real Remove after QA */} {isGroupV2 && isDevProd() && ( <> diff --git a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx index 9c7722532..fd7e66cd1 100644 --- a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx +++ b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx @@ -194,7 +194,7 @@ export const OverlayClosedGroupV2 = () => { /> - {/* TODO: localize those strings once out releasing those buttons for real */} + {/* TODO: localize those strings once out releasing those buttons for real Remove after QA */} {isDevProd() && ( <> diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index b33aa1c6d..d8bba60f4 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -32,7 +32,7 @@ import { showBanUserByConvoId, showInviteContactByConvoId, showLeaveGroupByConvoId, - showLeavePrivateConversationbyConvoId, + showLeavePrivateConversationByConvoId, showRemoveModeratorsByConvoId, showUnbanUserByConvoId, showUpdateGroupNameByConvoId, @@ -58,7 +58,10 @@ import { useSelectedConversationKey } from '../../state/selectors/selectedConver import type { LocalizerToken } from '../../types/localizer'; import { SessionButtonColor } from '../basic/SessionButton'; import { ItemWithDataTestId } from './items/MenuItemWithDataTestId'; -import { ConversationInteractionStatus, ConversationInteractionType } from '../../interactions/types'; +import { + ConversationInteractionStatus, + ConversationInteractionType, +} from '../../interactions/types'; /** Menu items standardized */ @@ -420,7 +423,7 @@ export const DeletePrivateConversationMenuItem = () => { return ( { - showLeavePrivateConversationbyConvoId(convoId); + showLeavePrivateConversationByConvoId(convoId); }} > {isMe ? window.i18n('noteToSelfHide') : window.i18n('conversationsDelete')} diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 6107ee36a..5a5172660 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -1,4 +1,5 @@ -import { isNil } from 'lodash'; +import { isEmpty, isNil, uniq } from 'lodash'; +import { PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs'; import { ConversationNotificationSettingType, READ_MESSAGE_STATE, @@ -53,6 +54,11 @@ import { BlockedNumberController } from '../util'; import { LocalizerComponentProps, LocalizerToken } from '../types/localizer'; import { sendInviteResponseToGroup } from '../session/sending/group/GroupInviteResponse'; import { NetworkTime } from '../util/NetworkTime'; +import { ClosedGroup } from '../session'; +import { GroupUpdateMessageFactory } from '../session/messages/message_factory/group/groupUpdateMessageFactory'; +import { GroupPromote } from '../session/utils/job_runners/jobs/GroupPromoteJob'; +import { MessageSender } from '../session/sending'; +import { StoreGroupRequestFactory } from '../session/apis/snode_api/factories/StoreGroupRequestFactory'; export async function copyPublicKeyByConvoId(convoId: string) { if (OpenGroupUtils.isOpenGroupV2(convoId)) { @@ -114,7 +120,7 @@ export const handleAcceptConversationRequest = async ({ convoId }: { convoId: st if (PubKey.is03Pubkey(convoId)) { const found = await UserGroupsWrapperActions.getGroup(convoId); if (!found) { - window.log.warn('cannot approve a non existing group in usergroup'); + window.log.warn('cannot approve a non existing group in user group'); return null; } // this updates the wrapper and refresh the redux slice @@ -307,7 +313,7 @@ export async function showUpdateGroupMembersByConvoId(conversationId: string) { window.inboxStore?.dispatch(updateGroupMembersModal({ conversationId })); } -export function showLeavePrivateConversationbyConvoId(conversationId: string) { +export function showLeavePrivateConversationByConvoId(conversationId: string) { const conversation = ConvoHub.use().get(conversationId); const isMe = conversation.isMe(); @@ -334,7 +340,7 @@ export function showLeavePrivateConversationbyConvoId(conversationId: string) { }); await clearConversationInteractionState({ conversationId }); } catch (err) { - window.log.warn(`showLeavePrivateConversationbyConvoId error: ${err}`); + window.log.warn(`showLeavePrivateConversationByConvoId error: ${err}`); await saveConversationInteractionErrorAsMessage({ conversationId, interactionType: isMe @@ -954,3 +960,73 @@ async function saveConversationInteractionErrorAsMessage({ conversation.updateLastMessage(); } + +export async function promoteUsersInGroup({ + groupPk, + toPromote, +}: { toPromote: Array } & WithGroupPubkey) { + if (!toPromote.length) { + window.log.debug('promoteUsersInGroup: no users to promote'); + return; + } + + const convo = ConvoHub.use().get(groupPk); + if (!convo) { + window.log.debug('promoteUsersInGroup: group convo not found'); + return; + } + + const groupInWrapper = await UserGroupsWrapperActions.getGroup(groupPk); + if (!groupInWrapper || !groupInWrapper.secretKey || isEmpty(groupInWrapper.secretKey)) { + window.log.debug('promoteUsersInGroup: groupInWrapper not found or no secretkey'); + return; + } + + // push one group change message were initial members are added to the group + const membersHex = uniq(toPromote); + const sentAt = NetworkTime.now(); + const us = UserUtils.getOurPubKeyStrFromCache(); + const msgModel = await ClosedGroup.addUpdateMessage({ + diff: { type: 'promoted', promoted: membersHex }, + expireUpdate: null, + sender: us, + sentAt, + convo, + markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier + }); + const groupMemberChange = await GroupUpdateMessageFactory.getPromotedControlMessage({ + adminSecretKey: groupInWrapper.secretKey, + convo, + groupPk, + promoted: membersHex, + createAtNetworkTimestamp: sentAt, + dbMsgIdentifier: msgModel.id, + }); + + if (!groupMemberChange) { + window.log.warn('promoteUsersInGroup: failed to build group change'); + throw new Error('promoteUsersInGroup: failed to build group change'); + } + + const storeRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest( + [groupMemberChange], + groupInWrapper + ); + + const result = await MessageSender.sendEncryptedDataToSnode({ + destination: groupPk, + method: 'batch', + sortedSubRequests: storeRequests, + }); + + if (result?.[0].code !== 200) { + window.log.warn('promoteUsersInGroup: failed to store change'); + throw new Error('promoteUsersInGroup: failed to store change'); + } + + for (let index = 0; index < membersHex.length; index++) { + const member = membersHex[index]; + // eslint-disable-next-line no-await-in-loop + await GroupPromote.addJob({ groupPk, member }); + } +} diff --git a/ts/models/message.ts b/ts/models/message.ts index 7c11fe757..5791d1629 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -320,9 +320,9 @@ export class MessageModel extends Backbone.Model { // TODO: clean up this typing return window.i18n.stripped(...([token, args] as GetMessageArgs)); } - if (groupUpdate.promoted) { + if (groupUpdate.promoted?.length) { // @ts-expect-error -- TODO: Fix by using new i18n builder - const { token, args } = getPromotedGroupUpdateChangeStr(groupUpdate.kicked, groupName); + const { token, args } = getPromotedGroupUpdateChangeStr(groupUpdate.promoted, groupName); // TODO: clean up this typing return window.i18n.stripped(...([token, args] as GetMessageArgs)); } @@ -1455,6 +1455,12 @@ export class MessageModel extends Backbone.Model { ? [groupUpdate.kicked] : undefined; + forcedArrayUpdate.promoted = Array.isArray(groupUpdate.promoted) + ? groupUpdate.promoted + : groupUpdate.promoted + ? [groupUpdate.promoted] + : undefined; + forcedArrayUpdate.left = Array.isArray(groupUpdate.left) ? groupUpdate.left : groupUpdate.left diff --git a/ts/session/apis/snode_api/SnodeRequestTypes.ts b/ts/session/apis/snode_api/SnodeRequestTypes.ts index b37014a86..1f849e2c0 100644 --- a/ts/session/apis/snode_api/SnodeRequestTypes.ts +++ b/ts/session/apis/snode_api/SnodeRequestTypes.ts @@ -17,15 +17,10 @@ import { } from './namespaces'; import { GroupDetailsNeededForSignature, SnodeGroupSignature } from './signature/groupSignature'; import { SnodeSignature } from './signature/snodeSignatures'; -import { - ShortenOrExtend, - WithMessagesHashes, - WithSecretKey, - WithSignature, - WithTimestamp, -} from './types'; +import { ShortenOrExtend, WithMessagesHashes } from './types'; import { TTL_DEFAULT } from '../../constants'; import { NetworkTime } from '../../../util/NetworkTime'; +import { WithSecretKey, WithSignature, WithTimestamp } from '../../types/with'; type WithMaxSize = { max_size?: number }; export type WithShortenOrExtend = { shortenOrExtend: 'shorten' | 'extend' | '' }; diff --git a/ts/session/apis/snode_api/signature/snodeSignatures.ts b/ts/session/apis/snode_api/signature/snodeSignatures.ts index 027aeadb8..fd49eb36f 100644 --- a/ts/session/apis/snode_api/signature/snodeSignatures.ts +++ b/ts/session/apis/snode_api/signature/snodeSignatures.ts @@ -11,14 +11,9 @@ import { PubKey } from '../../../types'; import { StringUtils, UserUtils } from '../../../utils'; import { fromHexToArray, fromUInt8ArrayToBase64 } from '../../../utils/String'; import { PreConditionFailed } from '../../../utils/errors'; -import { - SignedHashesParams, - WithMessagesHashes, - WithShortenOrExtend, - WithSignature, - WithTimestamp, -} from '../types'; +import { SignedHashesParams, WithMessagesHashes, WithShortenOrExtend } from '../types'; import { NetworkTime } from '../../../../util/NetworkTime'; +import { WithSignature, WithTimestamp } from '../../../types/with'; export type SnodeSignatureResult = WithSignature & WithTimestamp & { diff --git a/ts/session/apis/snode_api/types.ts b/ts/session/apis/snode_api/types.ts index 127ce6d2c..e3eeae587 100644 --- a/ts/session/apis/snode_api/types.ts +++ b/ts/session/apis/snode_api/types.ts @@ -2,6 +2,7 @@ import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; import { SnodeNamespaces } from './namespaces'; import { SubaccountRevokeSubRequest, SubaccountUnrevokeSubRequest } from './SnodeRequestTypes'; +import { WithSignature, WithTimestamp } from '../../types/with'; export type RetrieveMessageItem = { hash: string; @@ -10,10 +11,9 @@ export type RetrieveMessageItem = { storedAt: number; // **not** the envelope timestamp, but when the message was effectively stored on the snode }; - export type RetrieveMessageItemWithNamespace = RetrieveMessageItem & { namespace: SnodeNamespaces; // the namespace from which this message was fetched -} +}; export type RetrieveMessagesResultsContent = { hf?: Array; @@ -31,9 +31,6 @@ export type WithMessagesHashes = { messagesHashes: Array }; export type RetrieveMessagesResultsBatched = Array; -export type WithTimestamp = { timestamp: number }; -export type WithSignature = { signature: string }; -export type WithSecretKey = { secretKey: Uint8Array }; export type ShortenOrExtend = 'extend' | 'shorten' | ''; export type WithShortenOrExtend = { shortenOrExtend: ShortenOrExtend }; diff --git a/ts/session/messages/message_factory/group/groupUpdateMessageFactory.ts b/ts/session/messages/message_factory/group/groupUpdateMessageFactory.ts new file mode 100644 index 000000000..97da4af52 --- /dev/null +++ b/ts/session/messages/message_factory/group/groupUpdateMessageFactory.ts @@ -0,0 +1,155 @@ +import { Uint8ArrayLen64, WithGroupPubkey } from 'libsession_util_nodejs'; +import { getSodiumRenderer } from '../../../crypto'; +import { DisappearingMessages } from '../../../disappearing_messages'; + +import { GroupUpdateMemberChangeMessage } from '../../outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage'; +import { ConversationModel } from '../../../../models/conversation'; +import { + WithAddWithHistoryMembers, + WithAddWithoutHistoryMembers, + WithFromMemberLeftMessage, + WithPromotedMembers, + WithRemoveMembers, +} from '../../../types/with'; + +/** + * Return the control messages to be pushed to the group's swarm. + * Those are not going to change the state, they are just here as a "notification". + * i.e. "Alice was removed from the group" + */ +async function getRemovedControlMessage({ + convo, + groupPk, + removed, + adminSecretKey, + createAtNetworkTimestamp, + fromMemberLeftMessage, + dbMsgIdentifier, +}: WithFromMemberLeftMessage & + WithRemoveMembers & + WithGroupPubkey & { + convo: ConversationModel; + adminSecretKey: Uint8ArrayLen64; + createAtNetworkTimestamp: number; + dbMsgIdentifier: string; + }) { + const sodium = await getSodiumRenderer(); + + if (fromMemberLeftMessage || !removed.length) { + return null; + } + + return new GroupUpdateMemberChangeMessage({ + identifier: dbMsgIdentifier, + removed, + groupPk, + typeOfChange: 'removed', + createAtNetworkTimestamp, + secretKey: adminSecretKey, + sodium, + ...DisappearingMessages.getExpireDetailsForOutgoingMessage(convo, createAtNetworkTimestamp), + }); +} + +async function getWithoutHistoryControlMessage({ + convo, + withoutHistory, + groupPk, + adminSecretKey, + createAtNetworkTimestamp, + dbMsgIdentifier, +}: WithAddWithoutHistoryMembers & + WithGroupPubkey & { + dbMsgIdentifier: string; + convo: ConversationModel; + adminSecretKey: Uint8ArrayLen64; + createAtNetworkTimestamp: number; + }) { + const sodium = await getSodiumRenderer(); + + if (!withoutHistory.length) { + return null; + } + + return new GroupUpdateMemberChangeMessage({ + identifier: dbMsgIdentifier, + added: withoutHistory, + groupPk, + typeOfChange: 'added', + createAtNetworkTimestamp, + secretKey: adminSecretKey, + sodium, + ...DisappearingMessages.getExpireDetailsForOutgoingMessage(convo, createAtNetworkTimestamp), + }); +} + +async function getWithHistoryControlMessage({ + convo, + withHistory, + groupPk, + adminSecretKey, + createAtNetworkTimestamp, + dbMsgIdentifier, +}: WithAddWithHistoryMembers & + WithGroupPubkey & { + dbMsgIdentifier: string; + convo: ConversationModel; + adminSecretKey: Uint8ArrayLen64; + createAtNetworkTimestamp: number; + }) { + const sodium = await getSodiumRenderer(); + + if (!withHistory.length) { + return null; + } + + return new GroupUpdateMemberChangeMessage({ + identifier: dbMsgIdentifier, + added: withHistory, + groupPk, + typeOfChange: 'addedWithHistory', + createAtNetworkTimestamp, + secretKey: adminSecretKey, + sodium, + ...DisappearingMessages.getExpireDetailsForOutgoingMessage(convo, createAtNetworkTimestamp), + }); +} + +async function getPromotedControlMessage({ + convo, + promoted, + groupPk, + adminSecretKey, + createAtNetworkTimestamp, + dbMsgIdentifier, +}: WithPromotedMembers & + WithGroupPubkey & { + dbMsgIdentifier: string; + convo: ConversationModel; + adminSecretKey: Uint8ArrayLen64; + createAtNetworkTimestamp: number; + }) { + const sodium = await getSodiumRenderer(); + + if (!promoted.length) { + return null; + } + + return new GroupUpdateMemberChangeMessage({ + identifier: dbMsgIdentifier, + promoted, + groupPk, + typeOfChange: 'promoted', + createAtNetworkTimestamp, + secretKey: adminSecretKey, + sodium, + ...DisappearingMessages.getExpireDetailsForOutgoingMessage(convo, createAtNetworkTimestamp), + }); +} + +export const GroupUpdateMessageFactory = { + getRemovedControlMessage, + getWithoutHistoryControlMessage, + getWithHistoryControlMessage, + getPromotedControlMessage, +}; diff --git a/ts/session/types/with.ts b/ts/session/types/with.ts index 50f16911a..bd5cebcbb 100644 --- a/ts/session/types/with.ts +++ b/ts/session/types/with.ts @@ -1 +1,13 @@ +import { PubkeyType } from 'libsession_util_nodejs'; + export type WithMessageHash = { messageHash: string }; +export type WithTimestamp = { timestamp: number }; +export type WithSignature = { signature: string }; +export type WithSecretKey = { secretKey: Uint8Array }; + +export type WithFromMemberLeftMessage = { fromMemberLeftMessage: boolean }; // there are some changes we want to skip when doing changes triggered from a memberLeft message. + +export type WithAddWithoutHistoryMembers = { withoutHistory: Array }; +export type WithAddWithHistoryMembers = { withHistory: Array }; +export type WithRemoveMembers = { removed: Array }; +export type WithPromotedMembers = { promoted: Array }; diff --git a/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts b/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts index 81a0f332c..f250e2397 100644 --- a/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts @@ -1,5 +1,5 @@ /* eslint-disable no-await-in-loop */ -import { PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs'; +import { WithGroupPubkey } from 'libsession_util_nodejs'; import { compact, isEmpty, isNumber } from 'lodash'; import { v4 } from 'uuid'; import { StringUtils } from '../..'; @@ -17,7 +17,6 @@ import { } from '../../../apis/snode_api/SnodeRequestTypes'; import { StoreGroupRequestFactory } from '../../../apis/snode_api/factories/StoreGroupRequestFactory'; import { RevokeChanges, SnodeAPIRevoke } from '../../../apis/snode_api/revokeSubaccount'; -import { WithSecretKey } from '../../../apis/snode_api/types'; import { concatUInt8Array, getSodiumRenderer } from '../../../crypto'; import { GroupUpdateDeleteMemberContentMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateDeleteMemberContentMessage'; import { MessageSender } from '../../../sending'; @@ -31,10 +30,12 @@ import { } from '../PersistedJob'; import { GroupSync } from './GroupSyncJob'; import { NetworkTime } from '../../../../util/NetworkTime'; - -export type WithAddWithoutHistoryMembers = { withoutHistory: Array }; -export type WithAddWithHistoryMembers = { withHistory: Array }; -export type WithRemoveMembers = { removed: Array }; +import { + WithAddWithHistoryMembers, + WithAddWithoutHistoryMembers, + WithRemoveMembers, + WithSecretKey, +} from '../../../types/with'; const defaultMsBetweenRetries = 10000; const defaultMaxAttempts = 1; diff --git a/ts/session/utils/job_runners/jobs/GroupPromoteJob.ts b/ts/session/utils/job_runners/jobs/GroupPromoteJob.ts index f4b60acbb..d36f6306e 100644 --- a/ts/session/utils/job_runners/jobs/GroupPromoteJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupPromoteJob.ts @@ -20,7 +20,7 @@ import { import { MessageQueue } from '../../../sending'; const defaultMsBetweenRetries = 10000; -const defaultMaxAttemps = 1; +const defaultMaxAttempts = 1; type JobExtraArgs = { groupPk: GroupPubkeyType; @@ -75,7 +75,7 @@ class GroupPromoteJob extends PersistedJob { member, groupPk, delayBetweenRetries: defaultMsBetweenRetries, - maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttemps, + maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttempts, nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries, currentRetry: isNumber(currentRetry) ? currentRetry : 0, }); diff --git a/ts/session/utils/job_runners/jobs/GroupSyncJob.ts b/ts/session/utils/job_runners/jobs/GroupSyncJob.ts index 571974146..74d7dbd81 100644 --- a/ts/session/utils/job_runners/jobs/GroupSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupSyncJob.ts @@ -120,7 +120,7 @@ async function pushChangesToGroupSwarmIfNeeded({ if (window.sessionFeatureFlags.debug.debugLibsessionDumps) { const dumps = await MetaGroupWrapperActions.metaMakeDump(groupPk); window.log.info( - `pushChangesToGroupSwarmIfNeeded: current metadump: ${ed25519Str(groupPk)}:`, + `pushChangesToGroupSwarmIfNeeded: current meta dump: ${ed25519Str(groupPk)}:`, to_hex(dumps) ); } @@ -166,10 +166,10 @@ async function pushChangesToGroupSwarmIfNeeded({ }); const expectedReplyLength = - (supplementalKeysSubRequest ? 1 : 0) + // we are sending all the supplemental keys as a single subrequest - pendingConfigRequests.length + // each of those are sent as a subrequest - extraStoreRequests.length + // each of those are sent as a subrequest - extraRequestWithExpectedResults.length; // each of those are sent as a subrequest, but they don't all return something... + (supplementalKeysSubRequest ? 1 : 0) + // we are sending all the supplemental keys as a single sub request + pendingConfigRequests.length + // each of those are sent as a sub request + extraStoreRequests.length + // each of those are sent as a sub request + extraRequestWithExpectedResults.length; // each of those are sent as a sub request, but they don't all return something... // we do a sequence call here. If we do not have the right expected number of results, consider it a failure if (!isArray(result) || result.length !== expectedReplyLength) { @@ -184,6 +184,7 @@ async function pushChangesToGroupSwarmIfNeeded({ const changes = LibSessionUtil.batchResultsToGroupSuccessfulChange(result, { allOldHashes, messages: pendingConfigData, + }); if (isEmpty(changes)) { diff --git a/ts/session/utils/libsession/libsession_utils.ts b/ts/session/utils/libsession/libsession_utils.ts index eda71af77..9af909b4f 100644 --- a/ts/session/utils/libsession/libsession_utils.ts +++ b/ts/session/utils/libsession/libsession_utils.ts @@ -284,7 +284,6 @@ function batchResultsToGroupSuccessfulChange( * As it is a sequence, the delete might have failed but the new config message might still be posted. * So we need to check which request failed, and if it is the delete by hashes, we need to add the hash of the posted message to the list of hashes */ - if (!result?.length) { return successfulChanges; } diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index 8ad2bc9cf..65ee71451 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -5,14 +5,13 @@ import { GroupMemberGet, GroupPubkeyType, PubkeyType, - Uint8ArrayLen64, UserGroupsGet, WithGroupPubkey, WithPubkey, } from 'libsession_util_nodejs'; import { intersection, isEmpty, uniq } from 'lodash'; +import { from_hex } from 'libsodium-wrappers-sumo'; import { ConfigDumpData } from '../../data/configDump/configDump'; -import { ConversationModel } from '../../models/conversation'; import { HexString } from '../../node/hexStrings'; import { SignalService } from '../../protobuf'; import { getSwarmPollingInstance } from '../../session/apis/snode_api'; @@ -27,12 +26,7 @@ import { PubKey } from '../../session/types'; import { UserUtils } from '../../session/utils'; import { PreConditionFailed } from '../../session/utils/errors'; import { GroupInvite } from '../../session/utils/job_runners/jobs/GroupInviteJob'; -import { - GroupPendingRemovals, - WithAddWithHistoryMembers, - WithAddWithoutHistoryMembers, - WithRemoveMembers, -} from '../../session/utils/job_runners/jobs/GroupPendingRemovalsJob'; +import { GroupPendingRemovals } from '../../session/utils/job_runners/jobs/GroupPendingRemovalsJob'; import { GroupSync } from '../../session/utils/job_runners/jobs/GroupSyncJob'; import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob'; import { RunJobResult } from '../../session/utils/job_runners/PersistedJob'; @@ -53,9 +47,14 @@ import { openConversationWithMessages } from './conversations'; import { resetLeftOverlayMode } from './section'; import { ConversationTypeEnum } from '../../models/types'; import { NetworkTime } from '../../util/NetworkTime'; -import { from_hex } from 'libsodium-wrappers-sumo'; +import { GroupUpdateMessageFactory } from '../../session/messages/message_factory/group/groupUpdateMessageFactory'; +import { + WithAddWithHistoryMembers, + WithAddWithoutHistoryMembers, + WithFromMemberLeftMessage, + WithRemoveMembers, +} from '../../session/types/with'; -type WithFromMemberLeftMessage = { fromMemberLeftMessage: boolean }; // there are some changes we want to skip when doing changes triggered from a memberLeft message. export type GroupState = { infos: Record; members: Record>; @@ -206,7 +205,7 @@ const initNewGroupInWrapper = createAsyncThunk( convo, markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier }); - groupMemberChange = await getWithoutHistoryControlMessage({ + groupMemberChange = await GroupUpdateMessageFactory.getWithoutHistoryControlMessage({ adminSecretKey: groupSecretKey, convo, groupPk, @@ -343,7 +342,7 @@ const handleUserGroupUpdate = createAsyncThunk( /** * Called only when the app just loaded the SessionInbox (i.e. user logged in and fully loaded). - * This function populates the slice with any meta-dumps we have in the DB, if they also are part of what is the usergroup wrapper tracking. + * This function populates the slice with any meta-dumps we have in the DB, if they also are part of what is the user group wrapper tracking. * */ const loadMetaDumpsFromDB = createAsyncThunk( @@ -562,109 +561,6 @@ async function handleWithoutHistoryMembers({ } } -/** - * Return the control messages to be pushed to the group's swarm. - * Those are not going to change the state, they are just here as a "notification". - * i.e. "Alice was removed from the group" - */ -async function getRemovedControlMessage({ - convo, - groupPk, - removed, - adminSecretKey, - createAtNetworkTimestamp, - fromMemberLeftMessage, - dbMsgIdentifier, -}: WithFromMemberLeftMessage & - WithRemoveMembers & - WithGroupPubkey & { - convo: ConversationModel; - adminSecretKey: Uint8ArrayLen64; - createAtNetworkTimestamp: number; - dbMsgIdentifier: string; - }) { - const sodium = await getSodiumRenderer(); - - if (fromMemberLeftMessage || !removed.length) { - return null; - } - - return new GroupUpdateMemberChangeMessage({ - identifier: dbMsgIdentifier, - removed, - groupPk, - typeOfChange: 'removed', - createAtNetworkTimestamp, - secretKey: adminSecretKey, - sodium, - ...DisappearingMessages.getExpireDetailsForOutgoingMessage(convo, createAtNetworkTimestamp), - }); -} - -async function getWithoutHistoryControlMessage({ - convo, - withoutHistory, - groupPk, - adminSecretKey, - createAtNetworkTimestamp, - dbMsgIdentifier, -}: WithAddWithoutHistoryMembers & - WithGroupPubkey & { - dbMsgIdentifier: string; - convo: ConversationModel; - adminSecretKey: Uint8ArrayLen64; - createAtNetworkTimestamp: number; - }) { - const sodium = await getSodiumRenderer(); - - if (!withoutHistory.length) { - return null; - } - - return new GroupUpdateMemberChangeMessage({ - identifier: dbMsgIdentifier, - added: withoutHistory, - groupPk, - typeOfChange: 'added', - createAtNetworkTimestamp, - secretKey: adminSecretKey, - sodium, - ...DisappearingMessages.getExpireDetailsForOutgoingMessage(convo, createAtNetworkTimestamp), - }); -} - -async function getWithHistoryControlMessage({ - convo, - withHistory, - groupPk, - adminSecretKey, - createAtNetworkTimestamp, - dbMsgIdentifier, -}: WithAddWithHistoryMembers & - WithGroupPubkey & { - dbMsgIdentifier: string; - convo: ConversationModel; - adminSecretKey: Uint8ArrayLen64; - createAtNetworkTimestamp: number; - }) { - const sodium = await getSodiumRenderer(); - - if (!withHistory.length) { - return null; - } - - return new GroupUpdateMemberChangeMessage({ - identifier: dbMsgIdentifier, - added: withHistory, - groupPk, - typeOfChange: 'addedWithHistory', - createAtNetworkTimestamp, - secretKey: adminSecretKey, - sodium, - ...DisappearingMessages.getExpireDetailsForOutgoingMessage(convo, createAtNetworkTimestamp), - }); -} - async function handleMemberAddedFromUI({ addMembersWithHistory, addMembersWithoutHistory, @@ -728,7 +624,7 @@ async function handleMemberAddedFromUI({ diff: { type: 'add', added: withHistory, withHistory: true }, ...shared, }); - const groupChange = await getWithHistoryControlMessage({ + const groupChange = await GroupUpdateMessageFactory.getWithHistoryControlMessage({ adminSecretKey: group.secretKey, convo, groupPk, @@ -745,7 +641,7 @@ async function handleMemberAddedFromUI({ diff: { type: 'add', added: withoutHistory, withHistory: false }, ...shared, }); - const groupChange = await getWithoutHistoryControlMessage({ + const groupChange = await GroupUpdateMessageFactory.getWithoutHistoryControlMessage({ adminSecretKey: group.secretKey, convo, groupPk, @@ -866,7 +762,7 @@ async function handleMemberRemovedFromUI({ }, markAlreadySent: false, // the store below will mark the message as sent using dbMsgIdentifier }); - removedControlMessage = await getRemovedControlMessage({ + removedControlMessage = await GroupUpdateMessageFactory.getRemovedControlMessage({ adminSecretKey: group.secretKey, convo, groupPk, diff --git a/ts/test/session/unit/crypto/SnodeSignatures_test.ts b/ts/test/session/unit/crypto/SnodeSignatures_test.ts index b8bac771f..c28ff1902 100644 --- a/ts/test/session/unit/crypto/SnodeSignatures_test.ts +++ b/ts/test/session/unit/crypto/SnodeSignatures_test.ts @@ -7,11 +7,11 @@ import { getSodiumNode } from '../../../../node/sodiumNode'; import { SnodeNamespaces } from '../../../../session/apis/snode_api/namespaces'; import { SnodeGroupSignature } from '../../../../session/apis/snode_api/signature/groupSignature'; import { SnodeSignature } from '../../../../session/apis/snode_api/signature/snodeSignatures'; -import { WithSignature } from '../../../../session/apis/snode_api/types'; import { concatUInt8Array } from '../../../../session/crypto'; import { UserUtils } from '../../../../session/utils'; import { fromBase64ToArray, fromHexToArray } from '../../../../session/utils/String'; import { NetworkTime } from '../../../../util/NetworkTime'; +import { WithSignature } from '../../../../session/types/with'; use(chaiAsPromised);