webirc / js /irc.js
AstraOS's picture
Upload 4 files
814ddd5 verified
class IRCClient {
constructor() {
this.ws = null;
this.nickname = '';
this.channels = new Set();
this.callbacks = {
message: () => {},
join: () => {},
part: () => {},
connected: () => {},
error: () => {},
channelUsers: () => {},
topic: () => {},
serverInfo: () => {},
channelList: () => {}
};
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.channelUsers = new Map();
this.channelTopics = new Map();
this.privateMessages = new Map();
this.serverInfo = {
network: 'Libera Chat',
users: 0,
channels: 0,
operators: 0
};
this.pingInterval = null;
this.lastPing = Date.now();
this.connectionState = 'disconnected';
this.globalChannels = new Set();
}
connect(nickname) {
this.nickname = nickname;
this.connectionState = 'connecting';
try {
this.ws = new WebSocket('wss://web.libera.chat/webirc/websocket/');
this.pingInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.send('PING :' + Date.now());
}
}, 30000);
this.ws.onopen = () => {
this.connectionState = 'registering';
this.reconnectAttempts = 0;
this.send(`NICK ${this.nickname}`);
this.send(`USER ${this.nickname} 0 * :${this.nickname}`);
this.send('LUSERS');
this.send('LIST');
};
this.ws.onmessage = (event) => this.handleMessage(event.data);
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
let errorMessage = 'Connection error occurred';
if (error.message) {
errorMessage = error.message;
} else if (error.code) {
errorMessage = `Error code: ${error.code}`;
}
this.callbacks.error(errorMessage);
};
this.ws.onclose = (event) => {
let closeMessage = 'Connection closed';
if (event.code !== 1000) {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
closeMessage = `Connection lost. Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`;
this.callbacks.error(closeMessage);
setTimeout(() => this.connect(this.nickname), 2000);
return;
}
closeMessage = 'Connection closed unexpectedly. Please try reconnecting.';
}
this.callbacks.error(closeMessage);
};
} catch (error) {
this.connectionState = 'error';
console.error('Connection error:', error);
this.callbacks.error('Failed to establish connection: ' + error.message);
}
}
disconnect() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
if (this.ws) {
this.ws.close(1000, 'User initiated disconnect');
}
this.connectionState = 'disconnected';
}
send(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(message + '\r\n');
}
}
join(channel) {
if (!channel.startsWith('#')) channel = '#' + channel;
this.send(`JOIN ${channel}`);
this.channels.add(channel);
}
part(channel) {
this.send(`PART ${channel}`);
this.channels.delete(channel);
}
sendMessage(channel, message) {
this.send(`PRIVMSG ${channel} :${message}`);
}
getChannelUsers(channel) {
return Array.from(this.channelUsers.get(channel) || new Set());
}
getChannelTopic(channel) {
return this.channelTopics.get(channel) || '';
}
sendPrivateMessage(target, message) {
this.send(`PRIVMSG ${target} :${message}`);
if (!this.privateMessages.has(target)) {
this.privateMessages.set(target, []);
}
this.privateMessages.get(target).push({
from: this.nickname,
message,
timestamp: Date.now()
});
}
requestChannelInfo(channel) {
this.send(`MODE ${channel}`);
this.send(`WHO ${channel}`);
this.send(`TOPIC ${channel}`);
}
handleMessage(data) {
const lines = data.split('\r\n');
for (const line of lines) {
if (!line) continue;
if (line.startsWith('PING')) {
this.send('PONG' + line.substring(4));
this.lastPing = Date.now();
continue;
}
const match = line.match(/^(?::([\w.]+) )?([\w]+)(?: (?!:)(.+?))?(?: :(.+))?$/);
if (!match) continue;
const [, prefix, command, params = '', trailing] = match;
const args = params.split(' ').filter(arg => arg);
if (trailing) args.push(trailing);
switch (command) {
case '001':
this.connectionState = 'connected';
this.callbacks.connected();
break;
case '353': {
const channel = args[2];
const users = args[3].split(' ');
if (!this.channelUsers.has(channel)) {
this.channelUsers.set(channel, new Set());
}
users.forEach(user => this.channelUsers.get(channel).add(user));
this.callbacks.channelUsers?.({ channel, users: Array.from(this.channelUsers.get(channel)) });
break;
}
case '332': {
const topicChannel = args[1];
const topic = args[2];
this.channelTopics.set(topicChannel, topic);
this.callbacks.topic?.({ channel: topicChannel, topic });
break;
}
case '333':
break;
case '266': {
const match = trailing.match(/(\d+)/);
if (match) {
this.serverInfo.users = parseInt(match[1]);
this.callbacks.serverInfo?.(this.serverInfo);
}
break;
}
case '322': {
const [channel, userCount, topic] = args;
this.globalChannels.add(channel);
break;
}
case '323': {
this.callbacks.channelList?.(Array.from(this.globalChannels));
break;
}
case 'PRIVMSG': {
const [target, message] = [args[0], args[1]];
const nick = prefix.split('!')[0];
this.callbacks.message({
from: nick,
target,
message,
isAction: message.startsWith('\u0001ACTION') && message.endsWith('\u0001'),
timestamp: Date.now()
});
break;
}
case 'JOIN':
this.callbacks.join({
channel: args[0],
nick: prefix.split('!')[0]
});
this.requestChannelInfo(args[0]);
break;
case 'PART':
this.callbacks.part({
channel: args[0],
nick: prefix.split('!')[0]
});
break;
case 'QUIT':
this.channelUsers.forEach((users, channel) => {
if (users.delete(prefix.split('!')[0])) {
this.callbacks.channelUsers?.({ channel, users: Array.from(users) });
}
});
break;
case 'NICK': {
const oldNick = prefix.split('!')[0];
const newNick = args[0];
if (oldNick === this.nickname) {
this.nickname = newNick;
}
this.channelUsers.forEach((users, channel) => {
if (users.has(oldNick)) {
users.delete(oldNick);
users.add(newNick);
this.callbacks.channelUsers?.({ channel, users: Array.from(users) });
}
});
break;
}
}
}
}
on(event, callback) {
if (this.callbacks.hasOwnProperty(event)) {
this.callbacks[event] = callback;
}
}
onChannelUsers(callback) {
this.callbacks.channelUsers = callback;
}
onTopic(callback) {
this.callbacks.topic = callback;
}
onServerInfo(callback) {
this.callbacks.serverInfo = callback;
}
onChannelList(callback) {
this.callbacks.channelList = callback;
}
}