girc-atomic/state.go

344 lines
10 KiB
Go
Raw Normal View History

2017-02-06 07:45:31 +00:00
// 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.
2016-11-13 08:30:43 +00:00
package girc
import (
"fmt"
"net"
2016-11-13 08:30:43 +00:00
"strings"
"sync"
"time"
)
2016-11-14 11:59:08 +00:00
// state represents the actively-changing variables within the client
// runtime.
2016-11-14 11:59:08 +00:00
type state struct {
// m is a RW mutex lock, used to guard the state from goroutines causing
// corruption.
2016-12-10 11:43:26 +00:00
mu sync.RWMutex
// reader is the socket buffer reader from the IRC server.
reader *ircDecoder
// reader is the socket buffer write to the IRC server.
writer *ircEncoder
// conn is a net.Conn reference to the IRC server.
conn net.Conn
// connected is true if we're actively connected to a server.
connected bool
// connTime is the time at which the client has connected to a server.
connTime *time.Time
// quitting is used to determine if we've finished quitting/cleaning up.
quitting bool
// reconnecting lets the internal state know a reconnect is occurring.
reconnecting bool
// nick is the tracker for our nickname on the server.
nick string
// channels represents all channels we're active in.
channels map[string]*Channel
// 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
2017-01-19 11:58:08 +00:00
// 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
2016-11-13 08:30:43 +00:00
}
// User represents an IRC user and the state attached to them.
2016-11-13 08:30:43 +00:00
type User struct {
// Nick is the users current nickname.
Nick string
// 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
// 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
// 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
// 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
// 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.
// If you want to easily check if a user has permissions equal or greater
// than OP, use Perms.IsAdmin().
Perms UserPerms
// 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
// 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
// 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
}
}
2016-12-23 23:25:34 +00:00
// Message returns an event which can be used to send a response to the user
// as a private message.
2016-12-23 23:25:34 +00:00
func (u *User) Message(message string) *Event {
return &Event{Command: PRIVMSG, Params: []string{u.Nick}, Trailing: message}
}
2016-12-23 23:25:34 +00:00
// Messagef returns an event which can be used to send a response to the user
// as a private message. format is a printf format string, which a's
2017-01-06 14:01:53 +00:00
// arbitrary arguments will be passed to.
2016-12-23 23:25:34 +00:00
func (u *User) Messagef(format string, a ...interface{}) *Event {
return u.Message(fmt.Sprintf(format, a...))
}
2016-12-23 23:25:34 +00:00
// MessageTo returns an event which can be used to send a response to the
// user in a channel as a private message.
2016-12-23 23:25:34 +00:00
func (u *User) MessageTo(channel, message string) *Event {
return &Event{Command: PRIVMSG, Params: []string{u.Nick}, Trailing: channel + ": " + message}
}
2016-12-23 23:25:34 +00:00
// MessageTof returns an event which can be used to send a response to the
2017-01-06 14:01:53 +00:00
// channel. format is a printf format string, which a's arbitrary arguments
// will be passed to.
2016-12-23 23:25:34 +00:00
func (u *User) MessageTof(channel, format string, a ...interface{}) *Event {
return u.MessageTo(channel, fmt.Sprintf(format, a...))
}
// 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)
2016-11-13 08:30:43 +00:00
}
// Channel represents an IRC channel and the state attached to it.
2016-11-13 08:30:43 +00:00
type Channel struct {
// Name of the channel. Must be rfc compliant. Always represented as
// lower-case, to ensure that the channel is only being tracked once.
Name string
2016-12-10 11:43:26 +00:00
// Topic of the channel.
Topic string
// users represents the users that we can currently see within the
// channel.
users map[string]*User
// Joined represents the first time that the client joined the channel.
Joined time.Time
2017-02-07 11:08:17 +00:00
// Modes are the known channel modes that the bot has captured.
Modes CModes
2016-11-13 08:30:43 +00:00
}
2016-12-23 23:25:34 +00:00
// Message returns an event which can be used to send a response to the channel.
func (c *Channel) Message(message string) *Event {
return &Event{Command: PRIVMSG, Params: []string{c.Name}, Trailing: message}
}
2016-12-23 23:25:34 +00:00
// Messagef returns an event which can be used to send a response to the
2017-01-06 14:01:53 +00:00
// channel. format is a printf format string, which a's arbitrary arguments
// will be passed to.
2016-12-23 23:25:34 +00:00
func (c *Channel) Messagef(format string, a ...interface{}) *Event {
return c.Message(fmt.Sprintf(format, a...))
}
2016-12-14 02:32:42 +00:00
// Lifetime represents the amount of time that has passed since we have first
// joined the channel.
func (c *Channel) Lifetime() time.Duration {
return time.Since(c.Joined)
}
2016-11-14 11:59:08 +00:00
// newState returns a clean client state.
func newState() *state {
s := &state{}
2016-11-13 08:30:43 +00:00
s.channels = make(map[string]*Channel)
s.serverOptions = make(map[string]string)
2016-11-13 08:30:43 +00:00
s.connected = false
return s
}
// createChanIfNotExists creates the channel in state, if not already done.
// Always use state.mu for transaction.
func (s *state) createChanIfNotExists(name string) (channel *Channel) {
// Not a valid channel.
if !IsValidChannel(name) {
return nil
2016-11-13 08:30:43 +00:00
}
2017-02-06 13:02:26 +00:00
supported := s.chanModes()
prefixes, _ := parsePrefixes(s.userPrefixes())
name = strings.ToLower(name)
if _, ok := s.channels[name]; !ok {
channel = &Channel{
Name: name,
2016-11-13 08:30:43 +00:00
users: make(map[string]*User),
Joined: time.Now(),
2017-02-06 13:02:26 +00:00
Modes: newCModes(supported, prefixes),
2016-11-13 08:30:43 +00:00
}
s.channels[name] = channel
} else {
channel = s.channels[name]
2016-11-13 08:30:43 +00:00
}
return channel
2016-11-13 08:30:43 +00:00
}
// deleteChannel removes the channel from state, if not already done. Always
// use state.mu for transaction.
func (s *state) deleteChannel(name string) {
channel := s.createChanIfNotExists(name)
if channel == nil {
return
}
2016-11-13 08:30:43 +00:00
if _, ok := s.channels[channel.Name]; ok {
delete(s.channels, channel.Name)
2016-11-13 08:30:43 +00:00
}
}
2017-02-07 11:08:17 +00:00
// lookupChannel returns a reference to a channel with a given case-insensitive
// name. nil returned if no results found.
2017-02-06 13:02:26 +00:00
func (s *state) lookupChannel(name string) *Channel {
if !IsValidChannel(name) {
return nil
}
return s.channels[strings.ToLower(name)]
}
// createUserIfNotExists creates the channel and user in state, if not already
// done. Always use state.mu for transaction.
func (s *state) createUserIfNotExists(channelName, nick string) (user *User) {
if !IsValidNick(nick) {
return nil
}
channel := s.createChanIfNotExists(channelName)
if channel == nil {
return nil
}
if _, ok := channel.users[nick]; ok {
channel.users[nick].LastActive = time.Now()
return channel.users[nick]
2016-11-13 08:30:43 +00:00
}
user = &User{Nick: nick, FirstSeen: time.Now(), LastActive: time.Now()}
channel.users[nick] = user
return user
2016-11-13 08:30:43 +00:00
}
// deleteUser removes the user from channel state. Always use state.mu for
// transaction.
2016-11-14 11:59:08 +00:00
func (s *state) deleteUser(nick string) {
if !IsValidNick(nick) {
return
}
for k := range s.channels {
// Check to see if they're in this channel.
if _, ok := s.channels[k].users[nick]; !ok {
continue
}
delete(s.channels[k].users, nick)
2016-11-13 08:30:43 +00:00
}
}
// renameUser renames the user in state, in all locations where relevant.
// Always use state.mu for transaction.
2016-11-14 11:59:08 +00:00
func (s *state) renameUser(from, to string) {
if !IsValidNick(from) || !IsValidNick(to) {
return
}
2016-11-13 08:30:43 +00:00
for k := range s.channels {
// Check to see if they're in this channel.
2016-11-13 08:30:43 +00:00
if _, ok := s.channels[k].users[from]; !ok {
continue
2016-11-13 08:30:43 +00:00
}
// Take the actual reference to the pointer.
2016-11-13 08:30:43 +00:00
source := *s.channels[k].users[from]
// Update the nick field (as we not only have a key, but a matching
// struct field).
2016-11-13 08:30:43 +00:00
source.Nick = to
source.LastActive = time.Now()
2016-11-13 08:30:43 +00:00
// Delete the old reference.
2016-11-13 08:30:43 +00:00
delete(s.channels[k].users, from)
// In with the new.
2016-11-13 08:30:43 +00:00
s.channels[k].users[to] = &source
}
}
2017-02-07 11:08:17 +00:00
// lookupUsers returns a slice of references to users matching a given
// query. mathType is of "nick", "name", "ident" or "account".
func (s *state) lookupUsers(matchType, toMatch string) []*User {
var users []*User
for c := range s.channels {
for u := range s.channels[c].users {
switch matchType {
case "nick":
if s.channels[c].users[u].Nick == toMatch {
users = append(users, s.channels[c].users[u])
continue
}
case "name":
if s.channels[c].users[u].Extras.Name == toMatch {
users = append(users, s.channels[c].users[u])
continue
}
case "ident":
if s.channels[c].users[u].Ident == toMatch {
users = append(users, s.channels[c].users[u])
continue
}
case "account":
if s.channels[c].users[u].Extras.Account == toMatch {
users = append(users, s.channels[c].users[u])
continue
}
}
}
}
return users
}