majesticdev / commandnet-discord-plugin
Multi-guild Discord bridge for the Command Net plugin suite: syncs Discord roles, usernames, slash commands and notifications across one connection per unit's Discord server.
Package info
github.com/Spearhead-Gaming/commandnet-discord-plugin
Type:forumify-plugin
pkg:composer/majesticdev/commandnet-discord-plugin
Requires
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpstan/phpstan-symfony: ^2.0
- slevomat/coding-standard: *
- squizlabs/php_codesniffer: ^3.13
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-02 04:23:24 UTC
README
Multi-guild Discord bridge for the Command Net suite — one connection per unit's Discord server, not one bot shared by everyone.
A forumify plugin bridging the Command Net suite to Discord — across as many Discord servers as your community has, not just one. Every unit can get its own private server with its own role mapping and its own notification channels, instead of everyone sharing one community-wide bot connection.
Built for a specific MILSIM community's forumify install, using forumify's own forumify-discord-plugin as reference material while building this from scratch; not a general-purpose skeleton, and not a redistribution of that plugin.
Contents
- Requirements
- Install
- Entities
- Admin
- Permissions
- Importing Discord members
- Patrol posts
- Design notes
- Known gaps
- Works well with
- Deploying DM invites
📋 Requirements
- PHP 8.4 or newer
- A forumify 1.2.x install
- MySQL (for the migration in
migrations/) commandnet-discord-botrunning somewhere reachable from this install — this plugin talks to it over HTTP, it doesn't run a Discord gateway connection itself
📦 Install
composer require majesticdev/commandnet-discord-plugin
Then, from the forumify install:
bin/console forumify:plugins:refresh bin/console forumify:plugins:activate majesticdev/commandnet-discord-plugin bin/console doctrine:migrations:migrate
🧩 Entities
| Entity | Notes |
|---|---|
DiscordConnection |
One Discord server the bot has been invited to — the community server, or a unit's private one. Guild ID, invite link, invite/ops-log/announcements/patrols channel IDs, active flag. |
PatrolMessage |
Which Discord message a patrol was posted as, per server, so the post can be edited later. Holds the patrol as a plain id (commandnet-plugin is optional). |
DiscordRoleMapping |
"Grant/revoke this Discord role whenever a user gains/loses this forumify Role" — scoped to one DiscordConnection, so the same forumify Role can map to a different Discord role in each unit's server. |
🛠️ Admin
- Admin → Command Net Discord → Connections — CRUD for
DiscordConnection, with an embedded, add/remove-able role-mapping table per connection. - Admin → Command Net Discord → Settings — bot-wide toggles only (force-connect Discord account, force users to join the server, sync display names, calendar cross-posting). Invite links and role mapping used to live here too; they moved to per-connection once a single server stopped being a safe assumption.
🔐 Permissions
Checked as discord.<area>.<action>.
| Permission | Grants |
|---|---|
discord.admin.connections.view / .manage |
View / create, edit, and delete Discord connections and their role mappings. |
👥 Importing Discord members
Forumify's built-in user role only applies to accounts that exist, so a Discord member who has
never logged in has no permissions. To create accounts up front:
php bin/console discord:members:import # dry run: reports what it would do php bin/console discord:members:import --apply # creates the accounts
For every human member of every active connection's guild it creates a password-less, email-less
account linked to their Discord id. When they later log in with Discord, forumify finds that linked
account and reuses it. Members who are already linked are left alone. Requires a bot that supports
GET /data?type=guildMembers (commandnet-discord-bot).
Members whose Discord username or display name equals an existing forum username are skipped and listed, because a placeholder linked next to their real account would leave them with two once they connect Discord. Re-run the command after new people join the server.
Avoiding duplicate accounts
discord:members:import --matchlinks a member to the forum account whose username exactly equals their Discord username (display names are never used, since anyone can set theirs to anything). Run it without--applyfirst and read the list.discord:members:link <forum-username> <discord-id> [--apply]does the same for one person, for cases the exact match misses (Discord ids are visible with Developer Mode on).- If an imported placeholder already holds that Discord id, it is deleted and the id moves to the real account, but only when the placeholder is untouched: no email, never active, no roles. Otherwise nothing changes and the command says why. An account that already has a different Discord link is left alone too.
- Exact name matching can still be wrong if a different person holds that name on Discord, so
review the dry run before
--apply.
📣 Patrol posts
With commandnet-plugin installed, set Patrols Channel ID on a Discord connection and every new
patrol is posted there as an embed (when, where, leader, details, and who has joined) with Join,
Leave and Submit AAR buttons. The buttons run the existing /command-net-patrol-join,
-leave and -aar commands, so the same permissions and rules apply, and replies are private to
whoever clicked. Submit AAR opens a form.
The post is edited as people join or leave, and loses its buttons (and is marked Cancelled or Completed) when the patrol is cancelled or its AAR is filed. A connection with no patrols channel gets the plain text announcement in its announcements channel instead, with no buttons.
Deleting a patrol deletes its posts. However the patrol is deleted (the admin list, or a purge
in commandnet-plugin), its post is removed from every server's patrols channel and the stored
message records are dropped. This needs a bot that supports DeleteMessage. If a post cannot be
deleted (the bot is down, or has lost its permission), that is logged and the post is left for a
person to remove by hand.
Needs a bot that supports buttons and EditMessage (commandnet-discord-bot). Updating the post
runs after the patrol is saved, and a Discord failure is logged, never thrown, so it can never stop
a patrol being created. The plugin adds one migration: a patrols_channel_id column and the
discord_patrol_message table.
🧠 Design notes
One bot, many connections — and the seams other plugins build on
- One bot, many connections.
commandnet-discord-botis a single Discord application invited into every server; this plugin never assumes a guild ID, it always asks "which connections apply" and loops. BotService::updateRoles()/updateUsername()walk every active connection themselves rather than being told which one — callers (UpdateUserListener, etc.) never changed when this went from single-server to multi-server.BotService::postAnnouncement()is the generic "tell every server's announcements channel something happened" primitive.CalendarEventListeneruses it for calendar cross-posting today; it's the seam future plugins (id-card, a server manager, S3 tools) are meant to build on instead of talking to the bot directly.PatrolAnnouncementListenercross-posts a Command Net patrol the same way, on itsOperationpostPersist rather than a calendar event, so a patrol announces once whether it's posted from the web or from/command-net-patrol-create.DiscordPatrolReminderNotifierdecorates Command Net's ownPatrolReminderNotifier(a no-op there without this plugin) to also echo the AAR due/overdue reminder to Discord,@mention-ing the leader. Both referenceMajesticDev\CommandNet\*classes the same un-required wayCalendarEventListenerreferences the calendar plugin's — see phpstan.neon's matching ignore rule.TransferInviteListenerwatchesAssignmentpostPersist: a new primary, still-active assignment (a transfer, or first enlistment) started within the last 7 days, to a unit whosediscordGuildIdmatches an activeDiscordConnectionwith an invite link, hands off toTransferInviteNotifier. That DMs the soldier a single-use, 7-day invite to the connection'sinviteChannelId(BotService::createInvite+sendDirectMessage), so the permanent public link never reaches a private unit server. If the connection has no invite channel, the soldier has no linked Discord account, the DM can't be delivered (DMs closed, bot offline or not yet updated, no Create Invite permission), it falls back to a Forumify notification (plus an email when they have email notifications on) linking to/discord/join/{guildId}; never both. A member already in the server is left alone. The old unit's server is not touched (role sync removes the old roles). Only new assignments trigger it; editing an existing assignment's unit does not.- Discord role IDs are pasted, not fetched live.
DiscordRoleMappingTypeuses a plain text field with instructions (enable Developer Mode, right-click the role, copy ID) rather than a live dropdown from the bot — editing a mapping doesn't require the bot to be online.
⚠️ Known gaps
Test coverage, a stale bot-registration prefix, and two unfinished UI pieces
- Almost no automated test coverage. The
tests/harness boots (CI runs it green); the only real test isTransferInviteListenerTest, the decision logic for transfer invites. commandnet-discord-botself-registers viaPOST /api/discord/register-bot— the/apiprefix comes fromapi_platform's routing config (everyApiResource, including this plugin's, lives under it), not from anything in this plugin's own code, so it's easy to miss when readingDiscordRegistration'suriTemplatealone.- The connections form's role-mapping table has no add/remove UI wired up.
assets/dist/settings_form_controller.jsused to power exactly this for the old single-connection settings form; it's currently unused rather than repointed at the new per-connection form. JoinServerController's default connection is "whichever was created first" (no explicit "this is the community server" flag) — fine with one extra connection, would want a real default marker once several units have their own.
🔗 Works well with
commandnet-plugin—Unitcarries an optionaldiscordGuildIdfor future per-unit notifications (AWOL alerts, operation posts), matched against aDiscordConnectionhere by guild ID.commandnet-discord-bot— the bot process this plugin talks to. See its README for the full HTTP contract.milsim-id-card-plugin— reaches intoForumify\Discord's (nowMajesticDev\Discord's) command interface for its own Discord slash commands the same waycommandnet-plugindoes.
🚀 Deploying DM invites
Redeploy the bot (its README's Docker/Portainer guide) before this plugin goes to
production, and give the bot Create Instant Invite in each connection's invite channel.
Order is safe either way: until the bot understands CreateInvite/DirectMessage, the plugin
just falls back to the notification. Then set "Invite Channel ID" on each connection.