From e91396fe94643b2745e6350210d3637d9770d5b4 Mon Sep 17 00:00:00 2001 From: Hubert Hirtz Date: Tue, 25 Aug 2020 23:16:54 +0200 Subject: General refactor yay - Centralize message formatting - Make line formatting more flexible - Provide more information in irc events - Refactor command handling - Add a /help command - Make /me reply to last query if from home - Enforce argument for /me - Make BufferList and Editor public - Batch processing of IRC events --- irc/events.go | 49 ++++++-------- irc/states.go | 207 ++++++++++++++++++++++++++++++---------------------------- irc/tokens.go | 127 +++++++++++++++++++++++------------ 3 files changed, 211 insertions(+), 172 deletions(-) (limited to 'irc') diff --git a/irc/events.go b/irc/events.go index cd26369..62622b4 100644 --- a/irc/events.go +++ b/irc/events.go @@ -16,13 +16,12 @@ type RegisteredEvent struct{} type SelfNickEvent struct { FormerNick string - NewNick string Time time.Time } type UserNickEvent struct { + User *Prefix FormerNick string - NewNick string Time time.Time } @@ -31,7 +30,7 @@ type SelfJoinEvent struct { } type UserJoinEvent struct { - Nick string + User *Prefix Channel string Time time.Time } @@ -41,38 +40,32 @@ type SelfPartEvent struct { } type UserPartEvent struct { - Nick string - Channels []string - Time time.Time -} - -type QueryMessageEvent struct { - Nick string - Target string - Command string - Content string + User *Prefix + Channel string Time time.Time } -type ChannelMessageEvent struct { - Nick string - Channel string - Command string - Content string - Time time.Time +type UserQuitEvent struct { + User *Prefix + Channels []string + Time time.Time } -type QueryTagEvent struct { - Nick string - Typing int - Time time.Time +type MessageEvent struct { + User *Prefix + Target string + TargetIsChannel bool + Command string + Content string + Time time.Time } -type ChannelTagEvent struct { - Nick string - Channel string - Typing int - Time time.Time +type TagEvent struct { + User *Prefix + Target string + TargetIsChannel bool + Typing int + Time time.Time } type HistoryEvent struct { diff --git a/irc/states.go b/irc/states.go index 2628a9e..8e08275 100644 --- a/irc/states.go +++ b/irc/states.go @@ -106,7 +106,7 @@ type ( ) type User struct { - Nick string + Name *Prefix AwayMsg string } @@ -114,7 +114,7 @@ type Channel struct { Name string Members map[*User]string Topic string - TopicWho string + TopicWho *Prefix TopicTime time.Time Secret bool } @@ -161,13 +161,13 @@ type Session struct { func NewSession(conn io.ReadWriteCloser, params SessionParams) (*Session, error) { s := &Session{ conn: conn, - msgs: make(chan Message, 16), - acts: make(chan action, 16), - evts: make(chan Event, 16), + msgs: make(chan Message, 64), + acts: make(chan action, 64), + evts: make(chan Event, 64), debug: params.Debug, typingStamps: map[string]time.Time{}, nick: params.Nickname, - nickCf: strings.ToLower(params.Nickname), + nickCf: CasemapASCII(params.Nickname), user: params.Username, real: params.RealName, auth: params.Auth, @@ -191,7 +191,7 @@ func NewSession(conn io.ReadWriteCloser, params SessionParams) (*Session, error) for r.Scan() { line := r.Text() - msg, err := Tokenize(line) + msg, err := ParseMessage(line) if err != nil { continue } @@ -248,22 +248,43 @@ func (s *Session) IsChannel(name string) bool { return strings.IndexAny(name, "#&") == 0 // TODO compute CHANTYPES } -func (s *Session) Names(channel string) []Name { - var names []Name - if c, ok := s.channels[strings.ToLower(channel)]; ok { - names = make([]Name, 0, len(c.Members)) +func (s *Session) Casemap(name string) string { + // TODO use CASEMAPPING + return CasemapASCII(name) +} + +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, Name{ + names = append(names, Member{ PowerLevel: pl, - Nick: u.Nick, + Name: u.Name.Copy(), }) } } return names } -func (s *Session) Topic(channel string) (topic string, who string, at time.Time) { - channelCf := strings.ToLower(channel) +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 @@ -326,7 +347,7 @@ func (s *Session) typing(act actionTyping) (err error) { return } - to := strings.ToLower(act.Channel) + to := s.Casemap(act.Channel) now := time.Now() if t, ok := s.typingStamps[to]; ok && now.Sub(t).Seconds() < 3.0 { @@ -429,7 +450,7 @@ func (s *Session) handleStart(msg Message) (err error) { } s.acct = msg.Params[2] - _, _, s.host = FullMask(msg.Params[1]) + s.host = ParsePrefix(msg.Params[1]).Host case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs: err = s.send("CAP END\r\n") if err != nil { @@ -449,7 +470,7 @@ func (s *Session) handleStart(msg Message) (err error) { ls = msg.Params[2] } - for _, c := range TokenizeCaps(ls) { + for _, c := range ParseCaps(ls) { if c.Enable { s.availableCaps[c.Name] = c.Value } else { @@ -507,9 +528,11 @@ func (s *Session) handle(msg Message) (err error) { switch msg.Command { case rplWelcome: s.nick = msg.Params[0] - s.nickCf = strings.ToLower(s.nick) + s.nickCf = s.Casemap(s.nick) s.registered = true - s.users[s.nickCf] = &User{Nick: s.nick} + s.users[s.nickCf] = &User{Name: &Prefix{ + Name: s.nick, User: s.user, Host: s.host, + }} s.evts <- RegisteredEvent{} if s.host == "" { @@ -521,7 +544,7 @@ func (s *Session) handle(msg Message) (err error) { case rplIsupport: s.updateFeatures(msg.Params[1 : len(msg.Params)-1]) case rplWhoreply: - if s.nickCf == strings.ToLower(msg.Params[5]) { + if s.nickCf == s.Casemap(msg.Params[5]) { s.host = msg.Params[3] } case "CAP": @@ -556,7 +579,7 @@ func (s *Session) handle(msg Message) (err error) { delete(s.enabledCaps, c) } case "NEW": - diff := TokenizeCaps(msg.Params[2]) + diff := ParseCaps(msg.Params[2]) for _, c := range diff { if c.Enable { @@ -587,7 +610,7 @@ func (s *Session) handle(msg Message) (err error) { return } case "DEL": - diff := TokenizeCaps(msg.Params[2]) + diff := ParseCaps(msg.Params[2]) for i := range diff { diff[i].Enable = !diff[i].Enable @@ -623,9 +646,8 @@ func (s *Session) handle(msg Message) (err error) { } } case "JOIN": - nick, _, _ := FullMask(msg.Prefix) - nickCf := strings.ToLower(nick) - channelCf := strings.ToLower(msg.Params[0]) + nickCf := s.Casemap(msg.Prefix.Name) + channelCf := s.Casemap(msg.Params[0]) if nickCf == s.nickCf { s.channels[channelCf] = Channel{ @@ -635,21 +657,20 @@ func (s *Session) handle(msg Message) (err error) { s.evts <- SelfJoinEvent{Channel: msg.Params[0]} } else if c, ok := s.channels[channelCf]; ok { if _, ok := s.users[nickCf]; !ok { - s.users[nickCf] = &User{Nick: nick} + 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, - Nick: nick, Time: t, } } case "PART": - nick, _, _ := FullMask(msg.Prefix) - nickCf := strings.ToLower(nick) - channelCf := strings.ToLower(msg.Params[0]) + nickCf := s.Casemap(msg.Prefix.Name) + channelCf := s.Casemap(msg.Params[0]) if nickCf == s.nickCf { if c, ok := s.channels[channelCf]; ok { @@ -666,15 +687,14 @@ func (s *Session) handle(msg Message) (err error) { t := msg.TimeOrNow() s.evts <- UserPartEvent{ - Channels: []string{c.Name}, - Nick: nick, - Time: t, + User: msg.Prefix.Copy(), + Channel: c.Name, + Time: t, } } } case "QUIT": - nick, _, _ := FullMask(msg.Prefix) - nickCf := strings.ToLower(nick) + nickCf := s.Casemap(msg.Prefix.Name) if u, ok := s.users[nickCf]; ok { t := msg.TimeOrNow() @@ -687,24 +707,24 @@ func (s *Session) handle(msg Message) (err error) { } } - s.evts <- UserPartEvent{ + s.evts <- UserQuitEvent{ + User: msg.Prefix.Copy(), Channels: channels, - Nick: nick, Time: t, } } case rplNamreply: - channelCf := strings.ToLower(msg.Params[2]) + channelCf := s.Casemap(msg.Params[2]) if c, ok := s.channels[channelCf]; ok { c.Secret = msg.Params[1] == "@" - for _, name := range TokenizeNames(msg.Params[3], "~&@%+") { - nick := name.Nick - nickCf := strings.ToLower(nick) + // 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{Nick: nick} + s.users[nickCf] = &User{Name: name.Name.Copy()} } c.Members[s.users[nickCf]] = name.PowerLevel } @@ -712,40 +732,38 @@ func (s *Session) handle(msg Message) (err error) { s.channels[channelCf] = c } case rplTopic: - channelCf := strings.ToLower(msg.Params[1]) + 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 := strings.ToLower(msg.Params[1]) + channelCf := s.Casemap(msg.Params[1]) t, _ := strconv.ParseInt(msg.Params[3], 10, 64) if c, ok := s.channels[channelCf]; ok { - c.TopicWho, _, _ = FullMask(msg.Params[2]) + c.TopicWho = ParsePrefix(msg.Params[2]) c.TopicTime = time.Unix(t, 0) s.channels[channelCf] = c } case rplNotopic: - channelCf := strings.ToLower(msg.Params[1]) + channelCf := s.Casemap(msg.Params[1]) if c, ok := s.channels[channelCf]; ok { c.Topic = "" s.channels[channelCf] = c } case "TOPIC": - nick, _, _ := FullMask(msg.Prefix) - channelCf := strings.ToLower(msg.Params[0]) + channelCf := s.Casemap(msg.Params[0]) if c, ok := s.channels[channelCf]; ok { c.Topic = msg.Params[1] - c.TopicWho = nick + c.TopicWho = msg.Prefix.Copy() c.TopicTime = msg.TimeOrNow() s.channels[channelCf] = c } case "PRIVMSG", "NOTICE": s.evts <- s.privmsgToEvent(msg) case "TAGMSG": - nick, _, _ := FullMask(msg.Prefix) - nickCf := strings.ToLower(nick) - targetCf := strings.ToLower(msg.Params[0]) + nickCf := s.Casemap(msg.Prefix.Name) + targetCf := s.Casemap(msg.Params[0]) if nickCf == s.nickCf { // TAGMSG from self @@ -765,23 +783,17 @@ func (s *Session) handle(msg Message) (err error) { break } - t := msg.TimeOrNow() - if targetCf == s.nickCf { - // TAGMSG to self - s.evts <- QueryTagEvent{ - Nick: nick, - Typing: typing, - Time: t, - } - } else if c, ok := s.channels[targetCf]; ok { - // TAGMSG to channelCf - s.evts <- ChannelTagEvent{ - Nick: nick, - Channel: c.Name, - Typing: typing, - Time: t, - } + 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:] @@ -793,35 +805,38 @@ func (s *Session) handle(msg Message) (err error) { delete(s.chBatches, id) } case "NICK": - nick, _, _ := FullMask(msg.Prefix) - nickCf := strings.ToLower(nick) + nickCf := s.Casemap(msg.Prefix.Name) newNick := msg.Params[0] - newNickCf := strings.ToLower(newNick) + newNickCf := s.Casemap(newNick) t := msg.TimeOrNow() + var u *Prefix if formerUser, ok := s.users[nickCf]; ok { - formerUser.Nick = newNick + formerUser.Name.Name = newNick delete(s.users, nickCf) s.users[newNickCf] = formerUser + u = formerUser.Name.Copy() } if nickCf == s.nickCf { s.evts <- SelfNickEvent{ FormerNick: s.nick, - NewNick: newNick, Time: t, } s.nick = newNick s.nickCf = newNickCf } else { s.evts <- UserNickEvent{ - FormerNick: nick, - NewNick: newNick, + User: u, + FormerNick: msg.Prefix.Name, Time: t, } } case "FAIL": - fmt.Println("FAIL", msg.Params) + s.evts <- RawMessageEvent{ + Message: msg.String(), + IsValid: true, + } case "PING": err = s.send("PONG :%s\r\n", msg.Params[0]) if err != nil { @@ -839,29 +854,19 @@ func (s *Session) handle(msg Message) (err error) { return } -func (s *Session) privmsgToEvent(msg Message) (ev Event) { - nick, _, _ := FullMask(msg.Prefix) - targetCf := strings.ToLower(msg.Params[0]) - t := msg.TimeOrNow() - - if !s.IsChannel(targetCf) { - // PRIVMSG to self - ev = QueryMessageEvent{ - Nick: nick, - Target: msg.Params[0], - Command: msg.Command, - Content: msg.Params[1], - Time: t, - } - } else if c, ok := s.channels[targetCf]; ok { - // PRIVMSG to channel - ev = ChannelMessageEvent{ - Nick: nick, - Channel: c.Name, - Command: msg.Command, - Content: msg.Params[1], - Time: t, - } +func (s *Session) privmsgToEvent(msg Message) (ev MessageEvent) { + targetCf := s.Casemap(msg.Params[0]) + + 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 @@ -873,7 +878,7 @@ func (s *Session) cleanUser(parted *User) { return } } - delete(s.users, strings.ToLower(parted.Nick)) + delete(s.users, s.Casemap(parted.Name.Name)) } func (s *Session) updateFeatures(features []string) { diff --git a/irc/tokens.go b/irc/tokens.go index 57fd266..cee2f5e 100644 --- a/irc/tokens.go +++ b/irc/tokens.go @@ -8,6 +8,18 @@ import ( "time" ) +func CasemapASCII(name string) string { + var sb strings.Builder + sb.Grow(len(name)) + for _, r := range name { + if 'A' <= r && r <= 'Z' { + r += 'a' - 'A' + } + sb.WriteRune(r) + } + return sb.String() +} + func word(s string) (w, rest string) { split := strings.SplitN(s, " ", 2) @@ -125,14 +137,67 @@ var ( errUnknownCommand = errors.New("unknown command") ) +type Prefix struct { + Name string + User string + Host string +} + +func ParsePrefix(s string) (p *Prefix) { + if s == "" { + return + } + + p = &Prefix{} + + spl0 := strings.Split(s, "@") + if 1 < len(spl0) { + p.Host = spl0[1] + } + + spl1 := strings.Split(spl0[0], "!") + if 1 < len(spl1) { + p.User = spl1[1] + } + + p.Name = spl1[0] + + return +} + +func (p *Prefix) Copy() *Prefix { + if p == nil { + return nil + } + res := &Prefix{} + *res = *p + return res +} + +func (p *Prefix) String() string { + if p == nil { + return "" + } + + if p.User != "" && p.Host != "" { + return p.Name + "!" + p.User + "@" + p.Host + } else if p.User != "" { + return p.Name + "!" + p.User + } else if p.Host != "" { + return p.Name + "@" + p.Host + } else { + return p.Name + } +} + type Message struct { Tags map[string]string - Prefix string + Prefix *Prefix Command string Params []string } -func Tokenize(line string) (msg Message, err error) { +func ParseMessage(line string) (msg Message, err error) { line = strings.TrimLeft(line, " ") if line == "" { err = errEmptyMessage @@ -156,7 +221,7 @@ func Tokenize(line string) (msg Message, err error) { var prefix string prefix, line = word(line) - msg.Prefix = prefix[1:] + msg.Prefix = ParsePrefix(prefix[1:]) } line = strings.TrimLeft(line, " ") @@ -211,9 +276,9 @@ func (msg *Message) String() string { sb.WriteRune(' ') } - if msg.Prefix != "" { + if msg.Prefix != nil { sb.WriteRune(':') - sb.WriteString(msg.Prefix) + sb.WriteString(msg.Prefix.String()) sb.WriteRune(' ') } @@ -245,11 +310,11 @@ func (msg *Message) IsValid() bool { case rplWhoreply: return 8 <= len(msg.Params) case "JOIN", "NICK", "PART", "TAGMSG": - return 1 <= len(msg.Params) && msg.Prefix != "" + return 1 <= len(msg.Params) && msg.Prefix != nil case "PRIVMSG", "NOTICE", "TOPIC": - return 2 <= len(msg.Params) && msg.Prefix != "" + return 2 <= len(msg.Params) && msg.Prefix != nil case "QUIT": - return msg.Prefix != "" + return msg.Prefix != nil case "CAP": return 3 <= len(msg.Params) && (msg.Params[1] == "LS" || @@ -317,33 +382,13 @@ func (msg *Message) TimeOrNow() time.Time { return time.Now() } -func FullMask(s string) (nick, user, host string) { - if s == "" { - return - } - - spl0 := strings.Split(s, "@") - if 1 < len(spl0) { - host = spl0[1] - } - - spl1 := strings.Split(spl0[0], "!") - if 1 < len(spl1) { - user = spl1[1] - } - - nick = spl1[0] - - return -} - type Cap struct { Name string Value string Enable bool } -func TokenizeCaps(caps string) (diff []Cap) { +func ParseCaps(caps string) (diff []Cap) { for _, c := range strings.Split(caps, " ") { if c == "" || c == "-" || c == "=" || c == "-=" { continue @@ -370,26 +415,22 @@ func TokenizeCaps(caps string) (diff []Cap) { return } -type Name struct { +type Member struct { PowerLevel string - Nick string - User string - Host string + Name *Prefix } -func TokenizeNames(trailing string, prefixes string) (names []Name) { - for _, name := range strings.Split(trailing, " ") { - if name == "" { +func ParseNameReply(trailing string, prefixes string) (names []Member) { + for _, word := range strings.Split(trailing, " ") { + if word == "" { continue } - var item Name - - mask := strings.TrimLeft(name, prefixes) - item.Nick, item.User, item.Host = FullMask(mask) - item.PowerLevel = name[:len(name)-len(mask)] - - names = append(names, item) + name := strings.TrimLeft(word, prefixes) + names = append(names, Member{ + PowerLevel: word[:len(word)-len(name)], + Name: ParsePrefix(name), + }) } return -- cgit v1.2.3