slackapp

package
v1.76.2 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package slackapp is the part of Slack's Web API the Slack controller needs to make a workspace's channels match who is entitled to be in them — and the part the console needs to connect a workspace in the first place.

Slack cannot read a token claim, so sluis reconciles instead: it finds or creates the channels a policy names, invites the people who should be in them, removes the people who should not be from private channels, and, between two workspaces of the same owner, links a channel with Slack Connect. Every method here exists for one of those steps and says which. User groups are deliberately absent: nothing reconciles them.

A Client is bound to one workspace's bot token. The token travels in an Authorization header and nowhere else: it is not in a URL, not in a log line and not in any error this package returns, so an error may be logged as it is. Setup holds the calls that are not made with a bot token — creating the App from a manifest with an app configuration token, and exchanging an OAuth code for the bot token.

Every call is a form-encoded POST, which every method accepts, and is retried after a rate limit: on HTTP 429, and on a 200 whose error is "ratelimited", the call waits as long as Slack's Retry-After says (the wait ends with the context) and tries again a bounded number of times before giving up with a RateLimitError.

Slack's own error code is preserved: a refusal is an *APIError, and errors.Is against one of the Err* values below tells the controller which decision to take without matching strings.

Index

Constants

View Source
const DefaultBaseURL = "https://slack.com/api"

DefaultBaseURL is Slack's Web API.

View Source
const DefaultRetries = 3

DefaultRetries is how many times a rate-limited call is retried.

View Source
const InviteLimit = 1000

InviteLimit is how many users one conversations.invite takes.

Variables

View Source
var (
	// ErrNameTaken: a channel of that name exists. The controller adopts
	// it instead of creating.
	ErrNameTaken = &APIError{Code: "name_taken"}
	// ErrRestricted: the workspace's settings forbid the action for a
	// bot — for a kick, every public channel by default.
	ErrRestricted = &APIError{Code: "restricted_action"}
	// ErrNotInChannel: the caller (or, for a kick, the target) is not in
	// the channel.
	ErrNotInChannel = &APIError{Code: "not_in_channel"}
	// ErrAlreadyInChannel: the invitee is already there.
	ErrAlreadyInChannel = &APIError{Code: "already_in_channel"}
	// ErrCantKickSelf: the target is the bot itself.
	ErrCantKickSelf = &APIError{Code: "cant_kick_self"}
	// ErrCantKickFromGeneral: nobody is removed from #general.
	ErrCantKickFromGeneral = &APIError{Code: "cant_kick_from_general"}
	// ErrAlreadyArchived: the channel is archived already.
	ErrAlreadyArchived = &APIError{Code: "already_archived"}
	// ErrChannelNotFound: no such channel, or a private one the bot is
	// not in — Slack does not say which.
	ErrChannelNotFound = &APIError{Code: "channel_not_found"}
	// ErrMissingScope: the token lacks a scope the method needs.
	ErrMissingScope = &APIError{Code: "missing_scope"}
	// ErrInvalidAuth: the token is revoked or never was one.
	ErrInvalidAuth = &APIError{Code: "invalid_auth"}
	// ErrTokenRevoked: the token was revoked.
	ErrTokenRevoked = &APIError{Code: "token_revoked"}
	// ErrUserNotFound: no such user in this workspace.
	ErrUserNotFound = &APIError{Code: "user_not_found"}
	// ErrRateLimited is what a [RateLimitError] matches.
	ErrRateLimited = errors.New("slackapp: rate limited")
)

The codes a controller decides on. Match with errors.Is.

Functions

func MissingScopes

func MissingScopes(granted string, required []string) []string

MissingScopes returns the required scopes a granted scope string lacks, in the order required. Slack grants scopes as one comma-separated string. The console uses it to say "reinstall needed" when an App installed earlier predates a scope the controller now wants.

Types

type APIError

type APIError struct {
	Method string
	Code   string
	// Details are the per-item messages Slack sometimes adds (the
	// `errors` array of an invalid manifest or a multi-user invite).
	Details []string
}

APIError is Slack answering ok:false. Code is Slack's own `error` field.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

func (e *APIError) Is(target error) bool

Is matches another *APIError by code, ignoring the method when the target names none — which is how the Err* values below are written.

type AcceptParams

type AcceptParams struct {
	InviteID string
	// ChannelName is what the channel is called in this workspace; Slack
	// requires one.
	ChannelName string
	IsPrivate   bool
	// FreeTrialAccepted spends the workspace's free Slack Connect trial
	// when it has no paid plan. Never set without the operator's say.
	FreeTrialAccepted bool
}

AcceptParams is what accepting a Slack Connect invitation takes.

type App

type App struct {
	AppID             string         `json:"app_id"`
	Credentials       AppCredentials `json:"credentials"`
	OAuthAuthorizeURL string         `json:"oauth_authorize_url"`
}

App is a created App.

type AppCredentials

type AppCredentials struct {
	ClientID      string `json:"client_id"`
	ClientSecret  string `json:"client_secret"`
	SigningSecret string `json:"signing_secret"`
}

AppCredentials is what creating an App returns.

type Channel

type Channel struct {
	ID         string `json:"id"`
	Name       string `json:"name"`
	IsPrivate  bool   `json:"is_private"`
	IsArchived bool   `json:"is_archived"`
	IsGeneral  bool   `json:"is_general"`
	// IsMember is whether the bot is in it.
	IsMember bool `json:"is_member"`
	// IsExtShared is a Slack Connect channel.
	IsExtShared bool `json:"is_ext_shared"`
	// IsShared and IsOrgShared are Slack's other sharing flags: shared with
	// another workspace, and shared across the organisation's workspaces.
	IsShared    bool `json:"is_shared"`
	IsOrgShared bool `json:"is_org_shared"`
	// SharedTeamIDs are the workspaces a Slack Connect channel is shared
	// with, the channel's own among them. Empty for an ordinary channel.
	SharedTeamIDs []string `json:"shared_team_ids"`
	// ConnectedTeamIDs are the external workspaces a Slack Connect channel
	// reaches, from this side's view.
	ConnectedTeamIDs []string `json:"connected_team_ids"`
	// PendingSharedTeamIDs and PendingConnectedTeamIDs are the workspaces a
	// Slack Connect channel has been shared with and that have not accepted
	// yet. Their sides are not visible to their own bots, but Slack names
	// them here.
	PendingSharedTeamIDs    []string `json:"pending_shared"`
	PendingConnectedTeamIDs []string `json:"pending_connected_team_ids"`
	// InternalTeamIDs are the workspaces of the same organisation an
	// Enterprise Grid channel is shared across.
	InternalTeamIDs []string `json:"internal_team_ids"`
	// ConversationHostID is the team that owns a Slack Connect channel
	// (`conversation_host_id`); empty for an ordinary channel.
	ConversationHostID string `json:"conversation_host_id"`
	// NumMembers is how many members the channel has on this side's view.
	NumMembers int    `json:"num_members"`
	Creator    string `json:"creator"`
}

Channel is a conversation.

func (Channel) Teams

func (c Channel) Teams() []string

Teams are every workspace a Slack Connect channel reaches, as this side reports it: the shared, connected, pending and internal teams and the host, without repeats, in first-seen order.

type Client

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

Client is one workspace's bot.

func New

func New(token string, opts ...Option) *Client

New returns a client acting with token, a workspace's bot token.

func (*Client) AcceptSharedInvite

func (c *Client) AcceptSharedInvite(ctx context.Context, p AcceptParams) (string, error)

AcceptSharedInvite accepts a pending invitation, which is what makes the channel appear in this workspace. It returns the channel id.

func (*Client) AllChannels

func (c *Client) AllChannels(ctx context.Context) ([]Channel, error)

AllChannels is Client.Channels with the archived ones included, marked by IsArchived. It is how the controller tells "a channel of that name exists and is archived" from "none does": Slack keeps an archived channel's name, so creating a channel of that name is refused as taken, and the roster never unarchives.

func (*Client) Archive

func (c *Client) Archive(ctx context.Context, channel string) error

Archive archives a channel the bot is in (conversations.archive). A channel already archived is success: the goal is that it is archived.

func (*Client) AuthTest

func (c *Client) AuthTest(ctx context.Context) (Identity, error)

AuthTest asks who the token is. The controller compares TeamID with the workspace the connection says it belongs to, so a token pasted or installed into the wrong workspace is refused before it changes anything, and keeps UserID to recognise its own membership.

func (*Client) ChannelInfo

func (c *Client) ChannelInfo(ctx context.Context, channel string) (Channel, error)

ChannelInfo reads one channel, for its archived flag, privacy and whether the bot is in it, between listings.

func (*Client) Channels

func (c *Client) Channels(ctx context.Context) ([]Channel, error)

Channels lists every public and private channel the bot can see, archived ones excluded, following the cursor to the end. It is how the controller finds the channel a policy names by name before deciding to create or adopt it.

func (*Client) ConnectInvites

func (c *Client) ConnectInvites(ctx context.Context) ([]ConnectInvite, error)

ConnectInvites lists pending Slack Connect invitations, following the cursor. The receiving side's controller reads it to find the invitation its peer sent, and the sending side reads it to see one is already pending instead of sending another.

func (*Client) CreateChannel

func (c *Client) CreateChannel(ctx context.Context, name string, private bool) (Channel, error)

CreateChannel makes a channel the policy names and Slack lacks. The bot that creates it is in it, which is what lets it invite and, for a private one, remove. A name already taken is ErrNameTaken: the controller adopts the existing channel instead.

func (*Client) Invite

func (c *Client) Invite(ctx context.Context, channel string, users []string) error

Invite adds users to a channel, in batches of InviteLimit. Somebody already in is success — the reconcile asked for them to be there and they are — so a batch refused for one such user is retried user by user rather than failing the people who were not.

func (*Client) InviteShared

func (c *Client) InviteShared(ctx context.Context, channel string, target ConnectTarget, externalLimited bool) (string, error)

InviteShared invites another workspace into a channel, making it a Slack Connect channel. Slack takes one recipient per call. The recipient is named by user id when the controller already knows the other workspace's bot (the usual case between our own workspaces) and by address otherwise. externalLimited keeps the other side from changing the channel's settings. It returns the invite id.

func (*Client) JoinChannel

func (c *Client) JoinChannel(ctx context.Context, channel string) (Channel, error)

JoinChannel puts the bot into a public channel, which is how an existing public channel is adopted: a bot outside a channel cannot invite into it. A private channel cannot be joined; somebody inside has to invite the bot.

func (*Client) Kick

func (c *Client) Kick(ctx context.Context, channel, user string) error

Kick removes a user from a channel: the enforcement half of the reconcile, used on private channels only — Slack's default forbids a bot removing anybody from a public one, and that comes back as ErrRestricted for the controller to report as drift it cannot fix. A user already out is success. ErrCantKickSelf and ErrCantKickFromGeneral are returned for the controller to skip.

func (*Client) LookupByEmail

func (c *Client) LookupByEmail(ctx context.Context, email string) (user User, found bool, err error)

LookupByEmail finds the account for an address, to turn an entitled person into the user id an invite needs. found is false — not an error — when the workspace has no such account: the controller reports the person as not yet in Slack and moves on.

func (*Client) Members

func (c *Client) Members(ctx context.Context, channel string) ([]string, error)

Members lists the user ids in a channel, following the cursor. It is the "actual" side of the reconcile: the controller diffs it against who is entitled to decide who to invite and who to remove.

func (*Client) ProbeChannel

func (c *Client) ProbeChannel(ctx context.Context, channel string) (Channel, error)

ProbeChannel asks this bot about a channel by id, with the member count, whether or not the bot is in it or lists it. A Slack Connect channel that is public on this side answers; a private one the bot is not in is ErrChannelNotFound, which Slack does not tell apart from "not shared with this workspace".

func (*Client) Revoke

func (c *Client) Revoke(ctx context.Context) error

Revoke revokes the bot token this client holds (auth.revoke). The console uses it to take back a token minted for the wrong workspace and to end a connection it disconnects. A token already revoked, or never valid, is success: the goal is that it no longer works, and it does not.

func (*Client) UserInfo

func (c *Client) UserInfo(ctx context.Context, id string) (User, error)

UserInfo reads one account by id. The reconciler uses it to learn the address of a member of a channel it may remove from, because conversations.members answers with ids only. An id Slack does not know is ErrUserNotFound.

type ConnectInvite

type ConnectInvite struct {
	// Direction is "incoming" or "outgoing".
	Direction string `json:"direction"`
	Invite    struct {
		ID           string `json:"id"`
		InvitingTeam struct {
			ID   string `json:"id"`
			Name string `json:"name"`
		} `json:"inviting_team"`
		RecipientUserID string `json:"recipient_user_id"`
		RecipientEmail  string `json:"recipient_email"`
	} `json:"invite"`
	Channel struct {
		ID        string `json:"id"`
		Name      string `json:"name"`
		IsPrivate bool   `json:"is_private"`
	} `json:"channel"`
}

ConnectInvite is a Slack Connect invitation not yet accepted.

type ConnectTarget

type ConnectTarget struct {
	Email  string
	UserID string
}

ConnectTarget is who a Slack Connect invitation goes to: one address or one user id, never both.

type Identity

type Identity struct {
	TeamID string `json:"team_id"`
	Team   string `json:"team"`
	UserID string `json:"user_id"`
	BotID  string `json:"bot_id"`
}

Identity is who a token is.

type Installation

type Installation struct {
	// BotToken is the workspace's credential. The caller keeps it; this
	// package never logs it.
	BotToken  string
	TeamID    string
	TeamName  string
	BotUserID string
	// Scope is the granted scopes, comma-separated; compare it with
	// [MissingScopes].
	Scope string
}

Installation is the result of an owner installing the App.

type Option

type Option func(*transport)

Option configures a Client or Setup.

func WithBaseURL

func WithBaseURL(base string) Option

WithBaseURL points the client somewhere other than Slack: a test's fake.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient replaces the HTTP client.

func WithPageSize

func WithPageSize(n int) Option

WithPageSize sets the `limit` of every paginated call. Slack's own default and maximum suit production; a test sets a small one to make the cursor matter.

func WithRetries

func WithRetries(n int) Option

WithRetries sets how many times a rate-limited call is retried.

func WithSleep

func WithSleep(sleep func(context.Context, time.Duration) error) Option

WithSleep replaces how a rate-limit wait passes, for tests that must not spend real seconds. It must return early, with the context's error, when the context ends.

type RateLimitError

type RateLimitError struct {
	Method string
	// RetryAfter is what Slack last asked for.
	RetryAfter time.Duration
}

RateLimitError is a call still refused after every retry.

func (*RateLimitError) Error

func (e *RateLimitError) Error() string

func (*RateLimitError) Is

func (e *RateLimitError) Is(target error) bool

Is makes errors.Is(err, ErrRateLimited) true.

type Setup

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

Setup is the calls that are made before a workspace has a bot token — the console's connect flow. None uses one: creating and updating the App takes an app configuration token, and the OAuth exchange takes the App's own client credentials.

func NewSetup

func NewSetup(opts ...Option) *Setup

NewSetup returns the setup calls, against Slack or a test's fake.

func (*Setup) CreateApp

func (s *Setup) CreateApp(ctx context.Context, configToken, manifest string) (App, error)

CreateApp creates an App from a manifest (JSON text) with an app configuration token. The owner then installs it from the returned authorize URL, which is what makes connecting a workspace two clicks. A refused manifest is an *APIError "invalid_manifest" whose Details say where.

func (*Setup) OAuthAccess

func (s *Setup) OAuthAccess(ctx context.Context, clientID, clientSecret, code, redirectURI string) (Installation, error)

OAuthAccess exchanges the code Slack sent back to the redirect URI for the bot token, using the App's client credentials (as HTTP Basic, which Slack recommends over form fields).

func (*Setup) UpdateApp

func (s *Setup) UpdateApp(ctx context.Context, configToken, appID, manifest string) (permissionsUpdated bool, err error)

UpdateApp replaces an App's manifest — unmodified fields included, as Slack requires — which is how an App installed earlier gets the scopes a later release needs. permissionsUpdated is true when the owner must reinstall for them to take effect.

type User

type User struct {
	ID      string `json:"id"`
	TeamID  string `json:"team_id"`
	Deleted bool   `json:"deleted"`
	IsBot   bool   `json:"is_bot"`
	// IsRestricted is a multi-channel guest; IsUltraRestricted a
	// single-channel guest.
	IsRestricted      bool `json:"is_restricted"`
	IsUltraRestricted bool `json:"is_ultra_restricted"`
	// IsAppUser is an app's bot user, which is a bot for our purposes too.
	IsAppUser bool `json:"is_app_user"`
	Profile   struct {
		// Email is present only when the token has users:read.email.
		Email string `json:"email"`
	} `json:"profile"`
}

User is a workspace member as the reconciler needs to see them.

func (User) Email

func (u User) Email() string

Email is the address on the account's profile, empty when Slack did not return one (a bot, or a token without users:read.email).

func (User) IsAutomated

func (u User) IsAutomated() bool

IsAutomated reports a bot or an app's user.

func (User) IsGuest

func (u User) IsGuest() bool

IsGuest reports a guest of either kind.

Directories

Path Synopsis
Package catalogue is Slack Apps declared as data: which workspace each belongs to, what it is called and which bot scopes it asks for.
Package catalogue is Slack Apps declared as data: which workspace each belongs to, what it is called and which bot scopes it asks for.
Package catalogueapp is what creating a catalogue Slack App leaves behind: the App's record, its client credentials and, once installed, its bot token.
Package catalogueapp is what creating a catalogue Slack App leaves behind: the App's record, its client credentials and, once installed, its bot token.
Package console is the Slack module's half of the console: what an operator does to Slack through the console (connecting a workspace, creating and installing Apps, defining channels and Slack Connect channels) runs here, on the module's own records and credentials, as the caller the console authorized (docs/decisions/0071, v1.76 D-v176-1 b).
Package console is the Slack module's half of the console: what an operator does to Slack through the console (connecting a workspace, creating and installing Apps, defining channels and Slack Connect channels) runs here, on the module's own records and credentials, as the caller the console authorized (docs/decisions/0071, v1.76 D-v176-1 b).
Package slackfake is Slack, in memory: several workspaces behind one server, answering the calls the Slack controller makes and changing as they change it.
Package slackfake is Slack, in memory: several workspaces behind one server, answering the calls the Slack controller makes and changing as they change it.

Jump to

Keyboard shortcuts

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