slack-term 1.31.6 → 1.32.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.
Files changed (3) hide show
  1. package/dist/cli.js +112 -106
  2. package/package.json +1 -1
  3. package/ts/cli.ts +157 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.31.6",
3
+ "version": "1.32.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/cli.ts CHANGED
@@ -2615,6 +2615,37 @@ async function cmdDoctor(token: string): Promise<void> {
2615
2615
  }
2616
2616
 
2617
2617
  // --- schedule ---
2618
+
2619
+ /** The xoxb bot token, or a clean exit. `--as-bot` is only ever a token switch,
2620
+ * and its failure is always the same one: the env var is not set. */
2621
+ function requireBotToken(): string {
2622
+ const botToken = resolveBotToken();
2623
+ if (!botToken) {
2624
+ console.error(
2625
+ "Error: --as-bot needs a bot token, but no xoxb- token was found.\n" +
2626
+ " Set SLACK_BOT_TOKEN=xoxb-... in ~/.config/slack-cli/.env (or your shell).",
2627
+ );
2628
+ process.exit(1);
2629
+ }
2630
+ return botToken;
2631
+ }
2632
+
2633
+ /** The bot↔user DM for an `@handle`. The handle is resolved on the USER token
2634
+ * (which has users:read) and the DM is opened on the BOT token, so the channel
2635
+ * is the bot's IM with that person and not the person's own self-DM. That
2636
+ * distinction is the entire point of `--as-bot` here: Slack never notifies you
2637
+ * about your own message, so a digest scheduled into a self-DM is delivered
2638
+ * into silence at the one moment nobody is at the terminal to notice. */
2639
+ async function botDmForTarget(
2640
+ target: string,
2641
+ botToken: string,
2642
+ userToken: string,
2643
+ userCookie?: string,
2644
+ ): Promise<string> {
2645
+ const userId = await resolveUserId(userToken, target, userCookie);
2646
+ return openDm(botToken, userId);
2647
+ }
2648
+
2618
2649
  function parsePostAt(at: string): number {
2619
2650
  if (/^\d{10,}$/.test(at)) return parseInt(at, 10);
2620
2651
  const d = new Date(at.replace(" ", "T"));
@@ -2650,6 +2681,10 @@ interface ScheduleSendArgs {
2650
2681
  channelId?: string;
2651
2682
  userId?: string;
2652
2683
  cookie?: string;
2684
+ asBot?: boolean;
2685
+ /** How the caller overrode the target text, e.g. `--user-id U0123`. Set only
2686
+ * when a FLAG supplied it — never when the command resolved the id itself. */
2687
+ targetOverride?: string;
2653
2688
  }
2654
2689
  async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<void> {
2655
2690
  const getSelf = selfLookup(token, args.cookie);
@@ -2670,19 +2705,30 @@ async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<v
2670
2705
  // Self-DM is worst here: the message fires later, unattended, and Slack never
2671
2706
  // notifies you about your own post — so a scheduled reminder to yourself is
2672
2707
  // delivered into silence at exactly the moment you were counting on it.
2673
- if (await isSelfDmChannel(token, channelId, self?.userId, args.cookie)) {
2708
+ // --as-bot is exempt: that posts as the app, a different identity, which does
2709
+ // notify you. Being unable to say that is what made this warning a dead end —
2710
+ // it named the problem and offered no way out.
2711
+ if (!args.asBot && await isSelfDmChannel(token, channelId, self?.userId, args.cookie)) {
2674
2712
  console.error(
2675
- `Warning: "${args.target}" is a DM to yourself — Slack will NOT notify you when this fires.`,
2713
+ `Warning: "${args.target}" is a DM to yourself — Slack will NOT notify you when this fires.\n` +
2714
+ ` To be notified, schedule it as the bot: slack schedule send '${args.target}' '...' --at '${args.at}' --as-bot`,
2676
2715
  );
2677
2716
  }
2678
2717
 
2679
2718
  const code = safetyCode(channelId, args.message, String(postAt), self?.userId ?? "");
2680
2719
 
2720
+ // `--channel-id` / `--user-id` win over the target text, so the target stops
2721
+ // being where this goes the moment the two disagree — and the gate printed
2722
+ // only the target. `schedule send @alice … --user-id <bob>` read "To: @alice"
2723
+ // and went to bob. A confirm gate that names the wrong recipient with no sign
2724
+ // it was overridden is worse than no gate: it gets read, and it gets passed.
2725
+ const destOverride = args.targetOverride ? ` [${args.targetOverride} → ${channelId}]` : "";
2726
+
2681
2727
  if (args.code !== code) {
2682
2728
  requireCode(args.code, code, [
2683
2729
  `--- Scheduling message -----------------------`,
2684
- fromLine(self, { pad: 9 }),
2685
- ` To: ${ref}${threadTs ? ` (thread ${threadTs})` : ""}`,
2730
+ fromLine(self, { asBot: args.asBot, pad: 9 }),
2731
+ ` To: ${ref}${threadTs ? ` (thread ${threadTs})` : ""}${destOverride}`,
2686
2732
  ` At: ${fmtPostAt(postAt)}`,
2687
2733
  ` Message: ${args.message}`,
2688
2734
  `---------------------------------------------`,
@@ -2690,6 +2736,37 @@ async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<v
2690
2736
  }
2691
2737
  const id = await scheduleMessage(token, channelId, args.message, postAt, threadTs, args.cookie);
2692
2738
  console.log(`✓ Scheduled (id: ${id}, at: ${postAtDate})`);
2739
+
2740
+ if (args.asBot) {
2741
+ // The message now belongs to the BOT, so `schedule ls` on the user token
2742
+ // cannot see it and `schedule rm` cannot cancel it. Said at the only moment
2743
+ // the id is in front of the reader.
2744
+ console.error(
2745
+ ` Scheduled as the bot — the user token cannot see or cancel it. Use the same flag:\n` +
2746
+ ` slack schedule ls --as-bot\n` +
2747
+ ` slack schedule rm '${args.target}' ${id} --as-bot`,
2748
+ );
2749
+ // A scheduled bot DM fires unattended, so an app that cannot actually carry
2750
+ // a DM fails at the one moment nobody is watching. Same check `send --as-bot`
2751
+ // runs after the fact; here it is the difference between finding out now and
2752
+ // finding out from an empty inbox.
2753
+ if (channelId.startsWith("D")) {
2754
+ try {
2755
+ const diag = await diagnoseBotMessaging(token);
2756
+ if (!diag.ok) {
2757
+ for (const line of formatDiagnosis(diag, true)) console.error(line);
2758
+ } else {
2759
+ const appUrl = diag.appId ? `https://api.slack.com/apps/${diag.appId}` : "https://api.slack.com/apps";
2760
+ console.error(
2761
+ `Note: if ${args.target} can't reply (app messaging may be off), ` +
2762
+ `enable Messages Tab: ${appUrl} → App Home.`,
2763
+ );
2764
+ }
2765
+ } catch {
2766
+ // diagnosis is best-effort; never turn a successful schedule into a failure.
2767
+ }
2768
+ }
2769
+ }
2693
2770
  }
2694
2771
 
2695
2772
  async function cmdScheduleList(token: string, target?: string, channelId?: string, cookie?: string): Promise<void> {
@@ -2716,6 +2793,9 @@ interface ScheduleRmArgs {
2716
2793
  code?: string;
2717
2794
  channelId?: string;
2718
2795
  cookie?: string;
2796
+ asBot?: boolean;
2797
+ /** How the caller overrode the target text, e.g. `--channel-id C0123`. */
2798
+ targetOverride?: string;
2719
2799
  }
2720
2800
  async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void> {
2721
2801
  let channelId: string;
@@ -2727,8 +2807,8 @@ async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void>
2727
2807
  if (args.code !== code) {
2728
2808
  requireCode(args.code, code, [
2729
2809
  `--- Deleting scheduled message ---------------`,
2730
- fromLine(self, { pad: 9 }),
2731
- ` Channel: ${args.target}`,
2810
+ fromLine(self, { asBot: args.asBot, pad: 9 }),
2811
+ ` Channel: ${args.target}${args.targetOverride ? ` [${args.targetOverride} → ${channelId}]` : ""}`,
2732
2812
  ` ID: ${args.id}`,
2733
2813
  `---------------------------------------------`,
2734
2814
  ]);
@@ -3729,15 +3809,52 @@ async function main(): Promise<void> {
3729
3809
  .option("at", { type: "string", demandOption: true, describe: "Delivery time (ISO datetime or Unix ts)" })
3730
3810
  .option("code", { type: "string", describe: "Safety hash to confirm" })
3731
3811
  .option("channel-id", { type: "string", describe: "Raw channel ID" })
3732
- .option("user-id", { type: "string", describe: "Raw user ID (opens DM)" }),
3812
+ .option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
3813
+ .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
3814
  async (argv) => {
3734
3815
  const args: ScheduleSendArgs = { target: argv.target!, message: argv.message!, at: argv.at! };
3735
3816
  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);
3817
+ // Recorded here, not in the command, because only a flag counts as an
3818
+ // override — under --as-bot the command sets args.userId itself from
3819
+ // the target, and claiming THAT was an override would be a new lie.
3820
+ if (argv["channel-id"]) {
3821
+ args.channelId = argv["channel-id"];
3822
+ args.targetOverride = `--channel-id ${argv["channel-id"]}`;
3823
+ }
3824
+ if (argv["user-id"]) {
3825
+ args.userId = argv["user-id"];
3826
+ args.targetOverride = `--user-id ${argv["user-id"]}`;
3827
+ }
3828
+ let scheduleToken: string;
3829
+ if (argv["as-bot"]) {
3830
+ const botToken = requireBotToken();
3831
+ // Resolve @user → user_id with the *user* token (has users:read),
3832
+ // then let cmdScheduleSend open the DM with the bot token
3833
+ // (im:write). Same split as `send --as-bot`: the bot never needs
3834
+ // users:read, and the DM is bot↔user rather than a self-DM.
3835
+ if (!args.channelId && !args.userId && args.target.startsWith("@")) {
3836
+ try {
3837
+ args.userId = await resolveUserId(tok(argv as W), args.target, ck(argv as W));
3838
+ } catch (e: unknown) {
3839
+ console.error(
3840
+ `Error: could not resolve ${args.target} to a user for the bot DM.\n` +
3841
+ ` ${e instanceof Error ? e.message : String(e)}\n` +
3842
+ ` Tip: pass --user-id <Uxxxx> to skip the lookup.`,
3843
+ );
3844
+ process.exit(1);
3845
+ }
3846
+ }
3847
+ scheduleToken = botToken;
3848
+ args.asBot = true;
3849
+ } else {
3850
+ scheduleToken = tok(argv as W);
3851
+ // Session cookie for the token that will actually schedule — an
3852
+ // xoxc- desktop token is rejected by the public API without it.
3853
+ // Bot tokens need none.
3854
+ const cookie = ck(argv as W);
3855
+ if (cookie) args.cookie = cookie;
3856
+ }
3857
+ await cmdScheduleSend(scheduleToken, args);
3741
3858
  },
3742
3859
  )
3743
3860
  .command(
@@ -3745,9 +3862,20 @@ async function main(): Promise<void> {
3745
3862
  "List pending scheduled messages",
3746
3863
  (y2) => y2
3747
3864
  .positional("target", { type: "string", describe: "#channel to filter by" })
3748
- .option("channel-id", { type: "string", describe: "Raw channel ID" }),
3865
+ .option("channel-id", { type: "string", describe: "Raw channel ID" })
3866
+ .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
3867
  async (argv) => {
3750
- await cmdScheduleList(tok(argv as W), argv.target as string | undefined, argv["channel-id"], ck(argv as W));
3868
+ const target = argv.target as string | undefined;
3869
+ if (argv["as-bot"]) {
3870
+ const botToken = requireBotToken();
3871
+ let channelId = argv["channel-id"] as string | undefined;
3872
+ if (!channelId && target?.startsWith("@")) {
3873
+ channelId = await botDmForTarget(target, botToken, tok(argv as W), ck(argv as W));
3874
+ }
3875
+ await cmdScheduleList(botToken, target, channelId);
3876
+ return;
3877
+ }
3878
+ await cmdScheduleList(tok(argv as W), target, argv["channel-id"], ck(argv as W));
3751
3879
  },
3752
3880
  )
3753
3881
  .command(
@@ -3757,13 +3885,26 @@ async function main(): Promise<void> {
3757
3885
  .positional("target", { type: "string", demandOption: true, describe: "#channel or @user" })
3758
3886
  .positional("id", { type: "string", demandOption: true, describe: "Scheduled message ID" })
3759
3887
  .option("code", { type: "string", describe: "Safety hash to confirm" })
3760
- .option("channel-id", { type: "string", describe: "Raw channel ID" }),
3888
+ .option("channel-id", { type: "string", describe: "Raw channel ID" })
3889
+ .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
3890
  async (argv) => {
3762
3891
  const args: ScheduleRmArgs = { target: argv.target!, id: argv.id! };
3763
3892
  if (argv.code) args.code = argv.code;
3893
+ if (argv["channel-id"]) {
3894
+ args.channelId = argv["channel-id"];
3895
+ args.targetOverride = `--channel-id ${argv["channel-id"]}`;
3896
+ }
3897
+ if (argv["as-bot"]) {
3898
+ const botToken = requireBotToken();
3899
+ if (!args.channelId && args.target.startsWith("@")) {
3900
+ args.channelId = await botDmForTarget(args.target, botToken, tok(argv as W), ck(argv as W));
3901
+ }
3902
+ args.asBot = true;
3903
+ await cmdScheduleRm(botToken, args);
3904
+ return;
3905
+ }
3764
3906
  const cookie = ck(argv as W);
3765
3907
  if (cookie) args.cookie = cookie;
3766
- if (argv["channel-id"]) args.channelId = argv["channel-id"];
3767
3908
  await cmdScheduleRm(tok(argv as W), args);
3768
3909
  },
3769
3910
  )