Restricting Who Can Use Your Bot's Commands
Every bot eventually grows a command that not everyone should run — a ban, a config change, something that costs money. Discord gives you a field for this, and default_member_permissions is a default rather than an authorization boundary: the docs are explicit that setting it to "0" will "prohibit anyone in a guild from using the command unless a specific overwrite is configured or the user has admin permissions", that "members with the Administrator permission can use all commands", and that "command permissions can also be updated by users in the client" (Discord docs). So the server can change your mind about it, and admins skip it entirely. Telegram has no equivalent at all. Either way, the check that actually protects you runs in your handler.
What default_member_permissions actually does
It sets which permission bits a member needs for the command to appear and be usable by default. Ship a command with the ban-members bit and, in a fresh server, only people who can ban members see it.
That's genuinely useful, and it's worth setting on every privileged command — just not for the reason people assume.
Its real value is discoverability. Members who can't use a command don't see it in the picker. That means fewer people trying things they can't do, fewer confusing failures, and a shorter command list for everyone. It's a user-experience feature that happens to look like a security feature.
Setting it to "0" is the strictest option available, and per the docs that still leaves administrators — and any per-command overwrite the server configures — able to run it.
Why it isn't authorization
Two documented facts do the damage:
Administrators can use every command. Not "usually", not "unless configured" — the permission exists to be a master key, and your command is inside the lock it opens.
Server staff can change your defaults. Command permissions are editable in the Discord client by anyone with the rights to edit them. Your default is a starting position in someone else's settings page, and you will never be told it changed.
Neither is a flaw in Discord. It's the correct design: server owners run their servers, not you. But it means a command that spends your API budget or exposes data is governed by a setting a stranger can edit.
So treat the field as a filter on the menu, and put the decision that matters somewhere you control. It's the same reasoning as not relying on an unguessable URL — a control you don't own isn't a control.
Telegram gives you nothing, which is clearer
There's no per-command permission system. Commands are messages; anyone in the chat can send one, and your bot receives it.
The check is yours to write, and the tool is a membership lookup. getChatMember returns the member's status in the chat, documented in python-telegram-bot as one of creator, administrator, member, restricted, left or kicked (ChatMember docs).
For most bots the rule is one line: allow creator and administrator, refuse everything else.
There's an honesty to this. Telegram never suggests you're protected, so nobody ships a moderation bot believing the platform is checking on their behalf.
The check that actually protects you
Three tiers, and most bots need all three:
Bot owner. For commands that affect the bot itself — reload config, view stats, spend money. Put your user ID in an environment variable and compare against it. Not a hardcoded constant, so the same image can run as your staging bot with a different owner.
Server staff. For moderation commands, check the invoking member's permissions in that server at the moment they run it. Discord gives you the member object on the interaction; Telegram needs the membership lookup.
Explicit allowlist. For "this specific role can use this command", keep it in your own storage. Which means a volume, because per-guild configuration has to survive a redeploy — and the container is recreated on every deploy and every configuration change.
Then two rules about how the check behaves:
Fail closed. If the lookup errors, refuse. An exception path that falls through to "allowed" is the bug you least want, and it's the one that gets written when someone adds a try/except to stop a crash.
Answer the user. A silent refusal reads as a broken bot and generates the support message you were trying to avoid. "You need Manage Server to use this" costs nothing and ends the conversation.
Caching the lookup, cheaply
Checking membership on every command is an API call in your path — latency you're adding, and rate limit you're spending on a question whose answer rarely changes.
Cache it for a minute or two, keyed by user and guild. And here the platform makes this easy: there's one container, so a dictionary with a TTL is a complete cache. No shared store, no invalidation across instances, nothing to coordinate — the same reason you probably don't need Redis.
Two caveats worth stating:
- Keep the TTL short. Someone demoted thirty seconds ago should not still be running admin commands. A minute is a reasonable trade; an hour is not.
- The cache empties on every deploy, which for this is entirely fine — the first check after a restart is simply a real lookup.
Don't cache a denial for long, either. The most common moment someone is granted a role is right after they were told they lack it.
Log refusals, in moderation
A refused privileged command is worth one line: who, which command, which server. It's how you find out whether someone is probing, and it's the record you want when a server owner asks what happened.
Keep it to a line. Container logs are capped and rotate, so a bot that logs every denied interaction at debug level will push out the lines that mattered.
A note on LLM-backed commands
If a command feeds user input to a model, permissions are only half of it. Restricting who can run it limits your bill; it does nothing about what they say once allowed. That's a separate problem with its own set of defences.
FAQ
Does default_member_permissions stop people using a command?
Not reliably. It sets a default that hides the command from members without the permission, but administrators can use every command and server staff can change the setting.
Should I set it anyway?
Yes. It keeps privileged commands out of the picker for people who can't use them, which is a genuine improvement even though it isn't security.
What does setting it to "0" do?
It disables the command for everyone except administrators, unless the server configures an overwrite.
How do I restrict a Telegram bot command?
There's no platform mechanism. Look up the sender's status in the chat and allow only creator and administrator.
Where should the owner's user ID live?
In an environment variable, so the same image can run as staging and production with different owners.
Can I cache permission checks?
Yes — one container means an in-memory dictionary with a short TTL is enough. Keep the TTL to a minute or two so demotions take effect quickly.
Wrapping up
The pattern here shows up in most platform integrations: there's a built-in control that looks like it does authorization, and reading the documentation carefully shows it's a default that someone else can edit. Set it anyway — it makes your bot pleasanter to use — and then write the four lines in your handler that decide whether this person, in this server, right now, may do this thing. Those four lines are the only part that a server owner's settings page can't change, and they're the part you'll be glad of the first time someone tries.