Skip to content
Open
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e4b74e1
Updated documentation for chat-members plugin, adding new sections ab…
rayz1065 Jan 22, 2025
639036a
Improve documentation of chat-members based on suggestions
rayz1065 Jan 24, 2025
7032e84
Add diagram to chat-members plugin, showing the statuses correspondin…
rayz1065 Jan 24, 2025
4d41b1a
Merge branch 'main' into chat-member-filters
LWJerri Jan 27, 2025
8abdf8e
Updated chat-members documentation, adding section about hydration
rayz1065 Feb 6, 2025
808938c
Apply suggestions from code review
rayz1065 Feb 14, 2025
c72c38b
Merge branch 'main' into chat-member-filters
LWJerri Feb 17, 2025
aa44ba2
Apply suggestions from code review
rayz1065 Feb 19, 2025
1dec670
Chat members plugin, include DEFAULT_UPDATE_TYPES where they were mis…
rayz1065 Feb 19, 2025
69159ff
Chat members, added example showcasing filtering without the plugin
rayz1065 Feb 19, 2025
4e9b995
Chat members, apply suggested rephrasing from code review
rayz1065 Feb 19, 2025
73149ad
Merge branch 'main' into chat-member-filters
rojvv Mar 31, 2025
5a9b127
Merge branch 'main' into chat-member-filters
MasedMSD Apr 12, 2025
370b223
sync to Ukrainian
niusia-ua Apr 12, 2025
57a1f81
Apply suggestions from code review
rayz1065 Apr 12, 2025
50c14bd
Chat members, clean up string composition
rayz1065 Apr 12, 2025
6a0d5ea
[uk]: correct apostrophes
niusia-ua Apr 13, 2025
d4d02e8
Apply suggestions from code review
LWJerri Apr 13, 2025
9741d77
Merge branch 'main' into chat-member-filters
LWJerri Apr 19, 2025
be00ce1
sync with indonesia
ppabcd Apr 21, 2025
b2d61d6
Merge branch 'main' into chat-member-filters
LWJerri May 5, 2025
cfd8b31
Add Spanish
habemuscode Jun 21, 2025
1af712d
Remove non-needed spaces
habemuscode Jun 21, 2025
c3c0050
Merge branch 'main' into chat-member-filters
LWJerri Jul 17, 2025
4a6be97
Merge branch 'main' into chat-member-filters
MasedMSD Aug 16, 2025
93f9788
Merge branch 'main' into chat-member-filters
LWJerri Aug 23, 2025
db0f788
sync with bahasa indonesia
ppabcd Sep 3, 2025
c9d5cd7
Merge branch 'main' into chat-member-filters
LWJerri Sep 3, 2025
4bd0dbe
Merge branch 'main' into chat-member-filters
LWJerri Sep 12, 2025
da78d08
Merge branch 'main' into chat-member-filters
LWJerri Oct 12, 2025
c67e2ba
sync zh #1181
Nov 3, 2025
b2a4c48
Merge branch 'main' into chat-member-filters
KnorpelSenf Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 217 additions & 5 deletions site/docs/plugins/chat-members.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,230 @@ next: false

# Chat Members Plugin (`chat-members`)

Automatically store information about users in a chat and retrieve it easily.
Track group and channel members, and list them.
This plugin makes it easy to work with `ChatMember` objects, by offering a convenient way to listen for changes in the form of custom filters, and by storing and updating the objects.

## Introduction

In many situations, it is necessary for a bot to have information about all the users of a given chat.
Currently, though, the Telegram Bot API exposes no method that allows us to retrieve this information.
Working with `ChatMember` objects from the Telegram Bot API can sometimes be cumbersome, there are several different statuses that are often interchangeable in most applications, as well as a restricted status that can represent both members of the group and restricted users that are not in the group.

This plugin comes to the rescue: automatically listening to `chat_member` events and storing all `ChatMember` objects.
This plugin aims to simplify dealing with chat members, by offering fully typed filters for chat member updates.

## Usage

### Chat member filters

You can listen for two kinds of updates regarding chat members using a Telegram bot: `chat_member` and `my_chat_member`,
both of them specify the old and new status of the user.

- `my_chat_member` updates are received by your bot by default and they inform you about the status of the bot being updated in any chat, as well as users blocking the bot;
- `chat_member` updates are only received if you specifically include them in the list of allowed updates, they notify about any status changes for users in chats **where your bot is admin**.

Instead of manually filtering the old and new status, chat member filters do this automatically for you, allowing you to react to every type of transition you're interested in.
Within the handler, types of `old_chat_member` and `new_chat_member` are updated accordingly.

::: code-group

```ts [TypeScript]
import { API_CONSTANTS, Bot } from "grammy";
import { chatMemberFilter, myChatMemberFilter } from "@grammyjs/chat-members";

const bot = new Bot(process.env.BOT_TOKEN!);
const groups = bot.chatType(["group", "supergroup"]);

groups.filter(myChatMemberFilter("out", "regular"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group!");
});

groups.filter(myChatMemberFilter("out", "admin"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group as admin!");
});

groups.filter(myChatMemberFilter("regular", "admin"), async (ctx) => {
await ctx.reply("I was promoted to admin!");
});

groups.filter(myChatMemberFilter("admin", "regular"), async (ctx) => {
await ctx.reply("I am no longer admin");
});

groups.filter(chatMemberFilter("out", "in"), async (ctx) => {
const user = ctx.chatMember.new_chat_member.user;
await ctx.reply(`Welcome ${user.first_name} to the group!`);
});

bot.start({
// Make sure to specify the desired update types
allowed_updates: [...API_CONSTANTS.DEFAULT_UPDATE_TYPES, "chat_member"],
});
```

```js [JavaScript]
import { API_CONSTANTS, Bot } from "grammy";
import { chatMemberFilter, myChatMemberFilter } from "@grammyjs/chat-members";

const bot = new Bot(process.env.BOT_TOKEN);
const groups = bot.chatType(["group", "supergroup"]);

groups.filter(myChatMemberFilter("out", "regular"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group!");
});

groups.filter(myChatMemberFilter("out", "admin"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group as admin!");
});

groups.filter(myChatMemberFilter("regular", "admin"), async (ctx) => {
await ctx.reply("I was promoted to admin!");
});

groups.filter(myChatMemberFilter("admin", "regular"), async (ctx) => {
await ctx.reply("I am no longer admin");
});

groups.filter(chatMemberFilter("out", "in"), async (ctx) => {
const user = ctx.chatMember.new_chat_member.user;
await ctx.reply(`Welcome ${user.first_name} to the group!`);
});

bot.start({
// Make sure to specify the desired update types
allowed_updates: [...API_CONSTANTS.DEFAULT_UPDATE_TYPES, "chat_member"],
});
```

```ts [Deno]
import { API_CONSTANTS, Bot } from "https://deno.land/x/grammy/mod.ts";
import {
chatMemberFilter,
myChatMemberFilter,
} from "https://deno.land/x/grammy_chat_members/mod.ts";

const bot = new Bot(Deno.env.get("BOT_TOKEN")!);
const groups = bot.chatType(["group", "supergroup"]);

groups.filter(myChatMemberFilter("out", "regular"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group!");
});

groups.filter(myChatMemberFilter("out", "admin"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group as admin!");
});

groups.filter(myChatMemberFilter("regular", "admin"), async (ctx) => {
await ctx.reply("I was promoted to admin!");
});

groups.filter(myChatMemberFilter("admin", "regular"), async (ctx) => {
await ctx.reply("I am no longer admin");
});

groups.filter(chatMemberFilter("out", "in"), async (ctx) => {
const user = ctx.chatMember.new_chat_member.user;
await ctx.reply(`Welcome ${user.first_name} to the group!`);
});

bot.start({
// Make sure to specify the desired update types
allowed_updates: [...API_CONSTANTS.DEFAULT_UPDATE_TYPES, "chat_member"],
});
```

:::

Filters include the regular Telegram statuses (owner, administrator, member, restricted, left, kicked) and some additional ones for convenience:

- `restricted_in`: a member of the chat with restrictions;
- `restricted_out`: not a member of the chat, has restrictions;
- `in`: a member of the chat (administrator, creator, member, restricted_in);
- `out`: not a member of the chat (left, kicked, restricted_out);
- `free`: a member of the chat that isn't restricted (administrator, creator, member);
- `admin`: an admin of the chat (administrator, creator);
- `regular`: a non-admin member of the chat (member, restricted_in).

You can create your custom groupings of chat member types by passing an array instead of a string:

```typescript
groups.filter(
chatMemberFilter(["restricted", "kicked"], ["free", "left"]),
async (ctx) => {
const from = ctx.from;
const { status: oldStatus, user } = ctx.chatMember.old_chat_member;
await ctx.reply(
`${from.first_name} lifted ` +
`${oldStatus === "kicked" ? "ban" : "restrictions"} ` +
`from ${user.first_name}`,
);
},
);
```

#### Example usage

The best way to use the filters is to pick a set of relevant statuses, for example 'out', 'regular' and 'admin', then
make a table of the transitions between them:

| ↱ | Out | Regular | Admin |
| ----------- | ----------- | -------------------- | ------------------- |
| **Out** | ban-changed | join | join-and-promoted |
| **Regular** | exit | restrictions-changed | promoted |
| **Admin** | exit | demoted | permissions-changed |

Assign a listener to all the transitions that are relevant to your use-case.

Combine these filters with `bot.chatType` to only listen for transitions for a specific type of chat. Add a middleware to listen to all updates as a way to perform common operations (like updating your database) before handing off control to a specific handler.

```typescript
const groups = bot.chatType(["group", "supergroup"]);

groups.on("chat_member", (ctx, next) => {
// ran on all updates of type chat_member
const {
old_chat_member: { status: oldStatus },
new_chat_member: { user, status },
from,
chat,
} = ctx.chatMember;
console.log(
`In group ${chat.id} user ${from.id} changed status of ${user.id}:`,
`${oldStatus} -> ${status}`,
);

// update database data here

return next();
});

// specific handlers

groups.filter(chatMemberFilter("out", "in"), async (ctx, next) => {
const { new_chat_member: { user } } = ctx.chatMember;
await ctx.reply(`Welcome ${user.first_name}!`);
});
```

### Status checking utility

The `chatMemberIs` utility function can be useful whenever you want to use filtering logic within a handler.
It takes as input any of the regular and custom statuses (or an array of them) and updates the type of the passed variable.

```ts
bot.callbackQuery("foo", async (ctx) => {
const chatMember = await ctx.getChatMember(ctx.from.id);

if (!chatMemberIs(chatMember, "free")) {
chatMember.status; // "restricted" | "left" | "kicked"
await ctx.answerCallbackQuery({
show_alert: true,
text: "You don't have permission to do this!",
});
return;
}

chatMember.status; // "creator" | "administrator" | "member"
await ctx.answerCallbackQuery("bar");
});
```

### Storing Chat Members

You can use a valid grammY [storage adapter](./session#known-storage-adapters) or an instance of any class that implements the [`StorageAdapter`](/ref/core/storageadapter) interface.
Expand Down
Loading