slack-term 1.32.2 → 1.32.3

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.
Files changed (3) hide show
  1. package/dist/cli.js +108 -105
  2. package/package.json +1 -1
  3. package/ts/cli.ts +106 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.32.2",
3
+ "version": "1.32.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/cli.ts CHANGED
@@ -1168,7 +1168,62 @@ interface EditArgs {
1168
1168
  // Session cookie (xoxd) for `token` — required for an xoxc- desktop session
1169
1169
  // token to be accepted by the public Slack API at all.
1170
1170
  cookie?: string;
1171
+ asBot?: boolean;
1172
+ /** The USER token, kept for mention resolution when the edit itself runs as
1173
+ * the bot. A bot token has no users:read, and an unresolved @handle degrades
1174
+ * to plain text SILENTLY — which is the precise failure `edit` exists to
1175
+ * correct, so re-introducing it here would be circular. Same split as
1176
+ * `send --as-bot`. */
1177
+ mentionToken?: string;
1178
+ mentionCookie?: string;
1179
+ }
1180
+ /** The bot token, plus the bot↔user DM when the target names an `@handle`.
1181
+ *
1182
+ * Both halves are load-bearing. A bot token has no `users:read`, so resolving
1183
+ * `@bob` with it does not merely pick the wrong channel — it fails outright with
1184
+ * `missing_scope`, before the confirm gate is ever printed. And even where it
1185
+ * could resolve, a BOT-authored DM lives in the bot's conversation with that
1186
+ * person, not in the user's: looking the handle up as the user and opening the
1187
+ * DM as the bot is the only combination that names the message being corrected.
1188
+ * Same split `send --as-bot` makes.
1189
+ *
1190
+ * Found by the cross-vendor review of this change, which also named why the
1191
+ * first round of tests missed it: they all targeted a permalink, and
1192
+ * `resolveChannel` parses those locally without touching the API. */
1193
+ async function asBotTarget(
1194
+ target: string,
1195
+ userToken: string,
1196
+ userCookie: string | undefined,
1197
+ /** Already known from `--channel-id`. Passed in so the DM lookup is SKIPPED
1198
+ * rather than performed and discarded: `conversations.open` is a side effect,
1199
+ * and opening a DM the caller did not ask for is not undone by ignoring the
1200
+ * result. */
1201
+ knownChannelId?: string,
1202
+ ): Promise<{ botToken: string; channelId?: string }> {
1203
+ const botToken = resolveBotToken();
1204
+ if (!botToken) {
1205
+ console.error(
1206
+ "Error: --as-bot needs a bot token, but no xoxb- token was found.\n" +
1207
+ " Set SLACK_BOT_TOKEN=xoxb-... in ~/.config/slack-cli/.env (or your shell).",
1208
+ );
1209
+ process.exit(1);
1210
+ }
1211
+ if (knownChannelId) return { botToken };
1212
+ const { ref } = splitRefTs(target);
1213
+ if (!ref.startsWith("@")) return { botToken };
1214
+ try {
1215
+ const userId = await resolveUserId(userToken, ref, userCookie);
1216
+ return { botToken, channelId: await openDm(botToken, userId) };
1217
+ } catch (e: unknown) {
1218
+ console.error(
1219
+ `Error: could not resolve ${stripTerminalControls(ref)} to the bot's DM.\n` +
1220
+ ` ${e instanceof Error ? e.message : String(e)}\n` +
1221
+ ` Tip: pass the message permalink, or --channel-id <D…>, to skip the lookup.`,
1222
+ );
1223
+ process.exit(1);
1224
+ }
1171
1225
  }
1226
+
1172
1227
  async function cmdEdit(token: string, args: EditArgs): Promise<void> {
1173
1228
  const getSelf = selfLookup(token, args.cookie);
1174
1229
  const { ref, ts } = splitRefTs(args.target);
@@ -1192,8 +1247,9 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
1192
1247
  const originalText = typeof original.text === "string" ? original.text : "";
1193
1248
 
1194
1249
  // Convert @handle → <@USERID> before hashing/editing (unresolved stay as text).
1250
+ const mentionCookie = args.mentionCookie ?? args.cookie;
1195
1251
  const newText = args.mentions
1196
- ? await encodeMentions(token, args.newText, channelId, args.cookie ? { cookie: args.cookie } : {})
1252
+ ? await encodeMentions(args.mentionToken ?? token, args.newText, channelId, mentionCookie ? { cookie: mentionCookie } : {})
1197
1253
  : args.newText;
1198
1254
 
1199
1255
  // Identity is part of the hash for the same reason it is on `send`: a code
@@ -1205,7 +1261,7 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
1205
1261
  if (args.code !== code) {
1206
1262
  requireCode(args.code, code, [
1207
1263
  `--- Editing as -------------------------------`,
1208
- fromLine(self),
1264
+ fromLine(self, { asBot: args.asBot }),
1209
1265
  `--- Original message -------------------------`,
1210
1266
  ...originalText.split("\n").map((l) => ` ${l}`),
1211
1267
  `--- Replacing with ---------------------------`,
@@ -1224,6 +1280,7 @@ interface DeleteArgs {
1224
1280
  code?: string;
1225
1281
  channelId?: string;
1226
1282
  cookie?: string;
1283
+ asBot?: boolean;
1227
1284
  }
1228
1285
  async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
1229
1286
  const getSelf = selfLookup(token, args.cookie);
@@ -1255,7 +1312,7 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
1255
1312
  const dest = await destLabel(token, channelId, ref, args.cookie);
1256
1313
  requireCode(args.code, code, [
1257
1314
  `--- Deleting message -------------------------`,
1258
- fromLine(self),
1315
+ fromLine(self, { asBot: args.asBot }),
1259
1316
  ` → ${dest} at ${slackTsToIso(ts)}`,
1260
1317
  ...originalText.split("\n").map((l) => ` ${l}`),
1261
1318
  `---------------------------------------------`,
@@ -4021,15 +4078,39 @@ async function main(): Promise<void> {
4021
4078
  .positional("newText", { type: "string", demandOption: true })
4022
4079
  .option("code", { type: "string", describe: "Safety hash to confirm edit" })
4023
4080
  .option("channel-id", { type: "string", describe: "Raw channel ID" })
4024
- .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." }),
4081
+ .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." })
4082
+ .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." }),
4025
4083
  async (argv) => {
4026
4084
  const args: EditArgs = { target: argv.target!, newText: argv.newText! };
4027
4085
  if (argv.code) args.code = argv.code;
4028
4086
  if (argv["channel-id"]) args.channelId = argv["channel-id"];
4029
4087
  if (argv.mentions !== false) args.mentions = true;
4030
- const cookie = ck(argv as W);
4031
- if (cookie) args.cookie = cookie;
4032
- await cmdEdit(tok(argv as W), args);
4088
+ let editToken: string;
4089
+ if (argv["as-bot"]) {
4090
+ // --as-bot is not a switch on chat.update. It selects a DIFFERENT
4091
+ // resolved token, and until now `edit` never wired one — so every
4092
+ // message the bot posted was uncorrectable, and the workaround people
4093
+ // reached for was RESENDING, which this repo's own etiquette forbids.
4094
+ // Bot identity is what makes a DM notify at all, so "uncorrectable"
4095
+ // applied to exactly the messages that matter most.
4096
+ const resolved = await asBotTarget(args.target, tok(argv as W), ck(argv as W), args.channelId);
4097
+ editToken = resolved.botToken;
4098
+ if (resolved.channelId && !args.channelId) args.channelId = resolved.channelId;
4099
+ args.asBot = true;
4100
+ // Mentions still resolve on the USER token: the bot has no users:read,
4101
+ // and a silently unresolved @handle in a CORRECTION is the same defect
4102
+ // twice.
4103
+ args.mentionToken = tok(argv as W);
4104
+ const mc = ck(argv as W);
4105
+ if (mc) args.mentionCookie = mc;
4106
+ } else {
4107
+ editToken = tok(argv as W);
4108
+ // Not set under --as-bot: a bot token needs no session cookie, and
4109
+ // attaching one is how a working call starts failing.
4110
+ const cookie = ck(argv as W);
4111
+ if (cookie) args.cookie = cookie;
4112
+ }
4113
+ await cmdEdit(editToken, args);
4033
4114
  },
4034
4115
  )
4035
4116
  .command(
@@ -4038,14 +4119,28 @@ async function main(): Promise<void> {
4038
4119
  (y) => y
4039
4120
  .positional("target", { type: "string", demandOption: true, describe: "#chan:ts, @user:ts, or permalink" })
4040
4121
  .option("code", { type: "string", describe: "Safety hash to confirm delete" })
4041
- .option("channel-id", { type: "string", describe: "Raw channel ID" }),
4122
+ .option("channel-id", { type: "string", describe: "Raw channel ID" })
4123
+ .option("as-bot", { type: "boolean", default: false, describe: "Delete as the bot (xoxb / SLACK_BOT_TOKEN). REQUIRED for a message the bot posted: chat.delete refuses another token's message exactly as chat.update does." }),
4042
4124
  async (argv) => {
4043
4125
  const args: DeleteArgs = { target: argv.target! };
4044
4126
  if (argv.code) args.code = argv.code;
4045
4127
  if (argv["channel-id"]) args.channelId = argv["channel-id"];
4046
- const cookie = ck(argv as W);
4047
- if (cookie) args.cookie = cookie;
4048
- await cmdDelete(tok(argv as W), args);
4128
+ let deleteToken: string;
4129
+ if (argv["as-bot"]) {
4130
+ // The SECOND call site of one defect, not a second defect: `delete`
4131
+ // had the identical unconditional `tok(argv)`, and chat.delete refuses
4132
+ // another token's message exactly as chat.update does. Fixing one and
4133
+ // leaving the other is how the next person finds it the hard way.
4134
+ const resolved = await asBotTarget(args.target, tok(argv as W), ck(argv as W), args.channelId);
4135
+ deleteToken = resolved.botToken;
4136
+ if (resolved.channelId && !args.channelId) args.channelId = resolved.channelId;
4137
+ args.asBot = true;
4138
+ } else {
4139
+ deleteToken = tok(argv as W);
4140
+ const cookie = ck(argv as W);
4141
+ if (cookie) args.cookie = cookie;
4142
+ }
4143
+ await cmdDelete(deleteToken, args);
4049
4144
  },
4050
4145
  )
4051
4146
  .command(