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
|
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
|
||||||
|
|
||||||
|
137
downstream.go
137
downstream.go
@ -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 = " :@!*?"
|
||||||
@ -75,14 +115,16 @@ const illegalNickChars = " :@!*?"
|
|||||||
// permanentDownstreamCaps is the list of always-supported downstream
|
// permanentDownstreamCaps is the list of always-supported downstream
|
||||||
// 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
25
user.go
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user