rewrite state to support separate dataset for users

This commit is contained in:
Liam Stanley 2017-07-04 01:29:22 -04:00
parent 4c8bd8a350
commit 1f66c9ffec
8 changed files with 275 additions and 172 deletions

View File

@ -140,6 +140,7 @@ func handleJOIN(c *Client, e Event) {
// Assume extended-join (ircv3). // Assume extended-join (ircv3).
if len(e.Params) == 2 { if len(e.Params) == 2 {
c.state.mu.Lock()
if e.Params[1] != "*" { if e.Params[1] != "*" {
user.Extras.Account = e.Params[1] user.Extras.Account = e.Params[1]
} }
@ -147,6 +148,7 @@ func handleJOIN(c *Client, e Event) {
if len(e.Trailing) > 0 { if len(e.Trailing) > 0 {
user.Extras.Name = e.Trailing user.Extras.Name = e.Trailing
} }
c.state.mu.Unlock()
} }
if e.Source.Name == c.GetNick() { if e.Source.Name == c.GetNick() {
@ -176,19 +178,26 @@ func handlePART(c *Client, e Event) {
return return
} }
if len(e.Params) == 0 { var channel string
if len(e.Params) > 0 {
channel = e.Params[0]
} else {
channel = e.Trailing
}
if channel == "" {
return return
} }
if e.Source.Name == c.GetNick() { if e.Source.Name == c.GetNick() {
c.state.mu.Lock() c.state.mu.Lock()
c.state.deleteChannel(e.Params[0]) c.state.deleteChannel(channel)
c.state.mu.Unlock() c.state.mu.Unlock()
return return
} }
c.state.mu.Lock() c.state.mu.Lock()
c.state.deleteUser(e.Source.Name) c.state.deleteUser(channel, e.Source.Name)
c.state.mu.Unlock() c.state.mu.Unlock()
} }
@ -236,6 +245,7 @@ func handleWHO(c *Client, e Event) {
} }
channel, ident, host, nick, account = e.Params[2], e.Params[3], e.Params[4], e.Params[5], e.Params[6] channel, ident, host, nick, account = e.Params[2], e.Params[3], e.Params[4], e.Params[5], e.Params[6]
realname = e.Trailing
} else { } else {
// Assume RPL_WHOREPLY. // Assume RPL_WHOREPLY.
channel, ident, host, nick = e.Params[1], e.Params[2], e.Params[3], e.Params[5] channel, ident, host, nick = e.Params[1], e.Params[2], e.Params[3], e.Params[5]
@ -279,7 +289,7 @@ func handleKICK(c *Client, e Event) {
// Assume it's just another user. // Assume it's just another user.
c.state.mu.Lock() c.state.mu.Lock()
c.state.deleteUser(e.Params[1]) c.state.deleteUser(e.Params[0], e.Params[1])
c.state.mu.Unlock() c.state.mu.Unlock()
} }
@ -306,8 +316,12 @@ func handleQUIT(c *Client, e Event) {
return return
} }
if e.Source.Name == c.GetNick() {
return
}
c.state.mu.Lock() c.state.mu.Lock()
c.state.deleteUser(e.Source.Name) c.state.deleteUser("", e.Source.Name)
c.state.mu.Unlock() c.state.mu.Unlock()
} }
@ -450,10 +464,13 @@ func updateLastActive(c *Client, e Event) {
} }
c.state.mu.Lock() c.state.mu.Lock()
defer c.state.mu.Unlock()
// Update the users last active time, if they exist. // Update the users last active time, if they exist.
users := c.state.lookupUsers("nick", e.Source.Name) user := c.state.lookupUser(e.Source.Name)
for i := 0; i < len(users); i++ { if user == nil {
users[i].LastActive = time.Now() return
} }
c.state.mu.Unlock()
user.LastActive = time.Now()
} }

30
cap.go
View File

@ -321,11 +321,10 @@ func handleCHGHOST(c *Client, e Event) {
} }
c.state.mu.Lock() c.state.mu.Lock()
users := c.state.lookupUsers("nick", e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user != nil {
for i := 0; i < len(users); i++ { user.Ident = e.Params[0]
users[i].Ident = e.Params[0] user.Host = e.Params[1]
users[i].Host = e.Params[1]
} }
c.state.mu.Unlock() c.state.mu.Unlock()
} }
@ -334,10 +333,9 @@ func handleCHGHOST(c *Client, e Event) {
// when users are no longer away, or when they are away. // when users are no longer away, or when they are away.
func handleAWAY(c *Client, e Event) { func handleAWAY(c *Client, e Event) {
c.state.mu.Lock() c.state.mu.Lock()
users := c.state.lookupUsers("nick", e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user != nil {
for i := 0; i < len(users); i++ { user.Extras.Away = e.Trailing
users[i].Extras.Away = e.Trailing
} }
c.state.mu.Unlock() c.state.mu.Unlock()
} }
@ -357,10 +355,9 @@ func handleACCOUNT(c *Client, e Event) {
} }
c.state.mu.Lock() c.state.mu.Lock()
users := c.state.lookupUsers("nick", e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user != nil {
for i := 0; i < len(users); i++ { user.Extras.Account = account
users[i].Extras.Account = account
} }
c.state.mu.Unlock() c.state.mu.Unlock()
} }
@ -378,10 +375,9 @@ func handleTags(c *Client, e Event) {
} }
c.state.mu.Lock() c.state.mu.Lock()
users := c.state.lookupUsers("nick", e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user != nil {
for i := 0; i < len(users); i++ { user.Extras.Account = account
users[i].Extras.Account = account
} }
c.state.mu.Unlock() c.state.mu.Unlock()
} }

View File

@ -13,7 +13,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"runtime" "runtime"
"strings" "sort"
"sync" "sync"
"time" "time"
) )
@ -237,7 +237,7 @@ func New(config Config) *Client {
// Give ourselves a new state. // Give ourselves a new state.
c.state = &state{} c.state = &state{}
c.state.clean() c.state.reset()
// Register builtin handlers. // Register builtin handlers.
c.registerBuiltins() c.registerBuiltins()
@ -430,27 +430,50 @@ func (c *Client) GetHost() string {
// Panics if tracking is disabled. // Panics if tracking is disabled.
func (c *Client) Channels() []string { func (c *Client) Channels() []string {
c.panicIfNotTracking() c.panicIfNotTracking()
channels := make([]string, len(c.state.channels))
c.state.mu.RLock() c.state.mu.RLock()
channels := make([]string, len(c.state.channels))
var i int var i int
for channel := range c.state.channels { for channel := range c.state.channels {
channels[i] = channel channels[i] = channel
i++ i++
} }
c.state.mu.RUnlock() c.state.mu.RUnlock()
sort.Strings(channels)
return channels return channels
} }
// Lookup looks up a given channel in state. If the channel doesn't exist, // Users returns the active list of users that the client is tracking across
// channel is nil. Panics if tracking is disabled. // all files. Panics if tracking is disabled.
func (c *Client) Lookup(name string) *Channel { func (c *Client) Users() []string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.RLock()
users := make([]string, len(c.state.users))
var i int
for user := range c.state.users {
users[i] = user
i++
}
c.state.mu.RUnlock()
sort.Strings(users)
return users
}
// LookupChannel looks up a given channel in state. If the channel doesn't
// exist, nil is returned. Panics if tracking is disabled.
func (c *Client) LookupChannel(name string) *Channel {
c.panicIfNotTracking()
if name == "" {
return nil
}
c.state.mu.Lock() c.state.mu.Lock()
defer c.state.mu.Unlock()
channel := c.state.lookupChannel(name) channel := c.state.lookupChannel(name)
c.state.mu.Unlock()
if channel == nil { if channel == nil {
return nil return nil
} }
@ -458,13 +481,32 @@ func (c *Client) Lookup(name string) *Channel {
return channel.Copy() return channel.Copy()
} }
// LookupUser looks up a given user in state. If the user doesn't exist, nil
// is returned. Panics if tracking is disabled.
func (c *Client) LookupUser(nick string) *User {
c.panicIfNotTracking()
if nick == "" {
return nil
}
c.state.mu.Lock()
user := c.state.lookupUser(nick)
c.state.mu.Unlock()
if user == nil {
return nil
}
return user.Copy()
}
// IsInChannel returns true if the client is in channel. Panics if tracking // IsInChannel returns true if the client is in channel. Panics if tracking
// is disabled. // is disabled.
func (c *Client) IsInChannel(channel string) bool { func (c *Client) IsInChannel(channel string) bool {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.RLock() c.state.mu.RLock()
_, inChannel := c.state.channels[strings.ToLower(channel)] _, inChannel := c.state.channels[ToRFC1459(channel)]
c.state.mu.RUnlock() c.state.mu.RUnlock()
return inChannel return inChannel

View File

@ -265,7 +265,7 @@ func (c *Client) internalConnect(mock net.Conn) error {
} }
// Reset the state. // Reset the state.
c.state.clean() c.state.reset()
if mock == nil { if mock == nil {
// Validate info, and actually make the connection. // Validate info, and actually make the connection.

View File

@ -394,7 +394,7 @@ func (e *Event) GetChannel(c *Client) *Channel {
return nil return nil
} }
return c.Lookup(e.Params[0]) return c.LookupChannel(e.Params[0])
} }
// GetUser is a helper function around an event which lets you easily obtain // GetUser is a helper function around an event which lets you easily obtain
@ -411,12 +411,7 @@ func (e *Event) GetUser(c *Client) *User {
return nil return nil
} }
channel := c.Lookup(e.Params[0]) return c.LookupUser(e.Source.Name)
if channel == nil {
return nil
}
return channel.Lookup(e.Source.Name)
} }
// IsAction checks to see if the event is a PRIVMSG, and is an ACTION (/me). // IsAction checks to see if the event is a PRIVMSG, and is an ACTION (/me).

View File

@ -239,7 +239,7 @@ func IsValidUser(name string) bool {
// ToRFC1459 converts a string to the stripped down conversion within RFC // ToRFC1459 converts a string to the stripped down conversion within RFC
// 1459. This will do things like replace an "A" with an "a", "[]" with "{}", // 1459. This will do things like replace an "A" with an "a", "[]" with "{}",
// and so forth. Useful to compare two nicknames. // and so forth. Useful to compare two nicknames or channels.
func ToRFC1459(input string) (out string) { func ToRFC1459(input string) (out string) {
for i := 0; i < len(input); i++ { for i := 0; i < len(input); i++ {
if input[i] >= 65 && input[i] <= 94 { if input[i] >= 65 && input[i] <= 94 {

View File

@ -351,9 +351,9 @@ func handleMODE(c *Client, e Event) {
continue continue
} }
users := c.state.lookupUsers("nick", modes[i].args) user := c.state.lookupUser(modes[i].args)
for j := 0; j < len(users); j++ { if user != nil {
users[j].Perms.setFromMode(modes[i]) user.Perms.setFromMode(modes[i])
} }
} }

305
state.go
View File

@ -5,7 +5,7 @@
package girc package girc
import ( import (
"strings" "sort"
"sync" "sync"
"time" "time"
) )
@ -20,6 +20,8 @@ type state struct {
nick, ident, host string nick, ident, host string
// channels represents all channels we're active in. // channels represents all channels we're active in.
channels map[string]*Channel 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 are the capabilities which are enabled for this connection.
enabledCap []string enabledCap []string
// tmpCap are the capabilties which we share with the server during the // tmpCap are the capabilties which we share with the server during the
@ -34,12 +36,14 @@ type state struct {
motd string motd string
} }
func (s *state) clean() { // reset resets the state back to it's original form.
func (s *state) reset() {
s.mu.Lock() s.mu.Lock()
s.nick = "" s.nick = ""
s.ident = "" s.ident = ""
s.host = "" s.host = ""
s.channels = make(map[string]*Channel) s.channels = make(map[string]*Channel)
s.users = make(map[string]*User)
s.serverOptions = make(map[string]string) s.serverOptions = make(map[string]string)
s.enabledCap = []string{} s.enabledCap = []string{}
s.motd = "" s.motd = ""
@ -48,7 +52,7 @@ func (s *state) clean() {
// User represents an IRC user and the state attached to them. // User represents an IRC user and the state attached to them.
type User struct { type User struct {
// Nick is the users current nickname. // Nick is the users current nickname. rfc1459 compliant.
Nick string Nick string
// Ident is the users username/ident. Ident is commonly prefixed with a // Ident is the users username/ident. Ident is commonly prefixed with a
// "~", which indicates that they do not have a identd server setup for // "~", which indicates that they do not have a identd server setup for
@ -60,6 +64,10 @@ type User struct {
// reasons. // reasons.
Host string Host string
// Channels is a sorted list of all channels that we are currently tracking
// the user in. Each channel name is rfc1459 compliant.
Channels []string
// FirstSeen represents the first time that the user was seen by the // 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. // client for the given channel. Only usable if from state, not in past.
FirstSeen time.Time FirstSeen time.Time
@ -94,6 +102,46 @@ type User struct {
} }
} }
// 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
_ = copy(nu.Channels, u.Channels)
return nu
}
func (u *User) deleteChannel(name string) {
name = ToRFC1459(name)
j := -1
for i := 0; i < len(u.Channels); i++ {
if u.Channels[i] == name {
j = i
break
}
}
if j != -1 {
u.Channels = append(u.Channels[:j], u.Channels[j+1:]...)
}
}
// 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.Channels); i++ {
if u.Channels[i] == name {
return true
}
}
return false
}
// Lifetime represents the amount of time that has passed since we have first // Lifetime represents the amount of time that has passed since we have first
// seen the user. // seen the user.
func (u *User) Lifetime() time.Duration { func (u *User) Lifetime() time.Duration {
@ -113,30 +161,42 @@ func (u *User) IsActive() bool {
// Channel represents an IRC channel and the state attached to it. // Channel represents an IRC channel and the state attached to it.
type Channel struct { type Channel struct {
// Name of the channel. Must be rfc compliant. Always represented as // Name of the channel. Must be rfc1459 compliant.
// lower-case, to ensure that the channel is only being tracked once.
Name string Name string
// Topic of the channel. // Topic of the channel.
Topic string Topic string
// users represents the users that we can currently see within the
// channel. // Users is a sorted list of all users we are currently tracking within
users map[string]*User // the channel. Each is the nickname, and is rfc1459 compliant.
Users []string
// Joined represents the first time that the client joined the channel. // Joined represents the first time that the client joined the channel.
Joined time.Time Joined time.Time
// Modes are the known channel modes that the bot has captured. // Modes are the known channel modes that the bot has captured.
Modes CModes Modes CModes
} }
func (c *Channel) deleteUser(nick string) {
nick = ToRFC1459(nick)
j := -1
for i := 0; i < len(c.Users); i++ {
if c.Users[i] == nick {
j = i
break
}
}
if j != -1 {
c.Users = append(c.Users[:j], c.Users[j+1:]...)
}
}
// Copy returns a deep copy of a given channel. // Copy returns a deep copy of a given channel.
func (c *Channel) Copy() *Channel { func (c *Channel) Copy() *Channel {
nc := &Channel{} nc := &Channel{}
*nc = *c *nc = *c
// Copy the users. _ = copy(nc.Users, c.Users)
nc.users = make(map[string]*User)
for k, v := range c.users {
nc.users[k] = v
}
// And modes. // And modes.
nc.Modes = c.Modes.Copy() nc.Modes = c.Modes.Copy()
@ -144,51 +204,22 @@ func (c *Channel) Copy() *Channel {
return nc return nc
} }
// Users returns a list of users in a given channel.
func (c *Channel) Users() []*User {
out := make([]*User, len(c.users))
var index int
for _, u := range c.users {
out[index] = u
index++
}
return out
}
// NickList returns a list of nicknames in a given channel.
func (c *Channel) NickList() []string {
out := make([]string, len(c.users))
var index int
for k := range c.users {
out[index] = k
index++
}
return out
}
// Len returns the count of users in a given channel. // Len returns the count of users in a given channel.
func (c *Channel) Len() int { func (c *Channel) Len() int {
return len(c.users) return len(c.Users)
} }
// Lookup looks up a user in a channel based on a given nickname. If the // UserIn checks to see if a given user is in a channel.
// user wasn't found, user is nil. func (c *Channel) UserIn(name string) bool {
func (c *Channel) Lookup(nick string) *User { name = ToRFC1459(name)
for k, v := range c.users {
if ToRFC1459(k) == ToRFC1459(nick) { for i := 0; i < len(c.Users); i++ {
// No need to have a copy, as if one has access to a channel, if c.Users[i] == name {
// should already have a full copy. return true
return v
} }
} }
return nil return false
} }
// Lifetime represents the amount of time that has passed since we have first // Lifetime represents the amount of time that has passed since we have first
@ -208,43 +239,63 @@ func (s *state) createChanIfNotExists(name string) (channel *Channel) {
supported := s.chanModes() supported := s.chanModes()
prefixes, _ := parsePrefixes(s.userPrefixes()) prefixes, _ := parsePrefixes(s.userPrefixes())
name = strings.ToLower(name) if _, ok := s.channels[ToRFC1459(name)]; ok {
if _, ok := s.channels[name]; !ok { return s.channels[ToRFC1459(name)]
channel = &Channel{
Name: name,
users: make(map[string]*User),
Joined: time.Now(),
Modes: NewCModes(supported, prefixes),
}
s.channels[name] = channel
} else {
channel = s.channels[name]
} }
channel = &Channel{
Name: name,
Users: []string{},
Joined: time.Now(),
Modes: NewCModes(supported, prefixes),
}
s.channels[ToRFC1459(name)] = channel
return channel return channel
} }
// deleteChannel removes the channel from state, if not already done. Always // deleteChannel removes the channel from state, if not already done. Always
// use state.mu for transaction. // use state.mu for transaction.
func (s *state) deleteChannel(name string) { func (s *state) deleteChannel(name string) {
channel := s.createChanIfNotExists(name) name = ToRFC1459(name)
if channel == nil {
_, ok := s.channels[name]
if !ok {
return return
} }
if _, ok := s.channels[channel.Name]; ok { for _, user := range s.channels[name].Users {
delete(s.channels, channel.Name) s.users[user].deleteChannel(name)
if len(s.users[user].Channels) == 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 with a given case-insensitive // lookupChannel returns a reference to a channel, nil returned if no results
// name. nil returned if no results found. // found. Always use state.mu for transaction.
func (s *state) lookupChannel(name string) *Channel { func (s *state) lookupChannel(name string) *Channel {
if !IsValidChannel(name) { if !IsValidChannel(name) {
return nil return nil
} }
return s.channels[strings.ToLower(name)] return s.channels[ToRFC1459(name)]
}
// lookupUser returns a reference to a user, nil returned if no results
// found. Always use state.mu for transaction.
func (s *state) lookupUser(name string) *User {
if !IsValidNick(name) {
return nil
}
return s.users[ToRFC1459(name)]
} }
// createUserIfNotExists creates the channel and user in state, if not already // createUserIfNotExists creates the channel and user in state, if not already
@ -256,34 +307,66 @@ func (s *state) createUserIfNotExists(channelName, nick string) (user *User) {
channel := s.createChanIfNotExists(channelName) channel := s.createChanIfNotExists(channelName)
if channel == nil { if channel == nil {
return nil return
} }
if _, ok := channel.users[nick]; ok { user = s.lookupUser(nick)
channel.users[nick].LastActive = time.Now() if user != nil {
return channel.users[nick] if !user.InChannel(channelName) {
user.Channels = append(user.Channels, ToRFC1459(channelName))
sort.StringsAreSorted(user.Channels)
}
user.LastActive = time.Now()
return user
} }
user = &User{Nick: nick, FirstSeen: time.Now(), LastActive: time.Now()} user = &User{
channel.users[nick] = user Nick: nick,
FirstSeen: time.Now(),
LastActive: time.Now(),
}
s.users[ToRFC1459(nick)] = user
channel.Users = append(channel.Users, ToRFC1459(nick))
sort.Strings(channel.Users)
return user return user
} }
// deleteUser removes the user from channel state. Always use state.mu for // deleteUser removes the user from channel state. Always use state.mu for
// transaction. // transaction.
func (s *state) deleteUser(nick string) { func (s *state) deleteUser(channelName, nick string) {
if !IsValidNick(nick) { if !IsValidNick(nick) {
return return
} }
for k := range s.channels { user := s.lookupUser(nick)
// Check to see if they're in this channel. if user == nil {
if _, ok := s.channels[k].users[nick]; !ok { return
continue }
if channelName == "" {
for i := 0; i < len(user.Channels); i++ {
s.channels[user.Channels[i]].deleteUser(nick)
} }
delete(s.channels[k].users, nick) delete(s.users, ToRFC1459(nick))
return
}
channel := s.lookupChannel(channelName)
if channel == nil {
return
}
user.deleteChannel(channelName)
channel.deleteUser(nick)
if len(user.Channels) == 0 {
// This means they are no longer in any channels we track, delete
// them from state.
delete(s.users, ToRFC1459(nick))
} }
} }
@ -294,59 +377,29 @@ func (s *state) renameUser(from, to string) {
return return
} }
from = ToRFC1459(from)
// Update our nickname. // Update our nickname.
if from == s.nick { if from == ToRFC1459(s.nick) {
s.nick = to s.nick = to
} }
for k := range s.channels { user := s.lookupUser(from)
// Check to see if they're in this channel. if user == nil {
if _, ok := s.channels[k].users[from]; !ok { return
continue
}
// Take the actual reference to the pointer.
source := *s.channels[k].users[from]
// Update the nick field (as we not only have a key, but a matching
// struct field).
source.Nick = to
source.LastActive = time.Now()
// Delete the old reference.
delete(s.channels[k].users, from)
// In with the new.
s.channels[k].users[to] = &source
} }
}
// lookupUsers returns a slice of references to users matching a given delete(s.users, from)
// 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 { user.Nick = to
for u := range s.channels[c].users { user.LastActive = time.Now()
switch matchType { s.users[ToRFC1459(to)] = user
case "nick":
if ToRFC1459(s.channels[c].users[u].Nick) == ToRFC1459(toMatch) { for i := 0; i < len(user.Channels); i++ {
users = append(users, s.channels[c].users[u]) for j := 0; j < len(s.channels[user.Channels[i]].Users); j++ {
continue if s.channels[user.Channels[i]].Users[j] == from {
} s.channels[user.Channels[i]].Users[j] = ToRFC1459(to)
case "ident":
if ToRFC1459(s.channels[c].users[u].Ident) == ToRFC1459(toMatch) {
users = append(users, s.channels[c].users[u])
continue
}
case "account":
if ToRFC1459(s.channels[c].users[u].Extras.Account) == ToRFC1459(toMatch) {
users = append(users, s.channels[c].users[u])
continue
}
} }
} }
} }
return users
} }