forked from boos/BoosAPI
fix desktop tray icon telegram topics and checkin intervals
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const cronStopMock = vi.fn();
|
||||
const scheduleMock = vi.fn(() => ({
|
||||
stop: cronStopMock,
|
||||
}));
|
||||
const validateMock = vi.fn(() => true);
|
||||
const allMock = vi.fn();
|
||||
|
||||
vi.mock('node-cron', () => ({
|
||||
default: {
|
||||
schedule: (...args: unknown[]) => scheduleMock(...args),
|
||||
validate: (...args: unknown[]) => validateMock(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../db/index.js', () => {
|
||||
const queryChain = {
|
||||
where: () => queryChain,
|
||||
get: () => undefined,
|
||||
all: () => [],
|
||||
from: () => queryChain,
|
||||
innerJoin: () => queryChain,
|
||||
};
|
||||
|
||||
return {
|
||||
db: {
|
||||
select: () => queryChain,
|
||||
},
|
||||
schema: {
|
||||
settings: { key: 'key' },
|
||||
accounts: { checkinEnabled: 'checkinEnabled', status: 'status' },
|
||||
sites: { id: 'id' },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('./checkinService.js', () => ({
|
||||
checkinAll: (...args: unknown[]) => allMock(...args),
|
||||
}));
|
||||
|
||||
describe('checkinScheduler', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
cronStopMock.mockReset();
|
||||
scheduleMock.mockClear();
|
||||
validateMock.mockClear();
|
||||
allMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const scheduler = await import('./checkinScheduler.js');
|
||||
scheduler.__resetCheckinSchedulerForTests();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('switches from cron mode to interval mode and back', async () => {
|
||||
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
|
||||
const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval');
|
||||
const scheduler = await import('./checkinScheduler.js');
|
||||
|
||||
scheduler.updateCheckinSchedule({
|
||||
mode: 'cron',
|
||||
cronExpr: '0 8 * * *',
|
||||
intervalHours: 6,
|
||||
});
|
||||
expect(scheduleMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
scheduler.updateCheckinSchedule({
|
||||
mode: 'interval',
|
||||
intervalHours: 6,
|
||||
});
|
||||
expect(cronStopMock).toHaveBeenCalledTimes(1);
|
||||
expect(setIntervalSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
scheduler.updateCheckinSchedule({
|
||||
mode: 'cron',
|
||||
cronExpr: '5 9 * * *',
|
||||
intervalHours: 6,
|
||||
});
|
||||
expect(clearIntervalSpy).toHaveBeenCalledTimes(1);
|
||||
expect(scheduleMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('selects due accounts from the last successful checkin time', async () => {
|
||||
const scheduler = await import('./checkinScheduler.js');
|
||||
const now = new Date('2026-03-20T12:00:00.000Z');
|
||||
|
||||
expect(scheduler.selectDueIntervalCheckinAccountIds([
|
||||
{ id: 1, lastCheckinAt: null },
|
||||
{ id: 2, lastCheckinAt: '2026-03-20T05:59:59.000Z' },
|
||||
{ id: 3, lastCheckinAt: '2026-03-20T06:30:00.000Z' },
|
||||
], 6, now)).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
@@ -9,13 +9,18 @@ import { sendNotification } from './notifyService.js';
|
||||
import { buildDailySummaryNotification, collectDailySummaryMetrics } from './dailySummaryService.js';
|
||||
import { cleanupConfiguredLogs, normalizeLogCleanupRetentionDays } from './logCleanupService.js';
|
||||
|
||||
export type CheckinScheduleMode = 'cron' | 'interval';
|
||||
|
||||
let checkinTask: cron.ScheduledTask | null = null;
|
||||
let checkinIntervalTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let balanceTask: cron.ScheduledTask | null = null;
|
||||
let dailySummaryTask: cron.ScheduledTask | null = null;
|
||||
let logCleanupTask: cron.ScheduledTask | null = null;
|
||||
const intervalAttemptByAccount = new Map<number, number>();
|
||||
|
||||
const DAILY_SUMMARY_DEFAULT_CRON = '58 23 * * *';
|
||||
const LOG_CLEANUP_DEFAULT_CRON = '0 6 * * *';
|
||||
const CHECKIN_INTERVAL_POLL_MS = 60_000;
|
||||
|
||||
async function resolveJsonSetting<T>(
|
||||
settingKey: string,
|
||||
@@ -54,7 +59,7 @@ function createCheckinTask(cronExpr: string) {
|
||||
return cron.schedule(cronExpr, async () => {
|
||||
console.log(`[Scheduler] Running check-in at ${new Date().toISOString()}`);
|
||||
try {
|
||||
const results = await checkinAll();
|
||||
const results = await checkinAll({ scheduleMode: 'cron' });
|
||||
const success = results.filter((r) => r.result.success).length;
|
||||
const failed = results.length - success;
|
||||
console.log(`[Scheduler] Check-in complete: ${success} success, ${failed} failed`);
|
||||
@@ -64,6 +69,94 @@ function createCheckinTask(cronExpr: string) {
|
||||
});
|
||||
}
|
||||
|
||||
type IntervalCheckinCandidate = {
|
||||
id: number;
|
||||
lastCheckinAt?: string | null;
|
||||
};
|
||||
|
||||
export function selectDueIntervalCheckinAccountIds(
|
||||
rows: IntervalCheckinCandidate[],
|
||||
intervalHours: number,
|
||||
now = new Date(),
|
||||
attemptState = intervalAttemptByAccount,
|
||||
) {
|
||||
const nowMs = now.getTime();
|
||||
const intervalMs = Math.max(1, intervalHours) * 60 * 60 * 1000;
|
||||
|
||||
return rows
|
||||
.filter((row) => {
|
||||
const lastCheckinMs = row.lastCheckinAt ? Date.parse(row.lastCheckinAt) : Number.NaN;
|
||||
const lastAttemptMs = attemptState.get(row.id);
|
||||
if (Number.isFinite(lastCheckinMs)) {
|
||||
if (nowMs - lastCheckinMs < intervalMs) return false;
|
||||
if (typeof lastAttemptMs === 'number' && lastAttemptMs >= lastCheckinMs && nowMs - lastAttemptMs < intervalMs) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (typeof lastAttemptMs === 'number' && nowMs - lastAttemptMs < intervalMs) return false;
|
||||
return true;
|
||||
})
|
||||
.map((row) => row.id);
|
||||
}
|
||||
|
||||
async function runIntervalCheckinPass(now = new Date()) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(schema.accounts)
|
||||
.innerJoin(schema.sites, eq(schema.accounts.siteId, schema.sites.id))
|
||||
.all();
|
||||
|
||||
const dueAccountIds = selectDueIntervalCheckinAccountIds(
|
||||
rows
|
||||
.filter((row: any) => row.accounts?.checkinEnabled === true && row.accounts?.status === 'active' && row.sites?.status !== 'disabled')
|
||||
.map((row: any) => ({
|
||||
id: row.accounts.id,
|
||||
lastCheckinAt: row.accounts.lastCheckinAt,
|
||||
})),
|
||||
config.checkinIntervalHours,
|
||||
now,
|
||||
);
|
||||
|
||||
if (dueAccountIds.length === 0) return;
|
||||
|
||||
try {
|
||||
const results = await checkinAll({
|
||||
accountIds: dueAccountIds,
|
||||
scheduleMode: 'interval',
|
||||
});
|
||||
const nowMs = now.getTime();
|
||||
for (const item of results) {
|
||||
intervalAttemptByAccount.set(item.accountId, nowMs);
|
||||
}
|
||||
const success = results.filter((r) => r.result.success).length;
|
||||
const failed = results.length - success;
|
||||
console.log(`[Scheduler] Interval check-in complete: ${success} success, ${failed} failed`);
|
||||
} catch (err) {
|
||||
console.error('[Scheduler] Interval check-in error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function stopCheckinSchedule() {
|
||||
checkinTask?.stop();
|
||||
checkinTask = null;
|
||||
if (checkinIntervalTimer) {
|
||||
clearInterval(checkinIntervalTimer);
|
||||
checkinIntervalTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startCheckinSchedule() {
|
||||
stopCheckinSchedule();
|
||||
if (config.checkinScheduleMode === 'interval') {
|
||||
checkinIntervalTimer = setInterval(() => {
|
||||
void runIntervalCheckinPass();
|
||||
}, CHECKIN_INTERVAL_POLL_MS);
|
||||
return;
|
||||
}
|
||||
checkinTask = createCheckinTask(config.checkinCron);
|
||||
}
|
||||
|
||||
function createBalanceTask(cronExpr: string) {
|
||||
return cron.schedule(cronExpr, async () => {
|
||||
console.log(`[Scheduler] Refreshing balances at ${new Date().toISOString()}`);
|
||||
@@ -119,6 +212,15 @@ function createLogCleanupTask(cronExpr: string) {
|
||||
|
||||
export async function startScheduler() {
|
||||
const activeCheckinCron = await resolveCronSetting('checkin_cron', config.checkinCron);
|
||||
const activeCheckinScheduleMode = await resolveJsonSetting<CheckinScheduleMode>(
|
||||
'checkin_schedule_mode',
|
||||
(value): value is CheckinScheduleMode => value === 'cron' || value === 'interval',
|
||||
config.checkinScheduleMode as CheckinScheduleMode,
|
||||
);
|
||||
const activeCheckinIntervalHours = await resolvePositiveIntegerSetting(
|
||||
'checkin_interval_hours',
|
||||
config.checkinIntervalHours,
|
||||
);
|
||||
const activeBalanceCron = await resolveCronSetting('balance_refresh_cron', config.balanceRefreshCron);
|
||||
const activeDailySummaryCron = await resolveCronSetting('daily_summary_cron', DAILY_SUMMARY_DEFAULT_CRON);
|
||||
const activeLogCleanupCron = await resolveCronSetting('log_cleanup_cron', config.logCleanupCron || LOG_CLEANUP_DEFAULT_CRON);
|
||||
@@ -135,22 +237,24 @@ export async function startScheduler() {
|
||||
normalizeLogCleanupRetentionDays(config.logCleanupRetentionDays),
|
||||
);
|
||||
config.checkinCron = activeCheckinCron;
|
||||
config.checkinScheduleMode = activeCheckinScheduleMode;
|
||||
config.checkinIntervalHours = Math.min(24, Math.max(1, activeCheckinIntervalHours));
|
||||
config.balanceRefreshCron = activeBalanceCron;
|
||||
config.logCleanupCron = activeLogCleanupCron;
|
||||
config.logCleanupUsageLogsEnabled = activeLogCleanupUsageLogsEnabled;
|
||||
config.logCleanupProgramLogsEnabled = activeLogCleanupProgramLogsEnabled;
|
||||
config.logCleanupRetentionDays = activeLogCleanupRetentionDays;
|
||||
|
||||
checkinTask?.stop();
|
||||
stopCheckinSchedule();
|
||||
balanceTask?.stop();
|
||||
dailySummaryTask?.stop();
|
||||
logCleanupTask?.stop();
|
||||
checkinTask = createCheckinTask(activeCheckinCron);
|
||||
startCheckinSchedule();
|
||||
balanceTask = createBalanceTask(activeBalanceCron);
|
||||
dailySummaryTask = createDailySummaryTask(activeDailySummaryCron);
|
||||
logCleanupTask = createLogCleanupTask(activeLogCleanupCron);
|
||||
|
||||
console.log(`[Scheduler] Check-in cron: ${activeCheckinCron}`);
|
||||
console.log(`[Scheduler] Check-in schedule: ${config.checkinScheduleMode} (${config.checkinScheduleMode === 'cron' ? activeCheckinCron : `${config.checkinIntervalHours}h`})`);
|
||||
console.log(`[Scheduler] Balance refresh cron: ${activeBalanceCron}`);
|
||||
console.log(`[Scheduler] Daily summary cron: ${activeDailySummaryCron}`);
|
||||
console.log(
|
||||
@@ -159,10 +263,35 @@ export async function startScheduler() {
|
||||
}
|
||||
|
||||
export function updateCheckinCron(cronExpr: string) {
|
||||
if (!cron.validate(cronExpr)) throw new Error(`Invalid cron: ${cronExpr}`);
|
||||
config.checkinCron = cronExpr;
|
||||
checkinTask?.stop();
|
||||
checkinTask = createCheckinTask(cronExpr);
|
||||
updateCheckinSchedule({
|
||||
mode: 'cron',
|
||||
cronExpr,
|
||||
intervalHours: config.checkinIntervalHours,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateCheckinSchedule(input: {
|
||||
mode: CheckinScheduleMode;
|
||||
cronExpr?: string;
|
||||
intervalHours?: number;
|
||||
}) {
|
||||
const nextMode = input.mode;
|
||||
if (nextMode !== 'cron' && nextMode !== 'interval') {
|
||||
throw new Error(`Invalid checkin schedule mode: ${String(nextMode)}`);
|
||||
}
|
||||
|
||||
const nextCronExpr = input.cronExpr ?? config.checkinCron;
|
||||
if (!cron.validate(nextCronExpr)) throw new Error(`Invalid cron: ${nextCronExpr}`);
|
||||
|
||||
const nextIntervalHours = input.intervalHours ?? config.checkinIntervalHours;
|
||||
if (!Number.isFinite(nextIntervalHours) || nextIntervalHours < 1 || nextIntervalHours > 24) {
|
||||
throw new Error(`Invalid interval hours: ${String(nextIntervalHours)}`);
|
||||
}
|
||||
|
||||
config.checkinScheduleMode = nextMode;
|
||||
config.checkinCron = nextCronExpr;
|
||||
config.checkinIntervalHours = Math.trunc(nextIntervalHours);
|
||||
startCheckinSchedule();
|
||||
}
|
||||
|
||||
export function updateBalanceRefreshCron(cronExpr: string) {
|
||||
@@ -191,3 +320,14 @@ export function updateLogCleanupSettings(input: {
|
||||
logCleanupTask?.stop();
|
||||
logCleanupTask = createLogCleanupTask(cronExpr);
|
||||
}
|
||||
|
||||
export function __resetCheckinSchedulerForTests() {
|
||||
stopCheckinSchedule();
|
||||
balanceTask?.stop();
|
||||
dailySummaryTask?.stop();
|
||||
logCleanupTask?.stop();
|
||||
balanceTask = null;
|
||||
dailySummaryTask = null;
|
||||
logCleanupTask = null;
|
||||
intervalAttemptByAccount.clear();
|
||||
}
|
||||
|
||||
@@ -248,6 +248,63 @@ describe('checkinService auto relogin', () => {
|
||||
expect(notifyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not advance lastCheckinAt for already checked in responses in interval mode', async () => {
|
||||
selectAllMock.mockReturnValue([
|
||||
{
|
||||
accounts: {
|
||||
id: 16,
|
||||
username: 'interval-user',
|
||||
accessToken: 'token',
|
||||
status: 'active',
|
||||
extraConfig: null,
|
||||
},
|
||||
sites: {
|
||||
id: 16,
|
||||
name: 'demo',
|
||||
url: 'https://example.com',
|
||||
platform: 'new-api',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
adapterMock.checkin.mockResolvedValue({ success: false, message: '今天已经签到过啦' });
|
||||
|
||||
const { checkinAccount } = await import('./checkinService.js');
|
||||
const result = await checkinAccount(16, { scheduleMode: 'interval' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.status).toBe('success');
|
||||
expect(updateSetMock).not.toHaveBeenCalledWith(expect.objectContaining({ lastCheckinAt: expect.any(String) }));
|
||||
});
|
||||
|
||||
it('advances lastCheckinAt when interval mode gets a direct success', async () => {
|
||||
selectAllMock.mockReturnValue([
|
||||
{
|
||||
accounts: {
|
||||
id: 17,
|
||||
username: 'interval-success',
|
||||
accessToken: 'token',
|
||||
status: 'active',
|
||||
extraConfig: null,
|
||||
},
|
||||
sites: {
|
||||
id: 17,
|
||||
name: 'demo',
|
||||
url: 'https://example.com',
|
||||
platform: 'new-api',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
adapterMock.checkin.mockResolvedValue({ success: true, message: '签到成功' });
|
||||
|
||||
const { checkinAccount } = await import('./checkinService.js');
|
||||
const result = await checkinAccount(17, { scheduleMode: 'interval' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(updateSetMock).toHaveBeenCalledWith(expect.objectContaining({ lastCheckinAt: expect.any(String) }));
|
||||
});
|
||||
|
||||
it('treats unsupported checkin endpoint responses as skipped', async () => {
|
||||
selectAllMock.mockReturnValue([
|
||||
{
|
||||
|
||||
@@ -115,7 +115,7 @@ async function tryAutoRelogin(account: any, site: any): Promise<string | null> {
|
||||
return result.accessToken;
|
||||
}
|
||||
|
||||
export async function checkinAccount(accountId: number, options?: { skipEvent?: boolean }) {
|
||||
export async function checkinAccount(accountId: number, options?: { skipEvent?: boolean; scheduleMode?: 'cron' | 'interval' }) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(schema.accounts)
|
||||
@@ -192,6 +192,7 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?:
|
||||
const effectiveSuccess = result.success || alreadyCheckedIn || unsupportedCheckin || manualVerificationRequired;
|
||||
const shouldRefreshBalance = result.success || alreadyCheckedIn;
|
||||
const directCheckinSuccess = result.success && !alreadyCheckedIn && !unsupportedCheckin;
|
||||
const shouldAdvanceLastCheckinAt = directCheckinSuccess || (alreadyCheckedIn && options?.scheduleMode !== 'interval');
|
||||
const normalizedStatus: CheckinExecutionStatus = effectiveSuccess
|
||||
? ((unsupportedCheckin || manualVerificationRequired) ? 'skipped' : 'success')
|
||||
: 'failed';
|
||||
@@ -211,9 +212,10 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?:
|
||||
source: 'checkin',
|
||||
});
|
||||
|
||||
const updates: Record<string, unknown> = {
|
||||
lastCheckinAt: new Date().toISOString(),
|
||||
};
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (shouldAdvanceLastCheckinAt) {
|
||||
updates.lastCheckinAt = new Date().toISOString();
|
||||
}
|
||||
if (!storedPlatformUserId && guessedPlatformUserId) {
|
||||
updates.extraConfig = mergeAccountExtraConfig(account.extraConfig, {
|
||||
platformUserId: guessedPlatformUserId,
|
||||
@@ -308,7 +310,7 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?:
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkinAll() {
|
||||
export async function checkinAll(options?: { accountIds?: number[]; scheduleMode?: 'cron' | 'interval' }) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(schema.accounts)
|
||||
@@ -321,10 +323,12 @@ export async function checkinAll() {
|
||||
)
|
||||
.all();
|
||||
|
||||
const scopedAccountIds = options?.accountIds ? new Set(options.accountIds) : null;
|
||||
const results: Array<{ accountId: number; username: string | null; site: string; result: any }> = [];
|
||||
|
||||
const grouped = new Map<number, typeof rows>();
|
||||
for (const row of rows) {
|
||||
if (scopedAccountIds && !scopedAccountIds.has(row.accounts.id)) continue;
|
||||
const siteId = row.sites.id;
|
||||
if (!grouped.has(siteId)) grouped.set(siteId, []);
|
||||
grouped.get(siteId)!.push(row);
|
||||
@@ -332,7 +336,10 @@ export async function checkinAll() {
|
||||
|
||||
const promises = Array.from(grouped.entries()).map(async ([_, siteRows]) => {
|
||||
for (const row of siteRows) {
|
||||
const r = await checkinAccount(row.accounts.id, { skipEvent: true });
|
||||
const r = await checkinAccount(row.accounts.id, {
|
||||
skipEvent: true,
|
||||
scheduleMode: options?.scheduleMode,
|
||||
});
|
||||
results.push({
|
||||
accountId: row.accounts.id,
|
||||
username: row.accounts.username,
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('notifyService', () => {
|
||||
(config as any).telegramChatId = '';
|
||||
(config as any).telegramUseSystemProxy = false;
|
||||
config.systemProxyUrl = '';
|
||||
(config as any).telegramMessageThreadId = '';
|
||||
config.smtpEnabled = true;
|
||||
config.smtpHost = 'smtp.example.com';
|
||||
config.smtpPort = 465;
|
||||
@@ -181,11 +182,12 @@ describe('notifyService', () => {
|
||||
expect(payload?.text || '').toContain('UTC Time:');
|
||||
});
|
||||
|
||||
it('sends telegram message when telegram channel is enabled', async () => {
|
||||
it('sends telegram message without topic when telegram thread id is empty', async () => {
|
||||
const { config } = await import('../config.js');
|
||||
(config as any).telegramEnabled = true;
|
||||
(config as any).telegramBotToken = '123456:telegram-token';
|
||||
(config as any).telegramChatId = '-1001234567890';
|
||||
(config as any).telegramMessageThreadId = '';
|
||||
config.smtpEnabled = false;
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
@@ -206,13 +208,35 @@ describe('notifyService', () => {
|
||||
);
|
||||
|
||||
const rawBody = fetchMock.mock.calls[0]?.[1] as { body?: string };
|
||||
const payload = JSON.parse(rawBody?.body || '{}') as { chat_id?: string; text?: string };
|
||||
const payload = JSON.parse(rawBody?.body || '{}') as { chat_id?: string; text?: string; message_thread_id?: number };
|
||||
expect(payload.chat_id).toBe('-1001234567890');
|
||||
expect(payload.message_thread_id).toBeUndefined();
|
||||
expect(payload.text || '').toContain('Level: warning');
|
||||
expect(payload.text || '').toContain('Local Time:');
|
||||
expect(payload.text || '').toContain('UTC Time:');
|
||||
});
|
||||
|
||||
it('sends telegram topic id when telegram thread id is configured', async () => {
|
||||
const { config } = await import('../config.js');
|
||||
(config as any).telegramEnabled = true;
|
||||
(config as any).telegramBotToken = '123456:telegram-token';
|
||||
(config as any).telegramChatId = '-1001234567890';
|
||||
(config as any).telegramMessageThreadId = '77';
|
||||
config.smtpEnabled = false;
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const { sendNotification } = await import('./notifyService.js');
|
||||
await sendNotification('测试通知', 'message', 'warning', { bypassThrottle: true, throwOnFailure: true });
|
||||
|
||||
const rawBody = fetchMock.mock.calls[0]?.[1] as { body?: string };
|
||||
const payload = JSON.parse(rawBody?.body || '{}') as { message_thread_id?: number };
|
||||
expect(payload.message_thread_id).toBe(77);
|
||||
});
|
||||
|
||||
it('uses TELEGRAM_API_BASE_URL when configured', async () => {
|
||||
const { config } = await import('../config.js');
|
||||
(config as any).telegramEnabled = true;
|
||||
|
||||
@@ -222,6 +222,7 @@ export async function sendNotification(
|
||||
const telegramApiBaseUrl = String(config.telegramApiBaseUrl || 'https://api.telegram.org').replace(/\/+$/, '');
|
||||
const telegramApiUrl = `${telegramApiBaseUrl}/bot${config.telegramBotToken}/sendMessage`;
|
||||
const text = buildTelegramText(title, resolvedMessage, level, timeFootnote);
|
||||
const telegramMessageThreadId = Number.parseInt(String(config.telegramMessageThreadId || '').trim(), 10);
|
||||
tasks.push({
|
||||
channel: 'telegram',
|
||||
run: async () => {
|
||||
@@ -232,6 +233,9 @@ export async function sendNotification(
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chat_id: config.telegramChatId,
|
||||
...(Number.isFinite(telegramMessageThreadId) && telegramMessageThreadId > 0
|
||||
? { message_thread_id: telegramMessageThreadId }
|
||||
: {}),
|
||||
text,
|
||||
disable_web_page_preview: true,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user