notify

package
v0.260801.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: MPL-2.0 Imports: 15 Imported by: 0

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

View Source
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.

View Source
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

func RegisterRoutes(engine *gin.Engine, handler *Handler)

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

func (h *Handler) Notify(c *gin.Context)

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

func (h *Handler) Wait(c *gin.Context)

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL