Додані повідомлення та перепрацьована структура застосунку та 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
|
||||
|
||||
+48
-4
@@ -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-завдання запущено.");
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Generated
+2026
File diff suppressed because it is too large
Load Diff
+6
-4
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+26
-3
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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