diff --git a/client.go b/client.go index fd3429a..90ad5f6 100644 --- a/client.go +++ b/client.go @@ -43,15 +43,15 @@ var ErrNoConnection = errors.New("irc: no connection") // added to the client. var ErrTargetAlreadyAdded = errors.New("irc: target already added") -// ErrTargetConflict is returned by Clinet.AddTarget if there already exists a target +// ErrTargetConflict is returned by Client.AddTarget if there already exists a target // matching the name and kind. var ErrTargetConflict = errors.New("irc: target name and kind match existing target") -// ErrTargetNotFound is returned by Clinet.RemoveTarget if the target is not part of +// ErrTargetNotFound is returned by Client.RemoveTarget if the target is not part of // the client's target list var ErrTargetNotFound = errors.New("irc: target not found") -// ErrTargetIsStatus is returned by Clinet.RemoveTarget if the target is the client's +// ErrTargetIsStatus is returned by Client.RemoveTarget if the target is the client's // status target var ErrTargetIsStatus = errors.New("irc: cannot remove status target") @@ -85,6 +85,8 @@ type Client struct { status *Status targets []Target targetIds map[Target]string + + handlers []Handler } // New creates a new client. The context can be context.Background if you want manually to @@ -102,7 +104,7 @@ func New(ctx context.Context, config Config) *Client { status: &Status{}, } - client.AddTarget(client.status) + _, _ = client.AddTarget(client.status) client.ctx, client.cancel = context.WithCancel(ctx) @@ -174,6 +176,7 @@ func (client *Client) Ready() bool { func (client *Client) State() ClientState { client.mutex.RLock() + state := ClientState{ Nick: client.nick, User: client.user, @@ -190,6 +193,7 @@ func (client *Client) State() ClientState { state.Caps = append(state.Caps, key) } } + sort.Strings(state.Caps) for _, target := range client.targets { tstate := target.State() @@ -200,8 +204,6 @@ func (client *Client) State() ClientState { client.mutex.RUnlock() - sort.Strings(state.Caps) - return state } @@ -210,7 +212,7 @@ func (client *Client) Connect(addr string, ssl bool) (err error) { var conn net.Conn if client.Connected() { - client.Disconnect() + _ = client.Disconnect() } client.isupport.Reset() @@ -219,7 +221,7 @@ func (client *Client) Connect(addr string, ssl bool) (err error) { client.quit = false client.mutex.Unlock() - client.EmitSync(context.Background(), NewEvent("client", "connecting")) + _ = client.EmitSync(context.Background(), NewEvent("client", "connecting")) if ssl { conn, err = tls.Dial("tcp", addr, &tls.Config{ @@ -283,9 +285,7 @@ func (client *Client) Disconnect() error { client.quit = true - err := client.conn.Close() - - return err + return client.conn.Close() } // Connected returns true if the client has a connection @@ -314,7 +314,7 @@ func (client *Client) Send(line string) error { _, err := conn.Write([]byte(line)) if err != nil { client.EmitNonBlocking(NewErrorEvent("network", err.Error())) - client.Disconnect() + _ = client.Disconnect() } return err @@ -402,7 +402,7 @@ func (client *Client) Emit(event Event) context.Context { return event.ctx } -// EmitNonBlocking is just like emit, but it will spin off a goroutine if the channel is full. +// EmitNonBlocking is just like emitInGlobalHandlers, but it will spin off a goroutine if the channel is full. // This lets it be called from other handlers without ever blocking. See Emit for what the // returned context is for. func (client *Client) EmitNonBlocking(event Event) context.Context { @@ -476,7 +476,7 @@ func (client *Client) SetValue(key string, value interface{}) { // Destroy destroys the client, which will lead to a disconnect. Cancelling the // parent context will do the same. func (client *Client) Destroy() { - client.Disconnect() + _ = client.Disconnect() client.cancel() close(client.sends) close(client.events) @@ -668,6 +668,14 @@ func (client *Client) FindUser(nick string) (u list.User, ok bool) { return list.User{}, false } +// AddHandler adds a handler. This is thread safe, unlike adding global handlers. +func (client *Client) AddHandler(handler Handler) { + client.mutex.Lock() + client.handlers = append(client.handlers[:0], client.handlers...) + client.handlers = append(client.handlers, handler) + client.mutex.Unlock() +} + func (client *Client) handleEventLoop() { ticker := time.NewTicker(time.Second * 30) @@ -680,7 +688,6 @@ func (client *Client) handleEventLoop() { } client.handleEvent(event) - emit(event, client) // Turn an unhandled input into a raw command. if event.kind == "input" && !event.preventedDefault { @@ -695,7 +702,6 @@ func (client *Client) handleEventLoop() { event.ctx, event.cancel = context.WithCancel(client.ctx) client.handleEvent(&event) - emit(&event, client) event.cancel() } @@ -710,13 +716,12 @@ end: ticker.Stop() - client.Disconnect() + _ = client.Disconnect() event := NewEvent("client", "destroy") event.ctx, event.cancel = context.WithCancel(client.ctx) client.handleEvent(&event) - emit(&event, client) event.cancel() } @@ -742,7 +747,7 @@ func (client *Client) handleSendLoop() { queue = client.config.SendRate - 1 } - client.Send(line) + _ = client.Send(line) } } @@ -771,7 +776,7 @@ func (client *Client) handleEvent(event *Event) { client.mutex.RUnlock() if lastSend > time.Second*120 { - client.Sendf("PING :%x%x%x", mathRand.Int63(), mathRand.Int63(), mathRand.Int63()) + _ = client.Sendf("PING :%x%x%x", mathRand.Int63(), mathRand.Int63(), mathRand.Int63()) } } case "packet.ping": @@ -784,7 +789,7 @@ func (client *Client) handleEvent(event *Event) { message += " :" + event.Text } - client.Send(message) + _ = client.Send(message) } // Client Registration @@ -796,11 +801,11 @@ func (client *Client) handleEvent(event *Event) { delete(client.capEnabled, key) } client.mutex.Unlock() - client.Send("CAP LS 302") + _ = client.Send("CAP LS 302") // Send server password if configured. if client.config.Password != "" { - client.Sendf("PASS :%s", client.config.Password) + _ = client.Sendf("PASS :%s", client.config.Password) } // Reuse nick or get from config @@ -812,19 +817,22 @@ func (client *Client) handleEvent(event *Event) { client.mutex.RUnlock() // Start registration. - client.Sendf("NICK %s", nick) - client.Sendf("USER %s 8 * :%s", client.config.User, client.config.RealName) + _ = client.Sendf("NICK %s", nick) + _ = client.Sendf("USER %s 8 * :%s", client.config.User, client.config.RealName) } + // Welcome message case "packet.001": { client.mutex.Lock() client.nick = event.Args[0] client.mutex.Unlock() - client.Sendf("WHO %s", event.Args[0]) + // Send a WHO right away to gather enough client information for precise message cutting. + _ = client.Sendf("WHO %s", event.Args[0]) } + // Nick rotation case "packet.431", "packet.432", "packet.433", "packet.436": { client.mutex.RLock() @@ -839,7 +847,7 @@ func (client *Client) handleEvent(event *Event) { sent := false for _, alt := range client.config.Alternatives { if nick == prev { - client.Sendf("NICK %s", alt) + _ = client.Sendf("NICK %s", alt) sent = true break } @@ -849,7 +857,7 @@ func (client *Client) handleEvent(event *Event) { if !sent { // "LastAlt" -> "Nick23962" - client.Sendf("NICK %s%05d", client.config.Nick, mathRand.Int31n(99999)) + _ = client.Sendf("NICK %s%05d", client.config.Nick, mathRand.Int31n(99999)) } } } @@ -918,9 +926,9 @@ func (client *Client) handleEvent(event *Event) { requestedCaps := strings.Join(client.capsRequested, " ") client.mutex.RUnlock() - client.Send("CAP REQ :" + requestedCaps) + _ = client.Send("CAP REQ :" + requestedCaps) } else { - client.Send("CAP END") + _ = client.Send("CAP END") } } } @@ -934,7 +942,7 @@ func (client *Client) handleEvent(event *Event) { client.mutex.Unlock() } - client.Send("CAP END") + _ = client.Send("CAP END") } case "NAK": { @@ -954,7 +962,7 @@ func (client *Client) handleEvent(event *Event) { requestedCaps := strings.Join(client.capsRequested, " ") client.mutex.RUnlock() - client.Send("CAP REQ :" + requestedCaps) + _ = client.Send("CAP REQ :" + requestedCaps) } case "NEW": { @@ -969,7 +977,7 @@ func (client *Client) handleEvent(event *Event) { } if len(requests) > 0 { - client.Send("CAP REQ :" + strings.Join(requests, " ")) + _ = client.Send("CAP REQ :" + strings.Join(requests, " ")) } } case "DEL": @@ -1021,7 +1029,7 @@ func (client *Client) handleEvent(event *Event) { if event.Nick == client.nick { channel = &Channel{name: event.Arg(0), userlist: list.New(&client.isupport)} - client.AddTarget(channel) + _, _ = client.AddTarget(channel) } else { channel = client.Channel(event.Arg(0)) } @@ -1038,7 +1046,7 @@ func (client *Client) handleEvent(event *Event) { if event.Nick == client.nick { channel.parted = true - client.RemoveTarget(channel) + _, _ = client.RemoveTarget(channel) } else { client.handleInTarget(channel, event) } @@ -1053,7 +1061,7 @@ func (client *Client) handleEvent(event *Event) { if event.Arg(1) == client.nick { channel.parted = true - client.RemoveTarget(channel) + _, _ = client.RemoveTarget(channel) } else { client.handleInTarget(channel, event) } @@ -1095,9 +1103,8 @@ func (client *Client) handleEvent(event *Event) { // Message parsing case "packet.privmsg", "ctcp.action": { - // Target the mssage + // Target the message target := Target(client.status) - spawned := false targetName := event.Arg(0) if targetName == client.nick { target := client.Target("query", targetName) @@ -1108,9 +1115,8 @@ func (client *Client) handleEvent(event *Event) { Host: event.Host, }} - client.AddTarget(query) - - spawned = true + id, _ := client.AddTarget(query) + event.RenderTags["spawned"] = id target = query } @@ -1128,10 +1134,6 @@ func (client *Client) handleEvent(event *Event) { } client.handleInTarget(target, event) - - if spawned { - // TODO: Message has higher importance // 0:Normal, 1:Important, 2:Highlight - } } case "packet.notice": @@ -1202,7 +1204,7 @@ func (client *Client) handleEvent(event *Event) { client.mutex.RUnlock() if len(channels) > 0 { - client.Sendf("JOIN %s", strings.Join(channels, ",")) + _ = client.Sendf("JOIN %s", strings.Join(channels, ",")) client.EmitNonBlocking(rejoinEvent) } @@ -1217,6 +1219,17 @@ func (client *Client) handleEvent(event *Event) { if len(event.targets) == 0 { client.handleInTarget(client.status, event) } + + client.mutex.RLock() + clientHandlers := client.handlers + client.mutex.RUnlock() + + for _, handler := range globalHandlers { + handler(event, client) + } + for _, handler := range clientHandlers { + handler(event, client) + } } func (client *Client) handleInTargets(nick string, event *Event) { diff --git a/client_test.go b/client_test.go index 8b90eba..bf768c3 100644 --- a/client_test.go +++ b/client_test.go @@ -35,7 +35,7 @@ func TestClient(t *testing.T) { {Client: "NICK Test"}, {Client: "USER Tester 8 * :..."}, {Server: ":testserver.example.com CAP * LS :multi-prefix chghost userhost-in-names vendorname/custom-stuff echo-message =malformed vendorname/advanced-custom-stuff=things,and,items"}, - {Client: "CAP REQ :multi-prefix chghost userhost-in-names"}, + {Client: "CAP REQ :multi-prefix chghost userhost-in-names echo-message"}, {Server: ":testserver.example.com CAP * ACK :multi-prefix userhost-in-names"}, {Client: "CAP END"}, {Callback: func() error { @@ -377,8 +377,8 @@ func TestParenthesesBug(t *testing.T) { Nick: "Stuff", }) - irc.AddHandler(func(event *irc.Event, client *irc.Client) { - if event.Name() == "packet.privmsg" || event.Nick == "Dante" { + client.AddHandler(func(event *irc.Event, client *irc.Client) { + if event.Name() == "packet.privmsg" || event.Nick == "Beans" { gotMessage = true if event.Text != "((Remove :01 goofs!*))" { @@ -388,7 +388,7 @@ func TestParenthesesBug(t *testing.T) { } }) - packet, err := irc.ParsePacket("@example/tag=32; :Dante!TheBeans@captain.purple.beans PRIVMSG Stuff :((Remove :01 goofs!*))") + packet, err := irc.ParsePacket("@example/tag=32; :Beans!beans@beans.example.com PRIVMSG Stuff :((Remove :01 goofs!*))") if err != nil { t.Error("Parse", err) } diff --git a/handle.go b/handle.go index 9236dbd..d9492e2 100644 --- a/handle.go +++ b/handle.go @@ -4,25 +4,17 @@ package irc // events. type Handler func(event *Event, client *Client) -var eventHandler struct { - handlers []Handler -} - -func emit(event *Event, client *Client) { - for _, handler := range eventHandler.handlers { - handler(event, client) - } -} +var globalHandlers = make([]Handler, 0, 8) // AddHandler adds a new handler to the irc handling. The handler may be called from multiple threads at the same // time, so external resources should be locked if there are multiple clients. Adding handlers is not thread -// safe and should be done prior to clients being created.AddHandler. Also, this handler will block the individual +// safe and should be done prior to clients being created. Also, this handler will block the individual // client's event loop, so long operations that include network requests and the like should be done in a // goroutine with the needed data **copied** from the handler function. func AddHandler(handler Handler) { - eventHandler.handlers = append(eventHandler.handlers, handler) + globalHandlers = append(globalHandlers, handler) } func init() { - eventHandler.handlers = make([]Handler, 0, 8) + globalHandlers = make([]Handler, 0, 8) } diff --git a/internal/irctest/interaction.go b/internal/irctest/interaction.go index 667d1e2..ff1e4c3 100644 --- a/internal/irctest/interaction.go +++ b/internal/irctest/interaction.go @@ -50,6 +50,7 @@ func (interaction *Interaction) Listen() (addr string, err error) { line := lines[i] if line.Server != "" { + _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 2)) _, err := conn.Write(append([]byte(line.Server), '\r', '\n')) if err != nil { interaction.Failure = &InteractionFailure{ @@ -58,7 +59,7 @@ func (interaction *Interaction) Listen() (addr string, err error) { return } } else if line.Client != "" { - conn.SetReadDeadline(time.Now().Add(time.Second * 2)) + _ = conn.SetReadDeadline(time.Now().Add(time.Second * 2)) input, err := reader.ReadString('\n') if err != nil { interaction.Failure = &InteractionFailure{ diff --git a/isupport/isupport.go b/isupport/isupport.go index ac55df1..2310021 100644 --- a/isupport/isupport.go +++ b/isupport/isupport.go @@ -49,12 +49,12 @@ func (isupport *ISupport) ParsePrefixedNick(fullnick string) (nick, modes, prefi isupport.lock.RLock() defer isupport.lock.RUnlock() - if fullnick == "" || isupport.state.Prefixes == nil { + if fullnick == "" || isupport.state.PrefixMap == nil { return fullnick, "", "" } for i, ch := range fullnick { - if mode, ok := isupport.state.Prefixes[ch]; ok { + if mode, ok := isupport.state.PrefixMap[ch]; ok { modes += string(mode) prefixes += string(ch) } else { @@ -163,7 +163,7 @@ func (isupport *ISupport) Mode(prefix rune) rune { isupport.lock.RLock() defer isupport.lock.RUnlock() - return isupport.state.Prefixes[prefix] + return isupport.state.PrefixMap[prefix] } // Prefix gets the prefix for the mode. It's a bit slower @@ -173,7 +173,7 @@ func (isupport *ISupport) Prefix(mode rune) rune { isupport.lock.RLock() defer isupport.lock.RUnlock() - for prefix, mappedMode := range isupport.state.Prefixes { + for prefix, mappedMode := range isupport.state.PrefixMap { if mappedMode == mode { return prefix } @@ -182,7 +182,7 @@ func (isupport *ISupport) Prefix(mode rune) rune { return rune(0) } -// Prefixes gets the prefixes in the order of the modes, skipping any +// PrefixMap gets the prefixes in the order of the modes, skipping any // invalid modes. func (isupport *ISupport) Prefixes(modes string) string { result := "" @@ -280,9 +280,9 @@ func (isupport *ISupport) Set(key, value string) { isupport.state.PrefixOrder = split[1] isupport.state.ModeOrder = split[0] - isupport.state.Prefixes = make(map[rune]rune, len(split[0])) + isupport.state.PrefixMap = make(map[rune]rune, len(split[0])) for i, ch := range split[0] { - isupport.state.Prefixes[rune(split[1][i])] = ch + isupport.state.PrefixMap[rune(split[1][i])] = ch } } case "CHANMODES": // CHANMODES=eIbq,k,flj,CFLNPQcgimnprstz @@ -304,7 +304,7 @@ func (isupport *ISupport) Reset() { isupport.lock.Lock() isupport.state.PrefixOrder = "" isupport.state.ModeOrder = "" - isupport.state.Prefixes = nil + isupport.state.PrefixMap = nil isupport.state.ChannelModes = nil for key := range isupport.state.Raw { diff --git a/isupport/state.go b/isupport/state.go index 79d77c6..5b9f230 100644 --- a/isupport/state.go +++ b/isupport/state.go @@ -2,7 +2,7 @@ package isupport type State struct { Raw map[string]string `json:"raw"` - Prefixes map[rune]rune `json:"-"` + PrefixMap map[rune]rune `json:"prefixMap"` ModeOrder string `json:"modeOrder"` PrefixOrder string `json:"prefixOrder"` ChannelModes []string `json:"channelModes"` diff --git a/state.go b/state.go index 37e8761..1f13174 100644 --- a/state.go +++ b/state.go @@ -6,6 +6,7 @@ import ( ) type ClientState struct { + ID string `json:"id"` Nick string `json:"nick"` User string `json:"user"` Host string `json:"host"`