mirror of
https://github.com/lklynet/hypermind.git
synced 2026-08-23 23:06:29 +00:00
Merge branch 'main' into pr-52
This commit is contained in:
@@ -181,6 +181,22 @@ Add this to your `services.yaml`:
|
||||
|
||||
To get the icon to work, you have to add the icon to `/app/public/icons`. See detailed [instructions](https://gethomepage.dev/configs/services/#icons).
|
||||
|
||||
|
||||
### API Documentation
|
||||
|
||||
For developers who are not satisfied in just looking a number on the screen but are craving to add new features, a comprehensive development API documentation is available in [`devdocs/API.md`](devdocs/API.md).
|
||||
|
||||
**Available Endpoints:**
|
||||
- `GET /api/stats` - Current node statistics
|
||||
- `POST /api/chat` - Send P2P chat messages
|
||||
- `GET /api/github/latest-release` - Latest release information
|
||||
- `GET /events` - Server-Sent Events stream for real-time updates
|
||||
- `GET /js/lists.js` - Dynamic adjectives/nouns for screenname generation
|
||||
- `GET /js/screenname.js` - Screenname generation logic
|
||||
- `GET /` - Main HTML page
|
||||
|
||||
The documentation includes security best practices, rate limiting guidelines, modular route structure, and examples for creating new endpoints.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
# API Documentation
|
||||
|
||||
HTTP endpoints for Hypermind integration and development.
|
||||
|
||||
## Endpoints
|
||||
|
||||
You can test all the available endpoints running: `nude test-api.js`.
|
||||
|
||||
<details>
|
||||
<summary><code>GET /api/stats</code></summary>
|
||||
|
||||
Returns node statistics and swarm information.
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 42,
|
||||
"totalUnique": 1337,
|
||||
"direct": 8,
|
||||
"id": "abc123...",
|
||||
"screenname": "BraveElephant",
|
||||
"diagnostics": {...},
|
||||
"chatEnabled": true,
|
||||
"mapEnabled": true,
|
||||
"peers": [...]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>POST /api/chat</code></summary>
|
||||
|
||||
Send P2P chat message. Rate limited: 5 messages per 5 seconds.
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "Hello, world!",
|
||||
"scope": "GLOBAL",
|
||||
"target": null
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>GET /api/github/latest-release</code></summary>
|
||||
|
||||
Latest GitHub release information.
|
||||
|
||||
```json
|
||||
{
|
||||
"tag_name": "v1.2.3",
|
||||
"html_url": "https://github.com/lklynet/hypermind/releases/tag/v1.2.3",
|
||||
"published_at": "2026-01-08T12:00:00Z",
|
||||
"body": "Release notes..."
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>GET /events</code></summary>
|
||||
|
||||
Server-Sent Events stream for real-time updates. Returns same data as `/api/stats`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>GET /js/lists.js</code></summary>
|
||||
|
||||
Dynamic JavaScript file containing adjectives and nouns for screenname generation.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>GET /js/screenname.js</code></summary>
|
||||
|
||||
Dynamic JavaScript file with screenname generation logic for browser.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>GET /</code></summary>
|
||||
|
||||
Main HTML page with server-side template rendering.
|
||||
|
||||
</details>
|
||||
|
||||
## Development
|
||||
|
||||
### Route Structure
|
||||
|
||||
Routes are modular in `src/web/routes/`:
|
||||
|
||||
```
|
||||
src/web/routes/
|
||||
├── your-new-route.js
|
||||
```
|
||||
|
||||
### Creating Routes
|
||||
|
||||
<details>
|
||||
<summary>1. Create module</summary>
|
||||
|
||||
`src/web/routes/your-new-route.js`:
|
||||
|
||||
```javascript
|
||||
const setupYourNewRouteRoutes = (router, dependencies) => {
|
||||
const { identity, peerManager } = dependencies;
|
||||
|
||||
router.get("/api/your-new-route", (req, res) => {
|
||||
res.json({ success: true });
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { setupYourNewRouteRoutes };
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>2. Register route</summary>
|
||||
|
||||
In `src/web/routes.js`:
|
||||
|
||||
```javascript
|
||||
const { setupYourNewRouteRoutes } = require("./routes/your-new-route");
|
||||
|
||||
const yourNewRouteDeps = { identity, peerManager };
|
||||
setupYourNewRouteRoutes(app, yourNewRouteDeps);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>3. Add constants</summary>
|
||||
|
||||
In `src/config/constants.js`:
|
||||
|
||||
```javascript
|
||||
const YOUR_CONFIG = {
|
||||
enabled: process.env.ENABLE_YOUR_FEATURE === "true",
|
||||
rateLimit: parseInt(process.env.YOUR_RATE_LIMIT) || 1000,
|
||||
};
|
||||
|
||||
module.exports = { YOUR_CONFIG };
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Security
|
||||
|
||||
<details>
|
||||
<summary>Rate limiting</summary>
|
||||
|
||||
```javascript
|
||||
let requestHistory = [];
|
||||
|
||||
router.get("/api/your-new-route", (req, res) => {
|
||||
const now = Date.now();
|
||||
requestHistory = requestHistory.filter((time) => now - time < 10000);
|
||||
|
||||
if (requestHistory.length >= 5) {
|
||||
return res.status(429).json({ error: "Rate limit exceeded" });
|
||||
}
|
||||
|
||||
requestHistory.push(now);
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Input validation</summary>
|
||||
|
||||
```javascript
|
||||
router.post("/api/your-new-route", (req, res) => {
|
||||
const { data } = req.body;
|
||||
|
||||
if (!data || typeof data !== "string" || data.length > 1000) {
|
||||
return res.status(400).json({ error: "Invalid data" });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Error handling</summary>
|
||||
|
||||
```javascript
|
||||
router.get("/api/your-new-route", async (req, res) => {
|
||||
try {
|
||||
const result = await someOperation();
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
res.status(500).json({ error: "Operation failed" });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/your-new-route
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# Chat Commands
|
||||
|
||||
Chat commands are modular and located in `public/js/chat-commands/`.
|
||||
|
||||
## Adding a Command
|
||||
|
||||
1. Create `chat-commands/mycommand.js`:
|
||||
```javascript
|
||||
export const myCommand = {
|
||||
description: "What it does",
|
||||
execute: () => {
|
||||
const output = document.getElementById("terminal-output");
|
||||
// implementation
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
2. Import and register in `commands.js`:
|
||||
```javascript
|
||||
import { myCommand } from "./chat-commands/mycommand.js";
|
||||
|
||||
const actions = {
|
||||
// ... existing commands
|
||||
"/mycommand": myCommand,
|
||||
};
|
||||
+20
-6
@@ -479,7 +479,7 @@ const appendMessage = (msg) => {
|
||||
senderName = msg.sender.slice(-8);
|
||||
}
|
||||
|
||||
// Update name map
|
||||
// Update name map (before changing senderName to "You")
|
||||
nameToId.set(senderName, msg.sender);
|
||||
|
||||
if (msg.sender === myId) senderName = "You";
|
||||
@@ -504,7 +504,7 @@ const appendMessage = (msg) => {
|
||||
|
||||
const contentSpan = document.createElement("span");
|
||||
contentSpan.className = "msg-content";
|
||||
// Use formatMessage for rich text rendering
|
||||
|
||||
const rawContent = ` > ${msg.content}`;
|
||||
if (window.ChatCommands) {
|
||||
contentSpan.innerHTML = window.ChatCommands.formatMessage(rawContent);
|
||||
@@ -536,13 +536,24 @@ terminalInput.addEventListener("keypress", async (e) => {
|
||||
if (window.ChatCommands) {
|
||||
const result = window.ChatCommands.processInput(content);
|
||||
if (result.type === "action") {
|
||||
window.ChatCommands.actions[result.command].execute();
|
||||
const action = window.ChatCommands.actions[result.command];
|
||||
if (action && typeof action.execute === "function") {
|
||||
try {
|
||||
action.execute();
|
||||
} catch (err) {
|
||||
console.error("Command execution error:", err);
|
||||
systemStatusBar.innerText = `[SYSTEM] Command failed: ${result.command}`;
|
||||
}
|
||||
} else {
|
||||
systemStatusBar.innerText = `[SYSTEM] Unknown command: ${result.command}`;
|
||||
}
|
||||
return;
|
||||
} else if (result.type === "text") {
|
||||
content = result.content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let scope = "GLOBAL";
|
||||
let target = null;
|
||||
|
||||
@@ -607,10 +618,11 @@ terminalInput.addEventListener("keypress", async (e) => {
|
||||
target = nameToId.get(potentialName);
|
||||
content = msg;
|
||||
scope = "GLOBAL";
|
||||
} else if (!msg) {
|
||||
systemStatusBar.innerText = `[SYSTEM] Usage: /${potentialName} <message>`;
|
||||
return;
|
||||
} else {
|
||||
// If it looks like a command but we don't recognize the user or command
|
||||
// Prevent sending it as raw text to chat
|
||||
systemStatusBar.innerText = `[SYSTEM] Unknown command or user: ${potentialName}`;
|
||||
systemStatusBar.innerText = `[SYSTEM] Unknown user: ${potentialName}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -818,4 +830,6 @@ function cycleTheme() {
|
||||
}
|
||||
}
|
||||
|
||||
window.cycleTheme = cycleTheme;
|
||||
|
||||
document.getElementById("theme-switcher").addEventListener("click", cycleTheme);
|
||||
|
||||
+129
-145
@@ -1,81 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hypermind</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=palette"
|
||||
/>
|
||||
<script>
|
||||
(function () {
|
||||
var savedTheme =
|
||||
localStorage.getItem("hypermind-theme") || "default.css";
|
||||
document.write(
|
||||
'<link rel="stylesheet" href="/themes/' +
|
||||
savedTheme +
|
||||
'" id="theme-css">'
|
||||
);
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="/js/lists.js"></script>
|
||||
<script src="/js/screenname.js"></script>
|
||||
<script src="/js/commands.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="network" data-visual-limit="{{VISUAL_LIMIT}}"></canvas>
|
||||
<div class="container">
|
||||
<div id="count" class="count" data-initial-count="{{COUNT}}">
|
||||
{{COUNT}}
|
||||
</div>
|
||||
<div class="label">Active Nodes</div>
|
||||
<div class="footer">
|
||||
powered by
|
||||
<a
|
||||
href="https://github.com/lklynet/hypermind"
|
||||
target="_blank"
|
||||
class="footer-link"
|
||||
>hypermind</a
|
||||
>
|
||||
</div>
|
||||
<div class="debug">
|
||||
ID: <span id="my-screenname">{{ID}}</span><br />
|
||||
Direct Connections: <span id="direct">{{DIRECT}}</span><br />
|
||||
Total Unique: <span id="total-unique">{{TOTAL_UNIQUE}}</span><br />
|
||||
<span class="debug-link" onclick="openDiagnostics()">diagnostics</span>
|
||||
<span id="map-container" class="{{MAP_CLASS}}">
|
||||
|
|
||||
<span class="debug-link" id="map-link" onclick="openMap()"
|
||||
>map</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mapModal" class="modal">
|
||||
<div class="modal-content map-content">
|
||||
<button class="close-btn" onclick="closeMap()">×</button>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
<head>
|
||||
<title>Hypermind</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=palette" />
|
||||
<script>
|
||||
(function () {
|
||||
var savedTheme =
|
||||
localStorage.getItem("hypermind-theme") || "default.css";
|
||||
document.write(
|
||||
'<link rel="stylesheet" href="/themes/' +
|
||||
savedTheme +
|
||||
'" id="theme-css">'
|
||||
);
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="/js/lists.js"></script>
|
||||
<script src="/js/screenname.js"></script>
|
||||
<script type="module" src="/js/commands.js"></script>
|
||||
<script src="/js/version-checker.js"></script>
|
||||
</head>
|
||||
|
||||
<div id="diagnosticsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<button class="close-btn" onclick="closeDiagnostics()">×</button>
|
||||
<div class="modal-title">Diagnostics</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">ID</span>
|
||||
<span
|
||||
class="stat-value"
|
||||
id="diag-id"
|
||||
style="
|
||||
<body data-version="{{VERSION}}">
|
||||
<div id="version-notification" class="version-notification">
|
||||
<div class="version-content">
|
||||
<span class="version-text">New version available!</span>
|
||||
<a href="#" target="_blank" class="update-link">View Release</a>
|
||||
<button class="dismiss-btn" title="Dismiss">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="network" data-visual-limit="{{VISUAL_LIMIT}}"></canvas>
|
||||
<div class="container">
|
||||
<div id="count" class="count" data-initial-count="{{COUNT}}">
|
||||
{{COUNT}}
|
||||
</div>
|
||||
<div class="label">Active Nodes</div>
|
||||
<div class="footer">
|
||||
powered by
|
||||
<a href="https://github.com/lklynet/hypermind" target="_blank" class="footer-link">hypermind</a>
|
||||
</div>
|
||||
<div class="debug">
|
||||
ID: <span id="my-screenname">{{ID}}</span><br />
|
||||
Direct Connections: <span id="direct">{{DIRECT}}</span><br />
|
||||
Total Unique: <span id="total-unique">{{TOTAL_UNIQUE}}</span><br />
|
||||
<span class="debug-link" onclick="openDiagnostics()">diagnostics</span>
|
||||
<span id="map-container" class="{{MAP_CLASS}}">
|
||||
|
|
||||
<span class="debug-link" id="map-link" onclick="openMap()">map</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mapModal" class="modal">
|
||||
<div class="modal-content map-content">
|
||||
<button class="close-btn" onclick="closeMap()">×</button>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="diagnosticsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<button class="close-btn" onclick="closeDiagnostics()">×</button>
|
||||
<div class="modal-title">Diagnostics</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">ID</span>
|
||||
<span class="stat-value" id="diag-id" style="
|
||||
font-size: 0.8em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -83,81 +78,70 @@
|
||||
direction: rtl;
|
||||
max-width: 300px;
|
||||
display: block;
|
||||
"
|
||||
>{{FULL_ID}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Heartbeats Received</span>
|
||||
<span class="stat-value" id="diag-heartbeats-rx">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Heartbeats Relayed</span>
|
||||
<span class="stat-value" id="diag-heartbeats-tx">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">New Peers Added</span>
|
||||
<span class="stat-value" id="diag-new-peers">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Duplicate/Old Seq</span>
|
||||
<span class="stat-value" id="diag-dup-seq">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Invalid PoW</span>
|
||||
<span class="stat-value" id="diag-invalid-pow">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Invalid Signatures</span>
|
||||
<span class="stat-value" id="diag-invalid-sig">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Bandwidth In</span>
|
||||
<span class="stat-value" id="diag-bandwidth-in">0 KB</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Bandwidth Out</span>
|
||||
<span class="stat-value" id="diag-bandwidth-out">0 KB</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">LEAVE Messages</span>
|
||||
<span class="stat-value" id="diag-leave">0</span>
|
||||
</div>
|
||||
<div class="update-time" id="last-update">last 10 seconds</div>
|
||||
">{{FULL_ID}}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Heartbeats Received</span>
|
||||
<span class="stat-value" id="diag-heartbeats-rx">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Heartbeats Relayed</span>
|
||||
<span class="stat-value" id="diag-heartbeats-tx">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">New Peers Added</span>
|
||||
<span class="stat-value" id="diag-new-peers">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Duplicate/Old Seq</span>
|
||||
<span class="stat-value" id="diag-dup-seq">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Invalid PoW</span>
|
||||
<span class="stat-value" id="diag-invalid-pow">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Invalid Signatures</span>
|
||||
<span class="stat-value" id="diag-invalid-sig">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Bandwidth In</span>
|
||||
<span class="stat-value" id="diag-bandwidth-in">0 KB</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Bandwidth Out</span>
|
||||
<span class="stat-value" id="diag-bandwidth-out">0 KB</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">LEAVE Messages</span>
|
||||
<span class="stat-value" id="diag-leave">0</span>
|
||||
</div>
|
||||
<div class="update-time" id="last-update">last 10 seconds</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="theme-switcher"
|
||||
class="theme-btn {{THEMES_CLASS}}"
|
||||
title="Cycle Themes"
|
||||
>
|
||||
<span class="material-symbols-outlined">palette</span>
|
||||
<button id="theme-switcher" class="theme-btn {{THEMES_CLASS}}" title="Cycle Themes">
|
||||
<span class="material-symbols-outlined">palette</span>
|
||||
</button>
|
||||
<div id="terminal" class="terminal hidden">
|
||||
<div id="terminal-resizer" class="terminal-resizer"></div>
|
||||
<button id="terminal-toggle" class="terminal-toggle" title="Toggle Chat">
|
||||
▼
|
||||
</button>
|
||||
<div id="terminal" class="terminal hidden">
|
||||
<div id="terminal-resizer" class="terminal-resizer"></div>
|
||||
<button id="terminal-toggle" class="terminal-toggle" title="Toggle Chat">
|
||||
▼
|
||||
</button>
|
||||
<div id="system-status-bar" class="system-status-bar"></div>
|
||||
<div id="terminal-output" class="terminal-output"></div>
|
||||
<div class="terminal-input-line">
|
||||
<span class="prompt">></span>
|
||||
<input
|
||||
type="text"
|
||||
id="terminal-input"
|
||||
maxlength="140"
|
||||
placeholder="Broadcast..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div id="system-status-bar" class="system-status-bar"></div>
|
||||
<div id="terminal-output" class="terminal-output"></div>
|
||||
<div class="terminal-input-line">
|
||||
<span class="prompt">></span>
|
||||
<input type="text" id="terminal-input" maxlength="140" placeholder="Broadcast..." autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="contextMenu" class="context-menu">
|
||||
<div class="context-menu-item" id="contextWhisper">Whisper</div>
|
||||
<div class="context-menu-item" id="contextBlock">Block</div>
|
||||
</div>
|
||||
<div id="contextMenu" class="context-menu">
|
||||
<div class="context-menu-item" id="contextWhisper">Whisper</div>
|
||||
<div class="context-menu-item" id="contextBlock">Block</div>
|
||||
</div>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
export const clearCommand = {
|
||||
description: "Clears the chat history",
|
||||
execute: () => {
|
||||
const output = document.getElementById("terminal-output");
|
||||
if (output) output.innerHTML = "";
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
export const frenzyCommand = {
|
||||
description: "Rotates all themes for 20 seconds",
|
||||
execute: () => {
|
||||
const output = document.getElementById("terminal-output");
|
||||
if (!output) return;
|
||||
|
||||
if (typeof window.cycleTheme !== "function") {
|
||||
const errorDiv = document.createElement("div");
|
||||
errorDiv.className = "system-message";
|
||||
errorDiv.style.color = "#ff6b6b";
|
||||
errorDiv.innerText = "[SYSTEM] Theme frenzy unavailable";
|
||||
output.appendChild(errorDiv);
|
||||
output.scrollTop = output.scrollHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
const startDiv = document.createElement("div");
|
||||
startDiv.className = "system-message";
|
||||
startDiv.style.color = "#4ade80";
|
||||
startDiv.innerText = "[SYSTEM] Hold onto your trousers!!!";
|
||||
output.appendChild(startDiv);
|
||||
output.scrollTop = output.scrollHeight;
|
||||
|
||||
let count = 0;
|
||||
const maxRotations = 100;
|
||||
|
||||
const frenzyInterval = setInterval(() => {
|
||||
window.cycleTheme();
|
||||
count++;
|
||||
|
||||
if (count >= maxRotations) {
|
||||
clearInterval(frenzyInterval);
|
||||
|
||||
const endDiv = document.createElement("div");
|
||||
endDiv.className = "system-message";
|
||||
endDiv.style.color = "#4ade80";
|
||||
endDiv.innerText = "[SYSTEM] You survived the storm!";
|
||||
output.appendChild(endDiv);
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
export const helpCommand = {
|
||||
description: "Shows available commands",
|
||||
execute: () => {
|
||||
const output = document.getElementById("terminal-output");
|
||||
if (!output) return;
|
||||
|
||||
const createHelpSection = (title, items) => {
|
||||
const section = document.createElement("div");
|
||||
section.style.marginBottom = "10px";
|
||||
section.style.color = "#aaa";
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.style.fontWeight = "bold";
|
||||
header.style.color = "#fff";
|
||||
header.style.marginBottom = "4px";
|
||||
header.innerText = title;
|
||||
section.appendChild(header);
|
||||
|
||||
items.forEach((item) => {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = `<span style="color: #4ade80">${item.cmd}</span> - ${item.desc}`;
|
||||
section.appendChild(div);
|
||||
});
|
||||
|
||||
return section;
|
||||
};
|
||||
|
||||
const helpContainer = document.createElement("div");
|
||||
helpContainer.className = "system-message";
|
||||
helpContainer.style.padding = "10px";
|
||||
helpContainer.style.borderTop = "1px dashed #333";
|
||||
helpContainer.style.borderBottom = "1px dashed #333";
|
||||
helpContainer.style.margin = "10px 0";
|
||||
|
||||
// system
|
||||
const systemCmds = [
|
||||
{
|
||||
cmd: "/whisper <user> <msg>",
|
||||
desc: "Send a private message",
|
||||
},
|
||||
{ cmd: "/block <user>", desc: "Block messages from a user" },
|
||||
{ cmd: "/unblock <user>", desc: "Unblock a user" },
|
||||
{
|
||||
cmd: "/local <msg>",
|
||||
desc: "Send message to direct peers only (Global by default)",
|
||||
},
|
||||
{ cmd: "/clear", desc: "Clear chat history" },
|
||||
{ cmd: "/help", desc: "Show this help menu" },
|
||||
];
|
||||
helpContainer.appendChild(
|
||||
createHelpSection("System Commands", systemCmds)
|
||||
);
|
||||
|
||||
// Formatting
|
||||
const formatCmds = [
|
||||
{ cmd: "**text**", desc: "Bold" },
|
||||
{ cmd: "*text*", desc: "Italics" },
|
||||
{ cmd: "__text__", desc: "Underline" },
|
||||
{ cmd: "~~text~~", desc: "Strikethrough" },
|
||||
{ cmd: "`text`", desc: "Code" },
|
||||
];
|
||||
helpContainer.appendChild(createHelpSection("Formatting", formatCmds));
|
||||
|
||||
// Easter Eggs
|
||||
const eggs = Object.entries(window.ChatCommands.replacements).map(
|
||||
([k, v]) => ({
|
||||
cmd: k,
|
||||
desc: v,
|
||||
})
|
||||
);
|
||||
helpContainer.appendChild(createHelpSection("Easter Eggs", eggs));
|
||||
|
||||
output.appendChild(helpContainer);
|
||||
output.scrollTop = output.scrollHeight;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export const replacements = {
|
||||
"/shrug": "¯\\_(ツ)_/¯",
|
||||
"/heart": "♡",
|
||||
"/tableflip": "(╯°□°)╯︵ ┻━┻",
|
||||
"/unflip": "┬─┬ ノ( ゜-゜ノ)",
|
||||
"/lenny": "( ͡° ͜ʖ ͡°)",
|
||||
"/gimme": "༼ つ ◕_◕ ༽つ",
|
||||
"/disapproval": "ಠ_ಠ",
|
||||
"/magic": "(ノ◕ヮ◕)ノ*:・゚✧",
|
||||
};
|
||||
+51
-134
@@ -1,139 +1,56 @@
|
||||
// Chat Commands and Easter Eggs Configuration
|
||||
/**
|
||||
* If you are a developer, import all commands individually here.
|
||||
* This way the chat can scale and improve over time without making it a mess
|
||||
*/
|
||||
import { clearCommand } from "./chat-commands/clear.js";
|
||||
import { frenzyCommand } from "./chat-commands/frenzy.js";
|
||||
import { helpCommand } from "./chat-commands/help.js";
|
||||
import { replacements } from "./chat-commands/replacements.js";
|
||||
|
||||
const formatMessage = (text) => {
|
||||
if (!text) return "";
|
||||
|
||||
let html = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
html = html.replace(/__(.*?)__/g, "<u>$1</u>");
|
||||
html = html.replace(/~~(.*?)~~/g, "<del>$1</del>");
|
||||
html = html.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
const actions = {
|
||||
"/clear": clearCommand,
|
||||
"/frenzy": frenzyCommand,
|
||||
"/help": helpCommand,
|
||||
};
|
||||
|
||||
const processInput = (input) => {
|
||||
if (actions[input]) {
|
||||
return { type: "action", command: input };
|
||||
}
|
||||
|
||||
let processed = input;
|
||||
for (const [cmd, replacement] of Object.entries(replacements)) {
|
||||
const regex = new RegExp(cmd, "g");
|
||||
processed = processed.replace(regex, replacement);
|
||||
}
|
||||
|
||||
return { type: "text", content: processed };
|
||||
};
|
||||
|
||||
const ChatCommands = {
|
||||
// local
|
||||
actions: {
|
||||
"/clear": {
|
||||
description: "Clears the chat history",
|
||||
execute: () => {
|
||||
const output = document.getElementById("terminal-output");
|
||||
if (output) output.innerHTML = "";
|
||||
},
|
||||
},
|
||||
"/help": {
|
||||
description: "Shows available commands",
|
||||
execute: () => {
|
||||
const output = document.getElementById("terminal-output");
|
||||
if (!output) return;
|
||||
|
||||
const createHelpSection = (title, items) => {
|
||||
const section = document.createElement("div");
|
||||
section.style.marginBottom = "10px";
|
||||
section.style.color = "#aaa";
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.style.fontWeight = "bold";
|
||||
header.style.color = "#fff";
|
||||
header.style.marginBottom = "4px";
|
||||
header.innerText = title;
|
||||
section.appendChild(header);
|
||||
|
||||
items.forEach((item) => {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = `<span style="color: #4ade80">${item.cmd}</span> - ${item.desc}`;
|
||||
section.appendChild(div);
|
||||
});
|
||||
|
||||
return section;
|
||||
};
|
||||
|
||||
const helpContainer = document.createElement("div");
|
||||
helpContainer.className = "system-message";
|
||||
helpContainer.style.padding = "10px";
|
||||
helpContainer.style.borderTop = "1px dashed #333";
|
||||
helpContainer.style.borderBottom = "1px dashed #333";
|
||||
helpContainer.style.margin = "10px 0";
|
||||
|
||||
// system
|
||||
const systemCmds = [
|
||||
{
|
||||
cmd: "/whisper <user> <msg>",
|
||||
desc: "Send a private message",
|
||||
},
|
||||
{ cmd: "/block <user>", desc: "Block messages from a user" },
|
||||
{ cmd: "/unblock <user>", desc: "Unblock a user" },
|
||||
{
|
||||
cmd: "/local <msg>",
|
||||
desc: "Send message to direct peers only (Global by default)",
|
||||
},
|
||||
{ cmd: "/clear", desc: "Clear chat history" },
|
||||
{ cmd: "/help", desc: "Show this help menu" },
|
||||
];
|
||||
helpContainer.appendChild(
|
||||
createHelpSection("System Commands", systemCmds)
|
||||
);
|
||||
|
||||
// Formatting
|
||||
const formatCmds = [
|
||||
{ cmd: "**text**", desc: "Bold" },
|
||||
{ cmd: "*text*", desc: "Italics" },
|
||||
{ cmd: "__text__", desc: "Underline" },
|
||||
{ cmd: "~~text~~", desc: "Strikethrough" },
|
||||
{ cmd: "`text`", desc: "Code" },
|
||||
];
|
||||
helpContainer.appendChild(createHelpSection("Formatting", formatCmds));
|
||||
|
||||
// Easter Eggs
|
||||
const eggs = Object.entries(ChatCommands.replacements).map(
|
||||
([k, v]) => ({
|
||||
cmd: k,
|
||||
desc: v,
|
||||
})
|
||||
);
|
||||
helpContainer.appendChild(createHelpSection("Easter Eggs", eggs));
|
||||
|
||||
output.appendChild(helpContainer);
|
||||
output.scrollTop = output.scrollHeight;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// eastereggs
|
||||
replacements: {
|
||||
"/shrug": "¯\\_(ツ)_/¯",
|
||||
"/heart": "♡",
|
||||
"/tableflip": "(╯°□°)╯︵ ┻━┻",
|
||||
"/unflip": "┬─┬ ノ( ゜-゜ノ)",
|
||||
"/lenny": "( ͡° ͜ʖ ͡°)",
|
||||
"/gimme": "༼ つ ◕_◕ ༽つ",
|
||||
"/disapproval": "ಠ_ಠ",
|
||||
"/magic": "(ノ◕ヮ◕)ノ*:・゚✧",
|
||||
},
|
||||
|
||||
processInput: (input) => {
|
||||
if (ChatCommands.actions[input]) {
|
||||
return { type: "action", command: input };
|
||||
}
|
||||
|
||||
let processed = input;
|
||||
for (const [cmd, replacement] of Object.entries(
|
||||
ChatCommands.replacements
|
||||
)) {
|
||||
const regex = new RegExp(cmd, "g");
|
||||
processed = processed.replace(regex, replacement);
|
||||
}
|
||||
|
||||
return { type: "text", content: processed };
|
||||
},
|
||||
|
||||
formatMessage: (text) => {
|
||||
if (!text) return "";
|
||||
|
||||
let html = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
html = html.replace(/__(.*?)__/g, "<u>$1</u>");
|
||||
html = html.replace(/~~(.*?)~~/g, "<del>$1</del>");
|
||||
html = html.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||
|
||||
return html;
|
||||
},
|
||||
actions,
|
||||
replacements,
|
||||
processInput,
|
||||
formatMessage,
|
||||
};
|
||||
|
||||
window.ChatCommands = ChatCommands;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
const CACHE_KEY = "hypermind-version-check";
|
||||
const CACHE_DURATION = 86400000;
|
||||
|
||||
const getCurrentVersion = () => {
|
||||
return document.body.dataset.version || "no-version";
|
||||
};
|
||||
|
||||
const checkForNewVersion = async () => {
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
if (cached) {
|
||||
try {
|
||||
const parsedCache = JSON.parse(cached);
|
||||
const now = Date.now();
|
||||
|
||||
if (now - parsedCache.timestamp < CACHE_DURATION) {
|
||||
if (parsedCache.data) {
|
||||
showVersionNotification(parsedCache.data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch("/api/github/latest-release");
|
||||
if (!response.ok) return;
|
||||
|
||||
const release = await response.json();
|
||||
|
||||
const cacheData = {
|
||||
data: release,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
|
||||
|
||||
showVersionNotification(release);
|
||||
} catch (error) {
|
||||
console.error("Error checking for new version:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const showVersionNotification = (release) => {
|
||||
if (!release || !release.tag_name) return;
|
||||
|
||||
const latestVersion = release.tag_name.replace(/^v/, "");
|
||||
const currentVersion = getCurrentVersion();
|
||||
|
||||
if (latestVersion === currentVersion) return;
|
||||
|
||||
const dismissedKey = `hypermind-version-dismissed-${release.tag_name}`;
|
||||
if (localStorage.getItem(dismissedKey) === "true") return;
|
||||
|
||||
const banner = document.getElementById("version-notification");
|
||||
if (!banner) return;
|
||||
|
||||
const versionText = banner.querySelector(".version-text");
|
||||
const updateLink = banner.querySelector(".update-link");
|
||||
const dismissBtn = banner.querySelector(".dismiss-btn");
|
||||
|
||||
versionText.textContent = `New version ${release.tag_name} available!`;
|
||||
updateLink.href = release.html_url;
|
||||
|
||||
dismissBtn.onclick = () => {
|
||||
banner.classList.remove("active");
|
||||
localStorage.setItem(dismissedKey, "true");
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
banner.classList.add("active");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", checkForNewVersion);
|
||||
} else {
|
||||
checkForNewVersion();
|
||||
}
|
||||
@@ -371,3 +371,77 @@ a { color: var(--color-text-anchor-link); text-decoration: none; border-bottom:
|
||||
background: var(--color-modal-stat-div);
|
||||
color: var(--color-text-main-label);
|
||||
}
|
||||
|
||||
.version-notification {
|
||||
position: fixed;
|
||||
top: -100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-terminal-bg);
|
||||
border: 1px solid var(--color-terminal-border);
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding: 12px 20px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px var(--color-terminal-shadow);
|
||||
transition: top 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
min-width: 300px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.version-notification.active {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.version-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
color: var(--color-terminal-text-default);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.update-link {
|
||||
color: #4ade80;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #4ade80;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.update-link:hover {
|
||||
color: #22c55e;
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
.dismiss-btn {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-modal-close-btn);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.dismiss-btn:hover {
|
||||
color: var(--color-modal-close-btn-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.version-notification {
|
||||
min-width: unset;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.version-content {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
+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 };
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
const http = require("http");
|
||||
|
||||
const test = (method, path, body = null, validate, timeout = 5000) => {
|
||||
return new Promise((resolve) => {
|
||||
const options = {
|
||||
hostname: "localhost",
|
||||
port: 3000,
|
||||
path,
|
||||
method,
|
||||
headers: body ? { "Content-Type": "application/json" } : {},
|
||||
};
|
||||
|
||||
let resolved = false;
|
||||
const timer = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`✗ ${method} ${path} - Timeout`);
|
||||
resolve(false);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
if (path === "/events" && data.includes("data:")) {
|
||||
clearTimeout(timer);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
const valid = validate(data, res.headers["content-type"]);
|
||||
console.log(`${valid ? "✓" : "✗"} ${method} ${path}`);
|
||||
req.destroy();
|
||||
resolve(valid);
|
||||
}
|
||||
}
|
||||
});
|
||||
res.on("end", () => {
|
||||
clearTimeout(timer);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
try {
|
||||
const valid = validate(data, res.headers["content-type"]);
|
||||
console.log(`${valid ? "✓" : "✗"} ${method} ${path}`);
|
||||
resolve(valid);
|
||||
} catch (e) {
|
||||
console.log(`✗ ${method} ${path} - ${e.message}`);
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", (e) => {
|
||||
clearTimeout(timer);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`✗ ${method} ${path} - ${e.message}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
console.log("Testing API endpoints...\n");
|
||||
|
||||
const results = await Promise.all([
|
||||
test("GET", "/api/stats", null, (data) => {
|
||||
const json = JSON.parse(data);
|
||||
return json.count !== undefined && json.id && json.screenname && json.diagnostics;
|
||||
}),
|
||||
test("GET", "/api/github/latest-release", null, (data) => {
|
||||
const json = JSON.parse(data);
|
||||
return json.tag_name && json.html_url;
|
||||
}),
|
||||
test("POST", "/api/chat", { content: "test", scope: "LOCAL" }, (data) => {
|
||||
const json = JSON.parse(data);
|
||||
return json.success === true;
|
||||
}),
|
||||
test("GET", "/events", null, (data, contentType) => {
|
||||
return contentType.includes("text/event-stream") && data.includes("data:");
|
||||
}, 2000),
|
||||
test("GET", "/js/lists.js", null, (data, contentType) => {
|
||||
return contentType.includes("javascript") && data.includes("ADJECTIVES") && data.includes("NOUNS");
|
||||
}),
|
||||
test("GET", "/js/screenname.js", null, (data, contentType) => {
|
||||
return contentType.includes("javascript") && data.includes("generateScreenname");
|
||||
}),
|
||||
test("GET", "/", null, (data, contentType) => {
|
||||
return contentType.includes("text/html") && data.includes("Hypermind");
|
||||
}),
|
||||
]);
|
||||
|
||||
const passed = results.filter(Boolean).length;
|
||||
const total = results.length;
|
||||
|
||||
console.log(`\n${passed}/${total} tests passed`);
|
||||
process.exit(passed === total ? 0 : 1);
|
||||
})();
|
||||
Reference in New Issue
Block a user