Teach Telegram sender to accept an optional parse_mode and enable MarkdownV2 only for scheduled task summaries so headings, emphasis, and escaped characters render correctly without affecting other bot replies.
308 lines
9.3 KiB
JavaScript
308 lines
9.3 KiB
JavaScript
const { User, Task, Project, Tag } = require('../models');
|
|
const { Op } = require('sequelize');
|
|
const TelegramPoller = require('./telegramPoller');
|
|
|
|
// escape markdown special characters
|
|
const escapeMarkdown = (text) => {
|
|
if (!text) return '';
|
|
// Characters that need to be escaped in MarkdownV2: _*[]()~`>#+-=|{}.!
|
|
return text.toString().replace(/([_*\[\]()~`>#+\-=|{}.!])/g, '\\$1');
|
|
};
|
|
|
|
// get priority emoji
|
|
const getPriorityEmoji = (priority) => {
|
|
const emojiMap = {
|
|
2: '🔴', // high
|
|
1: '🟠', // medium
|
|
0: '🟢', // low
|
|
};
|
|
return emojiMap[priority] || '⚪';
|
|
};
|
|
|
|
// create date range for today
|
|
const createTodayDateRange = () => {
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
return { today, tomorrow };
|
|
};
|
|
|
|
// format task for display
|
|
const formatTaskForDisplay = (task, index, includeStatus = false) => {
|
|
const priorityEmoji = getPriorityEmoji(task.priority);
|
|
const statusEmoji = includeStatus ? '✅ ' : '';
|
|
const taskName = escapeMarkdown(task.name);
|
|
const projectInfo = task.Project
|
|
? ` \\[${escapeMarkdown(task.Project.name)}\\]`
|
|
: '';
|
|
return `${index + 1}\\. ${statusEmoji}${priorityEmoji} ${taskName}${projectInfo}\n`;
|
|
};
|
|
|
|
// build task section
|
|
const buildTaskSection = (tasks, title, includeStatus = false) => {
|
|
if (tasks.length === 0) return '';
|
|
|
|
let section = `${title}\n`;
|
|
section += tasks
|
|
.map((task, index) => formatTaskForDisplay(task, index, includeStatus))
|
|
.join('');
|
|
section += '\n';
|
|
|
|
return section;
|
|
};
|
|
|
|
// build summary message
|
|
const buildSummaryMessage = (taskSections) => {
|
|
let message = "📋 *Today's Task Summary*\n\n";
|
|
message += '━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
|
|
message += "✏️ *Today's Plan*\n\n";
|
|
|
|
message += taskSections.dueToday;
|
|
message += taskSections.inProgress;
|
|
message += taskSections.suggested;
|
|
message += taskSections.completed;
|
|
|
|
message += '━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
message += '🎯 *Stay focused and make it happen\\!*';
|
|
|
|
return message;
|
|
};
|
|
|
|
// calculate next run time
|
|
const calculateNextRunTime = (user, fromTime = new Date()) => {
|
|
const frequency = user.task_summary_frequency;
|
|
const from = new Date(fromTime);
|
|
|
|
const calculations = {
|
|
daily: () => {
|
|
const nextDay = new Date(from);
|
|
nextDay.setDate(nextDay.getDate() + 1);
|
|
nextDay.setHours(7, 0, 0, 0);
|
|
return nextDay;
|
|
},
|
|
|
|
weekdays: () => {
|
|
const currentDay = from.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
|
let daysToAdd = 1;
|
|
if (currentDay === 5) {
|
|
// Friday
|
|
daysToAdd = 3; // Skip to Monday
|
|
} else if (currentDay === 6) {
|
|
// Saturday
|
|
daysToAdd = 2; // Skip to Monday
|
|
}
|
|
const nextWeekday = new Date(from);
|
|
nextWeekday.setDate(nextWeekday.getDate() + daysToAdd);
|
|
nextWeekday.setHours(7, 0, 0, 0);
|
|
return nextWeekday;
|
|
},
|
|
|
|
weekly: () => {
|
|
const nextWeek = new Date(from);
|
|
nextWeek.setDate(nextWeek.getDate() + 7);
|
|
nextWeek.setHours(7, 0, 0, 0);
|
|
return nextWeek;
|
|
},
|
|
|
|
'1h': () => {
|
|
const nextHour = new Date(from);
|
|
nextHour.setHours(nextHour.getHours() + 1);
|
|
return nextHour;
|
|
},
|
|
|
|
'2h': () => {
|
|
const next = new Date(from);
|
|
next.setHours(next.getHours() + 2);
|
|
return next;
|
|
},
|
|
|
|
'4h': () => {
|
|
const next = new Date(from);
|
|
next.setHours(next.getHours() + 4);
|
|
return next;
|
|
},
|
|
|
|
'8h': () => {
|
|
const next = new Date(from);
|
|
next.setHours(next.getHours() + 8);
|
|
return next;
|
|
},
|
|
|
|
'12h': () => {
|
|
const next = new Date(from);
|
|
next.setHours(next.getHours() + 12);
|
|
return next;
|
|
},
|
|
};
|
|
|
|
const calculator = calculations[frequency];
|
|
return calculator ? calculator() : calculations.daily();
|
|
};
|
|
|
|
// Side effect function to fetch user by ID
|
|
const fetchUser = async (userId) => await User.findByPk(userId);
|
|
|
|
// Side effect function to fetch due today tasks
|
|
const fetchDueTodayTasks = async (userId, today, tomorrow) =>
|
|
await Task.findAll({
|
|
where: {
|
|
user_id: userId,
|
|
due_date: {
|
|
[Op.gte]: today,
|
|
[Op.lt]: tomorrow,
|
|
},
|
|
status: { [Op.ne]: 2 }, // not done
|
|
},
|
|
include: [{ model: Project, attributes: ['name'] }],
|
|
order: [['name', 'ASC']],
|
|
});
|
|
|
|
// Side effect function to fetch in progress tasks
|
|
const fetchInProgressTasks = async (userId) =>
|
|
await Task.findAll({
|
|
where: {
|
|
user_id: userId,
|
|
status: 1, // in_progress
|
|
},
|
|
include: [{ model: Project, attributes: ['name'] }],
|
|
order: [['name', 'ASC']],
|
|
});
|
|
|
|
// Side effect function to fetch completed today tasks
|
|
const fetchCompletedTodayTasks = async (userId, today, tomorrow) =>
|
|
await Task.findAll({
|
|
where: {
|
|
user_id: userId,
|
|
status: 2, // done
|
|
parent_task_id: null,
|
|
updated_at: {
|
|
[Op.gte]: today,
|
|
[Op.lt]: tomorrow,
|
|
},
|
|
},
|
|
include: [{ model: Project, attributes: ['name'] }],
|
|
order: [['name', 'ASC']],
|
|
});
|
|
|
|
// Side effect function to fetch suggested tasks
|
|
const fetchSuggestedTasks = async (userId, excludedIds) =>
|
|
await Task.findAll({
|
|
where: {
|
|
user_id: userId,
|
|
status: { [Op.ne]: 2 }, // not done
|
|
id: { [Op.notIn]: excludedIds },
|
|
},
|
|
include: [{ model: Project, attributes: ['name'] }],
|
|
order: [
|
|
['priority', 'DESC'],
|
|
['name', 'ASC'],
|
|
],
|
|
limit: 5,
|
|
});
|
|
|
|
// Side effect function to send telegram message
|
|
const sendTelegramMessage = async (token, chatId, message) => {
|
|
const poller = TelegramPoller;
|
|
return await poller.sendTelegramMessage(token, chatId, message);
|
|
};
|
|
|
|
// Side effect function to update user tracking fields
|
|
const updateUserTracking = async (user, lastRun, nextRun) =>
|
|
await user.update({
|
|
task_summary_last_run: lastRun,
|
|
task_summary_next_run: nextRun,
|
|
});
|
|
|
|
// Function to generate summary for user (contains side effects)
|
|
const generateSummaryForUser = async (userId) => {
|
|
try {
|
|
const user = await fetchUser(userId);
|
|
if (!user) return null;
|
|
|
|
const { today, tomorrow } = createTodayDateRange();
|
|
|
|
// Fetch all task data in parallel
|
|
const [dueToday, inProgress, completedToday] = await Promise.all([
|
|
fetchDueTodayTasks(userId, today, tomorrow),
|
|
fetchInProgressTasks(userId),
|
|
fetchCompletedTodayTasks(userId, today, tomorrow),
|
|
]);
|
|
|
|
// Get suggested tasks (excluding already fetched ones)
|
|
const excludedIds = [
|
|
...dueToday.map((t) => t.id),
|
|
...inProgress.map((t) => t.id),
|
|
];
|
|
const suggestedTasks = await fetchSuggestedTasks(userId, excludedIds);
|
|
|
|
// Build task sections
|
|
const taskSections = {
|
|
dueToday: buildTaskSection(dueToday, '🚀 *Tasks Due Today:*'),
|
|
inProgress: buildTaskSection(inProgress, '⚙️ *In Progress Tasks:*'),
|
|
suggested: buildTaskSection(
|
|
suggestedTasks,
|
|
'💡 *Suggested Tasks:*'
|
|
),
|
|
completed: buildTaskSection(
|
|
completedToday,
|
|
'✅ *Completed Today:*',
|
|
true
|
|
),
|
|
};
|
|
|
|
return buildSummaryMessage(taskSections);
|
|
} catch (error) {
|
|
console.error('Error generating task summary:', error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Function to send summary to user (contains side effects)
|
|
const sendSummaryToUser = async (userId) => {
|
|
try {
|
|
const user = await fetchUser(userId);
|
|
if (!user || !user.telegram_bot_token || !user.telegram_chat_id) {
|
|
return false;
|
|
}
|
|
|
|
const summary = await generateSummaryForUser(userId);
|
|
if (!summary) return false;
|
|
|
|
// Send the message via Telegram
|
|
await sendTelegramMessage(
|
|
user.telegram_bot_token,
|
|
user.telegram_chat_id,
|
|
summary,
|
|
null,
|
|
{ parseMode: 'MarkdownV2' }
|
|
);
|
|
|
|
// Update tracking fields
|
|
const now = new Date();
|
|
const nextRun = calculateNextRunTime(user, now);
|
|
await updateUserTracking(user, now, nextRun);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error(
|
|
`Error sending task summary to user ${userId}:`,
|
|
error.message
|
|
);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Export functional interface
|
|
module.exports = {
|
|
generateSummaryForUser,
|
|
sendSummaryToUser,
|
|
calculateNextRunTime,
|
|
// For testing
|
|
_escapeMarkdown: escapeMarkdown,
|
|
_getPriorityEmoji: getPriorityEmoji,
|
|
_createTodayDateRange: createTodayDateRange,
|
|
_formatTaskForDisplay: formatTaskForDisplay,
|
|
_buildTaskSection: buildTaskSection,
|
|
_buildSummaryMessage: buildSummaryMessage,
|
|
};
|