Documentation
¶
Overview ¶
Package notify — bot interaction API.
bot_api.go is the *general, caller-facing* bot interaction surface: any authenticated integration can drive a running bot's channel to deliver a one-way notification (Notify) or start an interactive prompt (Interact) and long-poll for the reply (Wait). It is distinct from handler.go, which is the Claude-Code-hook-specific shim under /tingly/:scenario/{notify,wait} whose value is *plugin classification* (hook_event_name → push vs interactive).
This handler is bot-scoped (the bot UUID is in the path) and bypasses the scenario plugin entirely: the caller has already decided whether the request is one-way or interactive by choosing the endpoint. Auth is inherited from the control-plane route group (getUserAuthMiddleware) it is registered on; see server_control.go and .design/bot-interaction-api.md.
The two interface kinds map directly onto the existing channel.Channel contract: Notify → Channel.Send (fire-and-forget); Interact → Channel.Prompt (blocking), with the reply delivered through the shared interaction.Registry the Wait handler reads from. No new domain types or runtime — this file is a thin HTTP adapter over remote/channel + remote/interaction.
Package notify is the HTTP front end for scenario plugin events. It is intentionally thin: it parses the request, dispatches to the registered scenario plugin (via internal/remote/scenario), and maps the plugin's Outcome into HTTP responses (200 push / 202 + wait URL / 404 unknown).
All business logic — what an event "means", how to render a prompt, how to encode a decision — lives in the plugin (see internal/remote/scenario/builtin/claudecode for the first one).
When no scenario plugin is registered for the URL parameter (or the plugin chose not to handle the event) the handler falls back to a desktop notification through pkg/notify so stock setups without IM bindings still surface hook activity.
Index ¶
- Constants
- func RegisterBotRoutes(router *swagger.RouteGroup, handler *BotAPIHandler)
- func RegisterRoutes(engine *gin.Engine, handler *Handler)
- type BotAPIHandler
- type BotChatSummary
- type BotChatsResponse
- type BotInteractOption
- type BotInteractRequest
- type BotNotifyRequest
- type ChatLister
- type ChatSummary
- type Handler
Constants ¶
const DefaultInteractTimeout = 5 * time.Minute
DefaultInteractTimeout bounds a single interactive prompt when the caller omits timeout_seconds. It must stay below interaction.Registry's entry TTL (30s today) plus a safety margin is NOT required — the registry TTL is a fallback eviction, not the prompt budget; the prompt's own context deadline governs how long Channel.Prompt blocks.
const MaxInteractTimeout = 30 * time.Minute
MaxInteractTimeout caps an interactive prompt's budget so a caller cannot pin a registry entry and an IM prompt open indefinitely.
Variables ¶
This section is empty.
Functions ¶
func RegisterBotRoutes ¶ added in v0.260801.1
func RegisterBotRoutes(router *swagger.RouteGroup, handler *BotAPIHandler)
RegisterBotRoutes registers the general bot interaction API on a control- plane route group (the existing apiV1 group, which already applies getUserAuthMiddleware). Routes:
POST /bots/:bot/notify one-way push POST /bots/:bot/interact start interactive GET /bots/:bot/interact/:id long-poll for the reply GET /bots/:bot/chats discover the chat_id the other three need
The group's base path determines the full URL; registered under apiV1 this yields /api/v1/bots/:bot/... — see .design/bot-interaction-api.md.
func RegisterRoutes ¶
RegisterRoutes registers notification hook routes
Types ¶
type BotAPIHandler ¶ added in v0.260801.1
type BotAPIHandler struct {
// contains filtered or unexported fields
}
BotAPIHandler is the HTTP front end for the general bot interaction API. It resolves a bot's channel from the registry and drives it directly.
func NewBotAPIHandler ¶ added in v0.260801.1
func NewBotAPIHandler(channels *channel.Registry, results *interaction.Registry[interaction.Result], chatLister ChatLister) *BotAPIHandler
NewBotAPIHandler builds the handler. channels and results are the same registries the Claude Code scenario path uses. chatLister may be nil — the /chats endpoint then reports unavailable.
func (*BotAPIHandler) Interact ¶ added in v0.260801.1
func (h *BotAPIHandler) Interact(c *gin.Context)
Interact handles POST /api/v1/bots/:bot/interact.
202 interactive flow started; client polls wait_url 400 malformed body or invalid kind 404 bot not running 503 interaction registry unavailable (no bot middle layer wired)
func (*BotAPIHandler) ListChats ¶ added in v0.260801.1
func (h *BotAPIHandler) ListChats(c *gin.Context)
ListChats handles GET /api/v1/bots/:bot/chats.
Returns the chats a bot can reach, so a caller of /notify and /interact can discover the channel-native chat_id those endpoints require. Without this, the chat_id the request body demands is undiscoverable (it is not in /help, not on the bot table, and not otherwise exposed).
A bot that isn't running simply has no reachable chats — that's an empty state, not an error — so this endpoint never returns 404. The response carries a `running` flag so the UI can tailor the empty message ("start the bot" vs "send it a message"). See ux-principles #11.
200 chat list (possibly empty) with running flag 503 chat listing unavailable (no store wired)
func (*BotAPIHandler) Notify ¶ added in v0.260801.1
func (h *BotAPIHandler) Notify(c *gin.Context)
Notify handles POST /api/v1/bots/:bot/notify.
200 delivered (one-way push, no reply expected) 400 malformed body 404 bot not running (unknown or stopped — same body shape) 500 delivery failed
func (*BotAPIHandler) Wait ¶ added in v0.260801.1
func (h *BotAPIHandler) Wait(c *gin.Context)
Wait handles GET /api/v1/bots/:bot/interact/:request_id?timeout=45s.
Status mapping is identical to the Claude Code /wait endpoint (handler.go): 200 answered/cancelled, 410 timeout, 504 pending, 404 expired. The :bot param is accepted for path symmetry but not re-resolved — the interaction id already encodes its owning bot implicitly via the channel it was started against, and the registry is shared.
type BotChatSummary ¶ added in v0.260801.1
type BotChatSummary struct {
ChatID string `json:"chat_id" example:"telegram:123456789"`
Platform string `json:"platform,omitempty" example:"telegram"`
IsPaired bool `json:"is_paired,omitempty" example:"true"`
IsWhitelisted bool `json:"is_whitelisted,omitempty" example:"false"`
ProjectPath string `json:"project_path,omitempty" example:"/home/user/proj"`
UpdatedAt string `json:"updated_at,omitempty" example:"2026-07-25T12:00:00Z"`
}
BotChatSummary is the swagger model for one entry in GET /bots/:bot/chats.
type BotChatsResponse ¶ added in v0.260801.1
type BotChatsResponse struct {
Chats []BotChatSummary `json:"chats"`
Running bool `json:"running" example:"true"`
}
BotChatsResponse is the swagger model for the GET /bots/:bot/chats body. Running is false when the bot's channel isn't registered — the list is then empty by definition, and the caller can say "start the bot" rather than "no chats yet".
type BotInteractOption ¶ added in v0.260801.1
type BotInteractOption struct {
Value string `json:"value" example:"yes"`
Label string `json:"label" example:"Yes"`
Style string `json:"style,omitempty" example:"primary"`
}
BotInteractOption mirrors interaction.Option for the swagger surface.
type BotInteractRequest ¶ added in v0.260801.1
type BotInteractRequest struct {
ChatID string `json:"chat_id" example:"dm:ops"`
Kind string `json:"kind" example:"confirm"`
Title string `json:"title" example:"Deploy to prod?"`
Body string `json:"body,omitempty" example:"commit a1b2c3"`
Options []BotInteractOption `json:"options,omitempty"`
TimeoutSeconds int `json:"timeout_seconds,omitempty" example:"120"`
}
BotInteractRequest is the swagger model for POST /bots/:bot/interact.
type BotNotifyRequest ¶ added in v0.260801.1
type BotNotifyRequest struct {
ChatID string `json:"chat_id" example:"dm:ops"`
Title string `json:"title,omitempty" example:"Build #412 failed"`
Body string `json:"body" example:"main branch is red"`
Level string `json:"level,omitempty" example:"info"`
}
BotNotifyRequest is the swagger model for POST /bots/:bot/notify.
type ChatLister ¶ added in v0.260801.1
type ChatLister func(botUUID string) ([]ChatSummary, error)
ChatLister returns the chats a bot can reach, scoped to that bot's platform (and to its chat-id lock when one is set). Defined here so the notify package does not import remote_control/bot — the server wires the concrete implementation. Returns (nil, nil) when no lister is configured.
type ChatSummary ¶ added in v0.260801.1
type ChatSummary struct {
ChatID string `json:"chat_id"`
Platform string `json:"platform,omitempty"`
IsPaired bool `json:"is_paired,omitempty"`
IsWhitelisted bool `json:"is_whitelisted,omitempty"`
ProjectPath string `json:"project_path,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
}
ChatSummary is the projection of a bot's chat record exposed by GET /bots/:bot/chats. ChatID is the channel-native conversation identifier the caller must pass as chat_id to /notify and /interact — surfacing it here is what makes those endpoints usable (see ux-principles #5/#11).
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler routes /tingly/:scenario/{notify,wait/:id} to the appropriate scenario plugin and the shared interaction registry.
func NewHandler ¶
func NewHandler() *Handler
NewHandler creates a handler with no scenario routing — every event falls back to a desktop notification. Used by stock setups before a scenario registry is wired in.
func NewHandlerWithRouting ¶ added in v0.260507.1
func NewHandlerWithRouting(scenarios *scenario.Registry, results *interaction.Registry[interaction.Result], runtime scenario.Runtime) *Handler
NewHandlerWithRouting wires the handler to a scenario registry, the shared interaction.Registry (used by Wait), and a runtime that exposes channels + bindings to plugins.
func (*Handler) Notify ¶
Notify handles POST /tingly/:scenario/notify.
200: scenario plugin handled the event as a push (no reply expected). 202: scenario plugin started an interactive flow; client polls wait_url. 200 + desktop fallback: no plugin (or plugin declined to handle).
func (*Handler) Wait ¶ added in v0.260507.1
Wait handles GET /tingly/:scenario/wait/:request_id?timeout=45s. Maps interaction.Result.Status to HTTP shape:
200 answered — final decision available 200 cancelled — user cancelled 410 timeout — fallback decision (policy on_timeout) 504 pending — long-poll timed out without an answer; client retries 404 expired — id is unknown / evicted