Files
barnard/barnard.go

324 lines
8.0 KiB
Go

package main
import (
"crypto/tls"
"sync"
"git.stormux.org/storm/barnard/config"
"git.stormux.org/storm/barnard/fileplayback"
"git.stormux.org/storm/barnard/gumble/gumble"
"git.stormux.org/storm/barnard/gumble/gumbleopenal"
"git.stormux.org/storm/barnard/noise"
"git.stormux.org/storm/barnard/recording"
"git.stormux.org/storm/barnard/uiterm"
)
type TreeItem struct {
User *gumble.User
Channel *gumble.Channel
display string
userSession uint32
channelID uint32
snapshot bool
}
type Barnard struct {
Config *gumble.Config
UserConfig *config.Config
Hotkeys *config.Hotkeys
Client *gumble.Client
Address string
TLSConfig tls.Config
Stream *gumbleopenal.Stream
connectionMutex sync.RWMutex
Tx bool
AutoTransmit bool // auto-start transmission on connect
Connected bool
stateMutex sync.RWMutex
Ui *uiterm.Ui
UiOutput uiterm.Textview
UiInput uiterm.Textbox
UiStatus uiterm.Label
UiTree uiterm.Tree
UiAdmin uiterm.Tree
UiInputStatus uiterm.Label
SelectedChannel *gumble.Channel
selectedUser *gumble.User
selectedUserMutex sync.RWMutex
adminTargetUser *gumble.User
adminTargetChan *gumble.Channel
adminReturnItem uiterm.TreeItem
statusText string
statusNotice bool
notifyChannel chan []string
exitStatus int
exitMessage string
// Added for channel muting
MutedChannels map[uint32]bool
MutedChannelsMutex sync.RWMutex
userChannels map[uint32]*gumble.Channel
// Added for noise suppression
NoiseSuppressor *noise.Suppressor
// Added for file playback
FileStream *fileplayback.Player
FileStreamMutex sync.Mutex
// Added for tone test mode (bypasses all soundcard/OpenAL)
ToneTest bool
ToneTestOutput string
toneTestStop chan struct{}
toneTestSaver *AudioFileSaver
toneTestSaverDetach gumble.Detacher
// Added for recording
RecordingMutex sync.Mutex
Recorder *recording.Recorder
recordingStarting bool
recordingAllowed *bool
pendingAdminPrompt *adminPrompt
adminBanList gumble.BanList
adminUserList gumble.RegisteredUsers
adminACL *gumble.ACL
reconnectStop chan struct{}
reconnectStopOnce sync.Once
}
// cleanupConnectionAudio releases connection-owned audio resources before a
// reconnect replaces them. It is intentionally idempotent for repeated
// disconnect notifications.
func (b *Barnard) cleanupConnectionAudio() {
// Connection audio operations that use both resources take FileStreamMutex
// before connectionMutex, so cleanup follows that order as well.
b.FileStreamMutex.Lock()
if b.FileStream != nil {
_ = b.FileStream.Stop()
b.FileStream = nil
}
b.FileStreamMutex.Unlock()
b.connectionMutex.Lock()
if b.Stream != nil {
stream := b.Stream
b.Stream = nil
stream.Destroy()
}
b.connectionMutex.Unlock()
}
func (b *Barnard) cleanupToneTestAudio() {
if b.toneTestSaverDetach != nil {
b.toneTestSaverDetach.Detach()
b.toneTestSaverDetach = nil
}
if b.toneTestSaver != nil {
b.toneTestSaver.Stop()
b.toneTestSaver = nil
}
}
func (b *Barnard) updateUserGain(user *gumble.User) {
b.withStream(func(stream *gumbleopenal.Stream) {
stream.UpdateUserGain(user)
})
}
// withStream keeps a connection-owned stream alive for the complete operation.
// Reconnect cleanup takes the write lock before destroying or replacing it.
func (b *Barnard) withStream(action func(*gumbleopenal.Stream)) bool {
b.connectionMutex.RLock()
defer b.connectionMutex.RUnlock()
if b.Stream == nil {
return false
}
action(b.Stream)
return true
}
func (b *Barnard) isChannelMuted(channelID uint32) bool {
b.MutedChannelsMutex.RLock()
defer b.MutedChannelsMutex.RUnlock()
return b.MutedChannels[channelID]
}
func (b *Barnard) setChannelMuted(channelID uint32, muted bool) {
b.MutedChannelsMutex.Lock()
defer b.MutedChannelsMutex.Unlock()
if b.MutedChannels == nil {
b.MutedChannels = make(map[uint32]bool)
}
if muted {
b.MutedChannels[channelID] = true
} else {
delete(b.MutedChannels, channelID)
}
}
func (b *Barnard) selectedUserValue() *gumble.User {
b.selectedUserMutex.RLock()
defer b.selectedUserMutex.RUnlock()
return b.selectedUser
}
func (b *Barnard) setSelectedUserValue(user *gumble.User) {
b.selectedUserMutex.Lock()
b.selectedUser = user
b.selectedUserMutex.Unlock()
}
func (b *Barnard) isTransmitting() bool {
b.stateMutex.RLock()
defer b.stateMutex.RUnlock()
return b.Tx
}
func (b *Barnard) setTransmitting(transmitting bool) {
b.stateMutex.Lock()
b.Tx = transmitting
b.stateMutex.Unlock()
}
func (b *Barnard) isConnected() bool {
b.stateMutex.RLock()
defer b.stateMutex.RUnlock()
return b.Connected
}
func (b *Barnard) setConnected(connected bool) {
b.stateMutex.Lock()
b.Connected = connected
b.stateMutex.Unlock()
}
func (b *Barnard) stopReconnects() {
b.reconnectStopOnce.Do(func() {
if b.reconnectStop != nil {
close(b.reconnectStop)
}
})
}
func (b *Barnard) reconnectCanceled() bool {
if b.reconnectStop == nil {
return false
}
select {
case <-b.reconnectStop:
return true
default:
return false
}
}
func (b *Barnard) StopTransmission() {
if b.isTransmitting() {
b.Notify("micdown", "me", "")
b.setTransmitting(false)
b.UpdateGeneralStatus(" Idle ", false)
if b.ToneTest {
// Stop the tone generator.
if b.toneTestStop != nil {
close(b.toneTestStop)
b.toneTestStop = nil
}
} else {
b.withStream(func(stream *gumbleopenal.Stream) { _ = stream.StopSource() })
}
}
}
func (b *Barnard) TreeItemCharacter(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, ch rune) {
}
func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, key uiterm.Key) {
treeItem := item.(TreeItem)
if key == uiterm.KeyEnter {
if treeItem.Channel != nil {
b.Client.Self.Move(treeItem.Channel)
b.SetSelectedUser(nil)
b.GotoChat()
}
if treeItem.User != nil {
if b.selectedUserValue() == treeItem.User {
b.SetSelectedUser(nil)
b.GotoChat()
} else {
b.SetSelectedUser(treeItem.User)
b.GotoChat()
}
}
}
// Handle mute toggle
if treeItem.Channel != nil {
if key == *b.Hotkeys.MuteToggle {
// Determine new channel mute state
channelWillBeMuted := !b.isChannelMuted(treeItem.Channel.ID)
// Set all users in channel to the same mute state
users := makeUsersArray(treeItem.Channel.Users)
for _, u := range users {
// Explicitly set user mute state to match channel state
if channelWillBeMuted && !u.LocallyMuted() {
if err := b.UserConfig.ToggleMute(u); err != nil {
b.AddOutputLine("Mute: could not save setting: " + err.Error())
}
} else if !channelWillBeMuted && u.LocallyMuted() {
if err := b.UserConfig.ToggleMute(u); err != nil {
b.AddOutputLine("Mute: could not save setting: " + err.Error())
}
}
b.updateUserGain(u)
}
// Update channel mute state
b.setChannelMuted(treeItem.Channel.ID, channelWillBeMuted)
if channelWillBeMuted && b.Client.Self.Channel.ID == treeItem.Channel.ID && b.isTransmitting() {
b.StopTransmission()
}
b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh()
}
if key == *b.Hotkeys.VolumeDown {
b.changeVolume(makeUsersArray(treeItem.Channel.Users), -0.1)
}
if key == *b.Hotkeys.VolumeUp {
b.changeVolume(makeUsersArray(treeItem.Channel.Users), 0.1)
}
if key == *b.Hotkeys.VolumeReset {
b.resetVolume(makeUsersArray(treeItem.Channel.Users))
}
}
if treeItem.User != nil {
if key == *b.Hotkeys.MuteToggle {
// Toggle mute for single user
if err := b.UserConfig.ToggleMute(treeItem.User); err != nil {
b.AddOutputLine("Mute: could not save setting: " + err.Error())
}
b.updateUserGain(treeItem.User)
b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh()
}
if key == *b.Hotkeys.VolumeDown {
b.changeVolume([]*gumble.User{treeItem.User}, -0.1)
}
if key == *b.Hotkeys.VolumeUp {
b.changeVolume([]*gumble.User{treeItem.User}, 0.1)
}
if key == *b.Hotkeys.VolumeReset {
b.resetVolume([]*gumble.User{treeItem.User})
}
}
}