commit 7835d39
Hubert Hirtz
·
2021-05-18 19:26:03 +0000 UTC
parent cdab4b7
Fix races conditions Refactor: - Split out reads/writes from irc.Session to irc.ChanInOut, - Message handling is now manual, messages must be passed to irc.Session.HandleMessage for its state to change, - Remove data-race-prone App.addLineNow (called from both the main eventLoop and irc loops) and add App.addStatusLine (to be called from the main event loop) and App.queueStatusLine (to be called from other goroutines). These two functions now write to both the current buffer and the home buffer, - add a irc.Typings.List function that locks the list of typings before accessing it. Changes as I went through the whole code... - CAP handling is fixed (especially CAP DEL and CAP ACK), - irc.Session now handles PREFIX, - unhandled messages are now shown, except for some rare cases where it's completely useless to show them.
9 files changed,
+583,
-799
M
app.go
+241,
-190
1@@ -79,7 +79,7 @@ func NewApp(cfg Config) (app *App, err error) {
2 func (app *App) Close() {
3 app.win.Close()
4 if app.s != nil {
5- app.s.Stop()
6+ app.s.Close()
7 }
8 }
9
10@@ -109,21 +109,8 @@ func (app *App) eventLoop() {
11
12 app.handleEvents(evs)
13 if !app.pasting {
14- app.draw()
15- }
16- }
17-}
18-
19-// handleEvents handles a batch of events.
20-func (app *App) handleEvents(evs []event) {
21- for _, ev := range evs {
22- switch ev.src {
23- case uiEvent:
24- app.handleUIEvent(ev.content.(tcell.Event))
25- case ircEvent:
26- app.handleIRCEvent(ev.content.(irc.Event))
27- default:
28- panic("unreachable")
29+ app.setStatus()
30+ app.win.Draw()
31 }
32 }
33 }
34@@ -131,15 +118,48 @@ func (app *App) handleEvents(evs []event) {
35 // ircLoop maintains a connection to the IRC server by connecting and then
36 // forwarding IRC events to app.events repeatedly.
37 func (app *App) ircLoop() {
38+ var auth irc.SASLClient
39+ if app.cfg.Password != nil {
40+ auth = &irc.SASLPlain{
41+ Username: app.cfg.User,
42+ Password: *app.cfg.Password,
43+ }
44+ }
45+ params := irc.SessionParams{
46+ Nickname: app.cfg.Nick,
47+ Username: app.cfg.User,
48+ RealName: app.cfg.Real,
49+ Auth: auth,
50+ }
51 for !app.win.ShouldExit() {
52- app.connect()
53- for ev := range app.s.Poll() {
54+ conn := app.connect()
55+ in, out := irc.ChanInOut(conn)
56+ if app.cfg.Debug {
57+ out = app.debugOutputMessages(out)
58+ }
59+ session := irc.NewSession(out, params)
60+ app.events <- event{
61+ src: ircEvent,
62+ content: session,
63+ }
64+ for msg := range in {
65+ if app.cfg.Debug {
66+ app.queueStatusLine(ui.Line{
67+ At: time.Now(),
68+ Head: "IN --",
69+ Body: msg.String(),
70+ })
71+ }
72 app.events <- event{
73 src: ircEvent,
74- content: ev,
75+ content: msg,
76 }
77 }
78- app.addLineNow(Home, ui.Line{
79+ app.events <- event{
80+ src: ircEvent,
81+ content: nil,
82+ }
83+ app.queueStatusLine(ui.Line{
84 Head: "!!",
85 HeadColor: ui.ColorRed,
86 Body: "Connection lost",
87@@ -147,17 +167,17 @@ func (app *App) ircLoop() {
88 }
89 }
90
91-func (app *App) connect() {
92+func (app *App) connect() net.Conn {
93 for {
94- app.addLineNow(Home, ui.Line{
95+ app.queueStatusLine(ui.Line{
96 Head: "--",
97 Body: fmt.Sprintf("Connecting to %s...", app.cfg.Addr),
98 })
99- err := app.tryConnect()
100+ conn, err := app.tryConnect()
101 if err == nil {
102- break
103+ return conn
104 }
105- app.addLineNow(Home, ui.Line{
106+ app.queueStatusLine(ui.Line{
107 Head: "!!",
108 HeadColor: ui.ColorRed,
109 Body: fmt.Sprintf("Connection failed: %v", err),
110@@ -166,7 +186,7 @@ func (app *App) connect() {
111 }
112 }
113
114-func (app *App) tryConnect() (err error) {
115+func (app *App) tryConnect() (conn net.Conn, err error) {
116 addr := app.cfg.Addr
117 colonIdx := strings.LastIndexByte(addr, ':')
118 bracketIdx := strings.LastIndexByte(addr, ']')
119@@ -180,7 +200,7 @@ func (app *App) tryConnect() (err error) {
120 }
121 }
122
123- conn, err := net.Dial("tcp", addr)
124+ conn, err = net.Dial("tcp", addr)
125 if err != nil {
126 return
127 }
128@@ -193,28 +213,24 @@ func (app *App) tryConnect() (err error) {
129 })
130 }
131
132- var auth irc.SASLClient
133- if app.cfg.Password != nil {
134- auth = &irc.SASLPlain{
135- Username: app.cfg.User,
136- Password: *app.cfg.Password,
137- }
138- }
139- app.s, err = irc.NewSession(conn, irc.SessionParams{
140- Nickname: app.cfg.Nick,
141- Username: app.cfg.User,
142- RealName: app.cfg.Real,
143- Auth: auth,
144- Debug: app.cfg.Debug,
145- })
146- if err != nil {
147- conn.Close()
148- return
149- }
150-
151 return
152 }
153
154+func (app *App) debugOutputMessages(out chan<- irc.Message) chan<- irc.Message {
155+ debugOut := make(chan irc.Message, cap(out))
156+ go func() {
157+ for msg := range debugOut {
158+ app.queueStatusLine(ui.Line{
159+ At: time.Now(),
160+ Head: "OUT --",
161+ Body: msg.String(),
162+ })
163+ out <- msg
164+ }
165+ }()
166+ return debugOut
167+}
168+
169 // uiLoop retrieves events from the UI and forwards them to app.events for
170 // handling in app.eventLoop().
171 func (app *App) uiLoop() {
172@@ -230,118 +246,34 @@ func (app *App) uiLoop() {
173 }
174 }
175
176-func (app *App) handleIRCEvent(ev irc.Event) {
177- switch ev := ev.(type) {
178- case irc.RawMessageEvent:
179- head := "IN --"
180- if ev.Outgoing {
181- head = "OUT --"
182- } else if !ev.IsValid {
183- head = "IN ??"
184- }
185- app.win.AddLine(Home, false, ui.Line{
186- At: time.Now(),
187- Head: head,
188- Body: ev.Message,
189- })
190- case irc.ErrorEvent:
191- var severity string
192- switch ev.Severity {
193- case irc.SeverityNote:
194- severity = "Note"
195- case irc.SeverityWarn:
196- severity = "Warning"
197- case irc.SeverityFail:
198- severity = "Error"
199- }
200- app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
201- At: time.Now(),
202- Head: "!!",
203- HeadColor: ui.ColorRed,
204- Body: fmt.Sprintf("%s (code %s): %s", severity, ev.Code, ev.Message),
205- })
206- case irc.RegisteredEvent:
207- body := "Connected to the server"
208- if app.s.Nick() != app.cfg.Nick {
209- body += " as " + app.s.Nick()
210- }
211- app.win.AddLine(Home, false, ui.Line{
212- At: time.Now(),
213- Head: "--",
214- Body: body,
215- })
216- case irc.SelfNickEvent:
217- app.win.AddLine(app.win.CurrentBuffer(), true, ui.Line{
218- At: ev.Time,
219- Head: "--",
220- Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, app.s.Nick()),
221- Highlight: true,
222- })
223- case irc.UserNickEvent:
224- for _, c := range app.s.ChannelsSharedWith(ev.User.Name) {
225- app.win.AddLine(c, false, ui.Line{
226- At: ev.Time,
227- Head: "--",
228- Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, ev.User.Name),
229- Mergeable: true,
230- })
231- }
232- case irc.SelfJoinEvent:
233- app.win.AddBuffer(ev.Channel)
234- app.s.RequestHistory(ev.Channel, time.Now())
235- case irc.UserJoinEvent:
236- app.win.AddLine(ev.Channel, false, ui.Line{
237- At: time.Now(),
238- Head: "--",
239- Body: fmt.Sprintf("\x033+\x0314%s\x03", ev.User.Name),
240- Mergeable: true,
241- })
242- case irc.SelfPartEvent:
243- app.win.RemoveBuffer(ev.Channel)
244- case irc.UserPartEvent:
245- app.win.AddLine(ev.Channel, false, ui.Line{
246- At: ev.Time,
247- Head: "--",
248- Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User.Name),
249- Mergeable: true,
250- })
251- case irc.UserQuitEvent:
252- for _, c := range ev.Channels {
253- app.win.AddLine(c, false, ui.Line{
254- At: ev.Time,
255- Head: "--",
256- Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User.Name),
257- Mergeable: true,
258- })
259- }
260- case irc.TopicChangeEvent:
261- app.win.AddLine(ev.Channel, false, ui.Line{
262- At: ev.Time,
263- Head: "--",
264- Body: fmt.Sprintf("\x0314Topic changed to: %s\x03", ev.Topic),
265- })
266- case irc.MessageEvent:
267- buffer, line, hlNotification := app.formatMessage(ev)
268- app.win.AddLine(buffer, hlNotification, line)
269- if hlNotification {
270- app.notifyHighlight(buffer, ev.User.Name, ev.Content)
271- }
272- if !ev.TargetIsChannel && app.s.NickCf() != app.s.Casemap(ev.User.Name) {
273- app.lastQuery = ev.User.Name
274- }
275- case irc.HistoryEvent:
276- var lines []ui.Line
277- for _, m := range ev.Messages {
278- switch m := m.(type) {
279- case irc.MessageEvent:
280- _, line, _ := app.formatMessage(m)
281- lines = append(lines, line)
282- default:
283- }
284+// handleEvents handles a batch of events.
285+func (app *App) handleEvents(evs []event) {
286+ for _, ev := range evs {
287+ switch ev.src {
288+ case uiEvent:
289+ app.handleUIEvent(ev.content)
290+ case ircEvent:
291+ app.handleIRCEvent(ev.content)
292+ default:
293+ panic("unreachable")
294 }
295- app.win.AddLines(ev.Target, lines)
296- case error:
297- panic(ev)
298+ }
299+}
300+
301+func (app *App) handleUIEvent(ev interface{}) {
302+ switch ev := ev.(type) {
303+ case *tcell.EventResize:
304+ app.win.Resize()
305+ case *tcell.EventPaste:
306+ app.pasting = ev.Start()
307+ case *tcell.EventMouse:
308+ app.handleMouseEvent(ev)
309+ case *tcell.EventKey:
310+ app.handleKeyEvent(ev)
311+ case ui.Line:
312+ app.addStatusLine(ev)
313+ default:
314+ return
315 }
316 }
317
318@@ -484,24 +416,6 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
319 }
320 }
321
322-func (app *App) handleUIEvent(ev tcell.Event) {
323- switch ev := ev.(type) {
324- case *tcell.EventResize:
325- app.win.Resize()
326- case *tcell.EventPaste:
327- app.pasting = ev.Start()
328- case *tcell.EventMouse:
329- app.handleMouseEvent(ev)
330- case *tcell.EventKey:
331- app.handleKeyEvent(ev)
332- default:
333- return
334- }
335- if !app.pasting {
336- app.draw()
337- }
338-}
339-
340 // requestHistory is a wrapper around irc.Session.RequestHistory to only request
341 // history when needed.
342 func (app *App) requestHistory() {
343@@ -510,22 +424,159 @@ func (app *App) requestHistory() {
344 }
345 buffer := app.win.CurrentBuffer()
346 if app.win.IsAtTop() && buffer != Home {
347- at := time.Now()
348- if t := app.win.CurrentBufferOldestTime(); t != nil {
349- at = *t
350+ t := time.Now()
351+ if oldest := app.win.CurrentBufferOldestTime(); oldest != nil {
352+ t = *oldest
353 }
354- app.s.RequestHistory(buffer, at)
355+ app.s.NewHistoryRequest(buffer).
356+ WithLimit(100).
357+ Before(t)
358 }
359 }
360
361+func (app *App) handleIRCEvent(ev interface{}) {
362+ if ev == nil {
363+ app.s.Close()
364+ app.s = nil
365+ return
366+ }
367+ if s, ok := ev.(*irc.Session); ok {
368+ app.s = s
369+ return
370+ }
371+
372+ msg := ev.(irc.Message)
373+
374+ // Mutate IRC state
375+ ev = app.s.HandleMessage(msg)
376+
377+ // Mutate UI state
378+ switch ev := ev.(type) {
379+ case irc.RegisteredEvent:
380+ body := "Connected to the server"
381+ if app.s.Nick() != app.cfg.Nick {
382+ body += " as " + app.s.Nick()
383+ }
384+ app.win.AddLine(Home, false, ui.Line{
385+ At: msg.TimeOrNow(),
386+ Head: "--",
387+ Body: body,
388+ })
389+ case irc.SelfNickEvent:
390+ app.win.AddLine(app.win.CurrentBuffer(), true, ui.Line{
391+ At: msg.TimeOrNow(),
392+ Head: "--",
393+ Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, app.s.Nick()),
394+ Highlight: true,
395+ })
396+ case irc.UserNickEvent:
397+ for _, c := range app.s.ChannelsSharedWith(ev.User) {
398+ app.win.AddLine(c, false, ui.Line{
399+ At: msg.TimeOrNow(),
400+ Head: "--",
401+ Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, ev.User),
402+ Mergeable: true,
403+ })
404+ }
405+ case irc.SelfJoinEvent:
406+ app.win.AddBuffer(ev.Channel)
407+ app.s.NewHistoryRequest(ev.Channel).
408+ WithLimit(200).
409+ Before(msg.TimeOrNow())
410+ case irc.UserJoinEvent:
411+ app.win.AddLine(ev.Channel, false, ui.Line{
412+ At: msg.TimeOrNow(),
413+ Head: "--",
414+ Body: fmt.Sprintf("\x033+\x0314%s\x03", ev.User),
415+ Mergeable: true,
416+ })
417+ case irc.SelfPartEvent:
418+ app.win.RemoveBuffer(ev.Channel)
419+ case irc.UserPartEvent:
420+ app.win.AddLine(ev.Channel, false, ui.Line{
421+ At: msg.TimeOrNow(),
422+ Head: "--",
423+ Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User),
424+ Mergeable: true,
425+ })
426+ case irc.UserQuitEvent:
427+ for _, c := range ev.Channels {
428+ app.win.AddLine(c, false, ui.Line{
429+ At: msg.TimeOrNow(),
430+ Head: "--",
431+ Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User),
432+ Mergeable: true,
433+ })
434+ }
435+ case irc.TopicChangeEvent:
436+ app.win.AddLine(ev.Channel, false, ui.Line{
437+ At: msg.TimeOrNow(),
438+ Head: "--",
439+ Body: fmt.Sprintf("\x0314Topic changed to: %s\x03", ev.Topic),
440+ })
441+ case irc.MessageEvent:
442+ buffer, line, hlNotification := app.formatMessage(ev)
443+ app.win.AddLine(buffer, hlNotification, line)
444+ if hlNotification {
445+ app.notifyHighlight(buffer, ev.User, ev.Content)
446+ }
447+ if !app.s.IsChannel(msg.Params[0]) && !app.s.IsMe(ev.User) {
448+ app.lastQuery = msg.Prefix.Name
449+ }
450+ case irc.HistoryEvent:
451+ var lines []ui.Line
452+ for _, m := range ev.Messages {
453+ switch ev := m.(type) {
454+ case irc.MessageEvent:
455+ _, line, _ := app.formatMessage(ev)
456+ lines = append(lines, line)
457+ }
458+ }
459+ app.win.AddLines(ev.Target, lines)
460+ case irc.ErrorEvent:
461+ if isBlackListed(msg.Command) {
462+ break
463+ }
464+ var head string
465+ var body string
466+ switch ev.Severity {
467+ case irc.SeverityFail:
468+ head = "--"
469+ body = fmt.Sprintf("Error (code %s): %s", ev.Code, ev.Message)
470+ case irc.SeverityWarn:
471+ head = "--"
472+ body = fmt.Sprintf("Warning (code %s): %s", ev.Code, ev.Message)
473+ case irc.SeverityNote:
474+ head = ev.Code + " --"
475+ body = ev.Message
476+ default:
477+ panic("unreachable")
478+ }
479+ app.addStatusLine(ui.Line{
480+ At: msg.TimeOrNow(),
481+ Head: head,
482+ Body: body,
483+ })
484+ }
485+}
486+
487+func isBlackListed(command string) bool {
488+ switch command {
489+ case "002", "003", "004", "422":
490+ // useless connection messages
491+ return true
492+ }
493+ return false
494+}
495+
496 // isHighlight reports whether the given message content is a highlight.
497 func (app *App) isHighlight(content string) bool {
498- contentCf := strings.ToLower(content)
499+ contentCf := app.s.Casemap(content)
500 if app.highlights == nil {
501 return strings.Contains(contentCf, app.s.NickCf())
502 }
503 for _, h := range app.highlights {
504- if strings.Contains(contentCf, h) {
505+ if strings.Contains(contentCf, app.s.Casemap(h)) {
506 return true
507 }
508 }
509@@ -556,7 +607,7 @@ func (app *App) notifyHighlight(buffer, nick, content string) {
510 output, err := cmd.CombinedOutput()
511 if err != nil {
512 body := fmt.Sprintf("Failed to invoke on-highlight command: %v. Output: %q", err, string(output))
513- app.win.AddLine(Home, false, ui.Line{
514+ app.addStatusLine(ui.Line{
515 At: time.Now(),
516 Head: "!!",
517 HeadColor: ui.ColorRed,
518@@ -615,7 +666,7 @@ func (app *App) completions(cursorIdx int, text []rune) []ui.Completion {
519 // - the UI line,
520 // - whether senpai must trigger the "on-highlight" command.
521 func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line, hlNotification bool) {
522- isFromSelf := app.s.NickCf() == app.s.Casemap(ev.User.Name)
523+ isFromSelf := app.s.IsMe(ev.User)
524 isHighlight := app.isHighlight(ev.Content)
525 isAction := strings.HasPrefix(ev.Content, "\x01ACTION")
526 isQuery := !ev.TargetIsChannel && ev.Command == "PRIVMSG"
527@@ -632,7 +683,7 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
528 hlLine := ev.TargetIsChannel && isHighlight && !isFromSelf
529 hlNotification = (isHighlight || isQuery) && !isFromSelf
530
531- head := ev.User.Name
532+ head := ev.User
533 headColor := ui.ColorWhite
534 if isFromSelf && isQuery {
535 head = "\u2192 " + ev.Target
536@@ -645,14 +696,14 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
537
538 body := strings.TrimSuffix(ev.Content, "\x01")
539 if isNotice && isAction {
540- c := ircColorSequence(ui.IdentColor(ev.User.Name))
541- body = fmt.Sprintf("(%s%s\x0F:%s)", c, ev.User.Name, body[7:])
542+ c := ircColorSequence(ui.IdentColor(ev.User))
543+ body = fmt.Sprintf("(%s%s\x0F:%s)", c, ev.User, body[7:])
544 } else if isAction {
545- c := ircColorSequence(ui.IdentColor(ev.User.Name))
546- body = fmt.Sprintf("%s%s\x0F%s", c, ev.User.Name, body[7:])
547+ c := ircColorSequence(ui.IdentColor(ev.User))
548+ body = fmt.Sprintf("%s%s\x0F%s", c, ev.User, body[7:])
549 } else if isNotice {
550- c := ircColorSequence(ui.IdentColor(ev.User.Name))
551- body = fmt.Sprintf("(%s%s\x0F: %s)", c, ev.User.Name, body)
552+ c := ircColorSequence(ui.IdentColor(ev.User))
553+ body = fmt.Sprintf("(%s%s\x0F: %s)", c, ev.User, body)
554 }
555
556 line = ui.Line{
+17,
-20
1@@ -56,17 +56,24 @@ func main() {
2 if password != "" {
3 auth = &irc.SASLPlain{Username: nick, Password: password}
4 }
5- cli, err := irc.NewSession(conn, irc.SessionParams{
6+
7+ in, out := irc.ChanInOut(conn)
8+ debugOut := make(chan irc.Message, 64)
9+ go func() {
10+ for msg := range debugOut {
11+ fmt.Fprintf(t, "C > S: %s\n", msg.String())
12+ out <- msg
13+ }
14+ close(out)
15+ }()
16+
17+ cli := irc.NewSession(debugOut, irc.SessionParams{
18 Nickname: nick,
19 Username: nick,
20 RealName: nick,
21 Auth: auth,
22- Debug: true,
23 })
24- if err != nil {
25- panic(fmt.Sprintf("Failed to connect to %s: %v", address, err))
26- }
27- defer cli.Stop()
28+ defer cli.Close()
29
30 go func() {
31 for {
32@@ -76,22 +83,12 @@ func main() {
33 }
34 cli.SendRaw(line)
35 }
36- cli.Stop()
37+ cli.Close()
38 }()
39
40- for ev := range cli.Poll() {
41- switch ev := ev.(type) {
42- case irc.RawMessageEvent:
43- if ev.Outgoing {
44- fmt.Fprintf(t, "C > S: %s\n", ev.Message)
45- } else {
46- fmt.Fprintf(t, "C < S: %s\n", ev.Message)
47- }
48- case error:
49- panic(ev)
50- default:
51- fmt.Fprintf(t, "=EVENT: %T%+v\n", ev, ev)
52- }
53+ for msg := range in {
54+ cli.HandleMessage(msg)
55+ fmt.Fprintf(t, "C < S: %s\n", msg.String())
56 }
57 t.SetPrompt("")
58 fmt.Fprintln(t, "Disconnected")
+5,
-5
1@@ -131,7 +131,7 @@ func commandDo(app *App, buffer string, args []string) (err error) {
2 app.s.PrivMsg(buffer, args[0])
3 if !app.s.HasCapability("echo-message") {
4 buffer, line, _ := app.formatMessage(irc.MessageEvent{
5- User: &irc.Prefix{Name: app.s.Nick()},
6+ User: app.s.Nick(),
7 Target: buffer,
8 TargetIsChannel: true,
9 Command: "PRIVMSG",
10@@ -215,7 +215,7 @@ func commandDoMe(app *App, buffer string, args []string) (err error) {
11 app.s.PrivMsg(buffer, content)
12 if !app.s.HasCapability("echo-message") {
13 buffer, line, _ := app.formatMessage(irc.MessageEvent{
14- User: &irc.Prefix{Name: app.s.Nick()},
15+ User: app.s.Nick(),
16 Target: buffer,
17 TargetIsChannel: true,
18 Command: "PRIVMSG",
19@@ -233,7 +233,7 @@ func commandDoMsg(app *App, buffer string, args []string) (err error) {
20 app.s.PrivMsg(target, content)
21 if !app.s.HasCapability("echo-message") {
22 buffer, line, _ := app.formatMessage(irc.MessageEvent{
23- User: &irc.Prefix{Name: app.s.Nick()},
24+ User: app.s.Nick(),
25 Target: target,
26 TargetIsChannel: true,
27 Command: "PRIVMSG",
28@@ -327,7 +327,7 @@ func commandDoR(app *App, buffer string, args []string) (err error) {
29 app.s.PrivMsg(app.lastQuery, args[0])
30 if !app.s.HasCapability("echo-message") {
31 buffer, line, _ := app.formatMessage(irc.MessageEvent{
32- User: &irc.Prefix{Name: app.s.Nick()},
33+ User: app.s.Nick(),
34 Target: app.lastQuery,
35 TargetIsChannel: true,
36 Command: "PRIVMSG",
37@@ -355,7 +355,7 @@ func commandDoTopic(app *App, buffer string, args []string) (err error) {
38 Body: body,
39 })
40 } else {
41- app.s.SetTopic(buffer, args[0])
42+ app.s.ChangeTopic(buffer, args[0])
43 }
44 return
45 }
+40,
-0
1@@ -0,0 +1,40 @@
2+package irc
3+
4+import (
5+ "bufio"
6+ "fmt"
7+ "net"
8+)
9+
10+const chanCapacity = 64
11+
12+func ChanInOut(conn net.Conn) (in <-chan Message, out chan<- Message) {
13+ in_ := make(chan Message, chanCapacity)
14+ out_ := make(chan Message, chanCapacity)
15+
16+ go func() {
17+ r := bufio.NewScanner(conn)
18+ for r.Scan() {
19+ line := r.Text()
20+ msg, err := ParseMessage(line)
21+ if err != nil {
22+ continue
23+ }
24+ in_ <- msg
25+ }
26+ close(in_)
27+ }()
28+
29+ go func() {
30+ for msg := range out_ {
31+ // TODO send messages by batches
32+ _, err := fmt.Fprintf(conn, "%s\r\n", msg.String())
33+ if err != nil {
34+ break
35+ }
36+ }
37+ _ = conn.Close()
38+ }()
39+
40+ return in_, out_
41+}
+6,
-29
1@@ -1,17 +1,9 @@
2 package irc
3
4-import (
5- "time"
6-)
7+import "time"
8
9 type Event interface{}
10
11-type RawMessageEvent struct {
12- Message string
13- Outgoing bool
14- IsValid bool
15-}
16-
17 type ErrorEvent struct {
18 Severity Severity
19 Code string
20@@ -22,13 +14,11 @@ type RegisteredEvent struct{}
21
22 type SelfNickEvent struct {
23 FormerNick string
24- Time time.Time
25 }
26
27 type UserNickEvent struct {
28- User *Prefix
29+ User string
30 FormerNick string
31- Time time.Time
32 }
33
34 type SelfJoinEvent struct {
35@@ -36,9 +26,8 @@ type SelfJoinEvent struct {
36 }
37
38 type UserJoinEvent struct {
39- User *Prefix
40+ User string
41 Channel string
42- Time time.Time
43 }
44
45 type SelfPartEvent struct {
46@@ -46,26 +35,22 @@ type SelfPartEvent struct {
47 }
48
49 type UserPartEvent struct {
50- User *Prefix
51+ User string
52 Channel string
53- Time time.Time
54 }
55
56 type UserQuitEvent struct {
57- User *Prefix
58+ User string
59 Channels []string
60- Time time.Time
61 }
62
63 type TopicChangeEvent struct {
64- User *Prefix
65 Channel string
66 Topic string
67- Time time.Time
68 }
69
70 type MessageEvent struct {
71- User *Prefix
72+ User string
73 Target string
74 TargetIsChannel bool
75 Command string
76@@ -73,14 +58,6 @@ type MessageEvent struct {
77 Time time.Time
78 }
79
80-type TagEvent struct {
81- User *Prefix
82- Target string
83- TargetIsChannel bool
84- Typing int
85- Time time.Time
86-}
87-
88 type HistoryEvent struct {
89 Target string
90 Messages []Event
R irc/states.go =>
irc/session.go
+225,
-544
1@@ -1,21 +1,17 @@
2 package irc
3
4 import (
5- "bufio"
6 "bytes"
7 "encoding/base64"
8 "errors"
9 "fmt"
10- "net"
11 "strconv"
12 "strings"
13- "sync/atomic"
14 "time"
15+ "unicode"
16 "unicode/utf8"
17 )
18
19-const writeDeadline = 10 * time.Second
20-
21 type SASLClient interface {
22 Handshake() (mech string)
23 Respond(challenge string) (res string, err error)
24@@ -74,62 +70,6 @@ const (
25 TypingDone
26 )
27
28-// action contains the arguments of a user action.
29-//
30-// To keep connection reads and writes in a single coroutine, the library
31-// interface functions like Join("#channel") or PrivMsg("target", "message")
32-// don't interact with the IRC session directly. Instead, they push an action
33-// in the action channel. This action is then processed by the correct
34-// coroutine.
35-type action interface{}
36-
37-type (
38- actionSendRaw struct {
39- raw string
40- }
41-
42- actionChangeNick struct {
43- Nick string
44- }
45- actionChangeMode struct {
46- Channel string
47- Flags string
48- Args []string
49- }
50-
51- actionJoin struct {
52- Channel string
53- }
54- actionPart struct {
55- Channel string
56- Reason string
57- }
58- actionSetTopic struct {
59- Channel string
60- Topic string
61- }
62- actionQuit struct {
63- Reason string
64- }
65-
66- actionPrivMsg struct {
67- Target string
68- Content string
69- }
70-
71- actionTyping struct {
72- Channel string
73- }
74- actionTypingStop struct {
75- Channel string
76- }
77-
78- actionRequestHistory struct {
79- Target string
80- Before time.Time
81- }
82-)
83-
84 // User is a known IRC user (we share a channel with it).
85 type User struct {
86 Name *Prefix // the nick, user and hostname of the user if known.
87@@ -155,20 +95,11 @@ type SessionParams struct {
88 RealName string
89
90 Auth SASLClient
91-
92- Debug bool // whether the Session should report all messages it sends and receive.
93 }
94
95-// Session is an IRC session/connection/whatever.
96 type Session struct {
97- conn net.Conn
98- msgs chan Message // incoming messages.
99- acts chan action // user actions.
100- evts chan Event // events sent to the user.
101-
102- debug bool
103-
104- running atomic.Value // bool
105+ out chan<- Message
106+ closed bool
107 registered bool
108 typings *Typings // incoming typing notifications.
109 typingStamps map[string]time.Time // user typing instants.
110@@ -185,9 +116,12 @@ type Session struct {
111 enabledCaps map[string]struct{}
112
113 // ISUPPORT features
114- casemap func(string) string
115- chantypes string
116- linelen int
117+ casemap func(string) string
118+ chantypes string
119+ linelen int
120+ historyLimit int
121+ prefixSymbols string
122+ prefixModes string
123
124 users map[string]*User // known users.
125 channels map[string]Channel // joined channels.
126@@ -195,18 +129,9 @@ type Session struct {
127 chReqs map[string]struct{} // set of targets for which history is currently requested.
128 }
129
130-// NewSession starts an IRC session from the given connection and session
131-// parameters.
132-//
133-// It returns an error when the paramaters are invalid, or when it cannot write
134-// to the connection.
135-func NewSession(conn net.Conn, params SessionParams) (*Session, error) {
136+func NewSession(out chan<- Message, params SessionParams) *Session {
137 s := &Session{
138- conn: conn,
139- msgs: make(chan Message, 64),
140- acts: make(chan action, 64),
141- evts: make(chan Event, 64),
142- debug: params.Debug,
143+ out: out,
144 typings: NewTypings(),
145 typingStamps: map[string]time.Time{},
146 nick: params.Nickname,
147@@ -219,66 +144,28 @@ func NewSession(conn net.Conn, params SessionParams) (*Session, error) {
148 casemap: CasemapRFC1459,
149 chantypes: "#&",
150 linelen: 512,
151+ historyLimit: 100,
152+ prefixSymbols: "@+",
153+ prefixModes: "ov",
154 users: map[string]*User{},
155 channels: map[string]Channel{},
156 chBatches: map[string]HistoryEvent{},
157 chReqs: map[string]struct{}{},
158 }
159
160- s.running.Store(true)
161-
162- go func() {
163- r := bufio.NewScanner(conn)
164+ s.out <- NewMessage("CAP", "LS", "302")
165+ s.out <- NewMessage("NICK", s.nick)
166+ s.out <- NewMessage("USER", s.user, "0", "*", s.real)
167
168- for r.Scan() {
169- line := r.Text()
170- msg, err := ParseMessage(line)
171- if err != nil {
172- continue
173- }
174- valid := msg.IsValid()
175- if s.debug {
176- s.evts <- RawMessageEvent{Message: line, IsValid: valid}
177- }
178- if valid {
179- s.msgs <- msg
180- }
181- }
182-
183- s.Stop()
184- }()
185-
186- err := s.send("CAP LS 302\r\nNICK %s\r\nUSER %s 0 * :%s\r\n", s.nick, s.user, s.real)
187- if err != nil {
188- return nil, err
189- }
190-
191- go s.run()
192-
193- return s, nil
194-}
195-
196-// Running reports whether we are still connected to the server.
197-func (s *Session) Running() bool {
198- return s.running.Load().(bool)
199+ return s
200 }
201
202-// Stop stops the session and closes the connection.
203-func (s *Session) Stop() {
204- if !s.Running() {
205+func (s *Session) Close() {
206+ if s.closed {
207 return
208 }
209- s.running.Store(false)
210- _ = s.conn.Close()
211- close(s.acts)
212- close(s.evts)
213- close(s.msgs)
214- s.typings.Stop()
215-}
216-
217-// Poll returns the event channel where incoming events are reported.
218-func (s *Session) Poll() (events <-chan Event) {
219- return s.evts
220+ s.closed = true
221+ close(s.out)
222 }
223
224 // HasCapability reports whether the given capability has been negociated
225@@ -297,6 +184,10 @@ func (s *Session) NickCf() string {
226 return s.nickCf
227 }
228
229+func (s *Session) IsMe(nick string) bool {
230+ return s.nickCf == s.casemap(nick)
231+}
232+
233 func (s *Session) IsChannel(name string) bool {
234 return strings.IndexAny(name, s.chantypes) == 0
235 }
236@@ -332,11 +223,14 @@ func (s *Session) Names(channel string) []Member {
237
238 // Typings returns the list of nickname who are currently typing.
239 func (s *Session) Typings(target string) []string {
240- targetCf := s.Casemap(target)
241- var res []string
242- for t := range s.typings.targets {
243- if targetCf == t.Target && s.Casemap(t.Name) != s.NickCf() {
244- res = append(res, s.users[t.Name].Name.Name)
245+ targetCf := s.casemap(target)
246+ res := s.typings.List(targetCf)
247+ for i := 0; i < len(res); i++ {
248+ if s.IsMe(res[i]) {
249+ res = append(res[:i], res[i+1:]...)
250+ i--
251+ } else if u, ok := s.users[res[i]]; ok {
252+ res[i] = u.Name.Name
253 }
254 }
255 return res
256@@ -368,50 +262,34 @@ func (s *Session) Topic(channel string) (topic string, who *Prefix, at time.Time
257 return
258 }
259
260-// SendRaw sends its given argument verbatim to the server.
261 func (s *Session) SendRaw(raw string) {
262- s.acts <- actionSendRaw{raw}
263-}
264-
265-func (s *Session) sendRaw(act actionSendRaw) (err error) {
266- err = s.send("%s\r\n", act.raw)
267- return
268+ s.out <- NewMessage(raw)
269 }
270
271 func (s *Session) Join(channel string) {
272- s.acts <- actionJoin{channel}
273-}
274-
275-func (s *Session) join(act actionJoin) (err error) {
276- err = s.send("JOIN %s\r\n", act.Channel)
277- return
278+ // TODO support keys
279+ s.out <- NewMessage("JOIN", channel)
280 }
281
282 func (s *Session) Part(channel, reason string) {
283- s.acts <- actionPart{channel, reason}
284+ s.out <- NewMessage("PART", channel, reason)
285 }
286
287-func (s *Session) part(act actionPart) (err error) {
288- err = s.send("PART %s :%s\r\n", act.Channel, act.Reason)
289- return
290+func (s *Session) ChangeTopic(channel, topic string) {
291+ s.out <- NewMessage("TOPIC", channel, topic)
292 }
293
294-func (s *Session) SetTopic(channel, topic string) {
295- s.acts <- actionSetTopic{channel, topic}
296-}
297-
298-func (s *Session) setTopic(act actionSetTopic) (err error) {
299- err = s.send("TOPIC %s :%s\r\n", act.Channel, act.Topic)
300- return
301+func (s *Session) Quit(reason string) {
302+ s.out <- NewMessage("QUIT", reason)
303 }
304
305-func (s *Session) Quit(reason string) {
306- s.acts <- actionQuit{reason}
307+func (s *Session) ChangeNick(nick string) {
308+ s.out <- NewMessage("NICK", nick)
309 }
310
311-func (s *Session) quit(act actionQuit) (err error) {
312- err = s.send("QUIT :%s\r\n", act.Reason)
313- return
314+func (s *Session) ChangeMode(channel, flags string, args []string) {
315+ args = append([]string{channel, flags}, args...)
316+ s.out <- NewMessage("MODE", args...)
317 }
318
319 func splitChunks(s string, chunkLen int) (chunks []string) {
320@@ -433,34 +311,7 @@ func splitChunks(s string, chunkLen int) (chunks []string) {
321 return
322 }
323
324-func (s *Session) ChangeNick(nick string) {
325- s.acts <- actionChangeNick{nick}
326-}
327-
328-func (s *Session) changeNick(act actionChangeNick) (err error) {
329- err = s.send("NICK %s\r\n", act.Nick)
330- return
331-}
332-
333-func (s *Session) ChangeMode(channel string, flags string, args []string) {
334- s.acts <- actionChangeMode{channel, flags, args}
335-}
336-
337-func (s *Session) changeMode(act actionChangeMode) (err error) {
338- if strings.IndexAny(act.Channel, s.chantypes) == 0 {
339- err = s.send("MODE %s %s %s\r\n",
340- act.Channel, act.Flags, strings.Join(act.Args, " "))
341- } else {
342- err = s.send("MODE %s %s\r\n", act.Channel, act.Flags)
343- }
344- return
345-}
346-
347 func (s *Session) PrivMsg(target, content string) {
348- s.acts <- actionPrivMsg{target, content}
349-}
350-
351-func (s *Session) privMsg(act actionPrivMsg) (err error) {
352 hostLen := len(s.host)
353 if hostLen == 0 {
354 hostLen = len("255.255.255.255")
355@@ -470,173 +321,115 @@ func (s *Session) privMsg(act actionPrivMsg) (err error) {
356 len(s.nick) -
357 len(s.user) -
358 hostLen -
359- len(act.Target)
360- chunks := splitChunks(act.Content, maxMessageLen)
361+ len(target)
362+ chunks := splitChunks(content, maxMessageLen)
363 for _, chunk := range chunks {
364- err = s.send("PRIVMSG %s :%s\r\n", act.Target, chunk)
365- if err != nil {
366- return
367- }
368+ s.out <- NewMessage("PRIVMSG", target, chunk)
369 }
370- target := s.Casemap(act.Target)
371- delete(s.typingStamps, target)
372- return
373-}
374-
375-func (s *Session) Typing(channel string) {
376- s.acts <- actionTyping{channel}
377+ targetCf := s.Casemap(target)
378+ delete(s.typingStamps, targetCf)
379 }
380
381-func (s *Session) typing(act actionTyping) (err error) {
382- if _, ok := s.enabledCaps["message-tags"]; !ok {
383+func (s *Session) Typing(target string) {
384+ if !s.HasCapability("message-tags") {
385 return
386 }
387-
388- to := s.Casemap(act.Channel)
389+ targetCf := s.casemap(target)
390 now := time.Now()
391-
392- if t, ok := s.typingStamps[to]; ok && now.Sub(t).Seconds() < 3.0 {
393+ if t, ok := s.typingStamps[targetCf]; ok && now.Sub(t).Seconds() < 3.0 {
394 return
395 }
396-
397- s.typingStamps[to] = now
398-
399- err = s.send("@+typing=active TAGMSG %s\r\n", act.Channel)
400- return
401-}
402-
403-func (s *Session) TypingStop(channel string) {
404- s.acts <- actionTypingStop{channel}
405+ s.typingStamps[targetCf] = now
406+ s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "active")
407 }
408
409-func (s *Session) typingStop(act actionTypingStop) (err error) {
410- if _, ok := s.enabledCaps["message-tags"]; !ok {
411+func (s *Session) TypingStop(target string) {
412+ if !s.HasCapability("message-tags") {
413 return
414 }
415+ s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "done")
416+}
417
418- err = s.send("@+typing=done TAGMSG %s\r\n", act.Channel)
419- return
420+type HistoryRequest struct {
421+ s *Session
422+ target string
423+ command string
424+ bounds []string
425+ limit int
426 }
427
428-func (s *Session) RequestHistory(target string, before time.Time) {
429- s.acts <- actionRequestHistory{target, before}
430+func formatTimestamp(t time.Time) string {
431+ return fmt.Sprintf("timestamp=%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
432+ t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
433 }
434
435-func (s *Session) requestHistory(act actionRequestHistory) (err error) {
436- if _, ok := s.enabledCaps["draft/chathistory"]; !ok {
437- return
438+func (r *HistoryRequest) WithLimit(limit int) *HistoryRequest {
439+ if limit < r.s.historyLimit {
440+ r.limit = limit
441+ } else {
442+ r.limit = r.s.historyLimit
443 }
444+ return r
445+}
446
447- target := s.Casemap(act.Target)
448- if _, ok := s.chReqs[target]; ok {
449+func (r *HistoryRequest) doRequest() {
450+ if !r.s.HasCapability("draft/chathistory") {
451 return
452 }
453- s.chReqs[target] = struct{}{}
454
455- t := act.Before.UTC().Add(1 * time.Second)
456- err = s.send("CHATHISTORY BEFORE %s timestamp=%04d-%02d-%02dT%02d:%02d:%02d.%03dZ 100\r\n", act.Target, t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
457+ targetCf := r.s.casemap(r.target)
458+ if _, ok := r.s.chReqs[targetCf]; ok {
459+ return
460+ }
461+ r.s.chReqs[targetCf] = struct{}{}
462
463- return
464+ args := make([]string, 0, len(r.bounds)+3)
465+ args = append(args, r.command)
466+ args = append(args, r.target)
467+ args = append(args, r.bounds...)
468+ args = append(args, strconv.Itoa(r.limit))
469+ r.s.out <- NewMessage("CHATHISTORY", args...)
470 }
471
472-func (s *Session) run() {
473- for s.Running() {
474- var err error
475+func (r *HistoryRequest) Before(t time.Time) {
476+ r.command = "BEFORE"
477+ r.bounds = []string{formatTimestamp(t)}
478+ r.doRequest()
479+}
480
481- select {
482- case act, ok := <-s.acts:
483- if !ok {
484- break
485- }
486- switch act := act.(type) {
487- case actionSendRaw:
488- err = s.sendRaw(act)
489- case actionChangeNick:
490- err = s.changeNick(act)
491- case actionChangeMode:
492- err = s.changeMode(act)
493- case actionJoin:
494- err = s.join(act)
495- case actionPart:
496- err = s.part(act)
497- case actionSetTopic:
498- err = s.setTopic(act)
499- case actionQuit:
500- err = s.quit(act)
501- case actionPrivMsg:
502- err = s.privMsg(act)
503- case actionTyping:
504- err = s.typing(act)
505- case actionTypingStop:
506- err = s.typingStop(act)
507- case actionRequestHistory:
508- err = s.requestHistory(act)
509- }
510- case msg, ok := <-s.msgs:
511- if !ok {
512- break
513- }
514- if s.registered {
515- err = s.handle(msg)
516- } else {
517- err = s.handleStart(msg)
518- }
519- case t, ok := <-s.typings.Stops():
520- if !ok {
521- break
522- }
523- u, ok := s.users[t.Name]
524- if !ok {
525- break
526- }
527- c, ok := s.channels[t.Target]
528- if !ok {
529- break
530- }
531- s.evts <- TagEvent{
532- User: u.Name,
533- Target: c.Name,
534- Typing: TypingDone,
535- Time: time.Now(),
536- }
537- }
538+func (s *Session) NewHistoryRequest(target string) *HistoryRequest {
539+ return &HistoryRequest{
540+ s: s,
541+ target: target,
542+ limit: s.historyLimit,
543+ }
544+}
545
546- if err != nil {
547- s.evts <- err
548- }
549+func (s *Session) HandleMessage(msg Message) Event {
550+ if s.registered {
551+ return s.handleRegistered(msg)
552+ } else {
553+ return s.handleUnregistered(msg)
554 }
555 }
556
557-func (s *Session) handleStart(msg Message) (err error) {
558+func (s *Session) handleUnregistered(msg Message) Event {
559 switch msg.Command {
560 case "AUTHENTICATE":
561 if s.auth != nil {
562- var res string
563-
564- res, err = s.auth.Respond(msg.Params[0])
565+ res, err := s.auth.Respond(msg.Params[0])
566 if err != nil {
567- err = s.send("AUTHENTICATE *\r\n")
568- return
569- }
570-
571- err = s.send("AUTHENTICATE %s\r\n", res)
572- if err != nil {
573- return
574+ s.out <- NewMessage("AUTHENTICATE", "*")
575+ } else {
576+ s.out <- NewMessage("AUTHENTICATE", res)
577 }
578 }
579 case rplLoggedin:
580- err = s.send("CAP END\r\n")
581- if err != nil {
582- return
583- }
584-
585+ s.out <- NewMessage("CAP", "END")
586 s.acct = msg.Params[2]
587 s.host = ParsePrefix(msg.Params[1]).Host
588 case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs:
589- err = s.send("CAP END\r\n")
590- if err != nil {
591- return
592- }
593+ s.out <- NewMessage("CAP", "END")
594 case "CAP":
595 switch msg.Params[1] {
596 case "LS":
597@@ -652,57 +445,44 @@ func (s *Session) handleStart(msg Message) (err error) {
598 }
599
600 for _, c := range ParseCaps(ls) {
601- if c.Enable {
602- s.availableCaps[c.Name] = c.Value
603- } else {
604- delete(s.availableCaps, c.Name)
605- }
606+ s.availableCaps[c.Name] = c.Value
607 }
608
609 if !willContinue {
610- var req strings.Builder
611-
612 for c := range s.availableCaps {
613 if _, ok := SupportedCapabilities[c]; !ok {
614 continue
615 }
616-
617- _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c)
618+ s.out <- NewMessage("CAP", "REQ", c)
619 }
620
621 _, ok := s.availableCaps["sasl"]
622 if s.auth == nil || !ok {
623- _, _ = fmt.Fprintf(&req, "CAP END\r\n")
624- }
625-
626- err = s.send(req.String())
627- if err != nil {
628- return
629+ s.out <- NewMessage("CAP", "END")
630 }
631 }
632 default:
633- s.handle(msg)
634+ return s.handleRegistered(msg)
635 }
636 case errNicknameinuse:
637- err = s.send("NICK %s_\r\n", msg.Params[1])
638- if err != nil {
639- return
640- }
641+ s.out <- NewMessage("NICK", msg.Params[1]+"_")
642+ case rplSaslsuccess:
643+ // do nothing
644 default:
645- err = s.handle(msg)
646+ return s.handleRegistered(msg)
647 }
648-
649- return
650+ return nil
651 }
652
653-func (s *Session) handle(msg Message) (err error) {
654+func (s *Session) handleRegistered(msg Message) Event {
655 if id, ok := msg.Tags["batch"]; ok {
656 if b, ok := s.chBatches[id]; ok {
657+ ev := s.newMessageEvent(msg)
658 s.chBatches[id] = HistoryEvent{
659 Target: b.Target,
660- Messages: append(b.Messages, s.privmsgToEvent(msg)),
661+ Messages: append(b.Messages, ev),
662 }
663- return
664+ return nil
665 }
666 }
667
668@@ -714,14 +494,10 @@ func (s *Session) handle(msg Message) (err error) {
669 s.users[s.nickCf] = &User{Name: &Prefix{
670 Name: s.nick, User: s.user, Host: s.host,
671 }}
672- s.evts <- RegisteredEvent{}
673-
674 if s.host == "" {
675- err = s.send("WHO %s\r\n", s.nick)
676- if err != nil {
677- return
678- }
679+ s.out <- NewMessage("WHO", s.nick)
680 }
681+ return RegisteredEvent{}
682 case rplIsupport:
683 s.updateFeatures(msg.Params[1 : len(msg.Params)-1])
684 case rplWhoreply:
685@@ -731,106 +507,49 @@ func (s *Session) handle(msg Message) (err error) {
686 case "CAP":
687 switch msg.Params[1] {
688 case "ACK":
689- for _, c := range strings.Split(msg.Params[2], " ") {
690- s.enabledCaps[c] = struct{}{}
691+ for _, c := range ParseCaps(msg.Params[2]) {
692+ if c.Enable {
693+ s.enabledCaps[c.Name] = struct{}{}
694+ } else {
695+ delete(s.enabledCaps, c.Name)
696+ }
697
698- if s.auth != nil && c == "sasl" {
699+ if s.auth != nil && c.Name == "sasl" {
700 h := s.auth.Handshake()
701- err = s.send("AUTHENTICATE %s\r\n", h)
702- if err != nil {
703- return
704- }
705- } else if len(s.channels) != 0 && c == "multi-prefix" {
706+ s.out <- NewMessage("AUTHENTICATE", h)
707+ } else if len(s.channels) != 0 && c.Name == "multi-prefix" {
708 // TODO merge NAMES commands
709- var sb strings.Builder
710- sb.Grow(512)
711- for _, c := range s.channels {
712- sb.WriteString("NAMES ")
713- sb.WriteString(c.Name)
714- sb.WriteString("\r\n")
715- }
716- err = s.send(sb.String())
717- if err != nil {
718- return
719+ for channel := range s.channels {
720+ s.out <- NewMessage("NAMES", channel)
721 }
722 }
723 }
724 case "NAK":
725- for _, c := range strings.Split(msg.Params[2], " ") {
726- delete(s.enabledCaps, c)
727- }
728+ // do nothing
729 case "NEW":
730- diff := ParseCaps(msg.Params[2])
731-
732- for _, c := range diff {
733- if c.Enable {
734- s.availableCaps[c.Name] = c.Value
735- } else {
736- delete(s.availableCaps, c.Name)
737- }
738- }
739-
740- var req strings.Builder
741-
742- for _, c := range diff {
743+ for _, c := range ParseCaps(msg.Params[2]) {
744+ s.availableCaps[c.Name] = c.Value
745 _, ok := SupportedCapabilities[c.Name]
746- if !c.Enable || !ok {
747+ if !ok {
748 continue
749 }
750-
751- _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c.Name)
752+ s.out <- NewMessage("CAP", "REQ", c.Name)
753 }
754
755 _, ok := s.availableCaps["sasl"]
756 if s.acct == "" && ok {
757 // TODO authenticate
758 }
759-
760- err = s.send(req.String())
761- if err != nil {
762- return
763- }
764 case "DEL":
765- diff := ParseCaps(msg.Params[2])
766-
767- for i := range diff {
768- diff[i].Enable = !diff[i].Enable
769- }
770-
771- for _, c := range diff {
772- if c.Enable {
773- s.availableCaps[c.Name] = c.Value
774- } else {
775- delete(s.availableCaps, c.Name)
776- }
777- }
778-
779- var req strings.Builder
780-
781- for _, c := range diff {
782- _, ok := SupportedCapabilities[c.Name]
783- if !c.Enable || !ok {
784- continue
785- }
786-
787- _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c.Name)
788- }
789-
790- _, ok := s.availableCaps["sasl"]
791- if s.acct == "" && ok {
792- // TODO authenticate
793- }
794-
795- err = s.send(req.String())
796- if err != nil {
797- return
798+ for _, c := range ParseCaps(msg.Params[2]) {
799+ delete(s.availableCaps, c.Name)
800+ delete(s.enabledCaps, c.Name)
801 }
802 }
803 case "JOIN":
804 nickCf := s.Casemap(msg.Prefix.Name)
805 channelCf := s.Casemap(msg.Params[0])
806-
807- if nickCf == s.nickCf {
808+ if s.IsMe(msg.Prefix.Name) {
809 s.channels[channelCf] = Channel{
810 Name: msg.Params[0],
811 Members: map[*User]string{},
812@@ -840,61 +559,56 @@ func (s *Session) handle(msg Message) (err error) {
813 s.users[nickCf] = &User{Name: msg.Prefix.Copy()}
814 }
815 c.Members[s.users[nickCf]] = ""
816- t := msg.TimeOrNow()
817-
818- s.evts <- UserJoinEvent{
819- User: msg.Prefix.Copy(),
820+ return UserJoinEvent{
821+ User: msg.Prefix.Name,
822 Channel: c.Name,
823- Time: t,
824 }
825 }
826 case "PART":
827 nickCf := s.Casemap(msg.Prefix.Name)
828 channelCf := s.Casemap(msg.Params[0])
829-
830- if nickCf == s.nickCf {
831+ if s.IsMe(msg.Prefix.Name) {
832 if c, ok := s.channels[channelCf]; ok {
833 delete(s.channels, channelCf)
834 for u := range c.Members {
835 s.cleanUser(u)
836 }
837- s.evts <- SelfPartEvent{Channel: c.Name}
838+ return SelfPartEvent{
839+ Channel: c.Name,
840+ }
841 }
842 } else if c, ok := s.channels[channelCf]; ok {
843 if u, ok := s.users[nickCf]; ok {
844 delete(c.Members, u)
845 s.cleanUser(u)
846 s.typings.Done(channelCf, nickCf)
847-
848- s.evts <- UserPartEvent{
849- User: msg.Prefix.Copy(),
850+ return UserPartEvent{
851+ User: u.Name.Name,
852 Channel: c.Name,
853- Time: msg.TimeOrNow(),
854 }
855 }
856 }
857 case "KICK":
858- channelCf := s.Casemap(msg.Params[0])
859 nickCf := s.Casemap(msg.Params[1])
860-
861- if nickCf == s.nickCf {
862+ channelCf := s.Casemap(msg.Params[0])
863+ if s.IsMe(msg.Prefix.Name) {
864 if c, ok := s.channels[channelCf]; ok {
865 delete(s.channels, channelCf)
866 for u := range c.Members {
867 s.cleanUser(u)
868 }
869- s.evts <- SelfPartEvent{Channel: c.Name}
870+ return SelfPartEvent{
871+ Channel: c.Name,
872+ }
873 }
874 } else if c, ok := s.channels[channelCf]; ok {
875 if u, ok := s.users[nickCf]; ok {
876 delete(c.Members, u)
877 s.cleanUser(u)
878 s.typings.Done(channelCf, nickCf)
879-
880- s.evts <- UserPartEvent{
881- User: u.Name.Copy(),
882+ return UserPartEvent{
883+ User: u.Name.Name,
884 Channel: c.Name,
885- Time: msg.TimeOrNow(),
886 }
887 }
888 }
889@@ -911,11 +625,9 @@ func (s *Session) handle(msg Message) (err error) {
890 s.typings.Done(channelCf, nickCf)
891 }
892 }
893-
894- s.evts <- UserQuitEvent{
895- User: msg.Prefix.Copy(),
896+ return UserQuitEvent{
897+ User: u.Name.Name,
898 Channels: channels,
899- Time: msg.TimeOrNow(),
900 }
901 }
902 case rplNamreply:
903@@ -924,8 +636,7 @@ func (s *Session) handle(msg Message) (err error) {
904 if c, ok := s.channels[channelCf]; ok {
905 c.Secret = msg.Params[1] == "@"
906
907- // TODO compute CHANTYPES
908- for _, name := range ParseNameReply(msg.Params[3], "~&@%+") {
909+ for _, name := range ParseNameReply(msg.Params[3], s.prefixSymbols) {
910 nickCf := s.Casemap(name.Name.Name)
911
912 if _, ok := s.users[nickCf]; !ok {
913@@ -941,7 +652,9 @@ func (s *Session) handle(msg Message) (err error) {
914 if c, ok := s.channels[channelCf]; ok && !c.complete {
915 c.complete = true
916 s.channels[channelCf] = c
917- s.evts <- SelfJoinEvent{Channel: c.Name}
918+ return SelfJoinEvent{
919+ Channel: c.Name,
920+ }
921 }
922 case rplTopic:
923 channelCf := s.Casemap(msg.Params[1])
924@@ -970,51 +683,34 @@ func (s *Session) handle(msg Message) (err error) {
925 c.TopicWho = msg.Prefix.Copy()
926 c.TopicTime = msg.TimeOrNow()
927 s.channels[channelCf] = c
928- s.evts <- TopicChangeEvent{
929- User: msg.Prefix.Copy(),
930+ return TopicChangeEvent{
931 Channel: c.Name,
932 Topic: c.Topic,
933- Time: c.TopicTime,
934 }
935 }
936 case "PRIVMSG", "NOTICE":
937- s.evts <- s.privmsgToEvent(msg)
938+ targetCf := s.casemap(msg.Params[0])
939+ nickCf := s.casemap(msg.Prefix.Name)
940+ s.typings.Done(targetCf, nickCf)
941+ return s.newMessageEvent(msg)
942 case "TAGMSG":
943 nickCf := s.Casemap(msg.Prefix.Name)
944 targetCf := s.Casemap(msg.Params[0])
945
946- if nickCf == s.nickCf {
947+ if s.IsMe(msg.Prefix.Name) {
948 // TAGMSG from self
949 break
950 }
951
952- typing := TypingUnspec
953 if t, ok := msg.Tags["+typing"]; ok {
954 if t == "active" {
955- typing = TypingActive
956 s.typings.Active(targetCf, nickCf)
957 } else if t == "paused" {
958- typing = TypingPaused
959- s.typings.Active(targetCf, nickCf)
960+ s.typings.Done(targetCf, nickCf)
961 } else if t == "done" {
962- typing = TypingDone
963 s.typings.Done(targetCf, nickCf)
964 }
965- } else {
966- break
967- }
968-
969- ev := TagEvent{
970- User: msg.Prefix.Copy(), // TODO correctly casemap
971- Target: msg.Params[0], // TODO correctly casemap
972- Typing: typing,
973- Time: msg.TimeOrNow(),
974 }
975- if c, ok := s.channels[targetCf]; ok {
976- ev.Target = c.Name
977- ev.TargetIsChannel = true
978- }
979- s.evts <- ev
980 case "BATCH":
981 batchStart := msg.Params[0][0] == '+'
982 id := msg.Params[0][1:]
983@@ -1022,90 +718,74 @@ func (s *Session) handle(msg Message) (err error) {
984 if batchStart && msg.Params[1] == "chathistory" {
985 s.chBatches[id] = HistoryEvent{Target: msg.Params[2]}
986 } else if b, ok := s.chBatches[id]; ok {
987- s.evts <- b
988 delete(s.chBatches, id)
989 delete(s.chReqs, s.Casemap(b.Target))
990+ return b
991 }
992 case "NICK":
993 nickCf := s.Casemap(msg.Prefix.Name)
994 newNick := msg.Params[0]
995 newNickCf := s.Casemap(newNick)
996- t := msg.TimeOrNow()
997
998- var u *Prefix
999 if formerUser, ok := s.users[nickCf]; ok {
1000 formerUser.Name.Name = newNick
1001 delete(s.users, nickCf)
1002 s.users[newNickCf] = formerUser
1003- u = formerUser.Name.Copy()
1004 } else {
1005 break
1006 }
1007
1008- if nickCf == s.nickCf {
1009- s.evts <- SelfNickEvent{
1010- FormerNick: s.nick,
1011- Time: t,
1012- }
1013+ if s.IsMe(msg.Prefix.Name) {
1014 s.nick = newNick
1015 s.nickCf = newNickCf
1016+ return SelfNickEvent{
1017+ FormerNick: msg.Prefix.Name,
1018+ }
1019 } else {
1020- s.evts <- UserNickEvent{
1021- User: u,
1022+ return UserNickEvent{
1023+ User: msg.Params[0],
1024 FormerNick: msg.Prefix.Name,
1025- Time: t,
1026 }
1027 }
1028+ case "PING":
1029+ s.out <- NewMessage("PONG", msg.Params[0])
1030+ case "ERROR":
1031+ s.Close()
1032 case "FAIL":
1033- s.evts <- ErrorEvent{
1034+ return ErrorEvent{
1035 Severity: SeverityFail,
1036 Code: msg.Params[1],
1037- Message: msg.Params[len(msg.Params)-1],
1038+ Message: strings.Join(msg.Params[2:], " "),
1039 }
1040 case "WARN":
1041- s.evts <- ErrorEvent{
1042+ return ErrorEvent{
1043 Severity: SeverityWarn,
1044 Code: msg.Params[1],
1045- Message: msg.Params[len(msg.Params)-1],
1046+ Message: strings.Join(msg.Params[2:], " "),
1047 }
1048 case "NOTE":
1049- s.evts <- ErrorEvent{
1050+ return ErrorEvent{
1051 Severity: SeverityNote,
1052 Code: msg.Params[1],
1053- Message: msg.Params[len(msg.Params)-1],
1054- }
1055- case "PING":
1056- err = s.send("PONG :%s\r\n", msg.Params[0])
1057- if err != nil {
1058- return
1059- }
1060- case "ERROR":
1061- err = errors.New("connection terminated")
1062- if len(msg.Params) > 0 {
1063- err = fmt.Errorf("connection terminated: %s", msg.Params[0])
1064+ Message: strings.Join(msg.Params[2:], " "),
1065 }
1066- _ = s.conn.Close()
1067 default:
1068- // reply handling
1069- if ReplySeverity(msg.Command) == SeverityFail {
1070- s.evts <- ErrorEvent{
1071- Severity: SeverityFail,
1072+ if msg.IsReply() {
1073+ return ErrorEvent{
1074+ Severity: ReplySeverity(msg.Command),
1075 Code: msg.Command,
1076- Message: msg.Params[len(msg.Params)-1],
1077+ Message: strings.Join(msg.Params[1:], " "),
1078 }
1079 }
1080 }
1081-
1082- return
1083+ return nil
1084 }
1085
1086-func (s *Session) privmsgToEvent(msg Message) (ev MessageEvent) {
1087+func (s *Session) newMessageEvent(msg Message) MessageEvent {
1088 targetCf := s.Casemap(msg.Params[0])
1089-
1090- s.typings.Done(targetCf, s.Casemap(msg.Prefix.Name))
1091- ev = MessageEvent{
1092- User: msg.Prefix.Copy(), // TODO correctly casemap
1093- Target: msg.Params[0], // TODO correctly casemap
1094+ ev := MessageEvent{
1095+ User: msg.Prefix.Name, // TODO correctly casemap
1096+ Target: msg.Params[0], // TODO correctly casemap
1097 Command: msg.Command,
1098 Content: msg.Params[1],
1099 Time: msg.TimeOrNow(),
1100@@ -1114,8 +794,7 @@ func (s *Session) privmsgToEvent(msg Message) (ev MessageEvent) {
1101 ev.Target = c.Name
1102 ev.TargetIsChannel = true
1103 }
1104-
1105- return
1106+ return ev
1107 }
1108
1109 func (s *Session) cleanUser(parted *User) {
1110@@ -1157,6 +836,7 @@ func (s *Session) updateFeatures(features []string) {
1111 continue
1112 }
1113
1114+ Switch:
1115 switch key {
1116 case "CASEMAPPING":
1117 switch value {
1118@@ -1167,31 +847,32 @@ func (s *Session) updateFeatures(features []string) {
1119 }
1120 case "CHANTYPES":
1121 s.chantypes = value
1122+ case "CHATHISTORY":
1123+ historyLimit, err := strconv.Atoi(value)
1124+ if err == nil {
1125+ s.historyLimit = historyLimit
1126+ }
1127 case "LINELEN":
1128 linelen, err := strconv.Atoi(value)
1129 if err == nil && linelen != 0 {
1130 s.linelen = linelen
1131 }
1132- }
1133- }
1134-}
1135-
1136-func (s *Session) send(format string, args ...interface{}) (err error) {
1137- msg := fmt.Sprintf(format, args...)
1138-
1139- s.conn.SetWriteDeadline(time.Now().Add(writeDeadline))
1140- _, err = s.conn.Write([]byte(msg))
1141-
1142- if s.debug {
1143- for _, line := range strings.Split(msg, "\r\n") {
1144- if line != "" {
1145- s.evts <- RawMessageEvent{
1146- Message: line,
1147- Outgoing: true,
1148+ case "PREFIX":
1149+ if value == "" {
1150+ s.prefixModes = ""
1151+ s.prefixSymbols = ""
1152+ }
1153+ if len(value)%2 != 0 {
1154+ break Switch
1155+ }
1156+ for i := 0; i < len(value); i++ {
1157+ if unicode.MaxASCII < value[i] {
1158+ break Switch
1159 }
1160 }
1161+ numPrefixes := len(value)/2 - 1
1162+ s.prefixModes = value[1 : numPrefixes+1]
1163+ s.prefixSymbols = value[numPrefixes+2:]
1164 }
1165 }
1166-
1167- return
1168 }
+21,
-3
1@@ -229,6 +229,10 @@ type Message struct {
2 Params []string
3 }
4
5+func NewMessage(command string, params ...string) Message {
6+ return Message{Command: command, Params: params}
7+}
8+
9 // ParseMessage parses the message from the given string, which must be trimmed
10 // of "\r\n" beforehand.
11 func ParseMessage(line string) (msg Message, err error) {
12@@ -282,6 +286,14 @@ func ParseMessage(line string) (msg Message, err error) {
13 return
14 }
15
16+func (msg Message) WithTag(key, value string) Message {
17+ if msg.Tags == nil {
18+ msg.Tags = map[string]string{}
19+ }
20+ msg.Tags[key] = escapeTagValue(value)
21+ return msg
22+}
23+
24 // IsReply reports whether the message command is a server reply.
25 func (msg *Message) IsReply() bool {
26 if len(msg.Command) != 3 {
27@@ -326,9 +338,15 @@ func (msg *Message) String() string {
28 sb.WriteRune(' ')
29 sb.WriteString(p)
30 }
31- sb.WriteRune(' ')
32- sb.WriteRune(':')
33- sb.WriteString(msg.Params[len(msg.Params)-1])
34+ lastParam := msg.Params[len(msg.Params)-1]
35+ if !strings.ContainsRune(lastParam, ' ') && !strings.HasPrefix(lastParam, ":") {
36+ sb.WriteRune(' ')
37+ sb.WriteString(lastParam)
38+ } else {
39+ sb.WriteRune(' ')
40+ sb.WriteRune(':')
41+ sb.WriteString(lastParam)
42+ }
43 }
44
45 return sb.String()
+13,
-0
1@@ -73,3 +73,16 @@ func (ts *Typings) Done(target, name string) {
2 delete(ts.targets, Typing{target, name})
3 ts.l.Unlock()
4 }
5+
6+func (ts *Typings) List(target string) []string {
7+ ts.l.Lock()
8+ defer ts.l.Unlock()
9+
10+ var res []string
11+ for t := range ts.targets {
12+ if target == t.Target {
13+ res = append(res, t.Name)
14+ }
15+ }
16+ return res
17+}
+15,
-8
1@@ -22,28 +22,35 @@ var homeMessages = []string{
2 func (app *App) initWindow() {
3 hmIdx := rand.Intn(len(homeMessages))
4 app.win.AddBuffer(Home)
5- app.addLineNow("", ui.Line{
6+ app.win.AddLine(Home, false, ui.Line{
7 Head: "--",
8 Body: homeMessages[hmIdx],
9+ At: time.Now(),
10 })
11 }
12
13-func (app *App) addLineNow(buffer string, line ui.Line) {
14+func (app *App) queueStatusLine(line ui.Line) {
15 if line.At.IsZero() {
16 line.At = time.Now()
17 }
18- app.win.AddLine(buffer, false, line)
19- app.draw()
20+ app.events <- event{
21+ src: uiEvent,
22+ content: line,
23+ }
24 }
25
26-func (app *App) draw() {
27- if app.s != nil {
28- app.setStatus()
29+func (app *App) addStatusLine(line ui.Line) {
30+ buffer := app.win.CurrentBuffer()
31+ if buffer != Home {
32+ app.win.AddLine(Home, false, line)
33 }
34- app.win.Draw()
35+ app.win.AddLine(buffer, false, line)
36 }
37
38 func (app *App) setStatus() {
39+ if app.s == nil {
40+ return
41+ }
42 ts := app.s.Typings(app.win.CurrentBuffer())
43 status := ""
44 if 3 < len(ts) {