Introduce the soju.im/bouncer-networks-notify capability

This commit is contained in:
Simon Ser 2021-03-10 09:27:59 +01:00
parent 29ad541ac7
commit 31f2d28508
3 changed files with 125 additions and 66 deletions

View File

@ -42,6 +42,13 @@ format. Clients MUST ignore unknown attributes.
The `bouncer-networks` extension defines a new `RPL_ISUPPORT` token and a new The `bouncer-networks` extension defines a new `RPL_ISUPPORT` token and a new
`BOUNCER` command. `BOUNCER` command.
The `bouncer-networks` capability MUST be negociated. This allows the server and
client to behave differently when the client is aware of the bouncer networks.
The `bouncer-networks-notify` capability MAY be negociated. This allows the
client to signal that it is capable of receiving and correctly processing
bouncer network notifications.
### `RPL_ISUPPORT` token ### `RPL_ISUPPORT` token
The server can advertise a `BOUNCER_NETID` token in its `RPL_ISUPPORT` message. The server can advertise a `BOUNCER_NETID` token in its `RPL_ISUPPORT` message.
@ -127,13 +134,25 @@ On success, the server replies with:
### Network notifications ### Network notifications
When a network attributes are updated, the bouncer MUST broadcast a If the client has negociated the `bouncer-networks-notify` capability, the
`BOUNCER NETWORK` message to all connected clients with the updated attributes: server MUST send an initial batch of `BOUNCER NETWORK` messages with the current
list of network, and MUST send notification messages whenever a network is
added, updated or removed.
If the client has not negociated the `bouncer-networks-notify` capability, the
server MUST NOT send implicit `BOUNCER NETWORK` messages.
When network attributes are updated, the bouncer MUST broadcast a
`BOUNCER NETWORK` message with the updated attributes to all connected clients
with the `bouncer-networks-notify` capability enabled:
BOUNCER NETWORK <netid> <attributes> BOUNCER NETWORK <netid> <attributes>
The notification SHOULD NOT contain attributes that haven't been updated.
When a network is removed, the bouncer MUST broadcast a `BOUNCER NETWORK` When a network is removed, the bouncer MUST broadcast a `BOUNCER NETWORK`
message to all connected clients: message with the special argument `*` to all connected clients with the
`bouncer-networks-notify` capability enabled:
BOUNCER NETWORK <netid> * BOUNCER NETWORK <netid> *
@ -231,7 +250,7 @@ Binding to a network:
C: CAP LS 302 C: CAP LS 302
C: NICK emersion C: NICK emersion
C: USER emersion 0 0 :Simon C: USER emersion 0 0 :Simon
S: CAP * LS :sasl=PLAIN bouncer-networks S: CAP * LS :sasl=PLAIN bouncer-networks bouncer-networks-notify
C: CAP REQ :sasl bouncer-networks C: CAP REQ :sasl bouncer-networks
[SASL authentication] [SASL authentication]
C: BOUNCER BIND 42 C: BOUNCER BIND 42
@ -248,7 +267,7 @@ Listing networks:
Adding a new network: Adding a new network:
C: BOUNCER ADDNETWORK name=OFTC;host=irc.oftc.net C: BOUNCER ADDNETWORK name=OFTC;host=irc.oftc.net
S: BOUNCER NETWORK 44 status=connecting S: BOUNCER NETWORK 44 name=OFTC;host=irc.oftc.net;status=connecting
S: BOUNCER ADDNETWORK 44 S: BOUNCER ADDNETWORK 44
S: BOUNCER NETWORK 44 status=connected S: BOUNCER NETWORK 44 status=connected

View File

@ -57,17 +57,57 @@ var errAuthFailed = ircError{&irc.Message{
Params: []string{"*", "Invalid username or password"}, Params: []string{"*", "Invalid username or password"},
}} }}
func parseBouncerNetID(s string) (int64, error) { func parseBouncerNetID(subcommand, s string) (int64, error) {
id, err := strconv.ParseInt(s, 10, 64) id, err := strconv.ParseInt(s, 10, 64)
if err != nil { if err != nil {
return 0, ircError{&irc.Message{ return 0, ircError{&irc.Message{
Command: "FAIL", Command: "FAIL",
Params: []string{"BOUNCER", "INVALID_NETID", s, "Invalid network ID"}, Params: []string{"BOUNCER", "INVALID_NETID", subcommand, s, "Invalid network ID"},
}} }}
} }
return id, nil return id, nil
} }
func getNetworkAttrs(network *network) irc.Tags {
state := "disconnected"
if uc := network.conn; uc != nil {
state = "connected"
}
attrs := irc.Tags{
"name": irc.TagValue(network.GetName()),
"state": irc.TagValue(state),
"nickname": irc.TagValue(network.Nick),
}
if network.Username != "" {
attrs["username"] = irc.TagValue(network.Username)
}
if network.Realname != "" {
attrs["realname"] = irc.TagValue(network.Realname)
}
if u, err := network.URL(); err == nil {
hasHostPort := true
switch u.Scheme {
case "ircs":
attrs["tls"] = irc.TagValue("1")
case "irc+insecure":
attrs["tls"] = irc.TagValue("0")
default:
hasHostPort = false
}
if host, port, err := net.SplitHostPort(u.Host); err == nil && hasHostPort {
attrs["host"] = irc.TagValue(host)
attrs["port"] = irc.TagValue(port)
} else if hasHostPort {
attrs["host"] = irc.TagValue(u.Host)
}
}
return attrs
}
// ' ' and ':' break the IRC message wire format, '@' and '!' break prefixes, // ' ' and ':' break the IRC message wire format, '@' and '!' break prefixes,
// '*' and '?' break masks // '*' and '?' break masks
const illegalNickChars = " :@!*?" const illegalNickChars = " :@!*?"
@ -76,13 +116,15 @@ const illegalNickChars = " :@!*?"
// capabilities. // capabilities.
var permanentDownstreamCaps = map[string]string{ var permanentDownstreamCaps = map[string]string{
"batch": "", "batch": "",
"soju.im/bouncer-networks": "",
"cap-notify": "", "cap-notify": "",
"echo-message": "", "echo-message": "",
"invite-notify": "", "invite-notify": "",
"message-tags": "", "message-tags": "",
"sasl": "PLAIN", "sasl": "PLAIN",
"server-time": "", "server-time": "",
"soju.im/bouncer-networks": "",
"soju.im/bouncer-networks-notify": "",
} }
// needAllDownstreamCaps is the list of downstream capabilities that // needAllDownstreamCaps is the list of downstream capabilities that
@ -598,7 +640,7 @@ func (dc *downstreamConn) handleMessageUnregistered(msg *irc.Message) error {
}} }}
} }
id, err := parseBouncerNetID(idStr) id, err := parseBouncerNetID(subcommand, idStr)
if err != nil { if err != nil {
return err return err
} }
@ -1022,6 +1064,29 @@ func (dc *downstreamConn) welcome() error {
dc.updateNick() dc.updateNick()
dc.updateSupportedCaps() dc.updateSupportedCaps()
if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BATCH",
Params: []string{"+networks", "bouncer-networks"},
})
dc.user.forEachNetwork(func(network *network) {
idStr := fmt.Sprintf("%v", network.ID)
attrs := getNetworkAttrs(network)
dc.SendMessage(&irc.Message{
Tags: irc.Tags{"batch": irc.TagValue("networks")},
Prefix: dc.srv.prefix(),
Command: "BOUNCER",
Params: []string{"NETWORK", idStr, attrs.String()},
})
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BATCH",
Params: []string{"-networks"},
})
}
dc.forEachUpstream(func(uc *upstreamConn) { dc.forEachUpstream(func(uc *upstreamConn) {
for _, entry := range uc.channels.innerMap { for _, entry := range uc.channels.innerMap {
ch := entry.value.(*upstreamChannel) ch := entry.value.(*upstreamChannel)
@ -1985,49 +2050,13 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
Params: []string{"+networks", "bouncer-networks"}, Params: []string{"+networks", "bouncer-networks"},
}) })
dc.user.forEachNetwork(func(network *network) { dc.user.forEachNetwork(func(network *network) {
id := fmt.Sprintf("%v", network.ID) idStr := fmt.Sprintf("%v", network.ID)
attrs := getNetworkAttrs(network)
state := "disconnected"
if uc := network.conn; uc != nil {
state = "connected"
}
attrs := irc.Tags{
"name": irc.TagValue(network.GetName()),
"state": irc.TagValue(state),
"nickname": irc.TagValue(network.Nick),
}
if network.Username != "" {
attrs["username"] = irc.TagValue(network.Username)
}
if network.Realname != "" {
attrs["realname"] = irc.TagValue(network.Realname)
}
if u, err := network.URL(); err == nil {
hasHostPort := true
switch u.Scheme {
case "ircs":
attrs["tls"] = irc.TagValue("1")
case "irc+insecure":
attrs["tls"] = irc.TagValue("0")
default:
hasHostPort = false
}
if host, port, err := net.SplitHostPort(u.Host); err == nil && hasHostPort {
attrs["host"] = irc.TagValue(host)
attrs["port"] = irc.TagValue(port)
} else if hasHostPort {
attrs["host"] = irc.TagValue(u.Host)
}
}
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Tags: irc.Tags{"batch": irc.TagValue("networks")}, Tags: irc.Tags{"batch": irc.TagValue("networks")},
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: "BOUNCER", Command: "BOUNCER",
Params: []string{"NETWORK", id, attrs.String()}, Params: []string{"NETWORK", idStr, attrs.String()},
}) })
}) })
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
@ -2095,7 +2124,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
if err := parseMessageParams(msg, nil, &idStr, &attrsStr); err != nil { if err := parseMessageParams(msg, nil, &idStr, &attrsStr); err != nil {
return err return err
} }
id, err := parseBouncerNetID(idStr) id, err := parseBouncerNetID(subcommand, idStr)
if err != nil { if err != nil {
return err return err
} }
@ -2105,7 +2134,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
if net == nil { if net == nil {
return ircError{&irc.Message{ return ircError{&irc.Message{
Command: "FAIL", Command: "FAIL",
Params: []string{"BOUNCER", "INVALID_NETID", idStr, "Invalid network ID"}, Params: []string{"BOUNCER", "INVALID_NETID", subcommand, idStr, "Invalid network ID"},
}} }}
} }
@ -2148,7 +2177,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
if err := parseMessageParams(msg, nil, &idStr); err != nil { if err := parseMessageParams(msg, nil, &idStr); err != nil {
return err return err
} }
id, err := parseBouncerNetID(idStr) id, err := parseBouncerNetID(subcommand, idStr)
if err != nil { if err != nil {
return err return err
} }
@ -2157,7 +2186,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
if net == nil { if net == nil {
return ircError{&irc.Message{ return ircError{&irc.Message{
Command: "FAIL", Command: "FAIL",
Params: []string{"BOUNCER", "INVALID_NETID", idStr, "Invalid network ID"}, Params: []string{"BOUNCER", "INVALID_NETID", subcommand, idStr, "Invalid network ID"},
}} }}
} }

25
user.go
View File

@ -518,7 +518,7 @@ func (u *user) run() {
uc.forEachDownstream(func(dc *downstreamConn) { uc.forEachDownstream(func(dc *downstreamConn) {
dc.updateSupportedCaps() dc.updateSupportedCaps()
if dc.caps["soju.im/bouncer-networks"] { if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: "BOUNCER", Command: "BOUNCER",
@ -657,7 +657,7 @@ func (u *user) handleUpstreamDisconnected(uc *upstreamConn) {
uc.forEachDownstream(func(dc *downstreamConn) { uc.forEachDownstream(func(dc *downstreamConn) {
dc.updateSupportedCaps() dc.updateSupportedCaps()
if dc.caps["soju.im/bouncer-networks"] { if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: "BOUNCER", Command: "BOUNCER",
@ -725,14 +725,14 @@ func (u *user) createNetwork(record *Network) (*network, error) {
u.addNetwork(network) u.addNetwork(network)
// TODO: broadcast network status
idStr := fmt.Sprintf("%v", network.ID) idStr := fmt.Sprintf("%v", network.ID)
attrs := getNetworkAttrs(network)
u.forEachDownstream(func(dc *downstreamConn) { u.forEachDownstream(func(dc *downstreamConn) {
if dc.caps["soju.im/bouncer-networks"] { if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: "BOUNCER", Command: "BOUNCER",
Params: []string{"NETWORK", idStr, "network=" + network.GetName()}, Params: []string{"NETWORK", idStr, attrs.String()},
}) })
} }
}) })
@ -790,7 +790,18 @@ func (u *user) updateNetwork(record *Network) (*network, error) {
// This will re-connect to the upstream server // This will re-connect to the upstream server
u.addNetwork(updatedNetwork) u.addNetwork(updatedNetwork)
// TODO: broadcast BOUNCER NETWORK notifications // TODO: only broadcast attributes that have changed
idStr := fmt.Sprintf("%v", updatedNetwork.ID)
attrs := getNetworkAttrs(updatedNetwork)
u.forEachDownstream(func(dc *downstreamConn) {
if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BOUNCER",
Params: []string{"NETWORK", idStr, attrs.String()},
})
}
})
return updatedNetwork, nil return updatedNetwork, nil
} }
@ -809,7 +820,7 @@ func (u *user) deleteNetwork(id int64) error {
idStr := fmt.Sprintf("%v", network.ID) idStr := fmt.Sprintf("%v", network.ID)
u.forEachDownstream(func(dc *downstreamConn) { u.forEachDownstream(func(dc *downstreamConn) {
if dc.caps["soju.im/bouncer-networks"] { if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: "BOUNCER", Command: "BOUNCER",