Implement the soju.im/bouncer-networks extension
This commit is contained in:
parent
61b68d6dfb
commit
db0f745193
264
doc/ext/bouncer-networks.md
Normal file
264
doc/ext/bouncer-networks.md
Normal file
@ -0,0 +1,264 @@
|
||||
---
|
||||
title: Bouncer networks extension
|
||||
layout: spec
|
||||
work-in-progress: true
|
||||
copyrights:
|
||||
-
|
||||
name: "Darren Whitlen"
|
||||
period: "2020"
|
||||
email: "darren@kiwiirc.com"
|
||||
-
|
||||
name: "Simon Ser"
|
||||
period: "2021"
|
||||
email: "contact@emersion.fr"
|
||||
---
|
||||
|
||||
## Notes for implementing experimental vendor extension
|
||||
|
||||
This is an experimental specification for a vendored extension.
|
||||
|
||||
No guarantees are made regarding the stability of this extension.
|
||||
Backwards-incompatible changes can be made at any time without prior notice.
|
||||
|
||||
Software implementing this work-in-progress specification MUST NOT use the
|
||||
unprefixed `bouncer-networks` CAP names. Instead, implementations SHOULD use
|
||||
the `soju.im/bouncer-networks` CAP names to be interoperable with other software
|
||||
implementing a compatible work-in-progress version.
|
||||
|
||||
## Description
|
||||
|
||||
This document describes the `bouncer-networks` extension. This enables clients
|
||||
to discover servers that are bouncers, list and edit upstream networks the
|
||||
bouncer is connected to.
|
||||
|
||||
Each network has a unique per-user ID called "netid". It MUST NOT change during
|
||||
the lifetime of the network. TODO: character restrictions for network IDs.
|
||||
|
||||
Networks also have attributes. Attributes are encoded in the message-tag
|
||||
format. Clients MUST ignore unknown attributes.
|
||||
|
||||
## Implementation
|
||||
|
||||
The `bouncer-networks` extension defines a new `RPL_ISUPPORT` token and a new
|
||||
`BOUNCER` command.
|
||||
|
||||
### `RPL_ISUPPORT` token
|
||||
|
||||
The server can advertise a `BOUNCER_NETID` token in its `RPL_ISUPPORT` message.
|
||||
Its optional value is the network ID bound for the current connection.
|
||||
|
||||
### `bouncer-networks` batch
|
||||
|
||||
The `bouncer-networks` batch does not take any parameter and can only contain
|
||||
`BOUNCER NETWORK` messages.
|
||||
|
||||
### `BOUNCER` command
|
||||
|
||||
A new `BOUNCER` command is introduced. It has a case-insensitive subcommand:
|
||||
|
||||
BOUNCER <subcommand> <params...>
|
||||
|
||||
#### `BIND` subcommand
|
||||
|
||||
The `BIND` subcommand selects an upstream network to bind to for the lifetime
|
||||
of the current connection. Clients can only send it after authentication but
|
||||
before the registration completes.
|
||||
|
||||
BOUNCER BIND <netid>
|
||||
|
||||
#### `LISTNETWORKS` subcommand
|
||||
|
||||
The `LISTNETWORKS` subcommand queries the list of upstream networks.
|
||||
|
||||
BOUNCER LISTNETWORKS
|
||||
|
||||
The server replies with a `bouncer-networks` batch, containing any number of
|
||||
`BOUNCER NETWORK` messages:
|
||||
|
||||
BOUNCER NETWORK <netid> <attributes>
|
||||
|
||||
#### `ADDNETWORK` subcommand
|
||||
|
||||
The `ADDNETWORK` subcommand registers a new upstream network in the bouncer.
|
||||
|
||||
BOUNCER ADDNETWORK <attributes>
|
||||
|
||||
The bouncer MAY reject this new network for any reason, in this case it MUST
|
||||
reply with an error. If the request is accepted, the bouncer MUST generate a
|
||||
new unique network ID. The bouncer MAY populate unspecified attributes with
|
||||
implementation-defined defaults.
|
||||
|
||||
Clients MUST specify at least the `host` attribute.
|
||||
|
||||
If the client doesn't specify the `tls` attribute, the server SHOULD use the
|
||||
default `1`. If the client doesn't specify the `port` attribute, the server
|
||||
SHOULD use the default `6697` if `tls=1` or `6667` if `tls=0`.
|
||||
|
||||
On success, the server replies with:
|
||||
|
||||
BOUNCER ADDNETWORK <netid>
|
||||
|
||||
#### `CHANGENETWORK` subcommand
|
||||
|
||||
The `CHANGENETWORK` subcommand changes attributes of an existing upstream
|
||||
network.
|
||||
|
||||
BOUNCER CHANGENETWORK <netid> <attributes>
|
||||
|
||||
The bouncer MAY reject the change for any reason, in this case it MUST reply
|
||||
with an error. At least one attribute MUST be specified by the client.
|
||||
|
||||
On success, the server replies with:
|
||||
|
||||
BOUNCER CHANGENETWORK <netid>
|
||||
|
||||
#### `DELNETWORK` subcommand
|
||||
|
||||
The `DELNETWORK` subcommand removes an existing upstream network.
|
||||
|
||||
BOUNCER DELNETWORK <netid>
|
||||
|
||||
The bouncer MAY reject the change for any reason, in this case it MUST reply
|
||||
with an error.
|
||||
|
||||
On success, the server replies with:
|
||||
|
||||
BOUNCER DELNETWORK <netid>
|
||||
|
||||
### Network notifications
|
||||
|
||||
When a network attributes are updated, the bouncer MUST broadcast a
|
||||
`BOUNCER NETWORK` message to all connected clients with the updated attributes:
|
||||
|
||||
BOUNCER NETWORK <netid> <attributes>
|
||||
|
||||
When a network is removed, the bouncer MUST broadcast a `BOUNCER NETWORK`
|
||||
message to all connected clients:
|
||||
|
||||
BOUNCER NETWORK <netid> *
|
||||
|
||||
### Errors
|
||||
|
||||
Errors are returned using the standard replies syntax. The general syntax is:
|
||||
|
||||
FAIL BOUNCER <code> <subcommand> [context...] <description>
|
||||
|
||||
If a client sends an unknown subcommand, the server MUST reply with:
|
||||
|
||||
FAIL BOUNCER UNKNOWN_COMMAND <subcommand> :Unknown subcommand
|
||||
|
||||
#### `ACCOUNT_REQUIRED` error
|
||||
|
||||
If a client sends a `BIND` subcommand before authentication, the server MAY
|
||||
reply with:
|
||||
|
||||
FAIL BOUNCER ACCOUNT_REQUIRED BIND :Authentication required
|
||||
|
||||
#### `REGISTRATION_IS_COMPLETED` error
|
||||
|
||||
If a client sends a `BIND` subcommand after registration, the server MAY reply
|
||||
with:
|
||||
|
||||
FAIL BOUNCER REGISTRATION_IS_COMPLETED BIND :Cannot bind to a network after registration
|
||||
|
||||
#### `INVALID_NETID` error
|
||||
|
||||
If a client sends a subcommand with an invalid network ID, the server MUST
|
||||
reply with:
|
||||
|
||||
FAIL BOUNCER INVALID_NETID <subcommand> <netid> :Network not found
|
||||
|
||||
#### `INVALID_ATTRIBUTE` error
|
||||
|
||||
If a client sends an `ADDNETWORK` or a `CHANGENETWORK` subcommand with an
|
||||
invalid attribute, the server MUST reply with:
|
||||
|
||||
FAIL BOUNCER INVALID_ATTRIBUTE <subcommand> <netid> <attribute> :Invalid attribute value
|
||||
|
||||
If the `subcommand` is `ADDNETWORK`, `netid` MUST be set to the special `*`
|
||||
value.
|
||||
|
||||
#### `READ_ONLY_ATTRIBUTE` error
|
||||
|
||||
If a client attempts to change a read-only network attribute using the
|
||||
`ADDNETWORK` or `CHANGENETWORK` subcommand, the server MUST reply with:
|
||||
|
||||
FAIL BOUNCER READ_ONLY_ATTRIBUTE <subcommand> <netid> <attribute> :Read-only attribute
|
||||
|
||||
If the `subcommand` is `ADDNETWORK`, `netid` MUST be set to the special `*`
|
||||
value.
|
||||
|
||||
#### `UNKNOWN_ATTRIBUTE` error
|
||||
|
||||
If a client sends an `ADDNETWORK` or a `CHANGENETWORK` subcommand with an
|
||||
unknown attribute, the server MUST reply with:
|
||||
|
||||
FAIL BOUNCER UNKNOWN_ATTRIBUTE <subcommand> <netid> <attribute> :Unknown attribute
|
||||
|
||||
If the `subcommand` is `ADDNETWORK`, `netid` MUST be set to the special `*`
|
||||
value.
|
||||
|
||||
#### `NEED_ATTRIBUTE` error
|
||||
|
||||
If a client sends an `ADDNETWORK` subcommand without a mandatory attribute, the
|
||||
server MUST reply with:
|
||||
|
||||
FAIL BOUNCER NEED_ATTRIBUTE ADDNETWORK <attribute> :Missing required attribute
|
||||
|
||||
TODO: more errors
|
||||
|
||||
### Standard network attributes
|
||||
|
||||
Bouncers MUST recognise the following network attributes:
|
||||
|
||||
* `name`: the human-readable name for the network.
|
||||
* `state` (read-only): one of `connected`, `connecting` or `disconnected`.
|
||||
Indicates the current state of the connection to the upstream network.
|
||||
* `host`: the hostname or literal IP address to connect to.
|
||||
* `port`: the TCP port to connect to.
|
||||
* `tls`: `1` to use a TLS connection, `0` to use a cleartext connection.
|
||||
* `nickname`: the nickname to use during registration.
|
||||
* `username`: the username to use during registration.
|
||||
* `realname`: the realname to use during registration.
|
||||
|
||||
TODO: more attributes
|
||||
|
||||
### Examples
|
||||
|
||||
Binding to a network:
|
||||
|
||||
C: CAP LS 302
|
||||
C: NICK emersion
|
||||
C: USER emersion 0 0 :Simon
|
||||
S: CAP * LS :sasl=PLAIN bouncer-networks
|
||||
C: CAP REQ :sasl bouncer-networks
|
||||
[SASL authentication]
|
||||
C: BOUNCER BIND 42
|
||||
C: CAP END
|
||||
|
||||
Listing networks:
|
||||
|
||||
C: BOUNCER LISTNETWORKS
|
||||
S: BATCH +asdf bouncer-networks
|
||||
S: @batch=asdf BOUNCER NETWORK 42 name=Freenode;state=connected
|
||||
S: @batch=asdf BOUNCER NETWORK 43 name=My\sAwesome\sNetwork;state=disconnected
|
||||
S: BATCH -asdf
|
||||
|
||||
Adding a new network:
|
||||
|
||||
C: BOUNCER ADDNETWORK name=OFTC;host=irc.oftc.net
|
||||
S: BOUNCER NETWORK 44 status=connecting
|
||||
S: BOUNCER ADDNETWORK 44
|
||||
S: BOUNCER NETWORK 44 status=connected
|
||||
|
||||
Changing an existing network:
|
||||
|
||||
C: BOUNCER CHANGENETWORK 44 realname=Simon
|
||||
S: BOUNCER NETWORK 44 realname=Simon
|
||||
S: BOUNCER CHANGENETWORK 44
|
||||
|
||||
Removing an existing network:
|
||||
|
||||
C: BOUNCER DELNETWORK 44
|
||||
S: BOUNCER NETWORK 44 *
|
||||
S: BOUNCER DELNETWORK 44
|
256
downstream.go
256
downstream.go
@ -57,6 +57,17 @@ var errAuthFailed = ircError{&irc.Message{
|
||||
Params: []string{"*", "Invalid username or password"},
|
||||
}}
|
||||
|
||||
func parseBouncerNetID(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"},
|
||||
}}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ' ' and ':' break the IRC message wire format, '@' and '!' break prefixes,
|
||||
// '*' and '?' break masks
|
||||
const illegalNickChars = " :@!*?"
|
||||
@ -64,13 +75,14 @@ const illegalNickChars = " :@!*?"
|
||||
// permanentDownstreamCaps is the list of always-supported downstream
|
||||
// capabilities.
|
||||
var permanentDownstreamCaps = map[string]string{
|
||||
"batch": "",
|
||||
"cap-notify": "",
|
||||
"echo-message": "",
|
||||
"invite-notify": "",
|
||||
"message-tags": "",
|
||||
"sasl": "PLAIN",
|
||||
"server-time": "",
|
||||
"batch": "",
|
||||
"soju.im/bouncer-networks": "",
|
||||
"cap-notify": "",
|
||||
"echo-message": "",
|
||||
"invite-notify": "",
|
||||
"message-tags": "",
|
||||
"sasl": "PLAIN",
|
||||
"server-time": "",
|
||||
}
|
||||
|
||||
// needAllDownstreamCaps is the list of downstream capabilities that
|
||||
@ -168,12 +180,15 @@ func (dc *downstreamConn) prefix() *irc.Prefix {
|
||||
func (dc *downstreamConn) forEachNetwork(f func(*network)) {
|
||||
if dc.network != nil {
|
||||
f(dc.network)
|
||||
} else {
|
||||
} else if !dc.caps["soju.im/bouncer-networks"] {
|
||||
dc.user.forEachNetwork(f)
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *downstreamConn) forEachUpstream(f func(*upstreamConn)) {
|
||||
if dc.network == nil && dc.caps["soju.im/bouncer-networks"] {
|
||||
return
|
||||
}
|
||||
dc.user.forEachUpstream(func(uc *upstreamConn) {
|
||||
if dc.network != nil && uc.network != dc.network {
|
||||
return
|
||||
@ -557,6 +572,52 @@ func (dc *downstreamConn) handleMessageUnregistered(msg *irc.Message) error {
|
||||
Params: []string{challengeStr},
|
||||
})
|
||||
}
|
||||
case "BOUNCER":
|
||||
var subcommand string
|
||||
if err := parseMessageParams(msg, &subcommand); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch strings.ToUpper(subcommand) {
|
||||
case "BIND":
|
||||
var idStr string
|
||||
if err := parseMessageParams(msg, nil, &idStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dc.registered {
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "REGISTRATION_IS_COMPLETED", "BIND", "Cannot bind bouncer network after registration"},
|
||||
}}
|
||||
}
|
||||
if dc.user == nil {
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "ACCOUNT_REQUIRED", "BIND", "Authentication needed to bind to bouncer network"},
|
||||
}}
|
||||
}
|
||||
|
||||
id, err := parseBouncerNetID(idStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var match *network
|
||||
dc.user.forEachNetwork(func(net *network) {
|
||||
if net.ID == id {
|
||||
match = net
|
||||
}
|
||||
})
|
||||
if match == nil {
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "INVALID_NETID", idStr, "Unknown network ID"},
|
||||
}}
|
||||
}
|
||||
|
||||
dc.networkName = match.GetName()
|
||||
}
|
||||
default:
|
||||
dc.logger.Printf("unhandled message: %v", msg)
|
||||
return newUnknownCommandError(msg.Command)
|
||||
@ -911,6 +972,10 @@ func (dc *downstreamConn) welcome() error {
|
||||
"CASEMAPPING=ascii",
|
||||
}
|
||||
|
||||
if dc.network != nil {
|
||||
isupport = append(isupport, fmt.Sprintf("BOUNCER_NETID=%v", dc.network.ID))
|
||||
}
|
||||
|
||||
if uc := dc.upstream(); uc != nil {
|
||||
for k := range passthroughIsupport {
|
||||
v, ok := uc.isupport[k]
|
||||
@ -1906,6 +1971,181 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
|
||||
Command: "BATCH",
|
||||
Params: []string{"-" + batchRef},
|
||||
})
|
||||
case "BOUNCER":
|
||||
var subcommand string
|
||||
if err := parseMessageParams(msg, &subcommand); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch strings.ToUpper(subcommand) {
|
||||
case "LISTNETWORKS":
|
||||
dc.SendMessage(&irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BATCH",
|
||||
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),
|
||||
}
|
||||
|
||||
dc.SendMessage(&irc.Message{
|
||||
Tags: irc.Tags{"batch": irc.TagValue("networks")},
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BOUNCER",
|
||||
Params: []string{"NETWORK", id, attrs.String()},
|
||||
})
|
||||
})
|
||||
dc.SendMessage(&irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BATCH",
|
||||
Params: []string{"-networks"},
|
||||
})
|
||||
case "ADDNETWORK":
|
||||
var attrsStr string
|
||||
if err := parseMessageParams(msg, nil, &attrsStr); err != nil {
|
||||
return err
|
||||
}
|
||||
attrs := irc.ParseTags(attrsStr)
|
||||
|
||||
host, ok := attrs.GetTag("host")
|
||||
if !ok {
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "NEED_ATTRIBUTE", subcommand, "host", "Missing required host attribute"},
|
||||
}}
|
||||
}
|
||||
|
||||
addr := host
|
||||
if port, ok := attrs.GetTag("port"); ok {
|
||||
addr += ":" + port
|
||||
}
|
||||
|
||||
if tlsStr, ok := attrs.GetTag("tls"); ok && tlsStr == "0" {
|
||||
addr = "irc+insecure://" + tlsStr
|
||||
}
|
||||
|
||||
nick, ok := attrs.GetTag("nickname")
|
||||
if !ok {
|
||||
nick = dc.nick
|
||||
}
|
||||
|
||||
username, _ := attrs.GetTag("username")
|
||||
realname, _ := attrs.GetTag("realname")
|
||||
|
||||
// TODO: reject unknown attributes
|
||||
|
||||
record := &Network{
|
||||
Addr: addr,
|
||||
Nick: nick,
|
||||
Username: username,
|
||||
Realname: realname,
|
||||
}
|
||||
network, err := dc.user.createNetwork(record)
|
||||
if err != nil {
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "UNKNOWN_ERROR", subcommand, fmt.Sprintf("Failed to create network: %v", err)},
|
||||
}}
|
||||
}
|
||||
|
||||
dc.SendMessage(&irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BOUNCER",
|
||||
Params: []string{"ADDNETWORK", fmt.Sprintf("%v", network.ID)},
|
||||
})
|
||||
case "CHANGENETWORK":
|
||||
var idStr, attrsStr string
|
||||
if err := parseMessageParams(msg, nil, &idStr, &attrsStr); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := parseBouncerNetID(idStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attrs := irc.ParseTags(attrsStr)
|
||||
|
||||
net := dc.user.getNetworkByID(id)
|
||||
if net == nil {
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "INVALID_NETID", idStr, "Invalid network ID"},
|
||||
}}
|
||||
}
|
||||
|
||||
record := net.Network // copy network record because we'll mutate it
|
||||
for k, v := range attrs {
|
||||
s := string(v)
|
||||
switch k {
|
||||
// TODO: host, port, tls
|
||||
case "nickname":
|
||||
record.Nick = s
|
||||
case "username":
|
||||
record.Username = s
|
||||
case "realname":
|
||||
record.Realname = s
|
||||
default:
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "UNKNOWN_ATTRIBUTE", subcommand, k, "Unknown attribute"},
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dc.user.updateNetwork(&record)
|
||||
if err != nil {
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "UNKNOWN_ERROR", subcommand, fmt.Sprintf("Failed to update network: %v", err)},
|
||||
}}
|
||||
}
|
||||
|
||||
dc.SendMessage(&irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BOUNCER",
|
||||
Params: []string{"CHANGENETWORK", idStr},
|
||||
})
|
||||
case "DELNETWORK":
|
||||
var idStr string
|
||||
if err := parseMessageParams(msg, nil, &idStr); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := parseBouncerNetID(idStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
net := dc.user.getNetworkByID(id)
|
||||
if net == nil {
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "INVALID_NETID", idStr, "Invalid network ID"},
|
||||
}}
|
||||
}
|
||||
|
||||
if err := dc.user.deleteNetwork(net.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dc.SendMessage(&irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BOUNCER",
|
||||
Params: []string{"DELNETWORK", idStr},
|
||||
})
|
||||
default:
|
||||
return ircError{&irc.Message{
|
||||
Command: "FAIL",
|
||||
Params: []string{"BOUNCER", "UNKNOWN_COMMAND", subcommand, "Unknown subcommand"},
|
||||
}}
|
||||
}
|
||||
default:
|
||||
dc.logger.Printf("unhandled message: %v", msg)
|
||||
return newUnknownCommandError(msg.Command)
|
||||
|
54
user.go
54
user.go
@ -141,6 +141,9 @@ func newNetwork(user *user, record *Network, channels []Channel) *network {
|
||||
|
||||
func (net *network) forEachDownstream(f func(*downstreamConn)) {
|
||||
net.user.forEachDownstream(func(dc *downstreamConn) {
|
||||
if dc.network == nil && dc.caps["soju.im/bouncer-networks"] {
|
||||
return
|
||||
}
|
||||
if dc.network != nil && dc.network != net {
|
||||
return
|
||||
}
|
||||
@ -511,9 +514,19 @@ func (u *user) run() {
|
||||
|
||||
uc.updateAway()
|
||||
|
||||
netIDStr := fmt.Sprintf("%v", uc.network.ID)
|
||||
uc.forEachDownstream(func(dc *downstreamConn) {
|
||||
dc.updateSupportedCaps()
|
||||
sendServiceNOTICE(dc, fmt.Sprintf("connected to %s", uc.network.GetName()))
|
||||
|
||||
if dc.caps["soju.im/bouncer-networks"] {
|
||||
dc.SendMessage(&irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BOUNCER",
|
||||
Params: []string{"NETWORK", netIDStr, "status=connected"},
|
||||
})
|
||||
} else {
|
||||
sendServiceNOTICE(dc, fmt.Sprintf("connected to %s", uc.network.GetName()))
|
||||
}
|
||||
|
||||
dc.updateNick()
|
||||
})
|
||||
@ -640,13 +653,24 @@ func (u *user) handleUpstreamDisconnected(uc *upstreamConn) {
|
||||
uch.updateAutoDetach(0)
|
||||
}
|
||||
|
||||
netIDStr := fmt.Sprintf("%v", uc.network.ID)
|
||||
uc.forEachDownstream(func(dc *downstreamConn) {
|
||||
dc.updateSupportedCaps()
|
||||
|
||||
if dc.caps["soju.im/bouncer-networks"] {
|
||||
dc.SendMessage(&irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BOUNCER",
|
||||
Params: []string{"NETWORK", netIDStr, "status=disconnected"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if uc.network.lastError == nil {
|
||||
uc.forEachDownstream(func(dc *downstreamConn) {
|
||||
sendServiceNOTICE(dc, fmt.Sprintf("disconnected from %s", uc.network.GetName()))
|
||||
if !dc.caps["soju.im/bouncer-networks"] {
|
||||
sendServiceNOTICE(dc, fmt.Sprintf("disconnected from %s", uc.network.GetName()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -701,6 +725,18 @@ func (u *user) createNetwork(record *Network) (*network, error) {
|
||||
|
||||
u.addNetwork(network)
|
||||
|
||||
// TODO: broadcast network status
|
||||
idStr := fmt.Sprintf("%v", network.ID)
|
||||
u.forEachDownstream(func(dc *downstreamConn) {
|
||||
if dc.caps["soju.im/bouncer-networks"] {
|
||||
dc.SendMessage(&irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BOUNCER",
|
||||
Params: []string{"NETWORK", idStr, "network=" + network.GetName()},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return network, nil
|
||||
}
|
||||
|
||||
@ -754,6 +790,8 @@ func (u *user) updateNetwork(record *Network) (*network, error) {
|
||||
// This will re-connect to the upstream server
|
||||
u.addNetwork(updatedNetwork)
|
||||
|
||||
// TODO: broadcast BOUNCER NETWORK notifications
|
||||
|
||||
return updatedNetwork, nil
|
||||
}
|
||||
|
||||
@ -768,6 +806,18 @@ func (u *user) deleteNetwork(id int64) error {
|
||||
}
|
||||
|
||||
u.removeNetwork(network)
|
||||
|
||||
idStr := fmt.Sprintf("%v", network.ID)
|
||||
u.forEachDownstream(func(dc *downstreamConn) {
|
||||
if dc.caps["soju.im/bouncer-networks"] {
|
||||
dc.SendMessage(&irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "BOUNCER",
|
||||
Params: []string{"NETWORK", idStr, "*"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user