MCL (Membership Checker)¶
Verify that users joined required Telegram channels or groups before granting access.
File: Libs/mcl.js · Access: Libs.mcl.* · Async (except getBtn) · v1.0.0
What problem does it solve?¶
Classic bot pattern: "Join our channel to unlock this feature."
mcl calls Telegram's getChatMember API live for each channel and tells you:
- Which channels the user joined
- Which they left or were kicked from
- Which channels the bot cannot access (invalid)
It also builds join buttons and ready-to-send status messages.
Requirements¶
| Rule | Detail |
|---|---|
| Bot must be in channel | Add bot as member/admin first |
getChatMember permission | Bot needs rights to check membership |
| Max channels | 10 per call |
getBtn usernames | Only public @username — numeric IDs skipped |
Quick start — simple gate¶
let channels = ["@MyChannel", "@MyGroup"]
if (await Libs.mcl.quick(user.id, channels)) {
Bot.run("/premiumMenu")
} else {
Bot.sendMessage("Join our channels first, then try again.")
}
Methods¶
| Method | Async | Description |
|---|---|---|
check(userId, channels) | Yes | Full breakdown |
quick(userId, channels) | Yes | true if all joined |
getLeftChannels(userId, channels) | Yes | Channels user left |
getInvalidChannels(userId, channels) | Yes | { channel, reason }[] |
summaryText(userId, channels, options?) | Yes | User-facing message |
getStats(userId, channels) | Yes | Numeric summary |
getBtn(channels, options?) | No | Inline join buttons |
check(userId, channels)¶
Main method — returns detailed result.
Parameters¶
| Param | Type | Description |
|---|---|---|
userId | number | Telegram user ID |
channels | string[] | "@username" or numeric chat ID |
Returns¶
| Field | Description |
|---|---|
allJoined | true only if user joined every valid channel |
joined | Confirmed memberships |
left | Status left or kicked |
invalid | { channel, reason } — bot can't access |
let result = await Libs.mcl.check(user.id, ["@NewsChannel", "@CommunityGroup"])
if (!result.allJoined) {
if (result.left.length) {
Bot.sendMessage("Please join: " + result.left.join(", "))
}
if (result.invalid.length) {
Bot.sendMessage("Some channels are misconfigured — contact admin.")
}
}
quick(userId, channels)¶
Returns true or false — shortcut for check().allJoined.
getLeftChannels / getInvalidChannels¶
let left = await Libs.mcl.getLeftChannels(user.id, channels)
// ["@Chan2"]
let bad = await Libs.mcl.getInvalidChannels(user.id, channels)
// [{ channel: "@Fake", reason: "Channel inaccessible" }]
summaryText(userId, channels, options?)¶
Ready-to-send status text. Customize via options:
| Option | Default |
|---|---|
joinedMessage | "You have joined all required channels." |
leftHeader | "Please join the following channels:" |
invalidHeader | "Inaccessible channels:" |
separator | "\n\n" |
let text = await Libs.mcl.summaryText(user.id, channels, {
joinedMessage: "All good — here's your reward!",
leftHeader: "Join these channels to continue:"
})
getStats(userId, channels)¶
let stats = await Libs.mcl.getStats(user.id, channels)
// {
// total: 3,
// joinedCount: 2,
// leftCount: 1,
// invalidCount: 0,
// percentJoined: 66.666...,
// allJoined: false,
// hasIssues: true
// }
Useful for admin dashboards or analytics commands.
getBtn(channels, options?)¶
Sync — no await. Builds inline keyboard join rows.
| Option | Default | Description |
|---|---|---|
buttonPrefix | "Join" | Text before @channel |
Only @username public channels get buttons. Private numeric IDs are skipped.
let buttons = Libs.mcl.getBtn(["@Chan1", "@Chan2"], { buttonPrefix: "📢 Join" })
// [
// [{ text: "📢 Join @Chan1", url: "https://t.me/Chan1" }],
// [{ text: "📢 Join @Chan2", url: "https://t.me/Chan2" }]
// ]
Api.sendMessage({
chat_id: chat.id,
text: "Join to continue:",
reply_markup: { inline_keyboard: buttons }
})
Full gate example¶
let channels = ["@MyChannel", "@MyGroup"]
let result = await Libs.mcl.check(user.id, channels)
if (result.allJoined) {
Bot.run("/mainMenu")
return
}
let left = result.left.length ? result.left : channels
await Api.sendMessage({
chat_id: chat.id,
text: await Libs.mcl.summaryText(user.id, channels),
reply_markup: {
inline_keyboard: Libs.mcl.getBtn(left, { buttonPrefix: "📢 Join" })
}
})
Full example — soft gate with retry button¶
// /verify command — user taps after joining
let ok = await Libs.mcl.quick(user.id, ["@RequiredChannel"])
if (ok) {
await Libs.ResourcesLibv2.userRes("gold").add(10)
return Bot.sendMessage("Verified! +10 gold.")
}
Api.sendMessage({
chat_id: chat.id,
text: "Join @RequiredChannel then tap Verify.",
reply_markup: {
inline_keyboard: [
[{ text: "Join Channel", url: "https://t.me/RequiredChannel" }],
[{ text: "Verify", callback_data: "verify_join" }]
]
}
})
Common mistakes¶
// Wrong — ok is a Promise, not boolean
if (Libs.mcl.quick(user.id, channels)) { ... }
// Correct
if (await Libs.mcl.quick(user.id, channels)) { ... }
// Wrong — numeric ID won't appear in getBtn
Libs.mcl.getBtn([-1001234567890])
// Correct — use @username for join buttons
Libs.mcl.getBtn(["@MyChannel"])
// Wrong — bot not in channel → silent invalid, gate never passes
// Fix: add bot to channel as member/admin BEFORE deploying gate
// Wrong — more than 10 channels
await Libs.mcl.check(user.id, bigArray) // throws LimitError
// Correct — split into batches of 10
Notes¶
- Membership checked live — not cached.
- Throws
[LibsError]if channels array empty or > 10 items. - Channel strings accept
@ChanorChan— normalized internally. - TBL does not support
.then()— alwaysawaitasync methods.