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/README.md +3 -0
- package/SKILL.md +3 -0
- package/dist/cli.js +175 -174
- package/package.json +1 -1
- package/ts/cli.ts +41 -30
- package/ts/slack.ts +3 -2
package/package.json
CHANGED
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
|
|
3185
|
-
//
|
|
3186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5777
|
-
//
|
|
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(
|
|
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(
|
|
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
|
|
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.
|