Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ Add this to your `services.yaml`:
| --- | --- | --- |
| `PORT` | `3000` | The port the web dashboard listens on. Since `--network host` is used, this port opens directly on the host. |
| `MAX_PEERS` | `1000000` | Maximum number of peers to track in the swarm. Unless you're expecting the entire internet to join, the default is probably fine. |
| `ENABLE_CHAT` | `false` | Set to `true` to enable the ephemeral P2P chat terminal. |

## » Features

### 1. The Counter
It counts. That's the main thing.

### 2. Ephemeral Chat
**New:** A completely decentralized, ephemeral chat system built directly on top of the swarm topology.

* **Ephemeral:** No database. No history. If you refresh, it's gone.
* **Restricted:** You can only talk to your ~32 direct connections.
* **Chaotic:** Every 30 seconds, the network rotates your connections. You might be mid-sentence and—*poof*—your audience changes.
* **Anonymous:** You are identified only by the last 4 characters of your node ID.

To enable this feature, set `ENABLE_CHAT=true`.

## » Usage

Expand Down
103 changes: 103 additions & 0 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,114 @@ document.addEventListener('keydown', (e) => {
}
});

const terminal = document.getElementById('terminal');
const terminalOutput = document.getElementById('terminal-output');
const terminalInput = document.getElementById('terminal-input');
const promptEl = document.querySelector('.prompt');
let myId = null;
let myChatHistory = [];

const updatePromptStatus = () => {
const now = Date.now();
myChatHistory = myChatHistory.filter(t => now - t < 10000);

if (myChatHistory.length >= 5) {
promptEl.style.color = 'orange';
} else {
promptEl.style.color = '#4ade80';
}
};

setInterval(updatePromptStatus, 500);

const getColorFromId = (id) => {
if (!id) return '#666';
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = id.charCodeAt(i) + ((hash << 5) - hash);
}
const c = (hash & 0x00FFFFFF).toString(16).toUpperCase();
return '#' + "00000".substring(0, 6 - c.length) + c;
}

const appendMessage = (msg) => {
const div = document.createElement('div');

if (msg.type === 'SYSTEM') {
div.className = 'msg-system';
div.innerText = `[SYSTEM] ${msg.content}`;
} else if (msg.type === 'CHAT') {
const senderColor = getColorFromId(msg.sender);
const senderName = msg.sender === myId ? 'You' : msg.sender.slice(-4);

const senderSpan = document.createElement('span');
senderSpan.className = 'msg-sender';
senderSpan.style.color = senderColor;
senderSpan.innerText = `[${senderName}]`;

const contentSpan = document.createElement('span');
contentSpan.className = 'msg-content';
contentSpan.innerText = ` > ${msg.content}`;

div.appendChild(senderSpan);
div.appendChild(contentSpan);
}

terminalOutput.appendChild(div);
terminalOutput.scrollTop = terminalOutput.scrollHeight;
}

terminalInput.addEventListener('keypress', async (e) => {
if (e.key === 'Enter') {
const content = terminalInput.value.trim();
if (!content) return;

terminalInput.value = '';

try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});

if (res.ok) {
myChatHistory.push(Date.now());
updatePromptStatus();
} else if (res.status === 429) {
// Force update if we hit the limit unexpectedly
// Add a dummy timestamp to force the limit state if not already there
if (myChatHistory.length < 5) {
myChatHistory.push(Date.now());
}
updatePromptStatus();
}
} catch (err) {
console.error('Failed to send message', err);
}
}
});

const evtSource = new EventSource("/events");

evtSource.onmessage = (event) => {
const data = JSON.parse(event.data);

if (data.type === 'CHAT' || data.type === 'SYSTEM') {
appendMessage(data);
return;
}

if (data.chatEnabled) {
terminal.classList.remove('hidden');
document.body.classList.add('chat-active');
} else {
terminal.classList.add('hidden');
document.body.classList.remove('chat-active');
}

if (data.id) myId = data.id;

updateParticles(data.count);

if (countEl.innerText != data.count) {
Expand Down
7 changes: 7 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@
</div>
</div>

<div id="terminal" class="terminal hidden">
<div id="terminal-output" class="terminal-output"></div>
<div class="terminal-input-line">
<span class="prompt">&gt;</span>
<input type="text" id="terminal-input" maxlength="140" placeholder="Broadcast to direct peers..." autocomplete="off">
</div>
</div>

<script src="/app.js"></script>
</body>
Expand Down
94 changes: 94 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ body {
background: #111;
color: #eee;
margin: 0;
transition: padding-bottom 0.3s ease;
}

body.chat-active {
padding-bottom: 250px;
}

.container { text-align: center; position: relative; z-index: 10; }
Expand Down Expand Up @@ -85,3 +90,92 @@ a { color: #4b5563; text-decoration: none; border-bottom: 1px dotted #4b5563; }
color: #333;
margin-top: 1rem;
}

.terminal {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 800px;
max-width: 100%;
height: 250px;
background: rgba(0, 0, 0, 0.9);
border: 1px solid #333;
border-bottom: none;
border-radius: 8px 8px 0 0;
z-index: 100;
font-family: "Courier New", Courier, monospace;
display: flex;
flex-direction: column;
padding: 12px;
color: #4ade80;
font-size: 12px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
}

.terminal.hidden {
display: none;
}

.terminal-output {
flex: 1;
overflow-y: auto;
margin-bottom: 5px;
display: flex;
flex-direction: column;
gap: 2px;
scrollbar-width: thin;
scrollbar-color: #333 transparent;
}

.terminal-output::-webkit-scrollbar {
width: 6px;
}

.terminal-output::-webkit-scrollbar-track {
background: transparent;
}

.terminal-output::-webkit-scrollbar-thumb {
background-color: #333;
border-radius: 3px;
}

.terminal-output::-webkit-scrollbar-thumb:hover {
background-color: #444;
}

.terminal-input-line {
display: flex;
align-items: center;
border-top: 1px solid #333;
padding-top: 10px;
}

.prompt {
margin-right: 10px;
color: #4ade80;
}

#terminal-input {
flex: 1;
background: transparent;
border: none;
color: #fff;
font-family: inherit;
font-size: inherit;
outline: none;
}

.msg-system {
color: #666;
font-style: italic;
}

.msg-sender {
font-weight: bold;
}

.msg-content {
color: #ddd;
}
18 changes: 15 additions & 3 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { relayMessage } = require("./src/p2p/relay");
const { SwarmManager } = require("./src/p2p/swarm");
const { SSEManager } = require("./src/web/sse");
const { createServer, startServer } = require("./src/web/server");
const { DIAGNOSTICS_INTERVAL } = require("./src/config/constants");
const { DIAGNOSTICS_INTERVAL, ENABLE_CHAT } = require("./src/config/constants");

const main = async () => {
const identity = generateIdentity();
Expand All @@ -25,14 +25,25 @@ const main = async () => {
direct: swarmManager.getSwarm().connections.size,
id: identity.id,
diagnostics: diagnostics.getStats(),
chatEnabled: ENABLE_CHAT,
});
};

const chatCallback = (msg) => {
sseManager.broadcast(msg);
};

const chatSystemFn = (msg) => {
sseManager.broadcast(msg);
};

const messageHandler = new MessageHandler(
peerManager,
diagnostics,
(msg, sourceSocket) => relayMessage(msg, sourceSocket, swarmManager.getSwarm(), diagnostics),
broadcastUpdate
broadcastUpdate,
chatCallback,
chatSystemFn
);

const swarmManager = new SwarmManager(
Expand All @@ -41,7 +52,8 @@ const main = async () => {
diagnostics,
messageHandler,
(msg, sourceSocket) => relayMessage(msg, sourceSocket, swarmManager.getSwarm(), diagnostics),
broadcastUpdate
broadcastUpdate,
chatSystemFn
);

await swarmManager.start();
Expand Down
4 changes: 4 additions & 0 deletions src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const PEER_TIMEOUT = 15000;
const BROADCAST_THROTTLE = 1000;
const DIAGNOSTICS_INTERVAL = 10000;
const PORT = process.env.PORT || 3000;
const ENABLE_CHAT = process.env.ENABLE_CHAT === 'true';
const CHAT_RATE_LIMIT = 5000;

module.exports = {
TOPIC_NAME,
Expand All @@ -39,4 +41,6 @@ module.exports = {
BROADCAST_THROTTLE,
DIAGNOSTICS_INTERVAL,
PORT,
ENABLE_CHAT,
CHAT_RATE_LIMIT,
};
Loading