Skip to content

Combining gates

Combine gates with or() and and() to let a command run when any one passes, like the owner or a moderator, or to group checks you reuse.

@Gated requires every gate you list. A command that a bot owner or a moderator can run needs only one of those two checks to pass.

or combines gates into one that passes when any gate passes. and combines them into one that needs every gate. The result goes on a handler like any other gate, or inside another combinator. Both take two gates or more.

or, when any one of them is enough

or runs each gate in order and stops at the first one that passes. The rest never run.

src/handlers/Award.ts
import {
    Gated,
    OwnerOnly,
    RequireRole,
    SlashHandler,
    SlashRoute,
    or
} from '@seedcord/gateway';

@Gated(or(OwnerOnly(), RequireRole('221439658686939136')))
@SlashRoute('award')
export class Award extends SlashHandler<'award'> {
    public async execute(): Promise<void> {
        const member = this.options.getUser('member');

        await this.reply(`Awarded ${member.username}.`);
    }
}

A bot owner passes the first gate and never reaches the role check. Anyone holding the role passes the second.

and, to treat several gates as one

@Gated(A(), B()) and and(A(), B()) refuse under the same conditions. Use and when you want those gates as one value, usually to put it inside an or.

const staff = and(GuildOnly(), RequireRole('221439658686939136'));

const staffOrOwner = or(staff, OwnerOnly());
const staffOrOwner: Gate<
    GateContextBase,
    "(GuildOnly & RequireRole) | OwnerOnly"
>

staff runs GuildOnly before RequireRole, since gates run left to right. The first refusal stops the rest.

The combined gate carries the names of its gates, joined with & for an and and | for an or. A combinator nested inside another gets parentheses around its name, which is why staffOrOwner above reads (GuildOnly & RequireRole) | OwnerOnly.

const guildOwner = and(GuildOnly(), OwnerOnly());
const guildOwner: Gate<GateContextBase, "GuildOnly & OwnerOnly">

A compile error prints that name when the gate goes on a handler it doesn't fit.

What each one accepts

The gates decide which handlers the combined gate fits.

required contexta handler fits when
andthe intersection of every gateit satisfies all of them
orthe union of every gateit satisfies at least one

An and of an event-only gate and an interaction-only gate doesn't fit any handler, since a handler can't provide both contexts. The compiler catches it at the @Gated line.

What counts as declining

A gate inside an or declines only when it throws a Notice with report left at false. Anything else stops the whole or, and the user sees whatever that throw produces.

a gate throwsor does
a Notice with report falsetries the next gate
a Notice with report truestops, reports it, shows that Notice
a Faultstops, shows the generic card, reports it unless its report is false
a Silencestops, and your bot doesn't reply
any other errorstops, reports it, shows the generic card

Setting report = true on your own Notice to get a refusal into your logs takes that gate out of the decline path.

The refusal when every gate refuses

or picks what to throw in three steps.

  1. The fail option, when you passed one.
  2. A list of what each gate needed, when every refusal carried a summary.
  3. A generic refusal reading "You are not allowed to use this command."

Every catalog gate that refuses with a Notice sets a summary, so an or built from those alone reaches step two. A notice you pass to a catalog gate replaces its default, summary included.

You need to meet at least one of these:
• be the bot owner
• hold the <@&221439658686939136> role

A gate of your own joins that list once its Notice sets summary. A single gate without a summary drops the whole list to the generic refusal, since a partial list would mislead by leaving an option out.

Wording it yourself

The summary list reads as a checklist in seedcord's wording. When the caller needs one sentence instead, like "Staff only" or a note on how to get the role, pass fail in a trailing options object after the gates. Pass a Notice for a fixed refusal.

or(OwnerOnly(), RequireRole('221439658686939136'), {
    fail: new StaffOnly('caller is neither')
});

Pass a function when the refusal needs to read the context. It receives the same ctx the gates did.

or(OwnerOnly(), RequireRole('221439658686939136'), {
    fail: (ctx) => new StaffOnly(`${ctx.userId ?? 'nobody'} tried`)
});

or throws fail first and never builds the summary list.

Two gates, at least

You can pass and or or as many gates as you like past two. With one gate, either is that gate on its own, so the compiler rejects it.

or(OwnerOnly());
Expected at least 2 arguments, but got 1.

The options object never counts as a gate, so or(A(), { fail }) fails the same way.