channels

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 47 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// OutboundKindMessage is a normal message (the default zero value).
	OutboundKindMessage = ""
	// OutboundKindStep is a compact progress step title from an in-flight
	// agentic session. Channels with rich formatting render it de-emphasized
	// (Slack: a context block in the reply thread) so the user sees the
	// session progressing without the full transcript that the pane
	// terminal shows.
	OutboundKindStep = "step"
)

OutboundKind values classify an outbound message for channel-specific rendering.

Variables

View Source
var PluginLookup func(channelType string, config msg.ChannelConfig) (ChannelAdapter, bool)

PluginLookup, when non-nil, resolves a non-built-in channel type to an out-of-process plugin adapter (internal/channels/plugin, design 002). It is a nil-safe hook set by plugin.Wire from the CLI layer so this package never imports the plugin package (no import cycle). Built-ins always win: the factory consults this only after its built-in switch, so a plugin named "slack" can never shadow the built-in adapter.

View Source
var PluginTypeKnown func(channelType string) bool

PluginTypeKnown, when non-nil, reports whether an installed channel plugin provides the given type (companion hook to PluginLookup; same wiring).

View Source
var ValidChannelTypes = []string{"whatsapp", "slack", "email", "phone", "chatbot", "discord", "telegram", "signal", "imessage", "teams"}

ValidChannelTypes lists the BUILT-IN channel type strings (installed plugin channels are additionally accepted via the PluginTypeKnown hook).

Functions

func IsValidChannelType

func IsValidChannelType(t string) bool

IsValidChannelType checks if a channel type string is supported: a built-in type, or an installed plugin channel via the nil-safe hook.

func QRHalfBlocks

func QRHalfBlocks(payload string) (string, error)

QRHalfBlocks renders payload as a scannable QR using Unicode half-block runes — two vertical modules per text row, so modules come out roughly square in a terminal cell. It is tuned for a DARK background: light modules (including the quiet zone) are drawn as block "ink" and dark modules as spaces, which on a dark terminal reads as a normal dark-on-light code that phone cameras scan. On a light-background terminal the code appears inverted; QRPNGDataURI and the raw payload are the theme-independent fallbacks.

func QRPNGDataURI

func QRPNGDataURI(payload string) (string, error)

QRPNGDataURI renders payload as a black-on-white PNG QR and returns it as a "data:image/png;base64,…" URI an <img> can show directly. This is the theme-independent, guaranteed-scannable form carried to the web dashboard.

func ShouldForwardStepToSlack

func ShouldForwardStepToSlack(step *sharedmsg.MsgAgenticStep) bool

ShouldForwardStepToSlack reports whether a step event earns a progress line in the Slack flow.

func SlackStepLine

func SlackStepLine(step *sharedmsg.MsgAgenticStep) string

SlackStepLine renders one agentic step event as a single compact mrkdwn line for the Slack progress flow: an emoji, optional nesting marker for sub-agent depth, and the step TITLE only (never the step detail/payload).

func ToSlackMrkdwn

func ToSlackMrkdwn(text string) string

ToSlackMrkdwn converts the session's markdown-ish LLM output to Slack mrkdwn so the final answer reads natively in Slack:

**bold**        → *bold*
# Heading       → *Heading*
- bullet        → • bullet
[text](url)     → <url|text>
```lang fences  → ``` (Slack fences carry no language tag)

Inline code, plain *italic*, and everything inside code fences are left as-is (Slack renders backtick spans natively).

Types

type Attachment

type Attachment struct {
	Filename    string
	ContentType string
	Data        []byte
}

Attachment holds a file attachment for an email draft.

type ChannelAdapter

type ChannelAdapter interface {
	// Type returns the channel type string (e.g., "whatsapp", "slack", "email", "phone", "chatbot").
	Type() string

	// Start connects to the external service and begins listening for inbound messages.
	// The provided context is cancelled when the adapter should stop.
	Start(ctx context.Context) error

	// Stop gracefully disconnects from the external service.
	Stop() error

	// Send delivers a message back to the external channel.
	Send(ctx context.Context, outbound OutboundMessage) error

	// InboundCh returns a channel that emits inbound messages from the external platform.
	InboundCh() <-chan InboundMessage

	// Status returns the current connection status.
	Status() msg.ChannelStatus

	// SetReplyMode changes the reply mode at runtime.
	// Mode is "messages" (respond to all channel messages) or "mentions"
	// (respond only when the bot is @mentioned).
	SetReplyMode(mode string)
}

ChannelAdapter is the interface all external communication channel adapters must implement. Each adapter bridges one external platform (WhatsApp, Slack, Email, Phone, or Chatbot) with the humanoid actor system.

Lifecycle: Start() is called when the humanoid is spawned with a configured channel. Stop() is called on humanoid shutdown or explicit channel stop. Inbound messages arrive via the InboundCh() channel and are forwarded to the humanoid's LLMPromptExecutionActor as prompts. Outbound responses are delivered via Send().

func NewAdapter

func NewAdapter(channelType string, config msg.ChannelConfig) (ChannelAdapter, error)

NewAdapter creates a ChannelAdapter for the given channel type and configuration. Returns an error if the channel type is not supported.

type ChatbotAdapter

type ChatbotAdapter struct {
	// contains filtered or unexported fields
}

ChatbotAdapter implements the ChannelAdapter interface for connecting to server-side chatbot panes via the rysh-server REST API. It monitors active chatbot sessions and forwards visitor messages to the humanoid's inbound channel, enabling observation and human takeover.

X2 fix. Two defects were fixed together here, because either alone left the channel silently dead:

  1. Every URL was wrong. The adapter called /api/chatbot/operator/sessions and friends, which do not exist on rysh-server — the operator routes are nested under /api/workspaces/:wsID/chatbots/:id/sessions/... So every request 404'd, refreshSessions swallowed the error at Debug level, and Start still reported Connected:true.
  2. Nothing ever pushed to c.inbound. The channel was allocated and returned by InboundCh, but no code path ever sent on it, so a visitor message could never reach the humanoid even had the URLs been right.

Inbound now polls GetMessages per tracked session using its after_seq cursor, so each visitor message is delivered exactly once.

func NewChatbotAdapter

func NewChatbotAdapter(config msg.ChannelConfig) *ChatbotAdapter

NewChatbotAdapter creates a new Chatbot adapter for remote server mode.

func (*ChatbotAdapter) InboundCh

func (c *ChatbotAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the channel for receiving inbound chatbot messages.

func (*ChatbotAdapter) Send

func (c *ChatbotAdapter) Send(_ context.Context, outbound OutboundMessage) error

Send delivers a human operator message to a chatbot session on the server. This triggers takeover if not already taken over.

func (*ChatbotAdapter) SetReplyMode

func (c *ChatbotAdapter) SetReplyMode(_ string)

SetReplyMode is a no-op for chatbot.

func (*ChatbotAdapter) Start

func (c *ChatbotAdapter) Start(ctx context.Context) error

Start connects to the remote server and begins monitoring chatbot sessions.

func (*ChatbotAdapter) Status

func (c *ChatbotAdapter) Status() msg.ChannelStatus

Status returns the current adapter status.

func (*ChatbotAdapter) Stop

func (c *ChatbotAdapter) Stop() error

Stop gracefully disconnects from the server.

func (*ChatbotAdapter) Type

func (c *ChatbotAdapter) Type() string

Type returns "chatbot".

type CredStore

type CredStore interface {
	LoadDeviceLink(humanoid, channel string) (json.RawMessage, bool)
	SaveDeviceLink(humanoid, channel string, blob json.RawMessage) error
}

CredStore is the two-method view of PairingStore handed to QR adapters at construction so they can persist/resume their own device-link session without the actor (or the skill file) ever holding the credentials.

type DiscordAdapter

type DiscordAdapter struct {
	// contains filtered or unexported fields
}

DiscordAdapter bridges Discord (bot Gateway) to the humanoid actor system.

func NewDiscordAdapter

func NewDiscordAdapter(config msg.ChannelConfig) *DiscordAdapter

NewDiscordAdapter creates a Discord channel adapter from the config.

func (*DiscordAdapter) InboundCh

func (d *DiscordAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the inbound message channel.

func (*DiscordAdapter) Send

func (d *DiscordAdapter) Send(_ context.Context, outbound OutboundMessage) error

Send delivers an outbound message to a Discord channel. The recipient is the channel (or thread) ID; when RecipientID is empty it falls back to ThreadID, which HumanoidActor sets from the inbound message.

func (*DiscordAdapter) SetReplyMode

func (d *DiscordAdapter) SetReplyMode(mode string)

SetReplyMode changes the reply mode ("messages" or "mentions") at runtime.

func (*DiscordAdapter) Start

func (d *DiscordAdapter) Start(ctx context.Context) error

Start connects to the Discord Gateway and begins listening for messages.

Unlike Slack's socketmode (where RunContext exits on disconnect and we run our own reconnect loop), discordgo owns its read/heartbeat goroutines and reconnects with backoff internally (ShouldReconnectOnError defaults to true). We therefore do not duplicate the Slack connectionLoop; instead we register Connect/Disconnect/Resumed handlers so Status() still reflects explicit connection-state transitions across those internal reconnects.

func (*DiscordAdapter) Status

func (d *DiscordAdapter) Status() msg.ChannelStatus

Status reports the connection status.

func (*DiscordAdapter) Stop

func (d *DiscordAdapter) Stop() error

Stop gracefully disconnects from Discord. Safe to call before Start or more than once.

func (*DiscordAdapter) Type

func (d *DiscordAdapter) Type() string

Type returns "discord".

type Draft

type Draft struct {
	ID          string
	To          string
	Subject     string
	Body        string
	InReplyTo   string
	Attachments []Attachment
	CreatedAt   time.Time

	// ApprovedAt is set when the OWNER confirms the draft (typing "send" in
	// the humanoid's pane). Zero means pending.
	//
	// This is the enforcement state behind human-governed channels. Without it
	// the "draft-and-confirm" guarantee is only prompt text — the model is
	// told not to send unbidden, and nothing stops it if it does. A send tool
	// under human governance must refuse a draft whose ApprovedAt is zero.
	ApprovedAt time.Time
}

Draft represents an unsent draft.

func (*Draft) Approved

func (d *Draft) Approved() bool

Approved reports whether the owner has confirmed this draft.

type DraftStore

type DraftStore struct {
	// contains filtered or unexported fields
}

DraftStore provides thread-safe in-memory draft storage.

func NewDraftStore

func NewDraftStore() *DraftStore

NewDraftStore creates a new empty DraftStore.

func (*DraftStore) AddAttachment

func (ds *DraftStore) AddAttachment(id string, att Attachment) error

AddAttachment appends an attachment to an existing draft.

func (*DraftStore) Approve

func (ds *DraftStore) Approve(id string) bool

Approve marks a draft as owner-confirmed. Returns false if the ID is unknown.

func (*DraftStore) ApproveLatest

func (ds *DraftStore) ApproveLatest() (string, bool)

ApproveLatest confirms the most recently created pending draft and returns its ID.

This backs the bare "send" confirmation, where the owner names no ID: the draft they are looking at is the one just created. Only PENDING drafts are considered, so repeating "send" cannot silently re-approve an older draft that was already sent and deleted, nor resurrect one the owner ignored.

func (*DraftStore) Create

func (ds *DraftStore) Create(to, subject, body, inReplyTo string) string

Create adds a new draft and returns its ID.

func (*DraftStore) Delete

func (ds *DraftStore) Delete(id string)

Delete removes a draft by ID.

func (*DraftStore) Get

func (ds *DraftStore) Get(id string) (*Draft, bool)

Get returns a draft by ID.

func (*DraftStore) List

func (ds *DraftStore) List() []*Draft

List returns all current drafts.

func (*DraftStore) Update

func (ds *DraftStore) Update(id, body string) error

Update replaces the body of an existing draft.

type EmailAdapter

type EmailAdapter struct {
	// contains filtered or unexported fields
}

EmailAdapter implements the ChannelAdapter interface for Email using IMAP/SMTP. It monitors an inbox via IMAP IDLE for real-time message arrival and replies via SMTP.

func NewEmailAdapter

func NewEmailAdapter(config msg.ChannelConfig) *EmailAdapter

NewEmailAdapter creates a new Email adapter with the given configuration.

func (*EmailAdapter) InboundCh

func (e *EmailAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the channel for receiving inbound emails.

func (*EmailAdapter) ListEmails

func (e *EmailAdapter) ListEmails(count int, search string) ([]msg.EmailSummary, error)

ListEmails opens a short-lived IMAP session and returns summaries of recent emails.

func (*EmailAdapter) ReadEmail

func (e *EmailAdapter) ReadEmail(uid int) (*msg.EmailDetail, error)

ReadEmail opens a short-lived IMAP session and fetches a single email by UID.

func (*EmailAdapter) ResolveShortID

func (e *EmailAdapter) ResolveShortID(id string) (int, bool)

ResolveShortID maps a short handle ("a3f9") back to an email UID. Matching is case-insensitive. Returns false if the handle is unknown (e.g. listed in a different session).

func (*EmailAdapter) Send

func (e *EmailAdapter) Send(_ context.Context, outbound OutboundMessage) error

Send delivers a reply email via SMTP.

func (*EmailAdapter) SendEmail

func (e *EmailAdapter) SendEmail(to, subject, body, inReplyTo string, attachments []Attachment) error

SendEmail sends an email with optional attachments. This is the full-featured send method used by the email_send tool (as opposed to Send() which is the simple ChannelAdapter interface method for auto-replies).

func (*EmailAdapter) SetReplyMode

func (e *EmailAdapter) SetReplyMode(_ string)

SetReplyMode is a no-op for email (all inbound emails are processed).

func (*EmailAdapter) Start

func (e *EmailAdapter) Start(ctx context.Context) error

Start connects to the IMAP server and begins monitoring the inbox via IDLE.

func (*EmailAdapter) Status

func (e *EmailAdapter) Status() msg.ChannelStatus

Status returns the current Email connection status.

func (*EmailAdapter) Stop

func (e *EmailAdapter) Stop() error

Stop gracefully disconnects from the IMAP server.

func (*EmailAdapter) Type

func (e *EmailAdapter) Type() string

Type returns "email".

type IMessageAdapter

type IMessageAdapter struct {
	// contains filtered or unexported fields
}

IMessageAdapter bridges iMessage (macOS Messages.app) to the humanoid actor system: AppleScript for outbound, a read-only chat.db poll for inbound.

func NewIMessageAdapter

func NewIMessageAdapter(config msg.ChannelConfig) *IMessageAdapter

NewIMessageAdapter creates an iMessage channel adapter from the config.

func (*IMessageAdapter) InboundCh

func (i *IMessageAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the inbound message channel.

func (*IMessageAdapter) Send

func (i *IMessageAdapter) Send(ctx context.Context, outbound OutboundMessage) error

Send delivers an outbound message via AppleScript (osascript). Sends are serialized and paced (~1 msg/s). The message body and recipient are passed as `on run argv` arguments — never interpolated into the AppleScript source — so user content cannot inject script.

func (*IMessageAdapter) SetReplyMode

func (i *IMessageAdapter) SetReplyMode(mode string)

SetReplyMode stores the mode but is effectively a no-op: iMessage is a DM-style channel and all allowed inbound is processed (design §4.5).

func (*IMessageAdapter) Start

func (i *IMessageAdapter) Start(ctx context.Context) error

Start verifies the macOS host bridge (darwin, chat.db readable, sqlite3 on PATH), snapshots the current MAX(message.ROWID) so only NEW messages flow, and launches the poll goroutine.

func (*IMessageAdapter) Status

func (i *IMessageAdapter) Status() msg.ChannelStatus

Status reports the connection status. Connected flips true after the first successful poll; poll errors surface in Error.

func (*IMessageAdapter) Stop

func (i *IMessageAdapter) Stop() error

Stop cancels the poll loop.

func (*IMessageAdapter) Type

func (i *IMessageAdapter) Type() string

Type returns "imessage".

type InboundMessage

type InboundMessage struct {
	SenderID   string            `json:"sender_id"`
	SenderName string            `json:"sender_name"`
	Content    string            `json:"content"`
	ThreadID   string            `json:"thread_id"`
	Metadata   map[string]string `json:"metadata,omitempty"`
}

InboundMessage represents a message received from an external channel. The json tags are the wire encoding of the channel-plugin protocol (internal/channels/plugin, design 002 §4.1).

type LinkableChannel

type LinkableChannel interface {
	TriggerLink(force bool) error
}

LinkableChannel is implemented by PairingChannel adapters whose link flow can also be started on demand (`##humanoid pair link`, X4 design 009 §3.4) rather than only at Start. TriggerLink returns immediately: the flow runs on its own goroutine and reports through PairingCh. Unless force is set, implementations must refuse when the daemon already holds a linked account (the re-link guard), reporting the refusal as an "error" pairing event.

type OutboundMessage

type OutboundMessage struct {
	RecipientID string `json:"recipient_id"`
	Content     string `json:"content"`
	ThreadID    string `json:"thread_id"`
	// Kind selects channel-specific rendering: OutboundKindMessage (default)
	// or OutboundKindStep (compact progress title).
	Kind string `json:"kind,omitempty"`
}

OutboundMessage represents a message to be sent to an external channel. The json tags are the wire encoding of the channel-plugin protocol (internal/channels/plugin, design 002 §4.1).

type PairingChannel

type PairingChannel interface {
	// PairingCh emits link-lifecycle events while Start() establishes the link.
	PairingCh() <-chan PairingEvent
}

PairingChannel is implemented only by adapters that establish a session via an out-of-band device link (WhatsApp non-Cloud, Signal). Credential- configured adapters (Slack, WhatsApp Cloud, email) do NOT implement it.

type PairingEvent

type PairingEvent struct {
	Kind    string          // "qr" | "linked" | "error"
	QR      string          // raw QR payload to render (Kind=="qr")
	Session json.RawMessage // device-link creds to persist (Kind=="linked"; secret — never log)
	Detail  string          // human message (Kind=="error")
}

PairingEvent is one device-link lifecycle event from a PairingChannel.

type PairingOption

type PairingOption func(*PairingStore)

PairingOption customises a PairingStore at construction.

func WithMaxPending

func WithMaxPending(n int) PairingOption

WithMaxPending overrides the pending-request cap (default 3).

func WithNow

func WithNow(now func() time.Time) PairingOption

WithNow overrides the store's time source. Test seam: TTL expiry tests advance a fake clock instead of racing real sleeps against wall-clock TTLs (which flaked under -race on loaded CI runners).

func WithPendingTTL

func WithPendingTTL(d time.Duration) PairingOption

WithPendingTTL overrides the pending-request lifetime (default 1h).

type PairingRecord

type PairingRecord struct {
	Allowlist  []string              `json:"allowlist"`   // approved SenderIDs
	Pending    map[string]PendingReq `json:"pending"`     // code -> request
	DeviceLink json.RawMessage       `json:"device_link"` // opaque adapter session creds (secret — never log)
	UpdatedAt  time.Time             `json:"updated_at"`
}

PairingRecord is one (humanoid, channel) pairing state cell.

type PairingStore

type PairingStore struct {
	// contains filtered or unexported fields
}

PairingStore persists allowlists, pending pairing requests, and device-link creds. It lives in the channels package (not the actor) because it is shared between the actor's Receive goroutine and adapter/pairingLoop goroutines, so — per the project convention that only adapters may hold locks — it guards its read-modify-write cycles with a mutex here rather than in actor state.

func NewPairingStore

func NewPairingStore(nc *nats.Conn, opts ...PairingOption) *PairingStore

NewPairingStore opens (or creates) the durable "rysh-pairings" KV bucket on nc's JetStream context, mirroring UsageActor.openKV. A nil connection or an unavailable JetStream leaves kv nil and every operation degrades to the file-backed channel-state path — the store never fails to construct.

func (*PairingStore) AddPending

func (s *PairingStore) AddPending(humanoid, channel string, im InboundMessage) (PendingReq, error)

AddPending registers a pairing request for an unknown sender and returns the request (with its approval code). Expired entries are swept first; a sender that already has a live pending request gets the SAME request back (so repeated messages do not mint new codes or eat pending slots). When the post-sweep count is at maxPending, the request is refused with an error the caller can surface as a rate-note.

func (*PairingStore) Allow

func (s *PairingStore) Allow(humanoid, channel, sender string) error

Allow adds sender directly to the allowlist, skipping the code flow. Additive and idempotent — used both by the ##humanoid allow command and by the spawn-time merge of a channel's declared allowlist.

func (*PairingStore) Allowed

func (s *PairingStore) Allowed(humanoid, channel, sender string) bool

Allowed reports whether sender is on the (humanoid, channel) allowlist. An empty sender, a missing record, or unavailable storage all yield false — the admission gate is fail-closed (design 003 G5).

func (*PairingStore) Approve

func (s *PairingStore) Approve(humanoid, channel, code string) (PendingReq, bool, error)

Approve consumes a pending code: the sender is promoted to the allowlist and the code is removed (single-use). An unknown or expired code is a no-op returning ok=false with no error, so the caller can print a notice.

func (*PairingStore) List

func (s *PairingStore) List(humanoid, channel string) (PairingRecord, error)

List returns the current record for approver display, sweeping expired pendings first. The DeviceLink blob is blanked in the returned copy: it is a secret and List feeds rendering paths (pane / dashboard) that must never see it — use LoadDeviceLink for the credential itself.

func (s *PairingStore) LoadDeviceLink(humanoid, channel string) (json.RawMessage, bool)

LoadDeviceLink returns the persisted device-link blob, if any.

func (s *PairingStore) SaveDeviceLink(humanoid, channel string, blob json.RawMessage) error

SaveDeviceLink persists an adapter's opaque device-link session blob. The blob is a secret: it is stored (0600 file / FileStorage KV under .rysh/) and never logged or rendered.

type PendingReq

type PendingReq struct {
	Code       string    `json:"code"` // base36, from newPairCode()
	SenderID   string    `json:"sender_id"`
	SenderName string    `json:"sender_name"`
	Channel    string    `json:"channel"`
	FirstMsg   string    `json:"first_msg"` // truncated, for the approver's context
	CreatedAt  time.Time `json:"created_at"`
	ExpiresAt  time.Time `json:"expires_at"` // CreatedAt + pending_ttl (default 1h)
}

PendingReq is one not-yet-approved contact request, keyed by its code.

type PhoneAdapter

type PhoneAdapter struct {
	// contains filtered or unexported fields
}

PhoneAdapter implements ChannelAdapter for Twilio SMS.

func NewPhoneAdapter

func NewPhoneAdapter(config msg.ChannelConfig) *PhoneAdapter

NewPhoneAdapter creates a Twilio SMS adapter from the config.

func (*PhoneAdapter) InboundCh

func (p *PhoneAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the channel for receiving inbound SMS messages.

func (*PhoneAdapter) RecentMessages

func (p *PhoneAdapter) RecentMessages(count int) []PhoneMessage

RecentMessages returns up to count of the most recent received messages (oldest first, newest last). count<=0 returns all retained messages.

func (*PhoneAdapter) Send

func (p *PhoneAdapter) Send(ctx context.Context, outbound OutboundMessage) error

Send delivers a message as one or more SMS. Long content is split under the REST API's 1600-character limit; each chunk is a separate message, which is how a long reply arrives on a phone regardless.

func (*PhoneAdapter) SetReplyMode

func (p *PhoneAdapter) SetReplyMode(_ string)

SetReplyMode is a no-op for SMS: a text message to the business number is always addressed to it, so there is no "mentions" distinction to draw.

func (*PhoneAdapter) Start

func (p *PhoneAdapter) Start(ctx context.Context) error

Start validates credentials and launches the inbound webhook server. The listener binds synchronously so a port conflict surfaces as a Start error rather than a silent goroutine failure.

func (*PhoneAdapter) Status

func (p *PhoneAdapter) Status() msg.ChannelStatus

Status returns the current Twilio connection status.

func (*PhoneAdapter) Stop

func (p *PhoneAdapter) Stop() error

Stop gracefully shuts down the webhook listener.

func (*PhoneAdapter) Type

func (p *PhoneAdapter) Type() string

Type returns "phone".

func (*PhoneAdapter) Validate

func (p *PhoneAdapter) Validate(ctx context.Context) error

Validate checks the Twilio credentials with a single account fetch — no webhook listener is bound — so `rysh doctor` can report a bad SID/token without side effects.

type PhoneMessage

type PhoneMessage struct {
	ID         string    `json:"id"`
	MessageSid string    `json:"message_sid"`
	From       string    `json:"from"` // sender number, E.164
	To         string    `json:"to"`   // the Twilio number that received it
	Text       string    `json:"text"`
	NumMedia   int       `json:"num_media,omitempty"`
	Time       time.Time `json:"time"`
}

PhoneMessage is a received inbound SMS retained for the human-governed draft/approve flow. ID is a short handle; MessageSid is Twilio's SMxxx id.

type SignalAdapter

type SignalAdapter struct {
	// contains filtered or unexported fields
}

SignalAdapter bridges Signal (via a signal-cli daemon) to the humanoid actor system.

func NewSignalAdapter

func NewSignalAdapter(config msg.ChannelConfig) *SignalAdapter

NewSignalAdapter creates a Signal channel adapter from the config.

func (*SignalAdapter) InboundCh

func (s *SignalAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the inbound message channel.

func (*SignalAdapter) PairingCh

func (s *SignalAdapter) PairingCh() <-chan PairingEvent

PairingCh implements PairingChannel: it emits device-link lifecycle events (qr / linked / error) while the signal-cli link flow runs (X4, design 009).

func (*SignalAdapter) Send

func (s *SignalAdapter) Send(ctx context.Context, outbound OutboundMessage) error

Send delivers an outbound message via a JSON-RPC "send" to the daemon.

func (*SignalAdapter) SetReplyMode

func (s *SignalAdapter) SetReplyMode(mode string)

SetReplyMode changes the reply mode at runtime. 1:1 messages are always processed; in "mentions" mode group messages that do not @-mention the linked number are forwarded observe_only (mirroring Slack).

func (*SignalAdapter) Start

func (s *SignalAdapter) Start(ctx context.Context) error

Start dials the signal-cli JSON-RPC endpoint, spawning the daemon first when SidecarCmd is configured, and launches the reader/reconnect loop.

func (*SignalAdapter) Status

func (s *SignalAdapter) Status() msg.ChannelStatus

Status reports the connection status.

func (*SignalAdapter) Stop

func (s *SignalAdapter) Stop() error

Stop closes the sidecar connection and kills the daemon if rysh spawned it.

func (s *SignalAdapter) TriggerLink(force bool) error

TriggerLink implements LinkableChannel: it starts the device-link flow on demand (`##humanoid pair link`, design 009 §3.4). It returns immediately — progress and the re-link-guard refusal both arrive as pairing events. The only synchronous errors are "not connected" and "already linking".

func (*SignalAdapter) Type

func (s *SignalAdapter) Type() string

Type returns "signal".

type SlackAdapter

type SlackAdapter struct {
	// contains filtered or unexported fields
}

SlackAdapter implements the ChannelAdapter interface for Slack using Socket Mode. It listens for messages on configured channels and replies in-thread.

func NewSlackAdapter

func NewSlackAdapter(config msg.ChannelConfig) *SlackAdapter

NewSlackAdapter creates a new Slack adapter with the given configuration.

func (*SlackAdapter) GetMessage

func (s *SlackAdapter) GetMessage(id string) (SlackMessage, bool)

GetMessage looks up a retained message by its short ID ("a3f9") or its ts. Matching is case-insensitive so an ID typed into a prompt resolves regardless of case.

func (*SlackAdapter) InboundCh

func (s *SlackAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the channel for receiving inbound Slack messages.

func (*SlackAdapter) RecentMessages

func (s *SlackAdapter) RecentMessages(count int) []SlackMessage

RecentMessages returns up to count of the most recent received messages (oldest first, newest last). count<=0 returns all retained messages.

func (*SlackAdapter) Send

func (s *SlackAdapter) Send(_ context.Context, outbound OutboundMessage) error

Send delivers a message back to Slack, replying in-thread if a ThreadID is set.

func (*SlackAdapter) SetReplyMode

func (s *SlackAdapter) SetReplyMode(mode string)

SetReplyMode changes the reply mode at runtime. "messages" responds to all channel messages, "mentions" only to @mentions.

func (*SlackAdapter) Start

func (s *SlackAdapter) Start(ctx context.Context) error

Start connects to Slack via Socket Mode and begins listening for messages.

func (*SlackAdapter) Status

func (s *SlackAdapter) Status() msg.ChannelStatus

Status returns the current Slack connection status.

func (*SlackAdapter) Stop

func (s *SlackAdapter) Stop() error

Stop gracefully disconnects from Slack.

func (*SlackAdapter) Type

func (s *SlackAdapter) Type() string

Type returns "slack".

func (*SlackAdapter) Validate

func (s *SlackAdapter) Validate(ctx context.Context) error

Validate checks the Slack credentials with a single auth.test call — the same call Start() makes first — without opening the Socket Mode connection. It verifies the bot token remotely; the app-level token is only checked for presence/shape (Slack offers no cheap non-binding check for app tokens — they are exercised when the socket connects).

type SlackMessage

type SlackMessage struct {
	ID       string    `json:"id"`        // short 4-char handle for tools
	From     string    `json:"from"`      // sender user ID
	Name     string    `json:"name"`      // sender display name
	Channel  string    `json:"channel"`   // channel ID the message arrived in
	Text     string    `json:"text"`      //
	ThreadTS string    `json:"thread_ts"` // thread root ts (reply target)
	TS       string    `json:"ts"`        // this message's ts
	Time     time.Time `json:"time"`
}

SlackMessage is a received Slack message retained for the human-governed list/read tools. Channel + ThreadTS are what a reply needs to post back into the right conversation.

type TeamsAdapter

type TeamsAdapter struct {
	// contains filtered or unexported fields
}

TeamsAdapter implements ChannelAdapter for Microsoft Teams.

func NewTeamsAdapter

func NewTeamsAdapter(config msg.ChannelConfig) *TeamsAdapter

NewTeamsAdapter creates a Teams channel adapter from the config.

func (*TeamsAdapter) InboundCh

func (t *TeamsAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the inbound message channel.

func (*TeamsAdapter) Send

func (t *TeamsAdapter) Send(ctx context.Context, outbound OutboundMessage) error

Send delivers a message to a Teams conversation. The conversation is taken from ThreadID when set — the inbound path puts the conversation id there — falling back to RecipientID.

func (*TeamsAdapter) SetReplyMode

func (t *TeamsAdapter) SetReplyMode(mode string)

SetReplyMode changes the reply mode ("messages" or "mentions") at runtime.

func (*TeamsAdapter) Start

func (t *TeamsAdapter) Start(ctx context.Context) error

Start verifies the bot credentials by acquiring a Connector token — the Teams analogue of Slack's auth.test — then binds the Messaging Endpoint listener. Both happen synchronously so a bad secret or a port conflict surfaces as a Start error rather than a silent goroutine failure.

func (*TeamsAdapter) Status

func (t *TeamsAdapter) Status() msg.ChannelStatus

Status reports the connection status.

func (*TeamsAdapter) Stop

func (t *TeamsAdapter) Stop() error

Stop shuts down the messaging endpoint.

func (*TeamsAdapter) Type

func (t *TeamsAdapter) Type() string

Type returns "teams".

func (*TeamsAdapter) Validate

func (t *TeamsAdapter) Validate(ctx context.Context) error

Validate checks the bot credentials by acquiring a Connector token — no listener is bound — so `rysh doctor` can report a bad app id or secret without side effects.

type TelegramAdapter

type TelegramAdapter struct {
	// contains filtered or unexported fields
}

TelegramAdapter bridges Telegram (Bot API) to the humanoid actor system.

func NewTelegramAdapter

func NewTelegramAdapter(config msg.ChannelConfig) *TelegramAdapter

NewTelegramAdapter creates a Telegram channel adapter from the config.

func (*TelegramAdapter) InboundCh

func (t *TelegramAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the inbound message channel.

func (*TelegramAdapter) Send

func (t *TelegramAdapter) Send(ctx context.Context, outbound OutboundMessage) error

Send delivers an outbound message via sendMessage. The chat (and optional forum topic) is taken from ThreadID when set — it carries the full routing key the inbound path composed — falling back to RecipientID. Long content is split across messages; Kind=="step" renders italic via HTML parse mode.

func (*TelegramAdapter) SetReplyMode

func (t *TelegramAdapter) SetReplyMode(mode string)

SetReplyMode changes the reply mode ("messages" or "mentions") at runtime.

func (*TelegramAdapter) Start

func (t *TelegramAdapter) Start(ctx context.Context) error

Start verifies the bot token via getMe (the Telegram analogue of Slack's auth.test — it also learns the bot's ID and username, needed for mention and reply-to-bot detection), then launches the configured inbound transport.

func (*TelegramAdapter) Status

func (t *TelegramAdapter) Status() msg.ChannelStatus

Status reports the connection status.

func (*TelegramAdapter) Stop

func (t *TelegramAdapter) Stop() error

Stop cancels the poll loop / shuts down the webhook server (the ctx-done goroutine in startWebhook also calls deleteWebhook so Telegram stops delivering to a dead endpoint).

func (*TelegramAdapter) Type

func (t *TelegramAdapter) Type() string

Type returns "telegram".

type Validator

type Validator interface {
	Validate(ctx context.Context) error
}

Validator is implemented by adapters that can check their credentials WITHOUT establishing a full connection or binding any port/webhook (e.g. Slack auth.test, a WhatsApp Graph token GET). `rysh doctor` prefers this non-binding check; adapters that don't implement it are reported as "not validated" rather than probed via Start(), which has side effects (socket-mode connections, webhook listeners). See design 004 §4.3.

type WhatsAppAdapter

type WhatsAppAdapter struct {
	// contains filtered or unexported fields
}

WhatsAppAdapter implements the ChannelAdapter interface for WhatsApp using the Meta Cloud API (WhatsApp Business Platform).

Outbound: a direct POST to the Graph API

https://graph.facebook.com/{version}/{phone_number_id}/messages

with a Bearer access token — no server round-trip required.

Inbound: Meta delivers messages via webhook to a public HTTPS endpoint, so the adapter runs a small local HTTP server (on WebhookPort). The user exposes that port publicly (a reverse proxy / tunnel) and registers the URL as the app's Callback URL in the Meta dashboard. The server answers the GET verification handshake and validates the X-Hub-Signature-256 HMAC on inbound POSTs.

func NewWhatsAppAdapter

func NewWhatsAppAdapter(config msg.ChannelConfig) *WhatsAppAdapter

NewWhatsAppAdapter creates a new WhatsApp adapter with the given configuration.

func (*WhatsAppAdapter) GetMessage

func (w *WhatsAppAdapter) GetMessage(id string) (WhatsAppMessage, bool)

GetMessage looks up a retained message by its short ID ("a3f9") or its wamid. Matching is case-insensitive so an ID typed into a prompt resolves regardless of case.

func (*WhatsAppAdapter) InboundCh

func (w *WhatsAppAdapter) InboundCh() <-chan InboundMessage

InboundCh returns the channel for receiving inbound WhatsApp messages.

func (*WhatsAppAdapter) RecentMessages

func (w *WhatsAppAdapter) RecentMessages(count int) []WhatsAppMessage

RecentMessages returns up to count of the most recent received messages (oldest first, newest last). count<=0 returns all retained messages.

func (*WhatsAppAdapter) RelayMode

func (w *WhatsAppAdapter) RelayMode() bool

RelayMode reports whether the adapter routes through rysh-server's channel relay rather than its own webhook + Graph API.

func (*WhatsAppAdapter) Send

func (w *WhatsAppAdapter) Send(ctx context.Context, outbound OutboundMessage) error

Send delivers a message via the WhatsApp Cloud API. Inside the 24h session window it sends free-form text (split across messages when over the Cloud API limit). Outside the window free-form text is undeliverable, so it falls back to the configured re-engagement template — or fails loudly when none is configured, rather than attempting a send Meta would reject.

func (*WhatsAppAdapter) SendTemplate

func (w *WhatsAppAdapter) SendTemplate(ctx context.Context, to, name, lang string, bodyParams []string) error

SendTemplate is the exported entry point used by the human-governed whatsapp_send_template tool: an explicit, approved template send. The template must already be approved in Meta Business Manager and the recipient must have opted in — the Cloud API enforces both; we surface its errors.

func (*WhatsAppAdapter) SetConfig

func (w *WhatsAppAdapter) SetConfig(cfg msg.ChannelConfig)

SetConfig replaces the adapter's configuration before Start.

The humanoid pre-creates this adapter at construction time (so the whatsapp_* tools and the running channel share one message store) but only resolves server-side credentials later, in startChannel. Without this the resolved config was computed and thrown away, and relay mode started with no upstream url, key or connection id.

func (*WhatsAppAdapter) SetReplyMode

func (w *WhatsAppAdapter) SetReplyMode(_ string)

SetReplyMode is a no-op for WhatsApp (all inbound direct messages are processed).

func (*WhatsAppAdapter) Start

func (w *WhatsAppAdapter) Start(ctx context.Context) error

Start validates credentials and launches the inbound webhook server.

func (*WhatsAppAdapter) Status

func (w *WhatsAppAdapter) Status() msg.ChannelStatus

Status returns the current WhatsApp connection status.

func (*WhatsAppAdapter) Stop

func (w *WhatsAppAdapter) Stop() error

Stop gracefully shuts down the webhook listener.

func (*WhatsAppAdapter) Type

func (w *WhatsAppAdapter) Type() string

Type returns "whatsapp".

func (*WhatsAppAdapter) Validate

func (w *WhatsAppAdapter) Validate(ctx context.Context) error

Validate checks the WhatsApp Cloud credentials with a lightweight Graph GET on the configured phone_number_id — no webhook listener is bound. A 401/403 is decoded as a rejected/expired token; other non-2xx statuses surface the Graph error message.

type WhatsAppMessage

type WhatsAppMessage struct {
	ID        string    `json:"id"`
	MessageID string    `json:"message_id"`
	From      string    `json:"from"` // sender wa_id (phone number)
	Name      string    `json:"name"`
	Text      string    `json:"text"`
	Time      time.Time `json:"time"`
}

WhatsAppMessage is a received inbound message retained for the human-governed list/read tools. ID is a short, human-friendly handle ("wa-1"); MessageID is the underlying WhatsApp wamid.

Directories

Path Synopsis
Package plugin implements the out-of-process channel plugin SDK (openclaw_roadmap design 002, WS2 P1-P3): a third party ships a channel as a separate process speaking a small wire contract, and the in-core PluginChannelAdapter shim proxies the ChannelAdapter interface to it over stdio JSON-RPC (fallback) or the embedded per-session NATS bus (preferred).
Package plugin implements the out-of-process channel plugin SDK (openclaw_roadmap design 002, WS2 P1-P3): a third party ships a channel as a separate process speaking a small wire contract, and the in-core PluginChannelAdapter shim proxies the ChannelAdapter interface to it over stdio JSON-RPC (fallback) or the embedded per-session NATS bus (preferred).

Jump to

Keyboard shortcuts

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