Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: optimization fixes #828

Merged
merged 2 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/restapi/src/lib/chat/approveRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import * as CryptoJS from 'crypto-js';
import { getGroup } from './getGroup';
import * as AES from '../chat/helpers/aes';
import { getGroupInfo } from './getGroupInfo';
import { getAllGroupMembersPublicKeys } from './getAllGroupMembersPublicKeys';

export interface ApproveRequestOptionsType extends EnvOptionsType {
/**
Expand Down Expand Up @@ -88,13 +90,18 @@ export const approveCore = async (
// pgpv2 is used for private grps
let sigType: 'pgp' | 'pgpv2' = 'pgp';
if (isGroup) {
const group = await getGroup({ chatId: senderAddress, env });
const group = await getGroupInfo({ chatId: senderAddress, env });

if (group && !group.isPublic) {
sigType = 'pgpv2';
const secretKey = AES.generateRandomSecret(15);

const groupMembers = await getAllGroupMembersPublicKeys({
chatId: group.chatId,
env,
});
// Encrypt secret key with group members public keys
const publicKeys: string[] = group.members.map(
const publicKeys: string[] = groupMembers.map(
(member) => member.publicKey
);
publicKeys.push(connectedUser.publicKey);
Expand Down
29 changes: 29 additions & 0 deletions packages/restapi/src/lib/chat/getAllGroupMembersPublicKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getChatMemberCount } from './getChatMemberCount';
import { EnvOptionsType } from '../types';
import { getGroupMembersPublicKeys } from './getGroupMembersPublicKeys';

export const getAllGroupMembersPublicKeys = async (options: {
chatId: string;
env: EnvOptionsType['env'];
}): Promise<{ did: string; publicKey: string }[]> => {
const { chatId, env } = options;
const count = await getChatMemberCount({ chatId, env });
const overallCount = count.approvedCount;
const limit = 5000;
const totalPages = Math.ceil(overallCount / limit);
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
const groupMembers: { did: string; publicKey: string }[] = [];

const memberFetchPromises = pageNumbers.map((page) =>
getGroupMembersPublicKeys({ chatId, env, page, limit })
);

const membersResults = await Promise.all(memberFetchPromises);
membersResults.forEach((result) => {
if (result.members.length > 0) {
groupMembers.push(...result.members);
}
});

return groupMembers;
};
2 changes: 1 addition & 1 deletion packages/restapi/src/lib/chat/getChatMemberCount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const getChatMemberCount = async (
}

const API_BASE_URL = getAPIBaseUrls(env);
const requestUrl = `${API_BASE_URL}/v1/chat/${chatId}/members/count`;
const requestUrl = `${API_BASE_URL}/v1/chat/groups/${chatId}/members/count`;

const response = await axios.get(requestUrl);
const { totalMembersCount } = response.data;
Expand Down
2 changes: 1 addition & 1 deletion packages/restapi/src/lib/chat/getGroupMembers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const getGroupMembers = async (
}

const API_BASE_URL = getAPIBaseUrls(env);
const requestUrl = `${API_BASE_URL}/v1/chat/${chatId}/members?pageNumber=${page}&pageSize=${limit}`;
const requestUrl = `${API_BASE_URL}/v1/chat/groups/${chatId}/members?pageNumber=${page}&pageSize=${limit}`;

const response = await axios.get(requestUrl);
return response.data;
Expand Down
35 changes: 35 additions & 0 deletions packages/restapi/src/lib/chat/getGroupMembersPublicKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import axios from 'axios';
import { getAPIBaseUrls } from '../helpers';
import Constants, { ENV } from '../constants';
import { ChatMemberCounts, ChatMemberProfile } from '../types';
import { FetchChatGroupInfoType } from './getGroupMembers';

/**
* GET /v1/chat/:chatId/members/public/keys
*/

export const getGroupMembersPublicKeys = async (
options: FetchChatGroupInfoType
): Promise<{ members: [{ did: string; publicKey: string }] }> => {
const { chatId, page = 1, limit = 20, env = Constants.ENV.PROD } = options;

try {
if (!chatId) {
throw new Error('Chat ID is required.');
}

const API_BASE_URL = getAPIBaseUrls(env);
const requestUrl = `${API_BASE_URL}/v1/chat/groups/${chatId}/members/public/keys?pageNumber=${page}&pageSize=${limit}`;

const response = await axios.get(requestUrl);
return response.data;
} catch (error) {
console.error(
`[Push SDK] - API - Error - API ${getGroupMembersPublicKeys.name} -: `,
error
);
throw new Error(
`[Push SDK] - API - Error - API ${getGroupMembersPublicKeys.name} -: ${error}`
);
}
};
10 changes: 6 additions & 4 deletions packages/restapi/src/lib/chat/helpers/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,11 @@ export const validateGroupMemberUpdateOptions = (
`Invalid role: ${role}. Allowed roles are ${allowedRoles.join(', ')}.`
);
}
if (upsert[role] && upsert[role].length > 100) {
throw new Error(`${role} array cannot have more than 100 addresses.`);

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)) {
Expand All @@ -239,8 +241,8 @@ export const validateGroupMemberUpdateOptions = (
});

// Validating remove array
if (remove && remove.length > 100) {
throw new Error('Remove array cannot have more than 100 addresses.');
if (remove && remove.length > 1000) {
throw new Error('Remove array cannot have more than 1000 addresses.');
}
remove.forEach((address) => {
if (!isValidETHAddress(address)) {
Expand Down
1 change: 1 addition & 0 deletions packages/restapi/src/lib/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export * from './getGroupMemberStatus';
export * from './getGroupMembers';
export * from './getGroupInfo';
export * from './getChatMemberCount';
export * from './getGroupMembersPublicKeys';
16 changes: 5 additions & 11 deletions packages/restapi/src/lib/chat/updateGroupMembers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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';
import { getAllGroupMembersPublicKeys } from './getAllGroupMembersPublicKeys';

export interface GroupMemberUpdateOptions extends EnvOptionsType {
chatId: string;
Expand Down Expand Up @@ -79,18 +79,15 @@ export const updateGroupMembers = async (
env,
});

const groupMembers = await getAllGroupMembers({ chatId, env });
const groupMembers = await getAllGroupMembersPublicKeys({ 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())
) {
if (removeParticipantSet.has(element.did.toLowerCase())) {
sameMembers = false;
}
});
Expand All @@ -101,11 +98,8 @@ export const updateGroupMembers = async (
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);
if (!removeParticipantSet.has(element.did.toLowerCase())) {
publicKeys.push(element.publicKey as string);
}
});

Expand Down
1 change: 1 addition & 0 deletions packages/restapi/src/lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ export interface ChatMemberCounts {
adminsCount: number;
membersCount: number;
pendingCount: number;
approvedCount: number;
}

export interface ChatMemberProfile {
Expand Down
94 changes: 92 additions & 2 deletions packages/restapi/tests/lib/benchmark/privateGroup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const _env = Constants.ENV.LOCAL;
* THIS TEST GROUP IS FOR BENCHMARKING SEND MESSAGE FOR PRIVATE GROUP
* These tests will be skipped
*/
describe.only('Private Groups', () => {
describe.skip('Private Groups', () => {
let account: string;
let account2: string;
let userAlice: PushAPI;
Expand Down Expand Up @@ -180,7 +180,7 @@ describe.only('Private Groups', () => {
* STEP 3 - AUTOJOIN
* This is imp for generating session keys so , do skip this test
*/
describe.skip('Private Group AutoJoin', () => {
describe('Private Group AutoJoin', () => {
it('10 Members', async () => {
const chatId =
'9e8bea378b4e4860956c177146786c2e96a0db8aa7c4156299181b3e56290a57';
Expand Down Expand Up @@ -354,6 +354,14 @@ describe.only('Private Groups', () => {
});
});

describe.skip('Update Group with Pending members', () => {
it('10 Members', async () => {
const chatId =
'd8892a41ccbb7d0c627d1e3976f3a0bd64540d1d535b1a339680f2ce5b0fbcf0';
await updateGroupWithPendingMembers(userAlice, chatId, 500);
});
});

// describe('Private Group Send Message', () => {
// it('10 Members', async () => {
// await createGroupAndSendMessages(userAlice, 10);
Expand Down Expand Up @@ -465,6 +473,88 @@ const createGroupWithPendingMembers = async (
return createdGroup.chatId;
};

/**
* CREATE GROUP WITH GIVEN MEMBERS COUNT PENDING MEMBERS
* @dev - Added members are pending members
*/
const updateGroupWithPendingMembers = async (
user: PushAPI,
chatId: string,
memberCount: number
): Promise<string> => {
/**
* STEP 1: Generate ENOUGH USERS
*/
console.log('Generating Users');
const users = await generateUsers(memberCount);

/**
* STEP 2: Add Members to Group
* Note - At max 100 members can be added at once
*/
console.log('Adding Members to Group');
let currentMemberCount = 1;
while (currentMemberCount < memberCount) {
const currentUsersIndex = currentMemberCount - 1;
if (currentMemberCount + 100 > memberCount) {
currentMemberCount = memberCount;
} else {
currentMemberCount += 100;
}
const nextUsersIndex = currentMemberCount - 1;

const membersToBeAdded = [];
for (let i = currentUsersIndex; i <= nextUsersIndex; i++) {
membersToBeAdded.push(users[i]);
}
await user.chat.group.add(chatId, {
role: 'MEMBER',
accounts: membersToBeAdded,
});
}
console.log('Added Members to Group : ', currentMemberCount);
return chatId;
};

const generateUsers = async (memberCount: number): Promise<string[]> => {
let users: string[] = []; // Now 'users' is explicitly typed as an array of strings
let generationCount = 0;
const batchSize = 20;

while (generationCount < memberCount) {
const userPromises: Promise<string>[] = []; // An array to hold the promises which will resolve to strings
for (let i = 0; i < batchSize && generationCount < memberCount; i++) {
userPromises.push(
(async () => {
const WALLET = ethers.Wallet.createRandom();
const signer = new ethers.Wallet(WALLET.privateKey);
const account = `eip155:${signer.address}`;
// Assume that PushAPI.initialize resolves successfully and you don't need anything from the resolved value
await PushAPI.initialize(signer, {
env: _env,
streamOptions: { enabled: false },
});
return account; // This resolves to a string
})()
);
generationCount++;
}

// Wait for all promises in the batch to resolve, and then spread their results into the 'users' array
const batchResults = await Promise.all(userPromises);
users = [...users, ...batchResults];

if (generationCount % 100 == 0) {
console.log('Generated Users : ', generationCount);
}
}

console.log(
`User Generation Completed, users generated : ${generationCount}`
);
return users; // 'users' is an array of strings representing accounts
};

/**
* CREATE GROUP WITH GIVEN MEMBERS COUNT NON-PENDING MEMBERS
* @dev - Added members are pending members
Expand Down