Files

528 lines
14 KiB
Go

package main
import (
"fmt"
"net"
"strings"
"time"
"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/gumble/gumbleutil"
"git.stormux.org/storm/barnard/gumble/opus"
)
func (b *Barnard) start() {
b.Config.Attach(gumbleutil.AutoBitrate)
b.Config.Attach(b)
b.Config.Address = b.Address
// test Audio
_, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), true)
if err != nil {
b.exitWithError(err)
return
}
//connect, not reconnect
b.connect(false)
}
func (b *Barnard) exitWithError(err error) {
b.Ui.Close()
b.exitStatus = 1
b.exitMessage = err.Error()
}
func (b *Barnard) connect(reconnect bool) bool {
var err error
_, err = gumble.DialWithDialer(new(net.Dialer), b.Config, &b.TLSConfig)
if err != nil {
if reconnect {
b.Log(err.Error())
} else {
b.exitWithError(err)
}
return false
}
stream, err := gumbleopenal.New(b.Client, b.UserConfig.GetInputDevice(), b.UserConfig.GetOutputDevice(), false)
if err != nil {
b.exitWithError(err)
return false
}
b.Stream = stream
b.Stream.AttachStream(b.Client)
b.Stream.SetNoiseProcessor(b.NoiseSuppressor)
// Initialize stereo encoder for file playback
b.Client.AudioEncoderStereo = opus.NewStereoEncoder()
// Initialize file player
b.FileStreamMutex.Lock()
b.FileStream = fileplayback.New(b.Client)
b.FileStream.SetErrorFunc(func(err error) {
// Disable stereo when file finishes or errors
b.Client.DisableStereoEncoder()
b.AddOutputLine(fmt.Sprintf("File playback: %s", err.Error()))
})
b.Stream.SetFilePlayer(b.FileStream)
b.FileStreamMutex.Unlock()
b.Connected = true
return true
}
func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
b.Client = e.Client
// Reset muted channels state on connect
b.MutedChannels = make(map[uint32]bool)
b.userChannels = make(map[uint32]*gumble.Channel)
b.userAudioStates = make(map[uint32]userAudioState)
b.RecordingMutex.Lock()
b.recordingAllowed = nil
b.recordingStarting = false
b.RecordingMutex.Unlock()
b.Ui.SetActive(uiViewInput)
b.UiTree.Rebuild()
b.Ui.Refresh()
for _, u := range b.Client.Users {
b.UserConfig.UpdateUser(u)
b.rememberUserChannel(u)
b.rememberUserAudioState(u)
}
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.Client.Self.Channel.Name))
b.AddOutputLine(fmt.Sprintf("Connected to %s", b.Client.Conn.RemoteAddr()))
wmsg := ""
if e.WelcomeMessage != nil {
wmsg = esc(*e.WelcomeMessage)
}
b.Notify("connect", "me", wmsg)
if wmsg != "" {
b.AddOutputLine(fmt.Sprintf("Welcome message: %s", wmsg))
}
b.Ui.Refresh()
}
func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
var reason string
switch e.Type {
case gumble.DisconnectError:
reason = "connection error"
case gumble.DisconnectKicked:
reason = "kicked"
case gumble.DisconnectBanned:
reason = "banned"
}
if e.String != "" {
reason = e.String
}
b.stopRecordingForDisconnect()
b.Notify("disconnect", "me", reason)
if reason == "" {
b.AddOutputLine("Disconnected")
} else {
b.AddOutputLine("Disconnected: " + reason)
}
b.Tx = false
b.Connected = false
b.UiTree.Rebuild()
b.Ui.Refresh()
go b.reconnectGoroutine()
}
func (b *Barnard) reconnectGoroutine() {
for {
res := b.connect(true)
if res == true {
break
}
time.Sleep(15 * time.Second)
}
}
func (b *Barnard) Log(s string) {
b.AddOutputMessage(nil, s)
}
func (b *Barnard) OnTextMessage(e *gumble.TextMessageEvent) {
var public = false
for _, c := range e.Channels {
if c.Name == b.Client.Self.Channel.Name {
public = true
break
}
}
if public {
b.Notify("msg", e.Sender.Name, e.Message)
b.AddOutputMessage(e.Sender, e.Message)
} else {
var sender string
if e.Sender == nil {
sender = "Server"
} else {
sender = e.Sender.Name
}
b.Notify("pm", sender, e.Message)
b.AddOutputPrivateMessage(e.Sender, b.Client.Self, e.Message)
}
}
func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
notification, hasNotification := b.userChangeNotification(e)
audioNotification, hasAudioNotification := b.userAudioChangeNotification(e)
if e.User != nil {
b.UserConfig.UpdateUser(e.User)
// Check if user is joining a muted channel
if e.Type.Has(gumble.UserChangeConnected) || e.Type.Has(gumble.UserChangeChannel) {
// If the channel is muted, ensure the user is muted
if b.MutedChannels[e.User.Channel.ID] {
// Only mute if not already muted
if !e.User.LocallyMuted {
b.UserConfig.ToggleMute(e.User)
}
if e.User.AudioSource != nil {
e.User.AudioSource.SetGain(0)
}
}
}
}
if e.Type.Has(gumble.UserChangeDisconnected) {
if e.User == b.selectedUser {
b.SetSelectedUser(nil)
}
}
if hasNotification {
b.Notify(notification.event, notification.who, notification.what)
b.AddOutputLine(notification.line)
}
if hasAudioNotification {
b.Notify(audioNotification.event, audioNotification.who, audioNotification.what)
b.AddOutputLine(audioNotification.line)
}
if e.Type.Has(gumble.UserChangeChannel) && e.User == b.Client.Self {
b.UpdateInputStatus(fmt.Sprintf("[%s]", e.User.Channel.Name))
}
if e.Type.Has(gumble.UserChangeRecording) {
b.HandleRecordingChange(e)
}
if e.Type.Has(gumble.UserChangeComment) {
comment := strings.TrimSpace(esc(e.User.Comment))
if comment == "" {
comment = "empty"
}
b.AddOutputLine(fmt.Sprintf("User comment for %s: %s", e.User.Name, comment))
}
if e.Type.Has(gumble.UserChangeStats) && e.User.Stats != nil {
b.AddOutputLine(formatUserStats(e.User))
}
b.updateUserChannel(e)
b.updateUserAudioState(e)
b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh()
}
type userChangeNotification struct {
event string
who string
what string
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
}
currentChannel := b.Client.Self.Channel
previousChannel := b.previousUserChannel(e.User)
userChannel := e.User.Channel
if e.Type.Has(gumble.UserChangeConnected) {
return buildUserChangeNotification("join", "joined", e.User, userChannel, currentChannel)
}
if e.Type.Has(gumble.UserChangeDisconnected) {
if previousChannel == nil {
previousChannel = userChannel
}
return buildUserChangeNotification("leave", "left", e.User, previousChannel, currentChannel)
}
if e.Type.Has(gumble.UserChangeChannel) && e.User != b.Client.Self {
if sameChannel(userChannel, currentChannel) && !sameChannel(previousChannel, currentChannel) {
return buildUserChangeNotification("join", "joined", e.User, userChannel, currentChannel)
}
if sameChannel(previousChannel, currentChannel) && !sameChannel(userChannel, currentChannel) {
return buildUserChangeNotification("leave", "left", e.User, previousChannel, currentChannel)
}
}
return userChangeNotification{}, false
}
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 {
return userChangeNotification{}, false
}
previous, known := b.userAudioStates[e.User.Session]
if !known || !sameChannel(e.User.Channel, b.Client.Self.Channel) {
return userChangeNotification{}, false
}
muteEvent := ""
muteVerb := ""
if previous.selfMuted != e.User.SelfMuted {
if e.User.SelfMuted {
muteEvent = "mute"
muteVerb = "muted"
} else {
muteEvent = "unmute"
muteVerb = "unmuted"
}
}
deafenEvent := ""
deafenVerb := ""
if previous.selfDeafened != e.User.SelfDeafened {
if e.User.SelfDeafened {
deafenEvent = "deafen"
deafenVerb = "deafened"
} else {
deafenEvent = "undeafen"
deafenVerb = "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 ")
who := e.User.Name
line := fmt.Sprintf("%s %s", e.User.Name, verb)
if e.User.Session == b.Client.Self.Session {
who = "me"
line = fmt.Sprintf("You %s yourself", verb)
}
return userChangeNotification{
event: event,
who: who,
what: e.User.Channel.Name,
line: line,
}, true
}
func buildUserChangeNotification(event string, verb string, user *gumble.User, eventChannel *gumble.Channel, currentChannel *gumble.Channel) (userChangeNotification, bool) {
if !sameChannel(eventChannel, currentChannel) {
return userChangeNotification{}, false
}
return userChangeNotification{
event: event,
who: user.Name,
what: eventChannel.Name,
line: fmt.Sprintf("%s %s %s", user.Name, verb, eventChannel.Name),
}, true
}
func sameChannel(a *gumble.Channel, b *gumble.Channel) bool {
return a != nil && b != nil && a.ID == b.ID
}
func (b *Barnard) previousUserChannel(user *gumble.User) *gumble.Channel {
if b.userChannels == nil || user == nil {
return nil
}
return b.userChannels[user.Session]
}
func (b *Barnard) rememberUserChannel(user *gumble.User) {
if user == nil || user.Channel == nil {
return
}
if b.userChannels == nil {
b.userChannels = make(map[uint32]*gumble.Channel)
}
b.userChannels[user.Session] = user.Channel
}
func (b *Barnard) updateUserChannel(e *gumble.UserChangeEvent) {
if e == nil || e.User == nil {
return
}
if e.Type.Has(gumble.UserChangeDisconnected) {
if b.userChannels != nil {
delete(b.userChannels, e.User.Session)
}
return
}
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) {
description := strings.TrimSpace(esc(e.Channel.Description))
if description == "" {
description = "empty"
}
b.AddOutputLine(fmt.Sprintf("Channel description for %s: %s", e.Channel.Name, description))
}
if e.Type.Has(gumble.ChannelChangePermission) {
if permission := e.Channel.Permission(); permission != nil {
b.AddOutputLine(fmt.Sprintf("Channel permissions for %s: %s", e.Channel.Name, permissionList(*permission)))
}
}
b.RebuildUserChannelTreePreservingSelection()
b.Ui.Refresh()
}
func formatUserStats(user *gumble.User) string {
stats := user.Stats
connected := "unknown"
if !stats.Connected.IsZero() {
connected = time.Since(stats.Connected).Round(time.Second).String()
}
ip := "unknown"
if stats.IP != nil {
ip = stats.IP.String()
}
version := formatUserVersion(stats.Version)
return fmt.Sprintf(
"User stats for %s: version %s, connected %s, idle %s, bandwidth %d, UDP ping %.1f ms, TCP ping %.1f ms, IP %s, Opus %s",
user.Name,
version,
connected,
stats.Idle.Round(time.Second),
stats.Bandwidth,
stats.UDPPingAverage,
stats.TCPPingAverage,
ip,
onOff(stats.Opus),
)
}
func formatUserVersion(version gumble.Version) string {
major, minor, patch := (&version).SemanticVersion()
semantic := fmt.Sprintf("%d.%d.%d", major, minor, patch)
parts := []string{}
if version.Release != "" {
parts = append(parts, version.Release)
}
if version.OS != "" {
parts = append(parts, version.OS)
}
if version.OSVersion != "" {
parts = append(parts, version.OSVersion)
}
if len(parts) == 0 && version.Version == 0 {
return "unknown"
}
if len(parts) == 0 {
return semantic
}
return semantic + " " + strings.Join(parts, " ")
}
func (b *Barnard) OnPermissionDenied(e *gumble.PermissionDeniedEvent) {
var info string
switch e.Type {
case gumble.PermissionDeniedOther:
info = e.String
case gumble.PermissionDeniedPermission:
info = "insufficient permissions"
case gumble.PermissionDeniedSuperUser:
info = "cannot modify SuperUser"
case gumble.PermissionDeniedInvalidChannelName:
info = "invalid channel name"
case gumble.PermissionDeniedTextTooLong:
info = "text too long"
case gumble.PermissionDeniedTemporaryChannel:
info = "temporary channel"
case gumble.PermissionDeniedMissingCertificate:
info = "missing certificate"
case gumble.PermissionDeniedInvalidUserName:
info = "invalid user name"
case gumble.PermissionDeniedChannelFull:
info = "channel full"
case gumble.PermissionDeniedNestingLimit:
info = "nesting limit"
}
b.AddOutputLine(fmt.Sprintf("Permission denied: %s", info))
}
func (b *Barnard) OnUserList(e *gumble.UserListEvent) {
b.adminUserList = e.UserList
b.AddOutputLine(fmt.Sprintf("Admin: received %d registered users", len(e.UserList)))
b.UiAdmin.Rebuild()
b.Ui.Refresh()
}
func (b *Barnard) OnACL(e *gumble.ACLEvent) {
b.adminACL = e.ACL
if e.ACL != nil && e.ACL.Channel != nil {
b.AddOutputLine(fmt.Sprintf("Admin: received ACLs for %s", e.ACL.Channel.Name))
}
b.UiAdmin.Rebuild()
b.Ui.Refresh()
}
func (b *Barnard) OnBanList(e *gumble.BanListEvent) {
b.adminBanList = e.BanList
b.AddOutputLine(fmt.Sprintf("Admin: received %d bans", len(e.BanList)))
b.UiAdmin.Rebuild()
b.Ui.Refresh()
}
func (b *Barnard) OnContextActionChange(e *gumble.ContextActionChangeEvent) {
if e.ContextAction != nil {
b.AddOutputLine(fmt.Sprintf("Admin: context action updated: %s", e.ContextAction.Name))
}
b.UiAdmin.Rebuild()
b.Ui.Refresh()
}
func (b *Barnard) OnServerConfig(e *gumble.ServerConfigEvent) {
b.HandleRecordingAllowed(e.RecordingAllowed)
}