slack

package
v0.11.0-beta.1 Latest Latest
Warning

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

Go to latest
Published: Oct 23, 2026 License: Apache-2.0, MIT Imports: 25 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func EscapeMrkdwn

func EscapeMrkdwn(s string) string

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

func FormatToolCall(name, args string) string

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

func MarkdownToMrkdwn(s string) string

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.

func (*Bot) Run

func (b *Bot) Run(ctx context.Context) error

Run drives the bot until ctx is cancelled or an unrecoverable error occurs. Network errors trigger reconnect with exponential backoff.

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

func (e Event) IsFromBot() bool

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

func (e Event) ThreadAnchor() string

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.

Jump to

Keyboard shortcuts

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