diff --git a/SKILL.md b/SKILL.md index b533e97..756f04a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -86,6 +86,8 @@ running — check `tgcli backfill status`). ```bash tgcli contacts search "alex" tgcli groups list --query "Nha Trang" +tgcli groups members list --chat @mygroup # list all members +tgcli groups members list --chat @mygroup --search "alex" # filter by name/username ``` ## Output Formats diff --git a/cli.js b/cli.js index 6d5c210..0707dc7 100755 --- a/cli.js +++ b/cli.js @@ -534,6 +534,13 @@ function buildProgram() { .option('--chat ', 'Group identifier') .option('--user ', 'User id', collectList) .action(withGlobalOptions((globalFlags, options) => runGroupMembersRemove(globalFlags, options))); + groupMembers + .command('list') + .description('List group members') + .option('--chat ', 'Group identifier') + .option('--limit ', 'Max members to fetch') + .option('--search ', 'Filter members by name or username') + .action(withGlobalOptions((globalFlags, options) => runGroupMembersList(globalFlags, options))); const groupInvite = groups.command('invite').description('Manage invite links'); groupInvite .command('get') @@ -3184,6 +3191,31 @@ async function runGroupMembersRemove(globalFlags, options = {}) { } } +async function runGroupMembersList(globalFlags, options = {}) { + if (!options.chat) { + throw new Error('--chat is required'); + } + const members = await runOperation(globalFlags, { + op: 'groupMembersList', + args: { + chat: options.chat, + limit: parsePositiveInt(options.limit, '--limit'), + search: options.search, + }, + }); + if (globalFlags.json) { + writeJson(members); + } else { + for (const member of members) { + const handle = member.username ? `@${member.username}` : member.userId; + const flags = [member.status, member.isBot ? 'bot' : null] + .filter(Boolean) + .join(', '); + console.log(`${member.name}\t${handle}\t${member.userId}${flags ? `\t(${flags})` : ''}`); + } + } +} + async function runGroupInviteLinkGet(globalFlags, options = {}) { if (!options.chat) { throw new Error('--chat is required'); diff --git a/core/operations.js b/core/operations.js index b7855b7..247814e 100644 --- a/core/operations.js +++ b/core/operations.js @@ -852,6 +852,14 @@ async function groupMembersRemove(ctx, args = {}) { return { channelId: args.chat, ...result }; } +// args: { chat, limit?, search? }. Lists group members. +async function groupMembersList(ctx, args = {}) { + return ctx.telegramClient.getGroupMembers(args.chat, { + limit: args.limit, + search: args.search, + }); +} + // args: { chat }. Returns the primary invite-link descriptor. async function getGroupInviteLink(ctx, args = {}) { return ctx.telegramClient.getGroupInviteLink(args.chat); @@ -967,6 +975,7 @@ export const OPERATIONS = { groupsRename, groupMembersAdd, groupMembersRemove, + groupMembersList, getGroupInviteLink, revokeGroupInviteLink, groupsJoin, diff --git a/docs/cli.md b/docs/cli.md index 9cb93b3..7e1c781 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -193,6 +193,7 @@ Legacy `--offset-id` is accepted as a hidden alias for `--before-id`. - groups rename --chat --name "New Name" - groups members add --chat --user [--user ...] - groups members remove --chat --user [--user ...] +- groups members list --chat [--limit ] [--search ] - groups invite get --chat - groups invite revoke --chat - groups join --code diff --git a/docs/mcp-tools.md b/docs/mcp-tools.md index 7182337..784e012 100644 --- a/docs/mcp-tools.md +++ b/docs/mcp-tools.md @@ -189,6 +189,10 @@ This document defines the consolidated MCP tool surface. The goal is fewer tools - Params: channelId (required), userIds (array, required) - Output: { channelId, removed, failed }. +### groupsMembersList +- Params: channelId (required), limit (optional, default 200), search (optional) +- Output: array of { userId, username, name, isBot, status }. + ### groupsInviteLinkGet - Params: channelId (required) - Output: invite link metadata. diff --git a/mcp-server.js b/mcp-server.js index 3b24295..505cac3 100644 --- a/mcp-server.js +++ b/mcp-server.js @@ -586,6 +586,20 @@ const groupsMembersRemoveSchema = { .describe("User IDs or usernames to remove"), }; +const groupsMembersListSchema = { + channelId: channelIdSchema.describe("Group ID or username"), + limit: z + .number({ invalid_type_error: "limit must be a number" }) + .int() + .positive() + .optional() + .describe("Maximum number of members to fetch (default 200)"), + search: z + .string({ invalid_type_error: "search must be a string" }) + .optional() + .describe("Filter members by name or username"), +}; + const groupsInviteLinkGetSchema = { channelId: channelIdSchema.describe("Group ID or username"), }; @@ -1351,6 +1365,29 @@ function createServerInstance() { }, ); + server.tool( + "groupsMembersList", + "Lists members of a group, optionally filtered by name or username.", + groupsMembersListSchema, + async ({ channelId, limit, search }) => { + await ensureTelegramConnected(); + const members = await OPERATIONS.groupMembersList(warmServices, { + chat: channelId, + limit, + search, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(members, null, 2), + }, + ], + }; + }, + ); + server.tool( "groupsInviteLinkGet", "Gets the primary invite link for a group.", diff --git a/telegram-client.js b/telegram-client.js index 7d7ed94..6f21a64 100644 --- a/telegram-client.js +++ b/telegram-client.js @@ -1367,6 +1367,40 @@ class TelegramClient { return { removed, failed }; } + async getGroupMembers(channelId, options = {}) { + await this.ensureLogin(); + const peerRef = normalizeChannelId(channelId); + const limit = options.limit ?? 200; + const query = (options.search ?? options.query ?? '').trim(); + const needle = query.toLowerCase(); + const members = []; + // Pass the query server-side when supported; also filter client-side so the + // search works reliably regardless of backend participant-search behaviour. + for await (const member of this.client.iterChatMembers(peerRef, { + query: query || undefined, + })) { + const user = member.user ?? {}; + const name = + user.displayName || + [user.firstName, user.lastName].filter(Boolean).join(' ') || + 'Unknown'; + const entry = { + userId: user.id?.toString?.() ?? null, + username: user.username ?? null, + name, + isBot: typeof user.isBot === 'boolean' ? user.isBot : null, + status: typeof member.status === 'string' ? member.status : null, + }; + if (needle) { + const haystack = `${entry.name} ${entry.username ?? ''}`.toLowerCase(); + if (!haystack.includes(needle)) continue; + } + members.push(entry); + if (members.length >= limit) break; + } + return members; + } + async getGroupInviteLink(channelId) { await this.ensureLogin(); const peerRef = normalizeChannelId(channelId);