package laniakea import ( "context" "fmt" "time" "git.scuroneko.dev/scuroneko/laniakea/tgapi" ) // HandlerEventKind identifies the kind of handler observed by runtime events. type HandlerEventKind string const ( // HandlerCommandKind identifies a command handler. HandlerCommandKind HandlerEventKind = "command" // HandlerMessageKind identifies a message fallback handler. HandlerMessageKind HandlerEventKind = "message" // HandlerMiddlewareKind identifies middleware execution. HandlerMiddlewareKind HandlerEventKind = "middleware" // HandlerPayloadKind identifies a callback payload handler. HandlerPayloadKind HandlerEventKind = "payload" // HandlerUpdateKind identifies a generic update handler. HandlerUpdateKind HandlerEventKind = "update" // HandlerRunnerKind identifies a background runner execution. HandlerRunnerKind HandlerEventKind = "runner" // HandlerPollingKind identifies polling and getUpdates runtime work. HandlerPollingKind HandlerEventKind = "polling" // HandlerSceneKind identifies a scene runtime handler wrapper. HandlerSceneKind HandlerEventKind = "scene" // HandlerSceneStepKind identifies a scene step handler. HandlerSceneStepKind HandlerEventKind = "scene_step" // HandlerSceneCommandKind identifies a scene-local command handler. HandlerSceneCommandKind HandlerEventKind = "scene_command" // HandlerScenePayloadKind identifies a scene-local callback payload handler. HandlerScenePayloadKind HandlerEventKind = "scene_payload" // HandlerSceneMessageKind identifies a scene message fallback handler. HandlerSceneMessageKind HandlerEventKind = "scene_message" ) // Event is the marker interface implemented by all observer runtime events. type Event interface { isEvent() } func emitContextError(ctx *MessageContext, event ErrorEvent) { if ctx == nil { return } if ctx.Logger != nil { ctx.Logger.Errorln(event.Err) } if ctx.eventEmitter != nil { ctx.eventEmitter(ctx.Context(), event) return } if ctx.observer == nil { return } defer func() { if recovered := recover(); recovered != nil && ctx.Logger != nil { ctx.Logger.Errorln(fmt.Sprintf("panic in observer: %v", recovered)) } }() ctx.observer.OnError(ctx.Context(), event) } // UpdateReceivedEvent describes an update entering the bot runtime. type UpdateReceivedEvent struct { // UpdateID identifies the Telegram update. UpdateID int // UpdateType identifies the normalized update kind. UpdateType tgapi.UpdateType // FromID identifies the originating user when available. FromID int64 // ChatID identifies the originating chat when available. ChatID int64 } // UpdateHandledEvent describes a completed update execution path. type UpdateHandledEvent struct { // UpdateID identifies the Telegram update. UpdateID int // UpdateType identifies the normalized update kind. UpdateType tgapi.UpdateType // FromID identifies the originating user when available. FromID int64 // ChatID identifies the originating chat when available. ChatID int64 // Duration is the total framework handling time. Duration time.Duration // Handled reports whether a registered path handled the update. Handled bool } // HandlerStartedEvent describes a handler about to execute. type HandlerStartedEvent struct { // UpdateID identifies the Telegram update. UpdateID int // UpdateType identifies the normalized update kind. UpdateType tgapi.UpdateType // Plugin names the plugin that owns the handler. Plugin string // HandlerKind classifies the handler. HandlerKind HandlerEventKind // HandlerName identifies the handler within its plugin and kind. HandlerName string // FromID identifies the originating user when available. FromID int64 // ChatID identifies the originating chat when available. ChatID int64 } // HandlerFinishedEvent describes a handler that has completed. type HandlerFinishedEvent struct { // UpdateID identifies the Telegram update. UpdateID int // UpdateType identifies the normalized update kind. UpdateType tgapi.UpdateType // Plugin names the plugin that owns the handler. Plugin string // HandlerKind classifies the handler. HandlerKind HandlerEventKind // HandlerName identifies the handler within its plugin and kind. HandlerName string // FromID identifies the originating user when available. FromID int64 // ChatID identifies the originating chat when available. ChatID int64 // Duration is the handler execution time. Duration time.Duration // Err is the error returned or recovered from the handler. Err error // UserFacing reports whether Err is safe to show to the user. UserFacing bool } // SceneTransitionEvent describes a scene state transition. type SceneTransitionEvent struct { // Plugin names the plugin that owns the scene. Plugin string // Scene names the transitioning scene. Scene string // From is the previous scene step. From string // To is the resulting scene step. To string // Action identifies the requested state transition. Action SceneAction // FromID identifies the session user when available. FromID int64 // ChatID identifies the session chat when available. ChatID int64 } // PolicyCheckedEvent describes the result of a policy evaluation. type PolicyCheckedEvent struct { // Name identifies the evaluated policy. Name string // Plugin names the plugin that requested the policy check. Plugin string // FromID identifies the evaluated user when available. FromID int64 // ChatID identifies the evaluated chat when available. ChatID int64 // Passed reports whether the policy accepted the context. Passed bool // Err is the policy evaluation error, if any. Err error // Internal reports whether evaluation failed internally rather than denying access. Internal bool } // RunnerFinishedEvent describes a completed background runner execution. type RunnerFinishedEvent struct { // Name identifies the runner. Name string // Duration is the callback execution time. Duration time.Duration // Err is the callback error or recovered panic. Err error } // PollingRetryEvent describes a polling retry after a failed getUpdates call. type PollingRetryEvent struct { // Attempt is the one-based retry number. Attempt int // Delay is the time before the next polling attempt. Delay time.Duration // Err is the polling error that triggered the retry. Err error } // ErrorEvent describes an error routed through framework error handling. type ErrorEvent struct { // UpdateID identifies the Telegram update when available. UpdateID int // UpdateType identifies the normalized update kind when available. UpdateType tgapi.UpdateType // Plugin names the component that reported the error. Plugin string // HandlerKind classifies the failing handler or runtime component. HandlerKind HandlerEventKind // HandlerName identifies the failing handler within its kind. HandlerName string // FromID identifies the originating user when available. FromID int64 // ChatID identifies the originating chat when available. ChatID int64 // Err is the reported error. Err error // UserFacing reports whether Err is safe to show to the user. UserFacing bool } func (UpdateReceivedEvent) isEvent() {} func (UpdateHandledEvent) isEvent() {} func (HandlerStartedEvent) isEvent() {} func (HandlerFinishedEvent) isEvent() {} func (SceneTransitionEvent) isEvent() {} func (PolicyCheckedEvent) isEvent() {} func (RunnerFinishedEvent) isEvent() {} func (PollingRetryEvent) isEvent() {} func (ErrorEvent) isEvent() {} // Observer receives best-effort runtime instrumentation events. // // During RunWithContext and RunWebhookWithContext, callbacks execute on a // dedicated dispatcher goroutine in enqueue order and never block update // handlers. The queue is bounded; overload drops events and emits sampled // warnings. Runtime shutdown cancels callback contexts and drains queued events; // Bot.Close returns ErrObserverShutdownTimeout if a callback ignores cancellation. type Observer interface { OnUpdateReceived(ctx context.Context, event UpdateReceivedEvent) OnUpdateHandled(ctx context.Context, event UpdateHandledEvent) OnHandlerStarted(ctx context.Context, event HandlerStartedEvent) OnHandlerFinished(ctx context.Context, event HandlerFinishedEvent) OnSceneTransition(ctx context.Context, event SceneTransitionEvent) OnPolicyChecked(ctx context.Context, event PolicyCheckedEvent) OnRunnerFinished(ctx context.Context, event RunnerFinishedEvent) OnPollingRetry(ctx context.Context, event PollingRetryEvent) OnError(ctx context.Context, event ErrorEvent) } func (bot *Bot[T]) safeEmitEvent(ctx context.Context, event Event) { if bot.observer == nil { return } if bot.observerAsync != nil { bot.observerAsync.enqueue(ctx, event) return } defer func() { if r := recover(); r != nil { if bot.logger != nil { bot.logger.Errorln(fmt.Sprintf("panic in observer: %v", r)) } } }() bot.emitEvent(ctx, event) } func (bot *Bot[T]) emitEvent(ctx context.Context, event Event) { emitObserverEvent(bot.observer, ctx, event) } func emitObserverEvent(observer Observer, ctx context.Context, event Event) { switch e := event.(type) { case UpdateReceivedEvent: observer.OnUpdateReceived(ctx, e) case UpdateHandledEvent: observer.OnUpdateHandled(ctx, e) case HandlerStartedEvent: observer.OnHandlerStarted(ctx, e) case HandlerFinishedEvent: observer.OnHandlerFinished(ctx, e) case SceneTransitionEvent: observer.OnSceneTransition(ctx, e) case PolicyCheckedEvent: observer.OnPolicyChecked(ctx, e) case RunnerFinishedEvent: observer.OnRunnerFinished(ctx, e) case PollingRetryEvent: observer.OnPollingRetry(ctx, e) case ErrorEvent: observer.OnError(ctx, e) } }