Add support for detached channels

Channels can now be detached by leaving them with the reason "detach",
and re-attached by joining them again. Upon detaching the channel is
no longer forwarded to downstream connections. Upon re-attaching the
history buffer is sent.
This commit is contained in:
Simon Ser 2020-04-28 15:27:41 +02:00
parent a99d6dd019
commit d9186e994d
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
5 changed files with 120 additions and 31 deletions

19
db.go
View File

@ -46,6 +46,7 @@ type Channel struct {
ID int64 ID int64
Name string Name string
Key string Key string
Detached bool
} }
const schema = ` const schema = `
@ -76,6 +77,7 @@ CREATE TABLE Channel (
network INTEGER NOT NULL, network INTEGER NOT NULL,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
key VARCHAR(255), key VARCHAR(255),
detached INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(network) REFERENCES Network(id), FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, name) UNIQUE(network, name)
); );
@ -84,6 +86,7 @@ CREATE TABLE Channel (
var migrations = []string{ var migrations = []string{
"", // migration #0 is reserved for schema initialization "", // migration #0 is reserved for schema initialization
"ALTER TABLE Network ADD COLUMN connect_commands VARCHAR(1023)", "ALTER TABLE Network ADD COLUMN connect_commands VARCHAR(1023)",
"ALTER TABLE Channel ADD COLUMN detached INTEGER NOT NULL DEFAULT 0",
} }
type DB struct { type DB struct {
@ -346,7 +349,9 @@ func (db *DB) ListChannels(networkID int64) ([]Channel, error) {
db.lock.RLock() db.lock.RLock()
defer db.lock.RUnlock() defer db.lock.RUnlock()
rows, err := db.db.Query("SELECT id, name, key FROM Channel WHERE network = ?", networkID) rows, err := db.db.Query(`SELECT id, name, key, detached
FROM Channel
WHERE network = ?`, networkID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -356,7 +361,7 @@ func (db *DB) ListChannels(networkID int64) ([]Channel, error) {
for rows.Next() { for rows.Next() {
var ch Channel var ch Channel
var key *string var key *string
if err := rows.Scan(&ch.ID, &ch.Name, &key); err != nil { if err := rows.Scan(&ch.ID, &ch.Name, &key, &ch.Detached); err != nil {
return nil, err return nil, err
} }
ch.Key = fromStringPtr(key) ch.Key = fromStringPtr(key)
@ -378,12 +383,14 @@ func (db *DB) StoreChannel(networkID int64, ch *Channel) error {
var err error var err error
if ch.ID != 0 { if ch.ID != 0 {
_, err = db.db.Exec(`UPDATE Channel _, err = db.db.Exec(`UPDATE Channel
SET network = ?, name = ?, key = ? SET network = ?, name = ?, key = ?, detached = ?
WHERE id = ?`, networkID, ch.Name, key, ch.ID) WHERE id = ?`,
networkID, ch.Name, key, ch.Detached, ch.ID)
} else { } else {
var res sql.Result var res sql.Result
res, err = db.db.Exec(`INSERT INTO Channel(network, name, key) res, err = db.db.Exec(`INSERT INTO Channel(network, name, key, detached)
VALUES (?, ?, ?)`, networkID, ch.Name, key) VALUES (?, ?, ?, ?)`,
networkID, ch.Name, key, ch.Detached)
if err != nil { if err != nil {
return err return err
} }

View File

@ -23,7 +23,8 @@ behalf of the user to provide extra features.
When joining a channel, the channel will be saved and automatically joined on When joining a channel, the channel will be saved and automatically joined on
the next connection. When registering or authenticating with NickServ, the the next connection. When registering or authenticating with NickServ, the
credentials will be saved and automatically used on the next connection if the credentials will be saved and automatically used on the next connection if the
server supports SASL. server supports SASL. When parting a channel with the reason "detach", the
channel will be detached instead of being left.
When all clients are disconnected from the bouncer, the user is automatically When all clients are disconnected from the bouncer, the user is automatically
marked as away. marked as away.

View File

@ -767,7 +767,13 @@ func (dc *downstreamConn) welcome() error {
dc.forEachUpstream(func(uc *upstreamConn) { dc.forEachUpstream(func(uc *upstreamConn) {
for _, ch := range uc.channels { for _, ch := range uc.channels {
if ch.complete { if !ch.complete {
continue
}
if record, ok := uc.network.channels[ch.Name]; ok && record.Detached {
continue
}
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.prefix(), Prefix: dc.prefix(),
Command: "JOIN", Command: "JOIN",
@ -776,7 +782,6 @@ func (dc *downstreamConn) welcome() error {
forwardChannel(dc, ch) forwardChannel(dc, ch)
} }
}
}) })
dc.forEachNetwork(func(net *network) { dc.forEachNetwork(func(net *network) {
@ -793,6 +798,10 @@ func (dc *downstreamConn) welcome() error {
func (dc *downstreamConn) sendNetworkHistory(net *network) { func (dc *downstreamConn) sendNetworkHistory(net *network) {
for target, history := range net.history { for target, history := range net.history {
if ch, ok := net.channels[target]; ok && ch.Detached {
continue
}
seq, ok := history.offlineClients[dc.clientName] seq, ok := history.offlineClients[dc.clientName]
if !ok { if !ok {
continue continue
@ -945,7 +954,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
Params: params, Params: params,
}) })
ch := &Channel{Name: upstreamName, Key: key} ch := &Channel{Name: upstreamName, Key: key, Detached: false}
if err := uc.network.createUpdateChannel(ch); err != nil { if err := uc.network.createUpdateChannel(ch); err != nil {
dc.logger.Printf("failed to create or update channel %q: %v", upstreamName, err) dc.logger.Printf("failed to create or update channel %q: %v", upstreamName, err)
} }
@ -967,6 +976,12 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
return err return err
} }
if strings.EqualFold(reason, "detach") {
ch := &Channel{Name: upstreamName, Detached: true}
if err := uc.network.createUpdateChannel(ch); err != nil {
dc.logger.Printf("failed to detach channel %q: %v", upstreamName, err)
}
} else {
params := []string{upstreamName} params := []string{upstreamName}
if reason != "" { if reason != "" {
params = append(params, reason) params = append(params, reason)
@ -980,6 +995,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
dc.logger.Printf("failed to delete channel %q: %v", upstreamName, err) dc.logger.Printf("failed to delete channel %q: %v", upstreamName, err)
} }
} }
}
case "KICK": case "KICK":
var channelStr, userStr string var channelStr, userStr string
if err := parseMessageParams(msg, &channelStr, &userStr); err != nil { if err := parseMessageParams(msg, &channelStr, &userStr); err != nil {

View File

@ -1405,8 +1405,13 @@ func (uc *upstreamConn) appendLog(entity string, msg *irc.Message) {
// appendHistory appends a message to the history. entity can be empty. // appendHistory appends a message to the history. entity can be empty.
func (uc *upstreamConn) appendHistory(entity string, msg *irc.Message) { func (uc *upstreamConn) appendHistory(entity string, msg *irc.Message) {
detached := false
if ch, ok := uc.network.channels[entity]; ok {
detached = ch.Detached
}
// If no client is offline, no need to append the message to the buffer // If no client is offline, no need to append the message to the buffer
if len(uc.network.offlineClients) == 0 { if len(uc.network.offlineClients) == 0 && !detached {
return return
} }
@ -1421,6 +1426,14 @@ func (uc *upstreamConn) appendHistory(entity string, msg *irc.Message) {
for clientName, _ := range uc.network.offlineClients { for clientName, _ := range uc.network.offlineClients {
history.offlineClients[clientName] = 0 history.offlineClients[clientName] = 0
} }
if detached {
// If the channel is detached, online clients act as offline
// clients too
uc.forEachDownstream(func(dc *downstreamConn) {
history.offlineClients[dc.clientName] = 0
})
}
} }
history.ring.Produce(msg) history.ring.Produce(msg)
@ -1438,6 +1451,11 @@ func (uc *upstreamConn) produce(target string, msg *irc.Message, origin *downstr
uc.appendHistory(target, msg) uc.appendHistory(target, msg)
// Don't forward messages if it's a detached channel
if ch, ok := uc.network.channels[target]; ok && ch.Detached {
return
}
uc.forEachDownstream(func(dc *downstreamConn) { uc.forEachDownstream(func(dc *downstreamConn) {
if dc != origin || dc.caps["echo-message"] { if dc != origin || dc.caps["echo-message"] {
dc.SendMessage(dc.marshalMessage(msg, uc.network)) dc.SendMessage(dc.marshalMessage(msg, uc.network))

49
user.go
View File

@ -150,7 +150,51 @@ func (net *network) createUpdateChannel(ch *Channel) error {
if err := net.user.srv.db.StoreChannel(net.ID, ch); err != nil { if err := net.user.srv.db.StoreChannel(net.ID, ch); err != nil {
return err return err
} }
prev := net.channels[ch.Name]
net.channels[ch.Name] = ch net.channels[ch.Name] = ch
if prev != nil && prev.Detached != ch.Detached {
history := net.history[ch.Name]
if ch.Detached {
net.user.srv.Logger.Printf("network %q: detaching channel %q", net.GetName(), ch.Name)
net.forEachDownstream(func(dc *downstreamConn) {
net.offlineClients[dc.clientName] = struct{}{}
if history != nil {
history.offlineClients[dc.clientName] = history.ring.Cur()
}
dc.SendMessage(&irc.Message{
Prefix: dc.prefix(),
Command: "PART",
Params: []string{dc.marshalEntity(net, ch.Name), "Detach"},
})
})
} else {
net.user.srv.Logger.Printf("network %q: attaching channel %q", net.GetName(), ch.Name)
var uch *upstreamChannel
if net.conn != nil {
uch = net.conn.channels[ch.Name]
}
net.forEachDownstream(func(dc *downstreamConn) {
dc.SendMessage(&irc.Message{
Prefix: dc.prefix(),
Command: "JOIN",
Params: []string{dc.marshalEntity(net, ch.Name)},
})
if uch != nil {
forwardChannel(dc, uch)
}
if history != nil {
dc.sendNetworkHistory(net)
}
})
}
}
return nil return nil
} }
@ -342,7 +386,10 @@ func (u *user) run() {
} }
net.offlineClients[dc.clientName] = struct{}{} net.offlineClients[dc.clientName] = struct{}{}
for _, history := range net.history { for target, history := range net.history {
if ch, ok := net.channels[target]; ok && ch.Detached {
continue
}
history.offlineClients[dc.clientName] = history.ring.Cur() history.offlineClients[dc.clientName] = history.ring.Cur()
} }
}) })