Files
techircd/commands.go
ComputerTech312 bab403557f Fix critical bugs and security vulnerabilities
- Fix race condition in client cleanup by serializing operations
- Add proper nil checks in SendMessage for server/config
- Add semaphore to limit concurrent health check goroutines
- Reduce buffer size to RFC-compliant 512 bytes (was 4096)
- Add comprehensive input validation (length, null bytes, UTF-8)
- Improve SSL error handling with graceful degradation
- Replace unsafe conn.Close() with proper cleanup() calls
- Prevent goroutine leaks and memory exhaustion attacks
- Enhanced logging and error recovery throughout

These fixes address the freezing issues and improve overall
server stability, security, and RFC compliance.
2025-09-27 15:13:55 +01:00

4096 lines
120 KiB
Go

package main
import (
"fmt"
"log"
"strings"
"time"
)
// IRCMessage represents a parsed IRC message with IRCv3 support
type IRCMessage struct {
Tags map[string]string
Prefix string
Command string
Params []string
}
// FormatMessage formats an IRC message with IRCv3 tags
func (m *IRCMessage) FormatMessage() string {
var parts []string
// Add tags if present
if len(m.Tags) > 0 {
var tagParts []string
for key, value := range m.Tags {
if value == "" {
tagParts = append(tagParts, key)
} else {
tagParts = append(tagParts, fmt.Sprintf("%s=%s", key, value))
}
}
parts = append(parts, "@"+strings.Join(tagParts, ";"))
}
// Add prefix if present
if m.Prefix != "" {
parts = append(parts, ":"+m.Prefix)
}
// Add command
parts = append(parts, m.Command)
// Add parameters
parts = append(parts, m.Params...)
return strings.Join(parts, " ")
}
// AddServerTime adds server-time tag to message
func (c *Client) AddServerTime(msg *IRCMessage) {
if c.HasCapability("server-time") {
if msg.Tags == nil {
msg.Tags = make(map[string]string)
}
msg.Tags["time"] = time.Now().UTC().Format(time.RFC3339Nano)
}
}
// AddAccountTag adds account tag if user is authenticated
func (c *Client) AddAccountTag(msg *IRCMessage) {
if c.HasCapability("account-tag") && c.Account() != "" {
if msg.Tags == nil {
msg.Tags = make(map[string]string)
}
msg.Tags["account"] = c.Account()
}
}
// IRC numeric reply codes
const (
RPL_WELCOME = 001
RPL_YOURHOST = 002
RPL_CREATED = 003
RPL_MYINFO = 004
RPL_ISUPPORT = 005
RPL_USERHOST = 302
RPL_ISON = 303
RPL_AWAY = 301
RPL_UNAWAY = 305
RPL_NOWAWAY = 306
RPL_WHOISUSER = 311
RPL_WHOISSERVER = 312
RPL_WHOISOPERATOR = 313
RPL_WHOISIDLE = 317
RPL_ENDOFWHOIS = 318
RPL_WHOISCHANNELS = 319
RPL_WHOISHOST = 378
RPL_LISTSTART = 321
RPL_LIST = 322
RPL_LISTEND = 323
RPL_CHANNELMODEIS = 324
RPL_NOTOPIC = 331
RPL_TOPIC = 332
RPL_TOPICWHOTIME = 333
RPL_NAMREPLY = 353
RPL_ENDOFNAMES = 366
RPL_MOTDSTART = 375
RPL_MOTD = 372
RPL_ENDOFMOTD = 376
RPL_UMODEIS = 221
RPL_INVITING = 341
RPL_YOUREOPER = 381
// New commands
RPL_TIME = 391
RPL_VERSION = 351
RPL_ADMINME = 256
RPL_ADMINLOC1 = 257
RPL_ADMINLOC2 = 258
RPL_ADMINEMAIL = 259
RPL_INFO = 371
RPL_ENDOFINFO = 374
RPL_LUSERCLIENT = 251
RPL_LUSEROP = 252
RPL_LUSERUNKNOWN = 253
RPL_LUSERCHANNELS = 254
RPL_LUSERME = 255
RPL_STATSCOMMANDS = 212
RPL_STATSOLINE = 243
RPL_STATSUPTIME = 242
RPL_STATSLINKINFO = 211
RPL_ENDOFSTATS = 219
// SILENCE command numerics
RPL_SILELIST = 271
RPL_ENDOFSILELIST = 272
ERR_SILELISTFULL = 511
// MONITOR command numerics (IRCv3)
RPL_MONONLINE = 730
RPL_MONOFFLINE = 731
RPL_MONLIST = 732
RPL_ENDOFMONLIST = 733
ERR_MONLISTFULL = 734
// Ban list numerics
RPL_BANLIST = 367
RPL_ENDOFBANLIST = 368
// SASL authentication numerics
RPL_SASLSUCCESS = 903
ERR_SASLFAIL = 904
ERR_SASLNOTSUPP = 908
ERR_SASLABORTED = 906
ERR_NOSUCHNICK = 401
ERR_NOSUCHSERVER = 402
ERR_NOSUCHCHANNEL = 403
ERR_CANNOTSENDTOCHAN = 404
ERR_TOOMANYCHANNELS = 405
ERR_WASNOSUCHNICK = 406
ERR_TOOMANYTARGETS = 407
ERR_NOORIGIN = 409
ERR_NORECIPIENT = 411
ERR_NOTEXTTOSEND = 412
ERR_UNKNOWNCOMMAND = 421
ERR_NOMOTD = 422
ERR_NONICKNAMEGIVEN = 431
ERR_ERRONEUSNICKNAME = 432
ERR_NICKNAMEINUSE = 433
ERR_NICKCOLLISION = 436
ERR_USERNOTINCHANNEL = 441
ERR_NOTONCHANNEL = 442
ERR_USERONCHANNEL = 443
ERR_NOLOGIN = 444
ERR_SUMMONDISABLED = 445
ERR_USERSDISABLED = 446
ERR_NOTREGISTERED = 451
ERR_NEEDMOREPARAMS = 461
ERR_ALREADYREGISTRED = 462
ERR_NOPERMFORHOST = 463
ERR_PASSWDMISMATCH = 464
ERR_YOUREBANNEDCREEP = 465
ERR_YOUWILLBEBANNED = 466
ERR_KEYSET = 467
ERR_CHANNELISFULL = 471
ERR_UNKNOWNMODE = 472
ERR_INVITEONLYCHAN = 473
ERR_BANNEDFROMCHAN = 474
ERR_BADCHANNELKEY = 475
ERR_BADCHANMASK = 476
ERR_NOCHANMODES = 477
ERR_BANLISTFULL = 478
ERR_MODERATED = 494 // Cannot send to channel (channel is moderated)
ERR_QUIETED = 404 // Cannot send to channel (you are quieted)
ERR_NOPRIVILEGES = 481
ERR_CHANOPRIVSNEEDED = 482
ERR_CANTKILLSERVER = 483
ERR_RESTRICTED = 484
ERR_UNIQOPPRIVSNEEDED = 485
ERR_NOOPERHOST = 491
ERR_UMODEUNKNOWNFLAG = 501
ERR_USERSDONTMATCH = 502
RPL_SNOMASK = 8
RPL_GLOBALNOTICE = 710
RPL_OPERWALL = 711
// New command numerics
RPL_ENDOFWHOWAS = 369
RPL_RULESSTART = 308
RPL_RULES = 232
RPL_ENDOFRULES = 309
RPL_MAP = 006
RPL_MAPEND = 007
RPL_KNOCKDLVR = 711
ERR_KNOCKONCHAN = 713
ERR_CHANOPEN = 713
ERR_INVALIDUSERNAME = 468
)
// handleNick handles NICK command
func (c *Client) handleNick(parts []string) {
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "NICK :Not enough parameters")
return
}
newNick := parts[1]
if len(newNick) > 0 && newNick[0] == ':' {
newNick = newNick[1:]
}
// Validate nickname
if !isValidNickname(newNick) {
c.SendNumeric(ERR_ERRONEUSNICKNAME, newNick+" :Erroneous nickname")
return
}
// Check if nick is already in use
if existing := c.server.GetClient(newNick); existing != nil && existing != c {
c.SendNumeric(ERR_NICKNAMEINUSE, newNick+" :Nickname is already in use")
return
}
oldNick := c.Nick()
// If already registered, notify channels
if c.IsRegistered() && oldNick != "" {
// Create the message with the OLD prefix before changing the nick
oldPrefix := fmt.Sprintf("%s!%s@%s", oldNick, c.User(), c.Host())
message := fmt.Sprintf(":%s NICK :%s", oldPrefix, newNick)
// Now change the nick
c.SetNick(newNick)
// Send to client themselves first (protocol-compliant NICK message)
// The client ALWAYS gets their own NICK message - config only affects others
c.SendMessage(message)
// Then broadcast to other users in channels based on config
for _, channel := range c.GetChannels() {
// Get all clients in this channel
for _, client := range channel.GetClients() {
if client == c { // Skip self - already handled above
continue
}
shouldSend := false
if c.server.config.NickChangeNotification.ShowToEveryone {
shouldSend = true
} else if c.server.config.NickChangeNotification.ShowToOpers && client.IsOper() {
shouldSend = true
}
if shouldSend {
client.SendMessage(message)
}
}
}
// Send snomask notification for nick change
if c.server != nil && oldNick != newNick {
c.server.sendSnomask('n', fmt.Sprintf("Nick change: %s -> %s (%s@%s)",
oldNick, newNick, c.User(), c.Host()))
}
} else {
// Not registered yet, just change the nick
c.SetNick(newNick)
}
c.checkRegistration()
}
// handleUser handles USER command
func (c *Client) handleUser(parts []string) {
if len(parts) < 5 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "USER :Not enough parameters")
return
}
if c.IsRegistered() {
c.SendNumeric(ERR_ALREADYREGISTRED, ":You may not reregister")
return
}
c.SetUser(parts[1])
// parts[2] and parts[3] are ignored (mode and unused)
realname := strings.Join(parts[4:], " ")
if len(realname) > 0 && realname[0] == ':' {
realname = realname[1:]
}
c.SetRealname(realname)
c.checkRegistration()
}
// checkRegistration checks if client is ready to be registered
func (c *Client) checkRegistration() {
// Don't complete registration if CAP negotiation is still in progress
if c.capNegotiation {
return
}
if !c.IsRegistered() && c.Nick() != "" && c.User() != "" {
c.SetRegistered(true)
c.sendWelcome()
}
}
// sendWelcome sends welcome messages to newly registered client
func (c *Client) sendWelcome() {
if c.server == nil {
return
}
if c.server.config == nil {
return
}
c.SendNumeric(RPL_WELCOME, fmt.Sprintf("Welcome to %s, %s", c.server.config.Server.Network, c.Prefix()))
c.SendNumeric(RPL_YOURHOST, fmt.Sprintf("Your host is %s, running version %s", c.server.config.Server.Name, c.server.config.Server.Version))
c.SendNumeric(RPL_CREATED, "This server was created recently")
c.SendNumeric(RPL_MYINFO, fmt.Sprintf("%s %s iowsBGSx beIklmnpstqaohv", c.server.config.Server.Name, c.server.config.Server.Version))
// Send ISUPPORT (005) messages to inform client about server capabilities
c.SendNumeric(RPL_ISUPPORT, "PREFIX=(qaohv)~&@%+ CHANTYPES=# CHANMODES=beI,k,l,imnpst NETWORK="+c.server.config.Server.Network+" :are supported by this server")
c.SendNumeric(RPL_ISUPPORT, "MAXCHANNELS="+fmt.Sprintf("%d", c.server.config.Limits.MaxChannels)+" NICKLEN="+fmt.Sprintf("%d", c.server.config.Limits.MaxNickLength)+" TOPICLEN="+fmt.Sprintf("%d", c.server.config.Limits.MaxTopicLength)+" :are supported by this server")
c.SendNumeric(RPL_ISUPPORT, "MODES=20 STATUSMSG=~&@%+ EXCEPTS=e INVEX=I CASEMAPPING=rfc1459 CHANNELLEN=32 :are supported by this server")
// Send user modes (221) - properly formatted
userModes := c.GetModes()
if userModes == "" {
userModes = "+"
} else if !strings.HasPrefix(userModes, "+") {
userModes = "+" + userModes
}
c.SendNumeric(RPL_UMODEIS, userModes)
// Send connection info (378) - shows real hostname
c.SendNumeric(RPL_WHOISHOST, fmt.Sprintf("%s :is connecting from %s", c.Nick(), c.Host()))
// Send LUSERS statistics
c.server.mu.RLock()
totalUsers := len(c.server.clients)
totalChannels := len(c.server.channels)
operCount := 0
for _, client := range c.server.clients {
if client.IsOper() {
operCount++
}
}
c.server.mu.RUnlock()
c.SendNumeric(RPL_LUSERCLIENT, fmt.Sprintf("There are %d users and 0 services on 1 servers", totalUsers))
if operCount > 0 {
c.SendNumeric(RPL_LUSEROP, fmt.Sprintf("%d :operator(s) online", operCount))
}
c.SendNumeric(RPL_LUSERUNKNOWN, "0 :unknown connection(s)")
if totalChannels > 0 {
c.SendNumeric(RPL_LUSERCHANNELS, fmt.Sprintf("%d :channels formed", totalChannels))
}
c.SendNumeric(RPL_LUSERME, fmt.Sprintf("I have %d clients and 0 servers", totalUsers))
// Send MOTD
if len(c.server.config.MOTD) > 0 {
c.SendNumeric(RPL_MOTDSTART, fmt.Sprintf("- %s Message of the Day -", c.server.config.Server.Name))
for _, line := range c.server.config.MOTD {
c.SendNumeric(RPL_MOTD, fmt.Sprintf("- %s", line))
}
c.SendNumeric(RPL_ENDOFMOTD, "End of /MOTD command")
}
// Send snomask notification for new client connection
if c.server != nil {
c.server.sendSnomask('c', fmt.Sprintf("Client connect: %s (%s@%s)",
c.Nick(), c.User(), c.Host()))
}
}
// handlePing handles PING command
func (c *Client) handlePing(parts []string) {
if len(parts) < 2 {
log.Printf("Invalid PING from %s: missing token", c.Nick())
return
}
token := parts[1]
if len(token) > 0 && token[0] == ':' {
token = token[1:]
}
// Log PING received for debugging
log.Printf("Received PING from client %s with token: %s", c.Nick(), token)
// Determine the correct PONG format based on the ping token
var pongMsg string
if strings.HasPrefix(token, "LAG") {
// HexChat LAG ping - use the exact format HexChat expects
// HexChat expects: :servername PONG servername :LAGxxxxx
pongMsg = fmt.Sprintf(":%s PONG %s :%s", c.server.config.Server.Name, c.server.config.Server.Name, token)
log.Printf("Handling HexChat LAG ping with token: %s", token)
} else if token == c.server.config.Server.Name {
// Standard server ping - respond with server name
pongMsg = fmt.Sprintf(":%s PONG %s :%s", c.server.config.Server.Name, c.server.config.Server.Name, token)
} else {
// Generic ping - echo back the token with colon prefix
pongMsg = fmt.Sprintf(":%s PONG %s :%s", c.server.config.Server.Name, c.server.config.Server.Name, token)
}
log.Printf("Sending PONG to client %s: %s", c.Nick(), pongMsg)
// Send the PONG response
c.SendMessage(pongMsg)
// Update ping tracking - treat any client PING as activity
c.mu.Lock()
c.lastPong = time.Now()
c.lastActivity = time.Now() // Update activity time too
c.waitingForPong = false
c.mu.Unlock()
log.Printf("Updated ping tracking for client %s", c.Nick())
}
// handlePong handles PONG command
func (c *Client) handlePong(parts []string) {
// Update the last pong time for ping timeout tracking
c.mu.Lock()
c.lastPong = time.Now()
c.lastActivity = time.Now() // Update activity time too
c.waitingForPong = false
c.mu.Unlock()
// Log PONG receipt for debugging
token := "unknown"
if len(parts) > 1 {
token = parts[1]
if len(token) > 0 && token[0] == ':' {
token = token[1:]
}
}
log.Printf("Received PONG from client %s with token: %s", c.Nick(), token)
}
// handleJoin handles JOIN command
func (c *Client) handleJoin(parts []string) {
log.Printf("JOIN command from %s (registered: %v): %v", c.Nick(), c.IsRegistered(), parts)
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "JOIN :Not enough parameters")
return
}
channelNames := strings.Split(parts[1], ",")
keys := []string{}
if len(parts) > 2 {
keys = strings.Split(parts[2], ",")
}
for i, channelName := range channelNames {
if channelName == "0" {
// Leave all channels
for _, channel := range c.GetChannels() {
c.handlePartChannel(channel.Name(), "Leaving all channels")
}
continue
}
if !isValidChannelName(channelName) {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
continue
}
channel := c.server.GetOrCreateChannel(channelName)
// Check if already in channel
if c.IsInChannel(channelName) {
continue
}
// Check channel modes and limits (God Mode can bypass all restrictions)
key := ""
if i < len(keys) {
key = keys[i]
}
if !c.HasGodMode() {
if channel.HasMode('k') && channel.Key() != key {
c.SendNumeric(ERR_BADCHANNELKEY, channelName+" :Cannot join channel (+k)")
continue
}
if channel.HasMode('l') && channel.UserCount() >= channel.Limit() {
c.SendNumeric(ERR_CHANNELISFULL, channelName+" :Cannot join channel (+l)")
continue
}
// Check for bans (God Mode bypasses bans)
if channel.IsBanned(c) {
c.SendNumeric(ERR_BANNEDFROMCHAN, channelName+" :Cannot join channel (+b)")
continue
}
// Check invite-only mode (God Mode bypasses invite requirement)
if channel.HasMode('i') && !channel.IsInvited(c) {
c.SendNumeric(ERR_INVITEONLYCHAN, channelName+" :Cannot join channel (+i)")
continue
}
} else {
// God Mode user joining - notify operators
c.sendSnomask('o', fmt.Sprintf("GOD MODE: %s bypassed restrictions to join %s", c.Nick(), channelName))
}
// Check if user is already in the channel
if c.IsInChannel(channelName) {
// User is already in the channel, skip
continue
}
// Join the channel
channel.AddClient(c)
c.AddChannel(channel)
message := fmt.Sprintf(":%s JOIN :%s", c.Prefix(), channelName)
// Send JOIN to the client themselves first
c.SendMessage(message)
// Then broadcast to others in the channel
channel.Broadcast(message, c)
// Send topic if exists
if channel.Topic() != "" {
c.SendNumeric(RPL_TOPIC, channelName+" :"+channel.Topic())
c.SendNumeric(RPL_TOPICWHOTIME, fmt.Sprintf("%s %s %d", channelName, channel.TopicBy(), channel.TopicTime().Unix()))
}
// Send names list
c.sendNames(channel)
}
}
// handlePart handles PART command
func (c *Client) handlePart(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "PART :Not enough parameters")
return
}
channelNames := strings.Split(parts[1], ",")
reason := "Leaving"
if len(parts) > 2 {
reason = strings.Join(parts[2:], " ")
if len(reason) > 0 && reason[0] == ':' {
reason = reason[1:]
}
}
for _, channelName := range channelNames {
c.handlePartChannel(channelName, reason)
}
}
func (c *Client) handlePartChannel(channelName, reason string) {
if !c.IsInChannel(channelName) {
c.SendNumeric(ERR_NOTONCHANNEL, channelName+" :You're not on that channel")
return
}
channel := c.server.GetChannel(channelName)
if channel == nil {
return
}
message := fmt.Sprintf(":%s PART %s :%s", c.Prefix(), channelName, reason)
channel.Broadcast(message, nil)
channel.RemoveClient(c)
c.RemoveChannel(channelName)
// Remove empty channel
if channel.UserCount() == 0 {
c.server.RemoveChannel(channelName)
}
}
// handlePrivmsg handles PRIVMSG command with IRCv3 support
func (c *Client) handlePrivmsg(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NORECIPIENT, ":No recipient given (PRIVMSG)")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NOTEXTTOSEND, ":No text to send")
return
}
target := parts[1]
message := strings.Join(parts[2:], " ")
if len(message) > 0 && message[0] == ':' {
message = message[1:]
}
// Create IRCv3 message
ircMsg := &IRCMessage{
Tags: make(map[string]string),
Prefix: c.Prefix(),
Command: "PRIVMSG",
Params: []string{target, ":" + message},
}
// Add IRCv3 tags
c.AddServerTime(ircMsg)
c.AddAccountTag(ircMsg)
if isChannelName(target) {
// Channel message
channel := c.server.GetChannel(target)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
return
}
if !c.IsInChannel(target) {
c.SendNumeric(ERR_CANNOTSENDTOCHAN, target+" :Cannot send to channel")
return
}
// Check if user is quieted first (takes priority over moderated check)
if channel.IsQuieted(c) {
// Check if user has privilege to speak despite being quieted
if !channel.IsOwner(c) && !channel.IsOperator(c) && !channel.IsHalfop(c) {
c.SendNumeric(ERR_QUIETED, target+" :Cannot send to channel (you are quieted)")
return
}
}
// Check if channel is moderated and user lacks privileges
if channel.HasMode('m') && !channel.CanSendMessage(c) {
c.SendNumeric(ERR_MODERATED, target+" :Cannot send to channel (channel is moderated)")
return
}
// Broadcast with IRCv3 support
c.broadcastToChannel(channel, ircMsg)
// Echo message back to sender if they have echo-message capability
// TEMPORARILY DISABLED TO FIX DUPLICATION ISSUE
// if c.HasCapability("echo-message") {
// c.SendMessage(ircMsg.FormatMessage())
// }
} else {
// Private message
targetClient := c.server.GetClient(target)
if targetClient == nil {
c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel")
return
}
if targetClient.Away() != "" {
c.SendNumeric(RPL_AWAY, fmt.Sprintf("%s :%s", target, targetClient.Away()))
}
// Send IRCv3 message to target
targetClient.SendMessage(ircMsg.FormatMessage())
// Echo message back to sender if they have echo-message capability
// TEMPORARILY DISABLED TO FIX DUPLICATION ISSUE
// if c.HasCapability("echo-message") {
// c.SendMessage(ircMsg.FormatMessage())
// }
}
}
// handleNotice handles NOTICE command
func (c *Client) handleNotice(parts []string) {
if !c.IsRegistered() {
return // NOTICE should not generate error responses
}
if len(parts) < 3 {
return
}
target := parts[1]
message := strings.Join(parts[2:], " ")
if len(message) > 0 && message[0] == ':' {
message = message[1:]
}
if isChannelName(target) {
// Channel notice
channel := c.server.GetChannel(target)
if channel == nil || !c.IsInChannel(target) {
return
}
msg := fmt.Sprintf(":%s NOTICE %s :%s", c.Prefix(), target, message)
channel.Broadcast(msg, c)
} else {
// Private notice
targetClient := c.server.GetClient(target)
if targetClient == nil {
return
}
msg := fmt.Sprintf(":%s NOTICE %s :%s", c.Prefix(), target, message)
targetClient.SendMessage(msg)
}
}
// handleTagmsg handles TAGMSG command (IRCv3.2)
// TAGMSG is used for tag-only messages like typing indicators, reactions, etc.
func (c *Client) handleTagmsg(parts []string, tags map[string]string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "TAGMSG :Not enough parameters")
return
}
target := parts[1]
// Build tag string
var tagString string
if len(tags) > 0 {
var tagPairs []string
for key, value := range tags {
if value == "" {
tagPairs = append(tagPairs, key)
} else {
tagPairs = append(tagPairs, fmt.Sprintf("%s=%s", key, value))
}
}
tagString = "@" + strings.Join(tagPairs, ";") + " "
}
if isChannelName(target) {
// Channel tagmsg
channel := c.server.GetChannel(target)
if channel == nil || !c.IsInChannel(target) {
c.SendNumeric(ERR_NOTONCHANNEL, target+" :You're not on that channel")
return
}
msg := fmt.Sprintf("%s:%s TAGMSG %s", tagString, c.Prefix(), target)
channel.Broadcast(msg, c)
} else {
// Private tagmsg
targetClient := c.server.GetClient(target)
if targetClient == nil {
c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel")
return
}
msg := fmt.Sprintf("%s:%s TAGMSG %s", tagString, c.Prefix(), target)
targetClient.SendMessage(msg)
}
}
// handleWho handles WHO command
func (c *Client) handleWho(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "WHO :Not enough parameters")
return
}
target := parts[1]
if isChannelName(target) {
channel := c.server.GetChannel(target)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
return
}
for _, client := range channel.GetClients() {
// Skip stealth mode users unless requester is an operator
if !client.IsVisibleTo(c) {
continue
}
flags := ""
if client.IsOper() {
flags += "*"
}
if client.Away() != "" {
flags += "G"
} else {
flags += "H"
}
// Add channel status flags in order of hierarchy
if channel.IsOwner(client) {
flags += "~"
} else if channel.IsAdmin(client) {
flags += "&"
} else if channel.IsOperator(client) {
flags += "@"
} else if channel.IsHalfop(client) {
flags += "%"
} else if channel.IsVoice(client) {
flags += "+"
}
c.SendNumeric(352, fmt.Sprintf("%s %s %s %s %s %s :0 %s",
target, client.User(), client.HostForUser(c), c.server.config.Server.Name,
client.Nick(), flags, client.Realname()))
}
}
c.SendNumeric(315, target+" :End of /WHO list")
}
// handleWhois handles WHOIS command
func (c *Client) handleWhois(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "WHOIS :Not enough parameters")
return
}
nick := parts[1]
target := c.server.GetClient(nick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, nick+" :No such nick")
return
}
// Basic user information (always shown)
hostname := target.HostForUser(c)
// Always show the public hostname in RPL_WHOISUSER
c.SendNumeric(RPL_WHOISUSER, fmt.Sprintf("%s %s %s * :%s",
target.Nick(), target.User(), hostname, target.Realname()))
// Show real host if configured and permitted
if c.canSeeWhoisInfo(target, "real_host") {
realHost := target.Host()
if hostname != realHost {
// Show real host if different from displayed host
c.SendNumeric(RPL_WHOISHOST, fmt.Sprintf("%s :is connecting from %s",
target.Nick(), realHost))
} else {
// Even if same, show real IP for debugging
c.SendNumeric(RPL_WHOISHOST, fmt.Sprintf("%s :is connecting from %s (real IP)",
target.Nick(), realHost))
}
}
// Server information
c.SendNumeric(RPL_WHOISSERVER, fmt.Sprintf("%s %s :%s",
target.Nick(), c.server.config.Server.Name, c.server.config.Server.Description))
// Operator status
if target.IsOper() {
c.SendNumeric(RPL_WHOISOPERATOR, target.Nick()+" :is an IRC operator")
// Show operator class if configured
if c.canSeeWhoisInfo(target, "oper_class") {
operClass := target.OperClass()
if operClass != "" {
// Load operator config to get class description and rank name
operConfig, err := LoadOperConfig(c.server.config.OperConfig.ConfigFile)
if err == nil && c.server.config.OperConfig.Enable {
class := operConfig.GetOperClass(operClass)
if class != nil {
rankName := operConfig.GetRankName(class.Rank)
c.SendMessage(fmt.Sprintf(":%s 313 %s %s :is an IRC operator (%s - %s) [%s]",
c.server.config.Server.Name, c.Nick(), target.Nick(), class.Name, class.Description, rankName))
} else {
c.SendMessage(fmt.Sprintf(":%s 313 %s %s :is an IRC operator (class: %s)",
c.server.config.Server.Name, c.Nick(), target.Nick(), operClass))
}
} else {
c.SendMessage(fmt.Sprintf(":%s 313 %s %s :is an IRC operator (class: %s)",
c.server.config.Server.Name, c.Nick(), target.Nick(), operClass))
}
}
}
}
// Away status
if target.Away() != "" {
c.SendNumeric(RPL_AWAY, fmt.Sprintf("%s :%s", target.Nick(), target.Away()))
}
// User modes
if c.canSeeWhoisInfo(target, "user_modes") {
modes := target.GetModes()
if modes != "" {
c.SendMessage(fmt.Sprintf(":%s 379 %s %s :is using modes %s",
c.server.config.Server.Name, c.Nick(), target.Nick(), modes))
}
}
// SSL status
if c.canSeeWhoisInfo(target, "ssl_status") && target.IsSSL() {
c.SendMessage(fmt.Sprintf(":%s 671 %s %s :is using a secure connection",
c.server.config.Server.Name, c.Nick(), target.Nick()))
}
// Channels
if c.canSeeChannels(target) {
var channels []string
config := c.server.config.WhoisFeatures.ShowChannels
for _, channel := range target.GetChannels() {
// Skip secret/private channels based on config
if config.HideSecret && channel.HasMode('s') && !channel.HasClient(c) {
continue
}
if config.HidePrivate && channel.HasMode('p') && !channel.HasClient(c) {
continue
}
channelName := channel.Name()
if config.ShowMembership {
if channel.IsOperator(target) {
channelName = "@" + channelName
} else if channel.IsVoice(target) {
channelName = "+" + channelName
}
}
channels = append(channels, channelName)
}
if len(channels) > 0 {
c.SendNumeric(RPL_WHOISCHANNELS, fmt.Sprintf("%s :%s", target.Nick(), strings.Join(channels, " ")))
}
}
// Idle time
if c.canSeeWhoisInfo(target, "idle_time") {
idleTime := int(time.Since(target.LastActivity()).Seconds())
c.SendNumeric(RPL_WHOISIDLE, fmt.Sprintf("%s %d %d :seconds idle, signon time",
target.Nick(), idleTime, target.ConnectTime().Unix()))
}
// Signon time (alternative if idle time is not shown)
if !c.canSeeWhoisInfo(target, "idle_time") && c.canSeeWhoisInfo(target, "signon_time") {
c.SendMessage(fmt.Sprintf(":%s 317 %s %s :signed on %s",
c.server.config.Server.Name, c.Nick(), target.Nick(),
target.ConnectTime().Format("Mon Jan 2 15:04:05 2006")))
}
// Account name (for services integration)
if c.canSeeWhoisInfo(target, "account_name") && target.Account() != "" {
c.SendMessage(fmt.Sprintf(":%s 330 %s %s %s :is logged in as",
c.server.config.Server.Name, c.Nick(), target.Nick(), target.Account()))
}
// Client information
if c.canSeeWhoisInfo(target, "client_info") {
c.SendMessage(fmt.Sprintf(":%s 351 %s %s :is using client TechIRCd-Client",
c.server.config.Server.Name, c.Nick(), target.Nick()))
}
c.SendNumeric(RPL_ENDOFWHOIS, target.Nick()+" :End of /WHOIS list")
}
// handleNames handles NAMES command
func (c *Client) handleNames(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
// Send names for all channels
for _, channel := range c.server.GetChannels() {
if c.IsInChannel(channel.Name()) {
c.sendNames(channel)
}
}
return
}
channelNames := strings.Split(parts[1], ",")
for _, channelName := range channelNames {
channel := c.server.GetChannel(channelName)
if channel != nil && c.IsInChannel(channelName) {
c.sendNames(channel)
}
}
}
func (c *Client) sendNames(channel *Channel) {
var names []string
for _, client := range channel.GetClients() {
// Skip stealth mode users unless requester is an operator
if !client.IsVisibleTo(c) {
continue
}
name := client.Nick()
var prefixes string
// Build prefixes in order of hierarchy: owner > admin > operator > halfop > voice
if channel.IsOwner(client) {
prefixes += "~"
}
if channel.IsAdmin(client) {
prefixes += "&"
}
if channel.IsOperator(client) {
prefixes += "@"
}
if channel.IsHalfop(client) {
prefixes += "%"
}
if channel.IsVoice(client) {
prefixes += "+"
}
// For multi-prefix clients, show all prefixes. For others, show only the highest
if c.HasCapability("multi-prefix") {
name = prefixes + name
} else {
// Show only the highest prefix for non-multi-prefix clients
if len(prefixes) > 0 {
name = string(prefixes[0]) + name
}
}
names = append(names, name)
}
symbol := "="
if channel.HasMode('s') {
symbol = "@"
} else if channel.HasMode('p') {
symbol = "*"
}
c.SendNumeric(RPL_NAMREPLY, fmt.Sprintf("%s %s :%s", symbol, channel.Name(), strings.Join(names, " ")))
c.SendNumeric(RPL_ENDOFNAMES, channel.Name()+" :End of /NAMES list")
}
// handleQuit handles QUIT command
func (c *Client) handleQuit(parts []string) {
reason := "Client quit"
if len(parts) > 1 {
reason = strings.Join(parts[1:], " ")
if len(reason) > 0 && reason[0] == ':' {
reason = reason[1:]
}
}
// Broadcast QUIT message to all channels the client is in
quitMessage := fmt.Sprintf(":%s QUIT :%s", c.Prefix(), reason)
for _, channel := range c.GetChannels() {
// Send QUIT message to all users in the channel
for _, client := range channel.GetClients() {
if client != c { // Don't send to the quitting client
client.SendMessage(quitMessage)
}
}
// Remove client from channel
channel.RemoveClient(c)
// Remove empty channels
if len(channel.GetClients()) == 0 && c.server != nil {
c.server.RemoveChannel(channel.name)
}
}
// Use proper cleanup instead of direct connection close
c.cleanup()
}
// handleMode handles MODE command
func (c *Client) handleMode(parts []string) {
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "MODE :Not enough parameters")
return
}
target := parts[1]
// Handle user mode requests
if !isChannelName(target) {
if target != c.Nick() {
c.SendNumeric(ERR_USERSDONTMATCH, ":Cannot change mode for other users")
return
}
// If no mode changes specified, return current user modes
if len(parts) == 2 {
modes := c.GetModes()
if modes == "" {
modes = "+"
}
c.SendNumeric(RPL_UMODEIS, modes)
return
}
// Parse user mode changes
modeString := parts[2]
adding := true
var appliedModes []string
for _, char := range modeString {
switch char {
case '+':
adding = true
case '-':
adding = false
case 'i': // invisible
c.SetMode('i', adding)
if adding {
appliedModes = append(appliedModes, "+i")
} else {
appliedModes = append(appliedModes, "-i")
}
case 'w': // wallops
c.SetMode('w', adding)
if adding {
appliedModes = append(appliedModes, "+w")
} else {
appliedModes = append(appliedModes, "-w")
}
case 's': // server notices (requires oper)
if !c.IsOper() && adding {
continue // silently ignore for non-opers
}
c.SetMode('s', adding)
if adding {
appliedModes = append(appliedModes, "+s")
} else {
appliedModes = append(appliedModes, "-s")
}
case 'o': // operator (cannot be set manually)
if adding {
c.SendNumeric(ERR_UMODEUNKNOWNFLAG, ":Unknown MODE flag")
} else {
// Allow de-opering
c.SetOper(false)
c.SetMode('o', false)
appliedModes = append(appliedModes, "-o")
// Clear snomasks when de-opering
c.snomasks = make(map[rune]bool)
c.sendSnomask('o', fmt.Sprintf("%s is no longer an IRC operator", c.Nick()))
}
case 'r': // registered (cannot be set manually, services only)
c.SendNumeric(ERR_UMODEUNKNOWNFLAG, ":Unknown MODE flag")
case 'x': // host masking (TechIRCd special)
c.SetMode('x', adding)
if adding {
appliedModes = append(appliedModes, "+x")
// TODO: Implement host masking
} else {
appliedModes = append(appliedModes, "-x")
}
case 'z': // SSL/TLS (automatic, cannot be manually set)
if c.IsSSL() {
c.SetMode('z', true)
}
// Ignore attempts to manually set/unset
case 'B': // bot flag (TechIRCd special)
c.SetMode('B', adding)
if adding {
appliedModes = append(appliedModes, "+B")
} else {
appliedModes = append(appliedModes, "-B")
}
case 'G': // God Mode (requires oper and god_mode permission)
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
continue
}
if !c.HasOperPermission("god_mode") {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied - You need god_mode permission")
continue
}
// Only change mode if it's different from current state
if c.HasMode('G') != adding {
c.SetMode('G', adding)
if adding {
appliedModes = append(appliedModes, "+G")
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** GOD MODE enabled - You have ultimate power!",
c.server.config.Server.Name, c.Nick()))
c.sendSnomask('o', fmt.Sprintf("%s has enabled GOD MODE - Ultimate channel override powers active", c.Nick()))
} else {
appliedModes = append(appliedModes, "-G")
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** GOD MODE disabled",
c.server.config.Server.Name, c.Nick()))
c.sendSnomask('o', fmt.Sprintf("%s has disabled GOD MODE", c.Nick()))
}
}
case 'S': // Stealth Mode (requires oper and stealth_mode permission)
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
continue
}
if !c.HasOperPermission("stealth_mode") {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied - You need stealth_mode permission")
continue
}
// Only change mode if it's different from current state
if c.HasMode('S') != adding {
c.SetMode('S', adding)
if adding {
appliedModes = append(appliedModes, "+S")
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** STEALTH MODE enabled - You are now invisible to users",
c.server.config.Server.Name, c.Nick()))
c.sendSnomask('o', fmt.Sprintf("%s has enabled STEALTH MODE - Now invisible to regular users", c.Nick()))
} else {
appliedModes = append(appliedModes, "-S")
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** STEALTH MODE disabled - You are now visible",
c.server.config.Server.Name, c.Nick()))
c.sendSnomask('o', fmt.Sprintf("%s has disabled STEALTH MODE", c.Nick()))
}
}
default:
c.SendNumeric(ERR_UMODEUNKNOWNFLAG, ":Unknown MODE flag")
}
}
// Send mode changes back to user
if len(appliedModes) > 0 {
modeStr := strings.Join(appliedModes, "")
c.SendMessage(fmt.Sprintf(":%s MODE %s :%s", c.Nick(), c.Nick(), modeStr))
}
return
}
// Handle channel mode requests
channel := c.server.GetChannel(target)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
return
}
if !c.IsInChannel(target) {
c.SendNumeric(ERR_NOTONCHANNEL, target+" :You're not on that channel")
return
}
// If no mode changes specified, return current channel modes
if len(parts) == 2 {
modes := channel.GetModes()
if modes == "" {
modes = "+"
}
c.SendNumeric(RPL_CHANNELMODEIS, fmt.Sprintf("%s %s", target, modes))
return
}
// Parse mode changes
modeString := parts[2]
args := parts[3:]
argIndex := 0
// Check if user has operator privileges (required for most mode changes)
// God Mode users can bypass operator requirement
if !c.HasGodMode() && !channel.IsOwner(c) && !channel.IsOperator(c) && !channel.IsHalfop(c) {
c.SendNumeric(ERR_CHANOPRIVSNEEDED, target+" :You're not channel operator")
return
}
// If using God Mode to set modes, notify operators
if c.HasGodMode() && !channel.IsOwner(c) && !channel.IsOperator(c) && !channel.IsHalfop(c) {
c.sendSnomask('o', fmt.Sprintf("GOD MODE: %s set modes on %s without operator privileges", c.Nick(), target))
}
adding := true
var appliedModes []string
var appliedArgs []string
for _, char := range modeString {
switch char {
case '+':
adding = true
case '-':
adding = false
case 'o': // operator
if argIndex >= len(args) {
continue
}
targetNick := args[argIndex]
argIndex++
// Check if operator mode is allowed in config
if !c.server.config.Channels.AllowedModes.Operator {
c.SendNumeric(ERR_UNKNOWNMODE, "+o :Operator mode is disabled")
continue
}
targetClient := c.server.GetClient(targetNick)
if targetClient == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
continue
}
if !targetClient.IsInChannel(target) {
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", targetNick, target))
continue
}
// Check if the user already has the operator status we're trying to set
currentlyOp := channel.IsOperator(targetClient)
if adding && currentlyOp {
// Already an operator, skip this change
continue
}
if !adding && !currentlyOp {
// Already not an operator, skip this change
continue
}
channel.SetOperator(targetClient, adding)
if adding {
appliedModes = append(appliedModes, "+o")
} else {
appliedModes = append(appliedModes, "-o")
}
appliedArgs = append(appliedArgs, targetNick)
case 'v': // voice
if argIndex >= len(args) {
continue
}
targetNick := args[argIndex]
argIndex++
// Check if voice mode is allowed in config
if !c.server.config.Channels.AllowedModes.Voice {
c.SendNumeric(ERR_UNKNOWNMODE, "+v :Voice mode is disabled")
continue
}
targetClient := c.server.GetClient(targetNick)
if targetClient == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
continue
}
if !targetClient.IsInChannel(target) {
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", targetNick, target))
continue
}
// Check if the user already has the voice status we're trying to set
currentlyVoiced := channel.IsVoice(targetClient)
if adding && currentlyVoiced {
// Already has voice, skip this change
continue
}
if !adding && !currentlyVoiced {
// Already doesn't have voice, skip this change
continue
}
channel.SetVoice(targetClient, adding)
if adding {
appliedModes = append(appliedModes, "+v")
} else {
appliedModes = append(appliedModes, "-v")
}
appliedArgs = append(appliedArgs, targetNick)
case 'h': // halfop
if argIndex >= len(args) {
continue
}
targetNick := args[argIndex]
argIndex++
// Check if halfop mode is allowed in config
if !c.server.config.Channels.AllowedModes.Halfop {
c.SendNumeric(ERR_UNKNOWNMODE, "+h :Halfop mode is disabled")
continue
}
targetClient := c.server.GetClient(targetNick)
if targetClient == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
continue
}
if !targetClient.IsInChannel(target) {
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", targetNick, target))
continue
}
// Check if the user already has the halfop status we're trying to set
currentlyHalfop := channel.IsHalfop(targetClient)
if adding && currentlyHalfop {
// Already has halfop, skip this change
continue
}
if !adding && !currentlyHalfop {
// Already doesn't have halfop, skip this change
continue
}
channel.SetHalfop(targetClient, adding)
if adding {
appliedModes = append(appliedModes, "+h")
} else {
appliedModes = append(appliedModes, "-h")
}
appliedArgs = append(appliedArgs, targetNick)
case 'q': // owner/founder
if argIndex >= len(args) {
continue
}
targetNick := args[argIndex]
argIndex++
// Check if owner mode is allowed in config
if !c.server.config.Channels.AllowedModes.Owner {
c.SendNumeric(ERR_UNKNOWNMODE, "+q :Owner mode is disabled")
continue
}
// Only existing owners can grant/remove owner status, or God Mode users
if !channel.IsOwner(c) && !c.HasGodMode() {
c.SendNumeric(ERR_CHANOPRIVSNEEDED, target+" :You're not channel owner")
continue
}
targetClient := c.server.GetClient(targetNick)
if targetClient == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
continue
}
if !targetClient.IsInChannel(target) {
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", targetNick, target))
continue
}
// Check if the user already has the owner status we're trying to set
currentlyOwner := channel.IsOwner(targetClient)
if adding && currentlyOwner {
// Already has owner, skip this change
continue
}
if !adding && !currentlyOwner {
// Already doesn't have owner, skip this change
continue
}
channel.SetOwner(targetClient, adding)
if adding {
appliedModes = append(appliedModes, "+q")
} else {
appliedModes = append(appliedModes, "-q")
}
appliedArgs = append(appliedArgs, targetNick)
case 'a': // admin
if argIndex >= len(args) {
continue
}
targetNick := args[argIndex]
argIndex++
// Check if admin mode is allowed in config
if !c.server.config.Channels.AllowedModes.Admin {
c.SendNumeric(ERR_UNKNOWNMODE, "+a :Admin mode is disabled")
continue
}
// Only existing owners/admins can grant/remove admin status, or God Mode users
if !channel.IsOwner(c) && !channel.IsAdmin(c) && !c.HasGodMode() {
c.SendNumeric(ERR_CHANOPRIVSNEEDED, target+" :You need admin or owner privileges")
continue
}
targetClient := c.server.GetClient(targetNick)
if targetClient == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
continue
}
if !targetClient.IsInChannel(target) {
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", targetNick, target))
continue
}
// Check if the user already has the admin status we're trying to set
currentlyAdmin := channel.IsAdmin(targetClient)
if adding && currentlyAdmin {
// Already has admin, skip this change
continue
}
if !adding && !currentlyAdmin {
// Already doesn't have admin, skip this change
continue
}
channel.SetAdmin(targetClient, adding)
if adding {
appliedModes = append(appliedModes, "+a")
} else {
appliedModes = append(appliedModes, "-a")
}
appliedArgs = append(appliedArgs, targetNick)
case 'm': // moderated
channel.SetMode('m', adding)
if adding {
appliedModes = append(appliedModes, "+m")
} else {
appliedModes = append(appliedModes, "-m")
}
case 'n': // no external messages
channel.SetMode('n', adding)
if adding {
appliedModes = append(appliedModes, "+n")
} else {
appliedModes = append(appliedModes, "-n")
}
case 't': // topic restriction
channel.SetMode('t', adding)
if adding {
appliedModes = append(appliedModes, "+t")
} else {
appliedModes = append(appliedModes, "-t")
}
case 'i': // invite only
channel.SetMode('i', adding)
if adding {
appliedModes = append(appliedModes, "+i")
} else {
appliedModes = append(appliedModes, "-i")
}
case 's': // secret
channel.SetMode('s', adding)
if adding {
appliedModes = append(appliedModes, "+s")
} else {
appliedModes = append(appliedModes, "-s")
}
case 'p': // private
channel.SetMode('p', adding)
if adding {
appliedModes = append(appliedModes, "+p")
} else {
appliedModes = append(appliedModes, "-p")
}
case 'k': // key (password)
if adding {
if argIndex >= len(args) {
continue
}
key := args[argIndex]
argIndex++
channel.SetKey(key)
channel.SetMode('k', true)
appliedModes = append(appliedModes, "+k")
appliedArgs = append(appliedArgs, key)
} else {
channel.SetKey("")
channel.SetMode('k', false)
appliedModes = append(appliedModes, "-k")
}
case 'l': // limit
if adding {
if argIndex >= len(args) {
continue
}
limitStr := args[argIndex]
argIndex++
// Parse limit (simplified - should validate it's a number)
limit := 0
fmt.Sscanf(limitStr, "%d", &limit)
if limit > 0 {
channel.SetLimit(limit)
channel.SetMode('l', true)
appliedModes = append(appliedModes, "+l")
appliedArgs = append(appliedArgs, limitStr)
}
} else {
channel.SetLimit(0)
channel.SetMode('l', false)
appliedModes = append(appliedModes, "-l")
}
case 'b': // ban (enhanced with extended ban types)
var mask string
if argIndex >= len(args) {
if adding {
// If no mask provided and we're setting +b, generate user's hostmask
mask = fmt.Sprintf("%s!%s@%s", c.Nick(), c.User(), c.Host())
} else {
// List bans - send ban list to client
bans := channel.GetBans()
for _, ban := range bans {
// RPL_BANLIST: 367 <channel> <banmask> <who> <when>
c.SendNumeric(RPL_BANLIST, fmt.Sprintf("%s %s %s %d", target, ban, "server", time.Now().Unix()))
}
// Also list quiet bans with ~q: prefix
for _, quiet := range channel.quietList {
c.SendNumeric(RPL_BANLIST, fmt.Sprintf("%s ~q:%s %s %d", target, quiet, "server", time.Now().Unix()))
}
// RPL_ENDOFBANLIST: 368 <channel> :End of channel ban list
c.SendNumeric(RPL_ENDOFBANLIST, target+" :End of channel ban list")
continue
}
} else {
mask = args[argIndex]
argIndex++
// Expand partial masks to full hostmask format
mask = expandBanMask(mask)
}
// Check for extended ban types (e.g., ~q:nick!user@host for quiet)
if strings.HasPrefix(mask, "~") && len(mask) > 2 && mask[2] == ':' {
banType := mask[1] // The character after ~
banMask := mask[3:] // The mask after ~x:
switch banType {
case 'q': // Quiet ban
if adding {
// Add to quiet list
channel.quietList = append(channel.quietList, banMask)
appliedModes = append(appliedModes, "+b")
appliedArgs = append(appliedArgs, mask)
// Send snomask to opers
if c.IsOper() {
c.server.sendSnomask('x', fmt.Sprintf("%s set quiet ban %s on %s", c.Nick(), banMask, target))
}
} else {
// Remove from quiet list
for i, quiet := range channel.quietList {
if quiet == banMask {
channel.quietList = append(channel.quietList[:i], channel.quietList[i+1:]...)
appliedModes = append(appliedModes, "-b")
appliedArgs = append(appliedArgs, mask)
// Send snomask to opers
if c.IsOper() {
c.server.sendSnomask('x', fmt.Sprintf("%s removed quiet ban %s on %s", c.Nick(), banMask, target))
}
break
}
}
}
default:
// Unknown extended ban type - treat as regular ban for now
if adding {
channel.AddBan(mask)
appliedModes = append(appliedModes, "+b")
} else {
channel.RemoveBan(mask)
appliedModes = append(appliedModes, "-b")
}
appliedArgs = append(appliedArgs, mask)
}
} else {
// Regular ban
if adding {
channel.AddBan(mask)
appliedModes = append(appliedModes, "+b")
} else {
channel.RemoveBan(mask)
appliedModes = append(appliedModes, "-b")
}
appliedArgs = append(appliedArgs, mask)
}
case 'R': // registered users only
channel.SetMode('R', adding)
if adding {
appliedModes = append(appliedModes, "+R")
} else {
appliedModes = append(appliedModes, "-R")
}
case 'M': // muted (only ops can speak)
channel.SetMode('M', adding)
if adding {
appliedModes = append(appliedModes, "+M")
} else {
appliedModes = append(appliedModes, "-M")
}
case 'N': // no notice messages
channel.SetMode('N', adding)
if adding {
appliedModes = append(appliedModes, "+N")
} else {
appliedModes = append(appliedModes, "-N")
}
case 'C': // no CTCP messages
channel.SetMode('C', adding)
if adding {
appliedModes = append(appliedModes, "+C")
} else {
appliedModes = append(appliedModes, "-C")
}
case 'c': // no colors/formatting
channel.SetMode('c', adding)
if adding {
appliedModes = append(appliedModes, "+c")
} else {
appliedModes = append(appliedModes, "-c")
}
case 'S': // SSL/TLS users only
channel.SetMode('S', adding)
if adding {
appliedModes = append(appliedModes, "+S")
} else {
appliedModes = append(appliedModes, "-S")
}
case 'O': // opers only
channel.SetMode('O', adding)
if adding {
appliedModes = append(appliedModes, "+O")
} else {
appliedModes = append(appliedModes, "-O")
}
case 'z': // reduced moderation (halfops can use voice)
channel.SetMode('z', adding)
if adding {
appliedModes = append(appliedModes, "+z")
} else {
appliedModes = append(appliedModes, "-z")
}
case 'D': // delay join (users don't appear until they speak)
channel.SetMode('D', adding)
if adding {
appliedModes = append(appliedModes, "+D")
} else {
appliedModes = append(appliedModes, "-D")
}
case 'G': // word filter/profanity filter
channel.SetMode('G', adding)
if adding {
appliedModes = append(appliedModes, "+G")
} else {
appliedModes = append(appliedModes, "-G")
}
case 'f': // flood protection
if adding {
if argIndex >= len(args) {
continue
}
floodSettings := args[argIndex]
argIndex++
// Parse flood settings (e.g., "10:5" = 10 messages in 5 seconds)
channel.SetFloodSettings(floodSettings)
channel.SetMode('f', true)
appliedModes = append(appliedModes, "+f")
appliedArgs = append(appliedArgs, floodSettings)
} else {
channel.SetFloodSettings("")
channel.SetMode('f', false)
appliedModes = append(appliedModes, "-f")
}
case 'j': // join throttling
if adding {
if argIndex >= len(args) {
continue
}
joinThrottle := args[argIndex]
argIndex++
// Parse join throttle (e.g., "3:10" = 3 joins per 10 seconds)
channel.SetJoinThrottle(joinThrottle)
channel.SetMode('j', true)
appliedModes = append(appliedModes, "+j")
appliedArgs = append(appliedArgs, joinThrottle)
} else {
channel.SetJoinThrottle("")
channel.SetMode('j', false)
appliedModes = append(appliedModes, "-j")
}
default:
// Unknown mode - ignore for now
}
}
// Broadcast mode changes to all channel members
if len(appliedModes) > 0 {
modeChangeMsg := fmt.Sprintf("MODE %s %s", target, strings.Join(appliedModes, ""))
if len(appliedArgs) > 0 {
modeChangeMsg += " " + strings.Join(appliedArgs, " ")
}
for _, client := range channel.GetClients() {
client.SendFrom(c.Prefix(), modeChangeMsg)
}
}
}
// handleTopic handles TOPIC command
func (c *Client) handleTopic(parts []string) {
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "TOPIC :Not enough parameters")
return
}
channelName := parts[1]
if !isChannelName(channelName) {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
return
}
channel := c.server.GetChannel(channelName)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
return
}
if !c.IsInChannel(channelName) {
c.SendNumeric(ERR_NOTONCHANNEL, channelName+" :You're not on that channel")
return
}
// If no topic provided, return current topic
if len(parts) == 2 {
topic := channel.Topic()
if topic == "" {
c.SendNumeric(RPL_NOTOPIC, channelName+" :No topic is set")
} else {
c.SendNumeric(RPL_TOPIC, fmt.Sprintf("%s :%s", channelName, topic))
}
return
}
// Check if user can set topic (for now, anyone in channel can)
// TODO: Add proper +t mode checking
newTopic := strings.Join(parts[2:], " ")
if len(newTopic) > 0 && newTopic[0] == ':' {
newTopic = newTopic[1:]
}
channel.SetTopic(newTopic, c.Nick())
// Broadcast topic change to all channel members
for _, client := range channel.GetClients() {
client.SendFrom(c.Prefix(), fmt.Sprintf("TOPIC %s :%s", channelName, newTopic))
}
}
// handleAway handles AWAY command
func (c *Client) handleAway(parts []string) {
if len(parts) == 1 {
// Remove away status
c.SetAway("")
c.SendNumeric(RPL_UNAWAY, ":You are no longer marked as being away")
return
}
// Set away message
awayMsg := strings.Join(parts[1:], " ")
if len(awayMsg) > 0 && awayMsg[0] == ':' {
awayMsg = awayMsg[1:]
}
c.SetAway(awayMsg)
c.SendNumeric(RPL_NOWAWAY, ":You have been marked as being away")
}
// handleList handles LIST command
func (c *Client) handleList() {
c.SendNumeric(RPL_LISTSTART, "Channel :Users Name")
for _, channel := range c.server.GetChannels() {
// For now, show all channels (TODO: Add proper mode checking for secret channels)
userCount := len(channel.GetClients())
topic := channel.Topic()
if topic == "" {
topic = ""
}
c.SendNumeric(RPL_LIST, fmt.Sprintf("%s %d :%s", channel.Name(), userCount, topic))
}
c.SendNumeric(RPL_LISTEND, ":End of /LIST")
}
// handleInvite handles INVITE command
func (c *Client) handleInvite(parts []string) {
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "INVITE :Not enough parameters")
return
}
nick := parts[1]
channelName := parts[2]
target := c.server.GetClient(nick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, nick+" :No such nick/channel")
return
}
if !isChannelName(channelName) {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
return
}
channel := c.server.GetChannel(channelName)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
return
}
if !c.IsInChannel(channelName) {
c.SendNumeric(ERR_NOTONCHANNEL, channelName+" :You're not on that channel")
return
}
if target.IsInChannel(channelName) {
c.SendNumeric(ERR_USERONCHANNEL, fmt.Sprintf("%s %s :is already on channel", nick, channelName))
return
}
// TODO: Check if user has operator privileges for invite-only channels
// Send invite to target
target.SendFrom(c.Prefix(), fmt.Sprintf("INVITE %s %s", target.Nick(), channelName))
c.SendNumeric(RPL_INVITING, fmt.Sprintf("%s %s", target.Nick(), channelName))
}
// handleKick handles KICK command
func (c *Client) handleKick(parts []string) {
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "KICK :Not enough parameters")
return
}
channelName := parts[1]
nick := parts[2]
reason := "No reason given"
if len(parts) > 3 {
reason = strings.Join(parts[3:], " ")
if len(reason) > 0 && reason[0] == ':' {
reason = reason[1:]
}
}
if !isChannelName(channelName) {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
return
}
channel := c.server.GetChannel(channelName)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
return
}
if !c.IsInChannel(channelName) {
c.SendNumeric(ERR_NOTONCHANNEL, channelName+" :You're not on that channel")
return
}
target := c.server.GetClient(nick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, nick+" :No such nick/channel")
return
}
if !target.IsInChannel(channelName) {
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", nick, channelName))
return
}
// God Mode users cannot be kicked
if target.HasGodMode() {
c.SendNumeric(ERR_NOPRIVILEGES, fmt.Sprintf("%s :Cannot kick user (GOD MODE)", nick))
c.sendSnomask('o', fmt.Sprintf("GOD MODE: %s attempted to kick %s from %s but was blocked", c.Nick(), target.Nick(), channelName))
return
}
// TODO: Check if user has operator privileges
// For now, allow anyone to kick (will fix with proper channel modes)
// Broadcast kick to all channel members
kickMsg := fmt.Sprintf("KICK %s %s :%s", channelName, target.Nick(), reason)
for _, client := range channel.GetClients() {
client.SendFrom(c.Prefix(), kickMsg)
}
// Remove target from channel
channel.RemoveClient(target)
target.RemoveChannel(channelName)
}
// handleKill handles KILL command (operator only)
func (c *Client) handleKill(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "KILL :Not enough parameters")
return
}
nick := parts[1]
reason := "Killed by operator"
if len(parts) > 2 {
reason = strings.Join(parts[2:], " ")
if len(reason) > 0 && reason[0] == ':' {
reason = reason[1:]
}
}
target := c.server.GetClient(nick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, nick+" :No such nick/channel")
return
}
// Can't kill other operators
if target.IsOper() {
c.SendNumeric(ERR_CANTKILLSERVER, ":You can't kill other operators")
return
}
// Send kill message to target and disconnect
target.SendMessage(fmt.Sprintf("ERROR :Killed (%s (%s))", c.Nick(), reason))
// Broadcast to other operators
for _, client := range c.server.GetClients() {
if client.IsOper() && client != c {
client.SendMessage(fmt.Sprintf(":%s WALLOPS :%s killed %s (%s)",
c.server.config.Server.Name, c.Nick(), target.Nick(), reason))
}
}
// Disconnect the target properly
target.cleanup()
}
// handleOper handles OPER command
func (c *Client) handleOper(parts []string) {
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "OPER :Not enough parameters")
return
}
if c.server == nil || c.server.config == nil {
c.SendNumeric(ERR_NOOPERHOST, ":No O-lines for your host")
return
}
name := parts[1]
password := parts[2]
// Check if opers are enabled
if !c.server.config.Features.EnableOper {
c.SendNumeric(ERR_NOOPERHOST, ":O-lines are disabled")
return
}
var matchedOper *Oper
var operConfig *OperConfig
// Try to load advanced oper configuration first
if c.server.config.OperConfig.Enable {
var err error
operConfig, err = LoadOperConfig(c.server.config.OperConfig.ConfigFile)
if err == nil {
oper := operConfig.GetOper(name)
if oper != nil && oper.Password == password {
// TODO: Implement proper host matching
if oper.Host == "*@localhost" || oper.Host == "*@*" {
matchedOper = oper
}
}
}
}
// Fallback to legacy configuration
if matchedOper == nil {
for _, oper := range c.server.config.Opers {
if oper.Name == name && oper.Password == password {
// Check host mask (simplified - just check if it matches *@localhost for now)
if oper.Host == "*@localhost" || oper.Host == "*@*" {
// Convert legacy oper to new format for consistency
matchedOper = &Oper{
Name: oper.Name,
Password: oper.Password,
Host: oper.Host,
Class: oper.Class,
Flags: oper.Flags,
}
break
}
}
}
}
if matchedOper == nil {
c.SendNumeric(ERR_PASSWDMISMATCH, ":Password incorrect")
return
}
// Set operator status
c.SetOper(true)
c.SetOperClass(matchedOper.Class)
// Set operator user mode
c.SetMode('o', true)
c.SetMode('s', true) // Enable server notices by default
c.SetMode('w', true) // Enable wallops by default
// Set default snomasks for new operators
c.SetSnomask('c', true) // Client connects/disconnects
c.SetSnomask('o', true) // Oper-up messages
c.SetSnomask('s', true) // Server messages
// Get operator class information for display
var className string
if operConfig != nil {
class := operConfig.GetOperClass(matchedOper.Class)
if class != nil {
className = fmt.Sprintf(" (%s)", class.Description)
}
}
c.SendNumeric(RPL_YOUREOPER, ":You are now an IRC operator"+className)
c.SendNumeric(RPL_SNOMASK, fmt.Sprintf("%s :Server notice mask", c.GetSnomasks()))
// Send mode change notification
c.SendMessage(fmt.Sprintf(":%s MODE %s :+osw", c.Nick(), c.Nick()))
// Send snomask to other operators
operSymbol := c.GetOperSymbol()
c.sendSnomask('o', fmt.Sprintf("%s%s (%s@%s) is now an IRC operator%s",
operSymbol, c.Nick(), c.User(), c.Host(), className))
}
// handleSnomask handles SNOMASK command (server notice masks for operators)
func (c *Client) handleSnomask(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 2 {
// Show current snomasks
current := c.GetSnomasks()
if current == "" {
current = "+"
}
c.SendNumeric(RPL_SNOMASK, fmt.Sprintf("%s :Server notice mask", current))
return
}
modeString := parts[1]
adding := true
changed := false
for _, char := range modeString {
switch char {
case '+':
adding = true
case '-':
adding = false
case 'c': // Client connects/disconnects
c.SetSnomask('c', adding)
changed = true
case 'k': // Kill messages
c.SetSnomask('k', adding)
changed = true
case 'o': // Oper-up messages
c.SetSnomask('o', adding)
changed = true
case 'x': // X-line (ban) messages
c.SetSnomask('x', adding)
changed = true
case 'f': // Flood messages
c.SetSnomask('f', adding)
changed = true
case 'n': // Nick changes
c.SetSnomask('n', adding)
changed = true
case 's': // Server messages
c.SetSnomask('s', adding)
changed = true
case 'd': // Debug messages (TechIRCd special)
c.SetSnomask('d', adding)
changed = true
}
}
if changed {
current := c.GetSnomasks()
if current == "" {
current = "+"
}
c.SendNumeric(RPL_SNOMASK, fmt.Sprintf("%s :Server notice mask", current))
}
}
// handleGlobalNotice handles GLOBALNOTICE command (TechIRCd special oper command)
func (c *Client) handleGlobalNotice(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "GLOBALNOTICE :Not enough parameters")
return
}
message := strings.Join(parts[1:], " ")
if len(message) > 0 && message[0] == ':' {
message = message[1:]
}
// Send global notice to all users
for _, client := range c.server.GetClients() {
client.SendMessage(fmt.Sprintf(":%s NOTICE %s :[GLOBAL] %s",
c.server.config.Server.Name, client.Nick(), message))
}
// Send snomask to operators watching global notices
c.sendSnomask('s', fmt.Sprintf("Global notice from %s: %s", c.Nick(), message))
}
// handleWallops handles WALLOPS command (send to users with +w mode)
func (c *Client) handleWallops(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "WALLOPS :Not enough parameters")
return
}
message := strings.Join(parts[1:], " ")
if len(message) > 0 && message[0] == ':' {
message = message[1:]
}
// Send to all users with +w mode
for _, client := range c.server.GetClients() {
if client.HasMode('w') {
client.SendMessage(fmt.Sprintf(":%s WALLOPS :%s", c.Nick(), message))
}
}
}
// handleOperWall handles OPERWALL command (message to all operators)
func (c *Client) handleOperWall(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "OPERWALL :Not enough parameters")
return
}
message := strings.Join(parts[1:], " ")
if len(message) > 0 && message[0] == ':' {
message = message[1:]
}
// Send to all operators
for _, client := range c.server.GetClients() {
if client.IsOper() {
client.SendMessage(fmt.Sprintf(":%s WALLOPS :%s", c.Nick(), message))
}
}
}
// handleRehash handles REHASH command (reload configuration)
func (c *Client) handleRehash() {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
// Reload configuration
if c.server != nil {
err := c.server.ReloadConfig()
if err != nil {
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** REHASH failed: %s",
c.server.config.Server.Name, c.Nick(), err.Error()))
c.sendSnomask('s', fmt.Sprintf("REHASH failed by %s: %s", c.Nick(), err.Error()))
} else {
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Configuration reloaded successfully",
c.server.config.Server.Name, c.Nick()))
c.sendSnomask('s', fmt.Sprintf("Configuration reloaded by %s", c.Nick()))
}
}
}
// handleTrace handles TRACE command (show server connection tree)
func (c *Client) handleTrace(_ []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
// Show basic server info (simplified implementation)
c.SendMessage(fmt.Sprintf(":%s 200 %s Link %s %s %s",
c.server.config.Server.Name, c.Nick(),
c.server.config.Server.Version,
c.server.config.Server.Name,
"TechIRCd"))
clientCount := len(c.server.GetClients())
c.SendMessage(fmt.Sprintf(":%s 262 %s %s :End of TRACE with %d clients",
c.server.config.Server.Name, c.Nick(),
c.server.config.Server.Name, clientCount))
}
// isValidNickname checks if a nickname is valid
func isValidNickname(nick string) bool {
if len(nick) == 0 || len(nick) > 30 {
return false
}
// First character must be a letter or special char
first := nick[0]
if !((first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z') ||
first == '[' || first == ']' || first == '\\' || first == '`' ||
first == '_' || first == '^' || first == '{' || first == '|' || first == '}') {
return false
}
// Rest can be letters, digits, or special chars
for i := 1; i < len(nick); i++ {
c := nick[i]
if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
c == '[' || c == ']' || c == '\\' || c == '`' ||
c == '_' || c == '^' || c == '{' || c == '|' || c == '}' || c == '-') {
return false
}
}
return true
}
// isValidChannelName checks if a channel name is valid
func isValidChannelName(name string) bool {
if len(name) == 0 || len(name) > 50 {
return false
}
return name[0] == '#' || name[0] == '&' || name[0] == '!' || name[0] == '+'
}
// isChannelName checks if a name is a channel name
func isChannelName(name string) bool {
if len(name) == 0 {
return false
}
return name[0] == '#' || name[0] == '&' || name[0] == '!' || name[0] == '+'
}
// Server linking commands
// handleConnect handles CONNECT command for server linking
func (c *Client) handleConnect(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "CONNECT :Not enough parameters")
return
}
serverName := parts[1]
portStr := parts[2]
host := "localhost"
if len(parts) > 3 {
host = parts[3]
}
port := 0
if _, err := fmt.Sscanf(portStr, "%d", &port); err != nil || port <= 0 || port > 65535 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "CONNECT :Invalid port number")
return
}
// Check if server is already connected
if c.server.GetLinkedServer(serverName) != nil {
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Server %s is already connected",
c.server.config.Server.Name, c.Nick(), serverName))
return
}
// Find the server in configuration
var linkConfig *struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
Password string `json:"password"`
AutoConnect bool `json:"auto_connect"`
Hub bool `json:"hub"`
Description string `json:"description"`
}
for _, link := range c.server.config.Linking.Links {
if link.Name == serverName {
linkConfig = &link
break
}
}
if linkConfig == nil {
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** No link configuration found for %s",
c.server.config.Server.Name, c.Nick(), serverName))
return
}
// Use configured values, but allow override from command
connectHost := linkConfig.Host
connectPort := linkConfig.Port
if host != "localhost" {
connectHost = host
}
if portStr != "0" {
connectPort = port
}
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Attempting to connect to %s at %s:%d",
c.server.config.Server.Name, c.Nick(), serverName, connectHost, connectPort))
// Send snomask to other operators
c.sendSnomask('s', fmt.Sprintf("%s initiated connection to %s (%s:%d)",
c.Nick(), serverName, connectHost, connectPort))
// Start connection attempt
go c.server.connectToServer(linkConfig.Name, connectHost, connectPort,
linkConfig.Password, linkConfig.Hub, linkConfig.Description)
}
// handleSquit handles SQUIT command for server disconnection
func (c *Client) handleSquit(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "SQUIT :Not enough parameters")
return
}
serverName := parts[1]
reason := "Operator requested disconnection"
if len(parts) > 2 {
reason = strings.Join(parts[2:], " ")
if len(reason) > 0 && reason[0] == ':' {
reason = reason[1:]
}
}
linkedServer := c.server.GetLinkedServer(serverName)
if linkedServer == nil {
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Server %s is not connected",
c.server.config.Server.Name, c.Nick(), serverName))
return
}
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Disconnecting from %s: %s",
c.server.config.Server.Name, c.Nick(), serverName, reason))
// Send snomask to other operators
c.sendSnomask('s', fmt.Sprintf("%s disconnected %s: %s", c.Nick(), serverName, reason))
// Send SQUIT to remote server
linkedServer.SendMessage(fmt.Sprintf("SQUIT %s :%s", c.server.config.Server.Name, reason))
// Remove the server
c.server.RemoveLinkedServer(serverName)
}
// handleLinks handles LINKS command to show server links
func (c *Client) handleLinks() {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
// Show our server first
c.SendNumeric(364, fmt.Sprintf("%s %s :0 %s",
c.server.config.Server.Name, c.server.config.Server.Name, c.server.config.Server.Description))
// Show linked servers
linkedServers := c.server.GetLinkedServers()
for _, linkedServer := range linkedServers {
if linkedServer.IsConnected() {
c.SendNumeric(364, fmt.Sprintf("%s %s :1 %s",
linkedServer.Name(), c.server.config.Server.Name, linkedServer.Description()))
}
}
c.SendNumeric(365, ":End of /LINKS list")
}
// handleUserhost handles USERHOST command
func (c *Client) handleUserhost(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "USERHOST :Not enough parameters")
return
}
var responses []string
// USERHOST can take up to 5 nicknames
maxNicks := 5
if len(parts)-1 < maxNicks {
maxNicks = len(parts) - 1
}
for i := 1; i <= maxNicks && i < len(parts); i++ {
nick := parts[i]
target := c.server.GetClient(nick)
if target != nil {
response := target.Nick() + "="
if target.IsOper() {
response += "*"
}
if target.Away() != "" {
response += "-"
} else {
response += "+"
}
response += target.User() + "@" + target.HostForUser(c)
responses = append(responses, response)
}
}
if len(responses) > 0 {
c.SendNumeric(RPL_USERHOST, ":"+strings.Join(responses, " "))
}
}
// handleIson handles ISON command
func (c *Client) handleIson(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "ISON :Not enough parameters")
return
}
var onlineNicks []string
for i := 1; i < len(parts); i++ {
nick := parts[i]
if c.server.GetClient(nick) != nil {
onlineNicks = append(onlineNicks, nick)
}
}
c.SendNumeric(RPL_ISON, ":"+strings.Join(onlineNicks, " "))
}
// handleTime handles TIME command
func (c *Client) handleTime() {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
currentTime := time.Now().Format("Mon Jan 2 15:04:05 2006")
c.SendNumeric(RPL_TIME, fmt.Sprintf("%s :%s", c.server.config.Server.Name, currentTime))
}
// handleVersion handles VERSION command
func (c *Client) handleVersion() {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
version := fmt.Sprintf("TechIRCd-%s", c.server.config.Server.Version)
c.SendNumeric(RPL_VERSION, fmt.Sprintf("%s %s :Go IRC Server by ComputerTech312",
version, c.server.config.Server.Name))
}
// handleAdmin handles ADMIN command
func (c *Client) handleAdmin() {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
c.SendNumeric(RPL_ADMINME, fmt.Sprintf("%s :Administrative info", c.server.config.Server.Name))
c.SendNumeric(RPL_ADMINLOC1, ":TechIRCd Server")
c.SendNumeric(RPL_ADMINLOC2, ":Modern IRC Server written in Go")
c.SendNumeric(RPL_ADMINEMAIL, fmt.Sprintf(":%s", c.server.config.Server.AdminInfo))
}
// handleInfo handles INFO command
func (c *Client) handleInfo() {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
infoLines := []string{
"TechIRCd - Modern IRC Server",
"",
"Version: " + c.server.config.Server.Version,
"Network: " + c.server.config.Server.Network,
"Description: " + c.server.config.Server.Description,
"",
"Features:",
"- Full RFC 2812 compliance",
"- Advanced operator system with hierarchical classes",
"- Comprehensive channel management",
"- God Mode and Stealth Mode",
"- Revolutionary WHOIS system",
"- Server linking support",
"- Real-time health monitoring",
"",
"Written in Go by ComputerTech312",
"https://github.com/ComputerTech312/TechIRCd",
}
for _, line := range infoLines {
c.SendNumeric(RPL_INFO, ":"+line)
}
c.SendNumeric(RPL_ENDOFINFO, ":End of /INFO list")
}
// handleLusers handles LUSERS command
func (c *Client) handleLusers() {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
totalUsers := len(c.server.GetClients())
totalChannels := len(c.server.GetChannels())
// Count operators
operCount := 0
invisibleCount := 0
for _, client := range c.server.GetClients() {
if client.IsOper() {
operCount++
}
if client.HasMode('i') {
invisibleCount++
}
}
visibleUsers := totalUsers - invisibleCount
unknownConnections := 0 // Connections that haven't completed registration
c.SendNumeric(RPL_LUSERCLIENT, fmt.Sprintf(":There are %d users and %d invisible on 1 servers",
visibleUsers, invisibleCount))
if operCount > 0 {
c.SendNumeric(RPL_LUSEROP, fmt.Sprintf("%d :operator(s) online", operCount))
}
if unknownConnections > 0 {
c.SendNumeric(RPL_LUSERUNKNOWN, fmt.Sprintf("%d :unknown connection(s)", unknownConnections))
}
if totalChannels > 0 {
c.SendNumeric(RPL_LUSERCHANNELS, fmt.Sprintf("%d :channels formed", totalChannels))
}
c.SendNumeric(RPL_LUSERME, fmt.Sprintf(":I have %d clients and 1 servers", totalUsers))
}
// handleStats handles STATS command
func (c *Client) handleStats(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
// Only operators can use most STATS queries
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
statsType := "l" // default to links
if len(parts) > 1 {
statsType = strings.ToLower(parts[1])
}
switch statsType {
case "l": // Links
// Show server links
linkedServers := c.server.GetLinkedServers()
for name, server := range linkedServers {
if server.IsConnected() {
c.SendNumeric(RPL_STATSLINKINFO, fmt.Sprintf("%s 0 0 0 0 0 0", name))
}
}
case "u": // Uptime
uptime := time.Since(c.server.healthMonitor.startTime)
c.SendNumeric(RPL_STATSUPTIME, fmt.Sprintf(":Server Up %d days %d:%02d:%02d",
int(uptime.Hours())/24, int(uptime.Hours())%24,
int(uptime.Minutes())%60, int(uptime.Seconds())%60))
case "o": // Operators
for _, oper := range c.server.config.Opers {
c.SendNumeric(RPL_STATSOLINE, fmt.Sprintf("O %s * %s", oper.Host, oper.Name))
}
case "m": // Commands
// Would show command usage statistics
c.SendNumeric(RPL_STATSCOMMANDS, "PRIVMSG 1234 567 890")
c.SendNumeric(RPL_STATSCOMMANDS, "JOIN 456 123 789")
c.SendNumeric(RPL_STATSCOMMANDS, "PART 234 89 456")
default:
c.SendNumeric(ERR_NOSUCHSERVER, fmt.Sprintf("%s :No such server", statsType))
return
}
c.SendNumeric(RPL_ENDOFSTATS, fmt.Sprintf("%s :End of /STATS report", statsType))
}
// handleSilence handles SILENCE command (user-level blocking)
func (c *Client) handleSilence(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
// List current silence masks
if len(c.silenceList) == 0 {
c.SendNumeric(RPL_ENDOFSILELIST, ":End of Silence list")
return
}
for _, mask := range c.silenceList {
c.SendNumeric(RPL_SILELIST, mask)
}
c.SendNumeric(RPL_ENDOFSILELIST, ":End of Silence list")
return
}
mask := parts[1]
if strings.HasPrefix(mask, "-") {
// Remove from silence list
mask = mask[1:]
for i, silenceMask := range c.silenceList {
if silenceMask == mask {
c.silenceList = append(c.silenceList[:i], c.silenceList[i+1:]...)
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :Removed %s from silence list",
c.server.config.Server.Name, c.Nick(), mask))
return
}
}
} else {
// Add to silence list
if len(c.silenceList) >= 32 { // Limit silence list size
c.SendNumeric(ERR_SILELISTFULL, ":Your silence list is full")
return
}
c.silenceList = append(c.silenceList, mask)
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :Added %s to silence list",
c.server.config.Server.Name, c.Nick(), mask))
}
}
// handleMonitor handles MONITOR command (IRCv3)
func (c *Client) handleMonitor(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "MONITOR :Not enough parameters")
return
}
subcommand := strings.ToUpper(parts[1])
switch subcommand {
case "+": // Add nicknames to monitor list
if len(parts) < 3 {
return
}
nicks := strings.Split(parts[2], ",")
var online []string
var offline []string
for _, nick := range nicks {
if len(c.monitorList) >= 100 { // Limit monitor list size
c.SendNumeric(ERR_MONLISTFULL, fmt.Sprintf("%d %s :Monitor list is full",
100, nick))
break
}
c.monitorList = append(c.monitorList, nick)
if target := c.server.GetClient(nick); target != nil {
online = append(online, nick)
} else {
offline = append(offline, nick)
}
}
if len(online) > 0 {
c.SendNumeric(RPL_MONONLINE, ":"+strings.Join(online, ","))
}
if len(offline) > 0 {
c.SendNumeric(RPL_MONOFFLINE, ":"+strings.Join(offline, ","))
}
case "-": // Remove nicknames from monitor list
if len(parts) < 3 {
return
}
nicks := strings.Split(parts[2], ",")
for _, nick := range nicks {
for i, monitorNick := range c.monitorList {
if strings.EqualFold(monitorNick, nick) {
c.monitorList = append(c.monitorList[:i], c.monitorList[i+1:]...)
break
}
}
}
case "C": // Clear monitor list
c.monitorList = []string{}
case "L": // List monitor list
if len(c.monitorList) == 0 {
c.SendNumeric(RPL_ENDOFMONLIST, ":End of MONITOR list")
return
}
// Send in batches of 10
for i := 0; i < len(c.monitorList); i += 10 {
end := i + 10
if end > len(c.monitorList) {
end = len(c.monitorList)
}
batch := c.monitorList[i:end]
c.SendNumeric(RPL_MONLIST, ":"+strings.Join(batch, ","))
}
c.SendNumeric(RPL_ENDOFMONLIST, ":End of MONITOR list")
case "S": // Show status
var online []string
var offline []string
for _, nick := range c.monitorList {
if c.server.GetClient(nick) != nil {
online = append(online, nick)
} else {
offline = append(offline, nick)
}
}
if len(online) > 0 {
c.SendNumeric(RPL_MONONLINE, ":"+strings.Join(online, ","))
}
if len(offline) > 0 {
c.SendNumeric(RPL_MONOFFLINE, ":"+strings.Join(offline, ","))
}
}
}
// handleAuthenticate handles AUTHENTICATE command for SASL
func (c *Client) handleAuthenticate(parts []string) {
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "AUTHENTICATE :Not enough parameters")
return
}
if c.IsRegistered() {
c.SendNumeric(ERR_ALREADYREGISTRED, ":You may not reregister")
return
}
mechanism := strings.ToUpper(parts[1])
// Support PLAIN mechanism for now
if mechanism == "PLAIN" {
c.saslMech = "PLAIN"
c.SendMessage("AUTHENTICATE +")
return
}
if mechanism == "*" {
// Abort SASL authentication
c.saslMech = ""
c.saslData = ""
c.SendNumeric(ERR_SASLABORTED, ":SASL authentication aborted")
return
}
if c.saslMech == "PLAIN" && mechanism != "PLAIN" {
// This is the SASL data
saslData := parts[1]
if saslData == "+" {
// Empty response, wait for data
return
}
// Decode base64 SASL data (simplified - in real implementation would use proper base64)
// Format: authzid\0authcid\0password
// For simplicity, we'll expect username:password format
if strings.Contains(saslData, ":") {
credentials := strings.SplitN(saslData, ":", 2)
if len(credentials) == 2 {
username := credentials[0]
password := credentials[1]
// Check against configured accounts (simplified)
if c.authenticateUser(username, password) {
c.account = username
c.SendNumeric(RPL_SASLSUCCESS, ":SASL authentication successful")
} else {
c.SendNumeric(ERR_SASLFAIL, ":SASL authentication failed")
}
} else {
c.SendNumeric(ERR_SASLFAIL, ":SASL authentication failed")
}
} else {
c.SendNumeric(ERR_SASLFAIL, ":SASL authentication failed")
}
c.saslMech = ""
c.saslData = ""
return
}
// Unsupported mechanism
c.SendNumeric(ERR_SASLNOTSUPP, fmt.Sprintf("%s :are available SASL mechanisms", "PLAIN"))
}
// authenticateUser checks user credentials (simplified implementation)
func (c *Client) authenticateUser(username, password string) bool {
// In a real implementation, this would check against a database or services
// For now, we'll use a simple hardcoded check
accounts := map[string]string{
"admin": "password123",
"testuser": "test123",
}
if storedPassword, exists := accounts[username]; exists {
return storedPassword == password
}
return false
}
// handleHelpop handles HELPOP command - help for IRC operators and users
func (c *Client) handleHelpop(parts []string) {
if !c.IsRegistered() {
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
return
}
topic := "index"
if len(parts) > 1 {
topic = strings.ToLower(parts[1])
}
switch topic {
case "index", "help", "":
c.sendHelpIndex()
case "modes":
c.sendHelpModes()
case "commands":
c.sendHelpCommands()
case "chanmodes":
c.sendHelpChannelModes()
case "usermodes":
c.sendHelpUserModes()
case "oper":
c.sendHelpOper()
case "god", "godmode":
c.sendHelpGodMode()
case "stealth", "stealthmode":
c.sendHelpStealthMode()
case "ircv3":
c.sendHelpIRCv3()
case "linking":
c.sendHelpLinking()
case "examples":
c.sendHelpExamples()
default:
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** Unknown HELPOP topic: %s",
c.server.config.Server.Name, c.Nick(), topic))
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** Use /HELPOP INDEX for available topics",
c.server.config.Server.Name, c.Nick()))
}
}
// sendHelpIndex sends the main help index
func (c *Client) sendHelpIndex() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd Help System ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Available help topics:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : COMMANDS - List of available commands",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : CHANMODES - Channel modes (+mntispkl etc)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : USERMODES - User modes (+iwsoBGS etc)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : OPER - IRC Operator commands",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : GODMODE - God Mode features (+G)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : STEALTHMODE - Stealth Mode features (+S)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : IRCV3 - IRCv3 capabilities",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : LINKING - Server linking",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : EXAMPLES - Usage examples",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Usage: /HELPOP <topic>",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpChannelModes sends help about channel modes
func (c *Client) sendHelpChannelModes() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd Channel Modes ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Basic Modes:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +m Moderated (only voiced users can speak)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +n No external messages",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +t Topic protection (ops only)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +i Invite only",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +s Secret channel",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +p Private channel",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +k Channel key/password",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +l User limit",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Advanced Modes:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +R Registered users only",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +M Muted (only ops can speak)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +N No notice messages",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +C No CTCP messages",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +c No colors/formatting",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +S SSL/TLS users only",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +O Opers only",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +z Reduced moderation",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +D Delay join",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +G Word filter",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +f Flood protection",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +j Join throttling",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :User Levels:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +q Owner/Founder (~)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +o Operator (@)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +h Half-operator (%%)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +v Voice (+)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpUserModes sends help about user modes
func (c *Client) sendHelpUserModes() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd User Modes ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Basic Modes:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +i Invisible (hidden from WHO)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +w Wallops (receive WALLOPS messages)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +s Server notices (oper only)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +o IRC Operator (automatic)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +r Registered (services only)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +x Host masking",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +z SSL/TLS connection (automatic)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +B Bot flag",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :TechIRCd Special Modes:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +G God Mode (oper only, ultimate power)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : +S Stealth Mode (oper only, invisible to users)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Usage: /MODE yournick +mode",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpGodMode sends help about God Mode
func (c *Client) sendHelpGodMode() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd God Mode (+G) ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :God Mode gives ultimate channel override powers:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Bypass ALL channel restrictions (+k, +l, +i, +b)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Set channel modes without operator privileges",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Cannot be kicked from channels",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Immune to channel bans",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Join invite-only channels instantly",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Requirements:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Must be an IRC operator (/OPER)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Operator class needs 'god_mode' permission",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Usage: /MODE yournick +G",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpCommands sends help about available commands
func (c *Client) sendHelpCommands() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd Commands ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Basic Commands:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /JOIN #channel - Join a channel",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /PART #channel - Leave a channel",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /PRIVMSG target - Send a message",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /NOTICE target - Send a notice",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /WHOIS nick - Get user info",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /MODE target - Set modes",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /TOPIC #channel - Set/view topic",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /KICK #chan nick - Kick user",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /INVITE nick #chan - Invite user",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Operator Commands:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /OPER name pass - Become operator",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /KILL nick reason - Disconnect user",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /WALLOPS message - Message to +w users",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /REHASH - Reload config",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :IRCv3 Commands:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /CAP LS - List capabilities",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /MONITOR +nick - Monitor user",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /AUTHENTICATE - SASL authentication",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpModes sends general help about modes
func (c *Client) sendHelpModes() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd Modes ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :TechIRCd supports both user modes and channel modes.",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Use /HELPOP USERMODES for user mode help",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Use /HELPOP CHANMODES for channel mode help",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpOper sends help about operator commands
func (c *Client) sendHelpOper() {
if !c.IsOper() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** You are not an IRC operator ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
return
}
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd Operator Help ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Available operator commands:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /KILL nick reason - Disconnect user",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /WALLOPS message - Send to +w users",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /OPERWALL message - Send to operators",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /GLOBALNOTICE message - Send to all users",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /REHASH - Reload configuration",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /SNOMASK +modes - Set notice masks",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /CONNECT server port - Link to server",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /SQUIT server reason - Unlink server",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Your operator class: %s",
c.server.config.Server.Name, c.Nick(), c.OperClass()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpStealthMode sends help about Stealth Mode
func (c *Client) sendHelpStealthMode() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd Stealth Mode (+S) ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Stealth Mode makes you invisible to regular users:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Hidden from WHO commands",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Hidden from NAMES lists",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• WHOIS restricted (operators can still see you)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Still visible to other operators",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Requirements:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Must be an IRC operator (/OPER)",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• Operator class needs 'stealth_mode' permission",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Usage: /MODE yournick +S",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpIRCv3 sends help about IRCv3 features
func (c *Client) sendHelpIRCv3() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd IRCv3 Support ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :TechIRCd supports modern IRCv3 features:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• server-time - Message timestamps",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• account-notify - Account change notifications",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• away-notify - Away status notifications",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• extended-join - Enhanced JOIN with account info",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• multi-prefix - Multiple user prefixes",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• sasl - SASL authentication",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• message-tags - Message tag support",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :• echo-message - Message echo back to sender",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Usage: /CAP REQ :server-time account-notify",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpLinking sends help about server linking
func (c *Client) sendHelpLinking() {
if !c.IsOper() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** Operator access required ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
return
}
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd Server Linking ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Server linking commands:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /CONNECT server port - Connect to remote server",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /SQUIT server reason - Disconnect server",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /LINKS - Show linked servers",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Servers must be configured in linking.json",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// sendHelpExamples sends usage examples
func (c *Client) sendHelpExamples() {
c.SendMessage(fmt.Sprintf(":%s 292 %s :*** TechIRCd Usage Examples ***",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Join a channel:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /JOIN #help",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Set channel to moderated:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /MODE #help +m",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Enable God Mode (operators only):",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /MODE yournick +G",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s :Request IRCv3 capabilities:",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 292 %s : /CAP REQ :server-time message-tags",
c.server.config.Server.Name, c.Nick()))
c.SendMessage(fmt.Sprintf(":%s 294 %s :End of /HELPOP",
c.server.config.Server.Name, c.Nick()))
}
// expandBanMask expands partial ban masks to full IRC hostmask format
func expandBanMask(mask string) string {
// If it's already a full hostmask (contains ! and @), return as-is
if strings.Contains(mask, "!") && strings.Contains(mask, "@") {
return mask
}
// If it's just a nickname, expand to nick!*@*
if !strings.Contains(mask, "!") && !strings.Contains(mask, "@") {
return fmt.Sprintf("%s!*@*", mask)
}
// If it has ! but no @, assume it's nick!user format, add @*
if strings.Contains(mask, "!") && !strings.Contains(mask, "@") {
return fmt.Sprintf("%s@*", mask)
}
// If it has @ but no !, assume it's @host format, add *!*
if !strings.Contains(mask, "!") && strings.Contains(mask, "@") {
return fmt.Sprintf("*!*%s", mask)
}
// Default case - return as-is
return mask
}
// Services/Admin Commands
// handleChghost handles CHGHOST command - change user's hostname
func (c *Client) handleChghost(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "CHGHOST :Not enough parameters")
return
}
targetNick := parts[1]
newHost := parts[2]
target := c.server.GetClient(targetNick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
return
}
oldHost := target.Host()
target.host = newHost
// Notify the target user
target.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Your hostname has been changed to %s",
c.server.config.Server.Name, target.Nick(), newHost))
// Notify all channels the user is in
for channelName := range target.channels {
channel := c.server.GetChannel(channelName)
if channel != nil {
channel.BroadcastFrom(c.server.config.Server.Name,
fmt.Sprintf("CHGHOST %s %s %s", target.Nick(), oldHost, newHost), nil)
}
}
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s used CHGHOST to change %s's hostname from %s to %s",
c.Nick(), target.Nick(), oldHost, newHost))
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Changed hostname of %s from %s to %s",
c.server.config.Server.Name, c.Nick(), target.Nick(), oldHost, newHost))
}
// handleSvsnick handles SVSNICK command - services force nickname change
func (c *Client) handleSvsnick(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "SVSNICK :Not enough parameters")
return
}
targetNick := parts[1]
newNick := parts[2]
target := c.server.GetClient(targetNick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
return
}
// Check if new nick is already in use
if c.server.IsNickInUse(newNick) {
c.SendNumeric(ERR_NICKNAMEINUSE, newNick+" :Nickname is already in use")
return
}
oldNick := target.Nick()
// Send NICK change to all channels the user is in
for channelName := range target.channels {
channel := c.server.GetChannel(channelName)
if channel != nil {
channel.BroadcastFrom(target.Prefix(), fmt.Sprintf("NICK :%s", newNick), nil)
}
}
// Change the nickname
target.SetNick(newNick)
// Notify the user
target.SendMessage(fmt.Sprintf(":%s NICK :%s", oldNick, newNick))
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s used SVSNICK to change %s's nickname to %s",
c.Nick(), oldNick, newNick))
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Changed nickname of %s to %s",
c.server.config.Server.Name, c.Nick(), oldNick, newNick))
}
// handleSvsmode handles SVSMODE command - services force mode change
func (c *Client) handleSvsmode(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "SVSMODE :Not enough parameters")
return
}
target := parts[1]
modeString := parts[2]
// Check if target is a channel or user
if strings.HasPrefix(target, "#") || strings.HasPrefix(target, "&") {
// Channel mode change
channel := c.server.GetChannel(target)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
return
}
// Apply mode changes (simplified implementation)
channel.BroadcastFrom(c.server.config.Server.Name, fmt.Sprintf("MODE %s %s", target, modeString), nil)
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s used SVSMODE to set %s on %s",
c.Nick(), modeString, target))
} else {
// User mode change
targetClient := c.server.GetClient(target)
if targetClient == nil {
c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel")
return
}
// Apply user mode changes
targetClient.SendMessage(fmt.Sprintf(":%s MODE %s %s",
c.server.config.Server.Name, target, modeString))
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s used SVSMODE to set %s on %s",
c.Nick(), modeString, target))
}
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Set mode %s on %s",
c.server.config.Server.Name, c.Nick(), modeString, target))
}
// handleSamode handles SAMODE command - services admin mode change
func (c *Client) handleSamode(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "SAMODE :Not enough parameters")
return
}
target := parts[1]
modeString := parts[2]
args := parts[3:]
if !strings.HasPrefix(target, "#") && !strings.HasPrefix(target, "&") {
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
return
}
channel := c.server.GetChannel(target)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
return
}
// Build complete mode command
modeCommand := fmt.Sprintf("MODE %s %s", target, modeString)
if len(args) > 0 {
modeCommand += " " + strings.Join(args, " ")
}
// Broadcast mode change
channel.BroadcastFrom(c.Prefix(), modeCommand, nil)
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s used SAMODE: %s", c.Nick(), modeCommand))
}
// handleSanick handles SANICK command - services admin nickname change
func (c *Client) handleSanick(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "SANICK :Not enough parameters")
return
}
targetNick := parts[1]
newNick := parts[2]
target := c.server.GetClient(targetNick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
return
}
// Check if new nick is already in use
if c.server.IsNickInUse(newNick) {
c.SendNumeric(ERR_NICKNAMEINUSE, newNick+" :Nickname is already in use")
return
}
oldNick := target.Nick()
// Send NICK change to all channels the user is in
for channelName := range target.channels {
channel := c.server.GetChannel(channelName)
if channel != nil {
channel.BroadcastFrom(target.Prefix(), fmt.Sprintf("NICK :%s", newNick), nil)
}
}
// Change the nickname
target.SetNick(newNick)
// Notify the user
target.SendMessage(fmt.Sprintf(":%s NICK :%s", oldNick, newNick))
target.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Your nickname has been changed by services",
c.server.config.Server.Name, newNick))
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s used SANICK to change %s's nickname to %s",
c.Nick(), oldNick, newNick))
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Changed nickname of %s to %s",
c.server.config.Server.Name, c.Nick(), oldNick, newNick))
}
// handleSakick handles SAKICK command - services admin kick
func (c *Client) handleSakick(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "SAKICK :Not enough parameters")
return
}
channelName := parts[1]
targetNick := parts[2]
reason := "Services Admin Kick"
if len(parts) > 3 {
reason = strings.Join(parts[3:], " ")
if strings.HasPrefix(reason, ":") {
reason = reason[1:]
}
}
channel := c.server.GetChannel(channelName)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
return
}
target := c.server.GetClient(targetNick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
return
}
if !target.IsInChannel(channelName) {
c.SendNumeric(ERR_USERNOTINCHANNEL, targetNick+" "+channelName+" :They aren't on that channel")
return
}
// Broadcast kick message
kickMsg := fmt.Sprintf("KICK %s %s :%s", channelName, targetNick, reason)
channel.BroadcastFrom(c.Prefix(), kickMsg, nil)
// Remove user from channel
channel.RemoveClient(target)
target.RemoveChannel(channelName)
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s used SAKICK to kick %s from %s (%s)",
c.Nick(), targetNick, channelName, reason))
}
// handleSapart handles SAPART command - services admin part
func (c *Client) handleSapart(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "SAPART :Not enough parameters")
return
}
targetNick := parts[1]
channelName := parts[2]
reason := "Services Admin Part"
if len(parts) > 3 {
reason = strings.Join(parts[3:], " ")
if strings.HasPrefix(reason, ":") {
reason = reason[1:]
}
}
target := c.server.GetClient(targetNick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
return
}
channel := c.server.GetChannel(channelName)
if channel == nil {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
return
}
if !target.IsInChannel(channelName) {
c.SendNumeric(ERR_NOTONCHANNEL, channelName+" :You're not on that channel")
return
}
// Broadcast part message
partMsg := fmt.Sprintf("PART %s :%s", channelName, reason)
channel.BroadcastFrom(target.Prefix(), partMsg, nil)
// Remove user from channel
channel.RemoveClient(target)
target.RemoveChannel(channelName)
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s used SAPART to part %s from %s (%s)",
c.Nick(), targetNick, channelName, reason))
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Forced %s to part %s",
c.server.config.Server.Name, c.Nick(), targetNick, channelName))
}
// handleSajoin handles SAJOIN command - services admin join
func (c *Client) handleSajoin(parts []string) {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
if len(parts) < 3 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "SAJOIN :Not enough parameters")
return
}
targetNick := parts[1]
channelName := parts[2]
target := c.server.GetClient(targetNick)
if target == nil {
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
return
}
if target.IsInChannel(channelName) {
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** %s is already on %s",
c.server.config.Server.Name, c.Nick(), targetNick, channelName))
return
}
// Get or create channel
channel := c.server.GetOrCreateChannel(channelName)
// Add user to channel
channel.AddClient(target)
target.AddChannel(channel)
// Broadcast join message
joinMsg := fmt.Sprintf("JOIN :%s", channelName)
channel.BroadcastFrom(target.Prefix(), joinMsg, nil)
// Send topic if exists
if channel.Topic() != "" {
target.SendNumeric(RPL_TOPIC, channelName+" :"+channel.Topic())
target.SendNumeric(RPL_TOPICWHOTIME, fmt.Sprintf("%s %s %d",
channelName, channel.TopicBy(), channel.TopicTime().Unix()))
}
// Send names list
target.sendNames(channel)
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s used SAJOIN to join %s to %s",
c.Nick(), targetNick, channelName))
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Forced %s to join %s",
c.server.config.Server.Name, c.Nick(), targetNick, channelName))
}
// handleWhowas handles WHOWAS command
func (c *Client) handleWhowas(parts []string) {
if len(parts) < 2 {
c.SendNumeric(ERR_NONICKNAMEGIVEN, ":No nickname given")
return
}
nick := parts[1]
// Send end of whowas (we don't store history)
c.SendNumeric(RPL_ENDOFWHOWAS, nick+" :End of WHOWAS")
}
// handleMotd handles MOTD command
func (c *Client) handleMotd() {
motd := []string{
"Welcome to TechIRCd!",
"",
"This is a modern IRC server with IRCv3 features.",
"For help, join #help or contact an operator.",
"",
"Enjoy your stay!",
}
c.SendNumeric(RPL_MOTDSTART, ":- " + c.server.config.Server.Name + " Message of the day - ")
for _, line := range motd {
c.SendNumeric(RPL_MOTD, ":- " + line)
}
c.SendNumeric(RPL_ENDOFMOTD, ":End of MOTD command")
}
// handleRules handles RULES command
func (c *Client) handleRules() {
rules := []string{
"Server Rules:",
"",
"1. Be respectful to other users",
"2. No spamming or flooding",
"3. No harassment or abuse",
"4. Keep channels on-topic",
"5. Follow operator instructions",
"",
"Violations may result in kicks, bans, or K-lines.",
}
c.SendNumeric(RPL_RULESSTART, ":- " + c.server.config.Server.Name + " server rules:")
for _, line := range rules {
c.SendNumeric(RPL_RULES, ":- " + line)
}
c.SendNumeric(RPL_ENDOFRULES, ":End of RULES command")
}
// handleMap handles MAP command
func (c *Client) handleMap() {
c.SendNumeric(RPL_MAP, fmt.Sprintf(":%s (Users: %d)",
c.server.config.Server.Name, len(c.server.clients)))
c.SendNumeric(RPL_MAPEND, ":End of MAP")
}
// handleKnock handles KNOCK command
func (c *Client) handleKnock(parts []string) {
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "KNOCK :Not enough parameters")
return
}
channelName := parts[1]
message := "has asked for an invite"
if len(parts) > 2 {
message = strings.Join(parts[2:], " ")
}
c.server.mu.RLock()
channel, exists := c.server.channels[channelName]
c.server.mu.RUnlock()
if !exists {
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
return
}
// Check if user is already on channel
if channel.HasClient(c) {
c.SendNumeric(ERR_KNOCKONCHAN, channelName+" :You are already on that channel")
return
}
// Check if channel is invite-only
if !channel.HasMode('i') {
c.SendNumeric(ERR_CHANOPEN, channelName+" :Channel is open")
return
}
// Send knock to channel operators
knockMsg := fmt.Sprintf("KNOCK %s :%s (%s@%s) %s",
channelName, c.Nick(), c.User(), c.Host(), message)
// Broadcast to operators only
clients := channel.GetClients()
for _, client := range clients {
if channel.IsOperator(client) || channel.IsOwner(client) || channel.IsAdmin(client) {
client.SendMessage(":" + c.server.config.Server.Name + " " + knockMsg)
}
}
c.SendNumeric(RPL_KNOCKDLVR, channelName+" :Your KNOCK has been delivered")
}
// handleSetname handles SETNAME command (change real name)
func (c *Client) handleSetname(parts []string) {
if len(parts) < 2 {
c.SendNumeric(ERR_NEEDMOREPARAMS, "SETNAME :Not enough parameters")
return
}
newRealname := strings.Join(parts[1:], " ")
if strings.HasPrefix(newRealname, ":") {
newRealname = newRealname[1:]
}
if len(newRealname) > 50 {
c.SendNumeric(ERR_INVALIDUSERNAME, ":Real name too long")
return
}
c.SetRealname(newRealname)
// Broadcast setname to users who can see this client
setnameMsg := fmt.Sprintf("SETNAME :%s", newRealname)
// Send to all channels user is in
c.server.mu.RLock()
for _, channel := range c.server.channels {
if channel.HasClient(c) {
channel.BroadcastFrom(c.Prefix(), setnameMsg, nil)
}
}
c.server.mu.RUnlock()
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Your real name is now '%s'",
c.server.config.Server.Name, c.Nick(), newRealname))
}
// handleDie handles DIE command (shutdown server)
func (c *Client) handleDie() {
if !c.IsOper() {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
return
}
// Check for die permission
if !c.HasOperPermission("die") {
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You need die permission")
return
}
// Send snomask to operators
c.server.sendSnomask('o', fmt.Sprintf("%s (%s@%s) issued DIE command",
c.Nick(), c.User(), c.Host()))
// Send notice to all users
dieMsg := fmt.Sprintf(":%s NOTICE * :*** Server shutting down by %s",
c.server.config.Server.Name, c.Nick())
c.server.mu.RLock()
for _, client := range c.server.clients {
client.SendMessage(dieMsg)
}
c.server.mu.RUnlock()
// Shutdown server
go c.server.Shutdown()
}