# Your own gates

Write a gate of your own with defineGate() when the shipped ones don't fit your rule, and limit it to the handlers it fits.

Some checks belong to your bot alone, like keeping a command to one channel or requiring a linked account. The shipped gates cover owners, roles, permissions, and cooldowns, so a check like that would otherwise go back to the top of every handler that needs it. [`defineGate`](https://seedcord.org/docs/packages/core/latest/functions/define-gate) turns that check into a gate you attach with the rest.

It takes a name and a check. The check reads the context, refuses by [throwing](https://seedcord.org/guide/replying/throwing/), and passes by returning. Here's an example that keeps a command to one channel.

```ts title="src/gates/InChannel.ts"
import {
    BuilderComponent,
    defineGate,
    Notice
} from '@seedcord/gateway';

import type {
    Gate,
    GateContextBase,
    ReplyResponse
} from '@seedcord/gateway';

class WrongChannelCard extends BuilderComponent<'container'> {
    public constructor(channelId: string) {
        super('container');

        this.instance.addTextDisplayComponents((text) =>
            text.setContent(
                `### Wrong channel\nTry <#${channelId}>.`
            )
        );
    }
}

class WrongChannel extends Notice {
    public constructor(private readonly channelId: string) {
        super(`caller was outside ${channelId}`);
    }

    public render(): ReplyResponse {
        return {
            components: [
                new WrongChannelCard(this.channelId).component
            ]
        };
    }
}

export function InChannel(
    channelId: string
): Gate<GateContextBase, 'InChannel'> {
    return defineGate('InChannel', (ctx) => {
        if (ctx.channelId !== channelId)
            throw new WrongChannel(channelId);
    });
}
```

`InChannel` is a function that returns a gate, which is how most of the catalog is built. The closure keeps the argument, so one factory covers any channel.

```ts
@Gated(InChannel('221439658686939136'))
@SlashRoute('leaderboard')
export class Leaderboard extends SlashHandler<'leaderboard'> {
    public async execute(): Promise<void> {
        await this.reply('Top ten.');
    }
}
```

The `'InChannel'` string you passed to `defineGate` appears in the compile error when you attach the gate to a handler it doesn't fit. [`and`](https://seedcord.org/docs/packages/core/latest/functions/and) and [`or`](https://seedcord.org/docs/packages/core/latest/functions/or) join the names of their arms into one.

## What a check reads

Leaving the `ctx` parameter unannotated makes it a [`GateContextBase`](https://seedcord.org/docs/packages/core/latest/interfaces/gate-context-base), which every handler on both transports provides. These are its fields.

{/* prettier-ignore-start */}

| field               | type                | holds                                                                                                                                  |
| ------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `core`              | `CoreBase`          | the running framework, for `core.config` and `core.rateLimiter`                                                                        |
| `userId`            | `string \| null`    | the acting user's id, null on an event that carries none                                                                               |
| `guildId`           | `string \| null`    | the server id, null in a direct message                                                                                                |
| `channelId`         | `string \| null`    | the channel id, null when the source carries none                                                                                      |
| `memberRoleIds`     | `readonly string[]` | the caller's role ids without the everyone role, empty outside a server                                                                |
| `memberPermissions` | `bigint \| null`    | the caller's permission bits, channel-scoped on an interaction and role-derived on a gateway event, null outside a server              |
| `appPermissions`    | `bigint \| null`    | your bot's permission bits in the invoked channel, null on gateway events                                                              |
| `declaredRoute`     | `string \| null`    | the route this dispatch matched, such as `slash:daily`, null off a route                                                               |
| `dispatch`          | `DispatchContext`   | the [dispatch context](https://seedcord.org/guide/checks/dispatch-context/), holding what a middleware wrote before the first gate ran |

{/* prettier-ignore-end */}

A gate that reads *only* these fields fits any handler you attach it to. `InChannel` above reads one of them, so it works on a slash command, a button, and a message event.

## Narrowing what the gate accepts

Annotating `ctx` with a narrower context changes two things. The check reads the extra fields, and [`@Gated`](https://seedcord.org/docs/packages/gateway/latest/functions/gated) rejects the gate on any handler that lacks them.

```ts title="src/gates/NoAttachments.ts"
export const NoAttachments = defineGate(
    'NoAttachments',
    (ctx: EventGateContext<Events.MessageCreate>) => {
        const [message] = ctx.payload;

        if (message.attachments.size > 0) {
            throw new Silence('message carried an attachment');
        }
    }
);
```

`ctx.payload` is typed to that one event's argument tuple, so `message` is a discord.js `Message`. Attaching this gate to any other handler fails to compile.

> **Tip**
>
> `NoAttachments` doesn't take an argument. Export the gate itself and attach it without calling, the way [`IgnoreBots`](https://seedcord.org/docs/packages/gateway/latest/variables/ignore-bots) does. `InChannel` above stays a function because it closes over a channel id.

The two gateway arms carry different fields. Narrow on `ctx.kind` before reading either set.

{/* prettier-ignore-start */}

| annotation                                                                                                        | fits                           | adds                                              |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------- |
| `GateContextBase`                                                                                                 | every handler, both transports | the nine fields above                             |
| [`InteractionGateContext`](https://seedcord.org/docs/packages/gateway/latest/interfaces/interaction-gate-context) | any interaction handler        | `interaction`, `user`, `guild`, `member`          |
| `InteractionGateContext<ButtonInteraction>`                                                                       | button handlers                | the same, with `interaction` typed to a button    |
| [`EventGateContext`](https://seedcord.org/docs/packages/gateway/latest/interfaces/event-gate-context)             | any event handler              | `eventName`, `payload`, `user`, `guild`, `member` |
| `EventGateContext<Events.MessageCreate>`                                                                          | that one event                 | the same, with `payload` typed to its tuple       |
| [`GateContext`](https://seedcord.org/docs/packages/gateway/latest/types/gate-context)                             | either arm                     | the union of the two rows above                   |

{/* prettier-ignore-end */}

> **Gateway and http differ**
>
> Both transports export a type called `InteractionGateContext`. Each transport's version has its own fields. Gateway's holds live discord.js objects: a `User` that's always present, plus a `Guild` and a `GuildMember` that can each be `null`, outside a server or when the cache lacks them. Http's holds an `APIUser` and an `APIInteractionGuildMember`, and it doesn't carry `guild` at all. A gate reading `ctx.guild` compiles on gateway. The same gate fails to compile on http.
>
> The gateway cache builds that discord.js `Guild`. An http bot doesn't run one. Discord sends it a partial guild carrying `id`, `features`, and `locale`, which `ctx.interaction.guild` reaches.

## The two guild permission fields

On an interaction, `memberPermissions` and `appPermissions` hold the set for the channel the command ran in, after overwrites. A gate about the whole server, like one guarding a server-wide setting, needs what the roles grant instead. [`GuildPermissionsContext`](https://seedcord.org/docs/packages/core/latest/interfaces/guild-permissions-context) adds `memberGuildPermissions` and `appGuildPermissions`, the permission sets computed from roles alone.

> **Gateway only**
>
> Only the gateway cache carries role-derived permissions. Annotating `ctx` with `GuildPermissionsContext` gives you a gate that the http transport rejects at the decorator line. [`RequirePermissions`](https://seedcord.org/docs/packages/core/latest/functions/require-permissions) applies the same rule under `in: 'guild'`.

## An async check

A check can return a promise, and seedcord awaits it before the handler runs. Read a database, call an API, or wait on anything else you need.

```ts
export const NotBanned = defineGate('NotBanned', async (ctx) => {
    if (ctx.userId === null) return;
    if (await bans.has(ctx.userId)) throw new Banned();
});
```

Gates run one after another, so the `await bans.has` above delays every gate behind it until the lookup returns. Discord gives you three seconds to answer an interaction, and the checks use part of it. seedcord logs a warning naming each gate's share when one handler's checks total more than 750ms. That warning is off in production.

## What to throw

If you throw a [`Notice`](https://seedcord.org/docs/packages/core/latest/classes/notice), seedcord replies with the card its `render()` builds. If you throw a [`Silence`](https://seedcord.org/docs/packages/core/latest/classes/silence), seedcord drops the request without replying, which suits an event handler. [Throwing](https://seedcord.org/guide/replying/throwing/) has both in full, plus what a check throwing anything else produces.

If you plan to pass a gate to an [`or`](https://seedcord.org/docs/packages/core/latest/functions/or), set `summary` in your `Notice`'s constructor, as in `this.summary = 'run it in #bots'`. When every arm refuses, the `or` [lists each arm's summary](https://seedcord.org/guide/checks/combining/#the-refusal-when-every-arm-refuses). If one arm doesn't set a summary, the `or` shows a generic refusal.
