275 lines
7.1 KiB
JavaScript
275 lines
7.1 KiB
JavaScript
const express = require("express");
|
|
const http = require("http");
|
|
const WebSocket = require("ws");
|
|
const path = require("path");
|
|
const pool = require("./db");
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3100;
|
|
|
|
app.use(express.json());
|
|
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
// Serve static files from public directory
|
|
app.use("/static", express.static(path.join(__dirname, "public")));
|
|
// In-memory store for latest item lists.
|
|
// Structure:
|
|
// {
|
|
// sourceIdOrName: {
|
|
// from: <string>,
|
|
// ts: <number>,
|
|
// items: [ { name, count }, ... ],
|
|
// clientId: <ws client id> // optional internal id
|
|
// },
|
|
// ...
|
|
// }
|
|
const latestItems = {};
|
|
|
|
// Convenience: sanitize/normalize an items array coming from clients
|
|
function normalizeItems(arr) {
|
|
if (typeof arr !== "object" || !Array.isArray(arr)) return [];
|
|
const out = [];
|
|
for (let i = 0; i < arr.length; i++) {
|
|
const it = arr[i];
|
|
if (!it) continue;
|
|
const name = String(it.name || it.id || it.item || "unknown");
|
|
const count = Number(it.count || it.qty || it.stackSize || 0) || 0;
|
|
out.push({ name, count });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Create HTTP server & WebSocket server
|
|
const server = http.createServer(app);
|
|
const wss = new WebSocket.Server({ server });
|
|
|
|
require("./users-api")(app, wss, pool);
|
|
|
|
// Helper to send JSON to a ws client
|
|
function sendJSON(ws, obj) {
|
|
try {
|
|
ws.send(JSON.stringify(obj));
|
|
} catch (err) {
|
|
// ignore send errors for now
|
|
}
|
|
}
|
|
|
|
wss.on("connection", (ws, req) => {
|
|
const clientId =
|
|
Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
ws._clientId = clientId;
|
|
console.log("Client connected:", clientId, "from", req.socket.remoteAddress);
|
|
|
|
// Welcome message
|
|
sendJSON(ws, { type: "welcome", id: clientId, timestamp: Date.now() });
|
|
|
|
ws.on("message", (raw) => {
|
|
const text = raw.toString();
|
|
|
|
// Try to parse JSON; if parse fails we still broadcast the raw text as a message
|
|
let parsed = null;
|
|
try {
|
|
parsed = JSON.parse(text);
|
|
} catch (e) {
|
|
parsed = null;
|
|
}
|
|
|
|
// If the parsed object contains an items array, store it
|
|
if (
|
|
parsed &&
|
|
typeof parsed === "object" &&
|
|
"items" in parsed &&
|
|
Array.isArray(parsed.items)
|
|
) {
|
|
const from = String(
|
|
parsed.from || parsed.source || parsed.client || parsed.id || clientId,
|
|
);
|
|
const id = parsed.id || clientId;
|
|
const ts = Number(parsed.ts || parsed.time || Date.now()) || Date.now();
|
|
const items = normalizeItems(parsed.items);
|
|
|
|
latestItems[id] = {
|
|
from,
|
|
ts,
|
|
items,
|
|
clientId,
|
|
};
|
|
|
|
console.log(`Stored items from '${from}' (${items.length} entries)`);
|
|
}
|
|
|
|
// Broadcast to other clients (keep original behaviour: broadcast as { type: 'message', ... })
|
|
var out = {};
|
|
if (parsed && parsed.type === "request") {
|
|
out = {
|
|
type: parsed.type,
|
|
items: parsed.items || [],
|
|
address: parsed.address || "",
|
|
to: parsed.to || "",
|
|
};
|
|
} else if (parsed) {
|
|
out = {
|
|
type: parsed.type || "message",
|
|
from:
|
|
parsed && parsed.from
|
|
? parsed.from
|
|
: parsed && parsed.id
|
|
? parsed.id
|
|
: clientId,
|
|
text:
|
|
(parsed && parsed.message) ||
|
|
(typeof parsed === "string" ? parsed : text),
|
|
raw: parsed || text,
|
|
ts: Date.now(),
|
|
};
|
|
}
|
|
if (parsed && parsed.type && parsed.type !== "items") {
|
|
wss.clients.forEach((client) => {
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
try {
|
|
client.send(JSON.stringify(out));
|
|
} catch (err) {
|
|
// Ignore send errors for now
|
|
console.warn(
|
|
`Failed to send message to client ${client._clientId}: ${err.message}`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
ws.on("close", () => {
|
|
console.log("Client disconnected:", clientId);
|
|
// delete stored items on disconnect
|
|
for (const shopId in latestItems) {
|
|
const shop = latestItems[shopId];
|
|
if (shop.clientId === clientId) {
|
|
delete latestItems[shopId];
|
|
}
|
|
}
|
|
});
|
|
|
|
ws.on("error", (err) => {
|
|
console.log("WebSocket error from", clientId, err && err.message);
|
|
});
|
|
});
|
|
|
|
// API endpoint that returns the latest items as JSON
|
|
app.get("/api/items", (req, res) => {
|
|
res.set("Access-Control-Allow-Origin", "*");
|
|
// Return the whole latestItems object with metadata
|
|
res.json({
|
|
ok: true,
|
|
ts: Date.now(),
|
|
itemCount: Object.values(latestItems).reduce(
|
|
(sum, item) => sum + item.items.length,
|
|
0,
|
|
),
|
|
data: latestItems,
|
|
});
|
|
});
|
|
|
|
app.get("/api/item/:id", (req, res) => {
|
|
const itemName = req.params.id;
|
|
res.set("Access-Control-Allow-Origin", "*");
|
|
|
|
for (const shopId in latestItems) {
|
|
const shop = latestItems[shopId];
|
|
const item = shop.items.find((i) => i.name === itemName);
|
|
|
|
if (item) {
|
|
return res.json({
|
|
ok: true,
|
|
shopId,
|
|
item,
|
|
source: shop.from,
|
|
timestamp: shop.ts,
|
|
});
|
|
}
|
|
}
|
|
|
|
res.status(404).json({
|
|
ok: false,
|
|
error: "Item not found",
|
|
});
|
|
});
|
|
|
|
app.post("/api/shop/items/buy", (req, res) => {
|
|
const { shopId, address, items } = req.body;
|
|
|
|
// Basic validation
|
|
if (
|
|
typeof shopId !== "number" ||
|
|
typeof address !== "string" ||
|
|
!Array.isArray(items)
|
|
) {
|
|
return res.status(400).json({
|
|
ok: false,
|
|
error: "Invalid parameters",
|
|
});
|
|
}
|
|
|
|
for (const item of items) {
|
|
if (typeof item.id !== "string" || typeof item.quantity !== "number") {
|
|
return res.status(400).json({
|
|
ok: false,
|
|
error: "Invalid item format",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Build request payload
|
|
const request = {
|
|
type: "request",
|
|
to: shopId,
|
|
address,
|
|
items: items.map((item) => ({
|
|
name: item.id,
|
|
_requestCount: item.quantity,
|
|
})),
|
|
};
|
|
|
|
// Send via WebSocket
|
|
wss.clients.forEach((client) => {
|
|
if (client.readyState === 1) {
|
|
client.send(JSON.stringify(request));
|
|
}
|
|
});
|
|
|
|
return res.status(200).json({
|
|
ok: true,
|
|
message: "Items sent successfully",
|
|
data: request,
|
|
});
|
|
});
|
|
|
|
// Serve the items UI page from public/items.html for the /items route.
|
|
// The actual HTML/JS for the page lives in public/items.html (moved from server-side rendering).
|
|
app.get("/", (req, res) => {
|
|
res.sendFile(path.join(__dirname, "public", "index.html"));
|
|
});
|
|
app.get("/items", (req, res) => {
|
|
res.sendFile(path.join(__dirname, "public", "items.html"));
|
|
});
|
|
app.get("/msg", (req, res) => {
|
|
res.sendFile(path.join(__dirname, "public", "msg.html"));
|
|
});
|
|
app.get("/register", (req, res) => {
|
|
res.sendFile(path.join(__dirname, "public", "register.html"));
|
|
});
|
|
app.get("/ws-test", (req, res) => {
|
|
res.sendFile(path.join(__dirname, "public", "ws-test.html"));
|
|
});
|
|
app.get("/admin", (req, res) => {
|
|
res.sendFile(path.join(__dirname, "public", "admin.html"));
|
|
});
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`🚀 Server listening on http://localhost:${PORT}`);
|
|
console.log(`🌐 Web UI: http://localhost:${PORT}/`);
|
|
console.log(`📄 Items page: http://localhost:${PORT}/items`);
|
|
console.log(`📦 Items API: http://localhost:${PORT}/api/items`);
|
|
});
|