Skip to content

Commit d209681

Browse files
committed
refactor: migrate to modular architecture with Web UI, logging, and auto dependency handling
A friend sent me an updated version to include the HTTP server so I updated it and moved to a modular system. - Modularized the BotServer into plug-and-play modules under /modules - Added optional HTTP server module with real-time Web UI - Integrated optional logging module to write terminal output to file - Auto-installs missing dependencies on first run - Replaced moment.js with native formatting (utils.formatTimestamp) - Updated run.bat → start-server.bat for clarity - Improved error handling and startup diagnostics
1 parent d56bc07 commit d209681

13 files changed

Lines changed: 1128 additions & 205 deletions

README.md

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,84 @@
1-
# NodeJS WebSocket Server
2-
### Website: https://www.trainorcreations.com
3-
### Discord: https://trainorcreations.com/discord
4-
### Donate: https://trainorcreations.com/donate
1+
# NodeJS BotServer
52

6-
This project is a simple WebSocket server based off [otcv8botserver](https://github.com/OTCv8/otcv8botserver), built with Node.js using the `ws` library. The server automatically installs the required dependencies if they are missing.
3+
A modular and extensible BotServer server inspired by [otcv8botserver](https://github.com/OTCv8/otcv8botserver), built with Node.js.
74

8-
## Requirements
5+
This server includes:
6+
- 📡 A WebSocket interface for bot communication
7+
- 🌐 An optional HTTP web UI for real-time monitoring
8+
- 🧩 Modular architecture (just drop modules into `/modules`)
9+
- 🪵 Optional logging to `logs/output.log`
10+
- 🔧 Auto-installs missing dependencies on first run
11+
---
12+
## 🔗 Links
913

10-
- **Node.js**
14+
- 🌐 [Website](https://www.trainorcreations.com)
15+
- 💬 [Discord](https://trainorcreations.com/discord)
16+
- 💖 [Donate](https://trainorcreations.com/donate)
1117

12-
Download and install Node.js from [https://nodejs.org/](https://nodejs.org/).
18+
---
1319

14-
## Setup Instructions
20+
## 📦 Requirements
1521

16-
Double click on `run.bat` which will start the BotServer this will download and install any required dependencies on first run.
22+
- [Node.js](https://nodejs.org/)
1723

18-
## vBot Setup
19-
Find `_Loader.lua` add the text below to first line
24+
---
2025

21-
```lua
22-
BotServer.url = "ws://localhost:8000/" -- add this line
23-
-- load all otui files, order doesn't matter
24-
```
25-
Save `_Loader.lua`.
26+
## ⚙️ Setup
2627

27-
Reload your bot config and connect using the botserver panel.
28+
1. **Download & Install Node.js**
29+
- Install it from [nodejs.org](https://nodejs.org/)
30+
31+
2. **Run the Server**
32+
- Launch `start-server.bat`
33+
- Automatically installs dependencies on first run
34+
35+
3. **Logging**
36+
- All console output (stdout & errors) is logged to `logs/output.log`
37+
- Colors are stripped from log files for clean reading
38+
39+
> 💡 **Optional:**
40+
> Don’t want the HTTP Web UI or logging?
41+
> - Disable the web interface: rename `modules/http.js``modules/http.js.disabled`
42+
> - Disable logging to file: rename `modules/a-logger.js``modules/a-logger.js.disabled`
43+
44+
---
45+
46+
## 🤖 vBot Integration
47+
48+
1. Open `_Loader.lua`
49+
2. Add the following at the top:
50+
```lua
51+
BotServer.url = "ws://localhost:8000/" -- add this line
52+
-- load all otui files, order doesn't matter
53+
```
54+
## 📤 Sending Character Info (Lua)
55+
56+
To allow the server to register your character data, you can send character information from your bot using a Lua script.
57+
58+
Add the following to your bot script (e.g., inside a Macro):
59+
```lua
60+
macro(10000, "Send Char Info", function()
61+
if not BotServer._websocket then return end
62+
63+
BotServer.send("char_info", {
64+
name = player:getName(),
65+
level = player:getLevel(),
66+
vocation = player:getVocation(),
67+
health = player:getHealth(),
68+
maxHealth = player:getMaxHealth(),
69+
mana = player:getMana(),
70+
maxMana = player:getMaxMana(),
71+
experience = player:getExperience(),
72+
expPercent = player:getLevelPercent(),
73+
location = pos() and string.format("%d, %d, %d", pos().x, pos().y, pos().z)
74+
})
75+
end)
76+
```
77+
78+
## Web UI
79+
80+
![Web UI Preview](assets/web-ui-preview.png)
81+
82+
## WebSocket Terminal View
83+
84+
![WebSocket Terminal](assets/ws-terminal.preview.png)

assets/web-ui-preview.png

86.8 KB
Loading

assets/ws-terminal.preview.png

26.9 KB
Loading

botserver.js

Lines changed: 18 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -1,189 +1,20 @@
1-
const appTitle = 'NodeJS BotServer';
2-
const statusIntervalMs = 60000;
3-
4-
const PORT = 8000;
5-
const MAX_PAYLOAD = 64 * 1024;
6-
const MAX_PACKETS = 100;
7-
const MAX_TOPIC_LENGTH = 30;
8-
9-
const dependencies = ['ws'];
10-
const { execSync } = require('child_process');
11-
12-
const log = (...args) => {
13-
const now = new Date();
14-
const time = now.toTimeString().slice(0, 5);
15-
console.log(`[${time}]`, ...args);
1+
const config = require('./config');
2+
const utils = require('./utils');
3+
4+
const state = {
5+
connections: 0,
6+
exceptions: 0,
7+
blocked: 0,
8+
packets: 0,
9+
httpAllowedRequests: 0,
10+
httpBlockedRequests: 0,
11+
channels: {},
12+
characters: {},
13+
wsStartTime: null
1614
};
1715

18-
function ensureDependencies(modules) {
19-
const missing = modules.filter(m => {
20-
try { require.resolve(m); return false; }
21-
catch { return true; }
22-
});
23-
24-
if (missing.length) {
25-
log(`\x1b[33mMissing dependencies:\x1b[0m ${missing.join(', ')}`);
26-
log(`\x1b[36mInstalling ${missing.length} module(s)...\x1b[0m`);
27-
execSync(`npm install ${missing.join(' ')}`, { stdio: 'inherit' });
28-
log(`\x1b[32m✓ Dependencies installed.\x1b[0m\n`);
29-
}
30-
}
31-
32-
console.log(`\n\x1b[1m\x1b[34m=== ${appTitle} ===\x1b[0m\n`);
33-
34-
ensureDependencies(dependencies);
35-
36-
const WebSocket = require('ws');
37-
38-
const server = new WebSocket.Server({
39-
port: PORT,
40-
maxPayload: MAX_PAYLOAD,
41-
});
42-
43-
let connections = 0;
44-
let exceptions = 0;
45-
let blocked = 0;
46-
const channels = {};
47-
48-
function millis() {
49-
return Date.now();
50-
}
51-
52-
function dispatchMessage(ws, message) {
53-
if (!channels[ws.userData.channel]) return;
54-
55-
channels[ws.userData.channel].forEach((client) => {
56-
if (client !== ws) {
57-
client.send(JSON.stringify(message));
58-
}
59-
});
60-
}
61-
62-
function processMessage(ws, message) {
63-
const userData = ws.userData;
64-
const msg = JSON.parse(message);
65-
66-
if (!userData.name || !userData.channel) {
67-
if (msg.type !== 'init') {
68-
return ws.close();
69-
}
70-
71-
userData.name = msg.name;
72-
userData.channel = msg.channel;
73-
userData.lastPingSent = millis();
74-
75-
if (!channels[userData.channel]) {
76-
channels[userData.channel] = new Set();
77-
}
78-
channels[userData.channel].add(ws);
79-
80-
log(`\x1b[36m${userData.name} has joined channel:\x1b[0m \x1b[35m${userData.channel}\x1b[0m`);
81-
return;
82-
}
83-
84-
const currentSeconds = Math.floor(Date.now() / 1000);
85-
if (userData.packetsTime < currentSeconds) {
86-
userData.packetsTime = currentSeconds + 1;
87-
userData.packets = 0;
88-
}
89-
90-
userData.packets += 1;
91-
if (userData.packets > MAX_PACKETS || message.length > MAX_PAYLOAD) {
92-
blocked += 1;
93-
return ws.close();
94-
}
95-
96-
if (msg.type === 'ping') {
97-
userData.lastPing = millis() - userData.lastPingSent;
98-
return;
99-
}
100-
101-
if (msg.type !== 'message') {
102-
return ws.close();
103-
}
104-
105-
const response = {
106-
type: 'message',
107-
id: ++ws.messageId,
108-
name: userData.name,
109-
topic: msg.topic,
110-
};
111-
112-
if (!msg.topic || msg.topic.length > MAX_TOPIC_LENGTH) {
113-
return ws.close();
114-
}
115-
116-
if (msg.topic === 'list') {
117-
const users = Array.from(channels[userData.channel]).map(client => client.userData.name);
118-
response.message = users;
119-
ws.send(JSON.stringify(response));
120-
return;
121-
}
122-
123-
response.message = msg.message;
124-
dispatchMessage(ws, response);
125-
}
126-
127-
function sendPing() {
128-
Object.keys(channels).forEach((channel) => {
129-
channels[channel].forEach((ws) => {
130-
const userData = ws.userData;
131-
userData.lastPingSent = millis();
132-
ws.send(JSON.stringify({ type: 'ping', ping: userData.lastPing }));
133-
});
134-
});
135-
}
136-
137-
server.on('connection', (ws) => {
138-
connections += 1;
139-
log(`\x1b[33mClient connected.\x1b[0m Total connections: \x1b[36m${connections}\x1b[0m`);
140-
141-
ws.userData = {
142-
name: '',
143-
channel: '',
144-
lastPing: 0,
145-
lastPingSent: 0,
146-
packets: 0,
147-
packetsTime: 0,
148-
};
149-
150-
ws.messageId = 0;
151-
152-
ws.on('message', (message) => {
153-
if (message.length > MAX_PAYLOAD) {
154-
blocked += 1;
155-
return ws.close();
156-
}
157-
158-
try {
159-
processMessage(ws, message);
160-
} catch (error) {
161-
exceptions += 1;
162-
ws.close();
163-
}
164-
});
165-
166-
ws.on('close', () => {
167-
const { name, channel } = ws.userData;
168-
if (channels[channel]) {
169-
channels[channel].delete(ws);
170-
if (channels[channel].size === 0) {
171-
delete channels[channel];
172-
}
173-
}
174-
log(`\x1b[31m${name} disconnected from channel:\x1b[0m \x1b[35m${channel}\x1b[0m`);
175-
connections -= 1;
176-
});
177-
178-
ws.on('error', (error) => {
179-
console.error(`WebSocket error: ${error.message}`);
180-
});
181-
});
182-
183-
setInterval(sendPing, 1000);
184-
185-
setInterval(() => {
186-
log(`Connections: ${connections}, Exceptions: ${exceptions}, Blocked: ${blocked}, Channels: ${Object.keys(channels).length}`);
187-
}, statusIntervalMs);
188-
189-
log(`\x1b[32mWebSocket server running on port ${PORT}\x1b[0m`);
16+
(async () => {
17+
console.clear();
18+
utils.log.title(config.appTitle);
19+
await utils.loadModules({ config, utils, state });
20+
})();

config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
appTitle: 'NodeJS BotServer',
3+
HTTP_HOST: 'localhost',
4+
HTTP_PORT: 8080,
5+
HTTP_ALLOWED_IPS: ['127.0.0.1', '::1'],
6+
WS_STATUS_INTERVAL: 60000,
7+
WS_PORT: 8000,
8+
WS_MAX_PAYLOAD: 64 * 1024,
9+
WS_MAX_PACKETS: 100,
10+
WS_MAX_TOPIC_LENGTH: 30,
11+
};

modules/a-logger.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const logDir = path.join(__dirname, '..', 'logs');
5+
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir);
6+
7+
const logFile = fs.createWriteStream(path.join(logDir, 'output.log'), { flags: 'a' });
8+
9+
let utils;
10+
const now = () => utils.formatTimestamp();
11+
12+
function stripAnsi(str) {
13+
return str.replace(/\x1b\[[0-9;]*m/g, '');
14+
}
15+
16+
function logToFile(type, ...args) {
17+
const msg = `[${now()}] [${type}] ${stripAnsi(args.join(' '))}\n`;
18+
logFile.write(msg);
19+
}
20+
21+
function logToConsole(type, color, ...args) {
22+
const prefix = `[${now()}] [${type}]`;
23+
console.log(color + prefix, ...args, '\x1b[0m');
24+
}
25+
26+
const logger = {
27+
info: (...args) => { logToConsole('INFO', '', ...args); logToFile('INFO', ...args); },
28+
warn: (...args) => { logToConsole('WARN', '\x1b[33m', ...args); logToFile('WARN', ...args); },
29+
success: (...args) => { logToConsole('SUCCESS', '\x1b[32m', ...args); logToFile('SUCCESS', ...args); },
30+
dim: (...args) => { logToConsole('DIM', '\x1b[2m', ...args); logToFile('DIM', ...args); },
31+
title: (text) => {
32+
const msg = `=== ${text} ===`;
33+
console.log(`\n\x1b[1m\x1b[34m${msg}\x1b[0m\n`);
34+
logToFile('TITLE', msg);
35+
}
36+
};
37+
38+
module.exports = async (ctx) => {
39+
utils = ctx.utils;
40+
ctx.utils.log = logger;
41+
};
42+
43+
module.exports.deps = [];

0 commit comments

Comments
 (0)