From d9186e994de9a83e6822a5bd08c4640e48e8368b Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Tue, 28 Apr 2020 15:27:41 +0200 Subject: [PATCH] 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. --- db.go | 25 ++++++++++++++--------- doc/soju.1.scd | 3 ++- downstream.go | 54 ++++++++++++++++++++++++++++++++------------------ upstream.go | 20 ++++++++++++++++++- user.go | 49 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 120 insertions(+), 31 deletions(-) diff --git a/db.go b/db.go index 08a1fa0..20ebb4e 100644 --- a/db.go +++ b/db.go @@ -43,9 +43,10 @@ func (net *Network) GetName() string { } type Channel struct { - ID int64 - Name string - Key string + ID int64 + Name string + Key string + Detached bool } const schema = ` @@ -76,6 +77,7 @@ CREATE TABLE Channel ( network INTEGER NOT NULL, name VARCHAR(255) NOT NULL, key VARCHAR(255), + detached INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(network) REFERENCES Network(id), UNIQUE(network, name) ); @@ -84,6 +86,7 @@ CREATE TABLE Channel ( var migrations = []string{ "", // migration #0 is reserved for schema initialization "ALTER TABLE Network ADD COLUMN connect_commands VARCHAR(1023)", + "ALTER TABLE Channel ADD COLUMN detached INTEGER NOT NULL DEFAULT 0", } type DB struct { @@ -346,7 +349,9 @@ func (db *DB) ListChannels(networkID int64) ([]Channel, error) { db.lock.RLock() 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 { return nil, err } @@ -356,7 +361,7 @@ func (db *DB) ListChannels(networkID int64) ([]Channel, error) { for rows.Next() { var ch Channel 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 } ch.Key = fromStringPtr(key) @@ -378,12 +383,14 @@ func (db *DB) StoreChannel(networkID int64, ch *Channel) error { var err error if ch.ID != 0 { _, err = db.db.Exec(`UPDATE Channel - SET network = ?, name = ?, key = ? - WHERE id = ?`, networkID, ch.Name, key, ch.ID) + SET network = ?, name = ?, key = ?, detached = ? + WHERE id = ?`, + networkID, ch.Name, key, ch.Detached, ch.ID) } else { var res sql.Result - res, err = db.db.Exec(`INSERT INTO Channel(network, name, key) - VALUES (?, ?, ?)`, networkID, ch.Name, key) + res, err = db.db.Exec(`INSERT INTO Channel(network, name, key, detached) + VALUES (?, ?, ?, ?)`, + networkID, ch.Name, key, ch.Detached) if err != nil { return err } diff --git a/doc/soju.1.scd b/doc/soju.1.scd index 5f05429..00bdc8e 100644 --- a/doc/soju.1.scd +++ b/doc/soju.1.scd @@ -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 the next connection. When registering or authenticating with NickServ, 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 marked as away. diff --git a/downstream.go b/downstream.go index 8697e15..be2b688 100644 --- a/downstream.go +++ b/downstream.go @@ -767,15 +767,20 @@ func (dc *downstreamConn) welcome() error { dc.forEachUpstream(func(uc *upstreamConn) { for _, ch := range uc.channels { - if ch.complete { - dc.SendMessage(&irc.Message{ - Prefix: dc.prefix(), - Command: "JOIN", - Params: []string{dc.marshalEntity(ch.conn.network, ch.Name)}, - }) - - forwardChannel(dc, ch) + if !ch.complete { + continue } + if record, ok := uc.network.channels[ch.Name]; ok && record.Detached { + continue + } + + dc.SendMessage(&irc.Message{ + Prefix: dc.prefix(), + Command: "JOIN", + Params: []string{dc.marshalEntity(ch.conn.network, ch.Name)}, + }) + + forwardChannel(dc, ch) } }) @@ -793,6 +798,10 @@ func (dc *downstreamConn) welcome() error { func (dc *downstreamConn) sendNetworkHistory(net *network) { for target, history := range net.history { + if ch, ok := net.channels[target]; ok && ch.Detached { + continue + } + seq, ok := history.offlineClients[dc.clientName] if !ok { continue @@ -945,7 +954,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error { Params: params, }) - ch := &Channel{Name: upstreamName, Key: key} + ch := &Channel{Name: upstreamName, Key: key, Detached: false} if err := uc.network.createUpdateChannel(ch); err != nil { dc.logger.Printf("failed to create or update channel %q: %v", upstreamName, err) } @@ -967,17 +976,24 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error { return err } - params := []string{upstreamName} - if reason != "" { - params = append(params, reason) - } - uc.SendMessage(&irc.Message{ - Command: "PART", - Params: params, - }) + 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} + if reason != "" { + params = append(params, reason) + } + uc.SendMessage(&irc.Message{ + Command: "PART", + Params: params, + }) - if err := uc.network.deleteChannel(upstreamName); err != nil { - dc.logger.Printf("failed to delete channel %q: %v", upstreamName, err) + if err := uc.network.deleteChannel(upstreamName); err != nil { + dc.logger.Printf("failed to delete channel %q: %v", upstreamName, err) + } } } case "KICK": diff --git a/upstream.go b/upstream.go index 3fd367c..9a47ef9 100644 --- a/upstream.go +++ b/upstream.go @@ -1405,8 +1405,13 @@ func (uc *upstreamConn) appendLog(entity string, msg *irc.Message) { // appendHistory appends a message to the history. entity can be empty. 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 len(uc.network.offlineClients) == 0 { + if len(uc.network.offlineClients) == 0 && !detached { return } @@ -1421,6 +1426,14 @@ func (uc *upstreamConn) appendHistory(entity string, msg *irc.Message) { for clientName, _ := range uc.network.offlineClients { 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) @@ -1438,6 +1451,11 @@ func (uc *upstreamConn) produce(target string, msg *irc.Message, origin *downstr 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) { if dc != origin || dc.caps["echo-message"] { dc.SendMessage(dc.marshalMessage(msg, uc.network)) diff --git a/user.go b/user.go index 6a93489..13d51e8 100644 --- a/user.go +++ b/user.go @@ -150,7 +150,51 @@ func (net *network) createUpdateChannel(ch *Channel) error { if err := net.user.srv.db.StoreChannel(net.ID, ch); err != nil { return err } + prev := net.channels[ch.Name] 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 } @@ -342,7 +386,10 @@ func (u *user) run() { } 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() } })