# Handling messages

Read the text of messages in a gateway event handler. Covers what the Message Content intent hides, the cases Discord exempts, turning the intent on, and when a slash command is the better route.

Most bots start by reacting to what people type. With the `GuildMessages` intent, a `messageCreate` handler runs for every message in every server your bot is in. Reading what that message says is a separate permission Discord controls. If `@RegisterEvent` and `this.event` are new to you, start with [Your first event handler](/events).

```ts title="src/events/Echo.ts"
import { EventHandler, RegisterEvent } from '@seedcord/gateway';
import { Events } from 'discord.js';

@RegisterEvent([Events.MessageCreate])
export class Echo extends EventHandler<Events.MessageCreate> {
    public async execute(): Promise<void> {
        const [message] = this.event;
        const text = message.content;

        this.logger.info(`content: "${text}"`);
    }
}
```

Without the Message Content intent, `Echo` logs an empty string every time.

```txt output
content: ""
```

Nothing throws, so you only notice when a feature quietly never matches. The compiler can't warn you either, since `text` is typed `string` with or without the intent.

## What the intent gates

If your app doesn't have the intent, Discord empties five fields on every message object: `content`, `embeds`, `attachments`, `components`, and `poll`. Discord applies this to every API that returns a message, so the same fields come back empty wherever your bot reads one.

## The exceptions

Discord sends the real content in these five cases, whatever intents you set. If your bot only reads messages like these, it can skip the intent entirely. Discord's [privileged intent guide](https://docs.discord.com/developers/gateway/you-might-not-need-a-privileged-intent#exceptions-when-you-get-message-content-without-the-privileged-intent) covers them too.

{/* prettier-ignore-start */}

| case              | what it covers                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------ |
| your own messages | anything your app sent. A message from a webhook your app created counts only if it mentions your app.       |
| direct messages   | a DM to your app                                                                                             |
| a mention         | a message that mentions your app as `<@id>`. Mentioning your app's role doesn't count.                       |
| a reply           | a reply to a regular message from your app, with ping on. A reply to a slash command response doesn't count. |
| a context menu    | the message a message context menu command ran on                                                            |

{/* prettier-ignore-end */}

A bot that answers mentions can read those messages in full without a privileged intent. `Mentioned` returns early unless the message mentions your bot.

```ts title="src/events/Mentioned.ts"
@Gated(IgnoreBots)
@RegisterEvent([Events.MessageCreate])
export class Mentioned extends EventHandler<Events.MessageCreate> {
    public async execute(): Promise<void> {
        const [message] = this.event;
        if (!message.mentions.users.has(message.client.user.id))
            return;

        await message.reply(`hey ${message.author.displayName}`);
    }
}
```

[`@Gated`](https://docs.seedcord.org/packages/gateway/latest/gated) with [`IgnoreBots`](https://docs.seedcord.org/packages/gateway/latest/ignore-bots) stops the handler on a message from any bot, including your own. Without it, `Mentioned` would answer another bot that mentions yours. Two bots like that can reply to each other forever. The [gate catalog](/checks/gates) lists the other checks.

## Turning the intent on

When your bot does need every message's text, like an auto-moderator scanning for banned words, you turn the intent on in two places. Turn on **Message Content** in the developer portal under your application's **Bot** tab, then add the intent to `clientOptions`. If you flip only the portal switch, the text stays empty. If you add only the client intent, Discord closes the connection with close code 4014, "Disallowed intent(s)".

```ts title="src/bot.ts"
export const seedcord = new Seedcord({
    bot: {
        ...rest,
        clientOptions: {
            intents: [
                GatewayIntentBits.Guilds,
                GatewayIntentBits.GuildMessages,
                GatewayIntentBits.MessageContent
            ]
        },
        events: { path: resolve(import.meta.dirname, './events') }
    },
    subscribers: { path: null }
});
```

`GuildMessages` delivers the event. `MessageContent` fills in the five fields.

## Prefix commands and the intent review

Message Content is a privileged intent. Once more than 10,000 unique users can see your app, Discord requires you to apply for it. You reapply every year to keep it. When slash commands could do the same job, Discord can deny the request.

Don't build commands on a prefix like `!help` or `?play`. Make them slash commands. Discord's page on avoiding a privileged intent opens its checklist with this question: "Is my bot using prefix commands (`!help`, `?play`) that could be migrated to slash commands?"

A slash command runs without a privileged intent. It also gives you typed options and a place in Discord's command picker. [Commands](/commands) covers slash commands. A message context menu command is the other route, since it receives the full message, content included.
