Files
2026-05-19 01:06:01 -04:00

1091 lines
31 KiB
Go

package main
import (
"fmt"
"net"
"strconv"
"strings"
"time"
"git.stormux.org/storm/barnard/gumble/gumble"
"git.stormux.org/storm/barnard/uiterm"
"github.com/nsf/termbox-go"
)
type adminPrompt struct {
label string
action func(string)
}
type adminItem struct {
label string
children []uiterm.TreeItem
action func()
}
func (i adminItem) String() string {
return i.label
}
func (i adminItem) TreeItemStyle(fg, bg uiterm.Attribute, active bool) (uiterm.Attribute, uiterm.Attribute) {
if len(i.children) > 0 {
fg |= uiterm.AttrBold
}
if active {
fg, bg = bg, fg
}
return fg, bg
}
func (b *Barnard) OnAdminMenuPress(ui *uiterm.Ui, key uiterm.Key) {
b.OpenAdminMenu()
}
func (b *Barnard) OnAdminEscape(ui *uiterm.Ui, key uiterm.Key) {
if b.pendingAdminPrompt != nil {
b.pendingAdminPrompt = nil
b.UiInput.Text = ""
b.AddOutputLine("Admin: prompt canceled")
b.UpdateInputStatus(fmt.Sprintf("[%s]", b.Client.Self.Channel.Name))
return
}
if b.Ui.Active() == uiViewAdmin {
b.Ui.SetActive(uiViewTree)
width, height := termboxSize()
b.OnUiResize(ui, width, height)
b.AddOutputLine("Admin: closed")
ui.Refresh()
}
}
func (b *Barnard) OpenAdminMenu() {
if b.Client == nil || b.Client.Self == nil {
b.AddOutputLine("Admin: not connected")
return
}
b.adminTargetUser = b.selectedUser
b.adminTargetChan = b.Client.Self.Channel
if b.Ui.Active() == uiViewTree {
switch item := b.UiTree.ActiveItem().(type) {
case TreeItem:
if item.User != nil {
b.adminTargetUser = item.User
b.adminTargetChan = item.User.Channel
}
if item.Channel != nil {
b.adminTargetChan = item.Channel
b.adminTargetUser = nil
}
}
}
if b.adminTargetChan != nil {
b.adminTargetChan.RequestPermission()
}
if root := b.Client.Channels[0]; root != nil && root != b.adminTargetChan {
root.RequestPermission()
}
b.UiAdmin.Rebuild()
b.Ui.SetActive(uiViewAdmin)
width, height := termboxSize()
b.OnUiResize(b.Ui, width, height)
if b.adminTargetUser != nil {
b.AddOutputLine(fmt.Sprintf("Admin: opened for user %s", b.adminTargetUser.Name))
} else if b.adminTargetChan != nil {
b.AddOutputLine(fmt.Sprintf("Admin: opened for channel %s", b.adminTargetChan.Name))
} else {
b.AddOutputLine("Admin: opened")
}
b.Ui.Refresh()
}
func termboxSize() (int, int) {
width, height := termbox.Size()
return width, height
}
func (b *Barnard) AdminItemBuild(item uiterm.TreeItem) []uiterm.TreeItem {
if item != nil {
if admin, ok := item.(adminItem); ok {
return admin.children
}
return nil
}
target := "server"
if b.adminTargetUser != nil {
target = "user " + b.adminTargetUser.Name
} else if b.adminTargetChan != nil {
target = "channel " + b.adminTargetChan.Name
}
items := []uiterm.TreeItem{
adminItem{label: "Action target: " + target},
adminItem{label: "Information actions", children: b.informationItems()},
}
if children := b.adminUserItems(); len(children) > 0 {
items = append(items, adminItem{label: "Selected user admin actions", children: children})
}
if children := b.adminChannelItems(); len(children) > 0 {
items = append(items, adminItem{label: "Channel admin actions", children: children})
}
if children := b.adminBanItems(); len(children) > 0 {
items = append(items, adminItem{label: "Ban list admin actions", children: children})
}
if children := b.adminRegisteredUserItems(); len(children) > 0 {
items = append(items, adminItem{label: "Registered user admin actions", children: children})
}
if children := b.adminACLItems(); len(children) > 0 {
items = append(items, adminItem{label: "ACL admin actions", children: children})
}
if children := b.adminContextActionItems(); len(children) > 0 {
items = append(items, adminItem{label: "Context actions", children: children})
}
items = append(items, adminItem{label: "Close actions menu", action: func() { b.OnAdminEscape(b.Ui, uiterm.KeyEsc) }})
return items
}
func (b *Barnard) AdminItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, key uiterm.Key) {
if key != uiterm.KeyEnter {
return
}
admin, ok := item.(adminItem)
if !ok || admin.action == nil {
return
}
admin.action()
b.UiAdmin.Rebuild()
b.Ui.Refresh()
}
func (b *Barnard) AdminItemCharacter(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem, ch rune) {
}
func (b *Barnard) informationItems() []uiterm.TreeItem {
items := []uiterm.TreeItem{}
if u := b.adminTargetUser; u != nil {
items = append(items,
adminItem{label: "Request user stats for " + u.Name, action: func() {
u.RequestStats()
b.AddOutputLine(fmt.Sprintf("Actions: requested stats for %s", u.Name))
b.showOutputAfterInfoRequest()
}},
adminItem{label: "Request user comment for " + u.Name, action: func() {
u.RequestComment()
b.AddOutputLine(fmt.Sprintf("Actions: requested comment for %s", u.Name))
b.showOutputAfterInfoRequest()
}},
)
}
if ch := b.adminTargetChan; ch != nil {
items = append(items,
adminItem{label: "Move self to " + ch.Name, action: func() {
b.Client.Self.Move(ch)
b.AddOutputLine(fmt.Sprintf("Actions: moving to %s", ch.Name))
}},
adminItem{label: "Request channel description for " + ch.Name, action: func() {
ch.RequestDescription()
b.AddOutputLine(fmt.Sprintf("Actions: requested description for %s", ch.Name))
b.showOutputAfterInfoRequest()
}},
adminItem{label: "Request channel permissions for " + ch.Name, action: func() {
ch.RequestPermission()
b.AddOutputLine(fmt.Sprintf("Actions: requested permissions for %s", ch.Name))
b.showOutputAfterInfoRequest()
}},
)
}
if len(items) == 0 {
return []uiterm.TreeItem{adminItem{label: "No information actions available"}}
}
return items
}
func (b *Barnard) showOutputAfterInfoRequest() {
b.Ui.SetActive(uiViewInput)
width, height := termboxSize()
b.OnUiResize(b.Ui, width, height)
}
func (b *Barnard) adminUserItems() []uiterm.TreeItem {
u := b.adminTargetUser
if u == nil {
return nil
}
items := []uiterm.TreeItem{}
if b.adminCanRoot(gumble.PermissionKick) {
items = append(items, adminItem{label: "Kick " + u.Name, action: func() {
b.promptAdmin("Kick reason for "+u.Name, func(reason string) {
u.Kick(reason)
b.AddOutputLine(fmt.Sprintf("Admin: kick sent for %s", u.Name))
})
}})
}
if b.adminCanRoot(gumble.PermissionBan) {
items = append(items, adminItem{label: "Ban " + u.Name, action: func() {
b.promptAdmin("Ban reason for "+u.Name, func(reason string) {
u.Ban(reason)
b.AddOutputLine(fmt.Sprintf("Admin: ban sent for %s", u.Name))
})
}})
}
if b.adminCanChannel(u.Channel, gumble.PermissionMuteDeafen) {
items = append(items, adminItem{label: fmt.Sprintf("Server mute: %s", onOff(u.Muted)), action: func() {
u.SetMuted(!u.Muted)
b.AddOutputLine(fmt.Sprintf("Admin: server mute toggled for %s", u.Name))
}})
items = append(items, adminItem{label: fmt.Sprintf("Server deafen: %s", onOff(u.Deafened)), action: func() {
u.SetDeafened(!u.Deafened)
b.AddOutputLine(fmt.Sprintf("Admin: server deafen toggled for %s", u.Name))
}})
items = append(items, adminItem{label: fmt.Sprintf("Suppress in channel: %s", onOff(u.Suppressed)), action: func() {
u.SetSuppressed(!u.Suppressed)
b.AddOutputLine(fmt.Sprintf("Admin: suppress toggled for %s", u.Name))
}})
items = append(items, adminItem{label: fmt.Sprintf("Priority speaker: %s", onOff(u.PrioritySpeaker)), action: func() {
u.SetPrioritySpeaker(!u.PrioritySpeaker)
b.AddOutputLine(fmt.Sprintf("Admin: priority speaker toggled for %s", u.Name))
}})
}
if b.adminCanChannel(b.Client.Self.Channel, gumble.PermissionMove) {
items = append(items, adminItem{label: "Move user to current channel", action: func() {
u.Move(b.Client.Self.Channel)
b.AddOutputLine(fmt.Sprintf("Admin: move sent for %s to %s", u.Name, b.Client.Self.Channel.Name))
}})
}
if b.adminCanRoot(gumble.PermissionRegister) {
items = append(items, adminItem{label: "Register user", action: func() {
u.Register()
b.AddOutputLine(fmt.Sprintf("Admin: register sent for %s", u.Name))
}})
}
return items
}
func (b *Barnard) adminChannelItems() []uiterm.TreeItem {
ch := b.adminTargetChan
if ch == nil {
return nil
}
items := []uiterm.TreeItem{}
if b.adminCanChannel(ch, gumble.PermissionMakeChannel) {
items = append(items, adminItem{label: "Create child channel", action: func() {
b.promptAdmin("New channel name under "+ch.Name, func(name string) {
name = strings.TrimSpace(name)
if name == "" {
b.AddOutputLine("Admin: channel name required")
return
}
ch.Add(name, false)
b.AddOutputLine(fmt.Sprintf("Admin: create channel sent for %s", name))
})
}})
}
if b.adminCanChannel(ch, gumble.PermissionMakeTemporaryChannel) {
items = append(items, adminItem{label: "Create temporary child channel", action: func() {
b.promptAdmin("Temporary channel name under "+ch.Name, func(name string) {
name = strings.TrimSpace(name)
if name == "" {
b.AddOutputLine("Admin: channel name required")
return
}
ch.Add(name, true)
b.AddOutputLine(fmt.Sprintf("Admin: create temporary channel sent for %s", name))
})
}})
}
if b.adminCanChannel(ch, gumble.PermissionWrite) {
items = append(items, adminItem{label: "Rename channel", action: func() {
b.promptAdmin("New name for "+ch.Name, func(name string) {
name = strings.TrimSpace(name)
if name == "" {
b.AddOutputLine("Admin: channel name required")
return
}
ch.SetName(name)
b.AddOutputLine(fmt.Sprintf("Admin: rename channel sent for %s", ch.Name))
})
}})
items = append(items, adminItem{label: "Set channel description", action: func() {
b.promptAdmin("Description for "+ch.Name, func(description string) {
ch.SetDescription(description)
b.AddOutputLine(fmt.Sprintf("Admin: channel description sent for %s", ch.Name))
})
}})
items = append(items, adminItem{label: "Set max users", action: func() {
b.promptAdmin("Max users for "+ch.Name+" (0 for server default)", func(text string) {
maxUsers, err := strconv.ParseUint(strings.TrimSpace(text), 10, 32)
if err != nil {
b.AddOutputLine("Admin: max users must be a number")
return
}
ch.SetMaxUsers(uint32(maxUsers))
b.AddOutputLine(fmt.Sprintf("Admin: max users sent for %s", ch.Name))
})
}})
items = append(items, adminItem{label: "Delete channel", action: func() {
if ch.IsRoot() {
b.AddOutputLine("Admin: cannot delete the root channel")
return
}
b.promptAdmin("Type delete to remove channel "+ch.Name, func(text string) {
if strings.TrimSpace(strings.ToLower(text)) != "delete" {
b.AddOutputLine("Admin: delete channel canceled")
return
}
ch.Remove()
b.AddOutputLine(fmt.Sprintf("Admin: delete channel sent for %s", ch.Name))
})
}})
}
if len(ch.Links) > 0 && b.adminCanChannel(ch, gumble.PermissionLinkChannel) {
items = append(items, adminItem{label: "Unlink all channels", action: func() {
ch.Unlink()
b.AddOutputLine(fmt.Sprintf("Admin: unlink all sent for %s", ch.Name))
}})
}
return items
}
func (b *Barnard) adminBanItems() []uiterm.TreeItem {
if !b.adminCanRoot(gumble.PermissionBan) {
return nil
}
items := []uiterm.TreeItem{
adminItem{label: "Request ban list", action: func() {
b.Client.RequestBanList()
b.AddOutputLine("Admin: requested ban list")
}},
adminItem{label: "Add manual ban", action: func() {
b.promptAdmin("Ban as ip/mask minutes reason", b.addManualBan)
}},
}
for index, ban := range b.adminBanList {
label := fmt.Sprintf("Ban %d: %s %s", index+1, ban.Address.String(), ban.Reason)
i := index
items = append(items, adminItem{label: label, children: []uiterm.TreeItem{
adminItem{label: "Unban this entry", action: func() {
b.unbanIndex(i)
}},
}})
}
return items
}
func (b *Barnard) adminRegisteredUserItems() []uiterm.TreeItem {
if !b.adminCanRoot(gumble.PermissionRegister) {
return nil
}
items := []uiterm.TreeItem{
adminItem{label: "Request registered user list", action: func() {
b.Client.RequestUserList()
b.AddOutputLine("Admin: requested registered user list")
}},
}
for _, user := range b.adminUserList {
u := user
label := fmt.Sprintf("Registered user %d: %s", u.UserID, u.Name)
items = append(items, adminItem{label: label, children: []uiterm.TreeItem{
adminItem{label: "Rename " + u.Name, action: func() {
b.promptAdmin("New name for "+u.Name, func(name string) {
name = strings.TrimSpace(name)
if name == "" {
b.AddOutputLine("Admin: registered user name required")
return
}
u.SetName(name)
b.Client.Send(b.adminUserList)
b.AddOutputLine(fmt.Sprintf("Admin: rename registered user sent for %d", u.UserID))
})
}},
adminItem{label: "Deregister " + u.Name, action: func() {
b.promptAdmin("Type deregister to remove "+u.Name, func(text string) {
if strings.TrimSpace(strings.ToLower(text)) != "deregister" {
b.AddOutputLine("Admin: deregister canceled")
return
}
u.Deregister()
b.Client.Send(b.adminUserList)
b.AddOutputLine(fmt.Sprintf("Admin: deregister sent for %s", u.Name))
})
}},
}})
}
return items
}
func (b *Barnard) adminACLItems() []uiterm.TreeItem {
ch := b.adminTargetChan
if ch == nil {
return nil
}
if !b.adminCanChannel(ch, gumble.PermissionWrite) {
return nil
}
items := []uiterm.TreeItem{
adminItem{label: "Request ACLs for " + ch.Name, action: func() {
ch.RequestACL()
b.AddOutputLine(fmt.Sprintf("Admin: requested ACLs for %s", ch.Name))
}},
}
if b.adminACL == nil || b.adminACL.Channel != ch {
return append(items, adminItem{label: "No ACL loaded for this channel"})
}
if b.adminTargetUser != nil && b.adminTargetUser.IsRegistered() {
for _, permission := range commonAdminPermissions() {
perm := permission
items = append(items, adminItem{label: "Toggle user grant " + perm.name, action: func() {
b.toggleACLUserGrant(b.adminTargetUser, perm.permission)
}})
}
} else if b.adminTargetUser != nil {
items = append(items, adminItem{label: "Selected user is not registered; user ACL toggles unavailable"})
}
items = append(items, adminItem{label: fmt.Sprintf("Inherit ACLs: %s", onOff(b.adminACL.Inherits)), action: func() {
b.adminACL.Inherits = !b.adminACL.Inherits
b.Client.Send(b.adminACL)
b.AddOutputLine(fmt.Sprintf("Admin: ACL inheritance toggled for %s", b.adminACL.Channel.Name))
}})
for index, rule := range b.adminACL.Rules {
items = append(items, adminItem{label: fmt.Sprintf("Rule %d: %s grant=%s deny=%s", index+1, aclSubject(rule), permissionList(rule.Granted), permissionList(rule.Denied))})
}
items = append(items, adminItem{label: "Advanced raw ACL command", action: func() {
b.promptAdmin("acl grant|deny permission user ID or group NAME", func(text string) {
b.executeAdminCommand("acl " + text)
})
}})
return items
}
func (b *Barnard) adminContextActionItems() []uiterm.TreeItem {
if b.Client == nil || len(b.Client.ContextActions) == 0 {
return []uiterm.TreeItem{adminItem{label: "No context actions available"}}
}
items := []uiterm.TreeItem{}
for _, action := range b.Client.ContextActions {
ca := action
label := ca.Label
if label == "" {
label = ca.Name
}
children := []uiterm.TreeItem{}
if ca.Type&gumble.ContextActionServer != 0 {
children = append(children, adminItem{label: "Trigger on server", action: func() {
ca.Trigger()
b.AddOutputLine("Admin: context action sent: " + ca.Name)
}})
}
if ca.Type&gumble.ContextActionUser != 0 && b.adminTargetUser != nil {
children = append(children, adminItem{label: "Trigger on user " + b.adminTargetUser.Name, action: func() {
ca.TriggerUser(b.adminTargetUser)
b.AddOutputLine("Admin: context action sent: " + ca.Name)
}})
}
if ca.Type&gumble.ContextActionChannel != 0 && b.adminTargetChan != nil {
children = append(children, adminItem{label: "Trigger on channel " + b.adminTargetChan.Name, action: func() {
ca.TriggerChannel(b.adminTargetChan)
b.AddOutputLine("Admin: context action sent: " + ca.Name)
}})
}
items = append(items, adminItem{label: label, children: children})
}
return items
}
func (b *Barnard) promptAdmin(label string, action func(string)) {
b.pendingAdminPrompt = &adminPrompt{label: label, action: action}
b.UpdateInputStatus("[admin]")
b.Ui.SetActive(uiViewInput)
width, height := termboxSize()
b.OnUiResize(b.Ui, width, height)
b.AddOutputLine("Admin: " + label)
}
func (b *Barnard) handleAdminPrompt(text string) bool {
if b.pendingAdminPrompt == nil {
return false
}
prompt := b.pendingAdminPrompt
b.pendingAdminPrompt = nil
prompt.action(strings.TrimSpace(text))
if b.Client != nil && b.Client.Self != nil {
b.UpdateInputStatus(fmt.Sprintf("[%s]", b.Client.Self.Channel.Name))
}
return true
}
func (b *Barnard) CommandAdmin(ui *uiterm.Ui, cmd string) {
b.executeAdminCommand(cmd)
}
func (b *Barnard) executeAdminCommand(cmd string) {
fields := strings.Fields(cmd)
if len(fields) == 0 || fields[0] == "menu" {
b.OpenAdminMenu()
return
}
if b.Client == nil || b.Client.Self == nil {
b.AddOutputLine("Admin: not connected")
return
}
switch fields[0] {
case "kick":
user, reason := b.adminCommandUserAndReason(fields[1:])
if user == nil {
return
}
user.Kick(reason)
b.AddOutputLine(fmt.Sprintf("Admin: kick sent for %s", user.Name))
case "ban":
user, reason := b.adminCommandUserAndReason(fields[1:])
if user == nil {
return
}
user.Ban(reason)
b.AddOutputLine(fmt.Sprintf("Admin: ban sent for %s", user.Name))
case "mute", "deafen", "suppress", "priority":
b.executeUserToggle(fields)
case "move":
b.executeMoveCommand(fields)
case "channel":
b.executeChannelCommand(fields)
case "banlist":
b.Client.RequestBanList()
b.AddOutputLine("Admin: requested ban list")
case "unban":
if len(fields) < 2 {
b.AddOutputLine("Admin: usage /admin unban <index>")
return
}
index, err := strconv.Atoi(fields[1])
if err != nil {
b.AddOutputLine("Admin: unban index must be a number")
return
}
b.unbanIndex(index - 1)
case "users":
b.Client.RequestUserList()
b.AddOutputLine("Admin: requested registered user list")
case "renameuser":
b.executeRenameRegisteredUser(fields)
case "deregister":
b.executeDeregisterRegisteredUser(fields)
case "acl":
b.executeACLCommand(fields[1:])
case "context":
b.executeContextCommand(fields)
default:
b.AddOutputLine(fmt.Sprintf("Admin: unknown command %s", fields[0]))
}
}
func (b *Barnard) adminCommandUserAndReason(args []string) (*gumble.User, string) {
if len(args) == 0 {
b.AddOutputLine("Admin: user required")
return nil, ""
}
user := b.findUser(args[0])
if user == nil {
b.AddOutputLine("Admin: user not found: " + args[0])
return nil, ""
}
return user, strings.Join(args[1:], " ")
}
func (b *Barnard) executeUserToggle(fields []string) {
if len(fields) < 2 {
b.AddOutputLine(fmt.Sprintf("Admin: usage /admin %s <user> [on|off|toggle]", fields[0]))
return
}
user := b.findUser(fields[1])
if user == nil {
b.AddOutputLine("Admin: user not found: " + fields[1])
return
}
stateArg := "toggle"
if len(fields) > 2 {
stateArg = fields[2]
}
var current bool
var set func(bool)
switch fields[0] {
case "mute":
current, set = user.Muted, user.SetMuted
case "deafen":
current, set = user.Deafened, user.SetDeafened
case "suppress":
current, set = user.Suppressed, user.SetSuppressed
case "priority":
current, set = user.PrioritySpeaker, user.SetPrioritySpeaker
}
state, ok := parseToggleState(stateArg, current)
if !ok {
b.AddOutputLine("Admin: state must be on, off, or toggle")
return
}
set(state)
b.AddOutputLine(fmt.Sprintf("Admin: %s set to %s for %s", fields[0], onOff(state), user.Name))
}
func (b *Barnard) executeMoveCommand(fields []string) {
if len(fields) < 3 {
b.AddOutputLine("Admin: usage /admin move <user> <channel-id-or-name>")
return
}
user := b.findUser(fields[1])
channel := b.findChannel(strings.Join(fields[2:], " "))
if user == nil {
b.AddOutputLine("Admin: user not found: " + fields[1])
return
}
if channel == nil {
b.AddOutputLine("Admin: channel not found")
return
}
user.Move(channel)
b.AddOutputLine(fmt.Sprintf("Admin: move sent for %s to %s", user.Name, channel.Name))
}
func (b *Barnard) executeChannelCommand(fields []string) {
if len(fields) < 3 {
b.AddOutputLine("Admin: usage /admin channel <add|temp|rename|describe|delete|maxusers> ...")
return
}
channel := b.findChannel(fields[2])
switch fields[1] {
case "add", "temp":
if len(fields) < 4 {
b.AddOutputLine("Admin: child channel name required")
return
}
if channel == nil {
b.AddOutputLine("Admin: parent channel not found")
return
}
name := strings.Join(fields[3:], " ")
channel.Add(name, fields[1] == "temp")
b.AddOutputLine(fmt.Sprintf("Admin: create channel sent for %s", name))
case "rename":
if len(fields) < 4 {
b.AddOutputLine("Admin: new channel name required")
return
}
if channel == nil {
b.AddOutputLine("Admin: channel not found")
return
}
channel.SetName(strings.Join(fields[3:], " "))
b.AddOutputLine("Admin: rename channel sent")
case "describe":
if channel == nil {
b.AddOutputLine("Admin: channel not found")
return
}
channel.SetDescription(strings.Join(fields[3:], " "))
b.AddOutputLine("Admin: channel description sent")
case "delete":
if channel == nil {
b.AddOutputLine("Admin: channel not found")
return
}
if channel.IsRoot() {
b.AddOutputLine("Admin: cannot delete the root channel")
return
}
channel.Remove()
b.AddOutputLine("Admin: delete channel sent")
case "maxusers":
if len(fields) < 4 {
b.AddOutputLine("Admin: max users value required")
return
}
if channel == nil {
b.AddOutputLine("Admin: channel not found")
return
}
maxUsers, err := strconv.ParseUint(fields[3], 10, 32)
if err != nil {
b.AddOutputLine("Admin: max users must be a number")
return
}
channel.SetMaxUsers(uint32(maxUsers))
b.AddOutputLine("Admin: max users sent")
default:
b.AddOutputLine("Admin: unknown channel command " + fields[1])
}
}
func (b *Barnard) executeRenameRegisteredUser(fields []string) {
if len(fields) < 3 {
b.AddOutputLine("Admin: usage /admin renameuser <id-or-name> <new-name>")
return
}
user := b.findRegisteredUser(fields[1])
if user == nil {
b.AddOutputLine("Admin: registered user not found")
return
}
user.SetName(strings.Join(fields[2:], " "))
b.Client.Send(b.adminUserList)
b.AddOutputLine(fmt.Sprintf("Admin: rename registered user sent for %d", user.UserID))
}
func (b *Barnard) executeDeregisterRegisteredUser(fields []string) {
if len(fields) < 2 {
b.AddOutputLine("Admin: usage /admin deregister <id-or-name>")
return
}
user := b.findRegisteredUser(fields[1])
if user == nil {
b.AddOutputLine("Admin: registered user not found")
return
}
user.Deregister()
b.Client.Send(b.adminUserList)
b.AddOutputLine(fmt.Sprintf("Admin: deregister sent for %s", user.Name))
}
func (b *Barnard) executeACLCommand(fields []string) {
if len(fields) == 0 || fields[0] == "request" {
ch := b.adminTargetChan
if len(fields) > 1 {
ch = b.findChannel(strings.Join(fields[1:], " "))
}
if ch == nil {
b.AddOutputLine("Admin: channel not found")
return
}
ch.RequestACL()
b.AddOutputLine(fmt.Sprintf("Admin: requested ACLs for %s", ch.Name))
return
}
if b.adminACL == nil {
b.AddOutputLine("Admin: request ACLs before editing")
return
}
if len(fields) < 4 {
b.AddOutputLine("Admin: usage /admin acl grant|deny <permission> user <id> | group <name>")
return
}
permission, ok := parsePermission(fields[1])
if !ok {
b.AddOutputLine("Admin: unknown permission " + fields[1])
return
}
switch fields[0] {
case "grant", "deny":
default:
b.AddOutputLine("Admin: ACL command must be grant or deny")
return
}
rule := b.findOrCreateACLRule(fields[2], strings.Join(fields[3:], " "))
if rule == nil {
return
}
if fields[0] == "grant" {
rule.Granted ^= permission
rule.Denied &^= permission
} else {
rule.Denied ^= permission
rule.Granted &^= permission
}
b.Client.Send(b.adminACL)
b.AddOutputLine("Admin: ACL update sent")
}
func (b *Barnard) executeContextCommand(fields []string) {
if len(fields) < 2 {
b.AddOutputLine("Admin: usage /admin context <action> [server|user|channel] [target]")
return
}
action := b.Client.ContextActions[fields[1]]
if action == nil {
b.AddOutputLine("Admin: context action not found")
return
}
scope := "server"
if len(fields) > 2 {
scope = fields[2]
}
switch scope {
case "server":
action.Trigger()
case "user":
if len(fields) < 4 {
b.AddOutputLine("Admin: context user target required")
return
}
user := b.findUser(fields[3])
if user == nil {
b.AddOutputLine("Admin: user not found")
return
}
action.TriggerUser(user)
case "channel":
if len(fields) < 4 {
b.AddOutputLine("Admin: context channel target required")
return
}
channel := b.findChannel(strings.Join(fields[3:], " "))
if channel == nil {
b.AddOutputLine("Admin: channel not found")
return
}
action.TriggerChannel(channel)
default:
b.AddOutputLine("Admin: context scope must be server, user, or channel")
return
}
b.AddOutputLine("Admin: context action sent: " + action.Name)
}
func (b *Barnard) addManualBan(text string) {
parts := strings.Fields(text)
if len(parts) < 3 {
b.AddOutputLine("Admin: usage ip/mask minutes reason")
return
}
ip, network, err := net.ParseCIDR(parts[0])
if err != nil {
b.AddOutputLine("Admin: ban address must be CIDR, for example 192.0.2.1/32")
return
}
minutes, err := strconv.Atoi(parts[1])
if err != nil {
b.AddOutputLine("Admin: ban minutes must be a number")
return
}
reason := strings.Join(parts[2:], " ")
b.adminBanList.Add(ip, network.Mask, reason, time.Duration(minutes)*time.Minute)
b.Client.Send(b.adminBanList)
b.AddOutputLine("Admin: manual ban sent")
}
func (b *Barnard) unbanIndex(index int) {
if index < 0 || index >= len(b.adminBanList) {
b.AddOutputLine("Admin: ban index out of range")
return
}
b.adminBanList[index].Unban()
b.Client.Send(b.adminBanList)
b.AddOutputLine(fmt.Sprintf("Admin: unban sent for entry %d", index+1))
}
func (b *Barnard) toggleACLUserGrant(user *gumble.User, permission gumble.Permission) {
rule := b.findOrCreateACLRule("user", strconv.FormatUint(uint64(user.UserID), 10))
if rule == nil {
return
}
rule.Granted ^= permission
rule.Denied &^= permission
b.Client.Send(b.adminACL)
b.AddOutputLine(fmt.Sprintf("Admin: ACL grant toggled for %s", user.Name))
}
func (b *Barnard) findOrCreateACLRule(subjectType, subject string) *gumble.ACLRule {
switch subjectType {
case "user":
userID64, err := strconv.ParseUint(subject, 10, 32)
if err != nil {
b.AddOutputLine("Admin: ACL user id must be numeric")
return nil
}
userID := uint32(userID64)
for _, rule := range b.adminACL.Rules {
if rule.User != nil && rule.User.UserID == userID {
return rule
}
}
rule := &gumble.ACLRule{
AppliesCurrent: true,
User: &gumble.ACLUser{UserID: userID},
}
b.adminACL.Rules = append(b.adminACL.Rules, rule)
return rule
case "group":
for _, rule := range b.adminACL.Rules {
if rule.Group != nil && rule.Group.Name == subject {
return rule
}
}
group := &gumble.ACLGroup{Name: subject}
rule := &gumble.ACLRule{
AppliesCurrent: true,
Group: group,
}
b.adminACL.Rules = append(b.adminACL.Rules, rule)
return rule
default:
b.AddOutputLine("Admin: ACL subject must be user or group")
return nil
}
}
func (b *Barnard) findUser(token string) *gumble.User {
if b.Client == nil {
return nil
}
if session, err := strconv.ParseUint(token, 10, 32); err == nil {
if user := b.Client.Users[uint32(session)]; user != nil {
return user
}
}
for _, user := range b.Client.Users {
if strings.EqualFold(user.Name, token) {
return user
}
}
return nil
}
func (b *Barnard) findChannel(token string) *gumble.Channel {
if b.Client == nil {
return nil
}
token = strings.TrimSpace(token)
if id, err := strconv.ParseUint(token, 10, 32); err == nil {
if channel := b.Client.Channels[uint32(id)]; channel != nil {
return channel
}
}
for _, channel := range b.Client.Channels {
if strings.EqualFold(channel.Name, token) {
return channel
}
}
return nil
}
func (b *Barnard) findRegisteredUser(token string) *gumble.RegisteredUser {
if id, err := strconv.ParseUint(token, 10, 32); err == nil {
for _, user := range b.adminUserList {
if user.UserID == uint32(id) {
return user
}
}
}
for _, user := range b.adminUserList {
if strings.EqualFold(user.Name, token) {
return user
}
}
return nil
}
func (b *Barnard) adminCanRoot(permission gumble.Permission) bool {
if b.Client == nil {
return true
}
return b.adminCanChannel(b.Client.Channels[0], permission)
}
func (b *Barnard) adminCanChannel(channel *gumble.Channel, permission gumble.Permission) bool {
if channel == nil {
return true
}
current := channel.Permission()
if current == nil {
return true
}
return current.Has(permission)
}
func parseToggleState(text string, current bool) (bool, bool) {
switch strings.ToLower(strings.TrimSpace(text)) {
case "", "toggle":
return !current, true
case "on", "true", "yes", "1":
return true, true
case "off", "false", "no", "0":
return false, true
default:
return false, false
}
}
type permissionOption struct {
name string
permission gumble.Permission
}
func commonAdminPermissions() []permissionOption {
return []permissionOption{
{"write", gumble.PermissionWrite},
{"enter", gumble.PermissionEnter},
{"speak", gumble.PermissionSpeak},
{"mute_deafen", gumble.PermissionMuteDeafen},
{"move", gumble.PermissionMove},
{"kick", gumble.PermissionKick},
{"ban", gumble.PermissionBan},
}
}
func parsePermission(text string) (gumble.Permission, bool) {
for _, option := range commonAdminPermissions() {
if option.name == text {
return option.permission, true
}
}
switch text {
case "traverse":
return gumble.PermissionTraverse, true
case "make_channel":
return gumble.PermissionMakeChannel, true
case "link_channel":
return gumble.PermissionLinkChannel, true
case "whisper":
return gumble.PermissionWhisper, true
case "text_message":
return gumble.PermissionTextMessage, true
case "make_temp_channel":
return gumble.PermissionMakeTemporaryChannel, true
case "register":
return gumble.PermissionRegister, true
case "register_self":
return gumble.PermissionRegisterSelf, true
default:
return 0, false
}
}
func permissionList(permission gumble.Permission) string {
if permission == 0 {
return "none"
}
names := []string{}
for _, option := range append(commonAdminPermissions(), []permissionOption{
{"traverse", gumble.PermissionTraverse},
{"make_channel", gumble.PermissionMakeChannel},
{"link_channel", gumble.PermissionLinkChannel},
{"whisper", gumble.PermissionWhisper},
{"text_message", gumble.PermissionTextMessage},
{"make_temp_channel", gumble.PermissionMakeTemporaryChannel},
{"register", gumble.PermissionRegister},
{"register_self", gumble.PermissionRegisterSelf},
}...) {
if permission.Has(option.permission) {
names = append(names, option.name)
}
}
return strings.Join(names, ",")
}
func aclSubject(rule *gumble.ACLRule) string {
if rule.User != nil {
if rule.User.Name != "" {
return "user " + rule.User.Name
}
return fmt.Sprintf("user %d", rule.User.UserID)
}
if rule.Group != nil {
return "group " + rule.Group.Name
}
return "all"
}
func onOff(value bool) string {
if value {
return "on"
}
return "off"
}