# Your own gates

Write a gate of your own with defineGate() when the shipped ones don't fit your rule. Covers what a check reads, narrowing it to certain handlers, the two guild permission fields, async checks, and what to throw.

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://docs.seedcord.org/packages/core/latest/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](/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://docs.seedcord.org/packages/core/latest/and) and [`or`](https://docs.seedcord.org/packages/core/latest/or) join the names of their arms into one.

## What a check reads

Leaving the `ctx` parameter unannotated makes it a [`GateContextBase`](https://docs.seedcord.org/packages/core/latest/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](/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://docs.seedcord.org/packages/gateway/latest/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://docs.seedcord.org/packages/gateway/latest/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://docs.seedcord.org/packages/gateway/latest/interaction-gate-context) | any interaction handler        | `interaction`, `user`, `guild`, `member`          |
| `InteractionGateContext<ButtonInteraction>`                                                            | button handlers                | the same, with `interaction` typed to a button    |
| [`EventGateContext`](https://docs.seedcord.org/packages/gateway/latest/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://docs.seedcord.org/packages/gateway/latest/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://docs.seedcord.org/packages/core/latest/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://docs.seedcord.org/packages/core/latest/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://docs.seedcord.org/packages/core/latest/notice), seedcord replies with the card its `render()` builds. If you throw a [`Silence`](https://docs.seedcord.org/packages/core/latest/silence), seedcord drops the request without replying, which suits an event handler. [Throwing](/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://docs.seedcord.org/packages/core/latest/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](/checks/combining#the-refusal-when-every-arm-refuses). If one arm doesn't set a summary, the `or` shows a generic refusal.
