On This Page

Send Messages, Embeds and Files

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 and call them from a command or message handler. They use the bot's existing client rather than opening another connection. The last section 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: Check isErr() first, then read value

import { describeError } from "@neontechspace/fluxerly"

export async function replyHello(client, 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
}
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

What Does a Failed Send Say?

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 into one record per minute by default. The error and log codes page lists every code with its meaning

Skip Join Notices and Other System Messages

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

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 for publishing messages and handling their copies

Sends and edits accept only MessageFlags.SuppressEmbeds and MessageFlags.SuppressNotifications. 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

import { format } from "@neontechspace/fluxerly"

export async function notifyParticipant(client, channelId, userId) {
    return await client.messages.send(channelId, {
        content: `${format.userMention(userId)} The game is starting`,
        allowedMentions: { users: [userId] },
    })
}
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 can also accept either an ID or a mention

Send a Text-to-Speech Message

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

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

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

    return await client.messages.edit(sent.value, "Report ready")
}
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

What if the Send or the Edit Fails?

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 explains these uncertain writes and when a retry is safe

Build an Embed

An 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

import { builders } from "@neontechspace/fluxerly"

export async function postSchedule(client, channelId) {
    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] })
}
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. 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

import { builders } from "@neontechspace/fluxerly"

export async function sendChecklist(client, channelId) {
    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)
}
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 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

import { openAsBlob } from "node:fs"
import { basename } from "node:path"
import { builders } from "@neontechspace/fluxerly"

export async function sendReport(client, channelId, path) {
    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)
}
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

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

import { describeError } from "@neontechspace/fluxerly"

export async function readFirstSmallFile(client, 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
}
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

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

export async function sendRequestedDm(client, recipientId) {
    return await client.directMessages.send(recipientId, "The requested game instructions are ready")
}
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. Creating groups is outside the SDK's supported operations. See private conversations 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

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] })
                },
            },
        },
    },
})
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 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 to turn these helpers into named bot actions, or see the Effect workflow examples for the same operations inside an Effect program