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 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
+ }
@@ -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>