find_members_without_direct_chat
Discover members in your WhatsApp groups that you haven't chatted with directly. Supports scanning multiple groups, live metadata refresh, and filtering by shared group count.
Instructions
Find group members that do not have a direct chat with you.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| group_limit | No | How many groups to scan | |
| refresh_group_info | No | Fetch live group metadata before analysis | |
| min_shared_groups | No | Only include members present in at least this many groups |
Implementation Reference
- src/services/whatsapp.ts:2911-2943 (handler)The core implementation of findMembersWithoutDirectChat in WhatsAppService. Builds a group audit matrix, then filters members who do NOT have a direct chat and meet the min_shared_groups threshold.
async findMembersWithoutDirectChat( groupLimit = 200, refreshGroupInfo = false, minSharedGroups = 1, ): Promise<{ groupsProcessed: number; totalMembers: number; members: Array<{ canonicalId: string; ids: string[]; name: string | null; pushname: string | null; number: string | null; groupCount: number; groups: Array<{ id: string; name: string }>; hasDirectChat: boolean; inContacts: boolean; }>; }> { const matrix = await this.buildGroupAuditMatrix( groupLimit, refreshGroupInfo, ); const threshold = Math.max(1, minSharedGroups); const members = matrix.members.filter( (m) => !m.hasDirectChat && m.groupCount >= threshold, ); return { groupsProcessed: matrix.groupsProcessed, totalMembers: matrix.members.length, members, }; } - src/tools/chats.ts:364-383 (schema)Zod schema definitions for the tool's input parameters: group_limit (optional, default 200), refresh_group_info (optional, default false), min_shared_groups (optional, default 1).
group_limit: z .number() .int() .positive() .optional() .default(200) .describe("How many groups to scan"), refresh_group_info: z .boolean() .optional() .default(false) .describe("Fetch live group metadata before analysis"), min_shared_groups: z .number() .int() .positive() .optional() .default(1) .describe("Only include members present in at least this many groups"), }, - src/tools/chats.ts:360-410 (registration)Tool registration via server.tool() in the registerChatTools function, which wires the tool name, description, schema, and handler that delegates to whatsappService.findMembersWithoutDirectChat.
server.tool( "find_members_without_direct_chat", "Find group members that do not have a direct chat with you.", { group_limit: z .number() .int() .positive() .optional() .default(200) .describe("How many groups to scan"), refresh_group_info: z .boolean() .optional() .default(false) .describe("Fetch live group metadata before analysis"), min_shared_groups: z .number() .int() .positive() .optional() .default(1) .describe("Only include members present in at least this many groups"), }, async ({ group_limit, refresh_group_info, min_shared_groups, }): Promise<CallToolResult> => { try { const result = await whatsappService.findMembersWithoutDirectChat( group_limit, refresh_group_info, min_shared_groups, ); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } catch (error: any) { log.error("Error in find_members_without_direct_chat tool:", error); return { content: [ { type: "text", text: `Error finding members without direct chat: ${error?.message || String(error)}`, }, ], isError: true, }; } }, - src/services/whatsapp.ts:2777-2877 (helper)The buildGroupAuditMatrix helper that enumerates groups, fetches participants, builds a member map with group counts, and enriches each member with hasDirectChat and inContacts flags.
private async buildGroupAuditMatrix( groupLimit = 200, refreshGroupInfo = false, ): Promise<{ groupsProcessed: number; members: Array<{ canonicalId: string; ids: string[]; name: string | null; pushname: string | null; number: string | null; groupCount: number; groups: Array<{ id: string; name: string }>; hasDirectChat: boolean; inContacts: boolean; }>; }> { let groups: SimpleChat[] = []; try { groups = await this.listGroups(groupLimit, false); } catch (error) { log.warn( { err: error, groupLimit, refreshGroupInfo }, "Group audit: failed to list groups", ); return { groupsProcessed: 0, members: [], }; } const memberMap = new Map< string, { canonicalId: string; ids: Set<string>; groups: Array<{ id: string; name: string }>; } >(); for (const group of groups) { const groupId = String(group.id || "").trim(); if (!groupId.endsWith("@g.us")) continue; let participants: string[] = []; try { participants = await this.getGroupParticipantJids( groupId, refreshGroupInfo, ); } catch (error) { log.warn( { err: error, groupId, refreshGroupInfo }, "Group audit: failed to load participants", ); continue; } for (const participantRaw of participants) { const participant = this.normalizeJid(participantRaw); if (!participant) continue; const canonicalId = this.resolveCanonicalChatId(participant); const existing = memberMap.get(canonicalId) || { canonicalId, ids: new Set<string>(), groups: [], }; existing.ids.add(participant); if (!existing.groups.some((g) => g.id === groupId)) { existing.groups.push({ id: groupId, name: group.name || groupId, }); } memberMap.set(canonicalId, existing); } } const members = Array.from(memberMap.values()) .map((entry) => { const profile = this.buildMemberDisplay(entry.canonicalId); return { canonicalId: entry.canonicalId, ids: Array.from(entry.ids.values()).sort(), name: profile.name, pushname: profile.pushname, number: profile.number, groupCount: entry.groups.length, groups: entry.groups.sort((a, b) => a.name.localeCompare(b.name)), hasDirectChat: this.hasDirectChatForParticipant(entry.canonicalId), inContacts: this.isParticipantInContacts(entry.canonicalId), }; }) .sort( (a, b) => b.groupCount - a.groupCount || String(a.name || "").localeCompare(String(b.name || "")), ); return { groupsProcessed: groups.length, members, }; } - src/services/whatsapp.ts:2715-2725 (helper)The hasDirectChatForParticipant helper that checks if a participant has a direct (non-group) chat by looking up related JIDs in the store.
private hasDirectChatForParticipant(jid: string): boolean { if (!this.storeService || !jid) return false; const related = this.getRelatedJids(jid); for (const entry of related) { const chat = this.storeService.getChatById(entry); if (chat && !chat.is_group) { return true; } } return false; }