cleanup state mutexs, add state change events

This commit is contained in:
Liam Stanley 2017-07-12 00:44:44 -04:00
parent bd2c3e31ea
commit 9e08ab340d
9 changed files with 191 additions and 171 deletions

View File

@ -58,7 +58,7 @@ func (c *Client) registerBuiltins() {
c.Handlers.register(true, CAP_CHGHOST, HandlerFunc(handleCHGHOST)) c.Handlers.register(true, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
c.Handlers.register(true, CAP_AWAY, HandlerFunc(handleAWAY)) c.Handlers.register(true, CAP_AWAY, HandlerFunc(handleAWAY))
c.Handlers.register(true, CAP_ACCOUNT, HandlerFunc(handleACCOUNT)) c.Handlers.register(true, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
c.Handlers.register(true, ALLEVENTS, HandlerFunc(handleTags)) c.Handlers.register(true, ALL_EVENTS, HandlerFunc(handleTags))
// SASL IRCv3 support. // SASL IRCv3 support.
c.Handlers.register(true, AUTHENTICATE, HandlerFunc(handleSASL)) c.Handlers.register(true, AUTHENTICATE, HandlerFunc(handleSASL))
@ -87,13 +87,14 @@ func handleConnect(c *Client, e Event) {
// the one we supplied during connection, but some networks will rename // the one we supplied during connection, but some networks will rename
// users on connect. // users on connect.
if len(e.Params) > 0 { if len(e.Params) > 0 {
c.state.mu.Lock() c.state.Lock()
c.state.nick = e.Params[0] c.state.nick = e.Params[0]
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_GENERAL)
} }
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
c.RunHandlers(&Event{Command: CONNECTED, Trailing: c.Server()}) c.RunHandlers(&Event{Command: CONNECTED, Trailing: c.Server()})
} }
@ -123,24 +124,39 @@ func handleJOIN(c *Client, e Event) {
return return
} }
var channel string var channelName string
if len(e.Params) > 0 { if len(e.Params) > 0 {
channel = e.Params[0] channelName = e.Params[0]
} else { } else {
channel = e.Trailing channelName = e.Trailing
} }
// Create the user in state. This will also verify the channel. if e.Source.Name == c.GetNick() {
c.state.mu.Lock() if ok := c.state.createChannel(channelName); !ok {
user := c.state.createUserIfNotExists(channel, e.Source.Name) return
c.state.mu.Unlock() }
if user == nil { }
c.state.Lock()
if ok := c.state.createUser(e.Source.Name); !ok {
c.state.Unlock()
return return
} }
channel := c.state.lookupChannel(channelName)
user := c.state.lookupUser(e.Source.Name)
if channel == nil || user == nil {
return
}
defer c.state.notify(c, UPDATE_STATE)
channel.addUser(user.Nick)
user.addChannel(channel.Name)
// 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]
} }
@ -148,23 +164,23 @@ 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()
} }
c.state.Unlock()
if e.Source.Name == c.GetNick() { if e.Source.Name == c.GetNick() {
// If it's us, don't just add our user to the list. Run a WHO which // If it's us, don't just add our user to the list. Run a WHO which
// will tell us who exactly is in the entire channel. // will tell us who exactly is in the entire channel.
c.Send(&Event{Command: WHO, Params: []string{channel, "%tacuhnr,1"}}) c.Send(&Event{Command: WHO, Params: []string{channelName, "%tacuhnr,1"}})
// Also send a MODE to obtain the list of channel modes. // Also send a MODE to obtain the list of channel modes.
c.Send(&Event{Command: MODE, Params: []string{channel}}) c.Send(&Event{Command: MODE, Params: []string{channelName}})
// Update our ident and host too, in state -- since there is no // Update our ident and host too, in state -- since there is no
// cleaner method to do this. // cleaner method to do this.
c.state.mu.Lock() c.state.Lock()
c.state.ident = e.Source.Ident c.state.ident = e.Source.Ident
c.state.host = e.Source.Host c.state.host = e.Source.Host
c.state.mu.Unlock() c.state.Unlock()
return return
} }
@ -189,16 +205,18 @@ func handlePART(c *Client, e Event) {
return return
} }
defer c.state.notify(c, UPDATE_STATE)
if e.Source.Name == c.GetNick() { if e.Source.Name == c.GetNick() {
c.state.mu.Lock() c.state.Lock()
c.state.deleteChannel(channel) c.state.deleteChannel(channel)
c.state.mu.Unlock() c.state.Unlock()
return return
} }
c.state.mu.Lock() c.state.Lock()
c.state.deleteUser(channel, e.Source.Name) c.state.deleteUser(channel, e.Source.Name)
c.state.mu.Unlock() c.state.Unlock()
} }
// handleTOPIC handles incoming TOPIC events and keeps channel tracking info // handleTOPIC handles incoming TOPIC events and keeps channel tracking info
@ -214,21 +232,22 @@ func handleTOPIC(c *Client, e Event) {
name = e.Params[len(e.Params)-1] name = e.Params[len(e.Params)-1]
} }
c.state.mu.Lock() c.state.Lock()
channel := c.state.createChanIfNotExists(name) channel := c.state.lookupChannel(name)
if channel == nil { if channel == nil {
c.state.mu.Unlock() c.state.Unlock()
return return
} }
channel.Topic = e.Trailing channel.Topic = e.Trailing
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
} }
// handlWHO updates our internal tracking of users/channels with WHO/WHOX // handlWHO updates our internal tracking of users/channels with WHO/WHOX
// information. // information.
func handleWHO(c *Client, e Event) { func handleWHO(c *Client, e Event) {
var channel, ident, host, nick, account, realname string var ident, host, nick, account, realname string
// Assume WHOX related. // Assume WHOX related.
if e.Command == RPL_WHOSPCRPL { if e.Command == RPL_WHOSPCRPL {
@ -244,20 +263,20 @@ func handleWHO(c *Client, e Event) {
return return
} }
channel, ident, host, nick, account = e.Params[2], e.Params[3], e.Params[4], e.Params[5], e.Params[6] ident, host, nick, account = e.Params[3], e.Params[4], e.Params[5], e.Params[6]
realname = e.Trailing 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] ident, host, nick = e.Params[2], e.Params[3], e.Params[5]
if len(e.Trailing) > 2 { if len(e.Trailing) > 2 {
realname = e.Trailing[2:] realname = e.Trailing[2:]
} }
} }
c.state.mu.Lock() c.state.Lock()
user := c.state.createUserIfNotExists(channel, nick) user := c.state.lookupUser(nick)
if user == nil { if user == nil {
c.state.mu.Unlock() c.state.Unlock()
return return
} }
@ -269,7 +288,8 @@ func handleWHO(c *Client, e Event) {
user.Extras.Account = account user.Extras.Account = account
} }
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
} }
// handleKICK ensures that users are cleaned up after being kicked from the // handleKICK ensures that users are cleaned up after being kicked from the
@ -280,17 +300,19 @@ func handleKICK(c *Client, e Event) {
return return
} }
defer c.state.notify(c, UPDATE_STATE)
if e.Params[1] == c.GetNick() { if e.Params[1] == c.GetNick() {
c.state.mu.Lock() c.state.Lock()
c.state.deleteChannel(e.Params[0]) c.state.deleteChannel(e.Params[0])
c.state.mu.Unlock() c.state.Unlock()
return return
} }
// Assume it's just another user. // Assume it's just another user.
c.state.mu.Lock() c.state.Lock()
c.state.deleteUser(e.Params[0], e.Params[1]) c.state.deleteUser(e.Params[0], e.Params[1])
c.state.mu.Unlock() c.state.Unlock()
} }
// handleNICK ensures that users are renamed in state, or the client name is // handleNICK ensures that users are renamed in state, or the client name is
@ -300,14 +322,15 @@ func handleNICK(c *Client, e Event) {
return return
} }
c.state.mu.Lock() c.state.Lock()
// renameUser updates the LastActive time automatically. // renameUser updates the LastActive time automatically.
if len(e.Params) == 1 { if len(e.Params) == 1 {
c.state.renameUser(e.Source.Name, e.Params[0]) c.state.renameUser(e.Source.Name, e.Params[0])
} else if len(e.Trailing) > 0 { } else if len(e.Trailing) > 0 {
c.state.renameUser(e.Source.Name, e.Trailing) c.state.renameUser(e.Source.Name, e.Trailing)
} }
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
} }
// handleQUIT handles users that are quitting from the network. // handleQUIT handles users that are quitting from the network.
@ -320,9 +343,10 @@ func handleQUIT(c *Client, e Event) {
return return
} }
c.state.mu.Lock() c.state.Lock()
c.state.deleteUser("", e.Source.Name) c.state.deleteUser("", e.Source.Name)
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
} }
// handleMYINFO handles incoming MYINFO events -- these are commonly used // handleMYINFO handles incoming MYINFO events -- these are commonly used
@ -335,10 +359,11 @@ func handleMYINFO(c *Client, e Event) {
return return
} }
c.state.mu.Lock() c.state.Lock()
c.state.serverOptions["SERVER"] = e.Params[1] c.state.serverOptions["SERVER"] = e.Params[1]
c.state.serverOptions["VERSION"] = e.Params[2] c.state.serverOptions["VERSION"] = e.Params[2]
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_GENERAL)
} }
// handleISUPPORT handles incoming RPL_ISUPPORT (also known as RPL_PROTOCTL) // handleISUPPORT handles incoming RPL_ISUPPORT (also known as RPL_PROTOCTL)
@ -359,7 +384,7 @@ func handleISUPPORT(c *Client, e Event) {
return return
} }
c.state.mu.Lock() c.state.Lock()
// Skip the first parameter, as it's our nickname. // Skip the first parameter, as it's our nickname.
for i := 1; i < len(e.Params); i++ { for i := 1; i < len(e.Params); i++ {
j := strings.IndexByte(e.Params[i], 0x3D) // = j := strings.IndexByte(e.Params[i], 0x3D) // =
@ -373,19 +398,22 @@ func handleISUPPORT(c *Client, e Event) {
val := e.Params[i][j+1:] val := e.Params[i][j+1:]
c.state.serverOptions[name] = val c.state.serverOptions[name] = val
} }
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_GENERAL)
} }
// handleMOTD handles incoming MOTD messages and buffers them up for use with // handleMOTD handles incoming MOTD messages and buffers them up for use with
// Client.ServerMOTD(). // Client.ServerMOTD().
func handleMOTD(c *Client, e Event) { func handleMOTD(c *Client, e Event) {
c.state.mu.Lock() c.state.Lock()
defer c.state.notify(c, UPDATE_GENERAL)
// Beginning of the MOTD. // Beginning of the MOTD.
if e.Command == RPL_MOTDSTART { if e.Command == RPL_MOTDSTART {
c.state.motd = "" c.state.motd = ""
c.state.mu.Unlock() c.state.Unlock()
return return
} }
@ -395,15 +423,19 @@ func handleMOTD(c *Client, e Event) {
} }
c.state.motd += e.Trailing c.state.motd += e.Trailing
c.state.Unlock()
c.state.mu.Unlock()
} }
// handleNAMES handles incoming NAMES queries, of which lists all users in // handleNAMES handles incoming NAMES queries, of which lists all users in
// a given channel. Optionally also obtains ident/host values, as well as // a given channel. Optionally also obtains ident/host values, as well as
// permissions for each user, depending on what capabilities are enabled. // permissions for each user, depending on what capabilities are enabled.
func handleNAMES(c *Client, e Event) { func handleNAMES(c *Client, e Event) {
if len(e.Params) < 1 || !IsValidChannel(e.Params[len(e.Params)-1]) { if len(e.Params) < 1 {
return
}
channel := c.state.lookupChannel(e.Params[len(e.Params)-1])
if channel == nil {
return return
} }
@ -412,7 +444,7 @@ func handleNAMES(c *Client, e Event) {
var host, ident, modes, nick string var host, ident, modes, nick string
var ok bool var ok bool
c.state.mu.Lock() c.state.Lock()
for i := 0; i < len(parts); i++ { for i := 0; i < len(parts); i++ {
modes, nick, ok = parseUserPrefix(parts[i]) modes, nick, ok = parseUserPrefix(parts[i])
if !ok { if !ok {
@ -435,11 +467,15 @@ func handleNAMES(c *Client, e Event) {
continue continue
} }
user := c.state.createUserIfNotExists(e.Params[len(e.Params)-1], nick) c.state.createUser(nick)
user := c.state.lookupUser(nick)
if user == nil { if user == nil {
continue continue
} }
user.addChannel(channel.Name)
channel.addUser(nick)
// Add necessary userhost-in-names data into the user. // Add necessary userhost-in-names data into the user.
if host != "" { if host != "" {
user.Host = host user.Host = host
@ -451,7 +487,8 @@ func handleNAMES(c *Client, e Event) {
// Don't append modes, overwrite them. // Don't append modes, overwrite them.
user.Perms.set(modes, false) user.Perms.set(modes, false)
} }
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
} }
// updateLastActive is a wrapper for any event which the source author // updateLastActive is a wrapper for any event which the source author
@ -463,14 +500,15 @@ func updateLastActive(c *Client, e Event) {
return return
} }
c.state.mu.Lock() c.state.Lock()
defer c.state.mu.Unlock()
// Update the users last active time, if they exist. // Update the users last active time, if they exist.
user := c.state.lookupUser(e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user == nil { if user == nil {
c.state.Unlock()
return return
} }
user.LastActive = time.Now() user.LastActive = time.Now()
c.state.Unlock()
} }

32
cap.go
View File

@ -92,7 +92,7 @@ func handleCAP(c *Client, e Event) {
possible := possibleCapList(c) possible := possibleCapList(c)
if len(e.Params) >= 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_LS { if len(e.Params) >= 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_LS {
c.state.mu.Lock() c.state.Lock()
caps := parseCap(e.Trailing) caps := parseCap(e.Trailing)
@ -124,7 +124,7 @@ func handleCAP(c *Client, e Event) {
c.state.tmpCap = append(c.state.tmpCap, k) c.state.tmpCap = append(c.state.tmpCap, k)
} }
c.state.mu.Unlock() c.state.Unlock()
// Indicates if this is a multi-line LS. (2 args means it's the // Indicates if this is a multi-line LS. (2 args means it's the
// last LS). // last LS).
@ -140,14 +140,14 @@ func handleCAP(c *Client, e Event) {
// Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests // Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests
// due to cap-notify, we can re-evaluate what we can support. // due to cap-notify, we can re-evaluate what we can support.
c.state.mu.Lock() c.state.Lock()
c.state.tmpCap = []string{} c.state.tmpCap = []string{}
c.state.mu.Unlock() c.state.Unlock()
} }
} }
if len(e.Params) == 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_ACK { if len(e.Params) == 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_ACK {
c.state.mu.Lock() c.state.Lock()
c.state.enabledCap = strings.Split(e.Trailing, " ") c.state.enabledCap = strings.Split(e.Trailing, " ")
// Do we need to do sasl auth? // Do we need to do sasl auth?
@ -158,7 +158,7 @@ func handleCAP(c *Client, e Event) {
break break
} }
} }
c.state.mu.Unlock() c.state.Unlock()
if wantsSASL { if wantsSASL {
c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}}) c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}})
@ -320,24 +320,26 @@ func handleCHGHOST(c *Client, e Event) {
return return
} }
c.state.mu.Lock() c.state.Lock()
user := c.state.lookupUser(e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user != nil { if user != nil {
user.Ident = e.Params[0] user.Ident = e.Params[0]
user.Host = e.Params[1] user.Host = e.Params[1]
} }
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
} }
// handleAWAY handles incoming IRCv3 AWAY events, for which are sent both // handleAWAY handles incoming IRCv3 AWAY events, for which are sent both
// 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.Lock()
user := c.state.lookupUser(e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user != nil { if user != nil {
user.Extras.Away = e.Trailing user.Extras.Away = e.Trailing
} }
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
} }
// handleACCOUNT handles incoming IRCv3 ACCOUNT events. ACCOUNT is sent when // handleACCOUNT handles incoming IRCv3 ACCOUNT events. ACCOUNT is sent when
@ -354,12 +356,13 @@ func handleACCOUNT(c *Client, e Event) {
account = "" account = ""
} }
c.state.mu.Lock() c.state.Lock()
user := c.state.lookupUser(e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user != nil { if user != nil {
user.Extras.Account = account user.Extras.Account = account
} }
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
} }
// handleTags handles any messages that have tags that will affect state. (e.g. // handleTags handles any messages that have tags that will affect state. (e.g.
@ -374,12 +377,13 @@ func handleTags(c *Client, e Event) {
return return
} }
c.state.mu.Lock() c.state.Lock()
user := c.state.lookupUser(e.Source.Name) user := c.state.lookupUser(e.Source.Name)
if user != nil { if user != nil {
user.Extras.Account = account user.Extras.Account = account
} }
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
} }
const ( const (

View File

@ -322,9 +322,10 @@ func (c *Client) DisableTracking() {
c.Config.disableTracking = true c.Config.disableTracking = true
c.Handlers.clearInternal() c.Handlers.clearInternal()
c.state.mu.Lock() c.state.Lock()
c.state.channels = nil c.state.channels = nil
c.state.mu.Unlock() c.state.Unlock()
c.state.notify(c, UPDATE_STATE)
c.registerBuiltins() c.registerBuiltins()
} }
@ -388,8 +389,8 @@ func (c *Client) IsConnected() (connected bool) {
func (c *Client) GetNick() string { func (c *Client) GetNick() string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.RLock() c.state.RLock()
defer c.state.mu.RUnlock() defer c.state.RUnlock()
if c.state.nick == "" { if c.state.nick == "" {
return c.Config.Nick return c.Config.Nick
@ -404,8 +405,8 @@ func (c *Client) GetNick() string {
func (c *Client) GetIdent() string { func (c *Client) GetIdent() string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.RLock() c.state.RLock()
defer c.state.mu.RUnlock() defer c.state.RUnlock()
if c.state.ident == "" { if c.state.ident == "" {
return c.Config.User return c.Config.User
@ -420,8 +421,8 @@ func (c *Client) GetIdent() string {
func (c *Client) GetHost() string { func (c *Client) GetHost() string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.RLock() c.state.RLock()
defer c.state.mu.RUnlock() defer c.state.RUnlock()
return c.state.host return c.state.host
} }
@ -431,14 +432,14 @@ func (c *Client) GetHost() string {
func (c *Client) Channels() []string { func (c *Client) Channels() []string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.RLock() c.state.RLock()
channels := make([]string, len(c.state.channels)) 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.RUnlock()
sort.Strings(channels) sort.Strings(channels)
return channels return channels
@ -449,14 +450,14 @@ func (c *Client) Channels() []string {
func (c *Client) Users() []string { func (c *Client) Users() []string {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.RLock() c.state.RLock()
users := make([]string, len(c.state.users)) users := make([]string, len(c.state.users))
var i int var i int
for user := range c.state.users { for user := range c.state.users {
users[i] = user users[i] = user
i++ i++
} }
c.state.mu.RUnlock() c.state.RUnlock()
sort.Strings(users) sort.Strings(users)
return users return users
@ -470,10 +471,10 @@ func (c *Client) LookupChannel(name string) *Channel {
return nil return nil
} }
c.state.mu.Lock() c.state.RLock()
defer c.state.RUnlock()
channel := c.state.lookupChannel(name) channel := c.state.lookupChannel(name)
c.state.mu.Unlock()
if channel == nil { if channel == nil {
return nil return nil
} }
@ -489,10 +490,10 @@ func (c *Client) LookupUser(nick string) *User {
return nil return nil
} }
c.state.mu.Lock() c.state.RLock()
defer c.state.RUnlock()
user := c.state.lookupUser(nick) user := c.state.lookupUser(nick)
c.state.mu.Unlock()
if user == nil { if user == nil {
return nil return nil
} }
@ -505,9 +506,9 @@ func (c *Client) LookupUser(nick string) *User {
func (c *Client) IsInChannel(channel string) bool { func (c *Client) IsInChannel(channel string) bool {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.RLock() c.state.RLock()
_, inChannel := c.state.channels[ToRFC1459(channel)] _, inChannel := c.state.channels[ToRFC1459(channel)]
c.state.mu.RUnlock() c.state.RUnlock()
return inChannel return inChannel
} }
@ -521,9 +522,9 @@ func (c *Client) IsInChannel(channel string) bool {
func (c *Client) GetServerOption(key string) (result string, ok bool) { func (c *Client) GetServerOption(key string) (result string, ok bool) {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.Lock() c.state.RLock()
result, ok = c.state.serverOptions[key] result, ok = c.state.serverOptions[key]
c.state.mu.Unlock() c.state.RUnlock()
return result, ok return result, ok
} }
@ -567,9 +568,9 @@ func (c *Client) ServerVersion() (version string) {
func (c *Client) ServerMOTD() (motd string) { func (c *Client) ServerMOTD() (motd string) {
c.panicIfNotTracking() c.panicIfNotTracking()
c.state.mu.Lock() c.state.RLock()
motd = c.state.motd motd = c.state.motd
c.state.mu.Unlock() c.state.RUnlock()
return motd return motd
} }

View File

@ -27,8 +27,8 @@ func TestDisableTracking(t *testing.T) {
t.Fatal("Client.Handlers contains capability tracking handlers, though disabled") t.Fatal("Client.Handlers contains capability tracking handlers, though disabled")
} }
client.state.mu.Lock() client.state.Lock()
defer client.state.mu.Unlock() defer client.state.Unlock()
if client.state.channels != nil { if client.state.channels != nil {
t.Fatal("Client.DisableTracking() called but channel state still exists") t.Fatal("Client.DisableTracking() called but channel state still exists")

View File

@ -444,7 +444,7 @@ func (c *Client) sendLoop(errs chan error, done chan struct{}, wg *sync.WaitGrou
// Check if tags exist on the event. If they do, and message-tags // Check if tags exist on the event. If they do, and message-tags
// isn't a supported capability, remove them from the event. // isn't a supported capability, remove them from the event.
if event.Tags != nil { if event.Tags != nil {
c.state.mu.Lock() c.state.RLock()
var in bool var in bool
for i := 0; i < len(c.state.enabledCap); i++ { for i := 0; i < len(c.state.enabledCap); i++ {
if c.state.enabledCap[i] == "message-tags" { if c.state.enabledCap[i] == "message-tags" {
@ -452,7 +452,7 @@ func (c *Client) sendLoop(errs chan error, done chan struct{}, wg *sync.WaitGrou
break break
} }
} }
c.state.mu.Unlock() c.state.RUnlock()
if !in { if !in {
event.Tags = Tags{} event.Tags = Tags{}

View File

@ -20,11 +20,13 @@ const (
// Emulated event commands used to allow easier hooks into the changing // Emulated event commands used to allow easier hooks into the changing
// state of the client. // state of the client.
const ( const (
ALLEVENTS = "*" // trigger on all events UPDATE_STATE = "CLIENT_STATE_UPDATED" // when channel/user state is updated.
CONNECTED = "CONNECTED" // when it's safe to send arbitrary commands (joins, list, who, etc), trailing is host:port UPDATE_GENERAL = "CLIENT_GENERAL_UPDATED" // when general state (client nick, server name, etc) is updated.
INITIALIZED = "INIT" // verifies successful socket connection, trailing is host:port ALL_EVENTS = "*" // trigger on all events
DISCONNECTED = "DISCONNECTED" // occurs when we're disconnected from the server (user-requested or not) CONNECTED = "CLIENT_CONNECTED" // when it's safe to send arbitrary commands (joins, list, who, etc), trailing is host:port
STOPPED = "STOPPED" // occurs when Client.Stop() has been called INITIALIZED = "CLIENT_INIT" // verifies successful socket connection, trailing is host:port
DISCONNECTED = "CLIENT_DISCONNECTED" // occurs when we're disconnected from the server (user-requested or not)
STOPPED = "CLIENT_STOPPED" // occurs when Client.Stop() has been called
) )
// User/channel prefixes :: RFC1459. // User/channel prefixes :: RFC1459.

View File

@ -30,7 +30,7 @@ func (c *Client) RunHandlers(event *Event) {
} }
// Regular wildcard handlers. // Regular wildcard handlers.
c.Handlers.exec(ALLEVENTS, c, event.Copy()) c.Handlers.exec(ALL_EVENTS, c, event.Copy())
// Then regular handlers. // Then regular handlers.
c.Handlers.exec(event.Command, c, event.Copy()) c.Handlers.exec(event.Command, c, event.Copy())

View File

@ -329,10 +329,10 @@ func handleMODE(c *Client, e Event) {
return return
} }
c.state.mu.Lock() c.state.RLock()
channel := c.state.lookupChannel(e.Params[0]) channel := c.state.lookupChannel(e.Params[0])
if channel == nil { if channel == nil {
c.state.mu.Unlock() c.state.RUnlock()
return return
} }
@ -357,7 +357,8 @@ func handleMODE(c *Client, e Event) {
} }
} }
c.state.mu.Unlock() c.state.RUnlock()
c.state.notify(c, UPDATE_STATE)
} }
// chanModes returns the ISUPPORT list of server-supported channel modes, // chanModes returns the ISUPPORT list of server-supported channel modes,

108
state.go
View File

@ -11,11 +11,10 @@ import (
) )
// state represents the actively-changing variables within the client // state represents the actively-changing variables within the client
// runtime. // runtime. Note that everything within the state should be guarded by the
// embedded sync.RWMutex.
type state struct { type state struct {
// m is a RW mutex lock, used to guard the state from goroutines causing sync.RWMutex
// corruption.
mu sync.RWMutex
// nick, ident, and host are the internal trackers for our user. // nick, ident, and host are the internal trackers for our user.
nick, ident, host string nick, ident, host string
// channels represents all channels we're active in. // channels represents all channels we're active in.
@ -36,9 +35,15 @@ type state struct {
motd string 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. // reset resets the state back to it's original form.
func (s *state) reset() { func (s *state) reset() {
s.mu.Lock() s.Lock()
s.nick = "" s.nick = ""
s.ident = "" s.ident = ""
s.host = "" s.host = ""
@ -47,7 +52,7 @@ func (s *state) reset() {
s.serverOptions = make(map[string]string) s.serverOptions = make(map[string]string)
s.enabledCap = []string{} s.enabledCap = []string{}
s.motd = "" s.motd = ""
s.mu.Unlock() s.Unlock()
} }
// User represents an IRC user and the state attached to them. // User represents an IRC user and the state attached to them.
@ -113,6 +118,13 @@ func (u *User) Copy() *User {
return nu return nu
} }
// addChannel adds the channel to the users channel list.
func (u *User) addChannel(name string) {
u.Channels = append(u.Channels, ToRFC1459(name))
sort.StringsAreSorted(u.Channels)
}
// deleteChannel removes an existing channel from the users channel list.
func (u *User) deleteChannel(name string) { func (u *User) deleteChannel(name string) {
name = ToRFC1459(name) name = ToRFC1459(name)
@ -175,6 +187,13 @@ type Channel struct {
Modes CModes Modes CModes
} }
// addUser adds a user to the users list.
func (c *Channel) addUser(nick string) {
c.Users = append(c.Users, ToRFC1459(nick))
sort.Strings(c.Users)
}
// deleteUser removes an existing user from the users list.
func (c *Channel) deleteUser(nick string) { func (c *Channel) deleteUser(nick string) {
nick = ToRFC1459(nick) nick = ToRFC1459(nick)
@ -228,34 +247,26 @@ func (c *Channel) Lifetime() time.Duration {
return time.Since(c.Joined) return time.Since(c.Joined)
} }
// createChanIfNotExists creates the channel in state, if not already done. // createChannel creates the channel in state, if not already done.
// Always use state.mu for transaction. func (s *state) createChannel(name string) (ok bool) {
func (s *state) createChanIfNotExists(name string) (channel *Channel) {
// Not a valid channel.
if !IsValidChannel(name) {
return nil
}
supported := s.chanModes() supported := s.chanModes()
prefixes, _ := parsePrefixes(s.userPrefixes()) prefixes, _ := parsePrefixes(s.userPrefixes())
if _, ok := s.channels[ToRFC1459(name)]; ok { if _, ok := s.channels[ToRFC1459(name)]; ok {
return s.channels[ToRFC1459(name)] return false
} }
channel = &Channel{ s.channels[ToRFC1459(name)] = &Channel{
Name: name, Name: name,
Users: []string{}, Users: []string{},
Joined: time.Now(), Joined: time.Now(),
Modes: NewCModes(supported, prefixes), Modes: NewCModes(supported, prefixes),
} }
s.channels[ToRFC1459(name)] = channel
return channel return true
} }
// deleteChannel removes the channel from state, if not already done. Always // deleteChannel removes the channel from state, if not already done.
// use state.mu for transaction.
func (s *state) deleteChannel(name string) { func (s *state) deleteChannel(name string) {
name = ToRFC1459(name) name = ToRFC1459(name)
@ -279,67 +290,35 @@ func (s *state) deleteChannel(name string) {
} }
// lookupChannel returns a reference to a channel, nil returned if no results // lookupChannel returns a reference to a channel, nil returned if no results
// found. Always use state.mu for transaction. // found.
func (s *state) lookupChannel(name string) *Channel { func (s *state) lookupChannel(name string) *Channel {
if !IsValidChannel(name) {
return nil
}
return s.channels[ToRFC1459(name)] return s.channels[ToRFC1459(name)]
} }
// lookupUser returns a reference to a user, nil returned if no results // lookupUser returns a reference to a user, nil returned if no results
// found. Always use state.mu for transaction. // found.
func (s *state) lookupUser(name string) *User { func (s *state) lookupUser(name string) *User {
if !IsValidNick(name) {
return nil
}
return s.users[ToRFC1459(name)] return s.users[ToRFC1459(name)]
} }
// createUserIfNotExists creates the channel and user in state, if not already // createUser creates the user in state, if not already done.
// done. Always use state.mu for transaction. func (s *state) createUser(nick string) (ok bool) {
func (s *state) createUserIfNotExists(channelName, nick string) (user *User) { if _, ok := s.users[ToRFC1459(nick)]; ok {
if !IsValidNick(nick) { // User already exists.
return nil return false
} }
channel := s.createChanIfNotExists(channelName) s.users[ToRFC1459(nick)] = &User{
if channel == nil {
return
}
user = s.lookupUser(nick)
if user != nil {
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, Nick: nick,
FirstSeen: time.Now(), FirstSeen: time.Now(),
LastActive: time.Now(), LastActive: time.Now(),
} }
s.users[ToRFC1459(nick)] = user
channel.Users = append(channel.Users, ToRFC1459(nick))
sort.Strings(channel.Users)
return user return true
} }
// deleteUser removes the user from channel state. Always use state.mu for // deleteUser removes the user from channel state.
// transaction.
func (s *state) deleteUser(channelName, nick string) { func (s *state) deleteUser(channelName, nick string) {
if !IsValidNick(nick) {
return
}
user := s.lookupUser(nick) user := s.lookupUser(nick)
if user == nil { if user == nil {
return return
@ -371,12 +350,7 @@ func (s *state) deleteUser(channelName, nick string) {
} }
// renameUser renames the user in state, in all locations where relevant. // renameUser renames the user in state, in all locations where relevant.
// Always use state.mu for transaction.
func (s *state) renameUser(from, to string) { func (s *state) renameUser(from, to string) {
if !IsValidNick(from) || !IsValidNick(to) {
return
}
from = ToRFC1459(from) from = ToRFC1459(from)
// Update our nickname. // Update our nickname.