Додані повідомлення та перепрацьована структура застосунку та api
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
async function pushToMetrics(metric) {
|
||||
if (!metric || !metric.type) return;
|
||||
|
||||
const payload = { ...metric, timestamp: Date.now() };
|
||||
|
||||
fetch("http://metrics:4005/push", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
}).catch(err => console.error("Metrics push error:", err));
|
||||
}
|
||||
|
||||
module.exports = { pushToMetrics };
|
||||
@@ -13,6 +13,7 @@
|
||||
"sqlite3": "^5.1.7",
|
||||
"url": "^0.11.4",
|
||||
"ws": "^8.18.0",
|
||||
"dotenv": "^17.2.0"
|
||||
"dotenv": "^17.2.0",
|
||||
"web-push": "^3.6.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,9 @@ const { updateApartment } = require("../services/apartments.service");
|
||||
const { updateBuilding } = require("../services/buildings.service");
|
||||
const { lockingStand, unlockingStand, updateStand } = require("../services/stand.service");
|
||||
const { broadcast } = require("../utils/broadcaster");
|
||||
const { pushToMetrics } = require("../middleware/pushToMetrics");
|
||||
|
||||
module.exports = async (wss, ws, message) => {
|
||||
try {
|
||||
pushToMetrics({
|
||||
type: "ws_out",
|
||||
length: message.length,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
switch (message.type) {
|
||||
case "apartment":
|
||||
await updateApartment(ws.user, message.data);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const db = require("../config/db");
|
||||
const Notification = require("../utils/notification.js");
|
||||
|
||||
function lockingStand(user, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sheepId = Number(data.sheep_id) || null;
|
||||
|
||||
|
||||
if (!user.possibilities.can_view_stand) {
|
||||
return reject(new Error("Forbidden: no rights to view stand"));
|
||||
}
|
||||
@@ -18,7 +19,7 @@ function lockingStand(user, data) {
|
||||
function unlockingStand(user, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sheepId = Number(data.sheep_id) || null;
|
||||
|
||||
|
||||
if (!user.possibilities.can_view_stand) {
|
||||
return reject(new Error("Forbidden: no rights to view stand"));
|
||||
}
|
||||
@@ -33,7 +34,7 @@ function unlockingStand(user, data) {
|
||||
function updateStand(user, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sheepId = Number(data.sheep_id) || null;
|
||||
|
||||
|
||||
if (!user.possibilities.can_view_stand) {
|
||||
return reject(new Error("Forbidden: no rights to view stand"));
|
||||
}
|
||||
@@ -52,11 +53,36 @@ function updateStand(user, data) {
|
||||
|
||||
const insertSql = `
|
||||
INSERT INTO stand_schedule_history
|
||||
(stand_schedule_id, sheep_id, created_at)
|
||||
VALUES (?, ?, ?)`;
|
||||
(stand_schedule_id, sheep_id, editor, created_at)
|
||||
VALUES (?, ?, ?, ?)`;
|
||||
|
||||
db.run(insertSql, [Number(data.id), sheepId, Date.now()], function (err) {
|
||||
db.run(insertSql, [Number(data.id), sheepId, user.id, Date.now()], function (err) {
|
||||
if (err) return reject(err);
|
||||
|
||||
if (sheepId === null) {
|
||||
let text = [
|
||||
'Звільнилося місце на одному зі стендів. Хто перший — той встигне 😉',
|
||||
'Є одне вільне місце на стенді. Запис відкрито — не проґавте 😉',
|
||||
'У одного зі стендів з’явилося вільне місце. Встигніть записатися!',
|
||||
'Раптова можливість! На стенді є вільне місце. Забронюйте його зараз 📋',
|
||||
'Одне місце стало вільним. Можливо, це саме ваше? 😉',
|
||||
'Стенд чекає нового учасника. Вільне місце вже доступне 📋',
|
||||
'Є шанс приєднатися — одне місце звільнилося 😊',
|
||||
'Вільне місце на стенді довго не чекатиме. Записуйтеся!',
|
||||
'Оголошуємо міні-набір: доступне одне місце на стенді.',
|
||||
'Щойно звільнилося місце. Хто швидший — той з нами 🚀',
|
||||
'З’явилася можливість долучитися до стенду. Кількість місць обмежена!',
|
||||
'Останнє вільне місце на стенді шукає свого власника.'
|
||||
];
|
||||
let randomMessage = text[Math.floor(Math.random() * text.length)];
|
||||
|
||||
Notification.sendStand({
|
||||
title: "Звільнилось місце",
|
||||
body: randomMessage,
|
||||
page: `/stand/card/${data.stand_id}`
|
||||
});
|
||||
}
|
||||
|
||||
resolve({ update: "ok", id: data.id, historyId: this.lastID });
|
||||
});
|
||||
});
|
||||
|
||||
152
ws/utils/notification.js
Normal file
152
ws/utils/notification.js
Normal file
@@ -0,0 +1,152 @@
|
||||
const db = require("../config/db");
|
||||
const webpush = require('web-push');
|
||||
|
||||
const VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY;
|
||||
const VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY;
|
||||
|
||||
webpush.setVapidDetails(
|
||||
'mailto:rozenrod320@gmail.com',
|
||||
VAPID_PUBLIC_KEY,
|
||||
VAPID_PRIVATE_KEY
|
||||
);
|
||||
|
||||
class Notification {
|
||||
async sendSheep({ sheep_id, title, body, page }) {
|
||||
const sql = `
|
||||
SELECT * FROM subscription
|
||||
WHERE sheep_id = ?
|
||||
ORDER BY id
|
||||
`;
|
||||
|
||||
db.all(sql, [sheep_id], async (err, rows) => {
|
||||
if (err) {
|
||||
console.error('DB error:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
console.log(`🐑 No subscriptions found for sheep_id: ${sheep_id}`);
|
||||
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 sendGroup({ group_id, title, body, page }) {
|
||||
const sql = `
|
||||
SELECT
|
||||
subscription.*
|
||||
FROM
|
||||
subscription
|
||||
JOIN
|
||||
sheeps
|
||||
ON
|
||||
sheeps.id = subscription.sheep_id
|
||||
WHERE
|
||||
sheeps.group_id = ?
|
||||
ORDER BY
|
||||
subscription.id;
|
||||
`;
|
||||
|
||||
db.all(sql, [group_id], async (err, rows) => {
|
||||
if (err) {
|
||||
console.error('DB error:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
console.log(`🐑 No subscriptions found for sheep_id: ${sheep_id}`);
|
||||
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 sendStand({ title, body, page }) {
|
||||
const sql = `
|
||||
SELECT
|
||||
subscription.*
|
||||
FROM
|
||||
subscription
|
||||
JOIN
|
||||
sheeps
|
||||
ON sheeps.id = subscription.sheep_id
|
||||
JOIN
|
||||
possibilities
|
||||
ON possibilities.sheep_id = sheeps.id
|
||||
WHERE
|
||||
possibilities.can_view_stand = '1'
|
||||
ORDER BY
|
||||
subscription.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}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = new Notification();
|
||||
11
ws/ws.js
11
ws/ws.js
@@ -2,7 +2,6 @@ const WebSocket = require("ws");
|
||||
const { URL } = require('url');
|
||||
const { routeMessage } = require("./routes");
|
||||
const { auth } = require("./middleware/auth");
|
||||
const { pushToMetrics } = require("./middleware/pushToMetrics");
|
||||
const { setupPing } = require("./utils/ping");
|
||||
require("dotenv").config();
|
||||
|
||||
@@ -29,19 +28,10 @@ wss.on("connection", async (ws, request) => {
|
||||
ws.user = user;
|
||||
ws.send(JSON.stringify({ connection: "success", api_version, user: {name: ws.user.name, id: ws.user.id } }));
|
||||
|
||||
pushToMetrics({ type: "connection_status", status: "online", api_version, user: {name: ws.user.name, id: ws.user.id } });
|
||||
|
||||
// Periodic ping to maintain a connection
|
||||
setupPing(ws);
|
||||
|
||||
ws.on("message", (raw) => {
|
||||
|
||||
pushToMetrics({
|
||||
type: "ws_in",
|
||||
length: raw.length,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
try {
|
||||
const message = JSON.parse(raw);
|
||||
routeMessage(wss, ws, message);
|
||||
@@ -53,7 +43,6 @@ wss.on("connection", async (ws, request) => {
|
||||
|
||||
ws.on("close", () => {
|
||||
console.log("🔌 Client disconnected");
|
||||
pushToMetrics({ type: "connection_status", status: "offline" });
|
||||
});
|
||||
ws.on("error", (err) => console.error("❌ WS error:", err));
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user