diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index 498d502edd..657c6e05f2 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -24,7 +24,7 @@ "docs": "typedoc" }, "dependencies": { - "@simplex-chat/types": "^0.3.0", + "@simplex-chat/types": "^0.4.0", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.3", "node-addon-api": "^8.5.0" diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index f5d2a5168e..8bc56db41c 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -872,4 +872,34 @@ export class ChatApi { const r = await this.sendChatCmd(CC.APISetContactPrefs.cmdString({contactId, preferences})) if (r.type !== "contactPrefsUpdated") throw new ChatCommandError("error setting contact prefs", r) } + + /** + * Create a direct message contact with a group member. + * Returns the created contact. + * Network usage: interactive. + */ + async apiCreateMemberContact(groupId: number, groupMemberId: number): Promise { + const r: any = await this.sendChatCmd(`/_create member contact #${groupId} ${groupMemberId}`) + if (r.type === "newMemberContact") return r.contact + throw new ChatCommandError("error creating member contact", r) + } + + /** + * Send a direct message invitation to a group member contact. + * The contact must have been created with {@link apiCreateMemberContact}. + * Network usage: interactive. + */ + async apiSendMemberContactInvitation(contactId: number, message?: T.MsgContent | string): Promise { + let cmd = `/_invite member contact @${contactId}` + if (message !== undefined) { + if (typeof message === "string") { + cmd += ` text ${message}` + } else { + cmd += ` json ${JSON.stringify(message)}` + } + } + const r: any = await this.sendChatCmd(cmd) + if (r.type === "newMemberContactSentInv") return r.contact + throw new ChatCommandError("error sending member contact invitation", r) + } } diff --git a/packages/simplex-chat-nodejs/tests/api.test.ts b/packages/simplex-chat-nodejs/tests/api.test.ts index 52153ecfed..7bc1a89b86 100644 --- a/packages/simplex-chat-nodejs/tests/api.test.ts +++ b/packages/simplex-chat-nodejs/tests/api.test.ts @@ -64,4 +64,89 @@ describe("API tests (use preset servers)", () => { expect(servers[0] !== servers[1]).toBe(true) expect(eventCount > 0).toBe(true) }, 30000) + + it("should create member contact and send invitation", async () => { + // create 3 users and start chat controllers + const alice = await api.ChatApi.init(alicePath) + const bob = await api.ChatApi.init(bobPath) + const carolPath = path.join(tmpDir, "carol") + const carol = await api.ChatApi.init(carolPath) + const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""}) + await bob.apiCreateActiveUser({displayName: "bob", fullName: ""}) + await carol.apiCreateActiveUser({displayName: "carol", fullName: ""}) + await alice.startChat() + await bob.startChat() + await carol.startChat() + // connect alice <-> bob + const aliceLink1 = await alice.apiCreateLink(aliceUser.userId) + await expect(bob.apiConnectActiveUser(aliceLink1)).resolves.toBe(api.ConnReqType.Invitation) + const [bobContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await bob.wait("contactConnected")).contact + ]) + // connect alice <-> carol + const aliceLink2 = await alice.apiCreateLink(aliceUser.userId) + await expect(carol.apiConnectActiveUser(aliceLink2)).resolves.toBe(api.ConnReqType.Invitation) + const [carolContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await carol.wait("contactConnected")).contact + ]) + // create group with direct messages enabled + const group = await alice.apiNewGroup(aliceUser.userId, { + displayName: "test-group", + fullName: "", + groupPreferences: { + directMessages: {enable: T.GroupFeatureEnabled.On}, + }, + }) + const groupId = group.groupId + // add bob to the group + const bobInvP = bob.wait("receivedGroupInvitation", 15000) + await alice.apiAddMember(groupId, bobContact.contactId, T.GroupMemberRole.Member) + const bobInvEvt = await bobInvP + expect(bobInvEvt).toBeDefined() + const aliceBobConnP = alice.wait("connectedToGroupMember", 15000) + const bobAliceConnP = bob.wait("connectedToGroupMember", 15000) + await bob.apiJoinGroup(bobInvEvt!.groupInfo.groupId) + await Promise.all([aliceBobConnP, bobAliceConnP]) + // add carol to the group + const carolInvP = carol.wait("receivedGroupInvitation", 30000) + await alice.apiAddMember(groupId, carolContact.contactId, T.GroupMemberRole.Member) + const carolInvEvt = await carolInvP + expect(carolInvEvt).toBeDefined() + // wait for carol to connect to both alice and bob (and vice versa) + const bobCarolConnP = bob.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000) + const carolAliceConnP = carol.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "alice", 30000) + const carolBobConnP = carol.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "bob", 30000) + const aliceCarolConnP = alice.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000) + await carol.apiJoinGroup(carolInvEvt!.groupInfo.groupId) + await Promise.all([bobCarolConnP, carolAliceConnP, carolBobConnP, aliceCarolConnP]) + // find carol's memberId from bob's perspective + const members = await bob.apiListMembers(groupId) + const carolMember = members.find(m => m.memberProfile.displayName === "carol") + expect(carolMember).toBeDefined() + // test apiCreateMemberContact + const dmContact = await bob.apiCreateMemberContact(groupId, carolMember!.groupMemberId) + expect(dmContact).toBeDefined() + expect(dmContact.contactId).toBeDefined() + // test apiSendMemberContactInvitation + const carolDmP = carol.wait("newMemberContactReceivedInv" as CEvt.Tag, 30000) + const invContact = await bob.apiSendMemberContactInvitation(dmContact.contactId, "hello from bob") + expect(invContact).toBeDefined() + // carol should receive the member contact invitation + const carolDmEvt = await carolDmP + expect(carolDmEvt).toBeDefined() + expect((carolDmEvt as any).contact).toBeDefined() + // cleanup + await alice.stopChat() + await bob.stopChat() + await carol.stopChat() + await alice.close() + await bob.close() + await carol.close() + }, 90000) })