Skip to content

Commit d7e24e0

Browse files
committed
fix(slack): centralize timestamp parsing
1 parent 19c70e2 commit d7e24e0

5 files changed

Lines changed: 35 additions & 22 deletions

File tree

extensions/slack/src/monitor/message-handler/prepare-dm-history.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
44
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
55
import type { ResolvedSlackAccount } from "../../accounts.js";
66
import type { SlackMonitorContext } from "../context.js";
7+
import { resolveSlackTimestampMs } from "./timestamp.js";
78

89
type SlackDmHistoryMessage = {
910
text?: string;
@@ -19,17 +20,6 @@ type SlackDmHistoryEntry = {
1920
timestamp?: number;
2021
};
2122

22-
const SLACK_TIMESTAMP_RE = /^\d+(?:\.\d+)?$/;
23-
24-
function resolveSlackTimestampMs(ts: string | undefined): number | undefined {
25-
const trimmed = ts?.trim();
26-
if (!trimmed || !SLACK_TIMESTAMP_RE.test(trimmed)) {
27-
return undefined;
28-
}
29-
const parsed = Number(trimmed);
30-
return Number.isFinite(parsed) ? Math.round(parsed * 1000) : undefined;
31-
}
32-
3323
export function resolveSlackDmHistoryLimit(params: {
3424
account: ResolvedSlackAccount;
3525
userId?: string;

extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,27 @@ describe("resolveSlackThreadContextData", () => {
293293
expect(result.threadHistoryBody).not.toContain("Unknown (user)");
294294
});
295295

296+
it("does not coerce malformed thread history timestamps into event times", async () => {
297+
const { result } = await resolveAllowlistedThreadContext({
298+
repliesMessages: [
299+
{ text: "starter from Alice", user: "U1", ts: "100.000" },
300+
{ text: "malformed timestamp follow-up", user: "U1", ts: "0x65" },
301+
{ text: "current message", user: "U1", ts: "101.000" },
302+
],
303+
threadStarter: {
304+
text: "starter from Alice",
305+
userId: "U1",
306+
ts: "100.000",
307+
},
308+
allowFromLower: ["u1"],
309+
allowNameMatching: false,
310+
});
311+
312+
expect(result.threadHistoryBody).toContain("malformed timestamp follow-up");
313+
expect(result.threadHistoryBody).toContain("[slack message id: 0x65 channel: C123]");
314+
expect(result.threadHistoryBody).not.toContain("1970-01-01");
315+
});
316+
296317
it("includes self-authored starter (identified by bot user id) for a new thread session (default)", async () => {
297318
const { result } = await resolveAllowlistedThreadContext({
298319
repliesMessages: [

extensions/slack/src/monitor/message-handler/prepare-thread-context.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
resolveSlackThreadHistoryFilterPolicy,
2222
shouldIncludeBotThreadStarterContext,
2323
} from "./prepare-thread-context-root.js";
24+
import { resolveSlackTimestampMs } from "./timestamp.js";
2425

2526
type SlackMediaModule = typeof import("../media.js");
2627
let slackMediaModulePromise: Promise<SlackMediaModule> | undefined;
@@ -310,7 +311,7 @@ export async function resolveSlackThreadContextData(params: {
310311
formatInboundEnvelope({
311312
channel: "Slack",
312313
from: `${msgSenderName} (${role})`,
313-
timestamp: historyMsg.ts ? Math.round(Number(historyMsg.ts) * 1000) : undefined,
314+
timestamp: resolveSlackTimestampMs(historyMsg.ts),
314315
body: msgWithId,
315316
chatType: "channel",
316317
envelope: params.envelopeOptions,

extensions/slack/src/monitor/message-handler/prepare.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { resolveSlackDmHistoryContext, resolveSlackDmHistoryLimit } from "./prep
7272
import { resolveSlackRoutingContext } from "./prepare-routing.js";
7373
import { resolveSlackThreadContextData } from "./prepare-thread-context.js";
7474
import { isSlackSubteamMentionForBot, normalizeSlackId } from "./subteam-mentions.js";
75+
import { resolveSlackTimestampMs } from "./timestamp.js";
7576
import type { PreparedSlackMessage } from "./types.js";
7677

7778
const mentionRegexCache = new WeakMap<SlackMonitorContext, Map<string, RegExp[]>>();
@@ -83,7 +84,6 @@ const SLACK_HISTORY_MEDIA_MAX_ATTACHMENTS = 4;
8384
const SLACK_HISTORY_MEDIA_MAX_BYTES = 10 * 1024 * 1024;
8485
const SLACK_HISTORY_MEDIA_IDLE_TIMEOUT_MS = 1_000;
8586
const SLACK_HISTORY_MEDIA_TOTAL_TIMEOUT_MS = 3_000;
86-
const SLACK_TIMESTAMP_RE = /^\d+(?:\.\d+)?$/;
8787

8888
function recordString(
8989
record: Record<string, unknown> | undefined,
@@ -105,15 +105,6 @@ function recordNullableString(
105105
return normalizeOptionalString(record[key]);
106106
}
107107

108-
function resolveSlackTimestampMs(ts: string | undefined): number | undefined {
109-
const trimmed = ts?.trim();
110-
if (!trimmed || !SLACK_TIMESTAMP_RE.test(trimmed)) {
111-
return undefined;
112-
}
113-
const parsed = Number(trimmed);
114-
return Number.isFinite(parsed) ? Math.round(parsed * 1000) : undefined;
115-
}
116-
117108
function mergeSlackAssistantThreadContext(
118109
primary: Omit<SlackAssistantThreadContext, "updatedAt"> | undefined,
119110
fallback: Omit<SlackAssistantThreadContext, "updatedAt"> | undefined,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const SLACK_TIMESTAMP_RE = /^\d+(?:\.\d+)?$/;
2+
3+
export function resolveSlackTimestampMs(ts: string | undefined): number | undefined {
4+
const trimmed = ts?.trim();
5+
if (!trimmed || !SLACK_TIMESTAMP_RE.test(trimmed)) {
6+
return undefined;
7+
}
8+
const parsed = Number(trimmed);
9+
return Number.isFinite(parsed) ? Math.round(parsed * 1000) : undefined;
10+
}

0 commit comments

Comments
 (0)