diff --git a/internal/db/queries.go b/internal/db/queries.go index 60e409e..d6fbb91 100644 --- a/internal/db/queries.go +++ b/internal/db/queries.go @@ -796,8 +796,8 @@ func (database *Database) DeleteClient( return nil } -// GetSessionCount returns the number of active sessions. -func (database *Database) GetSessionCount( +// GetUserCount returns the number of active users. +func (database *Database) GetUserCount( ctx context.Context, ) (int64, error) { var count int64 @@ -808,7 +808,7 @@ func (database *Database) GetSessionCount( ).Scan(&count) if err != nil { return 0, fmt.Errorf( - "get session count: %w", err, + "get user count: %w", err, ) } @@ -838,9 +838,9 @@ func (database *Database) ClientCountForSession( return count, nil } -// DeleteStaleSessions removes clients not seen since the -// cutoff and cleans up orphaned sessions. -func (database *Database) DeleteStaleSessions( +// DeleteStaleUsers removes clients not seen since the +// cutoff and cleans up orphaned users (sessions). +func (database *Database) DeleteStaleUsers( ctx context.Context, cutoff time.Time, ) (int64, error) { diff --git a/internal/handlers/api.go b/internal/handlers/api.go index 47923f4..b963f9b 100644 --- a/internal/handlers/api.go +++ b/internal/handlers/api.go @@ -1361,20 +1361,23 @@ func (hdlr *Handlers) canAccessChannelHistory( return true } -// HandleLogout deletes the authenticated client's token. +// HandleLogout deletes the authenticated client's token +// and cleans up the user (session) if no clients remain. func (hdlr *Handlers) HandleLogout() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { - _, clientID, _, ok := + sessionID, clientID, nick, ok := hdlr.requireAuth(writer, request) if !ok { return } + ctx := request.Context() + err := hdlr.params.Database.DeleteClient( - request.Context(), clientID, + ctx, clientID, ) if err != nil { hdlr.log.Error( @@ -1389,12 +1392,79 @@ func (hdlr *Handlers) HandleLogout() http.HandlerFunc { return } + // If no clients remain, clean up the user fully: + // part all channels (notifying members) and + // delete the session. + remaining, err := hdlr.params.Database. + ClientCountForSession(ctx, sessionID) + if err != nil { + hdlr.log.Error( + "client count check failed", "error", err, + ) + } + + if remaining == 0 { + hdlr.cleanupUser( + request, sessionID, nick, + ) + } + hdlr.respondJSON(writer, request, map[string]string{"status": "ok"}, http.StatusOK) } } +// cleanupUser parts the user from all channels (notifying +// members) and deletes the session. +func (hdlr *Handlers) cleanupUser( + request *http.Request, + sessionID int64, + nick string, +) { + ctx := request.Context() + + channels, _ := hdlr.params.Database. + GetSessionChannels(ctx, sessionID) + + notified := map[int64]bool{} + + var quitDBID int64 + + if len(channels) > 0 { + quitDBID, _, _ = hdlr.params.Database.InsertMessage( + ctx, "QUIT", nick, "", nil, nil, + ) + } + + for _, chanInfo := range channels { + memberIDs, _ := hdlr.params.Database. + GetChannelMemberIDs(ctx, chanInfo.ID) + + for _, mid := range memberIDs { + if mid != sessionID && !notified[mid] { + notified[mid] = true + + _ = hdlr.params.Database.EnqueueToSession( + ctx, mid, quitDBID, + ) + + hdlr.broker.Notify(mid) + } + } + + _ = hdlr.params.Database.PartChannel( + ctx, chanInfo.ID, sessionID, + ) + + _ = hdlr.params.Database.DeleteChannelIfEmpty( + ctx, chanInfo.ID, + ) + } + + _ = hdlr.params.Database.DeleteSession(ctx, sessionID) +} + // HandleUsersMe returns the current user's session info. func (hdlr *Handlers) HandleUsersMe() http.HandlerFunc { return hdlr.HandleState() @@ -1406,12 +1476,12 @@ func (hdlr *Handlers) HandleServerInfo() http.HandlerFunc { writer http.ResponseWriter, request *http.Request, ) { - users, err := hdlr.params.Database.GetSessionCount( + users, err := hdlr.params.Database.GetUserCount( request.Context(), ) if err != nil { hdlr.log.Error( - "get session count failed", "error", err, + "get user count failed", "error", err, ) hdlr.respondError( writer, request, diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 763edf1..7ce952e 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -124,8 +124,16 @@ func (hdlr *Handlers) idleTimeout() time.Duration { return dur } -func (hdlr *Handlers) startCleanup(ctx context.Context) { - cleanupCtx, cancel := context.WithCancel(ctx) +// startCleanup launches the idle-user cleanup goroutine. +// We use context.Background rather than the OnStart ctx +// because the OnStart context is startup-scoped and would +// cancel the goroutine once all start hooks complete. +// +//nolint:contextcheck // intentional Background ctx +func (hdlr *Handlers) startCleanup(_ context.Context) { + cleanupCtx, cancel := context.WithCancel( + context.Background(), + ) hdlr.cancelCleanup = cancel go hdlr.cleanupLoop(cleanupCtx) @@ -161,12 +169,12 @@ func (hdlr *Handlers) runCleanup( ) { cutoff := time.Now().Add(-timeout) - deleted, err := hdlr.params.Database.DeleteStaleSessions( + deleted, err := hdlr.params.Database.DeleteStaleUsers( ctx, cutoff, ) if err != nil { hdlr.log.Error( - "session cleanup failed", "error", err, + "user cleanup failed", "error", err, ) return @@ -174,7 +182,7 @@ func (hdlr *Handlers) runCleanup( if deleted > 0 { hdlr.log.Info( - "cleaned up stale clients", + "cleaned up stale users", "deleted", deleted, ) }