package main import ( "sync" "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" ) // Plugin implements the Mattermost plugin interface. type Plugin struct { plugin.MattermostPlugin // configurationLock synchronizes access to the configuration. configurationLock sync.RWMutex // configuration is the active plugin configuration. configuration *Configuration // store wraps KV store operations for session persistence. store *Store // botUserIDLock synchronizes access to botUserID. botUserIDLock sync.RWMutex // botUserID is the plugin's bot user ID (created on activation). botUserID string // stopCleanup signals the cleanup goroutine to stop. stopCleanup chan struct{} } // OnActivate is called when the plugin is activated. func (p *Plugin) OnActivate() error { p.store = NewStore(p.API) // Ensure plugin bot user exists botID, appErr := p.API.EnsureBotUser(&model.Bot{ Username: "livestatus", DisplayName: "Live Status", Description: "OpenClaw Live Status plugin bot", }) if appErr != nil { p.API.LogWarn("Failed to ensure bot user", "error", appErr.Error()) } else { p.botUserIDLock.Lock() p.botUserID = botID p.botUserIDLock.Unlock() p.API.LogInfo("Plugin bot user ensured", "botUserID", botID) } // Start session cleanup goroutine p.stopCleanup = make(chan struct{}) go p.sessionCleanupLoop() p.API.LogInfo("OpenClaw Live Status plugin activated") return nil } // sessionCleanupLoop runs periodically to clean up stale and expired sessions. func (p *Plugin) sessionCleanupLoop() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for { select { case <-ticker.C: staleThreshold := int64(30 * 60 * 1000) // 30 minutes — active sessions with no update expireThreshold := int64(60 * 60 * 1000) // 1 hour — completed/interrupted sessions cleaned, expired, err := p.store.CleanStaleSessions(staleThreshold, expireThreshold) if err != nil { p.API.LogWarn("Session cleanup error", "error", err.Error()) } else if cleaned > 0 || expired > 0 { p.API.LogInfo("Session cleanup completed", "stale_marked", cleaned, "expired_deleted", expired) } case <-p.stopCleanup: return } } } // getBotUserID returns the plugin's bot user ID (thread-safe). func (p *Plugin) getBotUserID() string { p.botUserIDLock.RLock() defer p.botUserIDLock.RUnlock() return p.botUserID } // OnDeactivate is called when the plugin is deactivated. func (p *Plugin) OnDeactivate() error { // Stop cleanup goroutine if p.stopCleanup != nil { close(p.stopCleanup) } // Mark all active sessions as interrupted sessions, err := p.store.ListAllSessions() if err != nil { p.API.LogWarn("Failed to list sessions on deactivate", "error", err.Error()) } else { for _, s := range sessions { // Skip sessions already in a terminal state — do not overwrite done/error if s.Status == "done" || s.Status == "error" { continue } s.Status = "interrupted" _ = p.store.SaveSession(s.SessionKey, s) p.broadcastUpdate(s.ChannelID, s) } } p.API.LogInfo("OpenClaw Live Status plugin deactivated") return nil }