mirror of
https://github.com/lklynet/hypermind.git
synced 2026-05-04 01:50:33 +00:00
Merge branch 'main' into pr-52
This commit is contained in:
+39
-1
@@ -18,11 +18,43 @@ const PEER_TIMEOUT = parseInt(process.env.PEER_TIMEOUT) || 45000;
|
||||
const BROADCAST_THROTTLE = 1000;
|
||||
const DIAGNOSTICS_INTERVAL = 10000;
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ENABLE_CHAT = process.env.ENABLE_CHAT === "true";
|
||||
const ENABLE_MAP = process.env.ENABLE_MAP === "true";
|
||||
const ENABLE_THEMES = process.env.ENABLE_THEMES !== "false";
|
||||
const CHAT_RATE_LIMIT = parseInt(process.env.CHAT_RATE_LIMIT) || 5000;
|
||||
const VISUAL_LIMIT = parseInt(process.env.VISUAL_LIMIT) || 500;
|
||||
const CHAT_RATE_LIMIT = parseInt(process.env.CHAT_RATE_LIMIT) || 5000;
|
||||
|
||||
const HTML_TEMPLATE = fs.readFileSync(
|
||||
path.join(__dirname, "../../public/index.html"),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
const ADJECTIVES = fs.readFileSync(
|
||||
path.join(__dirname, "../utils/adjectives.json"),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
const NOUNS = fs.readFileSync(
|
||||
path.join(__dirname, "../utils/nouns.json"),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
const GENERATOR_LOGIC = fs.readFileSync(
|
||||
path.join(__dirname, "../utils/name-generator.js"),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
const packageJson = require("../../package.json");
|
||||
const repoUrl = packageJson.repository?.url || "";
|
||||
const repoMatch = repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
||||
const GITHUB_REPO = repoMatch
|
||||
? { owner: repoMatch[1], name: repoMatch[2] }
|
||||
: { owner: "lklynet|test", name: "hypermind|test" };
|
||||
|
||||
const VERSION = packageJson.version || "no-version";
|
||||
|
||||
module.exports = {
|
||||
TOPIC_NAME,
|
||||
@@ -44,4 +76,10 @@ module.exports = {
|
||||
ENABLE_THEMES,
|
||||
CHAT_RATE_LIMIT,
|
||||
VISUAL_LIMIT,
|
||||
HTML_TEMPLATE,
|
||||
ADJECTIVES,
|
||||
NOUNS,
|
||||
GENERATOR_LOGIC,
|
||||
GITHUB_REPO,
|
||||
VERSION,
|
||||
};
|
||||
|
||||
+48
-152
@@ -1,33 +1,16 @@
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const { signMessage } = require("../core/security");
|
||||
const {
|
||||
ENABLE_CHAT,
|
||||
ENABLE_MAP,
|
||||
ENABLE_THEMES,
|
||||
CHAT_RATE_LIMIT,
|
||||
VISUAL_LIMIT,
|
||||
} = require("../config/constants");
|
||||
|
||||
const HTML_TEMPLATE = fs.readFileSync(
|
||||
path.join(__dirname, "../../public/index.html"),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
const adjectives = fs.readFileSync(
|
||||
path.join(__dirname, "../utils/adjectives.json"),
|
||||
"utf-8"
|
||||
);
|
||||
const nouns = fs.readFileSync(
|
||||
path.join(__dirname, "../utils/nouns.json"),
|
||||
"utf-8"
|
||||
);
|
||||
const generatorLogic = fs.readFileSync(
|
||||
path.join(__dirname, "../utils/name-generator.js"),
|
||||
"utf-8"
|
||||
);
|
||||
/**
|
||||
* Routes should always be imported here and created in the ./routes folder.
|
||||
* Please let's all make sure we follow coding standards so things don't get too messy.
|
||||
*/
|
||||
const { setupStatsRoutes } = require("./routes/stats");
|
||||
const { setupChatRoutes } = require("./routes/chat");
|
||||
const { setupGitHubRoutes } = require("./routes/github");
|
||||
const { setupUtilityRoutes } = require("./routes/utility");
|
||||
const { setupPageRoutes } = require("./routes/page");
|
||||
const { setupSSERoutes } = require("./routes/sse");
|
||||
|
||||
const setupRoutes = (
|
||||
app,
|
||||
@@ -39,137 +22,50 @@ const setupRoutes = (
|
||||
) => {
|
||||
app.use(express.json());
|
||||
|
||||
app.get("/js/lists.js", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/javascript");
|
||||
res.send(`window.ADJECTIVES = ${adjectives}; window.NOUNS = ${nouns};`);
|
||||
});
|
||||
const utilityDeps = {
|
||||
adjectives: require("../config/constants").ADJECTIVES,
|
||||
nouns: require("../config/constants").NOUNS,
|
||||
generatorLogic: require("../config/constants").GENERATOR_LOGIC,
|
||||
};
|
||||
|
||||
app.get("/js/screenname.js", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/javascript");
|
||||
const browserLogic = generatorLogic
|
||||
.replace(
|
||||
'const adjectives = require("./adjectives.json");',
|
||||
"const adjectives = window.ADJECTIVES;"
|
||||
)
|
||||
.replace(
|
||||
'const nouns = require("./nouns.json");',
|
||||
"const nouns = window.NOUNS;"
|
||||
)
|
||||
.replace(
|
||||
"module.exports = { generateScreenname };",
|
||||
"window.generateScreenname = generateScreenname;"
|
||||
);
|
||||
res.send(browserLogic);
|
||||
});
|
||||
const pageDeps = {
|
||||
htmlTemplate: require("../config/constants").HTML_TEMPLATE,
|
||||
identity,
|
||||
peerManager,
|
||||
swarm,
|
||||
};
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
const count = peerManager.size;
|
||||
const directPeers = swarm.getSwarm().connections.size;
|
||||
const totalUnique = peerManager.totalUniquePeers;
|
||||
const sseDeps = {
|
||||
identity,
|
||||
peerManager,
|
||||
swarm,
|
||||
sseManager,
|
||||
diagnostics,
|
||||
};
|
||||
|
||||
const html = HTML_TEMPLATE.replace(/\{\{COUNT\}\}/g, count)
|
||||
.replace(/\{\{ID\}\}/g, identity.screenname || "Unknown")
|
||||
.replace(/\{\{FULL_ID\}\}/g, identity.id)
|
||||
.replace(/\{\{DIRECT\}\}/g, directPeers)
|
||||
.replace(/\{\{TOTAL_UNIQUE\}\}/g, totalUnique)
|
||||
.replace(/\{\{MAP_CLASS\}\}/g, ENABLE_MAP ? "" : "hidden")
|
||||
.replace(/\{\{THEMES_CLASS\}\}/g, ENABLE_THEMES ? "" : "hidden")
|
||||
.replace(/\{\{VISUAL_LIMIT\}\}/g, VISUAL_LIMIT);
|
||||
const statsDeps = {
|
||||
identity,
|
||||
peerManager,
|
||||
swarm,
|
||||
diagnostics,
|
||||
};
|
||||
|
||||
res.send(html);
|
||||
});
|
||||
const chatDeps = {
|
||||
identity,
|
||||
swarm,
|
||||
sseManager,
|
||||
};
|
||||
|
||||
app.get("/events", (req, res) => {
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.flushHeaders();
|
||||
const githubDeps = {
|
||||
repo: require("../config/constants").GITHUB_REPO,
|
||||
};
|
||||
|
||||
sseManager.addClient(res);
|
||||
|
||||
const data = JSON.stringify({
|
||||
count: peerManager.size,
|
||||
totalUnique: peerManager.totalUniquePeers,
|
||||
direct: swarm.getSwarm().connections.size,
|
||||
id: identity.id,
|
||||
screenname: identity.screenname,
|
||||
diagnostics: diagnostics.getStats(),
|
||||
chatEnabled: ENABLE_CHAT,
|
||||
peers: peerManager.getPeersWithIps(),
|
||||
});
|
||||
res.write(`data: ${data}\n\n`);
|
||||
|
||||
req.on("close", () => {
|
||||
sseManager.removeClient(res);
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/stats", (req, res) => {
|
||||
res.json({
|
||||
count: peerManager.size,
|
||||
totalUnique: peerManager.totalUniquePeers,
|
||||
direct: swarm.getSwarm().connections.size,
|
||||
id: identity.id,
|
||||
screenname: identity.screenname,
|
||||
diagnostics: diagnostics.getStats(),
|
||||
chatEnabled: ENABLE_CHAT,
|
||||
peers: peerManager.getPeersWithIps(),
|
||||
});
|
||||
});
|
||||
|
||||
let chatHistory = [];
|
||||
|
||||
app.post("/api/chat", (req, res) => {
|
||||
if (!ENABLE_CHAT) {
|
||||
return res.status(403).json({ error: "Chat disabled" });
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
chatHistory = chatHistory.filter((time) => now - time < CHAT_RATE_LIMIT);
|
||||
|
||||
if (chatHistory.length >= 5) {
|
||||
return res.status(429).json({
|
||||
error: `Rate limit exceeded: Max 5 messages per ${
|
||||
CHAT_RATE_LIMIT / 1000
|
||||
} seconds`,
|
||||
});
|
||||
}
|
||||
|
||||
chatHistory.push(now);
|
||||
|
||||
const { content, scope = "GLOBAL", target } = req.body;
|
||||
if (!content || typeof content !== "string" || content.length > 140) {
|
||||
return res.status(400).json({ error: "Invalid content" });
|
||||
}
|
||||
|
||||
if (scope !== "LOCAL" && scope !== "GLOBAL") {
|
||||
return res.status(400).json({ error: "Invalid scope" });
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const idBase = identity.id + content + timestamp;
|
||||
const msgId = crypto.createHash("sha256").update(idBase).digest("hex");
|
||||
|
||||
const msg = {
|
||||
type: "CHAT",
|
||||
id: msgId,
|
||||
sender: identity.id,
|
||||
content: content,
|
||||
timestamp: timestamp,
|
||||
scope: scope,
|
||||
target: target,
|
||||
hops: 0,
|
||||
};
|
||||
|
||||
if (scope === "GLOBAL") {
|
||||
msg.sig = signMessage(`chat:${msgId}`, identity.privateKey);
|
||||
}
|
||||
|
||||
swarm.broadcastChat(msg);
|
||||
sseManager.broadcast(msg);
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
setupUtilityRoutes(app, utilityDeps);
|
||||
setupPageRoutes(app, pageDeps);
|
||||
setupSSERoutes(app, sseDeps);
|
||||
setupStatsRoutes(app, statsDeps);
|
||||
setupChatRoutes(app, chatDeps);
|
||||
setupGitHubRoutes(app, githubDeps);
|
||||
|
||||
app.use(express.static(path.join(__dirname, "../../public")));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const crypto = require("crypto");
|
||||
const { signMessage } = require("../../core/security");
|
||||
const { ENABLE_CHAT, CHAT_RATE_LIMIT } = require("../../config/constants");
|
||||
|
||||
const setupChatRoutes = (router, dependencies) => {
|
||||
const { identity, swarm, sseManager } = dependencies;
|
||||
let chatHistory = [];
|
||||
|
||||
router.post("/api/chat", (req, res) => {
|
||||
if (!ENABLE_CHAT) {
|
||||
return res.status(403).json({ error: "Chat disabled" });
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
chatHistory = chatHistory.filter((time) => now - time < CHAT_RATE_LIMIT);
|
||||
|
||||
if (chatHistory.length >= 5) {
|
||||
return res.status(429).json({
|
||||
error: `Rate limit exceeded: Max 5 messages per ${CHAT_RATE_LIMIT / 1000
|
||||
} seconds`,
|
||||
});
|
||||
}
|
||||
|
||||
chatHistory.push(now);
|
||||
|
||||
const { content, scope = "GLOBAL", target } = req.body;
|
||||
if (!content || typeof content !== "string" || content.length > 140) {
|
||||
return res.status(400).json({ error: "Invalid content" });
|
||||
}
|
||||
|
||||
if (scope !== "LOCAL" && scope !== "GLOBAL") {
|
||||
return res.status(400).json({ error: "Invalid scope" });
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const idBase = identity.id + content + timestamp;
|
||||
const msgId = crypto.createHash("sha256").update(idBase).digest("hex");
|
||||
|
||||
const msg = {
|
||||
type: "CHAT",
|
||||
id: msgId,
|
||||
sender: identity.id,
|
||||
content: content,
|
||||
timestamp: timestamp,
|
||||
scope: scope,
|
||||
target: target,
|
||||
hops: 0,
|
||||
};
|
||||
|
||||
if (scope === "GLOBAL") {
|
||||
msg.sig = signMessage(`chat:${msgId}`, identity.privateKey);
|
||||
}
|
||||
|
||||
swarm.broadcastChat(msg);
|
||||
sseManager.broadcast(msg);
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { setupChatRoutes };
|
||||
@@ -0,0 +1,68 @@
|
||||
const https = require("https");
|
||||
|
||||
const setupGitHubRoutes = (router, dependencies) => {
|
||||
const { repo } = dependencies;
|
||||
|
||||
router.get("/api/github/latest-release", (req, res) => {
|
||||
|
||||
const options = {
|
||||
hostname: "api.github.com",
|
||||
path: `/repos/${repo.owner}/${repo.name}/releases/latest`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"User-Agent": "Hypermind-Version-Checker",
|
||||
},
|
||||
};
|
||||
|
||||
const request = https.request(options, (response) => {
|
||||
let data = "";
|
||||
|
||||
response.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
response.on("end", () => {
|
||||
if (response.statusCode === 404) {
|
||||
return res.json({
|
||||
tag_name: null,
|
||||
html_url: null,
|
||||
published_at: null,
|
||||
body: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
console.error(
|
||||
"Failed to fetch latest GitHub release:",
|
||||
response.statusCode
|
||||
);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Failed to fetch latest release" });
|
||||
}
|
||||
|
||||
try {
|
||||
const release = JSON.parse(data);
|
||||
res.json({
|
||||
tag_name: release.tag_name,
|
||||
html_url: release.html_url,
|
||||
published_at: release.published_at,
|
||||
body: release.body,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error parsing GitHub response:", error);
|
||||
res.status(500).json({ error: "Failed to parse release data" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
request.on("error", (error) => {
|
||||
console.error("Error fetching latest GitHub release:", error);
|
||||
res.status(500).json({ error: "Failed to fetch latest release" });
|
||||
});
|
||||
|
||||
request.end();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { setupGitHubRoutes };
|
||||
@@ -0,0 +1,26 @@
|
||||
const { ENABLE_MAP, ENABLE_THEMES, VISUAL_LIMIT, VERSION } = require("../../config/constants");
|
||||
|
||||
const setupPageRoutes = (router, dependencies) => {
|
||||
const { htmlTemplate, identity, peerManager, swarm } = dependencies;
|
||||
|
||||
router.get("/", (req, res) => {
|
||||
const count = peerManager.size;
|
||||
const directPeers = swarm.getSwarm().connections.size;
|
||||
const totalUnique = peerManager.totalUniquePeers;
|
||||
|
||||
const html = htmlTemplate
|
||||
.replace(/\{\{COUNT\}\}/g, count)
|
||||
.replace(/\{\{ID\}\}/g, identity.screenname || "Unknown")
|
||||
.replace(/\{\{FULL_ID\}\}/g, identity.id)
|
||||
.replace(/\{\{DIRECT\}\}/g, directPeers)
|
||||
.replace(/\{\{TOTAL_UNIQUE\}\}/g, totalUnique)
|
||||
.replace(/\{\{MAP_CLASS\}\}/g, ENABLE_MAP ? "" : "hidden")
|
||||
.replace(/\{\{THEMES_CLASS\}\}/g, ENABLE_THEMES ? "" : "hidden")
|
||||
.replace(/\{\{VISUAL_LIMIT\}\}/g, VISUAL_LIMIT)
|
||||
.replace(/\{\{VERSION\}\}/g, VERSION);
|
||||
|
||||
res.send(html);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { setupPageRoutes };
|
||||
@@ -0,0 +1,33 @@
|
||||
const { ENABLE_CHAT, ENABLE_MAP } = require("../../config/constants");
|
||||
|
||||
const setupSSERoutes = (router, dependencies) => {
|
||||
const { identity, peerManager, swarm, sseManager, diagnostics } = dependencies;
|
||||
|
||||
router.get("/events", (req, res) => {
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.flushHeaders();
|
||||
|
||||
sseManager.addClient(res);
|
||||
|
||||
const data = JSON.stringify({
|
||||
count: peerManager.size,
|
||||
totalUnique: peerManager.totalUniquePeers,
|
||||
direct: swarm.getSwarm().connections.size,
|
||||
id: identity.id,
|
||||
screenname: identity.screenname,
|
||||
diagnostics: diagnostics.getStats(),
|
||||
chatEnabled: ENABLE_CHAT,
|
||||
mapEnabled: ENABLE_MAP,
|
||||
peers: peerManager.getPeersWithIps(),
|
||||
});
|
||||
res.write(`data: ${data}\n\n`);
|
||||
|
||||
req.on("close", () => {
|
||||
sseManager.removeClient(res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { setupSSERoutes };
|
||||
@@ -0,0 +1,20 @@
|
||||
const { ENABLE_CHAT } = require("../../config/constants");
|
||||
|
||||
const setupStatsRoutes = (router, dependencies) => {
|
||||
const { peerManager, swarm, diagnostics } = dependencies;
|
||||
|
||||
router.get("/api/stats", (req, res) => {
|
||||
res.json({
|
||||
count: peerManager.size,
|
||||
totalUnique: peerManager.totalUniquePeers,
|
||||
direct: swarm.getSwarm().connections.size,
|
||||
id: dependencies.identity.id,
|
||||
screenname: dependencies.identity.screenname,
|
||||
diagnostics: diagnostics.getStats(),
|
||||
chatEnabled: ENABLE_CHAT,
|
||||
peers: peerManager.getPeersWithIps(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { setupStatsRoutes };
|
||||
@@ -0,0 +1,28 @@
|
||||
const setupUtilityRoutes = (router, dependencies) => {
|
||||
const { adjectives, nouns, generatorLogic } = dependencies;
|
||||
|
||||
router.get("/js/lists.js", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/javascript");
|
||||
res.send(`window.ADJECTIVES = ${adjectives}; window.NOUNS = ${nouns};`);
|
||||
});
|
||||
|
||||
router.get("/js/screenname.js", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/javascript");
|
||||
const browserLogic = generatorLogic
|
||||
.replace(
|
||||
'const adjectives = require("./adjectives.json");',
|
||||
"const adjectives = window.ADJECTIVES;"
|
||||
)
|
||||
.replace(
|
||||
'const nouns = require("./nouns.json");',
|
||||
"const nouns = window.NOUNS;"
|
||||
)
|
||||
.replace(
|
||||
"module.exports = { generateScreenname };",
|
||||
"window.generateScreenname = generateScreenname;"
|
||||
);
|
||||
res.send(browserLogic);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { setupUtilityRoutes };
|
||||
Reference in New Issue
Block a user