slack-term 1.27.0 → 1.28.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.
- package/dist/cli.js +130 -113
- package/package.json +2 -2
- package/ts/ask.ts +78 -7
- package/ts/cli.ts +574 -22
- package/ts/poll.ts +244 -0
- package/ts/slack.ts +27 -12
- package/ts/todo.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slack-term",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/snomiao/slack-term.git"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"build": "bun build ts/cli.ts --target=node --outfile dist/cli.js --minify",
|
|
61
61
|
"typecheck": "tsgo --noEmit",
|
|
62
62
|
"typecheck:tsc": "tsc --noEmit",
|
|
63
|
-
"test": "bun test tests/ --timeout 90000 --
|
|
63
|
+
"test": "bun test tests/ --timeout 90000 --parallel=2",
|
|
64
64
|
"test:watch": "bun test tests/ --timeout 90000 --isolate --watch",
|
|
65
65
|
"test:parity": "bash tests/parity.sh",
|
|
66
66
|
"record": "bun run tests/record.ts",
|
package/ts/ask.ts
CHANGED
|
@@ -22,11 +22,25 @@ export const ASK_MAX_REACTION_CHOICES = ASK_KEYCAPS.length;
|
|
|
22
22
|
|
|
23
23
|
export type AskFound = { answer: string; how: string; who?: string };
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
/** The marker that says "this message is an `ask`". A shortcode, so it is the
|
|
26
|
+
* SAME token in every language — that is the whole point of it.
|
|
27
|
+
*
|
|
28
|
+
* Identity used to live in the trailing Japanese instruction line, which made
|
|
29
|
+
* the format untranslatable: rewording it in any language would have made every
|
|
30
|
+
* in-flight question unparseable. The marker carries identity now, and the
|
|
31
|
+
* instruction is free to be translated.
|
|
32
|
+
*
|
|
33
|
+
* It is also seeded as a REACTION, which is what makes `has:question` list
|
|
34
|
+
* every question the way `has:pushpin` lists every todo. `todo` gave this
|
|
35
|
+
* emoji up (its needs-discussion flag moved to :thinking_face:) precisely so
|
|
36
|
+
* the two would not collide in search. */
|
|
37
|
+
export const ASK_MARKER = "question";
|
|
38
|
+
export const ASK_MARKER_PREFIX = `:${ASK_MARKER}: `;
|
|
39
|
+
|
|
40
|
+
// The instruction lines. These are ordinary copy now — translate them freely.
|
|
41
|
+
// They are still matched verbatim when reading a message posted BEFORE the
|
|
42
|
+
// marker existed, which is the only reason the exact old strings survive here:
|
|
43
|
+
// deleting them would strand every question still in flight.
|
|
30
44
|
const ASK_INSTRUCTION_REACTION_THREAD =
|
|
31
45
|
"_下のリアクションを 1 つ押すと回答になります。当てはまるものがなければ、このメッセージの *スレッド* で返信してください (チャンネルへの通常投稿は回答として拾いません)。_";
|
|
32
46
|
const ASK_INSTRUCTION_REACTION_HERE =
|
|
@@ -37,10 +51,55 @@ const ASK_INSTRUCTION_TEXT_HERE =
|
|
|
37
51
|
"_このメッセージに返信してください。本文がそのまま回答になります。_";
|
|
38
52
|
const ASK_OVERFLOW_NOTE = "_11 番目以降はリアクションがないので、返信で答えてください。_";
|
|
39
53
|
|
|
54
|
+
// The invalid-ballot notice, shared by `ask` and `poll` and written INTO the
|
|
55
|
+
// message rather than only onto the collector's terminal. The person who has to
|
|
56
|
+
// fix it is the voter, and the voter is in Slack — a warning that only the
|
|
57
|
+
// collector sees never reaches them.
|
|
58
|
+
//
|
|
59
|
+
// A line in the body, not a thread reply, and that is the whole point: it is
|
|
60
|
+
// rewritten from the CURRENT state on every collection pass, so it cannot
|
|
61
|
+
// duplicate across reruns and it disappears by itself the moment the extra
|
|
62
|
+
// reaction is taken back. A reply, once posted, is a permanent record of a
|
|
63
|
+
// situation that has since been fixed.
|
|
64
|
+
const INVALID_PREFIX = "_:warning: 同時に複数選ばれているため回答として数えていません — どれか 1 つだけ残してください: ";
|
|
65
|
+
const INVALID_SUFFIX = "_";
|
|
66
|
+
|
|
67
|
+
/** The IDs named by a message's invalid-ballot line, or [] if it has none. */
|
|
68
|
+
export function readInvalidNotice(text: string): string[] {
|
|
69
|
+
const lines = text.split("\n");
|
|
70
|
+
const line = lines[lines.length - 2];
|
|
71
|
+
if (line === undefined || !line.startsWith(INVALID_PREFIX) || !line.endsWith(INVALID_SUFFIX)) return [];
|
|
72
|
+
return [...line.matchAll(/<@([UW][A-Z0-9]+)>/g)].map((m) => m[1]!);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Put `invalid` into the body — inserting, replacing or REMOVING the notice so
|
|
76
|
+
* the result depends only on the current state, never on how many times this
|
|
77
|
+
* has run. Returns the text unchanged when nothing needs to move, which is the
|
|
78
|
+
* caller's signal to skip the edit entirely.
|
|
79
|
+
*
|
|
80
|
+
* The notice sits directly above the instruction line because that line has to
|
|
81
|
+
* stay last: it is what identifies the message as an `ask`/`poll` at all. */
|
|
82
|
+
export function applyInvalidNotice(text: string, invalid: string[]): string {
|
|
83
|
+
const lines = text.split("\n");
|
|
84
|
+
if (lines.length < 2) return text;
|
|
85
|
+
if (readInvalidNotice(text).length) lines.splice(lines.length - 2, 1);
|
|
86
|
+
if (invalid.length) {
|
|
87
|
+
lines.splice(lines.length - 1, 0, `${INVALID_PREFIX}${invalid.map((u) => `<@${u}>`).join(", ")}${INVALID_SUFFIX}`);
|
|
88
|
+
}
|
|
89
|
+
const out = lines.join("\n");
|
|
90
|
+
return out === text ? text : out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** True if this line is the notice — parsers skip it when walking up. */
|
|
94
|
+
export function isInvalidNotice(line: string | undefined): boolean {
|
|
95
|
+
return line !== undefined && line.startsWith(INVALID_PREFIX) && line.endsWith(INVALID_SUFFIX);
|
|
96
|
+
}
|
|
97
|
+
|
|
40
98
|
/** Prefix `markResolved` stamps on a settled question. `--waitFor` keys "this
|
|
41
99
|
* was already answered while nobody was watching" off it, which is the whole
|
|
42
100
|
* reason a fire-and-forget ask can be collected later at all. */
|
|
43
|
-
export const
|
|
101
|
+
export const ASK_RESOLVED_MARKER = "white_check_mark";
|
|
102
|
+
export const ASK_RESOLVED_PREFIX = `:${ASK_RESOLVED_MARKER}: `;
|
|
44
103
|
|
|
45
104
|
/** One line, always: newlines inside a choice would break the line-based body. */
|
|
46
105
|
function askFlatten(s: string): string {
|
|
@@ -51,7 +110,7 @@ function askFlatten(s: string): string {
|
|
|
51
110
|
* and an instruction naming the answer paths this command actually reads.
|
|
52
111
|
* `askParseMessage` is its inverse — keep the two in step. */
|
|
53
112
|
export function askBuildText(question: string, body: string, reactable: string[], overflow: string[], threadOnly: boolean): string {
|
|
54
|
-
const lines: string[] = [
|
|
113
|
+
const lines: string[] = [`${ASK_MARKER_PREFIX}*${question}*`];
|
|
55
114
|
if (body) lines.push("", body);
|
|
56
115
|
if (reactable.length) {
|
|
57
116
|
lines.push("");
|
|
@@ -98,6 +157,9 @@ export type AskParsed =
|
|
|
98
157
|
export function askParseMessage(text: string): AskParsed {
|
|
99
158
|
const lines = text.split("\n");
|
|
100
159
|
|
|
160
|
+
// Resolved is checked FIRST: a settled question carries the ✅ prefix where an
|
|
161
|
+
// open one carries the ❓ marker, and reading a resolved body as an open one
|
|
162
|
+
// would re-poll a question that already has its answer.
|
|
101
163
|
if (text.startsWith(ASK_RESOLVED_PREFIX)) {
|
|
102
164
|
const head = lines[0]!.slice(ASK_RESOLVED_PREFIX.length);
|
|
103
165
|
const question = head.replace(/^\*/, "").replace(/\*$/, "");
|
|
@@ -114,6 +176,10 @@ export function askParseMessage(text: string): AskParsed {
|
|
|
114
176
|
const last = lines[lines.length - 1] ?? "";
|
|
115
177
|
let threadOnly: boolean;
|
|
116
178
|
let hasChoices: boolean;
|
|
179
|
+
// Old-format fallback. A question posted before the marker existed is
|
|
180
|
+
// identified the only way it can be: by the instruction line it was built
|
|
181
|
+
// with. Never delete these cases — they are what keeps those questions
|
|
182
|
+
// collectable.
|
|
117
183
|
switch (last) {
|
|
118
184
|
case ASK_INSTRUCTION_REACTION_THREAD: threadOnly = true; hasChoices = true; break;
|
|
119
185
|
case ASK_INSTRUCTION_REACTION_HERE: threadOnly = false; hasChoices = true; break;
|
|
@@ -125,6 +191,10 @@ export function askParseMessage(text: string): AskParsed {
|
|
|
125
191
|
// The question is the leading bold run. Taking it up to the first line that
|
|
126
192
|
// closes the `*` keeps a multi-line question intact instead of truncating it
|
|
127
193
|
// to its first line.
|
|
194
|
+
// Strip the marker before reading the question, so the bold run starts at
|
|
195
|
+
// index 0 either way and a pre-marker body still parses.
|
|
196
|
+
const head = lines[0]!.startsWith(ASK_MARKER_PREFIX) ? lines[0]!.slice(ASK_MARKER_PREFIX.length) : lines[0]!;
|
|
197
|
+
lines[0] = head;
|
|
128
198
|
if (!lines[0]!.startsWith("*")) return { kind: "other" };
|
|
129
199
|
let end = 0;
|
|
130
200
|
while (end < lines.length && !(lines[end]!.endsWith("*") && (end > 0 || lines[0]!.length > 1))) end++;
|
|
@@ -139,6 +209,7 @@ export function askParseMessage(text: string): AskParsed {
|
|
|
139
209
|
// would let a body line that merely starts with a keycap ("1️⃣ これは本文です")
|
|
140
210
|
// steal a choice slot and shift every number after it.
|
|
141
211
|
let i = lines.length - 2; // the instruction itself is lines[length-1]
|
|
212
|
+
if (isInvalidNotice(lines[i])) i--;
|
|
142
213
|
if (lines[i] !== "") return { kind: "other" };
|
|
143
214
|
i--;
|
|
144
215
|
if (lines[i] === ASK_OVERFLOW_NOTE) {
|