Introduce the soju.im/bouncer-networks-notify capability
This commit is contained in:
parent
29ad541ac7
commit
31f2d28508
@ -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
|
||||
|
||||
|
123
downstream.go
123
downstream.go
@ -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 = " :@!*?"
|
||||
@ -76,13 +116,15 @@ const illegalNickChars = " :@!*?"
|
||||
// capabilities.
|
||||
var permanentDownstreamCaps = map[string]string{
|
||||
"batch": "",
|
||||
"soju.im/bouncer-networks": "",
|
||||
"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
25
user.go
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user