slack-term 1.52.0 → 1.53.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 +226 -226
- package/package.json +1 -1
- package/ts/cli.ts +9 -4
- package/ts/urlGuard.ts +43 -6
package/package.json
CHANGED
package/ts/cli.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
9
9
|
|
|
10
10
|
import yargs, { type Options } from "yargs";
|
|
11
11
|
import { hideBin } from "yargs/helpers";
|
|
12
|
-
import { guardUrlBoundaries } from "./urlGuard.ts";
|
|
12
|
+
import { fixUrlBoundaries, guardUrlBoundaries } from "./urlGuard.ts";
|
|
13
13
|
import { listProfiles, removeProfile, resolveBotToken, resolveCookie, resolveToken, useProfile, type Profile } from "./profiles.ts";
|
|
14
14
|
import { diagnoseBotMessaging, formatDiagnosis } from "./botdoctor.ts";
|
|
15
15
|
import { cmdAuthLogin, cmdAuthLoginChrome, cmdAuthChrome, cmdAuthFirefox, cmdAuthToken, cmdAuthApp, cmdAuthSave, cmdAuthTokens } from "./auth.ts";
|
|
@@ -5825,7 +5825,7 @@ async function main(): Promise<void> {
|
|
|
5825
5825
|
* prefix and thread check first). */
|
|
5826
5826
|
type SendCli = {
|
|
5827
5827
|
workspace?: string | undefined; target?: string | undefined; message?: string | undefined; code?: string | undefined;
|
|
5828
|
-
"allow-url-adjacent"?: boolean | undefined; "channel-id"?: string | undefined; "user-id"?: string | undefined;
|
|
5828
|
+
"allow-url-adjacent"?: boolean | undefined; "fix-urls"?: boolean | undefined; "channel-id"?: string | undefined; "user-id"?: string | undefined;
|
|
5829
5829
|
"as-bot"?: boolean | undefined; broadcast?: boolean | undefined; mentions?: boolean | undefined;
|
|
5830
5830
|
json?: boolean | undefined;
|
|
5831
5831
|
};
|
|
@@ -5834,7 +5834,7 @@ async function main(): Promise<void> {
|
|
|
5834
5834
|
* its subscriptions. */
|
|
5835
5835
|
type EditCli = {
|
|
5836
5836
|
workspace?: string | undefined; target?: string | undefined; newText?: string | undefined; code?: string | undefined;
|
|
5837
|
-
force?: boolean | undefined; "allow-url-adjacent"?: boolean | undefined; "channel-id"?: string | undefined;
|
|
5837
|
+
force?: boolean | undefined; "allow-url-adjacent"?: boolean | undefined; "fix-urls"?: boolean | undefined; "channel-id"?: string | undefined;
|
|
5838
5838
|
mentions?: boolean | undefined; "as-bot"?: boolean | undefined; tick?: { n?: number } | undefined;
|
|
5839
5839
|
};
|
|
5840
5840
|
const runEdit = async (argv: EditCli): Promise<void> => {
|
|
@@ -5852,6 +5852,7 @@ async function main(): Promise<void> {
|
|
|
5852
5852
|
process.exit(1);
|
|
5853
5853
|
}
|
|
5854
5854
|
}
|
|
5855
|
+
if (argv["fix-urls"] && argv.newText) argv = { ...argv, newText: fixUrlBoundaries(argv.newText) };
|
|
5855
5856
|
const args: EditArgs = { target: argv.target!, newText: argv.newText ?? "", allowUrlAdjacent: argv["allow-url-adjacent"] === true };
|
|
5856
5857
|
if (argv.tick) args.tick = argv.tick;
|
|
5857
5858
|
if (settings.prefix) args.lanePrefix = settings.prefix;
|
|
@@ -5898,6 +5899,7 @@ async function main(): Promise<void> {
|
|
|
5898
5899
|
console.error("Error: message is empty");
|
|
5899
5900
|
process.exit(1);
|
|
5900
5901
|
}
|
|
5902
|
+
if (argv["fix-urls"]) argv = { ...argv, message: fixUrlBoundaries(argv.message!) };
|
|
5901
5903
|
const args: SendArgs = { target: argv.target!, message: argv.message!, allowUrlAdjacent: argv["allow-url-adjacent"] === true };
|
|
5902
5904
|
if (argv.json) args.json = true;
|
|
5903
5905
|
if (argv.code) args.code = argv.code;
|
|
@@ -6546,6 +6548,7 @@ async function main(): Promise<void> {
|
|
|
6546
6548
|
.positional("message", { type: "string", demandOption: true })
|
|
6547
6549
|
.option("code", { type: "string", describe: "Safety hash to confirm send" })
|
|
6548
6550
|
.option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
|
|
6551
|
+
.option("fix-urls", { type: "boolean", default: false, describe: "Wrap ambiguous bare URLs as <url> (cut at the first non-ASCII character / trailing punctuation) before sending" })
|
|
6549
6552
|
.option("channel-id", { type: "string", describe: "Raw channel ID" })
|
|
6550
6553
|
.option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
|
|
6551
6554
|
.option("as-bot", { type: "boolean", default: false, describe: "Send via the bot token (xoxb / SLACK_BOT_TOKEN) so a DM notifies the recipient and can be two-way" })
|
|
@@ -6724,6 +6727,7 @@ async function main(): Promise<void> {
|
|
|
6724
6727
|
.option("code", { type: "string", describe: "Safety hash to confirm send" })
|
|
6725
6728
|
.option("as-bot", { type: "boolean", describe: "Send via the bot token (default: settings' as_bot)" })
|
|
6726
6729
|
.option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
|
|
6730
|
+
.option("fix-urls", { type: "boolean", default: false, describe: "Wrap ambiguous bare URLs as <url> (cut at the first non-ASCII character / trailing punctuation) before sending" })
|
|
6727
6731
|
.option("broadcast", { type: "boolean", default: false, describe: "Also send to channel" })
|
|
6728
6732
|
.option("mentions", { type: "boolean", default: true, describe: "Convert @handle tokens to real mentions (--no-mentions for literal text)" }),
|
|
6729
6733
|
async (argv) => {
|
|
@@ -6758,7 +6762,7 @@ async function main(): Promise<void> {
|
|
|
6758
6762
|
if (!text.trim()) { console.error("Error: message is empty (only the prefix)"); process.exit(1); }
|
|
6759
6763
|
await runSend({
|
|
6760
6764
|
workspace: (argv as W).workspace, target: `${channel}:${threadTs}`, message: text, code: argv.code,
|
|
6761
|
-
"allow-url-adjacent": argv["allow-url-adjacent"], "as-bot": argv["as-bot"] ?? settings.as_bot ?? false,
|
|
6765
|
+
"allow-url-adjacent": argv["allow-url-adjacent"], "fix-urls": argv["fix-urls"], "as-bot": argv["as-bot"] ?? settings.as_bot ?? false,
|
|
6762
6766
|
broadcast: argv.broadcast, mentions: argv.mentions, json: argv.json,
|
|
6763
6767
|
});
|
|
6764
6768
|
},
|
|
@@ -7632,6 +7636,7 @@ async function main(): Promise<void> {
|
|
|
7632
7636
|
.option("code", { type: "string", describe: "Safety hash to confirm edit" })
|
|
7633
7637
|
.option("force", { type: "boolean", default: false, describe: "Apply an edit that BREAKS a `slack ask` / `slack poll` message (new text no longer reads as one, or an answered/void question made open again). Refused by default because collect reads that body back; edits that keep it readable (e.g. writing the ✅ form) need no flag. Prefer `slack ask --edit=` / `--void=`." })
|
|
7634
7638
|
.option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
|
|
7639
|
+
.option("fix-urls", { type: "boolean", default: false, describe: "Wrap ambiguous bare URLs as <url> (cut at the first non-ASCII character / trailing punctuation) before sending" })
|
|
7635
7640
|
.option("channel-id", { type: "string", describe: "Raw channel ID" })
|
|
7636
7641
|
.option("mentions", { type: "boolean", default: true, describe: "Convert @handle tokens in the new text to real <@USERID> mentions (on by default). Unresolved handles stay as plain text. Disable with --no-mentions for literal @text." })
|
|
7637
7642
|
.option("as-bot", { type: "boolean", default: false, describe: "Edit as the bot (xoxb / SLACK_BOT_TOKEN). REQUIRED to correct a message the bot posted: Slack only lets a token edit its OWN messages, so a bot-authored message is uneditable by the user token and chat.update returns cant_update_message. Address it by permalink or --channel-id; an @user target resolves against the BOT's DM list, which is where a bot-authored DM actually lives." }),
|
package/ts/urlGuard.ts
CHANGED
|
@@ -16,7 +16,15 @@ const URL_TOKEN = /<https?:\/\/[^\s<>|]+(?:\|[^<>\r\n]*)?>|https?:\/\/[^\t\n\v\f
|
|
|
16
16
|
/** What may sit right before a bare URL and still let it autolink. */
|
|
17
17
|
const SAFE_BEFORE = /[\t\n\v\f\r (*_~]/;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** `trailing`: the URL ends in ASCII punctuation (`)` without a matching `(`, or `.,:;!?`) that Slack may or may
|
|
20
|
+
* not take into the link: `(see https://x/a)` would link `https://x/a)`. Only warned, never refused. */
|
|
21
|
+
export type UrlBoundaryIssue = { url: string; side: "before" | "after" | "trailing"; char: string };
|
|
22
|
+
const TRAILING = /[.,:;!?]$/;
|
|
23
|
+
function trailingChar(url: string): string | undefined {
|
|
24
|
+
if (TRAILING.test(url)) return url.slice(-1);
|
|
25
|
+
if (url.endsWith(")") && count(url, "(") < count(url, ")")) return ")";
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
20
28
|
|
|
21
29
|
/** Code spans and fences are shown verbatim and never autolinked, so a URL in
|
|
22
30
|
* one is not a boundary problem; neither is a `[label](url)` link, which is
|
|
@@ -97,6 +105,11 @@ export function urlBoundaryIssues(text: string, { markdown = true }: { markdown?
|
|
|
97
105
|
}
|
|
98
106
|
const after = url.match(/[^\x00-\x7f]|>/);
|
|
99
107
|
if (after) issues.push({ url, side: "after", char: after[0] });
|
|
108
|
+
else {
|
|
109
|
+
// `(https://x/a)`: the `)` closes the paren before the URL, as GFM and Slack read it
|
|
110
|
+
const t = trailingChar(url);
|
|
111
|
+
if (t && !(t === ")" && before === "(")) issues.push({ url, side: "trailing", char: t });
|
|
112
|
+
}
|
|
100
113
|
}
|
|
101
114
|
return issues;
|
|
102
115
|
}
|
|
@@ -107,22 +120,25 @@ export function adjacentUrls(text: string): string[] {
|
|
|
107
120
|
|
|
108
121
|
export function guardUrlBoundaries(
|
|
109
122
|
text: string, allow = false, warn: (message: string) => void = console.error,
|
|
110
|
-
{ sides = ["before", "after"], markdown = true }: {
|
|
123
|
+
{ sides = ["before", "after", "trailing"], markdown = true }: {
|
|
111
124
|
sides?: ReadonlyArray<UrlBoundaryIssue["side"]>; markdown?: boolean;
|
|
112
125
|
} = {},
|
|
113
126
|
): void {
|
|
114
|
-
const
|
|
127
|
+
const all = urlBoundaryIssues(text, { markdown });
|
|
128
|
+
const trailing = all.filter((i) => i.side === "trailing" && sides.includes("trailing"));
|
|
129
|
+
if (trailing.length) warn(`⚠ URL ends in punctuation Slack may take into the link: ${trailing.map((i) => `${JSON.stringify(i.url)} → ${JSON.stringify(fixUrlBoundaries(`x ${i.url}`).slice(2))}`).join(", ")}. If the ${JSON.stringify(trailing[0]!.char)} is not part of the URL, send the suggested form (or --fix-urls).`);
|
|
130
|
+
const issues = all.filter((i) => i.side !== "trailing" && sides.includes(i.side));
|
|
115
131
|
if (!issues.length) return;
|
|
116
132
|
const described = issues.map((i) => i.side === "before"
|
|
117
133
|
? `${JSON.stringify(i.url)} (glued to ${JSON.stringify(i.char)} before it — Slack will NOT make it a link)`
|
|
118
|
-
: `${JSON.stringify(i.url)} (runs into ${JSON.stringify(i.char)} after it)`);
|
|
134
|
+
: `${JSON.stringify(i.url)} (runs into ${JSON.stringify(i.char)} after it; non-ASCII in a URL is OK only if intended, otherwise send ${JSON.stringify(fixUrlBoundaries(`x ${i.url}`).slice(2))} or put an ASCII space after the URL)`);
|
|
119
135
|
const fixes = issues.some((i) => i.side === "before")
|
|
120
136
|
? "Put an ASCII space (or a newline) before the URL, " : "";
|
|
121
137
|
const message = `Ambiguous URL boundary: ${described.join(", ")}. ` +
|
|
122
138
|
`${fixes}Put the URL on its own line, or wrap it as <url> / <url|label>. ` +
|
|
123
|
-
"Use --allow-url-adjacent to warn only.";
|
|
139
|
+
"Use --fix-urls to wrap them, or --allow-url-adjacent to warn only.";
|
|
124
140
|
if (!allow) throw new Error(message);
|
|
125
|
-
warn(
|
|
141
|
+
warn(`⚠ Warning: ${message}`);
|
|
126
142
|
}
|
|
127
143
|
|
|
128
144
|
type RichTextElement =
|
|
@@ -167,6 +183,27 @@ export function linkifyRichText(text: string): RichTextElement[] {
|
|
|
167
183
|
return out;
|
|
168
184
|
}
|
|
169
185
|
|
|
186
|
+
/** Optional auto-fix (`--fix-urls`): wrap every ambiguous bare URL as `<url>`, cut where Slack's link would end
|
|
187
|
+
* (at the first non-ASCII character, before trailing punctuation). Text, code spans and explicit links are kept. */
|
|
188
|
+
export function fixUrlBoundaries(text: string): string {
|
|
189
|
+
const masked = maskCode(text);
|
|
190
|
+
let out = "", pos = 0;
|
|
191
|
+
for (const m of masked.matchAll(URL_TOKEN)) {
|
|
192
|
+
const at = m.index ?? 0;
|
|
193
|
+
if (m[0].startsWith("<")) continue;
|
|
194
|
+
let url = text.slice(at, at + m[0].length);
|
|
195
|
+
const cut = url.search(/[^\x00-\x7f]|>/);
|
|
196
|
+
if (cut >= 0) url = url.slice(0, cut);
|
|
197
|
+
while (trailingChar(url)) url = url.slice(0, -1);
|
|
198
|
+
const before = at > 0 ? masked[at - 1]! : "";
|
|
199
|
+
const ambiguous = url.length < m[0].length || (before && !SAFE_BEFORE.test(before));
|
|
200
|
+
if (!ambiguous || !/^https?:\/\/./i.test(url)) continue;
|
|
201
|
+
out += text.slice(pos, at) + `<${url}>`;
|
|
202
|
+
pos = at + url.length;
|
|
203
|
+
}
|
|
204
|
+
return out + text.slice(pos);
|
|
205
|
+
}
|
|
206
|
+
|
|
170
207
|
function count(s: string, ch: string): number {
|
|
171
208
|
return s.split(ch).length - 1;
|
|
172
209
|
}
|