Files
openclaw/extensions/slack/src/client.ts

104 lines
3.4 KiB
TypeScript

import { type RetryOptions, type WebClientOptions, WebClient } from "@slack/web-api";
import { HttpsProxyAgent } from "https-proxy-agent";
import { resolveEnvHttpProxyUrl } from "openclaw/plugin-sdk/infra-runtime";
export const SLACK_DEFAULT_RETRY_OPTIONS: RetryOptions = {
retries: 2,
factor: 2,
minTimeout: 500,
maxTimeout: 3000,
randomize: true,
};
export const SLACK_WRITE_RETRY_OPTIONS: RetryOptions = {
retries: 0,
};
/**
* Check whether a hostname is excluded from proxying by `NO_PROXY` / `no_proxy`.
* Supports comma-separated entries with optional leading dots (e.g. `.slack.com`).
*/
function isHostExcludedByNoProxy(hostname: string, env: NodeJS.ProcessEnv = process.env): boolean {
const raw = env.no_proxy ?? env.NO_PROXY;
if (!raw) {
return false;
}
const entries = raw
.split(/[,\s]+/)
.map((e) => e.trim().toLowerCase())
.filter(Boolean);
const lower = hostname.toLowerCase();
for (const entry of entries) {
if (entry === "*") {
return true;
}
// Strip optional wildcard/leading dot so `*.slack.com` and `.slack.com`
// match both `slack.com` (apex) and Slack subdomains.
const bare = entry.startsWith("*.")
? entry.slice(2)
: entry.startsWith(".")
? entry.slice(1)
: entry;
if (lower === bare || lower.endsWith(`.${bare}`)) {
return true;
}
}
return false;
}
/**
* Build an HTTPS proxy agent from env vars (HTTPS_PROXY, HTTP_PROXY, etc.)
* for use as the `agent` option in Slack WebClient and Socket Mode connections.
*
* When set, this agent is forwarded through @slack/bolt → @slack/socket-mode →
* SlackWebSocket as the `httpAgent`, which the `ws` library uses to tunnel the
* WebSocket upgrade request through the proxy. This fixes Socket Mode in
* environments where outbound traffic must go through an HTTP CONNECT proxy.
*
* Respects `NO_PROXY` / `no_proxy` — if `*.slack.com` (or a matching pattern)
* appears in the exclusion list, returns `undefined` so the connection is direct.
*
* Returns `undefined` when no proxy env var is configured or when Slack hosts
* are excluded by `NO_PROXY`.
*/
function resolveSlackProxyAgent(): HttpsProxyAgent<string> | undefined {
const proxyUrl = resolveEnvHttpProxyUrl("https");
if (!proxyUrl) {
return undefined;
}
// Slack Socket Mode connects to these hosts; skip proxy if excluded.
if (isHostExcludedByNoProxy("slack.com")) {
return undefined;
}
try {
return new HttpsProxyAgent(proxyUrl);
} catch {
// Malformed proxy URL — degrade gracefully to direct connection.
return undefined;
}
}
export function resolveSlackWebClientOptions(options: WebClientOptions = {}): WebClientOptions {
return {
...options,
agent: options.agent ?? resolveSlackProxyAgent(),
retryConfig: options.retryConfig ?? SLACK_DEFAULT_RETRY_OPTIONS,
};
}
export function resolveSlackWriteClientOptions(options: WebClientOptions = {}): WebClientOptions {
return {
...options,
agent: options.agent ?? resolveSlackProxyAgent(),
retryConfig: options.retryConfig ?? SLACK_WRITE_RETRY_OPTIONS,
};
}
export function createSlackWebClient(token: string, options: WebClientOptions = {}) {
return new WebClient(token, resolveSlackWebClientOptions(options));
}
export function createSlackWriteClient(token: string, options: WebClientOptions = {}) {
return new WebClient(token, resolveSlackWriteClientOptions(options));
}