Read communities, members and channels, calculate permissions, grant roles and moderate members deliberately
A community, called a guild in the API, has its own members, roles and channels. Each kind of community data comes from its own request: Fetching a community does not load its members or roles. Fetch the data a decision needs, then let Fluxer check the final request, because a permission calculated locally does not authorize anything by itself
Read a Community and One Member Together
Fetch the community and one member to show their details. Add this helper to a module that already has a bot Client
export async function readGuildMember(client, guildId, userId) {
const [guild, member] = await Promise.all([
client.guilds.fetch(guildId),
client.members.fetch({ guildId, userId }),
])
if (guild.isErr())
return guild
if (member.isErr())
return member
return { guild: guild.value, member: member.value }
}
import type { Client } from "@neontechspace/fluxerly"
export async function readGuildMember(client: Client, guildId: string, userId: string) {
const [guild, member] = await Promise.all([
client.guilds.fetch(guildId),
client.members.fetch({ guildId, userId }),
])
if (guild.isErr()) return guild
if (member.isErr()) return member
return { guild: guild.value, member: member.value }
}
The helper returns the community and the member, or the first failed Result if either request fails. The returned objects are frozen snapshots that do not change afterward
The bot must be a member of the community for Guilds to succeed, and the result does not include the member list. The two reads are separate observations, so neither proves that a later role action will still be allowed
The guild event reports lag and recovery of a community's server through guild and degraded. A degraded server is lagging, not disconnected or unavailable. The notice does not reconnect the client or invalidate community caches
List Channels by Type
Each channel has a type. Comparing it with a Channel constant tells TypeScript which fields that channel has, such as topic for text channels or bitrate for voice channels
import { ChannelType } from "@neontechspace/fluxerly"
function describeChannel(channel) {
const name = channel.name ?? channel.id
switch (channel.type) {
case ChannelType.Text:
return `#${name}: ${channel.topic ?? "no topic"}`
case ChannelType.Announcement:
return `#${name}: announcements, ${channel.topic ?? "no topic"}`
case ChannelType.Voice:
return `${name}: voice at ${channel.bitrate ?? "unknown"} bps`
case ChannelType.Category:
return `${name}: category`
case ChannelType.Link:
return `${name}: link to ${channel.url ?? "an unknown address"}`
case ChannelType.Forum:
case ChannelType.Media:
return `${name}: posts with ${channel.availableTags?.length ?? 0} tags`
case ChannelType.AnnouncementThread:
case ChannelType.PublicThread:
case ChannelType.PrivateThread:
return `${name}: thread in ${channel.parentId}`
case "unknown":
console.warn(`Channel ${channel.id} has type ${channel.rawType}, which this SDK version does not know`)
return `${name}: unsupported type ${channel.rawType}`
}
}
export async function listChannels(client, guildId) {
const channels = await client.channels.fetchAll(guildId)
return channels.map((list) => list.map(describeChannel))
}
import { ChannelType, type Client, type GuildChannel } from "@neontechspace/fluxerly"
function describeChannel(channel: GuildChannel): string {
const name = channel.name ?? channel.id
switch (channel.type) {
case ChannelType.Text:
return `#${name}: ${channel.topic ?? "no topic"}`
case ChannelType.Announcement:
return `#${name}: announcements, ${channel.topic ?? "no topic"}`
case ChannelType.Voice:
return `${name}: voice at ${channel.bitrate ?? "unknown"} bps`
case ChannelType.Category:
return `${name}: category`
case ChannelType.Link:
return `${name}: link to ${channel.url ?? "an unknown address"}`
case ChannelType.Forum:
case ChannelType.Media:
return `${name}: posts with ${channel.availableTags?.length ?? 0} tags`
case ChannelType.AnnouncementThread:
case ChannelType.PublicThread:
case ChannelType.PrivateThread:
return `${name}: thread in ${channel.parentId}`
case "unknown":
console.warn(`Channel ${channel.id} has type ${channel.rawType}, which this SDK version does not know`)
return `${name}: unsupported type ${channel.rawType}`
}
}
export async function listChannels(client: Client, guildId: string) {
const channels = await client.channels.fetchAll(guildId)
return channels.map((list) => list.map(describeChannel))
}
The helper returns a Result with one line for each channel the bot can see. A channel type that Fluxer adds later, which this SDK version does not know yet, arrives with type set to "unknown" and the number Fluxer sent in raw. Handling the "unknown" case makes the switch complete, so TypeScript can check that every case returns a value
Announcement channels have the same topic and message fields as text channels. See announcement channels to create or convert one, publish a message and follow it from a text channel in the same community or another one
Threads, forum channels and media channels have their own types as well. See threads and forum channels for their fields
Reorder Channels and Check the Result
A reorder sends a list of moves that Fluxer applies one after another. If a later move fails, the earlier moves stay applied, and the error does not list which ones finished. Keep the failure, then read the channels to see their actual parents and positions before sending any move again
import { describeError } from "@neontechspace/fluxerly"
export async function reorderChannels(client, guildId, positions) {
const reordered = await client.channels.reorder(guildId, positions)
if (reordered.isOk())
return reordered
const actual = await client.channels.fetchAll(guildId)
if (actual.isErr()) {
console.error("Could not read the channels after a failed reorder:", describeError(actual.error))
}
else {
const layout = actual.value.map(({ id, parentId, position }) => ({ id, parentId, position }))
console.warn("Reorder failed, channels now:", JSON.stringify(layout))
}
return reordered
}
import { describeError, type ChannelPosition, type Client } from "@neontechspace/fluxerly"
export async function reorderChannels(client: Client, guildId: string, positions: readonly ChannelPosition[]) {
const reordered = await client.channels.reorder(guildId, positions)
if (reordered.isOk()) return reordered
const actual = await client.channels.fetchAll(guildId)
if (actual.isErr()) {
console.error("Could not read the channels after a failed reorder:", describeError(actual.error))
} else {
const layout = actual.value.map(({ id, parentId, position }) => ({ id, parentId, position }))
console.warn("Reorder failed, channels now:", JSON.stringify(layout))
}
return reordered
}
The helper returns the failed Result from channels unchanged and logs the layout that channels returns. A failed read is logged as well, and says nothing about whether the moves applied. The list holds the channels the bot can see, without threads. Compare it with the intended layout and send only the moves that are still missing
With the native Effect API, Effect runs the read while the reorder failure stays in the error channel. The read's own failure is caught, so it cannot replace the reorder failure:
import { Effect } from "effect"
import type { ChannelPosition, Client } from "@neontechspace/fluxerly/effect"
export function reorderChannelsWithEffect(client: Client, guildId: string, positions: readonly ChannelPosition[]) {
return client.channels.reorder(guildId, positions).pipe(
Effect.tapError(() =>
client.channels.fetchAll(guildId).pipe(
Effect.flatMap(channels => {
const layout = channels.map(({ id, parentId, position }) => ({ id, parentId, position }))
return Effect.logWarning("Reorder failed, channels now", JSON.stringify(layout))
}),
Effect.catch(error => Effect.logError("Could not read the channels after a failed reorder", error)),
),
),
)
}
Read the Member Verification Level
The community's verification describes its member verification policy. The Guild constants range from None through High. Fluxer retired its phone-verification level, so there is no Very constant. This policy is separate from the two-factor requirement for moderation
Check Permissions Using Available Data
Check whether a member has the Manage permission from data the application already holds. Supply a Permission with the community, the member, every role in member, the implicit everyone role whose ID equals guild and, optionally, the target channel
import { Permissions } from "@neontechspace/fluxerly"
export function hasManageRoles(client, input) {
const calculated = client.permissions.calculate(input)
return (calculated & Permissions.ManageRoles) === Permissions.ManageRoles
}
import { Permissions, type Client, type PermissionInput } from "@neontechspace/fluxerly"
export function hasManageRoles(client: Client, input: PermissionInput) {
const calculated = client.permissions.calculate(input)
return (calculated & Permissions.ManageRoles) === Permissions.ManageRoles
}
The helper returns true or false from the supplied data. It throws Guild with reason input if that data is inconsistent or incomplete
A thread has no permission overwrites of its own, because Fluxer calculates its permissions in its parent channel. To check a thread, supply the thread as channel and its parent channel as parent. The calculation then uses the parent's overwrites and grants Send exactly when Send is granted, as Fluxer does. A thread without its parent, or with another channel as parent, fails with reason input. The permissions method reads the parent itself when its channel names a thread
To reuse cached data, call client. It takes each part from an enabled cache and fetches only what is missing, which is how guards decides. Cached parts follow the gateway events applied so far and can come from different moments, so keep the default fresh read for the moment right before an action where a stale grant matters
To see what the bot can do in every channel, call permissions with the bot's own ID. It reads the community once and calculates each channel locally, instead of reading again for every channel
import { permissionBits } from "@neontechspace/fluxerly"
export async function channelsWhereBotCanSend(client, guildId) {
const botId = client.users.getSelf()?.id
if (botId === undefined)
return []
const result = await client.permissions.fetchChannels({ guildId, userId: botId })
if (result.isErr())
throw result.error
return result.value
.filter(({ permissions }) => permissionBits.hasAll(permissions, ["ViewChannel", "SendMessages"]))
.map(({ channel }) => channel.id)
}
import { permissionBits, type Client } from "@neontechspace/fluxerly"
export async function channelsWhereBotCanSend(client: Client, guildId: string) {
const botId = client.users.getSelf()?.id
if (botId === undefined) return []
const result = await client.permissions.fetchChannels({ guildId, userId: botId })
if (result.isErr()) throw result.error
return result.value
.filter(({ permissions }) => permissionBits.hasAll(permissions, ["ViewChannel", "SendMessages"]))
.map(({ channel }) => channel.id)
}
A timed-out member keeps their permission bits, while Fluxer blocks sending messages, editing and publishing their own messages, reacting, uploading, typing, changing their own nickname, writing in threads and joining voice. The client method returns "active", "none" or "unknown". A member without the communication field is "unknown", and members always includes it. To show a permission to people, permission gives Fluxer's own name for it, "Manage community". These are community-scope titles, and a channel's permission screen calls Manage "Manage channel" and Manage "Manage permissions"
What Does a Calculated Permission Not Cover?
The permissions method makes no request and reads no cache. Its result does not establish channel visibility, role hierarchy, multi-factor authentication requirements, application authorization or Fluxer's final decision.
In a thread, the result also does not show whether the member can see a private thread, which Fluxer allows only to its members and to members who can manage threads, or whether the thread is archived or locked.
For prefix commands, the guards guard in the commands guide performs this check from cached or fetched data
Change Part of a Channel Overwrite
The channels method replaces both bit sets of an overwrite. The permission helper changes only the named permissions and keeps every other bit
import { permissionBits } from "@neontechspace/fluxerly"
export async function quietRole(client, channelId, roleId, current) {
const { allow, deny } = permissionBits.patchOverwrite(current, {
deny: ["SendMessages", "AddReactions"],
inherit: ["ViewChannel"],
})
return await client.channels.setPermissionOverwrite(channelId, { id: roleId, type: "role", allow, deny })
}
import { permissionBits, type Client, type PermissionOverwrite } from "@neontechspace/fluxerly"
export async function quietRole(client: Client, channelId: string, roleId: string, current?: PermissionOverwrite) {
const { allow, deny } = permissionBits.patchOverwrite(current, {
deny: ["SendMessages", "AddReactions"],
inherit: ["ViewChannel"],
})
return await client.channels.setPermissionOverwrite(channelId, { id: roleId, type: "role", allow, deny })
}
Pass the overwrite as read from the channel, or undefined when there is none. In the change, allow also stops denying, deny also stops allowing and inherit clears both. A permission in more than one list throws Helper. A result with allow and deny both 0n has no effect, so remove such an overwrite with channels
Check What Fluxer Will Refuse in an Overwrite
Unless the bot has Administrator, an overwrite change can allow, or stop denying, only permissions the bot has in the channel. Fluxer refuses anything more with missing. The permission helper returns the bits Fluxer would refuse, so the application can check a change before sending it. The channels reference describes how each method applies the rule
import { permissionBits } from "@neontechspace/fluxerly"
export async function allowTtsIfPossible(client, guildId, channelId, roleId, current) {
const botId = client.users.getSelf()?.id
if (botId === undefined)
return
const held = await client.permissions.fetch({ guildId, userId: botId, channelId })
if (held.isErr())
return held
const { allow, deny } = permissionBits.patchOverwrite(current, { allow: ["SendTtsMessages"] })
if (permissionBits.refusedGrants(current, { allow, deny }, held.value) !== 0n)
return
return await client.channels.setPermissionOverwrite(channelId, { id: roleId, type: "role", allow, deny })
}
import { permissionBits, type Client, type PermissionOverwrite } from "@neontechspace/fluxerly"
export async function allowTtsIfPossible(
client: Client,
guildId: string,
channelId: string,
roleId: string,
current: PermissionOverwrite | undefined,
) {
const botId = client.users.getSelf()?.id
if (botId === undefined) return
const held = await client.permissions.fetch({ guildId, userId: botId, channelId })
if (held.isErr()) return held
const { allow, deny } = permissionBits.patchOverwrite(current, { allow: ["SendTtsMessages"] })
if (permissionBits.refusedGrants(current, { allow, deny }, held.value) !== 0n) return
return await client.channels.setPermissionOverwrite(channelId, { id: roleId, type: "role", allow, deny })
}
Find the Channels a Category Change Will Rewrite
When a category's overwrites change, Fluxer copies the new overwrites to every child whose overwrites equal the category's old ones, with no way to opt out. The permission helper compares two lists as Fluxer does, so the application can find those children before the change
import { permissionBits } from "@neontechspace/fluxerly"
export function syncedChildren(category, channels) {
const overwrites = category.permissionOverwrites
// Unknown until the category is read with its overwrites, such as through channels.fetch
if (overwrites === undefined)
return undefined
return channels.filter(({ parentId, permissionOverwrites }) => parentId === category.id &&
permissionOverwrites !== undefined &&
permissionBits.sameOverwrites(permissionOverwrites, overwrites))
}
import { permissionBits, type GuildChannel } from "@neontechspace/fluxerly"
export function syncedChildren(category: GuildChannel, channels: readonly GuildChannel[]) {
const overwrites = category.permissionOverwrites
// Unknown until the category is read with its overwrites, such as through channels.fetch
if (overwrites === undefined) return undefined
return channels.filter(
({ parentId, permissionOverwrites }) =>
parentId === category.id &&
permissionOverwrites !== undefined &&
permissionBits.sameOverwrites(permissionOverwrites, overwrites),
)
}
Grant a Configured Opt-in Role
Call this helper from a command in a community that lets a member opt into a role. Take role from trusted application configuration, not from command text, and configure only roles that members may assign to themselves. Fluxer checks the role hierarchy on the request: The bot's highest role must rank above the role, whichever member receives it
export async function grantOptInRole(client, message, roleId) {
if (message.author.isBot || !message.guildId)
return
return await client.members.addRole({ guildId: message.guildId, userId: message.author.id }, roleId)
}
import type { Client, Message } from "@neontechspace/fluxerly"
export async function grantOptInRole(
client: Client,
message: Message,
roleId: string,
) {
if (message.author.isBot || !message.guildId) return
return await client.members.addRole({ guildId: message.guildId, userId: message.author.id }, roleId)
}
The helper gives the configured role to the command's author. It returns undefined when the message is not from a person in a community, and otherwise the Result of the role request. Fluxer enforces the role hierarchy, management restrictions and the bot's Manage permission on add, so the command must inspect that Result and apply any additional access policy of the application
Give a Role an Icon
A role shows an image or a single Unicode emoji, never both. Setting one replaces the other, and null removes it
import { assets, orThrow } from "@neontechspace/fluxerly"
export async function setRoleEmoji(client, role) {
return orThrow(await client.roles.edit(role, { unicodeEmoji: "🛡️" }))
}
export async function setRoleImage(client, role, png) {
const updated = orThrow(await client.roles.edit(role, { icon: `data:image/png;base64,${png.toString("base64")}` }))
return assets.roleIcon(updated)
}
import { assets, orThrow, type Client, type RoleReference } from "@neontechspace/fluxerly"
export async function setRoleEmoji(client: Client, role: RoleReference) {
return orThrow(await client.roles.edit(role, { unicodeEmoji: "🛡️" }))
}
export async function setRoleImage(client: Client, role: RoleReference, png: Buffer) {
const updated = orThrow(await client.roles.edit(role, { icon: `data:image/png;base64,${png.toString("base64")}` }))
return assets.roleIcon(updated)
}
Fluxer accepts a still image of at most 256 KiB and from 32 through 1024 pixels on each side. The bot needs Manage, and its highest role must rank above the role
Moderate Members
Moderation actions follow the same pattern as roles. A moderation command should also check the moderator who typed it, so a member cannot use the bot against someone who ranks above them. The actor option of fetch checks that person instead of the bot. This helper bans a member for one day and deletes their messages from the last hour
export async function banForOneDay(client, target, moderatorId) {
const [bot, moderator] = await Promise.all([
client.members.fetchCanManage(target),
client.members.fetchCanManage(target, { actorUserId: moderatorId }),
])
if (bot.isErr())
return bot
if (moderator.isErr())
return moderator
if (!bot.value || !moderator.value)
return
return await client.members.ban(target, {
durationMs: 24 * 60 * 60 * 1000,
deleteMessagesMs: 60 * 60 * 1000,
})
}
import type { Client, MemberReference } from "@neontechspace/fluxerly"
export async function banForOneDay(client: Client, target: MemberReference, moderatorId: string) {
const [bot, moderator] = await Promise.all([
client.members.fetchCanManage(target),
client.members.fetchCanManage(target, { actorUserId: moderatorId }),
])
if (bot.isErr()) return bot
if (moderator.isErr()) return moderator
if (!bot.value || !moderator.value) return
return await client.members.ban(target, {
durationMs: 24 * 60 * 60 * 1000,
deleteMessagesMs: 60 * 60 * 1000,
})
}
The helper returns undefined when the bot or the moderator does not rank above the target, and otherwise the Result of a rank check or the ban. Omit duration for a permanent ban. Both durations are in milliseconds and must be whole seconds. Fluxer checks the bot's permissions, such as Ban Members, and the role hierarchy for each request
The SDK checks ban options before sending anything. An invalid option fails with a Guild whose reason is input, and its input names the field, such as input. A duration command argument can be passed to duration unchanged when it sets whole, min: 60_000 and max: 63_072_000_000
The members and members methods take the same member reference. Use members and members to review and lift bans. In a prefix command, guards makes sure the person who typed the command also has that permission
Compare Ranks from Data Already Held
The fetch method fetches fresh community, member and role data for each check. When the application already holds that data, the hierarchy helpers compare it without a request: hierarchy checks one member against another, and hierarchy and hierarchy order two roles. Like fetch, they check rank only, not permissions. For roles, hierarchy returns a member's highest explicit role, and hierarchy checks whether the actor outranks a role. Administrator does not bypass rank, so also check the Manage permission for the permission half
The caches can supply that data. With cache enabled, roles returns a community's roles once the cache knows the list is complete, such as after its guild or a roles while connected, and undefined otherwise. With cache enabled, members returns the bot's own member, which each guild and members stores. The member that Fluxer attaches to message, reaction and typing events refreshes the cache for any member who speaks, reacts or types, whichever message the client selects, while a voice state's member is never stored because it can lag behind role edits
Communities That Require Two-Factor Authentication
A community owner can require two-factor authentication (MFA) for moderation. In such a community, only the owner and accounts with two-factor authentication enabled can use Administrator, Ban, Kick, Manage, Manage, Manage, Manage, Manage, Manage and Moderate. That covers kicks, bans, timeouts, role changes, deleting other people's messages, moderating threads and changes to channels, webhooks and community settings
A bot cannot enable two-factor authentication itself. Its account follows the account that owns its application, so the bot can act in these communities only when that owner account has two-factor authentication enabled, or when the bot owns the community
Otherwise Fluxer refuses the request with HTTP 400 after confirming that the bot holds the permission. To tell this failure apart from other rejections, check errors. The errors function, imported with errors from the SDK, returns the name of Fluxer's error code, or undefined when the error carries no code that the SDK recognizes. Repeating the request fails the same way until the application owner enables two-factor authentication. A calculated permission from permissions or a passing guards guard does not rule this out, because both read role permissions and not the community's requirement
To check a community in advance, read the mfa of the community from guilds or a guild event. The value Guild means the community has this requirement, and Guild means it does not. The SDK cannot see whether the application owner has two-factor authentication enabled, so an elevated level means a moderation request can fail, not that it will. Fluxer's API reference describes MFA levels and elevated permissions
Read Audit Records
Use audit or the bounded audit scan with View permission. Recorded message-deletion durations are exposed in options, rounded to whole milliseconds. A duration that would overflow the safe integer range fails the REST page rather than returning partial entries. The same invalid duration in a gateway audit event follows the configured malformed-dispatch policy
Voice Controls Versus a Voice Connection
Fluxerly supports observing voice states in communities and moving, disconnecting, community muting and community deafening members who are already in a voice channel. These controls remain subject to Fluxer's permissions
A voice state, whether from voice, a voice or a private call, also reports is for end-to-end encrypted voice, is for a connection that does not pass through LiveKit, and version, Fluxer's counter for that participant's state. An omitted flag reads as false
Bots cannot join voice channels or send and receive audio or video yet. That feature waits for Fluxer's voice update and an SDK implementation after it. A Connect or Speak permission describes what Fluxer allows, not an SDK feature, and the bot's gateway connect() receives events rather than opening a voice connection. See member voice controls for the current operations
Wire It Up
In a run bot, the command context provides the client and the incoming message. This bot answers !notify by granting the role configured in OPT_
import { guards, runBot } from "@neontechspace/fluxerly"
const optInRoleId = process.env.OPT_IN_ROLE_ID
await runBot({
token: process.env.FLUXER_BOT_TOKEN,
commands: {
prefix: "!",
commands: {
notify: {
description: "Join the announcements role",
guard: guards.guildOnly(),
execute: async ({ client, message, reply }) => {
if (!optInRoleId || !message.guildId)
return reply("The opt-in role is not configured")
const member = { guildId: message.guildId, userId: message.author.id }
const added = await client.members.addRole(member, optInRoleId)
return added.isErr() ? added : reply("Role added")
},
},
},
},
})
import { guards, runBot } from "@neontechspace/fluxerly"
const optInRoleId = process.env.OPT_IN_ROLE_ID
await runBot({
token: process.env.FLUXER_BOT_TOKEN,
commands: {
prefix: "!",
commands: {
notify: {
description: "Join the announcements role",
guard: guards.guildOnly(),
execute: async ({ client, message, reply }) => {
if (!optInRoleId || !message.guildId) return reply("The opt-in role is not configured")
const member = { guildId: message.guildId, userId: message.author.id }
const added = await client.members.addRole(member, optInRoleId)
return added.isErr() ? added : reply("Role added")
},
},
},
},
})
Returning a failed Result from execute reports it with the command name, without stopping the bot. In a direct message, guards denies !notify, and the router explains why, as run answers rejected commands by default