Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
.env
data/*.json
uploads/
*.log
verification/
365 changes: 10 additions & 355 deletions index.js

Large diffs are not rendered by default.

7 changes: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Binary file removed public/QR.png
Binary file not shown.
254 changes: 81 additions & 173 deletions src/botLogic.js
Original file line number Diff line number Diff line change
@@ -1,199 +1,108 @@
const { DateTime } = require('luxon');
const timetableData = require('./timetable');
const flexTemplates = require('./flexTemplates');
const googleCalendar = require('./googleCalendar');
const fs = require('fs');
const path = require('path');
const flexTemplates = require('./flexTemplates');

const SUBSCRIBERS_FILE = path.join(__dirname, '../data/subscribers.json');
const UPLOADS_DIR = path.join(__dirname, '../uploads');
const CHATS_FILE = path.join(__dirname, '../data/chats.json');

const validCommands = [
'使い方', 'ヘルプ', '今日の時間割', '明日の時間割',
'今日の行事', '明日の行事',
'月曜日の時間割', '火曜日の時間割', '水曜日の時間割',
'木曜日の時間割', '金曜日の時間割', '土曜日の時間割', '日曜日の時間割',
'通知オン', '通知オフ'
];
// Ensure uploads directory exists
if (!fs.existsSync(UPLOADS_DIR)) {
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
}

/**
* Handle incoming LINE events
*/
async function handleEvent(event, client) {
// Track chats on follow/join/leave/unfollow events
if (['follow', 'join'].includes(event.type)) {
const chatId = event.source.userId || event.source.groupId || event.source.roomId;
saveChat(chatId, event.source.type);
if (event.type === 'join') {
return client.replyMessage(event.replyToken, {
async function handleEvent(event, client, blobClient) {
const chatId = event.source.userId || event.source.groupId || event.source.roomId;
saveChat(chatId, event.source.type);

if (event.type === 'follow' || event.type === 'join') {
return client.replyMessage({
replyToken: event.replyToken,
messages: [{
type: 'text',
text: 'はじめまして!時間割Botです。「使い方」と送ると、できることを確認できます。'
});
}
return null;
text: 'はじめまして!寄せ書き受付ボットです。寄せ書きのファイル(画像やPDFなど)を送ってください!'
}]
});
}

if (['unfollow', 'leave'].includes(event.type)) {
const chatId = event.source.userId || event.source.groupId || event.source.roomId;
if (event.type === 'unfollow' || event.type === 'leave') {
removeChat(chatId);
removeSubscriber(chatId);
return null;
}

if (event.type !== 'message' || event.message.type !== 'text') {
return null;
}

const userMessage = event.message.text.trim();
const source = event.source;
let chatId;
let isGroup = false;

if (source.type === 'user') {
chatId = source.userId;
} else if (source.type === 'group') {
chatId = source.groupId;
isGroup = true;
} else if (source.type === 'room') {
chatId = source.roomId;
isGroup = true;
}

// Filter non-commands in groups
if (isGroup && !validCommands.includes(userMessage)) {
return null;
}

// Ensure chat is tracked
saveChat(chatId, source.type);

let reply;

if (userMessage === '使い方' || userMessage === 'ヘルプ') {
reply = flexTemplates.createHelpFlex();
} else if (userMessage === '今日の時間割') {
reply = await getTodayTimetable();
} else if (userMessage === '明日の時間割') {
reply = await getTomorrowTimetable();
} else if (userMessage === '今日の行事') {
reply = await getTodayEvents();
} else if (userMessage === '明日の行事') {
reply = await getTomorrowEvents();
} else if (userMessage === '通知オン') {
const added = saveSubscriber(chatId);
return client.replyMessage(event.replyToken, {
type: 'text',
text: added ? '毎朝7時の時間割通知をオンにしました。' : '既に通知はオンになっています。',
quickReply: flexTemplates.getQuickReplies()
});
} else if (userMessage === '通知オフ') {
const removed = removeSubscriber(chatId);
return client.replyMessage(event.replyToken, {
type: 'text',
text: removed ? '時間割通知をオフにしました。再度オンにするには「通知オン」と送ってください。' : '通知は既にオフになっています。',
quickReply: flexTemplates.getQuickReplies()
});
} else if (userMessage.endsWith('曜日の時間割')) {
const day = userMessage.replace('の時間割', '');
reply = await getTimetableForDay(day);
} else {
if (!isGroup) {
return client.replyMessage(event.replyToken, {
type: 'text',
text: '「使い方」と入力すると、使用可能なコマンドが表示されます。',
quickReply: flexTemplates.getQuickReplies()
});
// Handle message events
if (event.type === 'message') {
const message = event.message;

// Handle image, video, file, audio
if (['image', 'file', 'video', 'audio'].includes(message.type)) {
try {
const stream = await blobClient.getMessageContent(message.id);
const unsafeFileName = message.fileName || `${Date.now()}_${chatId.substring(0, 8)}.${getFileExtension(message.type)}`;
const fileName = path.basename(unsafeFileName);
const filePath = path.join(UPLOADS_DIR, fileName);

const writer = fs.createWriteStream(filePath);

await new Promise((resolve, reject) => {
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';
}
}

Expand Down Expand Up @@ -242,6 +151,5 @@ function removeChat(id) {
}

module.exports = {
handleEvent,
getTodayTimetable
handleEvent
};
Loading