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
+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.