slack-term 1.31.0 → 1.31.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 +97 -94
- package/package.json +1 -1
- package/ts/ask.ts +53 -4
- package/ts/cli.ts +18 -2
package/package.json
CHANGED
package/ts/ask.ts
CHANGED
|
@@ -101,6 +101,32 @@ 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
|
+
function askStripPill(line: string, i: number): string | null {
|
|
116
|
+
const k = ASK_KEYCAPS[i]!;
|
|
117
|
+
for (const pre of [`:${k.name}:`, k.glyph]) {
|
|
118
|
+
if (line.startsWith(`${pre} `)) return line.slice(pre.length + 1);
|
|
119
|
+
if (line === pre) return "";
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** True if the line opens with ANY keycap — used only to find where the choice
|
|
125
|
+
* block starts; WHICH keycap it is gets checked afterwards, in order. */
|
|
126
|
+
function askIsPillLine(line: string): boolean {
|
|
127
|
+
return ASK_KEYCAPS.some((_k, i) => askStripPill(line, i) !== null);
|
|
128
|
+
}
|
|
129
|
+
|
|
104
130
|
/** One line, always: newlines inside a choice would break the line-based body. */
|
|
105
131
|
export function askFlatten(s: string): string {
|
|
106
132
|
return s.replace(/\s*\n\s*/g, " ");
|
|
@@ -154,6 +180,29 @@ export type AskParsed =
|
|
|
154
180
|
* by a loose heuristic: it is the one part of the body this command wrote
|
|
155
181
|
* verbatim, so matching it exactly is what keeps an unrelated message from
|
|
156
182
|
* being polled as if it were an ask. */
|
|
183
|
+
/** Why `askParseMessage` rejected a body, in the reader's terms.
|
|
184
|
+
*
|
|
185
|
+
* The single generic "this is not an ask" line sent a real user re-checking
|
|
186
|
+
* permalinks that were fine, and cost ~20 minutes while pressed answers went
|
|
187
|
+
* uncollected (2026-08-31). A rejection has to say WHICH check failed, because
|
|
188
|
+
* the three causes have completely different fixes: wrong link, a body someone
|
|
189
|
+
* edited, or a bug here. */
|
|
190
|
+
export function askExplainReject(text: string): string {
|
|
191
|
+
if (!text.trim()) return "本文が空です (bot 投稿や添付のみのメッセージかもしれません)";
|
|
192
|
+
const lines = text.split("\n");
|
|
193
|
+
const last = lines[lines.length - 1] ?? "";
|
|
194
|
+
if (text.startsWith(ASK_RESOLVED_PREFIX)) {
|
|
195
|
+
return "回答済みマーク付きですが、引用された回答本文が見つかりません";
|
|
196
|
+
}
|
|
197
|
+
if (!/_$/.test(last)) {
|
|
198
|
+
return "末尾が `ask` の案内文ではありません — このメッセージは `slack ask` で投稿されたものではないか、本文が編集されています";
|
|
199
|
+
}
|
|
200
|
+
if (!lines[0]!.startsWith(ASK_MARKER_PREFIX) && !lines[0]!.startsWith("*")) {
|
|
201
|
+
return "先頭が質問文 (太字) ではありません — 本文が編集されている可能性があります";
|
|
202
|
+
}
|
|
203
|
+
return "選択肢の並びを読み取れません — 番号が 1 から連番になっていないか、本文が編集されています";
|
|
204
|
+
}
|
|
205
|
+
|
|
157
206
|
export function askParseMessage(text: string): AskParsed {
|
|
158
207
|
const lines = text.split("\n");
|
|
159
208
|
|
|
@@ -221,7 +270,7 @@ export function askParseMessage(text: string): AskParsed {
|
|
|
221
270
|
const raw: string[] = [];
|
|
222
271
|
while (i >= 0) {
|
|
223
272
|
const line = lines[i]!;
|
|
224
|
-
if (!
|
|
273
|
+
if (!askIsPillLine(line)) break;
|
|
225
274
|
raw.unshift(line);
|
|
226
275
|
i--;
|
|
227
276
|
}
|
|
@@ -231,9 +280,9 @@ export function askParseMessage(text: string): AskParsed {
|
|
|
231
280
|
// block starting at 2️⃣ would have it waiting on a pill nobody can press.
|
|
232
281
|
if (!raw.length || raw.length > ASK_MAX_REACTION_CHOICES) return { kind: "other" };
|
|
233
282
|
for (let k = 0; k < raw.length; k++) {
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
236
|
-
reactable.push(
|
|
283
|
+
const rest = askStripPill(raw[k]!, k);
|
|
284
|
+
if (rest === null) return { kind: "other" };
|
|
285
|
+
reactable.push(rest);
|
|
237
286
|
}
|
|
238
287
|
if (lines[i] !== "") return { kind: "other" };
|
|
239
288
|
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
|
}
|