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