Skip to content

Commit 69fcf42

Browse files
committed
feat: add IRC client to web demo
1 parent eeac9cf commit 69fcf42

8 files changed

Lines changed: 571 additions & 11 deletions

File tree

Justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,10 @@ run-web-peer-autopeer:
8787
ws_peer_url="${WS_PEER_URL:-ws://127.0.0.1:${ws_port}}"; \
8888
countries="$(jq -r '[.peers[].country] | unique | join(",")' ygglib/autopeer/builtin_peers_generated.json)"; \
8989
go run ./yggd/yggd -genconf -json \
90-
| jq --arg admin "${ADMIN_LISTEN:-tcp://localhost:9001}" --arg admin_web "${ADMIN_WEB_LISTEN:-127.0.0.1:9003}" --arg ws_listen "ws://${ws_listen}?origin=*" --arg countries "${countries}" --arg schemes "${AUTOPEER_SCHEMES:-tls,tcp,quic,ws}" '.AdminListen = $admin | .AdminWebListen = $admin_web | .TunType = "none" | .IfName = "none" | .Listen = [$ws_listen] | .Peers = [] | .InterfacePeers = {} | .MulticastInterfaces = [] | .AutoPeer.Enabled = true | .AutoPeer.Sources = ["BUILTIN"] | .AutoPeer.FetchInterval = "1h" | .AutoPeer.CheckInterval = "5s" | .AutoPeer.MinimumConnected = 1 | .AutoPeer.MinimumConnectedFromFetch = 1 | .AutoPeer.Countries = ($countries | split(",") | map(select(. != ""))) | .AutoPeer.TransportSchemes = ($schemes | split(",") | map(gsub("^ +| +$"; "") | select(. != "")))' \
90+
| jq --arg admin "${ADMIN_LISTEN:-tcp://localhost:9004}" --arg admin_web "${ADMIN_WEB_LISTEN:-127.0.0.1:9005}" --arg ws_listen "ws://${ws_listen}?origin=*" --arg countries "${countries}" --arg schemes "${AUTOPEER_SCHEMES:-tls,tcp,quic,ws}" '.AdminListen = $admin | .AdminWebListen = $admin_web | .TunType = "none" | .IfName = "none" | .Listen = [$ws_listen] | .Peers = [] | .InterfacePeers = {} | .MulticastInterfaces = [] | .AutoPeer.Enabled = true | .AutoPeer.Sources = ["BUILTIN"] | .AutoPeer.FetchInterval = "1h" | .AutoPeer.CheckInterval = "5s" | .AutoPeer.MinimumConnected = 1 | .AutoPeer.MinimumConnectedFromFetch = 1 | .AutoPeer.Countries = ($countries | split(",") | map(select(. != ""))) | .AutoPeer.TransportSchemes = ($schemes | split(",") | map(gsub("^ +| +$"; "") | select(. != "")))' \
9191
>"${cfg}"; \
9292
echo "Admin: ${ADMIN_LISTEN:-tcp://localhost:9001}"; \
93-
echo "Admin web: http://${ADMIN_WEB_LISTEN:-127.0.0.1:9003}/"; \
93+
echo "Admin web: http://${ADMIN_WEB_LISTEN:-127.0.0.1:9005}/"; \
9494
echo "WebSocket listener: ws://${ws_listen}?origin=*"; \
9595
echo "Web demo manual peer: ${ws_peer_url}"; \
9696
go run ./yggd/yggd -useconffile "${cfg}" -logto stdout -loglevel "${LOGLEVEL:-info}"

go.work

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
go 1.25.5
1+
go 1.26.2
22

33
use (
44
./examples

web/app.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
let wasmReady = false;
22
let nodeRunning = false;
3+
let ircConnected = false;
34

45
const statusEl = document.getElementById('runtimeStatus');
56
const responseEl = document.getElementById('response');
67

8+
function randomIRCName(prefix) {
9+
const bytes = new Uint8Array(4);
10+
crypto.getRandomValues(bytes);
11+
const suffix = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
12+
return `${prefix}${suffix}`;
13+
}
14+
15+
function setRandomIRCIdentity() {
16+
const nick = randomIRCName('ygg');
17+
document.getElementById('ircNick').value = nick;
18+
document.getElementById('ircUsername').value = randomIRCName('web');
19+
}
20+
721
function appendLog(line) {
822
const logs = document.getElementById('logs');
923
if (!logs) {
@@ -18,6 +32,28 @@ function appendLog(line) {
1832

1933
window.yggDemoAppendLog = appendLog;
2034

35+
function appendIRC(kind, at, line) {
36+
const chat = document.getElementById('ircMessages');
37+
if (!chat) {
38+
return;
39+
}
40+
const atBottom = chat.scrollTop + chat.clientHeight >= chat.scrollHeight - 8;
41+
const row = document.createElement('div');
42+
row.className = `irc-line irc-${kind || 'in'}`;
43+
const time = document.createElement('span');
44+
time.className = 'irc-time';
45+
time.textContent = at || '';
46+
const text = document.createElement('span');
47+
text.textContent = line || '';
48+
row.append(time, text);
49+
chat.append(row);
50+
if (atBottom) {
51+
chat.scrollTop = chat.scrollHeight;
52+
}
53+
}
54+
55+
window.yggDemoAppendIRC = appendIRC;
56+
2157
function setStatus(kind, text) {
2258
statusEl.className = `status ${kind}`;
2359
statusEl.textContent = text;
@@ -28,6 +64,9 @@ function syncButtons() {
2864
document.getElementById('stopNode').disabled = !wasmReady || !nodeRunning;
2965
document.getElementById('refreshState').disabled = !wasmReady || !nodeRunning;
3066
document.getElementById('runRequest').disabled = !wasmReady || !nodeRunning;
67+
document.getElementById('ircConnect').disabled = !wasmReady || !nodeRunning;
68+
document.getElementById('ircDisconnect').disabled = !wasmReady || !nodeRunning || !ircConnected;
69+
document.getElementById('ircSend').disabled = !wasmReady || !nodeRunning || !ircConnected;
3170
}
3271

3372
function startConfig() {
@@ -48,6 +87,26 @@ function requestConfig() {
4887
};
4988
}
5089

90+
function ircConnectConfig() {
91+
return {
92+
server: document.getElementById('ircServer').value.trim(),
93+
nick: document.getElementById('ircNick').value.trim(),
94+
username: document.getElementById('ircUsername').value.trim(),
95+
realname: document.getElementById('ircRealname').value.trim(),
96+
channel: document.getElementById('ircChannel').value.trim(),
97+
nickServMode: document.getElementById('nickServMode').value,
98+
nickServPassword: document.getElementById('nickServPassword').value,
99+
nickServEmail: document.getElementById('nickServEmail').value.trim(),
100+
};
101+
}
102+
103+
function ircSendConfig() {
104+
return {
105+
target: document.getElementById('ircTarget').value.trim(),
106+
text: document.getElementById('ircText').value,
107+
};
108+
}
109+
51110
function renderState(state) {
52111
document.getElementById('address').textContent = state.address || '-';
53112
document.getElementById('subnet').textContent = state.subnet || '-';
@@ -109,6 +168,7 @@ async function stopNode() {
109168
await yggDemoStop();
110169
} finally {
111170
nodeRunning = false;
171+
ircConnected = false;
112172
renderState({ peers: [] });
113173
document.getElementById('address').textContent = '-';
114174
document.getElementById('subnet').textContent = '-';
@@ -149,9 +209,66 @@ async function runRequest() {
149209
}
150210
}
151211

212+
async function connectIRC() {
213+
setStatus('loading', 'Connecting IRC');
214+
syncButtons();
215+
try {
216+
await yggDemoIRCConnect(JSON.stringify(ircConnectConfig()));
217+
ircConnected = true;
218+
setStatus('success', 'IRC connected');
219+
await refreshState();
220+
} catch (error) {
221+
ircConnected = false;
222+
appendIRC('error', new Date().toLocaleTimeString(), `Error: ${error.message || error}`);
223+
setStatus('error', `IRC failed: ${error.message || error}`);
224+
}
225+
syncButtons();
226+
}
227+
228+
async function disconnectIRC() {
229+
try {
230+
await yggDemoIRCDisconnect();
231+
} finally {
232+
ircConnected = false;
233+
setStatus('ready', nodeRunning ? 'Node running' : 'Stopped');
234+
syncButtons();
235+
}
236+
}
237+
238+
async function sendIRC() {
239+
const text = document.getElementById('ircText');
240+
try {
241+
await yggDemoIRCSend(JSON.stringify(ircSendConfig()));
242+
text.value = '';
243+
} catch (error) {
244+
appendIRC('error', new Date().toLocaleTimeString(), `Error: ${error.message || error}`);
245+
setStatus('error', `IRC send failed: ${error.message || error}`);
246+
}
247+
}
248+
249+
function syncNickServFields() {
250+
const mode = document.getElementById('nickServMode').value;
251+
document.getElementById('nickServPassword').disabled = mode === 'none';
252+
document.getElementById('nickServEmailLabel').classList.toggle('hidden', mode !== 'register');
253+
}
254+
255+
setRandomIRCIdentity();
256+
syncNickServFields();
152257
document.getElementById('startNode').addEventListener('click', startNode);
153258
document.getElementById('stopNode').addEventListener('click', stopNode);
154259
document.getElementById('refreshState').addEventListener('click', refreshState);
155260
document.getElementById('runRequest').addEventListener('click', runRequest);
261+
document.getElementById('ircConnect').addEventListener('click', connectIRC);
262+
document.getElementById('ircDisconnect').addEventListener('click', disconnectIRC);
263+
document.getElementById('ircSend').addEventListener('click', sendIRC);
264+
document.getElementById('nickServMode').addEventListener('change', syncNickServFields);
265+
document.getElementById('ircText').addEventListener('keydown', (event) => {
266+
if (event.key === 'Enter' && !event.shiftKey) {
267+
event.preventDefault();
268+
if (!document.getElementById('ircSend').disabled) {
269+
sendIRC();
270+
}
271+
}
272+
});
156273
syncButtons();
157274
loadWasm();

web/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
module github.com/asciimoth/ygg/web
22

3-
go 1.25.5
3+
go 1.26.2
44

55
replace github.com/asciimoth/ygg/ygglib => ../ygglib
66

77
require (
88
github.com/asciimoth/gonnect v0.12.0
99
github.com/asciimoth/gonnect-netstack v0.3.15
10+
github.com/asciimoth/irca v0.1.0
1011
github.com/asciimoth/mnlib v0.2.0
1112
github.com/asciimoth/ygg/ygglib v0.0.0
1213
github.com/coder/websocket v1.8.14

web/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ github.com/asciimoth/gonnect v0.12.0 h1:peLCjuJcj4sQNaphxgN+im7FlOVQItRewTkuIvJg
88
github.com/asciimoth/gonnect v0.12.0/go.mod h1:XwXDVDjucVhOa/cifySEx+ij8veNKhtzUTnCdF1p+ug=
99
github.com/asciimoth/gonnect-netstack v0.3.15 h1:DCT111HutpzoPikOndJLC6AuWC+aTYruB2oi376Ygmw=
1010
github.com/asciimoth/gonnect-netstack v0.3.15/go.mod h1:m1cvfzQ3fe0F8P303Cqf9/O8BH6TCXwZPaVJ9qD9lEk=
11+
github.com/asciimoth/irca v0.1.0 h1:7vDps1Ht66lyGgLyfe2/+lut59DTOanP5I3BQHTgPrU=
12+
github.com/asciimoth/irca v0.1.0/go.mod h1:iY/5otigj5ZqsmKDWddVXSitYiUYkxz60fy055Jvq9o=
1113
github.com/asciimoth/mnlib v0.2.0 h1:BLxODGdkqXAhzI7waiCYpn5KDr+B8lHeMUCnGXBUurA=
1214
github.com/asciimoth/mnlib v0.2.0/go.mod h1:PptOOJSRe5JcIXvs6gZk16KGO1A7NIL7JlW34e2ksYE=
1315
github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=

web/index.html

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,72 @@ <h2>HTTP over VTun</h2>
8787
<textarea id="body"></textarea>
8888
</label>
8989
<button id="runRequest" disabled>Run Request</button>
90+
<h2 class="subheading">Response</h2>
91+
<pre id="response"></pre>
92+
</div>
93+
94+
<div class="panel">
95+
<h2>IRC over VTun</h2>
96+
<label>
97+
Server
98+
<input id="ircServer" value="[324:71e:281a:9ed3::41]:6667" placeholder="[300:...]:6667">
99+
</label>
100+
<div class="split">
101+
<label>
102+
Nick
103+
<input id="ircNick" maxlength="32">
104+
</label>
105+
<label>
106+
Channel
107+
<input id="ircChannel" placeholder="#channel">
108+
</label>
109+
</div>
110+
<div class="split">
111+
<label>
112+
Username
113+
<input id="ircUsername" maxlength="32">
114+
</label>
115+
<label>
116+
Real name
117+
<input id="ircRealname" value="Yggdrasil Web IRC">
118+
</label>
119+
</div>
120+
<div class="split nickserv-row">
121+
<label>
122+
NickServ
123+
<select id="nickServMode">
124+
<option value="none">None</option>
125+
<option value="login">Login</option>
126+
<option value="register">Register</option>
127+
</select>
128+
</label>
129+
<label>
130+
Password
131+
<input id="nickServPassword" type="password" autocomplete="current-password">
132+
</label>
133+
</div>
134+
<label id="nickServEmailLabel" class="hidden">
135+
Email
136+
<input id="nickServEmail" type="email" autocomplete="email">
137+
</label>
138+
<p class="note">
139+
ILITA IRC is suggested by default. Other services hosted inside Yggdrasil are listed at
140+
<a href="https://yggdrasil-network.github.io/services.html">yggdrasil-network.github.io</a>.
141+
</p>
142+
<div class="actions two">
143+
<button id="ircConnect" disabled>Connect</button>
144+
<button id="ircDisconnect" disabled>Disconnect</button>
145+
</div>
146+
<label>
147+
Target
148+
<input id="ircTarget" placeholder="#channel or nick">
149+
</label>
150+
<div id="ircMessages" class="irc-messages"></div>
151+
<label>
152+
Message
153+
<textarea id="ircText" placeholder="Message, or /join #channel, /msg nick text, /nick name"></textarea>
154+
</label>
155+
<button id="ircSend" disabled>Send</button>
90156
</div>
91157
</section>
92158

@@ -102,11 +168,6 @@ <h2>Peers</h2>
102168
</table>
103169
</section>
104170

105-
<section class="panel">
106-
<h2>Response</h2>
107-
<pre id="response"></pre>
108-
</section>
109-
110171
<section class="panel">
111172
<h2>Logs</h2>
112173
<div id="logs" class="logs"></div>

0 commit comments

Comments
 (0)