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
- Variables
- func RegisterBotRoutes(router *swagger.RouteGroup, handler *BotAPIHandler)
- func RegisterRoutes(engine *gin.Engine, handler *Handler)
- type BotAPIHandler
- type BotChatManager
- type BotChatOKResponse
- type BotChatSetDisabledRequest
- type BotChatSummary
- type BotChatsResponse
- type BotInteractOption
- type BotInteractRequest
- type BotNotifyRequest
- type ChatSummary
- type DeliveryAccess
- 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 ¶
var ErrChatNotFound = errors.New("chat not found")
ErrChatNotFound is returned by BotChatManager.Delete/SetDisabled when the chat is not in the bot's reachable set (unknown, wrong platform, or paired to a different bot). Mapped to HTTP 404.
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], chats BotChatManager, deliveryAccess ...DeliveryAccess) *BotAPIHandler
NewBotAPIHandler builds the handler. channels and results are the same registries the Claude Code scenario path uses. chats may be nil — the chat lifecycle endpoints and the disabled-check then report unavailable.
func (*BotAPIHandler) DeleteChat ¶ added in v0.260806.1
func (h *BotAPIHandler) DeleteChat(c *gin.Context)
DeleteChat handles DELETE /api/v1/bots/:bot/chats/:chat_id.
Hard-deletes the chat record: pairing, whitelist, and project binding are gone. If the chat messages the bot again, the normal auto-create path rebuilds it as a brand-new chat (re-pair required when pairing is enforced). Session history is untouched.
200 deleted 404 chat not in this bot's reachable set 503 chat management unavailable (no deleter wired)
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) SetChatDisabled ¶ added in v0.260806.1
func (h *BotAPIHandler) SetChatDisabled(c *gin.Context)
SetChatDisabled handles PUT /api/v1/bots/:bot/chats/:chat_id/disabled.
Toggles the chat's inbound blocklist flag. A disabled chat's messages are dropped before any handler runs (including /bind — it cannot re-enable itself) and it disappears from the reachable list, notify, and interact.
200 updated 400 malformed body 404 chat not in this bot's reachable set 503 chat management unavailable (no disabler wired)
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 BotChatManager ¶ added in v0.260806.1
type BotChatManager interface {
// ListChats returns the chats a bot can reach, scoped to that bot's
// platform; includeDisabled adds blocklisted chats.
ListChats(botUUID string, includeDisabled bool) ([]ChatSummary, error)
// DeleteChat hard-deletes a chat record reachable by the bot.
DeleteChat(botUUID, chatID string) error
// SetChatDisabled toggles a reachable chat's inbound blocklist flag.
SetChatDisabled(botUUID, chatID string, disabled bool) error
// IsChatDisabled reports whether a chat is blocklisted. Used by Notify and
// Interact so disable cuts both directions — a disabled chat neither
// reaches the bot nor is reachable from it. Unknown chats report false so
// pushes to fresh chat ids keep working.
IsChatDisabled(chatID string) bool
}
BotChatManager is the chat-lifecycle capability the bot interaction API needs: list / delete / toggle-disabled / is-disabled. Defined here as one interface so notify does not import remote_control/bot — the server wires the concrete implementation in server_control_chats.go. One seam instead of one func type per operation means adding a capability is one interface method, not a new field + constructor arg + wiring closure + nil check. Method names mirror the underlying ChatStoreInterface (ListChats/DeleteChat/ SetChatDisabled/IsChatDisabled) so a reader carries one vocabulary across both layers.
type BotChatOKResponse ¶ added in v0.260806.1
type BotChatOKResponse struct {
OK bool `json:"ok" example:"true"`
}
BotChatOKResponse is the swagger model for the chat mutation endpoints.
type BotChatSetDisabledRequest ¶ added in v0.260806.1
type BotChatSetDisabledRequest struct {
Disabled *bool `json:"disabled" example:"true"`
}
BotChatSetDisabledRequest is the swagger model for PUT /bots/:bot/chats/:chat_id/disabled.
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"`
Disabled bool `json:"disabled,omitempty" example:"false"`
DisabledAt string `json:"disabled_at,omitempty" example:"2026-07-28T12:00:00Z"`
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 {
Target access.TargetRef `json:"target"`
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 {
Target access.TargetRef `json:"target"`
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 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"`
Disabled bool `json:"disabled,omitempty"`
DisabledAt string `json:"disabled_at,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 DeliveryAccess ¶ added in v0.260806.1
type DeliveryAccess interface {
access.FactSource
GetDirectChat(context.Context, string, string) (access.DirectChat, bool, error)
GetGroup(context.Context, string, string) (access.Group, bool, error)
}
DeliveryAccess resolves stable, bot-scoped internal targets. Production delivery always authorizes the internal target before its platform-native identifier is revealed to the channel adapter.
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