diff --git a/irc/channel.go b/irc/channel.go index a48e7d8b..2ea7969a 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -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 } diff --git a/irc/chanserv.go b/irc/chanserv.go index f2306ca7..cd6cdd6f 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -5,6 +5,7 @@ package irc import ( "fmt" + "sort" "strings" "github.com/goshuirc/irc-go/ircfmt" @@ -42,6 +43,18 @@ 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.`, + authRequired: true, + }, } ) @@ -50,6 +63,71 @@ 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 + } + + clientAccount := client.Account() + if clientAccount == "" || clientAccount != channel.Founder() { + csNotice(rb, client.t("You must be the channel founder to use AMODE")) + return + } + + modeChanges, unknown := modes.ParseChannelModeChanges(strings.Fields(modeChange)...) + + if len(modeChanges) > 1 || len(unknown) > 0 { + csNotice(rb, client.t("Invalid mode change")) + return + } + + if len(modeChanges) == 0 || modeChanges[0].Op == modes.List { + persistentModes := channel.AccountToUmode() + // sort the persistent modes in descending order of priority, i.e., + // ascending order of their index in the ChannelUserModes list + sort.Slice(persistentModes, func(i, j int) bool { + index := func(modeChange modes.ModeChange) int { + for idx, mode := range modes.ChannelUserModes { + if modeChange.Mode == mode { + return idx + } + } + return len(modes.ChannelUserModes) + } + return index(persistentModes[i]) < index(persistentModes[j]) + }) + csNotice(rb, fmt.Sprintf(client.t("Channel %s has %d persistent modes set"), channelName, len(persistentModes))) + for _, modeChange := range persistentModes { + csNotice(rb, fmt.Sprintf(client.t("Account %s receives mode +%s"), modeChange.Arg, string(modeChange.Mode))) + } + return + } + + accountIsValid := false + change := modeChanges[0] + // Arg is the account name, casefold it here + change.Arg, _ = CasefoldName(change.Arg) + if change.Arg != "" { + _, err := server.accounts.LoadAccount(change.Arg) + accountIsValid = (err == nil) + } + if !accountIsValid { + csNotice(rb, client.t("Account does not exist")) + return + } + applied := channel.ApplyAccountToUmodeChange(change) + if applied { + csNotice(rb, fmt.Sprintf(client.t("Successfully set mode %s"), change.String())) + go server.channelRegistry.StoreChannel(channel, IncludeLists) + } 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) diff --git a/irc/getters.go b/irc/getters.go index 1972f53d..f30378df 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -303,3 +303,37 @@ func (channel *Channel) Founder() string { defer channel.stateMutex.RUnlock() return channel.registeredFounder } + +func (channel *Channel) AccountToUmode() (result []modes.ModeChange) { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + for account, mode := range channel.accountToUMode { + result = append(result, modes.ModeChange{ + Mode: mode, + Arg: account, + Op: modes.Add, + }) + } + + return +} + +func (channel *Channel) ApplyAccountToUmodeChange(change modes.ModeChange) (applied bool) { + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + + current := channel.accountToUMode[change.Arg] + switch change.Op { + case modes.Add: + applied = (current != change.Mode) + if applied { + channel.accountToUMode[change.Arg] = change.Mode + } + case modes.Remove: + applied = (current == change.Mode) + if applied { + delete(channel.accountToUMode, change.Arg) + } + } + return +} diff --git a/irc/modes/modes.go b/irc/modes/modes.go index 287dbd44..d6270feb 100644 --- a/irc/modes/modes.go +++ b/irc/modes/modes.go @@ -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 @@ -392,14 +388,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])