feat: implement 90s screenname generator and update ID display

This commit is contained in:
lklynet
2026-01-06 22:19:21 -05:00
parent eb884e7643
commit f150e39092
9 changed files with 157 additions and 8 deletions
+24 -3
View File
@@ -220,7 +220,8 @@ const updateMap = async (peers) => {
fillOpacity: 0.15
}).addTo(map);
marker.bindPopup(`<b>Node</b> ${peer.id.slice(-8)}<br>${loc.city}, ${loc.country}`);
const peerName = window.generateScreenname ? window.generateScreenname(peer.id) : peer.id.slice(-8);
marker.bindPopup(`<b>${peerName}</b><br>${loc.city}, ${loc.country}`);
peerMarkers[peer.id] = marker;
}
}
@@ -319,7 +320,16 @@ const appendMessage = (msg) => {
if (msg.type === 'CHAT') {
const senderColor = getColorFromId(msg.sender);
const senderName = msg.sender === myId ? 'You' : msg.sender.slice(-4);
let senderName = 'Unknown';
if (window.generateScreenname) {
senderName = window.generateScreenname(msg.sender);
} else {
senderName = msg.sender.slice(-8);
}
if (msg.sender === myId) senderName = 'You';
const scopeLabel = msg.scope === 'GLOBAL' ? '[GLOBAL] ' : '';
const senderSpan = document.createElement('span');
@@ -430,7 +440,18 @@ evtSource.onmessage = (event) => {
}
}
if (data.id) myId = data.id;
if (data.id) {
myId = data.id;
const diagIdEl = document.getElementById('diag-id');
if (diagIdEl && diagIdEl.innerText === '{{FULL_ID}}') {
diagIdEl.innerText = data.id;
}
}
if (data.screenname) {
const screennameEl = document.getElementById('my-screenname');
if (screennameEl) screennameEl.innerText = data.screenname;
}
if (data.peers) {
lastPeerData = data.peers;
+7 -1
View File
@@ -7,6 +7,8 @@
<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>
</head>
<body>
<canvas id="network" data-visual-limit="{{VISUAL_LIMIT}}"></canvas>
@@ -17,7 +19,7 @@
powered by <a href="https://github.com/lklynet/hypermind" target="_blank">hypermind</a>
</div>
<div class="debug">
ID: {{ID}}<br>
ID: <span id="my-screenname">{{ID}}</span><br>
Direct Connections: <span id="direct">{{DIRECT}}</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>
@@ -35,6 +37,10 @@
<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; text-overflow: ellipsis; 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>
+3 -1
View File
@@ -1,9 +1,11 @@
const crypto = require("crypto");
const { MY_POW_PREFIX } = require("../config/constants");
const { generateScreenname } = require("../utils/name-generator");
const generateIdentity = () => {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const id = publicKey.export({ type: "spki", format: "der" }).toString("hex");
const screenname = generateScreenname(id);
let nonce = 0;
while (true) {
@@ -15,7 +17,7 @@ const generateIdentity = () => {
nonce++;
}
return { publicKey, privateKey, id, nonce };
return { publicKey, privateKey, id, nonce, screenname };
}
module.exports = { generateIdentity };
+3 -2
View File
@@ -6,6 +6,7 @@ const {
const crypto = require("crypto");
const { MAX_RELAY_HOPS, ENABLE_CHAT, CHAT_RATE_LIMIT } = require("../config/constants");
const { BloomFilterManager } = require("../state/bloom");
const { generateScreenname } = require("../utils/name-generator");
class MessageHandler {
constructor(
@@ -93,7 +94,7 @@ class MessageHandler {
if (ENABLE_CHAT && this.chatSystemFn && hops === 0) {
this.chatSystemFn({
type: "SYSTEM",
content: `Connection established with Node ...${id.slice(-8)}`,
content: `Connection established with Node [${generateScreenname(id)}]`,
timestamp: Date.now(),
});
}
@@ -134,7 +135,7 @@ class MessageHandler {
if (ENABLE_CHAT && this.chatSystemFn && hops === 0) {
this.chatSystemFn({
type: "SYSTEM",
content: `Node ...${id.slice(-8)} disconnected.`,
content: `Node [${generateScreenname(id)}] disconnected.`,
timestamp: Date.now(),
});
}
+1
View File
@@ -1,6 +1,7 @@
const Hyperswarm = require("hyperswarm");
const { signMessage } = require("../core/security");
const { TOPIC, TOPIC_NAME, HEARTBEAT_INTERVAL, MAX_CONNECTIONS, CONNECTION_ROTATION_INTERVAL, ENABLE_CHAT } = require("../config/constants");
const { generateScreenname } = require("../utils/name-generator");
class SwarmManager {
constructor(identity, peerManager, diagnostics, messageHandler, relayFn, broadcastFn, chatSystemFn) {
File diff suppressed because one or more lines are too long
+91
View File
@@ -0,0 +1,91 @@
const adjectives = require("./adjectives.json");
const nouns = require("./nouns.json");
const hashStr = (str) => {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i);
}
return hash >>> 0;
};
class SeededRandom {
constructor(seedStr) {
this.seed = hashStr(seedStr);
}
next() {
const x = Math.sin(this.seed++) * 10000;
return x - Math.floor(x);
}
range(min, max) {
return Math.floor(this.next() * (max - min + 1)) + min;
}
bool(p = 0.5) {
return this.next() < p;
}
pick(array) {
return array[this.range(0, array.length - 1)];
}
}
const toLeet = (str, rng) => {
const map = {
'a': '4', 'e': '3', 'i': '1', 'o': '0', 's': '5', 't': '7'
};
return str.split('').map(char => {
const lower = char.toLowerCase();
if (map[lower] && rng.bool(0.5)) {
return map[lower];
}
if (rng.bool(0.3)) {
return char.toUpperCase();
}
return char;
}).join('');
};
const generateScreenname = (id) => {
if (!id) return "Guest_" + Math.floor(Math.random() * 1000);
const rng = new SeededRandom(id);
const adj = rng.pick(adjectives);
const noun = rng.pick(nouns);
let name = "";
const strategy = rng.range(0, 2);
if (strategy === 0) {
name = `${adj}_${noun}`;
} else if (strategy === 1) {
name = `${adj}${noun.charAt(0).toUpperCase() + noun.slice(1)}`;
} else {
name = `${adj.toUpperCase()}_${noun}`;
}
if (rng.bool(0.4)) {
name = toLeet(name, rng);
}
const wrapStrategy = rng.range(0, 4);
if (wrapStrategy === 0) {
name = `xX${name}Xx`;
} else if (wrapStrategy === 1) {
name = `_${name}_`;
} else if (wrapStrategy === 2) {
name = `${name}${rng.range(1, 999)}`;
}
return name;
};
module.exports = { generateScreenname };
File diff suppressed because one or more lines are too long
+26 -1
View File
@@ -10,16 +10,39 @@ const HTML_TEMPLATE = fs.readFileSync(
"utf-8"
);
// Pre-load lists and generator logic
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");
const setupRoutes = (app, identity, peerManager, swarm, sseManager, diagnostics) => {
app.use(express.json());
// Serve lists
app.get("/js/lists.js", (req, res) => {
res.setHeader("Content-Type", "application/javascript");
res.send(`window.ADJECTIVES = ${adjectives}; window.NOUNS = ${nouns};`);
});
// Serve generator
app.get("/js/screenname.js", (req, res) => {
res.setHeader("Content-Type", "application/javascript");
// Adapt CommonJS module to Browser script
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);
});
app.get("/", (req, res) => {
const count = peerManager.size;
const directPeers = swarm.getSwarm().connections.size;
const html = HTML_TEMPLATE
.replace(/\{\{COUNT\}\}/g, count)
.replace(/\{\{ID\}\}/g, "..." + identity.id.slice(-8))
.replace(/\{\{ID\}\}/g, identity.screenname || "Unknown")
.replace(/\{\{FULL_ID\}\}/g, identity.id)
.replace(/\{\{DIRECT\}\}/g, directPeers)
.replace(/\{\{MAP_CLASS\}\}/g, ENABLE_MAP ? '' : 'hidden')
.replace(/\{\{VISUAL_LIMIT\}\}/g, VISUAL_LIMIT);
@@ -40,6 +63,7 @@ const setupRoutes = (app, identity, peerManager, swarm, sseManager, diagnostics)
totalUnique: peerManager.totalUniquePeers,
direct: swarm.getSwarm().connections.size,
id: identity.id,
screenname: identity.screenname,
diagnostics: diagnostics.getStats(),
chatEnabled: ENABLE_CHAT,
peers: peerManager.getPeersWithIps()
@@ -57,6 +81,7 @@ const setupRoutes = (app, identity, peerManager, swarm, sseManager, diagnostics)
totalUnique: peerManager.totalUniquePeers,
direct: swarm.getSwarm().connections.size,
id: identity.id,
screenname: identity.screenname,
diagnostics: diagnostics.getStats(),
chatEnabled: ENABLE_CHAT,
peers: peerManager.getPeersWithIps()