diff options
Diffstat (limited to 'vendor/github.com/lrstanley/girc/modes.go')
-rw-r--r-- | vendor/github.com/lrstanley/girc/modes.go | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/vendor/github.com/lrstanley/girc/modes.go b/vendor/github.com/lrstanley/girc/modes.go new file mode 100644 index 00000000..c0ad7d11 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/modes.go @@ -0,0 +1,550 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "encoding/json" + "strings" + "sync" +) + +// CMode represents a single step of a given mode change. +type CMode struct { + add bool // if it's a +, or -. + name byte // character representation of the given mode. + setting bool // if it's a setting (should be stored) or temporary (op/voice/etc). + args string // arguments to the mode, if arguments are supported. +} + +// Short returns a short representation of a mode without arguments. E.g. "+a", +// or "-b". +func (c *CMode) Short() string { + var status string + if c.add { + status = "+" + } else { + status = "-" + } + + return status + string(c.name) +} + +// String returns a string representation of a mode, including optional +// arguments. E.g. "+b user*!ident@host.*.com" +func (c *CMode) String() string { + if len(c.args) == 0 { + return c.Short() + } + + return c.Short() + " " + c.args +} + +// CModes is a representation of a set of modes. This may be the given state +// of a channel/user, or the given state changes to a given channel/user. +type CModes struct { + raw string // raw supported modes. + modesListArgs string // modes that add/remove users from lists and support args. + modesArgs string // modes that support args. + modesSetArgs string // modes that support args ONLY when set. + modesNoArgs string // modes that do not support args. + + prefixes string // user permission prefixes. these aren't a CMode.setting. + modes []CMode // the list of modes for this given state. +} + +// Copy returns a deep copy of CModes. +func (c *CModes) Copy() (nc CModes) { + nc = CModes{} + nc = *c + + nc.modes = make([]CMode, len(c.modes)) + + // Copy modes. + for i := 0; i < len(c.modes); i++ { + nc.modes[i] = c.modes[i] + } + + return nc +} + +// String returns a complete set of modes for this given state (change?). For +// example, "+a-b+cde some-arg". +func (c *CModes) String() string { + var out string + var args string + + if len(c.modes) > 0 { + out += "+" + } + + for i := 0; i < len(c.modes); i++ { + out += string(c.modes[i].name) + + if len(c.modes[i].args) > 0 { + args += " " + c.modes[i].args + } + } + + return out + args +} + +// HasMode checks if the CModes state has a given mode. E.g. "m", or "I". +func (c *CModes) HasMode(mode string) bool { + for i := 0; i < len(c.modes); i++ { + if string(c.modes[i].name) == mode { + return true + } + } + + return false +} + +// Get returns the arguments for a given mode within this session, if it +// supports args. +func (c *CModes) Get(mode string) (args string, ok bool) { + for i := 0; i < len(c.modes); i++ { + if string(c.modes[i].name) == mode { + if len(c.modes[i].args) == 0 { + return "", false + } + + return c.modes[i].args, true + } + } + + return "", false +} + +// hasArg checks to see if the mode supports arguments. What ones support this?: +// A = Mode that adds or removes a nick or address to a list. Always has a parameter. +// B = Mode that changes a setting and always has a parameter. +// C = Mode that changes a setting and only has a parameter when set. +// D = Mode that changes a setting and never has a parameter. +// Note: Modes of type A return the list when there is no parameter present. +// Note: Some clients assumes that any mode not listed is of type D. +// Note: Modes in PREFIX are not listed but could be considered type B. +func (c *CModes) hasArg(set bool, mode byte) (hasArgs, isSetting bool) { + if len(c.raw) < 1 { + return false, true + } + + if strings.IndexByte(c.modesListArgs, mode) > -1 { + return true, false + } + + if strings.IndexByte(c.modesArgs, mode) > -1 { + return true, true + } + + if strings.IndexByte(c.modesSetArgs, mode) > -1 { + if set { + return true, true + } + + return false, true + } + + if strings.IndexByte(c.prefixes, mode) > -1 { + return true, false + } + + return false, true +} + +// Apply merges two state changes, or one state change into a state of modes. +// For example, the latter would mean applying an incoming MODE with the modes +// stored for a channel. +func (c *CModes) Apply(modes []CMode) { + var new []CMode + + for j := 0; j < len(c.modes); j++ { + isin := false + for i := 0; i < len(modes); i++ { + if !modes[i].setting { + continue + } + if c.modes[j].name == modes[i].name && modes[i].add { + new = append(new, modes[i]) + isin = true + break + } + } + + if !isin { + new = append(new, c.modes[j]) + } + } + + for i := 0; i < len(modes); i++ { + if !modes[i].setting || !modes[i].add { + continue + } + + isin := false + for j := 0; j < len(new); j++ { + if modes[i].name == new[j].name { + isin = true + break + } + } + + if !isin { + new = append(new, modes[i]) + } + } + + c.modes = new +} + +// Parse parses a set of flags and args, returning the necessary list of +// mappings for the mode flags. +func (c *CModes) Parse(flags string, args []string) (out []CMode) { + // add is the mode state we're currently in. Adding, or removing modes. + add := true + var argCount int + + for i := 0; i < len(flags); i++ { + if flags[i] == 0x2B { + add = true + continue + } + if flags[i] == 0x2D { + add = false + continue + } + + mode := CMode{ + name: flags[i], + add: add, + } + + hasArgs, isSetting := c.hasArg(add, flags[i]) + if hasArgs && len(args) >= argCount+1 { + mode.args = args[argCount] + argCount++ + } + mode.setting = isSetting + + out = append(out, mode) + } + + return out +} + +// NewCModes returns a new CModes reference. channelModes and userPrefixes +// would be something you see from the server's "CHANMODES" and "PREFIX" +// ISUPPORT capability messages (alternatively, fall back to the standard) +// DefaultPrefixes and ModeDefaults. +func NewCModes(channelModes, userPrefixes string) CModes { + split := strings.SplitN(channelModes, ",", 4) + if len(split) != 4 { + for i := len(split); i < 4; i++ { + split = append(split, "") + } + } + + return CModes{ + raw: channelModes, + modesListArgs: split[0], + modesArgs: split[1], + modesSetArgs: split[2], + modesNoArgs: split[3], + + prefixes: userPrefixes, + modes: []CMode{}, + } +} + +// IsValidChannelMode validates a channel mode (CHANMODES). +func IsValidChannelMode(raw string) bool { + if len(raw) < 1 { + return false + } + + for i := 0; i < len(raw); i++ { + // Allowed are: ",", A-Z and a-z. + if raw[i] != 0x2C && (raw[i] < 0x41 || raw[i] > 0x5A) && (raw[i] < 0x61 || raw[i] > 0x7A) { + return false + } + } + + return true +} + +// isValidUserPrefix validates a list of ISUPPORT-style user prefixes (PREFIX). +func isValidUserPrefix(raw string) bool { + if len(raw) < 1 { + return false + } + + if raw[0] != 0x28 { // (. + return false + } + + var keys, rep int + var passedKeys bool + + // Skip the first one as we know it's (. + for i := 1; i < len(raw); i++ { + if raw[i] == 0x29 { // ). + passedKeys = true + continue + } + + if passedKeys { + rep++ + } else { + keys++ + } + } + + return keys == rep +} + +// parsePrefixes parses the mode character mappings from the symbols of a +// ISUPPORT-style user prefixes list (PREFIX). +func parsePrefixes(raw string) (modes, prefixes string) { + if !isValidUserPrefix(raw) { + return modes, prefixes + } + + i := strings.Index(raw, ")") + if i < 1 { + return modes, prefixes + } + + return raw[1:i], raw[i+1:] +} + +// handleMODE handles incoming MODE messages, and updates the tracking +// information for each channel, as well as if any of the modes affect user +// permissions. +func handleMODE(c *Client, e Event) { + // Check if it's a RPL_CHANNELMODEIS. + if e.Command == RPL_CHANNELMODEIS && len(e.Params) > 2 { + // RPL_CHANNELMODEIS sends the user as the first param, skip it. + e.Params = e.Params[1:] + } + // Should be at least MODE <target> <flags>, to be useful. As well, only + // tracking channel modes at the moment. + if len(e.Params) < 2 || !IsValidChannel(e.Params[0]) { + return + } + + c.state.RLock() + channel := c.state.lookupChannel(e.Params[0]) + if channel == nil { + c.state.RUnlock() + return + } + + flags := e.Params[1] + var args []string + if len(e.Params) > 2 { + args = append(args, e.Params[2:]...) + } + + modes := channel.Modes.Parse(flags, args) + channel.Modes.Apply(modes) + + // Loop through and update users modes as necessary. + for i := 0; i < len(modes); i++ { + if modes[i].setting || len(modes[i].args) == 0 { + continue + } + + user := c.state.lookupUser(modes[i].args) + if user != nil { + perms, _ := user.Perms.Lookup(channel.Name) + perms.setFromMode(modes[i]) + user.Perms.set(channel.Name, perms) + } + } + + c.state.RUnlock() + c.state.notify(c, UPDATE_STATE) +} + +// chanModes returns the ISUPPORT list of server-supported channel modes, +// alternatively falling back to ModeDefaults. +func (s *state) chanModes() string { + if modes, ok := s.serverOptions["CHANMODES"]; ok && IsValidChannelMode(modes) { + return modes + } + + return ModeDefaults +} + +// userPrefixes returns the ISUPPORT list of server-supported user prefixes. +// This includes mode characters, as well as user prefix symbols. Falls back +// to DefaultPrefixes if not server-supported. +func (s *state) userPrefixes() string { + if prefix, ok := s.serverOptions["PREFIX"]; ok && isValidUserPrefix(prefix) { + return prefix + } + + return DefaultPrefixes +} + +// UserPerms contains all of the permissions for each channel the user is +// in. +type UserPerms struct { + mu sync.RWMutex + channels map[string]Perms +} + +// Copy returns a deep copy of the channel permissions. +func (p *UserPerms) Copy() (perms *UserPerms) { + np := &UserPerms{ + channels: make(map[string]Perms), + } + + p.mu.RLock() + for key := range p.channels { + np.channels[key] = p.channels[key] + } + p.mu.RUnlock() + + return np +} + +// MarshalJSON implements json.Marshaler. +func (p *UserPerms) MarshalJSON() ([]byte, error) { + p.mu.Lock() + out, err := json.Marshal(&p.channels) + p.mu.Unlock() + + return out, err +} + +// Lookup looks up the users permissions for a given channel. ok is false +// if the user is not in the given channel. +func (p *UserPerms) Lookup(channel string) (perms Perms, ok bool) { + p.mu.RLock() + perms, ok = p.channels[ToRFC1459(channel)] + p.mu.RUnlock() + + return perms, ok +} + +func (p *UserPerms) set(channel string, perms Perms) { + p.mu.Lock() + p.channels[ToRFC1459(channel)] = perms + p.mu.Unlock() +} + +func (p *UserPerms) remove(channel string) { + p.mu.Lock() + delete(p.channels, ToRFC1459(channel)) + p.mu.Unlock() +} + +// Perms contains all channel-based user permissions. The minimum op, and +// voice should be supported on all networks. This also supports non-rfc +// Owner, Admin, and HalfOp, if the network has support for it. +type Perms struct { + // Owner (non-rfc) indicates that the user has full permissions to the + // channel. More than one user can have owner permission. + Owner bool `json:"owner"` + // Admin (non-rfc) is commonly given to users that are trusted enough + // to manage channel permissions, as well as higher level service settings. + Admin bool `json:"admin"` + // Op is commonly given to trusted users who can manage a given channel + // by kicking, and banning users. + Op bool `json:"op"` + // HalfOp (non-rfc) is commonly used to give users permissions like the + // ability to kick, without giving them greater abilities to ban all users. + HalfOp bool `json:"half_op"` + // Voice indicates the user has voice permissions, commonly given to known + // users, with very light trust, or to indicate a user is active. + Voice bool `json:"voice"` +} + +// IsAdmin indicates that the user has banning abilities, and are likely a +// very trustable user (e.g. op+). +func (m Perms) IsAdmin() bool { + if m.Owner || m.Admin || m.Op { + return true + } + + return false +} + +// IsTrusted indicates that the user at least has modes set upon them, higher +// than a regular joining user. +func (m Perms) IsTrusted() bool { + if m.IsAdmin() || m.HalfOp || m.Voice { + return true + } + + return false +} + +// reset resets the modes of a user. +func (m *Perms) reset() { + m.Owner = false + m.Admin = false + m.Op = false + m.HalfOp = false + m.Voice = false +} + +// set translates raw prefix characters into proper permissions. Only +// use this function when you have a session lock. +func (m *Perms) set(prefix string, append bool) { + if !append { + m.reset() + } + + for i := 0; i < len(prefix); i++ { + switch string(prefix[i]) { + case OwnerPrefix: + m.Owner = true + case AdminPrefix: + m.Admin = true + case OperatorPrefix: + m.Op = true + case HalfOperatorPrefix: + m.HalfOp = true + case VoicePrefix: + m.Voice = true + } + } +} + +// setFromMode sets user-permissions based on channel user mode chars. E.g. +// "o" being oper, "v" being voice, etc. +func (m *Perms) setFromMode(mode CMode) { + switch string(mode.name) { + case ModeOwner: + m.Owner = mode.add + case ModeAdmin: + m.Admin = mode.add + case ModeOperator: + m.Op = mode.add + case ModeHalfOperator: + m.HalfOp = mode.add + case ModeVoice: + m.Voice = mode.add + } +} + +// parseUserPrefix parses a raw mode line, like "@user" or "@+user". +func parseUserPrefix(raw string) (modes, nick string, success bool) { + for i := 0; i < len(raw); i++ { + char := string(raw[i]) + + if char == OwnerPrefix || char == AdminPrefix || char == HalfOperatorPrefix || + char == OperatorPrefix || char == VoicePrefix { + modes += char + continue + } + + // Assume we've gotten to the nickname part. + return modes, raw[i:], true + } + + return +} |