forked from lug/matterbridge
		
	
		
			
				
	
	
		
			490 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			490 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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 (
 | |
| 	"sort"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| // state represents the actively-changing variables within the client
 | |
| // runtime. Note that everything within the state should be guarded by the
 | |
| // embedded sync.RWMutex.
 | |
| type state struct {
 | |
| 	sync.RWMutex
 | |
| 	// nick, ident, and host are the internal trackers for our user.
 | |
| 	nick, ident, host string
 | |
| 	// channels represents all channels we're active in.
 | |
| 	channels map[string]*Channel
 | |
| 	// users represents all of users that we're tracking.
 | |
| 	users map[string]*User
 | |
| 	// enabledCap are the capabilities which are enabled for this connection.
 | |
| 	enabledCap []string
 | |
| 	// tmpCap are the capabilties which we share with the server during the
 | |
| 	// last capability check. These will get sent once we have received the
 | |
| 	// last capability list command from the server.
 | |
| 	tmpCap []string
 | |
| 	// serverOptions are the standard capabilities and configurations
 | |
| 	// supported by the server at connection time. This also includes
 | |
| 	// RPL_ISUPPORT entries.
 | |
| 	serverOptions map[string]string
 | |
| 	// motd is the servers message of the day.
 | |
| 	motd string
 | |
| }
 | |
| 
 | |
| // notify sends state change notifications so users can update their refs
 | |
| // when state changes.
 | |
| func (s *state) notify(c *Client, ntype string) {
 | |
| 	c.RunHandlers(&Event{Command: ntype})
 | |
| }
 | |
| 
 | |
| // reset resets the state back to it's original form.
 | |
| func (s *state) reset() {
 | |
| 	s.Lock()
 | |
| 	s.nick = ""
 | |
| 	s.ident = ""
 | |
| 	s.host = ""
 | |
| 	s.channels = make(map[string]*Channel)
 | |
| 	s.users = make(map[string]*User)
 | |
| 	s.serverOptions = make(map[string]string)
 | |
| 	s.enabledCap = []string{}
 | |
| 	s.motd = ""
 | |
| 	s.Unlock()
 | |
| }
 | |
| 
 | |
| // User represents an IRC user and the state attached to them.
 | |
| type User struct {
 | |
| 	// Nick is the users current nickname. rfc1459 compliant.
 | |
| 	Nick string `json:"nick"`
 | |
| 	// Ident is the users username/ident. Ident is commonly prefixed with a
 | |
| 	// "~", which indicates that they do not have a identd server setup for
 | |
| 	// authentication.
 | |
| 	Ident string `json:"ident"`
 | |
| 	// Host is the visible host of the users connection that the server has
 | |
| 	// provided to us for their connection. May not always be accurate due to
 | |
| 	// many networks spoofing/hiding parts of the hostname for privacy
 | |
| 	// reasons.
 | |
| 	Host string `json:"host"`
 | |
| 
 | |
| 	// ChannelList is a sorted list of all channels that we are currently
 | |
| 	// tracking the user in. Each channel name is rfc1459 compliant. See
 | |
| 	// User.Channels() for a shorthand if you're looking for the *Channel
 | |
| 	// version of the channel list.
 | |
| 	ChannelList []string `json:"channels"`
 | |
| 
 | |
| 	// FirstSeen represents the first time that the user was seen by the
 | |
| 	// client for the given channel. Only usable if from state, not in past.
 | |
| 	FirstSeen time.Time `json:"first_seen"`
 | |
| 	// LastActive represents the last time that we saw the user active,
 | |
| 	// which could be during nickname change, message, channel join, etc.
 | |
| 	// Only usable if from state, not in past.
 | |
| 	LastActive time.Time `json:"last_active"`
 | |
| 
 | |
| 	// Perms are the user permissions applied to this user that affect the given
 | |
| 	// channel. This supports non-rfc style modes like Admin, Owner, and HalfOp.
 | |
| 	Perms *UserPerms `json:"perms"`
 | |
| 
 | |
| 	// Extras are things added on by additional tracking methods, which may
 | |
| 	// or may not work on the IRC server in mention.
 | |
| 	Extras struct {
 | |
| 		// Name is the users "realname" or full name. Commonly contains links
 | |
| 		// to the IRC client being used, or something of non-importance. May
 | |
| 		// also be empty if unsupported by the server/tracking is disabled.
 | |
| 		Name string `json:"name"`
 | |
| 		// Account refers to the account which the user is authenticated as.
 | |
| 		// This differs between each network (e.g. usually Nickserv, but
 | |
| 		// could also be something like Undernet). May also be empty if
 | |
| 		// unsupported by the server/tracking is disabled.
 | |
| 		Account string `json:"account"`
 | |
| 		// Away refers to the away status of the user. An empty string
 | |
| 		// indicates that they are active, otherwise the string is what they
 | |
| 		// set as their away message. May also be empty if unsupported by the
 | |
| 		// server/tracking is disabled.
 | |
| 		Away string `json:"away"`
 | |
| 	} `json:"extras"`
 | |
| }
 | |
| 
 | |
| // Channels returns a reference of *Channels that the client knows the user
 | |
| // is in. If you're just looking for the namme of the channels, use
 | |
| // User.ChannelList.
 | |
| func (u User) Channels(c *Client) []*Channel {
 | |
| 	if c == nil {
 | |
| 		panic("nil Client provided")
 | |
| 	}
 | |
| 
 | |
| 	channels := []*Channel{}
 | |
| 
 | |
| 	c.state.RLock()
 | |
| 	for i := 0; i < len(u.ChannelList); i++ {
 | |
| 		ch := c.state.lookupChannel(u.ChannelList[i])
 | |
| 		if ch != nil {
 | |
| 			channels = append(channels, ch)
 | |
| 		}
 | |
| 	}
 | |
| 	c.state.RUnlock()
 | |
| 
 | |
| 	return channels
 | |
| }
 | |
| 
 | |
| // Copy returns a deep copy of the user which can be modified without making
 | |
| // changes to the actual state.
 | |
| func (u *User) Copy() *User {
 | |
| 	nu := &User{}
 | |
| 	*nu = *u
 | |
| 
 | |
| 	nu.Perms = u.Perms.Copy()
 | |
| 	_ = copy(nu.ChannelList, u.ChannelList)
 | |
| 
 | |
| 	return nu
 | |
| }
 | |
| 
 | |
| // addChannel adds the channel to the users channel list.
 | |
| func (u *User) addChannel(name string) {
 | |
| 	if u.InChannel(name) {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	u.ChannelList = append(u.ChannelList, ToRFC1459(name))
 | |
| 	sort.StringsAreSorted(u.ChannelList)
 | |
| 
 | |
| 	u.Perms.set(name, Perms{})
 | |
| }
 | |
| 
 | |
| // deleteChannel removes an existing channel from the users channel list.
 | |
| func (u *User) deleteChannel(name string) {
 | |
| 	name = ToRFC1459(name)
 | |
| 
 | |
| 	j := -1
 | |
| 	for i := 0; i < len(u.ChannelList); i++ {
 | |
| 		if u.ChannelList[i] == name {
 | |
| 			j = i
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if j != -1 {
 | |
| 		u.ChannelList = append(u.ChannelList[:j], u.ChannelList[j+1:]...)
 | |
| 	}
 | |
| 
 | |
| 	u.Perms.remove(name)
 | |
| }
 | |
| 
 | |
| // InChannel checks to see if a user is in the given channel.
 | |
| func (u *User) InChannel(name string) bool {
 | |
| 	name = ToRFC1459(name)
 | |
| 
 | |
| 	for i := 0; i < len(u.ChannelList); i++ {
 | |
| 		if u.ChannelList[i] == name {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // Lifetime represents the amount of time that has passed since we have first
 | |
| // seen the user.
 | |
| func (u *User) Lifetime() time.Duration {
 | |
| 	return time.Since(u.FirstSeen)
 | |
| }
 | |
| 
 | |
| // Active represents the the amount of time that has passed since we have
 | |
| // last seen the user.
 | |
| func (u *User) Active() time.Duration {
 | |
| 	return time.Since(u.LastActive)
 | |
| }
 | |
| 
 | |
| // IsActive returns true if they were active within the last 30 minutes.
 | |
| func (u *User) IsActive() bool {
 | |
| 	return u.Active() < (time.Minute * 30)
 | |
| }
 | |
| 
 | |
| // Channel represents an IRC channel and the state attached to it.
 | |
| type Channel struct {
 | |
| 	// Name of the channel. Must be rfc1459 compliant.
 | |
| 	Name string `json:"name"`
 | |
| 	// Topic of the channel.
 | |
| 	Topic string `json:"topic"`
 | |
| 
 | |
| 	// UserList is a sorted list of all users we are currently tracking within
 | |
| 	// the channel. Each is the nickname, and is rfc1459 compliant.
 | |
| 	UserList []string `json:"user_list"`
 | |
| 	// Joined represents the first time that the client joined the channel.
 | |
| 	Joined time.Time `json:"joined"`
 | |
| 	// Modes are the known channel modes that the bot has captured.
 | |
| 	Modes CModes `json:"modes"`
 | |
| }
 | |
| 
 | |
| // Users returns a reference of *Users that the client knows the channel has
 | |
| // If you're just looking for just the name of the users, use Channnel.UserList.
 | |
| func (ch Channel) Users(c *Client) []*User {
 | |
| 	if c == nil {
 | |
| 		panic("nil Client provided")
 | |
| 	}
 | |
| 
 | |
| 	users := []*User{}
 | |
| 
 | |
| 	c.state.RLock()
 | |
| 	for i := 0; i < len(ch.UserList); i++ {
 | |
| 		user := c.state.lookupUser(ch.UserList[i])
 | |
| 		if user != nil {
 | |
| 			users = append(users, user)
 | |
| 		}
 | |
| 	}
 | |
| 	c.state.RUnlock()
 | |
| 
 | |
| 	return users
 | |
| }
 | |
| 
 | |
| // Trusted returns a list of users which have voice or greater in the given
 | |
| // channel. See Perms.IsTrusted() for more information.
 | |
| func (ch Channel) Trusted(c *Client) []*User {
 | |
| 	if c == nil {
 | |
| 		panic("nil Client provided")
 | |
| 	}
 | |
| 
 | |
| 	users := []*User{}
 | |
| 
 | |
| 	c.state.RLock()
 | |
| 	for i := 0; i < len(ch.UserList); i++ {
 | |
| 		user := c.state.lookupUser(ch.UserList[i])
 | |
| 		if user == nil {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		perms, ok := user.Perms.Lookup(ch.Name)
 | |
| 		if ok && perms.IsTrusted() {
 | |
| 			users = append(users, user)
 | |
| 		}
 | |
| 	}
 | |
| 	c.state.RUnlock()
 | |
| 
 | |
| 	return users
 | |
| }
 | |
| 
 | |
| // Admins returns a list of users which have half-op (if supported), or
 | |
| // greater permissions (op, admin, owner, etc) in the given channel. See
 | |
| // Perms.IsAdmin() for more information.
 | |
| func (ch Channel) Admins(c *Client) []*User {
 | |
| 	if c == nil {
 | |
| 		panic("nil Client provided")
 | |
| 	}
 | |
| 
 | |
| 	users := []*User{}
 | |
| 
 | |
| 	c.state.RLock()
 | |
| 	for i := 0; i < len(ch.UserList); i++ {
 | |
| 		user := c.state.lookupUser(ch.UserList[i])
 | |
| 		if user == nil {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		perms, ok := user.Perms.Lookup(ch.Name)
 | |
| 		if ok && perms.IsAdmin() {
 | |
| 			users = append(users, user)
 | |
| 		}
 | |
| 	}
 | |
| 	c.state.RUnlock()
 | |
| 
 | |
| 	return users
 | |
| }
 | |
| 
 | |
| // addUser adds a user to the users list.
 | |
| func (ch *Channel) addUser(nick string) {
 | |
| 	if ch.UserIn(nick) {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ch.UserList = append(ch.UserList, ToRFC1459(nick))
 | |
| 	sort.Strings(ch.UserList)
 | |
| }
 | |
| 
 | |
| // deleteUser removes an existing user from the users list.
 | |
| func (ch *Channel) deleteUser(nick string) {
 | |
| 	nick = ToRFC1459(nick)
 | |
| 
 | |
| 	j := -1
 | |
| 	for i := 0; i < len(ch.UserList); i++ {
 | |
| 		if ch.UserList[i] == nick {
 | |
| 			j = i
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if j != -1 {
 | |
| 		ch.UserList = append(ch.UserList[:j], ch.UserList[j+1:]...)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Copy returns a deep copy of a given channel.
 | |
| func (ch *Channel) Copy() *Channel {
 | |
| 	nc := &Channel{}
 | |
| 	*nc = *ch
 | |
| 
 | |
| 	_ = copy(nc.UserList, ch.UserList)
 | |
| 
 | |
| 	// And modes.
 | |
| 	nc.Modes = ch.Modes.Copy()
 | |
| 
 | |
| 	return nc
 | |
| }
 | |
| 
 | |
| // Len returns the count of users in a given channel.
 | |
| func (ch *Channel) Len() int {
 | |
| 	return len(ch.UserList)
 | |
| }
 | |
| 
 | |
| // UserIn checks to see if a given user is in a channel.
 | |
| func (ch *Channel) UserIn(name string) bool {
 | |
| 	name = ToRFC1459(name)
 | |
| 
 | |
| 	for i := 0; i < len(ch.UserList); i++ {
 | |
| 		if ch.UserList[i] == name {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // Lifetime represents the amount of time that has passed since we have first
 | |
| // joined the channel.
 | |
| func (ch *Channel) Lifetime() time.Duration {
 | |
| 	return time.Since(ch.Joined)
 | |
| }
 | |
| 
 | |
| // createChannel creates the channel in state, if not already done.
 | |
| func (s *state) createChannel(name string) (ok bool) {
 | |
| 	supported := s.chanModes()
 | |
| 	prefixes, _ := parsePrefixes(s.userPrefixes())
 | |
| 
 | |
| 	if _, ok := s.channels[ToRFC1459(name)]; ok {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	s.channels[ToRFC1459(name)] = &Channel{
 | |
| 		Name:     name,
 | |
| 		UserList: []string{},
 | |
| 		Joined:   time.Now(),
 | |
| 		Modes:    NewCModes(supported, prefixes),
 | |
| 	}
 | |
| 
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // deleteChannel removes the channel from state, if not already done.
 | |
| func (s *state) deleteChannel(name string) {
 | |
| 	name = ToRFC1459(name)
 | |
| 
 | |
| 	_, ok := s.channels[name]
 | |
| 	if !ok {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	for _, user := range s.channels[name].UserList {
 | |
| 		s.users[user].deleteChannel(name)
 | |
| 
 | |
| 		if len(s.users[user].ChannelList) == 0 {
 | |
| 			// Assume we were only tracking them in this channel, and they
 | |
| 			// should be removed from state.
 | |
| 
 | |
| 			delete(s.users, user)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	delete(s.channels, name)
 | |
| }
 | |
| 
 | |
| // lookupChannel returns a reference to a channel, nil returned if no results
 | |
| // found.
 | |
| func (s *state) lookupChannel(name string) *Channel {
 | |
| 	return s.channels[ToRFC1459(name)]
 | |
| }
 | |
| 
 | |
| // lookupUser returns a reference to a user, nil returned if no results
 | |
| // found.
 | |
| func (s *state) lookupUser(name string) *User {
 | |
| 	return s.users[ToRFC1459(name)]
 | |
| }
 | |
| 
 | |
| // createUser creates the user in state, if not already done.
 | |
| func (s *state) createUser(nick string) (ok bool) {
 | |
| 	if _, ok := s.users[ToRFC1459(nick)]; ok {
 | |
| 		// User already exists.
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	s.users[ToRFC1459(nick)] = &User{
 | |
| 		Nick:       nick,
 | |
| 		FirstSeen:  time.Now(),
 | |
| 		LastActive: time.Now(),
 | |
| 		Perms:      &UserPerms{channels: make(map[string]Perms)},
 | |
| 	}
 | |
| 
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // deleteUser removes the user from channel state.
 | |
| func (s *state) deleteUser(channelName, nick string) {
 | |
| 	user := s.lookupUser(nick)
 | |
| 	if user == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if channelName == "" {
 | |
| 		for i := 0; i < len(user.ChannelList); i++ {
 | |
| 			s.channels[user.ChannelList[i]].deleteUser(nick)
 | |
| 		}
 | |
| 
 | |
| 		delete(s.users, ToRFC1459(nick))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	channel := s.lookupChannel(channelName)
 | |
| 	if channel == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	user.deleteChannel(channelName)
 | |
| 	channel.deleteUser(nick)
 | |
| 
 | |
| 	if len(user.ChannelList) == 0 {
 | |
| 		// This means they are no longer in any channels we track, delete
 | |
| 		// them from state.
 | |
| 
 | |
| 		delete(s.users, ToRFC1459(nick))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // renameUser renames the user in state, in all locations where relevant.
 | |
| func (s *state) renameUser(from, to string) {
 | |
| 	from = ToRFC1459(from)
 | |
| 
 | |
| 	// Update our nickname.
 | |
| 	if from == ToRFC1459(s.nick) {
 | |
| 		s.nick = to
 | |
| 	}
 | |
| 
 | |
| 	user := s.lookupUser(from)
 | |
| 	if user == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	delete(s.users, from)
 | |
| 
 | |
| 	user.Nick = to
 | |
| 	user.LastActive = time.Now()
 | |
| 	s.users[ToRFC1459(to)] = user
 | |
| 
 | |
| 	for i := 0; i < len(user.ChannelList); i++ {
 | |
| 		for j := 0; j < len(s.channels[user.ChannelList[i]].UserList); j++ {
 | |
| 			if s.channels[user.ChannelList[i]].UserList[j] == from {
 | |
| 				s.channels[user.ChannelList[i]].UserList[j] = ToRFC1459(to)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | 
