diff --git a/irc/config.go b/irc/config.go index 246a1043..44934f22 100644 --- a/irc/config.go +++ b/irc/config.go @@ -1240,6 +1240,7 @@ func (config *Config) generateISupport() (err error) { if config.Server.Casemapping == CasemappingPRECIS { isupport.Add("UTF8MAPPING", precisUTF8MappingToken) } + isupport.Add("WHOX", "") err = isupport.RegenerateCachedReply() return diff --git a/irc/handlers.go b/irc/handlers.go index eaa83e27..2f35ec55 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2775,7 +2775,120 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re return true } -// WHO [ [o]] +const WhoFieldMinimum = int('a') // lowest rune value +const WhoFieldMaximum = int('z') + +type WhoFields [WhoFieldMaximum - WhoFieldMinimum + 1]bool + +func (fields *WhoFields) Set(field rune) bool { + index := int(field) + if WhoFieldMinimum <= index && index <= WhoFieldMaximum { + fields[int(field)-WhoFieldMinimum] = true + return true + } else { + return false + } +} +func (fields *WhoFields) Has(field rune) bool { + return fields[int(field)-WhoFieldMinimum] +} + +// rplWhoReply returns the WHO(X) reply between one user and another channel/user. +// who format: +// [*][~|&|@|%|+][B] : +// whox format: +// [*][~|&|@|%|+][B] : +func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *ResponseBuffer, isWhox bool, fields WhoFields, whoType string) { + params := []string{client.Nick()} + + details := target.Details() + + if fields.Has('t') { + params = append(params, whoType) + } + if fields.Has('c') { + fChannel := "*" + if channel != nil { + fChannel = channel.name + } + params = append(params, fChannel) + } + if fields.Has('u') { + params = append(params, details.username) + } + if fields.Has('i') { + fIP := "255.255.255.255" + if client.HasMode(modes.Operator) || client == target { + // you can only see a target's IP if they're you or you're an oper + fIP = target.IPString() + } + params = append(params, fIP) + } + if fields.Has('h') { + params = append(params, details.hostname) + } + if fields.Has('s') { + params = append(params, target.server.name) + } + if fields.Has('n') { + params = append(params, details.nick) + } + if fields.Has('f') { // "flags" (away + oper state + channel status prefix + bot) + var flags strings.Builder + if target.Away() { + flags.WriteRune('G') // Gone + } else { + flags.WriteRune('H') // Here + } + + if target.HasMode(modes.Operator) { + flags.WriteRune('*') + } + + if channel != nil { + flags.WriteString(channel.ClientPrefixes(target, false)) + } + + if target.HasMode(modes.Bot) { + flags.WriteRune('B') + } + + params = append(params, flags.String()) + + } + if fields.Has('d') { // server hops from us to target + params = append(params, "0") + } + if fields.Has('l') { + params = append(params, fmt.Sprintf("%d", target.IdleSeconds())) + } + if fields.Has('a') { + fAccount := "0" + if details.accountName != "*" { + // WHOX uses "0" to mean "no account" + fAccount = details.accountName + } + params = append(params, fAccount) + } + if fields.Has('o') { // target's channel power level + //TODO: implement this + params = append(params, "0") + } + if fields.Has('r') { + params = append(params, details.realname) + } + + numeric := RPL_WHOSPCRPL + if !isWhox { + numeric = RPL_WHOREPLY + // if this isn't WHOX, stick hops + realname at the end + params = append(params, "0 "+details.realname) + } + + rb.Add(nil, client.server.name, numeric, params...) +} + +// WHO [%,] func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { mask := msg.Params[0] var err error @@ -2793,6 +2906,26 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo return false } + sFields := "cuhsnf" + whoType := "0" + isWhox := false + if len(msg.Params) > 1 && strings.Contains(msg.Params[1], "%") { + isWhox = true + whoxData := msg.Params[1] + fieldStart := strings.Index(whoxData, "%") + sFields = whoxData[fieldStart+1:] + + typeIndex := strings.Index(sFields, ",") + if typeIndex > -1 && typeIndex < (len(sFields)-1) { // make sure there's , and a value after it + whoType = sFields[typeIndex+1:] + sFields = strings.ToLower(sFields[:typeIndex]) + } + } + var fields WhoFields + for _, field := range sFields { + fields.Set(field) + } + //TODO(dan): is this used and would I put this param in the Modern doc? // if not, can we remove it? //var operatorOnly bool @@ -2810,7 +2943,7 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo if !channel.flags.HasMode(modes.Secret) || isJoined || isOper { for _, member := range channel.Members() { if !member.HasMode(modes.Invisible) || isJoined || isOper { - client.rplWhoReply(channel, member, rb) + client.rplWhoReply(channel, member, rb, isWhox, fields, whoType) } } } @@ -2838,7 +2971,7 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo for mclient := range server.clients.FindAll(mask) { if isOper || !mclient.HasMode(modes.Invisible) || isFriend(mclient) { - client.rplWhoReply(nil, mclient, rb) + client.rplWhoReply(nil, mclient, rb, isWhox, fields, whoType) } } } diff --git a/irc/numerics.go b/irc/numerics.go index f34f0c12..76ad9397 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -86,6 +86,7 @@ const ( RPL_VERSION = "351" RPL_WHOREPLY = "352" RPL_NAMREPLY = "353" + RPL_WHOSPCRPL = "354" RPL_LINKS = "364" RPL_ENDOFLINKS = "365" RPL_ENDOFNAMES = "366" diff --git a/irc/server.go b/irc/server.go index a4671e23..b1fef8de 100644 --- a/irc/server.go +++ b/irc/server.go @@ -432,35 +432,6 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) { } } -// rplWhoReply returns the WHO reply between one user and another channel/user. -// ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ] -// : -func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *ResponseBuffer) { - channelName := "*" - flags := "" - - if target.Away() { - flags = "G" - } else { - flags = "H" - } - if target.HasMode(modes.Operator) { - flags += "*" - } - - if channel != nil { - // TODO is this right? - flags += channel.ClientPrefixes(target, rb.session.capabilities.Has(caps.MultiPrefix)) - channelName = channel.name - } - if target.HasMode(modes.Bot) { - flags += "B" - } - details := target.Details() - // hardcode a hopcount of 0 for now - rb.Add(nil, client.server.name, RPL_WHOREPLY, client.Nick(), channelName, details.username, details.hostname, client.server.name, details.nick, flags, "0 "+details.realname) -} - // rehash reloads the config and applies the changes from the config file. func (server *Server) rehash() error { server.logger.Info("server", "Attempting rehash")