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
`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
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
When a network attributes are updated, the bouncer MUST broadcast a
`BOUNCER NETWORK` message to all connected clients with the updated attributes:
If the client has negociated the `bouncer-networks-notify` capability, the
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>
The notification SHOULD NOT contain attributes that haven't been updated.
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> *
@ -231,7 +250,7 @@ Binding to a network:
C: CAP LS 302
C: NICK emersion
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
[SASL authentication]
C: BOUNCER BIND 42
@ -248,7 +267,7 @@ Listing networks:
Adding a new network:
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 NETWORK 44 status=connected

View File

@ -57,17 +57,57 @@ var errAuthFailed = ircError{&irc.Message{
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)
if err != nil {
return 0, ircError{&irc.Message{
Command: "FAIL",
Params: []string{"BOUNCER", "INVALID_NETID", s, "Invalid network ID"},
Params: []string{"BOUNCER", "INVALID_NETID", subcommand, s, "Invalid network ID"},
}}
}
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 masks
const illegalNickChars = " :@!*?"
@ -75,14 +115,16 @@ const illegalNickChars = " :@!*?"
// permanentDownstreamCaps is the list of always-supported downstream
// capabilities.
var permanentDownstreamCaps = map[string]string{
"batch": "",
"soju.im/bouncer-networks": "",
"cap-notify": "",
"echo-message": "",
"invite-notify": "",
"message-tags": "",
"sasl": "PLAIN",
"server-time": "",
"batch": "",
"cap-notify": "",
"echo-message": "",
"invite-notify": "",
"message-tags": "",
"sasl": "PLAIN",
"server-time": "",
"soju.im/bouncer-networks": "",
"soju.im/bouncer-networks-notify": "",
}
// 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 {
return err
}
@ -1022,6 +1064,29 @@ func (dc *downstreamConn) welcome() error {
dc.updateNick()
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) {
for _, entry := range uc.channels.innerMap {
ch := entry.value.(*upstreamChannel)
@ -1985,49 +2050,13 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
Params: []string{"+networks", "bouncer-networks"},
})
dc.user.forEachNetwork(func(network *network) {
id := fmt.Sprintf("%v", network.ID)
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)
}
}
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", id, attrs.String()},
Params: []string{"NETWORK", idStr, attrs.String()},
})
})
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 {
return err
}
id, err := parseBouncerNetID(idStr)
id, err := parseBouncerNetID(subcommand, idStr)
if err != nil {
return err
}
@ -2105,7 +2134,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
if net == nil {
return ircError{&irc.Message{
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 {
return err
}
id, err := parseBouncerNetID(idStr)
id, err := parseBouncerNetID(subcommand, idStr)
if err != nil {
return err
}
@ -2157,7 +2186,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
if net == nil {
return ircError{&irc.Message{
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) {
dc.updateSupportedCaps()
if dc.caps["soju.im/bouncer-networks"] {
if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BOUNCER",
@ -657,7 +657,7 @@ func (u *user) handleUpstreamDisconnected(uc *upstreamConn) {
uc.forEachDownstream(func(dc *downstreamConn) {
dc.updateSupportedCaps()
if dc.caps["soju.im/bouncer-networks"] {
if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BOUNCER",
@ -725,14 +725,14 @@ func (u *user) createNetwork(record *Network) (*network, error) {
u.addNetwork(network)
// TODO: broadcast network status
idStr := fmt.Sprintf("%v", network.ID)
attrs := getNetworkAttrs(network)
u.forEachDownstream(func(dc *downstreamConn) {
if dc.caps["soju.im/bouncer-networks"] {
if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
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
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
}
@ -809,7 +820,7 @@ func (u *user) deleteNetwork(id int64) error {
idStr := fmt.Sprintf("%v", network.ID)
u.forEachDownstream(func(dc *downstreamConn) {
if dc.caps["soju.im/bouncer-networks"] {
if dc.caps["soju.im/bouncer-networks-notify"] {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BOUNCER",