Keep active IRC listener users from expiring (closes #121) #126

Merged
clawbot merged 1 commits from issue-121-irc-last-seen into next 2026-10-08 10:47:56 +02:00
4 changed files with 70 additions and 5 deletions
+2 -2
View File
@@ -2243,7 +2243,7 @@ The database schema is managed via embedded SQL migration files in
| `signing_key` | TEXT | Public signing key (empty string if unset) |
| `away_message` | TEXT | Away message (empty string if not away) |
| `created_at` | DATETIME | Session creation time |
| `last_seen` | DATETIME | Last API request time |
| `last_seen` | DATETIME | Last API request or IRC listener command time |
Index on `(uuid)`.
@@ -2258,7 +2258,7 @@ Index on `(uuid)`.
| `ip` | TEXT | Real IP address of this client connection |
| `hostname` | TEXT | Reverse DNS hostname of this client connection |
| `created_at` | DATETIME | Client creation time |
| `last_seen` | DATETIME | Last API request time |
| `last_seen` | DATETIME | Last API request or IRC listener command time |
Indexes on `(token)` and `(session_id)`.
+24 -3
View File
@@ -204,21 +204,42 @@ func (database *Database) GetSessionByToken(
)
}
_ = database.UpdateLastSeen(ctx, sessionID, clientID)
return sessionID, clientID, nick, nil
}
// UpdateLastSeen sets last_seen to now on a session and on
// one of its clients, so that the idle cleanup keeps them.
func (database *Database) UpdateLastSeen(
ctx context.Context,
sessionID, clientID int64,
) error {
now := time.Now()
_, _ = database.conn.ExecContext(
_, err := database.conn.ExecContext(
ctx,
"UPDATE sessions SET last_seen = ? WHERE id = ?",
now, sessionID,
)
if err != nil {
return fmt.Errorf(
"update session last_seen: %w", err,
)
}
_, _ = database.conn.ExecContext(
_, err = database.conn.ExecContext(
ctx,
"UPDATE clients SET last_seen = ? WHERE id = ?",
now, clientID,
)
if err != nil {
return fmt.Errorf(
"update client last_seen: %w", err,
)
}
return sessionID, clientID, nick, nil
return nil
}
// GetSessionByNick returns session id for a given nick.
+11
View File
@@ -368,6 +368,17 @@ func (c *Conn) handleMessage(
return
}
// Every command is activity, so the idle cleanup must not
// remove this user.
err := c.database.UpdateLastSeen(
ctx, c.sessionID, c.clientID,
)
if err != nil {
c.log.Error(
"failed to update last_seen", "error", err,
)
}
handler, ok := c.commands[msg.Command]
if !ok {
c.sendNumeric(
+33
View File
@@ -369,6 +369,39 @@ func TestPingPong(t *testing.T) {
assertContains(t, lines, "PONG", "PONG response")
}
// TestIdleCleanupKeepsActiveUser checks that a user who
// sends commands is kept by the idle cleanup, and a user who
// sends nothing is not.
func TestIdleCleanupKeepsActiveUser(t *testing.T) {
t.Parallel()
const idleTimeout = 500 * time.Millisecond
env := newTestEnv(t)
active := env.dial(t)
active.register("active")
idle := env.dial(t)
idle.register("idle")
time.Sleep(idleTimeout)
active.sendAndExpect("PING :still here", "PONG")
// The idle cleanup in internal/handlers removes exactly
// the users this returns.
stale, err := env.database.GetStaleOrphanSessions(
t.Context(), time.Now().Add(-idleTimeout),
)
if err != nil {
t.Fatalf("get stale sessions: %v", err)
}
if len(stale) != 1 || stale[0].Nick != "idle" {
t.Errorf("cleanup removes %v, want only idle", stale)
}
}
func TestJoinChannel(t *testing.T) {
t.Parallel()