From 5914c232b6f919e6085d17ff780c6dd125cefbd5 Mon Sep 17 00:00:00 2001 From: Mohammed S Date: Thu, 23 Nov 2023 17:16:02 +0530 Subject: [PATCH] 853 improvement proposal add read only low functions to high level functions (#873) * fix: PUSH API read only mode * fix: fix error message --- .../pushNotification/pushNotificationBase.ts | 3 +- packages/restapi/src/lib/pushapi/PushAPI.ts | 141 ++++-- packages/restapi/src/lib/pushapi/chat.ts | 51 ++- .../restapi/src/lib/pushapi/encryption.ts | 47 +- packages/restapi/src/lib/pushapi/profile.ts | 6 +- .../restapi/src/lib/pushstream/PushStream.ts | 25 +- .../restapi/tests/lib/pushapi/chat.test.ts | 55 +++ .../restapi/tests/lib/pushapi/profile.test.ts | 25 + .../tests/lib/pushstream/initialize.test.ts | 433 ++++++++++++++++++ 9 files changed, 704 insertions(+), 82 deletions(-) diff --git a/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts b/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts index 4748d4a8e..2bbfc8557 100644 --- a/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts +++ b/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts @@ -25,6 +25,7 @@ import { validateCAIP, } from '../helpers'; import * as PUSH_ALIAS from '../alias'; +import { PushAPI } from '../pushapi/PushAPI'; // ERROR CONSTANTS const ERROR_ACCOUNT_NEEDED = 'Account is required'; @@ -113,7 +114,7 @@ export class PushNotificationBaseClass { // checks if the signer object is supplied protected checkSignerObjectExists() { - if (!this.signer) throw new Error(ERROR_SIGNER_NEEDED); + if (!this.signer) throw new Error(PushAPI.ensureSignerMessage()); return true; } diff --git a/packages/restapi/src/lib/pushapi/PushAPI.ts b/packages/restapi/src/lib/pushapi/PushAPI.ts index 5201b5ef5..33bfad85b 100644 --- a/packages/restapi/src/lib/pushapi/PushAPI.ts +++ b/packages/restapi/src/lib/pushapi/PushAPI.ts @@ -17,10 +17,11 @@ import { } from '../pushstream/pushStreamTypes'; export class PushAPI { - private signer: SignerType; + private signer?: SignerType; + private readMode: boolean; private account: string; - private decryptedPgpPvtKey: string; - private pgpPublicKey: string; + private decryptedPgpPvtKey?: string; + private pgpPublicKey?: string; private env: ENV; private progressHook?: (progress: ProgressHookType) => void; @@ -34,14 +35,16 @@ export class PushAPI { public notification!: Notification; private constructor( - signer: SignerType, env: ENV, account: string, - decryptedPgpPvtKey: string, - pgpPublicKey: string, + readMode: boolean, + decryptedPgpPvtKey?: string, + pgpPublicKey?: string, + signer?: SignerType, progressHook?: (progress: ProgressHookType) => void ) { this.signer = signer; + this.readMode = readMode; this.env = env; this.account = account; this.decryptedPgpPvtKey = decryptedPgpPvtKey; @@ -53,33 +56,61 @@ export class PushAPI { // Initialize the instances of the four classes this.chat = new Chat( this.account, - this.decryptedPgpPvtKey, this.env, + this.decryptedPgpPvtKey, this.signer, this.progressHook ); this.profile = new Profile( this.account, - this.decryptedPgpPvtKey, this.env, + this.decryptedPgpPvtKey, this.progressHook ); this.encryption = new Encryption( this.account, + this.env, this.decryptedPgpPvtKey, this.pgpPublicKey, - this.env, this.signer, this.progressHook ); this.user = new User(this.account, this.env); } - + // Overloaded initialize method signatures static async initialize( - signer: SignerType, + signer?: SignerType, options?: PushAPIInitializeProps - ): Promise { + ): Promise; + static async initialize(options?: PushAPIInitializeProps): Promise; + + static async initialize(...args: any[]): Promise { try { + let signer: SignerType | undefined; + let options: PushAPIInitializeProps | undefined; + + if ( + args.length === 1 && + typeof args[0] === 'object' && + 'account' in args[0] + ) { + // Single options object provided + options = args[0]; + } else if (args.length === 1) { + // Only signer provided + [signer] = args; + } else if (args.length === 2) { + // Separate signer and options arguments provided + [signer, options] = args; + } else { + // Handle other cases or throw an error + throw new Error('Invalid arguments provided to initialize method.'); + } + + if (!signer && !options?.account) { + throw new Error("Either 'signer' or 'account' must be provided."); + } + // Default options const defaultOptions: PushAPIInitializeProps = { env: ENV.STAGING, @@ -101,17 +132,29 @@ export class PushAPI { : defaultOptions.autoUpgrade, }; + const readMode = !signer; + // Get account // Derives account from signer if not provided - const derivedAccount = await getAccountAddress( - getWallet({ - account: settings.account as string | null, - signer: signer, - }) - ); - let decryptedPGPPrivateKey: string; - let pgpPublicKey: string; + let derivedAccount; + if (signer) { + derivedAccount = await getAccountAddress( + getWallet({ + account: settings.account as string | null, + signer: signer, + }) + ); + } else { + derivedAccount = options?.account; + } + + if (!derivedAccount) { + throw new Error('Account could not be derived.'); + } + + let decryptedPGPPrivateKey; + let pgpPublicKey; /** * Decrypt PGP private key @@ -122,37 +165,41 @@ export class PushAPI { account: derivedAccount, env: settings.env, }); - if (user && user.encryptedPrivateKey) { - decryptedPGPPrivateKey = await PUSH_CHAT.decryptPGPKey({ - encryptedPGPPrivateKey: user.encryptedPrivateKey, - signer: signer, - toUpgrade: settings.autoUpgrade, - additionalMeta: settings.versionMeta, - progressHook: settings.progressHook, - env: settings.env, - }); - pgpPublicKey = user.publicKey; - } else { - const newUser = await PUSH_USER.create({ - env: settings.env, - account: derivedAccount, - signer, - version: settings.version, - additionalMeta: settings.versionMeta, - origin: settings.origin, - progressHook: settings.progressHook, - }); - decryptedPGPPrivateKey = newUser.decryptedPrivateKey as string; - pgpPublicKey = newUser.publicKey; + + if (!readMode) { + if (user && user.encryptedPrivateKey) { + decryptedPGPPrivateKey = await PUSH_CHAT.decryptPGPKey({ + encryptedPGPPrivateKey: user.encryptedPrivateKey, + signer: signer, + toUpgrade: settings.autoUpgrade, + additionalMeta: settings.versionMeta, + progressHook: settings.progressHook, + env: settings.env, + }); + pgpPublicKey = user.publicKey; + } else { + const newUser = await PUSH_USER.create({ + env: settings.env, + account: derivedAccount, + signer, + version: settings.version, + additionalMeta: settings.versionMeta, + origin: settings.origin, + progressHook: settings.progressHook, + }); + decryptedPGPPrivateKey = newUser.decryptedPrivateKey as string; + pgpPublicKey = newUser.publicKey; + } } // Initialize PushAPI instance const api = new PushAPI( - signer, settings.env as ENV, derivedAccount, + readMode, decryptedPGPPrivateKey, pgpPublicKey, + signer, settings.progressHook ); @@ -173,11 +220,11 @@ export class PushAPI { this.stream = await PushStream.initialize( this.account, - this.decryptedPgpPvtKey, - this.signer, listen, this.env, + this.decryptedPgpPvtKey, this.progressHook, + this.signer, options ); @@ -190,4 +237,8 @@ export class PushAPI { env: this.env, }); } + + static ensureSignerMessage(): string { + return 'Operation not allowed in read-only mode. Signer is required.'; + } } diff --git a/packages/restapi/src/lib/pushapi/chat.ts b/packages/restapi/src/lib/pushapi/chat.ts index 193db597a..6cd5117e2 100644 --- a/packages/restapi/src/lib/pushapi/chat.ts +++ b/packages/restapi/src/lib/pushapi/chat.ts @@ -26,14 +26,15 @@ import { updateGroupProfile, } from '../chat/updateGroupProfile'; import { User } from './user'; +import { PushAPI } from './PushAPI'; export class Chat { private userInstance: User; constructor( private account: string, - private decryptedPgpPvtKey: string, private env: ENV, - private signer: SignerType, + private decryptedPgpPvtKey?: string, + private signer?: SignerType, private progressHook?: (progress: ProgressHookType) => void ) { this.userInstance = new User(this.account, this.env); @@ -55,7 +56,7 @@ export class Chat { page: options?.page, limit: options?.limit, env: this.env, - toDecrypt: true, + toDecrypt: !!this.signer, // Set to false if signer is undefined or null, }; switch (type) { @@ -77,7 +78,7 @@ export class Chat { return await PUSH_CHAT.latest({ threadhash: threadHash, - toDecrypt: true, + toDecrypt: !!this.signer, // Set to false if signer is undefined or null, pgpPrivateKey: this.decryptedPgpPvtKey, account: this.account, env: this.env, @@ -111,12 +112,16 @@ export class Chat { env: this.env, threadhash: reference, pgpPrivateKey: this.decryptedPgpPvtKey, - toDecrypt: true, + toDecrypt: !!this.signer, // Set to false if signer is undefined or null, limit: options?.limit, }); } async send(recipient: string, options: Message): Promise { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } + if (!options.type) { options.type = MessageType.TEXT; } @@ -131,6 +136,9 @@ export class Chat { } async decrypt(messagePayloads: IMessageIPFS[]) { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } return await PUSH_CHAT.decryptConversation({ pgpPrivateKey: this.decryptedPgpPvtKey, env: this.env, @@ -140,6 +148,9 @@ export class Chat { } async accept(target: string): Promise { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } return await PUSH_CHAT.approve({ senderAddress: target, env: this.env, @@ -150,6 +161,9 @@ export class Chat { } async reject(target: string): Promise { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } await PUSH_CHAT.reject({ senderAddress: target, env: this.env, @@ -160,6 +174,9 @@ export class Chat { } async block(users: Array): Promise { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } const user = await PUSH_USER.get({ account: this.account, env: this.env, @@ -194,6 +211,9 @@ export class Chat { } async unblock(users: Array): Promise { + if (!this.signer || !this.decryptedPgpPvtKey) { + throw new Error(PushAPI.ensureSignerMessage()); + } const user = await PUSH_USER.get({ account: this.account, env: this.env, @@ -236,6 +256,9 @@ export class Chat { group = { create: async (name: string, options?: GroupCreationOptions) => { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } const groupParams: PUSH_CHAT.ChatCreateGroupType = { groupName: name, groupDescription: options?.description, @@ -271,6 +294,9 @@ export class Chat { chatId: string, options: GroupUpdateOptions ): Promise => { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } const group = await PUSH_CHAT.getGroup({ chatId: chatId, env: this.env, @@ -301,6 +327,9 @@ export class Chat { }, add: async (chatId: string, options: ManageGroupOptions) => { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } const { role, accounts } = options; const validRoles = ['ADMIN', 'MEMBER']; @@ -340,6 +369,9 @@ export class Chat { }, remove: async (chatId: string, options: ManageGroupOptions) => { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } const { role, accounts } = options; const validRoles = ['ADMIN', 'MEMBER']; @@ -379,6 +411,9 @@ export class Chat { }, join: async (target: string): Promise => { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } const status = await PUSH_CHAT.getGroupMemberStatus({ chatId: target, did: this.account, @@ -411,6 +446,9 @@ export class Chat { }, leave: async (target: string): Promise => { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } const status = await PUSH_CHAT.getGroupMemberStatus({ chatId: target, did: this.account, @@ -438,6 +476,9 @@ export class Chat { } }, reject: async (target: string): Promise => { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } await PUSH_CHAT.reject({ senderAddress: target, env: this.env, diff --git a/packages/restapi/src/lib/pushapi/encryption.ts b/packages/restapi/src/lib/pushapi/encryption.ts index 14e1ec286..843c80236 100644 --- a/packages/restapi/src/lib/pushapi/encryption.ts +++ b/packages/restapi/src/lib/pushapi/encryption.ts @@ -1,9 +1,7 @@ import { ENCRYPTION_TYPE, ENV } from '../constants'; -import { - SignerType, - ProgressHookType, -} from '../types'; +import { SignerType, ProgressHookType } from '../types'; import * as PUSH_USER from '../user'; +import { PushAPI } from './PushAPI'; import { User } from './user'; export class Encryption { @@ -11,10 +9,10 @@ export class Encryption { constructor( private account: string, - private decryptedPgpPvtKey: string, - private pgpPublicKey: string, private env: ENV, - private signer: SignerType, + private decryptedPgpPvtKey?: string, + private pgpPublicKey?: string, + private signer?: SignerType, private progressHook?: (progress: ProgressHookType) => void ) { this.userInstance = new User(this.account, this.env); @@ -22,19 +20,22 @@ export class Encryption { async info() { const userInfo = await this.userInstance.info(); - const decryptedPassword = await PUSH_USER.decryptAuth({ - account: this.account, - env: this.env, - signer: this.signer, - progressHook: this.progressHook, - additionalMeta: { - NFTPGP_V1: { - encryptedPassword: JSON.stringify( - JSON.parse(userInfo.encryptedPrivateKey).encryptedPassword - ), + let decryptedPassword; + if (this.signer) { + decryptedPassword = await PUSH_USER.decryptAuth({ + account: this.account, + env: this.env, + signer: this.signer, + progressHook: this.progressHook, + additionalMeta: { + NFTPGP_V1: { + encryptedPassword: JSON.stringify( + JSON.parse(userInfo.encryptedPrivateKey).encryptedPassword + ), + }, }, - }, - }); + }); + } return { decryptedPgpPrivateKey: this.decryptedPgpPvtKey, @@ -53,6 +54,14 @@ export class Encryption { }; } ) { + if (!this.signer) { + throw new Error(PushAPI.ensureSignerMessage()); + } + + if (!this.decryptedPgpPvtKey || !this.pgpPublicKey) { + throw new Error(PushAPI.ensureSignerMessage()); + } + return await PUSH_USER.auth.update({ account: this.account, pgpEncryptionVersion: updatedEncryptionType, diff --git a/packages/restapi/src/lib/pushapi/profile.ts b/packages/restapi/src/lib/pushapi/profile.ts index f3a8861b0..efe602841 100644 --- a/packages/restapi/src/lib/pushapi/profile.ts +++ b/packages/restapi/src/lib/pushapi/profile.ts @@ -1,12 +1,13 @@ import { ProgressHookType } from '../types'; import * as PUSH_USER from '../user'; import { ENV } from '../constants'; +import { PushAPI } from './PushAPI'; export class Profile { constructor( private account: string, - private decryptedPgpPvtKey: string, private env: ENV, + private decryptedPgpPvtKey?: string, private progressHook?: (progress: ProgressHookType) => void ) {} @@ -19,6 +20,9 @@ export class Profile { } async update(options: { name?: string; desc?: string; picture?: string }) { + if (!this.decryptedPgpPvtKey) { + throw new Error(PushAPI.ensureSignerMessage()); + } const { name, desc, picture } = options; const response = await PUSH_USER.profile.update({ pgpPrivateKey: this.decryptedPgpPvtKey, diff --git a/packages/restapi/src/lib/pushstream/PushStream.ts b/packages/restapi/src/lib/pushstream/PushStream.ts index ecc0b39d2..bc3cab957 100644 --- a/packages/restapi/src/lib/pushstream/PushStream.ts +++ b/packages/restapi/src/lib/pushstream/PushStream.ts @@ -25,11 +25,11 @@ export class PushStream extends EventEmitter { constructor( account: string, - private decryptedPgpPvtKey: string, - private signer: SignerType, private _listen: STREAM[], options: PushStreamInitializeProps, - private progressHook?: (progress: ProgressHookType) => void + private decryptedPgpPvtKey?: string, + private progressHook?: (progress: ProgressHookType) => void, + private signer?: SignerType ) { super(); @@ -41,8 +41,8 @@ export class PushStream extends EventEmitter { this.chatInstance = new Chat( this.account, - this.decryptedPgpPvtKey, this.options.env as ENV, + this.decryptedPgpPvtKey, this.signer, this.progressHook ); @@ -50,11 +50,11 @@ export class PushStream extends EventEmitter { static async initialize( account: string, - decryptedPgpPvtKey: string, - signer: SignerType, listen: STREAM[], env: ENV, + decryptedPgpPvtKey?: string, progressHook?: (progress: ProgressHookType) => void, + signer?: SignerType, options?: PushStreamInitializeProps ): Promise { const defaultOptions: PushStreamInitializeProps = { @@ -81,11 +81,11 @@ export class PushStream extends EventEmitter { const stream = new PushStream( accountToUse, - decryptedPgpPvtKey, - signer, listen, settings, - progressHook + decryptedPgpPvtKey, + progressHook, + signer ); return stream; } @@ -265,8 +265,11 @@ export class PushStream extends EventEmitter { data.messageCategory == 'Chat' || data.messageCategory == 'Request' ) { - data = await this.chatInstance.decrypt([data]); - data = data[0]; + // Dont call this if read only mode ? + if (this.signer) { + data = await this.chatInstance.decrypt([data]); + data = data[0]; + } } const modifiedData = DataModifier.handleChatEvent(data, this.raw); diff --git a/packages/restapi/tests/lib/pushapi/chat.test.ts b/packages/restapi/tests/lib/pushapi/chat.test.ts index 10b88c9d1..b78b1fc0a 100644 --- a/packages/restapi/tests/lib/pushapi/chat.test.ts +++ b/packages/restapi/tests/lib/pushapi/chat.test.ts @@ -38,6 +38,24 @@ describe('PushAPI.chat functionality', () => { expect(response).to.be.an('array'); expect(response.length).to.equal(1); }); + + it('Should list request read only', async () => { + await userAlice.chat.send(account2, { content: MESSAGE }); + + const account = (await userBob.info()).did; + + const userBobReadOnly = await PushAPI.initialize({ + account: account, + }); + + const response = await userBobReadOnly.chat.list('REQUESTS', { + page: 1, + limit: 10, + }); + expect(response).to.be.an('array'); + expect(response.length).to.equal(1); + }); + it('Should list chats ', async () => { const response = await userAlice.chat.list('CHATS', { page: 1, @@ -45,6 +63,19 @@ describe('PushAPI.chat functionality', () => { }); expect(response).to.be.an('array'); }); + it('Should list chats read only', async () => { + const account = (await userAlice.info()).did; + + const userAliceReadOnly = await PushAPI.initialize({ + account: account, + }); + + const response = await userAliceReadOnly.chat.list('CHATS', { + page: 1, + limit: 10, + }); + expect(response).to.be.an('array'); + }); it('Should send message ', async () => { const response = await userAlice.chat.send(account2, { content: 'Hello', @@ -52,6 +83,30 @@ describe('PushAPI.chat functionality', () => { }); expect(response).to.be.an('object'); }); + it('Should send message read only', async () => { + const account = (await userAlice.info()).did; + + const userAliceReadOnly = await PushAPI.initialize({ + account: account, + }); + + let errorCaught: any = null; + + try { + await userAliceReadOnly.chat.send(account2, { + content: 'Hello', + type: CONSTANTS.CHAT.MESSAGE_TYPE.TEXT, + }); + } catch (error) { + errorCaught = error; + } + + expect(errorCaught).to.be.an('error'); + expect(errorCaught.message).to.equal( + 'Operation not allowed in read-only mode. Signer is required.' + ); + }); + it('Should decrypt message ', async () => { await userAlice.chat.send(account2, { content: 'Hello', diff --git a/packages/restapi/tests/lib/pushapi/profile.test.ts b/packages/restapi/tests/lib/pushapi/profile.test.ts index ac7094f8c..1d2d9e71f 100644 --- a/packages/restapi/tests/lib/pushapi/profile.test.ts +++ b/packages/restapi/tests/lib/pushapi/profile.test.ts @@ -26,4 +26,29 @@ describe('PushAPI.profile functionality', () => { expect(response.name).to.equal(updatedName); expect(response.desc).to.equal(updatedDesc); }); + + it('Should get profile read only mode', async () => { + const updatedName = 'Bob The Builder'; + const updatedDesc = 'Yes We Can'; + await userAlice.profile.update({ + name: updatedName, + desc: updatedDesc, + }); + + const response = await userAlice.profile.update({ + name: updatedName, + desc: updatedDesc, + }); + expect(response.name).to.equal(updatedName); + expect(response.desc).to.equal(updatedDesc); + + const account = (await userAlice.info()).did; + + const userAliceReadOnly = await PushAPI.initialize({ + account: account, + }); + + expect((await userAliceReadOnly.info()).profile.name).to.equal(updatedName); + expect((await userAliceReadOnly.info()).profile.desc).to.equal(updatedDesc); + }); }); diff --git a/packages/restapi/tests/lib/pushstream/initialize.test.ts b/packages/restapi/tests/lib/pushstream/initialize.test.ts index 0e797f415..6b517c1df 100644 --- a/packages/restapi/tests/lib/pushstream/initialize.test.ts +++ b/packages/restapi/tests/lib/pushstream/initialize.test.ts @@ -392,6 +392,439 @@ describe('PushStream.initialize functionality', () => { + 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);*/ + + let timeoutTriggered = false; + + const timeout = new Promise((_, reject) => { + setTimeout(() => { + timeoutTriggered = true; + reject(new Error('Timeout after 5 seconds')); + }, 5000); + }); + + // Wrap the Promise.allSettled inside a Promise.race with the timeout + try { + const result = await Promise.race([ + Promise.allSettled([ + onDataReceived, + onMessageReceived, + onNoitificationsReceived, + ]), + timeout, + ]); + + if (timeoutTriggered) { + console.error('Timeout reached before events were emitted.'); + } else { + (result as PromiseSettledResult[]).forEach((outcome) => { + if (outcome.status === 'fulfilled') { + //console.log(outcome.value); + } else if (outcome.status === 'rejected') { + console.error(outcome.reason); + } + }); + } + } catch (error) { + console.error(error); + } + }); + + it('Should initialize new stream(readonly) 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: CONSTANTS.ENV.LOCAL, + }); + + const WALLET2 = ethers.Wallet.createRandom(); + const signer2 = new ethers.Wallet(WALLET2.privateKey, provider); + const user2 = await PushAPI.initialize(signer2, { + env: CONSTANTS.ENV.LOCAL, + }); + + const WALLET3 = ethers.Wallet.createRandom(); + const signer3 = new ethers.Wallet(WALLET3.privateKey, provider); + const user3 = await PushAPI.initialize(signer3, { + env: CONSTANTS.ENV.LOCAL, + }); + + const WALLET4 = ethers.Wallet.createRandom(); + const signer4 = new ethers.Wallet(WALLET4.privateKey, provider); + const user4 = await PushAPI.initialize(signer4, { + env: CONSTANTS.ENV.LOCAL, + }); + + const GROUP_RULES = { + entry: { + conditions: [ + { + any: [ + { + type: CONSTANTS.CHAT.GROUP.RULES.CONDITION_TYPE.PUSH, + category: CONSTANTS.CHAT.GROUP.RULES.CATEGORY.CUSTOM_ENDPOINT, + subcategory: CONSTANTS.CHAT.GROUP.RULES.SUBCATEGORY.GET, + data: { + url: 'https://api.ud-staging.com/profile/badges/dead_pixel/validate/{{user_address}}?rule=join', + }, + }, + ], + }, + ], + }, + chat: { + conditions: [ + { + any: [ + { + type: CONSTANTS.CHAT.GROUP.RULES.CONDITION_TYPE.PUSH, + category: CONSTANTS.CHAT.GROUP.RULES.CATEGORY.CUSTOM_ENDPOINT, + subcategory: CONSTANTS.CHAT.GROUP.RULES.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: [], + admins: [], + private: false, + rules: { + chat: { + conditions: { + any: [ + { + type: CONSTANTS.CHAT.GROUP.RULES.CONDITION_TYPE.PUSH, + category: CONSTANTS.CHAT.GROUP.RULES.CATEGORY.ERC20, + subcategory: CONSTANTS.CHAT.GROUP.RULES.SUBCATEGORY.HOLDER, + data: { + contract: + 'eip155:1:0xf418588522d5dd018b425E472991E52EBBeEEEEE', + amount: 1, + decimals: 18, + }, + }, + { + type: CONSTANTS.CHAT.GROUP.RULES.CONDITION_TYPE.PUSH, + category: CONSTANTS.CHAT.GROUP.RULES.CATEGORY.INVITE, + subcategory: CONSTANTS.CHAT.GROUP.RULES.SUBCATEGORY.DEFAULT, + data: { + inviterRoles: [ + CONSTANTS.CHAT.GROUP.RULES.INVITER_ROLE.ADMIN, + CONSTANTS.CHAT.GROUP.RULES.INVITER_ROLE.OWNER, + ], + }, + }, + ], + }, + }, + }, + }; + + const CREATE_GROUP_REQUEST_2 = { + description: 'test', + image: 'test', + members: [], + admins: [], + private: false, + rules: {}, + }; + + /*const stream = await user.stream( + [CONSTANTS.STREAM.CHAT, CONSTANTS.STREAM.CHAT_OPS], + { + // stream supports other products as well, such as STREAM.CHAT, STREAM.CHAT_OPS + // more info can be found at push.org/docs/chat + + filter: { + channels: ['*'], + chats: ['*'], + }, + connection: { + auto: false, // should connection be automatic, else need to call stream.connect(); + retries: 3, // number of retries in case of error + }, + raw: true, // enable true to show all data + } + ); + + await stream.connect();*/ + + // Initialize wallet user, pass 'prod' instead of 'staging' for mainnet apps + const userAlice = await PushAPI.initialize(signer, { + env: CONSTANTS.ENV.STAGING, + }); + + const account = (await userAlice.info()).did; + + const userAliceReadOnly = await PushAPI.initialize({ + account: account, + }); + + // This will be the wallet address of the recipient + const pushAIWalletAddress = '0x99A08ac6254dcf7ccc37CeC662aeba8eFA666666'; + + // Listen for stream + // Checkout all chat stream listen options - https://push.org/docs/chat/build/stream-chats/ + // Alternatively, just initialize userAlice.stream.initialize() without any listen options to listen to all events + const stream = await userAliceReadOnly.initStream( + [ + CONSTANTS.STREAM.CHAT, + CONSTANTS.STREAM.CHAT_OPS, + CONSTANTS.STREAM.NOTIF, + CONSTANTS.STREAM.CONNECT, + CONSTANTS.STREAM.DISCONNECT, + ], + {} + ); + + stream.on(CONSTANTS.STREAM.CONNECT, (a) => { + console.log('Stream Connected'); + + // Send a message to Bob after socket connection so that messages as an example + console.log('Sending message to PushAI Bot'); + userAlice.chat.send(pushAIWalletAddress, { + content: "Gm gm! It's a me... Mario", + }); + }); + + await stream.connect(); + + stream.on(CONSTANTS.STREAM.DISCONNECT, () => { + console.log('Stream Disconnected'); + }); + + // React to message payload getting recieved + stream.on(CONSTANTS.STREAM.CHAT, (message) => { + console.log('Encrypted Message Received'); + console.log(message); + stream.disconnect(); + }); + + const createEventPromise = ( + expectedEvent: string, + eventType: string, + expectedEventCount: number + ) => { + return new Promise((resolve, reject) => { + let eventCount = 0; + if (expectedEventCount == 0) { + resolve('Done'); + } + const receivedEvents: any[] = []; + stream.on(eventType, (data: any) => { + try { + receivedEvents.push(data); + eventCount++; + + console.log( + `Event ${eventCount} for ${expectedEvent}:`, + util.inspect(JSON.stringify(data), { + showHidden: false, + depth: null, + colors: true, + }) + ); + expect(data).to.not.be.null; + + if (eventCount === expectedEventCount) { + resolve(receivedEvents); + } + } catch (error) { + console.error('An error occurred:', error); + reject(error); + } + }); + }); + }; + + // leave admin bug + // group creator check remove add + + const onDataReceived = createEventPromise( + 'CHAT_OPS', + CONSTANTS.STREAM.CHAT_OPS, + 5 + ); + const onMessageReceived = createEventPromise( + 'CHAT', + CONSTANTS.STREAM.CHAT, + 4 + ); + const onNoitificationsReceived = createEventPromise( + 'NOTIF', + CONSTANTS.STREAM.NOTIF, + 4 + ); + + // Create and update group + const createdGroup = await user.chat.group.create( + 'test', + CREATE_GROUP_REQUEST_2 + ); + + const updatedGroup = await user.chat.group.update(createdGroup.chatId, { + description: 'Updated Description', + }); + + const updatedGroup2 = await user.chat.group.add(createdGroup.chatId, { + role: 'ADMIN', + accounts: [signer2.address, signer3.address, signer4.address], + }); + + const w2wRejectRequest = await user2.chat.group.join(createdGroup.chatId); + + /*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 channelPrivateKey = process.env['WALLET_PRIVATE_KEY']; + + const signerChannel = new ethers.Wallet(`0x${channelPrivateKey}`); + const channelAddress = signerChannel.address; + + console.log(channelAddress); + + const response = await subscribe({ + signer: signer, + channelAddress: `eip155:5:${channelAddress}`, // channel address in CAIP + userAddress: `eip155:5:${signer.address}`, // user address in CAIP + onSuccess: () => { + console.log('opt in success'); + }, + onError: () => { + console.error('opt in error'); + }, + env: ENV.LOCAL, + }); + + + const apiResponse = await sendNotification({ + signer: signerChannel, // Needs to resolve to channel address + type: 1, // broadcast + identityType: 2, // direct payload + notification: { + title: `notification TITLE:`, + body: `notification BODY`, + }, + payload: { + title: `payload title`, + body: `sample msg body`, + cta: '', + img: '', + }, + channel: `eip155:5:${channelAddress}`, // your channel address + env: ENV.LOCAL, + }); + + + const response2 = await unsubscribe({ + signer: signer, + channelAddress: `eip155:5:${channelAddress}`, // channel address in CAIP + userAddress: `eip155:5:${signer.address}`, // user address in CAIP + onSuccess: () => { + console.log('opt out success'); + }, + onError: () => { + console.error('opt out error'); + }, + env: ENV.LOCAL, + }); + + + const apiResponse2 = await sendNotification({ + signer: signerChannel, // Needs to resolve to channel address + type: 3, // broadcast + identityType: 2, // direct payload + notification: { + title: `notification TITLE:`, + body: `notification BODY`, + }, + payload: { + title: `payload title`, + body: `sample msg body`, + cta: '', + img: '', + }, + recipients: `eip155:5:${signer.address}`, + channel: `eip155:5:${channelAddress}`, // your channel address + env: ENV.LOCAL, + }); + + + + //const w2wRejectRequest = await user2.chat.group.join(createdGroup.chatId); + //const updatedGroup2 = await user2.chat.group.leave(createdGroup.chatId); + + /*const updatedGroup3 = await user.chat.group.add(createdGroup.chatId, { + role: 'ADMIN', + accounts: [signer2.address], + }); + + + + const w2wAcceptsRequest = await user2.chat.group.join(createdGroup.chatId); + + /* const updatedGroup4 = await user.chat.group.add( + createdGroup.chatId, + { + role: 'ADMIN', + accounts: [signer3.address], + } + );*/ + + /*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 w2wRejectRequest = await user2.chat.group.join(createdGroup.chatId); + + /* + + + const updatedGroup2 = await user2.chat.group.leave(createdGroup.chatId); + + + + const updatedGroup = await user.chat.group.update(createdGroup.chatId, { + description: 'Updated Description', + }); + + const groupMessageResponse = await user.chat.send(createdGroup.chatId, { + content: 'Hello', + type: MessageType.TEXT, + }); + + + const w2wMessageResponse2 = await user2.chat.send(signer.address, { content: MESSAGE, });