mirror of
https://github.com/paymog/slack-cli.git
synced 2026-09-18 23:43:49 +08:00
add logic to improve fetching channels list on slack workspaces with enterprise setting
This commit is contained in:
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23 AS build
|
||||
FROM golang:1.24 AS build
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOTOOLCHAIN=local
|
||||
@@ -21,7 +21,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
FROM build AS dev
|
||||
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
go install github.com/go-delve/delve/cmd/dlv@v1.23.1 && cp /go/bin/dlv /dlv
|
||||
go install github.com/go-delve/delve/cmd/dlv@v1.25.0 && cp /go/bin/dlv /dlv
|
||||
|
||||
WORKDIR /app/mcp-server
|
||||
|
||||
|
||||
@@ -26,8 +26,10 @@ func main() {
|
||||
p,
|
||||
)
|
||||
|
||||
go newUsersWatcher(p)()
|
||||
go newChannelsWatcher(p)()
|
||||
go func() {
|
||||
newUsersWatcher(p)()
|
||||
newChannelsWatcher(p)()
|
||||
}()
|
||||
|
||||
switch transport {
|
||||
case "stdio":
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
context: .
|
||||
target: dev
|
||||
dockerfile: Dockerfile
|
||||
# command: /dlv debug /app/cmd/slack-mcp-server/main.go --headless --listen=:40000 --api-version=2 --log -- --transport sse
|
||||
command: /dlv debug /app/cmd/slack-mcp-server/main.go --accept-multiclient --headless --listen=:40000 --api-version=2 --log -- --transport sse
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- app-tier
|
||||
@@ -14,8 +14,8 @@ services:
|
||||
# Optional: Uncomment to mount the CA certificate if you need to trust a custom CA
|
||||
# Don't forget to set SLACK_MCP_SERVER_CA=/usr/local/share/ca-certificates/ca.crt
|
||||
#
|
||||
# volumes:
|
||||
# - ./ca-cert.crt:/usr/local/share/ca-certificates/ca.crt
|
||||
volumes:
|
||||
- ./ca-cert.crt:/usr/local/share/ca-certificates/ca.crt
|
||||
# - ./.users_cache.json:/app/mcp-server/.users_cache.json
|
||||
env_file:
|
||||
- .env
|
||||
@@ -24,8 +24,8 @@ services:
|
||||
SLACK_MCP_PORT: "3001"
|
||||
# Uncomment if you use HTTP Toolkit with proxy on 127.0.0.1:8000
|
||||
# on your host, otherwise leave it commented.
|
||||
# extra_hosts:
|
||||
# - "host.docker.internal:host-gateway"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
networks:
|
||||
app-tier:
|
||||
|
||||
@@ -1,20 +1,70 @@
|
||||
module github.com/korotovsky/slack-mcp-server
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.23.5
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/bbalet/stopwords v1.0.0
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mark3labs/mcp-go v0.31.0
|
||||
github.com/rusq/slack v0.9.6-0.20250408103104-dd80d1b6337f
|
||||
github.com/rusq/slackauth v0.6.1
|
||||
github.com/rusq/slackdump/v3 v3.1.6
|
||||
github.com/rusq/tagops v0.1.1
|
||||
github.com/slack-go/slack v0.16.0
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/time v0.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.0 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.5 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.3.1 // indirect
|
||||
github.com/charmbracelet/huh v0.7.0 // indirect
|
||||
github.com/charmbracelet/huh/spinner v0.0.0-20250519092748-d6f1597485e0 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.9.2 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20250520193441-8304e91a28cb // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-rod/rod v0.116.2 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/playwright-community/playwright-go v0.5200.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/rusq/chttp v1.1.0 // indirect
|
||||
github.com/rusq/fsadapter v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
github.com/ysmood/fetchup v0.3.0 // indirect
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/got v0.40.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,9 +1,66 @@
|
||||
github.com/bbalet/stopwords v1.0.0 h1:0TnGycCtY0zZi4ltKoOGRFIlZHv0WqpoIGUsObjztfo=
|
||||
github.com/bbalet/stopwords v1.0.0/go.mod h1:sAWrQoDMfqARGIn4s6dp7OW7ISrshUD8IP2q3KoqPjc=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403 h1:EtZwYyLbkEcIt+B//6sujwRCnHuTEK3qiSypAX5aJeM=
|
||||
github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403/go.mod h1:mM6WvakkX2m+NgMiPCfFFjwfH4KzENC07zeGEqq9U7s=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
|
||||
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
|
||||
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
||||
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
|
||||
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
|
||||
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
|
||||
github.com/charmbracelet/huh/spinner v0.0.0-20250519092748-d6f1597485e0 h1:CiQY7CVtEigidVu1vzLxqdW3Tg2DB66R/2OaM3E2rbI=
|
||||
github.com/charmbracelet/huh/spinner v0.0.0-20250519092748-d6f1597485e0/go.mod h1:D/ml7UtSMq/cwoJiHJ78KFzGrx4m01ALekBSHImKiu4=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY=
|
||||
github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20250520193441-8304e91a28cb h1:JFEeU2KTS+W0dkZVbLeEgXI+PLBRZGomQKeWDpQD+V0=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20250520193441-8304e91a28cb/go.mod h1:Rgw3/F+xlcUc5XygUtimVSxAqCOsqyvJjqF5UHRvc5k=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
|
||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
|
||||
@@ -13,29 +70,143 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4=
|
||||
github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/playwright-community/playwright-go v0.5200.0 h1:z/5LGuX2tBrg3ug1HupMXLjIG93f1d2MWdDsNhkMQ9c=
|
||||
github.com/playwright-community/playwright-go v0.5200.0/go.mod h1:UnnyQZaqUOO5ywAZu60+N4EiWReUqX1MQBBA3Oofvf8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rusq/chttp v1.1.0 h1:lfUALJ51uRLgb4tc7joXFOgz9pzKBmc4vGq0UDu3dmk=
|
||||
github.com/rusq/chttp v1.1.0/go.mod h1:bmuoQMUFs9fmigUmT7xbp8s0rHyzUrf7+78yLklr1so=
|
||||
github.com/rusq/fsadapter v1.1.0 h1:/tuzrPNGr4Tx2f8fPK+WudSRBLDvjjDaqVvto1yrVdk=
|
||||
github.com/rusq/fsadapter v1.1.0/go.mod h1:aSH7MYrWvAGiFkz1qGPE8OknkplFfQSj66leC0eSqYg=
|
||||
github.com/rusq/slack v0.9.6-0.20250408103104-dd80d1b6337f h1:w4klfw1A3iZv5qWg1YHcRF2bJuRDV7aOpsF6sLLSs0A=
|
||||
github.com/rusq/slack v0.9.6-0.20250408103104-dd80d1b6337f/go.mod h1:gULX17QqyNX4BF001nHKlSe0uKYI+MAKiDQ7oi80BYI=
|
||||
github.com/rusq/slackauth v0.6.1 h1:s09G3WHSA1yz6H9dHT+Yo6DCZF34ClY31tQz849B++Q=
|
||||
github.com/rusq/slackauth v0.6.1/go.mod h1:wAtNCbeKH0pnaZnqJjG5RKY3e5BF9F2L/YTzhOjBIb0=
|
||||
github.com/rusq/slackdump/v3 v3.1.6 h1:t6hi49jSDWpiXqyna8OlEd2I2zkLBgi9XZGr+xDl5ik=
|
||||
github.com/rusq/slackdump/v3 v3.1.6/go.mod h1:c9AiEEkmLWIbQJuxDIK+K9H5g6kdfc06Eqk6DmLWWps=
|
||||
github.com/rusq/tagops v0.1.1 h1:R5MHPR822lSg3LFr0RS3DFS0CapRiqtuHVD5NlOMOvY=
|
||||
github.com/rusq/tagops v0.1.1/go.mod h1:mUJ5WoHxrSv9wreCrHQkAeMevt5aXFadlOdLM6UsoHc=
|
||||
github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
|
||||
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8=
|
||||
github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A=
|
||||
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
||||
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
||||
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
|
||||
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
|
||||
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
|
||||
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
|
||||
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
|
||||
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
|
||||
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
||||
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+76
-76
@@ -2,15 +2,14 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gocarina/gocsv"
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/provider"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/slack-go/slack"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
@@ -65,86 +64,29 @@ func (ch *ChannelsHandler) ChannelsHandler(ctx context.Context, request mcp.Call
|
||||
return nil, fmt.Errorf("limit must be less than 1000, got %d", limit)
|
||||
}
|
||||
|
||||
api, err := ch.apiProvider.Provide()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channelList []Channel
|
||||
|
||||
params := &slack.GetConversationsParameters{
|
||||
Types: channelTypes,
|
||||
Limit: limit,
|
||||
ExcludeArchived: true,
|
||||
Cursor: cursor,
|
||||
}
|
||||
var (
|
||||
total int
|
||||
nextcur string
|
||||
nextcur string
|
||||
channelList []Channel
|
||||
)
|
||||
for {
|
||||
var chans []slack.Channel
|
||||
|
||||
chans, nextcur, err = api.GetConversationsContext(ctx, params)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
channels := filterChannelsByTypes(ch.apiProvider.ProvideChannelsMaps().Channels, channelTypes)
|
||||
|
||||
usersMap := ch.apiProvider.ProvideUsersMap()
|
||||
var chans []provider.Channel
|
||||
|
||||
if l := len(chans); l > 0 {
|
||||
for _, channel := range chans {
|
||||
channelName := "#" + channel.Name
|
||||
purpose := channel.Purpose.Value
|
||||
numMembers := channel.NumMembers
|
||||
if channel.IsIM {
|
||||
numMembers = 2
|
||||
user, ok := usersMap[channel.User]
|
||||
if ok {
|
||||
channelName = "@" + user.Name
|
||||
purpose = "DM " + user.RealName
|
||||
} else {
|
||||
channelName = "@" + channel.User
|
||||
purpose = "DM with " + channel.User
|
||||
}
|
||||
} else if channel.IsMpIM && channel.IsPrivate && channel.NumMembers > 0 {
|
||||
numMembers = channel.NumMembers
|
||||
userNames := make([]string, 0, channel.NumMembers)
|
||||
for _, userID := range channel.Members {
|
||||
if user, ok := usersMap[userID]; ok {
|
||||
userNames = append(userNames, user.RealName)
|
||||
} else {
|
||||
userNames = append(userNames, userID)
|
||||
}
|
||||
}
|
||||
chans, nextcur = paginateChannels(
|
||||
channels,
|
||||
cursor,
|
||||
limit,
|
||||
)
|
||||
|
||||
channelName = "@" + channel.NameNormalized
|
||||
purpose = "Group DM with " + strings.Join(userNames, ", ")
|
||||
}
|
||||
|
||||
channelList = append(channelList, Channel{
|
||||
ID: channel.ID,
|
||||
Name: channelName,
|
||||
Topic: channel.Topic.Value,
|
||||
Purpose: purpose,
|
||||
MemberCount: numMembers,
|
||||
})
|
||||
}
|
||||
|
||||
total += l
|
||||
params.Limit -= l
|
||||
}
|
||||
|
||||
if total >= limit {
|
||||
log.Printf("channels fetch limit reached %v", total)
|
||||
break
|
||||
}
|
||||
|
||||
if nextcur == "" {
|
||||
log.Printf("channels fetch exhausted")
|
||||
break
|
||||
}
|
||||
params.Cursor = nextcur
|
||||
for _, channel := range chans {
|
||||
channelList = append(channelList, Channel{
|
||||
ID: channel.ID,
|
||||
Name: channel.Name,
|
||||
Topic: channel.Topic,
|
||||
Purpose: channel.Purpose,
|
||||
MemberCount: channel.MemberCount,
|
||||
})
|
||||
}
|
||||
|
||||
switch sortType {
|
||||
@@ -167,3 +109,61 @@ func (ch *ChannelsHandler) ChannelsHandler(ctx context.Context, request mcp.Call
|
||||
|
||||
return mcp.NewToolResultText(string(csvBytes)), nil
|
||||
}
|
||||
|
||||
func filterChannelsByTypes(channels map[string]provider.Channel, types []string) []provider.Channel {
|
||||
var result []provider.Channel
|
||||
typeSet := make(map[string]bool)
|
||||
|
||||
for _, t := range types {
|
||||
typeSet[t] = true
|
||||
}
|
||||
|
||||
for _, ch := range channels {
|
||||
if typeSet["public_channel"] && !ch.IsPrivate && !ch.IsIM && !ch.IsMpIM {
|
||||
result = append(result, ch)
|
||||
}
|
||||
if typeSet["private_channel"] && ch.IsPrivate && !ch.IsIM && !ch.IsMpIM {
|
||||
result = append(result, ch)
|
||||
}
|
||||
if typeSet["im"] && ch.IsIM {
|
||||
result = append(result, ch)
|
||||
}
|
||||
if typeSet["mpim"] && ch.IsMpIM {
|
||||
result = append(result, ch)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func paginateChannels(channels []provider.Channel, cursor string, limit int) ([]provider.Channel, string) {
|
||||
sort.Slice(channels, func(i, j int) bool {
|
||||
return channels[i].ID < channels[j].ID
|
||||
})
|
||||
|
||||
startIndex := 0
|
||||
if cursor != "" {
|
||||
if decoded, err := base64.StdEncoding.DecodeString(cursor); err == nil {
|
||||
lastID := string(decoded)
|
||||
for i, ch := range channels {
|
||||
if ch.ID > lastID {
|
||||
startIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endIndex := startIndex + limit
|
||||
if endIndex > len(channels) {
|
||||
endIndex = len(channels)
|
||||
}
|
||||
|
||||
paged := channels[startIndex:endIndex]
|
||||
|
||||
var nextCursor string
|
||||
if endIndex < len(channels) {
|
||||
nextCursor = base64.StdEncoding.EncodeToString([]byte(channels[endIndex-1].ID))
|
||||
}
|
||||
|
||||
return paged, nextCursor
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (ch *ConversationsHandler) ConversationsHistoryHandler(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api, err := ch.apiProvider.Provide()
|
||||
api, err := ch.apiProvider.ProvideGeneric()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func (ch *ConversationsHandler) ConversationsRepliesHandler(ctx context.Context,
|
||||
return nil, errors.New("thread_ts must be a string")
|
||||
}
|
||||
|
||||
api, err := ch.apiProvider.Provide()
|
||||
api, err := ch.apiProvider.ProvideGeneric()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package limiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type tier struct {
|
||||
// once every
|
||||
t time.Duration
|
||||
// burst
|
||||
b int
|
||||
}
|
||||
|
||||
func (t tier) Limiter() *rate.Limiter {
|
||||
return rate.NewLimiter(rate.Every(t.t), t.b)
|
||||
}
|
||||
|
||||
var (
|
||||
// tier1 = tier{t: 1 * time.Minute, b: 2}
|
||||
// tier2 = tier{t: 3 * time.Second, b: 3}
|
||||
Tier2boost = tier{t: 300 * time.Millisecond, b: 5}
|
||||
Tier3 = tier{t: 1200 * time.Millisecond, b: 4}
|
||||
// tier4 = tier{t: 60 * time.Millisecond, b: 5}
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
The "edge" package has been partially copied from `rusq/slackdump`.
|
||||
|
||||
Credits to the https://github.com/rusq.
|
||||
+277
-102
@@ -10,8 +10,13 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/limiter"
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/provider/edge"
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/transport"
|
||||
slack2 "github.com/rusq/slack"
|
||||
"github.com/rusq/slackdump/v3/auth"
|
||||
"github.com/slack-go/slack"
|
||||
)
|
||||
|
||||
@@ -20,27 +25,53 @@ var AllChanTypes = []string{"mpim", "im", "public_channel", "private_channel"}
|
||||
var PubChanType = "public_channel"
|
||||
|
||||
type ChannelsCache struct {
|
||||
Channels map[string]slack.Channel `json:"channels"`
|
||||
ChannelsInv map[string]string `json:"channels_inv"`
|
||||
Channels map[string]Channel `json:"channels"`
|
||||
ChannelsInv map[string]string `json:"channels_inv"`
|
||||
}
|
||||
|
||||
type ApiProvider struct {
|
||||
boot func() *slack.Client
|
||||
client *slack.Client
|
||||
boot func(ap *ApiProvider) *slack.Client
|
||||
|
||||
authProvider *auth.ValueAuth
|
||||
authResponse *slack2.AuthTestResponse
|
||||
|
||||
clientGeneric *slack.Client
|
||||
clientEnterprise *edge.Client
|
||||
|
||||
users map[string]slack.User
|
||||
usersCache string
|
||||
|
||||
channels map[string]slack.Channel
|
||||
channels map[string]Channel
|
||||
channelsInv map[string]string
|
||||
channelsCache string
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Topic string `json:"topic"`
|
||||
Purpose string `json:"purpose"`
|
||||
MemberCount int `json:"memberCount"`
|
||||
IsMpIM bool `json:"mpim"`
|
||||
IsIM bool `json:"im"`
|
||||
IsPrivate bool `json:"private"`
|
||||
}
|
||||
|
||||
func New() *ApiProvider {
|
||||
var (
|
||||
authProvider auth.ValueAuth
|
||||
err error
|
||||
)
|
||||
|
||||
// Check for XOXP token first (User OAuth)
|
||||
xoxpToken := os.Getenv("SLACK_MCP_XOXP_TOKEN")
|
||||
if xoxpToken != "" {
|
||||
return newWithXOXP(xoxpToken)
|
||||
authProvider, err = auth.NewValueAuth(xoxpToken, "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return newWithXOXP(authProvider)
|
||||
}
|
||||
|
||||
// Fall back to XOXC/XOXD tokens (session-based)
|
||||
@@ -51,10 +82,15 @@ func New() *ApiProvider {
|
||||
panic("Authentication required: Either SLACK_MCP_XOXP_TOKEN (User OAuth) or both SLACK_MCP_XOXC_TOKEN and SLACK_MCP_XOXD_TOKEN (session-based) environment variables must be provided")
|
||||
}
|
||||
|
||||
return newWithXOXC(xoxcToken, xoxdToken)
|
||||
authProvider, err = auth.NewValueAuth(xoxcToken, xoxdToken)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return newWithXOXC(authProvider)
|
||||
}
|
||||
|
||||
func newWithXOXP(token string) *ApiProvider {
|
||||
func newWithXOXP(authProvider auth.ValueAuth) *ApiProvider {
|
||||
usersCache := os.Getenv("SLACK_MCP_USERS_CACHE")
|
||||
if usersCache == "" {
|
||||
usersCache = ".users_cache.json"
|
||||
@@ -66,12 +102,21 @@ func newWithXOXP(token string) *ApiProvider {
|
||||
}
|
||||
|
||||
return &ApiProvider{
|
||||
boot: func() *slack.Client {
|
||||
api := slack.New(token)
|
||||
boot: func(ap *ApiProvider) *slack.Client {
|
||||
api := slack.New(authProvider.SlackToken())
|
||||
res, err := api.AuthTest()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
ap.authResponse = &slack2.AuthTestResponse{
|
||||
URL: res.URL,
|
||||
Team: res.Team,
|
||||
User: res.User,
|
||||
TeamID: res.TeamID,
|
||||
UserID: res.UserID,
|
||||
EnterpriseID: res.EnterpriseID,
|
||||
BotID: res.BotID,
|
||||
}
|
||||
log.Printf("Authenticated as: %s\n", res)
|
||||
}
|
||||
|
||||
@@ -81,13 +126,13 @@ func newWithXOXP(token string) *ApiProvider {
|
||||
users: make(map[string]slack.User),
|
||||
usersCache: usersCache,
|
||||
|
||||
channels: make(map[string]slack.Channel),
|
||||
channels: make(map[string]Channel),
|
||||
channelsInv: map[string]string{},
|
||||
channelsCache: channelsCache,
|
||||
}
|
||||
}
|
||||
|
||||
func newWithXOXC(token, cookie string) *ApiProvider {
|
||||
func newWithXOXC(authProvider auth.ValueAuth) *ApiProvider {
|
||||
usersCache := os.Getenv("SLACK_MCP_USERS_CACHE")
|
||||
if usersCache == "" {
|
||||
usersCache = ".users_cache.json"
|
||||
@@ -99,19 +144,29 @@ func newWithXOXC(token, cookie string) *ApiProvider {
|
||||
}
|
||||
|
||||
return &ApiProvider{
|
||||
boot: func() *slack.Client {
|
||||
api := slack.New(token,
|
||||
withHTTPClientOption(cookie),
|
||||
boot: func(ap *ApiProvider) *slack.Client {
|
||||
api := slack.New(authProvider.SlackToken(),
|
||||
withHTTPClientOption(authProvider.Cookies()),
|
||||
)
|
||||
res, err := api.AuthTest()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
ap.authProvider = &authProvider
|
||||
ap.authResponse = &slack2.AuthTestResponse{
|
||||
URL: res.URL,
|
||||
Team: res.Team,
|
||||
User: res.User,
|
||||
TeamID: res.TeamID,
|
||||
UserID: res.UserID,
|
||||
EnterpriseID: res.EnterpriseID,
|
||||
BotID: res.BotID,
|
||||
}
|
||||
log.Printf("Authenticated as: %s\n", res)
|
||||
}
|
||||
|
||||
api = slack.New(token,
|
||||
withHTTPClientOption(cookie),
|
||||
api = slack.New(authProvider.SlackToken(),
|
||||
withHTTPClientOption(authProvider.Cookies()),
|
||||
withTeamEndpointOption(res.URL),
|
||||
)
|
||||
|
||||
@@ -121,18 +176,28 @@ func newWithXOXC(token, cookie string) *ApiProvider {
|
||||
users: make(map[string]slack.User),
|
||||
usersCache: usersCache,
|
||||
|
||||
channels: make(map[string]slack.Channel),
|
||||
channels: make(map[string]Channel),
|
||||
channelsInv: map[string]string{},
|
||||
channelsCache: channelsCache,
|
||||
}
|
||||
}
|
||||
|
||||
func (ap *ApiProvider) Provide() (*slack.Client, error) {
|
||||
if ap.client == nil {
|
||||
ap.client = ap.boot()
|
||||
func (ap *ApiProvider) ProvideGeneric() (*slack.Client, error) {
|
||||
if ap.clientGeneric == nil {
|
||||
ap.clientGeneric = ap.boot(ap)
|
||||
}
|
||||
|
||||
return ap.client, nil
|
||||
return ap.clientGeneric, nil
|
||||
}
|
||||
|
||||
func (ap *ApiProvider) ProvideEnterprise() (*edge.Client, error) {
|
||||
if ap.clientEnterprise == nil {
|
||||
ap.clientEnterprise, _ = edge.NewWithInfo(ap.authResponse, ap.authProvider,
|
||||
withHTTPClientEdgeOption(ap.authProvider.Cookies()),
|
||||
)
|
||||
}
|
||||
|
||||
return ap.clientEnterprise, nil
|
||||
}
|
||||
|
||||
func (ap *ApiProvider) RefreshUsers(ctx context.Context) error {
|
||||
@@ -151,7 +216,7 @@ func (ap *ApiProvider) RefreshUsers(ctx context.Context) error {
|
||||
|
||||
optionLimit := slack.GetUsersOptionLimit(1000)
|
||||
|
||||
client, err := ap.Provide()
|
||||
client, err := ap.ProvideGeneric()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -183,14 +248,14 @@ func (ap *ApiProvider) RefreshUsers(ctx context.Context) error {
|
||||
|
||||
func (ap *ApiProvider) RefreshChannels(ctx context.Context) error {
|
||||
if data, err := ioutil.ReadFile(ap.channelsCache); err == nil {
|
||||
var cachedChannels []slack.Channel
|
||||
var cachedChannels []Channel
|
||||
if err := json.Unmarshal(data, &cachedChannels); err != nil {
|
||||
log.Printf("Failed to unmarshal %s: %v; will refetch", ap.usersCache, err)
|
||||
log.Printf("Failed to unmarshal %s: %v; will refetch", cachedChannels, err)
|
||||
} else {
|
||||
for _, c := range cachedChannels {
|
||||
ap.channels[c.ID] = c
|
||||
}
|
||||
log.Printf("Loaded %d channels from cache %q", len(cachedChannels), ap.usersCache)
|
||||
log.Printf("Loaded %d channels from cache %q", len(cachedChannels), ap.channelsCache)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -210,7 +275,7 @@ func (ap *ApiProvider) RefreshChannels(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *ApiProvider) GetChannels(ctx context.Context, channelTypes []string) []slack.Channel {
|
||||
func (ap *ApiProvider) GetChannels(ctx context.Context, channelTypes []string) []Channel {
|
||||
if len(channelTypes) == 0 {
|
||||
channelTypes = AllChanTypes
|
||||
}
|
||||
@@ -220,33 +285,82 @@ func (ap *ApiProvider) GetChannels(ctx context.Context, channelTypes []string) [
|
||||
Limit: 999,
|
||||
ExcludeArchived: true,
|
||||
}
|
||||
|
||||
var (
|
||||
total int
|
||||
chans1 []slack.Channel
|
||||
chans2 []slack2.Channel
|
||||
chans []Channel
|
||||
|
||||
nextcur string
|
||||
err error
|
||||
)
|
||||
|
||||
client, err := ap.Provide()
|
||||
clientGeneric, err := ap.ProvideGeneric()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
var chans []slack.Channel
|
||||
clientE, err := ap.ProvideEnterprise()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
chans, nextcur, err = client.GetConversationsContext(ctx, params)
|
||||
if err != nil {
|
||||
break
|
||||
lim := limiter.Tier2boost.Limiter()
|
||||
for {
|
||||
if ap.authResponse.EnterpriseID == "" {
|
||||
chans1, nextcur, err = clientGeneric.GetConversationsContext(ctx, params)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for _, channel := range chans1 {
|
||||
ch := mapChannel(
|
||||
channel.ID,
|
||||
channel.Name,
|
||||
channel.NameNormalized,
|
||||
channel.Topic.Value,
|
||||
channel.Purpose.Value,
|
||||
channel.User,
|
||||
channel.Members,
|
||||
channel.NumMembers,
|
||||
channel.IsIM,
|
||||
channel.IsMpIM,
|
||||
channel.IsPrivate,
|
||||
ap.ProvideUsersMap(),
|
||||
)
|
||||
chans = append(chans, ch)
|
||||
}
|
||||
if err := lim.Wait(ctx); err != nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
chans2, _, err = clientE.GetConversationsContext(ctx, nil)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for _, channel := range chans2 {
|
||||
ch := mapChannel(
|
||||
channel.ID,
|
||||
channel.Name,
|
||||
channel.NameNormalized,
|
||||
channel.Topic.Value,
|
||||
channel.Purpose.Value,
|
||||
channel.User,
|
||||
channel.Members,
|
||||
channel.NumMembers,
|
||||
channel.IsIM,
|
||||
channel.IsMpIM,
|
||||
channel.IsPrivate,
|
||||
ap.ProvideUsersMap(),
|
||||
)
|
||||
chans = append(chans, ch)
|
||||
}
|
||||
if err := lim.Wait(ctx); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if l := len(chans); l > 0 {
|
||||
for _, channel := range chans {
|
||||
ap.channels[channel.ID] = channel
|
||||
ap.channelsInv["#"+channel.Name] = channel.ID
|
||||
}
|
||||
|
||||
total += l
|
||||
params.Limit -= l
|
||||
for _, ch := range chans {
|
||||
ap.channels[ch.ID] = ch
|
||||
ap.channelsInv["#"+ch.Name] = ch.ID
|
||||
}
|
||||
|
||||
if nextcur == "" {
|
||||
@@ -257,7 +371,7 @@ func (ap *ApiProvider) GetChannels(ctx context.Context, channelTypes []string) [
|
||||
params.Cursor = nextcur
|
||||
}
|
||||
|
||||
var res []slack.Channel
|
||||
var res []Channel
|
||||
for _, t := range channelTypes {
|
||||
for _, channel := range ap.channels {
|
||||
if t == "public_channel" && !channel.IsPrivate {
|
||||
@@ -289,66 +403,15 @@ func (ap *ApiProvider) ProvideChannelsMaps() *ChannelsCache {
|
||||
}
|
||||
}
|
||||
|
||||
func withHTTPClientOption(cookie string) func(c *slack.Client) {
|
||||
func withHTTPClientOption(cookies []*http.Cookie) func(c *slack.Client) {
|
||||
return func(c *slack.Client) {
|
||||
var proxy func(*http.Request) (*url.URL, error)
|
||||
if proxyURL := os.Getenv("SLACK_MCP_PROXY"); proxyURL != "" {
|
||||
parsed, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse proxy URL: %v", err)
|
||||
}
|
||||
slack.OptionHTTPClient(provideHTTPClient(cookies))(c)
|
||||
}
|
||||
}
|
||||
|
||||
proxy = http.ProxyURL(parsed)
|
||||
} else {
|
||||
proxy = nil
|
||||
}
|
||||
|
||||
rootCAs, _ := x509.SystemCertPool()
|
||||
if rootCAs == nil {
|
||||
rootCAs = x509.NewCertPool()
|
||||
}
|
||||
|
||||
if localCertFile := os.Getenv("SLACK_MCP_SERVER_CA"); localCertFile != "" {
|
||||
certs, err := ioutil.ReadFile(localCertFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to append %q to RootCAs: %v", localCertFile, err)
|
||||
}
|
||||
|
||||
if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
|
||||
log.Println("No certs appended, using system certs only")
|
||||
}
|
||||
}
|
||||
|
||||
insecure := false
|
||||
if os.Getenv("SLACK_MCP_SERVER_CA_INSECURE") != "" {
|
||||
if localCertFile := os.Getenv("SLACK_MCP_SERVER_CA"); localCertFile != "" {
|
||||
log.Fatalf("Variable SLACK_MCP_SERVER_CA is at the same time with SLACK_MCP_SERVER_CA_INSECURE")
|
||||
}
|
||||
insecure = true
|
||||
}
|
||||
|
||||
customHTTPTransport := &http.Transport{
|
||||
Proxy: proxy,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
RootCAs: rootCAs,
|
||||
},
|
||||
}
|
||||
|
||||
userAgent := defaultUA
|
||||
if os.Getenv("SLACK_MCP_USER_AGENT") != "" {
|
||||
userAgent = os.Getenv("SLACK_MCP_USER_AGENT")
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport.New(
|
||||
customHTTPTransport,
|
||||
userAgent,
|
||||
cookie,
|
||||
),
|
||||
}
|
||||
|
||||
slack.OptionHTTPClient(client)(c)
|
||||
func withHTTPClientEdgeOption(cookies []*http.Cookie) func(c *edge.Client) {
|
||||
return func(c *edge.Client) {
|
||||
edge.OptionHTTPClient(provideHTTPClient(cookies))(c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,3 +420,115 @@ func withTeamEndpointOption(url string) slack.Option {
|
||||
slack.OptionAPIURL(url + "api/")(c)
|
||||
}
|
||||
}
|
||||
|
||||
func provideHTTPClient(cookies []*http.Cookie) *http.Client {
|
||||
var proxy func(*http.Request) (*url.URL, error)
|
||||
if proxyURL := os.Getenv("SLACK_MCP_PROXY"); proxyURL != "" {
|
||||
parsed, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse proxy URL: %v", err)
|
||||
}
|
||||
|
||||
proxy = http.ProxyURL(parsed)
|
||||
} else {
|
||||
proxy = nil
|
||||
}
|
||||
|
||||
rootCAs, _ := x509.SystemCertPool()
|
||||
if rootCAs == nil {
|
||||
rootCAs = x509.NewCertPool()
|
||||
}
|
||||
|
||||
if localCertFile := os.Getenv("SLACK_MCP_SERVER_CA"); localCertFile != "" {
|
||||
certs, err := ioutil.ReadFile(localCertFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to append %q to RootCAs: %v", localCertFile, err)
|
||||
}
|
||||
|
||||
if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
|
||||
log.Println("No certs appended, using system certs only")
|
||||
}
|
||||
}
|
||||
|
||||
insecure := false
|
||||
if os.Getenv("SLACK_MCP_SERVER_CA_INSECURE") != "" {
|
||||
if localCertFile := os.Getenv("SLACK_MCP_SERVER_CA"); localCertFile != "" {
|
||||
log.Fatalf("Variable SLACK_MCP_SERVER_CA is at the same time with SLACK_MCP_SERVER_CA_INSECURE")
|
||||
}
|
||||
insecure = true
|
||||
}
|
||||
|
||||
customHTTPTransport := &http.Transport{
|
||||
Proxy: proxy,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
RootCAs: rootCAs,
|
||||
},
|
||||
}
|
||||
|
||||
userAgent := defaultUA
|
||||
if os.Getenv("SLACK_MCP_USER_AGENT") != "" {
|
||||
userAgent = os.Getenv("SLACK_MCP_USER_AGENT")
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport.New(
|
||||
customHTTPTransport,
|
||||
userAgent,
|
||||
cookies,
|
||||
),
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func mapChannel(
|
||||
id, name, nameNormalized, topic, purpose, user string,
|
||||
members []string,
|
||||
numMembers int,
|
||||
isIM, isMpIM, isPrivate bool,
|
||||
usersMap map[string]slack.User,
|
||||
) Channel {
|
||||
channelName := "#" + name
|
||||
finalPurpose := purpose
|
||||
finalTopic := topic
|
||||
finalMemberCount := numMembers
|
||||
|
||||
if isIM {
|
||||
finalMemberCount = 2
|
||||
if u, ok := usersMap[user]; ok {
|
||||
channelName = "@" + u.Name
|
||||
finalPurpose = "DM with " + u.RealName
|
||||
} else {
|
||||
channelName = "@" + user
|
||||
finalPurpose = "DM with " + user
|
||||
}
|
||||
finalTopic = ""
|
||||
} else if isMpIM {
|
||||
if len(members) > 0 {
|
||||
finalMemberCount = len(members)
|
||||
var userNames []string
|
||||
for _, uid := range members {
|
||||
if u, ok := usersMap[uid]; ok {
|
||||
userNames = append(userNames, u.RealName)
|
||||
} else {
|
||||
userNames = append(userNames, uid)
|
||||
}
|
||||
}
|
||||
channelName = "@" + nameNormalized
|
||||
finalPurpose = "Group DM with " + strings.Join(userNames, ", ")
|
||||
finalTopic = ""
|
||||
}
|
||||
}
|
||||
|
||||
return Channel{
|
||||
ID: id,
|
||||
Name: channelName,
|
||||
Topic: finalTopic,
|
||||
Purpose: finalPurpose,
|
||||
MemberCount: finalMemberCount,
|
||||
IsIM: isIM,
|
||||
IsMpIM: isMpIM,
|
||||
IsPrivate: isPrivate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime/trace"
|
||||
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/limiter"
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/provider/edge/fasttime"
|
||||
"github.com/rusq/slack"
|
||||
)
|
||||
|
||||
// client.* API
|
||||
|
||||
type clientCountsForm struct {
|
||||
BaseRequest
|
||||
ThreadCountsByChannel bool `json:"thread_counts_by_channel"`
|
||||
OrgWideAware bool `json:"org_wide_aware"`
|
||||
IncludeFileChannels bool `json:"include_file_channels"`
|
||||
WebClientFields
|
||||
}
|
||||
|
||||
type ClientCountsResponse struct {
|
||||
baseResponse
|
||||
Channels []ChannelSnapshot `json:"channels,omitempty"`
|
||||
MPIMs []ChannelSnapshot `json:"mpims,omitempty"`
|
||||
IMs []ChannelSnapshot `json:"ims,omitempty"`
|
||||
}
|
||||
|
||||
type ChannelSnapshot struct {
|
||||
ID string `json:"id"`
|
||||
LastRead fasttime.Time `json:"last_read"`
|
||||
Latest fasttime.Time `json:"latest"`
|
||||
HistoryInvalid fasttime.Time `json:"history_invalid"`
|
||||
MentionCount int `json:"mention_count"`
|
||||
HasUnreads bool `json:"has_unreads"`
|
||||
}
|
||||
|
||||
func (cl *Client) ClientCounts(ctx context.Context) (ClientCountsResponse, error) {
|
||||
ctx, task := trace.NewTask(ctx, "ClientCounts")
|
||||
defer task.End()
|
||||
|
||||
form := clientCountsForm{
|
||||
BaseRequest: BaseRequest{Token: cl.token},
|
||||
ThreadCountsByChannel: true,
|
||||
OrgWideAware: true,
|
||||
IncludeFileChannels: true,
|
||||
WebClientFields: webclientReason("client-counts-api/fetchClientCounts"),
|
||||
}
|
||||
|
||||
resp, err := cl.PostForm(ctx, "client.counts", values(form, true))
|
||||
if err != nil {
|
||||
return ClientCountsResponse{}, err
|
||||
}
|
||||
r := ClientCountsResponse{}
|
||||
if err := cl.ParseResponse(&r, resp); err != nil {
|
||||
return ClientCountsResponse{}, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type clientDMsForm struct {
|
||||
BaseRequest
|
||||
Count int `json:"count"`
|
||||
IncludeClosed bool `json:"include_closed"`
|
||||
IncludeChannel bool `json:"include_channel"`
|
||||
ExcludeBots bool `json:"exclude_bots"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
WebClientFields
|
||||
}
|
||||
|
||||
type clientDMsResponse struct {
|
||||
baseResponse
|
||||
IMs []ClientDM `json:"ims,omitempty"`
|
||||
MPIMs []ClientDM `json:"mpims,omitempty"` //TODO
|
||||
}
|
||||
|
||||
type ClientDM struct {
|
||||
ID string `json:"id"`
|
||||
// Message slack.Message `json:"message,omitempty"`
|
||||
Channel IM `json:"channel,omitempty"`
|
||||
Latest fasttime.Time `json:"latest,omitempty"` // i.e. "1710632873.037269"
|
||||
}
|
||||
|
||||
type IM struct {
|
||||
ID string `json:"id"`
|
||||
Created slack.JSONTime `json:"created"`
|
||||
IsFrozen bool `json:"is_frozen"`
|
||||
IsArchived bool `json:"is_archived"`
|
||||
IsIM bool `json:"is_im"`
|
||||
IsOrgShared bool `json:"is_org_shared"`
|
||||
ContextTeamID string `json:"context_team_id"`
|
||||
Updated slack.JSONTime `json:"updated"`
|
||||
User string `json:"user"`
|
||||
LastRead fasttime.Time `json:"last_read"`
|
||||
Latest fasttime.Time `json:"latest"`
|
||||
IsOpen bool `json:"is_open"`
|
||||
}
|
||||
|
||||
func (c IM) SlackChannel() slack.Channel {
|
||||
return slack.Channel{
|
||||
GroupConversation: slack.GroupConversation{
|
||||
Conversation: slack.Conversation{
|
||||
ID: c.ID,
|
||||
Created: c.Created,
|
||||
IsIM: c.IsIM,
|
||||
IsOrgShared: c.IsOrgShared,
|
||||
User: c.User,
|
||||
LastRead: c.LastRead.SlackString(),
|
||||
},
|
||||
IsArchived: c.IsArchived,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (cl *Client) ClientDMs(ctx context.Context) ([]ClientDM, error) {
|
||||
form := clientDMsForm{
|
||||
BaseRequest: BaseRequest{Token: cl.token},
|
||||
Count: 250,
|
||||
IncludeClosed: true,
|
||||
IncludeChannel: true,
|
||||
ExcludeBots: false,
|
||||
Cursor: "",
|
||||
WebClientFields: webclientReason("dms-tab-populate"),
|
||||
}
|
||||
lim := limiter.Tier2boost.Limiter()
|
||||
var IMs []ClientDM
|
||||
for {
|
||||
resp, err := cl.PostFormRaw(ctx, cl.webapiURL("client.dms"), values(form, true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := clientDMsResponse{}
|
||||
if err := cl.ParseResponse(&r, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
IMs = append(IMs, r.IMs...)
|
||||
if r.ResponseMetadata.NextCursor == "" {
|
||||
break
|
||||
}
|
||||
form.Cursor = r.ResponseMetadata.NextCursor
|
||||
if err := lim.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return IMs, nil
|
||||
}
|
||||
@@ -0,0 +1,611 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"runtime/trace"
|
||||
"time"
|
||||
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/provider/edge/fasttime"
|
||||
"github.com/rusq/slack"
|
||||
)
|
||||
|
||||
// client.userBoot API. It was too large to chuck into the client.* file.
|
||||
|
||||
type clientUserBootForm struct {
|
||||
BaseRequest
|
||||
MinChannelUpdated int64 `json:"min_channel_updated"`
|
||||
IncludeMinVersionBumpCheck int `json:"include_min_version_bump_check"`
|
||||
VersionTS int64 `json:"version_ts"`
|
||||
BuildVersionTS int64 `json:"build_version_ts"`
|
||||
WebClientFields
|
||||
}
|
||||
|
||||
// ClientUserBoot calls the client.userBoot API.
|
||||
func (cl *Client) ClientUserBoot(ctx context.Context) (*ClientUserBootResponse, error) {
|
||||
ctx, task := trace.NewTask(ctx, "ClientUserBoot")
|
||||
defer task.End()
|
||||
|
||||
future := time.Now().Add(24 * time.Hour)
|
||||
form := clientUserBootForm{
|
||||
BaseRequest: BaseRequest{Token: cl.token},
|
||||
IncludeMinVersionBumpCheck: 1,
|
||||
VersionTS: future.Unix(),
|
||||
BuildVersionTS: future.Unix(),
|
||||
WebClientFields: webclientReason("initial-data"),
|
||||
}
|
||||
var ub ClientUserBootResponse
|
||||
resp, err := cl.PostForm(ctx, "client.userBoot", values(form, true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cl.ParseResponse(&ub, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ub, nil
|
||||
}
|
||||
|
||||
func UnmarshalClientUserBootResponse(data []byte) (ClientUserBootResponse, error) {
|
||||
var r ClientUserBootResponse
|
||||
err := json.Unmarshal(data, &r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
func (r *ClientUserBootResponse) Marshal() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
// "client.userBoot"
|
||||
type ClientUserBootResponse struct {
|
||||
baseResponse
|
||||
Self Self `json:"self"`
|
||||
Team Team `json:"team"`
|
||||
IMs []IM `json:"ims"`
|
||||
Workspaces []Workspace `json:"workspaces"`
|
||||
DefaultWorkspace string `json:"default_workspace"`
|
||||
AccountTypes AccountTypes `json:"account_types"`
|
||||
AcceptTosURL any `json:"accept_tos_url"`
|
||||
IsOpen []string `json:"is_open"`
|
||||
IsEurope bool `json:"is_europe"`
|
||||
TranslationsCacheTs fasttime.Time `json:"translations_cache_ts"`
|
||||
EmojiCacheTs fasttime.Time `json:"emoji_cache_ts"`
|
||||
AppCommandsCacheTs fasttime.Time `json:"app_commands_cache_ts"`
|
||||
CacheTsVersion string `json:"cache_ts_version"`
|
||||
DND DND `json:"dnd"`
|
||||
Prefs map[string]any `json:"prefs"`
|
||||
Subteams Subteams `json:"subteams"`
|
||||
MobileAppRequiresUpgrade bool `json:"mobile_app_requires_upgrade"`
|
||||
Starred []any `json:"starred"`
|
||||
ChannelsPriority ChannelsPriority `json:"channels_priority"`
|
||||
ReadOnlyChannels []string `json:"read_only_channels"`
|
||||
NonThreadableChannels []any `json:"non_threadable_channels"`
|
||||
ThreadOnlyChannels []any `json:"thread_only_channels"`
|
||||
Channels []UserBootChannel `json:"channels"`
|
||||
UnchangedChannelIDS []any `json:"unchanged_channel_ids"`
|
||||
CacheVersion string `json:"cache_version"`
|
||||
SlackRoute string `json:"slack_route"`
|
||||
AuthMinLastFetched int64 `json:"auth_min_last_fetched"`
|
||||
CanAccessClientV2 bool `json:"can_access_client_v2"`
|
||||
ShouldReload bool `json:"should_reload"`
|
||||
ClientMinVersion int64 `json:"client_min_version"`
|
||||
BuildVersionEnabled bool `json:"build_version_enabled"`
|
||||
Links Links `json:"links"`
|
||||
}
|
||||
|
||||
type UserBootChannel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsChannel bool `json:"is_channel"`
|
||||
IsGroup bool `json:"is_group"`
|
||||
IsIM bool `json:"is_im"`
|
||||
IsMpim bool `json:"is_mpim"`
|
||||
IsPrivate bool `json:"is_private"`
|
||||
Created int64 `json:"created"`
|
||||
IsArchived bool `json:"is_archived"`
|
||||
IsGeneral bool `json:"is_general"`
|
||||
Unlinked int64 `json:"unlinked"`
|
||||
NameNormalized string `json:"name_normalized"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
IsFrozen bool `json:"is_frozen"`
|
||||
IsOrgShared bool `json:"is_org_shared"`
|
||||
IsPendingEXTShared bool `json:"is_pending_ext_shared"`
|
||||
PendingShared []json.RawMessage `json:"pending_shared"`
|
||||
ContextTeamID string `json:"context_team_id"`
|
||||
Updated int64 `json:"updated"`
|
||||
ParentConversation json.RawMessage `json:"parent_conversation"`
|
||||
Creator string `json:"creator"`
|
||||
IsEXTShared bool `json:"is_ext_shared"`
|
||||
SharedTeamIDS []string `json:"shared_team_ids"`
|
||||
PendingConnectedTeamIDS []json.RawMessage `json:"pending_connected_team_ids"`
|
||||
Topic Purpose `json:"topic"`
|
||||
Purpose Purpose `json:"purpose"`
|
||||
Properties *Properties `json:"properties,omitempty"`
|
||||
PreviousNames []json.RawMessage `json:"previous_names"`
|
||||
IsMember bool `json:"is_member,omitempty"`
|
||||
LastRead fasttime.Time `json:"last_read,omitempty"`
|
||||
Latest fasttime.Time `json:"latest,omitempty"`
|
||||
IsOpen bool `json:"is_open,omitempty"`
|
||||
Members []string `json:"members"`
|
||||
}
|
||||
|
||||
func (c *UserBootChannel) SlackChannel() slack.Channel {
|
||||
return slack.Channel{
|
||||
GroupConversation: slack.GroupConversation{
|
||||
Conversation: slack.Conversation{
|
||||
ID: c.ID,
|
||||
Created: slack.JSONTime(c.Created),
|
||||
IsOpen: c.IsOpen,
|
||||
LastRead: c.LastRead.SlackString(),
|
||||
Latest: &slack.Message{},
|
||||
UnreadCount: 0,
|
||||
UnreadCountDisplay: 0,
|
||||
IsGroup: c.IsGroup,
|
||||
IsShared: c.IsShared,
|
||||
IsIM: c.IsIM,
|
||||
IsExtShared: c.IsEXTShared,
|
||||
IsOrgShared: c.IsOrgShared,
|
||||
IsGlobalShared: false,
|
||||
IsPendingExtShared: c.IsPendingEXTShared,
|
||||
IsPrivate: c.IsPrivate,
|
||||
IsMpIM: c.IsMpim,
|
||||
Unlinked: int(c.Unlinked),
|
||||
NameNormalized: c.NameNormalized,
|
||||
NumMembers: len(c.Members),
|
||||
Priority: 0,
|
||||
User: "",
|
||||
ConnectedTeamIDs: []string{},
|
||||
SharedTeamIDs: []string{},
|
||||
InternalTeamIDs: []string{},
|
||||
},
|
||||
Name: c.Name,
|
||||
Creator: c.Creator,
|
||||
IsArchived: c.IsArchived,
|
||||
Members: c.Members,
|
||||
Topic: slack.Topic{
|
||||
Value: c.Topic.Value,
|
||||
Creator: c.Topic.Creator,
|
||||
LastSet: slack.JSONTime(c.Topic.LastSet),
|
||||
},
|
||||
Purpose: slack.Purpose{
|
||||
Value: c.Purpose.Value,
|
||||
Creator: c.Purpose.Creator,
|
||||
LastSet: slack.JSONTime(c.Purpose.LastSet),
|
||||
},
|
||||
},
|
||||
IsChannel: c.IsChannel,
|
||||
IsGeneral: c.IsGeneral,
|
||||
IsMember: c.IsMember,
|
||||
Locale: "",
|
||||
}
|
||||
}
|
||||
|
||||
type AccountTypes struct {
|
||||
IsAdmin []any `json:"is_admin"`
|
||||
IsOwner []any `json:"is_owner"`
|
||||
IsPrimaryOwner []any `json:"is_primary_owner"`
|
||||
}
|
||||
|
||||
type Properties struct {
|
||||
PostingRestrictedTo SlackConnectAllowedWorkspaces `json:"posting_restricted_to"`
|
||||
}
|
||||
|
||||
type SlackConnectAllowedWorkspaces struct {
|
||||
Type []string `json:"type"`
|
||||
}
|
||||
|
||||
type Purpose struct {
|
||||
Value string `json:"value"`
|
||||
Creator string `json:"creator"`
|
||||
LastSet int64 `json:"last_set"`
|
||||
}
|
||||
|
||||
type ChannelsPriority struct {
|
||||
}
|
||||
|
||||
type DND struct {
|
||||
DNDEnabled bool `json:"dnd_enabled"`
|
||||
NextDNDStartTs slack.JSONTime `json:"next_dnd_start_ts"`
|
||||
NextDNDEndTs slack.JSONTime `json:"next_dnd_end_ts"`
|
||||
SnoozeEnabled bool `json:"snooze_enabled"`
|
||||
}
|
||||
|
||||
type Links struct {
|
||||
DomainsTs int64 `json:"domains_ts"`
|
||||
}
|
||||
|
||||
type Self struct {
|
||||
ID string `json:"id"`
|
||||
TeamID string `json:"team_id"`
|
||||
Name string `json:"name"`
|
||||
Deleted bool `json:"deleted"`
|
||||
Color string `json:"color"`
|
||||
RealName string `json:"real_name"`
|
||||
Tz string `json:"tz"`
|
||||
TzLabel string `json:"tz_label"`
|
||||
TzOffset int64 `json:"tz_offset"`
|
||||
Profile Profile `json:"profile"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
IsOwner bool `json:"is_owner"`
|
||||
IsPrimaryOwner bool `json:"is_primary_owner"`
|
||||
IsRestricted bool `json:"is_restricted"`
|
||||
IsUltraRestricted bool `json:"is_ultra_restricted"`
|
||||
IsBot bool `json:"is_bot"`
|
||||
IsAppUser bool `json:"is_app_user"`
|
||||
Updated slack.JSONTime `json:"updated"`
|
||||
IsEmailConfirmed bool `json:"is_email_confirmed"`
|
||||
WhoCanShareContactCard string `json:"who_can_share_contact_card"`
|
||||
FirstLogin slack.JSONTime `json:"first_login"`
|
||||
LobSalesHomeEnabled bool `json:"lob_sales_home_enabled"`
|
||||
ManualPresence string `json:"manual_presence"`
|
||||
}
|
||||
|
||||
type Profile1 struct {
|
||||
Title string `json:"title"`
|
||||
Phone string `json:"phone"`
|
||||
Skype string `json:"skype"`
|
||||
RealName string `json:"real_name"`
|
||||
RealNameNormalized string `json:"real_name_normalized"`
|
||||
DisplayName string `json:"display_name"`
|
||||
DisplayNameNormalized string `json:"display_name_normalized"`
|
||||
Fields any `json:"fields"`
|
||||
StatusText string `json:"status_text"`
|
||||
StatusEmoji string `json:"status_emoji"`
|
||||
StatusEmojiDisplayInfo []any `json:"status_emoji_display_info"`
|
||||
StatusExpiration int64 `json:"status_expiration"`
|
||||
AvatarHash string `json:"avatar_hash"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
StatusTextCanonical string `json:"status_text_canonical"`
|
||||
Team string `json:"team"`
|
||||
}
|
||||
|
||||
type Subteams struct {
|
||||
Self []any `json:"self"`
|
||||
}
|
||||
|
||||
type Team struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Domain string `json:"domain"`
|
||||
EmailDomain string `json:"email_domain"`
|
||||
Icon Icon `json:"icon"`
|
||||
AvatarBaseURL string `json:"avatar_base_url"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
Plan string `json:"plan"`
|
||||
IsPlanFrozen bool `json:"is_plan_frozen"`
|
||||
Prefs Prefs `json:"prefs"`
|
||||
OnboardingChannelID string `json:"onboarding_channel_id"`
|
||||
ImageProxyURL string `json:"image_proxy_url"`
|
||||
OverStorageLimit bool `json:"over_storage_limit"`
|
||||
MessagesCount int64 `json:"messages_count"`
|
||||
LobSalesHomeEnabled bool `json:"lob_sales_home_enabled"`
|
||||
}
|
||||
|
||||
type Icon struct {
|
||||
ImageDefault bool `json:"image_default"`
|
||||
Image34 string `json:"image_34"`
|
||||
Image44 string `json:"image_44"`
|
||||
Image68 string `json:"image_68"`
|
||||
Image88 string `json:"image_88"`
|
||||
Image102 string `json:"image_102"`
|
||||
Image230 string `json:"image_230"`
|
||||
Image132 string `json:"image_132"`
|
||||
}
|
||||
|
||||
type Prefs struct {
|
||||
Locale string `json:"locale"`
|
||||
InvitesOnlyAdmins bool `json:"invites_only_admins"`
|
||||
InvitesLimit bool `json:"invites_limit"`
|
||||
ShowJoinLeave bool `json:"show_join_leave"`
|
||||
DefaultChannels []string `json:"default_channels"`
|
||||
Image34 string `json:"image_34"`
|
||||
Image44 string `json:"image_44"`
|
||||
Image68 string `json:"image_68"`
|
||||
Image88 string `json:"image_88"`
|
||||
Image102 string `json:"image_102"`
|
||||
Image132 string `json:"image_132"`
|
||||
Image230 string `json:"image_230"`
|
||||
ImageOriginal string `json:"image_original"`
|
||||
SeenInvitesOnlyAdminsWarning bool `json:"seen_invites_only_admins_warning"`
|
||||
WhoCanAtEveryone string `json:"who_can_at_everyone"`
|
||||
WhoCanAtChannel string `json:"who_can_at_channel"`
|
||||
WhoCanPostGeneral string `json:"who_can_post_general"`
|
||||
WarnBeforeAtChannel string `json:"warn_before_at_channel"`
|
||||
WhoCanCreateChannels string `json:"who_can_create_channels"`
|
||||
WhoCanArchiveChannels string `json:"who_can_archive_channels"`
|
||||
WhoCanCreateGroups string `json:"who_can_create_groups"`
|
||||
WhoCanKickChannels string `json:"who_can_kick_channels"`
|
||||
WhoCanKickGroups string `json:"who_can_kick_groups"`
|
||||
InvitedUserPreset InvitedUserPreset `json:"invited_user_preset"`
|
||||
WelcomePlaceEnabled bool `json:"welcome_place_enabled"`
|
||||
HasInstalledApps bool `json:"has_installed_apps"`
|
||||
WhoCanManageChannelPostingPrefs string `json:"who_can_manage_channel_posting_prefs"`
|
||||
AdminCustomizedQuickReactions []string `json:"admin_customized_quick_reactions"`
|
||||
AllUsersCanPurchase bool `json:"all_users_can_purchase"`
|
||||
AllowAdminRetentionOverride int64 `json:"allow_admin_retention_override"`
|
||||
AllowAudioClipSharingSlackConnect bool `json:"allow_audio_clip_sharing_slack_connect"`
|
||||
AllowAudioClips bool `json:"allow_audio_clips"`
|
||||
AllowBoxCfs bool `json:"allow_box_cfs"`
|
||||
AllowCalls bool `json:"allow_calls"`
|
||||
AllowCallsInteractiveScreenSharing bool `json:"allow_calls_interactive_screen_sharing"`
|
||||
AllowClipDownloads string `json:"allow_clip_downloads"`
|
||||
AllowContentReview bool `json:"allow_content_review"`
|
||||
AllowDeveloperSandboxes string `json:"allow_developer_sandboxes"`
|
||||
AllowFreeAutomatedTrials bool `json:"allow_free_automated_trials"`
|
||||
AllowHuddles bool `json:"allow_huddles"`
|
||||
AllowHuddlesTranscriptions bool `json:"allow_huddles_transcriptions"`
|
||||
AllowHuddlesVideo bool `json:"allow_huddles_video"`
|
||||
AllowLists string `json:"allow_lists"`
|
||||
AllowLockThread bool `json:"allow_lock_thread"`
|
||||
AllowMediaTranscriptions bool `json:"allow_media_transcriptions"`
|
||||
AllowMessageDeletion bool `json:"allow_message_deletion"`
|
||||
AllowNativeGIFPicker bool `json:"allow_native_gif_picker"`
|
||||
AllowRetentionOverride bool `json:"allow_retention_override"`
|
||||
AllowSpaceship string `json:"allow_spaceship"`
|
||||
AllowSponsoredSlackConnections bool `json:"allow_sponsored_slack_connections"`
|
||||
AllowVideoClipSharingSlackConnect bool `json:"allow_video_clip_sharing_slack_connect"`
|
||||
AllowVideoClips bool `json:"allow_video_clips"`
|
||||
AppDirOnly bool `json:"app_dir_only"`
|
||||
AppManagementApps []any `json:"app_management_apps"`
|
||||
AppWhitelistEnabled bool `json:"app_whitelist_enabled"`
|
||||
AppWhitelistRequestsRequireCommentEnabled bool `json:"app_whitelist_requests_require_comment_enabled"`
|
||||
AtlasOrgChartsAccess string `json:"atlas_org_charts_access"`
|
||||
AtlasProfilesAccess string `json:"atlas_profiles_access"`
|
||||
AutomaticWelcomeDmEnabled bool `json:"automatic_welcome_dm_enabled"`
|
||||
BillingEmailDaily bool `json:"billing_email_daily"`
|
||||
BillingEmailMonthly bool `json:"billing_email_monthly"`
|
||||
BlockFileDownload bool `json:"block_file_download"`
|
||||
BlockFileTypes bool `json:"block_file_types"`
|
||||
BoxAppInstalled bool `json:"box_app_installed"`
|
||||
CallsApps CallsApps `json:"calls_apps"`
|
||||
CallsLocations []any `json:"calls_locations"`
|
||||
CanAcceptSlackConnectChannelInvites bool `json:"can_accept_slack_connect_channel_invites"`
|
||||
CanCreateExternalLimitedInvite bool `json:"can_create_external_limited_invite"`
|
||||
CanCreateSlackConnectChannelInvite bool `json:"can_create_slack_connect_channel_invite"`
|
||||
CanReceiveSharedChannelsInvites bool `json:"can_receive_shared_channels_invites"`
|
||||
CanvasRetentionDuration int64 `json:"canvas_retention_duration"`
|
||||
CanvasRetentionType int64 `json:"canvas_retention_type"`
|
||||
CanvasVersionHistoryEnabled bool `json:"canvas_version_history_enabled"`
|
||||
ChannelAuditExportEnabled bool `json:"channel_audit_export_enabled"`
|
||||
ChannelEmailAddressesEnabled bool `json:"channel_email_addresses_enabled"`
|
||||
ComplianceExportStart int64 `json:"compliance_export_start"`
|
||||
ContentReviewEnabled bool `json:"content_review_enabled"`
|
||||
CreatedWithGoogle bool `json:"created_with_google"`
|
||||
CustomContactEmail any `json:"custom_contact_email"`
|
||||
CustomStatusDefaultEmoji string `json:"custom_status_default_emoji"`
|
||||
CustomStatusPresets [][]string `json:"custom_status_presets"`
|
||||
DailyPromptsEnabled bool `json:"daily_prompts_enabled"`
|
||||
DefaultChannelCreationEnabled bool `json:"default_channel_creation_enabled"`
|
||||
DefaultCreatePrivateChannel bool `json:"default_create_private_channel"`
|
||||
DefaultFunctionReuseVisibility DefaultFunctionReuseVisibility `json:"default_function_reuse_visibility"`
|
||||
DefaultRxns []string `json:"default_rxns"`
|
||||
DisableEmailIngestion bool `json:"disable_email_ingestion"`
|
||||
DisableFileDeleting bool `json:"disable_file_deleting"`
|
||||
DisableFileEditing bool `json:"disable_file_editing"`
|
||||
DisableFileUploads string `json:"disable_file_uploads"`
|
||||
DisablePrivacyAndCookiePolicy bool `json:"disable_privacy_and_cookie_policy"`
|
||||
DisableSidebarConnectPrompts []any `json:"disable_sidebar_connect_prompts"`
|
||||
DisableSidebarInstallPrompts []any `json:"disable_sidebar_install_prompts"`
|
||||
DisallowPublicFileUrls bool `json:"disallow_public_file_urls"`
|
||||
Discoverable string `json:"discoverable"`
|
||||
DisplayAnniversaryCelebration bool `json:"display_anniversary_celebration"`
|
||||
DisplayDefaultPhone bool `json:"display_default_phone"`
|
||||
DisplayEmailAddresses bool `json:"display_email_addresses"`
|
||||
DisplayExternalEmailAddresses bool `json:"display_external_email_addresses"`
|
||||
DisplayNewHireCelebration bool `json:"display_new_hire_celebration"`
|
||||
DisplayPronouns bool `json:"display_pronouns"`
|
||||
DisplayRealNames bool `json:"display_real_names"`
|
||||
DmRetentionDuration int64 `json:"dm_retention_duration"`
|
||||
DmRetentionRedactionDuration int64 `json:"dm_retention_redaction_duration"`
|
||||
DmRetentionType int64 `json:"dm_retention_type"`
|
||||
DNDAfterFriday string `json:"dnd_after_friday"`
|
||||
DNDAfterMonday string `json:"dnd_after_monday"`
|
||||
DNDAfterSaturday string `json:"dnd_after_saturday"`
|
||||
DNDAfterSunday string `json:"dnd_after_sunday"`
|
||||
DNDAfterThursday string `json:"dnd_after_thursday"`
|
||||
DNDAfterTuesday string `json:"dnd_after_tuesday"`
|
||||
DNDAfterWednesday string `json:"dnd_after_wednesday"`
|
||||
DNDBeforeFriday string `json:"dnd_before_friday"`
|
||||
DNDBeforeMonday string `json:"dnd_before_monday"`
|
||||
DNDBeforeSaturday string `json:"dnd_before_saturday"`
|
||||
DNDBeforeSunday string `json:"dnd_before_sunday"`
|
||||
DNDBeforeThursday string `json:"dnd_before_thursday"`
|
||||
DNDBeforeTuesday string `json:"dnd_before_tuesday"`
|
||||
DNDBeforeWednesday string `json:"dnd_before_wednesday"`
|
||||
DNDDays string `json:"dnd_days"`
|
||||
DNDEnabled bool `json:"dnd_enabled"`
|
||||
DNDEnabledFriday string `json:"dnd_enabled_friday"`
|
||||
DNDEnabledMonday string `json:"dnd_enabled_monday"`
|
||||
DNDEnabledSaturday string `json:"dnd_enabled_saturday"`
|
||||
DNDEnabledSunday string `json:"dnd_enabled_sunday"`
|
||||
DNDEnabledThursday string `json:"dnd_enabled_thursday"`
|
||||
DNDEnabledTuesday string `json:"dnd_enabled_tuesday"`
|
||||
DNDEnabledWednesday string `json:"dnd_enabled_wednesday"`
|
||||
DNDEndHour string `json:"dnd_end_hour"`
|
||||
DNDStartHour string `json:"dnd_start_hour"`
|
||||
DNDWeekdaysOffAllday bool `json:"dnd_weekdays_off_allday"`
|
||||
DropboxLegacyPicker bool `json:"dropbox_legacy_picker"`
|
||||
EmojiOnlyAdmins bool `json:"emoji_only_admins"`
|
||||
EnableConnectDmEarlyAccess bool `json:"enable_connect_dm_early_access"`
|
||||
EnableDomainAllowlistForCea bool `json:"enable_domain_allowlist_for_cea"`
|
||||
EnableInfoBarriers bool `json:"enable_info_barriers"`
|
||||
EnableMpdmToPrivateChannelConversion bool `json:"enable_mpdm_to_private_channel_conversion"`
|
||||
EnableSharedChannels int64 `json:"enable_shared_channels"`
|
||||
EnterpriseDefaultChannels []any `json:"enterprise_default_channels"`
|
||||
EnterpriseHasCorporateExports bool `json:"enterprise_has_corporate_exports"`
|
||||
EnterpriseIntuneEnabled bool `json:"enterprise_intune_enabled"`
|
||||
EnterpriseJointeamRequests any `json:"enterprise_jointeam_requests"`
|
||||
EnterpriseMandatoryChannels []any `json:"enterprise_mandatory_channels"`
|
||||
EnterpriseMdmDateEnabled int64 `json:"enterprise_mdm_date_enabled"`
|
||||
EnterpriseMdmDisableFileDownload bool `json:"enterprise_mdm_disable_file_download"`
|
||||
EnterpriseMdmLevel int64 `json:"enterprise_mdm_level"`
|
||||
EnterpriseMdmToken string `json:"enterprise_mdm_token"`
|
||||
EnterpriseMobileDeviceCheck bool `json:"enterprise_mobile_device_check"`
|
||||
EnterpriseTeamCreationRequest EnterpriseTeamCreationRequest `json:"enterprise_team_creation_request"`
|
||||
EXTAuditLogRetentionDuration int64 `json:"ext_audit_log_retention_duration"`
|
||||
EXTAuditLogRetentionType int64 `json:"ext_audit_log_retention_type"`
|
||||
FileLimitWhitelisted bool `json:"file_limit_whitelisted"`
|
||||
FileRetentionDuration int64 `json:"file_retention_duration"`
|
||||
FileRetentionType int64 `json:"file_retention_type"`
|
||||
FilepickerAppFirstInstall bool `json:"filepicker_app_first_install"`
|
||||
FlagContentAdminDash bool `json:"flag_content_admin_dash"`
|
||||
FlagMessageUsersToNotify []any `json:"flag_message_users_to_notify"`
|
||||
GdprEnabled bool `json:"gdpr_enabled"`
|
||||
GdriveEnabledTeam bool `json:"gdrive_enabled_team"`
|
||||
GroupRetentionDuration int64 `json:"group_retention_duration"`
|
||||
GroupRetentionType int64 `json:"group_retention_type"`
|
||||
HasComplianceExport bool `json:"has_compliance_export"`
|
||||
HasHipaaCompliance bool `json:"has_hipaa_compliance"`
|
||||
HasSeenPartnerPromo bool `json:"has_seen_partner_promo"`
|
||||
HasSharedInvites bool `json:"has_shared_invites"`
|
||||
HermesAllowInteractionsWithWorkflowsOwnedBySlackConnectedTeams bool `json:"hermes_allow_interactions_with_workflows_owned_by_slack_connected_teams"`
|
||||
HermesHasAcceptedTos bool `json:"hermes_has_accepted_tos"`
|
||||
HermesTriggersTrippableBySlackConnectedTeams bool `json:"hermes_triggers_trippable_by_slack_connected_teams"`
|
||||
HideGsuiteInviteOption bool `json:"hide_gsuite_invite_option"`
|
||||
HidePersonOptOut bool `json:"hide_person_opt_out"`
|
||||
HideReferers bool `json:"hide_referers"`
|
||||
IdentityLinksPrefs EnterpriseTeamCreationRequest `json:"identity_links_prefs"`
|
||||
ImageDefault bool `json:"image_default"`
|
||||
InstantSlackEnabled bool `json:"instant_slack_enabled"`
|
||||
InviteRequestsEnabled bool `json:"invite_requests_enabled"`
|
||||
LoadingOnlyAdmins bool `json:"loading_only_admins"`
|
||||
LoudChannelMentionsLimit int64 `json:"loud_channel_mentions_limit"`
|
||||
MagicUnfurlsEnabled bool `json:"magic_unfurls_enabled"`
|
||||
MemberAnalyticsDisabled bool `json:"member_analytics_disabled"`
|
||||
MlOptOut bool `json:"ml_opt_out"`
|
||||
MobilePasscodeTimeoutInSeconds int64 `json:"mobile_passcode_timeout_in_seconds"`
|
||||
MobileSessionDuration int64 `json:"mobile_session_duration"`
|
||||
MsgEditWindowMins int64 `json:"msg_edit_window_mins"`
|
||||
NoEmailUserProvisionType string `json:"no_email_user_provision_type"`
|
||||
NotificationRedactionType string `json:"notification_redaction_type"`
|
||||
NotifyPendingEnabled bool `json:"notify_pending_enabled"`
|
||||
NTLMCredentialDomains string `json:"ntlm_credential_domains"`
|
||||
OnedriveAppInstalled bool `json:"onedrive_app_installed"`
|
||||
OnedriveEnabledTeam bool `json:"onedrive_enabled_team"`
|
||||
PremiumWorkflowNotifications PremiumWorkflowNotifications `json:"premium_workflow_notifications"`
|
||||
PrivateChannelAnalyticsDisabled bool `json:"private_channel_analytics_disabled"`
|
||||
PrivateChannelMembershipLimit int64 `json:"private_channel_membership_limit"`
|
||||
PrivateRetentionRedactionDuration int64 `json:"private_retention_redaction_duration"`
|
||||
PublicRetentionRedactionDuration int64 `json:"public_retention_redaction_duration"`
|
||||
ReceivedEscRouteToChannelAwarenessMessage bool `json:"received_esc_route_to_channel_awareness_message"`
|
||||
RetentionDuration int64 `json:"retention_duration"`
|
||||
RetentionType int64 `json:"retention_type"`
|
||||
RichPreviewsDefault string `json:"rich_previews_default"`
|
||||
SamlEnable bool `json:"saml_enable"`
|
||||
SearchFeedbackOptOut bool `json:"search_feedback_opt_out"`
|
||||
SelfServeSelect bool `json:"self_serve_select"`
|
||||
SessionDuration int64 `json:"session_duration"`
|
||||
SessionDurationType int64 `json:"session_duration_type"`
|
||||
ShowLegacyPaidBenefitsPage bool `json:"show_legacy_paid_benefits_page"`
|
||||
ShowLegacyWorkflows bool `json:"show_legacy_workflows"`
|
||||
ShowMobilePromos bool `json:"show_mobile_promos"`
|
||||
SignInWithSlackDefault string `json:"sign_in_with_slack_default"`
|
||||
SignInWithSlackDisabled bool `json:"sign_in_with_slack_disabled"`
|
||||
SingleUserExports bool `json:"single_user_exports"`
|
||||
SlackAIDailyRecapOptOut bool `json:"slack_ai_daily_recap_opt_out"`
|
||||
SlackAIDetailedFeedbackOptOut bool `json:"slack_ai_detailed_feedback_opt_out"`
|
||||
SlackAISearchSuggestedQueries []any `json:"slack_ai_search_suggested_queries"`
|
||||
SlackConnectAccountVisibility string `json:"slack_connect_account_visibility"`
|
||||
SlackConnectAllowedWorkspaces SlackConnectAllowedWorkspaces `json:"slack_connect_allowed_workspaces"`
|
||||
SlackConnectApprovalType string `json:"slack_connect_approval_type"`
|
||||
SlackConnectDmOnlyVerifiedOrgs bool `json:"slack_connect_dm_only_verified_orgs"`
|
||||
SlackConnectFileUploadSharingEnabled bool `json:"slack_connect_file_upload_sharing_enabled"`
|
||||
SlackbotResponsesDisabled bool `json:"slackbot_responses_disabled"`
|
||||
SlackbotResponsesOnlyAdmins bool `json:"slackbot_responses_only_admins"`
|
||||
SpaceshipWorkspaceSettingVisible bool `json:"spaceship_workspace_setting_visible"`
|
||||
SsoChangeEmail bool `json:"sso_change_email"`
|
||||
SsoChooseUsername bool `json:"sso_choose_username"`
|
||||
SsoDisableEmails bool `json:"sso_disable_emails"`
|
||||
SsoOptional bool `json:"sso_optional"`
|
||||
SsoSignupRestrictions int64 `json:"sso_signup_restrictions"`
|
||||
SsoSyncWithProvider bool `json:"sso_sync_with_provider"`
|
||||
StatsOnlyAdmins bool `json:"stats_only_admins"`
|
||||
SubteamsAutoCreateAdmin bool `json:"subteams_auto_create_admin"`
|
||||
SubteamsAutoCreateOwner bool `json:"subteams_auto_create_owner"`
|
||||
ThornSaferScan bool `json:"thorn_safer_scan"`
|
||||
TwoFactorAuthRequired int64 `json:"two_factor_auth_required"`
|
||||
TwoFactorPreventSMS int64 `json:"two_factor_prevent_sms"`
|
||||
TwoFactorRequired bool `json:"two_factor_required"`
|
||||
UneditableUserProfileFields []any `json:"uneditable_user_profile_fields"`
|
||||
UseBrowserPicker bool `json:"use_browser_picker"`
|
||||
UseWorkspaceIconForSingleWorkspaceUsers bool `json:"use_workspace_icon_for_single_workspace_users"`
|
||||
UsesCustomizedCustomStatusPresets bool `json:"uses_customized_custom_status_presets"`
|
||||
WarnUserBeforeLogoutDesktop bool `json:"warn_user_before_logout_desktop"`
|
||||
WarnUserBeforeLogoutMobile bool `json:"warn_user_before_logout_mobile"`
|
||||
WfbDefaultConnectorVisibility string `json:"wfb_default_connector_visibility"`
|
||||
WhoCanAcceptSlackConnectChannelInvites SlackConnectAllowedWorkspaces `json:"who_can_accept_slack_connect_channel_invites"`
|
||||
WhoCanChangeTeamProfile string `json:"who_can_change_team_profile"`
|
||||
WhoCanCreateChannelEmailAddresses SlackConnectAllowedWorkspaces `json:"who_can_create_channel_email_addresses"`
|
||||
WhoCanCreateDeleteUserGroups string `json:"who_can_create_delete_user_groups"`
|
||||
WhoCanCreateExternalLimitedInvite SlackConnectAllowedWorkspaces `json:"who_can_create_external_limited_invite"`
|
||||
WhoCanCreateSharedChannels string `json:"who_can_create_shared_channels"`
|
||||
WhoCanCreateSlackConnectChannelInvite SlackConnectAllowedWorkspaces `json:"who_can_create_slack_connect_channel_invite"`
|
||||
WhoCanCreateWorkflows SlackConnectAllowedWorkspaces `json:"who_can_create_workflows"`
|
||||
WhoCanDmAnyone SlackConnectAllowedWorkspaces `json:"who_can_dm_anyone"`
|
||||
WhoCanEditUserGroups string `json:"who_can_edit_user_groups"`
|
||||
WhoCanManageEXTSharedChannels SlackConnectAllowedWorkspaces `json:"who_can_manage_ext_shared_channels"`
|
||||
WhoCanManageGuests SlackConnectAllowedWorkspaces `json:"who_can_manage_guests"`
|
||||
WhoCanManageIntegrations SlackConnectAllowedWorkspaces `json:"who_can_manage_integrations"`
|
||||
WhoCanManagePrivateChannels WhoCanManageP `json:"who_can_manage_private_channels"`
|
||||
WhoCanManagePrivateChannelsAtWorkspaceLevel WhoCanManageP `json:"who_can_manage_private_channels_at_workspace_level"`
|
||||
WhoCanManagePublicChannels WhoCanManageP `json:"who_can_manage_public_channels"`
|
||||
WhoCanManageSharedChannels SlackConnectAllowedWorkspaces `json:"who_can_manage_shared_channels"`
|
||||
WhoCanPostInSharedChannels SlackConnectAllowedWorkspaces `json:"who_can_post_in_shared_channels"`
|
||||
WhoCanRequestEXTSharedChannels SlackConnectAllowedWorkspaces `json:"who_can_request_ext_shared_channels"`
|
||||
WhoCanReviewFlaggedContent SlackConnectAllowedWorkspaces `json:"who_can_review_flagged_content"`
|
||||
WhoCanUseHermes SlackConnectAllowedWorkspaces `json:"who_can_use_hermes"`
|
||||
WhoCanViewMessageActivity WhoCanViewMessageActivity `json:"who_can_view_message_activity"`
|
||||
WhoHasTeamVisibility string `json:"who_has_team_visibility"`
|
||||
WorkflowBuilderEnabled bool `json:"workflow_builder_enabled"`
|
||||
WorkflowExtensionStepsBetaOptIn bool `json:"workflow_extension_steps_beta_opt_in"`
|
||||
WorkflowExtensionStepsEnabled bool `json:"workflow_extension_steps_enabled"`
|
||||
WorkflowsExportCSVEnabled bool `json:"workflows_export_csv_enabled"`
|
||||
WorkflowsWebhookTriggerEnabled bool `json:"workflows_webhook_trigger_enabled"`
|
||||
AuthMode string `json:"auth_mode"`
|
||||
}
|
||||
|
||||
type CallsApps struct {
|
||||
Video []any `json:"video"`
|
||||
Audio []any `json:"audio"`
|
||||
}
|
||||
|
||||
type DefaultFunctionReuseVisibility struct {
|
||||
Visibility string `json:"visibility"`
|
||||
}
|
||||
|
||||
type EnterpriseTeamCreationRequest struct {
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
}
|
||||
|
||||
type InvitedUserPreset struct {
|
||||
EnableInvitedUser bool `json:"enable_invited_user"`
|
||||
}
|
||||
|
||||
type PremiumWorkflowNotifications struct {
|
||||
NotificationsEnabled bool `json:"notifications_enabled"`
|
||||
NotificationLocation string `json:"notification_location"`
|
||||
}
|
||||
|
||||
type WhoCanManageP struct {
|
||||
User []any `json:"user"`
|
||||
Type []string `json:"type"`
|
||||
}
|
||||
|
||||
type WhoCanViewMessageActivity struct {
|
||||
Type []string `json:"type"`
|
||||
ChannelType []string `json:"channel_type"`
|
||||
}
|
||||
|
||||
type Workspace struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Domain string `json:"domain"`
|
||||
EmailDomain string `json:"email_domain"`
|
||||
Icon Icon `json:"icon"`
|
||||
AvatarBaseURL string `json:"avatar_base_url"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
Prefs Prefs `json:"prefs"`
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"runtime/trace"
|
||||
|
||||
"github.com/rusq/slack"
|
||||
)
|
||||
|
||||
// conversations.* API
|
||||
|
||||
// conversationsGenericInfoForm is the request to conversations.genericInfo
|
||||
type conversationsGenericInfoForm struct {
|
||||
BaseRequest
|
||||
UpdatedChannels string `json:"updated_channels"` // i.e. {"C065H568ZAT":0}
|
||||
WebClientFields
|
||||
}
|
||||
|
||||
type conversationsGenericInfoResponse struct {
|
||||
baseResponse
|
||||
Channels []slack.Channel `json:"channels"`
|
||||
UnchangedChannelIDs []string `json:"unchanged_channel_ids"`
|
||||
}
|
||||
|
||||
func (cl *Client) ConversationsGenericInfo(ctx context.Context, channelID ...string) ([]slack.Channel, error) {
|
||||
ctx, task := trace.NewTask(ctx, "ConversationsGenericInfo")
|
||||
defer task.End()
|
||||
trace.Logf(ctx, "params", "channelID=%v", channelID)
|
||||
|
||||
updChannel := make(map[string]int, len(channelID))
|
||||
for _, id := range channelID {
|
||||
updChannel[id] = 0
|
||||
}
|
||||
b, err := json.Marshal(updChannel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
form := conversationsGenericInfoForm{
|
||||
BaseRequest: BaseRequest{
|
||||
Token: cl.token,
|
||||
},
|
||||
UpdatedChannels: string(b),
|
||||
WebClientFields: webclientReason("fallback:UnknownFetchManager"),
|
||||
}
|
||||
resp, err := cl.PostForm(ctx, "conversations.genericInfo", values(form, true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var r conversationsGenericInfoResponse
|
||||
if err := cl.ParseResponse(&r, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Channels, nil
|
||||
}
|
||||
|
||||
type conversationsViewForm struct {
|
||||
BaseRequest
|
||||
CanonicalAvatars bool `json:"canonical_avatars"`
|
||||
NoUserProfile bool `json:"no_user_profile"`
|
||||
IgnoreReplies bool `json:"ignore_replies"`
|
||||
NoSelf bool `json:"no_self"`
|
||||
IncludeFullUsers bool `json:"include_full_users"`
|
||||
IncludeUseCases bool `json:"include_use_cases"`
|
||||
IncludeStories bool `json:"include_stories"`
|
||||
NoMembers bool `json:"no_members"`
|
||||
IncludeMutationTimestamps bool `json:"include_mutation_timestamps"`
|
||||
Count int `json:"count"`
|
||||
Channel string `json:"channel"`
|
||||
IncludeFreeTeamExtraMessages bool `json:"include_free_team_extra_messages"`
|
||||
WebClientFields
|
||||
}
|
||||
|
||||
type ConversationsViewResponse struct {
|
||||
Users []User `json:"users"`
|
||||
IM IM `json:"im"`
|
||||
Emojis map[string]string `json:"emojis"`
|
||||
// we don't care about the rest of the response
|
||||
}
|
||||
|
||||
func (cl *Client) ConversationsView(ctx context.Context, channelID string) (ConversationsViewResponse, error) {
|
||||
ctx, task := trace.NewTask(ctx, "ConversationsView")
|
||||
defer task.End()
|
||||
trace.Logf(ctx, "params", "channelID=%v", channelID)
|
||||
|
||||
form := conversationsViewForm{
|
||||
BaseRequest: BaseRequest{
|
||||
Token: cl.token,
|
||||
},
|
||||
CanonicalAvatars: true,
|
||||
NoUserProfile: true,
|
||||
IgnoreReplies: true,
|
||||
NoSelf: true,
|
||||
IncludeFullUsers: false,
|
||||
IncludeUseCases: false,
|
||||
IncludeStories: false,
|
||||
NoMembers: true,
|
||||
IncludeMutationTimestamps: false,
|
||||
Count: 50,
|
||||
Channel: channelID,
|
||||
WebClientFields: webclientReason(""),
|
||||
}
|
||||
resp, err := cl.PostForm(ctx, "conversations.view", values(form, true))
|
||||
if err != nil {
|
||||
return ConversationsViewResponse{}, err
|
||||
}
|
||||
var r = struct {
|
||||
baseResponse
|
||||
ConversationsViewResponse
|
||||
}{}
|
||||
if err := cl.ParseResponse(&r, resp); err != nil {
|
||||
return ConversationsViewResponse{}, err
|
||||
}
|
||||
return r.ConversationsViewResponse, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime/trace"
|
||||
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/limiter"
|
||||
)
|
||||
|
||||
// im.* API
|
||||
|
||||
type imListForm struct {
|
||||
BaseRequest
|
||||
GetLatest bool `json:"get_latest"`
|
||||
GetReadState bool `json:"get_read_state"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
WebClientFields
|
||||
}
|
||||
|
||||
type imListResponse struct {
|
||||
baseResponse
|
||||
IMs []IM `json:"ims,omitempty"`
|
||||
}
|
||||
|
||||
func (cl *Client) IMList(ctx context.Context) ([]IM, error) {
|
||||
ctx, task := trace.NewTask(ctx, "IMList")
|
||||
defer task.End()
|
||||
|
||||
form := imListForm{
|
||||
BaseRequest: BaseRequest{Token: cl.token},
|
||||
GetLatest: true,
|
||||
GetReadState: true,
|
||||
WebClientFields: WebClientFields{
|
||||
XReason: "guided-search-people-empty-state",
|
||||
XMode: "online",
|
||||
XSonic: true,
|
||||
XAppName: "client",
|
||||
},
|
||||
Cursor: "",
|
||||
}
|
||||
lim := limiter.Tier2boost.Limiter()
|
||||
var IMs []IM
|
||||
for {
|
||||
resp, err := cl.PostForm(ctx, "im.list", values(form, true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := imListResponse{}
|
||||
if err := cl.ParseResponse(&r, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
IMs = append(IMs, r.IMs...)
|
||||
if r.ResponseMetadata.NextCursor == "" {
|
||||
break
|
||||
}
|
||||
form.Cursor = r.ResponseMetadata.NextCursor
|
||||
if err := lim.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return IMs, nil
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// Package edge provides a limited implementation of undocumented Slack Edge
|
||||
// API necessary to get the data from a slack workspace.
|
||||
//
|
||||
// It handles rate limit retries internally (but only once).
|
||||
package edge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rusq/slack"
|
||||
"github.com/rusq/slackauth"
|
||||
"github.com/rusq/slackdump/v3/auth"
|
||||
"github.com/rusq/tagops"
|
||||
)
|
||||
|
||||
type httpClient interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
// cl is the http client to use
|
||||
cl httpClient
|
||||
// edgeAPI is the edge API endpoint
|
||||
edgeAPI string
|
||||
// webclientAPI is the webclient APIs endpoint
|
||||
webclientAPI string
|
||||
// token is the slack token
|
||||
token string
|
||||
|
||||
// teamID is the team ID
|
||||
teamID string
|
||||
tape io.WriteCloser
|
||||
}
|
||||
|
||||
type Option func(*Client)
|
||||
|
||||
func WithTape(tape io.WriteCloser) Option {
|
||||
return func(cl *Client) {
|
||||
cl.tape = tape
|
||||
}
|
||||
}
|
||||
|
||||
// OptionHTTPClient - provide a custom http client to the slack client.
|
||||
func OptionHTTPClient(client httpClient) func(*Client) {
|
||||
return func(cl *Client) {
|
||||
cl.cl = client
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoTeamID = errors.New("teamID is empty")
|
||||
ErrNoToken = errors.New("token is empty")
|
||||
)
|
||||
|
||||
func NewWithClient(workspaceName string, teamID string, token string, cl *http.Client, opt ...Option) (*Client, error) {
|
||||
if teamID == "" {
|
||||
return nil, ErrNoTeamID
|
||||
}
|
||||
if token == "" {
|
||||
return nil, ErrNoToken
|
||||
}
|
||||
tape, err := os.Create("tape.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{
|
||||
cl: cl,
|
||||
token: token,
|
||||
teamID: teamID,
|
||||
webclientAPI: fmt.Sprintf("https://%s.slack.com/api/", workspaceName),
|
||||
edgeAPI: fmt.Sprintf("https://edgeapi.slack.com/cache/%s/", teamID),
|
||||
tape: tape,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewWithToken(ctx context.Context, token string, cookies []*http.Cookie) (*Client, error) {
|
||||
if token == "" {
|
||||
return nil, ErrNoToken
|
||||
}
|
||||
prov, err := auth.NewValueCookiesAuth(token, cookies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(ctx, prov)
|
||||
}
|
||||
|
||||
type nopTape struct{}
|
||||
|
||||
func (nopTape) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (nopTape) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewWithInfo is the same as New, but doesn't call the AuthTest on
|
||||
// initialisation. Caller must ensure that the token is valid.
|
||||
func NewWithInfo(info *slack.AuthTestResponse, prov auth.Provider, opt ...Option) (*Client, error) {
|
||||
hcl, err := prov.HTTPClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &Client{
|
||||
cl: hcl,
|
||||
token: prov.SlackToken(),
|
||||
teamID: info.TeamID,
|
||||
webclientAPI: info.URL + "api/",
|
||||
edgeAPI: fmt.Sprintf("https://edgeapi.slack.com/cache/%s/", info.TeamID),
|
||||
tape: nopTape{},
|
||||
}
|
||||
|
||||
for _, o := range opt {
|
||||
o(c)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func New(ctx context.Context, prov auth.Provider, opt ...Option) (*Client, error) {
|
||||
info, err := prov.Test(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewWithInfo(info, prov, opt...)
|
||||
}
|
||||
|
||||
func (cl *Client) Raw() httpClient {
|
||||
return cl.cl
|
||||
}
|
||||
|
||||
func (cl *Client) Close() error {
|
||||
if cl.tape != nil {
|
||||
return cl.tape.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BaseRequest struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type baseResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ResponseMetadata ResponseMetadata `json:"response_metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (r baseResponse) validate(ep string) error {
|
||||
if !r.Ok {
|
||||
return &APIError{Err: r.Error, Metadata: r.ResponseMetadata, Endpoint: ep}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ResponseMetadata struct {
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
}
|
||||
|
||||
func (r *BaseRequest) SetToken(token string) {
|
||||
r.Token = token
|
||||
}
|
||||
|
||||
func (r *BaseRequest) IsTokenSet() bool {
|
||||
return len(r.Token) > 0
|
||||
}
|
||||
|
||||
type PostRequest interface {
|
||||
SetToken(string)
|
||||
IsTokenSet() bool
|
||||
}
|
||||
|
||||
const (
|
||||
hdrContentType = "Content-Type"
|
||||
)
|
||||
|
||||
// PostJSON posts a request to the edge API. The request is marshalled to
|
||||
// JSON and the response is unmarshalled to the req, which must be a pointer
|
||||
// to a struct.
|
||||
func (cl *Client) PostJSON(ctx context.Context, path string, req PostRequest) (*http.Response, error) {
|
||||
if !req.IsTokenSet() {
|
||||
req.SetToken(cl.token)
|
||||
}
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tape := cl.recorder(bytes.NewReader(data))
|
||||
defer cl.record([]byte("\n\n"))
|
||||
r, err := http.NewRequestWithContext(ctx, http.MethodPost, cl.edgeAPI+path, tape)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set(hdrContentType, "application/json")
|
||||
|
||||
return do(ctx, cl.cl, r)
|
||||
}
|
||||
|
||||
// callEdgeAPI calls the edge API.
|
||||
func (cl *Client) callEdgeAPI(ctx context.Context, v any, endpoint string, req PostRequest) error {
|
||||
r, err := cl.PostJSON(ctx, endpoint, req)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
return cl.ParseResponse(v, r)
|
||||
}
|
||||
|
||||
// PostForm sends a POST request to a webclient API, it marshals the form
|
||||
// values to url.Values, omitting empty fields, and sends the request.
|
||||
func (cl *Client) Post(ctx context.Context, path string, a any) (*http.Response, error) {
|
||||
return cl.PostFormRaw(ctx, cl.webclientAPI+path, values(a, true))
|
||||
}
|
||||
|
||||
// PostForm sends a POST request to a webclient API with form values.
|
||||
func (cl *Client) PostForm(ctx context.Context, path string, form url.Values) (*http.Response, error) {
|
||||
return cl.PostFormRaw(ctx, cl.webclientAPI+path, form)
|
||||
}
|
||||
|
||||
func (cl *Client) record(b []byte) {
|
||||
if cl.tape != nil {
|
||||
if _, err := cl.tape.Write(b); err != nil {
|
||||
slog.Default().Error("error writing to tape", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cl *Client) PostFormRaw(ctx context.Context, url string, form url.Values) (*http.Response, error) {
|
||||
if form["token"] == nil {
|
||||
form.Set("token", cl.token)
|
||||
}
|
||||
r := cl.recorder(strings.NewReader(form.Encode()))
|
||||
defer cl.record([]byte("\n\n"))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
return do(ctx, cl.cl, req)
|
||||
}
|
||||
|
||||
func (cl *Client) ParseResponse(req any, r *http.Response) error {
|
||||
if r.StatusCode < http.StatusOK || http.StatusMultipleChoices <= r.StatusCode {
|
||||
return fmt.Errorf("error: status code: %s", r.Status)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Read the body into a byte slice
|
||||
bodyBytes, err := io.ReadAll(cl.recorder(r.Body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert body to string and print it
|
||||
bodyStr := string(bodyBytes)
|
||||
fmt.Println("Raw response body:", bodyStr)
|
||||
|
||||
// Decode the body into the expected struct
|
||||
if err := json.Unmarshal(bodyBytes, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// do is a helper function to do the request and handle rate limiting. it
|
||||
// tries to handle the rate limiting by waiting for the Retry-After only once,
|
||||
// if it receives another rate limit error, it returns slack.RateLimitedError
|
||||
// to let the caller handle it.
|
||||
func do(ctx context.Context, cl httpClient, req *http.Request) (*http.Response, error) {
|
||||
ctx, task := trace.NewTask(ctx, "edge.do")
|
||||
defer task.End()
|
||||
|
||||
lg := slog.Default()
|
||||
req.Header.Set("Accept-Language", "en-NZ,en-AU;q=0.9,en;q=0.8")
|
||||
req.Header.Set("User-Agent", slackauth.DefaultUserAgent)
|
||||
|
||||
rgn := trace.StartRegion(ctx, "http.Do")
|
||||
resp, err := cl.Do(req)
|
||||
rgn.End()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
wait, err := parseRetryAfter(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lg.InfoContext(ctx, "got rate limited, waiting", "delay", wait)
|
||||
|
||||
time.Sleep(wait)
|
||||
resp, err = cl.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if we are still rate limited, then we are in trouble
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
lg.DebugContext(ctx, "edge.do: did my best, but still rate limited, giving up, not my problem")
|
||||
wait, err = parseRetryAfter(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, &slack.RateLimitedError{RetryAfter: wait}
|
||||
}
|
||||
}
|
||||
if resp.StatusCode < http.StatusOK || http.StatusMultipleChoices <= resp.StatusCode {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, slack.StatusCodeError{Code: resp.StatusCode, Status: string(body)}
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func parseRetryAfter(resp *http.Response) (time.Duration, error) {
|
||||
strWait := resp.Header.Get("Retry-After")
|
||||
if strWait == "" {
|
||||
return 0, errors.New("got rate limited, but did not get a Retry-After header")
|
||||
}
|
||||
wait, err := time.ParseDuration(strWait + "s")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return wait, nil
|
||||
}
|
||||
|
||||
// values returns url.Values from a struct. If omitempty is true, then the
|
||||
// empty values are omitted for the fields that have the `omitempty` tag.
|
||||
func values(s any, omitempty bool) url.Values {
|
||||
var v = make(url.Values)
|
||||
m := tagops.ToMap(s, "json", omitempty, true)
|
||||
for k, val := range m {
|
||||
v.Set(k, fmt.Sprint(val))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (cl *Client) webapiURL(endpoint string) string {
|
||||
return cl.webclientAPI + endpoint
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Err string
|
||||
Metadata ResponseMetadata
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
if len(e.Metadata.Messages) > 0 {
|
||||
return e.Err + ": " + e.Metadata.Messages[0]
|
||||
}
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type WebClientFields struct {
|
||||
XReason string `json:"_x_reason,omitempty"`
|
||||
XMode string `json:"_x_mode,omitempty"`
|
||||
XSonic bool `json:"_x_sonic"`
|
||||
XAppName string `json:"_x_app_name"`
|
||||
}
|
||||
|
||||
func webclientReason(reason string) WebClientFields {
|
||||
return WebClientFields{
|
||||
XReason: reason,
|
||||
XMode: "online",
|
||||
XSonic: true,
|
||||
XAppName: "client",
|
||||
}
|
||||
}
|
||||
|
||||
func (cl *Client) recorder(r io.Reader) io.Reader {
|
||||
if cl.tape == nil {
|
||||
return r
|
||||
}
|
||||
return io.TeeReader(r, cl.tape)
|
||||
}
|
||||
|
||||
// Pagination contains the pagination information. It is truly fucked, Slack
|
||||
// does not allow to seek past Page 100, when page > 100 requested, Slack
|
||||
// returns the first page (Page=1). Seems to be an internal limitation. The
|
||||
// workaround would be to use the Query parameter, to be more specific about
|
||||
// the channel names, but to get all channels, this would require iterating
|
||||
// through all 65536 runes of unicode give or take the special characters.
|
||||
//
|
||||
// For now, this doesn't work as a replacement for conversation.list (202403).
|
||||
type Pagination struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
PageCount int `json:"page_count"`
|
||||
First int64 `json:"first"`
|
||||
Last int64 `json:"last"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package fasttime
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Time time.Time
|
||||
|
||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||
clean := strings.Trim(string(data), `"`)
|
||||
if clean == "" || clean == "null" {
|
||||
*t = Time{}
|
||||
return nil
|
||||
}
|
||||
ts, err := TS2int(clean)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = Time(Int2Time(ts))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t Time) MarshalJSON() ([]byte, error) {
|
||||
ts := time.Time(t).UnixMicro()
|
||||
return []byte(`"` + Int2TS(ts) + `"`), nil
|
||||
}
|
||||
|
||||
// SlackString returns the time as a slack timestamp (i.e. "1234567890.123456").
|
||||
func (t Time) SlackString() string {
|
||||
return Int2TS(time.Time(t).UnixMicro())
|
||||
}
|
||||
|
||||
var ErrNotATimestamp = errors.New("not a slack timestamp")
|
||||
|
||||
// Int2TS converts an int64 to a slack timestamp by inserting a dot in the
|
||||
// right place.
|
||||
func Int2TS(ts int64) string {
|
||||
const cut = 6
|
||||
s := strconv.FormatInt(ts, 10)
|
||||
l := len(s)
|
||||
if l < cut+1 {
|
||||
return ""
|
||||
}
|
||||
lo := s[l-cut:]
|
||||
hi := s[:l-cut]
|
||||
return hi + "." + lo
|
||||
}
|
||||
|
||||
// Int2Time converts an int64 to a time.Time.
|
||||
func Int2Time(ts int64) time.Time {
|
||||
return time.UnixMicro(ts)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build 386
|
||||
|
||||
package fasttime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// int size on the 32-bit systems is 32 bit (surprise), this constraints us to slower 64-bit implementation.
|
||||
|
||||
// TS2int converts a slack timestamp to an int64 by stripping the dot and
|
||||
// converting the string to an int64. It is useful for fast comparison.
|
||||
func TS2int(ts string) (int64, error) {
|
||||
if ts == "" {
|
||||
return 0, nil
|
||||
}
|
||||
i := strings.IndexByte(ts, '.')
|
||||
if i == -1 {
|
||||
return 0, fmt.Errorf("%w: %q", ErrNotATimestamp, ts)
|
||||
}
|
||||
val, err := strconv.ParseInt(ts[:i]+ts[i+1:], 10, 64)
|
||||
return int64(val), err
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package fasttime
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTs2int(t *testing.T) {
|
||||
type args struct {
|
||||
ts string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"valid ts",
|
||||
args{"1638494510.037400"},
|
||||
1638494510037400,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid ts",
|
||||
args{"x"},
|
||||
0,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"real ts",
|
||||
args{"1674255434.388009"},
|
||||
1674255434388009,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"maxint64",
|
||||
args{strconv.FormatInt(math.MaxInt64/1000, 10) + ".000"},
|
||||
math.MaxInt64 / 1000 * 1000,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no dot",
|
||||
args{"1674255434"},
|
||||
0,
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := TS2int(tt.args.ts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TS2Int64() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("TS2Int64() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInt2ts(t *testing.T) {
|
||||
type args struct {
|
||||
ts int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"valid ts",
|
||||
args{1638494510037400},
|
||||
"1638494510.037400",
|
||||
},
|
||||
{
|
||||
"real ts",
|
||||
args{1674255434388009},
|
||||
"1674255434.388009",
|
||||
},
|
||||
{
|
||||
"zero",
|
||||
args{0},
|
||||
"",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Int2TS(tt.args.ts); got != tt.want {
|
||||
t.Errorf("Int642TS() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInt2Time(t *testing.T) {
|
||||
type args struct {
|
||||
ts int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want time.Time
|
||||
}{
|
||||
{
|
||||
"valid ts",
|
||||
args{1645054781942979},
|
||||
time.Unix(1645054781, 942979000),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Int2Time(tt.args.ts); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Int2Time() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTs2Int(b *testing.B) {
|
||||
var n int64
|
||||
for i := 0; i < b.N; i++ {
|
||||
n, _ = TS2int("1638494510.037400")
|
||||
}
|
||||
if n != 1638494510037400 {
|
||||
b.Errorf("Expected 1638494510037400, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInt2Ts(b *testing.B) {
|
||||
var s string
|
||||
|
||||
for b.Loop() {
|
||||
s = Int2TS(1638494510037400)
|
||||
}
|
||||
if s != "1638494510.037400" {
|
||||
b.Errorf("Expected 1638494510.037400, got %s", s)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//go:build !386
|
||||
|
||||
package fasttime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TS2int converts a slack timestamp to an int64 by stripping the dot and
|
||||
// converting the string to an int64. It is useful for fast comparison.
|
||||
func TS2int(ts string) (int64, error) {
|
||||
if ts == "" {
|
||||
return 0, nil
|
||||
}
|
||||
i := strings.IndexByte(ts, '.')
|
||||
if i == -1 {
|
||||
return 0, fmt.Errorf("%w: %q", ErrNotATimestamp, ts)
|
||||
}
|
||||
val, err := strconv.Atoi(ts[:i] + ts[i+1:])
|
||||
return int64(val), err
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"runtime/trace"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/limiter"
|
||||
"github.com/rusq/slack"
|
||||
)
|
||||
|
||||
// search.* API
|
||||
|
||||
const perPage = 100
|
||||
|
||||
var ErrPagination = errors.New("pagination fault")
|
||||
|
||||
type Channel struct {
|
||||
slack.GroupConversation
|
||||
IsChannel bool `json:"is_channel"`
|
||||
IsGeneral bool `json:"is_general"`
|
||||
IsMember bool `json:"is_member"`
|
||||
NumMembers int `json:"member_count"`
|
||||
Locale string `json:"locale"`
|
||||
Properties *slack.Properties `json:"properties"`
|
||||
}
|
||||
|
||||
type SearchResponse[T any] struct {
|
||||
baseResponse
|
||||
Module string `json:"module"`
|
||||
Query string `json:"query"`
|
||||
Filters json.RawMessage `json:"filters"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
Items []T `json:"items"`
|
||||
}
|
||||
|
||||
// searchForm is the form to be sent to the search endpoint.
|
||||
type searchForm struct {
|
||||
BaseRequest
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
Module string `json:"module"`
|
||||
Query string `json:"query"`
|
||||
Page int `json:"page,omitempty"`
|
||||
ClientReqID string `json:"client_req_id"`
|
||||
BrowseID string `json:"browse_session_id"`
|
||||
Extracts int `json:"extracts"`
|
||||
Highlight int `json:"highlight"`
|
||||
ExtraMsg int `json:"extra_message_data"`
|
||||
NoUserProfile int `json:"no_user_profile"`
|
||||
Count int `json:"count"`
|
||||
FileTitleOnly bool `json:"file_title_only"`
|
||||
QueryRewriteDisabled bool `json:"query_rewrite_disabled"`
|
||||
IncludeFilesShares int `json:"include_files_shares"`
|
||||
Browse string `json:"browse"`
|
||||
SearchContext string `json:"search_context"`
|
||||
MaxFilterSuggestions int `json:"max_filter_suggestions"`
|
||||
Sort searchSortType `json:"sort"`
|
||||
SortDir searchSortDir `json:"sort_dir"`
|
||||
ChannelType searchChannelType `json:"channel_type"`
|
||||
ExcludeMyChannels int `json:"exclude_my_channels"`
|
||||
SearchOnlyMyChannels bool `json:"search_only_my_channels"`
|
||||
RecommendSource string `json:"recommend_source"`
|
||||
WebClientFields
|
||||
}
|
||||
|
||||
type searchChannelType string
|
||||
|
||||
const (
|
||||
sctPublic searchChannelType = "public"
|
||||
sctPrivate searchChannelType = "private"
|
||||
scpArchived searchChannelType = "archived"
|
||||
scpExternalShared searchChannelType = "external_shared"
|
||||
scpExcludeArchived searchChannelType = "exclude_archived"
|
||||
scpPrivateExclude searchChannelType = "private_exclude"
|
||||
scpAll searchChannelType = ""
|
||||
)
|
||||
|
||||
type searchSortDir string
|
||||
|
||||
const (
|
||||
ssdEmpty searchSortDir = ""
|
||||
ssdAsc searchSortDir = "asc"
|
||||
ssdDesc searchSortDir = "desc"
|
||||
)
|
||||
|
||||
type searchSortType string
|
||||
|
||||
const (
|
||||
sstRecommended searchSortType = "recommended"
|
||||
sstName searchSortType = "name"
|
||||
)
|
||||
|
||||
func (cl *Client) SearchChannels(ctx context.Context, query string) ([]slack.Channel, error) {
|
||||
ctx, task := trace.NewTask(ctx, "SearchChannels")
|
||||
defer task.End()
|
||||
lg := slog.With("in", "SearchChannels", "query", query)
|
||||
|
||||
trace.Logf(ctx, "params", "query=%q", query)
|
||||
|
||||
clientReq, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
browseID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
form := searchForm{
|
||||
BaseRequest: BaseRequest{Token: cl.token},
|
||||
Module: "channels",
|
||||
Query: query,
|
||||
Page: 0,
|
||||
ClientReqID: clientReq.String(),
|
||||
BrowseID: browseID.String(),
|
||||
Extracts: 0,
|
||||
Highlight: 0,
|
||||
Cursor: "*",
|
||||
ExtraMsg: 0,
|
||||
NoUserProfile: 1,
|
||||
Count: perPage,
|
||||
FileTitleOnly: false,
|
||||
QueryRewriteDisabled: false,
|
||||
IncludeFilesShares: 1,
|
||||
Browse: "standard",
|
||||
SearchContext: "desktop_channel_browser",
|
||||
MaxFilterSuggestions: 10,
|
||||
Sort: sstName,
|
||||
SortDir: ssdAsc,
|
||||
ChannelType: scpAll,
|
||||
ExcludeMyChannels: 0,
|
||||
SearchOnlyMyChannels: false,
|
||||
RecommendSource: "channel-browser",
|
||||
WebClientFields: WebClientFields{
|
||||
XReason: "browser-query",
|
||||
XMode: "online",
|
||||
XSonic: true,
|
||||
XAppName: "client",
|
||||
},
|
||||
}
|
||||
|
||||
const ep = "search.modules.channels"
|
||||
lim := limiter.Tier2boost.Limiter()
|
||||
var cc []slack.Channel
|
||||
for {
|
||||
resp, err := cl.PostForm(ctx, ep, values(form, true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sr SearchResponse[Channel]
|
||||
if err := cl.ParseResponse(&sr, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sr.validate(ep); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fix for the members count, mapping is incorrect in the slack.Channel
|
||||
// if the object is being used for search.modules.channels ep
|
||||
for _, c := range sr.Items {
|
||||
obj := slack.Channel{
|
||||
GroupConversation: c.GroupConversation,
|
||||
IsChannel: true,
|
||||
IsGeneral: c.IsGeneral,
|
||||
IsMember: c.IsMember,
|
||||
Locale: c.Locale,
|
||||
Properties: c.Properties,
|
||||
}
|
||||
obj.NumMembers = c.NumMembers
|
||||
if obj.NumMembers == 0 {
|
||||
obj.IsArchived = true
|
||||
}
|
||||
|
||||
cc = append(cc, obj)
|
||||
}
|
||||
if sr.Pagination.NextCursor == "" {
|
||||
lg.Debug("no more channels")
|
||||
break
|
||||
}
|
||||
lg.DebugContext(ctx, "pagination", "next_cursor", sr.Pagination.NextCursor)
|
||||
form.Cursor = sr.Pagination.NextCursor
|
||||
if err := lim.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
trace.Logf(ctx, "info", "channels found=%d", len(cc))
|
||||
lg.DebugContext(ctx, "channels", "count", len(cc))
|
||||
return cc, nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/rusq/slack"
|
||||
)
|
||||
|
||||
var ErrParameterMissing = errors.New("required parameter missing")
|
||||
|
||||
// High level functions that wrap low level calls to webclient API to return
|
||||
// the data in the format close to the Slack API.
|
||||
|
||||
func (cl *Client) GetConversationsContext(ctx context.Context, _ *slack.GetConversationsParameters) (channels []slack.Channel, _ string, err error) {
|
||||
type result struct {
|
||||
Channels []slack.Channel
|
||||
Err error
|
||||
}
|
||||
|
||||
var resultC = make(chan result, 2)
|
||||
var pipeline = []func(){
|
||||
func() {
|
||||
// getting client.userBoot information
|
||||
ub, err := cl.ClientUserBoot(ctx)
|
||||
if err != nil {
|
||||
resultC <- result{Err: err}
|
||||
return
|
||||
}
|
||||
var ch = make([]slack.Channel, 0, len(ub.Channels))
|
||||
for _, c := range ub.Channels {
|
||||
ch = append(ch, c.SlackChannel())
|
||||
}
|
||||
resultC <- result{Channels: ch, Err: err}
|
||||
},
|
||||
func() {
|
||||
// collecting the IMs.
|
||||
ims, err := cl.IMList(ctx)
|
||||
var ch = make([]slack.Channel, 0, len(ims))
|
||||
for _, c := range ims {
|
||||
ch = append(ch, c.SlackChannel())
|
||||
}
|
||||
resultC <- result{Channels: ch, Err: err}
|
||||
},
|
||||
func() {
|
||||
// collecting the channels.
|
||||
ch, err := cl.SearchChannels(ctx, "")
|
||||
resultC <- result{Channels: ch, Err: err}
|
||||
},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(pipeline))
|
||||
for _, f := range pipeline {
|
||||
go func(f func()) {
|
||||
defer wg.Done()
|
||||
f()
|
||||
}(f)
|
||||
}
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultC)
|
||||
}()
|
||||
|
||||
// create a map of channels that we have already seen
|
||||
var seenChannels = make(map[string]struct{})
|
||||
for r := range resultC {
|
||||
if r.Err != nil {
|
||||
return nil, "", r.Err
|
||||
}
|
||||
for _, c := range r.Channels {
|
||||
if _, seen := seenChannels[c.ID]; !seen {
|
||||
seenChannels[c.ID] = struct{}{}
|
||||
channels = append(channels, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ClientCounts hopefully returns MPIM IDs that we haven't seen in the
|
||||
// user boot response.
|
||||
cr, err := cl.ClientCounts(ctx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// determine which mpims are already in the list, and which need to be
|
||||
// fetched
|
||||
var fetchIDs = make([]string, 0, len(cr.MPIMs))
|
||||
for _, c := range cr.MPIMs {
|
||||
if _, seen := seenChannels[c.ID]; !seen {
|
||||
fetchIDs = append(fetchIDs, c.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// getting the info on any MPIMs that we haven't seen yet.
|
||||
mpims, err := cl.ConversationsGenericInfo(ctx, fetchIDs...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
channels = append(channels, mpims...)
|
||||
return channels, "", nil
|
||||
}
|
||||
|
||||
func (cl *Client) GetUsersInConversationContext(ctx context.Context, p *slack.GetUsersInConversationParameters) (ids []string, _ string, err error) {
|
||||
if p.ChannelID == "" {
|
||||
return nil, "", ErrParameterMissing
|
||||
}
|
||||
uu, err := cl.UsersList(ctx, p.ChannelID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
for _, u := range uu {
|
||||
ids = append(ids, u.ID)
|
||||
}
|
||||
return ids, "", nil
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
func (cl *Client) GetConversationInfoContext(ctx context.Context, input *slack.GetConversationInfoInput) (*slack.Channel, error) {
|
||||
cc, err := cl.ConversationsGenericInfo(ctx, input.ChannelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cc) == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return &cc[0], nil
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/korotovsky/slack-mcp-server/pkg/limiter"
|
||||
"github.com/rusq/slack"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type UsersListRequest struct {
|
||||
BaseRequest
|
||||
Channels []string `json:"channels"`
|
||||
PresentFirst bool `json:"present_first,omitempty"`
|
||||
Filter string `json:"filter"`
|
||||
Index string `json:"index,omitempty"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
IncludeProfileOnlyUsers bool `json:"include_profile_only_users,omitempty"`
|
||||
Marker string `json:"marker,omitempty"` // pagination, it must contain the next_marker from the previous response
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type UsersListResponse struct {
|
||||
Results []User `json:"results"`
|
||||
NextMarker string `json:"next_marker"` // pagination, marker value which must be used in the next request, if not empty.
|
||||
baseResponse
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
TeamID string `json:"team_id"`
|
||||
Name string `json:"name"`
|
||||
Deleted bool `json:"deleted"`
|
||||
Color string `json:"color"`
|
||||
RealName string `json:"real_name"`
|
||||
Tz string `json:"tz"`
|
||||
TzLabel string `json:"tz_label"`
|
||||
TzOffset int64 `json:"tz_offset"`
|
||||
Profile Profile `json:"profile"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
IsOwner bool `json:"is_owner"`
|
||||
IsPrimaryOwner bool `json:"is_primary_owner"`
|
||||
IsRestricted bool `json:"is_restricted"`
|
||||
IsUltraRestricted bool `json:"is_ultra_restricted"`
|
||||
IsBot bool `json:"is_bot"`
|
||||
IsAppUser bool `json:"is_app_user"`
|
||||
Updated slack.JSONTime `json:"updated"`
|
||||
IsEmailConfirmed bool `json:"is_email_confirmed"`
|
||||
WhoCanShareContactCard string `json:"who_can_share_contact_card"`
|
||||
Has2Fa *bool `json:"has_2fa,omitempty"`
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
Title string `json:"title"`
|
||||
Phone string `json:"phone"`
|
||||
Skype string `json:"skype"`
|
||||
RealName string `json:"real_name"`
|
||||
RealNameNormalized string `json:"real_name_normalized"`
|
||||
DisplayName string `json:"display_name"`
|
||||
DisplayNameNormalized string `json:"display_name_normalized"`
|
||||
Fields any `json:"fields"`
|
||||
StatusText string `json:"status_text"`
|
||||
StatusEmoji string `json:"status_emoji"`
|
||||
StatusEmojiDisplayInfo []any `json:"status_emoji_display_info"`
|
||||
StatusExpiration int64 `json:"status_expiration"`
|
||||
AvatarHash string `json:"avatar_hash"`
|
||||
GuestInvitedBy string `json:"guest_invited_by"`
|
||||
ImageOriginal *string `json:"image_original,omitempty"`
|
||||
IsCustomImage *bool `json:"is_custom_image,omitempty"`
|
||||
Email string `json:"email"`
|
||||
FirstName *string `json:"first_name,omitempty"`
|
||||
LastName *string `json:"last_name,omitempty"`
|
||||
StatusTextCanonical string `json:"status_text_canonical"`
|
||||
Team string `json:"team"`
|
||||
}
|
||||
|
||||
type UsersInfoRequest struct {
|
||||
BaseRequest
|
||||
CheckInteraction bool `json:"check_interaction"`
|
||||
IncludeProfileOnlyUsers bool `json:"include_profile_only_users"`
|
||||
UpdatedIDS map[string]int64 `json:"updated_ids"`
|
||||
}
|
||||
|
||||
type UserInfoResponse struct {
|
||||
Results []UserInfo `json:"results"`
|
||||
FailedIDS []string `json:"failed_ids"`
|
||||
PendingIDS []string `json:"pending_ids"`
|
||||
CanInteract map[string]bool `json:"can_interact"`
|
||||
baseResponse
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
ID string `json:"id"`
|
||||
TeamID string `json:"team_id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
IsBot bool `json:"is_bot"`
|
||||
IsAppUser bool `json:"is_app_user"`
|
||||
Deleted bool `json:"deleted"`
|
||||
Profile Profile `json:"profile"`
|
||||
IsStranger bool `json:"is_stranger"`
|
||||
Updated int64 `json:"updated"`
|
||||
WhoCanShareContactCard string `json:"who_can_share_contact_card"`
|
||||
}
|
||||
|
||||
var ErrNotOK = errors.New("server returned NOT OK")
|
||||
|
||||
// GetUsers returns users from the slack edge api for the channel. User IDs
|
||||
// should be provided by the caller. If ids is empty, does nothing.
|
||||
//
|
||||
// This tries to replicate the logic of the Slack client, when it fetches
|
||||
// the channel users while being logged in as a guest user.
|
||||
func (cl *Client) GetUsers(ctx context.Context, userID ...string) ([]UserInfo, error) {
|
||||
if len(userID) == 0 {
|
||||
return []UserInfo{}, nil
|
||||
}
|
||||
updatedIds := make(map[string]int64, len(userID))
|
||||
for _, id := range userID {
|
||||
updatedIds[id] = 0
|
||||
}
|
||||
|
||||
lim := limiter.Tier3.Limiter()
|
||||
var users []UserInfo
|
||||
for {
|
||||
uiresp, err := cl.UsersInfo(ctx, &UsersInfoRequest{
|
||||
CheckInteraction: true,
|
||||
IncludeProfileOnlyUsers: true,
|
||||
UpdatedIDS: updatedIds,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !uiresp.Ok {
|
||||
return nil, ErrNotOK
|
||||
}
|
||||
if len(uiresp.Results) > 0 {
|
||||
users = append(users, uiresp.Results...)
|
||||
}
|
||||
if len(uiresp.PendingIDS) == 0 {
|
||||
break
|
||||
}
|
||||
for _, ui := range uiresp.Results {
|
||||
updatedIds[ui.ID] = ui.Updated
|
||||
}
|
||||
if err := lim.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UsersInfo calls a users.info endpoint. This endpoint does not return results
|
||||
// straight away. It may return "pending ids", and when it does, it should be
|
||||
// called again to get the actual user info (see [Client.GetUsers]).
|
||||
func (cl *Client) UsersInfo(ctx context.Context, req *UsersInfoRequest) (*UserInfoResponse, error) {
|
||||
var ui UserInfoResponse
|
||||
if err := cl.callEdgeAPI(ctx, &ui, "users/info", req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ui, nil
|
||||
}
|
||||
|
||||
type ChannelsMembershipRequest struct {
|
||||
BaseRequest
|
||||
Channel string `json:"channel"`
|
||||
Users []string `json:"users"`
|
||||
AsAdmin bool `json:"as_admin"`
|
||||
}
|
||||
|
||||
type ChannelsMembershipResponse struct {
|
||||
Channel string `json:"channel"`
|
||||
NonMembers []string `json:"non_members"`
|
||||
baseResponse
|
||||
}
|
||||
|
||||
// ChannelsMembership calls channels/membership endpoint.
|
||||
func (cl *Client) ChannelsMembership(ctx context.Context, req *ChannelsMembershipRequest) (*ChannelsMembershipResponse, error) {
|
||||
var um ChannelsMembershipResponse
|
||||
if err := cl.callEdgeAPI(ctx, &um, "channels/membership", req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &um, nil
|
||||
}
|
||||
|
||||
// UserList lists users in the conversation(s).
|
||||
func (cl *Client) UsersList(ctx context.Context, channelIDs ...string) ([]User, error) {
|
||||
if len(channelIDs) == 0 {
|
||||
return nil, errors.New("no channel IDs provided")
|
||||
}
|
||||
channelIDs, dmIDs := splitDMs(channelIDs)
|
||||
var uu []User
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
if len(channelIDs) > 0 {
|
||||
eg.Go(func() error {
|
||||
u, err := cl.publicUserList(ctx, channelIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uu = append(uu, u...)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if len(dmIDs) > 0 {
|
||||
eg.Go(func() error {
|
||||
u, err := cl.directUserList(ctx, dmIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uu = append(uu, u...)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uu, nil
|
||||
}
|
||||
|
||||
func (cl *Client) publicUserList(ctx context.Context, channelIDs []string) ([]User, error) {
|
||||
const (
|
||||
// everyone = "everyone AND NOT bots AND NOT apps"
|
||||
everyone = "everyone"
|
||||
filter = "people"
|
||||
index = "users_by_display_name"
|
||||
|
||||
count = 50
|
||||
)
|
||||
req := UsersListRequest{
|
||||
Channels: channelIDs,
|
||||
Filter: everyone,
|
||||
PresentFirst: false,
|
||||
Index: index,
|
||||
Locale: "en-US",
|
||||
Marker: "",
|
||||
Count: count,
|
||||
}
|
||||
uu := make([]User, 0, count)
|
||||
lim := limiter.Tier3.Limiter()
|
||||
for {
|
||||
var ur UsersListResponse
|
||||
if err := cl.callEdgeAPI(ctx, &ur, "users/list", &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ur.Results) == 0 && ur.NextMarker == "" {
|
||||
break
|
||||
}
|
||||
uu = append(uu, ur.Results...)
|
||||
if ur.NextMarker == "" {
|
||||
break
|
||||
}
|
||||
req.Marker = ur.NextMarker
|
||||
if err := lim.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return uu, nil
|
||||
}
|
||||
|
||||
// directUserList tries to get users from the direct message channels. It is
|
||||
// much slower than getting users from the public channels, as it uses
|
||||
// conversations.view endpoint.
|
||||
func (cl *Client) directUserList(ctx context.Context, dmIDs []string) ([]User, error) {
|
||||
if len(dmIDs) == 0 {
|
||||
return nil, errors.New("no direct message IDs provided")
|
||||
}
|
||||
var ret []User
|
||||
lim := limiter.Tier3.Limiter()
|
||||
for _, id := range dmIDs {
|
||||
resp, err := cl.ConversationsView(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, resp.Users...)
|
||||
if err := lim.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func splitDMs(IDs []string) (chans []string, dms []string) {
|
||||
for _, id := range IDs {
|
||||
if len(id) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(id) > 0 && id[0] == 'D' {
|
||||
dms = append(dms, id)
|
||||
} else {
|
||||
chans = append(chans, id)
|
||||
}
|
||||
}
|
||||
return chans, dms
|
||||
}
|
||||
@@ -5,14 +5,14 @@ import "net/http"
|
||||
type UserAgentTransport struct {
|
||||
roundTripper http.RoundTripper
|
||||
userAgent string
|
||||
cookie string
|
||||
cookies []*http.Cookie
|
||||
}
|
||||
|
||||
func New(roundTripper http.RoundTripper, userAgent string, cookie string) *UserAgentTransport {
|
||||
func New(roundTripper http.RoundTripper, userAgent string, cookies []*http.Cookie) *UserAgentTransport {
|
||||
return &UserAgentTransport{
|
||||
roundTripper: roundTripper,
|
||||
userAgent: userAgent,
|
||||
cookie: cookie,
|
||||
cookies: cookies,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ func New(roundTripper http.RoundTripper, userAgent string, cookie string) *UserA
|
||||
func (t *UserAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
clonedReq := req.Clone(req.Context())
|
||||
clonedReq.Header.Set("User-Agent", t.userAgent)
|
||||
clonedReq.Header.Set("Cookie", "d="+t.cookie+";d-s=1744415074")
|
||||
|
||||
for _, cookie := range t.cookies {
|
||||
clonedReq.AddCookie(cookie)
|
||||
}
|
||||
|
||||
return t.roundTripper.RoundTrip(clonedReq)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user