Skip to content

Commit f623c19

Browse files
authored
fix(twitch): reject invalid IRC timestamps (#908)
1 parent f158555 commit f623c19

3 files changed

Lines changed: 35 additions & 7 deletions

File tree

packages/bots/twitch/src/index.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { connect as netConnect, type Socket } from "node:net";
22
import { connect as tlsConnect } from "node:tls";
33
import { defineBot, tokenSetup, type BotCtx, type BotEvent, type BotHandler } from "@profullstack/sh1pt-core";
4+
import { parseTwitchTimestamp } from "./timestamp.js";
45

56
// Twitch bot using Twitch IRC chat over TLS. OAuth token via TWITCH_OAUTH_TOKEN
67
// with chat:read and chat:write scopes.
@@ -203,7 +204,7 @@ function toBotEvent(message: IrcMessage, commandPrefix: string): BotEvent | unde
203204
const channel = toChannelName(message.params[0] ?? "");
204205
const text = message.trailing ?? "";
205206
const user = parseUser(message);
206-
const timestamp = tagTimestamp(message.tags["tmi-sent-ts"]);
207+
const timestamp = parseTwitchTimestamp(message.tags["tmi-sent-ts"]);
207208
if (text.startsWith(commandPrefix) && text.length > commandPrefix.length) {
208209
const [command, ...args] = text.slice(commandPrefix.length).trim().split(/\s+/).filter(Boolean);
209210
if (command) {
@@ -254,12 +255,6 @@ function parseUser(message: IrcMessage): BotEvent["user"] {
254255
};
255256
}
256257

257-
function tagTimestamp(value: string | undefined): string {
258-
if (!value) return new Date().toISOString();
259-
const timestamp = Number(value);
260-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : new Date().toISOString();
261-
}
262-
263258
function matches(handler: BotHandler, event: BotEvent): boolean {
264259
const match = handler.match;
265260
if (match.type !== event.type) return false;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import { parseTwitchTimestamp } from "./timestamp.js";
3+
4+
afterEach(() => {
5+
vi.useRealTimers();
6+
});
7+
8+
describe("parseTwitchTimestamp", () => {
9+
it("converts a decimal millisecond timestamp", () => {
10+
expect(parseTwitchTimestamp("1700000000000")).toBe("2023-11-14T22:13:20.000Z");
11+
});
12+
13+
it.each([
14+
undefined,
15+
"",
16+
"1e3",
17+
"1.5",
18+
"-1",
19+
" 1700000000000 ",
20+
"9007199254740992",
21+
"8640000000000001",
22+
])("falls back to the current time for an invalid timestamp: %s", (value) => {
23+
vi.useFakeTimers().setSystemTime(new Date("2026-08-01T18:20:00.000Z"));
24+
expect(parseTwitchTimestamp(value)).toBe("2026-08-01T18:20:00.000Z");
25+
});
26+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function parseTwitchTimestamp(value: string | undefined): string {
2+
if (!value || !/^\d+$/.test(value)) return new Date().toISOString();
3+
const timestamp = Number(value);
4+
if (!Number.isSafeInteger(timestamp)) return new Date().toISOString();
5+
const date = new Date(timestamp);
6+
return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString();
7+
}

0 commit comments

Comments
 (0)