signal

package
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 25 Imported by: 0

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

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

func Chunk(s string, limit int, mode string) []string

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.

func SafeName

func SafeName(s string) string

SafeName replaces delimiter characters in a sender-controlled name so it can't forge or escape the inbound notification meta.

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 NewClient

func NewClient(baseURL, account string) *Client

NewClient builds a Client for the daemon at baseURL.

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

func (c *Client) GetAttachment(ctx context.Context, chatID, id string) ([]byte, error)

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.

func (*Client) SendReaction

func (c *Client) SendReaction(ctx context.Context, chatID, emoji, targetAuthor string, targetTimestamp int64) error

SendReaction reacts to the message sent by targetAuthor at targetTimestamp.

func (*Client) SendTyping

func (c *Client) SendTyping(ctx context.Context, chatID string) error

SendTyping triggers a typing indicator for the chat (shown ~15s or until the next message).

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

func NewHandler(c *Client, cfg *config.Config, log *transcript.Logger, opts *optionStore) *Handler

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) FlushAll

func (h *Handler) FlushAll(ctx context.Context)

FlushAll drains every pending buffer immediately (shutdown path).

func (*Handler) HandleEnvelope

func (h *Handler) HandleEnvelope(ctx context.Context, e *envelope)

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

func (h *Handler) HandleEvent(ctx context.Context, ev sseEvent)

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

func NewProvider(name string, cfg *config.Config, log *transcript.Logger) (*Provider, error)

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

func (p *Provider) DownloadAttachment(ctx context.Context, in mcpchan.DownloadInput) (string, bool)

DownloadAttachment implements mcpchan.ToolSet.

func (*Provider) EditMessage

func (p *Provider) EditMessage(ctx context.Context, in mcpchan.EditInput) (string, bool)

EditMessage implements mcpchan.ToolSet.

func (*Provider) Name

func (p *Provider) Name() string

Name implements provider.Provider.

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) React

func (p *Provider) React(ctx context.Context, in mcpchan.ReactInput) (string, bool)

React implements mcpchan.ToolSet.

func (*Provider) Reply

func (p *Provider) Reply(ctx context.Context, in mcpchan.ReplyInput) (string, bool)

Reply implements mcpchan.ToolSet.

func (*Provider) Start

func (p *Provider) Start(ctx context.Context, sink provider.InboundSink) error

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

func (p *Provider) TranscriptFile() string

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 NewTools

func NewTools(c *Client, cfg *config.Config, log *transcript.Logger, opts *optionStore) *Tools

NewTools builds the tool set sharing the Handler's option store.

func (*Tools) DownloadAttachment

func (t *Tools) DownloadAttachment(ctx context.Context, in mcpchan.DownloadInput) (string, bool)

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

func (t *Tools) EditMessage(ctx context.Context, in mcpchan.EditInput) (string, bool)

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

func (t *Tools) React(ctx context.Context, in mcpchan.ReactInput) (string, bool)

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

func (t *Tools) Reply(ctx context.Context, in mcpchan.ReplyInput) (string, bool)

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.

Jump to

Keyboard shortcuts

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