528 lines
14 KiB
Go
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)
|
|
}
|