# Read communities and plan permission-aware work

Read communities, members and channels, calculate permissions, grant roles and moderate members deliberately

A [community](https://fluxerly.neontechspace.com/docs/glossary.md#guild), called a guild in the API, has its own [members](https://fluxerly.neontechspace.com/docs/glossary.md#member), [roles](https://fluxerly.neontechspace.com/docs/glossary.md#role) 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`](https://fluxerly.neontechspace.com/docs/api/js-ts/client/)

```ts
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](https://fluxerly.neontechspace.com/docs/glossary.md#result) if either request fails. The returned objects are [frozen snapshots](https://fluxerly.neontechspace.com/docs/glossary.md#frozen-snapshot) that do not change afterward

The bot must be a member of the community for [`Guilds.fetch`](https://fluxerly.neontechspace.com/docs/api/js-ts/guilds-and-members/#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

```ts
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 `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](https://fluxerly.neontechspace.com/docs/announcement-channels.md) 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](https://fluxerly.neontechspace.com/docs/threads.md) and [forum channels](https://fluxerly.neontechspace.com/docs/forum-channels.md) 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

```ts
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:

```ts
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](https://fluxerly.neontechspace.com/docs/guilds-and-permissions.md#communities-that-require-two-factor-authentication)

## Check permissions using available data

Check whether a member has the `ManageRoles` permission from data the application already holds. Supply a [`PermissionInput`](https://fluxerly.neontechspace.com/docs/api/js-ts/roles-and-permissions/#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

```ts
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`](https://fluxerly.neontechspace.com/docs/api/js-ts/roles-and-permissions/#permissionhelpers-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`](https://fluxerly.neontechspace.com/docs/api/js-ts/roles-and-permissions/#permissionhelpers-fetchchannels) with the bot's own ID. It reads the community once and calculates each channel locally, instead of reading again for every channel

```ts
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"

<details>
<summary>What does a calculated permission not cover?</summary>

The [`permissions.calculate`](https://fluxerly.neontechspace.com/docs/api/js-ts/roles-and-permissions/#permissionhelpers-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](https://fluxerly.neontechspace.com/docs/commands.md) performs this check from cached or fetched data

</details>

## 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

```ts
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`](https://fluxerly.neontechspace.com/docs/api/js-ts/roles-and-permissions/#permissionbits) helper returns the bits Fluxer would refuse, so the application can check a change before sending it. The [`channels`](https://fluxerly.neontechspace.com/docs/api/js-ts/channels/#channels-setpermissionoverwrite) reference describes how each method applies the rule

```ts
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

```ts
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](https://fluxerly.neontechspace.com/docs/glossary.md#role-hierarchy) on the request: The bot's highest role must rank above the role, whichever member receives it

```ts
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`](https://fluxerly.neontechspace.com/docs/api/js-ts/guilds-and-members/#members-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

```ts
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

```ts
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](https://fluxerly.neontechspace.com/docs/commands.md#convert-arguments-before-execution) can be passed to `durationMs` unchanged when it sets `wholeSeconds: true`, `min: 60_000` and `max: 63_072_000_000`

The [`members.kick`](https://fluxerly.neontechspace.com/docs/api/js-ts/guilds-and-members/#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

<details>
<summary>Compare ranks from data already held</summary>

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

</details>

## Communities that require two-factor authentication

A community owner can require two-factor authentication ([MFA](https://fluxerly.neontechspace.com/docs/glossary.md#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`](https://fluxerly.neontechspace.com/docs/api/js-ts/guilds-and-members/#guild-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](https://docs.fluxer.app/http-api/guilds/#mfa-levels) and [elevated permissions](https://docs.fluxer.app/http-api/permissions/#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](https://fluxerly.neontechspace.com/docs/api/js-ts/guilds-and-members/#members-move) 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`

```ts
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
