Skip to content

Commit fa5e001

Browse files
authored
Initial support for IRCv3 CAP negotiation (#58)
* Initial support for IRCv3 CAP negotiation * Send NICK and USER directly after our CAP REQ calls for simplicity * Add documentation for the maybeStartX methods
1 parent 01e72ff commit fa5e001

File tree

3 files changed

+287
-21
lines changed

3 files changed

+287
-21
lines changed

client.go

+120-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"io"
7+
"strings"
78
"sync"
89
"time"
910
)
@@ -52,6 +53,49 @@ var clientFilters = map[string]func(*Client, *Message){
5253
c.currentNick = m.Params[0]
5354
}
5455
},
56+
"CAP": func(c *Client, m *Message) {
57+
if c.remainingCapResponses <= 0 || len(m.Params) <= 2 {
58+
return
59+
}
60+
61+
switch m.Params[1] {
62+
case "LS":
63+
for _, key := range strings.Split(m.Trailing(), " ") {
64+
cap := c.caps[key]
65+
cap.Available = true
66+
c.caps[key] = cap
67+
}
68+
c.remainingCapResponses--
69+
case "ACK":
70+
for _, key := range strings.Split(m.Trailing(), " ") {
71+
cap := c.caps[key]
72+
cap.Enabled = true
73+
c.caps[key] = cap
74+
}
75+
c.remainingCapResponses--
76+
case "NAK":
77+
// If we got a NAK and this REQ was required, we need to bail
78+
// with an error.
79+
for _, key := range strings.Split(m.Trailing(), " ") {
80+
if c.caps[key].Required {
81+
c.sendError(fmt.Errorf("CAP %s requested but was rejected", key))
82+
return
83+
}
84+
}
85+
c.remainingCapResponses--
86+
}
87+
88+
if c.remainingCapResponses <= 0 {
89+
for key, cap := range c.caps {
90+
if cap.Required && !cap.Enabled {
91+
c.sendError(fmt.Errorf("CAP %s requested but not accepted", key))
92+
return
93+
}
94+
}
95+
96+
c.Write("CAP END")
97+
}
98+
},
5599
}
56100

57101
// ClientConfig is a structure used to configure a Client.
@@ -77,18 +121,34 @@ type ClientConfig struct {
77121
Handler Handler
78122
}
79123

124+
type cap struct {
125+
// Requested means that this cap was requested by the user
126+
Requested bool
127+
128+
// Required will be true if this cap is non-optional
129+
Required bool
130+
131+
// Enabled means that this cap was accepted by the server
132+
Enabled bool
133+
134+
// Available means that the server supports this cap
135+
Available bool
136+
}
137+
80138
// Client is a wrapper around Conn which is designed to make common operations
81139
// much simpler.
82140
type Client struct {
83141
*Conn
84142
config ClientConfig
85143

86144
// Internal state
87-
currentNick string
88-
limiter chan struct{}
89-
incomingPongChan chan string
90-
errChan chan error
91-
connected bool
145+
currentNick string
146+
limiter chan struct{}
147+
incomingPongChan chan string
148+
errChan chan error
149+
caps map[string]cap
150+
remainingCapResponses int
151+
connected bool
92152
}
93153

94154
// NewClient creates a client given an io stream and a client config.
@@ -97,6 +157,7 @@ func NewClient(rw io.ReadWriter, config ClientConfig) *Client {
97157
Conn: NewConn(rw),
98158
config: config,
99159
errChan: make(chan error, 1),
160+
caps: make(map[string]cap),
100161
}
101162

102163
// Replace the writer writeCallback with one of our own
@@ -114,6 +175,8 @@ func (c *Client) writeCallback(w *Writer, line string) error {
114175
return err
115176
}
116177

178+
// maybeStartLimiter will start a ticker which will limit how quickly messages
179+
// can be written to the connection if the SendLimit is set in the config.
117180
func (c *Client) maybeStartLimiter(wg *sync.WaitGroup, exiting chan struct{}) {
118181
if c.config.SendLimit == 0 {
119182
return
@@ -147,6 +210,8 @@ func (c *Client) maybeStartLimiter(wg *sync.WaitGroup, exiting chan struct{}) {
147210
}()
148211
}
149212

213+
// maybeStartPingLoop will start a goroutine to send out PING messages at the
214+
// PingFrequency in the config if the frequency is not 0.
150215
func (c *Client) maybeStartPingLoop(wg *sync.WaitGroup, exiting chan struct{}) {
151216
if c.config.PingFrequency <= 0 {
152217
return
@@ -177,6 +242,7 @@ func (c *Client) maybeStartPingLoop(wg *sync.WaitGroup, exiting chan struct{}) {
177242
case data := <-c.incomingPongChan:
178243
// Make sure the pong gets routed to the correct
179244
// goroutine.
245+
180246
c := pingHandlers[data]
181247
delete(pingHandlers, data)
182248

@@ -212,6 +278,51 @@ func (c *Client) handlePing(timestamp int64, pongChan chan struct{}, wg *sync.Wa
212278
}
213279
}
214280

281+
// maybeStartCapHandshake will run a CAP LS and all the relevant CAP REQ
282+
// commands if there are any CAPs requested.
283+
func (c *Client) maybeStartCapHandshake() error {
284+
if len(c.caps) <= 0 {
285+
return nil
286+
}
287+
288+
c.Write("CAP LS")
289+
c.remainingCapResponses = 1 // We count the CAP LS response as a normal response
290+
for key, cap := range c.caps {
291+
if cap.Requested {
292+
c.Writef("CAP REQ :%s", key)
293+
c.remainingCapResponses++
294+
}
295+
}
296+
297+
return nil
298+
}
299+
300+
// CapRequest allows you to request IRCv3 capabilities from the server during
301+
// the handshake. The behavior is undefined if this is called before the
302+
// handshake completes so it is recommended that this be called before Run. If
303+
// the CAP is marked as required, the client will exit if that CAP could not be
304+
// negotiated during the handshake.
305+
func (c *Client) CapRequest(capName string, required bool) {
306+
cap := c.caps[capName]
307+
cap.Requested = true
308+
cap.Required = cap.Required || required
309+
c.caps[capName] = cap
310+
}
311+
312+
// CapEnabled allows you to check if a CAP is enabled for this connection. Note
313+
// that it will not be populated until after the CAP handshake is done, so it is
314+
// recommended to wait to check this until after a message like 001.
315+
func (c *Client) CapEnabled(capName string) bool {
316+
return c.caps[capName].Enabled
317+
}
318+
319+
// CapAvailable allows you to check if a CAP is available on this server. Note
320+
// that it will not be populated until after the CAP handshake is done, so it is
321+
// recommended to wait to check this until after a message like 001.
322+
func (c *Client) CapAvailable(capName string) bool {
323+
return c.caps[capName].Available
324+
}
325+
215326
func (c *Client) sendError(err error) {
216327
select {
217328
case c.errChan <- err:
@@ -237,6 +348,10 @@ func (c *Client) Run() error {
237348
c.Writef("PASS :%s", c.config.Pass)
238349
}
239350

351+
c.maybeStartCapHandshake()
352+
353+
// This feels wrong because it results in CAP LS, CAP REQ, NICK, USER, CAP
354+
// END, but it works and lets us keep the code a bit simpler.
240355
c.Writef("NICK :%s", c.config.Nick)
241356
c.Writef("USER %s 0.0.0.0 0.0.0.0 :%s", c.config.User, c.config.Name)
242357

0 commit comments

Comments
 (0)