> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloak.chat/llms.txt
> Use this file to discover all available pages before exploring further.

# Editing, deleting, and pinning

> Edit, delete, pin, and unpin server messages, and react to those changes.

Your bot can change a message after it sends it, remove one, and pin or unpin it. It can also hear when any of that happens, including its own changes. This page covers `msg.edit()`, `msg.delete()`, `msg.pin()`, and `msg.unpin()`, plus the events that report each change.

<Warning>
  All four methods reject with a plain `Error` on a direct message, unconditionally and regardless of permissions. Direct messages arrive on the same `messageCreate` event as server messages, so guard with `if (msg.isDM || !msg.serverId) return;` before you capture or act on a message. Only `msg.reply()` and `msg.channel.send()` work in a DM.
</Warning>

## Edit a message the bot sent

`msg.edit(newContent)` replaces the text of a message and returns `Promise<void>`. The server allows only the message's author to edit it, so a bot can edit its own messages and never another member's.

The bot receives its own messages on the firehose, so it can hold one and edit it later.

```ts theme={null}
import { Client } from '@cloak-software/bot-sdk';
import type { Message } from '@cloak-software/bot-sdk';

const client = new Client({
  token: process.env.CLOAK_TOKEN!,
  keystorePath: './editor-bot.cloak-keystore.json',
  requiredPermissions: ['message_send'],
});

// The last message the bot posted, captured from the firehose.
let lastPost: Message | undefined;

client.on('messageCreate', async (msg) => {
  if (msg.isDM || !msg.serverId) return; // edit() rejects on a DM message
  if (msg.authorId === client.user?.id) {
    lastPost = msg; // our own message, so remember it
    return;
  }
  if (msg.content.trim() === '!revise' && lastPost) {
    await lastPost.edit('Edited: the details changed.').catch((e) => console.error('edit failed', e));
  }
});
```

<Note>
  An edit is re-encrypted through the same lane as a fresh send, under the channel's **current** key. If the channel's key cycled between the original send and the edit, the edited text is stored under the newer key. Members who hold that key read it normally.
</Note>

## Delete a message

`msg.delete()` removes a message and returns `Promise<void>`. The server allows the author to delete their own message, or a member holding `message_manage` to delete another member's.

This is the method a moderation bot reaches for. Pre-gate with an advisory `can()` check so the bot skips a delete the server would only refuse.

```ts theme={null}
client.on('messageCreate', async (msg) => {
  if (msg.authorId === client.user?.id) return; // ignore our own messages
  if (msg.isDM || !msg.serverId) return; // delete() rejects on a DM message
  if (!msg.content.toLowerCase().includes('bannedword')) return;
  if (!client.can('message_manage', msg.serverId)) return;
  await msg.delete().catch((e) => console.error('delete failed', e));
});
```

The `msg.isDM || !msg.serverId` guard is what narrows `msg.serverId` from `string | null` to `string`, which is what `can()` requires. Skip it and TypeScript rejects the call, while plain JavaScript silently treats every message as unpermitted.

## Pin and unpin

`msg.pin()` and `msg.unpin()` both return `Promise<void>`. Pinning is allowed for the message's author, or for a member holding the channel's pin permission. Both work on any server `Message` the bot holds, whether it came from the firehose or from `fetchMessages`.

```ts theme={null}
// message is any server Message the bot is holding on to.
await message.pin();

// Later, take it back down.
await message.unpin();
```

<Note>
  Pinning is gated by a channel-level pin permission that is not one of the 31 named permissions (codes 0 to 30). Because it has no name in that vocabulary, a pin denial's `CloakActionError` carries no `.permission` attribution, though `.message` still explains what happened.
</Note>

`Message.pinned` is a boolean that is available when known, from history via `fetchMessages` or from a `pinUpdate` event. Treat `undefined` as unknown rather than as unpinned.

## Every action needs createdAt

`edit()`, `delete()`, `pin()`, and `unpin()` all need the message's `createdAt`, because the server locates the row by its own send time. Live messages from the firehose and messages loaded with `fetchMessages` both carry it, so you rarely have to think about this.

<Note>
  If `createdAt` is missing, the call rejects with a plain `Error` before it ever reaches the server. That is a separate check from the DM rule above: a DM message rejects even when `createdAt` is present.
</Note>

## Handle a denied action

All four methods reject with a `CloakActionError` when the server refuses. Catch it, read `.message`, and branch on `.code` when you need the specific reason.

```ts theme={null}
import { CloakActionError } from '@cloak-software/bot-sdk';

try {
  await msg.delete();
} catch (err) {
  if (err instanceof CloakActionError) {
    console.error(`the server refused the delete (${err.code}): ${err.message}`);
    if (err.permission) console.error(`missing ${err.permission}`);
  } else {
    throw err; // a plain Error: a DM message, or no createdAt
  }
}
```

<AccordionGroup>
  <Accordion title="Edit codes">
    | Code | Meaning                                |
    | ---- | -------------------------------------- |
    | `-1` | Not authenticated, or not your message |
    | `-2` | Edit denied                            |
    | `-3` | Message empty or not found             |
    | `-4` | Server error while editing the message |
  </Accordion>

  <Accordion title="Delete codes">
    | Code | Meaning                                                  |
    | ---- | -------------------------------------------------------- |
    | `-1` | Not authenticated                                        |
    | `-2` | Server error while deleting the message                  |
    | `-3` | Message not found                                        |
    | `-4` | Delete denied (not your message and no `message_manage`) |
  </Accordion>

  <Accordion title="Pin and unpin codes">
    | Code | Meaning                             |
    | ---- | ----------------------------------- |
    | `-1` | Not authenticated                   |
    | `-2` | Server error while updating the pin |
    | `-3` | Message not found                   |
    | `-4` | Pin limit reached for this channel  |
    | `-5` | Missing the pin-messages permission |
  </Accordion>
</AccordionGroup>

Confirming a pin needs a Cloak server new enough to send the bot a success acknowledgement. Against an older backend the call cannot confirm and times out even though the pin landed. See [What the server must support](/production/backend-requirements).

See [Handling errors](/guides/handling-errors) for the full pattern.

## React to changes

Three events report changes to existing messages. `messageUpdate` and `messageDelete` fire for the bot's own edits and deletes too, so filter where you only care about other members.

```ts theme={null}
client.on('messageUpdate', ({ messageId, content, channelId }) => {
  console.log(`message ${messageId} in ${channelId} now reads: ${content}`);
});

client.on('messageDelete', ({ messageId, channelId }) => {
  console.log(`message ${messageId} was removed from ${channelId}`);
});

client.on('pinUpdate', ({ messageId, pinned }) => {
  console.log(`message ${messageId} is ${pinned ? 'pinned' : 'unpinned'}`);
});
```

<ResponseField name="messageUpdate" type="{ messageId, content, serverId, channelId }">
  `content` is the decrypted new text, best-effort. When the key is missing it is an empty string, never raw ciphertext.
</ResponseField>

<ResponseField name="messageDelete" type="{ messageId, serverId, channelId }">
  Fires when a message is removed.
</ResponseField>

<ResponseField name="pinUpdate" type="{ messageId, pinned, serverId, channelId }">
  `pinned` is a boolean: `true` when the message was pinned, `false` when it was unpinned. The acknowledgement the server sends back for the bot's own `pin()` call is a separate reply and does not fire this event.
</ResponseField>

Because `messageUpdate` and `messageDelete` fire for the bot's own changes as well, remember the message ids the bot just acted on and skip them if you only want other members' changes.

<Note>
  On an older Cloak server, `serverId` and `channelId` on these events can fall back to the current selection. An up-to-date Cloak server always reports the message's own location.
</Note>

## Next

<CardGroup cols={2}>
  <Card title="Reactions" icon="face-smile" href="/guides/reactions">
    The other message action, under the same DM rule.
  </Card>

  <Card title="Message history" icon="clock-rotate-left" href="/guides/message-history">
    Load past messages with `fetchMessages` so you have one to act on.
  </Card>

  <Card title="Moderation" icon="gavel" href="/guides/moderation">
    Kicks, bans, and timeouts alongside message removal.
  </Card>

  <Card title="Handling errors" icon="triangle-exclamation" href="/guides/handling-errors">
    What `CloakActionError` carries and how to catch it.
  </Card>
</CardGroup>
