2 Commits
Author SHA1 Message Date
clawbot f375c0c26b Bring README.md in line with the modes and the schema (closes #119)
check / check (push) Canceled after 0s
The MODE section described a query-only command. It now covers channel
and user mode queries and changes on the HTTP API and the IRC listener,
the letters each accepts, what is broadcast, and the error numerics.
The schema section now lists every table and column in
001_initial.sql.

Every other statement this change touches was checked against the code:
polled numerics carry their name in command and their number in code;
command errors are numerics with HTTP 200; the broker is keyed by
session; WAL is never on; a hostname falls back to the IP and is
public; and the IRC listener handles modes, NOTICE, INVITE, +s and +H
more narrowly, with gaps in its bridge to the HTTP API.

Model: opus-5-5
2026-10-08 08:58:25 +00:00
clawbot ccb2bf7960 Keep active IRC listener users from expiring (closes #121)
check / check (push) Canceled after 0s
Every command a registered IRC listener client sends, PING and PONG
included, now sets last_seen on its session and its client, so the
idle cleanup no longer removes an IRC user who is still active; one
who sends nothing for SESSION_IDLE_TIMEOUT is still removed. The two
UPDATEs that GetSessionByToken ran for every authenticated HTTP API
request move into UpdateLastSeen, which both transports now call. The
IRC listener refreshes on every command, with no interval between
writes: that is what every HTTP API request already costs. README.md
describes last_seen to match.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
2026-10-08 10:47:55 +02:00
4 changed files with 781 additions and 434 deletions
+713 -431
View File
File diff suppressed because it is too large Load Diff
+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()