commit 992fcfa

Hubert Hirtz  ·  2021-10-23 11:13:52 +0000 UTC
parent 2aab421
Don't expect the server to send correct IRC messages

Instead of panicking, print an error in the home buffer
3 files changed,  +305, -157
M app.go
M app.go
+9, -1
 1@@ -527,7 +527,15 @@ func (app *App) handleIRCEvent(ev interface{}) {
 2 	msg := ev.(irc.Message)
 3 
 4 	// Mutate IRC state
 5-	ev = app.s.HandleMessage(msg)
 6+	ev, err := app.s.HandleMessage(msg)
 7+	if err != nil {
 8+		app.win.AddLine(Home, ui.NotifyUnread, ui.Line{
 9+			Head:      "!!",
10+			HeadColor: tcell.ColorRed,
11+			Body:      ui.PlainSprintf("Received corrupt message %q: %s", msg.String(), err),
12+		})
13+		return
14+	}
15 
16 	// Mutate UI state
17 	switch ev := ev.(type) {
+283, -98
  1@@ -82,7 +82,6 @@ type Channel struct {
  2 	Topic     string           // the topic of the channel, or "" if absent.
  3 	TopicWho  *Prefix          // the name of the last user who set the topic.
  4 	TopicTime time.Time        // the last time the topic has been changed.
  5-	Secret    bool             // whether the channel is on the server channel list.
  6 
  7 	complete bool // whether this structure is fully initialized.
  8 }
  9@@ -450,7 +449,7 @@ func (s *Session) NewHistoryRequest(target string) *HistoryRequest {
 10 	}
 11 }
 12 
 13-func (s *Session) HandleMessage(msg Message) Event {
 14+func (s *Session) HandleMessage(msg Message) (Event, error) {
 15 	if s.registered {
 16 		return s.handleRegistered(msg)
 17 	} else {
 18@@ -458,35 +457,53 @@ func (s *Session) HandleMessage(msg Message) Event {
 19 	}
 20 }
 21 
 22-func (s *Session) handleUnregistered(msg Message) Event {
 23+func (s *Session) handleUnregistered(msg Message) (Event, error) {
 24 	switch msg.Command {
 25 	case "AUTHENTICATE":
 26-		if s.auth != nil {
 27-			res, err := s.auth.Respond(msg.Params[0])
 28-			if err != nil {
 29-				s.out <- NewMessage("AUTHENTICATE", "*")
 30-			} else {
 31-				s.out <- NewMessage("AUTHENTICATE", res)
 32-			}
 33+		if s.auth == nil {
 34+			break
 35+		}
 36+
 37+		var payload string
 38+		if err := msg.ParseParams(&payload); err != nil {
 39+			return nil, err
 40+		}
 41+
 42+		res, err := s.auth.Respond(payload)
 43+		if err != nil {
 44+			s.out <- NewMessage("AUTHENTICATE", "*")
 45+		} else {
 46+			s.out <- NewMessage("AUTHENTICATE", res)
 47 		}
 48 	case rplLoggedin:
 49+		var userhost string
 50+		if err := msg.ParseParams(nil, &userhost, &s.acct); err != nil {
 51+			return nil, err
 52+		}
 53+
 54 		s.out <- NewMessage("CAP", "END")
 55-		s.acct = msg.Params[2]
 56-		s.host = ParsePrefix(msg.Params[1]).Host
 57+		s.host = ParsePrefix(userhost).Host
 58 	case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs:
 59 		s.out <- NewMessage("CAP", "END")
 60 	case "CAP":
 61-		switch msg.Params[1] {
 62+		var subcommand string
 63+		if err := msg.ParseParams(nil, &subcommand); err != nil {
 64+			return nil, err
 65+		}
 66+
 67+		switch subcommand {
 68 		case "LS":
 69-			var willContinue bool
 70 			var ls string
 71+			if err := msg.ParseParams(nil, nil, &ls); err != nil {
 72+				return nil, err
 73+			}
 74 
 75-			if msg.Params[2] == "*" {
 76+			willContinue := false
 77+			if ls == "*" {
 78+				if err := msg.ParseParams(nil, nil, nil, &ls); err != nil {
 79+					return nil, err
 80+				}
 81 				willContinue = true
 82-				ls = msg.Params[3]
 83-			} else {
 84-				willContinue = false
 85-				ls = msg.Params[2]
 86 			}
 87 
 88 			for _, c := range ParseCaps(ls) {
 89@@ -510,30 +527,41 @@ func (s *Session) handleUnregistered(msg Message) Event {
 90 			return s.handleRegistered(msg)
 91 		}
 92 	case errNicknameinuse:
 93-		s.out <- NewMessage("NICK", msg.Params[1]+"_")
 94+		var nick string
 95+		if err := msg.ParseParams(nil, &nick); err != nil {
 96+			return nil, err
 97+		}
 98+
 99+		s.out <- NewMessage("NICK", nick+"_")
100 	case rplSaslsuccess:
101 		// do nothing
102 	default:
103 		return s.handleRegistered(msg)
104 	}
105-	return nil
106+	return nil, nil
107 }
108 
109-func (s *Session) handleRegistered(msg Message) Event {
110+func (s *Session) handleRegistered(msg Message) (Event, error) {
111 	if id, ok := msg.Tags["batch"]; ok {
112 		if b, ok := s.chBatches[id]; ok {
113-			ev := s.newMessageEvent(msg)
114+			ev, err := s.newMessageEvent(msg)
115+			if err != nil {
116+				return nil, err
117+			}
118 			s.chBatches[id] = HistoryEvent{
119 				Target:   b.Target,
120 				Messages: append(b.Messages, ev),
121 			}
122-			return nil
123+			return nil, nil
124 		}
125 	}
126 
127 	switch msg.Command {
128 	case rplWelcome:
129-		s.nick = msg.Params[0]
130+		if err := msg.ParseParams(&s.nick); err != nil {
131+			return nil, err
132+		}
133+
134 		s.nickCf = s.Casemap(s.nick)
135 		s.registered = true
136 		s.users[s.nickCf] = &User{Name: &Prefix{
137@@ -542,17 +570,30 @@ func (s *Session) handleRegistered(msg Message) Event {
138 		if s.host == "" {
139 			s.out <- NewMessage("WHO", s.nick)
140 		}
141-		return RegisteredEvent{}
142+		return RegisteredEvent{}, nil
143 	case rplIsupport:
144+		if len(msg.Params) < 3 {
145+			return nil, msg.errNotEnoughParams(3)
146+		}
147 		s.updateFeatures(msg.Params[1 : len(msg.Params)-1])
148 	case rplWhoreply:
149-		if s.nickCf == s.Casemap(msg.Params[5]) {
150-			s.host = msg.Params[3]
151+		var nick, host string
152+		if err := msg.ParseParams(nil, nil, nil, &host, nil, &nick); err != nil {
153+			return nil, err
154+		}
155+
156+		if s.nickCf == s.Casemap(nick) {
157+			s.host = host
158 		}
159 	case "CAP":
160-		switch msg.Params[1] {
161+		var subcommand, caps string
162+		if err := msg.ParseParams(nil, &subcommand, &caps); err != nil {
163+			return nil, err
164+		}
165+
166+		switch subcommand {
167 		case "ACK":
168-			for _, c := range ParseCaps(msg.Params[2]) {
169+			for _, c := range ParseCaps(caps) {
170 				if c.Enable {
171 					s.enabledCaps[c.Name] = struct{}{}
172 				} else {
173@@ -572,7 +613,7 @@ func (s *Session) handleRegistered(msg Message) Event {
174 		case "NAK":
175 			// do nothing
176 		case "NEW":
177-			for _, c := range ParseCaps(msg.Params[2]) {
178+			for _, c := range ParseCaps(caps) {
179 				s.availableCaps[c.Name] = c.Value
180 				_, ok := SupportedCapabilities[c.Name]
181 				if !ok {
182@@ -586,15 +627,25 @@ func (s *Session) handleRegistered(msg Message) Event {
183 				// TODO authenticate
184 			}
185 		case "DEL":
186-			for _, c := range ParseCaps(msg.Params[2]) {
187+			for _, c := range ParseCaps(caps) {
188 				delete(s.availableCaps, c.Name)
189 				delete(s.enabledCaps, c.Name)
190 			}
191 		}
192 	case "JOIN":
193+		if msg.Prefix == nil {
194+			return nil, errMissingPrefix
195+		}
196+
197+		var channel string
198+		if err := msg.ParseParams(&channel); err != nil {
199+			return nil, err
200+		}
201+
202 		nickCf := s.Casemap(msg.Prefix.Name)
203-		channelCf := s.Casemap(msg.Params[0])
204-		if s.IsMe(msg.Prefix.Name) {
205+		channelCf := s.Casemap(channel)
206+
207+		if s.IsMe(nickCf) {
208 			s.channels[channelCf] = Channel{
209 				Name:    msg.Params[0],
210 				Members: map[*User]string{},
211@@ -607,12 +658,22 @@ func (s *Session) handleRegistered(msg Message) Event {
212 			return UserJoinEvent{
213 				User:    msg.Prefix.Name,
214 				Channel: c.Name,
215-			}
216+			}, nil
217 		}
218 	case "PART":
219+		if msg.Prefix == nil {
220+			return nil, errMissingPrefix
221+		}
222+
223+		var channel string
224+		if err := msg.ParseParams(&channel); err != nil {
225+			return nil, err
226+		}
227+
228 		nickCf := s.Casemap(msg.Prefix.Name)
229-		channelCf := s.Casemap(msg.Params[0])
230-		if s.IsMe(msg.Prefix.Name) {
231+		channelCf := s.Casemap(channel)
232+
233+		if s.IsMe(nickCf) {
234 			if c, ok := s.channels[channelCf]; ok {
235 				delete(s.channels, channelCf)
236 				for u := range c.Members {
237@@ -620,7 +681,7 @@ func (s *Session) handleRegistered(msg Message) Event {
238 				}
239 				return SelfPartEvent{
240 					Channel: c.Name,
241-				}
242+				}, nil
243 			}
244 		} else if c, ok := s.channels[channelCf]; ok {
245 			if u, ok := s.users[nickCf]; ok {
246@@ -630,13 +691,19 @@ func (s *Session) handleRegistered(msg Message) Event {
247 				return UserPartEvent{
248 					User:    u.Name.Name,
249 					Channel: c.Name,
250-				}
251+				}, nil
252 			}
253 		}
254 	case "KICK":
255-		nickCf := s.Casemap(msg.Params[1])
256-		channelCf := s.Casemap(msg.Params[0])
257-		if s.IsMe(msg.Prefix.Name) {
258+		var channel, nick string
259+		if err := msg.ParseParams(&channel, &nick); err != nil {
260+			return nil, err
261+		}
262+
263+		nickCf := s.Casemap(nick)
264+		channelCf := s.Casemap(channel)
265+
266+		if s.IsMe(nickCf) {
267 			if c, ok := s.channels[channelCf]; ok {
268 				delete(s.channels, channelCf)
269 				for u := range c.Members {
270@@ -644,7 +711,7 @@ func (s *Session) handleRegistered(msg Message) Event {
271 				}
272 				return SelfPartEvent{
273 					Channel: c.Name,
274-				}
275+				}, nil
276 			}
277 		} else if c, ok := s.channels[channelCf]; ok {
278 			if u, ok := s.users[nickCf]; ok {
279@@ -652,12 +719,16 @@ func (s *Session) handleRegistered(msg Message) Event {
280 				s.cleanUser(u)
281 				s.typings.Done(channelCf, nickCf)
282 				return UserPartEvent{
283-					User:    u.Name.Name,
284+					User:    nick,
285 					Channel: c.Name,
286-				}
287+				}, nil
288 			}
289 		}
290 	case "QUIT":
291+		if msg.Prefix == nil {
292+			return nil, errMissingPrefix
293+		}
294+
295 		nickCf := s.Casemap(msg.Prefix.Name)
296 
297 		if u, ok := s.users[nickCf]; ok {
298@@ -673,15 +744,19 @@ func (s *Session) handleRegistered(msg Message) Event {
299 			return UserQuitEvent{
300 				User:     u.Name.Name,
301 				Channels: channels,
302-			}
303+			}, nil
304 		}
305 	case rplNamreply:
306-		channelCf := s.Casemap(msg.Params[2])
307+		var channel, names string
308+		if err := msg.ParseParams(nil, nil, &channel, &names); err != nil {
309+			return nil, err
310+		}
311+
312+		channelCf := s.Casemap(channel)
313 
314 		if c, ok := s.channels[channelCf]; ok {
315-			c.Secret = msg.Params[1] == "@"
316 
317-			for _, name := range ParseNameReply(msg.Params[3], s.prefixSymbols) {
318+			for _, name := range ParseNameReply(names, s.prefixSymbols) {
319 				nickCf := s.Casemap(name.Name.Name)
320 
321 				if _, ok := s.users[nickCf]; !ok {
322@@ -693,7 +768,13 @@ func (s *Session) handleRegistered(msg Message) Event {
323 			s.channels[channelCf] = c
324 		}
325 	case rplEndofnames:
326-		channelCf := s.Casemap(msg.Params[1])
327+		var channel string
328+		if err := msg.ParseParams(nil, &channel); err != nil {
329+			return nil, err
330+		}
331+
332+		channelCf := s.Casemap(channel)
333+
334 		if c, ok := s.channels[channelCf]; ok && !c.complete {
335 			c.complete = true
336 			s.channels[channelCf] = c
337@@ -701,59 +782,114 @@ func (s *Session) handleRegistered(msg Message) Event {
338 				Channel: c.Name,
339 				Topic:   c.Topic,
340 			}
341-			if stamp, ok := s.pendingChannels[channelCf]; ok && time.Now().Sub(stamp) < 5*time.Second {
342+			if stamp, ok := s.pendingChannels[channelCf]; ok && time.Since(stamp) < 5*time.Second {
343 				ev.Requested = true
344 			}
345-			return ev
346+			return ev, nil
347 		}
348 	case rplTopic:
349-		channelCf := s.Casemap(msg.Params[1])
350+		var channel, topic string
351+		if err := msg.ParseParams(nil, &channel, &topic); err != nil {
352+			return nil, err
353+		}
354+
355+		channelCf := s.Casemap(channel)
356+
357 		if c, ok := s.channels[channelCf]; ok {
358-			c.Topic = msg.Params[2]
359+			c.Topic = topic
360 			s.channels[channelCf] = c
361 		}
362 	case rplTopicwhotime:
363-		channelCf := s.Casemap(msg.Params[1])
364-		t, _ := strconv.ParseInt(msg.Params[3], 10, 64)
365+		var channel, topicWho, topicTime string
366+		if err := msg.ParseParams(nil, &channel, &topicWho, &topicTime); err != nil {
367+			return nil, err
368+		}
369+
370+		channelCf := s.Casemap(channel)
371+
372+		// ignore the error, we still have topicWho
373+		t, _ := strconv.ParseInt(topicTime, 10, 64)
374+
375 		if c, ok := s.channels[channelCf]; ok {
376-			c.TopicWho = ParsePrefix(msg.Params[2])
377+			c.TopicWho = ParsePrefix(topicWho)
378 			c.TopicTime = time.Unix(t, 0)
379 			s.channels[channelCf] = c
380 		}
381 	case rplNotopic:
382-		channelCf := s.Casemap(msg.Params[1])
383+		var channel string
384+		if err := msg.ParseParams(nil, &channel); err != nil {
385+			return nil, err
386+		}
387+
388+		channelCf := s.Casemap(channel)
389+
390 		if c, ok := s.channels[channelCf]; ok {
391 			c.Topic = ""
392 			s.channels[channelCf] = c
393 		}
394 	case "TOPIC":
395-		channelCf := s.Casemap(msg.Params[0])
396+		if msg.Prefix == nil {
397+			return nil, errMissingPrefix
398+		}
399+
400+		var channel, topic string
401+		if err := msg.ParseParams(&channel, &topic); err != nil {
402+			return nil, err
403+		}
404+
405+		channelCf := s.Casemap(channel)
406+
407 		if c, ok := s.channels[channelCf]; ok {
408-			c.Topic = msg.Params[1]
409+			c.Topic = topic
410 			c.TopicWho = msg.Prefix.Copy()
411 			c.TopicTime = msg.TimeOrNow()
412 			s.channels[channelCf] = c
413 			return TopicChangeEvent{
414 				Channel: c.Name,
415 				Topic:   c.Topic,
416-			}
417+			}, nil
418 		}
419 	case "MODE":
420-		channelCf := s.Casemap(msg.Params[0])
421+		var channel string
422+		if err := msg.ParseParams(&channel); err != nil {
423+			return nil, err
424+		}
425+
426+		channelCf := s.Casemap(channel)
427+
428 		if c, ok := s.channels[channelCf]; ok {
429 			return ModeChangeEvent{
430 				Channel: c.Name,
431 				Mode:    strings.Join(msg.Params[1:], " "),
432-			}
433+			}, nil
434 		}
435 	case "PRIVMSG", "NOTICE":
436-		targetCf := s.casemap(msg.Params[0])
437+		if msg.Prefix == nil {
438+			return nil, errMissingPrefix
439+		}
440+
441+		var target string
442+		if err := msg.ParseParams(&target); err != nil {
443+			return nil, err
444+		}
445+
446+		targetCf := s.casemap(target)
447 		nickCf := s.casemap(msg.Prefix.Name)
448 		s.typings.Done(targetCf, nickCf)
449+
450 		return s.newMessageEvent(msg)
451 	case "TAGMSG":
452-		nickCf := s.Casemap(msg.Prefix.Name)
453-		targetCf := s.Casemap(msg.Params[0])
454+		if msg.Prefix == nil {
455+			return nil, errMissingPrefix
456+		}
457+
458+		var target string
459+		if err := msg.ParseParams(&target); err != nil {
460+			return nil, err
461+		}
462+
463+		targetCf := s.casemap(target)
464+		nickCf := s.casemap(msg.Prefix.Name)
465 
466 		if s.IsMe(msg.Prefix.Name) {
467 			// TAGMSG from self
468@@ -770,19 +906,46 @@ func (s *Session) handleRegistered(msg Message) Event {
469 			}
470 		}
471 	case "BATCH":
472-		batchStart := msg.Params[0][0] == '+'
473-		id := msg.Params[0][1:]
474+		var id string
475+		if err := msg.ParseParams(&id); err != nil {
476+			return nil, err
477+		}
478+
479+		batchStart := id[0] == '+' // id is not empty since it's not a trailing param
480+		id = id[1:]
481 
482-		if batchStart && msg.Params[1] == "chathistory" {
483-			s.chBatches[id] = HistoryEvent{Target: msg.Params[2]}
484+		if batchStart {
485+			var name string
486+			if err := msg.ParseParams(nil, &name); err != nil {
487+				return nil, err
488+			}
489+
490+			switch name {
491+			case "chathistory":
492+				var target string
493+				if err := msg.ParseParams(nil, nil, &target); err != nil {
494+					return nil, err
495+				}
496+
497+				s.chBatches[id] = HistoryEvent{Target: target}
498+			}
499 		} else if b, ok := s.chBatches[id]; ok {
500 			delete(s.chBatches, id)
501 			delete(s.chReqs, s.Casemap(b.Target))
502-			return b
503+			return b, nil
504 		}
505 	case "NICK":
506+		if msg.Prefix == nil {
507+			return nil, errMissingPrefix
508+		}
509+
510+		var nick string
511+		if err := msg.ParseParams(&nick); err != nil {
512+			return nil, err
513+		}
514+
515 		nickCf := s.Casemap(msg.Prefix.Name)
516-		newNick := msg.Params[0]
517+		newNick := nick
518 		newNickCf := s.Casemap(newNick)
519 
520 		if formerUser, ok := s.users[nickCf]; ok {
521@@ -798,61 +961,83 @@ func (s *Session) handleRegistered(msg Message) Event {
522 			s.nickCf = newNickCf
523 			return SelfNickEvent{
524 				FormerNick: msg.Prefix.Name,
525-			}
526+			}, nil
527 		} else {
528 			return UserNickEvent{
529-				User:       msg.Params[0],
530+				User:       nick,
531 				FormerNick: msg.Prefix.Name,
532-			}
533+			}, nil
534 		}
535 	case "PING":
536-		s.out <- NewMessage("PONG", msg.Params[0])
537+		var payload string
538+		if err := msg.ParseParams(&payload); err != nil {
539+			return nil, err
540+		}
541+
542+		s.out <- NewMessage("PONG", payload)
543 	case "ERROR":
544 		s.Close()
545-	case "FAIL":
546-		return ErrorEvent{
547-			Severity: SeverityFail,
548-			Code:     msg.Params[1],
549-			Message:  strings.Join(msg.Params[2:], " "),
550+	case "FAIL", "WARN", "NOTE":
551+		var severity Severity
552+		var code string
553+		if err := msg.ParseParams(nil, &code); err != nil {
554+			return nil, err
555 		}
556-	case "WARN":
557-		return ErrorEvent{
558-			Severity: SeverityWarn,
559-			Code:     msg.Params[1],
560-			Message:  strings.Join(msg.Params[2:], " "),
561+
562+		switch msg.Command {
563+		case "FAIL":
564+			severity = SeverityFail
565+		case "WARN":
566+			severity = SeverityWarn
567+		case "NOTE":
568+			severity = SeverityNote
569 		}
570-	case "NOTE":
571+
572 		return ErrorEvent{
573-			Severity: SeverityNote,
574-			Code:     msg.Params[1],
575+			Severity: severity,
576+			Code:     code,
577 			Message:  strings.Join(msg.Params[2:], " "),
578-		}
579+		}, nil
580 	default:
581 		if msg.IsReply() {
582+			if len(msg.Params) < 2 {
583+				return nil, msg.errNotEnoughParams(2)
584+			}
585 			return ErrorEvent{
586 				Severity: ReplySeverity(msg.Command),
587 				Code:     msg.Command,
588 				Message:  strings.Join(msg.Params[1:], " "),
589-			}
590+			}, nil
591 		}
592 	}
593-	return nil
594+	return nil, nil
595 }
596 
597-func (s *Session) newMessageEvent(msg Message) MessageEvent {
598-	targetCf := s.Casemap(msg.Params[0])
599-	ev := MessageEvent{
600+func (s *Session) newMessageEvent(msg Message) (ev MessageEvent, err error) {
601+	if msg.Prefix == nil {
602+		return ev, errMissingPrefix
603+	}
604+
605+	var target, content string
606+	if err := msg.ParseParams(&target, &content); err != nil {
607+		return ev, err
608+	}
609+
610+	ev = MessageEvent{
611 		User:    msg.Prefix.Name, // TODO correctly casemap
612-		Target:  msg.Params[0],   // TODO correctly casemap
613+		Target:  target,          // TODO correctly casemap
614 		Command: msg.Command,
615-		Content: msg.Params[1],
616+		Content: content,
617 		Time:    msg.TimeOrNow(),
618 	}
619+
620+	targetCf := s.Casemap(target)
621 	if c, ok := s.channels[targetCf]; ok {
622 		ev.Target = c.Name
623 		ev.TargetIsChannel = true
624 	}
625-	return ev
626+
627+	return ev, nil
628 }
629 
630 func (s *Session) cleanUser(parted *User) {
+13, -58
 1@@ -3,7 +3,6 @@ package irc
 2 import (
 3 	"errors"
 4 	"fmt"
 5-	"strconv"
 6 	"strings"
 7 	"time"
 8 )
 9@@ -155,6 +154,7 @@ func parseTags(s string) (tags map[string]string) {
10 var (
11 	errEmptyMessage      = errors.New("empty message")
12 	errIncompleteMessage = errors.New("message is incomplete")
13+	errMissingPrefix     = errors.New("missing message prefix")
14 )
15 
16 type Prefix struct {
17@@ -345,65 +345,20 @@ func (msg *Message) String() string {
18 	return sb.String()
19 }
20 
21-// IsValid reports whether the message is correctly formed.
22-func (msg *Message) IsValid() bool {
23-	switch msg.Command {
24-	case "AUTHENTICATE", "PING", "PONG":
25-		return 1 <= len(msg.Params)
26-	case rplEndofnames, rplLoggedout, rplMotd, errNicknameinuse, rplNotopic, rplWelcome, rplYourhost:
27-		return 2 <= len(msg.Params)
28-	case rplIsupport, rplLoggedin, rplTopic, "FAIL", "WARN", "NOTE":
29-		return 3 <= len(msg.Params)
30-	case rplNamreply:
31-		return 4 <= len(msg.Params)
32-	case rplWhoreply:
33-		return 8 <= len(msg.Params)
34-	case "JOIN", "NICK", "PART", "TAGMSG":
35-		return 1 <= len(msg.Params) && msg.Prefix != nil
36-	case "KICK", "PRIVMSG", "NOTICE", "TOPIC":
37-		return 2 <= len(msg.Params) && msg.Prefix != nil
38-	case "QUIT":
39-		return msg.Prefix != nil
40-	case "CAP":
41-		return 3 <= len(msg.Params) &&
42-			(msg.Params[1] == "LS" ||
43-				msg.Params[1] == "LIST" ||
44-				msg.Params[1] == "ACK" ||
45-				msg.Params[1] == "NAK" ||
46-				msg.Params[1] == "NEW" ||
47-				msg.Params[1] == "DEL")
48-	case rplTopicwhotime:
49-		if len(msg.Params) < 4 {
50-			return false
51-		}
52-		_, err := strconv.ParseInt(msg.Params[3], 10, 64)
53-		return err == nil
54-	case "BATCH":
55-		if len(msg.Params) < 1 {
56-			return false
57-		}
58-		if len(msg.Params[0]) < 2 {
59-			return false
60-		}
61-		if msg.Params[0][0] == '+' {
62-			if len(msg.Params) < 2 {
63-				return false
64-			}
65-			switch msg.Params[1] {
66-			case "chathistory":
67-				return 3 <= len(msg.Params)
68-			default:
69-				return false
70-			}
71-		}
72-		return msg.Params[0][0] == '-'
73-	default:
74-		if len(msg.Command) != 3 || len(msg.Params) < 2 {
75-			return false
76+func (msg *Message) errNotEnoughParams(expected int) error {
77+	return fmt.Errorf("expected at least %d params, got %d", expected, len(msg.Params))
78+}
79+
80+func (msg *Message) ParseParams(out ...*string) error {
81+	if len(msg.Params) < len(out) {
82+		return msg.errNotEnoughParams(len(out))
83+	}
84+	for i := range out {
85+		if out[i] != nil {
86+			*out[i] = msg.Params[i]
87 		}
88-		_, err := strconv.Atoi(msg.Command)
89-		return err == nil
90 	}
91+	return nil
92 }
93 
94 // Time returns the time when the message has been sent, if present.