diff --git a/bridge.go b/bridge.go index 845e337..82c0301 100644 --- a/bridge.go +++ b/bridge.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "strconv" - "strings" "gopkg.in/irc.v3" @@ -74,47 +73,16 @@ func sendTopic(dc *downstreamConn, ch *upstreamChannel) { func sendNames(dc *downstreamConn, ch *upstreamChannel) { downstreamName := dc.marshalEntity(ch.conn.network, ch.Name) - emptyNameReply := &irc.Message{ - Prefix: dc.srv.prefix(), - Command: irc.RPL_NAMREPLY, - Params: []string{dc.nick, string(ch.Status), downstreamName, ""}, - } - maxLength := maxMessageLength - len(emptyNameReply.String()) - - var buf strings.Builder + var members []string for _, entry := range ch.Members.innerMap { nick := entry.originalKey memberships := entry.value.(*memberships) s := memberships.Format(dc) + dc.marshalEntity(ch.conn.network, nick) - - n := buf.Len() + 1 + len(s) - if buf.Len() != 0 && n > maxLength { - // There's not enough space for the next space + nick. - dc.SendMessage(&irc.Message{ - Prefix: dc.srv.prefix(), - Command: irc.RPL_NAMREPLY, - Params: []string{dc.nick, string(ch.Status), downstreamName, buf.String()}, - }) - buf.Reset() - } - - if buf.Len() != 0 { - buf.WriteByte(' ') - } - buf.WriteString(s) + members = append(members, s) } - if buf.Len() != 0 { - dc.SendMessage(&irc.Message{ - Prefix: dc.srv.prefix(), - Command: irc.RPL_NAMREPLY, - Params: []string{dc.nick, string(ch.Status), downstreamName, buf.String()}, - }) + msgs := xirc.GenerateNamesReply(dc.srv.prefix(), dc.nick, downstreamName, ch.Status, members) + for _, msg := range msgs { + dc.SendMessage(msg) } - - dc.SendMessage(&irc.Message{ - Prefix: dc.srv.prefix(), - Command: irc.RPL_ENDOFNAMES, - Params: []string{dc.nick, downstreamName, "End of /NAMES list"}, - }) } diff --git a/downstream.go b/downstream.go index c4985ff..d799878 100644 --- a/downstream.go +++ b/downstream.go @@ -1531,7 +1531,7 @@ func (dc *downstreamConn) welcome(ctx context.Context) error { Command: irc.RPL_MYINFO, Params: []string{dc.nick, dc.srv.Config().Hostname, "soju", "aiwroO", "OovaimnqpsrtklbeI"}, }) - for _, msg := range generateIsupport(dc.srv.prefix(), dc.nick, isupport) { + for _, msg := range xirc.GenerateIsupport(dc.srv.prefix(), dc.nick, isupport) { dc.SendMessage(msg) } if uc := dc.upstream(); uc != nil { @@ -1554,7 +1554,7 @@ func (dc *downstreamConn) welcome(ctx context.Context) error { dc.updateAccount() if motd := dc.user.srv.Config().MOTD; motd != "" && dc.network == nil { - for _, msg := range generateMOTD(dc.srv.prefix(), dc.nick, motd) { + for _, msg := range xirc.GenerateMOTD(dc.srv.prefix(), dc.nick, motd) { dc.SendMessage(msg) } } else { diff --git a/irc.go b/irc.go index 9c51d52..df11f87 100644 --- a/irc.go +++ b/irc.go @@ -2,7 +2,6 @@ package soju import ( "fmt" - "sort" "strings" "time" "unicode" @@ -16,11 +15,7 @@ import ( // TODO: generalize and move helpers to the xirc package -const ( - maxMessageLength = 512 - maxMessageParams = 15 - maxSASLLength = 400 -) +const maxSASLLength = 400 type userModes string @@ -280,158 +275,6 @@ type batch struct { Label string } -func generateJoin(channels, keys []string) []*irc.Message { - // Put channels with a key first - js := joinSorter{channels, keys} - sort.Sort(&js) - - // Two spaces because there are three words (JOIN, channels and keys) - maxLength := maxMessageLength - (len("JOIN") + 2) - - var msgs []*irc.Message - var channelsBuf, keysBuf strings.Builder - for i, channel := range channels { - key := keys[i] - - n := channelsBuf.Len() + keysBuf.Len() + 1 + len(channel) - if key != "" { - n += 1 + len(key) - } - - if channelsBuf.Len() > 0 && n > maxLength { - // No room for the new channel in this message - params := []string{channelsBuf.String()} - if keysBuf.Len() > 0 { - params = append(params, keysBuf.String()) - } - msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params}) - channelsBuf.Reset() - keysBuf.Reset() - } - - if channelsBuf.Len() > 0 { - channelsBuf.WriteByte(',') - } - channelsBuf.WriteString(channel) - if key != "" { - if keysBuf.Len() > 0 { - keysBuf.WriteByte(',') - } - keysBuf.WriteString(key) - } - } - if channelsBuf.Len() > 0 { - params := []string{channelsBuf.String()} - if keysBuf.Len() > 0 { - params = append(params, keysBuf.String()) - } - msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params}) - } - - return msgs -} - -func generateIsupport(prefix *irc.Prefix, nick string, tokens []string) []*irc.Message { - maxTokens := maxMessageParams - 2 // 2 reserved params: nick + text - - var msgs []*irc.Message - for len(tokens) > 0 { - var msgTokens []string - if len(tokens) > maxTokens { - msgTokens = tokens[:maxTokens] - tokens = tokens[maxTokens:] - } else { - msgTokens = tokens - tokens = nil - } - - msgs = append(msgs, &irc.Message{ - Prefix: prefix, - Command: irc.RPL_ISUPPORT, - Params: append(append([]string{nick}, msgTokens...), "are supported"), - }) - } - - return msgs -} - -func generateMOTD(prefix *irc.Prefix, nick string, motd string) []*irc.Message { - var msgs []*irc.Message - msgs = append(msgs, &irc.Message{ - Prefix: prefix, - Command: irc.RPL_MOTDSTART, - Params: []string{nick, fmt.Sprintf("- Message of the Day -")}, - }) - - for _, l := range strings.Split(motd, "\n") { - msgs = append(msgs, &irc.Message{ - Prefix: prefix, - Command: irc.RPL_MOTD, - Params: []string{nick, l}, - }) - } - - msgs = append(msgs, &irc.Message{ - Prefix: prefix, - Command: irc.RPL_ENDOFMOTD, - Params: []string{nick, "End of /MOTD command."}, - }) - - return msgs -} - -func generateMonitor(subcmd string, targets []string) []*irc.Message { - maxLength := maxMessageLength - len("MONITOR "+subcmd+" ") - - var msgs []*irc.Message - var buf []string - n := 0 - for _, target := range targets { - if n+len(target)+1 > maxLength { - msgs = append(msgs, &irc.Message{ - Command: "MONITOR", - Params: []string{subcmd, strings.Join(buf, ",")}, - }) - buf = buf[:0] - n = 0 - } - - buf = append(buf, target) - n += len(target) + 1 - } - - if len(buf) > 0 { - msgs = append(msgs, &irc.Message{ - Command: "MONITOR", - Params: []string{subcmd, strings.Join(buf, ",")}, - }) - } - - return msgs -} - -type joinSorter struct { - channels []string - keys []string -} - -func (js *joinSorter) Len() int { - return len(js.channels) -} - -func (js *joinSorter) Less(i, j int) bool { - if (js.keys[i] != "") != (js.keys[j] != "") { - // Only one of the channels has a key - return js.keys[i] != "" - } - return js.channels[i] < js.channels[j] -} - -func (js *joinSorter) Swap(i, j int) { - js.channels[i], js.channels[j] = js.channels[j], js.channels[i] - js.keys[i], js.keys[j] = js.keys[j], js.keys[i] -} - type casemapping func(string) string func casemapNone(name string) string { diff --git a/upstream.go b/upstream.go index bc597bf..479d4ee 100644 --- a/upstream.go +++ b/upstream.go @@ -773,7 +773,7 @@ func (uc *upstreamConn) handleMessage(ctx context.Context, msg *irc.Message) err keys = append(keys, ch.Key) } - for _, msg := range generateJoin(channels, keys) { + for _, msg := range xirc.GenerateJoin(channels, keys) { uc.SendMessage(ctx, msg) } } @@ -852,7 +852,7 @@ func (uc *upstreamConn) handleMessage(ctx context.Context, msg *irc.Message) err if dc.network == nil { return } - msgs := generateIsupport(dc.srv.prefix(), dc.nick, downstreamIsupport) + msgs := xirc.GenerateIsupport(dc.srv.prefix(), dc.nick, downstreamIsupport) for _, msg := range msgs { dc.SendMessage(msg) } @@ -2223,8 +2223,8 @@ func (uc *upstreamConn) updateMonitor() { Params: []string{"C"}, }) } else { - msgs := generateMonitor("-", removeList) - msgs = append(msgs, generateMonitor("+", addList)...) + msgs := xirc.GenerateMonitor("-", removeList) + msgs = append(msgs, xirc.GenerateMonitor("+", addList)...) for _, msg := range msgs { uc.SendMessage(ctx, msg) } diff --git a/xirc/genmsg.go b/xirc/genmsg.go new file mode 100644 index 0000000..134bf0c --- /dev/null +++ b/xirc/genmsg.go @@ -0,0 +1,205 @@ +package xirc + +import ( + "fmt" + "sort" + "strings" + + "gopkg.in/irc.v3" +) + +func GenerateJoin(channels, keys []string) []*irc.Message { + // Put channels with a key first + js := joinSorter{channels, keys} + sort.Sort(&js) + + // Two spaces because there are three words (JOIN, channels and keys) + maxLength := maxMessageLength - (len("JOIN") + 2) + + var msgs []*irc.Message + var channelsBuf, keysBuf strings.Builder + for i, channel := range channels { + key := keys[i] + + n := channelsBuf.Len() + keysBuf.Len() + 1 + len(channel) + if key != "" { + n += 1 + len(key) + } + + if channelsBuf.Len() > 0 && n > maxLength { + // No room for the new channel in this message + params := []string{channelsBuf.String()} + if keysBuf.Len() > 0 { + params = append(params, keysBuf.String()) + } + msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params}) + channelsBuf.Reset() + keysBuf.Reset() + } + + if channelsBuf.Len() > 0 { + channelsBuf.WriteByte(',') + } + channelsBuf.WriteString(channel) + if key != "" { + if keysBuf.Len() > 0 { + keysBuf.WriteByte(',') + } + keysBuf.WriteString(key) + } + } + if channelsBuf.Len() > 0 { + params := []string{channelsBuf.String()} + if keysBuf.Len() > 0 { + params = append(params, keysBuf.String()) + } + msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params}) + } + + return msgs +} + +type joinSorter struct { + channels []string + keys []string +} + +func (js *joinSorter) Len() int { + return len(js.channels) +} + +func (js *joinSorter) Less(i, j int) bool { + if (js.keys[i] != "") != (js.keys[j] != "") { + // Only one of the channels has a key + return js.keys[i] != "" + } + return js.channels[i] < js.channels[j] +} + +func (js *joinSorter) Swap(i, j int) { + js.channels[i], js.channels[j] = js.channels[j], js.channels[i] + js.keys[i], js.keys[j] = js.keys[j], js.keys[i] +} + +func GenerateIsupport(prefix *irc.Prefix, nick string, tokens []string) []*irc.Message { + maxTokens := maxMessageParams - 2 // 2 reserved params: nick + text + + var msgs []*irc.Message + for len(tokens) > 0 { + var msgTokens []string + if len(tokens) > maxTokens { + msgTokens = tokens[:maxTokens] + tokens = tokens[maxTokens:] + } else { + msgTokens = tokens + tokens = nil + } + + msgs = append(msgs, &irc.Message{ + Prefix: prefix, + Command: irc.RPL_ISUPPORT, + Params: append(append([]string{nick}, msgTokens...), "are supported"), + }) + } + + return msgs +} + +func GenerateMOTD(prefix *irc.Prefix, nick string, motd string) []*irc.Message { + var msgs []*irc.Message + msgs = append(msgs, &irc.Message{ + Prefix: prefix, + Command: irc.RPL_MOTDSTART, + Params: []string{nick, fmt.Sprintf("- Message of the Day -")}, + }) + + for _, l := range strings.Split(motd, "\n") { + msgs = append(msgs, &irc.Message{ + Prefix: prefix, + Command: irc.RPL_MOTD, + Params: []string{nick, l}, + }) + } + + msgs = append(msgs, &irc.Message{ + Prefix: prefix, + Command: irc.RPL_ENDOFMOTD, + Params: []string{nick, "End of /MOTD command."}, + }) + + return msgs +} + +func GenerateMonitor(subcmd string, targets []string) []*irc.Message { + maxLength := maxMessageLength - len("MONITOR "+subcmd+" ") + + var msgs []*irc.Message + var buf []string + n := 0 + for _, target := range targets { + if n+len(target)+1 > maxLength { + msgs = append(msgs, &irc.Message{ + Command: "MONITOR", + Params: []string{subcmd, strings.Join(buf, ",")}, + }) + buf = buf[:0] + n = 0 + } + + buf = append(buf, target) + n += len(target) + 1 + } + + if len(buf) > 0 { + msgs = append(msgs, &irc.Message{ + Command: "MONITOR", + Params: []string{subcmd, strings.Join(buf, ",")}, + }) + } + + return msgs +} + +func GenerateNamesReply(prefix *irc.Prefix, nick string, channel string, status ChannelStatus, members []string) []*irc.Message { + emptyNameReply := irc.Message{ + Prefix: prefix, + Command: irc.RPL_NAMREPLY, + Params: []string{nick, string(status), channel, ""}, + } + maxLength := maxMessageLength - len(emptyNameReply.String()) + + var msgs []*irc.Message + var buf strings.Builder + for _, s := range members { + n := buf.Len() + 1 + len(s) + if buf.Len() != 0 && n > maxLength { + // There's not enough space for the next space + nick + msgs = append(msgs, &irc.Message{ + Prefix: prefix, + Command: irc.RPL_NAMREPLY, + Params: []string{nick, string(status), channel, buf.String()}, + }) + buf.Reset() + } + + if buf.Len() != 0 { + buf.WriteByte(' ') + } + buf.WriteString(s) + } + + if buf.Len() != 0 { + msgs = append(msgs, &irc.Message{ + Prefix: prefix, + Command: irc.RPL_NAMREPLY, + Params: []string{nick, string(status), channel, buf.String()}, + }) + } + + msgs = append(msgs, &irc.Message{ + Prefix: prefix, + Command: irc.RPL_ENDOFNAMES, + Params: []string{nick, channel, "End of /NAMES list"}, + }) + return msgs +} diff --git a/xirc/xirc.go b/xirc/xirc.go index f58c571..332d8af 100644 --- a/xirc/xirc.go +++ b/xirc/xirc.go @@ -9,6 +9,11 @@ import ( "gopkg.in/irc.v3" ) +const ( + maxMessageLength = 512 + maxMessageParams = 15 +) + const ( RPL_STATSPING = "246" RPL_LOCALUSERS = "265"