slack-term 1.31.0 → 1.31.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/dist/cli.js +97 -94
- package/package.json +1 -1
- package/ts/ask.ts +65 -5
- package/ts/cli.ts +18 -2
package/package.json
CHANGED
package/ts/ask.ts
CHANGED
|
@@ -101,6 +101,38 @@ export function isInvalidNotice(line: string | undefined): boolean {
|
|
|
101
101
|
export const ASK_RESOLVED_MARKER = "white_check_mark";
|
|
102
102
|
export const ASK_RESOLVED_PREFIX = `:${ASK_RESOLVED_MARKER}: `;
|
|
103
103
|
|
|
104
|
+
/** How a keycap is WRITTEN into the body, and how it is read back.
|
|
105
|
+
*
|
|
106
|
+
* Slack NORMALISES a unicode keycap in the stored `text`: post `1️⃣` and
|
|
107
|
+
* `conversations.history` returns `:one:`. So a body built with glyphs never
|
|
108
|
+
* reads back as what was sent, and `--waitFor` cannot recognise its own
|
|
109
|
+
* question. `poll` already handled this; `ask` did not, which made every
|
|
110
|
+
* pill-answered question uncollectable (reported from real use 2026-08-31:
|
|
111
|
+
* four questions answered, nothing collected for ~20 minutes).
|
|
112
|
+
*
|
|
113
|
+
* Both spellings are accepted on the way in: the shortcode is what Slack
|
|
114
|
+
* stores, and the glyph is what a body hand-edited in the Slack UI can be. */
|
|
115
|
+
/** How a keycap is WRITTEN into the body: the shortcode, because that is the
|
|
116
|
+
* form Slack stores and therefore the form that reads back unchanged. */
|
|
117
|
+
function askPill(i: number): string {
|
|
118
|
+
return `:${ASK_KEYCAPS[i]!.name}:`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function askStripPill(line: string, i: number): string | null {
|
|
122
|
+
const k = ASK_KEYCAPS[i]!;
|
|
123
|
+
for (const pre of [askPill(i), k.glyph]) {
|
|
124
|
+
if (line.startsWith(`${pre} `)) return line.slice(pre.length + 1);
|
|
125
|
+
if (line === pre) return "";
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** True if the line opens with ANY keycap — used only to find where the choice
|
|
131
|
+
* block starts; WHICH keycap it is gets checked afterwards, in order. */
|
|
132
|
+
function askIsPillLine(line: string): boolean {
|
|
133
|
+
return ASK_KEYCAPS.some((_k, i) => askStripPill(line, i) !== null);
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
/** One line, always: newlines inside a choice would break the line-based body. */
|
|
105
137
|
export function askFlatten(s: string): string {
|
|
106
138
|
return s.replace(/\s*\n\s*/g, " ");
|
|
@@ -116,7 +148,12 @@ export function askBuildText(question: string, body: string, reactable: string[]
|
|
|
116
148
|
lines.push("");
|
|
117
149
|
// Flattened: a choice is a pill label, and a newline in one would split it
|
|
118
150
|
// into a line the parser cannot tell from body text.
|
|
119
|
-
|
|
151
|
+
// Built with the SHORTCODE, matching what Slack stores. Building with the
|
|
152
|
+
// glyph worked only because the parser now accepts both — the body still
|
|
153
|
+
// came back as `:one:` and never matched what was sent. Writing the stored
|
|
154
|
+
// form keeps posted and read-back bytes identical, which is what the
|
|
155
|
+
// round-trip test can actually check. (`poll` already did this.)
|
|
156
|
+
lines.push(...reactable.map((s, i) => `${askPill(i)} ${askFlatten(s)}`));
|
|
120
157
|
if (overflow.length) {
|
|
121
158
|
lines.push("");
|
|
122
159
|
lines.push(...overflow.map((s, i) => `(${i + 11}) ${askFlatten(s)}`));
|
|
@@ -154,6 +191,29 @@ export type AskParsed =
|
|
|
154
191
|
* by a loose heuristic: it is the one part of the body this command wrote
|
|
155
192
|
* verbatim, so matching it exactly is what keeps an unrelated message from
|
|
156
193
|
* being polled as if it were an ask. */
|
|
194
|
+
/** Why `askParseMessage` rejected a body, in the reader's terms.
|
|
195
|
+
*
|
|
196
|
+
* The single generic "this is not an ask" line sent a real user re-checking
|
|
197
|
+
* permalinks that were fine, and cost ~20 minutes while pressed answers went
|
|
198
|
+
* uncollected (2026-08-31). A rejection has to say WHICH check failed, because
|
|
199
|
+
* the three causes have completely different fixes: wrong link, a body someone
|
|
200
|
+
* edited, or a bug here. */
|
|
201
|
+
export function askExplainReject(text: string): string {
|
|
202
|
+
if (!text.trim()) return "本文が空です (bot 投稿や添付のみのメッセージかもしれません)";
|
|
203
|
+
const lines = text.split("\n");
|
|
204
|
+
const last = lines[lines.length - 1] ?? "";
|
|
205
|
+
if (text.startsWith(ASK_RESOLVED_PREFIX)) {
|
|
206
|
+
return "回答済みマーク付きですが、引用された回答本文が見つかりません";
|
|
207
|
+
}
|
|
208
|
+
if (!/_$/.test(last)) {
|
|
209
|
+
return "末尾が `ask` の案内文ではありません — このメッセージは `slack ask` で投稿されたものではないか、本文が編集されています";
|
|
210
|
+
}
|
|
211
|
+
if (!lines[0]!.startsWith(ASK_MARKER_PREFIX) && !lines[0]!.startsWith("*")) {
|
|
212
|
+
return "先頭が質問文 (太字) ではありません — 本文が編集されている可能性があります";
|
|
213
|
+
}
|
|
214
|
+
return "選択肢の並びを読み取れません — 番号が 1 から連番になっていないか、本文が編集されています";
|
|
215
|
+
}
|
|
216
|
+
|
|
157
217
|
export function askParseMessage(text: string): AskParsed {
|
|
158
218
|
const lines = text.split("\n");
|
|
159
219
|
|
|
@@ -221,7 +281,7 @@ export function askParseMessage(text: string): AskParsed {
|
|
|
221
281
|
const raw: string[] = [];
|
|
222
282
|
while (i >= 0) {
|
|
223
283
|
const line = lines[i]!;
|
|
224
|
-
if (!
|
|
284
|
+
if (!askIsPillLine(line)) break;
|
|
225
285
|
raw.unshift(line);
|
|
226
286
|
i--;
|
|
227
287
|
}
|
|
@@ -231,9 +291,9 @@ export function askParseMessage(text: string): AskParsed {
|
|
|
231
291
|
// block starting at 2️⃣ would have it waiting on a pill nobody can press.
|
|
232
292
|
if (!raw.length || raw.length > ASK_MAX_REACTION_CHOICES) return { kind: "other" };
|
|
233
293
|
for (let k = 0; k < raw.length; k++) {
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
236
|
-
reactable.push(
|
|
294
|
+
const rest = askStripPill(raw[k]!, k);
|
|
295
|
+
if (rest === null) return { kind: "other" };
|
|
296
|
+
reactable.push(rest);
|
|
237
297
|
}
|
|
238
298
|
if (lines[i] !== "") return { kind: "other" };
|
|
239
299
|
if (i < end + 1) return { kind: "other" };
|
package/ts/cli.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
askBuildText,
|
|
22
22
|
askBuildResolvedText,
|
|
23
23
|
askParseMessage,
|
|
24
|
+
askExplainReject,
|
|
24
25
|
askFlatten,
|
|
25
26
|
applyInvalidNotice,
|
|
26
27
|
readInvalidNotice,
|
|
@@ -295,6 +296,17 @@ function slimMsg(m: Record<string, Json>): Record<string, Json> {
|
|
|
295
296
|
// Thread stats, present only on thread parents. Without these a caller
|
|
296
297
|
// reading top-level history cannot tell a post with replies from one
|
|
297
298
|
// without, and silently misses everything said inside the thread.
|
|
299
|
+
// Reactions, when the message carries any. `ask` and `poll` record ANSWERS
|
|
300
|
+
// as reactions, so omitting them made `--json` useless as the fallback
|
|
301
|
+
// people reach for when a poller fails — the pressed pills were right there
|
|
302
|
+
// in the API response and the CLI dropped them (reported 2026-08-31).
|
|
303
|
+
...(Array.isArray(m.reactions) && m.reactions.length
|
|
304
|
+
? { reactions: asArray(m.reactions).map(asRecord).map((r) => ({
|
|
305
|
+
name: r.name ?? null,
|
|
306
|
+
count: r.count ?? null,
|
|
307
|
+
users: Array.isArray(r.users) ? r.users : [],
|
|
308
|
+
})) }
|
|
309
|
+
: {}),
|
|
298
310
|
...(m.reply_count !== undefined ? { reply_count: m.reply_count } : {}),
|
|
299
311
|
...(m.reply_users_count !== undefined ? { reply_users_count: m.reply_users_count } : {}),
|
|
300
312
|
...(m.latest_reply !== undefined ? { latest_reply: m.latest_reply } : {}),
|
|
@@ -2235,8 +2247,12 @@ async function cmdAskWaitFor(token: string, args: { link: string; timeout: numbe
|
|
|
2235
2247
|
const parsed = askParseMessage(text);
|
|
2236
2248
|
if (parsed.kind === "other") {
|
|
2237
2249
|
console.error(
|
|
2238
|
-
`Error: これは \`slack ask\`
|
|
2239
|
-
` ${
|
|
2250
|
+
`Error: これは \`slack ask\` の質問として読み取れません。\n` +
|
|
2251
|
+
` 理由: ${askExplainReject(text)}\n` +
|
|
2252
|
+
` ${stripTerminalControls(args.link)}\n` +
|
|
2253
|
+
// The pills are still readable by hand, so a failure here is not a dead
|
|
2254
|
+
// end — say so, because the reported cost was not knowing that.
|
|
2255
|
+
` (押されたリアクションは残っています: slack read '${stripTerminalControls(args.link)}' で確認できます)`,
|
|
2240
2256
|
);
|
|
2241
2257
|
process.exit(ASK_EXIT_ERROR);
|
|
2242
2258
|
}
|