instbyte 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/client/css/app.css +943 -443
- package/client/index.html +11 -4
- package/client/js/app.js +569 -168
- package/package.json +1 -1
- package/server/cleanup.js +3 -4
- package/server/config.js +1 -0
- package/server/db.js +3 -0
- package/server/server.js +120 -5
package/package.json
CHANGED
package/server/cleanup.js
CHANGED
|
@@ -3,13 +3,12 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const config = require("./config");
|
|
5
5
|
|
|
6
|
-
const DAY = config.storage.retention;
|
|
7
|
-
|
|
8
6
|
setInterval(() => {
|
|
9
|
-
|
|
7
|
+
if (config.storage.retention === null) return;
|
|
8
|
+
const cutoff = Date.now() - config.storage.retention;
|
|
10
9
|
|
|
11
10
|
db.all(
|
|
12
|
-
`SELECT * FROM items WHERE created_at <
|
|
11
|
+
`SELECT * FROM items WHERE created_at < ? AND pinned = 0`,
|
|
13
12
|
[cutoff],
|
|
14
13
|
(err, rows) => {
|
|
15
14
|
rows.forEach((item) => {
|
package/server/config.js
CHANGED
|
@@ -30,6 +30,7 @@ function parseFileSize(val) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function parseRetention(val) {
|
|
33
|
+
if (String(val).toLowerCase() === "never") return null;
|
|
33
34
|
if (typeof val === "number") return val;
|
|
34
35
|
const units = { h: 3600000, d: 86400000 };
|
|
35
36
|
const match = String(val).match(/^(\d+)(h|d)$/i);
|
package/server/db.js
CHANGED
|
@@ -36,6 +36,9 @@ db.serialize(() => {
|
|
|
36
36
|
db.run(`ALTER TABLE items ADD COLUMN size INTEGER DEFAULT 0`, () => { });
|
|
37
37
|
db.run(`ALTER TABLE channels ADD COLUMN pinned INTEGER DEFAULT 0`, () => { });
|
|
38
38
|
|
|
39
|
+
db.run(`ALTER TABLE items ADD COLUMN title TEXT DEFAULT ''`, () => { });
|
|
40
|
+
db.run(`ALTER TABLE items ADD COLUMN edited_at INTEGER DEFAULT NULL`, () => { });
|
|
41
|
+
|
|
39
42
|
// Insert default channels if empty
|
|
40
43
|
db.get("SELECT COUNT(*) as count FROM channels", (err, row) => {
|
|
41
44
|
if (err) {
|
package/server/server.js
CHANGED
|
@@ -366,16 +366,47 @@ app.post("/pin/:id", (req, res) => {
|
|
|
366
366
|
/* GET ITEMS */
|
|
367
367
|
app.get("/items/:channel", (req, res) => {
|
|
368
368
|
const channel = req.params.channel;
|
|
369
|
+
const page = parseInt(req.query.page) || 1;
|
|
370
|
+
const limit = 10;
|
|
371
|
+
const offset = (page - 1) * limit;
|
|
369
372
|
|
|
370
|
-
db.
|
|
371
|
-
`SELECT * FROM items WHERE channel=?
|
|
373
|
+
db.get(
|
|
374
|
+
`SELECT COUNT(*) as count FROM items WHERE channel=? AND pinned=0`,
|
|
372
375
|
[channel],
|
|
373
|
-
(err,
|
|
374
|
-
res.json(
|
|
376
|
+
(err, row) => {
|
|
377
|
+
if (err) return res.status(500).json({ error: "DB error" });
|
|
378
|
+
|
|
379
|
+
const totalUnpinned = row.count;
|
|
380
|
+
const hasMore = offset + limit < totalUnpinned;
|
|
381
|
+
|
|
382
|
+
db.all(
|
|
383
|
+
`SELECT * FROM items WHERE channel=? AND pinned=0
|
|
384
|
+
ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
|
385
|
+
[channel, limit, offset],
|
|
386
|
+
(err, unpinned) => {
|
|
387
|
+
if (err) return res.status(500).json({ error: "DB error" });
|
|
388
|
+
|
|
389
|
+
if (page === 1) {
|
|
390
|
+
// only fetch pinned on first page
|
|
391
|
+
db.all(
|
|
392
|
+
`SELECT * FROM items WHERE channel=? AND pinned=1
|
|
393
|
+
ORDER BY created_at DESC`,
|
|
394
|
+
[channel],
|
|
395
|
+
(err, pinned) => {
|
|
396
|
+
if (err) return res.status(500).json({ error: "DB error" });
|
|
397
|
+
res.json({ items: [...pinned, ...unpinned], hasMore, page });
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
} else {
|
|
401
|
+
res.json({ items: unpinned, hasMore, page });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
);
|
|
375
405
|
}
|
|
376
406
|
);
|
|
377
407
|
});
|
|
378
408
|
|
|
409
|
+
|
|
379
410
|
/* SEARCH */
|
|
380
411
|
app.get("/search/:channel/:q", (req, res) => {
|
|
381
412
|
const { channel, q } = req.params;
|
|
@@ -494,6 +525,45 @@ app.patch("/item/:id/move", (req, res) => {
|
|
|
494
525
|
});
|
|
495
526
|
});
|
|
496
527
|
|
|
528
|
+
/* UPDATE ITEM TITLE */
|
|
529
|
+
app.patch("/item/:id/title", (req, res) => {
|
|
530
|
+
const { id } = req.params;
|
|
531
|
+
const { title } = req.body;
|
|
532
|
+
|
|
533
|
+
if (title === undefined) return res.status(400).json({ error: "Title required" });
|
|
534
|
+
|
|
535
|
+
db.run(
|
|
536
|
+
"UPDATE items SET title=? WHERE id=?",
|
|
537
|
+
[title.trim(), id],
|
|
538
|
+
function (err) {
|
|
539
|
+
if (err) return res.status(500).json({ error: "Update failed" });
|
|
540
|
+
if (this.changes === 0) return res.status(404).json({ error: "Item not found" });
|
|
541
|
+
io.emit("item-updated", { id: parseInt(id), title: title.trim() });
|
|
542
|
+
res.json({ id, title: title.trim() });
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
/* UPDATE ITEM CONTENT */
|
|
548
|
+
app.patch("/item/:id/content", (req, res) => {
|
|
549
|
+
const { id } = req.params;
|
|
550
|
+
const { content } = req.body;
|
|
551
|
+
|
|
552
|
+
if (content === undefined) return res.status(400).json({ error: "Content required" });
|
|
553
|
+
if (content.trim() === "") return res.status(400).json({ error: "Content cannot be empty" });
|
|
554
|
+
|
|
555
|
+
db.run(
|
|
556
|
+
"UPDATE items SET content=?, edited_at=? WHERE id=? AND type='text'",
|
|
557
|
+
[content.trim(), Date.now(), id],
|
|
558
|
+
function (err) {
|
|
559
|
+
if (err) return res.status(500).json({ error: "Update failed" });
|
|
560
|
+
if (this.changes === 0) return res.status(404).json({ error: "Item not found or not editable" });
|
|
561
|
+
io.emit("item-updated", { id: parseInt(id), content: content.trim(), edited_at: Date.now() });
|
|
562
|
+
res.json({ id, content: content.trim() });
|
|
563
|
+
}
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
497
567
|
/* RENAME CHANNEL */
|
|
498
568
|
app.patch("/channels/:name", (req, res) => {
|
|
499
569
|
const oldName = req.params.name;
|
|
@@ -542,7 +612,8 @@ app.post("/channels/:name/pin", (req, res) => {
|
|
|
542
612
|
app.get("/info", (req, res) => {
|
|
543
613
|
res.json({
|
|
544
614
|
url: `http://${localIP}:${PORT}`,
|
|
545
|
-
hasAuth: !!config.auth.passphrase
|
|
615
|
+
hasAuth: !!config.auth.passphrase,
|
|
616
|
+
retention: config.storage.retention // null means "never"
|
|
546
617
|
});
|
|
547
618
|
});
|
|
548
619
|
|
|
@@ -619,11 +690,15 @@ app.get("/logo-dynamic.png", (req, res) => {
|
|
|
619
690
|
/* ============================
|
|
620
691
|
SOCKET CONNECTION LOGGING
|
|
621
692
|
============================ */
|
|
693
|
+
// in-memory seen tracking — item id → Set of socket ids
|
|
694
|
+
// resets on server restart, no DB needed
|
|
695
|
+
const seenBy = new Map();
|
|
622
696
|
|
|
623
697
|
let connectedUsers = 0;
|
|
624
698
|
|
|
625
699
|
io.on("connection", (socket) => {
|
|
626
700
|
connectedUsers++;
|
|
701
|
+
io.emit("user-count", connectedUsers);
|
|
627
702
|
|
|
628
703
|
let username = "Unknown";
|
|
629
704
|
|
|
@@ -632,9 +707,19 @@ io.on("connection", (socket) => {
|
|
|
632
707
|
console.log(username + " connected | total:", connectedUsers);
|
|
633
708
|
});
|
|
634
709
|
|
|
710
|
+
socket.on("seen", ({ id, name }) => {
|
|
711
|
+
if (!id || !name) return;
|
|
712
|
+
if (!seenBy.has(id)) seenBy.set(id, new Set());
|
|
713
|
+
seenBy.get(id).add(name); // name instead of socket.id
|
|
714
|
+
const count = seenBy.get(id).size;
|
|
715
|
+
console.log(`seen: item ${id} | count: ${count}`);
|
|
716
|
+
io.emit("seen-update", { id, count });
|
|
717
|
+
});
|
|
718
|
+
|
|
635
719
|
socket.on("disconnect", () => {
|
|
636
720
|
connectedUsers--;
|
|
637
721
|
console.log(username + " disconnected | total:", connectedUsers);
|
|
722
|
+
io.emit("user-count", connectedUsers);
|
|
638
723
|
});
|
|
639
724
|
});
|
|
640
725
|
|
|
@@ -696,3 +781,33 @@ findFreePort(PREFERRED).then(p => {
|
|
|
696
781
|
console.log("");
|
|
697
782
|
});
|
|
698
783
|
});
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
// ========================
|
|
787
|
+
// GRACEFUL SHUTDOWN
|
|
788
|
+
// ========================
|
|
789
|
+
function shutdown(signal) {
|
|
790
|
+
console.log(`\n${signal} received — shutting down gracefully...`);
|
|
791
|
+
|
|
792
|
+
// stop accepting new connections
|
|
793
|
+
server.close(() => {
|
|
794
|
+
console.log("HTTP server closed");
|
|
795
|
+
|
|
796
|
+
// close database connection
|
|
797
|
+
db.close((err) => {
|
|
798
|
+
if (err) console.error("Error closing database:", err);
|
|
799
|
+
else console.log("Database connection closed");
|
|
800
|
+
console.log("Shutdown complete");
|
|
801
|
+
process.exit(0);
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// force exit after 10 seconds if something hangs
|
|
806
|
+
setTimeout(() => {
|
|
807
|
+
console.error("Forced shutdown after timeout");
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}, 10000);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
813
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|