Browse Source

clean up code, add client-specific handlers.

master
Gisle Aune 4 years ago
parent
commit
ec7511e0df
  1. 105
      client.go
  2. 8
      client_test.go
  3. 16
      handle.go
  4. 3
      internal/irctest/interaction.go
  5. 16
      isupport/isupport.go
  6. 2
      isupport/state.go
  7. 1
      state.go

105
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) {

8
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)
}

16
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)
}

3
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{

16
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 {

2
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"`

1
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"`

Loading…
Cancel
Save