Documentation
¶
Overview ¶
Package signal adapts the Signal transport to the provider.Provider interface. Unlike telegram/discord it holds no credentials: it talks to a locally running `signal-cli --account +E164 daemon --http HOST:PORT` (a linked secondary device) over two plain HTTP surfaces:
- POST /api/v1/rpc — JSON-RPC requests (send, sendReaction, sendTyping, getAttachment, …)
- GET /api/v1/events — an SSE stream of incoming envelopes
Both endpoints are documented in signal-cli's man/signal-cli-jsonrpc.5.adoc.
Index ¶
- Constants
- func Chunk(s string, limit int, mode string) []string
- func SafeName(s string) string
- type Client
- func (c *Client) Call(ctx context.Context, method string, params map[string]any) (json.RawMessage, error)
- func (c *Client) GetAttachment(ctx context.Context, chatID, id string) ([]byte, error)
- func (c *Client) Send(ctx context.Context, chatID, message string, attachments []string, ...) (int64, error)
- func (c *Client) SendReaction(ctx context.Context, chatID, emoji, targetAuthor string, targetTimestamp int64) error
- func (c *Client) SendTyping(ctx context.Context, chatID string) error
- type Extracted
- type Handler
- func (h *Handler) BindNotifier(sink provider.InboundSink)
- func (h *Handler) FlushAll(ctx context.Context)
- func (h *Handler) HandleEnvelope(ctx context.Context, e *envelope)
- func (h *Handler) HandleEvent(ctx context.Context, ev sseEvent)
- func (h *Handler) Notifier() provider.InboundSink
- func (h *Handler) OnPermissionRequest(ctx context.Context, p mcpchan.PermissionRequestParams)
- type Provider
- func (p *Provider) Capabilities() provider.Capabilities
- func (p *Provider) DownloadAttachment(ctx context.Context, in mcpchan.DownloadInput) (string, bool)
- func (p *Provider) EditMessage(ctx context.Context, in mcpchan.EditInput) (string, bool)
- func (p *Provider) Name() string
- func (p *Provider) OnPermissionRequest(ctx context.Context, params mcpchan.PermissionRequestParams)
- func (p *Provider) React(ctx context.Context, in mcpchan.ReactInput) (string, bool)
- func (p *Provider) Reply(ctx context.Context, in mcpchan.ReplyInput) (string, bool)
- func (p *Provider) Start(ctx context.Context, sink provider.InboundSink) error
- func (p *Provider) TranscriptFile() string
- type Tools
- func (t *Tools) DownloadAttachment(ctx context.Context, in mcpchan.DownloadInput) (string, bool)
- func (t *Tools) EditMessage(ctx context.Context, in mcpchan.EditInput) (string, bool)
- func (t *Tools) React(ctx context.Context, in mcpchan.ReactInput) (string, bool)
- func (t *Tools) Reply(ctx context.Context, in mcpchan.ReplyInput) (string, bool)
Constants ¶
const MaxChunkLimit = 2000
MaxChunkLimit is the per-message character cap we enforce. Signal clients render roughly 2000 characters inline and turn longer bodies into "long text" attachments, so we chunk at 2000 like the discord adapter.
Variables ¶
This section is empty.
Functions ¶
func Chunk ¶
Chunk splits text into pieces no longer than limit runes, clamped to 1..MaxChunkLimit. In "newline" mode the split prefers the last paragraph break, then the last line break, then the last space within the window, falling back to a hard cut. Operates on runes so multibyte text isn't split mid-character. Mirrors the telegram/discord splitters.
Types ¶
type Client ¶
type Client struct {
// BaseURL is the daemon root, e.g. "http://127.0.0.1:8080" (no trailing
// slash).
BaseURL string
// Account is the linked account's E.164 number. Used to recognize our own
// envelopes and as the default reaction target author.
Account string
// HTTP is the underlying client; a 60s-timeout client when nil.
HTTP *http.Client
// contains filtered or unexported fields
}
Client is a minimal JSON-RPC-over-HTTP client for the signal-cli daemon. The daemon is expected to run in single-account mode (`-a ACCOUNT daemon --http`), so requests carry no account param.
func (*Client) Call ¶
func (c *Client) Call(ctx context.Context, method string, params map[string]any) (json.RawMessage, error)
Call posts one JSON-RPC request to /api/v1/rpc and returns the raw result.
func (*Client) GetAttachment ¶
GetAttachment fetches an attachment's raw bytes via the daemon's getAttachment command (returned base64-encoded per the signal-cli docs).
func (*Client) Send ¶
func (c *Client) Send(ctx context.Context, chatID, message string, attachments []string, editTimestamp int64) (int64, error)
Send sends a message (with optional local-path attachments); when editTimestamp is non-zero it edits the previous message with that timestamp instead (signal-cli's send --edit-timestamp). Returns the new message's timestamp — Signal's message identity.
type Extracted ¶
type Extracted struct {
Kind string // photo|voice|audio|video|document
Name string // SafeName'd, may be empty
IsPhoto bool
Synthetic string // fallback content when the message has no text
}
Extracted describes a media attachment found on an inbound message.
type Handler ¶
type Handler struct {
Client *Client
Cfg *config.Config
Log *transcript.Logger
Options *optionStore
// contains filtered or unexported fields
}
Handler processes the daemon's receive events: it gates on the sender (allowlist by E.164, same pairing-code flow as the other adapters), relays inbound messages to Claude, answers pending permission requests from text replies, and maps bare-number answers back to the last reply's numbered options. It mirrors the telegram/discord Handler structure: same access model, same coalescing, same permission claim semantics.
func NewHandler ¶
NewHandler builds a Handler sharing the Tools' option store.
func (*Handler) BindNotifier ¶
func (h *Handler) BindNotifier(sink provider.InboundSink)
BindNotifier sets the inbound sink. Safe to call while events are flowing.
func (*Handler) HandleEnvelope ¶
HandleEnvelope normalizes and relays one envelope. Receipts, typing notifications, and sync messages carry no dataMessage.message — they are ignored (except reactions, relayed as kind=reaction events, and attachments-only messages, which relay a synthetic marker).
func (*Handler) HandleEvent ¶
HandleEvent processes one SSE event from the daemon's event stream.
func (*Handler) Notifier ¶
func (h *Handler) Notifier() provider.InboundSink
Notifier returns the currently bound inbound sink (nil before binding).
func (*Handler) OnPermissionRequest ¶
func (h *Handler) OnPermissionRequest(ctx context.Context, p mcpchan.PermissionRequestParams)
OnPermissionRequest fans a permission request out to allowlisted numbers as a plain-text prompt. Signal has no buttons, so the answer path is the text reply the other adapters also accept: "yes <code>" / "no <code>".
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider adapts the Signal transport (signal-cli HTTP daemon: SSE inbound, JSON-RPC outbound) to the provider.Provider interface — the same thin lifecycle wrapper shape as the telegram and discord providers.
func NewProvider ¶
NewProvider builds the Signal provider. name is its source tag ("signal", or "signal:<instance>"). With no SIGNAL_ACCOUNT in cfg the provider still serves the outbound tools (which report "no signal account configured") and Start blocks idle — the same unconfigured handshake mode the other providers have.
func (*Provider) Capabilities ¶
func (p *Provider) Capabilities() provider.Capabilities
Capabilities implements provider.Provider.
- Buttons: false — Signal has no inline buttons; the adapter degrades them to numbered text options and maps a bare-number answer back.
- Reactions: true — sendReaction.
- Edits: true — signal-cli's send --edit-timestamp (JSON-RPC editTimestamp) edits a previously sent message.
- TypingPause: true — sendTyping paces bubble bursts.
- PermissionRelay: the daemon authenticates senders (Signal's E2E identity), and the "yes <code>" / "no <code>" text path answers prompts without buttons. Needs a configured account.
func (*Provider) DownloadAttachment ¶
DownloadAttachment implements mcpchan.ToolSet.
func (*Provider) EditMessage ¶
EditMessage implements mcpchan.ToolSet.
func (*Provider) OnPermissionRequest ¶
func (p *Provider) OnPermissionRequest(ctx context.Context, params mcpchan.PermissionRequestParams)
OnPermissionRequest implements provider.Provider: fan the prompt out to allowlisted numbers as text. No-op without a configured account.
func (*Provider) Start ¶
Start implements provider.Provider: claim the single-consumer slot for this state dir, bind the sink, and run the daemon's SSE event stream (with reconnect/backoff) until ctx is cancelled. Without an account it blocks idle so the MCP handshake stays up.
func (*Provider) TranscriptFile ¶
TranscriptFile is the durable conversation log path for this provider's state dir.
type Tools ¶
type Tools struct {
Client *Client // nil when SIGNAL_ACCOUNT is not configured (handshake-only)
Cfg *config.Config
Log *transcript.Logger
Options *optionStore
}
Tools implements mcpchan.ToolSet against the signal-cli daemon's JSON-RPC endpoint.
func (*Tools) DownloadAttachment ¶
DownloadAttachment fetches an attachment through the daemon's getAttachment command into the inbox and returns its local path. The file_id is the "<id>|<chat>" pair from inbound meta.
func (*Tools) EditMessage ¶
EditMessage edits a message we previously sent, via signal-cli's send --edit-timestamp (JSON-RPC param editTimestamp). Returns the edit's own timestamp as the new message_id — Signal chains subsequent edits off the newest revision.
func (*Tools) React ¶
React adds an emoji reaction to a message. Inbound message_ids carry "<timestamp>:<author>"; a bare timestamp addresses one of our own messages.
func (*Tools) Reply ¶
Reply sends bubbles as sequential sends paced with sendTyping (mirroring the other adapters) or a single text (auto-split at the 2000-char cap), then each file as its own send. Signal has no inline buttons, so buttons degrade to numbered text options appended to the last bubble; the numbered labels are remembered so a bare-number answer maps back to its label.