chat admin

build moderation bots for groups, supergroups, channels, join requests, permissions, and forum topics.

guard admin commands

don't hand-roll "is this user an admin" per command — @yaebal/guards ships isAdmin/isOwner/hasPermission as live getChatMember lookups, membership() to cache them, and guardOr to answer a denial instead of silently dropping the command.

guard.ts
import { createBot } from "yaebal";
import { guardOr, isAdmin, membership } from "@yaebal/guards";

export const bot = createBot(process.env.BOT_TOKEN!)
  // caches the getChatMember lookup isAdmin/isOwner/hasPermission need, with
  // event-driven invalidation — without it, every guard check is a live api call.
  .install(membership())
  .use(guardOr(isAdmin, (ctx) => ctx.reply("admins only")));

ban from a reply

a simple admin command usually targets the user in the replied-to message. ctx.ban(userId) fills chat_id from the current chat; pass the target id explicitly, since it otherwise defaults to whoever sent the command.

ban.ts
import { createBot } from "yaebal";
import { guardOr, isAdmin, membership } from "@yaebal/guards";

const bot = createBot(process.env.BOT_TOKEN!)
  .install(membership())
  .use(guardOr(isAdmin, (ctx) => ctx.reply("admins only")))
  .command("ban", async (ctx) => {
    const target = ctx.message?.reply_to_message?.from;
    if (!target) return ctx.reply("reply to a user first");

    // chat_id comes from ctx; userId defaults to the sender of *this* message
    // (i.e. the admin) when omitted — always pass it explicitly here.
    await ctx.ban(target.id);
  });

approve or decline join requests

join requests are opt-in updates. include chat_join_request in allowedUpdates when using long polling.

join-request.ts
import { createBot } from "yaebal";

const isAllowed = async (userId: number) => true;
const bot = createBot(process.env.BOT_TOKEN!);

bot.on("chat_join_request", async (ctx) => {
  if (await isAllowed(ctx.from.id)) {
    await ctx.approve();
  } else {
    await ctx.decline();
  }
});

track member changes

chat_member updates tell you when users join, leave, get promoted, or are restricted. they are useful for audit logs and permission caches.

chat-member.ts
import { createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.on("chat_member", async (ctx) => {
  const update = ctx.update.chat_member!;
  console.log(update.from.id, update.old_chat_member.status, "->", update.new_chat_member.status);
});

temporary restrictions

ctx.mute(seconds, params) is sugar over restrictChatMember — telegram lifts the restriction itself once the duration elapses, no cron job needed.

mute.ts
import { createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.command("mute", async (ctx) => {
  const target = ctx.message?.reply_to_message?.from;
  if (!target) return ctx.reply("reply to a user first");

  // restricts (no messages) for 10 minutes, then telegram lifts it automatically.
  await ctx.mute(600, { user_id: target.id });
});

admin checklist

  • ask botfather for the permissions your bot actually needs.
  • check botIsAdmin/botHasPermission (also from @yaebal/guards) before promising a moderation feature — the bot needs the right permission, not just the caller.
  • handle supergroup migrations if your bot stores old group ids.
  • keep audit logs for destructive actions — see observability.
  • rate-limit public admin commands to avoid accidental spam.