REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY
5
Scenes
ScuroNeko edited this page 2026-05-20 13:19:27 +03:00

Scenes

Scenes are Laniakea's stateful routing layer for multi-step and modal bot flows. A scene is registered inside a plugin, entered through MessageContext, stored through SessionStore, and routed before normal command handling while the session is active.

What scenes give you

  • Scene registration through Plugin.NewScene(...) and Plugin.AddScene(...).
  • Explicit entry and exit through MessageContext.EnterScene(...), EnterSceneStep(...), and ExitScene().
  • Per-user, per-chat, or per-user-chat session scopes.
  • Step handlers, scene-local commands, and a scene-level message fallback.
  • JSON-backed scene state through SceneContext.BindData(...) and SaveData(...).
  • A default in-memory store plus SessionStore for custom persistence.

Core API

type SceneScope int

const (
	SceneScopeUser SceneScope = iota
	SceneScopeChat
	SceneScopeUserChat
)

type SceneSession struct {
	Scene string
	Step  string
	Data  []byte
}

type SessionStore interface {
	Get(key string) (SceneSession, error)
	Set(key string, session SceneSession) error
	Delete(key string) error
}

MemorySessionStore is the default implementation. Use Bot.SetSessionStore(...) to replace it.

Scopes

  • SceneScopeUser shares one scene session across all chats for a user.
  • SceneScopeChat shares one scene session across all users in a chat.
  • SceneScopeUserChat isolates one session per (user, chat) pair.

Recommended default:

  • Use SceneScopeUserChat for most interactive flows.
  • Use SceneScopeUser only when the same logical flow should continue across chats.
  • Use SceneScopeChat for room-level shared workflows.

Registration

Scenes are registered inside plugins in the same style as commands and payloads.

plugin.NewScene("signup").
	SetScope(laniakea.SceneScopeUserChat).
	SetEntry("ask_name").
	OnStep("ask_name", askName).
	OnStep("confirm", confirmSignup).
	OnCommand("cancel", cancelSignup).
	OnMessage(fallbackMessage)

SetEntry(...) is required for ctx.EnterScene(...). ctx.EnterSceneStep(...) can start from a specific registered step instead.

Handler model

Normal commands use *MessageContext. Scene handlers use *SceneContext.

type SceneHandler[T any] func(ctx *SceneContext, db T) (SceneResult, error)

SceneContext embeds *MessageContext and adds scene helpers:

  • ctx.Stay()
  • ctx.Next(step)
  • ctx.Exit()
  • ctx.Pass()
  • ctx.BindData(&dst)
  • ctx.SaveData(src)

Scene handlers return SceneResult to control session flow:

  • Stay: keep the same scene and step.
  • Next(step): move to another registered step.
  • Exit: delete the current session.
  • Pass: do not change the current session and continue normal routing.

SceneActionPass is intentionally a no-op for scene state. If a handler calls SaveData(...) and then returns Pass, that state is not persisted.

Routing order

While a scene session is active, routing works like this:

  1. Bot middleware runs first.
  2. The bot resolves the active scene session from the configured scope priority.
  3. Plugin middleware for the owning plugin runs.
  4. Scene-local commands are checked first.
  5. If no scene command matches, the current step handler runs.
  6. If no step handler matches, the scene-level OnMessage(...) fallback runs.
  7. If the scene returns Pass, normal plugin command and update routing continues.

The scene text comes from message text or caption. Scenes are therefore designed around message-based flows.

State between steps

Use SceneSession.Data only through SceneContext.BindData(...) and SaveData(...) unless you are implementing custom store behavior.

type ProfileDraft struct {
	Name string
	Age  int
}

func askName(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) {
	var draft ProfileDraft
	if err := ctx.BindData(&draft); err != nil {
		return laniakea.SceneResult{}, err
	}

	draft.Name = ctx.Text
	if err := ctx.SaveData(draft); err != nil {
		return laniakea.SceneResult{}, err
	}

	return ctx.Next("age"), nil
}

The store contract stays intentionally small because Data []byte is storage-agnostic.

Example flow

Command entry:

func startSignup(ctx *laniakea.MessageContext, db *App) error {
	return ctx.EnterScene("signup")
}

Step handler:

func askName(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) {
	if ctx.Text == "" {
		ctx.Answer("What is your name?")
		return ctx.Stay(), nil
	}

	ctx.Answer("Thanks.")
	return ctx.Next("confirm"), nil
}

Scene-local command:

func cancelSignup(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) {
	ctx.Answer("Signup cancelled")
	return ctx.Exit(), nil
}

Deliberate limits of the current model

  • Scene-local payload routing is not implemented.
  • The internal scene runtime is not exposed as public inspection API.
  • The helper surface for scene state is intentionally small.

Related pages: