~emersion/soju-dev

Forward RPL_TOPICWHOTIME to downstreams v1 PROPOSED

Hubert Hirtz: 3
 Forward RPL_TOPICWHOTIME to downstreams
 More explicit error messages on network mismatch
 Handle channel and nickname case (casefolding/casemapping)

 10 files changed, 172 insertions(+), 85 deletions(-)
Thanks for working on this!

Are there plans to support case-mapping more complicated than strings.ToLower?

If so, in the future we'll need to get the case-mapping algorithm from upstream
and have case-folding helpers tied to upstreamConn. I'd prefer to tie the
helpers to upstreamConn sooner than later.

If not, we can inline strings.ToLower and strings.EqualFold everywhere.
Le 20/08/2020 à 09:22, Simon Ser a écrit :
Next
Out of lazyness I'd choose inlining Casefold().  Also, as a client, I
don't think soju will need to handle full UTF-8 casefolding (and it
doesn't need it now, only oragono implements this I believe).

That's also why strings.EqualFold is not used: it performs UTF-8
casefolding (instead of just ASCII ToLower).



          
          
          
        
      

      
      
      
      

      

      
      
      
      

      

      
      
      
      

      
      
        
          







Apparently case-folding and case-mapping aren't the same things. To
refer to the weird IRC thing, use "case-mapping".
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~emersion/soju-dev/patches/12018/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH 1/3] Forward RPL_TOPICWHOTIME to downstreams Export this patch

Also fix forwarding of RPL_CREATIONTIME that wasn't marshaled??
---
 bridge.go   | 15 ++++++++++++---
 upstream.go | 25 +++++++++++++++++++++++--
 2 files changed, 35 insertions(+), 5 deletions(-)

diff --git a/bridge.go b/bridge.go
index e434c49..20a4b84 100644
--- a/bridge.go
@@ -1,8 +1,10 @@
package soju

import (
	"gopkg.in/irc.v3"
	"strconv"
	"strings"

	"gopkg.in/irc.v3"
)

func forwardChannel(dc *downstreamConn, ch *upstreamChannel) {
@@ -11,8 +13,6 @@ func forwardChannel(dc *downstreamConn, ch *upstreamChannel) {
	}

	sendTopic(dc, ch)

	// TODO: rpl_topicwhotime
	sendNames(dc, ch)
}

@@ -25,6 +25,15 @@ func sendTopic(dc *downstreamConn, ch *upstreamChannel) {
			Command: irc.RPL_TOPIC,
			Params:  []string{dc.nick, downstreamName, ch.Topic},
		})
		if ch.TopicWho != "" {
			topicWho := dc.marshalEntity(ch.conn.network, ch.TopicWho)
			topicTime := strconv.FormatInt(ch.TopicTime.Unix(), 10)
			dc.SendMessage(&irc.Message{
				Prefix:  dc.srv.prefix(),
				Command: rpl_topicwhotime,
				Params:  []string{dc.nick, downstreamName, topicWho, topicTime},
			})
		}
	} else {
		dc.SendMessage(&irc.Message{
			Prefix:  dc.srv.prefix(),
diff --git a/upstream.go b/upstream.go
index 07d6266..ac7c4f2 100644
--- a/upstream.go
+++ b/upstream.go
@@ -830,6 +830,10 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
			ch.Topic = ""
		}
	case "TOPIC":
		if msg.Prefix == nil {
			return fmt.Errorf("expected a prefix")
		}

		var name string
		if err := parseMessageParams(msg, &name); err != nil {
			return err
@@ -840,6 +844,8 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		}
		if len(msg.Params) > 1 {
			ch.Topic = msg.Params[1]
			ch.TopicWho = msg.Prefix.Name
			ch.TopicTime = time.Now() // TODO use msg.Tags["time"]
		} else {
			ch.Topic = ""
		}
@@ -958,7 +964,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
				dc.SendMessage(&irc.Message{
					Prefix:  dc.srv.prefix(),
					Command: rpl_creationtime,
					Params:  []string{dc.nick, channel, creationTime},
					Params:  []string{dc.nick, dc.marshalEntity(uc.network, ch.Name), creationTime},
				})
			})
		}
@@ -971,12 +977,27 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		if err != nil {
			return err
		}
		ch.TopicWho = who
		firstTopicWhoTime := ch.TopicWho == ""
		ch.TopicWho = irc.ParsePrefix(who).Name
		sec, err := strconv.ParseInt(timeStr, 10, 64)
		if err != nil {
			return fmt.Errorf("failed to parse topic time: %v", err)
		}
		ch.TopicTime = time.Unix(sec, 0)
		if firstTopicWhoTime {
			uc.forEachDownstream(func(dc *downstreamConn) {
				dc.SendMessage(&irc.Message{
					Prefix:  dc.srv.prefix(),
					Command: rpl_topicwhotime,
					Params: []string{
						dc.nick,
						dc.marshalEntity(uc.network, ch.Name),
						dc.marshalEntity(uc.network, ch.TopicWho),
						timeStr,
					},
				})
			})
		}
	case irc.RPL_LIST:
		var channel, clients, topic string
		if err := parseMessageParams(msg, nil, &channel, &clients, &topic); err != nil {
-- 
2.28.0

[PATCH 2/3] More explicit error messages on network mismatch Export this patch

Examples:

    INVITE user/a #channel/b
    KICK #channel/b user/a,user/b
---
 downstream.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/downstream.go b/downstream.go
index 8863f28..c33907e 100644
--- a/downstream.go
+++ b/downstream.go
@@ -1120,7 +1120,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
			if ucChannel != ucUser {
				return ircError{&irc.Message{
					Command: irc.ERR_USERNOTINCHANNEL,
					Params:  []string{dc.nick, user, channel, "They aren't on that channel"},
					Params:  []string{dc.nick, user, channel, "They are on another network"},
				}}
			}
			uc := ucChannel
@@ -1539,7 +1539,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
		if ucChannel != ucUser {
			return ircError{&irc.Message{
				Command: irc.ERR_USERNOTINCHANNEL,
				Params:  []string{dc.nick, user, channel, "They aren't on that channel"},
				Params:  []string{dc.nick, user, channel, "They are on another network"},
			}}
		}
		uc := ucChannel
-- 
2.28.0

[PATCH 3/3] Handle channel and nickname case (casefolding/casemapping) Export this patch

---
 bridge.go     |   4 +-
 db.go         |   2 +-
 downstream.go |  44 +++++++++++------
 irc.go        |  10 ++--
 service.go    |   1 +
 upstream.go   | 133 ++++++++++++++++++++++++++++++++------------------
 user.go       |  19 ++++----
 7 files changed, 135 insertions(+), 78 deletions(-)

diff --git a/bridge.go b/bridge.go
index 20a4b84..c6364fa 100644
--- a/bridge.go
@@ -54,8 +54,8 @@ func sendNames(dc *downstreamConn, ch *upstreamChannel) {
	maxLength := maxMessageLength - len(emptyNameReply.String())

	var buf strings.Builder
	for nick, memberships := range ch.Members {
		s := memberships.Format(dc) + dc.marshalEntity(ch.conn.network, nick)
	for _, m := range ch.Members {
		s := m.MemberShips.Format(dc) + dc.marshalEntity(ch.conn.network, m.Nick)

		n := buf.Len() + 1 + len(s)
		if buf.Len() != 0 && n > maxLength {
diff --git a/db.go b/db.go
index 46d04df..f276938 100644
--- a/db.go
+++ b/db.go
@@ -465,6 +465,6 @@ func (db *DB) DeleteChannel(networkID int64, name string) error {
	db.lock.Lock()
	defer db.lock.Unlock()

	_, err := db.db.Exec("DELETE FROM Channel WHERE network = ? AND name = ?", networkID, name)
	_, err := db.db.Exec("DELETE FROM Channel WHERE network = ? AND LOWER(name) = LOWER(?)", networkID, name)
	return err
}
diff --git a/downstream.go b/downstream.go
index c33907e..9c1f7f7 100644
--- a/downstream.go
+++ b/downstream.go
@@ -83,6 +83,7 @@ type downstreamConn struct {
	registered  bool
	user        *user
	nick        string
	nickCf      string
	rawUsername string
	networkName string
	clientName  string
@@ -329,13 +330,15 @@ func (dc *downstreamConn) handleMessageUnregistered(msg *irc.Message) error {
		if err := parseMessageParams(msg, &nick); err != nil {
			return err
		}
		if nick == serviceNick {
		nickCf := Casefold(nick)
		if nickCf == serviceNickCf {
			return ircError{&irc.Message{
				Command: irc.ERR_NICKNAMEINUSE,
				Params:  []string{dc.nick, nick, "Nickname reserved for bouncer service"},
			}}
		}
		dc.nick = nick
		dc.nickCf = nickCf
	case "USER":
		if err := parseMessageParams(msg, &dc.rawUsername, nil, nil, &dc.realname); err != nil {
			return err
@@ -642,6 +645,7 @@ func (dc *downstreamConn) updateNick() {
			Params:  []string{uc.nick},
		})
		dc.nick = uc.nick
		dc.nickCf = uc.nickCf
	}
}

@@ -816,11 +820,11 @@ func (dc *downstreamConn) welcome() error {
	dc.updateNick()

	dc.forEachUpstream(func(uc *upstreamConn) {
		for _, ch := range uc.channels {
		for name, ch := range uc.channels {
			if !ch.complete {
				continue
			}
			if record, ok := uc.network.channels[ch.Name]; ok && record.Detached {
			if record, ok := uc.network.channels[name]; ok && record.Detached {
				continue
			}

@@ -999,6 +1003,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
				Params:  []string{nick},
			})
			dc.nick = nick
			dc.nickCf = Casefold(nick)
		}
	case "JOIN":
		var namesStr string
@@ -1016,6 +1021,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
			if err != nil {
				return err
			}
			upstreamNameCf := Casefold(upstreamName)

			var key string
			if len(keys) > i {
@@ -1032,7 +1038,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
			})

			ch := &Channel{Name: upstreamName, Key: key, Detached: false}
			if current, ok := uc.network.channels[ch.Name]; ok && key == "" {
			if current, ok := uc.network.channels[upstreamNameCf]; ok && key == "" {
				// Don't clear the channel key if there's one set
				// TODO: add a way to unset the channel key
				ch.Key = current.Key
@@ -1139,13 +1145,14 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
		if err := parseMessageParams(msg, &name); err != nil {
			return err
		}
		nameCf := Casefold(name)

		var modeStr string
		if len(msg.Params) > 1 {
			modeStr = msg.Params[1]
		}

		if name == dc.nick {
		if nameCf == dc.nickCf {
			if modeStr != "" {
				dc.forEachUpstream(func(uc *upstreamConn) {
					uc.SendMessageLabeled(dc.id, &irc.Message{
@@ -1167,6 +1174,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
		if err != nil {
			return err
		}
		upstreamNameCf := Casefold(upstreamName)

		if !uc.isChannel(upstreamName) {
			return ircError{&irc.Message{
@@ -1183,7 +1191,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
				Params:  params,
			})
		} else {
			ch, ok := uc.channels[upstreamName]
			ch, ok := uc.channels[upstreamNameCf]
			if !ok {
				return ircError{&irc.Message{
					Command: irc.ERR_NOSUCHCHANNEL,
@@ -1224,6 +1232,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
		if err != nil {
			return err
		}
		upstreamChannelCf := Casefold(upstreamChannel)

		if len(msg.Params) > 1 { // setting topic
			topic := msg.Params[1]
@@ -1232,7 +1241,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
				Params:  []string{upstreamChannel, topic},
			})
		} else { // getting topic
			ch, ok := uc.channels[upstreamChannel]
			ch, ok := uc.channels[upstreamChannelCf]
			if !ok {
				return ircError{&irc.Message{
					Command: irc.ERR_NOSUCHCHANNEL,
@@ -1302,9 +1311,9 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
			if err != nil {
				return err
			}
			upstreamChannelCf := Casefold(upstreamChannel)

			ch, ok := uc.channels[upstreamChannel]
			if ok {
			if ch, ok := uc.channels[upstreamChannelCf]; ok {
				sendNames(dc, ch)
			} else {
				// NAMES on a channel we have not joined, ask upstream
@@ -1327,8 +1336,9 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {

		// TODO: support WHO masks
		entity := msg.Params[0]
		entityCf := Casefold(entity)

		if entity == dc.nick {
		if entityCf == dc.nickCf {
			// TODO: support AWAY (H/G) in self WHO reply
			dc.SendMessage(&irc.Message{
				Prefix:  dc.srv.prefix(),
@@ -1342,7 +1352,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
			})
			return nil
		}
		if entity == serviceNick {
		if entityCf == serviceNickCf {
			dc.SendMessage(&irc.Message{
				Prefix:  dc.srv.prefix(),
				Command: irc.RPL_WHOREPLY,
@@ -1392,8 +1402,9 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
		if i := strings.IndexByte(mask, ','); i >= 0 {
			mask = mask[:i]
		}
		maskCf := Casefold(mask)

		if mask == dc.nick {
		if maskCf == dc.nickCf {
			dc.SendMessage(&irc.Message{
				Prefix:  dc.srv.prefix(),
				Command: irc.RPL_WHOISUSER,
@@ -1441,7 +1452,8 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
		tags := copyClientTags(msg.Tags)

		for _, name := range strings.Split(targetsStr, ",") {
			if name == serviceNick {
			nameCf := Casefold(name)
			if nameCf == serviceNickCf {
				handleServicePRIVMSG(dc, text)
				continue
			}
@@ -1450,8 +1462,9 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
			if err != nil {
				return err
			}
			upstreamNameCf := Casefold(upstreamName)

			if upstreamName == "NickServ" {
			if upstreamNameCf == "nickserv" {
				dc.handleNickServPRIVMSG(uc, text)
			}

@@ -1476,7 +1489,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
				Command: "PRIVMSG",
				Params:  []string{upstreamName, text},
			}
			uc.produce(upstreamName, echoMsg, dc)
			uc.produce(upstreamNameCf, echoMsg, dc)
		}
	case "NOTICE":
		var targetsStr, text string
@@ -1572,6 +1585,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
		if err != nil {
			return err
		}
		entity = Casefold(entity)

		// TODO: support msgid criteria
		criteriaParts := strings.SplitN(criteria, "=", 2)
diff --git a/irc.go b/irc.go
index 34d90cd..ba9ca4c 100644
--- a/irc.go
+++ b/irc.go
@@ -112,13 +112,13 @@ outer:
				if nextArgument >= len(arguments) {
					return nil, fmt.Errorf("malformed modestring %q: missing mode argument for %c%c", modeStr, plusMinus, mode)
				}
				member := arguments[nextArgument]
				member := Casefold(arguments[nextArgument])
				if _, ok := ch.Members[member]; ok {
					if plusMinus == '+' {
						ch.Members[member].Add(ch.conn.availableMemberships, membership)
						ch.Members[member].MemberShips.Add(ch.conn.availableMemberships, membership)
					} else {
						// TODO: for upstreams without multi-prefix, query the user modes again
						ch.Members[member].Remove(membership)
						ch.Members[member].MemberShips.Remove(membership)
					}
				}
				needMarshaling[nextArgument] = struct{}{}
@@ -386,3 +386,7 @@ func parseCTCPMessage(msg *irc.Message) (cmd string, params string, ok bool) {

	return cmd, params, true
}

func Casefold(name string) string {
	return strings.ToLower(name)
}
diff --git a/service.go b/service.go
index c8d31f4..86bc5a5 100644
--- a/service.go
+++ b/service.go
@@ -27,6 +27,7 @@ import (
)

const serviceNick = "BouncerServ"
const serviceNickCf = "bouncerserv"
const serviceRealname = "soju bouncer service"

var servicePrefix = &irc.Prefix{
diff --git a/upstream.go b/upstream.go
index ac7c4f2..62b093a 100644
--- a/upstream.go
+++ b/upstream.go
@@ -32,6 +32,11 @@ var permanentUpstreamCaps = map[string]bool{
	"server-time":      true,
}

type upstreamMember struct {
	Nick        string
	MemberShips *memberships
}

type upstreamChannel struct {
	Name         string
	conn         *upstreamConn
@@ -41,7 +46,7 @@ type upstreamChannel struct {
	Status       channelStatus
	modes        channelModes
	creationTime string
	Members      map[string]*memberships
	Members      map[string]upstreamMember
	complete     bool
}

@@ -59,6 +64,7 @@ type upstreamConn struct {

	registered    bool
	nick          string
	nickCf        string
	username      string
	realname      string
	modes         userModes
@@ -187,6 +193,7 @@ func (uc *upstreamConn) forEachDownstreamByID(id uint64, f func(*downstreamConn)
	})
}

// name must be casefolded.
func (uc *upstreamConn) getChannel(name string) (*upstreamChannel, error) {
	ch, ok := uc.channels[name]
	if !ok {
@@ -376,12 +383,14 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
				return err
			}
		}
		entity = Casefold(entity)
		prefix := Casefold(msg.Prefix.Name)

		if msg.Prefix.Name == serviceNick {
		if prefix == serviceNickCf {
			uc.logger.Printf("skipping %v from soju's service: %v", msg.Command, msg)
			break
		}
		if entity == serviceNick {
		if entity == serviceNickCf {
			uc.logger.Printf("skipping %v to soju's service: %v", msg.Command, msg)
			break
		}
@@ -390,12 +399,12 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
			uc.produce("", msg, nil)
		} else { // regular user message
			target := entity
			if target == uc.nick {
				target = msg.Prefix.Name
			if target == uc.nickCf {
				target = prefix
			}
			uc.produce(target, msg, nil)

			highlight := msg.Prefix.Name != uc.nick && isHighlight(text, uc.nick)
			highlight := prefix != uc.nickCf && isHighlight(text, uc.nick)
			if ch, ok := uc.network.channels[target]; ok && ch.Detached && highlight {
				uc.forEachDownstream(func(dc *downstreamConn) {
					sendServiceNOTICE(dc, fmt.Sprintf("highlight in %v: <%v> %v", dc.marshalEntity(uc.network, ch.Name), msg.Prefix.Name, text))
@@ -683,20 +692,23 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		if err := parseMessageParams(msg, &newNick); err != nil {
			return err
		}
		newNickCf := Casefold(newNick)
		prefix := Casefold(msg.Prefix.Name)

		me := false
		if msg.Prefix.Name == uc.nick {
		if prefix == uc.nickCf {
			uc.logger.Printf("changed nick from %q to %q", uc.nick, newNick)
			me = true
			uc.nick = newNick
			uc.nickCf = newNickCf
		}

		for _, ch := range uc.channels {
			if memberships, ok := ch.Members[msg.Prefix.Name]; ok {
				delete(ch.Members, msg.Prefix.Name)
				ch.Members[newNick] = memberships
				uc.appendLog(ch.Name, msg)
				uc.appendHistory(ch.Name, msg)
		for chName, ch := range uc.channels {
			if memberships, ok := ch.Members[prefix]; ok {
				delete(ch.Members, prefix)
				ch.Members[newNickCf] = memberships
				uc.appendLog(chName, msg)
				uc.appendHistory(chName, msg)
			}
		}

@@ -718,14 +730,16 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		if err := parseMessageParams(msg, &channels); err != nil {
			return err
		}
		prefix := Casefold(msg.Prefix.Name)

		for _, ch := range strings.Split(channels, ",") {
			if msg.Prefix.Name == uc.nick {
			chCf := Casefold(ch)
			if prefix == uc.nickCf {
				uc.logger.Printf("joined channel %q", ch)
				uc.channels[ch] = &upstreamChannel{
				uc.channels[chCf] = &upstreamChannel{
					Name:    ch,
					conn:    uc,
					Members: make(map[string]*memberships),
					Members: make(map[string]upstreamMember),
				}

				uc.SendMessage(&irc.Message{
@@ -733,16 +747,19 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
					Params:  []string{ch},
				})
			} else {
				ch, err := uc.getChannel(ch)
				ch, err := uc.getChannel(chCf)
				if err != nil {
					return err
				}
				ch.Members[msg.Prefix.Name] = &memberships{}
				ch.Members[prefix] = upstreamMember{
					Nick:        msg.Prefix.Name,
					MemberShips: &memberships{},
				}
			}

			chMsg := msg.Copy()
			chMsg.Params[0] = ch
			uc.produce(ch, chMsg, nil)
			uc.produce(chCf, chMsg, nil)
		}
	case "PART":
		if msg.Prefix == nil {
@@ -753,22 +770,24 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		if err := parseMessageParams(msg, &channels); err != nil {
			return err
		}
		prefix := Casefold(msg.Prefix.Name)

		for _, ch := range strings.Split(channels, ",") {
			if msg.Prefix.Name == uc.nick {
			chCf := Casefold(ch)
			if prefix == uc.nickCf {
				uc.logger.Printf("parted channel %q", ch)
				delete(uc.channels, ch)
				delete(uc.channels, chCf)
			} else {
				ch, err := uc.getChannel(ch)
				ch, err := uc.getChannel(chCf)
				if err != nil {
					return err
				}
				delete(ch.Members, msg.Prefix.Name)
				delete(ch.Members, prefix)
			}

			chMsg := msg.Copy()
			chMsg.Params[0] = ch
			uc.produce(ch, chMsg, nil)
			uc.produce(chCf, chMsg, nil)
		}
	case "KICK":
		if msg.Prefix == nil {
@@ -779,38 +798,41 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		if err := parseMessageParams(msg, &channel, &user); err != nil {
			return err
		}
		user = Casefold(user)
		channelCf := Casefold(channel)

		if user == uc.nick {
		if user == uc.nickCf {
			uc.logger.Printf("kicked from channel %q by %s", channel, msg.Prefix.Name)
			delete(uc.channels, channel)
			delete(uc.channels, channelCf)
		} else {
			ch, err := uc.getChannel(channel)
			ch, err := uc.getChannel(channelCf)
			if err != nil {
				return err
			}
			delete(ch.Members, user)
		}

		uc.produce(channel, msg, nil)
		uc.produce(channelCf, msg, nil)
	case "QUIT":
		if msg.Prefix == nil {
			return fmt.Errorf("expected a prefix")
		}
		prefix := Casefold(msg.Prefix.Name)

		if msg.Prefix.Name == uc.nick {
		if prefix == uc.nickCf {
			uc.logger.Printf("quit")
		}

		for _, ch := range uc.channels {
			if _, ok := ch.Members[msg.Prefix.Name]; ok {
				delete(ch.Members, msg.Prefix.Name)
		for chName, ch := range uc.channels {
			if _, ok := ch.Members[prefix]; ok {
				delete(ch.Members, prefix)

				uc.appendLog(ch.Name, msg)
				uc.appendHistory(ch.Name, msg)
				uc.appendLog(chName, msg)
				uc.appendHistory(chName, msg)
			}
		}

		if msg.Prefix.Name != uc.nick {
		if prefix != uc.nick {
			uc.forEachDownstream(func(dc *downstreamConn) {
				dc.SendMessage(dc.marshalMessage(msg, uc.network))
			})
@@ -820,6 +842,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		if err := parseMessageParams(msg, nil, &name, &topic); err != nil {
			return err
		}
		name = Casefold(name)
		ch, err := uc.getChannel(name)
		if err != nil {
			return err
@@ -838,6 +861,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		if err := parseMessageParams(msg, &name); err != nil {
			return err
		}
		name = Casefold(name)
		ch, err := uc.getChannel(name)
		if err != nil {
			return err
@@ -849,15 +873,16 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		} else {
			ch.Topic = ""
		}
		uc.produce(ch.Name, msg, nil)
		uc.produce(name, msg, nil)
	case "MODE":
		var name, modeStr string
		if err := parseMessageParams(msg, &name, &modeStr); err != nil {
			return err
		}
		name = Casefold(name)

		if !uc.isChannel(name) { // user mode change
			if name != uc.nick {
			if name != uc.nickCf {
				return fmt.Errorf("received MODE message for unknown nick %q", name)
			}
			return uc.modes.Apply(modeStr)
@@ -873,12 +898,12 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
				return err
			}

			uc.appendLog(ch.Name, msg)
			uc.appendLog(name, msg)

			if ch, ok := uc.network.channels[name]; !ok || !ch.Detached {
				uc.forEachDownstream(func(dc *downstreamConn) {
					params := make([]string, len(msg.Params))
					params[0] = dc.marshalEntity(uc.network, name)
					params[0] = dc.marshalEntity(uc.network, ch.Name)
					params[1] = modeStr

					copy(params[2:], msg.Params[2:])
@@ -915,6 +940,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		if err := parseMessageParams(msg, nil, &channel); err != nil {
			return err
		}
		channel = Casefold(channel)
		modeStr := ""
		if len(msg.Params) > 2 {
			modeStr = msg.Params[2]
@@ -935,7 +961,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
				modeStr, modeParams := ch.modes.Format()

				uc.forEachDownstream(func(dc *downstreamConn) {
					params := []string{dc.nick, dc.marshalEntity(uc.network, channel), modeStr}
					params := []string{dc.nick, dc.marshalEntity(uc.network, ch.Name), modeStr}
					params = append(params, modeParams...)

					dc.SendMessage(&irc.Message{
@@ -951,6 +977,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		if err := parseMessageParams(msg, nil, &channel, &creationTime); err != nil {
			return err
		}
		channel = Casefold(channel)

		ch, err := uc.getChannel(channel)
		if err != nil {
@@ -969,11 +996,12 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
			})
		}
	case rpl_topicwhotime:
		var name, who, timeStr string
		if err := parseMessageParams(msg, nil, &name, &who, &timeStr); err != nil {
		var channel, who, timeStr string
		if err := parseMessageParams(msg, nil, &channel, &who, &timeStr); err != nil {
			return err
		}
		ch, err := uc.getChannel(name)
		channel = Casefold(channel)
		ch, err := uc.getChannel(channel)
		if err != nil {
			return err
		}
@@ -1027,7 +1055,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
			return err
		}

		ch, ok := uc.channels[name]
		ch, ok := uc.channels[Casefold(name)]
		if !ok {
			// NAMES on a channel we have not joined, forward to downstream
			uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
@@ -1056,15 +1084,19 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {

		for _, s := range splitSpace(members) {
			memberships, nick := uc.parseMembershipPrefix(s)
			ch.Members[nick] = memberships
			ch.Members[Casefold(nick)] = upstreamMember{
				Nick:        nick,
				MemberShips: memberships,
			}
		}
	case irc.RPL_ENDOFNAMES:
		var name string
		if err := parseMessageParams(msg, nil, &name); err != nil {
			return err
		}
		nameCf := Casefold(name)

		ch, ok := uc.channels[name]
		ch, ok := uc.channels[nameCf]
		if !ok {
			// NAMES on a channel we have not joined, forward to downstream
			uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
@@ -1084,7 +1116,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
		}
		ch.complete = true

		if c, ok := uc.network.channels[name]; !ok || !c.Detached {
		if c, ok := uc.network.channels[nameCf]; !ok || !c.Detached {
			uc.forEachDownstream(func(dc *downstreamConn) {
				forwardChannel(dc, ch)
			})
@@ -1502,6 +1534,7 @@ func splitSpace(s string) []string {

func (uc *upstreamConn) register() {
	uc.nick = uc.network.Nick
	uc.nickCf = Casefold(uc.nick)
	uc.username = uc.network.Username
	if uc.username == "" {
		uc.username = uc.nick
@@ -1592,6 +1625,7 @@ func (uc *upstreamConn) SendMessageLabeled(downstreamID uint64, msg *irc.Message
	uc.SendMessage(msg)
}

// entity must be casefolded.
// TODO: handle moving logs when a network name changes, when support for this is added
func (uc *upstreamConn) appendLog(entity string, msg *irc.Message) {
	if uc.srv.LogPath == "" {
@@ -1609,7 +1643,8 @@ func (uc *upstreamConn) appendLog(entity string, msg *irc.Message) {
	}
}

// appendHistory appends a message to the history. entity can be empty.
// appendHistory appends a message to the history.
// entity can be empty, and must be casefolded.
func (uc *upstreamConn) appendHistory(entity string, msg *irc.Message) {
	detached := false
	if ch, ok := uc.network.channels[entity]; ok {
@@ -1650,6 +1685,8 @@ func (uc *upstreamConn) appendHistory(entity string, msg *irc.Message) {
//
// If origin is not nil and origin doesn't support echo-message, the message is
// forwarded to all connections except origin.
//
// target must be casefolded.
func (uc *upstreamConn) produce(target string, msg *irc.Message, origin *downstreamConn) {
	if target != "" {
		uc.appendLog(target, msg)
diff --git a/user.go b/user.go
index 0eec316..17b1845 100644
--- a/user.go
+++ b/user.go
@@ -61,8 +61,8 @@ type network struct {
	stopped chan struct{}

	conn           *upstreamConn
	channels       map[string]*Channel
	history        map[string]*networkHistory // indexed by entity
	channels       map[string]*Channel        // indexed by casefolded channel name
	history        map[string]*networkHistory // indexed by casefolded entity
	offlineClients map[string]struct{}        // indexed by client name
	lastError      error
}
@@ -71,7 +71,7 @@ func newNetwork(user *user, record *Network, channels []Channel) *network {
	m := make(map[string]*Channel, len(channels))
	for _, ch := range channels {
		ch := ch
		m[ch.Name] = &ch
		m[Casefold(ch.Name)] = &ch
	}

	return &network{
@@ -174,17 +174,18 @@ func (net *network) stop() {
}

func (net *network) createUpdateChannel(ch *Channel) error {
	if current, ok := net.channels[ch.Name]; ok {
	nameCf := Casefold(ch.Name)
	if current, ok := net.channels[nameCf]; ok {
		ch.ID = current.ID // update channel if it already exists
	}
	if err := net.user.srv.db.StoreChannel(net.ID, ch); err != nil {
		return err
	}
	prev := net.channels[ch.Name]
	net.channels[ch.Name] = ch
	prev := net.channels[nameCf]
	net.channels[nameCf] = ch

	if prev != nil && prev.Detached != ch.Detached {
		history := net.history[ch.Name]
		history := net.history[nameCf]
		if ch.Detached {
			net.user.srv.Logger.Printf("network %q: detaching channel %q", net.GetName(), ch.Name)
			net.forEachDownstream(func(dc *downstreamConn) {
@@ -204,7 +205,7 @@ func (net *network) createUpdateChannel(ch *Channel) error {

			var uch *upstreamChannel
			if net.conn != nil {
				uch = net.conn.channels[ch.Name]
				uch = net.conn.channels[nameCf]
			}

			net.forEachDownstream(func(dc *downstreamConn) {
@@ -232,7 +233,7 @@ func (net *network) deleteChannel(name string) error {
	if err := net.user.srv.db.DeleteChannel(net.ID, name); err != nil {
		return err
	}
	delete(net.channels, name)
	delete(net.channels, Casefold(name))
	return nil
}

-- 
2.28.0
Thanks for working on this!

Are there plans to support case-mapping more complicated than strings.ToLower?

If so, in the future we'll need to get the case-mapping algorithm from upstream
and have case-folding helpers tied to upstreamConn. I'd prefer to tie the
helpers to upstreamConn sooner than later.

If not, we can inline strings.ToLower and strings.EqualFold everywhere.