Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
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
251 changes: 243 additions & 8 deletions site/docs/plugins/chat-members.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,253 @@ 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.
Telegram doesn't offer a method in the Bot API to retrieve the members of a chat, you have to keep track of them yourself.
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.
In addition, the restricted status is ambiguous because it 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 simplifies 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 explicitly 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, the types of `old_chat_member` and `new_chat_member` are narrowed down accordingly.

::: code-group

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

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

// Listen for updates where the bot is added to a group as a regular user.
groups.filter(myChatMemberFilter("out", "regular"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group!");
});

// Listen for updates where the bot is added to a group as an admin.
groups.filter(myChatMemberFilter("out", "admin"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group as admin!");
});

// Listen for updates where the bot is promoted to admin.
groups.filter(myChatMemberFilter("regular", "admin"), async (ctx) => {
await ctx.reply("I was promoted to admin!");
});

// Listen for updates where the bot is demoted to a regular user.
groups.filter(myChatMemberFilter("admin", "regular"), async (ctx) => {
await ctx.reply("I am no longer admin");
});

// Listen for updates where a user joins a group where your bot is 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("");
const groups = bot.chatType(["group", "supergroup"]);

// Listen for updates where the bot is added to a group as a regular user.
groups.filter(myChatMemberFilter("out", "regular"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group!");
});

// Listen for updates where the bot is added to a group as an admin.
groups.filter(myChatMemberFilter("out", "admin"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group as admin!");
});

// Listen for updates where the bot is promoted to admin.
groups.filter(myChatMemberFilter("regular", "admin"), async (ctx) => {
await ctx.reply("I was promoted to admin!");
});

// Listen for updates where the bot is demoted to a regular user.
groups.filter(myChatMemberFilter("admin", "regular"), async (ctx) => {
await ctx.reply("I am no longer admin");
});

// Listen for updates where a user joins a group where your bot is 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("");
const groups = bot.chatType(["group", "supergroup"]);

// Listen for updates where the bot is added to a group as a regular user.
groups.filter(myChatMemberFilter("out", "regular"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group!");
});

// Listen for updates where the bot is added to a group as an admin.
groups.filter(myChatMemberFilter("out", "admin"), async (ctx) => {
await ctx.reply("Hello, thank you for adding me to the group as admin!");
});

// Listen for updates where the bot is promoted to admin.
groups.filter(myChatMemberFilter("regular", "admin"), async (ctx) => {
await ctx.reply("I was promoted to admin!");
});

// Listen for updates where the bot is demoted to a regular user.
groups.filter(myChatMemberFilter("admin", "regular"), async (ctx) => {
await ctx.reply("I am no longer admin");
});

// Listen for updates where a user joins a group where your bot is 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).

To summarize, here is a diagram showing what each query corresponds to:

![Diagram showing the statuses corresponding to each query.](/images/chat-members-statuses.svg)

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", async (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

await 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 All @@ -40,7 +275,7 @@ const bot = new Bot<MyContext>("");
bot.use(chatMembers(adapter));

bot.start({
// Make sure to specify the desired update types
// Make sure to specify the desired update types.
allowed_updates: ["chat_member", "message"],
});
```
Expand All @@ -56,7 +291,7 @@ const bot = new Bot("");
bot.use(chatMembers(adapter));

bot.start({
// Make sure to specify the desired update types
// Make sure to specify the desired update types.
allowed_updates: ["chat_member", "message"],
});
```
Expand All @@ -82,7 +317,7 @@ const bot = new Bot<MyContext>("");
bot.use(chatMembers(adapter));

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