aboutsummaryrefslogtreecommitdiff
path: root/hotline
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2026-06-25 07:44:23 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2026-06-25 07:44:23 -0700
commitbba70d0a2903ab1fff5a2e89cff97cb1f6c7f82a (patch)
treecb8bd5cda76f287358bd7b73292ae024e756c824 /hotline
parent4912c3a7c20d765692993f6caaa98a94f5ae28cb (diff)
Introduce PresenceTracker interface for online-user tracking
Replace the leaky *redis.Client field on Server with a PresenceTracker interface that receives connect/rename/disconnect lifecycle events. This removes Redis-specific set encoding from hotline/server.go and the session handlers, and drops the redis dependency from the hotline package. The Redis implementation moves to mobius.RedisPresenceTracker, which owns the legacy "login::ip"/"login:nickname:ip" set encoding so existing deployments keep working. The API server reads online users through a new OnlineLister interface and falls back to the in-memory ClientMgr when no presence tracker is configured. Startup clearing of stale online state now happens unconditionally when Redis is configured, not only when the API server is enabled.
Diffstat (limited to 'hotline')
-rw-r--r--hotline/ban.go3
-rw-r--r--hotline/presence.go20
-rw-r--r--hotline/server.go32
3 files changed, 37 insertions, 18 deletions
diff --git a/hotline/ban.go b/hotline/ban.go
index 2713fbe..43988ab 100644
--- a/hotline/ban.go
+++ b/hotline/ban.go
@@ -9,13 +9,12 @@ import (
// BanDuration is the length of time for temporary bans.
const BanDuration = 30 * time.Minute
-// Redis key constants for ban management and online user tracking
+// Redis key constants for ban management
const (
RedisKeyBannedIPs = "mobius:banned:ips"
RedisKeyBannedUsers = "mobius:banned:users"
RedisKeyBannedNicknames = "mobius:banned:nicknames"
RedisKeyTempBannedIPs = "mobius:temp_banned:ips:"
- RedisKeyOnline = "mobius:online"
)
type BanMgr interface {
diff --git a/hotline/presence.go b/hotline/presence.go
new file mode 100644
index 0000000..7e41357
--- /dev/null
+++ b/hotline/presence.go
@@ -0,0 +1,20 @@
+package hotline
+
+// PresenceTracker receives best-effort notifications about user session lifecycle
+// events so an external system (e.g. Redis) can maintain a list of online users.
+//
+// It is optional: when a Server has no PresenceTracker, online presence is derived
+// from the in-memory ClientManager instead. Implementations must be safe for
+// concurrent use by multiple goroutines.
+type PresenceTracker interface {
+ // UserConnected is called after a successful login, before the nickname is known.
+ UserConnected(login, ip string)
+
+ // UserRenamed is called when a user's nickname is set or changed. oldNickname is
+ // empty the first time a nickname is set (e.g. the 1.2.3 login flow or TranAgreed).
+ UserRenamed(login, oldNickname, newNickname, ip string)
+
+ // UserDisconnected is called when a user's session ends. nickname is empty if the
+ // user disconnected before setting one.
+ UserDisconnected(login, nickname, ip string)
+}
diff --git a/hotline/server.go b/hotline/server.go
index 8a530c2..020f878 100644
--- a/hotline/server.go
+++ b/hotline/server.go
@@ -16,7 +16,6 @@ import (
"sync"
"time"
- "github.com/redis/go-redis/v9"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/charmap"
"golang.org/x/time/rate"
@@ -62,7 +61,9 @@ type Server struct {
MessageBoard io.ReadWriteSeeker
- Redis *redis.Client
+ // Presence optionally records user session lifecycle events for an external online-user
+ // list. When nil, online presence is derived from ClientMgr.
+ Presence PresenceTracker
// TrackerRegistrar handles tracker registration (injectable for testing)
TrackerRegistrar TrackerRegistrar
@@ -133,6 +134,13 @@ func WithTrackerRegistrar(registrar TrackerRegistrar) func(s *Server) {
}
}
+// WithPresenceTracker optionally sets a PresenceTracker to record user session lifecycle events.
+func WithPresenceTracker(p PresenceTracker) func(s *Server) {
+ return func(s *Server) {
+ s.Presence = p
+ }
+}
+
// WithTLS optionally enables TLS support on the specified port.
func WithTLS(tlsConfig *tls.Config, port int) func(s *Server) {
return func(s *Server) {
@@ -577,18 +585,14 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
// transaction ordering and prevents interleaved writes.
go c.writeLoop()
- // TODO: refactor this into a connection manager interface, maybe?
- if s.Redis != nil {
- s.Redis.SAdd(context.Background(), RedisKeyOnline, login+"::"+ipAddr)
+ if s.Presence != nil {
+ s.Presence.UserConnected(login, ipAddr)
}
// Remove the client from the list of connected clients when they disconnect
defer func() {
- if s.Redis != nil {
- s.Redis.SRem(context.Background(), RedisKeyOnline, login+"::"+ipAddr)
- if userName := c.GetUserName(); len(userName) != 0 {
- s.Redis.SRem(context.Background(), RedisKeyOnline, login+":"+string(userName)+":"+ipAddr)
- }
+ if s.Presence != nil {
+ s.Presence.UserDisconnected(login, string(c.GetUserName()), ipAddr)
}
c.Disconnect()
}()
@@ -665,12 +669,8 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
c.Logger = c.Logger.With("name", string(userName))
c.Logger.Info("Login successful")
- // Update the Redis set with the new information
- if s.Redis != nil {
- // Remove old entry (login::ip)
- s.Redis.SRem(context.Background(), RedisKeyOnline, login+"::"+ipAddr)
- // Add new entry with login, nickname, ip
- s.Redis.SAdd(context.Background(), RedisKeyOnline, login+":"+string(userName)+":"+ipAddr)
+ if s.Presence != nil {
+ s.Presence.UserRenamed(login, "", string(userName), ipAddr)
}
// Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this