summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app.go10
-rw-r--r--irc/session.go381
-rw-r--r--irc/tokens.go71
3 files changed, 305 insertions, 157 deletions
diff --git a/app.go b/app.go
index b7bd45c..2447175 100644
--- a/app.go
+++ b/app.go
@@ -527,7 +527,15 @@ func (app *App) handleIRCEvent(ev interface{}) {
msg := ev.(irc.Message)
// Mutate IRC state
- ev = app.s.HandleMessage(msg)
+ ev, err := app.s.HandleMessage(msg)
+ if err != nil {
+ app.win.AddLine(Home, ui.NotifyUnread, ui.Line{
+ Head: "!!",
+ HeadColor: tcell.ColorRed,
+ Body: ui.PlainSprintf("Received corrupt message %q: %s", msg.String(), err),
+ })
+ return
+ }
// Mutate UI state
switch ev := ev.(type) {
diff --git a/irc/session.go b/irc/session.go
index 54c1ac6..3b6a813 100644
--- a/irc/session.go
+++ b/irc/session.go
@@ -82,7 +82,6 @@ type Channel struct {
Topic string // the topic of the channel, or "" if absent.
TopicWho *Prefix // the name of the last user who set the topic.
TopicTime time.Time // the last time the topic has been changed.
- Secret bool // whether the channel is on the server channel list.
complete bool // whether this structure is fully initialized.
}
@@ -450,7 +449,7 @@ func (s *Session) NewHistoryRequest(target string) *HistoryRequest {
}
}
-func (s *Session) HandleMessage(msg Message) Event {
+func (s *Session) HandleMessage(msg Message) (Event, error) {
if s.registered {
return s.handleRegistered(msg)
} else {
@@ -458,35 +457,53 @@ func (s *Session) HandleMessage(msg Message) Event {
}
}
-func (s *Session) handleUnregistered(msg Message) Event {
+func (s *Session) handleUnregistered(msg Message) (Event, error) {
switch msg.Command {
case "AUTHENTICATE":
- if s.auth != nil {
- res, err := s.auth.Respond(msg.Params[0])
- if err != nil {
- s.out <- NewMessage("AUTHENTICATE", "*")
- } else {
- s.out <- NewMessage("AUTHENTICATE", res)
- }
+ if s.auth == nil {
+ break
+ }
+
+ var payload string
+ if err := msg.ParseParams(&payload); err != nil {
+ return nil, err
+ }
+
+ res, err := s.auth.Respond(payload)
+ if err != nil {
+ s.out <- NewMessage("AUTHENTICATE", "*")
+ } else {
+ s.out <- NewMessage("AUTHENTICATE", res)
}
case rplLoggedin:
+ var userhost string
+ if err := msg.ParseParams(nil, &userhost, &s.acct); err != nil {
+ return nil, err
+ }
+
s.out <- NewMessage("CAP", "END")
- s.acct = msg.Params[2]
- s.host = ParsePrefix(msg.Params[1]).Host
+ s.host = ParsePrefix(userhost).Host
case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs:
s.out <- NewMessage("CAP", "END")
case "CAP":
- switch msg.Params[1] {
+ var subcommand string
+ if err := msg.ParseParams(nil, &subcommand); err != nil {
+ return nil, err
+ }
+
+ switch subcommand {
case "LS":
- var willContinue bool
var ls string
+ if err := msg.ParseParams(nil, nil, &ls); err != nil {
+ return nil, err
+ }
- if msg.Params[2] == "*" {
+ willContinue := false
+ if ls == "*" {
+ if err := msg.ParseParams(nil, nil, nil, &ls); err != nil {
+ return nil, err
+ }
willContinue = true
- ls = msg.Params[3]
- } else {
- willContinue = false
- ls = msg.Params[2]
}
for _, c := range ParseCaps(ls) {
@@ -510,30 +527,41 @@ func (s *Session) handleUnregistered(msg Message) Event {
return s.handleRegistered(msg)
}
case errNicknameinuse:
- s.out <- NewMessage("NICK", msg.Params[1]+"_")
+ var nick string
+ if err := msg.ParseParams(nil, &nick); err != nil {
+ return nil, err
+ }
+
+ s.out <- NewMessage("NICK", nick+"_")
case rplSaslsuccess:
// do nothing
default:
return s.handleRegistered(msg)
}
- return nil
+ return nil, nil
}
-func (s *Session) handleRegistered(msg Message) Event {
+func (s *Session) handleRegistered(msg Message) (Event, error) {
if id, ok := msg.Tags["batch"]; ok {
if b, ok := s.chBatches[id]; ok {
- ev := s.newMessageEvent(msg)
+ ev, err := s.newMessageEvent(msg)
+ if err != nil {
+ return nil, err
+ }
s.chBatches[id] = HistoryEvent{
Target: b.Target,
Messages: append(b.Messages, ev),
}
- return nil
+ return nil, nil
}
}
switch msg.Command {
case rplWelcome:
- s.nick = msg.Params[0]
+ if err := msg.ParseParams(&s.nick); err != nil {
+ return nil, err
+ }
+
s.nickCf = s.Casemap(s.nick)
s.registered = true
s.users[s.nickCf] = &User{Name: &Prefix{
@@ -542,17 +570,30 @@ func (s *Session) handleRegistered(msg Message) Event {
if s.host == "" {
s.out <- NewMessage("WHO", s.nick)
}
- return RegisteredEvent{}
+ return RegisteredEvent{}, nil
case rplIsupport:
+ if len(msg.Params) < 3 {
+ return nil, msg.errNotEnoughParams(3)
+ }
s.updateFeatures(msg.Params[1 : len(msg.Params)-1])
case rplWhoreply:
- if s.nickCf == s.Casemap(msg.Params[5]) {
- s.host = msg.Params[3]
+ var nick, host string
+ if err := msg.ParseParams(nil, nil, nil, &host, nil, &nick); err != nil {
+ return nil, err
+ }
+
+ if s.nickCf == s.Casemap(nick) {
+ s.host = host
}
case "CAP":
- switch msg.Params[1] {
+ var subcommand, caps string
+ if err := msg.ParseParams(nil, &subcommand, &caps); err != nil {
+ return nil, err
+ }
+
+ switch subcommand {
case "ACK":
- for _, c := range ParseCaps(msg.Params[2]) {
+ for _, c := range ParseCaps(caps) {
if c.Enable {
s.enabledCaps[c.Name] = struct{}{}
} else {
@@ -572,7 +613,7 @@ func (s *Session) handleRegistered(msg Message) Event {
case "NAK":
// do nothing
case "NEW":
- for _, c := range ParseCaps(msg.Params[2]) {
+ for _, c := range ParseCaps(caps) {
s.availableCaps[c.Name] = c.Value
_, ok := SupportedCapabilities[c.Name]
if !ok {
@@ -586,15 +627,25 @@ func (s *Session) handleRegistered(msg Message) Event {
// TODO authenticate
}
case "DEL":
- for _, c := range ParseCaps(msg.Params[2]) {
+ for _, c := range ParseCaps(caps) {
delete(s.availableCaps, c.Name)
delete(s.enabledCaps, c.Name)
}
}
case "JOIN":
+ if msg.Prefix == nil {
+ return nil, errMissingPrefix
+ }
+
+ var channel string
+ if err := msg.ParseParams(&channel); err != nil {
+ return nil, err
+ }
+
nickCf := s.Casemap(msg.Prefix.Name)
- channelCf := s.Casemap(msg.Params[0])
- if s.IsMe(msg.Prefix.Name) {
+ channelCf := s.Casemap(channel)
+
+ if s.IsMe(nickCf) {
s.channels[channelCf] = Channel{
Name: msg.Params[0],
Members: map[*User]string{},
@@ -607,12 +658,22 @@ func (s *Session) handleRegistered(msg Message) Event {
return UserJoinEvent{
User: msg.Prefix.Name,
Channel: c.Name,
- }
+ }, nil
}
case "PART":
+ if msg.Prefix == nil {
+ return nil, errMissingPrefix
+ }
+
+ var channel string
+ if err := msg.ParseParams(&channel); err != nil {
+ return nil, err
+ }
+
nickCf := s.Casemap(msg.Prefix.Name)
- channelCf := s.Casemap(msg.Params[0])
- if s.IsMe(msg.Prefix.Name) {
+ channelCf := s.Casemap(channel)
+
+ if s.IsMe(nickCf) {
if c, ok := s.channels[channelCf]; ok {
delete(s.channels, channelCf)
for u := range c.Members {
@@ -620,7 +681,7 @@ func (s *Session) handleRegistered(msg Message) Event {
}
return SelfPartEvent{
Channel: c.Name,
- }
+ }, nil
}
} else if c, ok := s.channels[channelCf]; ok {
if u, ok := s.users[nickCf]; ok {
@@ -630,13 +691,19 @@ func (s *Session) handleRegistered(msg Message) Event {
return UserPartEvent{
User: u.Name.Name,
Channel: c.Name,
- }
+ }, nil
}
}
case "KICK":
- nickCf := s.Casemap(msg.Params[1])
- channelCf := s.Casemap(msg.Params[0])
- if s.IsMe(msg.Prefix.Name) {
+ var channel, nick string
+ if err := msg.ParseParams(&channel, &nick); err != nil {
+ return nil, err
+ }
+
+ nickCf := s.Casemap(nick)
+ channelCf := s.Casemap(channel)
+
+ if s.IsMe(nickCf) {
if c, ok := s.channels[channelCf]; ok {
delete(s.channels, channelCf)
for u := range c.Members {
@@ -644,7 +711,7 @@ func (s *Session) handleRegistered(msg Message) Event {
}
return SelfPartEvent{
Channel: c.Name,
- }
+ }, nil
}
} else if c, ok := s.channels[channelCf]; ok {
if u, ok := s.users[nickCf]; ok {
@@ -652,12 +719,16 @@ func (s *Session) handleRegistered(msg Message) Event {
s.cleanUser(u)
s.typings.Done(channelCf, nickCf)
return UserPartEvent{
- User: u.Name.Name,
+ User: nick,
Channel: c.Name,
- }
+ }, nil
}
}
case "QUIT":
+ if msg.Prefix == nil {
+ return nil, errMissingPrefix
+ }
+
nickCf := s.Casemap(msg.Prefix.Name)
if u, ok := s.users[nickCf]; ok {
@@ -673,15 +744,19 @@ func (s *Session) handleRegistered(msg Message) Event {
return UserQuitEvent{
User: u.Name.Name,
Channels: channels,
- }
+ }, nil
}
case rplNamreply:
- channelCf := s.Casemap(msg.Params[2])
+ var channel, names string
+ if err := msg.ParseParams(nil, nil, &channel, &names); err != nil {
+ return nil, err
+ }
+
+ channelCf := s.Casemap(channel)
if c, ok := s.channels[channelCf]; ok {
- c.Secret = msg.Params[1] == "@"
- for _, name := range ParseNameReply(msg.Params[3], s.prefixSymbols) {
+ for _, name := range ParseNameReply(names, s.prefixSymbols) {
nickCf := s.Casemap(name.Name.Name)
if _, ok := s.users[nickCf]; !ok {
@@ -693,7 +768,13 @@ func (s *Session) handleRegistered(msg Message) Event {
s.channels[channelCf] = c
}
case rplEndofnames:
- channelCf := s.Casemap(msg.Params[1])
+ var channel string
+ if err := msg.ParseParams(nil, &channel); err != nil {
+ return nil, err
+ }
+
+ channelCf := s.Casemap(channel)
+
if c, ok := s.channels[channelCf]; ok && !c.complete {
c.complete = true
s.channels[channelCf] = c
@@ -701,59 +782,114 @@ func (s *Session) handleRegistered(msg Message) Event {
Channel: c.Name,
Topic: c.Topic,
}
- if stamp, ok := s.pendingChannels[channelCf]; ok && time.Now().Sub(stamp) < 5*time.Second {
+ if stamp, ok := s.pendingChannels[channelCf]; ok && time.Since(stamp) < 5*time.Second {
ev.Requested = true
}
- return ev
+ return ev, nil
}
case rplTopic:
- channelCf := s.Casemap(msg.Params[1])
+ var channel, topic string
+ if err := msg.ParseParams(nil, &channel, &topic); err != nil {
+ return nil, err
+ }
+
+ channelCf := s.Casemap(channel)
+
if c, ok := s.channels[channelCf]; ok {
- c.Topic = msg.Params[2]
+ c.Topic = topic
s.channels[channelCf] = c
}
case rplTopicwhotime:
- channelCf := s.Casemap(msg.Params[1])
- t, _ := strconv.ParseInt(msg.Params[3], 10, 64)
+ var channel, topicWho, topicTime string
+ if err := msg.ParseParams(nil, &channel, &topicWho, &topicTime); err != nil {
+ return nil, err
+ }
+
+ channelCf := s.Casemap(channel)
+
+ // ignore the error, we still have topicWho
+ t, _ := strconv.ParseInt(topicTime, 10, 64)
+
if c, ok := s.channels[channelCf]; ok {
- c.TopicWho = ParsePrefix(msg.Params[2])
+ c.TopicWho = ParsePrefix(topicWho)
c.TopicTime = time.Unix(t, 0)
s.channels[channelCf] = c
}
case rplNotopic:
- channelCf := s.Casemap(msg.Params[1])
+ var channel string
+ if err := msg.ParseParams(nil, &channel); err != nil {
+ return nil, err
+ }
+
+ channelCf := s.Casemap(channel)
+
if c, ok := s.channels[channelCf]; ok {
c.Topic = ""
s.channels[channelCf] = c
}
case "TOPIC":
- channelCf := s.Casemap(msg.Params[0])
+ if msg.Prefix == nil {
+ return nil, errMissingPrefix
+ }
+
+ var channel, topic string
+ if err := msg.ParseParams(&channel, &topic); err != nil {
+ return nil, err
+ }
+
+ channelCf := s.Casemap(channel)
+
if c, ok := s.channels[channelCf]; ok {
- c.Topic = msg.Params[1]
+ c.Topic = topic
c.TopicWho = msg.Prefix.Copy()
c.TopicTime = msg.TimeOrNow()
s.channels[channelCf] = c
return TopicChangeEvent{
Channel: c.Name,
Topic: c.Topic,
- }
+ }, nil
}
case "MODE":
- channelCf := s.Casemap(msg.Params[0])
+ var channel string
+ if err := msg.ParseParams(&channel); err != nil {
+ return nil, err
+ }
+
+ channelCf := s.Casemap(channel)
+
if c, ok := s.channels[channelCf]; ok {
return ModeChangeEvent{
Channel: c.Name,
Mode: strings.Join(msg.Params[1:], " "),
- }
+ }, nil
}
case "PRIVMSG", "NOTICE":
- targetCf := s.casemap(msg.Params[0])
+ if msg.Prefix == nil {
+ return nil, errMissingPrefix
+ }
+
+ var target string
+ if err := msg.ParseParams(&target); err != nil {
+ return nil, err
+ }
+
+ targetCf := s.casemap(target)
nickCf := s.casemap(msg.Prefix.Name)
s.typings.Done(targetCf, nickCf)
+
return s.newMessageEvent(msg)
case "TAGMSG":
- nickCf := s.Casemap(msg.Prefix.Name)
- targetCf := s.Casemap(msg.Params[0])
+ if msg.Prefix == nil {
+ return nil, errMissingPrefix
+ }
+
+ var target string
+ if err := msg.ParseParams(&target); err != nil {
+ return nil, err
+ }
+
+ targetCf := s.casemap(target)
+ nickCf := s.casemap(msg.Prefix.Name)
if s.IsMe(msg.Prefix.Name) {
// TAGMSG from self
@@ -770,19 +906,46 @@ func (s *Session) handleRegistered(msg Message) Event {
}
}
case "BATCH":
- batchStart := msg.Params[0][0] == '+'
- id := msg.Params[0][1:]
+ var id string
+ if err := msg.ParseParams(&id); err != nil {
+ return nil, err
+ }
+
+ batchStart := id[0] == '+' // id is not empty since it's not a trailing param
+ id = id[1:]
- if batchStart && msg.Params[1] == "chathistory" {
- s.chBatches[id] = HistoryEvent{Target: msg.Params[2]}
+ if batchStart {
+ var name string
+ if err := msg.ParseParams(nil, &name); err != nil {
+ return nil, err
+ }
+
+ switch name {
+ case "chathistory":
+ var target string
+ if err := msg.ParseParams(nil, nil, &target); err != nil {
+ return nil, err
+ }
+
+ s.chBatches[id] = HistoryEvent{Target: target}
+ }
} else if b, ok := s.chBatches[id]; ok {
delete(s.chBatches, id)
delete(s.chReqs, s.Casemap(b.Target))
- return b
+ return b, nil
}
case "NICK":
+ if msg.Prefix == nil {
+ return nil, errMissingPrefix
+ }
+
+ var nick string
+ if err := msg.ParseParams(&nick); err != nil {
+ return nil, err
+ }
+
nickCf := s.Casemap(msg.Prefix.Name)
- newNick := msg.Params[0]
+ newNick := nick
newNickCf := s.Casemap(newNick)
if formerUser, ok := s.users[nickCf]; ok {
@@ -798,61 +961,83 @@ func (s *Session) handleRegistered(msg Message) Event {
s.nickCf = newNickCf
return SelfNickEvent{
FormerNick: msg.Prefix.Name,
- }
+ }, nil
} else {
return UserNickEvent{
- User: msg.Params[0],
+ User: nick,
FormerNick: msg.Prefix.Name,
- }
+ }, nil
}
case "PING":
- s.out <- NewMessage("PONG", msg.Params[0])
+ var payload string
+ if err := msg.ParseParams(&payload); err != nil {
+ return nil, err
+ }
+
+ s.out <- NewMessage("PONG", payload)
case "ERROR":
s.Close()
- case "FAIL":
- return ErrorEvent{
- Severity: SeverityFail,
- Code: msg.Params[1],
- Message: strings.Join(msg.Params[2:], " "),
+ case "FAIL", "WARN", "NOTE":
+ var severity Severity
+ var code string
+ if err := msg.ParseParams(nil, &code); err != nil {
+ return nil, err
}
- case "WARN":
- return ErrorEvent{
- Severity: SeverityWarn,
- Code: msg.Params[1],
- Message: strings.Join(msg.Params[2:], " "),
+
+ switch msg.Command {
+ case "FAIL":
+ severity = SeverityFail
+ case "WARN":
+ severity = SeverityWarn
+ case "NOTE":
+ severity = SeverityNote
}
- case "NOTE":
+
return ErrorEvent{
- Severity: SeverityNote,
- Code: msg.Params[1],
+ Severity: severity,
+ Code: code,
Message: strings.Join(msg.Params[2:], " "),
- }
+ }, nil
default:
if msg.IsReply() {
+ if len(msg.Params) < 2 {
+ return nil, msg.errNotEnoughParams(2)
+ }
return ErrorEvent{
Severity: ReplySeverity(msg.Command),
Code: msg.Command,
Message: strings.Join(msg.Params[1:], " "),
- }
+ }, nil
}
}
- return nil
+ return nil, nil
}
-func (s *Session) newMessageEvent(msg Message) MessageEvent {
- targetCf := s.Casemap(msg.Params[0])
- ev := MessageEvent{
+func (s *Session) newMessageEvent(msg Message) (ev MessageEvent, err error) {
+ if msg.Prefix == nil {
+ return ev, errMissingPrefix
+ }
+
+ var target, content string
+ if err := msg.ParseParams(&target, &content); err != nil {
+ return ev, err
+ }
+
+ ev = MessageEvent{
User: msg.Prefix.Name, // TODO correctly casemap
- Target: msg.Params[0], // TODO correctly casemap
+ Target: target, // TODO correctly casemap
Command: msg.Command,
- Content: msg.Params[1],
+ Content: content,
Time: msg.TimeOrNow(),
}
+
+ targetCf := s.Casemap(target)
if c, ok := s.channels[targetCf]; ok {
ev.Target = c.Name
ev.TargetIsChannel = true
}
- return ev
+
+ return ev, nil
}
func (s *Session) cleanUser(parted *User) {
diff --git a/irc/tokens.go b/irc/tokens.go
index f7569a1..1d0db1b 100644
--- a/irc/tokens.go
+++ b/irc/tokens.go
@@ -3,7 +3,6 @@ package irc
import (
"errors"
"fmt"
- "strconv"
"strings"
"time"
)
@@ -155,6 +154,7 @@ func parseTags(s string) (tags map[string]string) {
var (
errEmptyMessage = errors.New("empty message")
errIncompleteMessage = errors.New("message is incomplete")
+ errMissingPrefix = errors.New("missing message prefix")
)
type Prefix struct {
@@ -345,65 +345,20 @@ func (msg *Message) String() string {
return sb.String()
}
-// IsValid reports whether the message is correctly formed.
-func (msg *Message) IsValid() bool {
- switch msg.Command {
- case "AUTHENTICATE", "PING", "PONG":
- return 1 <= len(msg.Params)
- case rplEndofnames, rplLoggedout, rplMotd, errNicknameinuse, rplNotopic, rplWelcome, rplYourhost:
- return 2 <= len(msg.Params)
- case rplIsupport, rplLoggedin, rplTopic, "FAIL", "WARN", "NOTE":
- return 3 <= len(msg.Params)
- case rplNamreply:
- return 4 <= len(msg.Params)
- case rplWhoreply:
- return 8 <= len(msg.Params)
- case "JOIN", "NICK", "PART", "TAGMSG":
- return 1 <= len(msg.Params) && msg.Prefix != nil
- case "KICK", "PRIVMSG", "NOTICE", "TOPIC":
- return 2 <= len(msg.Params) && msg.Prefix != nil
- case "QUIT":
- return msg.Prefix != nil
- case "CAP":
- return 3 <= len(msg.Params) &&
- (msg.Params[1] == "LS" ||
- msg.Params[1] == "LIST" ||
- msg.Params[1] == "ACK" ||
- msg.Params[1] == "NAK" ||
- msg.Params[1] == "NEW" ||
- msg.Params[1] == "DEL")
- case rplTopicwhotime:
- if len(msg.Params) < 4 {
- return false
- }
- _, err := strconv.ParseInt(msg.Params[3], 10, 64)
- return err == nil
- case "BATCH":
- if len(msg.Params) < 1 {
- return false
- }
- if len(msg.Params[0]) < 2 {
- return false
- }
- if msg.Params[0][0] == '+' {
- if len(msg.Params) < 2 {
- return false
- }
- switch msg.Params[1] {
- case "chathistory":
- return 3 <= len(msg.Params)
- default:
- return false
- }
- }
- return msg.Params[0][0] == '-'
- default:
- if len(msg.Command) != 3 || len(msg.Params) < 2 {
- return false
+func (msg *Message) errNotEnoughParams(expected int) error {
+ return fmt.Errorf("expected at least %d params, got %d", expected, len(msg.Params))
+}
+
+func (msg *Message) ParseParams(out ...*string) error {
+ if len(msg.Params) < len(out) {
+ return msg.errNotEnoughParams(len(out))
+ }
+ for i := range out {
+ if out[i] != nil {
+ *out[i] = msg.Params[i]
}
- _, err := strconv.Atoi(msg.Command)
- return err == nil
}
+ return nil
}
// Time returns the time when the message has been sent, if present.