From 4ef8553c7d45096bd666374e8d08d3f84d5963de Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 6 Sep 2026 01:13:31 -0400 Subject: [PATCH] First attempt didn't work. --- barnard.go | 1 - client.go | 76 +++-------------------- client_notification_test.go | 72 ++++++--------------- gumble/gumble/event.go | 5 ++ gumble/gumble/handlers.go | 2 + gumble/gumble/handlers_regression_test.go | 43 +++++++++++++ 6 files changed, 79 insertions(+), 120 deletions(-) diff --git a/barnard.go b/barnard.go index 71ceb2d..9499b7f 100644 --- a/barnard.go +++ b/barnard.go @@ -68,7 +68,6 @@ type Barnard struct { MutedChannels map[uint32]bool MutedChannelsMutex sync.RWMutex userChannels map[uint32]*gumble.Channel - userAudioStates map[uint32]userAudioState // Added for noise suppression NoiseSuppressor *noise.Suppressor diff --git a/client.go b/client.go index 858e3a6..15c6559 100644 --- a/client.go +++ b/client.go @@ -161,7 +161,6 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) { b.MutedChannels = make(map[uint32]bool) b.MutedChannelsMutex.Unlock() b.userChannels = make(map[uint32]*gumble.Channel) - b.userAudioStates = make(map[uint32]userAudioState) b.RecordingMutex.Lock() b.recordingAllowed = nil b.recordingStarting = false @@ -183,7 +182,6 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) { for _, u := range users { b.UserConfig.UpdateUser(u) b.rememberUserChannel(u) - b.rememberUserAudioState(u) } b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Client.Self.Channel.Name)) @@ -391,7 +389,6 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) { b.AddOutputLine(formatUserStats(e.User)) } b.updateUserChannel(e) - b.updateUserAudioState(e) b.postUI(func() { b.RebuildUserChannelTreePreservingSelection() b.Ui.Refresh() @@ -405,11 +402,6 @@ type userChangeNotification struct { line string } -type userAudioState struct { - selfMuted bool - selfDeafened bool -} - func (b *Barnard) userChangeNotification(e *gumble.UserChangeEvent) (userChangeNotification, bool) { if e == nil || e.User == nil || b.Client == nil || b.Client.Self == nil || b.Client.Self.Channel == nil { return userChangeNotification{}, false @@ -441,7 +433,7 @@ func (b *Barnard) userChangeNotification(e *gumble.UserChangeEvent) (userChangeN } func (b *Barnard) userAudioChangeNotification(e *gumble.UserChangeEvent) (userChangeNotification, bool) { - if e == nil || e.User == nil || !e.Type.Has(gumble.UserChangeAudio) || b.Client == nil || b.Client.Self == nil || b.Client.Self.Channel == nil { + if e == nil || e.User == nil || (e.SelfMute == nil && e.SelfDeaf == nil) || b.Client == nil || b.Client.Self == nil || b.Client.Self.Channel == nil { return userChangeNotification{}, false } // Barnard announces its own hotkey actions immediately. The protocol echo @@ -449,53 +441,29 @@ func (b *Barnard) userAudioChangeNotification(e *gumble.UserChangeEvent) (userCh if e.User.Session == b.Client.Self.Session { return userChangeNotification{}, false } - previous, known := b.userAudioStates[e.User.Session] - if !known || !sameChannel(e.User.Channel, b.Client.Self.Channel) { + if !sameChannel(e.User.Channel, b.Client.Self.Channel) { return userChangeNotification{}, false } - muteEvent, muteVerb := "", "" - if previous.selfMuted != e.User.SelfMuted { - if e.User.SelfMuted { - muteEvent, muteVerb = "mute", "muted" - } else { - muteEvent, muteVerb = "unmute", "unmuted" - } + event, state := "unmute", "unmuted" + if e.User.SelfDeafened { + event, state = "mutedeafen", "muted and deafened" + } else if e.User.SelfMuted { + event, state = "mute", "muted" } - deafenEvent, deafenVerb := "", "" - if previous.selfDeafened != e.User.SelfDeafened { - if e.User.SelfDeafened { - deafenEvent, deafenVerb = "deafen", "deafened" - } else { - deafenEvent, deafenVerb = "undeafen", "undeafened" - } - } - - event := muteEvent + deafenEvent - verbs := make([]string, 0, 2) - if muteVerb != "" { - verbs = append(verbs, muteVerb) - } - if deafenVerb != "" { - verbs = append(verbs, deafenVerb) - } - if event == "" { - return userChangeNotification{}, false - } - verb := strings.Join(verbs, " and ") return userChangeNotification{ event: event, who: e.User.Name, what: e.User.Channel.Name, - line: fmt.Sprintf("%s %s", e.User.Name, verb), + line: fmt.Sprintf("%s is now %s", e.User.Name, state), }, true } func (b *Barnard) restoreTransmissionAfterUndeafen(e *gumble.UserChangeEvent) { - if e == nil || e.User == nil || !e.Type.Has(gumble.UserChangeAudio) || b.Client == nil || b.Client.Self == nil { + if e == nil || e.User == nil || e.SelfDeaf == nil || b.Client == nil || b.Client.Self == nil { return } - if e.User.Session != b.Client.Self.Session || e.User.SelfDeafened || !b.shouldTransmitOnUndeafen() { + if e.User.Session != b.Client.Self.Session || *e.SelfDeaf || !b.shouldTransmitOnUndeafen() { return } b.setTransmitOnUndeafen(false) @@ -548,30 +516,6 @@ func (b *Barnard) updateUserChannel(e *gumble.UserChangeEvent) { b.rememberUserChannel(e.User) } -func (b *Barnard) rememberUserAudioState(user *gumble.User) { - if user == nil { - return - } - if b.userAudioStates == nil { - b.userAudioStates = make(map[uint32]userAudioState) - } - b.userAudioStates[user.Session] = userAudioState{ - selfMuted: user.SelfMuted, - selfDeafened: user.SelfDeafened, - } -} - -func (b *Barnard) updateUserAudioState(e *gumble.UserChangeEvent) { - if e == nil || e.User == nil { - return - } - if e.Type.Has(gumble.UserChangeDisconnected) { - delete(b.userAudioStates, e.User.Session) - return - } - b.rememberUserAudioState(e.User) -} - func (b *Barnard) OnChannelChange(e *gumble.ChannelChangeEvent) { b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Channel.Name)) if e.Type.Has(gumble.ChannelChangeDescription) { diff --git a/client_notification_test.go b/client_notification_test.go index 8ff2e8f..3a41e54 100644 --- a/client_notification_test.go +++ b/client_notification_test.go @@ -195,65 +195,42 @@ func TestUserAudioChangeNotification(t *testing.T) { tests := []struct { name string user *gumble.User - previous userAudioState - remember bool - change gumble.UserChangeType + selfMute *bool + selfDeaf *bool want userChangeNotification wantOK bool }{ { name: "user mutes", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true}, - remember: true, change: gumble.UserChangeAudio, - want: userChangeNotification{event: "mute", who: "Guest", what: "Current", line: "Guest muted"}, wantOK: true, + selfMute: boolPointer(true), + want: userChangeNotification{event: "mute", who: "Guest", what: "Current", line: "Guest is now muted"}, wantOK: true, }, { name: "user unmutes", user: &gumble.User{Session: 2, Name: "Guest", Channel: current}, - previous: userAudioState{selfMuted: true}, remember: true, change: gumble.UserChangeAudio, - want: userChangeNotification{event: "unmute", who: "Guest", what: "Current", line: "Guest unmuted"}, wantOK: true, - }, - { - name: "user deafens", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true, SelfDeafened: true}, - previous: userAudioState{selfMuted: true}, remember: true, change: gumble.UserChangeAudio, - want: userChangeNotification{event: "deafen", who: "Guest", what: "Current", line: "Guest deafened"}, wantOK: true, - }, - { - name: "user undeafens", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true}, - previous: userAudioState{selfMuted: true, selfDeafened: true}, remember: true, change: gumble.UserChangeAudio, - want: userChangeNotification{event: "undeafen", who: "Guest", what: "Current", line: "Guest undeafened"}, wantOK: true, + selfMute: boolPointer(false), + want: userChangeNotification{event: "unmute", who: "Guest", what: "Current", line: "Guest is now unmuted"}, wantOK: true, }, { name: "user mutes and deafens", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true, SelfDeafened: true}, - remember: true, change: gumble.UserChangeAudio, - want: userChangeNotification{event: "mutedeafen", who: "Guest", what: "Current", line: "Guest muted and deafened"}, wantOK: true, + selfMute: boolPointer(true), selfDeaf: boolPointer(true), + want: userChangeNotification{event: "mutedeafen", who: "Guest", what: "Current", line: "Guest is now muted and deafened"}, wantOK: true, }, { - name: "user unmutes and undeafens", user: &gumble.User{Session: 2, Name: "Guest", Channel: current}, - previous: userAudioState{selfMuted: true, selfDeafened: true}, remember: true, change: gumble.UserChangeAudio, - want: userChangeNotification{event: "unmuteundeafen", who: "Guest", what: "Current", line: "Guest unmuted and undeafened"}, wantOK: true, + name: "server includes both fields for a mute", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true}, + selfMute: boolPointer(true), selfDeaf: boolPointer(false), + want: userChangeNotification{event: "mute", who: "Guest", what: "Current", line: "Guest is now muted"}, wantOK: true, }, - { - name: "user unmutes while deafening", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfDeafened: true}, - previous: userAudioState{selfMuted: true}, remember: true, change: gumble.UserChangeAudio, - want: userChangeNotification{event: "unmutedeafen", who: "Guest", what: "Current", line: "Guest unmuted and deafened"}, wantOK: true, - }, - { - name: "user mutes while undeafening", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true}, - previous: userAudioState{selfDeafened: true}, remember: true, change: gumble.UserChangeAudio, - want: userChangeNotification{event: "muteundeafen", who: "Guest", what: "Current", line: "Guest muted and undeafened"}, wantOK: true, - }, - {name: "self protocol echo is ignored", user: &gumble.User{Session: 1, Name: "Username", Channel: current, SelfMuted: true}, remember: true, change: gumble.UserChangeAudio}, - {name: "other channel is ignored", user: &gumble.User{Session: 2, Name: "Guest", Channel: other, SelfMuted: true}, remember: true, change: gumble.UserChangeAudio}, - {name: "unknown initial state is ignored", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true}, change: gumble.UserChangeConnected | gumble.UserChangeAudio}, - {name: "unrelated change is ignored", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true}, remember: true, change: gumble.UserChangeComment}, + {name: "self protocol echo is ignored", user: &gumble.User{Session: 1, Name: "Username", Channel: current, SelfMuted: true}, selfMute: boolPointer(true)}, + {name: "other channel is ignored", user: &gumble.User{Session: 2, Name: "Guest", Channel: other, SelfMuted: true}, selfMute: boolPointer(true)}, + {name: "packet without self state is ignored", user: &gumble.User{Session: 2, Name: "Guest", Channel: current, SelfMuted: true}}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { b := &Barnard{Client: &gumble.Client{Self: self}} - if test.remember { - b.userAudioStates = map[uint32]userAudioState{test.user.Session: test.previous} - } - got, ok := b.userAudioChangeNotification(&gumble.UserChangeEvent{Client: b.Client, Type: test.change, User: test.user}) + got, ok := b.userAudioChangeNotification(&gumble.UserChangeEvent{ + Client: b.Client, User: test.user, SelfMute: test.selfMute, SelfDeaf: test.selfDeaf, + }) if ok != test.wantOK || got != test.want { t.Fatalf("got (%#v, %t), want (%#v, %t)", got, ok, test.want, test.wantOK) } @@ -261,24 +238,13 @@ func TestUserAudioChangeNotification(t *testing.T) { } } -func TestUpdateUserAudioState(t *testing.T) { - user := &gumble.User{Session: 2, SelfMuted: true, SelfDeafened: true} - b := &Barnard{} - b.updateUserAudioState(&gumble.UserChangeEvent{User: user}) - if got := b.userAudioStates[user.Session]; got != (userAudioState{selfMuted: true, selfDeafened: true}) { - t.Fatalf("unexpected remembered audio state: %#v", got) - } - b.updateUserAudioState(&gumble.UserChangeEvent{Type: gumble.UserChangeDisconnected, User: user}) - if _, ok := b.userAudioStates[user.Session]; ok { - t.Fatal("disconnected user audio state was retained") - } -} +func boolPointer(value bool) *bool { return &value } func TestUndeafenConsumesRememberedTransmission(t *testing.T) { self := &gumble.User{Session: 1} b := &Barnard{Client: &gumble.Client{Self: self}} b.setTransmitOnUndeafen(true) - b.restoreTransmissionAfterUndeafen(&gumble.UserChangeEvent{Type: gumble.UserChangeAudio, User: self}) + b.restoreTransmissionAfterUndeafen(&gumble.UserChangeEvent{User: self, SelfDeaf: boolPointer(false)}) if b.shouldTransmitOnUndeafen() { t.Fatal("undeafen did not consume remembered transmission state") } diff --git a/gumble/gumble/event.go b/gumble/gumble/event.go index 281b61c..2f20b0e 100644 --- a/gumble/gumble/event.go +++ b/gumble/gumble/event.go @@ -94,6 +94,11 @@ type UserChangeEvent struct { Type UserChangeType User *User Actor *User + // SelfMute and SelfDeaf preserve the optional fields from the UserState + // packet. A nil pointer means the server did not include that state in + // this update. + SelfMute *bool + SelfDeaf *bool String string } diff --git a/gumble/gumble/handlers.go b/gumble/gumble/handlers.go index 9d47dde..54d9b90 100644 --- a/gumble/gumble/handlers.go +++ b/gumble/gumble/handlers.go @@ -752,12 +752,14 @@ func (c *Client) handleUserState(buffer []byte) error { user.Suppressed = *packet.Suppress } if packet.SelfMute != nil { + event.SelfMute = packet.SelfMute if *packet.SelfMute != user.SelfMuted { event.Type |= UserChangeAudio } user.SelfMuted = *packet.SelfMute } if packet.SelfDeaf != nil { + event.SelfDeaf = packet.SelfDeaf if *packet.SelfDeaf != user.SelfDeafened { event.Type |= UserChangeAudio } diff --git a/gumble/gumble/handlers_regression_test.go b/gumble/gumble/handlers_regression_test.go index f1a86d4..a196836 100644 --- a/gumble/gumble/handlers_regression_test.go +++ b/gumble/gumble/handlers_regression_test.go @@ -26,3 +26,46 @@ func TestUserStateUnknownChannelDoesNotDeadlock(t *testing.T) { t.Fatal("volatile lock was left locked") } } + +type userStateListener struct { + EventListener + event *UserChangeEvent +} + +func (l *userStateListener) OnUserChange(event *UserChangeEvent) { + l.event = event +} + +func TestUserStateEventPreservesSelfMuteAndDeafFields(t *testing.T) { + channel := &Channel{ID: 0, Users: make(Users)} + c := &Client{ + Config: NewConfig(), + Users: make(Users), + Channels: Channels{0: channel}, + state: uint32(StateSynced), + } + user := c.Users.create(1) + user.Channel = channel + channel.Users[user.Session] = user + listener := &userStateListener{} + c.Config.Attach(listener) + + session, selfMute, selfDeaf := uint32(1), true, false + data, err := proto.Marshal(&MumbleProto.UserState{ + Session: &session, + SelfMute: &selfMute, + SelfDeaf: &selfDeaf, + }) + if err != nil { + t.Fatal(err) + } + if err := c.handleUserState(data); err != nil { + t.Fatal(err) + } + if listener.event == nil || listener.event.SelfMute == nil || listener.event.SelfDeaf == nil { + t.Fatalf("self state fields were not preserved: %#v", listener.event) + } + if !*listener.event.SelfMute || *listener.event.SelfDeaf { + t.Fatalf("unexpected self state values: mute=%t deaf=%t", *listener.event.SelfMute, *listener.event.SelfDeaf) + } +}