package main import ( "fmt" "strings" "time" ) // Dummy Server type definition for compilation. // Replace or expand this with your actual Server struct definition. type Server struct { clients map[string]*Client // Map of nicknames to clients } // GetClient returns the client with the given nickname, or nil if not found. func (s *Server) GetClient(nick string) *Client { if s == nil || s.clients == nil { return nil } return s.clients[nick] } // Dummy Client type definition for compilation. // Replace or expand this with your actual Client struct definition. type Client struct { // Add fields as needed for your implementation. server *Server // Reference to the server instance Nickname string // Nickname of the client Username string // Username of the client Hostname string // Hostname of the client } // Prefix returns the client's prefix in the format nick!user@host. func (c *Client) Prefix() string { return fmt.Sprintf("%s!%s@%s", c.Nick(), c.User(), c.Host()) } // Nick returns the client's nickname. func (c *Client) Nick() string { return c.Nickname } // SetNick sets the client's nickname. func (c *Client) SetNick(nick string) { c.Nickname = nick } // User returns the client's username. func (c *Client) User() string { return c.Username } // SetUser sets the client's username. func (c *Client) SetUser(user string) { c.Username = user } // Host returns the client's hostname. func (c *Client) Host() string { return c.Hostname } // SetHost sets the client's hostname. func (c *Client) SetHost(host string) { c.Hostname = host } // SendNumeric sends a numeric reply to the client. // This is a stub implementation for compilation; replace with your actual logic. func (c *Client) SendNumeric(code int, message string) { fmt.Printf("Numeric %03d: %s\n", code, message) } // IRC numeric reply codes const ( RPL_WELCOME = 001 RPL_YOURHOST = 002 RPL_CREATED = 003 RPL_MYINFO = 004 RPL_ISUPPORT = 005 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_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 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_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 ) // 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() c.SetNick(newNick) // If already registered, notify channels if c.IsRegistered() && oldNick != "" { message := fmt.Sprintf(":%s NICK :%s", c.Prefix(), newNick) for _, channel := range c.GetChannels() { channel.Broadcast(message, nil) } // 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())) } } c.checkRegistration() } // Add a Nick field to Client and implement Nick/SetNick methods // Add this field to Client struct above (not shown here): // Nickname string func (c *Client) Nick() string { // Return the client's nickname return c.Nickname } func (c *Client) SetNick(nick string) { c.Nickname = nick } // 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() { if !c.IsRegistered() && c.Nick() != "" && c.User() != "" { c.SetRegistered(true) c.sendWelcome() } } // sendWelcome sends welcome messages to newly registered client func (c *Client) sendWelcome() { fmt.Printf("DEBUG: sendWelcome called\n") if c.server == nil { fmt.Printf("DEBUG: sendWelcome - server is nil\n") return } if c.server.config == nil { fmt.Printf("DEBUG: sendWelcome - config is nil\n") return } fmt.Printf("DEBUG: sendWelcome - about to send RPL_WELCOME\n") c.SendNumeric(RPL_WELCOME, fmt.Sprintf("Welcome to %s, %s", c.server.config.Server.Network, c.Prefix())) fmt.Printf("DEBUG: sendWelcome - sent RPL_WELCOME\n") 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 o o", c.server.config.Server.Name, c.server.config.Server.Version)) // 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())) } fmt.Printf("DEBUG: sendWelcome completed\n") } // handlePing handles PING command func (c *Client) handlePing(parts []string) { if len(parts) < 2 { return } token := parts[1] if len(token) > 0 && token[0] == ':' { token = token[1:] } serverName := "localhost" if c.server != nil && c.server.config != nil { serverName = c.server.config.Server.Name } c.SendMessage(fmt.Sprintf("PONG %s :%s", serverName, token)) } // handlePong handles PONG command func (c *Client) handlePong(parts []string) { // Update the last pong time for ping timeout tracking // This is used by the client Handler's ping timeout mechanism c.mu.Lock() c.lastPong = time.Now() c.waitingForPong = false c.mu.Unlock() } // handleJoin handles JOIN command func (c *Client) handleJoin(parts []string) { 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 key := "" if i < len(keys) { key = keys[i] } 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 } // Join the channel channel.AddClient(c) c.AddChannel(channel) message := fmt.Sprintf(":%s JOIN :%s", c.Prefix(), channelName) channel.Broadcast(message, nil) // 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 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:] } 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 can send messages to this channel (moderated mode check) if !channel.CanSendMessage(c) { c.SendNumeric(ERR_CANNOTSENDTOCHAN, target+" :Cannot send to channel (+m)") return } msg := fmt.Sprintf(":%s PRIVMSG %s :%s", c.Prefix(), target, message) channel.Broadcast(msg, c) } 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())) } msg := fmt.Sprintf(":%s PRIVMSG %s :%s", c.Prefix(), target, message) targetClient.SendMessage(msg) } } // 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) } } // 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() { flags := "" if client.IsOper() { flags += "*" } if client.Away() != "" { flags += "G" } else { flags += "H" } if channel.IsOperator(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.Host(), 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 } c.SendNumeric(RPL_WHOISUSER, fmt.Sprintf("%s %s %s * :%s", target.Nick(), target.User(), target.Host(), target.Realname())) c.SendNumeric(RPL_WHOISSERVER, fmt.Sprintf("%s %s :%s", target.Nick(), c.server.config.Server.Name, c.server.config.Server.Description)) if target.IsOper() { c.SendNumeric(RPL_WHOISOPERATOR, target.Nick()+" :is an IRC operator") } if target.Away() != "" { c.SendNumeric(RPL_AWAY, fmt.Sprintf("%s :%s", target.Nick(), target.Away())) } // Send channels var channels []string for _, channel := range target.GetChannels() { channelName := channel.Name() 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, " "))) } // Show user modes if the requester is an operator or the target user if c.IsOper() || c.Nick() == target.Nick() { 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)) } } // Show SSL status if target.IsSSL() { c.SendMessage(fmt.Sprintf(":%s 671 %s %s :is using a secure connection", 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() { name := client.Nick() if channel.IsOwner(client) { name = "~" + name } else if channel.IsOperator(client) { name = "@" + name } else if channel.IsHalfop(client) { name = "%" + name } else if channel.IsVoice(client) { name = "+" + 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:] } } c.server.RemoveClient(c) } // 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") } 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) if !channel.IsOwner(c) && !channel.IsOperator(c) && !channel.IsHalfop(c) && !c.IsOper() { c.SendNumeric(ERR_CHANOPRIVSNEEDED, target+" :You're not channel operator") return } 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++ 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 } 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++ 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 } 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++ 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 } 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++ // Only existing owners can grant/remove owner status if !channel.IsOwner(c) && !c.IsOper() { 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 } channel.SetOwner(targetClient, adding) if adding { appliedModes = append(appliedModes, "+q") } else { appliedModes = append(appliedModes, "-q") } 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) if argIndex >= len(args) { // List bans (TODO: implement ban list display) continue } mask := args[argIndex] argIndex++ // 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.banList = append(channel.banList, mask) appliedModes = append(appliedModes, "+b") } else { for i, ban := range channel.banList { if ban == mask { channel.banList = append(channel.banList[:i], channel.banList[i+1:]...) appliedModes = append(appliedModes, "-b") break } } } appliedArgs = append(appliedArgs, mask) } } else { // Regular ban if adding { channel.banList = append(channel.banList, mask) appliedModes = append(appliedModes, "+b") } else { for i, ban := range channel.banList { if ban == mask { channel.banList = append(channel.banList[:i], channel.banList[i+1:]...) appliedModes = append(appliedModes, "-b") break } } } appliedArgs = append(appliedArgs, mask) } 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(parts []string) { 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 } // 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 target.conn.Close() } // 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 } // Find matching oper configuration 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 == "*@*" { c.SetOper(true) // 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 c.SendNumeric(RPL_YOUREOPER, ":You are now an IRC operator") 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 c.sendSnomask('o', fmt.Sprintf("%s (%s@%s) is now an IRC operator", c.Nick(), c.User(), c.Host())) return } } } c.SendNumeric(ERR_PASSWDMISMATCH, ":Password incorrect") } // 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(parts []string) { 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(parts []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)) } // handleSpy handles SPY command - covert surveillance and stealth operations func (c *Client) handleSpy(parts []string) { if !c.IsOper() { c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator") return } if len(parts) < 2 { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** SPY Usage: SPY ", c.server.config.Server.Name, c.Nick())) return } command := strings.ToLower(parts[1]) switch command { case "hide": c.handleSpyHide(parts[2:]) case "watch": c.handleSpyWatch(parts[2:]) case "track": c.handleSpyTrack(parts[2:]) case "listen": c.handleSpyListen(parts[2:]) case "cloak": c.handleSpyCloak(parts[2:]) case "ghost": c.handleSpyGhost(parts[2:]) case "shadow": c.handleSpyShadow(parts[2:]) case "status": c.handleSpyStatus() default: c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Unknown SPY command: %s", c.server.config.Server.Name, c.Nick(), command)) } } // handleSpyHide - become invisible to most commands and lists func (c *Client) handleSpyHide(args []string) { if len(args) == 0 || strings.ToLower(args[0]) == "on" { c.SetMode('H', true) // Hidden mode c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You are now HIDDEN from WHO, WHOIS, and NAMES", c.server.config.Server.Name, c.Nick())) c.sendSnomask('d', fmt.Sprintf("Operator %s has entered STEALTH mode", c.Nick())) } else if strings.ToLower(args[0]) == "off" { c.SetMode('H', false) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You are now VISIBLE again", c.server.config.Server.Name, c.Nick())) c.sendSnomask('d', fmt.Sprintf("Operator %s has left STEALTH mode", c.Nick())) } } // handleSpyWatch - monitor a specific user's activities func (c *Client) handleSpyWatch(args []string) { if len(args) < 1 { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY WATCH ", c.server.config.Server.Name, c.Nick())) return } target := args[0] if strings.ToLower(target) == "off" { // TODO: Remove from watch list c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Surveillance disabled", c.server.config.Server.Name, c.Nick())) return } targetClient := c.server.GetClient(target) if targetClient == nil { c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel") return } // TODO: Add to watch list c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Now watching %s (%s@%s)", c.server.config.Server.Name, c.Nick(), target, targetClient.User(), targetClient.Host())) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Target is in channels: %s", c.server.config.Server.Name, c.Nick(), c.getChannelList(targetClient))) } // handleSpyTrack - get real-time location and movement tracking func (c *Client) handleSpyTrack(args []string) { if len(args) < 1 { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY TRACK ", c.server.config.Server.Name, c.Nick())) return } target := args[0] targetClient := c.server.GetClient(target) if targetClient == nil { c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel") return } // Show detailed tracking info c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** TRACKING %s", c.server.config.Server.Name, c.Nick(), target)) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Location: %s@%s", c.server.config.Server.Name, c.Nick(), targetClient.User(), targetClient.Host())) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Status: %s", c.server.config.Server.Name, c.Nick(), c.getUserStatus(targetClient))) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Channels: %s", c.server.config.Server.Name, c.Nick(), c.getChannelList(targetClient))) if targetClient.Away() != "" { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Away: %s", c.server.config.Server.Name, c.Nick(), targetClient.Away())) } } // handleSpyListen - tap into channel conversations invisibly func (c *Client) handleSpyListen(args []string) { if len(args) < 1 { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY LISTEN <#channel|off>", c.server.config.Server.Name, c.Nick())) return } target := args[0] if strings.ToLower(target) == "off" { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Wiretaps disabled", c.server.config.Server.Name, c.Nick())) return } if !isChannelName(target) { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Invalid channel name", c.server.config.Server.Name, c.Nick())) return } channel := c.server.GetChannel(target) if channel == nil { c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel") return } // TODO: Add to wiretap list c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Now listening to %s (%d users)", c.server.config.Server.Name, c.Nick(), target, channel.UserCount())) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Wiretap established - you will receive covert copies of all messages", c.server.config.Server.Name, c.Nick())) } // handleSpyCloak - disguise your identity func (c *Client) handleSpyCloak(args []string) { if len(args) < 1 { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY CLOAK ", c.server.config.Server.Name, c.Nick())) return } identity := args[0] if strings.ToLower(identity) == "off" { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Identity cloak removed", c.server.config.Server.Name, c.Nick())) return } // TODO: Implement identity cloaking c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Identity cloaked as: %s", c.server.config.Server.Name, c.Nick(), identity)) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Your true identity is hidden from WHOIS and other commands", c.server.config.Server.Name, c.Nick())) } // handleSpyGhost - become completely invisible in a channel func (c *Client) handleSpyGhost(args []string) { if len(args) < 1 { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY GHOST <#channel|off>", c.server.config.Server.Name, c.Nick())) return } target := args[0] if strings.ToLower(target) == "off" { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Ghost mode disabled", c.server.config.Server.Name, c.Nick())) return } if !isChannelName(target) { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Invalid channel name", c.server.config.Server.Name, c.Nick())) return } channel := c.server.GetChannel(target) if channel == nil { c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel") return } // Join channel invisibly if !c.IsInChannel(target) { channel.AddClient(c) } c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You are now a GHOST in %s", c.server.config.Server.Name, c.Nick(), target)) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You can see everything but are invisible to users", c.server.config.Server.Name, c.Nick())) } // handleSpyShadow - follow a user invisibly across channels func (c *Client) handleSpyShadow(args []string) { if len(args) < 1 { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY SHADOW ", c.server.config.Server.Name, c.Nick())) return } target := args[0] if strings.ToLower(target) == "off" { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Shadow mode disabled", c.server.config.Server.Name, c.Nick())) return } targetClient := c.server.GetClient(target) if targetClient == nil { c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel") return } c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Now shadowing %s", c.server.config.Server.Name, c.Nick(), target)) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You will automatically follow them to any channel they join", c.server.config.Server.Name, c.Nick())) } // handleSpyStatus - show current spy operations func (c *Client) handleSpyStatus() { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** === SPY STATUS ===", c.server.config.Server.Name, c.Nick())) if c.HasMode('H') { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** STEALTH: Active (Hidden from WHO/WHOIS/NAMES)", c.server.config.Server.Name, c.Nick())) } else { c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** STEALTH: Inactive", c.server.config.Server.Name, c.Nick())) } // TODO: Show other active spy operations c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** WATCH: None active", c.server.config.Server.Name, c.Nick())) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** WIRETAPS: None active", c.server.config.Server.Name, c.Nick())) c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** SHADOWS: None active", c.server.config.Server.Name, c.Nick())) } // Helper functions for spy operations func (c *Client) getChannelList(target *Client) string { channels := target.GetChannels() var channelNames []string for _, channel := range channels { channelNames = append(channelNames, channel.Name()) } if len(channelNames) == 0 { return "None" } return strings.Join(channelNames, " ") } func (c *Client) getUserStatus(target *Client) string { status := "Online" if target.Away() != "" { status = "Away" } if target.IsOper() { status += " (Operator)" } if target.HasMode('i') { status += " (Invisible)" } if target.HasMode('B') { status += " (Bot)" } return status } // sendSnomask sends a server notice to operators watching a specific snomask func (c *Client) sendSnomask(snomask rune, message string) { if c.server == nil { return } for _, client := range c.server.GetClients() { if client.IsOper() && client.HasSnomask(snomask) { client.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** %s", c.server.config.Server.Name, client.Nick(), message)) } } } // 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] == '+' }