Files
Dmitry VerkhoturovandUmputun 80c12a3f10 chore(deps): update Go modules
Bump Go dependencies in both backend/ and backend/_example/memory_store.

Notable updates:
- github.com/go-pkgz/lgr v0.12.1 -> v0.12.3
- github.com/klauspost/compress v1.18.2 -> v1.18.5
- github.com/PuerkitoBio/goquery v1.11.0 -> v1.12.0
- github.com/montanaflynn/stats v0.7.1 -> v0.9.0
- github.com/redis/go-redis/v9 v9.17.2 -> v9.18.0
- github.com/slack-go/slack v0.17.3 -> v0.21.1
- go.mongodb.org/mongo-driver v1.17.6 -> v1.17.9
- golang.org/x/crypto v0.48.0 -> v0.50.0
- golang.org/x/net v0.49.0 -> v0.53.0
- golang.org/x/image v0.36.0 -> v0.39.0
- golang.org/x/sys v0.41.0 -> v0.43.0
- golang.org/x/{oauth2,sync,text} minor bumps

Key markdown/sanitisation libs (bluemonday v1.0.27,
alecthomas/chroma/v2 v2.23.1, russross/blackfriday/v2 v2.1.0,
Depado/bfchroma/v2 v2.0.0) are already at the latest available
versions and were not bumped.

Verified the Chroma span-class allowlist regex in
backend/app/store/comment.go:128-131 is still fully in sync with
chroma/v2 types.go StandardTypes map (86 classes, byte-equal after
sorting). The inline comment references commit c263f6f which is
stale (Chroma is at v2 now), but the class list content is current.

Ran `go mod tidy` + `go mod vendor` + full race test suite on both
modules. All green. Added a reminder in CLAUDE.md that updating
backend/ Go modules also requires `go mod tidy` in
backend/_example/memory_store since the example module uses a
local replace directive and inherits indirect deps from the main
module.
2026-04-12 11:52:57 -05:00

281 lines
8.3 KiB
Go

package slack
import (
"context"
"fmt"
"log"
"net/http"
"net/url"
"os"
)
const (
// APIURL of the slack api.
APIURL = "https://slack.com/api/"
// AuditAPIURL is the base URL for the Audit Logs API.
AuditAPIURL = "https://api.slack.com/"
// WEBAPIURLFormat ...
WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d"
)
// httpClient defines the minimal interface needed for an http.Client to be implemented.
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
// ResponseMetadata holds pagination metadata
type ResponseMetadata struct {
Cursor string `json:"next_cursor"`
Messages []string `json:"messages"`
Warnings []string `json:"warnings"`
}
func (t *ResponseMetadata) initialize() *ResponseMetadata {
if t != nil {
return t
}
return &ResponseMetadata{}
}
// AuthTestResponse ...
type AuthTestResponse struct {
URL string `json:"url"`
Team string `json:"team"`
User string `json:"user"`
TeamID string `json:"team_id"`
UserID string `json:"user_id"`
// EnterpriseID is only returned when an enterprise id present
EnterpriseID string `json:"enterprise_id,omitempty"`
BotID string `json:"bot_id"`
Header http.Header `json:"-"`
}
type authTestResponseFull struct {
SlackResponse
AuthTestResponse
responseHeaders
}
type ParamOption func(*url.Values)
// Client for the slack api.
type Client struct {
token string
appLevelToken string
configToken string
configRefreshToken string
endpoint string
auditEndpoint string
debug bool
log ilogger
httpclient httpClient
onWarning func(path string, request any, w *Warning)
onResponseHeaders func(path string, headers http.Header)
}
// Option defines an option for a Client
type Option func(*Client)
// OptionHTTPClient - provide a custom http client to the slack client.
func OptionHTTPClient(client httpClient) func(*Client) {
return func(c *Client) {
c.httpclient = client
}
}
// OptionDebug enable debugging for the client
func OptionDebug(b bool) func(*Client) {
return func(c *Client) {
c.debug = b
}
}
// OptionLog set logging for client.
func OptionLog(l logger) func(*Client) {
return func(c *Client) {
c.log = internalLog{logger: l}
}
}
// OptionOnWarning sets a callback invoked whenever an API response contains
// warnings. The callback receives the API method path (e.g.
// "conversations.join"), the request payload ([url.Values] for form-encoded
// requests or []byte for JSON requests), and a [Warning] with the warning
// codes and messages.
//
// Example:
//
// api := slack.New("YOUR_TOKEN",
// slack.OptionOnWarning(func(path string, request any, w *slack.Warning) {
// log.Printf("slack warnings for %s: codes=%v warnings=%v", path, w.Codes, w.Warnings)
// }),
// )
func OptionOnWarning(fn func(path string, request any, w *Warning)) func(*Client) {
return func(c *Client) {
c.onWarning = fn
}
}
// OptionOnResponseHeaders sets a callback invoked after every API request
// with the API method path and the HTTP response headers. This allows
// accessing headers like X-OAuth-Scopes and X-Ratelimit-* for any request.
func OptionOnResponseHeaders(fn func(path string, headers http.Header)) func(*Client) {
return func(c *Client) {
c.onResponseHeaders = fn
}
}
// OptionAPIURL set the url for the client. only useful for testing.
func OptionAPIURL(u string) func(*Client) {
return func(c *Client) { c.endpoint = u }
}
// OptionAuditAPIURL set the url for the Audit Logs API. only useful for testing.
func OptionAuditAPIURL(u string) func(*Client) {
return func(c *Client) { c.auditEndpoint = u }
}
// OptionAppLevelToken sets an app-level token for the client.
func OptionAppLevelToken(token string) func(*Client) {
return func(c *Client) { c.appLevelToken = token }
}
// OptionConfigToken sets a configuration token for the client.
func OptionConfigToken(token string) func(*Client) {
return func(c *Client) { c.configToken = token }
}
// OptionConfigRefreshToken sets a configuration refresh token for the client.
func OptionConfigRefreshToken(token string) func(*Client) {
return func(c *Client) { c.configRefreshToken = token }
}
// OptionRetry enables HTTP retries for rate limit (429) only; 5xx and connection errors are not retried.
// Uses DefaultRetryHandlers. Use OptionRetryConfig with AllBuiltinRetryHandlers for connection + 429.
// If maxRetries is zero or negative, the client is not wrapped (no retries).
// When using a custom HTTP client, pass OptionRetry after OptionHTTPClient so the retry wrapper is applied to it.
func OptionRetry(maxRetries int) func(*Client) {
return func(c *Client) {
if maxRetries <= 0 {
return
}
cfg := DefaultRetryConfig()
cfg.MaxRetries = maxRetries
cfg.Handlers = DefaultRetryHandlers(cfg)
c.httpclient = &retryClient{client: c.httpclient, config: cfg, debug: c}
}
}
// OptionRetryConfig enables HTTP retries with a custom config.
// If config.MaxRetries is 0, the client is not wrapped (no retries).
// If config.Handlers is nil, DefaultRetryHandlers(cfg) is used (429 only).
// When using a custom HTTP client, pass OptionRetryConfig after OptionHTTPClient so the retry wrapper is applied to it.
func OptionRetryConfig(config RetryConfig) func(*Client) {
return func(c *Client) {
if config.MaxRetries <= 0 {
return
}
cfg := config
if cfg.Handlers == nil {
cfg.Handlers = DefaultRetryHandlers(cfg)
}
c.httpclient = &retryClient{client: c.httpclient, config: cfg, debug: c}
}
}
// New builds a slack client from the provided token and options.
func New(token string, options ...Option) *Client {
s := &Client{
token: token,
endpoint: APIURL,
auditEndpoint: AuditAPIURL,
httpclient: &http.Client{},
log: log.New(os.Stderr, "slack-go/slack", log.LstdFlags|log.Lshortfile),
}
for _, opt := range options {
opt(s)
}
return s
}
// AuthTest tests if the user is able to do authenticated requests or not
func (api *Client) AuthTest() (response *AuthTestResponse, error error) {
return api.AuthTestContext(context.Background())
}
// AuthTestContext tests if the user is able to do authenticated requests or not with a custom context
func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, err error) {
api.Debugf("Challenging auth...")
responseFull := &authTestResponseFull{}
err = api.postMethod(ctx, "auth.test", url.Values{"token": {api.token}}, responseFull)
if err != nil {
return nil, err
}
responseFull.AuthTestResponse.Header = responseFull.responseHeaders.header
return &responseFull.AuthTestResponse, responseFull.Err()
}
// Debugf print a formatted debug line.
func (api *Client) Debugf(format string, v ...any) {
if api.debug {
api.log.Output(2, fmt.Sprintf(format, v...))
}
}
// Debugln print a debug line.
func (api *Client) Debugln(v ...any) {
if api.debug {
api.log.Output(2, fmt.Sprintln(v...))
}
}
// Debug returns if debug is enabled.
func (api *Client) Debug() bool {
return api.debug
}
// post to a slack web method.
func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf any) error {
headers, err := postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api)
api.checkWarnings(intf, path, values)
api.fireResponseHeaders(path, headers)
return err
}
// get a slack web method.
func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf any) error {
headers, err := getResource(ctx, api.httpclient, api.endpoint+path, token, values, intf, api)
api.checkWarnings(intf, path, values)
api.fireResponseHeaders(path, headers)
return err
}
// postJSONMethod posts JSON to a slack web method.
func (api *Client) postJSONMethod(ctx context.Context, path string, token string, jsonBody []byte, intf any) error {
headers, err := postJSON(ctx, api.httpclient, api.endpoint+path, token, jsonBody, intf, api)
api.checkWarnings(intf, path, jsonBody)
api.fireResponseHeaders(path, headers)
return err
}
func (api *Client) checkWarnings(intf any, path string, request any) {
if api.onWarning == nil {
return
}
if w, ok := intf.(warner); ok {
if warning := w.Warn(); warning != nil {
api.onWarning(path, request, warning)
}
}
}
func (api *Client) fireResponseHeaders(path string, headers http.Header) {
if api.onResponseHeaders != nil && headers != nil {
api.onResponseHeaders(path, headers)
}
}