slack-term 1.31.6 → 1.32.1

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.
Files changed (3) hide show
  1. package/dist/cli.js +112 -106
  2. package/package.json +1 -1
  3. package/ts/cli.ts +178 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.31.6",
3
+ "version": "1.32.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/cli.ts CHANGED
@@ -929,14 +929,27 @@ async function recipientTzFor(token: string, channelId: string, cookie?: string)
929
929
  }
930
930
  }
931
931
 
932
- function requireCode(provided: string | undefined, expected: string, contextLines: string[], recipientTz?: string | null): void {
932
+ /** `when` is the moment the message will ACTUALLY reach the person — which is
933
+ * now for a send, and `post_at` for a schedule. Judging a scheduled message by
934
+ * the clock at schedule time answers a question nobody asked: `schedule send`
935
+ * is the one command where the delivery time is stated outright, and reading
936
+ * it off `Date.now()` warned about a message set for December because it was
937
+ * scheduled after 23:00. A warning that is false on the command where the
938
+ * answer is written in the arguments teaches people to ignore the warning. */
939
+ function requireCode(
940
+ provided: string | undefined,
941
+ expected: string,
942
+ contextLines: string[],
943
+ recipientTz?: string | null,
944
+ when: Date = new Date(),
945
+ ): void {
933
946
  // The clock goes to stdout with the rest of the preview, not to stderr with
934
947
  // the code. The preview is what you are asked to LOOK at before committing,
935
948
  // and "what time is it where they are" is part of that picture — a caller
936
949
  // doing `send ... 2>/dev/null` to read the preview would otherwise drop the
937
950
  // one line added to stop a 01:00 send.
938
951
  for (const line of contextLines) console.log(line);
939
- const clock = quietHoursNotice(new Date(), recipientTz);
952
+ const clock = quietHoursNotice(when, recipientTz);
940
953
  if (clock) console.log(clock);
941
954
  if (provided !== undefined) {
942
955
  console.error(`Code mismatch (got ${provided}, expected ${expected})`);
@@ -2615,6 +2628,37 @@ async function cmdDoctor(token: string): Promise<void> {
2615
2628
  }
2616
2629
 
2617
2630
  // --- schedule ---
2631
+
2632
+ /** The xoxb bot token, or a clean exit. `--as-bot` is only ever a token switch,
2633
+ * and its failure is always the same one: the env var is not set. */
2634
+ function requireBotToken(): string {
2635
+ const botToken = resolveBotToken();
2636
+ if (!botToken) {
2637
+ console.error(
2638
+ "Error: --as-bot needs a bot token, but no xoxb- token was found.\n" +
2639
+ " Set SLACK_BOT_TOKEN=xoxb-... in ~/.config/slack-cli/.env (or your shell).",
2640
+ );
2641
+ process.exit(1);
2642
+ }
2643
+ return botToken;
2644
+ }
2645
+
2646
+ /** The bot↔user DM for an `@handle`. The handle is resolved on the USER token
2647
+ * (which has users:read) and the DM is opened on the BOT token, so the channel
2648
+ * is the bot's IM with that person and not the person's own self-DM. That
2649
+ * distinction is the entire point of `--as-bot` here: Slack never notifies you
2650
+ * about your own message, so a digest scheduled into a self-DM is delivered
2651
+ * into silence at the one moment nobody is at the terminal to notice. */
2652
+ async function botDmForTarget(
2653
+ target: string,
2654
+ botToken: string,
2655
+ userToken: string,
2656
+ userCookie?: string,
2657
+ ): Promise<string> {
2658
+ const userId = await resolveUserId(userToken, target, userCookie);
2659
+ return openDm(botToken, userId);
2660
+ }
2661
+
2618
2662
  function parsePostAt(at: string): number {
2619
2663
  if (/^\d{10,}$/.test(at)) return parseInt(at, 10);
2620
2664
  const d = new Date(at.replace(" ", "T"));
@@ -2650,6 +2694,10 @@ interface ScheduleSendArgs {
2650
2694
  channelId?: string;
2651
2695
  userId?: string;
2652
2696
  cookie?: string;
2697
+ asBot?: boolean;
2698
+ /** How the caller overrode the target text, e.g. `--user-id U0123`. Set only
2699
+ * when a FLAG supplied it — never when the command resolved the id itself. */
2700
+ targetOverride?: string;
2653
2701
  }
2654
2702
  async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<void> {
2655
2703
  const getSelf = selfLookup(token, args.cookie);
@@ -2670,26 +2718,73 @@ async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<v
2670
2718
  // Self-DM is worst here: the message fires later, unattended, and Slack never
2671
2719
  // notifies you about your own post — so a scheduled reminder to yourself is
2672
2720
  // delivered into silence at exactly the moment you were counting on it.
2673
- if (await isSelfDmChannel(token, channelId, self?.userId, args.cookie)) {
2721
+ // --as-bot is exempt: that posts as the app, a different identity, which does
2722
+ // notify you. Being unable to say that is what made this warning a dead end —
2723
+ // it named the problem and offered no way out.
2724
+ if (!args.asBot && await isSelfDmChannel(token, channelId, self?.userId, args.cookie)) {
2674
2725
  console.error(
2675
- `Warning: "${args.target}" is a DM to yourself — Slack will NOT notify you when this fires.`,
2726
+ `Warning: "${args.target}" is a DM to yourself — Slack will NOT notify you when this fires.\n` +
2727
+ ` To be notified, schedule it as the bot: slack schedule send '${args.target}' '...' --at '${args.at}' --as-bot`,
2676
2728
  );
2677
2729
  }
2678
2730
 
2679
2731
  const code = safetyCode(channelId, args.message, String(postAt), self?.userId ?? "");
2680
2732
 
2733
+ // `--channel-id` / `--user-id` win over the target text, so the target stops
2734
+ // being where this goes the moment the two disagree — and the gate printed
2735
+ // only the target. `schedule send @alice … --user-id <bob>` read "To: @alice"
2736
+ // and went to bob. A confirm gate that names the wrong recipient with no sign
2737
+ // it was overridden is worse than no gate: it gets read, and it gets passed.
2738
+ const destOverride = args.targetOverride ? ` [${args.targetOverride} → ${channelId}]` : "";
2739
+
2681
2740
  if (args.code !== code) {
2741
+ // Quiet hours are judged on post_at, in the recipient's zone — the two facts
2742
+ // that decide whether a phone buzzes at 3am, and the two this gate was
2743
+ // guessing at. `send` already resolves the zone; here the time is known
2744
+ // exactly, which is the one command where it is.
2745
+ const recipientTz = await recipientTzFor(token, channelId, args.cookie);
2682
2746
  requireCode(args.code, code, [
2683
2747
  `--- Scheduling message -----------------------`,
2684
- fromLine(self, { pad: 9 }),
2685
- ` To: ${ref}${threadTs ? ` (thread ${threadTs})` : ""}`,
2748
+ fromLine(self, { asBot: args.asBot, pad: 9 }),
2749
+ ` To: ${ref}${threadTs ? ` (thread ${threadTs})` : ""}${destOverride}`,
2686
2750
  ` At: ${fmtPostAt(postAt)}`,
2687
2751
  ` Message: ${args.message}`,
2688
2752
  `---------------------------------------------`,
2689
- ]);
2753
+ ], recipientTz, new Date(postAt * 1000));
2690
2754
  }
2691
2755
  const id = await scheduleMessage(token, channelId, args.message, postAt, threadTs, args.cookie);
2692
2756
  console.log(`✓ Scheduled (id: ${id}, at: ${postAtDate})`);
2757
+
2758
+ if (args.asBot) {
2759
+ // The message now belongs to the BOT, so `schedule ls` on the user token
2760
+ // cannot see it and `schedule rm` cannot cancel it. Said at the only moment
2761
+ // the id is in front of the reader.
2762
+ console.error(
2763
+ ` Scheduled as the bot — the user token cannot see or cancel it. Use the same flag:\n` +
2764
+ ` slack schedule ls --as-bot\n` +
2765
+ ` slack schedule rm '${args.target}' ${id} --as-bot`,
2766
+ );
2767
+ // A scheduled bot DM fires unattended, so an app that cannot actually carry
2768
+ // a DM fails at the one moment nobody is watching. Same check `send --as-bot`
2769
+ // runs after the fact; here it is the difference between finding out now and
2770
+ // finding out from an empty inbox.
2771
+ if (channelId.startsWith("D")) {
2772
+ try {
2773
+ const diag = await diagnoseBotMessaging(token);
2774
+ if (!diag.ok) {
2775
+ for (const line of formatDiagnosis(diag, true)) console.error(line);
2776
+ } else {
2777
+ const appUrl = diag.appId ? `https://api.slack.com/apps/${diag.appId}` : "https://api.slack.com/apps";
2778
+ console.error(
2779
+ `Note: if ${args.target} can't reply (app messaging may be off), ` +
2780
+ `enable Messages Tab: ${appUrl} → App Home.`,
2781
+ );
2782
+ }
2783
+ } catch {
2784
+ // diagnosis is best-effort; never turn a successful schedule into a failure.
2785
+ }
2786
+ }
2787
+ }
2693
2788
  }
2694
2789
 
2695
2790
  async function cmdScheduleList(token: string, target?: string, channelId?: string, cookie?: string): Promise<void> {
@@ -2716,6 +2811,9 @@ interface ScheduleRmArgs {
2716
2811
  code?: string;
2717
2812
  channelId?: string;
2718
2813
  cookie?: string;
2814
+ asBot?: boolean;
2815
+ /** How the caller overrode the target text, e.g. `--channel-id C0123`. */
2816
+ targetOverride?: string;
2719
2817
  }
2720
2818
  async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void> {
2721
2819
  let channelId: string;
@@ -2727,8 +2825,8 @@ async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void>
2727
2825
  if (args.code !== code) {
2728
2826
  requireCode(args.code, code, [
2729
2827
  `--- Deleting scheduled message ---------------`,
2730
- fromLine(self, { pad: 9 }),
2731
- ` Channel: ${args.target}`,
2828
+ fromLine(self, { asBot: args.asBot, pad: 9 }),
2829
+ ` Channel: ${args.target}${args.targetOverride ? ` [${args.targetOverride} → ${channelId}]` : ""}`,
2732
2830
  ` ID: ${args.id}`,
2733
2831
  `---------------------------------------------`,
2734
2832
  ]);
@@ -3729,15 +3827,52 @@ async function main(): Promise<void> {
3729
3827
  .option("at", { type: "string", demandOption: true, describe: "Delivery time (ISO datetime or Unix ts)" })
3730
3828
  .option("code", { type: "string", describe: "Safety hash to confirm" })
3731
3829
  .option("channel-id", { type: "string", describe: "Raw channel ID" })
3732
- .option("user-id", { type: "string", describe: "Raw user ID (opens DM)" }),
3830
+ .option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
3831
+ .option("as-bot", { type: "boolean", default: false, describe: "Schedule via the bot token (xoxb / SLACK_BOT_TOKEN) so a DM notifies the recipient. Without it a scheduled DM to yourself is posted by you, and Slack never notifies you of your own message — it fires into silence." }),
3733
3832
  async (argv) => {
3734
3833
  const args: ScheduleSendArgs = { target: argv.target!, message: argv.message!, at: argv.at! };
3735
3834
  if (argv.code) args.code = argv.code;
3736
- if (argv["channel-id"]) args.channelId = argv["channel-id"];
3737
- if (argv["user-id"]) args.userId = argv["user-id"];
3738
- const cookie = ck(argv as W);
3739
- if (cookie) args.cookie = cookie;
3740
- await cmdScheduleSend(tok(argv as W), args);
3835
+ // Recorded here, not in the command, because only a flag counts as an
3836
+ // override — under --as-bot the command sets args.userId itself from
3837
+ // the target, and claiming THAT was an override would be a new lie.
3838
+ if (argv["channel-id"]) {
3839
+ args.channelId = argv["channel-id"];
3840
+ args.targetOverride = `--channel-id ${argv["channel-id"]}`;
3841
+ }
3842
+ if (argv["user-id"]) {
3843
+ args.userId = argv["user-id"];
3844
+ args.targetOverride = `--user-id ${argv["user-id"]}`;
3845
+ }
3846
+ let scheduleToken: string;
3847
+ if (argv["as-bot"]) {
3848
+ const botToken = requireBotToken();
3849
+ // Resolve @user → user_id with the *user* token (has users:read),
3850
+ // then let cmdScheduleSend open the DM with the bot token
3851
+ // (im:write). Same split as `send --as-bot`: the bot never needs
3852
+ // users:read, and the DM is bot↔user rather than a self-DM.
3853
+ if (!args.channelId && !args.userId && args.target.startsWith("@")) {
3854
+ try {
3855
+ args.userId = await resolveUserId(tok(argv as W), args.target, ck(argv as W));
3856
+ } catch (e: unknown) {
3857
+ console.error(
3858
+ `Error: could not resolve ${args.target} to a user for the bot DM.\n` +
3859
+ ` ${e instanceof Error ? e.message : String(e)}\n` +
3860
+ ` Tip: pass --user-id <Uxxxx> to skip the lookup.`,
3861
+ );
3862
+ process.exit(1);
3863
+ }
3864
+ }
3865
+ scheduleToken = botToken;
3866
+ args.asBot = true;
3867
+ } else {
3868
+ scheduleToken = tok(argv as W);
3869
+ // Session cookie for the token that will actually schedule — an
3870
+ // xoxc- desktop token is rejected by the public API without it.
3871
+ // Bot tokens need none.
3872
+ const cookie = ck(argv as W);
3873
+ if (cookie) args.cookie = cookie;
3874
+ }
3875
+ await cmdScheduleSend(scheduleToken, args);
3741
3876
  },
3742
3877
  )
3743
3878
  .command(
@@ -3745,9 +3880,20 @@ async function main(): Promise<void> {
3745
3880
  "List pending scheduled messages",
3746
3881
  (y2) => y2
3747
3882
  .positional("target", { type: "string", describe: "#channel to filter by" })
3748
- .option("channel-id", { type: "string", describe: "Raw channel ID" }),
3883
+ .option("channel-id", { type: "string", describe: "Raw channel ID" })
3884
+ .option("as-bot", { type: "boolean", default: false, describe: "List what the BOT token scheduled. A message from `schedule send --as-bot` belongs to the app, so the user token's listing does not contain it — and an invisible pending message cannot be cancelled." }),
3749
3885
  async (argv) => {
3750
- await cmdScheduleList(tok(argv as W), argv.target as string | undefined, argv["channel-id"], ck(argv as W));
3886
+ const target = argv.target as string | undefined;
3887
+ if (argv["as-bot"]) {
3888
+ const botToken = requireBotToken();
3889
+ let channelId = argv["channel-id"] as string | undefined;
3890
+ if (!channelId && target?.startsWith("@")) {
3891
+ channelId = await botDmForTarget(target, botToken, tok(argv as W), ck(argv as W));
3892
+ }
3893
+ await cmdScheduleList(botToken, target, channelId);
3894
+ return;
3895
+ }
3896
+ await cmdScheduleList(tok(argv as W), target, argv["channel-id"], ck(argv as W));
3751
3897
  },
3752
3898
  )
3753
3899
  .command(
@@ -3757,13 +3903,26 @@ async function main(): Promise<void> {
3757
3903
  .positional("target", { type: "string", demandOption: true, describe: "#channel or @user" })
3758
3904
  .positional("id", { type: "string", demandOption: true, describe: "Scheduled message ID" })
3759
3905
  .option("code", { type: "string", describe: "Safety hash to confirm" })
3760
- .option("channel-id", { type: "string", describe: "Raw channel ID" }),
3906
+ .option("channel-id", { type: "string", describe: "Raw channel ID" })
3907
+ .option("as-bot", { type: "boolean", default: false, describe: "Cancel a message scheduled with `schedule send --as-bot`. It belongs to the bot token, so the user token cannot delete it." }),
3761
3908
  async (argv) => {
3762
3909
  const args: ScheduleRmArgs = { target: argv.target!, id: argv.id! };
3763
3910
  if (argv.code) args.code = argv.code;
3911
+ if (argv["channel-id"]) {
3912
+ args.channelId = argv["channel-id"];
3913
+ args.targetOverride = `--channel-id ${argv["channel-id"]}`;
3914
+ }
3915
+ if (argv["as-bot"]) {
3916
+ const botToken = requireBotToken();
3917
+ if (!args.channelId && args.target.startsWith("@")) {
3918
+ args.channelId = await botDmForTarget(args.target, botToken, tok(argv as W), ck(argv as W));
3919
+ }
3920
+ args.asBot = true;
3921
+ await cmdScheduleRm(botToken, args);
3922
+ return;
3923
+ }
3764
3924
  const cookie = ck(argv as W);
3765
3925
  if (cookie) args.cookie = cookie;
3766
- if (argv["channel-id"]) args.channelId = argv["channel-id"];
3767
3926
  await cmdScheduleRm(tok(argv as W), args);
3768
3927
  },
3769
3928
  )