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.
- package/dist/cli.js +112 -106
- package/package.json +1 -1
- package/ts/cli.ts +178 -19
package/package.json
CHANGED
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
if (
|
|
3740
|
-
|
|
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
|
-
|
|
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
|
)
|