notify

package
v0.260806.1 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: MPL-2.0 Imports: 16 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

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

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], 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

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