slack-term 1.22.0 → 1.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/cli.ts CHANGED
@@ -15,6 +15,7 @@ import { cmdTail } from "./tail.ts";
15
15
 
16
16
  import {
17
17
  authTest,
18
+ authScopes,
18
19
  authTestSession,
19
20
  conversationInfoSession,
20
21
  createChannel,
@@ -1021,6 +1022,15 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
1021
1022
  let lastText = "";
1022
1023
  let lastUser = "?";
1023
1024
  let threadMsgs: Record<string, Json>[] = [];
1025
+ // Author of the destination's TRUE most recent message (thread-scoped for a
1026
+ // thread reply, channel-scoped for a top-level send) — feeds the unreplied-warn
1027
+ // check below. Tracks `user` (normal message) and `bot_id` (a `bot_message`
1028
+ // subtype post — e.g. `send --as-bot` — which carries bot_id/username but no
1029
+ // `user`) so ownership can be determined either way. Undefined when the
1030
+ // preview fetch failed (fail-soft).
1031
+ let lastMsgUserId: string | undefined;
1032
+ let lastMsgBotId: string | undefined;
1033
+ let lastMsgTs: string | undefined;
1024
1034
  if (threadTs) {
1025
1035
  try {
1026
1036
  // Fetch a generous window and preview its tail. conversations.replies is
@@ -1032,14 +1042,23 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
1032
1042
  // Bind the hash to the thread's most recent message: if someone replies
1033
1043
  // between preview and confirm, the code invalidates and re-previews.
1034
1044
  lastText = typeof lastMsg?.text === "string" ? lastMsg.text : "";
1045
+ lastMsgUserId = typeof lastMsg?.user === "string" ? lastMsg.user : undefined;
1046
+ lastMsgBotId = typeof lastMsg?.bot_id === "string" ? lastMsg.bot_id : undefined;
1047
+ lastMsgTs = typeof lastMsg?.ts === "string" ? lastMsg.ts : undefined;
1035
1048
  } catch {
1036
1049
  // best-effort preview only
1037
1050
  }
1038
1051
  } else {
1039
1052
  try {
1040
1053
  const ctx = (await history(token, channelId, 1)) as Record<string, Json>;
1041
- const lastMsg = asArray(ctx.messages).map(asRecord)
1042
- .filter((m) => m.subtype === undefined || m.subtype === null)[0];
1054
+ // limit=1 already narrows this to exactly the channel's true last message
1055
+ // (subtype or not) take it raw for the ownership check below. The
1056
+ // subtype filter is only for the human-readable preview text/author: it
1057
+ // deliberately hides system events (channel_join etc.), but a bot_message
1058
+ // post is a real message and must NOT be dropped here, or a bot's own
1059
+ // last post would silently disable the unreplied-warn for --as-bot sends.
1060
+ const rawMsgs = asArray(ctx.messages).map(asRecord);
1061
+ const lastMsg = rawMsgs.filter((m) => m.subtype === undefined || m.subtype === null)[0];
1043
1062
  lastText = typeof lastMsg?.text === "string" ? lastMsg.text : "";
1044
1063
  // Resolve the author's user ID to a display name for the preview; fall back
1045
1064
  // to the bot username, then the raw ID. userName() is fail-soft.
@@ -1048,11 +1067,54 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
1048
1067
  : typeof lastMsg?.username === "string"
1049
1068
  ? lastMsg.username
1050
1069
  : "?";
1070
+ const rawLast = rawMsgs[0];
1071
+ lastMsgUserId = typeof rawLast?.user === "string" ? rawLast.user : undefined;
1072
+ lastMsgBotId = typeof rawLast?.bot_id === "string" ? rawLast.bot_id : undefined;
1073
+ lastMsgTs = typeof rawLast?.ts === "string" ? rawLast.ts : undefined;
1051
1074
  } catch {
1052
1075
  // best-effort preview only
1053
1076
  }
1054
1077
  }
1055
1078
 
1079
+ // Unreplied guard: the destination's last message is our own and no one has
1080
+ // replied since (by definition, since it's still the most recent message) —
1081
+ // sending another new message risks spamming the same person twice. Warn
1082
+ // (never block) and point at `slack edit` on that message instead. Reply-status
1083
+ // based, not time-based, so it still fires even long after the last send.
1084
+ // Self-identity covers both a normal post (`user` = our user id) and a
1085
+ // `bot_message`-subtype post (`bot_id` = our app's bot id, no `user`), the
1086
+ // shape `send --as-bot` produces. Opt out with SLACK_UNREPLIED_WARN=0.
1087
+ if (process.env.SLACK_UNREPLIED_WARN !== "0" && lastMsgTs && (lastMsgUserId || lastMsgBotId)) {
1088
+ try {
1089
+ const self = await authScopes(token);
1090
+ const isSelfAuthor =
1091
+ (!!lastMsgUserId && !!self.userId && self.userId === lastMsgUserId) ||
1092
+ (!!lastMsgBotId && !!self.botId && self.botId === lastMsgBotId);
1093
+ if (isSelfAuthor) {
1094
+ // Prefer a real permalink (exact, copy-pasteable). If that lookup fails,
1095
+ // fall back to a form guaranteed parseable by `slack edit` regardless of
1096
+ // what shape `ref` is in (a bare channel/user name, a raw ID resolved
1097
+ // from a permalink, or a thread ref) — `#<channelId>:<ts>` always yields
1098
+ // a ts split, and `--channel-id` makes the leading "#name" irrelevant to
1099
+ // resolution, so it never depends on ref's original prefix or an
1100
+ // embedded thread_ts colliding with the appended message ts.
1101
+ let editCmd = `slack edit "#${channelId}:${lastMsgTs}" "<新しい本文>" --channel-id ${channelId}`;
1102
+ try {
1103
+ const permalink = await getPermalink(token, channelId, lastMsgTs);
1104
+ if (permalink) editCmd = `slack edit "${permalink}" "<新しい本文>"`;
1105
+ } catch {
1106
+ // fall back to the --channel-id form above
1107
+ }
1108
+ console.error(`⚠ 相手はまだ返信していません(最後のメッセージはあなたのものです)。`);
1109
+ console.error(` 連投を避けるため、新規送信でなく直前のメッセージの edit を検討してください:`);
1110
+ console.error(` ${editCmd}`);
1111
+ console.error(` このまま送信する場合は --code=XXXX で確定。`);
1112
+ }
1113
+ } catch {
1114
+ // best-effort; a failed self-identity lookup must not block the send
1115
+ }
1116
+ }
1117
+
1056
1118
  // Duplicate guard: if the outgoing message closely matches something already
1057
1119
  // in the thread, warn (never block). Catches re-posting a reply you already
1058
1120
  // sent. Threshold is deliberately high so only near-identical text trips it.