diff options
author | Hubert Hirtz <hubert@hirtz.pm> | 2021-05-18 21:26:03 +0200 |
---|---|---|
committer | Hubert Hirtz <hubert@hirtz.pm> | 2021-05-20 22:45:48 +0200 |
commit | 7835d39dda5f7a1dde3056de3dd8a84913b94c14 (patch) | |
tree | cb989997170121baee7d227b7e909a624c2bdbf2 /irc/states.go | |
parent | Add /mode command (diff) |
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.
Diffstat (limited to 'irc/states.go')
-rw-r--r-- | irc/states.go | 1197 |
1 files changed, 0 insertions, 1197 deletions
diff --git a/irc/states.go b/irc/states.go deleted file mode 100644 index b2a129a..0000000 --- a/irc/states.go +++ /dev/null @@ -1,1197 +0,0 @@ -package irc - -import ( - "bufio" - "bytes" - "encoding/base64" - "errors" - "fmt" - "net" - "strconv" - "strings" - "sync/atomic" - "time" - "unicode/utf8" -) - -const writeDeadline = 10 * time.Second - -type SASLClient interface { - Handshake() (mech string) - Respond(challenge string) (res string, err error) -} - -type SASLPlain struct { - Username string - Password string -} - -func (auth *SASLPlain) Handshake() (mech string) { - mech = "PLAIN" - return -} - -func (auth *SASLPlain) Respond(challenge string) (res string, err error) { - if challenge != "+" { - err = errors.New("unexpected challenge") - return - } - - user := []byte(auth.Username) - pass := []byte(auth.Password) - payload := bytes.Join([][]byte{user, user, pass}, []byte{0}) - res = base64.StdEncoding.EncodeToString(payload) - - return -} - -// SupportedCapabilities is the set of capabilities supported by this library. -var SupportedCapabilities = map[string]struct{}{ - "account-notify": {}, - "account-tag": {}, - "away-notify": {}, - "batch": {}, - "cap-notify": {}, - "draft/chathistory": {}, - "echo-message": {}, - "extended-join": {}, - "invite-notify": {}, - "labeled-response": {}, - "message-tags": {}, - "multi-prefix": {}, - "server-time": {}, - "sasl": {}, - "setname": {}, - "userhost-in-names": {}, -} - -// Values taken by the "@+typing=" client tag. TypingUnspec means the value or -// tag is absent. -const ( - TypingUnspec = iota - TypingActive - TypingPaused - TypingDone -) - -// action contains the arguments of a user action. -// -// To keep connection reads and writes in a single coroutine, the library -// interface functions like Join("#channel") or PrivMsg("target", "message") -// don't interact with the IRC session directly. Instead, they push an action -// in the action channel. This action is then processed by the correct -// coroutine. -type action interface{} - -type ( - actionSendRaw struct { - raw string - } - - actionChangeNick struct { - Nick string - } - actionChangeMode struct { - Channel string - Flags string - Args []string - } - - actionJoin struct { - Channel string - } - actionPart struct { - Channel string - Reason string - } - actionSetTopic struct { - Channel string - Topic string - } - actionQuit struct { - Reason string - } - - actionPrivMsg struct { - Target string - Content string - } - - actionTyping struct { - Channel string - } - actionTypingStop struct { - Channel string - } - - actionRequestHistory struct { - Target string - Before time.Time - } -) - -// User is a known IRC user (we share a channel with it). -type User struct { - Name *Prefix // the nick, user and hostname of the user if known. - AwayMsg string // the away message if the user is away, "" otherwise. -} - -// Channel is a joined channel. -type Channel struct { - Name string // the name of the channel. - Members map[*User]string // the set of members associated with their membership. - 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 stucture is fully initialized. -} - -// SessionParams defines how to connect to an IRC server. -type SessionParams struct { - Nickname string - Username string - RealName string - - Auth SASLClient - - Debug bool // whether the Session should report all messages it sends and receive. -} - -// Session is an IRC session/connection/whatever. -type Session struct { - conn net.Conn - msgs chan Message // incoming messages. - acts chan action // user actions. - evts chan Event // events sent to the user. - - debug bool - - running atomic.Value // bool - registered bool - typings *Typings // incoming typing notifications. - typingStamps map[string]time.Time // user typing instants. - - nick string - nickCf string // casemapped nickname. - user string - real string - acct string - host string - auth SASLClient - - availableCaps map[string]string - enabledCaps map[string]struct{} - - // ISUPPORT features - casemap func(string) string - chantypes string - linelen int - - users map[string]*User // known users. - channels map[string]Channel // joined channels. - chBatches map[string]HistoryEvent // channel history batches being processed. - chReqs map[string]struct{} // set of targets for which history is currently requested. -} - -// NewSession starts an IRC session from the given connection and session -// parameters. -// -// It returns an error when the paramaters are invalid, or when it cannot write -// to the connection. -func NewSession(conn net.Conn, params SessionParams) (*Session, error) { - s := &Session{ - conn: conn, - msgs: make(chan Message, 64), - acts: make(chan action, 64), - evts: make(chan Event, 64), - debug: params.Debug, - typings: NewTypings(), - typingStamps: map[string]time.Time{}, - nick: params.Nickname, - nickCf: CasemapASCII(params.Nickname), - user: params.Username, - real: params.RealName, - auth: params.Auth, - availableCaps: map[string]string{}, - enabledCaps: map[string]struct{}{}, - casemap: CasemapRFC1459, - chantypes: "#&", - linelen: 512, - users: map[string]*User{}, - channels: map[string]Channel{}, - chBatches: map[string]HistoryEvent{}, - chReqs: map[string]struct{}{}, - } - - s.running.Store(true) - - go func() { - r := bufio.NewScanner(conn) - - for r.Scan() { - line := r.Text() - msg, err := ParseMessage(line) - if err != nil { - continue - } - valid := msg.IsValid() - if s.debug { - s.evts <- RawMessageEvent{Message: line, IsValid: valid} - } - if valid { - s.msgs <- msg - } - } - - s.Stop() - }() - - err := s.send("CAP LS 302\r\nNICK %s\r\nUSER %s 0 * :%s\r\n", s.nick, s.user, s.real) - if err != nil { - return nil, err - } - - go s.run() - - return s, nil -} - -// Running reports whether we are still connected to the server. -func (s *Session) Running() bool { - return s.running.Load().(bool) -} - -// Stop stops the session and closes the connection. -func (s *Session) Stop() { - if !s.Running() { - return - } - s.running.Store(false) - _ = s.conn.Close() - close(s.acts) - close(s.evts) - close(s.msgs) - s.typings.Stop() -} - -// Poll returns the event channel where incoming events are reported. -func (s *Session) Poll() (events <-chan Event) { - return s.evts -} - -// HasCapability reports whether the given capability has been negociated -// successfully. -func (s *Session) HasCapability(capability string) bool { - _, ok := s.enabledCaps[capability] - return ok -} - -func (s *Session) Nick() string { - return s.nick -} - -// NickCf is our casemapped nickname. -func (s *Session) NickCf() string { - return s.nickCf -} - -func (s *Session) IsChannel(name string) bool { - return strings.IndexAny(name, s.chantypes) == 0 -} - -func (s *Session) Casemap(name string) string { - return s.casemap(name) -} - -// Users returns the list of all known nicknames. -func (s *Session) Users() []string { - users := make([]string, 0, len(s.users)) - for _, u := range s.users { - users = append(users, u.Name.Name) - } - return users -} - -// Names returns the list of users in the given channel, or nil if this channel -// is not known by the session. -func (s *Session) Names(channel string) []Member { - var names []Member - if c, ok := s.channels[s.Casemap(channel)]; ok { - names = make([]Member, 0, len(c.Members)) - for u, pl := range c.Members { - names = append(names, Member{ - PowerLevel: pl, - Name: u.Name.Copy(), - }) - } - } - return names -} - -// Typings returns the list of nickname who are currently typing. -func (s *Session) Typings(target string) []string { - targetCf := s.Casemap(target) - var res []string - for t := range s.typings.targets { - if targetCf == t.Target && s.Casemap(t.Name) != s.NickCf() { - res = append(res, s.users[t.Name].Name.Name) - } - } - return res -} - -func (s *Session) ChannelsSharedWith(name string) []string { - var user *User - if u, ok := s.users[s.Casemap(name)]; ok { - user = u - } else { - return nil - } - var channels []string - for _, c := range s.channels { - if _, ok := c.Members[user]; ok { - channels = append(channels, c.Name) - } - } - return channels -} - -func (s *Session) Topic(channel string) (topic string, who *Prefix, at time.Time) { - channelCf := s.Casemap(channel) - if c, ok := s.channels[channelCf]; ok { - topic = c.Topic - who = c.TopicWho - at = c.TopicTime - } - return -} - -// SendRaw sends its given argument verbatim to the server. -func (s *Session) SendRaw(raw string) { - s.acts <- actionSendRaw{raw} -} - -func (s *Session) sendRaw(act actionSendRaw) (err error) { - err = s.send("%s\r\n", act.raw) - return -} - -func (s *Session) Join(channel string) { - s.acts <- actionJoin{channel} -} - -func (s *Session) join(act actionJoin) (err error) { - err = s.send("JOIN %s\r\n", act.Channel) - return -} - -func (s *Session) Part(channel, reason string) { - s.acts <- actionPart{channel, reason} -} - -func (s *Session) part(act actionPart) (err error) { - err = s.send("PART %s :%s\r\n", act.Channel, act.Reason) - return -} - -func (s *Session) SetTopic(channel, topic string) { - s.acts <- actionSetTopic{channel, topic} -} - -func (s *Session) setTopic(act actionSetTopic) (err error) { - err = s.send("TOPIC %s :%s\r\n", act.Channel, act.Topic) - return -} - -func (s *Session) Quit(reason string) { - s.acts <- actionQuit{reason} -} - -func (s *Session) quit(act actionQuit) (err error) { - err = s.send("QUIT :%s\r\n", act.Reason) - return -} - -func splitChunks(s string, chunkLen int) (chunks []string) { - if chunkLen <= 0 { - return []string{s} - } - for chunkLen < len(s) { - i := chunkLen - min := chunkLen - utf8.UTFMax - for min <= i && !utf8.RuneStart(s[i]) { - i-- - } - chunks = append(chunks, s[:i]) - s = s[i:] - } - if len(s) != 0 { - chunks = append(chunks, s) - } - return -} - -func (s *Session) ChangeNick(nick string) { - s.acts <- actionChangeNick{nick} -} - -func (s *Session) changeNick(act actionChangeNick) (err error) { - err = s.send("NICK %s\r\n", act.Nick) - return -} - -func (s *Session) ChangeMode(channel string, flags string, args []string) { - s.acts <- actionChangeMode{channel, flags, args} -} - -func (s *Session) changeMode(act actionChangeMode) (err error) { - if strings.IndexAny(act.Channel, s.chantypes) == 0 { - err = s.send("MODE %s %s %s\r\n", - act.Channel, act.Flags, strings.Join(act.Args, " ")) - } else { - err = s.send("MODE %s %s\r\n", act.Channel, act.Flags) - } - return -} - -func (s *Session) PrivMsg(target, content string) { - s.acts <- actionPrivMsg{target, content} -} - -func (s *Session) privMsg(act actionPrivMsg) (err error) { - hostLen := len(s.host) - if hostLen == 0 { - hostLen = len("255.255.255.255") - } - maxMessageLen := s.linelen - - len(":!@ PRIVMSG :\r\n") - - len(s.nick) - - len(s.user) - - hostLen - - len(act.Target) - chunks := splitChunks(act.Content, maxMessageLen) - for _, chunk := range chunks { - err = s.send("PRIVMSG %s :%s\r\n", act.Target, chunk) - if err != nil { - return - } - } - target := s.Casemap(act.Target) - delete(s.typingStamps, target) - return -} - -func (s *Session) Typing(channel string) { - s.acts <- actionTyping{channel} -} - -func (s *Session) typing(act actionTyping) (err error) { - if _, ok := s.enabledCaps["message-tags"]; !ok { - return - } - - to := s.Casemap(act.Channel) - now := time.Now() - - if t, ok := s.typingStamps[to]; ok && now.Sub(t).Seconds() < 3.0 { - return - } - - s.typingStamps[to] = now - - err = s.send("@+typing=active TAGMSG %s\r\n", act.Channel) - return -} - -func (s *Session) TypingStop(channel string) { - s.acts <- actionTypingStop{channel} -} - -func (s *Session) typingStop(act actionTypingStop) (err error) { - if _, ok := s.enabledCaps["message-tags"]; !ok { - return - } - - err = s.send("@+typing=done TAGMSG %s\r\n", act.Channel) - return -} - -func (s *Session) RequestHistory(target string, before time.Time) { - s.acts <- actionRequestHistory{target, before} -} - -func (s *Session) requestHistory(act actionRequestHistory) (err error) { - if _, ok := s.enabledCaps["draft/chathistory"]; !ok { - return - } - - target := s.Casemap(act.Target) - if _, ok := s.chReqs[target]; ok { - return - } - s.chReqs[target] = struct{}{} - - t := act.Before.UTC().Add(1 * time.Second) - 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) - - return -} - -func (s *Session) run() { - for s.Running() { - var err error - - select { - case act, ok := <-s.acts: - if !ok { - break - } - switch act := act.(type) { - case actionSendRaw: - err = s.sendRaw(act) - case actionChangeNick: - err = s.changeNick(act) - case actionChangeMode: - err = s.changeMode(act) - case actionJoin: - err = s.join(act) - case actionPart: - err = s.part(act) - case actionSetTopic: - err = s.setTopic(act) - case actionQuit: - err = s.quit(act) - case actionPrivMsg: - err = s.privMsg(act) - case actionTyping: - err = s.typing(act) - case actionTypingStop: - err = s.typingStop(act) - case actionRequestHistory: - err = s.requestHistory(act) - } - case msg, ok := <-s.msgs: - if !ok { - break - } - if s.registered { - err = s.handle(msg) - } else { - err = s.handleStart(msg) - } - case t, ok := <-s.typings.Stops(): - if !ok { - break - } - u, ok := s.users[t.Name] - if !ok { - break - } - c, ok := s.channels[t.Target] - if !ok { - break - } - s.evts <- TagEvent{ - User: u.Name, - Target: c.Name, - Typing: TypingDone, - Time: time.Now(), - } - } - - if err != nil { - s.evts <- err - } - } -} - -func (s *Session) handleStart(msg Message) (err error) { - switch msg.Command { - case "AUTHENTICATE": - if s.auth != nil { - var res string - - res, err = s.auth.Respond(msg.Params[0]) - if err != nil { - err = s.send("AUTHENTICATE *\r\n") - return - } - - err = s.send("AUTHENTICATE %s\r\n", res) - if err != nil { - return - } - } - case rplLoggedin: - err = s.send("CAP END\r\n") - if err != nil { - return - } - - s.acct = msg.Params[2] - s.host = ParsePrefix(msg.Params[1]).Host - case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs: - err = s.send("CAP END\r\n") - if err != nil { - return - } - case "CAP": - switch msg.Params[1] { - case "LS": - var willContinue bool - var ls string - - if msg.Params[2] == "*" { - willContinue = true - ls = msg.Params[3] - } else { - willContinue = false - ls = msg.Params[2] - } - - for _, c := range ParseCaps(ls) { - if c.Enable { - s.availableCaps[c.Name] = c.Value - } else { - delete(s.availableCaps, c.Name) - } - } - - if !willContinue { - var req strings.Builder - - for c := range s.availableCaps { - if _, ok := SupportedCapabilities[c]; !ok { - continue - } - - _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c) - } - - _, ok := s.availableCaps["sasl"] - if s.auth == nil || !ok { - _, _ = fmt.Fprintf(&req, "CAP END\r\n") - } - - err = s.send(req.String()) - if err != nil { - return - } - } - default: - s.handle(msg) - } - case errNicknameinuse: - err = s.send("NICK %s_\r\n", msg.Params[1]) - if err != nil { - return - } - default: - err = s.handle(msg) - } - - return -} - -func (s *Session) handle(msg Message) (err error) { - if id, ok := msg.Tags["batch"]; ok { - if b, ok := s.chBatches[id]; ok { - s.chBatches[id] = HistoryEvent{ - Target: b.Target, - Messages: append(b.Messages, s.privmsgToEvent(msg)), - } - return - } - } - - switch msg.Command { - case rplWelcome: - s.nick = msg.Params[0] - s.nickCf = s.Casemap(s.nick) - s.registered = true - s.users[s.nickCf] = &User{Name: &Prefix{ - Name: s.nick, User: s.user, Host: s.host, - }} - s.evts <- RegisteredEvent{} - - if s.host == "" { - err = s.send("WHO %s\r\n", s.nick) - if err != nil { - return - } - } - case rplIsupport: - s.updateFeatures(msg.Params[1 : len(msg.Params)-1]) - case rplWhoreply: - if s.nickCf == s.Casemap(msg.Params[5]) { - s.host = msg.Params[3] - } - case "CAP": - switch msg.Params[1] { - case "ACK": - for _, c := range strings.Split(msg.Params[2], " ") { - s.enabledCaps[c] = struct{}{} - - if s.auth != nil && c == "sasl" { - h := s.auth.Handshake() - err = s.send("AUTHENTICATE %s\r\n", h) - if err != nil { - return - } - } else if len(s.channels) != 0 && c == "multi-prefix" { - // TODO merge NAMES commands - var sb strings.Builder - sb.Grow(512) - for _, c := range s.channels { - sb.WriteString("NAMES ") - sb.WriteString(c.Name) - sb.WriteString("\r\n") - } - err = s.send(sb.String()) - if err != nil { - return - } - } - } - case "NAK": - for _, c := range strings.Split(msg.Params[2], " ") { - delete(s.enabledCaps, c) - } - case "NEW": - diff := ParseCaps(msg.Params[2]) - - for _, c := range diff { - if c.Enable { - s.availableCaps[c.Name] = c.Value - } else { - delete(s.availableCaps, c.Name) - } - } - - var req strings.Builder - - for _, c := range diff { - _, ok := SupportedCapabilities[c.Name] - if !c.Enable || !ok { - continue - } - - _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c.Name) - } - - _, ok := s.availableCaps["sasl"] - if s.acct == "" && ok { - // TODO authenticate - } - - err = s.send(req.String()) - if err != nil { - return - } - case "DEL": - diff := ParseCaps(msg.Params[2]) - - for i := range diff { - diff[i].Enable = !diff[i].Enable - } - - for _, c := range diff { - if c.Enable { - s.availableCaps[c.Name] = c.Value - } else { - delete(s.availableCaps, c.Name) - } - } - - var req strings.Builder - - for _, c := range diff { - _, ok := SupportedCapabilities[c.Name] - if !c.Enable || !ok { - continue - } - - _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c.Name) - } - - _, ok := s.availableCaps["sasl"] - if s.acct == "" && ok { - // TODO authenticate - } - - err = s.send(req.String()) - if err != nil { - return - } - } - case "JOIN": - nickCf := s.Casemap(msg.Prefix.Name) - channelCf := s.Casemap(msg.Params[0]) - - if nickCf == s.nickCf { - s.channels[channelCf] = Channel{ - Name: msg.Params[0], - Members: map[*User]string{}, - } - } else if c, ok := s.channels[channelCf]; ok { - if _, ok := s.users[nickCf]; !ok { - s.users[nickCf] = &User{Name: msg.Prefix.Copy()} - } - c.Members[s.users[nickCf]] = "" - t := msg.TimeOrNow() - - s.evts <- UserJoinEvent{ - User: msg.Prefix.Copy(), - Channel: c.Name, - Time: t, - } - } - case "PART": - nickCf := s.Casemap(msg.Prefix.Name) - channelCf := s.Casemap(msg.Params[0]) - - if nickCf == s.nickCf { - if c, ok := s.channels[channelCf]; ok { - delete(s.channels, channelCf) - for u := range c.Members { - s.cleanUser(u) - } - s.evts <- SelfPartEvent{Channel: c.Name} - } - } else if c, ok := s.channels[channelCf]; ok { - if u, ok := s.users[nickCf]; ok { - delete(c.Members, u) - s.cleanUser(u) - s.typings.Done(channelCf, nickCf) - - s.evts <- UserPartEvent{ - User: msg.Prefix.Copy(), - Channel: c.Name, - Time: msg.TimeOrNow(), - } - } - } - case "KICK": - channelCf := s.Casemap(msg.Params[0]) - nickCf := s.Casemap(msg.Params[1]) - - if nickCf == s.nickCf { - if c, ok := s.channels[channelCf]; ok { - delete(s.channels, channelCf) - for u := range c.Members { - s.cleanUser(u) - } - s.evts <- SelfPartEvent{Channel: c.Name} - } - } else if c, ok := s.channels[channelCf]; ok { - if u, ok := s.users[nickCf]; ok { - delete(c.Members, u) - s.cleanUser(u) - s.typings.Done(channelCf, nickCf) - - s.evts <- UserPartEvent{ - User: u.Name.Copy(), - Channel: c.Name, - Time: msg.TimeOrNow(), - } - } - } - case "QUIT": - nickCf := s.Casemap(msg.Prefix.Name) - - if u, ok := s.users[nickCf]; ok { - var channels []string - for channelCf, c := range s.channels { - if _, ok := c.Members[u]; ok { - channels = append(channels, c.Name) - delete(c.Members, u) - s.cleanUser(u) - s.typings.Done(channelCf, nickCf) - } - } - - s.evts <- UserQuitEvent{ - User: msg.Prefix.Copy(), - Channels: channels, - Time: msg.TimeOrNow(), - } - } - case rplNamreply: - channelCf := s.Casemap(msg.Params[2]) - - if c, ok := s.channels[channelCf]; ok { - c.Secret = msg.Params[1] == "@" - - // TODO compute CHANTYPES - for _, name := range ParseNameReply(msg.Params[3], "~&@%+") { - nickCf := s.Casemap(name.Name.Name) - - if _, ok := s.users[nickCf]; !ok { - s.users[nickCf] = &User{Name: name.Name.Copy()} - } - c.Members[s.users[nickCf]] = name.PowerLevel - } - - s.channels[channelCf] = c - } - case rplEndofnames: - channelCf := s.Casemap(msg.Params[1]) - if c, ok := s.channels[channelCf]; ok && !c.complete { - c.complete = true - s.channels[channelCf] = c - s.evts <- SelfJoinEvent{Channel: c.Name} - } - case rplTopic: - channelCf := s.Casemap(msg.Params[1]) - if c, ok := s.channels[channelCf]; ok { - c.Topic = msg.Params[2] - s.channels[channelCf] = c - } - case rplTopicwhotime: - channelCf := s.Casemap(msg.Params[1]) - t, _ := strconv.ParseInt(msg.Params[3], 10, 64) - if c, ok := s.channels[channelCf]; ok { - c.TopicWho = ParsePrefix(msg.Params[2]) - c.TopicTime = time.Unix(t, 0) - s.channels[channelCf] = c - } - case rplNotopic: - channelCf := s.Casemap(msg.Params[1]) - if c, ok := s.channels[channelCf]; ok { - c.Topic = "" - s.channels[channelCf] = c - } - case "TOPIC": - channelCf := s.Casemap(msg.Params[0]) - if c, ok := s.channels[channelCf]; ok { - c.Topic = msg.Params[1] - c.TopicWho = msg.Prefix.Copy() - c.TopicTime = msg.TimeOrNow() - s.channels[channelCf] = c - s.evts <- TopicChangeEvent{ - User: msg.Prefix.Copy(), - Channel: c.Name, - Topic: c.Topic, - Time: c.TopicTime, - } - } - case "PRIVMSG", "NOTICE": - s.evts <- s.privmsgToEvent(msg) - case "TAGMSG": - nickCf := s.Casemap(msg.Prefix.Name) - targetCf := s.Casemap(msg.Params[0]) - - if nickCf == s.nickCf { - // TAGMSG from self - break - } - - typing := TypingUnspec - if t, ok := msg.Tags["+typing"]; ok { - if t == "active" { - typing = TypingActive - s.typings.Active(targetCf, nickCf) - } else if t == "paused" { - typing = TypingPaused - s.typings.Active(targetCf, nickCf) - } else if t == "done" { - typing = TypingDone - s.typings.Done(targetCf, nickCf) - } - } else { - break - } - - ev := TagEvent{ - User: msg.Prefix.Copy(), // TODO correctly casemap - Target: msg.Params[0], // TODO correctly casemap - Typing: typing, - Time: msg.TimeOrNow(), - } - if c, ok := s.channels[targetCf]; ok { - ev.Target = c.Name - ev.TargetIsChannel = true - } - s.evts <- ev - case "BATCH": - batchStart := msg.Params[0][0] == '+' - id := msg.Params[0][1:] - - if batchStart && msg.Params[1] == "chathistory" { - s.chBatches[id] = HistoryEvent{Target: msg.Params[2]} - } else if b, ok := s.chBatches[id]; ok { - s.evts <- b - delete(s.chBatches, id) - delete(s.chReqs, s.Casemap(b.Target)) - } - case "NICK": - nickCf := s.Casemap(msg.Prefix.Name) - newNick := msg.Params[0] - newNickCf := s.Casemap(newNick) - t := msg.TimeOrNow() - - var u *Prefix - if formerUser, ok := s.users[nickCf]; ok { - formerUser.Name.Name = newNick - delete(s.users, nickCf) - s.users[newNickCf] = formerUser - u = formerUser.Name.Copy() - } else { - break - } - - if nickCf == s.nickCf { - s.evts <- SelfNickEvent{ - FormerNick: s.nick, - Time: t, - } - s.nick = newNick - s.nickCf = newNickCf - } else { - s.evts <- UserNickEvent{ - User: u, - FormerNick: msg.Prefix.Name, - Time: t, - } - } - case "FAIL": - s.evts <- ErrorEvent{ - Severity: SeverityFail, - Code: msg.Params[1], - Message: msg.Params[len(msg.Params)-1], - } - case "WARN": - s.evts <- ErrorEvent{ - Severity: SeverityWarn, - Code: msg.Params[1], - Message: msg.Params[len(msg.Params)-1], - } - case "NOTE": - s.evts <- ErrorEvent{ - Severity: SeverityNote, - Code: msg.Params[1], - Message: msg.Params[len(msg.Params)-1], - } - case "PING": - err = s.send("PONG :%s\r\n", msg.Params[0]) - if err != nil { - return - } - case "ERROR": - err = errors.New("connection terminated") - if len(msg.Params) > 0 { - err = fmt.Errorf("connection terminated: %s", msg.Params[0]) - } - _ = s.conn.Close() - default: - // reply handling - if ReplySeverity(msg.Command) == SeverityFail { - s.evts <- ErrorEvent{ - Severity: SeverityFail, - Code: msg.Command, - Message: msg.Params[len(msg.Params)-1], - } - } - } - - return -} - -func (s *Session) privmsgToEvent(msg Message) (ev MessageEvent) { - targetCf := s.Casemap(msg.Params[0]) - - s.typings.Done(targetCf, s.Casemap(msg.Prefix.Name)) - ev = MessageEvent{ - User: msg.Prefix.Copy(), // TODO correctly casemap - Target: msg.Params[0], // TODO correctly casemap - Command: msg.Command, - Content: msg.Params[1], - Time: msg.TimeOrNow(), - } - if c, ok := s.channels[targetCf]; ok { - ev.Target = c.Name - ev.TargetIsChannel = true - } - - return -} - -func (s *Session) cleanUser(parted *User) { - for _, c := range s.channels { - if _, ok := c.Members[parted]; ok { - return - } - } - delete(s.users, s.Casemap(parted.Name.Name)) -} - -func (s *Session) updateFeatures(features []string) { - for _, f := range features { - if f == "" || f == "-" || f == "=" || f == "-=" { - continue - } - - var ( - add bool - key string - value string - ) - - if strings.HasPrefix(f, "-") { - add = false - f = f[1:] - } else { - add = true - } - - kv := strings.SplitN(f, "=", 2) - key = strings.ToUpper(kv[0]) - if len(kv) > 1 { - value = kv[1] - } - - if !add { - // TODO support ISUPPORT negations - continue - } - - switch key { - case "CASEMAPPING": - switch value { - case "ascii": - s.casemap = CasemapASCII - default: - s.casemap = CasemapRFC1459 - } - case "CHANTYPES": - s.chantypes = value - case "LINELEN": - linelen, err := strconv.Atoi(value) - if err == nil && linelen != 0 { - s.linelen = linelen - } - } - } -} - -func (s *Session) send(format string, args ...interface{}) (err error) { - msg := fmt.Sprintf(format, args...) - - s.conn.SetWriteDeadline(time.Now().Add(writeDeadline)) - _, err = s.conn.Write([]byte(msg)) - - if s.debug { - for _, line := range strings.Split(msg, "\r\n") { - if line != "" { - s.evts <- RawMessageEvent{ - Message: line, - Outgoing: true, - } - } - } - } - - return -} |