mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 13:29:27 +01:00
Merge pull request #270 from slingamn/amode.1
frontend for persistent account modes in channels
This commit is contained in:
commit
7bf18443a8
@ -232,13 +232,7 @@ func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) b
|
||||
|
||||
clientModes := channel.members[client]
|
||||
|
||||
// get voice, since it's not a part of ChannelPrivModes
|
||||
if clientModes.HasMode(permission) {
|
||||
return true
|
||||
}
|
||||
|
||||
// check regular modes
|
||||
for _, mode := range modes.ChannelPrivModes {
|
||||
for _, mode := range modes.ChannelUserModes {
|
||||
if clientModes.HasMode(mode) {
|
||||
return true
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
@ -42,6 +43,17 @@ remembered.`,
|
||||
helpShort: `$bREGISTER$b lets you own a given channel.`,
|
||||
authRequired: true,
|
||||
},
|
||||
"amode": {
|
||||
handler: csAmodeHandler,
|
||||
help: `Syntax: $bAMODE #channel [mode change] [account]$b
|
||||
|
||||
AMODE lists or modifies persistent mode settings that affect channel members.
|
||||
For example, $bAMODE #channel +o dan$b grants the the holder of the "dan"
|
||||
account the +o operator mode every time they join #channel. To list current
|
||||
accounts and modes, use $bAMODE #channel$b. Note that users are always
|
||||
referenced by their registered account names, not their nicknames.`,
|
||||
helpShort: `$bAMODE$b modifies persistent mode settings for channel members.`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@ -50,6 +62,79 @@ func csNotice(rb *ResponseBuffer, text string) {
|
||||
rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text)
|
||||
}
|
||||
|
||||
func csAmodeHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
channelName, modeChange := utils.ExtractParam(params)
|
||||
|
||||
channel := server.channels.Get(channelName)
|
||||
if channel == nil {
|
||||
csNotice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
} else if channel.Founder() == "" {
|
||||
csNotice(rb, client.t("Channel is not registered"))
|
||||
return
|
||||
}
|
||||
|
||||
modeChanges, unknown := modes.ParseChannelModeChanges(strings.Fields(modeChange)...)
|
||||
var change modes.ModeChange
|
||||
if len(modeChanges) > 1 || len(unknown) > 0 {
|
||||
csNotice(rb, client.t("Invalid mode change"))
|
||||
return
|
||||
} else if len(modeChanges) == 1 {
|
||||
change = modeChanges[0]
|
||||
} else {
|
||||
change = modes.ModeChange{Op: modes.List}
|
||||
}
|
||||
|
||||
// normalize and validate the account argument
|
||||
accountIsValid := false
|
||||
change.Arg, _ = CasefoldName(change.Arg)
|
||||
switch change.Op {
|
||||
case modes.List:
|
||||
accountIsValid = true
|
||||
case modes.Add:
|
||||
// if we're adding a mode, the account must exist
|
||||
if change.Arg != "" {
|
||||
_, err := server.accounts.LoadAccount(change.Arg)
|
||||
accountIsValid = (err == nil)
|
||||
}
|
||||
case modes.Remove:
|
||||
// allow removal of accounts that may have been deleted
|
||||
accountIsValid = (change.Arg != "")
|
||||
}
|
||||
if !accountIsValid {
|
||||
csNotice(rb, client.t("Account does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
affectedModes, err := channel.ProcessAccountToUmodeChange(client, change)
|
||||
|
||||
if err == errInsufficientPrivs {
|
||||
csNotice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
} else if err != nil {
|
||||
csNotice(rb, client.t("Internal error"))
|
||||
return
|
||||
}
|
||||
|
||||
switch change.Op {
|
||||
case modes.List:
|
||||
// sort the persistent modes in descending order of priority
|
||||
sort.Slice(affectedModes, func(i, j int) bool {
|
||||
return umodeGreaterThan(affectedModes[i].Mode, affectedModes[j].Mode)
|
||||
})
|
||||
csNotice(rb, fmt.Sprintf(client.t("Channel %s has %d persistent modes set"), channelName, len(affectedModes)))
|
||||
for _, modeChange := range affectedModes {
|
||||
csNotice(rb, fmt.Sprintf(client.t("Account %s receives mode +%s"), modeChange.Arg, string(modeChange.Mode)))
|
||||
}
|
||||
case modes.Add, modes.Remove:
|
||||
if len(affectedModes) > 0 {
|
||||
csNotice(rb, fmt.Sprintf(client.t("Successfully set mode %s"), change.String()))
|
||||
} else {
|
||||
csNotice(rb, client.t("Change was a no-op"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func csOpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
channelName, clientToOp := utils.ExtractParam(params)
|
||||
|
||||
|
@ -34,6 +34,7 @@ var (
|
||||
errNoExistingBan = errors.New("Ban does not exist")
|
||||
errNoSuchChannel = errors.New("No such channel")
|
||||
errRenamePrivsNeeded = errors.New("Only chanops can rename channels")
|
||||
errInsufficientPrivs = errors.New("Insufficient privileges")
|
||||
errSaslFail = errors.New("SASL failed")
|
||||
)
|
||||
|
||||
|
76
irc/modes.go
76
irc/modes.go
@ -240,3 +240,79 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||
|
||||
return applied
|
||||
}
|
||||
|
||||
// tests whether l > r, in the channel-user mode ordering (e.g., Halfop > Voice)
|
||||
func umodeGreaterThan(l modes.Mode, r modes.Mode) bool {
|
||||
for _, mode := range modes.ChannelUserModes {
|
||||
if l == mode && r != mode {
|
||||
return true
|
||||
} else if r == mode {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ProcessAccountToUmodeChange processes Add/Remove/List operations for channel persistent usermodes.
|
||||
func (channel *Channel) ProcessAccountToUmodeChange(client *Client, change modes.ModeChange) (results []modes.ModeChange, err error) {
|
||||
umodeGEQ := func(l modes.Mode, r modes.Mode) bool {
|
||||
return l == r || umodeGreaterThan(l, r)
|
||||
}
|
||||
|
||||
account := client.Account()
|
||||
isOperChange := client.HasRoleCapabs("chanreg")
|
||||
|
||||
channel.stateMutex.Lock()
|
||||
defer channel.stateMutex.Unlock()
|
||||
|
||||
clientMode := channel.accountToUMode[account]
|
||||
targetModeNow := channel.accountToUMode[change.Arg]
|
||||
var targetModeAfter modes.Mode
|
||||
if change.Op == modes.Add {
|
||||
targetModeAfter = change.Mode
|
||||
}
|
||||
|
||||
// operators and founders can do anything
|
||||
hasPrivs := isOperChange || (account != "" && account == channel.registeredFounder)
|
||||
// halfop and up can list, and do add/removes at levels <= their own
|
||||
if change.Op == modes.List && umodeGEQ(clientMode, modes.Halfop) {
|
||||
hasPrivs = true
|
||||
} else if umodeGEQ(clientMode, modes.Halfop) && umodeGEQ(clientMode, targetModeNow) && umodeGEQ(clientMode, targetModeAfter) {
|
||||
hasPrivs = true
|
||||
}
|
||||
if !hasPrivs {
|
||||
return nil, errInsufficientPrivs
|
||||
}
|
||||
|
||||
switch change.Op {
|
||||
case modes.Add:
|
||||
if targetModeNow != targetModeAfter {
|
||||
channel.accountToUMode[change.Arg] = change.Mode
|
||||
go client.server.channelRegistry.StoreChannel(channel, IncludeLists)
|
||||
return []modes.ModeChange{change}, nil
|
||||
}
|
||||
return nil, nil
|
||||
case modes.Remove:
|
||||
if targetModeNow == change.Mode {
|
||||
delete(channel.accountToUMode, change.Arg)
|
||||
go client.server.channelRegistry.StoreChannel(channel, IncludeLists)
|
||||
return []modes.ModeChange{change}, nil
|
||||
}
|
||||
return nil, nil
|
||||
case modes.List:
|
||||
result := make([]modes.ModeChange, len(channel.accountToUMode))
|
||||
pos := 0
|
||||
for account, mode := range channel.accountToUMode {
|
||||
result[pos] = modes.ModeChange{
|
||||
Mode: mode,
|
||||
Arg: account,
|
||||
Op: modes.Add,
|
||||
}
|
||||
pos++
|
||||
}
|
||||
return result, nil
|
||||
default:
|
||||
// shouldn't happen
|
||||
return nil, errInvalidCharacter
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +147,12 @@ var (
|
||||
ChannelFounder, ChannelAdmin, ChannelOperator, Halfop,
|
||||
}
|
||||
|
||||
// ChannelUserModes holds the list of all modes that can be applied to a user in a channel,
|
||||
// including Voice, in descending order of precedence
|
||||
ChannelUserModes = Modes{
|
||||
ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, Voice,
|
||||
}
|
||||
|
||||
ChannelModePrefixes = map[Mode]string{
|
||||
ChannelFounder: "~",
|
||||
ChannelAdmin: "&",
|
||||
@ -176,20 +182,13 @@ func SplitChannelMembershipPrefixes(target string) (prefixes string, name string
|
||||
}
|
||||
|
||||
// GetLowestChannelModePrefix returns the lowest channel prefix mode out of the given prefixes.
|
||||
func GetLowestChannelModePrefix(prefixes string) *Mode {
|
||||
var lowest *Mode
|
||||
|
||||
if strings.Contains(prefixes, "+") {
|
||||
lowest = &Voice
|
||||
} else {
|
||||
for i, mode := range ChannelPrivModes {
|
||||
if strings.Contains(prefixes, ChannelModePrefixes[mode]) {
|
||||
lowest = &ChannelPrivModes[i]
|
||||
}
|
||||
func GetLowestChannelModePrefix(prefixes string) (lowest *Mode) {
|
||||
for i, mode := range ChannelUserModes {
|
||||
if strings.Contains(prefixes, ChannelModePrefixes[mode]) {
|
||||
lowest = &ChannelPrivModes[i]
|
||||
}
|
||||
}
|
||||
|
||||
return lowest
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
@ -304,15 +303,12 @@ func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, supportedMode := range ChannelPrivModes {
|
||||
for _, supportedMode := range ChannelUserModes {
|
||||
if rune(supportedMode) == mode {
|
||||
isKnown = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if mode == rune(Voice) {
|
||||
isKnown = true
|
||||
}
|
||||
if !isKnown {
|
||||
unknown[mode] = true
|
||||
continue
|
||||
@ -405,14 +401,11 @@ func (set *ModeSet) Prefixes(isMultiPrefix bool) (prefixes string) {
|
||||
defer set.RUnlock()
|
||||
|
||||
// add prefixes in order from highest to lowest privs
|
||||
for _, mode := range ChannelPrivModes {
|
||||
for _, mode := range ChannelUserModes {
|
||||
if set.modes[mode] {
|
||||
prefixes += ChannelModePrefixes[mode]
|
||||
}
|
||||
}
|
||||
if set.modes[Voice] {
|
||||
prefixes += ChannelModePrefixes[Voice]
|
||||
}
|
||||
|
||||
if !isMultiPrefix && len(prefixes) > 1 {
|
||||
prefixes = string(prefixes[0])
|
||||
|
@ -8,6 +8,47 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseChannelModeChanges(t *testing.T) {
|
||||
modes, unknown := ParseChannelModeChanges("+h", "wrmsr")
|
||||
if len(unknown) > 0 {
|
||||
t.Errorf("unexpected unknown mode change: %v", unknown)
|
||||
}
|
||||
expected := ModeChange{
|
||||
Op: Add,
|
||||
Mode: Halfop,
|
||||
Arg: "wrmsr",
|
||||
}
|
||||
if len(modes) != 1 || modes[0] != expected {
|
||||
t.Errorf("unexpected mode change: %v", modes)
|
||||
}
|
||||
|
||||
modes, unknown = ParseChannelModeChanges("-v", "shivaram")
|
||||
if len(unknown) > 0 {
|
||||
t.Errorf("unexpected unknown mode change: %v", unknown)
|
||||
}
|
||||
expected = ModeChange{
|
||||
Op: Remove,
|
||||
Mode: Voice,
|
||||
Arg: "shivaram",
|
||||
}
|
||||
if len(modes) != 1 || modes[0] != expected {
|
||||
t.Errorf("unexpected mode change: %v", modes)
|
||||
}
|
||||
|
||||
modes, unknown = ParseChannelModeChanges("+tx")
|
||||
if len(unknown) != 1 || !unknown['x'] {
|
||||
t.Errorf("expected that x is an unknown mode, instead: %v", unknown)
|
||||
}
|
||||
expected = ModeChange{
|
||||
Op: Add,
|
||||
Mode: OpOnlyTopic,
|
||||
Arg: "",
|
||||
}
|
||||
if len(modes) != 1 || modes[0] != expected {
|
||||
t.Errorf("unexpected mode change: %v", modes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMode(t *testing.T) {
|
||||
set := NewModeSet()
|
||||
|
||||
|
@ -36,3 +36,17 @@ func TestParseDefaultChannelModes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUmodeGreaterThan(t *testing.T) {
|
||||
if !umodeGreaterThan(modes.Halfop, modes.Voice) {
|
||||
t.Errorf("expected Halfop > Voice")
|
||||
}
|
||||
|
||||
if !umodeGreaterThan(modes.Voice, modes.Mode(0)) {
|
||||
t.Errorf("expected Voice > 0 (the zero value of modes.Mode)")
|
||||
}
|
||||
|
||||
if umodeGreaterThan(modes.ChannelAdmin, modes.ChannelAdmin) {
|
||||
t.Errorf("modes should not be greater than themselves")
|
||||
}
|
||||
}
|
||||
|
@ -284,6 +284,7 @@ oper-classes:
|
||||
- "sajoin"
|
||||
- "samode"
|
||||
- "vhosts"
|
||||
- "chanreg"
|
||||
|
||||
# ircd operators
|
||||
opers:
|
||||
|
Loading…
Reference in New Issue
Block a user