slack-term 1.41.0 → 1.41.2
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 +3 -0
- package/SKILL.md +4 -0
- package/dist/cli.js +189 -187
- package/package.json +1 -1
- package/ts/botIntent.ts +68 -0
- package/ts/cli.ts +75 -34
- package/ts/slack.ts +3 -2
package/package.json
CHANGED
package/ts/botIntent.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Refuse to act as the USER when the caller clearly meant the BOT.
|
|
2
|
+
//
|
|
3
|
+
// SLACK_BOT_TOKEN is only ever used by `--as-bot`: its documented home is the
|
|
4
|
+
// global ~/.config/slack-cli/.env, where it sits for `slack ask` and friends,
|
|
5
|
+
// and it must not turn every post into a bot post. But a lane that EXPORTS it
|
|
6
|
+
// (or puts it in its project's .env) without SLACK_TOKEN is saying "I am the
|
|
7
|
+
// bot" — and without --as-bot every write still went out under the logged-in
|
|
8
|
+
// person's session, silently (reported 2026-10-26: a lane posted a pinlog as
|
|
9
|
+
// the user). In that case writes are refused until the caller picks a side.
|
|
10
|
+
|
|
11
|
+
/** Write commands, as `argv._` spells them (aliases included). Read-only
|
|
12
|
+
* commands are never refused — reading as the user impersonates no one. */
|
|
13
|
+
const WRITES = new Set([
|
|
14
|
+
"send", "ask", "poll", "vote", "edit", "delete", "react", "upload",
|
|
15
|
+
"schedule send", "schedule rm",
|
|
16
|
+
"pinlog new", "pinlog set", "pinlog note", "pinlog pin",
|
|
17
|
+
"todo set", "todo flag",
|
|
18
|
+
"lists create", "lists add", "lists set", "lists share",
|
|
19
|
+
"drafts new", "drafts save", "drafts edit", "drafts update", "drafts delete", "drafts rm",
|
|
20
|
+
"channel create", "channel new", "channel rename", "channel mv",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** `argv._` holds what was TYPED, so top-level aliases must be folded first. */
|
|
24
|
+
const TOP_ALIAS: Record<string, string> = { channels: "channel", ch: "channel" };
|
|
25
|
+
|
|
26
|
+
export function isWriteCommand(path: (string | number)[]): boolean {
|
|
27
|
+
const [raw = "", b = ""] = path.map(String);
|
|
28
|
+
const a = TOP_ALIAS[raw] ?? raw;
|
|
29
|
+
return WRITES.has(a) || WRITES.has(`${a} ${b}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read-only modes of a write command. `ask --ls` only reads the local
|
|
33
|
+
* sent-log and the questions' live state. (`ask --waitFor` and
|
|
34
|
+
* `poll --results` are NOT here: both can edit the posted message.) */
|
|
35
|
+
function isReadMode(path: (string | number)[], argv: Record<string, unknown>): boolean {
|
|
36
|
+
return String(path[0] ?? "") === "ask" && argv.ls === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type BotIntent = {
|
|
40
|
+
/** Where SLACK_BOT_TOKEN (an xoxb-) came from: "shell", a dotenv path, or undefined when unset. */
|
|
41
|
+
botTokenSource: string | undefined;
|
|
42
|
+
/** dotenv paths that are the bot token's GLOBAL home — not a statement of intent. */
|
|
43
|
+
globalPaths: string[];
|
|
44
|
+
slackTokenSet: boolean;
|
|
45
|
+
/** The token this command would act with is itself the bot token (no profiles). */
|
|
46
|
+
activeTokenIsBot: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A refusal message, or null when the write may proceed. */
|
|
50
|
+
export function botIntentRefusal(
|
|
51
|
+
cmd: (string | number)[],
|
|
52
|
+
argv: Record<string, unknown>,
|
|
53
|
+
intent: BotIntent,
|
|
54
|
+
): string | null {
|
|
55
|
+
const src = intent.botTokenSource;
|
|
56
|
+
if (!src || intent.globalPaths.includes(src)) return null;
|
|
57
|
+
if (intent.slackTokenSet || intent.activeTokenIsBot) return null;
|
|
58
|
+
if (!isWriteCommand(cmd) || isReadMode(cmd, argv) || argv["as-bot"] === true) return null;
|
|
59
|
+
const from = src === "shell" ? "exported in the environment" : `set in ${src}`;
|
|
60
|
+
const canBot = "as-bot" in argv;
|
|
61
|
+
return [
|
|
62
|
+
`Error: refusing to act as YOU — SLACK_BOT_TOKEN is ${from}, but \`${cmd.slice(0, 2).join(" ")}\` would use your user session, not the bot.`,
|
|
63
|
+
canBot
|
|
64
|
+
? " As the app: add --as-bot"
|
|
65
|
+
: " This command has no --as-bot: it can only act as a user.",
|
|
66
|
+
" As yourself: set SLACK_TOKEN, or clear the bot token for this run (SLACK_BOT_TOKEN= slack ...)",
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
package/ts/cli.ts
CHANGED
|
@@ -130,11 +130,21 @@ import {
|
|
|
130
130
|
import { setCacheEnabled } from "./cache.ts";
|
|
131
131
|
import { escapeArg, unescapeArg } from "./escapes.ts";
|
|
132
132
|
import { attachmentLines, parseAttachments } from "./attachments.ts";
|
|
133
|
-
import {
|
|
133
|
+
import { botIntentRefusal } from "./botIntent.ts";
|
|
134
|
+
import { dayLabel, encodeMentions, encodeMentionsDetailed, findUntaggedMentions, formatYmdHm, maskSecrets, mentionWarnings, resolveDateMarkup, resolveMentions, type MentionEncodeResult, type UnresolvedMention } from "./format.ts";
|
|
134
135
|
import { quietHoursNotice } from "./quietHours.ts";
|
|
135
136
|
import { installSyncConsole } from "./stdout.ts";
|
|
136
137
|
import { LockBusyError, PINLOG_MARKER, acquireLock, composeHead, formatJst, headUpdatedAt, isPinlogHead, parsePinlogId, pinlogId, stripPinlogFooter } from "./pinlog.ts";
|
|
137
138
|
|
|
139
|
+
// Which dotenv file supplied each variable (absent = the shell's own env).
|
|
140
|
+
const dotenvSource = new Map<string, string>();
|
|
141
|
+
// Every value a GLOBAL dotenv holds, even when the shell already had the key.
|
|
142
|
+
// A child of `slack agent` (or any process the CLI spawned) inherits the
|
|
143
|
+
// global SLACK_BOT_TOKEN as a plain env var; matching it back to its global
|
|
144
|
+
// file keeps that inherited copy from reading as an explicit export.
|
|
145
|
+
const globalDotenvValue = new Map<string, { path: string; val: string }[]>();
|
|
146
|
+
const GLOBAL_DOTENVS = [join(homedir(), ".slack-cli", ".env.local"), join(homedir(), ".config/slack-cli", ".env")];
|
|
147
|
+
|
|
138
148
|
function loadDotenv(path: string): void {
|
|
139
149
|
if (!existsSync(path)) return;
|
|
140
150
|
const text = readFileSync(path, "utf8");
|
|
@@ -148,7 +158,13 @@ function loadDotenv(path: string): void {
|
|
|
148
158
|
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
149
159
|
val = val.slice(1, -1);
|
|
150
160
|
}
|
|
151
|
-
if (
|
|
161
|
+
if (GLOBAL_DOTENVS.includes(path)) {
|
|
162
|
+
globalDotenvValue.set(key, [...(globalDotenvValue.get(key) ?? []), { path, val }]);
|
|
163
|
+
}
|
|
164
|
+
if (!(key in process.env)) {
|
|
165
|
+
process.env[key] = val;
|
|
166
|
+
dotenvSource.set(key, path);
|
|
167
|
+
}
|
|
152
168
|
}
|
|
153
169
|
}
|
|
154
170
|
|
|
@@ -160,8 +176,7 @@ function ensureSlackCliDir(dir: string): void {
|
|
|
160
176
|
|
|
161
177
|
function loadDotenvFiles(): void {
|
|
162
178
|
// Global home config
|
|
163
|
-
|
|
164
|
-
loadDotenv(join(homedir(), ".config/slack-cli", ".env"));
|
|
179
|
+
for (const p of GLOBAL_DOTENVS) loadDotenv(p);
|
|
165
180
|
// Local project overrides (highest priority — loaded last so they win)
|
|
166
181
|
loadDotenv(join(process.cwd(), ".slack-cli", ".env.local"));
|
|
167
182
|
loadDotenv(join(process.cwd(), ".env.local"));
|
|
@@ -2510,6 +2525,22 @@ const ASK_ANSWERABLE_SUBTYPES = new Set(["thread_broadcast"]);
|
|
|
2510
2525
|
// ends it (`\b` is unreliable outside ASCII).
|
|
2511
2526
|
const ASK_BROADCAST_RE = /(?<![A-Za-z0-9._@<-])@(here|channel|everyone)(?![A-Za-z0-9._-])/giu;
|
|
2512
2527
|
|
|
2528
|
+
/** `ask` cannot degrade an unresolved @tag to plain text the way `send` does:
|
|
2529
|
+
* the tags ARE who may answer, so a tag that named nobody would silently drop
|
|
2530
|
+
* an intended answerer (or, in a 1:1 DM, hand the decision to the counterpart
|
|
2531
|
+
* the caller meant to bypass). Refuse instead, before anything is posted. */
|
|
2532
|
+
function askRefuseUnresolved(unresolved: UnresolvedMention[]): void {
|
|
2533
|
+
if (!unresolved.length) return;
|
|
2534
|
+
const reasons = unresolved.map((u) => {
|
|
2535
|
+
const tag = stripTerminalControls(u.surface);
|
|
2536
|
+
if (u.reason === "unavailable") return `${tag}: could not fetch the user list or channel members (users:read missing or an API/connection error)`;
|
|
2537
|
+
if (u.reason === "ambiguous") return `${tag}: matches multiple people — use an explicit <@USERID> tag`;
|
|
2538
|
+
return `${tag}: matched no one`;
|
|
2539
|
+
});
|
|
2540
|
+
console.error(`Error: \`ask\` will not post with unresolved answerer tags.\n ${reasons.join("\n ")}`);
|
|
2541
|
+
process.exit(ASK_EXIT_ERROR);
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2513
2544
|
/** Rewrite `@here`/`@channel`/`@everyone` to the `<!here>` form Slack actually
|
|
2514
2545
|
* broadcasts on, and report which were used. Run BEFORE user-mention encoding
|
|
2515
2546
|
* so those tokens are never offered to the directory as if they were handles. */
|
|
@@ -3166,9 +3197,9 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
|
|
|
3166
3197
|
const body = bRep.text;
|
|
3167
3198
|
const resolved = [...qRep.resolved, ...bRep.resolved];
|
|
3168
3199
|
const unresolved = [...qRep.unresolved, ...bRep.unresolved];
|
|
3169
|
-
// An unresolved tag
|
|
3170
|
-
//
|
|
3171
|
-
|
|
3200
|
+
// An unresolved tag names nobody, so it grants nobody the right to answer —
|
|
3201
|
+
// and posting anyway would silently drop someone the caller meant to ask.
|
|
3202
|
+
askRefuseUnresolved(unresolved);
|
|
3172
3203
|
|
|
3173
3204
|
const broadcast = broadcastKinds.size > 0;
|
|
3174
3205
|
// Tagging yourself grants nothing — your own reactions are the seeds.
|
|
@@ -3196,9 +3227,6 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
|
|
|
3196
3227
|
if (!audience.size) {
|
|
3197
3228
|
console.error(
|
|
3198
3229
|
`Error: \`ask\` will not post a question nobody is addressed to — no one is tagged.\n` +
|
|
3199
|
-
(unresolved.length
|
|
3200
|
-
? ` (${unresolved.map((u) => stripTerminalControls(u.surface)).join(", ")} matched no one, so it names nobody.)\n`
|
|
3201
|
-
: "") +
|
|
3202
3230
|
` Tag the person who should answer: slack ask '${args.target}' '@alice ${args.question}' ...\n` +
|
|
3203
3231
|
` Or open it to the whole channel: slack ask '${args.target}' '@here ${args.question}' ...\n` +
|
|
3204
3232
|
` (In a 1:1 DM the other person counts automatically.)`,
|
|
@@ -3490,7 +3518,9 @@ async function cmdAskEdit(token: string, args: {
|
|
|
3490
3518
|
const old = t.parsed;
|
|
3491
3519
|
const encode = async (raw: string): Promise<string> => {
|
|
3492
3520
|
const enc = askEncodeBroadcasts(unescapeArg(raw));
|
|
3493
|
-
|
|
3521
|
+
const rep = await encodeMentionsDetailed(args.mentionToken, enc.text, t.channelId, args.mentionCookie);
|
|
3522
|
+
askRefuseUnresolved(rep.unresolved);
|
|
3523
|
+
return rep.text;
|
|
3494
3524
|
};
|
|
3495
3525
|
const question = args.question !== undefined ? await encode(args.question) : old.question;
|
|
3496
3526
|
const body = args.body !== undefined ? await encode(args.body) : old.body;
|
|
@@ -4965,7 +4995,22 @@ async function main(): Promise<void> {
|
|
|
4965
4995
|
// `sent` reads only the local log — no token, no network.
|
|
4966
4996
|
if (!cmd || cmd === "auth" || cmd === "login" || cmd === "agent" || cmd === "sent") return;
|
|
4967
4997
|
try {
|
|
4968
|
-
resolveToken((argv as W).workspace);
|
|
4998
|
+
const active = resolveToken((argv as W).workspace);
|
|
4999
|
+
const bot = resolveBotToken();
|
|
5000
|
+
const refusal = botIntentRefusal(argv._ ?? [], argv as Record<string, unknown>, {
|
|
5001
|
+
botTokenSource: bot
|
|
5002
|
+
? dotenvSource.get("SLACK_BOT_TOKEN")
|
|
5003
|
+
?? globalDotenvValue.get("SLACK_BOT_TOKEN")?.find((g) => g.val === bot)?.path
|
|
5004
|
+
?? "shell"
|
|
5005
|
+
: undefined,
|
|
5006
|
+
globalPaths: GLOBAL_DOTENVS,
|
|
5007
|
+
slackTokenSet: !!process.env.SLACK_TOKEN,
|
|
5008
|
+
activeTokenIsBot: !!bot && active === bot,
|
|
5009
|
+
});
|
|
5010
|
+
if (refusal) {
|
|
5011
|
+
console.error(refusal);
|
|
5012
|
+
process.exit(2);
|
|
5013
|
+
}
|
|
4969
5014
|
} catch (e) {
|
|
4970
5015
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4971
5016
|
if (msg.startsWith("No profiles configured")) {
|
|
@@ -5501,7 +5546,7 @@ async function main(): Promise<void> {
|
|
|
5501
5546
|
.option("timeout", { type: "number", default: 3600, describe: "Overall limit for --wait / --waitFor, in seconds (0 with --waitFor = check once)" })
|
|
5502
5547
|
.option("channel-id", { type: "string", describe: "Raw channel ID" })
|
|
5503
5548
|
.option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
|
|
5504
|
-
.option("as-bot", { type: "boolean", default: false, describe: "Ask via the bot token (xoxb / SLACK_BOT_TOKEN) so a DM notifies the recipient" }),
|
|
5549
|
+
.option("as-bot", { type: "boolean", default: false, describe: "Ask via the bot token (xoxb / SLACK_BOT_TOKEN) so a DM notifies the recipient. @tags and an @user target then resolve with the bot token too (needs users:read)." }),
|
|
5505
5550
|
async (argv) => {
|
|
5506
5551
|
// Verbs. A target is always #chan / @user / a permalink / a raw ID, so a
|
|
5507
5552
|
// bare word here cannot be one — `slack ask void …` is unambiguous.
|
|
@@ -5550,7 +5595,9 @@ async function main(): Promise<void> {
|
|
|
5550
5595
|
}
|
|
5551
5596
|
const { token, cookie } = askTokenFor(!!argv["as-bot"]);
|
|
5552
5597
|
const a: Parameters<typeof cmdAskEdit>[1] = {
|
|
5553
|
-
|
|
5598
|
+
// Tags resolve as whoever posts the question — the bot under --as-bot,
|
|
5599
|
+
// so a dead or absent user token cannot break a bot edit.
|
|
5600
|
+
link: String(argv.edit), asBot: !!argv["as-bot"], mentionToken: token,
|
|
5554
5601
|
allowUrlAdjacent: !!argv["allow-url-adjacent"],
|
|
5555
5602
|
};
|
|
5556
5603
|
if (newQuestion !== undefined) a.question = newQuestion;
|
|
@@ -5558,9 +5605,7 @@ async function main(): Promise<void> {
|
|
|
5558
5605
|
if (argv.body !== undefined) a.body = String(argv.body);
|
|
5559
5606
|
if (argv.lang) a.lang = String(argv.lang);
|
|
5560
5607
|
if (argv.code) a.code = String(argv.code);
|
|
5561
|
-
if (cookie) a.cookie = cookie;
|
|
5562
|
-
const mc = ck(argv as W);
|
|
5563
|
-
if (mc) a.mentionCookie = mc;
|
|
5608
|
+
if (cookie) { a.cookie = cookie; a.mentionCookie = cookie; }
|
|
5564
5609
|
try {
|
|
5565
5610
|
await cmdAskEdit(token, a);
|
|
5566
5611
|
} catch (e: unknown) {
|
|
@@ -5727,11 +5772,6 @@ async function main(): Promise<void> {
|
|
|
5727
5772
|
if (voidAfter) args.voidAfter = voidAfter;
|
|
5728
5773
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
5729
5774
|
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
5730
|
-
// Always resolve @tags with the user token (it has users:read), even
|
|
5731
|
-
// when the question itself is posted by the bot.
|
|
5732
|
-
args.mentionToken = tok(argv as W);
|
|
5733
|
-
const mc = ck(argv as W);
|
|
5734
|
-
if (mc) args.mentionCookie = mc;
|
|
5735
5775
|
|
|
5736
5776
|
let askToken: string;
|
|
5737
5777
|
if (argv["as-bot"]) {
|
|
@@ -5743,11 +5783,12 @@ async function main(): Promise<void> {
|
|
|
5743
5783
|
);
|
|
5744
5784
|
process.exit(ASK_EXIT_ERROR);
|
|
5745
5785
|
}
|
|
5746
|
-
// Resolve @user
|
|
5747
|
-
//
|
|
5786
|
+
// Resolve the @user destination AND the answerer tags as the posting
|
|
5787
|
+
// bot: the bot's directory is the one the question is read against, and
|
|
5788
|
+
// a revoked user credential must not break an otherwise valid bot ask.
|
|
5748
5789
|
if (!args.channelId && !args.userId && args.target.startsWith("@")) {
|
|
5749
5790
|
try {
|
|
5750
|
-
args.userId = await resolveUserId(
|
|
5791
|
+
args.userId = await resolveUserId(botToken, args.target);
|
|
5751
5792
|
} catch (e: unknown) {
|
|
5752
5793
|
console.error(
|
|
5753
5794
|
`Error: could not resolve ${args.target} to a user for the bot DM.\n` +
|
|
@@ -5758,11 +5799,13 @@ async function main(): Promise<void> {
|
|
|
5758
5799
|
}
|
|
5759
5800
|
}
|
|
5760
5801
|
askToken = botToken;
|
|
5802
|
+
args.mentionToken = botToken;
|
|
5761
5803
|
args.asBot = true;
|
|
5762
5804
|
} else {
|
|
5763
5805
|
askToken = tok(argv as W);
|
|
5806
|
+
args.mentionToken = askToken;
|
|
5764
5807
|
const sc = ck(argv as W);
|
|
5765
|
-
if (sc) args.cookie = sc;
|
|
5808
|
+
if (sc) { args.cookie = sc; args.mentionCookie = sc; }
|
|
5766
5809
|
}
|
|
5767
5810
|
try {
|
|
5768
5811
|
await cmdAsk(askToken, args);
|
|
@@ -5792,7 +5835,7 @@ async function main(): Promise<void> {
|
|
|
5792
5835
|
.option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
|
|
5793
5836
|
.option("channel-id", { type: "string", describe: "Raw channel ID" })
|
|
5794
5837
|
.option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
|
|
5795
|
-
.option("as-bot", { type: "boolean", default: false, describe: "Post via the bot token (xoxb / SLACK_BOT_TOKEN) so a DM notifies the recipient — and so YOUR OWN vote counts, since the seeds are then the bot's" }),
|
|
5838
|
+
.option("as-bot", { type: "boolean", default: false, describe: "Post via the bot token (xoxb / SLACK_BOT_TOKEN) so a DM notifies the recipient — and so YOUR OWN vote counts, since the seeds are then the bot's. @tags and an @user target then resolve with the bot token too (needs users:read)." }),
|
|
5796
5839
|
async (argv) => {
|
|
5797
5840
|
const readLink = argv.close ? String(argv.close) : argv.results ? String(argv.results) : "";
|
|
5798
5841
|
if (argv.results && argv.close) {
|
|
@@ -5862,18 +5905,14 @@ async function main(): Promise<void> {
|
|
|
5862
5905
|
if (argv["allow-url-adjacent"]) args.allowUrlAdjacent = true;
|
|
5863
5906
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
5864
5907
|
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
5865
|
-
// Always resolve @tags with the user token (it has users:read), even
|
|
5866
|
-
// when the poll itself is posted by the bot.
|
|
5867
|
-
args.mentionToken = tok(argv as W);
|
|
5868
|
-
const mc = ck(argv as W);
|
|
5869
|
-
if (mc) args.mentionCookie = mc;
|
|
5870
5908
|
|
|
5871
5909
|
let pollToken: string;
|
|
5872
5910
|
if (argv["as-bot"]) {
|
|
5873
5911
|
const botToken = botTokenOrExit();
|
|
5912
|
+
// Resolve the @user destination and @tags as the posting bot, same as `ask --as-bot`.
|
|
5874
5913
|
if (!args.channelId && !args.userId && args.target.startsWith("@")) {
|
|
5875
5914
|
try {
|
|
5876
|
-
args.userId = await resolveUserId(
|
|
5915
|
+
args.userId = await resolveUserId(botToken, args.target);
|
|
5877
5916
|
} catch (e: unknown) {
|
|
5878
5917
|
console.error(
|
|
5879
5918
|
`Error: could not resolve ${args.target} to a user for the bot DM.\n` +
|
|
@@ -5884,11 +5923,13 @@ async function main(): Promise<void> {
|
|
|
5884
5923
|
}
|
|
5885
5924
|
}
|
|
5886
5925
|
pollToken = botToken;
|
|
5926
|
+
args.mentionToken = botToken;
|
|
5887
5927
|
args.asBot = true;
|
|
5888
5928
|
} else {
|
|
5889
5929
|
pollToken = tok(argv as W);
|
|
5930
|
+
args.mentionToken = pollToken;
|
|
5890
5931
|
const sc = ck(argv as W);
|
|
5891
|
-
if (sc) args.cookie = sc;
|
|
5932
|
+
if (sc) { args.cookie = sc; args.mentionCookie = sc; }
|
|
5892
5933
|
}
|
|
5893
5934
|
try {
|
|
5894
5935
|
await cmdPoll(pollToken, args);
|
package/ts/slack.ts
CHANGED
|
@@ -852,8 +852,9 @@ function userNotFoundError(ref: string, found: UserSearch): Error {
|
|
|
852
852
|
|
|
853
853
|
/**
|
|
854
854
|
* Resolve `@name` (or a raw U…/W… id) to a user ID. Used when DMing as a bot:
|
|
855
|
-
* the
|
|
856
|
-
*
|
|
855
|
+
* the caller then opens the DM with the bot token. `send` runs the lookup on the
|
|
856
|
+
* *user* token; `ask`/`poll --as-bot` run it on the bot token (the bot scopes
|
|
857
|
+
* include users:read), so a dead user token cannot break a bot question.
|
|
857
858
|
*/
|
|
858
859
|
export async function resolveUserId(token: string, ref: string, cookie?: string): Promise<string> {
|
|
859
860
|
// `@U0123456789` — an id wearing the `@` that every other user target takes.
|