// scripts/bridge/fetch-broadcast.js // ๐ŸŒ‰ ๆกฅๆŽฅยทไปŽ Notion ๆ‹‰ๅ–ๅนฟๆ’ญๅ†…ๅฎน // // ๆ นๆฎ่ฐƒๅบฆ้˜Ÿๅˆ—ไธญ BROADCAST_READY ไปปๅŠก็š„ๅ…ณ่”้กต้ข๏ผŒ // ไปŽ Notion ่ฏปๅ–ๅนฟๆ’ญ้กต้ขๅฎŒๆ•ดๅ†…ๅฎน๏ผŒ่ฝฌๆขไธบ Markdown ๆ ผๅผ // // ็Žฏๅขƒๅ˜้‡๏ผš // NOTION_TOKEN Notion API token // QUEUE_FILE check-queue.js ่พ“ๅ‡บ็š„ไปปๅŠกๅˆ—่กจๆ–‡ไปถ่ทฏๅพ„ 'use strict'; const https = require('https'); const fs = require('fs'); const path = require('path'); const NOTION_VERSION = '2022-06-28'; const NOTION_API_HOSTNAME = 'api.notion.com'; const OUTPUT_DIR = path.join('data', 'broadcasts', 'pdf'); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // Notion API // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function notionRequest(method, endpoint, body, token) { return new Promise((resolve, reject) => { const payload = body ? JSON.stringify(body) : ''; const opts = { hostname: NOTION_API_HOSTNAME, port: 443, path: endpoint, method: method, headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', 'Notion-Version': NOTION_VERSION, }, }; if (payload) { opts.headers['Content-Length'] = Buffer.byteLength(payload); } const req = https.request(opts, res => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const parsed = JSON.parse(data); if (res.statusCode >= 200 && res.statusCode < 300) { resolve(parsed); } else { reject(new Error(`Notion API ${res.statusCode}: ${parsed.message || data}`)); } } catch (e) { reject(new Error(`Notion API parse error: ${data}`)); } }); }); req.on('error', reject); if (payload) req.write(payload); req.end(); }); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // Notion Block โ†’ Markdown ่ฝฌๆข // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function blockToMarkdown(block) { const type = block.type; const content = block[type]; if (!content) return ''; const text = extractRichTextContent(content.rich_text); switch (type) { case 'paragraph': return text + '\n'; case 'heading_1': return '# ' + text + '\n'; case 'heading_2': return '## ' + text + '\n'; case 'heading_3': return '### ' + text + '\n'; case 'bulleted_list_item': return '- ' + text + '\n'; case 'numbered_list_item': return '1. ' + text + '\n'; case 'to_do': return (content.checked ? '- [x] ' : '- [ ] ') + text + '\n'; case 'toggle': return '> ' + text + '\n'; case 'code': return '```' + (content.language || '') + '\n' + text + '\n```\n'; case 'quote': return '> ' + text + '\n'; case 'divider': return '---\n'; case 'callout': return '> ' + (content.icon?.emoji || '๐Ÿ’ก') + ' ' + text + '\n'; default: return text ? text + '\n' : ''; } } function extractRichTextContent(richTextArray) { if (!richTextArray || !Array.isArray(richTextArray)) return ''; return richTextArray.map(t => { let text = t.plain_text || ''; if (t.annotations) { if (t.annotations.bold) text = '**' + text + '**'; if (t.annotations.italic) text = '*' + text + '*'; if (t.annotations.code) text = '`' + text + '`'; if (t.annotations.strikethrough) text = '~~' + text + '~~'; } if (t.href) text = '[' + text + '](' + t.href + ')'; return text; }).join(''); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ่Žทๅ–้กต้ขๆ‰€ๆœ‰ blocks // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• async function getPageBlocks(pageId, token) { let allBlocks = []; let cursor = undefined; do { const endpoint = `/v1/blocks/${pageId}/children?page_size=100` + (cursor ? `&start_cursor=${cursor}` : ''); const result = await notionRequest('GET', endpoint, null, token); allBlocks = allBlocks.concat(result.results || []); cursor = result.has_more ? result.next_cursor : undefined; } while (cursor); return allBlocks; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ่Žทๅ–้กต้ขๆ ‡้ข˜ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• async function getPageTitle(pageId, token) { const page = await notionRequest('GET', `/v1/pages/${pageId}`, null, token); const props = page.properties || {}; for (const key of Object.keys(props)) { if (props[key].type === 'title' && props[key].title) { return props[key].title.map(t => t.plain_text).join(''); } } return 'ๆœชๅ‘ฝๅๅนฟๆ’ญ'; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ไธป้€ป่พ‘ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• async function main() { const token = process.env.NOTION_TOKEN; const queueFile = process.env.QUEUE_FILE; if (!token) { console.log('โš ๏ธ ็ผบๅฐ‘ NOTION_TOKEN๏ผŒ่ทณ่ฟ‡ๅนฟๆ’ญๆ‹‰ๅ–'); process.exit(0); } if (!queueFile || !fs.existsSync(queueFile)) { console.log('๐Ÿ“ญ ๆ— ๅพ…ๅค„็†็š„ BROADCAST_READY ไปปๅŠก๏ผŒ่ทณ่ฟ‡'); process.exit(0); } const tasks = JSON.parse(fs.readFileSync(queueFile, 'utf8')); if (tasks.length === 0) { console.log('๐Ÿ“ญ ไปปๅŠกๅˆ—่กจไธบ็ฉบ๏ผŒ่ทณ่ฟ‡'); process.exit(0); } if (!fs.existsSync(OUTPUT_DIR)) { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } console.log(`๐Ÿ“ฅ ๅ‡†ๅค‡ๆ‹‰ๅ– ${tasks.length} ๆกๅนฟๆ’ญๅ†…ๅฎนโ€ฆ`); const mdFiles = []; let ok = 0, failed = 0; for (const task of tasks) { // ๅฐ่ฏ•ไปŽ Payload ไธญๆๅ–้กต้ข ID let pageId = ''; try { const payload = JSON.parse(task.payload || '{}'); pageId = payload.page_id || payload.broadcast_page_id || ''; } catch (_) { /* ignore */ } if (!pageId) { console.log(` โš ๏ธ ไปปๅŠก ${task.task_name} ๆ— ๅ…ณ่”้กต้ข ID๏ผŒ่ทณ่ฟ‡`); failed++; continue; } try { console.log(` ๐Ÿ“– ๆ‹‰ๅ–ๅนฟๆ’ญ: ${task.task_name} (page: ${pageId})`); const title = await getPageTitle(pageId, token); const blocks = await getPageBlocks(pageId, token); // ่ฝฌๆขไธบ Markdown let markdown = `# ${title}\n\n`; markdown += `> ๅนฟๆ’ญ็ผ–ๅท: ${task.broadcast_id || 'N/A'}\n`; markdown += `> ็”Ÿๆˆๆ—ถ้—ด: ${new Date().toISOString()}\n\n`; markdown += '---\n\n'; for (const block of blocks) { markdown += blockToMarkdown(block); } // ๅ†™ๅ…ฅ Markdown ๆ–‡ไปถ const safeName = (task.broadcast_id || task.task_name || 'broadcast') .replace(/[^a-zA-Z0-9_\-\u4e00-\u9fa5]/g, '_'); const mdFile = path.join(OUTPUT_DIR, `${safeName}.md`); fs.writeFileSync(mdFile, markdown, 'utf8'); mdFiles.push({ file: mdFile, task_id: task.id, title }); console.log(` โœ… ${mdFile} (${blocks.length} blocks)`); ok++; } catch (e) { console.error(` โŒ ๆ‹‰ๅ–ๅคฑ่ดฅ: ${task.task_name}: ${e.message}`); failed++; } } console.log(`\nโœ… ๅนฟๆ’ญๅ†…ๅฎนๆ‹‰ๅ–ๅฎŒๆˆ ยท ๆˆๅŠŸ ${ok} ๆก ยท ๅคฑ่ดฅ ${failed} ๆก`); // ๅฐ† Markdown ๆ–‡ไปถๅˆ—่กจๅ†™ๅ…ฅไพ›ๅŽ็ปญๆญฅ้ชคไฝฟ็”จ const manifestFile = path.join(OUTPUT_DIR, 'manifest.json'); fs.writeFileSync(manifestFile, JSON.stringify(mdFiles, null, 2)); console.log(`๐Ÿ“ ๆธ…ๅ•ๅทฒๅ†™ๅ…ฅ: ${manifestFile}`); if (process.env.GITHUB_OUTPUT) { fs.appendFileSync(process.env.GITHUB_OUTPUT, `broadcast_count=${ok}\nmanifest_file=${manifestFile}\n` ); } } main().catch(e => { console.error(e); process.exit(1); });