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 run
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 is 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 describe 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 (.
The error's hint suggests a next step. For a missing permission, details lists the permissions Fluxer checks for the operation, such as ["View 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 holds it. When Fluxer's answer does not have the expected shape, details 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, 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 on 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 with a Message constant to tell them apart. For example, message is a join notice, and ordinary messages are Message or Message
A message or message event for a community message can carry message, 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 enabled, members then answers for that author without a request. The same member arrives as reaction on message and message events and as typing on typing events. A member that Fluxer sends in a form the SDK cannot read is left off, and the event still arrives
A message event, and no other message event, can also carry message, the numeric type of the channel, with 1 for a direct message and 3 for a group direct message, message, which Fluxer sends only as true for a here mention, and message, the nicknames of a group direct message keyed by user ID. REST reads and message events never carry them, and the message cache does not keep them. A message event can carry deletion, 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, which points at a replied-to or forwarded message, or at the source of an announcement copy or follow notice. Its channel is always present, but id is optional. Check id, then build a Message 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 Message and Message. The SDK rejects any other flag before sending the request. Fluxer sets Crossposted, Is and Source itself
Mention Someone Deliberately
Create a mention with format, then allow a notification for that specific user. Role and channel mentions have matching role and channel 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, 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, messages, direct or the reply helper of a run 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 message 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 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 or thumbnail can use attachment:// 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 Embed 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, open 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 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 direct 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, direct 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 run 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, run logs it once and sets process to 1, so the file needs no error handling of its own
Helpers that take a client, such as post above, receive it from the command context, for example with execute: (
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