8b558e39b7
Some IPv6 hostnames can start with a colon (eg '::1'). This breaks the IRC line format. To work around this issue, prefix the hostname with a '0'. This changes the representation of the IP but not its value. References: https://todo.sr.ht/~taiite/senpai/109 Co-authored-by: Simon Ser <contact@emersion.fr>
168 lines
3.6 KiB
Go
168 lines
3.6 KiB
Go
package xirc
|
|
|
|
import (
|
|
"gopkg.in/irc.v4"
|
|
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// whoxFields is the list of all WHOX field letters, by order of appearance in
|
|
// RPL_WHOSPCRPL messages.
|
|
var whoxFields = []byte("tcuihsnfdlaor")
|
|
|
|
type WHOXInfo struct {
|
|
Token string
|
|
Username string
|
|
Hostname string
|
|
Server string
|
|
Nickname string
|
|
Flags string
|
|
Account string
|
|
Realname string
|
|
}
|
|
|
|
func (info *WHOXInfo) get(k byte) string {
|
|
switch k {
|
|
case 't':
|
|
return info.Token
|
|
case 'c':
|
|
return "*"
|
|
case 'u':
|
|
return info.Username
|
|
case 'i':
|
|
return "255.255.255.255"
|
|
case 'h':
|
|
hostname := info.Hostname
|
|
if strings.HasPrefix(info.Hostname, ":") {
|
|
// The hostname cannot start with a colon as this would get parsed
|
|
// as a trailing parameter. IPv6 addresses such as "::1" are
|
|
// prefixed with a zero to ensure this.
|
|
hostname = "0" + hostname
|
|
}
|
|
return hostname
|
|
case 's':
|
|
return info.Server
|
|
case 'n':
|
|
return info.Nickname
|
|
case 'f':
|
|
return info.Flags
|
|
case 'd':
|
|
return "0"
|
|
case 'l': // idle time
|
|
return "0"
|
|
case 'a':
|
|
account := "0" // WHOX uses "0" to mean "no account"
|
|
if info.Account != "" && info.Account != "*" {
|
|
account = info.Account
|
|
}
|
|
return account
|
|
case 'o':
|
|
return "0"
|
|
case 'r':
|
|
return info.Realname
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (info *WHOXInfo) set(k byte, v string) {
|
|
switch k {
|
|
case 't':
|
|
info.Token = v
|
|
case 'u':
|
|
info.Username = v
|
|
case 'h':
|
|
info.Hostname = v
|
|
case 's':
|
|
info.Server = v
|
|
case 'n':
|
|
info.Nickname = v
|
|
case 'f':
|
|
info.Flags = v
|
|
case 'a':
|
|
info.Account = v
|
|
case 'r':
|
|
info.Realname = v
|
|
}
|
|
}
|
|
|
|
func GenerateWHOXReply(prefix *irc.Prefix, nick, fields string, info *WHOXInfo) *irc.Message {
|
|
if fields == "" {
|
|
hostname := info.Hostname
|
|
if strings.HasPrefix(info.Hostname, ":") {
|
|
// The hostname cannot start with a colon as this would get parsed
|
|
// as a trailing parameter. IPv6 addresses such as "::1" are
|
|
// prefixed with a zero to ensure this.
|
|
hostname = "0" + hostname
|
|
}
|
|
|
|
return &irc.Message{
|
|
Prefix: prefix,
|
|
Command: irc.RPL_WHOREPLY,
|
|
Params: []string{nick, "*", info.Username, 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 values []string
|
|
for _, field := range whoxFields {
|
|
if !fieldSet[field] {
|
|
continue
|
|
}
|
|
values = append(values, info.get(field))
|
|
}
|
|
|
|
return &irc.Message{
|
|
Prefix: prefix,
|
|
Command: RPL_WHOSPCRPL,
|
|
Params: append([]string{nick}, values...),
|
|
}
|
|
}
|
|
|
|
func ParseWHOXOptions(options string) (fields, whoxToken string) {
|
|
optionsParts := strings.SplitN(options, "%", 2)
|
|
// TODO: add support for WHOX flags in optionsParts[0]
|
|
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]
|
|
}
|
|
}
|
|
return fields, whoxToken
|
|
}
|
|
|
|
func ParseWHOXReply(msg *irc.Message, fields string) (*WHOXInfo, error) {
|
|
if msg.Command != RPL_WHOSPCRPL {
|
|
return nil, fmt.Errorf("invalid WHOX reply %q", msg.Command)
|
|
} else if len(msg.Params) == 0 {
|
|
return nil, fmt.Errorf("invalid RPL_WHOSPCRPL: no params")
|
|
}
|
|
|
|
fieldSet := make(map[byte]bool)
|
|
for i := 0; i < len(fields); i++ {
|
|
fieldSet[fields[i]] = true
|
|
}
|
|
|
|
var info WHOXInfo
|
|
values := msg.Params[1:]
|
|
for _, field := range whoxFields {
|
|
if !fieldSet[field] {
|
|
continue
|
|
}
|
|
|
|
if len(values) == 0 {
|
|
return nil, fmt.Errorf("invalid RPL_WHOSPCRPL: missing value for field %q", string(field))
|
|
}
|
|
|
|
info.set(field, values[0])
|
|
values = values[1:]
|
|
}
|
|
|
|
return &info, nil
|
|
}
|