Додані повідомлення та перепрацьована структура застосунку та api
This commit is contained in:
@@ -8,4 +8,8 @@ RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY ./fonts/ /usr/share/fonts/truetype/roboto/
|
||||
|
||||
RUN fc-cache -f -v
|
||||
|
||||
CMD npm start
|
||||
|
||||
52
cron/cron.js
52
cron/cron.js
@@ -1,13 +1,57 @@
|
||||
const cron = require("node-cron");
|
||||
const Backup = require("./tasks/backup");
|
||||
const Stand = require("./tasks/stands");
|
||||
const Messages = require('./tasks/messages');
|
||||
const Rept = require('./tasks/rept');
|
||||
|
||||
// Завдання: резервна копія БД кожен день в 22:30
|
||||
// 1. Резервна копія БД щодня о 22:35
|
||||
cron.schedule("30 22 * * *", () => {
|
||||
Backup.database();
|
||||
|
||||
const now = new Date().toLocaleString();
|
||||
console.log(`[${now}] Завдання «Backup» виконане!`);
|
||||
console.log(`[${new Date().toLocaleString()}] Завдання «Backup» виконане!`);
|
||||
});
|
||||
|
||||
// 2. Перевірка стендів без графіку щосуботи о 18:00
|
||||
cron.schedule("0 18 * * 6", async () => {
|
||||
console.log(`[${new Date().toLocaleString()}] Запуск перевірки стендів без графіку...`);
|
||||
try {
|
||||
await Stand.check_add();
|
||||
console.log(`[${new Date().toLocaleString()}] Завдання «Stand check_add» виконане!`);
|
||||
} catch (err) {
|
||||
console.error("Помилка Stand check_add:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Відправка графіку стендів щодня о 18:00
|
||||
cron.schedule("0 18 * * *", async () => {
|
||||
console.log(`[${new Date().toLocaleString()}] Запуск відправки графіків...`);
|
||||
try {
|
||||
await Stand.check_entries();
|
||||
console.log(`[${new Date().toLocaleString()}] Завдання «Stand check_entries» виконане!`);
|
||||
} catch (err) {
|
||||
console.error("Помилка Stand check_entries:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Очищення старих повідомлень щодня о 17:59
|
||||
cron.schedule('59 17 * * *', async () => {
|
||||
console.log(`[${new Date().toLocaleString()}] Запуск очищення старих повідомлень...`);
|
||||
try {
|
||||
await Messages.cleanup_old();
|
||||
console.log(`[${new Date().toLocaleString()}] Завдання «Messages cleanup_old» виконане успішно!`);
|
||||
} catch (err) {
|
||||
console.error(`[${new Date().toLocaleString()}] Помилка Messages cleanup_old:`, err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Нагадування про звіт щомісяця о 10:00
|
||||
cron.schedule('00 10 1 * *', async () => {
|
||||
console.log(`[${new Date().toLocaleString()}] Запуск відправки повідомлень...`);
|
||||
try {
|
||||
await Rept.send_notification();
|
||||
console.log(`[${new Date().toLocaleString()}] Завдання Rept send_notification виконане успішно!`);
|
||||
} catch (err) {
|
||||
console.error(`[${new Date().toLocaleString()}] Помилка Rept send_notification:`, err.message);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Cron-завдання запущено.");
|
||||
BIN
cron/font/RobotoMono-Bold.ttf
Normal file
BIN
cron/font/RobotoMono-Bold.ttf
Normal file
Binary file not shown.
BIN
cron/font/RobotoMono-Light.ttf
Normal file
BIN
cron/font/RobotoMono-Light.ttf
Normal file
Binary file not shown.
BIN
cron/font/RobotoMono-Medium.ttf
Normal file
BIN
cron/font/RobotoMono-Medium.ttf
Normal file
Binary file not shown.
BIN
cron/font/RobotoMono-Regular.ttf
Normal file
BIN
cron/font/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
BIN
cron/fonts/RobotoMono-Bold.ttf
Normal file
BIN
cron/fonts/RobotoMono-Bold.ttf
Normal file
Binary file not shown.
BIN
cron/fonts/RobotoMono-Light.ttf
Normal file
BIN
cron/fonts/RobotoMono-Light.ttf
Normal file
Binary file not shown.
BIN
cron/fonts/RobotoMono-Medium.ttf
Normal file
BIN
cron/fonts/RobotoMono-Medium.ttf
Normal file
Binary file not shown.
BIN
cron/fonts/RobotoMono-Regular.ttf
Normal file
BIN
cron/fonts/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
2026
cron/package-lock.json
generated
2026
cron/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,12 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"node-cron": "^4.2.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"web-push": "^3.6.7",
|
||||
"@aws-sdk/client-s3": "^3.1004.0",
|
||||
"dotenv": "^17.2.0",
|
||||
"node-telegram-bot-api": "^0.66.0"
|
||||
"node-cron": "^4.2.1",
|
||||
"node-telegram-bot-api": "^0.66.0",
|
||||
"sharp": "^0.34.5",
|
||||
"sqlite3": "^5.1.7",
|
||||
"web-push": "^3.6.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const TelegramBot = require("node-telegram-bot-api");
|
||||
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
|
||||
|
||||
const TOKEN = process.env.TELEGRAM_TOKEN;
|
||||
const CHAT_ID = process.env.CHAT_ID;
|
||||
@@ -9,6 +10,16 @@ const FILE = path.join(DB_PATH, "database.sqlite");
|
||||
|
||||
const bot = new TelegramBot(TOKEN, { polling: false });
|
||||
|
||||
// Настройки S3 / R2
|
||||
const s3Client = new S3Client({
|
||||
region: "auto",
|
||||
endpoint: process.env.S3_ENDPOINT, // Напр: https://<id>.r2.cloudflarestorage.com
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
class Backup {
|
||||
async database() {
|
||||
try {
|
||||
@@ -17,13 +28,25 @@ class Backup {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📤 Надсилаю файл: ${FILE}`);
|
||||
const fileName = `backup_${new Date().toISOString().replace(/[:.]/g, "-")}.sqlite`;
|
||||
|
||||
// 1. Отправка в Telegram
|
||||
await bot.sendDocument(CHAT_ID, fs.createReadStream(FILE), {
|
||||
caption: "📦 Резервна копія бази даних",
|
||||
caption: `📦 Резервна копія бази даних`,
|
||||
});
|
||||
console.log("✅ Telegram: успішно надіслано");
|
||||
|
||||
console.log("✅ Файл успішно надіслано!");
|
||||
// 2. Отправка в S3 (R2)
|
||||
const fileBuffer = fs.readFileSync(FILE);
|
||||
const uploadParams = {
|
||||
Bucket: process.env.S3_BUCKET_NAME,
|
||||
Key: `sheep-service.com/database/${fileName}`, // Путь внутри бакета
|
||||
Body: fileBuffer,
|
||||
ContentType: "application/x-sqlite3",
|
||||
};
|
||||
|
||||
await s3Client.send(new PutObjectCommand(uploadParams));
|
||||
console.log("✅ S3: успішно завантажено в хмару");
|
||||
} catch (err) {
|
||||
console.error("❌ Помилка при надсиланні файлу:", err.message);
|
||||
}
|
||||
|
||||
55
cron/tasks/messages.js
Normal file
55
cron/tasks/messages.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const db = require("../config/db");
|
||||
const TelegramBot = require("node-telegram-bot-api");
|
||||
|
||||
const util = require('util');
|
||||
const dbAll = util.promisify(db.all).bind(db);
|
||||
const dbRun = util.promisify(db.run).bind(db);
|
||||
|
||||
const TOKEN = process.env.TELEGRAM_TOKEN;
|
||||
const STAND_CHAT_ID = process.env.STAND_CHAT_ID;
|
||||
|
||||
const bot = new TelegramBot(TOKEN, { polling: false });
|
||||
|
||||
class Messages {
|
||||
async cleanup_old() {
|
||||
try {
|
||||
// "Зараз мінус 24 години"
|
||||
const oneDayAgo = Date.now() - (24 * 60 * 55 * 1000);
|
||||
|
||||
// 1. Отримуємо повідомлення, які старші за добу
|
||||
const oldMessages = await dbAll(
|
||||
`SELECT last_message_id FROM sent_messages WHERE created_at < ?`,
|
||||
[oneDayAgo]
|
||||
);
|
||||
|
||||
if (!oldMessages || oldMessages.length === 0) {
|
||||
console.log('🧹 Застарілих повідомлень не знайдено.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🧹 Знайдено ${oldMessages.length} повідомлень для видалення...`);
|
||||
|
||||
// 2. Видаляємо повідомлення з Telegram по черзі
|
||||
for (const msg of oldMessages) {
|
||||
try {
|
||||
await bot.deleteMessage(STAND_CHAT_ID, msg.last_message_id);
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // пауза 50мс
|
||||
} catch (e) {
|
||||
// Код помилки 400 зазвичай означає, що повідомлення вже видалено вручну
|
||||
// або минуло понад 48 годин
|
||||
console.log(`⚠️ Повідомлення ${msg.last_message_id} не знайдено в Telegram або застаре.`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Видаляємо записи з бази даних одним махом
|
||||
await dbRun(`DELETE FROM sent_messages WHERE created_at < ?`, [oneDayAgo]);
|
||||
|
||||
console.log(`✅ Очистка завершена.`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Помилка під час виконання cleanup_old:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Messages();
|
||||
28
cron/tasks/rept.js
Normal file
28
cron/tasks/rept.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const Notification = require("../utils/notification");
|
||||
|
||||
class Rept {
|
||||
async send_notification() {
|
||||
let text = [
|
||||
'Час підбивати підсумки! Не забудьте здати свій звіт про служіння за минулий місяць 📋',
|
||||
'Нагадуємо про щомісячний звіт. Будь ласка, надішліть його сьогодні 😊',
|
||||
'Кінець місяця вже тут, а це значить — пора здавати звіти про служіння 🕒',
|
||||
'Ваш звіт дуже важливий! Не забудьте поділитися результатами служіння за цей місяць.',
|
||||
'Маленьке нагадування: пора заповнити звіт про служіння за місяць. Дякуємо за вашу працю!',
|
||||
'Ще не здали звіт? Саме час це зробити! 😉',
|
||||
'Звітність — це порядок. Будь ласка, надішліть дані про своє служіння за минулий місяць.',
|
||||
'Місяць завершився, тож не забудьте прозвітувати про ваші успіхи в служінні 📝',
|
||||
'Чекаємо на ваш звіт! Це допоможе нам мати загальну картину нашого спільного служіння.',
|
||||
'Пора здавати звіти! Дякуємо кожному за активність у цьому місяці ✨',
|
||||
'Не відкладайте на потім — здайте звіт про служіння вже зараз 📋'
|
||||
];
|
||||
let randomMessage = text[Math.floor(Math.random() * text.length)];
|
||||
|
||||
Notification.sendAll({
|
||||
title: "Звіт про служіння",
|
||||
body: randomMessage,
|
||||
page: `/`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Rept();
|
||||
260
cron/tasks/stands.js
Normal file
260
cron/tasks/stands.js
Normal file
@@ -0,0 +1,260 @@
|
||||
const db = require("../config/db");
|
||||
const Notification = require("../utils/notification");
|
||||
const sharp = require('sharp');
|
||||
const TelegramBot = require("node-telegram-bot-api");
|
||||
|
||||
const util = require('util');
|
||||
const dbAll = util.promisify(db.all).bind(db);
|
||||
const dbRun = util.promisify(db.run).bind(db);
|
||||
|
||||
const TOKEN = process.env.TELEGRAM_TOKEN;
|
||||
const STAND_CHAT_ID = process.env.STAND_CHAT_ID;
|
||||
|
||||
const bot = new TelegramBot(TOKEN, { polling: false });
|
||||
|
||||
|
||||
class Stands {
|
||||
async check_add() {
|
||||
const sqlStands = `
|
||||
SELECT id
|
||||
FROM stand_list
|
||||
WHERE status = '1'
|
||||
ORDER BY id
|
||||
`;
|
||||
|
||||
db.all(sqlStands, (err, stands) => {
|
||||
if (err) {
|
||||
console.error('DB error:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stands.length) {
|
||||
console.log('There are no active stands');
|
||||
return;
|
||||
}
|
||||
|
||||
const dateNow = Date.now();
|
||||
|
||||
const sqlSchedule = `
|
||||
SELECT 1
|
||||
FROM stand_schedule
|
||||
WHERE stand_id = ?
|
||||
AND date >= ?
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
let checked = 0;
|
||||
let emptyStands = 0;
|
||||
|
||||
stands.forEach(stand => {
|
||||
db.get(sqlSchedule, [stand.id, dateNow], (err, row) => {
|
||||
if (err) {
|
||||
console.error('Schedule error:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!row) emptyStands++;
|
||||
|
||||
checked++;
|
||||
|
||||
if (checked === stands.length) {
|
||||
console.log('Empty stands:', emptyStands);
|
||||
|
||||
if (emptyStands > 0) {
|
||||
Notification.sendStandAdd({
|
||||
title: 'Додайте нові дні служіння',
|
||||
body: `${emptyStands} з ${stands.length} стендів потребують додавання днів служіння.`,
|
||||
page: '/stand'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async check_entries() {
|
||||
try {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
// 1. Отримуємо вісників і одразу робимо Map для швидкого пошуку
|
||||
const sheeps = await dbAll(`SELECT id, name FROM sheeps`);
|
||||
if (!sheeps.length) return console.log('There are no sheeps');
|
||||
|
||||
const sheepMap = new Map(sheeps.map(s => [s.id, s.name]));
|
||||
|
||||
const startTomorrow = today.getTime() + 86400000;
|
||||
const startDayAfterTomorrow = today.getTime() + 86400000 * 2;
|
||||
|
||||
const sqlStands = `
|
||||
SELECT
|
||||
stand_schedule.*,
|
||||
(SELECT stand_list.title FROM stand_list WHERE stand_list.id = stand_schedule.stand_id) AS title
|
||||
FROM
|
||||
stand_schedule
|
||||
WHERE
|
||||
stand_schedule.date >= ? AND stand_schedule.date < ?
|
||||
ORDER BY
|
||||
stand_schedule.stand_id
|
||||
`;
|
||||
|
||||
const schedule = await dbAll(sqlStands, [startTomorrow, startDayAfterTomorrow]);
|
||||
if (!schedule.length) return console.log('No active schedule');
|
||||
|
||||
// 2. Угруповання даних (Transform)
|
||||
const standsData = schedule.reduce((acc, item) => {
|
||||
if (!acc[item.stand_id]) {
|
||||
acc[item.stand_id] = { id: item.stand_id, title: item.title || `Стенд ${sId}`, hours: {}, maxSheepIdx: 0, date: item.date };
|
||||
}
|
||||
if (!acc[item.stand_id].hours[item.hour]) {
|
||||
acc[item.stand_id].hours[item.hour] = {};
|
||||
}
|
||||
acc[item.stand_id].hours[item.hour][item.number_sheep] = item.sheep_id;
|
||||
acc[item.stand_id].maxSheepIdx = Math.max(acc[item.stand_id].maxSheepIdx, item.number_sheep);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 3. Генерація
|
||||
for (const standId in standsData) {
|
||||
const data = standsData[standId];
|
||||
const title = data.title;
|
||||
const sortedHours = Object.keys(data.hours).map(Number).sort((a, b) => a - b);
|
||||
|
||||
// Визначаємо крок (мінімальна різниця між годинником або 1)
|
||||
let step = 1;
|
||||
if (sortedHours.length > 1) {
|
||||
step = sortedHours[1] - sortedHours[0];
|
||||
}
|
||||
|
||||
const sheepIdsMatrix = sortedHours.map(h => {
|
||||
const row = [];
|
||||
for (let i = 0; i <= data.maxSheepIdx; i++) {
|
||||
row.push(data.hours[h][i] || null);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
|
||||
const formattedDate = new Date(data.date).toLocaleDateString('ru-RU');
|
||||
|
||||
const result = await generateSchedule({
|
||||
title: title,
|
||||
date: formattedDate,
|
||||
options: {
|
||||
start: sortedHours[0],
|
||||
quantity: data.maxSheepIdx + 1,
|
||||
step: step
|
||||
},
|
||||
sheep_ids: sheepIdsMatrix,
|
||||
sheepMap // Передаємо Map для імен
|
||||
});
|
||||
|
||||
if (result && result.buffer) {
|
||||
try {
|
||||
let isNotFull = result.isNotFull ? '\n\n✏️ Ще є вільні місця, встигніть записатись' : '';
|
||||
|
||||
// 1. Надсилаємо нове фото
|
||||
const sentMessage = await bot.sendPhoto(STAND_CHAT_ID, result.buffer, {
|
||||
caption: `📍 *${title}*\n📅 ${formattedDate}${isNotFull}`,
|
||||
parse_mode: 'Markdown'
|
||||
}, {
|
||||
filename: result.fileName,
|
||||
contentType: 'image/png'
|
||||
});
|
||||
|
||||
// 2. Зберігаємо ID нового повідомлення у базі
|
||||
await dbRun(
|
||||
`INSERT INTO sent_messages (last_message_id, created_at) VALUES (?, ?)`, [sentMessage.message_id, Date.now()]
|
||||
);
|
||||
|
||||
console.log(`✅ Обновлено: ${title}`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Помилка відправки в Telegram:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Logic error:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Генерування зображення графіка
|
||||
async function generateSchedule({
|
||||
title, date, options, sheep_ids, sheepMap
|
||||
}) {
|
||||
const { start, step, quantity } = options;
|
||||
const rowsCount = sheep_ids.length;
|
||||
|
||||
const rowHeight = 50;
|
||||
const timeWidth = 120;
|
||||
const gap = 10;
|
||||
const padding = 10;
|
||||
|
||||
const totalWidth = 200 + (200 * quantity);
|
||||
const headerHeight = 85;
|
||||
const totalHeight = 136 + (rowHeight * (rowsCount - 1));
|
||||
|
||||
const mainRectWidth = totalWidth - (padding * 2);
|
||||
const availableForBlocks = mainRectWidth - timeWidth - 5;
|
||||
const blockWidth = (availableForBlocks - (gap * (quantity - 1))) / quantity;
|
||||
|
||||
let isNotFull = false;
|
||||
|
||||
const formatTime = (h) => {
|
||||
const hh = Math.floor(h).toString().padStart(2, '0');
|
||||
const mm = (h % 1 * 60).toString().padStart(2, '0');
|
||||
return `${hh}:${mm}`;
|
||||
};
|
||||
|
||||
let rowsSvg = '';
|
||||
sheep_ids.forEach((rowSheepIds, i) => {
|
||||
const y = headerHeight + (i * rowHeight);
|
||||
const hStart = formatTime(start + (i * step));
|
||||
const hEnd = formatTime(start + ((i + 1) * step));
|
||||
const isEven = i % 2 === 0;
|
||||
|
||||
// Фон рядка
|
||||
rowsSvg += `<rect x="${padding}" y="${y}" width="${totalWidth - padding * 2}" height="40" rx="8" fill="${isEven ? '#fbfbfb' : '#e7e7e1'}" />`;
|
||||
|
||||
// Текст часу
|
||||
rowsSvg += `<text x="${padding + 7}" y="${y + 27}" font-family="Roboto Mono" font-size="15" fill="#313131">${hStart}-${hEnd}</text>`;
|
||||
|
||||
// Блоки з іменами
|
||||
rowSheepIds.forEach((id, j) => {
|
||||
const x = padding + timeWidth + (j * (blockWidth + gap));
|
||||
const name = sheepMap.get(id) || '';
|
||||
if(!sheepMap.get(id)) isNotFull = true; // Якщо є вільне місце
|
||||
|
||||
rowsSvg += `
|
||||
<g>
|
||||
<rect x="${x}" y="${y + 5}" width="${blockWidth}" height="30" rx="5" fill="${isEven ? '#e7e7e1' : '#fbfbfb'}" />
|
||||
<text x="${x + 5}" y="${y + 25}" font-family="Roboto Mono" font-size="15" fill="#313131">${name}</text>
|
||||
</g>`;
|
||||
});
|
||||
});
|
||||
|
||||
const finalSvg = `
|
||||
<svg width="${totalWidth * 2}" height="${totalHeight * 2}" viewBox="0 0 ${totalWidth} ${totalHeight}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect height="${totalHeight}" width="${totalWidth}" fill="#e7e7e1"/>
|
||||
<text font-weight="bold" text-anchor="middle" font-family="'Roboto Mono'" font-size="24" y="52" x="${totalWidth / 2}" fill="#313131">
|
||||
${date} • ${title}
|
||||
</text>
|
||||
${rowsSvg}
|
||||
</svg>`;
|
||||
|
||||
try {
|
||||
const buffer = await sharp(Buffer.from(finalSvg))
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
return { buffer, fileName: `${title}_${date}.png`, isNotFull };
|
||||
} catch (e) {
|
||||
console.error('Sharp error:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Stands();
|
||||
@@ -11,6 +11,44 @@ webpush.setVapidDetails(
|
||||
);
|
||||
|
||||
class Notification {
|
||||
async sendAll({ title, body, page }) {
|
||||
const sql = `
|
||||
SELECT * FROM subscription
|
||||
ORDER BY id
|
||||
`;
|
||||
|
||||
db.all(sql, async (err, rows) => {
|
||||
if (err) {
|
||||
console.error('DB error:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
console.log(`🐑 No subscriptions`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📨 Sending notification to ${rows.length} subscriptions...`);
|
||||
|
||||
const payload = JSON.stringify({
|
||||
title: title ?? "Тестове повідомлення",
|
||||
body: body ?? "Ви успішно підписалися на отримання push повідомлень!",
|
||||
url: `https://${process.env.DOMAIN}${page ?? ""}`
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(rows.map(row => {
|
||||
const subscription = {
|
||||
endpoint: row.endpoint,
|
||||
keys: JSON.parse(row.keys),
|
||||
};
|
||||
return webpush.sendNotification(subscription, payload);
|
||||
}));
|
||||
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
console.log(`✅ Sent: ${rows.length - failed}, ❌ Failed: ${failed}`);
|
||||
});
|
||||
}
|
||||
|
||||
async sendSheep({ sheep_id, title, body, page }) {
|
||||
const sql = `
|
||||
SELECT * FROM subscription
|
||||
@@ -73,7 +111,7 @@ class Notification {
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
console.log(`🐑 No subscriptions found for sheep_id: ${sheep_id}`);
|
||||
console.log(`🐑 No subscriptions found for group_id: ${group_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,7 +136,7 @@ class Notification {
|
||||
});
|
||||
}
|
||||
|
||||
async sendStand({ title, body, page }) {
|
||||
async sendStandAdd({title, body, page }) {
|
||||
const sql = `
|
||||
SELECT
|
||||
subscription.*
|
||||
@@ -111,7 +149,7 @@ class Notification {
|
||||
possibilities
|
||||
ON possibilities.sheep_id = sheeps.id
|
||||
WHERE
|
||||
possibilities.can_view_stand = '1'
|
||||
possibilities.can_add_stand = '1'
|
||||
ORDER BY
|
||||
subscription.id;
|
||||
`;
|
||||
@@ -147,6 +185,22 @@ class Notification {
|
||||
console.log(`✅ Sent: ${rows.length - failed}, ❌ Failed: ${failed}`);
|
||||
});
|
||||
}
|
||||
|
||||
async sendEndpoint({ endpoint, keys, title, body, page }) {
|
||||
const payload = JSON.stringify({
|
||||
title: title ?? "Тестове повідомлення",
|
||||
body: body ?? "Ви успішно підписалися на отримання push повідомлень!",
|
||||
url: `https://${process.env.DOMAIN}${page ?? ""}`
|
||||
});
|
||||
|
||||
const subscription = {
|
||||
endpoint,
|
||||
keys: typeof keys === "string" ? JSON.parse(keys) : keys
|
||||
};
|
||||
|
||||
await webpush.sendNotification(subscription, payload);
|
||||
console.log("✅ Push sent");
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = new Notification();
|
||||
Reference in New Issue
Block a user