slack-term 1.7.0 → 1.13.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 +124 -32
- package/package.json +12 -5
- package/ts/auth.ts +242 -0
- package/ts/cli.ts +763 -472
- package/ts/profiles.ts +17 -12
- package/ts/slack.ts +90 -4
- package/ts/tail.ts +268 -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/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,
|
|
@@ -360,6 +421,23 @@ export async function userName(token: string, userId: string): Promise<string> {
|
|
|
360
421
|
}
|
|
361
422
|
}
|
|
362
423
|
|
|
424
|
+
export async function listUsers(token: string): Promise<Json> {
|
|
425
|
+
const allMembers: Json[] = [];
|
|
426
|
+
let cursor = "";
|
|
427
|
+
while (true) {
|
|
428
|
+
const params: Record<string, string> = { limit: "200" };
|
|
429
|
+
if (cursor) params.cursor = cursor;
|
|
430
|
+
const resp = (await get(token, "users.list", params)) as {
|
|
431
|
+
members?: Json[];
|
|
432
|
+
response_metadata?: { next_cursor?: string };
|
|
433
|
+
};
|
|
434
|
+
allMembers.push(...(resp.members ?? []));
|
|
435
|
+
cursor = resp.response_metadata?.next_cursor ?? "";
|
|
436
|
+
if (!cursor) break;
|
|
437
|
+
}
|
|
438
|
+
return { members: allMembers };
|
|
439
|
+
}
|
|
440
|
+
|
|
363
441
|
// Draft API (internal — requires xoxc session token + xoxd cookie)
|
|
364
442
|
export async function listDrafts(token: string, cookie?: string): Promise<Json> {
|
|
365
443
|
return postSession(token, "drafts.list", {}, cookie);
|
|
@@ -412,6 +490,14 @@ export async function conversationInfoSession(token: string, channelId: string,
|
|
|
412
490
|
return postSession(token, "conversations.info", { channel: channelId }, cookie);
|
|
413
491
|
}
|
|
414
492
|
|
|
493
|
+
export async function userInfo(token: string, userId: string): Promise<Json> {
|
|
494
|
+
return get(token, "users.info", { user: userId });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export async function conversationInfo(token: string, channelId: string): Promise<Json> {
|
|
498
|
+
return get(token, "conversations.info", { channel: channelId });
|
|
499
|
+
}
|
|
500
|
+
|
|
415
501
|
export async function deleteDraft(token: string, draftId: string, cookie?: string): Promise<Json> {
|
|
416
502
|
const nowTs = (Date.now() / 1000).toFixed(6);
|
|
417
503
|
return postSession(token, "drafts.delete", { draft_id: draftId, client_last_updated_ts: nowTs }, cookie);
|
package/ts/tail.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { history, resolveChannel, userInfoPair, authTest, conversationInfo, RateLimitError, type Json } from "./slack.ts";
|
|
2
|
+
import { resolveDateMarkup, resolveMentions } from "./format.ts";
|
|
3
|
+
|
|
4
|
+
export function parseSince(s: string): number {
|
|
5
|
+
const m = s.match(/^(\d+(?:\.\d+)?)(s|m|h|d)$/);
|
|
6
|
+
if (!m) throw new Error(`Invalid --since format: "${s}" (expected e.g. 10m, 2h, 1d)`);
|
|
7
|
+
const n = parseFloat(m[1]!);
|
|
8
|
+
const unit = m[2]! as "s" | "m" | "h" | "d";
|
|
9
|
+
switch (unit) {
|
|
10
|
+
case "s": return n;
|
|
11
|
+
case "m": return n * 60;
|
|
12
|
+
case "h": return n * 3600;
|
|
13
|
+
case "d": return n * 86400;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const _internals = {
|
|
18
|
+
sleep: (ms: number): Promise<void> => new Promise((res) => setTimeout(res, ms)),
|
|
19
|
+
now: (): number => Date.now(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function asRecord(v: Json | undefined): Record<string, Json> {
|
|
23
|
+
return v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, Json>) : {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function asArray(v: Json | undefined): Json[] {
|
|
27
|
+
return Array.isArray(v) ? v : [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function slackTsToIso(tsRaw: string): string {
|
|
31
|
+
const [secStr, fracStr = "000000"] = tsRaw.split(".");
|
|
32
|
+
const d = new Date(Number(secStr) * 1000);
|
|
33
|
+
const y = d.getUTCFullYear();
|
|
34
|
+
const mo = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
35
|
+
const da = String(d.getUTCDate()).padStart(2, "0");
|
|
36
|
+
const h = String(d.getUTCHours()).padStart(2, "0");
|
|
37
|
+
const mi = String(d.getUTCMinutes()).padStart(2, "0");
|
|
38
|
+
const s = String(d.getUTCSeconds()).padStart(2, "0");
|
|
39
|
+
const frac = fracStr.padEnd(6, "0").slice(0, 6);
|
|
40
|
+
return `${y}-${mo}-${da}T${h}:${mi}:${s}.${frac}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function formatTailLine(
|
|
44
|
+
token: string,
|
|
45
|
+
m: Record<string, Json>,
|
|
46
|
+
cache: Map<string, string>,
|
|
47
|
+
chLabel?: string,
|
|
48
|
+
): Promise<string> {
|
|
49
|
+
const rawTs = typeof m.ts === "string" ? m.ts : "0.000000";
|
|
50
|
+
const stamp = slackTsToIso(rawTs);
|
|
51
|
+
let handle = "?";
|
|
52
|
+
if (typeof m.user === "string") {
|
|
53
|
+
const uid = m.user;
|
|
54
|
+
const handleKey = "@" + uid;
|
|
55
|
+
if (!cache.has(handleKey)) {
|
|
56
|
+
const [, h] = await userInfoPair(token, uid);
|
|
57
|
+
cache.set(handleKey, h);
|
|
58
|
+
}
|
|
59
|
+
handle = cache.get(handleKey) ?? uid;
|
|
60
|
+
} else if (typeof m.username === "string") {
|
|
61
|
+
handle = m.username;
|
|
62
|
+
}
|
|
63
|
+
const raw = typeof m.text === "string" ? m.text : "";
|
|
64
|
+
const resolved = resolveDateMarkup(await resolveMentions(token, raw, cache));
|
|
65
|
+
const lines = resolved.split("\n");
|
|
66
|
+
const body = lines[0] + (lines.length > 1 ? "\n" + lines.slice(1).map((l) => ` ${l}`).join("\n") : "");
|
|
67
|
+
const who = chLabel ? `${chLabel} @${handle}` : `@${handle}`;
|
|
68
|
+
return `${stamp} ${who}: ${body}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type PollOpts = {
|
|
72
|
+
thread?: string;
|
|
73
|
+
me?: boolean;
|
|
74
|
+
myUserId?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export async function pollCycle(
|
|
78
|
+
token: string,
|
|
79
|
+
channelId: string,
|
|
80
|
+
cursor: string,
|
|
81
|
+
opts: PollOpts,
|
|
82
|
+
seen: Set<string>,
|
|
83
|
+
cache: Map<string, string>,
|
|
84
|
+
pageCursor?: string,
|
|
85
|
+
): Promise<{ newCursor: string; lines: string[]; hasMore: boolean; nextPageCursor: string | undefined }> {
|
|
86
|
+
const histResp = asRecord((await history(
|
|
87
|
+
token,
|
|
88
|
+
channelId,
|
|
89
|
+
20,
|
|
90
|
+
pageCursor ? undefined : (cursor || undefined),
|
|
91
|
+
pageCursor,
|
|
92
|
+
)) as Json);
|
|
93
|
+
// history returns newest-first; reverse to emit oldest-first
|
|
94
|
+
const msgs = asArray(histResp.messages).map(asRecord).reverse();
|
|
95
|
+
|
|
96
|
+
const meta = asRecord(histResp.response_metadata as Json | undefined);
|
|
97
|
+
const nextPageCursor = (histResp.has_more === true && typeof meta.next_cursor === "string")
|
|
98
|
+
? meta.next_cursor
|
|
99
|
+
: undefined;
|
|
100
|
+
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
let newCursor = cursor;
|
|
103
|
+
|
|
104
|
+
for (const m of msgs) {
|
|
105
|
+
const ts = typeof m.ts === "string" ? m.ts : "";
|
|
106
|
+
if (!ts || seen.has(ts)) continue;
|
|
107
|
+
|
|
108
|
+
// Skip edit/delete events; only display original messages
|
|
109
|
+
const subtype = typeof m.subtype === "string" ? m.subtype : "";
|
|
110
|
+
if (subtype === "message_changed" || subtype === "message_deleted") continue;
|
|
111
|
+
|
|
112
|
+
if (opts.thread) {
|
|
113
|
+
const parentTs = typeof m.thread_ts === "string" ? m.thread_ts : ts;
|
|
114
|
+
if (parentTs !== opts.thread && ts !== opts.thread) continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (opts.me && opts.myUserId) {
|
|
118
|
+
const text = typeof m.text === "string" ? m.text : "";
|
|
119
|
+
if (!text.includes(`<@${opts.myUserId}>`)) continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
seen.add(ts);
|
|
123
|
+
if (seen.size > 1000) {
|
|
124
|
+
const oldest = seen.values().next().value;
|
|
125
|
+
if (oldest !== undefined) seen.delete(oldest);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
lines.push(await formatTailLine(token, m, cache));
|
|
129
|
+
newCursor = ts;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { newCursor, lines, hasMore: histResp.has_more === true, nextPageCursor };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type TailOpts = {
|
|
136
|
+
since?: string;
|
|
137
|
+
thread?: string;
|
|
138
|
+
me?: boolean;
|
|
139
|
+
interval?: number;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export async function cmdTail(
|
|
143
|
+
token: string,
|
|
144
|
+
target: string | undefined,
|
|
145
|
+
opts: TailOpts = {},
|
|
146
|
+
signal?: AbortSignal,
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
if (opts.me && !target) {
|
|
149
|
+
console.error(
|
|
150
|
+
"slack tail --me requires a <target> channel.\n" +
|
|
151
|
+
"Cross-channel mention streaming is not yet supported.\n" +
|
|
152
|
+
"Use: slack tail \"#channel\" --me",
|
|
153
|
+
);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!target) {
|
|
158
|
+
console.error("Usage: slack tail <#channel|@user>\n Run `slack tail --help` for details.");
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const channelId = await resolveChannel(token, target);
|
|
163
|
+
const interval = opts.interval ?? 60000;
|
|
164
|
+
const cache = new Map<string, string>();
|
|
165
|
+
const seen = new Set<string>();
|
|
166
|
+
|
|
167
|
+
// Preflight: verify channel access before entering the poll loop
|
|
168
|
+
try {
|
|
169
|
+
const info = asRecord(await conversationInfo(token, channelId) as Json);
|
|
170
|
+
const ch = asRecord(info.channel as Json);
|
|
171
|
+
if (ch.is_archived === true) {
|
|
172
|
+
console.error(`Warning: this channel is archived — no new messages will arrive.`);
|
|
173
|
+
}
|
|
174
|
+
} catch (e: unknown) {
|
|
175
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
176
|
+
if (msg.includes("not_in_channel")) {
|
|
177
|
+
console.error(`Error: you are not a member of that channel. Join it in Slack first, then retry.`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
if (msg.includes("missing_scope")) {
|
|
181
|
+
console.error(`Error: token lacks the required history scope. Check your token's channels:history (or groups:history) permission.`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
if (msg.includes("channel_not_found")) {
|
|
185
|
+
console.error(`Error: channel not found — it may be a private channel inaccessible with your token.`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
// Non-fatal: warn and continue (e.g. transient network error)
|
|
189
|
+
console.error(`Warning: preflight check failed: ${msg}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let cursor = "";
|
|
193
|
+
if (opts.since) {
|
|
194
|
+
const secondsBack = parseSince(opts.since);
|
|
195
|
+
cursor = (_internals.now() / 1000 - secondsBack).toFixed(6);
|
|
196
|
+
} else {
|
|
197
|
+
// Seed with most recent message so we only emit new messages going forward
|
|
198
|
+
const histResp = asRecord((await history(token, channelId, 1)) as Json);
|
|
199
|
+
const msgs = asArray(histResp.messages).map(asRecord);
|
|
200
|
+
if (msgs.length > 0 && typeof msgs[0]?.ts === "string") {
|
|
201
|
+
cursor = msgs[0].ts;
|
|
202
|
+
seen.add(cursor);
|
|
203
|
+
} else {
|
|
204
|
+
cursor = (_internals.now() / 1000).toFixed(6);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let myUserId: string | undefined;
|
|
209
|
+
if (opts.me) {
|
|
210
|
+
const info = await authTest(token);
|
|
211
|
+
myUserId = info.userId || undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const pollOpts: PollOpts = {
|
|
215
|
+
...(opts.thread !== undefined ? { thread: opts.thread } : {}),
|
|
216
|
+
...(opts.me !== undefined ? { me: opts.me } : {}),
|
|
217
|
+
...(myUserId !== undefined ? { myUserId } : {}),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
let isFirstPoll = true;
|
|
221
|
+
let lastPollEndTime = _internals.now();
|
|
222
|
+
|
|
223
|
+
while (!signal?.aborted) {
|
|
224
|
+
try {
|
|
225
|
+
const now = _internals.now();
|
|
226
|
+
const elapsed = now - lastPollEndTime;
|
|
227
|
+
// If we woke after an unexpectedly long gap (e.g. laptop sleep), paginate to
|
|
228
|
+
// avoid missing messages beyond the single-page limit.
|
|
229
|
+
const shouldPaginate = interval > 0 && elapsed > interval * 5;
|
|
230
|
+
|
|
231
|
+
let pageCursor: string | undefined;
|
|
232
|
+
let isFirstPage = true;
|
|
233
|
+
|
|
234
|
+
do {
|
|
235
|
+
const { newCursor, lines, nextPageCursor } = await pollCycle(
|
|
236
|
+
token,
|
|
237
|
+
channelId,
|
|
238
|
+
cursor,
|
|
239
|
+
pollOpts,
|
|
240
|
+
seen,
|
|
241
|
+
cache,
|
|
242
|
+
pageCursor,
|
|
243
|
+
);
|
|
244
|
+
cursor = newCursor;
|
|
245
|
+
|
|
246
|
+
if (isFirstPoll && isFirstPage && opts.since && lines.length === 0) {
|
|
247
|
+
process.stdout.write(`(no messages in the last ${opts.since} — watching for new)\n`);
|
|
248
|
+
}
|
|
249
|
+
isFirstPage = false;
|
|
250
|
+
|
|
251
|
+
for (const line of lines) process.stdout.write(line + "\n");
|
|
252
|
+
pageCursor = shouldPaginate ? nextPageCursor : undefined;
|
|
253
|
+
} while (pageCursor);
|
|
254
|
+
|
|
255
|
+
isFirstPoll = false;
|
|
256
|
+
} catch (e: unknown) {
|
|
257
|
+
if (e instanceof RateLimitError) {
|
|
258
|
+
console.error(`Warning: Slack rate limit hit — backing off for ${e.retryAfter}s`);
|
|
259
|
+
await _internals.sleep(e.retryAfter * 1000);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
throw e;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
lastPollEndTime = _internals.now();
|
|
266
|
+
await _internals.sleep(interval);
|
|
267
|
+
}
|
|
268
|
+
}
|