slack-term 1.41.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.41.1",
3
+ "version": "1.41.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/cli.ts CHANGED
@@ -131,7 +131,7 @@ import { setCacheEnabled } from "./cache.ts";
131
131
  import { escapeArg, unescapeArg } from "./escapes.ts";
132
132
  import { attachmentLines, parseAttachments } from "./attachments.ts";
133
133
  import { botIntentRefusal } from "./botIntent.ts";
134
- import { dayLabel, encodeMentions, encodeMentionsDetailed, findUntaggedMentions, formatYmdHm, maskSecrets, mentionWarnings, resolveDateMarkup, resolveMentions, type MentionEncodeResult } from "./format.ts";
134
+ import { dayLabel, encodeMentions, encodeMentionsDetailed, findUntaggedMentions, formatYmdHm, maskSecrets, mentionWarnings, resolveDateMarkup, resolveMentions, type MentionEncodeResult, type UnresolvedMention } from "./format.ts";
135
135
  import { quietHoursNotice } from "./quietHours.ts";
136
136
  import { installSyncConsole } from "./stdout.ts";
137
137
  import { LockBusyError, PINLOG_MARKER, acquireLock, composeHead, formatJst, headUpdatedAt, isPinlogHead, parsePinlogId, pinlogId, stripPinlogFooter } from "./pinlog.ts";
@@ -2525,6 +2525,22 @@ const ASK_ANSWERABLE_SUBTYPES = new Set(["thread_broadcast"]);
2525
2525
  // ends it (`\b` is unreliable outside ASCII).
2526
2526
  const ASK_BROADCAST_RE = /(?<![A-Za-z0-9._@<-])@(here|channel|everyone)(?![A-Za-z0-9._-])/giu;
2527
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
+
2528
2544
  /** Rewrite `@here`/`@channel`/`@everyone` to the `<!here>` form Slack actually
2529
2545
  * broadcasts on, and report which were used. Run BEFORE user-mention encoding
2530
2546
  * so those tokens are never offered to the directory as if they were handles. */
@@ -3181,9 +3197,9 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
3181
3197
  const body = bRep.text;
3182
3198
  const resolved = [...qRep.resolved, ...bRep.resolved];
3183
3199
  const unresolved = [...qRep.unresolved, ...bRep.unresolved];
3184
- // An unresolved tag is not just cosmetic here: it names nobody, so it grants
3185
- // nobody the right to answer. Say so before the rejection below.
3186
- for (const line of mentionWarnings(unresolved)) console.error(`⚠ ${stripTerminalControls(line)}`);
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);
3187
3203
 
3188
3204
  const broadcast = broadcastKinds.size > 0;
3189
3205
  // Tagging yourself grants nothing — your own reactions are the seeds.
@@ -3211,9 +3227,6 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
3211
3227
  if (!audience.size) {
3212
3228
  console.error(
3213
3229
  `Error: \`ask\` will not post a question nobody is addressed to — no one is tagged.\n` +
3214
- (unresolved.length
3215
- ? ` (${unresolved.map((u) => stripTerminalControls(u.surface)).join(", ")} matched no one, so it names nobody.)\n`
3216
- : "") +
3217
3230
  ` Tag the person who should answer: slack ask '${args.target}' '@alice ${args.question}' ...\n` +
3218
3231
  ` Or open it to the whole channel: slack ask '${args.target}' '@here ${args.question}' ...\n` +
3219
3232
  ` (In a 1:1 DM the other person counts automatically.)`,
@@ -3505,7 +3518,9 @@ async function cmdAskEdit(token: string, args: {
3505
3518
  const old = t.parsed;
3506
3519
  const encode = async (raw: string): Promise<string> => {
3507
3520
  const enc = askEncodeBroadcasts(unescapeArg(raw));
3508
- return (await encodeMentionsDetailed(args.mentionToken, enc.text, t.channelId, args.mentionCookie)).text;
3521
+ const rep = await encodeMentionsDetailed(args.mentionToken, enc.text, t.channelId, args.mentionCookie);
3522
+ askRefuseUnresolved(rep.unresolved);
3523
+ return rep.text;
3509
3524
  };
3510
3525
  const question = args.question !== undefined ? await encode(args.question) : old.question;
3511
3526
  const body = args.body !== undefined ? await encode(args.body) : old.body;
@@ -5531,7 +5546,7 @@ async function main(): Promise<void> {
5531
5546
  .option("timeout", { type: "number", default: 3600, describe: "Overall limit for --wait / --waitFor, in seconds (0 with --waitFor = check once)" })
5532
5547
  .option("channel-id", { type: "string", describe: "Raw channel ID" })
5533
5548
  .option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
5534
- .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)." }),
5535
5550
  async (argv) => {
5536
5551
  // Verbs. A target is always #chan / @user / a permalink / a raw ID, so a
5537
5552
  // bare word here cannot be one — `slack ask void …` is unambiguous.
@@ -5580,7 +5595,9 @@ async function main(): Promise<void> {
5580
5595
  }
5581
5596
  const { token, cookie } = askTokenFor(!!argv["as-bot"]);
5582
5597
  const a: Parameters<typeof cmdAskEdit>[1] = {
5583
- link: String(argv.edit), asBot: !!argv["as-bot"], mentionToken: tok(argv as W),
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,
5584
5601
  allowUrlAdjacent: !!argv["allow-url-adjacent"],
5585
5602
  };
5586
5603
  if (newQuestion !== undefined) a.question = newQuestion;
@@ -5588,9 +5605,7 @@ async function main(): Promise<void> {
5588
5605
  if (argv.body !== undefined) a.body = String(argv.body);
5589
5606
  if (argv.lang) a.lang = String(argv.lang);
5590
5607
  if (argv.code) a.code = String(argv.code);
5591
- if (cookie) a.cookie = cookie;
5592
- const mc = ck(argv as W);
5593
- if (mc) a.mentionCookie = mc;
5608
+ if (cookie) { a.cookie = cookie; a.mentionCookie = cookie; }
5594
5609
  try {
5595
5610
  await cmdAskEdit(token, a);
5596
5611
  } catch (e: unknown) {
@@ -5757,11 +5772,6 @@ async function main(): Promise<void> {
5757
5772
  if (voidAfter) args.voidAfter = voidAfter;
5758
5773
  if (argv["channel-id"]) args.channelId = argv["channel-id"];
5759
5774
  if (argv["user-id"]) args.userId = argv["user-id"];
5760
- // Always resolve @tags with the user token (it has users:read), even
5761
- // when the question itself is posted by the bot.
5762
- args.mentionToken = tok(argv as W);
5763
- const mc = ck(argv as W);
5764
- if (mc) args.mentionCookie = mc;
5765
5775
 
5766
5776
  let askToken: string;
5767
5777
  if (argv["as-bot"]) {
@@ -5773,11 +5783,12 @@ async function main(): Promise<void> {
5773
5783
  );
5774
5784
  process.exit(ASK_EXIT_ERROR);
5775
5785
  }
5776
- // Resolve @user with the *user* token (has users:read), then let cmdAsk
5777
- // open the DM with the bot token — same split as `send --as-bot`.
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.
5778
5789
  if (!args.channelId && !args.userId && args.target.startsWith("@")) {
5779
5790
  try {
5780
- args.userId = await resolveUserId(tok(argv as W), args.target, ck(argv as W));
5791
+ args.userId = await resolveUserId(botToken, args.target);
5781
5792
  } catch (e: unknown) {
5782
5793
  console.error(
5783
5794
  `Error: could not resolve ${args.target} to a user for the bot DM.\n` +
@@ -5788,11 +5799,13 @@ async function main(): Promise<void> {
5788
5799
  }
5789
5800
  }
5790
5801
  askToken = botToken;
5802
+ args.mentionToken = botToken;
5791
5803
  args.asBot = true;
5792
5804
  } else {
5793
5805
  askToken = tok(argv as W);
5806
+ args.mentionToken = askToken;
5794
5807
  const sc = ck(argv as W);
5795
- if (sc) args.cookie = sc;
5808
+ if (sc) { args.cookie = sc; args.mentionCookie = sc; }
5796
5809
  }
5797
5810
  try {
5798
5811
  await cmdAsk(askToken, args);
@@ -5822,7 +5835,7 @@ async function main(): Promise<void> {
5822
5835
  .option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
5823
5836
  .option("channel-id", { type: "string", describe: "Raw channel ID" })
5824
5837
  .option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
5825
- .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)." }),
5826
5839
  async (argv) => {
5827
5840
  const readLink = argv.close ? String(argv.close) : argv.results ? String(argv.results) : "";
5828
5841
  if (argv.results && argv.close) {
@@ -5892,18 +5905,14 @@ async function main(): Promise<void> {
5892
5905
  if (argv["allow-url-adjacent"]) args.allowUrlAdjacent = true;
5893
5906
  if (argv["channel-id"]) args.channelId = argv["channel-id"];
5894
5907
  if (argv["user-id"]) args.userId = argv["user-id"];
5895
- // Always resolve @tags with the user token (it has users:read), even
5896
- // when the poll itself is posted by the bot.
5897
- args.mentionToken = tok(argv as W);
5898
- const mc = ck(argv as W);
5899
- if (mc) args.mentionCookie = mc;
5900
5908
 
5901
5909
  let pollToken: string;
5902
5910
  if (argv["as-bot"]) {
5903
5911
  const botToken = botTokenOrExit();
5912
+ // Resolve the @user destination and @tags as the posting bot, same as `ask --as-bot`.
5904
5913
  if (!args.channelId && !args.userId && args.target.startsWith("@")) {
5905
5914
  try {
5906
- args.userId = await resolveUserId(tok(argv as W), args.target, ck(argv as W));
5915
+ args.userId = await resolveUserId(botToken, args.target);
5907
5916
  } catch (e: unknown) {
5908
5917
  console.error(
5909
5918
  `Error: could not resolve ${args.target} to a user for the bot DM.\n` +
@@ -5914,11 +5923,13 @@ async function main(): Promise<void> {
5914
5923
  }
5915
5924
  }
5916
5925
  pollToken = botToken;
5926
+ args.mentionToken = botToken;
5917
5927
  args.asBot = true;
5918
5928
  } else {
5919
5929
  pollToken = tok(argv as W);
5930
+ args.mentionToken = pollToken;
5920
5931
  const sc = ck(argv as W);
5921
- if (sc) args.cookie = sc;
5932
+ if (sc) { args.cookie = sc; args.mentionCookie = sc; }
5922
5933
  }
5923
5934
  try {
5924
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 lookup runs on a *user* token (which has users:read), then the caller
856
- * opens the DM with the bot token — so the bot never needs users:read.
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.