This commit is contained in:
zefie
2025-06-15 22:40:10 -04:00
parent f3e482a8a1
commit 308cea515e

View File

@@ -40,6 +40,7 @@ class WTVIRC {
this.channels = new Map(); this.channels = new Map();
this.channeltimestamps = new Map(); // channel -> timestamp of creation this.channeltimestamps = new Map(); // channel -> timestamp of creation
this.channelops = new Map(); // channel -> Set of operators this.channelops = new Map(); // channel -> Set of operators
this.channelhalfops = new Map(); // channel -> Set of half-operators
this.channelvoices = new Map(); // channel -> Set of voiced users this.channelvoices = new Map(); // channel -> Set of voiced users
this.channeltopics = new Map(); // channel -> topic this.channeltopics = new Map(); // channel -> topic
this.channelinvites = new Map(); // channel -> Set of invited users this.channelinvites = new Map(); // channel -> Set of invited users
@@ -86,7 +87,7 @@ class WTVIRC {
this.clientpeak = 0; this.clientpeak = 0;
this.globalpeak = 0; this.globalpeak = 0;
this.caps = [ this.caps = [
`AWAYLEN=${this.awaylen} CASEMAPPING=rfc1459 CHANMODES=beI,k,l,itmnpcTVZRrNQO CHANNELLEN=${this.channellen} CHANTYPES=${this.channelprefixes.join('')} PREFIX=(ov)@+ USERMODES=oxizZws MAXLIST=b:${this.maxbans},e:${this.maxexcept},i:${this.maxinvite},k:${this.maxkeylen},l:${this.maxlimit}`, `AWAYLEN=${this.awaylen} CASEMAPPING=rfc1459 CHANMODES=beI,k,l,itmnpcTVZRrNQO CHANNELLEN=${this.channellen} CHANTYPES=${this.channelprefixes.join('')} PREFIX=(ohv)@%+ USERMODES=oxirzZws MAXLIST=b:${this.maxbans},e:${this.maxexcept},i:${this.maxinvite},k:${this.maxkeylen},l:${this.maxlimit}`,
`CHARSET=ascii MODES=3 EXCEPTS=e INVEX=I CHANLIMIT=${this.channelprefixes.join('')}:${this.channellimit} NICKLEN=${this.nicklen} TOPICLEN=${this.topiclen} KICKLEN=${this.kicklen}` `CHARSET=ascii MODES=3 EXCEPTS=e INVEX=I CHANLIMIT=${this.channelprefixes.join('')}:${this.channellimit} NICKLEN=${this.nicklen} TOPICLEN=${this.topiclen} KICKLEN=${this.kicklen}`
]; ];
} }
@@ -393,13 +394,11 @@ class WTVIRC {
break; break;
} }
var oldNick = this.findUserByUniqueId(parts[1]); var oldNick = this.findUserByUniqueId(parts[1]);
var oldTs = parseInt(parts[2]);
var newNick = parts[3]; var newNick = parts[3];
var newTs = parseInt(parts[4]) || Math.floor(Date.now() / 1000);
var targetSocket = this.findSocketByUniqueId(parts[1]); var targetSocket = this.findSocketByUniqueId(parts[1]);
targetSocket.write(`:${targetSocket.nickname}!${targetSocket.username}@${targetSocket.host} NICK :${newNick}\r\n`); targetSocket.write(`:${targetSocket.nickname}!${targetSocket.username}@${targetSocket.host} NICK :${newNick}\r\n`);
this.broadcastUser(oldNick, `:${targetSocket.nickname}!${targetSocket.username}@${targetSocket.host} NICK :${newNick}\r\n`); this.broadcastUser(oldNick, `:${targetSocket.nickname}!${targetSocket.username}@${targetSocket.host} NICK :${newNick}\r\n`);
this.processNickChange(targetSocket, oldNick, newNick); this.processNickChange(targetSocket, newNick);
this.broadcastToAllServers(line, socket); this.broadcastToAllServers(line, socket);
break; break;
case 'SQUIT': case 'SQUIT':
@@ -527,11 +526,11 @@ class WTVIRC {
this.usernames.delete(nick_name); this.usernames.delete(nick_name);
this.uniqueids.delete(nick_name); this.uniqueids.delete(nick_name);
break; break;
} } else if (srvCommand === "GLOBOPS") {
if (srvCommand === "GLOBOPS") {
var message = parts.slice(3).join(' '); var message = parts.slice(3).join(' ');
this.broadcastToAllServers(`:${sourceUniqueId} GLOBOPS :${message}`, socket); this.broadcastToAllServers(`:${sourceUniqueId} GLOBOPS :${message}`, socket);
} break;
} else if (srvCommand === 'PRIVMSG' || srvCommand === 'NOTICE') {
var targetUniqueId = parts[2]; var targetUniqueId = parts[2];
var message = parts.slice(3).join(' '); var message = parts.slice(3).join(' ');
@@ -548,17 +547,45 @@ class WTVIRC {
var sourceNickname = this.getUsernameFromUniqueId(sourceUniqueId); var sourceNickname = this.getUsernameFromUniqueId(sourceUniqueId);
var sourceUsername = this.usernames.get(sourceNickname) || sourceNickname; var sourceUsername = this.usernames.get(sourceNickname) || sourceNickname;
var targetNickname = this.getUsernameFromUniqueId(targetUniqueId); var targetNickname = this.getUsernameFromUniqueId(targetUniqueId);
// Process the command based on the source and target unique IDs
if (srvCommand === 'PRIVMSG' || srvCommand === 'NOTICE') {
if (message.startsWith(':')) { if (message.startsWith(':')) {
message = message.slice(1); // Remove leading ':' message = message.slice(1); // Remove leading ':'
} }
targetSocket.write(`:${sourceNickname}!${sourceUsername}@${sourceSocket.serverinfo.name} ${srvCommand} ${targetNickname} :${message}\r\n`); targetSocket.write(`:${sourceNickname}!${sourceUsername}@${sourceSocket.serverinfo.name} ${srvCommand} ${targetNickname} :${message}\r\n`);
break; break;
} else if (srvCommand === "SVSMODE") {
if (parts.length < 4) {
console.warn('Invalid SVSMODE command from server');
break;
}
var targetUniqueId = parts[2];
var targetSocket = this.findSocketByUniqueId(targetUniqueId);
var modes = parts[4].split('');
let adding = true;
for (const char of modes.join('')) {
if (char === '+') {
adding = true;
} else if (char === '-') {
adding = false;
} else {
let usermodes = this.usermodes.get(targetNickname) || [];
if (usermodes === true) usermodes = [];
if (adding) {
if (!usermodes.includes(char)) {
usermodes.push(char);
}
} else {
usermodes = usermodes.filter(m => m !== char);
}
this.usermodes.set(targetNickname, usermodes);
}
}
targetSocket.write(`:${socket.servername} MODE ${targetSocket.nickname} ${modes.join('')}\r\n`);
this.broadcastToAllServers(`:${sourceUniqueId} SVSMODE ${targetUniqueId} ${modes.join('')}\r\n`, socket);
break;
} }
if (this.debug) { if (this.debug) {
console.log(`Unhandled server command from ${sourceUniqueId} to ${targetUniqueId}: ${message}`); console.log(`Unhandled server command from ${sourceUniqueId} to ${targetUniqueId}: ${srvCommand} ${message}`);
} }
} }
console.warn(`Unknown server command: ${command}`); console.warn(`Unknown server command: ${command}`);
@@ -667,22 +694,28 @@ class WTVIRC {
socket.write(`:${this.servername} 451 ${socket.nickname} :You have not registered\r\n`); socket.write(`:${this.servername} 451 ${socket.nickname} :You have not registered\r\n`);
break; break;
} }
if (!this.channelops.has(channel) || this.channelops.get(channel) === true) { var channel = params[0];
socket.write(`:${this.servername} 482 ${socket.nickname} ${channel} :You're not channel operator\r\n`); var targetNick = params[1];
break; // Check if the user is a channel operator
} else { if (this.channelops.has(channel) && this.channelops.get(channel) instanceof Set && this.channelops.get(channel).has(socket.nickname)) {
if (!this.channelops.get(channel).has(socket.nickname)) { // Allow kick
socket.write(`:${this.servername} 482 ${socket.nickname} ${channel} :You're not channel operator\r\n`); } else if (this.channelhalfops.has(channel) && this.channelhalfops.get(channel) instanceof Set && this.channelhalfops.get(channel).has(socket.nickname)) {
// Only allow kick if the target is NOT a channel operator
if (this.channelops.has(channel) && this.channelops.get(channel) instanceof Set && this.channelops.get(channel).has(targetNick)) {
socket.write(`:${this.servername} 482 ${socket.nickname} ${channel} :You cannot kick a channel operator\r\n`);
break; break;
} }
// Allow kick
} else {
socket.write(`:${this.servername} 482 ${socket.nickname} ${channel} :You're not channel operator\r\n`);
break;
} }
if (params.length < 2) { if (params.length < 2) {
socket.write(`:${this.servername} 461 ${socket.nickname} KICK :Not enough parameters\r\n`); socket.write(`:${this.servername} 461 ${socket.nickname} KICK :Not enough parameters\r\n`);
break; break;
} }
this.usertimestamps.set(socket.nickname, Date.now()); this.usertimestamps.set(socket.nickname, Date.now());
var channel = params[0];
const targetNick = params[1];
if (!this.channels.has(channel)) { if (!this.channels.has(channel)) {
socket.write(`:${this.servername} 403 ${socket.nickname} ${channel} :No such channel\r\n`); socket.write(`:${this.servername} 403 ${socket.nickname} ${channel} :No such channel\r\n`);
@@ -712,7 +745,7 @@ class WTVIRC {
} else { } else {
targetSocket.write(`:${socket.nickname}!${socket.username}@${socket.host} KICK ${channel} ${targetNick}\r\n`); targetSocket.write(`:${socket.nickname}!${socket.username}@${socket.host} KICK ${channel} ${targetNick}\r\n`);
this.broadcastChannel(channel, `:${socket.nickname}!${socket.username}@${socket.host} KICK ${channel} ${targetNick}\r\n`); this.broadcastChannel(channel, `:${socket.nickname}!${socket.username}@${socket.host} KICK ${channel} ${targetNick}\r\n`);
this.broadcastToAllServers(`:${socket.uniqueId} KICK ${channel} ${targetSocket.uniqueId} :${reason}\r\n`); this.broadcastToAllServers(`:${socket.uniqueId} KICK ${channel} ${targetSocket.uniqueId}\r\n`);
} }
break; break;
case 'TOPIC': case 'TOPIC':
@@ -944,7 +977,8 @@ class WTVIRC {
break; break;
} }
if (this.nicknames.size > 0) { if (this.nicknames.size > 0) {
var result = Array.from(this.nicknames.values()).find(nick => nick.toLowerCase() === new_nickname.toLowerCase());
var result = Array.from(this.nicknames.keys()).find(s => this.nicknames.get(s).toLowerCase() === new_nickname.toLowerCase());
if (result) { if (result) {
socket.write(`:${this.servername} 433 * ${new_nickname} :Nickname is already in use\r\n`); socket.write(`:${this.servername} 433 * ${new_nickname} :Nickname is already in use\r\n`);
break; break;
@@ -1776,6 +1810,7 @@ class WTVIRC {
if (!this.isReservedChannel(channel)) { if (!this.isReservedChannel(channel)) {
this.channels.delete(channel); this.channels.delete(channel);
this.channelops.delete(channel); this.channelops.delete(channel);
this.channelhalfops.delete(channel);
this.channelvoices.delete(channel); this.channelvoices.delete(channel);
this.channeltopics.delete(channel); this.channeltopics.delete(channel);
this.channelbans.delete(channel); this.channelbans.delete(channel);
@@ -1806,6 +1841,11 @@ class WTVIRC {
ops.delete(nickname); ops.delete(nickname);
} }
} }
for (const [ch, ops] of this.channelhalfops.entries()) {
if (ops && ops !== true && ops.has(nickname)) {
ops.delete(nickname);
}
}
for (const [ch, voices] of this.channelvoices.entries()) { for (const [ch, voices] of this.channelvoices.entries()) {
if (voices && voices !== true && voices.has(nickname)) { if (voices && voices !== true && voices.has(nickname)) {
voices.delete(nickname); voices.delete(nickname);
@@ -1869,11 +1909,14 @@ class WTVIRC {
getUsersInChannel(channel) { getUsersInChannel(channel) {
if (this.channels.has(channel)) { if (this.channels.has(channel)) {
const ops = this.channelops.get(channel) || new Set(); const ops = this.channelops.get(channel) || new Set();
const halfops = this.channelhalfops.get(channel) || new Set();
const voices = this.channelvoices.get(channel) || new Set(); const voices = this.channelvoices.get(channel) || new Set();
return Array.from(this.channels.get(channel)).map(user => { return Array.from(this.channels.get(channel)).map(user => {
if (ops === true) return user; if (ops === true) return user;
if (voices === true) return user; if (voices === true) return user;
if (halfops === true) return user;
if (ops.has(user)) return '@' + user; if (ops.has(user)) return '@' + user;
if (halfops.has(user)) return '%' + user;
if (voices.has(user)) return '+' + user; if (voices.has(user)) return '+' + user;
return user; return user;
}); });
@@ -1949,6 +1992,7 @@ class WTVIRC {
if (!this.channels.has(channel)) { if (!this.channels.has(channel)) {
this.channels.set(channel, new Set()); this.channels.set(channel, new Set());
this.channelops.set(channel, new Set([creator])); this.channelops.set(channel, new Set([creator]));
this.channelhalfops.set(channel, new Set());
this.channelvoices.set(channel, new Set()); this.channelvoices.set(channel, new Set());
this.channeltopics.set(channel, 'No topic set'); this.channeltopics.set(channel, 'No topic set');
this.channelbans.set(channel, new Set()); this.channelbans.set(channel, new Set());
@@ -2144,6 +2188,7 @@ class WTVIRC {
'b': true, // ban 'b': true, // ban
'e': true, // exception 'e': true, // exception
'I': true, // invite-exception 'I': true, // invite-exception
'h': true, // halfop
'o': true, // op 'o': true, // op
'v': true // voice 'v': true // voice
}; };
@@ -2176,6 +2221,7 @@ class WTVIRC {
( (
!this.channelops.has(channel) || !this.channelops.has(channel) ||
this.channelops.get(channel) === true || this.channelops.get(channel) === true ||
this.channelhalfops.get(channel) === true ||
!this.channelops.get(channel).has(nickname) !this.channelops.get(channel).has(nickname)
) )
) { ) {
@@ -2344,6 +2390,24 @@ class WTVIRC {
this.channelops.set(channel, (this.channelops.get(channel) || new Set()).delete(target_nickname)); this.channelops.set(channel, (this.channelops.get(channel) || new Set()).delete(target_nickname));
this.broadcastChannel(channel, `:${nickname}!${username}@${socket.host} MODE ${channel} -o ${target_nickname}\r\n`); this.broadcastChannel(channel, `:${nickname}!${username}@${socket.host} MODE ${channel} -o ${target_nickname}\r\n`);
return; return;
} else if (mode.startsWith('+h')) {
if (params.length < 3) {
socket.write(`:${this.servername} 461 ${nickname} MODE :Not enough parameters\r\n`);
return;
}
const target_nickname = params[2];
this.channelhalfops.set(channel, (this.channelhalfops.get(channel) || new Set()).add(target_nickname));
this.broadcastChannel(channel, `:${nickname}!${username}@${socket.host} MODE ${channel} +h ${target_nickname}\r\n`);
return;
} else if (mode.startsWith('-h')) {
if (params.length < 3) {
socket.write(`:${this.servername} 461 ${nickname} MODE :Not enough parameters\r\n`);
return;
}
const target_nickname = params[2];
this.channelhalfops.set(channel, (this.channelhalfops.get(channel) || new Set()).delete(target_nickname));
this.broadcastChannel(channel, `:${nickname}!${username}@${socket.host} MODE ${channel} -h ${target_nickname}\r\n`);
return;
} else if (mode.startsWith('+v')) { } else if (mode.startsWith('+v')) {
if (params.length < 3) { if (params.length < 3) {
socket.write(`:${this.servername} 461 ${nickname} MODE :Not enough parameters\r\n`); socket.write(`:${this.servername} 461 ${nickname} MODE :Not enough parameters\r\n`);
@@ -2577,7 +2641,7 @@ class WTVIRC {
chan_modes = []; chan_modes = [];
} }
if (!this.isIRCOp(nickname)) { if (!this.isIRCOp(nickname)) {
socket.write(`:${this.servername} 482 ${nickname} ${channel} :You're not an IRC operator\r\n`); socket.write(`:${this.servername} ${nickname} ${channel} :You're not an IRC operator\r\n`);
return; return;
} }
this.channelmodes.set(channel, [...chan_modes, 'O']); this.channelmodes.set(channel, [...chan_modes, 'O']);
@@ -2809,7 +2873,7 @@ class WTVIRC {
socket.write(`:${this.servername} 001 ${nickname} :Welcome to the IRC server, ${nickname}\r\n`); socket.write(`:${this.servername} 001 ${nickname} :Welcome to the IRC server, ${nickname}\r\n`);
socket.write(`:${this.servername} 002 ${nickname} :Your host is ${this.servername}, running version minisrv ${this.minisrv_config.version}\r\n`); socket.write(`:${this.servername} 002 ${nickname} :Your host is ${this.servername}, running version minisrv ${this.minisrv_config.version}\r\n`);
socket.write(`:${this.servername} 003 ${nickname} :This server is ready to accept commands\r\n`); socket.write(`:${this.servername} 003 ${nickname} :This server is ready to accept commands\r\n`);
socket.write(`:${this.servername} 004 ${nickname} ${this.servername} minisrv ${this.minisrv_config.version} oxizZws obtkmeZIlvTVROQrnc beIklov\r\n`); socket.write(`:${this.servername} 004 ${nickname} ${this.servername} minisrv ${this.minisrv_config.version} oxizrZws obtkmeZIlhvTVROQrnc beIklohv\r\n`);
for (const caps of this.caps) { for (const caps of this.caps) {
socket.write(`:${this.servername} 005 ${caps}\r\n`); socket.write(`:${this.servername} 005 ${caps}\r\n`);
} }