From f663661568c08c4e13fdb093a95156b463d2c7d9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:15:11 +0000 Subject: [PATCH] Repurpose LINE Bot into a Yosegaki collection bot. - Removed legacy timetable, Google Calendar integration, and web dashboard. - Updated to @line/bot-sdk v9 (MessagingApiClient, MessagingApiBlobClient). - Added file upload handling for image, video, audio, and documents. - Implemented filename sanitization (path.basename) to prevent path traversal. - Created 'uploads/' directory for storage and updated .gitignore. - Simplified index.js and flexTemplates.js. - Added a new test script (src/test-yosegaki.js) to verify the new functionality. Co-authored-by: systemcmd0122 <155505304+systemcmd0122@users.noreply.github.com> --- .gitignore | 1 + index.js | 365 ++---------------------------------------- package.json | 7 +- public/QR.png | Bin 1690 -> 0 bytes src/botLogic.js | 254 ++++++++++------------------- src/flexTemplates.js | 315 +----------------------------------- src/googleCalendar.js | 103 ------------ src/test-logic.js | 79 --------- src/test-yosegaki.js | 83 ++++++++++ src/timetable.js | 9 -- 10 files changed, 184 insertions(+), 1032 deletions(-) delete mode 100644 public/QR.png delete mode 100644 src/googleCalendar.js delete mode 100644 src/test-logic.js create mode 100644 src/test-yosegaki.js delete mode 100644 src/timetable.js diff --git a/.gitignore b/.gitignore index 5532e03..5bffdf8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ .env data/*.json +uploads/ *.log verification/ diff --git a/index.js b/index.js index 42242b8..67d5d82 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,9 @@ require('dotenv').config(); const express = require('express'); -const line = require('@line/bot-sdk'); +const { middleware, messagingApi } = require('@line/bot-sdk'); const path = require('path'); -const fs = require('fs'); -const cron = require('node-cron'); -const { DateTime } = require('luxon'); -const axios = require('axios'); const botLogic = require('./src/botLogic'); -const timetableData = require('./src/timetable'); -const googleCalendar = require('./src/googleCalendar'); const config = { channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN, @@ -17,239 +11,17 @@ const config = { }; const app = express(); -const client = new line.Client(config); - -app.use(express.static(path.join(__dirname, 'public'))); -app.use('/images', express.static(path.join(__dirname, 'images'))); - -const SUBSCRIBERS_FILE = path.join(__dirname, 'data/subscribers.json'); -const CHATS_FILE = path.join(__dirname, 'data/chats.json'); - -// API to get events -app.get('/api/events', async (req, res) => { - try { - const year = parseInt(req.query.year) || DateTime.now().setZone('Asia/Tokyo').year; - const month = parseInt(req.query.month) || DateTime.now().setZone('Asia/Tokyo').month; - const calendarIds = (process.env.NEXT_PUBLIC_GOOGLE_CALENDAR_ID || '').split(',').filter(id => id.trim()); - - if (calendarIds.length === 0) { - return res.json({ success: true, data: [] }); - } - - const events = await googleCalendar.getEventsForMonth(calendarIds, year, month); - res.json({ success: true, data: events }); - } catch (err) { - console.error('API Error:', err); - res.status(500).json({ success: false, error: err.message }); - } +const client = new messagingApi.MessagingApiClient({ + channelAccessToken: config.channelAccessToken }); - -// Root route - Modernized Web Dashboard -app.get('/', (req, res) => { - const lastUpdate = DateTime.now().setZone('Asia/Tokyo').toFormat('yyyy/MM/dd HH:mm:ss'); - - const html = ` - - -
- - -毎日の授業をもっとスマートに確認
-Status
-稼働中
-Last Sync
-${lastUpdate}
-Auto Notification
-毎日 07:00 JST
-| 曜日 | -1限 | -2限 | -3限 | -4限 | -5限 | -6限 | -
|---|---|---|---|---|---|---|
| ${day} | - ${subjects.map(s => `${s} | `).join('')} - ${subjects.length < 6 ? Array(6 - subjects.length).fill('- | ').join('') : ''} -
今日の時間割 / 今日の行事
-直近の授業や行事を即座に確認できます
-明日の時間割 / 明日の行事
-翌日の予定を先取りしてチェック
-〇曜日の時間割
-特定の曜日の授業をチェック
-モーニング通知
-毎朝7時に自動で通知が届きます
-QRコードをスキャンして、
あなたのLINEに時間割を。
- SHS 2D Class Bot Project
-Invalid password.
'); - } - - let subscribersCount = 0; - if (fs.existsSync(SUBSCRIBERS_FILE)) { - subscribersCount = JSON.parse(fs.readFileSync(SUBSCRIBERS_FILE, 'utf8')).length; - } - - let chats = {}; - if (fs.existsSync(CHATS_FILE)) { - chats = JSON.parse(fs.readFileSync(CHATS_FILE, 'utf8')); - } - const chatsCount = Object.keys(chats).length; - - const html = ` - - - - -Subscribers
-${subscribersCount}
-Total Chats (Groups/Users)
-${chatsCount}
-Messages are being sent to all tracked chats.
Back to Dashboard'); -}); - -// Self-pinging to stay awake on Koyeb (every 10 minutes) -const SITE_URL = process.env.SITE_URL; -if (SITE_URL) { - cron.schedule('*/10 * * * *', async () => { - try { - console.log(`Self-pinging: ${SITE_URL}`); - await axios.get(SITE_URL); - } catch (err) { - console.error('Self-ping error:', err.message); - } - }); -} - -// Morning Notification Cron Job (7:00 AM JST) -cron.schedule('0 7 * * *', async () => { - console.log('Running morning notification task...'); - try { - if (fs.existsSync(SUBSCRIBERS_FILE)) { - const subscribers = JSON.parse(fs.readFileSync(SUBSCRIBERS_FILE, 'utf8')); - const timetableFlex = await botLogic.getTodayTimetable(); - - console.log(`Sending morning notification to ${subscribers.length} subscribers`); - - for (const id of subscribers) { - try { - await client.pushMessage(id, timetableFlex); - } catch (pushErr) { - console.error(`Failed to push to ${id}:`, pushErr.message); - } - } - } - } catch (err) { - console.error('Cron job error:', err); - } -}, { - scheduled: true, - timezone: "Asia/Tokyo" +// Basic Root Route +app.get('/', (req, res) => { + res.send('The bot is running.
'); }); const port = process.env.PORT || 3000; diff --git a/package.json b/package.json index 2c38f8b..dcfaf2f 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,6 @@ "dependencies": { "@line/bot-sdk": "^9.8.0", "dotenv": "^17.3.1", - "axios": "^1.7.9", - "express": "^5.1.0", - "ical.js": "^2.1.0", - "luxon": "^3.7.2", - "node-cron": "^3.0.3", - "node-schedule": "^2.1.1" + "express": "^5.1.0" } } diff --git a/public/QR.png b/public/QR.png deleted file mode 100644 index 6359dddcbf250e10a231844bf44f181a401826e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1690 zcmZuy4NOy4811r%j*PJ^CS-%wWdE2rVvGXX(lKR7Cf4~!2CQZ30yEZD3euMKHIZe6 z8E9ZpGbv@-ZOwE%K%me8eL>NN0cFUfKdGrD!l}p6h}(FH?=!13#o3)PDsn6i_Yu7-w>L4na=p(p z&YMcLRBttu;XIH>@U4~6M#g{e8?<71|fb~1y;KDBc zxv^QXF!WXYT!_!q`rBm{Fk6i6vr`xp;!8K;+*jT8rqF<>k-r1l(<2Z~RjyTP{khRB zUUE&R7~_O^j@`fwtOUzFccFu({WwMFP |ZB4I @uzQ4?+8I&bWXpNGPULzix7Q?BkP#yO sDtgR!F9gVYS^Z(<;T`rJ?ZCyD9e!ulRDU^EJeUvok@Q1$vnV0Y(3| z3=p(STjBvBJVx48YLqdfM-Mm_^puqDp3d;pR3$7YVX#l&?L>R^-d-{0BR`I={E$nk zqno9(U2@kgCE|JAW*}os5)2l>03{_!Fs!awy98&uZ&jVO8ZQBfJaL`M-mwt<)-2~8 z=MS{uBi*vgFf n{dH&aEOV1967%Y%GL}s8}RY>D*EP{N`C}LyOXRc%g z_`I)s< ie@h6s1gaMe)d*yQMI^9n ?Tf7%fyi3Syo?1^=srx z5du`QoqoFAu+~g1l1&ZW;anuz#<-xJysSzaLkvqlN6B8csTo9i!|XLVjlXwiE%9M& RYNW%^a { + stream.pipe(writer); + writer.on('finish', resolve); + writer.on('error', reject); + }); + + console.log(`Saved file: ${fileName}`); + return client.replyMessage({ + replyToken: event.replyToken, + messages: [{ + type: 'text', + text: `寄せ書きを受け取りました!ありがとうございます。\n保存名: ${fileName}` + }] + }); + } catch (err) { + console.error('Error saving file:', err); + return client.replyMessage({ + replyToken: event.replyToken, + messages: [{ + type: 'text', + text: 'ファイルの保存中にエラーが発生しました。もう一度試してみてください。' + }] + }); + } } - return null; - } - - // Add quick replies to all replies - reply.quickReply = flexTemplates.getQuickReplies(); - - return client.replyMessage(event.replyToken, reply); -} - -async function getTodayTimetable() { - const now = DateTime.now().setZone('Asia/Tokyo'); - const dayOfWeek = ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'][now.weekday % 7]; - return getTimetableForDay(dayOfWeek, now); -} - -async function getTomorrowTimetable() { - const tomorrow = DateTime.now().setZone('Asia/Tokyo').plus({ days: 1 }); - const dayOfWeek = ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'][tomorrow.weekday % 7]; - return getTimetableForDay(dayOfWeek, tomorrow); -} - -async function getTodayEvents() { - const now = DateTime.now().setZone('Asia/Tokyo'); - const calendarIds = (process.env.NEXT_PUBLIC_GOOGLE_CALENDAR_ID || '').split(',').filter(id => id.trim()); - const events = calendarIds.length > 0 ? await googleCalendar.getEventsForDate(calendarIds, now) : []; - return flexTemplates.createEventsFlex('今日', events); -} -async function getTomorrowEvents() { - const tomorrow = DateTime.now().setZone('Asia/Tokyo').plus({ days: 1 }); - const calendarIds = (process.env.NEXT_PUBLIC_GOOGLE_CALENDAR_ID || '').split(',').filter(id => id.trim()); - const events = calendarIds.length > 0 ? await googleCalendar.getEventsForDate(calendarIds, tomorrow) : []; - return flexTemplates.createEventsFlex('明日', events); -} + // Handle text messages + if (message.type === 'text') { + const userMessage = message.text.trim(); -async function getTimetableForDay(dayOfWeek, date = null) { - const subjects = timetableData[dayOfWeek] || []; - let events = []; + if (userMessage === '使い方' || userMessage === 'ヘルプ') { + return client.replyMessage({ + replyToken: event.replyToken, + messages: [flexTemplates.createHelpFlex()] + }); + } - if (date) { - const calendarIds = (process.env.NEXT_PUBLIC_GOOGLE_CALENDAR_ID || '').split(',').filter(id => id.trim()); - if (calendarIds.length > 0) { - events = await googleCalendar.getEventsForDate(calendarIds, date); + // Default response for text + return client.replyMessage({ + replyToken: event.replyToken, + messages: [{ + type: 'text', + text: '寄せ書きのファイルや画像を送ってください。「使い方」と送るとヘルプを表示します。' + }] + }); } } - return flexTemplates.createTimetableFlex(dayOfWeek, subjects, events); -} - -function saveSubscriber(id) { - if (!id) return false; - try { - if (!fs.existsSync(path.dirname(SUBSCRIBERS_FILE))) { - fs.mkdirSync(path.dirname(SUBSCRIBERS_FILE), { recursive: true }); - } - - let subscribers = []; - if (fs.existsSync(SUBSCRIBERS_FILE)) { - subscribers = JSON.parse(fs.readFileSync(SUBSCRIBERS_FILE, 'utf8')); - } - - if (!subscribers.includes(id)) { - subscribers.push(id); - fs.writeFileSync(SUBSCRIBERS_FILE, JSON.stringify(subscribers, null, 2)); - console.log(`New subscriber added: ${id}`); - return true; - } - return false; - } catch (err) { - console.error('Error saving subscriber:', err); - return false; - } + return null; } -function removeSubscriber(id) { - if (!id) return false; - try { - if (fs.existsSync(SUBSCRIBERS_FILE)) { - let subscribers = JSON.parse(fs.readFileSync(SUBSCRIBERS_FILE, 'utf8')); - if (subscribers.includes(id)) { - subscribers = subscribers.filter(sid => sid !== id); - fs.writeFileSync(SUBSCRIBERS_FILE, JSON.stringify(subscribers, null, 2)); - console.log(`Subscriber removed: ${id}`); - return true; - } - } - return false; - } catch (err) { - console.error('Error removing subscriber:', err); - return false; +function getFileExtension(type) { + switch (type) { + case 'image': return 'jpg'; + case 'video': return 'mp4'; + case 'audio': return 'm4a'; + default: return 'bin'; } } @@ -242,6 +151,5 @@ function removeChat(id) { } module.exports = { - handleEvent, - getTodayTimetable + handleEvent }; diff --git a/src/flexTemplates.js b/src/flexTemplates.js index 96eaf6c..4429265 100644 --- a/src/flexTemplates.js +++ b/src/flexTemplates.js @@ -1,133 +1,11 @@ /** - * Flex Message templates for the LINE Bot + * Flex Message templates for the Yosegaki LINE Bot */ -function createTimetableFlex(day, subjects, events = []) { - const isWeekend = subjects.length === 0; - - const bodyContents = []; - - // Add subjects - if (isWeekend) { - bodyContents.push({ - "type": "text", - "text": `${day}は授業がありません。`, - "size": "md", - "color": "#666666", - "wrap": true - }); - } else { - subjects.forEach((subject, index) => { - bodyContents.push({ - "type": "box", - "layout": "horizontal", - "contents": [ - { - "type": "text", - "text": `${index + 1}限`, - "size": "sm", - "color": "#888888", - "flex": 1 - }, - { - "type": "text", - "text": subject || "-", - "size": "md", - "color": "#333333", - "flex": 4, - "weight": "bold", - "wrap": true - } - ], - "margin": "md", - "paddingBottom": "sm" - }); - }); - } - - // Add events if any - if (events && events.length > 0) { - bodyContents.push({ - "type": "separator", - "margin": "xl" - }); - bodyContents.push({ - "type": "text", - "text": "📅 本日の行事", - "weight": "bold", - "size": "sm", - "margin": "md", - "color": "#4CAF50" - }); - - events.forEach(event => { - bodyContents.push({ - "type": "box", - "layout": "vertical", - "contents": [ - { - "type": "text", - "text": event.summary, - "size": "sm", - "weight": "bold", - "wrap": true - } - ], - "margin": "sm", - "paddingStart": "md" - }); - }); - } - - return { - "type": "flex", - "altText": `【${day}の時間割】`, - "contents": { - "type": "bubble", - "header": { - "type": "box", - "layout": "vertical", - "contents": [ - { - "type": "text", - "text": day, - "weight": "bold", - "size": "xl", - "color": "#ffffff" - } - ], - "backgroundColor": "#4CAF50" - }, - "body": { - "type": "box", - "layout": "vertical", - "contents": bodyContents - }, - "footer": { - "type": "box", - "layout": "vertical", - "spacing": "sm", - "contents": [ - { - "type": "button", - "style": "link", - "height": "sm", - "action": { - "type": "uri", - "label": "詳細をサイトで見る", - "uri": process.env.SITE_URL || "https://shs2d-linebot.aeroindust.com" - } - } - ] - } - } - }; -} - function createHelpFlex() { return { "type": "flex", - "altText": "【時間割ボットの使い方】", + "altText": "【寄せ書きボットの使い方】", "contents": { "type": "bubble", "header": { @@ -142,7 +20,7 @@ function createHelpFlex() { "color": "#ffffff" } ], - "backgroundColor": "#2196F3" + "backgroundColor": "#FF9800" }, "body": { "type": "box", @@ -151,7 +29,7 @@ function createHelpFlex() { "contents": [ { "type": "text", - "text": "以下のコマンドを送信するか、下のボタンをタップしてください。", + "text": "寄せ書きのファイルや画像を受け付けています。", "wrap": true, "size": "sm" }, @@ -160,53 +38,10 @@ function createHelpFlex() { }, { "type": "text", - "text": "• 今日の時間割\n• 今日の行事\n• 明日の時間割 / 行事\n• 〇曜日の時間割\n• 通知オン / 通知オフ", + "text": "• 画像、動画、音声、ファイルを送信してください。\n• 受信したファイルは自動的に保存されます。\n• テキストメッセージは現在受け付けていません。", "wrap": true, - "margin": "md" - } - ] - }, - "footer": { - "type": "box", - "layout": "vertical", - "spacing": "sm", - "contents": [ - { - "type": "button", - "style": "primary", - "color": "#4CAF50", - "action": { - "type": "message", - "label": "今日の時間割", - "text": "今日の時間割" - } - }, - { - "type": "box", - "layout": "horizontal", - "spacing": "sm", - "contents": [ - { - "type": "button", - "style": "secondary", - "action": { - "type": "message", - "label": "通知オン", - "text": "通知オン" - }, - "flex": 1 - }, - { - "type": "button", - "style": "secondary", - "action": { - "type": "message", - "label": "通知オフ", - "text": "通知オフ" - }, - "flex": 1 - } - ] + "margin": "md", + "size": "sm" } ] } @@ -214,140 +49,6 @@ function createHelpFlex() { }; } -function getQuickReplies() { - return { - "items": [ - { - "type": "action", - "action": { - "type": "message", - "label": "今日の予定", - "text": "今日の時間割" - } - }, - { - "type": "action", - "action": { - "type": "message", - "label": "今日の行事", - "text": "今日の行事" - } - }, - { - "type": "action", - "action": { - "type": "message", - "label": "明日", - "text": "明日の時間割" - } - }, - { - "type": "action", - "action": { - "type": "message", - "label": "通知ON", - "text": "通知オン" - } - }, - { - "type": "action", - "action": { - "type": "message", - "label": "通知OFF", - "text": "通知オフ" - } - }, - { - "type": "action", - "action": { - "type": "message", - "label": "ヘルプ", - "text": "ヘルプ" - } - } - ] - }; -} - -function createEventsFlex(day, events = []) { - const bodyContents = []; - - if (events.length === 0) { - bodyContents.push({ - "type": "text", - "text": `${day}に予定されている行事はありません。`, - "size": "md", - "color": "#666666", - "wrap": true - }); - } else { - events.forEach(event => { - bodyContents.push({ - "type": "box", - "layout": "vertical", - "contents": [ - { - "type": "text", - "text": event.summary, - "weight": "bold", - "size": "md", - "wrap": true - } - ], - "margin": "md", - "backgroundColor": "#f0fdf4", - "cornerRadius": "md", - "paddingAll": "md" - }); - if (event.location || event.description) { - const details = []; - if (event.location) details.push(`📍 ${event.location}`); - if (event.description) details.push(event.description); - - bodyContents.push({ - "type": "text", - "text": details.join('\n'), - "size": "xs", - "color": "#888888", - "wrap": true, - "margin": "sm", - "paddingStart": "md" - }); - } - }); - } - - return { - "type": "flex", - "altText": `【${day}の行事予定】`, - "contents": { - "type": "bubble", - "header": { - "type": "box", - "layout": "vertical", - "contents": [ - { - "type": "text", - "text": `${day} の行事`, - "weight": "bold", - "size": "xl", - "color": "#ffffff" - } - ], - "backgroundColor": "#4CAF50" - }, - "body": { - "type": "box", - "layout": "vertical", - "contents": bodyContents - } - } - }; -} - module.exports = { - createTimetableFlex, - createHelpFlex, - getQuickReplies, - createEventsFlex + createHelpFlex }; diff --git a/src/googleCalendar.js b/src/googleCalendar.js deleted file mode 100644 index 3013bf2..0000000 --- a/src/googleCalendar.js +++ /dev/null @@ -1,103 +0,0 @@ -const axios = require('axios'); -const ICAL = require('ical.js'); -const { DateTime } = require('luxon'); - -/** - * Fetches and parses Google Calendar iCal feeds. - * @param {string[]} calendarIds Array of Google Calendar IDs. - * @param {DateTime} targetDate The date to filter events for. - * @returns {Promise } List of events for the target date. - */ -async function getEventsForDate(calendarIds, targetDate) { - const allEvents = []; - - for (const id of calendarIds) { - try { - const url = `https://calendar.google.com/calendar/ical/${encodeURIComponent(id)}/public/basic.ics`; - const response = await axios.get(url); - const jcalData = ICAL.parse(response.data); - const comp = new ICAL.Component(jcalData); - const vevents = comp.getAllSubcomponents('vevent'); - - vevents.forEach(vevent => { - const event = new ICAL.Event(vevent); - const dtstart = event.startDate.toJSDate(); - const dtend = event.endDate.toJSDate(); - - const start = DateTime.fromJSDate(dtstart).setZone('Asia/Tokyo'); - const end = DateTime.fromJSDate(dtend).setZone('Asia/Tokyo'); - - const targetStart = targetDate.startOf('day'); - const targetEnd = targetDate.endOf('day'); - - if (start <= targetEnd && end >= targetStart) { - allEvents.push({ - summary: event.summary, - location: event.location, - description: event.description, - start: start.toISO(), - end: end.toISO(), - isAllDay: event.startDate.isDate - }); - } - }); - } catch (error) { - console.error(`Error fetching calendar ${id}:`, error.message); - } - } - return allEvents; -} - -/** - * Fetches events for a specific month. - * @param {string[]} calendarIds Array of Google Calendar IDs. - * @param {number} year - * @param {number} month 1-12 - * @returns {Promise } List of events for the month. - */ -async function getEventsForMonth(calendarIds, year, month) { - const allEvents = []; - const targetMonthStart = DateTime.fromObject({ year, month, day: 1 }).setZone('Asia/Tokyo').startOf('month'); - const targetMonthEnd = targetMonthStart.endOf('month'); - - for (const id of calendarIds) { - try { - const url = `https://calendar.google.com/calendar/ical/${encodeURIComponent(id)}/public/basic.ics`; - const response = await axios.get(url); - const jcalData = ICAL.parse(response.data); - const comp = new ICAL.Component(jcalData); - const vevents = comp.getAllSubcomponents('vevent'); - - vevents.forEach(vevent => { - const event = new ICAL.Event(vevent); - const dtstart = event.startDate.toJSDate(); - const dtend = event.endDate.toJSDate(); - - const start = DateTime.fromJSDate(dtstart).setZone('Asia/Tokyo'); - const end = DateTime.fromJSDate(dtend).setZone('Asia/Tokyo'); - - // If event overlaps with target month - if (start <= targetMonthEnd && end >= targetMonthStart) { - allEvents.push({ - summary: event.summary, - location: event.location, - description: event.description, - start: start.toISO(), - end: end.toISO(), - isAllDay: event.startDate.isDate, - calendarId: id - }); - } - }); - } catch (error) { - console.error(`Error fetching calendar ${id}:`, error.message); - } - } - // Sort events by start date - return allEvents.sort((a, b) => DateTime.fromISO(a.start).toMillis() - DateTime.fromISO(b.start).toMillis()); -} - -module.exports = { - getEventsForDate, - getEventsForMonth -}; diff --git a/src/test-logic.js b/src/test-logic.js deleted file mode 100644 index 4e43eec..0000000 --- a/src/test-logic.js +++ /dev/null @@ -1,79 +0,0 @@ -const botLogic = require('./botLogic'); -const { DateTime } = require('luxon'); - -// Mock client -const client = { - replyMessage: (token, message) => { - console.log(`[MOCK] Replying with:`, JSON.stringify(message, null, 2)); - return Promise.resolve(message); - } -}; - -async function runTests() { - console.log('--- Testing Bot Logic ---'); - - // Test 1: Notification On - console.log('\nTest 1: Notification On'); - await botLogic.handleEvent({ - type: 'message', - message: { type: 'text', text: '通知オン' }, - source: { type: 'user', userId: 'U999' }, - replyToken: 'T1' - }, client); - - // Test 2: Notification Off - console.log('\nTest 2: Notification Off'); - await botLogic.handleEvent({ - type: 'message', - message: { type: 'text', text: '通知オフ' }, - source: { type: 'user', userId: 'U999' }, - replyToken: 'T2' - }, client); - - // Test 3: Today's Events - console.log('\nTest 3: Today\'s Events'); - await botLogic.handleEvent({ - type: 'message', - message: { type: 'text', text: '今日の行事' }, - source: { type: 'user', userId: 'U999' }, - replyToken: 'T3' - }, client); - - // Test 4: Tracking on Join - console.log('\nTest 4: Tracking on Join'); - await botLogic.handleEvent({ - type: 'join', - source: { type: 'group', groupId: 'G123' }, - replyToken: 'T4' - }, client); - - // Test 5: Verification - console.log('\nTest 5: Final Verification'); - const fs = require('fs'); - const path = require('path'); - const subsFile = path.join(__dirname, '../data/subscribers.json'); - if (fs.existsSync(subsFile)) { - const subs = JSON.parse(fs.readFileSync(subsFile, 'utf8')); - console.log('Subscribers in file:', subs); - if (!subs.includes('U999')) { - console.log('SUCCESS: User U999 was removed.'); - } else { - console.log('FAILURE: User U999 was NOT removed.'); - } - } - - const chatsFile = path.join(__dirname, '../data/chats.json'); - if (fs.existsSync(chatsFile)) { - const chats = JSON.parse(fs.readFileSync(chatsFile, 'utf8')); - console.log('Chats in file:', Object.keys(chats)); - if (chats['G123']) { - console.log('SUCCESS: Group G123 was tracked.'); - } else { - console.log('FAILURE: Group G123 was NOT tracked.'); - } - } - - console.log('\n--- Tests Completed ---'); -} - -runTests().catch(console.error); diff --git a/src/test-yosegaki.js b/src/test-yosegaki.js new file mode 100644 index 0000000..ab0f297 --- /dev/null +++ b/src/test-yosegaki.js @@ -0,0 +1,83 @@ +const botLogic = require('./botLogic'); +const fs = require('fs'); +const path = require('path'); +const stream = require('stream'); + +// Mock clients +const client = { + replyMessage: (params) => { + console.log(`[MOCK] Replying with:`, JSON.stringify(params, null, 2)); + return Promise.resolve(params); + } +}; + +const blobClient = { + getMessageContent: (id) => { + console.log(`[MOCK] Getting content for message: ${id}`); + const s = new stream.Readable(); + s.push('mock content'); + s.push(null); + return Promise.resolve(s); + } +}; + +async function runTests() { + console.log('--- Testing Yosegaki Bot Logic (v9 SDK) ---'); + + // Test 1: Follow event + console.log('\nTest 1: Follow event'); + await botLogic.handleEvent({ + type: 'follow', + source: { type: 'user', userId: 'U111' }, + replyToken: 'T1' + }, client, blobClient); + + // Test 2: Image message + console.log('\nTest 2: Image message'); + await botLogic.handleEvent({ + type: 'message', + message: { type: 'image', id: 'img123' }, + source: { type: 'user', userId: 'U111' }, + replyToken: 'T2' + }, client, blobClient); + + // Test 3: File message with potential path traversal + console.log('\nTest 3: File message (Path Traversal attempt)'); + await botLogic.handleEvent({ + type: 'message', + message: { type: 'file', id: 'file456', fileName: '../../attack.txt' }, + source: { type: 'user', userId: 'U111' }, + replyToken: 'T3' + }, client, blobClient); + + // Test 4: Help command + console.log('\nTest 4: Help command'); + await botLogic.handleEvent({ + type: 'message', + message: { type: 'text', text: '使い方' }, + source: { type: 'user', userId: 'U111' }, + replyToken: 'T4' + }, client, blobClient); + + // Verification + console.log('\n--- Final Verification ---'); + const uploadsDir = path.join(__dirname, '../uploads'); + const files = fs.readdirSync(uploadsDir); + console.log('Files in uploads directory:', files); + + const hasAttackTxt = files.includes('attack.txt'); // It should be basename-d + const rootFiles = fs.readdirSync(path.join(__dirname, '../')); + const traversalSucceeded = rootFiles.includes('attack.txt'); + + if (hasAttackTxt && !traversalSucceeded) { + console.log('SUCCESS: Path traversal prevented and file saved as basename.'); + } else if (traversalSucceeded) { + console.log('FAILURE: Path traversal succeeded!'); + } else { + console.log('FAILURE: attack.txt not found.'); + } + + console.log('\n--- Tests Completed ---'); +} + +runTests().catch(console.error); diff --git a/src/timetable.js b/src/timetable.js deleted file mode 100644 index aaf038b..0000000 --- a/src/timetable.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - "月曜日": ["地理総合", "ハードウェア技術", "プログラミング技術", "コンピュータシステム", "体育", "Ⅰ類:数学・Ⅱ類:物理"], - "火曜日": ["プログラミング技術", "Ⅰ類:電気回路・Ⅱ類:物理", "Ⅰ類:英語・Ⅱ類:国語", "Ⅰ類:物理・Ⅱ類:数学", "コンピュータシステム", "Ⅰ類:国語・Ⅱ類:英語"], - "水曜日": ["Ⅰ類:数学・Ⅱ類:物理", "ハードウェア技術", "Ⅰ類:国語・Ⅱ類:英語", "Ⅰ類:物理・Ⅱ類:数学", "Ⅰ類:英語・Ⅱ類:国語", "体育"], - "木曜日": ["実習", "実習", "実習", "実習", "ハードウェア技術", "LHR"], - "金曜日": ["Ⅰ類:国語・Ⅱ類:英語", "Ⅰ類:英語・Ⅱ類:国語", "Ⅰ類:電気回路・Ⅱ類:数学", "地理総合", "Ⅰ類:数学・Ⅱ類:物理", "保健"], - "土曜日": [], - "日曜日": [] -};