Add support for WHOX

This adds support for WHOX, without bothering about flags and mask2
because Solanum and Ergo [1] don't support it either.

The motivation is to allow clients to reliably query account names.

It's not possible to use WHOX tokens to route replies to the right
client, because RPL_ENDOFWHO doesn't contain it.

[1]: https://github.com/ergochat/ergo/pull/1184

Closes: https://todo.sr.ht/~emersion/soju/135
This commit is contained in:
Simon Ser 2021-11-02 18:15:45 +01:00
parent 8c7c907d6f
commit 241e27b00e
3 changed files with 143 additions and 23 deletions

View File

@ -236,6 +236,7 @@ var passthroughIsupport = map[string]bool{
"TOPICLEN": true, "TOPICLEN": true,
"USERLEN": true, "USERLEN": true,
"UTF8ONLY": true, "UTF8ONLY": true,
"WHOX": true,
} }
type downstreamConn struct { type downstreamConn struct {
@ -1157,6 +1158,10 @@ func (dc *downstreamConn) welcome() error {
isupport = append(isupport, fmt.Sprintf("BOUNCER_NETID=%v", dc.network.ID)) isupport = append(isupport, fmt.Sprintf("BOUNCER_NETID=%v", dc.network.ID))
} }
if dc.network == nil && dc.caps["soju.im/bouncer-networks"] {
isupport = append(isupport, "WHOX")
}
if uc := dc.upstream(); uc != nil { if uc := dc.upstream(); uc != nil {
for k := range passthroughIsupport { for k := range passthroughIsupport {
v, ok := uc.isupport[k] v, ok := uc.isupport[k]
@ -1882,6 +1887,10 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
}) })
} }
} }
// For WHOX docs, see:
// - http://faerion.sourceforge.net/doc/irc/whox.var
// - https://github.com/quakenet/snircd/blob/master/doc/readme.who
// Note, many features aren't widely implemented, such as flags and mask2
case "WHO": case "WHO":
if len(msg.Params) == 0 { if len(msg.Params) == 0 {
// TODO: support WHO without parameters // TODO: support WHO without parameters
@ -1893,52 +1902,80 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
return nil return nil
} }
// TODO: support WHO masks // Clients will use the first mask to match RPL_ENDOFWHO
entity := msg.Params[0] endOfWhoToken := msg.Params[0]
entityCM := casemapASCII(entity)
if dc.network == nil && entityCM == dc.nickCM { // TODO: add support for WHOX mask2
mask := msg.Params[0]
var options string
if len(msg.Params) > 1 {
options = msg.Params[1]
}
optionsParts := strings.SplitN(options, "%", 2)
// TODO: add support for WHOX flags in optionsParts[0]
var fields, whoxToken string
if len(optionsParts) == 2 {
optionsParts := strings.SplitN(optionsParts[1], ",", 2)
fields = strings.ToLower(optionsParts[0])
if len(optionsParts) == 2 && strings.Contains(fields, "t") {
whoxToken = optionsParts[1]
}
}
// TODO: support mixed bouncer/upstream WHO queries
maskCM := casemapASCII(mask)
if dc.network == nil && maskCM == dc.nickCM {
// TODO: support AWAY (H/G) in self WHO reply // TODO: support AWAY (H/G) in self WHO reply
flags := "H" flags := "H"
if dc.user.Admin { if dc.user.Admin {
flags += "*" flags += "*"
} }
dc.SendMessage(&irc.Message{ info := whoxInfo{
Prefix: dc.srv.prefix(), Token: whoxToken,
Command: irc.RPL_WHOREPLY, Username: dc.user.Username,
Params: []string{dc.nick, "*", dc.user.Username, dc.hostname, dc.srv.Hostname, dc.nick, flags, "0 " + dc.realname}, Hostname: dc.hostname,
}) Server: dc.srv.Hostname,
Nickname: dc.nick,
Flags: flags,
Realname: dc.realname,
}
dc.SendMessage(generateWHOXReply(dc.srv.prefix(), dc.nick, fields, &info))
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: irc.RPL_ENDOFWHO, Command: irc.RPL_ENDOFWHO,
Params: []string{dc.nick, dc.nick, "End of /WHO list"}, Params: []string{dc.nick, endOfWhoToken, "End of /WHO list"},
}) })
return nil return nil
} }
if entityCM == serviceNickCM { if maskCM == serviceNickCM {
dc.SendMessage(&irc.Message{ info := whoxInfo{
Prefix: dc.srv.prefix(), Token: whoxToken,
Command: irc.RPL_WHOREPLY, Username: servicePrefix.User,
Params: []string{serviceNick, "*", servicePrefix.User, servicePrefix.Host, dc.srv.Hostname, serviceNick, "H*", "0 " + serviceRealname}, Hostname: servicePrefix.Host,
}) Server: dc.srv.Hostname,
Nickname: serviceNick,
Flags: "H*",
Realname: serviceRealname,
}
dc.SendMessage(generateWHOXReply(dc.srv.prefix(), dc.nick, fields, &info))
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: irc.RPL_ENDOFWHO, Command: irc.RPL_ENDOFWHO,
Params: []string{dc.nick, serviceNick, "End of /WHO list"}, Params: []string{dc.nick, endOfWhoToken, "End of /WHO list"},
}) })
return nil return nil
} }
uc, upstreamName, err := dc.unmarshalEntity(entity) // TODO: properly support WHO masks
uc, upstreamMask, err := dc.unmarshalEntity(mask)
if err != nil { if err != nil {
return err return err
} }
var params []string params := []string{upstreamMask}
if len(msg.Params) == 2 { if options != "" {
params = []string{upstreamName, msg.Params[1]} params = append(params, options)
} else {
params = []string{upstreamName}
} }
uc.SendMessageLabeled(dc.id, &irc.Message{ uc.SendMessageLabeled(dc.id, &irc.Message{

78
irc.go
View File

@ -17,6 +17,7 @@ const (
rpl_globalusers = "266" rpl_globalusers = "266"
rpl_creationtime = "329" rpl_creationtime = "329"
rpl_topicwhotime = "333" rpl_topicwhotime = "333"
rpl_whospcrpl = "354"
err_invalidcapcmd = "410" err_invalidcapcmd = "410"
) )
@ -682,3 +683,80 @@ func parseChatHistoryBound(param string) time.Time {
return time.Time{} return time.Time{}
} }
} }
type whoxInfo struct {
Token string
Username string
Hostname string
Server string
Nickname string
Flags string
Account string
Realname string
}
func generateWHOXReply(prefix *irc.Prefix, nick, fields string, info *whoxInfo) *irc.Message {
if fields == "" {
return &irc.Message{
Prefix: prefix,
Command: irc.RPL_WHOREPLY,
Params: []string{nick, "*", info.Username, info.Hostname, info.Server, info.Nickname, info.Flags, "0 " + info.Realname},
}
}
fieldSet := make(map[byte]bool)
for i := 0; i < len(fields); i++ {
fieldSet[fields[i]] = true
}
var params []string
if fieldSet['t'] {
params = append(params, info.Token)
}
if fieldSet['c'] {
params = append(params, "*")
}
if fieldSet['u'] {
params = append(params, info.Username)
}
if fieldSet['i'] {
params = append(params, "255.255.255.255")
}
if fieldSet['h'] {
params = append(params, info.Hostname)
}
if fieldSet['s'] {
params = append(params, info.Server)
}
if fieldSet['n'] {
params = append(params, info.Nickname)
}
if fieldSet['f'] {
params = append(params, info.Flags)
}
if fieldSet['d'] {
params = append(params, "0")
}
if fieldSet['l'] { // idle time
params = append(params, "0")
}
if fieldSet['a'] {
account := "0" // WHOX uses "0" to mean "no account"
if info.Account != "" && info.Account != "*" {
account = info.Account
}
params = append(params, account)
}
if fieldSet['o'] {
params = append(params, "0")
}
if fieldSet['r'] {
params = append(params, info.Realname)
}
return &irc.Message{
Prefix: prefix,
Command: rpl_whospcrpl,
Params: append([]string{nick}, params...),
}
}

View File

@ -1452,6 +1452,11 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
// Ignore // Ignore
case irc.RPL_YOURHOST, irc.RPL_CREATED: case irc.RPL_YOURHOST, irc.RPL_CREATED:
// Ignore // Ignore
case rpl_whospcrpl:
// Not supported in multi-upstream mode, forward as-is
uc.forEachDownstream(func(dc *downstreamConn) {
dc.SendMessage(msg)
})
case irc.RPL_LUSERCLIENT, irc.RPL_LUSEROP, irc.RPL_LUSERUNKNOWN, irc.RPL_LUSERCHANNELS, irc.RPL_LUSERME: case irc.RPL_LUSERCLIENT, irc.RPL_LUSEROP, irc.RPL_LUSERUNKNOWN, irc.RPL_LUSERCHANNELS, irc.RPL_LUSERME:
fallthrough fallthrough
case irc.RPL_STATSVLINE, rpl_statsping, irc.RPL_STATSBLINE, irc.RPL_STATSDLINE: case irc.RPL_STATSVLINE, rpl_statsping, irc.RPL_STATSBLINE, irc.RPL_STATSDLINE: