Skip to content

Send permission bit fields as decimal, not binary - #134

Open
mikield wants to merge 1 commit into
dc-Ragnarok:masterfrom
mikield:fix/permission-bitfields
Open

Send permission bit fields as decimal, not binary#134
mikield wants to merge 1 commit into
dc-Ragnarok:masterfrom
mikield:fix/permission-bitfields

Conversation

@mikield

@mikield mikield commented Aug 22, 2026

Copy link
Copy Markdown

The bug

Bitwise::getBitSet() returns decbin(), and three payloads send its result straight to Discord:

  • CommandBuilder::setDefaultMemberPermissions()default_member_permissions
  • EditPermissionsBuilder::setAllow()allow
  • EditPermissionsBuilder::setDeny()deny

Discord reads all three as decimal. So asking for ADMINISTRATOR, which is 1 << 3, sends "1000" — and Discord reads that as one thousand:

$b = Bitwise::from(Permission::ADMINISTRATOR);

$b->get();        // 8   — what you asked for
$b->getBitSet();  // "1000" — what was sent
(int) "1000";     // 1000 — what Discord stores

Decimal 1000 is ADMINISTRATOR | MANAGE_GUILD | ADD_REACTIONS | VIEW_AUDIT_LOG | PRIORITY_SPEAKER | STREAM.

Every command registered with default permissions has been granting five permissions nobody asked for, and channel permission overwrites have been allowing and denying the wrong things.

The fix

All three send the decimal value, and the matching getters read it back the same way rather than through fromBitSet.

getBitSet() itself is untouched — a binary representation is a reasonable thing to expose and it has its own test. It just isn't what goes on the wire.

Why it survived

The test asserted the payload equalled getBitSet():

$this->assertEquals($permissions->getBitSet(), $commandBuilder->get()['default_member_permissions']);

That describes the behaviour rather than the requirement, so it passed either way. Both tests now assert the literal value Discord expects.

While there: EditPermissionsBuilderTest built its Bitwise with new Bitwise(1 << 1, 1 << 2, 1 << 3). The constructor takes a single int, so the second and third arguments were silently dropped and the test only ever exercised one flag. It now uses Bitwise::from.

Notes

This is split out of #133 deliberately — it's a small correctness fix against master that stands on its own and is worth releasing without waiting on that larger branch. #133 contains this same commit; whichever merges first, the other rebases cleanly.

composer test (526), composer cs and util/verify-namespacing.sh all pass.

Bitwise::getBitSet() returns decbin(), and three payloads were sending its
result to Discord: a command's default_member_permissions, and the allow and
deny of a channel permission overwrite. Discord reads all three as decimal.

Asking for ADMINISTRATOR, which is 1 << 3, therefore sent "1000". Discord read
that as one thousand, which is ADMINISTRATOR together with MANAGE_GUILD,
ADD_REACTIONS, VIEW_AUDIT_LOG, PRIORITY_SPEAKER and STREAM. Every command
registered with default permissions has been granting five permissions nobody
asked for, and channel overwrites have been allowing and denying the wrong
things.

The three now send the decimal value, and the matching getters read it back the
same way rather than through fromBitSet.

getBitSet itself is untouched: a binary representation is a reasonable thing to
expose, and it has its own test. It just is not what goes on the wire.

The reason this survived is that the test asserted the payload equalled
getBitSet(), so it described the behaviour rather than the requirement and
passed either way. Both tests now assert the literal value Discord expects.

While there, EditPermissionsBuilderTest built its Bitwise with
new Bitwise(1 << 1, 1 << 2, 1 << 3). The constructor takes a single int, so the
second and third arguments were dropped and the test only ever exercised one
flag; it now uses Bitwise::from.
mikield added a commit to Tempcord/framework that referenced this pull request Aug 22, 2026
…sions

Two fixes that come from the same place.

Registration used to POST each command individually. That costs a request per
command, and Discord's create endpoint only ever adds: a command deleted from
the code stayed registered forever and users kept seeing it. Registration now
groups commands into the sets Discord replaces atomically — the global set, and
one set per guild — and sends each as a single PUT. Anything absent from the
set is removed, which is what makes a deleted command actually disappear.

#[Command(permissions: [])] was collected all the way into CommandDefinition
and then never read, so setting it did nothing. It is now typed as Fenrir's
Permission enum rather than a list of strings, and reaches the payload.

Two things are worth explaining rather than leaving to be discovered:

The permission bit field is written onto the payload directly instead of going
through CommandBuilder::setDefaultMemberPermissions, which sends the binary
representation of the bit field where Discord expects the decimal one. Asking
for ADMINISTRATOR that way grants five permissions nobody asked for. Fixed
upstream in dc-Ragnarok/Fenrir#134; the direct write is correct either way, so
it does not need reverting when that lands.

Fenrir has no bulk overwrite method yet and keeps its HTTP client private on
both Discord and Rest, so CommandRegistrar builds one for itself from the token
already in TempcordConfig. Registration runs once, before the gateway opens, so
the two clients never compete for a rate limit bucket. The method is added in
dc-Ragnarok/Fenrir#133 and this can move over to it once released.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant