Repository navigation
Expand file tree
/
Copy pathsetup.js
More file actions
351 lines (295 loc) · 16.5 KB
/
Copy pathsetup.js
File metadata and controls
351 lines (295 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
import readline from "readline";
import fs from "fs";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function ask(question, defaultVal = "") {
const suffix = defaultVal ? ` [${defaultVal}]` : "";
return new Promise((resolve) => {
rl.question(`${question}${suffix}: `, (answer) => {
resolve(answer.trim() || defaultVal);
});
});
}
async function askYesNo(question, defaultVal = "y") {
const answer = await ask(`${question} (y/n)`, defaultVal);
return answer.toLowerCase().startsWith("y");
}
function printSection(title) {
console.log(`\n${"=".repeat(60)}`);
console.log(` ${title}`);
console.log(`${"=".repeat(60)}\n`);
}
function printHelp(text) {
console.log(` -> ${text}`);
}
async function main() {
console.log(`
__ __ _ ____
| \\/ | ___ ___| |__ / ___|___ _ __ ___
| |\\/| |/ _ \\/ __| '_ \\| | / _ \\| '__/ _ \\
| | | | __/\\__ \\ | | | |__| (_) | | | __/
|_| |_|\\___||___/_| |_|\\____\\___/|_| \\___|
Discord Bridge Setup Wizard
`);
// Load existing config if present
let existing = {};
try {
existing = JSON.parse(fs.readFileSync("./config.json", "utf8"));
console.log(" Found existing config.json — current values will be shown as defaults.\n");
} catch {
console.log(" No existing config.json found — starting fresh.\n");
}
// Start from the existing config so keys the wizard doesn't ask about (web UI,
// language filter, send gap, pruning, ...) survive a re-run.
const config = { ...existing };
// ============================================================
// Discord Bot Setup
// ============================================================
printSection("Discord Bot Setup");
printHelp("Go to https://discord.com/developers/applications");
printHelp("Click your application (or create one).");
printHelp("The Application ID is on the General Information page.\n");
config.CLIENT_ID = await ask("Application (Client) ID", existing.CLIENT_ID || "");
printHelp("\nGo to the Bot tab in the Developer Portal.");
printHelp("Click 'Reset Token' to generate a new token, or use your existing one.");
printHelp("Make sure these Privileged Intents are enabled:");
printHelp(" - Presence Intent");
printHelp(" - Server Members Intent");
printHelp(" - Message Content Intent\n");
config.DISCORD_TOKEN = await ask("Bot Token", existing.DISCORD_TOKEN || "");
printHelp("\nRight-click your Discord server name -> Copy Server ID.");
printHelp("(Enable Developer Mode in Discord Settings -> Advanced if you don't see this.)\n");
const guildIdInput = await ask("Server (Guild) ID(s), comma-separated", (existing.GUILD_IDS || []).join(", "));
config.GUILD_IDS = guildIdInput.split(",").map((s) => s.trim()).filter(Boolean);
// ============================================================
// Serial Port
// ============================================================
printSection("MeshCore Device");
printHelp("Connect your MeshCore device via USB.");
printHelp("Common ports: /dev/ttyUSB0 (Linux), /dev/tty.usbserial-XXXX (macOS), COM3 (Windows).");
printHelp("You can use 'ls /dev/ttyUSB*' or 'ls /dev/ttyACM*' on Linux to find it.\n");
config.SERIAL_PORT = await ask("Serial port", existing.SERIAL_PORT || "/dev/ttyUSB0");
printHelp("\nThis is the name your MeshCore device advertises on the mesh network.");
printHelp("Other mesh users will see this name. Check your device's config if unsure.\n");
config.MESH_NODE_NAME = await ask("Mesh node name", existing.MESH_NODE_NAME || "");
// ============================================================
// Discord Channels
// ============================================================
printSection("Discord Channel Setup");
printHelp("Right-click any Discord channel -> Copy Channel ID to get its ID.\n");
printHelp("This is the default channel for messages that don't match a specific route.");
config.DISCORD_CHANNEL_ID = await ask("Default/Public Discord channel ID", existing.DISCORD_CHANNEL_ID || "");
// ============================================================
// Channel Routing
// ============================================================
printSection("Channel Routing");
printHelp("Map mesh channel indices to Discord channel IDs.");
printHelp("Mesh channel 0 is typically 'Public'.");
printHelp("You can find mesh channel indices in MeshCoreOne or the companion app.");
printHelp("Enter routes as: meshIndex=discordChannelId (one per line).");
printHelp("Press Enter on an empty line when done.\n");
config.DISCORD_ROUTES = { ...(existing.DISCORD_ROUTES || {}) };
config.DISCORD_TO_MESH_ROUTES = { ...(existing.DISCORD_TO_MESH_ROUTES || {}) };
if (Object.keys(config.DISCORD_ROUTES).length > 0) {
console.log(" Current routes:");
for (const [idx, chId] of Object.entries(config.DISCORD_ROUTES)) {
console.log(` Mesh ch ${idx} -> Discord ${chId}`);
}
const keepRoutes = await askYesNo("\n Keep existing routes?", "y");
if (!keepRoutes) {
config.DISCORD_ROUTES = {};
config.DISCORD_TO_MESH_ROUTES = {};
}
}
if (await askYesNo("Add channel routes?", Object.keys(config.DISCORD_ROUTES).length === 0 ? "y" : "n")) {
while (true) {
const route = await ask(" meshIndex=discordChannelId (or Enter to finish)");
if (!route) break;
const [idx, chId] = route.split("=").map((s) => s.trim());
if (idx && chId) {
config.DISCORD_ROUTES[idx] = chId;
config.DISCORD_TO_MESH_ROUTES[chId] = Number(idx);
console.log(` Added: Mesh ch ${idx} <-> Discord ${chId}`);
}
}
}
// ============================================================
// Always-Forward Channels
// ============================================================
printSection("Always-Forward Channels");
printHelp("These Discord channels automatically forward ALL messages to mesh.");
printHelp("They must also have a route configured above.");
printHelp("Enter Discord channel IDs, one per line. Enter to finish.\n");
config.DISCORD_ALWAYS_FORWARD_CHANNEL_IDS = [...(existing.DISCORD_ALWAYS_FORWARD_CHANNEL_IDS || [])];
if (config.DISCORD_ALWAYS_FORWARD_CHANNEL_IDS.length > 0) {
console.log(" Current always-forward channels:");
for (const id of config.DISCORD_ALWAYS_FORWARD_CHANNEL_IDS) {
console.log(` ${id}`);
}
const keepForward = await askYesNo("\n Keep existing list?", "y");
if (!keepForward) {
config.DISCORD_ALWAYS_FORWARD_CHANNEL_IDS = [];
}
}
if (await askYesNo("Add always-forward channels?", config.DISCORD_ALWAYS_FORWARD_CHANNEL_IDS.length === 0 ? "y" : "n")) {
while (true) {
const chId = await ask(" Discord channel ID (or Enter to finish)");
if (!chId) break;
if (!config.DISCORD_ALWAYS_FORWARD_CHANNEL_IDS.includes(chId)) {
config.DISCORD_ALWAYS_FORWARD_CHANNEL_IDS.push(chId);
console.log(` Added: ${chId}`);
}
}
}
// ============================================================
// ImgBB (Image Uploads)
// ============================================================
printSection("Image Uploads (Optional)");
printHelp("Images and files posted in Discord can be uploaded to your own file host");
printHelp("(a service that accepts POST /upload with a Bearer token and returns {\"url\"}).");
config.FILE_HOST_URL = await ask("File host base URL, e.g. https://unym.pics (or Enter to skip)", existing.FILE_HOST_URL || "");
config.FILE_HOST_TOKEN = config.FILE_HOST_URL
? await ask("File host upload token", existing.FILE_HOST_TOKEN || "")
: (existing.FILE_HOST_TOKEN || "");
printHelp("\nImgBB is used for images when no file host is set, or as a fallback if it fails.");
printHelp("Sign up at https://imgbb.com, then get an API key at https://api.imgbb.com\n");
config.IMGBB_API_KEY = await ask("ImgBB API key (or Enter to skip)", existing.IMGBB_API_KEY || "");
// ============================================================
// Emergency Channel (Optional)
// ============================================================
printSection("Emergency Channel (Optional)");
printHelp("An emergency mesh channel can trigger @everyone alerts on Discord.");
printHelp("First message pings everyone, auto-replies to mesh, and reminds if no response.\n");
if (await askYesNo("Configure an emergency channel?", existing.EMERGENCY_MESH_CHANNEL_IDX != null ? "y" : "n")) {
// Empty must not become Number("") = 0, which would make Public the emergency channel
const emergencyIdx = (await ask(" Emergency mesh channel index", String(existing.EMERGENCY_MESH_CHANNEL_IDX ?? ""))).trim();
config.EMERGENCY_MESH_CHANNEL_IDX = emergencyIdx === "" || isNaN(Number(emergencyIdx)) ? null : Number(emergencyIdx);
if (config.EMERGENCY_MESH_CHANNEL_IDX === null) console.log(" No valid index entered; emergency channel left unset.");
config.EMERGENCY_DISCORD_CHANNEL_ID = await ask(" Emergency Discord channel ID", existing.EMERGENCY_DISCORD_CHANNEL_ID || "");
config.EMERGENCY_COOLDOWN_MINUTES = Number(await ask(" Cooldown before re-alerting (minutes)", String(existing.EMERGENCY_COOLDOWN_MINUTES ?? 30)));
config.EMERGENCY_REMINDER_MINUTES = Number(await ask(" Reminder if no reply (minutes)", String(existing.EMERGENCY_REMINDER_MINUTES ?? 5)));
} else {
config.EMERGENCY_MESH_CHANNEL_IDX = null;
config.EMERGENCY_DISCORD_CHANNEL_ID = "";
config.EMERGENCY_COOLDOWN_MINUTES = 30;
config.EMERGENCY_REMINDER_MINUTES = 5;
}
// ============================================================
// Bridge Prefixes
// ============================================================
printSection("Bridge Prefix Stripping (Optional)");
printHelp("If other bridges relay messages to your mesh network, their node name");
printHelp("gets prepended to messages (e.g. 'txtMesh: SomeUser: hello').");
printHelp("List bridge names here to strip them for cleaner display.\n");
config.BRIDGE_PREFIXES = [...(existing.BRIDGE_PREFIXES || [])];
if (config.BRIDGE_PREFIXES.length > 0) {
console.log(" Current prefixes:", config.BRIDGE_PREFIXES.join(", "));
}
if (await askYesNo("Add bridge prefixes?", config.BRIDGE_PREFIXES.length === 0 ? "y" : "n")) {
while (true) {
const prefix = await ask(" Bridge node name (or Enter to finish)");
if (!prefix) break;
if (!config.BRIDGE_PREFIXES.includes(prefix)) {
config.BRIDGE_PREFIXES.push(prefix);
}
}
}
// ============================================================
// Welcome Messages (Optional)
// ============================================================
printSection("Welcome Messages (Optional)");
printHelp("New mesh users can receive a welcome message in two steps:");
printHelp("1. A channel message when they first chat on Public");
printHelp("2. A DM when their advert is received");
printHelp("Use {name} as a placeholder for the username in the channel message.\n");
const welcomeEnabled = await ask("Enable welcome messages? (yes/no)", (existing.WELCOME_ENABLED !== false) ? "yes" : "no");
config.WELCOME_ENABLED = welcomeEnabled.toLowerCase().startsWith("y");
if (config.WELCOME_ENABLED) {
config.WELCOME_CHANNEL_MESSAGE = await ask("Channel welcome message", existing.WELCOME_CHANNEL_MESSAGE || "Welcome, {name}! Send an advert for more info.");
config.WELCOME_DM_MESSAGE = await ask("Welcome DM message", existing.WELCOME_DM_MESSAGE || "Welcome! Join our Discord and add channels for local chat.");
} else {
config.WELCOME_CHANNEL_MESSAGE = existing.WELCOME_CHANNEL_MESSAGE || "";
config.WELCOME_DM_MESSAGE = existing.WELCOME_DM_MESSAGE || "";
}
// ============================================================
// DM Forwarding (Optional)
// ============================================================
printSection("DM Forwarding (Optional)");
printHelp("Mesh DMs sent to your bridge node can be forwarded to a Discord user.");
printHelp("Right-click a Discord user -> Copy User ID to get their ID.\n");
config.DM_FORWARD_DISCORD_USER_ID = await ask("Discord user ID to forward DMs to (or Enter to skip)", existing.DM_FORWARD_DISCORD_USER_ID || "");
// ============================================================
// Advanced Settings
// ============================================================
printSection("Advanced Settings");
config.identifier = await ask("Command prefix for legacy text commands", existing.identifier || "!");
config.MESH_MAXLEN = Number(await ask("Max mesh message length", String(existing.MESH_MAXLEN ?? 120)));
config.MESH_CHUNK_DELAY_MS = Number(await ask("Delay between chunked messages (ms)", String(existing.MESH_CHUNK_DELAY_MS ?? 4000)));
config.DEBUG = await askYesNo("Enable debug logging?", existing.DEBUG ? "y" : "n");
// Flood protection
const floodCfg = existing.FLOOD_PROTECT || {};
config.FLOOD_PROTECT = {
WINDOW_SECONDS: Number(await ask("Flood window (seconds)", String(floodCfg.WINDOW_SECONDS ?? 15))),
MAX_MESSAGES_PER_WINDOW: Number(await ask("Max messages per window", String(floodCfg.MAX_MESSAGES_PER_WINDOW ?? 6))),
COOLDOWN_SECONDS: Number(await ask("Flood cooldown (seconds)", String(floodCfg.COOLDOWN_SECONDS ?? 300))),
WARN_IN_CHANNEL: await askYesNo("Warn in channel when flood protection triggers?", floodCfg.WARN_IN_CHANNEL !== false ? "y" : "n"),
};
// ============================================================
// Optional fields with defaults
// ============================================================
config.DISCORD_CHANNEL_ID_MESHMONDAY = existing.DISCORD_CHANNEL_ID_MESHMONDAY || "";
config.NODE_ANNOUNCE_CHANNEL_ID = existing.NODE_ANNOUNCE_CHANNEL_ID || "";
config.SUBSCRIBE_CHANNEL_ID = existing.SUBSCRIBE_CHANNEL_ID || "";
config.SUBSCRIBE_MESSAGE_ID = existing.SUBSCRIBE_MESSAGE_ID || "";
config.SUBSCRIBABLE_CHANNELS = existing.SUBSCRIBABLE_CHANNELS || [];
config.VISIBILITY_SYNC_SKIP_CHANNEL_IDS = existing.VISIBILITY_SYNC_SKIP_CHANNEL_IDS || [];
config.MESH_CHANNEL_SCOPES = existing.MESH_CHANNEL_SCOPES || {};
config.MESH_KNOWN_SCOPES = existing.MESH_KNOWN_SCOPES || [];
config.BRIDGE_ADMIN_ROLE_IDS = existing.BRIDGE_ADMIN_ROLE_IDS || [];
config._SUBSCRIBE_ROLE_MAP = existing._SUBSCRIBE_ROLE_MAP || [];
config.BLOCKED_MESH_USERS = existing.BLOCKED_MESH_USERS || [];
config.SCHEDULED_MESSAGES = existing.SCHEDULED_MESSAGES || [];
config.VOTE_BLOCK_HISTORY = existing.VOTE_BLOCK_HISTORY || {};
// ============================================================
// Save
// ============================================================
printSection("Save Configuration");
console.log(" Config preview:\n");
console.log(` Bot Token: ${config.DISCORD_TOKEN ? "****" + config.DISCORD_TOKEN.slice(-6) : "(not set)"}`);
console.log(` Client ID: ${config.CLIENT_ID || "(not set)"}`);
console.log(` Guild IDs: ${config.GUILD_IDS.join(", ") || "(none)"}`);
console.log(` Serial Port: ${config.SERIAL_PORT}`);
console.log(` Node Name: ${config.MESH_NODE_NAME || "(not set)"}`);
console.log(` Routes: ${Object.keys(config.DISCORD_ROUTES).length} configured`);
console.log(` Always-Forward: ${config.DISCORD_ALWAYS_FORWARD_CHANNEL_IDS.length} channels`);
console.log(` File Host: ${config.FILE_HOST_URL || "not set"}`);
console.log(` ImgBB: ${config.IMGBB_API_KEY ? "configured" : "not set"}`);
console.log(` Emergency: ${config.EMERGENCY_MESH_CHANNEL_IDX != null ? "configured" : "not set"}`);
console.log(` Welcome: ${config.WELCOME_CHANNEL_MESSAGE ? "configured" : "not set"}`);
console.log(` DM Forward: ${config.DM_FORWARD_DISCORD_USER_ID ? "configured" : "not set"}`);
console.log(` Debug: ${config.DEBUG}`);
console.log("");
if (fs.existsSync("./config.json")) {
const overwrite = await askYesNo("config.json already exists. Overwrite?", "y");
if (!overwrite) {
console.log("\n Aborted. Config not saved.\n");
rl.close();
return;
}
}
fs.writeFileSync("./config.json", JSON.stringify(config, null, 2), { mode: 0o600 }); // holds bot token + secrets
fs.chmodSync("./config.json", 0o600); // mode only applies when the file is created
console.log("\n config.json saved successfully!");
console.log("\n Next steps:");
console.log(" 1. npm install");
console.log(" 2. node main.js (or pm2 start main.js --name mesh-bridge)");
console.log(" 3. Run /subscribe-setup in Discord to set up channel subscriptions\n");
rl.close();
}
main().catch((e) => {
console.error("Setup error:", e);
rl.close();
process.exit(1);
});