On This Page

Read Communities and Plan Permission-Aware Work

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.fetch 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 guildHealthUpdate event reports lag and recovery of a community's server through guildId 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 ChannelType 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:
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:

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 rawType. 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.reorder unchanged and logs the layout that channels.fetchAll 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.tapError 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 verificationLevel describes its member verification policy. The GuildVerificationLevels constants range from None through High. Fluxer retired its phone-verification level, so there is no VeryHigh constant. This policy is separate from the two-factor requirement for moderation

Check Permissions Using Available Data

Check whether a member has the ManageRoles permission from data the application already holds. Supply a PermissionInput with the community, the member, every role in member.roleIds, the implicit everyone role whose ID equals guild.id 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 GuildOperationError 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 parentChannel. The calculation then uses the parent's overwrites and grants SendMessages exactly when SendMessagesInThreads is granted, as Fluxer does. A thread without its parent, or with another channel as parentChannel, fails with reason input. The permissions.fetch method reads the parent itself when its channelId names a thread

To reuse cached data, call client.permissions.fetch(target, { preferCache: true }). It takes each part from an enabled cache and fetches only what is missing, which is how guards.requirePermissions 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.fetchChannels 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.members.timeoutState(member) method returns "active", "none" or "unknown". A member without the communicationDisabledUntil field is "unknown", and members.fetch always includes it. To show a permission to people, permissionBits.label("ManageGuild") gives Fluxer's own name for it, "Manage community". These are community-scope titles, and a channel's permission screen calls ManageChannels "Manage channel" and ManageRoles "Manage permissions"

What Does a Calculated Permission Not Cover?

The permissions.calculate 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.requirePermissions guard in the commands guide performs this check from cached or fetched data

Change Part of a Channel Overwrite

The channels.setPermissionOverwrite method replaces both bit sets of an overwrite. The permissionBits.patchOverwrite 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 HelperError. A result with allow and deny both 0n has no effect, so remove such an overwrite with channels.removePermissionOverwrite

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 missingPermissions. The permissionBits.refusedGrants 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 permissionBits.sameOverwrites(first, second) 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 roleId 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 ManageRoles permission on addRole, 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 ManageRoles, 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 actorUserId option of fetchCanManage 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 durationMs 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 GuildOperationError whose reason is input, and its inputValidation.path names the field, such as input.durationMs. A duration command argument can be passed to durationMs unchanged when it sets wholeSeconds: true, min: 60_000 and max: 63_072_000_000

The members.kick and members.timeout methods take the same member reference. Use members.unban and members.fetchBans to review and lift bans. In a prefix command, guards.requirePermissions(["BanMembers"]) makes sure the person who typed the command also has that permission

Compare Ranks from Data Already Held

The fetchCanManage 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.canManage({ guild, actor, target, roles }) checks one member against another, and hierarchy.compare and hierarchy.isAbove order two roles. Like fetchCanManage, they check rank only, not permissions. For roles, hierarchy.highestRole(member, roles) returns a member's highest explicit role, and hierarchy.canManageRole({ guild, actor, role, roles }) checks whether the actor outranks a role. Administrator does not bypass rank, so also check the ManageRoles permission for the permission half

The caches can supply that data. With cache.roles enabled, roles.getAll(guildId) returns a community's roles once the cache knows the list is complete, such as after its guildCreate or a roles.fetchAll while connected, and undefined otherwise. With cache.members enabled, members.getSelf(guildId) returns the bot's own member, which each guildCreate and members.fetchSelf stores. The member that Fluxer attaches to message, reaction and typing events refreshes the cache for any member who speaks, reacts or types, whichever messageFields 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, BanMembers, KickMembers, ManageChannels, ManageGuild, ManageMessages, ManageRoles, ManageThreads, ManageWebhooks and ModerateMembers. 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.apiCode(result.error) === "twoFactorRequired". The errors.apiCode 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.calculate or a passing guards.requirePermissions 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 mfaLevel of the community from guilds.fetch or a guildCreate event. The value GuildMfaLevels.Elevated means the community has this requirement, and GuildMfaLevels.None 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 auditLogs.fetchPage or the bounded auditLogs.iterate scan with ViewAuditLog permission. Recorded message-deletion durations are exposed in options.deleteMessagesMs, 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 voiceStateUpdate, a voiceStateSnapshot or a private call, also reports isE2eeCapable for end-to-end encrypted voice, isPeerToPeer 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 runBot bot, the command context provides the client and the incoming message. This bot answers !notify by granting the role configured in OPT_IN_ROLE_ID

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.guildOnly() denies !notify, and the router explains why, as runBot answers rejected commands by default