mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 18:52:41 +01:00
91cfdb963d
Make snomask add/remove behavior match other ircds
471 lines
11 KiB
Go
471 lines
11 KiB
Go
// Copyright (c) 2012-2014 Jeremy Latt
|
|
// Copyright (c) 2014-2015 Edmund Huber
|
|
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
|
// released under the MIT license
|
|
|
|
package modes
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/oragono/oragono/irc/utils"
|
|
)
|
|
|
|
var (
|
|
// SupportedUserModes are the user modes that we actually support (modifying).
|
|
SupportedUserModes = Modes{
|
|
Bot, Invisible, Operator, RegisteredOnly, ServerNotice, UserRoleplaying,
|
|
UserNoCTCP,
|
|
}
|
|
|
|
// SupportedChannelModes are the channel modes that we support.
|
|
SupportedChannelModes = Modes{
|
|
BanMask, ChanRoleplaying, ExceptMask, InviteMask, InviteOnly, Key,
|
|
Moderated, NoOutside, OpOnlyTopic, RegisteredOnly, RegisteredOnlySpeak,
|
|
Secret, UserLimit, NoCTCP, Auditorium, OpModerated, Forward,
|
|
}
|
|
)
|
|
|
|
// ModeOp is an operation performed with modes
|
|
type ModeOp rune
|
|
|
|
const (
|
|
// Add is used when adding the given key.
|
|
Add ModeOp = '+'
|
|
// List is used when listing modes (for instance, listing the current bans on a channel).
|
|
List ModeOp = '='
|
|
// Remove is used when taking away the given key.
|
|
Remove ModeOp = '-'
|
|
)
|
|
|
|
// Mode represents a user/channel/server mode
|
|
type Mode rune
|
|
|
|
func (mode Mode) String() string {
|
|
return string(mode)
|
|
}
|
|
|
|
// ModeChange is a single mode changing
|
|
type ModeChange struct {
|
|
Mode Mode
|
|
Op ModeOp
|
|
Arg string
|
|
}
|
|
|
|
// ModeChanges are a collection of 'ModeChange's
|
|
type ModeChanges []ModeChange
|
|
|
|
func (changes ModeChanges) Strings() (result []string) {
|
|
if len(changes) == 0 {
|
|
return
|
|
}
|
|
|
|
var builder strings.Builder
|
|
|
|
op := changes[0].Op
|
|
builder.WriteRune(rune(op))
|
|
|
|
for _, change := range changes {
|
|
if change.Op != op {
|
|
op = change.Op
|
|
builder.WriteRune(rune(op))
|
|
}
|
|
builder.WriteRune(rune(change.Mode))
|
|
}
|
|
|
|
result = append(result, builder.String())
|
|
|
|
for _, change := range changes {
|
|
if change.Arg == "" {
|
|
continue
|
|
}
|
|
result = append(result, change.Arg)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Modes is just a raw list of modes
|
|
type Modes []Mode
|
|
|
|
func (modes Modes) String() string {
|
|
var builder strings.Builder
|
|
for _, m := range modes {
|
|
builder.WriteRune(rune(m))
|
|
}
|
|
return builder.String()
|
|
}
|
|
|
|
// User Modes
|
|
const (
|
|
Bot Mode = 'B'
|
|
Invisible Mode = 'i'
|
|
Operator Mode = 'o'
|
|
Restricted Mode = 'r'
|
|
RegisteredOnly Mode = 'R'
|
|
ServerNotice Mode = 's'
|
|
TLS Mode = 'Z'
|
|
UserNoCTCP Mode = 'T'
|
|
UserRoleplaying Mode = 'E'
|
|
WallOps Mode = 'w'
|
|
)
|
|
|
|
// Channel Modes
|
|
const (
|
|
Auditorium Mode = 'u' // flag
|
|
BanMask Mode = 'b' // arg
|
|
ChanRoleplaying Mode = 'E' // flag
|
|
ExceptMask Mode = 'e' // arg
|
|
InviteMask Mode = 'I' // arg
|
|
InviteOnly Mode = 'i' // flag
|
|
Key Mode = 'k' // flag arg
|
|
Moderated Mode = 'm' // flag
|
|
NoOutside Mode = 'n' // flag
|
|
OpOnlyTopic Mode = 't' // flag
|
|
// RegisteredOnly mode is reused here from umode definition
|
|
RegisteredOnlySpeak Mode = 'M' // flag
|
|
Secret Mode = 's' // flag
|
|
UserLimit Mode = 'l' // flag arg
|
|
NoCTCP Mode = 'C' // flag
|
|
OpModerated Mode = 'U' // flag
|
|
Forward Mode = 'f' // flag arg
|
|
)
|
|
|
|
var (
|
|
ChannelFounder Mode = 'q' // arg
|
|
ChannelAdmin Mode = 'a' // arg
|
|
ChannelOperator Mode = 'o' // arg
|
|
Halfop Mode = 'h' // arg
|
|
Voice Mode = 'v' // arg
|
|
|
|
// 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: "&",
|
|
ChannelOperator: "@",
|
|
Halfop: "%",
|
|
Voice: "+",
|
|
}
|
|
)
|
|
|
|
//
|
|
// channel membership prefixes
|
|
//
|
|
|
|
// SplitChannelMembershipPrefixes takes a target and returns the prefixes on it, then the name.
|
|
func SplitChannelMembershipPrefixes(target string) (prefixes string, name string) {
|
|
name = target
|
|
for i := 0; i < len(name); i++ {
|
|
switch name[i] {
|
|
case '~', '&', '@', '%', '+':
|
|
prefixes = target[:i+1]
|
|
name = target[i+1:]
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// GetLowestChannelModePrefix returns the lowest channel prefix mode out of the given prefixes.
|
|
func GetLowestChannelModePrefix(prefixes string) (lowest Mode) {
|
|
for i, mode := range ChannelUserModes {
|
|
if strings.Contains(prefixes, ChannelModePrefixes[mode]) {
|
|
lowest = ChannelUserModes[i]
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
//
|
|
// commands
|
|
//
|
|
|
|
// ParseUserModeChanges returns the valid changes, and the list of unknown chars.
|
|
func ParseUserModeChanges(params ...string) (ModeChanges, map[rune]bool) {
|
|
changes := make(ModeChanges, 0)
|
|
unknown := make(map[rune]bool)
|
|
|
|
op := List
|
|
|
|
if 0 < len(params) {
|
|
modeArg := params[0]
|
|
skipArgs := 1
|
|
|
|
for _, mode := range modeArg {
|
|
if mode == '-' || mode == '+' {
|
|
op = ModeOp(mode)
|
|
continue
|
|
}
|
|
change := ModeChange{
|
|
Mode: Mode(mode),
|
|
Op: op,
|
|
}
|
|
|
|
// put arg into modechange if needed
|
|
switch Mode(mode) {
|
|
case ServerNotice:
|
|
// arg is optional for ServerNotice (we accept bare `-s`)
|
|
if len(params) > skipArgs {
|
|
change.Arg = params[skipArgs]
|
|
skipArgs++
|
|
}
|
|
}
|
|
|
|
var isKnown bool
|
|
for _, supportedMode := range SupportedUserModes {
|
|
if rune(supportedMode) == mode {
|
|
isKnown = true
|
|
break
|
|
}
|
|
}
|
|
if !isKnown {
|
|
unknown[mode] = true
|
|
continue
|
|
}
|
|
|
|
changes = append(changes, change)
|
|
}
|
|
}
|
|
|
|
return changes, unknown
|
|
}
|
|
|
|
// ParseChannelModeChanges returns the valid changes, and the list of unknown chars.
|
|
func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) {
|
|
changes := make(ModeChanges, 0)
|
|
unknown := make(map[rune]bool)
|
|
|
|
op := List
|
|
|
|
if 0 < len(params) {
|
|
modeArg := params[0]
|
|
skipArgs := 1
|
|
|
|
for _, mode := range modeArg {
|
|
if mode == '-' || mode == '+' {
|
|
op = ModeOp(mode)
|
|
continue
|
|
}
|
|
change := ModeChange{
|
|
Mode: Mode(mode),
|
|
Op: op,
|
|
}
|
|
|
|
// put arg into modechange if needed
|
|
switch Mode(mode) {
|
|
case BanMask, ExceptMask, InviteMask:
|
|
if len(params) > skipArgs {
|
|
change.Arg = params[skipArgs]
|
|
skipArgs++
|
|
} else {
|
|
change.Op = List
|
|
}
|
|
case ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, Voice:
|
|
if len(params) > skipArgs {
|
|
change.Arg = params[skipArgs]
|
|
skipArgs++
|
|
} else {
|
|
continue
|
|
}
|
|
case UserLimit, Forward:
|
|
// don't require value when removing
|
|
if change.Op == Add {
|
|
if len(params) > skipArgs {
|
|
change.Arg = params[skipArgs]
|
|
skipArgs++
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
case Key:
|
|
// #874: +k is technically a type B mode, requiring a parameter
|
|
// both for add and remove. so attempt to consume a parameter,
|
|
// but allow remove (but not add) even if no parameter is available.
|
|
// however, the remove parameter should always display as "*", matching
|
|
// the freenode behavior.
|
|
if len(params) > skipArgs {
|
|
if change.Op == Add {
|
|
change.Arg = params[skipArgs]
|
|
}
|
|
skipArgs++
|
|
} else if change.Op == Add {
|
|
continue
|
|
}
|
|
if change.Op == Remove {
|
|
change.Arg = "*"
|
|
}
|
|
}
|
|
|
|
var isKnown bool
|
|
for _, supportedMode := range SupportedChannelModes {
|
|
if rune(supportedMode) == mode {
|
|
isKnown = true
|
|
break
|
|
}
|
|
}
|
|
for _, supportedMode := range ChannelUserModes {
|
|
if rune(supportedMode) == mode {
|
|
isKnown = true
|
|
break
|
|
}
|
|
}
|
|
if !isKnown {
|
|
unknown[mode] = true
|
|
continue
|
|
}
|
|
|
|
changes = append(changes, change)
|
|
}
|
|
}
|
|
|
|
return changes, unknown
|
|
}
|
|
|
|
// ModeSet holds a set of modes.
|
|
type ModeSet [2]uint32
|
|
|
|
// valid modes go from 65 ('A') to 122 ('z'), making at most 58 possible values;
|
|
// subtract 65 from the mode value and use that bit of the uint32 to represent it
|
|
const (
|
|
minMode = 65 // 'A'
|
|
maxMode = 122 // 'z'
|
|
)
|
|
|
|
// returns a pointer to a new ModeSet
|
|
func NewModeSet() *ModeSet {
|
|
var set ModeSet
|
|
return &set
|
|
}
|
|
|
|
// test whether `mode` is set
|
|
func (set *ModeSet) HasMode(mode Mode) bool {
|
|
if set == nil {
|
|
return false
|
|
}
|
|
|
|
return utils.BitsetGet(set[:], uint(mode)-minMode)
|
|
}
|
|
|
|
// set `mode` to be on or off, return whether the value actually changed
|
|
func (set *ModeSet) SetMode(mode Mode, on bool) (applied bool) {
|
|
return utils.BitsetSet(set[:], uint(mode)-minMode, on)
|
|
}
|
|
|
|
// copy the contents of another modeset on top of this one
|
|
func (set *ModeSet) Copy(other *ModeSet) {
|
|
utils.BitsetCopy(set[:], other[:])
|
|
}
|
|
|
|
// return the modes in the set as a slice
|
|
func (set *ModeSet) AllModes() (result []Mode) {
|
|
if set == nil {
|
|
return
|
|
}
|
|
|
|
var i Mode
|
|
for i = minMode; i <= maxMode; i++ {
|
|
if set.HasMode(i) {
|
|
result = append(result, i)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// String returns the modes in this set.
|
|
func (set *ModeSet) String() (result string) {
|
|
if set == nil {
|
|
return
|
|
}
|
|
|
|
var buf strings.Builder
|
|
for _, mode := range set.AllModes() {
|
|
buf.WriteRune(rune(mode))
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// Prefixes returns a list of prefixes for the given set of channel modes.
|
|
func (set *ModeSet) Prefixes(isMultiPrefix bool) (prefixes string) {
|
|
if set == nil {
|
|
return
|
|
}
|
|
|
|
// add prefixes in order from highest to lowest privs
|
|
for _, mode := range ChannelUserModes {
|
|
if set.HasMode(mode) {
|
|
prefixes += ChannelModePrefixes[mode]
|
|
}
|
|
}
|
|
|
|
if !isMultiPrefix && len(prefixes) > 1 {
|
|
prefixes = string(prefixes[0])
|
|
}
|
|
|
|
return prefixes
|
|
}
|
|
|
|
// HighestChannelUserMode returns the most privileged channel-user mode
|
|
// (e.g., ChannelFounder, Halfop, Voice) present in the ModeSet.
|
|
// If no such modes are present, or `set` is nil, returns the zero mode.
|
|
func (set *ModeSet) HighestChannelUserMode() (result Mode) {
|
|
for _, mode := range ChannelUserModes {
|
|
if set.HasMode(mode) {
|
|
return mode
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
type ByCodepoint Modes
|
|
|
|
func (a ByCodepoint) Len() int { return len(a) }
|
|
func (a ByCodepoint) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a ByCodepoint) Less(i, j int) bool { return a[i] < a[j] }
|
|
|
|
func RplMyInfo() (param1, param2, param3 string) {
|
|
userModes := make(Modes, len(SupportedUserModes), len(SupportedUserModes)+1)
|
|
copy(userModes, SupportedUserModes)
|
|
// TLS is not in SupportedUserModes because it can't be modified
|
|
userModes = append(userModes, TLS)
|
|
sort.Sort(ByCodepoint(userModes))
|
|
|
|
channelModes := make(Modes, len(SupportedChannelModes)+len(ChannelUserModes))
|
|
copy(channelModes, SupportedChannelModes)
|
|
copy(channelModes[len(SupportedChannelModes):], ChannelUserModes)
|
|
sort.Sort(ByCodepoint(channelModes))
|
|
|
|
// XXX enumerate these by hand, i can't see any way to DRY this
|
|
channelParametrizedModes := Modes{BanMask, ExceptMask, InviteMask, Key, UserLimit, Forward}
|
|
channelParametrizedModes = append(channelParametrizedModes, ChannelUserModes...)
|
|
sort.Sort(ByCodepoint(channelParametrizedModes))
|
|
|
|
return userModes.String(), channelModes.String(), channelParametrizedModes.String()
|
|
}
|
|
|
|
func ChanmodesToken() (result string) {
|
|
// https://modern.ircdocs.horse#chanmodes-parameter
|
|
// type A: listable modes with parameters
|
|
A := Modes{BanMask, ExceptMask, InviteMask}
|
|
// type B: modes with parameters
|
|
B := Modes{Key}
|
|
// type C: modes that take a parameter only when set, never when unset
|
|
C := Modes{UserLimit, Forward}
|
|
// type D: modes without parameters
|
|
D := Modes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, ChanRoleplaying, Secret, NoCTCP, RegisteredOnly, RegisteredOnlySpeak, Auditorium, OpModerated}
|
|
|
|
sort.Sort(ByCodepoint(A))
|
|
sort.Sort(ByCodepoint(B))
|
|
sort.Sort(ByCodepoint(C))
|
|
sort.Sort(ByCodepoint(D))
|
|
|
|
return fmt.Sprintf("%s,%s,%s,%s", A.String(), B.String(), C.String(), D.String())
|
|
}
|