summaryrefslogtreecommitdiff
path: root/irc
diff options
context:
space:
mode:
authorHubert Hirtz <hubert@hirtzfr.eu>2020-08-25 23:16:54 +0200
committerHubert Hirtz <hubert@hirtzfr.eu>2020-08-26 17:53:40 +0200
commite91396fe94643b2745e6350210d3637d9770d5b4 (patch)
tree23fdbd7a0eb9ae7d6409c0d0aed31f4512d51bc8 /irc
parentAllow /part with a reason (diff)
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
Diffstat (limited to 'irc')
-rw-r--r--irc/events.go49
-rw-r--r--irc/states.go207
-rw-r--r--irc/tokens.go127
3 files changed, 211 insertions, 172 deletions
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