fix(scheduler): 봇 JSON 컬럼 파싱에 safeParse 가드 추가

깨진 JSON 값 하나가 getAllBots 전체를 throw시켜 startAll(전체 봇 기동)을
막던 위험 제거. weekly_schedule_config/title_filters/default_member_ids/
auto_schedule_config/text_filters를 safeParse(fallback)로 처리.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-06-07 15:41:57 +09:00
parent e16d3f1230
commit 6e559a52b7

View file

@ -20,6 +20,19 @@ function isActiveMonth(bot) {
return months.includes(monthKST()); return months.includes(monthKST());
} }
/**
* DB JSON 컬럼 안전 파싱. 깨진 하나가 getAllBots 전체를 막지 않도록 fallback 반환.
*/
function safeParse(value, fallback = null) {
if (value == null) return fallback;
if (typeof value !== 'string') return value;
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
/** /**
* 에러 객체에서 활동 로그용 details 구성 * 에러 객체에서 활동 로그용 details 구성
* - err.cause(Node fetch failed의 진짜 원인 ), err.code를 함께 포함 * - err.cause(Node fetch failed의 진짜 원인 ), err.code를 함께 포함
@ -47,11 +60,7 @@ async function schedulerPlugin(fastify, opts) {
'SELECT * FROM bot_youtube' 'SELECT * FROM bot_youtube'
); );
return rows.map(row => { return rows.map(row => {
const weekly = row.weekly_schedule_config const weekly = safeParse(row.weekly_schedule_config, null);
? (typeof row.weekly_schedule_config === 'string'
? JSON.parse(row.weekly_schedule_config)
: row.weekly_schedule_config)
: null;
// weekly 모드면 시작 시각에만 트리거, 아니면 cron_interval 분 주기 // weekly 모드면 시작 시각에만 트리거, 아니면 cron_interval 분 주기
let cronExpr; let cronExpr;
@ -72,23 +81,11 @@ async function schedulerPlugin(fastify, opts) {
bannerUrl: row.banner_url, bannerUrl: row.banner_url,
cron: cronExpr, cron: cronExpr,
enabled: row.enabled === 1, enabled: row.enabled === 1,
titleFilters: row.title_filters titleFilters: safeParse(row.title_filters, []),
? (typeof row.title_filters === 'string' defaultMemberIds: safeParse(row.default_member_ids, []),
? JSON.parse(row.title_filters)
: row.title_filters)
: [],
defaultMemberIds: row.default_member_ids
? (typeof row.default_member_ids === 'string'
? JSON.parse(row.default_member_ids)
: row.default_member_ids)
: [],
extractMembersFromDesc: row.extract_members_from_desc === 1, extractMembersFromDesc: row.extract_members_from_desc === 1,
extractMembersFromTitle: row.extract_members_from_title === 1, extractMembersFromTitle: row.extract_members_from_title === 1,
autoScheduleNext: row.auto_schedule_config autoScheduleNext: safeParse(row.auto_schedule_config, null),
? (typeof row.auto_schedule_config === 'string'
? JSON.parse(row.auto_schedule_config)
: row.auto_schedule_config)
: null,
weeklySchedule: weekly, weeklySchedule: weekly,
}; };
}); });
@ -111,11 +108,7 @@ async function schedulerPlugin(fastify, opts) {
nitterUrl: process.env.NITTER_URL || 'http://nitter:8080', nitterUrl: process.env.NITTER_URL || 'http://nitter:8080',
cron: `*/${row.cron_interval} * * * *`, cron: `*/${row.cron_interval} * * * *`,
enabled: row.enabled === 1, enabled: row.enabled === 1,
textFilters: row.text_filters textFilters: safeParse(row.text_filters, []),
? (typeof row.text_filters === 'string'
? JSON.parse(row.text_filters)
: row.text_filters)
: [],
includeRetweets: row.include_retweets === 1, includeRetweets: row.include_retweets === 1,
extractYoutube: row.extract_youtube === 1, extractYoutube: row.extract_youtube === 1,
excludeManagedChannels: row.exclude_managed_channels === 1, excludeManagedChannels: row.exclude_managed_channels === 1,