Documentation
¶
Overview ¶
Deprecated: This package is the pre-gateway Slack bot implementation. It does NOT implement the gateway.Channel interface (gateway/types.go). The replacement is internal/gateway/slack/ — a full gateway bot that integrates with the dispatcher, hub, and cron notification system.
This package is kept for reference only and will be removed in a future release. Nothing in the codebase imports it (verified 2025-05-28).
Package slack implements a minimal Slack bot daemon for overkill.
We deliberately stay on stdlib only — Slack's API is plain JSON over HTTP and a single Socket-Mode WebSocket. Pulling in slack-go/slack would add thousands of lines of API surface we never call.
Index ¶
- func EscapeMrkdwn(s string) string
- func FormatToolCall(name, args string) string
- func MarkdownToMrkdwn(s string) string
- type AgentSender
- type Bot
- type Event
- type EventsAPIPayload
- type HTTPSlackAPI
- func (c *HTTPSlackAPI) AddReaction(ctx context.Context, channel, ts, name string) error
- func (c *HTTPSlackAPI) PostMessage(ctx context.Context, channel, threadTS, text string) (string, error)
- func (c *HTTPSlackAPI) RemoveReaction(ctx context.Context, channel, ts, name string) error
- func (c *HTTPSlackAPI) UpdateMessage(ctx context.Context, channel, ts, text string) error
- type SessionMap
- type SlackAPI
- type SocketClient
- type SocketEnvelope
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func EscapeMrkdwn ¶
EscapeMrkdwn escapes the three characters Slack treats as control characters in user-supplied text: &, <, >. Use this when injecting untrusted content (tool args, error messages) that should NOT be parsed as Slack formatting.
func FormatToolCall ¶
FormatToolCall renders a tool invocation as a collapsed Slack block. Long argument payloads are truncated so we don't blow Slack's 40k message limit on a single noisy tool.
func MarkdownToMrkdwn ¶
MarkdownToMrkdwn converts a small subset of CommonMark into Slack's "mrkdwn" dialect. Slack uses single asterisks for bold (*bold*), single underscores for italics (_italic_), and triple-backtick fences for code. Inline code (`code`), links, headings, and lists pass through largely unchanged because Slack already understands them.
The conversion is intentionally simple — we walk the string once and transform the few constructs whose syntax actually differs. We do NOT try to be a full CommonMark parser; for the agent's output (which is mostly plain text + the occasional bold/code) this is sufficient and avoids the brittleness of regex-driven markdown rewrites.
Types ¶
type AgentSender ¶
type AgentSender interface {
Stream(ctx context.Context, in string) (<-chan agent.StreamEvent, error)
SetSessionID(id string)
SessionID() string
}
AgentSender is the agent-side interface the bot needs. Mirrors the shape used by internal/web — kept small so tests can drop in a fake.
type Bot ¶
type Bot struct {
API SlackAPI
Agent AgentSender
Sessions *SessionMap
AppToken string
APIBaseURL string // override for tests; default https://slack.com/api
Allowed map[string]bool
DryRun bool
Logger *log.Logger
UpdateEvery time.Duration // batch chat.update interval; default 500ms
// SocketFactory builds the Socket Mode client — overridable so tests can
// supply a stub that doesn't dial Slack. Returning nil opts the bot into
// a "drain in-memory channel" path where Run pulls envelopes from
// EnvelopeSource until the source closes.
SocketFactory func() *SocketClient
EnvelopeSource <-chan *SocketEnvelope
AckSink func(envelopeID string) error // used in tests instead of WS write
}
Bot wires a Socket-Mode connection, a Web API client, an overkill agent, and a per-thread session map together. One Bot per Slack workspace.
func New ¶
func New(api SlackAPI, ag AgentSender, sessions *SessionMap, appToken string, allowed []string) *Bot
New returns a Bot with sensible defaults filled in.
type Event ¶
type Event struct {
Type string `json:"type"` // "app_mention" | "message" | "assistant_thread_started"
Subtype string `json:"subtype"` // "bot_message" etc — we ignore non-empty subtypes
User string `json:"user"`
BotID string `json:"bot_id"` // present on bot-authored messages — skip those
Text string `json:"text"`
TS string `json:"ts"`
ThreadTS string `json:"thread_ts"` // empty for top-level posts
Channel string `json:"channel"`
ChannelType string `json:"channel_type"` // "im" for DMs, "channel"/"group" otherwise
}
Event covers app_mention and message events. Slack uses a single shape with a discriminator field; we type the union and let downstream code switch on Type/ChannelType.
func (Event) IsFromBot ¶
IsFromBot reports whether the event was authored by a bot (including us). Used to drop our own posts so we don't loop on @mentions of ourselves.
func (Event) ThreadAnchor ¶
ThreadAnchor returns the ts to use as the "session anchor" for a thread. Slack semantics: a top-level message has thread_ts == "" and ts == message-ts; replies have thread_ts == parent-ts. We always anchor on the parent so all replies in a thread share the same session.
type EventsAPIPayload ¶
type EventsAPIPayload struct {
Type string `json:"type"` // "event_callback"
TeamID string `json:"team_id"`
APIAppID string `json:"api_app_id"`
Event Event `json:"event"`
EventID string `json:"event_id"`
}
EventsAPIPayload is the inner envelope for the "events_api" type.
type HTTPSlackAPI ¶
type HTTPSlackAPI struct {
BotToken string
BaseURL string // override for tests; defaults to https://slack.com/api
HTTP *http.Client
}
HTTPSlackAPI is the production implementation — talks to slack.com over HTTPS.
func NewHTTPSlackAPI ¶
func NewHTTPSlackAPI(botToken string) *HTTPSlackAPI
NewHTTPSlackAPI returns a client with sensible timeouts.
func (*HTTPSlackAPI) AddReaction ¶
func (c *HTTPSlackAPI) AddReaction(ctx context.Context, channel, ts, name string) error
AddReaction adds an emoji reaction. `name` is the bare emoji name without surrounding colons ("white_check_mark", not ":white_check_mark:").
func (*HTTPSlackAPI) PostMessage ¶
func (c *HTTPSlackAPI) PostMessage(ctx context.Context, channel, threadTS, text string) (string, error)
PostMessage posts a new message. threadTS may be empty.
func (*HTTPSlackAPI) RemoveReaction ¶
func (c *HTTPSlackAPI) RemoveReaction(ctx context.Context, channel, ts, name string) error
RemoveReaction removes an emoji reaction.
func (*HTTPSlackAPI) UpdateMessage ¶
func (c *HTTPSlackAPI) UpdateMessage(ctx context.Context, channel, ts, text string) error
UpdateMessage edits a previously-posted message in place.
type SessionMap ¶
type SessionMap struct {
// contains filtered or unexported fields
}
SessionMap maps a Slack thread anchor (channel + thread_ts) to an overkill session id. Persisted as JSON so the bot can resume in-flight threads across restarts. New thread → new session; replies inside a thread reuse the same session.
func NewSessionMap ¶
func NewSessionMap(path string) (*SessionMap, error)
NewSessionMap loads the persisted map from path. A missing file is not an error — we start with an empty map. An empty path disables persistence (used by tests).
func NewSessionMapDB ¶
func NewSessionMapDB(db *sql.DB, tableName string) (*SessionMap, error)
NewSessionMapDB creates a SessionMap backed by a Postgres table. The table is auto-created if it doesn't exist. All existing rows are loaded into memory on construction so lookups are fast; writes are persisted to both memory and Postgres on every GetOrCreate.
func (*SessionMap) Get ¶
func (m *SessionMap) Get(channel, threadTS string) (string, bool)
Get returns (sessionID, true) if a session exists for the thread.
func (*SessionMap) GetOrCreate ¶
func (m *SessionMap) GetOrCreate(channel, threadTS, newID string) (id string, created bool, err error)
GetOrCreate returns the existing session id for the thread, or registers `newID` and returns it. The created flag tells the caller whether they just minted a new session.
type SlackAPI ¶
type SlackAPI interface {
PostMessage(ctx context.Context, channel, threadTS, text string) (msgTS string, err error)
UpdateMessage(ctx context.Context, channel, ts, text string) error
AddReaction(ctx context.Context, channel, ts, name string) error
RemoveReaction(ctx context.Context, channel, ts, name string) error
}
SlackAPI is the minimal subset of Slack's Web API we use. Defining it as an interface lets bot_test.go drop in an httptest-backed fake without ever touching the real Slack network.
type SocketClient ¶
type SocketClient struct {
APIBaseURL string // override for tests; default https://slack.com/api
AppToken string // xapp-...
HTTP *http.Client // for the connections.open call
Dial func(ctx context.Context, network, addr string) (net.Conn, error) // override for tests
// contains filtered or unexported fields
}
SocketClient is a tiny client-side WebSocket for Slack's Socket Mode. Slack publishes envelopes; we ack each one. The client handles control frames (ping/close) and exposes a simple Events channel.
We hand-roll the WS handshake to avoid pulling in nhooyr/gorilla. The existing wsConn under internal/web is server-side (it expects masked inbound frames and writes unmasked outbound frames); RFC 6455 requires the opposite for clients, so we keep the two implementations separate.
func (*SocketClient) Close ¶
func (c *SocketClient) Close() error
Close terminates the WS, sending a close frame on a best-effort basis.
func (*SocketClient) Connect ¶
func (c *SocketClient) Connect(ctx context.Context) error
Connect performs the apps.connections.open handshake then upgrades to WS. Returns when the WS is fully open and the "hello" envelope has been read. On any error the underlying connection is closed before returning.
func (*SocketClient) ReadEnvelope ¶
func (c *SocketClient) ReadEnvelope(deadline time.Time) (*SocketEnvelope, error)
ReadEnvelope blocks until the next text frame arrives, decoding it as SocketEnvelope. Pings and pongs are handled transparently.
func (*SocketClient) SendAck ¶
func (c *SocketClient) SendAck(envelopeID string) error
SendAck writes the {"envelope_id":"..."} ack Slack expects after each events_api delivery.
type SocketEnvelope ¶
type SocketEnvelope struct {
Type string `json:"type"` // "hello" | "events_api" | "disconnect" | "interactive" | ...
EnvelopeID string `json:"envelope_id"` // omitted for "hello"
AcceptsResponsePayload bool `json:"accepts_response_payload"`
Payload EventsAPIPayload `json:"payload"`
Reason string `json:"reason"` // for "disconnect"
}
SocketEnvelope wraps every payload Slack sends over Socket Mode. envelope_id must be echoed back as the ack so Slack stops retrying.