4099 lines
120 KiB
Go
4099 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)
|
|
}
|
|
}
|
|
|
|
// Remove client from server
|
|
c.server.RemoveClient(c)
|
|
|
|
// Close the connection
|
|
c.conn.Close()
|
|
}
|
|
|
|
// 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
|
|
target.conn.Close()
|
|
}
|
|
|
|
// handleOper handles OPER command
|
|
func (c *Client) handleOper(parts []string) {
|
|
if len(parts) < 3 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "OPER :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
if c.server == nil || c.server.config == nil {
|
|
c.SendNumeric(ERR_NOOPERHOST, ":No O-lines for your host")
|
|
return
|
|
}
|
|
|
|
name := parts[1]
|
|
password := parts[2]
|
|
|
|
// Check if opers are enabled
|
|
if !c.server.config.Features.EnableOper {
|
|
c.SendNumeric(ERR_NOOPERHOST, ":O-lines are disabled")
|
|
return
|
|
}
|
|
|
|
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()
|
|
}
|