# Send messages, embeds and files

This guide describes SDK version 1001.0.0-nightly.20261104.56

Reply, send and edit messages, build embeds, attach files and send direct messages

Start with a bot that can reply to `!ping`. Add these helpers to the [first bot example](https://fluxerly.neontechspace.com/docs/nightly/quick-start.md) and call them from a command or message [handler](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#handler). They use the bot's existing `client` rather than opening another connection. The [last section](https://fluxerly.neontechspace.com/docs/nightly/messages.md#wire-it-up) shows where they plug into `runBot`

## Reply to a message and check what happened

Use `reply` when the answer belongs to an incoming message. Awaiting an operation in the default API gives a [Result](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#result): Check `isErr()` first, then read `value`

```ts
import { describeError, type Client, type Message } from "@neontechspace/fluxerly"

export async function replyHello(client: Client, message: Message) {
    if (message.author.isBot) return

    const result = await client.messages.reply(message, `Hello, ${message.author.username}!`)
    if (result.isErr()) {
        console.error(describeError(result.error))
        return
    }
    return result.value
}
```

A plain string sends a text-only message. On success, `value` is the message Fluxer created, including its ID. The `describeError` helper prints the failure's message, code, suggested fix and cause. Replies do not notify anyone by default, not even the author of the replied-to message

<details>
<summary>What does a failed send say?</summary>

The error message names the operation and Fluxer's answer, then Fluxer's code and the HTTP status, for example `Message send failed: Fluxer reports that the bot lacks a required permission (MISSING_PERMISSIONS, HTTP 403)`. The error's `hint` suggests a next step. For a missing permission, `details.requiredPermissions` lists the permissions Fluxer checks for the operation, such as `["ViewChannel", "SendMessages"]` for a send, and the hint names them

Fluxer does not report which one is missing, so the bot may already have some of them. When Fluxer answers with a code this SDK version does not know yet, `details.providerCode` holds it. When Fluxer's answer does not have the expected shape, `details.responseField` names the field that failed

A rejection with HTTP 401 or 403 that the application handles also logs a Warn record with the code `rest.rejected`, because it usually points at a lasting token or permission problem. The SDK holds that Warn for up to one second. A handler's failure record replaces it only if the handler fails with the same rejection while the Warn is held and no `onError` hook is configured for that failure

[Identical repeats collapse](https://fluxerly.neontechspace.com/docs/nightly/logging.md#collapse-repeated-errors) into one record per minute by default. The [error and log codes](https://fluxerly.neontechspace.com/docs/nightly/codes/) page lists every code with its meaning

</details>

<details>
<summary>Skip join notices and other system messages</summary>

Fluxer also delivers notices, such as a member joining or a pinned message, as messages. Compare `message.type` with a `MessageType` constant to tell them apart. For example, `message.type === MessageType.UserJoin` is a join notice, and ordinary messages are `MessageType.Default` or `MessageType.Reply`

</details>

A `messageCreate` or `messageUpdate` event for a community message can carry `message.member`, the author's community membership as Fluxer held it when it sent the event, with roles, nickname and join time. It is absent on messages from REST reads, direct messages, webhooks and authors who left the community, and the message cache does not keep it, so read it in the handler

With `cache.members` enabled, `members.get` then answers for that author without a request. The same member arrives as `reaction.member` on `messageReactionAdd` and `messageReactionRemove` events and as `typing.member` on `typingStart` events. A member that Fluxer sends in a form the SDK cannot read is left off, and the event still arrives

A `messageCreate` event, and no other message event, can also carry `message.channelType`, the numeric type of the channel, with 1 for a direct message and 3 for a group direct message, `message.mentionedHere`, which Fluxer sends only as `true` for a here mention, and `message.nicknames`, the nicknames of a group direct message keyed by user ID

REST reads and `messageUpdate` events never carry them, and the message cache does not keep them. A `messageDelete` event can carry `deletion.member`, the author's community membership without a username or bot flag, because Fluxer removes the member's user from it. The member cache does not store it

Received messages can carry `message.messageReference`, which points at a replied-to or forwarded message, or at the source of an announcement copy or follow notice. Its `channelId` is always present, but `id` is optional. Check `id`, then build a `MessageReference` from both fields for a fetch or reply. A follow notice identifies only the source channel. See [announcement channels](https://fluxerly.neontechspace.com/docs/nightly/announcement-channels.md) for publishing messages and handling their copies

Sends and edits accept `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications`. Message sends also accept `MessageFlags.VoiceMessage` for a [voice note](#mark-media-as-explicit-or-send-a-voice-note). Message sends include replies, direct messages, the first message of a forum post and webhook sends. Edits and webhook forwards reject that flag, and the SDK rejects any other flag before sending the request. Fluxer sets `Crossposted`, `IsCrosspost` and `SourceMessageDeleted` itself

## Mention someone deliberately

Create a mention with `format.userMention`, then allow a notification for that specific user. Role and channel mentions have matching `roleMention` and `channelMention` helpers

```ts
import { format, type Client } from "@neontechspace/fluxerly"

export async function notifyParticipant(client: Client, channelId: string, userId: string) {
    return await client.messages.send(channelId, {
        content: `${format.userMention(userId)} The game is starting`,
        allowedMentions: { users: [userId] },
    })
}
```

Methods that take a user, role or channel ID expect the decimal ID. To read a mention typed by a user, use `format.tryParseMention`, which returns a Result instead of throwing on invalid text. [Command arguments](https://fluxerly.neontechspace.com/docs/nightly/commands.md) can also accept either an ID or a mention

<details>
<summary>Send a text-to-speech message</summary>

Add `tts: true` to the content passed to `messages.send`, `messages.reply`, `directMessages.send` or the `reply` helper of a `runBot` handler or command. A value other than a boolean fails with reason `input` before any request.
In a community, a bot without the Send TTS Messages permission gets a normal message and no error

The `tts` field of a message is meaningful only on the message returned by the send and on the `messageCreate` event. Fluxer does not store it, so fetches, history pages and other later reads always report `false`.
Webhook messages do not accept `tts`: A webhook send, reply or forward that includes it fails with reason `input` before any request

</details>

## Send, then edit the returned message

Use `send` for a channel message that does not reply to anything. Keep the returned message to edit it later

```ts
import type { Client } from "@neontechspace/fluxerly"

export async function postStatus(client: Client, channelId: string) {
    const sent = await client.messages.send(channelId, "Preparing the report…")
    if (sent.isErr()) return sent

    return await client.messages.edit(sent.value, "Report ready")
}
```

The helper posts a message, then edits it, and returns the edit's Result or the send's failure

<details>
<summary>What if the send or the edit fails?</summary>

If the edit fails, the first message stays posted. If the connection fails after a send or edit was dispatched, the change may still have reached Fluxer. Check the returned Result before deciding whether to try again, and avoid repeating a write whose outcome is unknown.
The [reliability guide](https://fluxerly.neontechspace.com/docs/nightly/reliability.md) explains these uncertain writes and when a retry is safe

</details>

## Build an embed

An [embed](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#embed) is a formatted card with a title, description, color and fields. The embed builder assembles one step by step. Pass the builder in the message's `embeds` list

```ts
import { builders, type Client } from "@neontechspace/fluxerly"

export async function postSchedule(client: Client, channelId: string) {
    const embed = builders.embed()
        .title("Tonight's game")
        .description("Meet in the lobby before the first round")
        .color("#8b7cf8")
        .field("Start", "19:00 UTC", { inline: true })
        .field("Mode", "Co-op", { inline: true })
        .footer({ text: "Snacks provided" })

    return await client.messages.send(channelId, { embeds: [embed] })
}
```

The `inline` option places fields side by side. The `color` method accepts a hex string such as `"#8b7cf8"`, a number or an RGB tuple, and `timestamp` accepts a `Date`, epoch milliseconds or an ISO 8601 string with a time zone

The send checks the embed against Fluxer's limits. It reads the builder once, so later builder changes do not alter the sent message. Use `messages.edit` for that. The builder's `build()` method returns the same embed as a plain object, which could also be written by hand

An embed's `image.url` or `thumbnail.url` can use `attachment://filename` to show a new image or video uploaded in the same request. Avoid two dots in a row and names such as CON or AUX, because Fluxer renames those files and the reference then fails. The name must match exactly one upload, including case, and its extension must map to an image or video MIME type in Fluxer's filename lookup, such as AVIF or MP4. Referencing an existing attachment ID is not supported

In a community channel, sending or editing a message with embeds needs the `EmbedLinks` permission. New communities grant it to everyone, but where it is removed, Fluxer rejects the whole message with a missing-permissions error instead of sending it without the embeds

## Upload a small generated file

For text already in memory, encode it to bytes and give the attachment a file name. No temporary file on disk is needed

```ts
import { builders, type Client } from "@neontechspace/fluxerly"

export async function sendChecklist(client: Client, channelId: string) {
    const data = new TextEncoder().encode("[ ] Check permissions\n[ ] Test !ping\n")
    const message = builders.message()
        .content("Bot checklist")
        .attachment({ data, filename: "checklist.txt", contentType: "text/plain" })
        .build()

    return await client.messages.send(channelId, message)
}
```

The builder keeps a reference to the bytes, and the send copies them when it is called. For larger files, pass a `file` source such as a `Blob`, or a `stream` with its exact `size`, instead of `data`. The [attachment input reference](https://fluxerly.neontechspace.com/docs/nightly/api/js-ts/messages/#attachmentinput) lists these forms, which avoid loading everything into memory

## Send a file from disk

On Node.js, `openAsBlob` from `node:fs` opens a file without reading it into memory. Pass the result as `file` and give the upload a name. A path string is not accepted, so open the file first

```ts
import { openAsBlob } from "node:fs"
import { basename } from "node:path"
import { builders, type Client } from "@neontechspace/fluxerly"

export async function sendReport(client: Client, channelId: string, path: string) {
    const file = await openAsBlob(path)
    const message = builders.message()
        .content("Daily report")
        .attachment({ file, filename: basename(path) })
        .build()

    return await client.messages.send(channelId, message)
}
```

The send reads the file while it uploads, so keep the file unchanged until the send finishes

## Mark media as explicit or send a voice note

Set `explicitMedia: true` on an attachment to mark it as sensitive, next to `spoiler`. Fluxer's apps then treat it like media that Fluxer's own check flags. The mark does not make Fluxer refuse the file. Fluxer still checks media itself and refuses explicit media where the community's explicit content filter applies, with `apiError.code` `explicitContentCannotBeSent`

On a received attachment, the explicit-media mark is `(attachment.flags & (1 << 4)) !== 0`. It can be present even when `attachment.nsfw` is false or absent

A voice note is an uploaded audio file that Fluxer's apps show with a player and a waveform. It has nothing to do with voice channels, which the SDK does not connect to. Send one with `MessageFlags.VoiceMessage` and exactly one audio attachment that has `duration` in whole seconds and `waveform`, the Base64 amplitude bytes the player draws. The SDK does not record or decode audio. Decode the file into the samples of its first channel, the one Fluxer's apps read, and `voiceWaveform(samples, sampleRate)` computes both values the way Fluxer's apps do, so the bars match a note recorded in Fluxer

```ts
import { MessageFlags, voiceWaveform, type Client } from "@neontechspace/fluxerly"

// The samples are one channel of the decoded file, at sampleRate samples per second
export async function sendVoiceNote(client: Client, channelId: string, audio: Uint8Array, samples: Float32Array, sampleRate: number) {
    return await client.messages.send(channelId, {
        attachments: [{ data: audio, filename: "note.ogg", contentType: "audio/ogg", ...voiceWaveform(samples, sampleRate) }],
        flags: MessageFlags.VoiceMessage,
    })
}
```

With ffmpeg, `ffmpeg -i note.ogg -af 'pan=mono|c0=c0' -ar 48000 -f f32le -` writes the first channel as 32-bit float samples at 48,000 samples per second. Mixing the channels, as `-ac 1` does, gives other bars for a stereo file. Copy the output into a `Float32Array`, for example with `new Float32Array(new Uint8Array(output).buffer)`, and pass 48000 as the rate. Exact parity with Fluxer's apps needs the same first-channel samples at the same sample rate, which another decoder or resampling can change slightly. Ordinary decoded sample scales work, such as floats from -1 through 1 or 16-bit integers from a WAV file, because the waveform is relative to the loudest part

A voice note cannot have content, embeds or stickers, and the SDK refuses those before sending. Fluxer also checks that the file is audio and that it is no longer than the instance allows, 1,200 seconds by default. `messages.send`, `messages.reply`, direct message sends, the first message of a forum or media post and webhook sends and replies accept voice notes. A waveform holds at most 4,096 characters, except in a webhook send or reply. Webhook files always go as multipart, where Fluxer reads a waveform of at most 256 characters, so a longer waveform fails locally on `attachments[].waveform`. A `voiceWaveform` result fits for audio up to 19.2 seconds, and `voiceWaveform(samples, sampleRate, { maxPoints: 192 })` fits for audio of any length. For audio over 19.2 seconds, each of its bars covers more audio than in a note recorded in Fluxer. Edits and webhook forwards do not accept voice notes. To keep an existing voice note as a voice note, omit `flags` when editing it. On the bot's own message, and on a webhook message edited through its webhook, supplying flags, including 0, clears its voice-note flag, and an edit cannot set it again. When the bot client edits another author's message, Fluxer requires Manage Messages in a community and changes only `SuppressEmbeds`, keeping the other flags

## Download an attachment when needed

Received messages describe their attachments but do not download them. Set a byte limit and handle a failed download before processing any content

```ts
import { describeError, type Client, type Message } from "@neontechspace/fluxerly"

export async function readFirstSmallFile(client: Client, message: Message) {
    const attachment = message.attachments[0]
    if (!attachment) return

    const downloaded = await client.attachments.download(attachment, { maxBytes: 64 * 1024 })
    if (downloaded.isErr()) {
        console.error(describeError(downloaded.error))
        return
    }
    return downloaded.value
}
```

Check the file type and contents before processing downloaded bytes. The download uses the attachment's media address on the bot's Fluxer instance and does not accept arbitrary URLs from message text

Buffered downloads and `attachments.stream` preserve a sanitized cause for discovery and transport failures, including a safe code such as `ECONNRESET` when available. The original transport error, attachment URL and response body are not retained in that cause

Attachment links can expire while the file still exists. The SDK does not refresh links automatically. Before downloading a stored link, call `attachments.refreshUrls` and use the returned URL. Refreshing does not restore a deleted file

```ts
import type { Attachment, Client } from "@neontechspace/fluxerly"

export async function downloadStored(client: Client, attachment: Attachment) {
    if (attachment.url === undefined) return
    const refreshed = await client.attachments.refreshUrls([attachment.url])
    if (refreshed.isErr()) return refreshed
    const url = refreshed.value[0]?.refreshed ?? attachment.url
    return client.attachments.download({ ...attachment, url }, { maxBytes: 64 * 1024 })
}
```

The same steps in the Effect API:

```ts
import { Effect } from "effect"
import type { Attachment, Client } from "@neontechspace/fluxerly/effect"

export const downloadStored = (client: Client, attachment: Attachment) =>
    Effect.gen(function* () {
        if (attachment.url === undefined) return
        const [link] = yield* client.attachments.refreshUrls([attachment.url])
        const url = link?.refreshed ?? attachment.url
        return yield* client.attachments.download({ ...attachment, url }, { maxBytes: 64 * 1024 })
    })
```

## Send a requested direct message

Send a private message only after the recipient asks for one, for example through an opt-in command. The `directMessages.send` method opens the conversation with that user and sends the message

```ts
import type { Client } from "@neontechspace/fluxerly"

export async function sendRequestedDm(client: Client, recipientId: string) {
    return await client.directMessages.send(recipientId, "The requested game instructions are ready")
}
```

A send can still fail when the recipient's privacy settings block it. To reuse the conversation, `directMessages.open` returns its channel, and existing group conversations use the normal message methods with their channel ID. No method creates a group conversation. Fluxer lets a bot create one with `client.rest.request` and `POST /users/@me/channels` with a `recipients` list, for people who share a community with the bot and allow it. Where the instance turns on CAPTCHA, Fluxer also asks for a solved challenge, which a raw request cannot send, so it fails with `apiError.code` `captchaRequired`. See [private conversations](https://fluxerly.neontechspace.com/docs/nightly/api/js-ts/users-and-dms/#directmessages) for reads and permitted management actions

## Wire it up

In a `runBot` bot, each command receives a `reply` helper already bound to the incoming message. This bot answers `!schedule` with an embed and stops cleanly on Ctrl+C

```ts
import { builders, runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    commands: {
        prefix: "!",
        commands: {
            schedule: {
                description: "Show tonight's game",
                execute: ({ reply }) => {
                    const embed = builders.embed()
                        .title("Tonight's game")
                        .description("Meet in the lobby before the first round")
                        .color("#8b7cf8")
                    return reply({ embeds: [embed] })
                },
            },
        },
    },
})
```

Returning the reply's Result lets the [command router](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#command-router) report a failed reply with the command name. If the bot itself stops with a failure, such as a rejected token, `runBot` logs it once and sets `process.exitCode` to 1, so the file needs no error handling of its own

Helpers that take a `client`, such as `postSchedule` above, receive it from the command context, for example with `execute: ({ client, message }) => postSchedule(client, message.channelId)`

Continue with [prefix commands](https://fluxerly.neontechspace.com/docs/nightly/commands.md) to turn these helpers into named bot actions, or see the [Effect workflow examples](https://fluxerly.neontechspace.com/docs/nightly/effect-workflows.md) for the same operations inside an Effect program
