slack-term 1.7.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -2
- package/dist/cli.js +132 -31
- package/package.json +12 -5
- package/ts/auth.ts +242 -0
- package/ts/cli.ts +780 -476
- package/ts/profiles.ts +17 -12
- package/ts/rtm.ts +180 -0
- package/ts/slack.ts +116 -8
- package/ts/tail.ts +279 -0
package/ts/profiles.ts
CHANGED
|
@@ -29,8 +29,12 @@ type ProfileStore = {
|
|
|
29
29
|
profiles: Record<string, Profile>;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
function home(): string {
|
|
33
|
+
return process.env.HOME || homedir();
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
function profilesPath(): string {
|
|
33
|
-
return join(
|
|
37
|
+
return join(home(), ".config", "slack-cli", "profiles.json");
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
function localLockfilePath(): string {
|
|
@@ -38,7 +42,7 @@ function localLockfilePath(): string {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
function globalLockfilePath(): string {
|
|
41
|
-
return join(
|
|
45
|
+
return join(home(), ".slack-cli", "workspace");
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
function load(): ProfileStore {
|
|
@@ -115,18 +119,19 @@ export function resolveToken(workspaceFlag?: string): string {
|
|
|
115
119
|
const names = Object.keys(profiles);
|
|
116
120
|
const envToken = process.env.SLACK_MCP_XOXP_TOKEN;
|
|
117
121
|
|
|
118
|
-
// Conflict: env token and profiles are mutually exclusive
|
|
122
|
+
// Conflict: env token and profiles are mutually exclusive — prefer profiles
|
|
119
123
|
if (envToken && names.length > 0) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
if (!(globalThis as Record<string, unknown>).__slackEnvWarnShown) {
|
|
125
|
+
(globalThis as Record<string, unknown>).__slackEnvWarnShown = true;
|
|
126
|
+
console.error(
|
|
127
|
+
"Warning: SLACK_MCP_XOXP_TOKEN is set but workspace profiles exist - using profiles.\n" +
|
|
128
|
+
" Run: unset SLACK_MCP_XOXP_TOKEN (and remove from ~/.zshrc or ~/.bashrc)",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
} else if (envToken) {
|
|
132
|
+
return envToken;
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
// Env token only (no profiles) — highest priority when unambiguous
|
|
128
|
-
if (envToken) return envToken;
|
|
129
|
-
|
|
130
135
|
// Explicit per-command selection
|
|
131
136
|
const selected = workspaceFlag ?? process.env.SLACK_WORKSPACE;
|
|
132
137
|
if (selected) {
|
|
@@ -157,7 +162,7 @@ export function resolveToken(workspaceFlag?: string): string {
|
|
|
157
162
|
|
|
158
163
|
// No profiles and no env token
|
|
159
164
|
if (names.length === 0) {
|
|
160
|
-
throw new Error("No profiles configured. Run: slack
|
|
165
|
+
throw new Error("No profiles configured. Run: slack auth login");
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
// Profiles exist but none selected — never silently pick one
|
package/ts/rtm.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { type Json, userInfoPair, clientBoot as clientBootImpl } from "./slack.ts";
|
|
2
|
+
import { resolveDateMarkup, resolveMentions } from "./format.ts";
|
|
3
|
+
|
|
4
|
+
export type RtmPollOpts = {
|
|
5
|
+
thread?: string;
|
|
6
|
+
me?: boolean;
|
|
7
|
+
myUserId?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Minimal WebSocket interface — avoids requiring DOM lib in tsconfig
|
|
11
|
+
interface WsLike {
|
|
12
|
+
addEventListener(type: string, handler: (event: unknown) => void): void;
|
|
13
|
+
send(data: string): void;
|
|
14
|
+
close(): void;
|
|
15
|
+
}
|
|
16
|
+
interface WsConstructor {
|
|
17
|
+
new(url: string): WsLike;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function slackTsToIso(tsRaw: string): string {
|
|
21
|
+
const [secStr, fracStr = "000000"] = tsRaw.split(".");
|
|
22
|
+
const d = new Date(Number(secStr) * 1000);
|
|
23
|
+
const y = d.getUTCFullYear();
|
|
24
|
+
const mo = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
25
|
+
const da = String(d.getUTCDate()).padStart(2, "0");
|
|
26
|
+
const h = String(d.getUTCHours()).padStart(2, "0");
|
|
27
|
+
const mi = String(d.getUTCMinutes()).padStart(2, "0");
|
|
28
|
+
const s = String(d.getUTCSeconds()).padStart(2, "0");
|
|
29
|
+
const frac = fracStr.padEnd(6, "0").slice(0, 6);
|
|
30
|
+
return `${y}-${mo}-${da}T${h}:${mi}:${s}.${frac}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function formatRtmLine(
|
|
34
|
+
token: string,
|
|
35
|
+
m: Record<string, Json>,
|
|
36
|
+
cache: Map<string, string>,
|
|
37
|
+
): Promise<string> {
|
|
38
|
+
const rawTs = typeof m.ts === "string" ? m.ts : "0.000000";
|
|
39
|
+
const stamp = slackTsToIso(rawTs);
|
|
40
|
+
let handle = "?";
|
|
41
|
+
if (typeof m.user === "string") {
|
|
42
|
+
const uid = m.user;
|
|
43
|
+
const handleKey = "@" + uid;
|
|
44
|
+
if (!cache.has(handleKey)) {
|
|
45
|
+
const [, h] = await userInfoPair(token, uid);
|
|
46
|
+
cache.set(handleKey, h);
|
|
47
|
+
}
|
|
48
|
+
handle = cache.get(handleKey) ?? uid;
|
|
49
|
+
} else if (typeof m.username === "string") {
|
|
50
|
+
handle = m.username;
|
|
51
|
+
}
|
|
52
|
+
const raw = typeof m.text === "string" ? m.text : "";
|
|
53
|
+
const resolved = resolveDateMarkup(await resolveMentions(token, raw, cache));
|
|
54
|
+
const lines = resolved.split("\n");
|
|
55
|
+
const body = lines[0] + (lines.length > 1 ? "\n" + lines.slice(1).map((l) => ` ${l}`).join("\n") : "");
|
|
56
|
+
return `${stamp} @${handle}: ${body}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function connectAndStream(
|
|
60
|
+
WS: WsConstructor,
|
|
61
|
+
token: string,
|
|
62
|
+
wsUrl: string,
|
|
63
|
+
channelId: string,
|
|
64
|
+
opts: RtmPollOpts,
|
|
65
|
+
seen: Set<string>,
|
|
66
|
+
cache: Map<string, string>,
|
|
67
|
+
signal?: AbortSignal,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
// If already aborted before we even create the WS, resolve immediately.
|
|
70
|
+
if (signal?.aborted) return;
|
|
71
|
+
return new Promise<void>((resolve) => {
|
|
72
|
+
const ws = new WS(wsUrl);
|
|
73
|
+
|
|
74
|
+
signal?.addEventListener("abort", () => {
|
|
75
|
+
ws.close();
|
|
76
|
+
resolve();
|
|
77
|
+
}, { once: true } as AddEventListenerOptions);
|
|
78
|
+
|
|
79
|
+
ws.addEventListener("open", () => {
|
|
80
|
+
process.stderr.write("Connected via RTM (experimental).\n");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
ws.addEventListener("close", () => resolve());
|
|
84
|
+
|
|
85
|
+
ws.addEventListener("error", () => resolve());
|
|
86
|
+
|
|
87
|
+
ws.addEventListener("message", (event: unknown) => {
|
|
88
|
+
void (async () => {
|
|
89
|
+
try {
|
|
90
|
+
let data: Record<string, Json>;
|
|
91
|
+
try {
|
|
92
|
+
data = JSON.parse(String((event as { data?: unknown }).data)) as Record<string, Json>;
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const type = typeof data.type === "string" ? data.type : "";
|
|
98
|
+
|
|
99
|
+
if (type === "ping") {
|
|
100
|
+
ws.send(JSON.stringify({ type: "pong", reply_to: data.id ?? 0 }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (type !== "message") return;
|
|
105
|
+
|
|
106
|
+
const msgChannel = typeof data.channel === "string" ? data.channel : "";
|
|
107
|
+
if (msgChannel !== channelId) return;
|
|
108
|
+
|
|
109
|
+
const ts = typeof data.ts === "string" ? data.ts : "";
|
|
110
|
+
if (!ts || seen.has(ts)) return;
|
|
111
|
+
|
|
112
|
+
const subtype = typeof data.subtype === "string" ? data.subtype : "";
|
|
113
|
+
if (subtype === "message_changed" || subtype === "message_deleted") return;
|
|
114
|
+
|
|
115
|
+
if (opts.thread) {
|
|
116
|
+
const parentTs = typeof data.thread_ts === "string" ? data.thread_ts : ts;
|
|
117
|
+
if (parentTs !== opts.thread && ts !== opts.thread) return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (opts.me && opts.myUserId) {
|
|
121
|
+
const text = typeof data.text === "string" ? data.text : "";
|
|
122
|
+
if (!text.includes(`<@${opts.myUserId}>`)) return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
seen.add(ts);
|
|
126
|
+
if (seen.size > 1000) {
|
|
127
|
+
const oldest = seen.values().next().value;
|
|
128
|
+
if (oldest !== undefined) seen.delete(oldest);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const line = await formatRtmLine(token, data, cache);
|
|
132
|
+
process.stdout.write(line + "\n");
|
|
133
|
+
} catch (e: unknown) {
|
|
134
|
+
process.stderr.write(`RTM message error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const MAX_RETRIES = 3;
|
|
142
|
+
const RETRY_DELAY_MS = 2000;
|
|
143
|
+
|
|
144
|
+
export const _internals = {
|
|
145
|
+
sleep: (ms: number): Promise<void> => new Promise((res) => setTimeout(res, ms)),
|
|
146
|
+
clientBoot: clientBootImpl,
|
|
147
|
+
getWebSocket: (): WsConstructor | undefined => {
|
|
148
|
+
const ws = (globalThis as Record<string, unknown>).WebSocket;
|
|
149
|
+
return typeof ws === "function" ? ws as WsConstructor : undefined;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export async function tailRTMImpl(
|
|
154
|
+
token: string,
|
|
155
|
+
cookie: string,
|
|
156
|
+
channelId: string,
|
|
157
|
+
opts: RtmPollOpts,
|
|
158
|
+
seen: Set<string>,
|
|
159
|
+
cache: Map<string, string>,
|
|
160
|
+
signal?: AbortSignal,
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
const WS = _internals.getWebSocket();
|
|
163
|
+
if (!WS) return;
|
|
164
|
+
|
|
165
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
166
|
+
if (signal?.aborted) return;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const { wsUrl } = await _internals.clientBoot(token, cookie);
|
|
170
|
+
await connectAndStream(WS, token, wsUrl, channelId, opts, seen, cache, signal);
|
|
171
|
+
} catch {
|
|
172
|
+
// boot or connection error — retry
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (signal?.aborted) return;
|
|
176
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
177
|
+
await _internals.sleep(RETRY_DELAY_MS);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
package/ts/slack.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
// Slack Web API client (user token, Authorization: Bearer)
|
|
2
2
|
|
|
3
|
+
export class RateLimitError extends Error {
|
|
4
|
+
retryAfter: number;
|
|
5
|
+
constructor(retryAfter: number) {
|
|
6
|
+
super(`Slack rate limited — retry after ${retryAfter}s`);
|
|
7
|
+
this.name = "RateLimitError";
|
|
8
|
+
this.retryAfter = retryAfter;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
export type Json =
|
|
4
13
|
| string
|
|
5
14
|
| number
|
|
@@ -23,9 +32,14 @@ async function call(token: string, method: string, init: RequestInit, cookie?: s
|
|
|
23
32
|
...extraHeaders,
|
|
24
33
|
},
|
|
25
34
|
});
|
|
35
|
+
if (res.status === 429) {
|
|
36
|
+
const retryAfter = parseInt(res.headers.get("retry-after") ?? "60", 10);
|
|
37
|
+
throw new RateLimitError(isNaN(retryAfter) ? 60 : retryAfter);
|
|
38
|
+
}
|
|
26
39
|
const body = (await res.json()) as { ok?: boolean; error?: string } & Record<string, Json>;
|
|
27
40
|
if (body.ok !== true) {
|
|
28
41
|
const err = body.error ?? "unknown";
|
|
42
|
+
if (err === "ratelimited") throw new RateLimitError(60);
|
|
29
43
|
if (err === "invalid_auth" && token.startsWith("xoxc-") && !cookie) {
|
|
30
44
|
throw new Error(
|
|
31
45
|
`Desktop app token (xoxc-) is not accepted by the public Slack API.\n` +
|
|
@@ -52,9 +66,14 @@ async function callSession(token: string, method: string, init: RequestInit, coo
|
|
|
52
66
|
...extraHeaders,
|
|
53
67
|
},
|
|
54
68
|
});
|
|
69
|
+
if (res.status === 429) {
|
|
70
|
+
const retryAfter = parseInt(res.headers.get("retry-after") ?? "60", 10);
|
|
71
|
+
throw new RateLimitError(isNaN(retryAfter) ? 60 : retryAfter);
|
|
72
|
+
}
|
|
55
73
|
const body = (await res.json()) as { ok?: boolean; error?: string } & Record<string, Json>;
|
|
56
74
|
if (body.ok !== true) {
|
|
57
75
|
const err = body.error ?? "unknown";
|
|
76
|
+
if (err === "ratelimited") throw new RateLimitError(60);
|
|
58
77
|
if ((err === "invalid_auth" || err === "not_authed") && !token.startsWith("xoxc-")) {
|
|
59
78
|
throw new Error(
|
|
60
79
|
`The draft API requires a desktop app session token (xoxc-).\n` +
|
|
@@ -96,20 +115,24 @@ function post(token: string, method: string, body: Record<string, Json>): Promis
|
|
|
96
115
|
});
|
|
97
116
|
}
|
|
98
117
|
|
|
99
|
-
export async function authTest(token: string): Promise<{ team: string; teamId: string; url: string; user: string }> {
|
|
118
|
+
export async function authTest(token: string): Promise<{ team: string; teamId: string; url: string; user: string; userId: string }> {
|
|
100
119
|
const resp = (await get(token, "auth.test", {})) as {
|
|
101
|
-
team?: string; team_id?: string; url?: string; user?: string;
|
|
120
|
+
team?: string; team_id?: string; url?: string; user?: string; user_id?: string;
|
|
102
121
|
};
|
|
103
122
|
return {
|
|
104
123
|
team: resp.team ?? "",
|
|
105
124
|
teamId: resp.team_id ?? "",
|
|
106
125
|
url: resp.url ?? "",
|
|
107
126
|
user: resp.user ?? "",
|
|
127
|
+
userId: resp.user_id ?? "",
|
|
108
128
|
};
|
|
109
129
|
}
|
|
110
130
|
|
|
111
|
-
export async function history(token: string, channel: string, limit = 20): Promise<Json> {
|
|
112
|
-
|
|
131
|
+
export async function history(token: string, channel: string, limit = 20, oldest?: string, cursor?: string): Promise<Json> {
|
|
132
|
+
const params: Record<string, string> = { channel, limit: String(limit) };
|
|
133
|
+
if (oldest !== undefined) params.oldest = oldest;
|
|
134
|
+
if (cursor !== undefined) params.cursor = cursor;
|
|
135
|
+
return get(token, "conversations.history", params);
|
|
113
136
|
}
|
|
114
137
|
|
|
115
138
|
export async function replies(
|
|
@@ -177,6 +200,44 @@ export async function send(
|
|
|
177
200
|
return resp.ts ?? "";
|
|
178
201
|
}
|
|
179
202
|
|
|
203
|
+
export async function scheduleMessage(
|
|
204
|
+
token: string,
|
|
205
|
+
channel: string,
|
|
206
|
+
text: string,
|
|
207
|
+
postAt: number,
|
|
208
|
+
threadTs?: string,
|
|
209
|
+
): Promise<string> {
|
|
210
|
+
const body: Record<string, Json> = {
|
|
211
|
+
channel,
|
|
212
|
+
text,
|
|
213
|
+
post_at: postAt,
|
|
214
|
+
blocks: [{ type: "markdown", text }],
|
|
215
|
+
};
|
|
216
|
+
if (threadTs !== undefined) body.thread_ts = threadTs;
|
|
217
|
+
const resp = (await post(token, "chat.scheduleMessage", body)) as { scheduled_message_id?: string };
|
|
218
|
+
return resp.scheduled_message_id ?? "";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function listScheduledMessages(
|
|
222
|
+
token: string,
|
|
223
|
+
channel?: string,
|
|
224
|
+
): Promise<Json> {
|
|
225
|
+
const params: Record<string, string> = {};
|
|
226
|
+
if (channel) params.channel = channel;
|
|
227
|
+
return get(token, "chat.scheduledMessages.list", params);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function deleteScheduledMessage(
|
|
231
|
+
token: string,
|
|
232
|
+
channel: string,
|
|
233
|
+
scheduledMessageId: string,
|
|
234
|
+
): Promise<void> {
|
|
235
|
+
await post(token, "chat.deleteScheduledMessage", {
|
|
236
|
+
channel,
|
|
237
|
+
scheduled_message_id: scheduledMessageId,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
180
241
|
export async function editMessage(
|
|
181
242
|
token: string,
|
|
182
243
|
channel: string,
|
|
@@ -224,19 +285,27 @@ function normName(s: string): string {
|
|
|
224
285
|
return s.toLowerCase().replace(/[-_\s]/g, "");
|
|
225
286
|
}
|
|
226
287
|
|
|
227
|
-
/** Parse a Slack permalink, returning channel ID
|
|
288
|
+
/** Parse a Slack permalink, returning channel ID, optional message ts, and optional thread_ts.
|
|
228
289
|
* Supports both forms:
|
|
229
290
|
* https://app.slack.com/client/T.../C...[/p1700000000000100]
|
|
230
|
-
* https://<ws>.slack.com/archives/C...[/p1700000000000100][?thread_ts=...]
|
|
291
|
+
* https://<ws>.slack.com/archives/C...[/p1700000000000100][?thread_ts=...][&cid=C...]
|
|
231
292
|
*/
|
|
232
|
-
export function parseSlackPermalink(s: string): { channel: string; ts?: string } | undefined {
|
|
293
|
+
export function parseSlackPermalink(s: string): { channel: string; ts?: string; threadTs?: string } | undefined {
|
|
233
294
|
const m = s.match(
|
|
234
295
|
/(?:app\.slack\.com\/client\/T[A-Za-z0-9]+|[A-Za-z0-9-]+\.slack\.com\/archives)\/([A-Za-z0-9]+)(?:\/p(\d{10})(\d{6}))?/,
|
|
235
296
|
);
|
|
236
297
|
if (!m) return undefined;
|
|
237
298
|
const channel = m[1]!;
|
|
238
299
|
const ts = m[2] && m[3] ? `${m[2]}.${m[3]}` : undefined;
|
|
239
|
-
|
|
300
|
+
const qsStart = s.indexOf("?");
|
|
301
|
+
const qs = qsStart >= 0 ? new URLSearchParams(s.slice(qsStart)) : undefined;
|
|
302
|
+
const threadTs = qs?.get("thread_ts") ?? undefined;
|
|
303
|
+
const cidFromQs = qs?.get("cid") ?? undefined;
|
|
304
|
+
const resolvedChannel = cidFromQs ?? channel;
|
|
305
|
+
const result: { channel: string; ts?: string; threadTs?: string } = { channel: resolvedChannel };
|
|
306
|
+
if (ts) result.ts = ts;
|
|
307
|
+
if (threadTs) result.threadTs = threadTs;
|
|
308
|
+
return result;
|
|
240
309
|
}
|
|
241
310
|
|
|
242
311
|
function parseSlackUrl(s: string): string | undefined {
|
|
@@ -360,6 +429,23 @@ export async function userName(token: string, userId: string): Promise<string> {
|
|
|
360
429
|
}
|
|
361
430
|
}
|
|
362
431
|
|
|
432
|
+
export async function listUsers(token: string): Promise<Json> {
|
|
433
|
+
const allMembers: Json[] = [];
|
|
434
|
+
let cursor = "";
|
|
435
|
+
while (true) {
|
|
436
|
+
const params: Record<string, string> = { limit: "200" };
|
|
437
|
+
if (cursor) params.cursor = cursor;
|
|
438
|
+
const resp = (await get(token, "users.list", params)) as {
|
|
439
|
+
members?: Json[];
|
|
440
|
+
response_metadata?: { next_cursor?: string };
|
|
441
|
+
};
|
|
442
|
+
allMembers.push(...(resp.members ?? []));
|
|
443
|
+
cursor = resp.response_metadata?.next_cursor ?? "";
|
|
444
|
+
if (!cursor) break;
|
|
445
|
+
}
|
|
446
|
+
return { members: allMembers };
|
|
447
|
+
}
|
|
448
|
+
|
|
363
449
|
// Draft API (internal — requires xoxc session token + xoxd cookie)
|
|
364
450
|
export async function listDrafts(token: string, cookie?: string): Promise<Json> {
|
|
365
451
|
return postSession(token, "drafts.list", {}, cookie);
|
|
@@ -412,6 +498,14 @@ export async function conversationInfoSession(token: string, channelId: string,
|
|
|
412
498
|
return postSession(token, "conversations.info", { channel: channelId }, cookie);
|
|
413
499
|
}
|
|
414
500
|
|
|
501
|
+
export async function userInfo(token: string, userId: string): Promise<Json> {
|
|
502
|
+
return get(token, "users.info", { user: userId });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export async function conversationInfo(token: string, channelId: string): Promise<Json> {
|
|
506
|
+
return get(token, "conversations.info", { channel: channelId });
|
|
507
|
+
}
|
|
508
|
+
|
|
415
509
|
export async function deleteDraft(token: string, draftId: string, cookie?: string): Promise<Json> {
|
|
416
510
|
const nowTs = (Date.now() / 1000).toFixed(6);
|
|
417
511
|
return postSession(token, "drafts.delete", { draft_id: draftId, client_last_updated_ts: nowTs }, cookie);
|
|
@@ -476,6 +570,20 @@ export async function uploadFile(
|
|
|
476
570
|
return result;
|
|
477
571
|
}
|
|
478
572
|
|
|
573
|
+
// client.boot — internal endpoint that returns a WebSocket URL for RTM
|
|
574
|
+
export async function clientBoot(token: string, cookie: string): Promise<{ wsUrl: string; selfId: string }> {
|
|
575
|
+
const resp = (await postSession(token, "client.boot", {}, cookie)) as Record<string, Json>;
|
|
576
|
+
const wsUrl = typeof resp.url === "string" ? resp.url : undefined;
|
|
577
|
+
if (!wsUrl) {
|
|
578
|
+
const keys = Object.keys(resp).join(", ");
|
|
579
|
+
throw new Error(`client.boot did not return a WebSocket URL. Keys: ${keys}`);
|
|
580
|
+
}
|
|
581
|
+
const self_ = resp.self && typeof resp.self === "object" && !Array.isArray(resp.self)
|
|
582
|
+
? resp.self as Record<string, Json>
|
|
583
|
+
: {};
|
|
584
|
+
return { wsUrl, selfId: typeof self_.id === "string" ? self_.id : "" };
|
|
585
|
+
}
|
|
586
|
+
|
|
479
587
|
// Safe nested access
|
|
480
588
|
export function getPath(obj: Json, path: readonly (string | number)[]): Json | undefined {
|
|
481
589
|
let cur: Json | undefined = obj;
|