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 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 :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 ", 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() }