Private Messages
Private messages are cross-server: the sender's request is routed through RabbitMQ to the recipient's server, the response comes back the same way. The flow is fronted by PrivateMessageService and the underlying packets are PrivateMessageRequest/PrivateMessageResponse.
Sending
Aliases: /message, /m, /t.
The sender and recipient both see the message rendered with the formats.private-messages template from config.yml. The format receives:
Variable | Value |
|---|---|
| Sender's display name. |
| Recipient's display name. |
| The message body. |
Error responses (in order of priority):
Condition | Message key |
|---|---|
Recipient is the sender |
|
Recipient is unknown, or on a server outside |
|
Recipient has |
|
Sender is ignored by the recipient |
|
Anything else |
|
Every successful message is persisted to the chat_messages table with channel set to private-message and recipient_id set — the same table public chat goes to. It is only submitted to the classifier when dataset.store-private-messages is on.
Replying
Replies to the last person you exchanged a PM with. If no conversation has happened (or the cached target has since gone offline), the player receives no-message-to-reply-to or reply-player-offline.
Toggling private messages
Toggles PlayerChatData.privateMessagesEnabled. The persistent flag is checked at every incoming /msg: when off, the sender gets private-message-recipient-disabled and nothing is delivered.
Confirmation messages: private-message-enabled/private-message-disabled.
Ignoring
/ignorewith no argument opens a menu of currently ignored players./ignore <player>adds the player to the ignore list (database + Redis cache)./unignore <player>removes the entry./ignore <player>refuses staff: before creating anything it resolves whether the target holdsastralchat.ignore.bypassand answerscannot-ignore-staffif they do.
Ignore state is preloaded into an in-memory cache on every join by IgnoreListener so the PM and chat pipelines can check it synchronously without a DB round-trip.
Staff bypass
A sender holding astralchat.ignore.bypass is never ignored: every delivery check resolves the bypass first and treats the relationship as absent when it holds. That covers both halves — /ignore refuses to create the relationship, and any relationship created before the sender got the node stops applying.
The lookup is network-wide. When the sender is online on this server their live permission is authoritative; otherwise it is resolved from LuckPerms' network-synced storage and cached for five minutes. An uncached remote lookup returns false until it resolves, which is why blocking the ignore at creation time is the primary safeguard rather than the delivery check.
The unignore-player AstralCore action (see Actions) lets menus remove an entry by UUID without exposing the command.
Social spy
Toggles whether the staff member receives a copy of every private message sent on the network. The command is gated by the hard-coded astralchat.socialspy node — the social-spy.permission key in config.yml is not read. social-spy.bypass-permission (shipped default chat.socialspy.bypass) is: a sender holding it is never spied on.
The spy fan-out reads the set of players who have the toggle on from Redis, drops the sender and the recipient from it, and delivers through the chat service — so a spy on another server still gets the copy. The toggle itself is not re-checked against the permission at delivery time.
Spy output uses the social-spy.format template with:
Variable | Value |
|---|---|
| Sender display name. |
| Recipient display name. |
| The message body. |
Confirmation messages: social-spy-enabled/social-spy-disabled.
Cross-server topology
The response status enum carries SUCCESS, USER_MESSAGING_DISABLED, USER_NOT_FOUND, or IGNORED, which the sender side maps to the appropriate message key.