From 0f82a088f9c90dd24ecc6eaca12836c1d4e5f3f9 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sat, 4 Jul 2026 10:36:44 -0700 Subject: [PATCH] appservice: start presence heartbeat before plugin init so bot shows online from boot --- internal/bot/appservice.go | 2 -- internal/bot/session.go | 22 +++++++++++++++++++-- main.go | 39 ++++++++++++++++++++++++-------------- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/internal/bot/appservice.go b/internal/bot/appservice.go index e7b5b8d..3de2776 100644 --- a/internal/bot/appservice.go +++ b/internal/bot/appservice.go @@ -227,8 +227,6 @@ func (s *Session) runAppservice(ctx context.Context) error { close(errCh) }() - go s.runPresenceHeartbeat(ctx) - slog.Info("appservice listener started", "address", s.as.Host.Address()) select { diff --git a/internal/bot/session.go b/internal/bot/session.go index 86e1adc..d42c3d1 100644 --- a/internal/bot/session.go +++ b/internal/bot/session.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "sync" "time" "maunium.net/go/mautrix" @@ -30,8 +31,9 @@ type Session struct { syncer *mautrix.DefaultSyncer // appservice - as *appservice.AppService - ep *appservice.EventProcessor + as *appservice.AppService + ep *appservice.EventProcessor + presenceOnce sync.Once } // EventHandler matches both DefaultSyncer.OnEventType and EventProcessor.On. @@ -75,6 +77,22 @@ func (s *Session) OnEventType(evtType event.Type, fn EventHandler) { } } +// StartPresence begins actively maintaining the bot's Matrix presence, tied to +// ctx. In appservice mode it launches the online heartbeat (runPresenceHeartbeat); +// masdevice mode relies on the /sync long-poll's implicit presence refresh, so it +// is a no-op there. Call this early — before the slow plugin init — so the bot +// shows online from boot rather than only once the transaction listener starts +// (~2min later). The presence PUT is outbound-only and needs no event handlers, so +// starting it ahead of the listener is safe. Idempotent: extra calls do nothing. +func (s *Session) StartPresence(ctx context.Context) { + if s.mode != "appservice" { + return + } + s.presenceOnce.Do(func() { + go s.runPresenceHeartbeat(ctx) + }) +} + // Run blocks, delivering events to registered handlers, until ctx is cancelled. func (s *Session) Run(ctx context.Context) error { switch s.mode { diff --git a/main.go b/main.go index 5ca6931..54d89a0 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,30 @@ func main() { } client := sess.Client + // ---- Process lifecycle + graceful shutdown ---- + // Set up the run context and signal handling before the (slow ~2min) plugin + // init so the presence heartbeat can start immediately. scheduler is wired + // later; the signal handler nil-checks it. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var scheduler *cron.Cron + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-sigCh + slog.Info("shutting down", "signal", sig) + if scheduler != nil { + scheduler.Stop() + } + sess.Stop() + cancel() + }() + + // Show the bot online from boot (appservice mode; no-op under masdevice, where + // /sync refreshes presence implicitly). Safe before the listener — outbound only. + sess.StartPresence(ctx) + // Create plugin registry registry := bot.NewRegistry() @@ -330,7 +354,7 @@ func main() { }) // ---- Set up cron scheduler ---- - scheduler := cron.New(cron.WithChain(cron.Recover(cronLogger{}))) + scheduler = cron.New(cron.WithChain(cron.Recover(cronLogger{}))) setupScheduledJobs(scheduler, client, wotdPlugin, holidaysPlugin, gamingPlugin, birthdayPlugin, animePlugin, moviesPlugin, concertsPlugin, esteemedPlugin, forexPlugin, minifluxPlugin, marketPlugin) scheduler.Start() @@ -340,19 +364,6 @@ func main() { // ---- Start receiving events ---- slog.Info("GogoBee starting", "auth_mode", envOr("AUTH_MODE", "masdevice")) - ctx, cancel := context.WithCancel(context.Background()) - - // Graceful shutdown - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - go func() { - sig := <-sigCh - slog.Info("shutting down", "signal", sig) - scheduler.Stop() - sess.Stop() - cancel() - }() - if err := sess.Run(ctx); err != nil { slog.Error("event loop stopped", "err", err) }