Added mute option. It can work per user or per channel, so covers deafen functionality too.
This commit is contained in:
parent
46d0cbf8f5
commit
a5c0e7a71c
@ -1,19 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"git.2mb.codes/~cmb/barnard/uiterm"
|
||||
"git.2mb.codes/~cmb/barnard/uiterm"
|
||||
)
|
||||
|
||||
type Hotkeys struct {
|
||||
Talk *uiterm.Key
|
||||
VolumeDown *uiterm.Key
|
||||
VolumeUp *uiterm.Key
|
||||
Exit *uiterm.Key
|
||||
ToggleTimestamps *uiterm.Key
|
||||
SwitchViews *uiterm.Key
|
||||
ClearOutput *uiterm.Key
|
||||
ScrollUp *uiterm.Key
|
||||
ScrollDown *uiterm.Key
|
||||
ScrollToTop *uiterm.Key
|
||||
ScrollToBottom *uiterm.Key
|
||||
Talk *uiterm.Key
|
||||
VolumeDown *uiterm.Key
|
||||
VolumeUp *uiterm.Key
|
||||
MuteToggle *uiterm.Key
|
||||
Exit *uiterm.Key
|
||||
ToggleTimestamps *uiterm.Key
|
||||
SwitchViews *uiterm.Key
|
||||
ClearOutput *uiterm.Key
|
||||
ScrollUp *uiterm.Key
|
||||
ScrollDown *uiterm.Key
|
||||
ScrollToTop *uiterm.Key
|
||||
ScrollToBottom *uiterm.Key
|
||||
}
|
||||
|
@ -1,281 +1,291 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.2mb.codes/~cmb/barnard/uiterm"
|
||||
"gopkg.in/yaml.v2"
|
||||
// "encoding/yaml"
|
||||
"git.2mb.codes/~cmb/barnard/gumble/gumble"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
"fmt"
|
||||
"git.2mb.codes/~cmb/barnard/uiterm"
|
||||
"gopkg.in/yaml.v2"
|
||||
"git.2mb.codes/~cmb/barnard/gumble/gumble"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
config *exportableConfig
|
||||
fn string
|
||||
config *exportableConfig
|
||||
fn string
|
||||
}
|
||||
|
||||
type exportableConfig struct {
|
||||
Hotkeys *Hotkeys
|
||||
MicVolume *float32
|
||||
InputDevice *string
|
||||
OutputDevice *string
|
||||
Servers []*server
|
||||
DefaultServer *string
|
||||
Username *string
|
||||
NotifyCommand *string
|
||||
Hotkeys *Hotkeys
|
||||
MicVolume *float32
|
||||
InputDevice *string
|
||||
OutputDevice *string
|
||||
Servers []*server
|
||||
DefaultServer *string
|
||||
Username *string
|
||||
NotifyCommand *string
|
||||
}
|
||||
|
||||
type server struct {
|
||||
Host string
|
||||
Port int
|
||||
Users []*eUser
|
||||
Host string
|
||||
Port int
|
||||
Users []*eUser
|
||||
}
|
||||
|
||||
type eUser struct {
|
||||
Username string
|
||||
Boost uint16
|
||||
Volume float32
|
||||
Username string
|
||||
Boost uint16
|
||||
Volume float32
|
||||
LocallyMuted bool // Changed from Muted to LocallyMuted to match User struct
|
||||
}
|
||||
|
||||
func (c *Config) SaveConfig() {
|
||||
var data []byte
|
||||
data, err := yaml.Marshal(c.config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = ioutil.WriteFile(c.fn+".tmp", data, 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = os.Rename(c.fn+".tmp", c.fn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var data []byte
|
||||
data, err := yaml.Marshal(c.config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = ioutil.WriteFile(c.fn+".tmp", data, 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = os.Rename(c.fn+".tmp", c.fn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func key(k uiterm.Key) *uiterm.Key {
|
||||
return &k
|
||||
return &k
|
||||
}
|
||||
|
||||
func (c *Config) LoadConfig() {
|
||||
var jc exportableConfig
|
||||
jc = exportableConfig{}
|
||||
jc.Hotkeys = &Hotkeys{
|
||||
Talk: key(uiterm.KeyF1),
|
||||
VolumeDown: key(uiterm.KeyF5),
|
||||
VolumeUp: key(uiterm.KeyF6),
|
||||
Exit: key(uiterm.KeyF10),
|
||||
ToggleTimestamps: key(uiterm.KeyF3),
|
||||
SwitchViews: key(uiterm.KeyTab),
|
||||
ScrollUp: key(uiterm.KeyPgup),
|
||||
ScrollDown: key(uiterm.KeyPgdn),
|
||||
}
|
||||
if fileExists(c.fn) {
|
||||
var data []byte
|
||||
data = readFile(c.fn)
|
||||
if data != nil {
|
||||
err := yaml.UnmarshalStrict(data, &jc)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing \"%s\".\n%s\n", c.fn, err.Error())
|
||||
os.Exit(1)
|
||||
} //panic
|
||||
} //if data
|
||||
} //if exists
|
||||
c.config = &jc
|
||||
if c.config.MicVolume == nil {
|
||||
micvol := float32(1.0)
|
||||
jc.MicVolume = &micvol
|
||||
}
|
||||
if c.config.InputDevice == nil {
|
||||
idev := string("")
|
||||
jc.InputDevice = &idev
|
||||
}
|
||||
if c.config.OutputDevice == nil {
|
||||
odev := string("")
|
||||
jc.OutputDevice = &odev
|
||||
}
|
||||
if c.config.DefaultServer == nil {
|
||||
defaultServer := string("localhost:64738")
|
||||
jc.DefaultServer = &defaultServer
|
||||
}
|
||||
if c.config.Username == nil {
|
||||
username := string("")
|
||||
jc.Username = &username
|
||||
}
|
||||
if c.config.NotifyCommand == nil {
|
||||
ncmd := string("")
|
||||
jc.NotifyCommand = &ncmd
|
||||
}
|
||||
var jc exportableConfig
|
||||
jc = exportableConfig{}
|
||||
jc.Hotkeys = &Hotkeys{
|
||||
Talk: key(uiterm.KeyF1),
|
||||
VolumeDown: key(uiterm.KeyF5),
|
||||
VolumeUp: key(uiterm.KeyF6),
|
||||
MuteToggle: key(uiterm.KeyF7), // Added mute toggle hotkey
|
||||
Exit: key(uiterm.KeyF10),
|
||||
ToggleTimestamps: key(uiterm.KeyF3),
|
||||
SwitchViews: key(uiterm.KeyTab),
|
||||
ScrollUp: key(uiterm.KeyPgup),
|
||||
ScrollDown: key(uiterm.KeyPgdn),
|
||||
}
|
||||
if fileExists(c.fn) {
|
||||
var data []byte
|
||||
data = readFile(c.fn)
|
||||
if data != nil {
|
||||
err := yaml.UnmarshalStrict(data, &jc)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing \"%s\".\n%s\n", c.fn, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.config = &jc
|
||||
if c.config.MicVolume == nil {
|
||||
micvol := float32(1.0)
|
||||
jc.MicVolume = &micvol
|
||||
}
|
||||
if c.config.InputDevice == nil {
|
||||
idev := string("")
|
||||
jc.InputDevice = &idev
|
||||
}
|
||||
if c.config.OutputDevice == nil {
|
||||
odev := string("")
|
||||
jc.OutputDevice = &odev
|
||||
}
|
||||
if c.config.DefaultServer == nil {
|
||||
defaultServer := string("localhost:64738")
|
||||
jc.DefaultServer = &defaultServer
|
||||
}
|
||||
if c.config.Username == nil {
|
||||
username := string("")
|
||||
jc.Username = &username
|
||||
}
|
||||
if c.config.NotifyCommand == nil {
|
||||
ncmd := string("")
|
||||
jc.NotifyCommand = &ncmd
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) findServer(address string) *server {
|
||||
if c.config.Servers == nil {
|
||||
c.config.Servers = make([]*server, 0)
|
||||
}
|
||||
host, port := makeHostPort(address)
|
||||
var t *server
|
||||
for _, s := range c.config.Servers {
|
||||
if s.Port == port && s.Host == host {
|
||||
t = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if t == nil {
|
||||
t = &server{
|
||||
Host: host,
|
||||
Port: port,
|
||||
}
|
||||
c.config.Servers = append(c.config.Servers, t)
|
||||
}
|
||||
return t
|
||||
if c.config.Servers == nil {
|
||||
c.config.Servers = make([]*server, 0)
|
||||
}
|
||||
host, port := makeHostPort(address)
|
||||
var t *server
|
||||
for _, s := range c.config.Servers {
|
||||
if s.Port == port && s.Host == host {
|
||||
t = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if t == nil {
|
||||
t = &server{
|
||||
Host: host,
|
||||
Port: port,
|
||||
}
|
||||
c.config.Servers = append(c.config.Servers, t)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (c *Config) findUser(address string, username string) *eUser {
|
||||
var s *server
|
||||
s = c.findServer(address)
|
||||
if s.Users == nil {
|
||||
s.Users = make([]*eUser, 0)
|
||||
}
|
||||
var t *eUser
|
||||
for _, u := range s.Users {
|
||||
if u.Username == username {
|
||||
t = u
|
||||
break
|
||||
}
|
||||
}
|
||||
if t == nil {
|
||||
t = &eUser{
|
||||
Username: username,
|
||||
Boost: uint16(1),
|
||||
Volume: 1.0,
|
||||
}
|
||||
s.Users = append(s.Users, t)
|
||||
}
|
||||
return t
|
||||
var s *server
|
||||
s = c.findServer(address)
|
||||
if s.Users == nil {
|
||||
s.Users = make([]*eUser, 0)
|
||||
}
|
||||
var t *eUser
|
||||
for _, u := range s.Users {
|
||||
if u.Username == username {
|
||||
t = u
|
||||
break
|
||||
}
|
||||
}
|
||||
if t == nil {
|
||||
t = &eUser{
|
||||
Username: username,
|
||||
Boost: uint16(1),
|
||||
Volume: 1.0,
|
||||
LocallyMuted: false, // Initialize local mute state
|
||||
}
|
||||
s.Users = append(s.Users, t)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (c *Config) ToggleMute(u *gumble.User) {
|
||||
j := c.findUser(u.GetClient().Config.Address, u.Name)
|
||||
j.LocallyMuted = !j.LocallyMuted
|
||||
u.LocallyMuted = j.LocallyMuted
|
||||
c.SaveConfig()
|
||||
}
|
||||
|
||||
func (c *Config) SetMicVolume(v float32) {
|
||||
t := float32(v)
|
||||
c.config.MicVolume = &t
|
||||
t := float32(v)
|
||||
c.config.MicVolume = &t
|
||||
}
|
||||
|
||||
func (c *Config) GetHotkeys() *Hotkeys {
|
||||
return c.config.Hotkeys
|
||||
return c.config.Hotkeys
|
||||
}
|
||||
|
||||
func (c *Config) GetNotifyCommand() *string {
|
||||
return c.config.NotifyCommand
|
||||
return c.config.NotifyCommand
|
||||
}
|
||||
|
||||
func (c *Config) GetInputDevice() *string {
|
||||
return c.config.InputDevice
|
||||
return c.config.InputDevice
|
||||
}
|
||||
|
||||
func (c *Config) GetOutputDevice() *string {
|
||||
return c.config.OutputDevice
|
||||
return c.config.OutputDevice
|
||||
}
|
||||
|
||||
func (c *Config) GetDefaultServer() *string {
|
||||
return c.config.DefaultServer
|
||||
return c.config.DefaultServer
|
||||
}
|
||||
|
||||
func (c *Config) GetUsername() *string {
|
||||
return c.config.Username
|
||||
return c.config.Username
|
||||
}
|
||||
|
||||
func (c *Config) UpdateUser(u *gumble.User) {
|
||||
var j *eUser
|
||||
var uc *gumble.Client
|
||||
uc = u.GetClient()
|
||||
if uc != nil {
|
||||
j = c.findUser(uc.Config.Address, u.Name)
|
||||
u.Boost = j.Boost
|
||||
u.Volume = j.Volume
|
||||
if u.Boost < 1 {
|
||||
u.Boost = 1
|
||||
}
|
||||
}
|
||||
var j *eUser
|
||||
var uc *gumble.Client
|
||||
uc = u.GetClient()
|
||||
if uc != nil {
|
||||
j = c.findUser(uc.Config.Address, u.Name)
|
||||
u.Boost = j.Boost
|
||||
u.Volume = j.Volume
|
||||
u.LocallyMuted = j.LocallyMuted // Update LocallyMuted state from config
|
||||
if u.Boost < 1 {
|
||||
u.Boost = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) UpdateConfig(u *gumble.User) {
|
||||
var j *eUser
|
||||
j = c.findUser(u.GetClient().Config.Address, u.Name)
|
||||
j.Boost = u.Boost
|
||||
j.Volume = u.Volume
|
||||
var j *eUser
|
||||
j = c.findUser(u.GetClient().Config.Address, u.Name)
|
||||
j.Boost = u.Boost
|
||||
j.Volume = u.Volume
|
||||
j.LocallyMuted = u.LocallyMuted // Save LocallyMuted state to config
|
||||
}
|
||||
|
||||
func NewConfig(fn *string) *Config {
|
||||
var c *Config
|
||||
c = &Config{}
|
||||
c.fn = resolvePath(*fn)
|
||||
c.LoadConfig()
|
||||
return c
|
||||
var c *Config
|
||||
c = &Config{}
|
||||
c.fn = resolvePath(*fn)
|
||||
c.LoadConfig()
|
||||
return c
|
||||
}
|
||||
|
||||
func readFile(path string) []byte {
|
||||
if !fileExists(path) {
|
||||
return nil
|
||||
}
|
||||
dat, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return dat
|
||||
if !fileExists(path) {
|
||||
return nil
|
||||
}
|
||||
dat, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return dat
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
info, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
func resolvePath(path string) string {
|
||||
if strings.HasPrefix(path, "~/") || strings.Contains(path, "$HOME") {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var hd = usr.HomeDir
|
||||
if strings.Contains(path, "$HOME") {
|
||||
path = strings.Replace(path, "$HOME", hd, 1)
|
||||
} else {
|
||||
path = strings.Replace(path, "~", hd, 1)
|
||||
}
|
||||
}
|
||||
return path
|
||||
if strings.HasPrefix(path, "~/") || strings.Contains(path, "$HOME") {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var hd = usr.HomeDir
|
||||
if strings.Contains(path, "$HOME") {
|
||||
path = strings.Replace(path, "$HOME", hd, 1)
|
||||
} else {
|
||||
path = strings.Replace(path, "~", hd, 1)
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func makeHostPort(addr string) (string, int) {
|
||||
parts := strings.Split(addr, ":")
|
||||
host := parts[0]
|
||||
port, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return host, port
|
||||
parts := strings.Split(addr, ":")
|
||||
host := parts[0]
|
||||
port, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
func Log(s string) {
|
||||
log(s)
|
||||
log(s)
|
||||
}
|
||||
|
||||
func log(s string) {
|
||||
s += "\n"
|
||||
// If the file doesn't exist, create it, or append to the file
|
||||
f, err := os.OpenFile("log.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := f.Write([]byte(s)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s += "\n"
|
||||
f, err := os.OpenFile("log.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := f.Write([]byte(s)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -1,228 +1,235 @@
|
||||
package gumble
|
||||
|
||||
import (
|
||||
"git.2mb.codes/~cmb/barnard/gumble/gumble/MumbleProto"
|
||||
"git.2mb.codes/~cmb/go-openal/openal"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"git.2mb.codes/~cmb/barnard/gumble/gumble/MumbleProto"
|
||||
"git.2mb.codes/~cmb/go-openal/openal"
|
||||
"github.com/golang/protobuf/proto"
|
||||
)
|
||||
|
||||
// User represents a user that is currently connected to the server.
|
||||
type User struct {
|
||||
// The user's unique session ID.
|
||||
Session uint32
|
||||
// The user's ID. Contains an invalid value if the user is not registered.
|
||||
UserID uint32
|
||||
// The user's name.
|
||||
Name string
|
||||
// The channel that the user is currently in.
|
||||
Channel *Channel
|
||||
// The user's unique session ID.
|
||||
Session uint32
|
||||
// The user's ID. Contains an invalid value if the user is not registered.
|
||||
UserID uint32
|
||||
// The user's name.
|
||||
Name string
|
||||
// The channel that the user is currently in.
|
||||
Channel *Channel
|
||||
|
||||
// Has the user has been muted?
|
||||
Muted bool
|
||||
// Has the user been deafened?
|
||||
Deafened bool
|
||||
// Has the user been suppressed?
|
||||
Suppressed bool
|
||||
// Has the user been muted by him/herself?
|
||||
SelfMuted bool
|
||||
// Has the user been deafened by him/herself?
|
||||
SelfDeafened bool
|
||||
// Is the user a priority speaker in the channel?
|
||||
PrioritySpeaker bool
|
||||
// Is the user recording audio?
|
||||
Recording bool
|
||||
// Has the user has been muted?
|
||||
Muted bool
|
||||
// Has the user been deafened?
|
||||
Deafened bool
|
||||
// Has the user been suppressed?
|
||||
Suppressed bool
|
||||
// Has the user been muted by him/herself?
|
||||
SelfMuted bool
|
||||
// Has the user been deafened by him/herself?
|
||||
SelfDeafened bool
|
||||
// Is the user a priority speaker in the channel?
|
||||
PrioritySpeaker bool
|
||||
// Is the user recording audio?
|
||||
Recording bool
|
||||
// Has the user been locally muted by the client?
|
||||
LocallyMuted bool
|
||||
|
||||
// The user's comment. Contains the empty string if the user does not have a
|
||||
// comment, or if the comment needs to be requested.
|
||||
Comment string
|
||||
// The user's comment hash. nil if User.Comment has been populated.
|
||||
CommentHash []byte
|
||||
// The hash of the user's certificate (can be empty).
|
||||
Hash string
|
||||
// The user's texture (avatar). nil if the user does not have a
|
||||
// texture, or if the texture needs to be requested.
|
||||
Texture []byte
|
||||
// The user's texture hash. nil if User.Texture has been populated.
|
||||
TextureHash []byte
|
||||
// The user's comment. Contains the empty string if the user does not have a
|
||||
// comment, or if the comment needs to be requested.
|
||||
Comment string
|
||||
// The user's comment hash. nil if User.Comment has been populated.
|
||||
CommentHash []byte
|
||||
// The hash of the user's certificate (can be empty).
|
||||
Hash string
|
||||
// The user's texture (avatar). nil if the user does not have a
|
||||
// texture, or if the texture needs to be requested.
|
||||
Texture []byte
|
||||
// The user's texture hash. nil if User.Texture has been populated.
|
||||
TextureHash []byte
|
||||
|
||||
// The user's stats. Contains nil if the stats have not yet been requested.
|
||||
Stats *UserStats
|
||||
// The user's stats. Contains nil if the stats have not yet been requested.
|
||||
Stats *UserStats
|
||||
|
||||
client *Client
|
||||
decoder AudioDecoder
|
||||
client *Client
|
||||
decoder AudioDecoder
|
||||
|
||||
AudioSource *openal.Source
|
||||
Boost uint16
|
||||
Volume float32
|
||||
AudioSource *openal.Source
|
||||
Boost uint16
|
||||
Volume float32
|
||||
}
|
||||
|
||||
// IsMuted returns true if the user is muted either server-side or locally
|
||||
func (u *User) IsMuted() bool {
|
||||
return u.Muted || u.LocallyMuted
|
||||
}
|
||||
|
||||
func (u *User) GetClient() *Client {
|
||||
return u.client
|
||||
return u.client
|
||||
}
|
||||
|
||||
// SetTexture sets the user's texture.
|
||||
func (u *User) SetTexture(texture []byte) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Texture: texture,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Texture: texture,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// SetPrioritySpeaker sets if the user is a priority speaker in the channel.
|
||||
func (u *User) SetPrioritySpeaker(prioritySpeaker bool) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
PrioritySpeaker: &prioritySpeaker,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
PrioritySpeaker: &prioritySpeaker,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// SetRecording sets if the user is recording audio.
|
||||
func (u *User) SetRecording(recording bool) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Recording: &recording,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Recording: &recording,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// IsRegistered returns true if the user's certificate has been registered with
|
||||
// the server. A registered user will have a valid user ID.
|
||||
func (u *User) IsRegistered() bool {
|
||||
return u.UserID > 0
|
||||
return u.UserID > 0
|
||||
}
|
||||
|
||||
// Register will register the user with the server. If the client has
|
||||
// permission to do so, the user will shortly be given a UserID.
|
||||
func (u *User) Register() {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
UserId: proto.Uint32(0),
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
UserId: proto.Uint32(0),
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// SetComment will set the user's comment to the given string. The user's
|
||||
// comment will be erased if the comment is set to the empty string.
|
||||
func (u *User) SetComment(comment string) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Comment: &comment,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Comment: &comment,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// Move will move the user to the given channel.
|
||||
func (u *User) Move(channel *Channel) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
ChannelId: &channel.ID,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
ChannelId: &channel.ID,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// Kick will kick the user from the server.
|
||||
func (u *User) Kick(reason string) {
|
||||
packet := MumbleProto.UserRemove{
|
||||
Session: &u.Session,
|
||||
Reason: &reason,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserRemove{
|
||||
Session: &u.Session,
|
||||
Reason: &reason,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// Ban will ban the user from the server.
|
||||
func (u *User) Ban(reason string) {
|
||||
packet := MumbleProto.UserRemove{
|
||||
Session: &u.Session,
|
||||
Reason: &reason,
|
||||
Ban: proto.Bool(true),
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserRemove{
|
||||
Session: &u.Session,
|
||||
Reason: &reason,
|
||||
Ban: proto.Bool(true),
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// SetMuted sets whether the user can transmit audio or not.
|
||||
func (u *User) SetMuted(muted bool) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Mute: &muted,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Mute: &muted,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// SetSuppressed sets whether the user is suppressed by the server or not.
|
||||
func (u *User) SetSuppressed(supressed bool) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Suppress: &supressed,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Suppress: &supressed,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// SetDeafened sets whether the user can receive audio or not.
|
||||
func (u *User) SetDeafened(muted bool) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Deaf: &muted,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
Deaf: &muted,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// SetSelfMuted sets whether the user can transmit audio or not.
|
||||
//
|
||||
// This method should only be called on Client.Self().
|
||||
func (u *User) SetSelfMuted(muted bool) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
SelfMute: &muted,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
SelfMute: &muted,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// SetSelfDeafened sets whether the user can receive audio or not.
|
||||
//
|
||||
// This method should only be called on Client.Self().
|
||||
func (u *User) SetSelfDeafened(muted bool) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
SelfDeaf: &muted,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
SelfDeaf: &muted,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// RequestStats requests that the user's stats be sent to the client.
|
||||
func (u *User) RequestStats() {
|
||||
packet := MumbleProto.UserStats{
|
||||
Session: &u.Session,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserStats{
|
||||
Session: &u.Session,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// RequestTexture requests that the user's actual texture (i.e. non-hashed) be
|
||||
// sent to the client.
|
||||
func (u *User) RequestTexture() {
|
||||
packet := MumbleProto.RequestBlob{
|
||||
SessionTexture: []uint32{u.Session},
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.RequestBlob{
|
||||
SessionTexture: []uint32{u.Session},
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// RequestComment requests that the user's actual comment (i.e. non-hashed) be
|
||||
// sent to the client.
|
||||
func (u *User) RequestComment() {
|
||||
packet := MumbleProto.RequestBlob{
|
||||
SessionComment: []uint32{u.Session},
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.RequestBlob{
|
||||
SessionComment: []uint32{u.Session},
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
||||
// Send will send a text message to the user.
|
||||
func (u *User) Send(message string) {
|
||||
textMessage := TextMessage{
|
||||
Users: []*User{u},
|
||||
Message: message,
|
||||
}
|
||||
u.client.Send(&textMessage)
|
||||
textMessage := TextMessage{
|
||||
Users: []*User{u},
|
||||
Message: message,
|
||||
}
|
||||
u.client.Send(&textMessage)
|
||||
}
|
||||
|
||||
// SetPlugin sets the user's plugin data.
|
||||
@ -233,10 +240,10 @@ func (u *User) Send(message string) {
|
||||
//
|
||||
// PluginShortName + "\x00" + AdditionalContextInformation
|
||||
func (u *User) SetPlugin(context []byte, identity string) {
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
PluginContext: context,
|
||||
PluginIdentity: &identity,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
packet := MumbleProto.UserState{
|
||||
Session: &u.Session,
|
||||
PluginContext: context,
|
||||
PluginIdentity: &identity,
|
||||
}
|
||||
u.client.Conn.WriteProto(&packet)
|
||||
}
|
||||
|
@ -164,7 +164,13 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||||
go func(e *gumble.AudioStreamEvent) {
|
||||
var source = openal.NewSource()
|
||||
e.User.AudioSource = &source
|
||||
e.User.AudioSource.SetGain(e.User.Volume)
|
||||
|
||||
// Set initial gain based on volume and mute state
|
||||
if e.User.LocallyMuted {
|
||||
e.User.AudioSource.SetGain(0)
|
||||
} else {
|
||||
e.User.AudioSource.SetGain(e.User.Volume)
|
||||
}
|
||||
|
||||
bufferCount := e.Client.Config.Buffers
|
||||
if bufferCount < 64 {
|
||||
@ -183,6 +189,11 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||||
var raw [maxBufferSize]byte
|
||||
|
||||
for packet := range e.C {
|
||||
// Skip processing if user is locally muted
|
||||
if e.User.LocallyMuted {
|
||||
continue
|
||||
}
|
||||
|
||||
var boost uint16 = uint16(1)
|
||||
samples := len(packet.AudioBuffer)
|
||||
if samples > cap(raw)/2 {
|
||||
@ -192,17 +203,13 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||||
boost = e.User.Boost
|
||||
|
||||
// Check if sample count suggests stereo data
|
||||
// If it's not a multiple of 2, it must be mono
|
||||
// If it's more than standard frameSize, it's likely stereo
|
||||
isStereo := samples > gumble.AudioDefaultFrameSize && samples%2 == 0
|
||||
format := openal.FormatMono16
|
||||
if isStereo {
|
||||
format = openal.FormatStereo16
|
||||
// Adjust samples to represent stereo frame count
|
||||
samples = samples / 2
|
||||
}
|
||||
|
||||
// Process samples
|
||||
rawPtr := 0
|
||||
if isStereo {
|
||||
// Process stereo samples as pairs
|
||||
@ -244,7 +251,6 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||||
buffer := emptyBufs[last]
|
||||
emptyBufs = emptyBufs[:last]
|
||||
|
||||
// Set buffer data with correct format
|
||||
buffer.SetData(format, raw[:rawPtr], gumble.AudioSampleRate)
|
||||
source.QueueBuffer(buffer)
|
||||
|
||||
|
316
ui_tree.go
316
ui_tree.go
@ -1,176 +1,204 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
//"math"
|
||||
// "fmt"
|
||||
"git.2mb.codes/~cmb/barnard/gumble/gumble"
|
||||
"git.2mb.codes/~cmb/barnard/uiterm"
|
||||
"sort"
|
||||
"git.2mb.codes/~cmb/barnard/gumble/gumble"
|
||||
"git.2mb.codes/~cmb/barnard/uiterm"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type TreeItem struct {
|
||||
User *gumble.User
|
||||
Channel *gumble.Channel
|
||||
User *gumble.User
|
||||
Channel *gumble.Channel
|
||||
}
|
||||
|
||||
func (ti TreeItem) String() string {
|
||||
if ti.User != nil {
|
||||
return ti.User.Name
|
||||
}
|
||||
if ti.Channel != nil {
|
||||
return "#" + ti.Channel.Name
|
||||
}
|
||||
return ""
|
||||
if ti.User != nil {
|
||||
if ti.User.LocallyMuted {
|
||||
return "[MUTED] " + ti.User.Name
|
||||
}
|
||||
return ti.User.Name
|
||||
}
|
||||
if ti.Channel != nil {
|
||||
return "#" + ti.Channel.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ti TreeItem) TreeItemStyle(fg, bg uiterm.Attribute, active bool) (uiterm.Attribute, uiterm.Attribute) {
|
||||
if ti.Channel != nil {
|
||||
fg |= uiterm.AttrBold
|
||||
}
|
||||
if active {
|
||||
fg, bg = bg, fg
|
||||
}
|
||||
return fg, bg
|
||||
if ti.Channel != nil {
|
||||
fg |= uiterm.AttrBold
|
||||
}
|
||||
if active {
|
||||
fg, bg = bg, fg
|
||||
}
|
||||
return fg, bg
|
||||
}
|
||||
|
||||
func (b *Barnard) TreeItemCharacter(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, ch rune) {
|
||||
}
|
||||
|
||||
func (b *Barnard) changeVolume(users []*gumble.User, change float32) {
|
||||
for _, u := range users {
|
||||
au := u.AudioSource
|
||||
if au == nil {
|
||||
continue
|
||||
}
|
||||
var boost uint16
|
||||
var cv float32
|
||||
var ng float32
|
||||
var curboost float32
|
||||
curboost = float32((u.Boost - 1)) / 10
|
||||
cv = au.GetGain() + curboost
|
||||
ng = cv + change
|
||||
boost = uint16(1)
|
||||
//b.AddOutputLine(fmt.Sprintf("cv %.2f change %.2f ng %.2f",cv,change,ng))
|
||||
if ng > 1.0 {
|
||||
//1.0 will give volume of one and boost of 1
|
||||
//1.1 will give volume of 1 and boost of 2
|
||||
//b.AddOutputLine(fmt.Sprintf("partperc %.2f",(ng*10)))
|
||||
perc := uint16((ng * 10)) - 10
|
||||
perc += 1
|
||||
boost = perc
|
||||
ng = 1.0
|
||||
}
|
||||
if ng < 0 {
|
||||
ng = 0.0
|
||||
}
|
||||
//b.AddOutputLine(fmt.Sprintf("boost %d ng %.2f",boost,ng))
|
||||
u.Boost = boost
|
||||
u.Volume = ng
|
||||
au.SetGain(ng)
|
||||
b.UserConfig.UpdateConfig(u)
|
||||
}
|
||||
b.UserConfig.SaveConfig()
|
||||
for _, u := range users {
|
||||
au := u.AudioSource
|
||||
if au == nil {
|
||||
continue
|
||||
}
|
||||
var boost uint16
|
||||
var cv float32
|
||||
var ng float32
|
||||
var curboost float32
|
||||
curboost = float32((u.Boost - 1)) / 10
|
||||
cv = au.GetGain() + curboost
|
||||
ng = cv + change
|
||||
boost = uint16(1)
|
||||
if ng > 1.0 {
|
||||
perc := uint16((ng * 10)) - 10
|
||||
perc += 1
|
||||
boost = perc
|
||||
ng = 1.0
|
||||
}
|
||||
if ng < 0 {
|
||||
ng = 0.0
|
||||
}
|
||||
u.Boost = boost
|
||||
u.Volume = ng
|
||||
if !u.LocallyMuted {
|
||||
au.SetGain(ng)
|
||||
}
|
||||
b.UserConfig.UpdateConfig(u)
|
||||
}
|
||||
b.UserConfig.SaveConfig()
|
||||
}
|
||||
|
||||
func makeUsersArray(users gumble.Users) []*gumble.User {
|
||||
t := make([]*gumble.User, 0, len(users))
|
||||
for _, u := range users {
|
||||
t = append(t, u)
|
||||
}
|
||||
return t
|
||||
t := make([]*gumble.User, 0, len(users))
|
||||
for _, u := range users {
|
||||
t = append(t, u)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
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.selectedUser == treeItem.User {
|
||||
b.SetSelectedUser(nil)
|
||||
b.GotoChat()
|
||||
} else {
|
||||
b.SetSelectedUser(treeItem.User)
|
||||
b.GotoChat()
|
||||
} //select
|
||||
} //if user and not selected
|
||||
} //if enter key
|
||||
if treeItem.Channel != nil {
|
||||
var c = treeItem.Channel
|
||||
if key == *b.Hotkeys.VolumeDown {
|
||||
b.changeVolume(makeUsersArray(c.Users), -0.1)
|
||||
}
|
||||
if key == *b.Hotkeys.VolumeUp {
|
||||
b.changeVolume(makeUsersArray(c.Users), 0.1)
|
||||
}
|
||||
} //set volume
|
||||
if treeItem.User != nil {
|
||||
var u = treeItem.User
|
||||
if key == *b.Hotkeys.VolumeDown {
|
||||
b.changeVolume([]*gumble.User{u}, -0.1)
|
||||
}
|
||||
if key == *b.Hotkeys.VolumeUp {
|
||||
b.changeVolume([]*gumble.User{u}, 0.1)
|
||||
}
|
||||
} //user highlighted
|
||||
} //func
|
||||
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.selectedUser == 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 {
|
||||
// Toggle mute for all users in channel
|
||||
users := makeUsersArray(treeItem.Channel.Users)
|
||||
for _, u := range users {
|
||||
b.UserConfig.ToggleMute(u)
|
||||
if u.AudioSource != nil {
|
||||
if u.LocallyMuted {
|
||||
u.AudioSource.SetGain(0)
|
||||
} else {
|
||||
u.AudioSource.SetGain(u.Volume)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.UiTree.Rebuild()
|
||||
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 treeItem.User != nil {
|
||||
if key == *b.Hotkeys.MuteToggle {
|
||||
// Toggle mute for single user
|
||||
b.UserConfig.ToggleMute(treeItem.User)
|
||||
if treeItem.User.AudioSource != nil {
|
||||
if treeItem.User.LocallyMuted {
|
||||
treeItem.User.AudioSource.SetGain(0)
|
||||
} else {
|
||||
treeItem.User.AudioSource.SetGain(treeItem.User.Volume)
|
||||
}
|
||||
}
|
||||
b.UiTree.Rebuild()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Barnard) TreeItemBuild(item uiterm.TreeItem) []uiterm.TreeItem {
|
||||
if b.Client == nil {
|
||||
return nil
|
||||
}
|
||||
if b.Client == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var treeItem TreeItem
|
||||
if ti, ok := item.(TreeItem); !ok {
|
||||
root := b.Client.Channels[0]
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
return []uiterm.TreeItem{
|
||||
TreeItem{
|
||||
Channel: root,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
treeItem = ti
|
||||
}
|
||||
var treeItem TreeItem
|
||||
if ti, ok := item.(TreeItem); !ok {
|
||||
root := b.Client.Channels[0]
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
return []uiterm.TreeItem{
|
||||
TreeItem{
|
||||
Channel: root,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
treeItem = ti
|
||||
}
|
||||
|
||||
if treeItem.User != nil {
|
||||
return nil
|
||||
}
|
||||
if treeItem.User != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
users := []uiterm.TreeItem{}
|
||||
ul := []*gumble.User{}
|
||||
for _, user := range treeItem.Channel.Users {
|
||||
ul = append(ul, user)
|
||||
var u = ul[len(ul)-1]
|
||||
_ = u
|
||||
}
|
||||
sort.Slice(ul, func(i, j int) bool {
|
||||
return ul[i].Name < ul[j].Name
|
||||
})
|
||||
for _, user := range ul {
|
||||
users = append(users, TreeItem{
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
users := []uiterm.TreeItem{}
|
||||
ul := []*gumble.User{}
|
||||
for _, user := range treeItem.Channel.Users {
|
||||
ul = append(ul, user)
|
||||
var u = ul[len(ul)-1]
|
||||
_ = u
|
||||
}
|
||||
sort.Slice(ul, func(i, j int) bool {
|
||||
return ul[i].Name < ul[j].Name
|
||||
})
|
||||
for _, user := range ul {
|
||||
users = append(users, TreeItem{
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
|
||||
channels := []uiterm.TreeItem{}
|
||||
cl := []*gumble.Channel{}
|
||||
for _, subchannel := range treeItem.Channel.Children {
|
||||
cl = append(cl, subchannel)
|
||||
}
|
||||
sort.Slice(cl, func(i, j int) bool {
|
||||
return cl[i].Name < cl[j].Name
|
||||
})
|
||||
for _, subchannel := range cl {
|
||||
channels = append(channels, TreeItem{
|
||||
Channel: subchannel,
|
||||
})
|
||||
}
|
||||
channels := []uiterm.TreeItem{}
|
||||
cl := []*gumble.Channel{}
|
||||
for _, subchannel := range treeItem.Channel.Children {
|
||||
cl = append(cl, subchannel)
|
||||
}
|
||||
sort.Slice(cl, func(i, j int) bool {
|
||||
return cl[i].Name < cl[j].Name
|
||||
})
|
||||
for _, subchannel := range cl {
|
||||
channels = append(channels, TreeItem{
|
||||
Channel: subchannel,
|
||||
})
|
||||
}
|
||||
|
||||
return append(users, channels...)
|
||||
return append(users, channels...)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user