From 76bd15ce72b46a728264be08ad43b11826fea557 Mon Sep 17 00:00:00 2001 From: Mohammed S Date: Tue, 7 Nov 2023 17:34:41 +0530 Subject: [PATCH] fix: pagination group apis (#813) * fix: SDK delta API changes * fix: updategroupMembers * fix: test cases and minor fixes * fix: getAllGroupMembers --------- Co-authored-by: aman035 --- .../src/app/ChatTest/GetGroupMembersTest.tsx | 4 +- packages/restapi/src/lib/chat/addAdmins.ts | 93 ++------ packages/restapi/src/lib/chat/addMembers.ts | 75 ++---- .../src/lib/chat/getAllGroupMembers.ts | 29 +++ .../restapi/src/lib/chat/getGroupMembers.ts | 13 +- .../src/lib/chat/helpers/payloadHelper.ts | 56 ++++- .../restapi/src/lib/chat/helpers/validator.ts | 40 ++++ packages/restapi/src/lib/chat/removeAdmins.ts | 83 ++----- .../restapi/src/lib/chat/removeMembers.ts | 73 ++---- .../src/lib/chat/updateGroupMembers.ts | 174 ++++++++++++++ packages/restapi/src/lib/pushapi/chat.ts | 7 +- .../restapi/src/lib/space/addListeners.ts | 4 +- packages/restapi/src/lib/space/addSpeakers.ts | 4 +- .../restapi/src/lib/space/removeListeners.ts | 14 +- .../restapi/src/lib/space/removeSpeakers.ts | 4 +- .../tests/lib/chat/privateGroup.test.ts | 221 +++++++++++++++++- .../lib/chatLowLevel/updateGroup.test.ts | 15 +- 17 files changed, 616 insertions(+), 293 deletions(-) create mode 100644 packages/restapi/src/lib/chat/getAllGroupMembers.ts create mode 100644 packages/restapi/src/lib/chat/updateGroupMembers.ts diff --git a/packages/examples/sdk-frontend-react/src/app/ChatTest/GetGroupMembersTest.tsx b/packages/examples/sdk-frontend-react/src/app/ChatTest/GetGroupMembersTest.tsx index 4c6aa26ad..82bcc6ad6 100644 --- a/packages/examples/sdk-frontend-react/src/app/ChatTest/GetGroupMembersTest.tsx +++ b/packages/examples/sdk-frontend-react/src/app/ChatTest/GetGroupMembersTest.tsx @@ -30,8 +30,8 @@ const GetGroupMembersTest = () => { try { const result = await PushAPI.chat.getGroupMembers({ chatId, - pageNumber, - pageSize, + page: pageNumber, + limit: pageSize, env, }); setSendResponse(result); diff --git a/packages/restapi/src/lib/chat/addAdmins.ts b/packages/restapi/src/lib/chat/addAdmins.ts index f45eef5b9..0a15e31f5 100644 --- a/packages/restapi/src/lib/chat/addAdmins.ts +++ b/packages/restapi/src/lib/chat/addAdmins.ts @@ -1,27 +1,20 @@ -import { isValidETHAddress, walletToPCAIP10 } from '../helpers'; import Constants from '../constants'; -import { EnvOptionsType, SignerType, GroupDTO } from '../types'; +import { EnvOptionsType, SignerType, GroupInfoDTO } from '../types'; import { - getMembersList, - getAdminsList, -} from './helpers'; -import { - getGroup -} from './getGroup'; -import { - updateGroup -} from './updateGroup'; + GroupMemberUpdateOptions, + updateGroupMembers, +} from './updateGroupMembers'; export interface AddAdminsToGroupType extends EnvOptionsType { chatId: string; admins: Array; account?: string | null; signer?: SignerType | null; - pgpPrivateKey?: string | null; + pgpPrivateKey?: string | null; } export const addAdmins = async ( options: AddAdminsToGroupType -): Promise => { +): Promise => { const { chatId, admins, @@ -34,70 +27,30 @@ export const addAdmins = async ( if (account == null && signer == null) { throw new Error(`At least one from account or signer is necessary!`); } - + if (!admins || admins.length === 0) { - throw new Error("Admin address array cannot be empty!"); + throw new Error('Admin address array cannot be empty!'); } - - admins.forEach((admin) => { - if (!isValidETHAddress(admin)) { - throw new Error(`Invalid admin address: ${admin}`); - } - }); - - const group = await getGroup({ - chatId: chatId, - env, - }) - - // TODO: look at user did in updateGroup - const convertedMembers = getMembersList( - group.members, group.pendingMembers - ); - - // TODO: look at user did in updateGroup - const adminsToBeAdded = admins.map((admin) => walletToPCAIP10(admin)); - adminsToBeAdded.forEach((admin) => { - if (!convertedMembers.includes(admin)) { - convertedMembers.push(admin); - } - }); - - const convertedAdmins = getAdminsList( - group.members, group.pendingMembers - ); - - adminsToBeAdded.forEach((admin) => { - if (convertedAdmins.includes(admin)) { - throw new Error(`Admin ${admin} already exists in the list`); - } - }); - - convertedAdmins.push(...adminsToBeAdded); - - return await updateGroup({ - chatId: chatId, - groupName: group.groupName, - groupImage: group.groupImage, - groupDescription: group.groupDescription, - members: convertedMembers, - admins: convertedAdmins, - scheduleAt: group.scheduleAt, - scheduleEnd: group.scheduleEnd, - status: group.status, - account: account, - signer: signer, - env: env, - pgpPrivateKey: pgpPrivateKey - }); + const upsertPayload = { + admins: admins, + }; + + const groupMemberUpdateOptions: GroupMemberUpdateOptions = { + chatId: chatId, + upsert: upsertPayload, + remove: [], // No members to remove in this case + account: account, + signer: signer, + pgpPrivateKey: pgpPrivateKey, + env: env, + }; + return await updateGroupMembers(groupMemberUpdateOptions); } catch (err) { console.error( `[Push SDK] - API - Error - API ${addAdmins.name} -: `, err ); - throw Error( - `[Push SDK] - API - Error - API ${addAdmins.name} -: ${err}` - ); + throw Error(`[Push SDK] - API - Error - API ${addAdmins.name} -: ${err}`); } }; diff --git a/packages/restapi/src/lib/chat/addMembers.ts b/packages/restapi/src/lib/chat/addMembers.ts index ba2eceebc..e43172706 100644 --- a/packages/restapi/src/lib/chat/addMembers.ts +++ b/packages/restapi/src/lib/chat/addMembers.ts @@ -1,16 +1,7 @@ -import { isValidETHAddress, walletToPCAIP10 } from '../helpers'; import Constants from '../constants'; -import { EnvOptionsType, SignerType, GroupDTO } from '../types'; -import { - getMembersList, - getAdminsList -} from './helpers'; -import { - getGroup -} from './getGroup'; -import { - updateGroup -} from './updateGroup'; +import { EnvOptionsType, SignerType, GroupInfoDTO } from '../types'; +import { updateGroupMembers } from './updateGroupMembers'; +import { GroupMemberUpdateOptions } from './updateGroupMembers'; export interface AddMembersToGroupType extends EnvOptionsType { chatId: string; members: Array; @@ -24,7 +15,7 @@ export interface AddMembersToGroupType extends EnvOptionsType { */ export const addMembers = async ( options: AddMembersToGroupType -): Promise => { +): Promise => { const { chatId, members, @@ -37,64 +28,30 @@ export const addMembers = async ( if (account == null && signer == null) { throw new Error(`At least one from account or signer is necessary!`); } - + if (!members || members.length === 0) { - throw new Error("Member address array cannot be empty!"); + throw new Error('Member address array cannot be empty!'); } - - members.forEach((member) => { - if (!isValidETHAddress(member)) { - throw new Error(`Invalid member address: ${member}`); - } - }); - - const group = await getGroup({ - chatId: chatId, - env, - }) - - const convertedMembers = getMembersList( - group.members, group.pendingMembers - ); - - const membersToBeAdded = members.map((member) => walletToPCAIP10(member)); - - membersToBeAdded.forEach((member) => { - if (convertedMembers.includes(member)) { - throw new Error(`Member ${member} already exists in the list`); - } - }); - convertedMembers.push(...membersToBeAdded); + const upsertPayload = { + members: members, + }; - const convertedAdmins = getAdminsList( - group.members, group.pendingMembers - ); - - return await updateGroup({ + const groupMemberUpdateOptions: GroupMemberUpdateOptions = { chatId: chatId, - groupName: group.groupName, - groupImage: group.groupImage, - groupDescription: group.groupDescription, - members: convertedMembers, - admins: convertedAdmins, - scheduleAt: group.scheduleAt, - scheduleEnd: group.scheduleEnd, - status: group.status, + upsert: upsertPayload, + remove: [], // No members to remove in this case account: account, signer: signer, - env: env, - rules: group.rules, - meta: group.meta, pgpPrivateKey: pgpPrivateKey, - }); + env: env, + }; + return await updateGroupMembers(groupMemberUpdateOptions); } catch (err) { console.error( `[Push SDK] - API - Error - API ${addMembers.name} -: `, err ); - throw Error( - `[Push SDK] - API - Error - API ${addMembers.name} -: ${err}` - ); + throw Error(`[Push SDK] - API - Error - API ${addMembers.name} -: ${err}`); } }; diff --git a/packages/restapi/src/lib/chat/getAllGroupMembers.ts b/packages/restapi/src/lib/chat/getAllGroupMembers.ts new file mode 100644 index 000000000..a715d5ab4 --- /dev/null +++ b/packages/restapi/src/lib/chat/getAllGroupMembers.ts @@ -0,0 +1,29 @@ +import { getGroupMembers } from './getGroupMembers'; +import { getChatMemberCount } from './getChatMemberCount'; +import { ChatMemberProfile, EnvOptionsType } from '../types'; + +export const getAllGroupMembers = async (options: { + chatId: string; + env: EnvOptionsType['env']; +}): Promise => { + const { chatId, env } = options; + const count = await getChatMemberCount({ chatId, env }); + const overallCount = count.overallCount; + const limit = 5000; + const totalPages = Math.ceil(overallCount / limit); + const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1); + const groupMembers: ChatMemberProfile[] = []; + + const memberFetchPromises = pageNumbers.map((page) => + getGroupMembers({ chatId, env, page, limit }) + ); + + const membersResults = await Promise.all(memberFetchPromises); + membersResults.forEach((result) => { + if (result.members.length > 0) { + groupMembers.push(...result.members); + } + }); + + return groupMembers; +}; diff --git a/packages/restapi/src/lib/chat/getGroupMembers.ts b/packages/restapi/src/lib/chat/getGroupMembers.ts index 9a824a5d9..3daa7e556 100644 --- a/packages/restapi/src/lib/chat/getGroupMembers.ts +++ b/packages/restapi/src/lib/chat/getGroupMembers.ts @@ -9,8 +9,8 @@ import { ChatMemberCounts, ChatMemberProfile } from '../types'; export interface FetchChatGroupInfoType { chatId: string; - pageNumber?: number; - pageSize?: number; + page?: number; + limit?: number; env?: ENV; } @@ -20,12 +20,7 @@ export const getGroupMembers = async ( totalMembersCount: ChatMemberCounts; members: ChatMemberProfile[]; }> => { - const { - chatId, - pageNumber = 1, - pageSize = 20, - env = Constants.ENV.PROD, - } = options; + const { chatId, page = 1, limit = 20, env = Constants.ENV.PROD } = options; try { if (!chatId) { @@ -33,7 +28,7 @@ export const getGroupMembers = async ( } const API_BASE_URL = getAPIBaseUrls(env); - const requestUrl = `${API_BASE_URL}/v1/chat/${chatId}/members?pageNumber=${pageNumber}&pageSize=${pageSize}`; + const requestUrl = `${API_BASE_URL}/v1/chat/${chatId}/members?pageNumber=${page}&pageSize=${limit}`; const response = await axios.get(requestUrl); return response.data; diff --git a/packages/restapi/src/lib/chat/helpers/payloadHelper.ts b/packages/restapi/src/lib/chat/helpers/payloadHelper.ts index f74e2130b..591ef58f0 100644 --- a/packages/restapi/src/lib/chat/helpers/payloadHelper.ts +++ b/packages/restapi/src/lib/chat/helpers/payloadHelper.ts @@ -10,15 +10,15 @@ import { GroupAccess, SpaceAccess, GroupInfoDTO, + ChatMemberProfile, } from '../../types'; -import { getEncryptedRequest } from './crypto'; import { ENV } from '../../constants'; import { IPGPHelper, PGPHelper, pgpDecrypt } from './pgp'; import * as AES from './aes'; -import { sign } from './pgp'; import { MessageObj } from '../../types/messageTypes'; import * as CryptoJS from 'crypto-js'; +import { getAllGroupMembers } from '../getAllGroupMembers'; export interface ISendMessagePayload { fromDID: string; toDID: string; @@ -281,6 +281,58 @@ export const groupDtoToSpaceDto = (groupDto: GroupDTO): SpaceDTO => { return spaceDto; }; +export const groupDtoToSpaceDtoV2 = async ( + groupDto: GroupInfoDTO, + env: ENV = ENV.PROD +): Promise => { + const members = await getAllGroupMembers({ + chatId: groupDto.chatId, + env: env, + }); + + const spaceDto: SpaceDTO = { + members: members + .filter((member) => member.intent) + .map((member) => ({ + wallet: member.address, + publicKey: member.userInfo.publicKey ?? '', + isSpeaker: member.role === 'admin', + image: member.userInfo.profile.picture ?? '', + })), + pendingMembers: members + .filter((member) => !member.intent) + .map((pendingMember) => ({ + wallet: pendingMember.address, + publicKey: pendingMember.userInfo.publicKey ?? '', + isSpeaker: pendingMember.role === 'admin', + image: pendingMember.userInfo.profile.picture ?? '', + })), + contractAddressERC20: null, + numberOfERC20: 0, + contractAddressNFT: null, + numberOfNFTTokens: 0, + verificationProof: 'a', + spaceImage: groupDto.groupImage, + spaceName: groupDto.groupName, + isPublic: groupDto.isPublic, + spaceDescription: groupDto.groupDescription, + spaceCreator: groupDto.groupCreator, + spaceId: groupDto.chatId, + scheduleAt: groupDto.scheduleAt, + scheduleEnd: groupDto.scheduleEnd, + status: groupDto.status ?? null, + meta: groupDto.meta, + }; + + if (groupDto.rules) { + spaceDto.rules = { + entry: groupDto.rules.entry, + }; + } + + return spaceDto; +}; + export const convertSpaceRulesToRules = (spaceRules: SpaceRules): Rules => { return { entry: spaceRules.entry, diff --git a/packages/restapi/src/lib/chat/helpers/validator.ts b/packages/restapi/src/lib/chat/helpers/validator.ts index 5c241a7c3..9c4797e8e 100644 --- a/packages/restapi/src/lib/chat/helpers/validator.ts +++ b/packages/restapi/src/lib/chat/helpers/validator.ts @@ -1,4 +1,5 @@ import { isValidETHAddress, isValidNFTCAIP10Address } from '../../helpers'; +import { GroupMemberUpdateOptions } from '../updateGroupMembers'; export const createGroupRequestValidator = ( groupName: string, @@ -208,3 +209,42 @@ export const updateGroupRequestValidator = ( throw new Error(`Invalid address field!`); } }; + +export const validateGroupMemberUpdateOptions = ( + options: GroupMemberUpdateOptions +): void => { + const { chatId, upsert, remove, } = options; + + if (!chatId || chatId.trim().length === 0) { + throw new Error('Chat ID cannot be null or empty.'); + } + + // Validating upsert object + const allowedRoles = ['members', 'admins']; // Define allowed roles + Object.keys(upsert).forEach((role) => { + if (!allowedRoles.includes(role)) { + throw new Error( + `Invalid role: ${role}. Allowed roles are ${allowedRoles.join(', ')}.` + ); + } + if (upsert[role] && upsert[role].length > 1000) { + throw new Error(`${role} array cannot have more than 1000 addresses.`); + } + // Assuming you have a function `isValidETHAddress` to validate Ethereum addresses + upsert[role].forEach((address) => { + if (!isValidETHAddress(address)) { + throw new Error(`Invalid address found in ${role} list.`); + } + }); + }); + + // Validating remove array + if (remove && remove.length > 1000) { + throw new Error('Remove array cannot have more than 1000 addresses.'); + } + remove.forEach((address) => { + if (!isValidETHAddress(address)) { + throw new Error('Invalid address found in remove list.'); + } + }); +}; diff --git a/packages/restapi/src/lib/chat/removeAdmins.ts b/packages/restapi/src/lib/chat/removeAdmins.ts index c0d49936b..8b7a996e6 100644 --- a/packages/restapi/src/lib/chat/removeAdmins.ts +++ b/packages/restapi/src/lib/chat/removeAdmins.ts @@ -1,23 +1,16 @@ -import { isValidETHAddress, walletToPCAIP10 } from '../helpers'; import Constants from '../constants'; -import { EnvOptionsType, SignerType, GroupDTO } from '../types'; +import { EnvOptionsType, SignerType, GroupInfoDTO } from '../types'; import { - getWallet, - getMembersList, - getAdminsList -} from './helpers'; -import { - getGroup -} from './getGroup'; -import { - updateGroup -} from './updateGroup'; + GroupMemberUpdateOptions, + updateGroupMembers, +} from './updateGroupMembers'; + export interface RemoveAdminsFromGroupType extends EnvOptionsType { chatId: string; admins: Array; account?: string | null; signer?: SignerType | null; - pgpPrivateKey?: string | null; + pgpPrivateKey?: string | null; } /** @@ -25,7 +18,7 @@ export interface RemoveAdminsFromGroupType extends EnvOptionsType { */ export const removeAdmins = async ( options: RemoveAdminsFromGroupType -): Promise => { +): Promise => { const { chatId, admins, @@ -38,67 +31,21 @@ export const removeAdmins = async ( if (account == null && signer == null) { throw new Error(`At least one from account or signer is necessary!`); } - + if (!admins || admins.length === 0) { - throw new Error("Admin address array cannot be empty!"); + throw new Error('Admin address array cannot be empty!'); } - - admins.forEach((admin) => { - if (!isValidETHAddress(admin)) { - throw new Error(`Invalid admin address: ${admin}`); - } - }); - - const group = await getGroup({ - chatId: chatId, - env, - }) - - let convertedMembers = getMembersList( - group.members, group.pendingMembers - ); - - const adminsToBeRemoved = admins.map((admin) => walletToPCAIP10(admin)); - - adminsToBeRemoved.forEach((admin) => { - if (!convertedMembers.includes(admin)) { - throw new Error(`Member ${admin} not present in the list`); - } - }); - - let convertedAdmins = getAdminsList( - group.members, group.pendingMembers - ); - - adminsToBeRemoved.forEach((admin) => { - if (!convertedAdmins.includes(admin)) { - throw new Error(`Admin ${admin} not present in the list`); - } - }); - - convertedMembers = convertedMembers.filter( - (member) => !adminsToBeRemoved.includes(member) - ); - - convertedAdmins = convertedAdmins.filter( - (member) => !adminsToBeRemoved.includes(member) - ); - return await updateGroup({ + const groupMemberUpdateOptions: GroupMemberUpdateOptions = { chatId: chatId, - groupName: group.groupName, - groupImage: group.groupImage, - groupDescription: group.groupDescription, - members: convertedMembers, - admins: convertedAdmins, - scheduleAt: group.scheduleAt, - scheduleEnd: group.scheduleEnd, - status: group.status, + upsert: {}, + remove: admins, account: account, signer: signer, + pgpPrivateKey: pgpPrivateKey, env: env, - pgpPrivateKey: pgpPrivateKey - }); + }; + return await updateGroupMembers(groupMemberUpdateOptions); } catch (err) { console.error( `[Push SDK] - API - Error - API ${removeAdmins.name} -: `, diff --git a/packages/restapi/src/lib/chat/removeMembers.ts b/packages/restapi/src/lib/chat/removeMembers.ts index a06bdc0bc..ee37bf9ce 100644 --- a/packages/restapi/src/lib/chat/removeMembers.ts +++ b/packages/restapi/src/lib/chat/removeMembers.ts @@ -1,27 +1,20 @@ -import { isValidETHAddress, walletToPCAIP10 } from '../helpers'; import Constants from '../constants'; -import { EnvOptionsType, SignerType, GroupDTO } from '../types'; +import { EnvOptionsType, SignerType, GroupInfoDTO } from '../types'; import { - getMembersList, - getAdminsList -} from './helpers'; -import { - getGroup -} from './getGroup'; -import { - updateGroup -} from './updateGroup'; + GroupMemberUpdateOptions, + updateGroupMembers, +} from './updateGroupMembers'; export interface RemoveMembersFromGroupType extends EnvOptionsType { chatId: string; members: Array; account?: string | null; signer?: SignerType | null; - pgpPrivateKey?: string | null; + pgpPrivateKey?: string | null; } export const removeMembers = async ( options: RemoveMembersFromGroupType -): Promise => { +): Promise => { const { chatId, members, @@ -34,59 +27,21 @@ export const removeMembers = async ( if (account == null && signer == null) { throw new Error(`At least one from account or signer is necessary!`); } - + if (!members || members.length === 0) { - throw new Error("Member address array cannot be empty!"); + throw new Error('Member address array cannot be empty!'); } - - members.forEach((member) => { - if (!isValidETHAddress(member)) { - throw new Error(`Invalid member address: ${member}`); - } - }); - - const group = await getGroup({ - chatId: chatId, - env, - }) - - let convertedMembers = getMembersList( - group.members, group.pendingMembers - ); - const membersToBeRemoved = members.map((member) => walletToPCAIP10(member)); - - membersToBeRemoved.forEach((member) => { - if (!convertedMembers.includes(member)) { - throw new Error(`Member ${member} not present in the list`); - } - }); - - convertedMembers = convertedMembers.filter( - (member) => !membersToBeRemoved.includes(member) - ); - - const convertedAdmins = getAdminsList( - group.members, group.pendingMembers - ); - - return await updateGroup({ + const groupMemberUpdateOptions: GroupMemberUpdateOptions = { chatId: chatId, - groupName: group.groupName, - groupImage: group.groupImage, - groupDescription: group.groupDescription, - members: convertedMembers, - admins: convertedAdmins, - scheduleAt: group.scheduleAt, - scheduleEnd: group.scheduleEnd, - status: group.status, - rules: group.rules, - meta: group.meta, + upsert: {}, + remove: members, account: account, signer: signer, + pgpPrivateKey: pgpPrivateKey, env: env, - pgpPrivateKey: pgpPrivateKey - }); + }; + return await updateGroupMembers(groupMemberUpdateOptions); } catch (err) { console.error( `[Push SDK] - API - Error - API ${removeMembers.name} -: `, diff --git a/packages/restapi/src/lib/chat/updateGroupMembers.ts b/packages/restapi/src/lib/chat/updateGroupMembers.ts new file mode 100644 index 000000000..452748e4d --- /dev/null +++ b/packages/restapi/src/lib/chat/updateGroupMembers.ts @@ -0,0 +1,174 @@ +import axios from 'axios'; +import { getAPIBaseUrls } from '../helpers'; +import Constants from '../constants'; +import { + getWallet, + PGPHelper, + getConnectedUserV2Core, + getUserDID, + validateGroupMemberUpdateOptions, + pgpEncrypt, +} from './helpers'; +import * as CryptoJS from 'crypto-js'; +import { + EnvOptionsType, + GroupInfoDTO, + SignerType, +} from '../types'; +import { getGroupInfo } from './getGroupInfo'; +import { getGroupMemberStatus } from './getGroupMemberStatus'; +import * as AES from '../chat/helpers/aes'; +import { getAllGroupMembers } from './getAllGroupMembers'; + +export interface GroupMemberUpdateOptions extends EnvOptionsType { + chatId: string; + upsert: { + [role: string]: Array; + }; + remove: Array; + account?: string | null; + signer?: SignerType | null; + pgpPrivateKey?: string | null; +} + +export const updateGroupMembers = async ( + options: GroupMemberUpdateOptions +): Promise => { + const { + chatId, + upsert, + remove, + account = null, + signer = null, + env = Constants.ENV.PROD, + pgpPrivateKey = null, + } = options; + try { + validateGroupMemberUpdateOptions(options); + const wallet = getWallet({ account, signer }); + + const connectedUser = await getConnectedUserV2Core( + wallet, + pgpPrivateKey, + env, + PGPHelper + ); + + const convertedUpsertPromise = Object.entries(upsert).map( + async ([role, userDIDs]) => { + const userIDs = await Promise.all( + userDIDs.map((userDID) => getUserDID(userDID, env)) + ); + return [role, userIDs]; + } + ); + const convertedUpsert = Object.fromEntries( + await Promise.all(convertedUpsertPromise) + ); + const convertedRemove = await Promise.all( + remove.map((userDID) => getUserDID(userDID, env)) + ); + + let sessionKey: string | null = null; + let encryptedSecret: string | null = null; + + const group = await getGroupInfo({ chatId, env }); + if (!group) { + throw new Error(`Group not found`); + } + + if (!group.isPublic) { + const { isMember } = await getGroupMemberStatus({ + chatId, + did: connectedUser.did, + env, + }); + + const groupMembers = await getAllGroupMembers({ chatId, env }); + + const removeParticipantSet = new Set( + convertedRemove.map((participant) => participant.toLowerCase()) + ); + let sameMembers = true; + + groupMembers.map((element) => { + if ( + element.intent && + removeParticipantSet.has(element.address.toLowerCase()) + ) { + sameMembers = false; + } + }); + + if (!sameMembers || !isMember) { + const secretKey = AES.generateRandomSecret(15); + + const publicKeys: string[] = []; + // This will now only take keys of non-removed members + groupMembers.map((element) => { + if ( + element.intent && + !removeParticipantSet.has(element.address.toLowerCase()) + ) { + publicKeys.push(element.userInfo.publicKey as string); + } + }); + + // This is autoJoin Case + if (!isMember) { + publicKeys.push(connectedUser.publicKey); + } + + // Encrypt secret key with group members public keys + encryptedSecret = await pgpEncrypt({ + plainText: secretKey, + keys: publicKeys, + }); + + sessionKey = CryptoJS.SHA256(encryptedSecret).toString(); + } + } + + const bodyToBeHashed = { + upsert: convertedUpsert, + remove: convertedRemove, + sessionKey, + encryptedSecret, + }; + + const hash = CryptoJS.SHA256(JSON.stringify(bodyToBeHashed)).toString(); + const signature = await PGPHelper.sign({ + message: hash, + signingKey: connectedUser.privateKey!, + }); + const sigType = 'pgpv2'; + const verificationProof = `${sigType}:${signature}:${connectedUser.did}`; + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v1/chat/groups/${chatId}/members`; + + const body = { + upsert: convertedUpsert, + remove: convertedRemove, + sessionKey, + encryptedSecret, + verificationProof, + }; + return axios + .post(apiEndpoint, body) + .then((response) => { + return response.data; + }) + .catch((err) => { + if (err?.response?.data) throw new Error(err?.response?.data); + throw new Error(err); + }); + } catch (err) { + console.error( + `[Push SDK] - API - Error - API ${updateGroupMembers.name} -: `, + err + ); + throw Error( + `[Push SDK] - API - Error - API ${updateGroupMembers.name} -: ${err}` + ); + } +}; diff --git a/packages/restapi/src/lib/pushapi/chat.ts b/packages/restapi/src/lib/pushapi/chat.ts index 8a9077bf8..7de852ebe 100644 --- a/packages/restapi/src/lib/pushapi/chat.ts +++ b/packages/restapi/src/lib/pushapi/chat.ts @@ -10,6 +10,7 @@ import { ProgressHookType, IUser, IMessageIPFS, + GroupInfoDTO, } from '../types'; import { GroupUpdateOptions, @@ -378,7 +379,7 @@ export class Chat { } }, - join: async (target: string): Promise => { + join: async (target: string): Promise => { const status = await PUSH_CHAT.getGroupMemberStatus({ chatId: target, did: this.account, @@ -404,13 +405,13 @@ export class Chat { }); } - return await PUSH_CHAT.getGroup({ + return await PUSH_CHAT.getGroupInfo({ chatId: target, env: this.env, }); }, - leave: async (target: string): Promise => { + leave: async (target: string): Promise => { const status = await PUSH_CHAT.getGroupMemberStatus({ chatId: target, did: this.account, diff --git a/packages/restapi/src/lib/space/addListeners.ts b/packages/restapi/src/lib/space/addListeners.ts index 995959456..cf20d072b 100644 --- a/packages/restapi/src/lib/space/addListeners.ts +++ b/packages/restapi/src/lib/space/addListeners.ts @@ -1,7 +1,7 @@ import Constants from '../constants'; import { EnvOptionsType, SignerType, SpaceDTO } from '../types'; import { - groupDtoToSpaceDto + groupDtoToSpaceDto, groupDtoToSpaceDtoV2 } from '../chat/helpers'; @@ -36,7 +36,7 @@ export const addListeners = async ( pgpPrivateKey: pgpPrivateKey }); - return groupDtoToSpaceDto(group); + return groupDtoToSpaceDtoV2(group, env); } catch (err) { console.error( `[Push SDK] - API - Error - API ${addListeners.name} -: `, diff --git a/packages/restapi/src/lib/space/addSpeakers.ts b/packages/restapi/src/lib/space/addSpeakers.ts index f7121363f..065d9f536 100644 --- a/packages/restapi/src/lib/space/addSpeakers.ts +++ b/packages/restapi/src/lib/space/addSpeakers.ts @@ -1,6 +1,6 @@ import Constants from '../constants'; import { EnvOptionsType, SignerType, SpaceDTO } from '../types'; -import { groupDtoToSpaceDto } from '../chat/helpers'; +import { groupDtoToSpaceDto, groupDtoToSpaceDtoV2 } from '../chat/helpers'; import { addAdmins } from '../chat/addAdmins'; export interface AddSpeakersToSpaceType extends EnvOptionsType { @@ -29,5 +29,5 @@ export const addSpeakers = async ( pgpPrivateKey: pgpPrivateKey, }); - return groupDtoToSpaceDto(group); + return groupDtoToSpaceDtoV2(group, env); }; diff --git a/packages/restapi/src/lib/space/removeListeners.ts b/packages/restapi/src/lib/space/removeListeners.ts index 30ab12f5e..4adce554f 100644 --- a/packages/restapi/src/lib/space/removeListeners.ts +++ b/packages/restapi/src/lib/space/removeListeners.ts @@ -1,11 +1,7 @@ import Constants from '../constants'; import { EnvOptionsType, SignerType, SpaceDTO } from '../types'; -import { - groupDtoToSpaceDto -} from '../chat/helpers'; -import { - removeMembers -} from '../chat/removeMembers'; +import { groupDtoToSpaceDto, groupDtoToSpaceDtoV2 } from '../chat/helpers'; +import { removeMembers } from '../chat/removeMembers'; export interface RemoveListenersFromSpaceType extends EnvOptionsType { spaceId: string; @@ -26,17 +22,17 @@ export const removeListeners = async ( env = Constants.ENV.PROD, pgpPrivateKey = null, } = options || {}; - try { + try { const group = await removeMembers({ chatId: spaceId, members: listeners, account: account, signer: signer, env: env, - pgpPrivateKey: pgpPrivateKey + pgpPrivateKey: pgpPrivateKey, }); - return groupDtoToSpaceDto(group); + return groupDtoToSpaceDtoV2(group, env); } catch (err) { console.error( `[Push SDK] - API - Error - API ${removeListeners.name} -: `, diff --git a/packages/restapi/src/lib/space/removeSpeakers.ts b/packages/restapi/src/lib/space/removeSpeakers.ts index e72137957..f7593be54 100644 --- a/packages/restapi/src/lib/space/removeSpeakers.ts +++ b/packages/restapi/src/lib/space/removeSpeakers.ts @@ -1,6 +1,6 @@ import Constants from '../constants'; import { EnvOptionsType, SignerType, SpaceDTO } from '../types'; -import { groupDtoToSpaceDto } from '../chat/helpers'; +import { groupDtoToSpaceDto, groupDtoToSpaceDtoV2 } from '../chat/helpers'; import { removeAdmins } from '../chat/removeAdmins'; export interface RemoveSpeakersFromSpaceType extends EnvOptionsType { spaceId: string; @@ -28,7 +28,7 @@ export const removeSpeakers = async ( pgpPrivateKey: pgpPrivateKey, }); - return groupDtoToSpaceDto(group); + return groupDtoToSpaceDtoV2(group, env); } catch (err) { console.error( `[Push SDK] - API - Error - API ${removeSpeakers.name} -: `, diff --git a/packages/restapi/tests/lib/chat/privateGroup.test.ts b/packages/restapi/tests/lib/chat/privateGroup.test.ts index 272b5f3bd..7b4bf91e7 100644 --- a/packages/restapi/tests/lib/chat/privateGroup.test.ts +++ b/packages/restapi/tests/lib/chat/privateGroup.test.ts @@ -180,7 +180,7 @@ describe('Private Groups', () => { const updatedGroup = await userBob.chat.group.leave(group.chatId); expect(updatedGroup.sessionKey).to.be.null; }); - it.skip('Session Key should be null on AutoLeave Pending Admin', async () => { + it('Session Key should be null on AutoLeave Pending Admin', async () => { // Added Pending Admin await userAlice.chat.group.add(group.chatId, { role: 'ADMIN', @@ -214,9 +214,216 @@ describe('Private Groups', () => { expect(updatedGroup.sessionKey).to.not.be.null; expect(updatedGroup.sessionKey).to.not.equal(updatedGroup1.sessionKey); }); + it('Session Key should not change on promotion of Pending member to admin', async () => { + // Added Pending Member + const updatedGroup1 = await userAlice.chat.group.add(group.chatId, { + role: 'MEMBER', + accounts: [account2], + }); + // Promote to Admin + const updatedGroup2 = await userAlice.chat.group.add(group.chatId, { + role: 'ADMIN', + accounts: [account2], + }); + expect(updatedGroup1.sessionKey).to.be.null; + expect(updatedGroup1.sessionKey).to.equal(updatedGroup2.sessionKey); + }); + it('Session Key should not change on promotion of Pending admin to member', async () => { + // Added Pending Member + const updatedGroup1 = await userAlice.chat.group.add(group.chatId, { + role: 'ADMIN', + accounts: [account2], + }); + // Promote to Admin + const updatedGroup2 = await userAlice.chat.group.add(group.chatId, { + role: 'MEMBER', + accounts: [account2], + }); + expect(updatedGroup1.sessionKey).to.be.null; + expect(updatedGroup1.sessionKey).to.equal(updatedGroup2.sessionKey); + }); + it('Session Key should not change on promotion of Non-Pending member to admin', async () => { + const updatedGroup1 = await userBob.chat.group.join(group.chatId); + // Promote to Admin + const updatedGroup2 = await userAlice.chat.group.add(group.chatId, { + role: 'ADMIN', + accounts: [account2], + }); + expect(updatedGroup1.sessionKey).to.not.be.null; + expect(updatedGroup1.sessionKey).to.equal(updatedGroup2.sessionKey); + }); + it('Session Key should not change on promotion of Non-Pending admin to member', async () => { + // Added Pending Admin + await userAlice.chat.group.add(group.chatId, { + role: 'ADMIN', + accounts: [account2], + }); + // Accept Invite + const updatedGroup1 = await userBob.chat.group.join(group.chatId); + // Promote to Admin + const updatedGroup2 = await userAlice.chat.group.add(group.chatId, { + role: 'MEMBER', + accounts: [account2], + }); + expect(updatedGroup1.sessionKey).to.not.be.null; + expect(updatedGroup1.sessionKey).to.equal(updatedGroup2.sessionKey); + }); }); - - describe('Private Group Send Message', () => { + describe('Private Group Send Message Permissions', () => { + const Content = 'Sending Message to Private Group'; + it('Non-Member should not be able to send messages', async () => { + await expect( + userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + }); + it('Pending-Member should not be able to send messages', async () => { + await userAlice.chat.group.add(group.chatId, { + role: 'MEMBER', + accounts: [account2], + }); + await expect( + userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + }); + it('Pending-Admin should not be able to send messages', async () => { + await userAlice.chat.group.add(group.chatId, { + role: 'ADMIN', + accounts: [account2], + }); + await expect( + userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + }); + it('Pending-Member who left should not be able to send messages', async () => { + await userAlice.chat.group.add(group.chatId, { + role: 'MEMBER', + accounts: [account2], + }); + await userBob.chat.group.leave(group.chatId); + await expect( + userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + }); + it('Pending-Admin who left should not be able to send messages', async () => { + await userAlice.chat.group.add(group.chatId, { + role: 'MEMBER', + accounts: [account2], + }); + await userBob.chat.group.leave(group.chatId); + await expect( + userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + }); + it('Member who left should not be able to send messages', async () => { + await userAlice.chat.group.add(group.chatId, { + role: 'MEMBER', + accounts: [account2], + }); + await userBob.chat.group.join(group.chatId); + await userBob.chat.group.leave(group.chatId); + await expect( + userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + }); + it('Admin who left should not be able to send messages', async () => { + await userAlice.chat.group.add(group.chatId, { + role: 'ADMIN', + accounts: [account2], + }); + await userBob.chat.group.join(group.chatId); + await userBob.chat.group.leave(group.chatId); + await expect( + userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + }); + it('Member who were removed should not be able to send messages', async () => { + await userAlice.chat.group.add(group.chatId, { + role: 'MEMBER', + accounts: [account2, account3], + }); + await userBob.chat.group.join(group.chatId); + await userAlice.chat.group.remove(group.chatId, { + role: 'MEMBER', + accounts: [account2, account3], + }); + await expect( + userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + await expect( + userJohn.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + }); + it('Admin who were removed should not be able to send messages', async () => { + await userAlice.chat.group.add(group.chatId, { + role: 'ADMIN', + accounts: [account2, account3], + }); + await userBob.chat.group.join(group.chatId); + await userBob.chat.group.remove(group.chatId, { + role: 'ADMIN', + accounts: [account2, account3], + }); + await expect( + userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + await expect( + userJohn.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }) + ).to.be.rejected; + }); + it('Member should be able to send messages', async () => { + await userBob.chat.group.join(group.chatId); + const msg = await userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }); + }); + it('Admin should be able to send messages', async () => { + await userBob.chat.group.join(group.chatId); + // Promotion + await userAlice.chat.group.add(group.chatId, { + role: 'ADMIN', + accounts: [account2], + }); + const msg = await userBob.chat.send(group.chatId, { + content: 'Sending Message to Private Group', + type: 'Text', + }); + }); + }); + describe('Private Group Message Encryption', () => { const Content = 'Sending Message to Private Group'; it('Send Message should have pgp encryption on Create Group', async () => { await userAlice.chat.send(group.chatId, { @@ -235,7 +442,7 @@ describe('Private Groups', () => { role: 'MEMBER', accounts: [account2], }); - const updatedGrp = await userBob.chat.group.join(group.chatId); + await userBob.chat.group.join(group.chatId); await userAlice.chat.send(group.chatId, { content: 'Sending Message to Private Group', @@ -396,6 +603,9 @@ describe('Private Groups', () => { const msg2 = ((await userBob.chat.latest(group.chatId)) as any)[0]; expectMsg(msg2, Content, account1, group.chatId, 'pgpv1:group', true); + + const msg3 = ((await userJohn.chat.latest(group.chatId)) as any)[0]; + expectMsg(msg3, Content, account1, group.chatId, 'pgpv1:group', false); }); it('Non-Pending Admins should be able to decrypt message', async () => { // Added Pending Member @@ -417,6 +627,9 @@ describe('Private Groups', () => { const msg2 = ((await userBob.chat.latest(group.chatId)) as any)[0]; expectMsg(msg2, Content, account1, group.chatId, 'pgpv1:group', true); + + const msg3 = ((await userJohn.chat.latest(group.chatId)) as any)[0]; + expectMsg(msg3, Content, account1, group.chatId, 'pgpv1:group', false); }); it('Non-Pending Menbers who left should not be able to decrypt message', async () => { // Added Pending Member diff --git a/packages/restapi/tests/lib/chatLowLevel/updateGroup.test.ts b/packages/restapi/tests/lib/chatLowLevel/updateGroup.test.ts index 5f10b2db5..3d2e32dd1 100644 --- a/packages/restapi/tests/lib/chatLowLevel/updateGroup.test.ts +++ b/packages/restapi/tests/lib/chatLowLevel/updateGroup.test.ts @@ -3,7 +3,12 @@ import { expect } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { ethers } from 'ethers'; import Constants from '../../../src/lib/constants'; -import { createGroup, updateGroup, removeMembers } from '../../../src/lib/chat'; +import { + createGroup, + updateGroup, + removeMembers, + getGroup, +} from '../../../src/lib/chat'; import { GroupDTO } from '../../../src/lib/types'; import { adjectives, @@ -147,15 +152,21 @@ describe('Update Group', () => { env: _env, }); + // timeout for 1 sec + await new Promise((resolve) => setTimeout(resolve, 1000)); + const updatedMembers = [ 'eip155:0xDB0Bb1C25e36a5Ec9d199688bB01eADa4e70225E', ]; - const updatedGroup = await removeMembers({ + + await removeMembers({ members: [account2], // account to be removed chatId: group.chatId, signer: signer2, //acount2 env: _env, }); + + const updatedGroup = await getGroup({ chatId: group.chatId, env: _env }); await expectGroup(updatedGroup, true, admins, updatedMembers, false); }); });