mirror of https://github.com/openclaw/openclaw.git
156 lines
4.6 KiB
TypeScript
156 lines
4.6 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { loadConfig, resolveStorePath } from "openclaw/plugin-sdk/config-runtime";
|
|
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
|
|
|
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
const TELEGRAM_SENT_MESSAGES_STATE_KEY = Symbol.for("openclaw.telegramSentMessagesState");
|
|
|
|
type SentMessageStore = Map<string, Map<string, number>>;
|
|
|
|
type SentMessageState = {
|
|
persistedPath?: string;
|
|
store?: SentMessageStore;
|
|
};
|
|
|
|
function getSentMessageState(): SentMessageState {
|
|
const globalStore = globalThis as Record<PropertyKey, unknown>;
|
|
const existing = globalStore[TELEGRAM_SENT_MESSAGES_STATE_KEY] as SentMessageState | undefined;
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
const state: SentMessageState = {};
|
|
globalStore[TELEGRAM_SENT_MESSAGES_STATE_KEY] = state;
|
|
return state;
|
|
}
|
|
|
|
function createSentMessageStore(): SentMessageStore {
|
|
return new Map<string, Map<string, number>>();
|
|
}
|
|
|
|
function resolveSentMessageStorePath(): string {
|
|
const cfg = loadConfig();
|
|
return `${resolveStorePath(cfg.session?.store)}.telegram-sent-messages.json`;
|
|
}
|
|
|
|
function cleanupExpired(scopeKey: string, entry: Map<string, number>, now: number): void {
|
|
for (const [id, timestamp] of entry) {
|
|
if (now - timestamp > TTL_MS) {
|
|
entry.delete(id);
|
|
}
|
|
}
|
|
if (entry.size === 0) {
|
|
getSentMessages().delete(scopeKey);
|
|
}
|
|
}
|
|
|
|
function readPersistedSentMessages(filePath: string): SentMessageStore {
|
|
if (!fs.existsSync(filePath)) {
|
|
return createSentMessageStore();
|
|
}
|
|
try {
|
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
const parsed = JSON.parse(raw) as Record<string, Record<string, number>>;
|
|
const now = Date.now();
|
|
const store = createSentMessageStore();
|
|
for (const [chatId, entry] of Object.entries(parsed)) {
|
|
const messages = new Map<string, number>();
|
|
for (const [messageId, timestamp] of Object.entries(entry)) {
|
|
if (
|
|
typeof timestamp === "number" &&
|
|
Number.isFinite(timestamp) &&
|
|
now - timestamp <= TTL_MS
|
|
) {
|
|
messages.set(messageId, timestamp);
|
|
}
|
|
}
|
|
if (messages.size > 0) {
|
|
store.set(chatId, messages);
|
|
}
|
|
}
|
|
return store;
|
|
} catch (error) {
|
|
logVerbose(`telegram: failed to read sent-message cache: ${String(error)}`);
|
|
return createSentMessageStore();
|
|
}
|
|
}
|
|
|
|
function getSentMessages(): SentMessageStore {
|
|
const state = getSentMessageState();
|
|
const persistedPath = resolveSentMessageStorePath();
|
|
if (!state.store || state.persistedPath !== persistedPath) {
|
|
state.store = readPersistedSentMessages(persistedPath);
|
|
state.persistedPath = persistedPath;
|
|
}
|
|
return state.store;
|
|
}
|
|
|
|
function persistSentMessages(): void {
|
|
const state = getSentMessageState();
|
|
const store = state.store;
|
|
const filePath = state.persistedPath;
|
|
if (!store || !filePath) {
|
|
return;
|
|
}
|
|
const now = Date.now();
|
|
const serialized: Record<string, Record<string, number>> = {};
|
|
for (const [chatId, entry] of store) {
|
|
cleanupExpired(chatId, entry, now);
|
|
if (entry.size > 0) {
|
|
serialized[chatId] = Object.fromEntries(entry);
|
|
}
|
|
}
|
|
if (Object.keys(serialized).length === 0) {
|
|
fs.rmSync(filePath, { force: true });
|
|
return;
|
|
}
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
fs.writeFileSync(tempPath, JSON.stringify(serialized), "utf-8");
|
|
fs.renameSync(tempPath, filePath);
|
|
}
|
|
|
|
export function recordSentMessage(chatId: number | string, messageId: number): void {
|
|
const scopeKey = String(chatId);
|
|
const idKey = String(messageId);
|
|
const now = Date.now();
|
|
const store = getSentMessages();
|
|
let entry = store.get(scopeKey);
|
|
if (!entry) {
|
|
entry = new Map<string, number>();
|
|
store.set(scopeKey, entry);
|
|
}
|
|
entry.set(idKey, now);
|
|
if (entry.size > 100) {
|
|
cleanupExpired(scopeKey, entry, now);
|
|
}
|
|
try {
|
|
persistSentMessages();
|
|
} catch (error) {
|
|
logVerbose(`telegram: failed to persist sent-message cache: ${String(error)}`);
|
|
}
|
|
}
|
|
|
|
export function wasSentByBot(chatId: number | string, messageId: number): boolean {
|
|
const scopeKey = String(chatId);
|
|
const idKey = String(messageId);
|
|
const entry = getSentMessages().get(scopeKey);
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
cleanupExpired(scopeKey, entry, Date.now());
|
|
return entry.has(idKey);
|
|
}
|
|
|
|
export function clearSentMessageCache(): void {
|
|
const state = getSentMessageState();
|
|
getSentMessages().clear();
|
|
if (state.persistedPath) {
|
|
fs.rmSync(state.persistedPath, { force: true });
|
|
}
|
|
}
|
|
|
|
export function resetSentMessageCacheForTest(): void {
|
|
getSentMessageState().store = undefined;
|
|
}
|