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
- Variables
- func MissingScopes(granted string, required []string) []string
- type APIError
- type AcceptParams
- type App
- type AppCredentials
- type Channel
- type Client
- func (c *Client) AcceptSharedInvite(ctx context.Context, p AcceptParams) (string, error)
- func (c *Client) AllChannels(ctx context.Context) ([]Channel, error)
- func (c *Client) Archive(ctx context.Context, channel string) error
- func (c *Client) AuthTest(ctx context.Context) (Identity, error)
- func (c *Client) ChannelInfo(ctx context.Context, channel string) (Channel, error)
- func (c *Client) Channels(ctx context.Context) ([]Channel, error)
- func (c *Client) ConnectInvites(ctx context.Context) ([]ConnectInvite, error)
- func (c *Client) CreateChannel(ctx context.Context, name string, private bool) (Channel, error)
- func (c *Client) Invite(ctx context.Context, channel string, users []string) error
- func (c *Client) InviteShared(ctx context.Context, channel string, target ConnectTarget, ...) (string, error)
- func (c *Client) JoinChannel(ctx context.Context, channel string) (Channel, error)
- func (c *Client) Kick(ctx context.Context, channel, user string) error
- func (c *Client) LookupByEmail(ctx context.Context, email string) (user User, found bool, err error)
- func (c *Client) Members(ctx context.Context, channel string) ([]string, error)
- func (c *Client) ProbeChannel(ctx context.Context, channel string) (Channel, error)
- func (c *Client) Revoke(ctx context.Context) error
- func (c *Client) UserInfo(ctx context.Context, id string) (User, error)
- type ConnectInvite
- type ConnectTarget
- type Identity
- type Installation
- type Option
- type RateLimitError
- type Setup
- func (s *Setup) CreateApp(ctx context.Context, configToken, manifest string) (App, error)
- func (s *Setup) OAuthAccess(ctx context.Context, clientID, clientSecret, code, redirectURI string) (Installation, error)
- func (s *Setup) UpdateApp(ctx context.Context, configToken, appID, manifest string) (permissionsUpdated bool, err error)
- type User
Constants ¶
const DefaultBaseURL = "https://slack.com/api"
DefaultBaseURL is Slack's Web API.
const DefaultRetries = 3
DefaultRetries is how many times a rate-limited call is retried.
const InviteLimit = 1000
InviteLimit is how many users one conversations.invite takes.
Variables ¶
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 ¶
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.
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 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.
// 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.
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.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is one workspace's bot.
func (*Client) AcceptSharedInvite ¶
AcceptSharedInvite accepts a pending invitation, which is what makes the channel appear in this workspace. It returns the channel id.
func (*Client) AllChannels ¶
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 ¶
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 ¶
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 ¶
ChannelInfo reads one channel, for its archived flag, privacy and whether the bot is in it, between listings.
func (*Client) Channels ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
WithBaseURL points the client somewhere other than Slack: a test's fake.
func WithHTTPClient ¶
WithHTTPClient replaces the HTTP client.
func WithPageSize ¶
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 ¶
WithRetries sets how many times a rate-limited call is retried.
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 (*Setup) CreateApp ¶
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 ¶
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 ¶
IsAutomated reports a bot or an app's user.
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. |