Context
Voice XP tracking is completely non-functional. The infrastructure exists
but nothing is wired together — users in voice channels always get 0 XP.
What exists today
DynamicVoiceEvent captures Discord VOICE_STATE_UPDATE and calls
PersistVoiceStateAction, which stores records with
obtained_experience = 0 always.
NewVoiceMessage (the action that would calculate and award XP) is
never called anywhere — dead code.
IncrementExperience::incrementByVoiceMessage() works correctly and
is tested, but never invoked for voice.
- Mute/deaf state is never captured — Discord sends
self_mute /
self_deaf but the code only tracks join/leave.
- Schema mismatch —
voice_messages.state stores 'joined'/'left'
strings, but VoiceStatesEnum expects {disabled, muted, unmuted}.
How the old bot handled it
The previous Node.js bot (he4rt-bot-next) used a ticker/polling
approach that worked well:
// Every VOICE_COUNTER_XP_IN_MINUTES, scan all members in voice channels
const isUnmuted = !member.voice.selfMute && !member.voice.serverMute
const isAble = !member.voice.selfDeaf && !member.voice.serverDeaf
state: isAble && !isUnmuted ? 'muted'
: isAble && isUnmuted ? 'unmuted'
: 'disabled'
This maps directly to the existing VoiceStatesEnum multipliers:
unmuted → 5 × level
muted → 3 × level
disabled → 0 × level
What needs to happen
- Decide approach: replicate the ticker/polling model (scheduled
command that scans voice channels every N minutes) or fix the
event-driven flow. Ticker is simpler and proven.
- Capture mute/deaf state from Discord's voice state data and map
to VoiceStatesEnum.
- Call
IncrementExperience::incrementByVoiceMessage() to actually
award XP.
- Fix or replace
NewVoiceMessage — currently depends on the old
FindExternalIdentity pattern and request()->tenant_id (breaks in
queue/scheduler context). Should use ResolveUserContext like
NewMessage does.
- Reconcile schema —
voice_messages.state should store
VoiceStatesEnum values (disabled/muted/unmuted), not
joined/left.
- Exclude AFK channel — old bot skipped the AFK voice channel, same
logic should apply.
- Add tests for the full voice XP flow.
Context
Voice XP tracking is completely non-functional. The infrastructure exists
but nothing is wired together — users in voice channels always get 0 XP.
What exists today
DynamicVoiceEventcaptures DiscordVOICE_STATE_UPDATEand callsPersistVoiceStateAction, which stores records withobtained_experience = 0always.NewVoiceMessage(the action that would calculate and award XP) isnever called anywhere — dead code.
IncrementExperience::incrementByVoiceMessage()works correctly andis tested, but never invoked for voice.
self_mute/self_deafbut the code only tracks join/leave.voice_messages.statestores'joined'/'left'strings, but
VoiceStatesEnumexpects{disabled, muted, unmuted}.How the old bot handled it
The previous Node.js bot (
he4rt-bot-next) used a ticker/pollingapproach that worked well:
This maps directly to the existing
VoiceStatesEnummultipliers:unmuted→ 5 × levelmuted→ 3 × leveldisabled→ 0 × levelWhat needs to happen
command that scans voice channels every N minutes) or fix the
event-driven flow. Ticker is simpler and proven.
to
VoiceStatesEnum.IncrementExperience::incrementByVoiceMessage()to actuallyaward XP.
NewVoiceMessage— currently depends on the oldFindExternalIdentitypattern andrequest()->tenant_id(breaks inqueue/scheduler context). Should use
ResolveUserContextlikeNewMessagedoes.voice_messages.stateshould storeVoiceStatesEnumvalues (disabled/muted/unmuted), notjoined/left.logic should apply.