webchat-irc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/bin/webchat.js +18 -0
- package/index.js +270 -0
- package/package.json +44 -0
- package/public/demo.html +178 -0
- package/public/embed.html +692 -0
- package/public/img/LogoV1.png +0 -0
- package/public/index.html +64 -0
- package/public/style.css +276 -0
- package/webchat.config.example.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# webchat-IRC
|
|
2
|
+
|
|
3
|
+
Bring back the classic web to your website.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install webchat-irc
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Copy the example config into your project directory:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cp node_modules/webchat-irc/webchat.config.example.json webchat.config.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Edit `webchat.config.json` with your IRC server details, then start the server:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx webchat-irc
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Open `http://localhost:3000` to see the results!
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Create a `webchat.config.json` in your project root:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"port": 3000,
|
|
32
|
+
"irc": {
|
|
33
|
+
"host": "irc.libera.chat",
|
|
34
|
+
"port": 6697,
|
|
35
|
+
"tls": true,
|
|
36
|
+
"channel": "#your-channel"
|
|
37
|
+
},
|
|
38
|
+
"guestbook": {
|
|
39
|
+
"enabled": true,
|
|
40
|
+
"maxEntries": 200
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Option | Default | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `port` | `3000` | HTTP server port |
|
|
48
|
+
| `irc.host` | `irc.libera.chat` | IRC server hostname |
|
|
49
|
+
| `irc.port` | `6697` | IRC server port |
|
|
50
|
+
| `irc.tls` | `true` | Use TLS/SSL |
|
|
51
|
+
| `irc.channel` | `#webchatirc-general` | IRC channel to join |
|
|
52
|
+
| `guestbook.enabled` | `true` | Enable/disable the guestbook feature |
|
|
53
|
+
| `guestbook.maxEntries` | `200` | Maximum stored guestbook entries |
|
|
54
|
+
|
|
55
|
+
## Embedding on Your Website
|
|
56
|
+
|
|
57
|
+
Once the server is running, embed the widget on any page using an iframe:
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<iframe
|
|
61
|
+
src="https://your-server.com/embed.html"
|
|
62
|
+
width="400"
|
|
63
|
+
height="500"
|
|
64
|
+
frameborder="0"
|
|
65
|
+
style="border-radius: 6px; border: 1px solid #333;">
|
|
66
|
+
</iframe>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The `/embed.html` widget includes tabbed panels for **Chat** and **Guestbook** — fully self-contained with no external dependencies.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
Clone and run locally:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/byeoon/webchat-irc.git
|
|
78
|
+
cd webchat-irc
|
|
79
|
+
npm install
|
|
80
|
+
cp webchat.config.example.json webchat.config.json
|
|
81
|
+
npm start
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Pages
|
|
85
|
+
|
|
86
|
+
| Route | Description |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `/` | Landing page |
|
|
89
|
+
| `/demo.html` | Full demo with navbar |
|
|
90
|
+
| `/embed.html` | Embeddable widget (chat + guestbook tabs) |
|
package/bin/webchat.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { createServer } = require('../index.js');
|
|
6
|
+
|
|
7
|
+
const configName = 'webchat.config.json';
|
|
8
|
+
const configPath = path.join(process.cwd(), configName);
|
|
9
|
+
|
|
10
|
+
let config = {};
|
|
11
|
+
try {
|
|
12
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
13
|
+
console.log(`[webchatIRC] Loaded config from ${configPath}`);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.log(`[webchatIRC] No ${configName} found in current directory, using defaults.`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
createServer(config);
|
package/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { Server } = require('socket.io');
|
|
6
|
+
const IRC = require('irc-framework');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
port: 3000,
|
|
10
|
+
irc: {
|
|
11
|
+
mode: 'centralized',
|
|
12
|
+
host: 'irc.libera.chat',
|
|
13
|
+
port: 6697,
|
|
14
|
+
tls: true,
|
|
15
|
+
channel: '#webchatirc-general'
|
|
16
|
+
},
|
|
17
|
+
guestbook: {
|
|
18
|
+
enabled: true,
|
|
19
|
+
maxEntries: 200
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create and start a webchatIRC server.
|
|
25
|
+
* @param {object} [userConfig] - Configuration object (merged over defaults)
|
|
26
|
+
* @returns {{ app, server, io }} Express app, HTTP server, and Socket.IO instance
|
|
27
|
+
*/
|
|
28
|
+
function createServer(userConfig) {
|
|
29
|
+
const config = {
|
|
30
|
+
...DEFAULT_CONFIG,
|
|
31
|
+
...userConfig,
|
|
32
|
+
irc: { ...DEFAULT_CONFIG.irc, ...(userConfig && userConfig.irc) },
|
|
33
|
+
guestbook: { ...DEFAULT_CONFIG.guestbook, ...(userConfig && userConfig.guestbook) }
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const app = express();
|
|
37
|
+
const server = http.createServer(app);
|
|
38
|
+
const io = new Server(server);
|
|
39
|
+
|
|
40
|
+
const port = config.port || 3000;
|
|
41
|
+
const ircConfig = config.irc;
|
|
42
|
+
const domainMap = {};
|
|
43
|
+
|
|
44
|
+
// persistence
|
|
45
|
+
const guestbookPath = path.join(process.cwd(), 'guestbook.json');
|
|
46
|
+
let guestbookEntries = [];
|
|
47
|
+
if (config.guestbook.enabled) {
|
|
48
|
+
try {
|
|
49
|
+
guestbookEntries = JSON.parse(fs.readFileSync(guestbookPath, 'utf-8'));
|
|
50
|
+
console.log(`[webchatIRC] Loaded ${guestbookEntries.length} guestbook entries`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
guestbookEntries = [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function saveGuestbook() {
|
|
57
|
+
try {
|
|
58
|
+
fs.writeFileSync(guestbookPath, JSON.stringify(guestbookEntries, null, 2), 'utf-8');
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('[webchatIRC] Failed to save guestbook:', err.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function populateWHOIS(irc, channel, socket) {
|
|
65
|
+
irc.raw('NAMES', channel);
|
|
66
|
+
irc.once('userlist', (event) => {
|
|
67
|
+
if (!event.users) return;
|
|
68
|
+
socket.emit('server_info', {
|
|
69
|
+
server: ircConfig.host,
|
|
70
|
+
userCount: event.users.length
|
|
71
|
+
});
|
|
72
|
+
event.users.forEach((user) => {
|
|
73
|
+
const nick = user.nick;
|
|
74
|
+
if (nick === irc.user.nick) return;
|
|
75
|
+
irc.whois(nick, (info) => {
|
|
76
|
+
domainMap[nick] = info.real_name || 'Unknown';
|
|
77
|
+
socket.emit('whois', { nick, domain: domainMap[nick] });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sanitizeUsername(username) {
|
|
84
|
+
if (!username || typeof username !== 'string') return 'Guest';
|
|
85
|
+
const clean = username.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 16);
|
|
86
|
+
return clean || 'Guest';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sanitizeDomain(domain) {
|
|
90
|
+
if (!domain || typeof domain !== 'string') return 'Unknown';
|
|
91
|
+
const clean = domain.replace(/[^a-zA-Z0-9.\-:]/g, '').slice(0, 64);
|
|
92
|
+
return clean || 'Unknown';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
io.on('connection', (socket) => {
|
|
96
|
+
console.log('[webchatIRC] Client connected');
|
|
97
|
+
|
|
98
|
+
socket.on('register', ({ domain, username }) => {
|
|
99
|
+
if (socket.irc) {
|
|
100
|
+
socket.emit('error_msg', 'Already connected.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cleanUser = sanitizeUsername(username);
|
|
105
|
+
const cleanDomain = sanitizeDomain(domain);
|
|
106
|
+
const nick = `${cleanUser}_${Math.floor(Math.random() * 10000)}`;
|
|
107
|
+
const irc = new IRC.Client();
|
|
108
|
+
irc.userDomain = cleanDomain;
|
|
109
|
+
|
|
110
|
+
irc.connect({
|
|
111
|
+
host: ircConfig.host,
|
|
112
|
+
port: ircConfig.port,
|
|
113
|
+
nick: nick,
|
|
114
|
+
username: cleanUser,
|
|
115
|
+
gecos: cleanDomain,
|
|
116
|
+
tls: ircConfig.tls
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
irc.on('registered', () => {
|
|
120
|
+
const channel = ircConfig.channel;
|
|
121
|
+
irc.join(channel);
|
|
122
|
+
socket.emit('system', `Connected to IRC as ${nick}`);
|
|
123
|
+
|
|
124
|
+
irc.whois(nick, (info) => {
|
|
125
|
+
domainMap[nick] = info.real_name || cleanDomain;
|
|
126
|
+
});
|
|
127
|
+
populateWHOIS(irc, channel, socket);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
irc.on('message', (event) => {
|
|
131
|
+
if (event.nick === irc.user.nick) return;
|
|
132
|
+
if (!event.nick || !domainMap[event.nick]) return;
|
|
133
|
+
|
|
134
|
+
const senderDomain = domainMap[event.nick];
|
|
135
|
+
socket.emit('message', {
|
|
136
|
+
channel: event.target,
|
|
137
|
+
nick: event.nick,
|
|
138
|
+
text: event.message,
|
|
139
|
+
domain: senderDomain,
|
|
140
|
+
timestamp: Date.now()
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
irc.on('notice', (event) => {
|
|
145
|
+
if (event.message) {
|
|
146
|
+
socket.emit('system', event.message);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
irc.on('join', (event) => {
|
|
151
|
+
socket.emit('system', `${event.nick} joined ${event.channel}`);
|
|
152
|
+
socket.emit('user_joined');
|
|
153
|
+
if (event.nick === irc.user.nick) return;
|
|
154
|
+
irc.whois(event.nick, (info) => {
|
|
155
|
+
domainMap[event.nick] = info.real_name || 'Unknown';
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
irc.on('part', (event) => {
|
|
160
|
+
socket.emit('system', `${event.nick} left ${event.channel}`);
|
|
161
|
+
socket.emit('user_left');
|
|
162
|
+
delete domainMap[event.nick];
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
irc.on('quit', (event) => {
|
|
166
|
+
socket.emit('system', `${event.nick} quit IRC`);
|
|
167
|
+
socket.emit('user_left');
|
|
168
|
+
delete domainMap[event.nick];
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
irc.on('error', (event) => {
|
|
172
|
+
console.error('[webchatIRC] IRC error:', event);
|
|
173
|
+
socket.emit('error_msg', `IRC error: ${event.reason || event.message || 'Unknown error'}`);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
irc.on('close', () => {
|
|
177
|
+
socket.emit('system', 'Disconnected from IRC.');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
socket.irc = irc;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
socket.on('chat', (text) => {
|
|
184
|
+
if (!socket.irc) return;
|
|
185
|
+
if (typeof text !== 'string' || !text.trim()) return;
|
|
186
|
+
|
|
187
|
+
const cleanText = text.trim().slice(0, 500);
|
|
188
|
+
const channel = ircConfig.channel;
|
|
189
|
+
|
|
190
|
+
socket.irc.say(channel, cleanText);
|
|
191
|
+
socket.emit('message', {
|
|
192
|
+
channel: channel,
|
|
193
|
+
nick: socket.irc.user.nick,
|
|
194
|
+
text: cleanText,
|
|
195
|
+
domain: socket.irc.userDomain,
|
|
196
|
+
timestamp: Date.now(),
|
|
197
|
+
self: true
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Guestbook events
|
|
202
|
+
if (config.guestbook.enabled) {
|
|
203
|
+
socket.on('guestbook:load', () => {
|
|
204
|
+
socket.emit('guestbook:entries', guestbookEntries);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
socket.on('guestbook:sign', ({ name, message }) => {
|
|
208
|
+
if (!name || typeof name !== 'string') return;
|
|
209
|
+
if (!message || typeof message !== 'string') return;
|
|
210
|
+
|
|
211
|
+
const ip = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address;
|
|
212
|
+
const alreadySigned = guestbookEntries.some(e => e.ip === ip);
|
|
213
|
+
if (alreadySigned) {
|
|
214
|
+
socket.emit('guestbook:error', 'You have already signed the guestbook!');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const entry = {
|
|
219
|
+
name: name.replace(/[^a-zA-Z0-9_ -]/g, '').slice(0, 32),
|
|
220
|
+
message: message.trim().slice(0, 200),
|
|
221
|
+
domain: socket.handshake.headers.origin || 'Unknown',
|
|
222
|
+
date: new Date().toISOString(),
|
|
223
|
+
ip: ip
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
guestbookEntries.push(entry);
|
|
227
|
+
if (guestbookEntries.length > config.guestbook.maxEntries) {
|
|
228
|
+
guestbookEntries = guestbookEntries.slice(-config.guestbook.maxEntries);
|
|
229
|
+
}
|
|
230
|
+
saveGuestbook();
|
|
231
|
+
|
|
232
|
+
// Broadcast without IP (important!!)
|
|
233
|
+
const publicEntry = { name: entry.name, message: entry.message, domain: entry.domain, date: entry.date };
|
|
234
|
+
io.emit('guestbook:new', publicEntry);
|
|
235
|
+
socket.emit('guestbook:signed');
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
socket.on('disconnect', () => {
|
|
240
|
+
if (socket.irc) {
|
|
241
|
+
const nick = socket.irc.user ? socket.irc.user.nick : null;
|
|
242
|
+
socket.irc.quit('webchatIRC Client Disconnected');
|
|
243
|
+
if (nick) delete domainMap[nick];
|
|
244
|
+
}
|
|
245
|
+
console.log('[webchatIRC] Client disconnected');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
250
|
+
|
|
251
|
+
server.listen(port, () => {
|
|
252
|
+
console.log(`[webchatIRC] Server running at http://localhost:${port}`);
|
|
253
|
+
console.log(`[webchatIRC] IRC: ${ircConfig.host}:${ircConfig.port} ${ircConfig.channel}`);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return { app, server, io };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = { createServer };
|
|
260
|
+
|
|
261
|
+
if (require.main === module) {
|
|
262
|
+
const configPath = path.join(__dirname, 'webchat.config.json');
|
|
263
|
+
let fileConfig = {};
|
|
264
|
+
try {
|
|
265
|
+
fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.log('[webchatIRC] No webchat.config.json found, using defaults.');
|
|
268
|
+
}
|
|
269
|
+
createServer(fileConfig);
|
|
270
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "webchat-irc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interlinked IRC for your website.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"irc",
|
|
7
|
+
"chat",
|
|
8
|
+
"webchat",
|
|
9
|
+
"embed",
|
|
10
|
+
"widget"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/byeoon/webchat-irc#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/byeoon/webchat-irc/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/byeoon/webchat-irc.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"author": "byeoon",
|
|
22
|
+
"type": "commonjs",
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"bin": {
|
|
25
|
+
"webchat-irc": "./bin/webchat.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin/",
|
|
29
|
+
"public/",
|
|
30
|
+
"index.js",
|
|
31
|
+
"webchat.config.example.json",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"start": "node bin/webchat.js",
|
|
36
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"express": "^5.2.1",
|
|
40
|
+
"irc-framework": "^4.14.0",
|
|
41
|
+
"socket.io": "^4.8.3",
|
|
42
|
+
"socket.io-client": "^4.8.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/public/demo.html
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>webchat - IRC for your website</title>
|
|
8
|
+
<link rel="stylesheet" href="https://getbootstrap.com/1.4.0/assets/css/bootstrap.min.css">
|
|
9
|
+
<link rel="stylesheet" href="style.css">
|
|
10
|
+
</head>
|
|
11
|
+
|
|
12
|
+
<body>
|
|
13
|
+
<div class="topbar">
|
|
14
|
+
<div class="fill">
|
|
15
|
+
<div class="container">
|
|
16
|
+
<a class="brand" href="#">webchat IRC</a>
|
|
17
|
+
<ul class="nav">
|
|
18
|
+
<li><a href="/index.html">Home</a></li>
|
|
19
|
+
<li><a href="#about">Setup</a></li>
|
|
20
|
+
<li class="active"><a href="/demo.html">Demo</a></li>
|
|
21
|
+
</ul>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="chat-wrapper">
|
|
27
|
+
<div class="username-overlay" id="usernameOverlay">
|
|
28
|
+
<div class="username-form">
|
|
29
|
+
<h2>Enter webchatIRC Demo</h2>
|
|
30
|
+
<p class="form-subtitle">Pick a name and start chatting</p>
|
|
31
|
+
<input type="text" id="usernameInput" class="username-input" placeholder="Enter your name..."
|
|
32
|
+
maxlength="16" autocomplete="off">
|
|
33
|
+
<button class="btn primary" id="connectBtn">Connect</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="webchatcontainer" id="chatContainer">
|
|
38
|
+
<div class="chat-header">
|
|
39
|
+
<div class="chat-header-left">
|
|
40
|
+
<h1>webchat-IRC</h1>
|
|
41
|
+
<span class="channel-label" id="channelLabel">#webchatirc-general</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="chat-header-right">
|
|
44
|
+
<span class="server-label" id="serverLabel"></span>
|
|
45
|
+
<span class="users-label" id="usersLabel"></span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="divider"></div>
|
|
49
|
+
<div id="log">
|
|
50
|
+
<ul id="chat-list"></ul>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="chatbox-wrapper" id="chatboxWrapper" style="display: none;">
|
|
55
|
+
<input type="text" class="chatbox" id="chatInput" placeholder="Type a message..." maxlength="500"
|
|
56
|
+
autocomplete="off" disabled>
|
|
57
|
+
<button class="send-btn" id="sendBtn" disabled>Send</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<footer>
|
|
62
|
+
<p>Open source on <b><a href="https://github.com/byeoon/webchatIRC">GitHub</a></b>!</p>
|
|
63
|
+
</footer>
|
|
64
|
+
|
|
65
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
66
|
+
<script>
|
|
67
|
+
const chatList = document.getElementById('chat-list');
|
|
68
|
+
const logContainer = document.getElementById('log');
|
|
69
|
+
const usernameOverlay = document.getElementById('usernameOverlay');
|
|
70
|
+
const usernameInput = document.getElementById('usernameInput');
|
|
71
|
+
const connectBtn = document.getElementById('connectBtn');
|
|
72
|
+
const chatInput = document.getElementById('chatInput');
|
|
73
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
74
|
+
const chatboxWrapper = document.getElementById('chatboxWrapper');
|
|
75
|
+
const channelLabel = document.getElementById('channelLabel');
|
|
76
|
+
const serverLabel = document.getElementById('serverLabel');
|
|
77
|
+
const usersLabel = document.getElementById('usersLabel');
|
|
78
|
+
|
|
79
|
+
const socket = io();
|
|
80
|
+
let connected = false;
|
|
81
|
+
let currentUserCount = 0;
|
|
82
|
+
|
|
83
|
+
function formatTime(ts) {
|
|
84
|
+
const d = ts ? new Date(ts) : new Date();
|
|
85
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function escapeHTML(str) {
|
|
89
|
+
const div = document.createElement('div');
|
|
90
|
+
div.textContent = str;
|
|
91
|
+
return div.innerHTML;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function addSystemMessage(text) {
|
|
95
|
+
const li = document.createElement('li');
|
|
96
|
+
li.classList.add('system-msg');
|
|
97
|
+
li.innerHTML = `<span class="timestamp">${formatTime()}</span> <span class="system-text">* ${escapeHTML(text)}</span>`;
|
|
98
|
+
chatList.appendChild(li);
|
|
99
|
+
logContainer.scrollTop = logContainer.scrollHeight;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addChatMessage(msg) {
|
|
103
|
+
const li = document.createElement('li');
|
|
104
|
+
li.classList.add('chat-msg');
|
|
105
|
+
if (msg.self) li.classList.add('self-msg');
|
|
106
|
+
|
|
107
|
+
li.innerHTML = `<span class="timestamp">${formatTime(msg.timestamp)}</span> <span class="domain">[${escapeHTML(msg.domain)}]</span> <span class="nick">${escapeHTML(msg.nick)}</span>: <span class="text">${escapeHTML(msg.text)}</span>`;
|
|
108
|
+
chatList.appendChild(li);
|
|
109
|
+
logContainer.scrollTop = logContainer.scrollHeight;
|
|
110
|
+
}
|
|
111
|
+
socket.on('system', (text) => addSystemMessage(text));
|
|
112
|
+
socket.on('message', (msg) => addChatMessage(msg));
|
|
113
|
+
socket.on('error_msg', (text) => {
|
|
114
|
+
addSystemMessage(`ERROR: ${text}`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
socket.on('server_info', (data) => {
|
|
118
|
+
serverLabel.textContent = `Server: ${data.server}`;
|
|
119
|
+
currentUserCount = data.userCount;
|
|
120
|
+
usersLabel.textContent = `${currentUserCount} user${currentUserCount !== 1 ? 's' : ''}`;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
socket.on('user_joined', () => {
|
|
124
|
+
if (currentUserCount > 0) {
|
|
125
|
+
currentUserCount++;
|
|
126
|
+
usersLabel.textContent = `${currentUserCount} user${currentUserCount !== 1 ? 's' : ''}`;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
socket.on('user_left', () => {
|
|
131
|
+
if (currentUserCount > 0) {
|
|
132
|
+
currentUserCount--;
|
|
133
|
+
usersLabel.textContent = `${currentUserCount} user${currentUserCount !== 1 ? 's' : ''}`;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
function doConnect() {
|
|
138
|
+
const username = usernameInput.value.trim() || 'Guest';
|
|
139
|
+
socket.emit('register', {
|
|
140
|
+
domain: window.location.hostname,
|
|
141
|
+
username: username
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
usernameOverlay.classList.add('hidden');
|
|
145
|
+
chatboxWrapper.style.display = 'flex';
|
|
146
|
+
chatInput.disabled = false;
|
|
147
|
+
sendBtn.disabled = false;
|
|
148
|
+
chatInput.focus();
|
|
149
|
+
connected = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
connectBtn.addEventListener('click', doConnect);
|
|
153
|
+
usernameInput.addEventListener('keydown', (e) => {
|
|
154
|
+
if (e.key === 'Enter') doConnect();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
usernameInput.focus();
|
|
158
|
+
|
|
159
|
+
function sendMessage() {
|
|
160
|
+
const text = chatInput.value.trim();
|
|
161
|
+
if (!text || !connected) return;
|
|
162
|
+
socket.emit('chat', text);
|
|
163
|
+
chatInput.value = '';
|
|
164
|
+
chatInput.focus();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
chatInput.addEventListener('keydown', (e) => {
|
|
168
|
+
if (e.key === 'Enter') {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
sendMessage();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
175
|
+
</script>
|
|
176
|
+
</body>
|
|
177
|
+
|
|
178
|
+
</html>
|