From 421307dcf8766a8ebabf6aeec5f34920d356c1e4 Mon Sep 17 00:00:00 2001 From: Mohammed S Date: Fri, 22 Sep 2023 18:37:17 +0530 Subject: [PATCH] fix: stream changes --- .../src/app/ChatTest/ChatTest.tsx | 3 + .../app/ChatTest/GetGroupMemberStatusTest.tsx | 86 +++++++++++ .../sdk-frontend-react/src/app/app.tsx | 3 + .../src/lib/chat/getGroupMemberStatus.ts | 56 +++++++ packages/restapi/src/lib/chat/index.ts | 1 + packages/restapi/src/lib/pushapi/PushAPI.ts | 32 +++- .../src/lib/pushstream/DataModifier.ts | 141 +++++++++++------- .../restapi/src/lib/pushstream/PushStream.ts | 4 +- .../src/lib/pushstream/pushStreamTypes.ts | 69 +++++---- packages/restapi/src/lib/types/index.ts | 6 + .../tests/lib/pushstream/initialize.test.ts | 130 +++++++++++++--- 11 files changed, 425 insertions(+), 106 deletions(-) create mode 100644 packages/examples/sdk-frontend-react/src/app/ChatTest/GetGroupMemberStatusTest.tsx create mode 100644 packages/restapi/src/lib/chat/getGroupMemberStatus.ts diff --git a/packages/examples/sdk-frontend-react/src/app/ChatTest/ChatTest.tsx b/packages/examples/sdk-frontend-react/src/app/ChatTest/ChatTest.tsx index d898b3736..789c84588 100644 --- a/packages/examples/sdk-frontend-react/src/app/ChatTest/ChatTest.tsx +++ b/packages/examples/sdk-frontend-react/src/app/ChatTest/ChatTest.tsx @@ -71,6 +71,9 @@ const ChatTest = () => { CHAT.GETGROUPACCESS + + CHAT.GETGROUPMEMBERSTATUS + CHAT.SEARCHGROUPS diff --git a/packages/examples/sdk-frontend-react/src/app/ChatTest/GetGroupMemberStatusTest.tsx b/packages/examples/sdk-frontend-react/src/app/ChatTest/GetGroupMemberStatusTest.tsx new file mode 100644 index 000000000..085c86ac9 --- /dev/null +++ b/packages/examples/sdk-frontend-react/src/app/ChatTest/GetGroupMemberStatusTest.tsx @@ -0,0 +1,86 @@ +import { useState, useContext } from 'react'; +import { + Section, + SectionItem, + CodeFormatter, + SectionButton, +} from '../components/StyledComponents'; +import Loader from '../components/Loader'; +import { EnvContext } from '../context'; +import * as PushAPI from '@pushprotocol/restapi'; + +const GetGroupMemberStatusTest = () => { + const { env } = useContext(EnvContext); + const [isLoading, setLoading] = useState(false); + const [chatId, setChatId] = useState(''); + const [did, setDid] = useState(''); + const [sendResponse, setSendResponse] = useState(''); + + const updateChatId = (e: React.SyntheticEvent) => { + setChatId((e.target as HTMLInputElement).value); + }; + + const updateDid = (e: React.SyntheticEvent) => { + setDid((e.target as HTMLInputElement).value); + }; + + const testGetGroupMemberStatus = async () => { + try { + setLoading(true); + + const response = await PushAPI.chat.getGroupMemberStatus({ + chatId: chatId, + did: did, + env, + }); + setSendResponse(response); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + return ( +
+

Get Group Member Status Test Page

+ + + +
+ + get group member status + + + + + + + + + + +
+ {sendResponse ? ( + + {JSON.stringify(sendResponse, null, 4)} + + ) : null} +
+
+
+
+ ); +}; + +export default GetGroupMemberStatusTest; diff --git a/packages/examples/sdk-frontend-react/src/app/app.tsx b/packages/examples/sdk-frontend-react/src/app/app.tsx index 04ed98fef..7c90e3559 100644 --- a/packages/examples/sdk-frontend-react/src/app/app.tsx +++ b/packages/examples/sdk-frontend-react/src/app/app.tsx @@ -87,6 +87,7 @@ import { lightChatTheme } from '@pushprotocol/uiweb'; import SearchSpaceTest from './SpaceTest/SearchSpaceTest'; import SearchGroupTest from './ChatTest/SearchGroupTest'; import RejectRequestTest from './ChatTest/RejectRequestTest'; +import GetGroupMemberStatusTest from './ChatTest/GetGroupMemberStatusTest'; window.Buffer = window.Buffer || Buffer; @@ -454,6 +455,8 @@ export function App() { } /> } /> } /> + } /> + } diff --git a/packages/restapi/src/lib/chat/getGroupMemberStatus.ts b/packages/restapi/src/lib/chat/getGroupMemberStatus.ts new file mode 100644 index 000000000..ddfd223b4 --- /dev/null +++ b/packages/restapi/src/lib/chat/getGroupMemberStatus.ts @@ -0,0 +1,56 @@ +import axios from 'axios'; +import { getAPIBaseUrls } from '../helpers'; +import Constants, { ENV } from '../constants'; +import { GroupAccess, GroupMemberStatus } from '../types'; +import { getUserDID } from './helpers'; + +/** + * GET /v1/chat/groups/:chatId/access/:did + */ + +export interface GetGroupMemberStatusType { + chatId: string; + did: string; // Decentralized Identifier + env?: ENV; +} + +export const getGroupMemberStatus = async ( + options: GetGroupMemberStatusType +): Promise => { + // Replace "any" with the actual response type + const { chatId, did, env = Constants.ENV.PROD } = options || {}; + try { + if (chatId == null || chatId.length === 0) { + throw new Error(`chatId cannot be null or empty`); + } + + if (did == null || did.length === 0) { + throw new Error(`did cannot be null or empty`); + } + + const user = await getUserDID(did, env); + + const API_BASE_URL = getAPIBaseUrls(env); + const requestUrl = `${API_BASE_URL}/v1/chat/groups/${chatId}/members/${user}/status`; + + console.log(requestUrl) + + return axios + .get(requestUrl) + .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 ${getGroupMemberStatus.name} -: `, + err + ); + throw Error( + `[Push SDK] - API - Error - API ${getGroupMemberStatus.name} -: ${err}` + ); + } +}; diff --git a/packages/restapi/src/lib/chat/index.ts b/packages/restapi/src/lib/chat/index.ts index 54660ee0c..64512ed4b 100644 --- a/packages/restapi/src/lib/chat/index.ts +++ b/packages/restapi/src/lib/chat/index.ts @@ -21,3 +21,4 @@ export * from './removeAdmins'; export * from './getGroupAccess'; export * from './searchGroups'; export * from './rejectRequest'; +export * from './getGroupMemberStatus'; diff --git a/packages/restapi/src/lib/pushapi/PushAPI.ts b/packages/restapi/src/lib/pushapi/PushAPI.ts index b90ada8cf..d501e7712 100644 --- a/packages/restapi/src/lib/pushapi/PushAPI.ts +++ b/packages/restapi/src/lib/pushapi/PushAPI.ts @@ -395,7 +395,6 @@ export class PushAPI { chatId: string, options: GroupUpdateOptions ): Promise => { - // Fetch Group Details const group = await PUSH_CHAT.getGroup({ chatId: chatId, env: this.env, @@ -505,15 +504,34 @@ export class PushAPI { } }, - // TODO: If invite was sent, the inside accept should be called here. join: async (target: string): Promise => { - return await PUSH_CHAT.addMembers({ + const status = await PUSH_CHAT.getGroupMemberStatus({ + chatId: target, + did: this.account, + }); + + if (status.isPending) { + await PUSH_CHAT.approve({ + senderAddress: target, + env: this.env, + account: this.account, + signer: this.signer, + pgpPrivateKey: this.decryptedPgpPvtKey, + }); + } else if (!status.isMember) { + return await PUSH_CHAT.addMembers({ + chatId: target, + members: [this.account], + env: this.env, + account: this.account, + signer: this.signer, + pgpPrivateKey: this.decryptedPgpPvtKey, + }); + } + + return await PUSH_CHAT.getGroup({ chatId: target, - members: [this.account], env: this.env, - account: this.account, - signer: this.signer, - pgpPrivateKey: this.decryptedPgpPvtKey, }); }, diff --git a/packages/restapi/src/lib/pushstream/DataModifier.ts b/packages/restapi/src/lib/pushstream/DataModifier.ts index 1107aa190..f1c820088 100644 --- a/packages/restapi/src/lib/pushstream/DataModifier.ts +++ b/packages/restapi/src/lib/pushstream/DataModifier.ts @@ -1,17 +1,17 @@ import { CreateGroupEvent, - Meta, + GroupMeta, GroupEventRawData, UpdateGroupEvent, MessageRawData, MessageEvent, + MessageEventType, + Member, + GroupEventType, } from './pushStreamTypes'; export class DataModifier { - public static handleChatGroupEvent( - data: any, - includeRaw = false - ): any { + public static handleChatGroupEvent(data: any, includeRaw = false): any { if (data.eventType === 'create') { return this.mapToCreateGroupEvent(data, includeRaw); } else if (data.eventType === 'update') { @@ -26,17 +26,48 @@ export class DataModifier { incomingData: any, includeRaw: boolean ): { - meta: Meta; + meta: GroupMeta; raw?: GroupEventRawData; } { - const meta: Meta = { + const mapMembersAdmins = (arr: any[]): Member[] => { + return arr.map((item) => ({ + address: item.wallet, + profile: { + image: item.image, + publicKey: item.publicKey, + }, + })); + }; + + const mapPendingMembersAdmins = (arr: any[]): Member[] => { + return arr.map((item) => ({ + address: item.wallet, + profile: { + image: item.image, + publicKey: item.publicKey, + }, + })); + }; + + const meta: GroupMeta = { name: incomingData.groupName, description: incomingData.groupDescription, image: incomingData.groupImage, owner: incomingData.groupCreator, - members: incomingData?.members || [], // TODO: only latest 20, TODO: Fix member user profiles - admins: incomingData?.admins || [], // TODO: only latest 20 - pending: incomingData?.pending || [], // TODO: only latest 20 + members: mapMembersAdmins( + incomingData.members.filter((m: any) => !m.isAdmin) + ), + admins: mapMembersAdmins( + incomingData.members.filter((m: any) => m.isAdmin) + ), + pending: { + members: mapPendingMembersAdmins( + incomingData.pendingMembers.filter((m: any) => !m.isAdmin) + ), + admins: mapPendingMembersAdmins( + incomingData.pendingMembers.filter((m: any) => m.isAdmin) + ), + }, private: !incomingData.isPublic, rules: incomingData.rules || {}, }; @@ -51,73 +82,74 @@ export class DataModifier { return { meta }; } - public static mapToCreateGroupEvent( + public static mapToGroupEvent( + eventType: GroupEventType, incomingData: any, includeRaw: boolean - ): CreateGroupEvent { + ): CreateGroupEvent | UpdateGroupEvent { const { meta, raw } = this.buildChatGroupEventMetaAndRaw( incomingData, includeRaw ); - const createGroupEvent: CreateGroupEvent = { - event: 'createGroup', - origin: 'self', // TODO: This is missing in the event - timestamp: String(Date.now()), // TODO: This is missing in the event + const groupEvent: any = { + event: eventType, + origin: incomingData.messageOrigin, + timestamp: incomingData.timestamp, chatId: incomingData.chatId, - from: incomingData.groupCreator, + from: incomingData.from, meta, }; if (includeRaw) { - createGroupEvent.raw = raw; + groupEvent.raw = raw; } - return createGroupEvent; + + return groupEvent as CreateGroupEvent | UpdateGroupEvent; + } + + public static mapToCreateGroupEvent( + incomingData: any, + includeRaw: boolean + ): CreateGroupEvent { + return this.mapToGroupEvent( + GroupEventType.createGroup, + incomingData, + includeRaw + ) as CreateGroupEvent; } public static mapToUpdateGroupEvent( incomingData: any, includeRaw: boolean ): UpdateGroupEvent { - const { meta, raw } = this.buildChatGroupEventMetaAndRaw( + return this.mapToGroupEvent( + GroupEventType.updateGroup, incomingData, includeRaw - ); - - const updateGroupEvent: UpdateGroupEvent = { - event: 'updateGroup', - origin: incomingData.origin, // TODO: This is missing in the event - timestamp: String(Date.now()), // TODO: This is missing in the event - chatId: incomingData.chatId, - from: incomingData.from, // TODO: This is missing in the event - meta, - }; - - if (includeRaw) { - updateGroupEvent.raw = raw; - } - - return updateGroupEvent; + ) as UpdateGroupEvent; } public static mapToMessageEvent( data: any, - includeRaw = false + includeRaw = false, + eventType: MessageEventType ): MessageEvent { const messageEvent: MessageEvent = { - event: 'message', - origin: data.messageOrigin === 'other' ? 'other' : 'self', // Replace with the actual logic + event: eventType, + origin: data.messageOrigin, timestamp: data.timestamp.toString(), - chatId: data.chatId, + chatId: data.chatId, // TODO: ChatId not working for w2w from: data.fromCAIP10, - to: [data.toCAIP10], // Assuming 'to' is an array in MessageEvent. Update as necessary. + to: [data.toCAIP10], // TODO: Assuming 'to' is an array in MessageEvent. Update as necessary. message: { type: data.messageType, content: data.messageContent, }, meta: { - group: true, // Replace with the actual logic + group: data.isGroup || false, }, + reference: data.cid, }; if (includeRaw) { @@ -126,29 +158,34 @@ export class DataModifier { toCAIP10: data.toCAIP10, fromDID: data.fromDID, toDID: data.toDID, - messageObj: data.messageObj, - messageContent: data.messageContent, - messageType: data.messageType, - timestamp: data.timestamp, encType: data.encType, encryptedSecret: data.encryptedSecret, signature: data.signature, sigType: data.sigType, verificationProof: data.verificationProof, - link: data.link, - cid: data.cid, - chatId: data.chatId, + previousReference: data.link, }; messageEvent.raw = rawData; } + return messageEvent; } public static handleChatEvent(data: any, includeRaw = false): any { - if (data.messageCategory === 'Chat') { - return this.mapToMessageEvent(data, includeRaw); + const eventTypeMap: { [key: string]: MessageEventType } = { + Chat: 'message', + Request: 'request', + Approve: 'accept', + Reject: 'reject', + }; + + const eventType: MessageEventType | undefined = + eventTypeMap[data.messageCategory]; + + if (eventType) { + return this.mapToMessageEvent(data, includeRaw, eventType); } else { - console.warn('Unknown eventType:', data.eventType); + console.warn('Unknown messageCategory:', data.messageCategory); return data; } } diff --git a/packages/restapi/src/lib/pushstream/PushStream.ts b/packages/restapi/src/lib/pushstream/PushStream.ts index 81b137e9c..5e0ed9ea1 100644 --- a/packages/restapi/src/lib/pushstream/PushStream.ts +++ b/packages/restapi/src/lib/pushstream/PushStream.ts @@ -20,13 +20,13 @@ export class PushStream extends EventEmitter { autoConnect: options.connection?.auto ?? true, reconnectionAttempts: options.connection?.retries ?? 3, }, - env: options.env, + env: options.env as ENV, }); if (!this.pushChatSocket) { throw new Error('Push chat socket not connected'); } else { - console.log('Push socket connected'); + console.log('Push socket connected ' + `eip155:${account}`); } this.raw = options.raw ?? false; diff --git a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts index 8821dbdb4..3c475dde2 100644 --- a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts +++ b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts @@ -2,7 +2,7 @@ import { ENV } from "../constants"; import { Rules } from "../types"; export type PushStreamInitializeProps = { - listen: string[]; + listen?: string[]; filter?: { channels?: string[]; chats?: string[]; @@ -12,7 +12,7 @@ export type PushStreamInitializeProps = { retries?: number; }; raw?: boolean; - env: ENV; + env?: ENV; }; export enum STREAM { @@ -26,16 +26,36 @@ export enum STREAM { DISCONNECT = 'STREAM.DISCONNECT', } -type Origin = 'other' | 'self'; +export type MessageOrigin = 'other' | 'self'; +export type MessageEventType = 'message' | 'request' | 'accept' | 'reject'; +export enum GroupEventType { + createGroup = 'createGroup', + updateGroup = 'updateGroup', +} + +export interface Profile { + image: string; + publicKey: string; +} + +export interface Member { + address: string; + profile: Profile; +} + +export interface Pending { + members: Member[]; + admins: Member[]; +} -export interface Meta { +export interface GroupMeta { name: string; description: string; image: string; owner: string; - members: string[]; - admins: string[]; - pending: string[]; + members: Member[]; + admins: Member[]; + pending: Pending; private: boolean; rules: Rules; } @@ -44,29 +64,27 @@ export interface GroupEventRawData { verificationProof: string; } -export interface CreateGroupEvent { - event: 'createGroup'; - origin: Origin; +export interface GroupEventBase { + origin: MessageOrigin; timestamp: string; chatId: string; from: string; - meta: Meta; + meta: GroupMeta; raw?: GroupEventRawData; + event: GroupEventType; } -export interface UpdateGroupEvent { - event: 'updateGroup'; - origin: Origin; - timestamp: string; - chatId: string; - from: string; - meta: Meta; - raw?: GroupEventRawData; +export interface CreateGroupEvent extends GroupEventBase { + event: GroupEventType.createGroup; +} + +export interface UpdateGroupEvent extends GroupEventBase { + event: GroupEventType.updateGroup; } export interface MessageEvent { - event: 'message'; - origin: 'other' | 'self'; + event: MessageEventType; + origin: MessageOrigin; timestamp: string; chatId: string; from: string; @@ -78,6 +96,7 @@ export interface MessageEvent { meta: { group: boolean; }; + reference: string; raw?: MessageRawData; } @@ -86,16 +105,10 @@ export interface MessageRawData { toCAIP10: string; fromDID: string; toDID: string; - messageObj: string; - messageContent: string; - messageType: string; - timestamp: number; encType: string; encryptedSecret: string; signature: string; sigType: string; verificationProof: string; - link: string; - cid: string; - chatId: string; + previousReference: string; } \ No newline at end of file diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index 22e172532..8ca1dd8fe 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -355,6 +355,12 @@ export interface GroupAccess { rules?: Rules; } +export interface GroupMemberStatus { + isMember: boolean; + isPending: boolean; + isAdmin: boolean; +} + export interface SpaceAccess { entry: boolean; rules?: SpaceRules; diff --git a/packages/restapi/tests/lib/pushstream/initialize.test.ts b/packages/restapi/tests/lib/pushstream/initialize.test.ts index 58538546e..1d8ebd875 100644 --- a/packages/restapi/tests/lib/pushstream/initialize.test.ts +++ b/packages/restapi/tests/lib/pushstream/initialize.test.ts @@ -5,19 +5,84 @@ import { PushStream } from '../../../src/lib/pushstream/PushStream'; import { expect } from 'chai'; // Assuming you're using chai for assertions import { ethers } from 'ethers'; import { PushAPI } from '../../../src/lib/pushapi/PushAPI'; -import { ENV, MessageType } from '../../../src/lib/constants'; +import { ENV } from '../../../src/lib/constants'; import { STREAM } from '../../../src/lib/pushstream/pushStreamTypes'; describe.only('PushStream.initialize functionality', () => { it('Should initialize new stream and listen to events', async () => { + const MESSAGE = 'Hey There!!!'; + const provider = ethers.getDefaultProvider(); + const WALLET = ethers.Wallet.createRandom(); const signer = new ethers.Wallet(WALLET.privateKey, provider); - const user = await PushAPI.initialize(signer, { env: ENV.LOCAL, }); - const stream = await PushStream.initialize(signer.address); + + const WALLET2 = ethers.Wallet.createRandom(); + const signer2 = new ethers.Wallet(WALLET2.privateKey, provider); + const user2 = await PushAPI.initialize(signer2, { + env: ENV.LOCAL, + }); + + const WALLET3 = ethers.Wallet.createRandom(); + const signer3 = new ethers.Wallet(WALLET3.privateKey, provider); + const user3 = await PushAPI.initialize(signer3, { + env: ENV.LOCAL, + }); + + const WALLET4 = ethers.Wallet.createRandom(); + const signer4 = new ethers.Wallet(WALLET4.privateKey, provider); + const user4 = await PushAPI.initialize(signer4, { + env: ENV.LOCAL, + }); + + const GROUP_RULES = { + entry: { + conditions: [ + { + any: [ + { + type: 'PUSH', + category: 'CustomEndpoint', + subcategory: 'GET', + data: { + url: 'https://api.ud-staging.com/profile/badges/dead_pixel/validate/{{user_address}}?rule=join', + }, + }, + ], + }, + ], + }, + chat: { + conditions: [ + { + any: [ + { + type: 'PUSH', + category: 'CustomEndpoint', + subcategory: 'GET', + data: { + url: 'https://api.ud-staging.com/profile/badges/dead_pixel/validate/{{user_address}}?rule=chat', + }, + }, + ], + }, + ], + }, + }; + + const CREATE_GROUP_REQUEST = { + description: 'test', + image: 'test', + members: [signer2.address], + admins: [], + private: false, + rules: {}, + }; + + const stream = await PushStream.initialize(signer.address, { raw: true }); const createEventPromise = ( expectedEvent: string, @@ -31,7 +96,15 @@ describe.only('PushStream.initialize functionality', () => { try { receivedEvents.push(data); eventCount++; - console.log(`Event ${eventCount} for ${expectedEvent}:`, data); + + /*console.log( + `Event ${eventCount} for ${expectedEvent}:`, + util.inspect(data, { + showHidden: false, + depth: null, + colors: true, + }) + );*/ expect(data).to.not.be.null; if (eventCount === expectedEventCount) { @@ -45,40 +118,63 @@ describe.only('PushStream.initialize functionality', () => { }); }; - const onDataReceived = createEventPromise('CHAT_OPS', STREAM.CHAT_OPS, 2); + const onDataReceived = createEventPromise('CHAT_OPS', STREAM.CHAT_OPS, 1); const onMessageReceived = createEventPromise('CHAT', STREAM.CHAT, 1); // Create and update group - const createdGroup = await user.chat.group.create('test', { - description: 'test', - image: 'test', - members: [], - admins: [], - private: false, - }); + const createdGroup = await user.chat.group.create( + 'test', + CREATE_GROUP_REQUEST + ); - const updatedGroup = await user.chat.group.update(createdGroup.chatId, { + const w2wRejectRequest = await user2.chat.group.join(createdGroup.chatId); + + console.log(w2wRejectRequest); + + /*const updatedGroup = await user.chat.group.update(createdGroup.chatId, { description: 'Updated Description', }); - const response = await user.chat.send(createdGroup.chatId, { + const groupMessageResponse = await user.chat.send(createdGroup.chatId, { content: 'Hello', type: MessageType.TEXT, }); + const w2wMessageResponse = await user2.chat.send(signer.address, { + content: MESSAGE, + }); + const w2wAcceptsRequest = await user.chat.accept(signer2.address); + + const w2wMessageResponse2 = await user2.chat.send(signer.address, { + content: MESSAGE, + }); + + const w2wMessageResponse2 = await user3.chat.send(signer.address, { + content: MESSAGE, + }); + const w2wRejectRequest = await user.chat.reject(signer3.address);*/ + // Timeout promise const timeout = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Timeout after 10 seconds')), 10000); + setTimeout(() => reject(new Error('Timeout after 5 seconds')), 5); }); // Wait for either one of the events to be emitted or for the timeout try { - const data = await Promise.race([ + const results = await Promise.allSettled([ onDataReceived, onMessageReceived, timeout, ]); - console.log(data); + + results.forEach((result) => { + if (result.status === 'rejected') { + console.error(result.reason); + } else { + // Handle result.value if necessary + console.log(result.value); + } + }); } catch (error) { console.error(error); }