summaryrefslogblamecommitdiffstats
path: root/vendor/github.com/lrstanley/girc/modes.go
blob: 864df71fc05b133447d3803ffeb2dafd6d1d29c7 (plain) (tree)














































































































































































































                                                                                           
                                    

                                  
                                    




















































                                                                          
                                                                                                      











                                                                               
                          






                                                
                                  

































































































































































































































































                                                                                              
// 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] == '+' {
			add = true
			continue
		}
		if flags[i] == '-' {
			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] != ',' && (raw[i] < 'A' || raw[i] > 'Z') && (raw[i] < 'a' || raw[i] > 'z') {
			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] != '(' {
		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] == ')' {
			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
}