ergo/irc/modes/modes.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/ergochat/ergo/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())
}