1964 lines
52 KiB
Go
1964 lines
52 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Dummy Server type definition for compilation.
|
|
// Replace or expand this with your actual Server struct definition.
|
|
type Server struct {
|
|
clients map[string]*Client // Map of nicknames to clients
|
|
}
|
|
|
|
// GetClient returns the client with the given nickname, or nil if not found.
|
|
func (s *Server) GetClient(nick string) *Client {
|
|
if s == nil || s.clients == nil {
|
|
return nil
|
|
}
|
|
return s.clients[nick]
|
|
}
|
|
|
|
// Dummy Client type definition for compilation.
|
|
// Replace or expand this with your actual Client struct definition.
|
|
type Client struct {
|
|
// Add fields as needed for your implementation.
|
|
server *Server // Reference to the server instance
|
|
Nickname string // Nickname of the client
|
|
Username string // Username of the client
|
|
Hostname string // Hostname of the client
|
|
}
|
|
|
|
// Prefix returns the client's prefix in the format nick!user@host.
|
|
func (c *Client) Prefix() string {
|
|
return fmt.Sprintf("%s!%s@%s", c.Nick(), c.User(), c.Host())
|
|
}
|
|
|
|
// Nick returns the client's nickname.
|
|
func (c *Client) Nick() string {
|
|
return c.Nickname
|
|
}
|
|
|
|
// SetNick sets the client's nickname.
|
|
func (c *Client) SetNick(nick string) {
|
|
c.Nickname = nick
|
|
}
|
|
|
|
// User returns the client's username.
|
|
func (c *Client) User() string {
|
|
return c.Username
|
|
}
|
|
|
|
// SetUser sets the client's username.
|
|
func (c *Client) SetUser(user string) {
|
|
c.Username = user
|
|
}
|
|
|
|
// Host returns the client's hostname.
|
|
func (c *Client) Host() string {
|
|
return c.Hostname
|
|
}
|
|
|
|
// SetHost sets the client's hostname.
|
|
func (c *Client) SetHost(host string) {
|
|
c.Hostname = host
|
|
}
|
|
|
|
// SendNumeric sends a numeric reply to the client.
|
|
// This is a stub implementation for compilation; replace with your actual logic.
|
|
func (c *Client) SendNumeric(code int, message string) {
|
|
fmt.Printf("Numeric %03d: %s\n", code, message)
|
|
}
|
|
|
|
// IRC numeric reply codes
|
|
const (
|
|
RPL_WELCOME = 001
|
|
RPL_YOURHOST = 002
|
|
RPL_CREATED = 003
|
|
RPL_MYINFO = 004
|
|
RPL_ISUPPORT = 005
|
|
RPL_AWAY = 301
|
|
RPL_UNAWAY = 305
|
|
RPL_NOWAWAY = 306
|
|
RPL_WHOISUSER = 311
|
|
RPL_WHOISSERVER = 312
|
|
RPL_WHOISOPERATOR = 313
|
|
RPL_WHOISIDLE = 317
|
|
RPL_ENDOFWHOIS = 318
|
|
RPL_WHOISCHANNELS = 319
|
|
RPL_LISTSTART = 321
|
|
RPL_LIST = 322
|
|
RPL_LISTEND = 323
|
|
RPL_CHANNELMODEIS = 324
|
|
RPL_NOTOPIC = 331
|
|
RPL_TOPIC = 332
|
|
RPL_TOPICWHOTIME = 333
|
|
RPL_NAMREPLY = 353
|
|
RPL_ENDOFNAMES = 366
|
|
RPL_MOTDSTART = 375
|
|
RPL_MOTD = 372
|
|
RPL_ENDOFMOTD = 376
|
|
RPL_UMODEIS = 221
|
|
RPL_INVITING = 341
|
|
RPL_YOUREOPER = 381
|
|
ERR_NOSUCHNICK = 401
|
|
ERR_NOSUCHSERVER = 402
|
|
ERR_NOSUCHCHANNEL = 403
|
|
ERR_CANNOTSENDTOCHAN = 404
|
|
ERR_TOOMANYCHANNELS = 405
|
|
ERR_WASNOSUCHNICK = 406
|
|
ERR_TOOMANYTARGETS = 407
|
|
ERR_NOORIGIN = 409
|
|
ERR_NORECIPIENT = 411
|
|
ERR_NOTEXTTOSEND = 412
|
|
ERR_UNKNOWNCOMMAND = 421
|
|
ERR_NOMOTD = 422
|
|
ERR_NONICKNAMEGIVEN = 431
|
|
ERR_ERRONEUSNICKNAME = 432
|
|
ERR_NICKNAMEINUSE = 433
|
|
ERR_NICKCOLLISION = 436
|
|
ERR_USERNOTINCHANNEL = 441
|
|
ERR_NOTONCHANNEL = 442
|
|
ERR_USERONCHANNEL = 443
|
|
ERR_NOLOGIN = 444
|
|
ERR_SUMMONDISABLED = 445
|
|
ERR_USERSDISABLED = 446
|
|
ERR_NOTREGISTERED = 451
|
|
ERR_NEEDMOREPARAMS = 461
|
|
ERR_ALREADYREGISTRED = 462
|
|
ERR_NOPERMFORHOST = 463
|
|
ERR_PASSWDMISMATCH = 464
|
|
ERR_YOUREBANNEDCREEP = 465
|
|
ERR_YOUWILLBEBANNED = 466
|
|
ERR_KEYSET = 467
|
|
ERR_CHANNELISFULL = 471
|
|
ERR_UNKNOWNMODE = 472
|
|
ERR_INVITEONLYCHAN = 473
|
|
ERR_BANNEDFROMCHAN = 474
|
|
ERR_BADCHANNELKEY = 475
|
|
ERR_BADCHANMASK = 476
|
|
ERR_NOCHANMODES = 477
|
|
ERR_BANLISTFULL = 478
|
|
ERR_NOPRIVILEGES = 481
|
|
ERR_CHANOPRIVSNEEDED = 482
|
|
ERR_CANTKILLSERVER = 483
|
|
ERR_RESTRICTED = 484
|
|
ERR_UNIQOPPRIVSNEEDED = 485
|
|
ERR_NOOPERHOST = 491
|
|
ERR_UMODEUNKNOWNFLAG = 501
|
|
ERR_USERSDONTMATCH = 502
|
|
RPL_SNOMASK = 8
|
|
RPL_GLOBALNOTICE = 710
|
|
RPL_OPERWALL = 711
|
|
)
|
|
|
|
// handleNick handles NICK command
|
|
func (c *Client) handleNick(parts []string) {
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "NICK :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
newNick := parts[1]
|
|
if len(newNick) > 0 && newNick[0] == ':' {
|
|
newNick = newNick[1:]
|
|
}
|
|
|
|
// Validate nickname
|
|
if !isValidNickname(newNick) {
|
|
c.SendNumeric(ERR_ERRONEUSNICKNAME, newNick+" :Erroneous nickname")
|
|
return
|
|
}
|
|
|
|
// Check if nick is already in use
|
|
if existing := c.server.GetClient(newNick); existing != nil && existing != c {
|
|
c.SendNumeric(ERR_NICKNAMEINUSE, newNick+" :Nickname is already in use")
|
|
return
|
|
}
|
|
|
|
oldNick := c.Nick()
|
|
c.SetNick(newNick)
|
|
|
|
// If already registered, notify channels
|
|
if c.IsRegistered() && oldNick != "" {
|
|
message := fmt.Sprintf(":%s NICK :%s", c.Prefix(), newNick)
|
|
for _, channel := range c.GetChannels() {
|
|
channel.Broadcast(message, nil)
|
|
}
|
|
|
|
// Send snomask notification for nick change
|
|
if c.server != nil && oldNick != newNick {
|
|
c.server.sendSnomask('n', fmt.Sprintf("Nick change: %s -> %s (%s@%s)",
|
|
oldNick, newNick, c.User(), c.Host()))
|
|
}
|
|
}
|
|
|
|
c.checkRegistration()
|
|
}
|
|
|
|
// Add a Nick field to Client and implement Nick/SetNick methods
|
|
|
|
// Add this field to Client struct above (not shown here):
|
|
// Nickname string
|
|
|
|
func (c *Client) Nick() string {
|
|
// Return the client's nickname
|
|
return c.Nickname
|
|
}
|
|
|
|
func (c *Client) SetNick(nick string) {
|
|
c.Nickname = nick
|
|
}
|
|
|
|
// handleUser handles USER command
|
|
func (c *Client) handleUser(parts []string) {
|
|
if len(parts) < 5 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "USER :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
if c.IsRegistered() {
|
|
c.SendNumeric(ERR_ALREADYREGISTRED, ":You may not reregister")
|
|
return
|
|
}
|
|
|
|
c.SetUser(parts[1])
|
|
// parts[2] and parts[3] are ignored (mode and unused)
|
|
realname := strings.Join(parts[4:], " ")
|
|
if len(realname) > 0 && realname[0] == ':' {
|
|
realname = realname[1:]
|
|
}
|
|
c.SetRealname(realname)
|
|
|
|
c.checkRegistration()
|
|
}
|
|
|
|
// checkRegistration checks if client is ready to be registered
|
|
func (c *Client) checkRegistration() {
|
|
if !c.IsRegistered() && c.Nick() != "" && c.User() != "" {
|
|
c.SetRegistered(true)
|
|
c.sendWelcome()
|
|
}
|
|
}
|
|
|
|
// sendWelcome sends welcome messages to newly registered client
|
|
func (c *Client) sendWelcome() {
|
|
fmt.Printf("DEBUG: sendWelcome called\n")
|
|
if c.server == nil {
|
|
fmt.Printf("DEBUG: sendWelcome - server is nil\n")
|
|
return
|
|
}
|
|
if c.server.config == nil {
|
|
fmt.Printf("DEBUG: sendWelcome - config is nil\n")
|
|
return
|
|
}
|
|
|
|
fmt.Printf("DEBUG: sendWelcome - about to send RPL_WELCOME\n")
|
|
c.SendNumeric(RPL_WELCOME, fmt.Sprintf("Welcome to %s, %s", c.server.config.Server.Network, c.Prefix()))
|
|
fmt.Printf("DEBUG: sendWelcome - sent RPL_WELCOME\n")
|
|
c.SendNumeric(RPL_YOURHOST, fmt.Sprintf("Your host is %s, running version %s", c.server.config.Server.Name, c.server.config.Server.Version))
|
|
c.SendNumeric(RPL_CREATED, "This server was created recently")
|
|
c.SendNumeric(RPL_MYINFO, fmt.Sprintf("%s %s o o", c.server.config.Server.Name, c.server.config.Server.Version))
|
|
|
|
// Send MOTD
|
|
if len(c.server.config.MOTD) > 0 {
|
|
c.SendNumeric(RPL_MOTDSTART, fmt.Sprintf("- %s Message of the Day -", c.server.config.Server.Name))
|
|
for _, line := range c.server.config.MOTD {
|
|
c.SendNumeric(RPL_MOTD, fmt.Sprintf("- %s", line))
|
|
}
|
|
c.SendNumeric(RPL_ENDOFMOTD, "End of /MOTD command")
|
|
}
|
|
|
|
// Send snomask notification for new client connection
|
|
if c.server != nil {
|
|
c.server.sendSnomask('c', fmt.Sprintf("Client connect: %s (%s@%s)",
|
|
c.Nick(), c.User(), c.Host()))
|
|
}
|
|
|
|
fmt.Printf("DEBUG: sendWelcome completed\n")
|
|
}
|
|
|
|
// handlePing handles PING command
|
|
func (c *Client) handlePing(parts []string) {
|
|
if len(parts) < 2 {
|
|
return
|
|
}
|
|
|
|
token := parts[1]
|
|
if len(token) > 0 && token[0] == ':' {
|
|
token = token[1:]
|
|
}
|
|
|
|
serverName := "localhost"
|
|
if c.server != nil && c.server.config != nil {
|
|
serverName = c.server.config.Server.Name
|
|
}
|
|
|
|
c.SendMessage(fmt.Sprintf("PONG %s :%s", serverName, token))
|
|
}
|
|
|
|
// handlePong handles PONG command
|
|
func (c *Client) handlePong(parts []string) {
|
|
// Update the last pong time for ping timeout tracking
|
|
// This is used by the client Handler's ping timeout mechanism
|
|
c.mu.Lock()
|
|
c.lastPong = time.Now()
|
|
c.waitingForPong = false
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// handleJoin handles JOIN command
|
|
func (c *Client) handleJoin(parts []string) {
|
|
if !c.IsRegistered() {
|
|
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "JOIN :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
channelNames := strings.Split(parts[1], ",")
|
|
keys := []string{}
|
|
if len(parts) > 2 {
|
|
keys = strings.Split(parts[2], ",")
|
|
}
|
|
|
|
for i, channelName := range channelNames {
|
|
if channelName == "0" {
|
|
// Leave all channels
|
|
for _, channel := range c.GetChannels() {
|
|
c.handlePartChannel(channel.Name(), "Leaving all channels")
|
|
}
|
|
continue
|
|
}
|
|
|
|
if !isValidChannelName(channelName) {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
|
|
continue
|
|
}
|
|
|
|
channel := c.server.GetOrCreateChannel(channelName)
|
|
|
|
// Check if already in channel
|
|
if c.IsInChannel(channelName) {
|
|
continue
|
|
}
|
|
|
|
// Check channel modes and limits
|
|
key := ""
|
|
if i < len(keys) {
|
|
key = keys[i]
|
|
}
|
|
|
|
if channel.HasMode('k') && channel.Key() != key {
|
|
c.SendNumeric(ERR_BADCHANNELKEY, channelName+" :Cannot join channel (+k)")
|
|
continue
|
|
}
|
|
|
|
if channel.HasMode('l') && channel.UserCount() >= channel.Limit() {
|
|
c.SendNumeric(ERR_CHANNELISFULL, channelName+" :Cannot join channel (+l)")
|
|
continue
|
|
}
|
|
|
|
// Join the channel
|
|
channel.AddClient(c)
|
|
c.AddChannel(channel)
|
|
|
|
message := fmt.Sprintf(":%s JOIN :%s", c.Prefix(), channelName)
|
|
channel.Broadcast(message, nil)
|
|
|
|
// Send topic if exists
|
|
if channel.Topic() != "" {
|
|
c.SendNumeric(RPL_TOPIC, channelName+" :"+channel.Topic())
|
|
c.SendNumeric(RPL_TOPICWHOTIME, fmt.Sprintf("%s %s %d", channelName, channel.TopicBy(), channel.TopicTime().Unix()))
|
|
}
|
|
|
|
// Send names list
|
|
c.sendNames(channel)
|
|
}
|
|
}
|
|
|
|
// handlePart handles PART command
|
|
func (c *Client) handlePart(parts []string) {
|
|
if !c.IsRegistered() {
|
|
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "PART :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
channelNames := strings.Split(parts[1], ",")
|
|
reason := "Leaving"
|
|
if len(parts) > 2 {
|
|
reason = strings.Join(parts[2:], " ")
|
|
if len(reason) > 0 && reason[0] == ':' {
|
|
reason = reason[1:]
|
|
}
|
|
}
|
|
|
|
for _, channelName := range channelNames {
|
|
c.handlePartChannel(channelName, reason)
|
|
}
|
|
}
|
|
|
|
func (c *Client) handlePartChannel(channelName, reason string) {
|
|
if !c.IsInChannel(channelName) {
|
|
c.SendNumeric(ERR_NOTONCHANNEL, channelName+" :You're not on that channel")
|
|
return
|
|
}
|
|
|
|
channel := c.server.GetChannel(channelName)
|
|
if channel == nil {
|
|
return
|
|
}
|
|
|
|
message := fmt.Sprintf(":%s PART %s :%s", c.Prefix(), channelName, reason)
|
|
channel.Broadcast(message, nil)
|
|
|
|
channel.RemoveClient(c)
|
|
c.RemoveChannel(channelName)
|
|
|
|
// Remove empty channel
|
|
if channel.UserCount() == 0 {
|
|
c.server.RemoveChannel(channelName)
|
|
}
|
|
}
|
|
|
|
// handlePrivmsg handles PRIVMSG command
|
|
func (c *Client) handlePrivmsg(parts []string) {
|
|
if !c.IsRegistered() {
|
|
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NORECIPIENT, ":No recipient given (PRIVMSG)")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 3 {
|
|
c.SendNumeric(ERR_NOTEXTTOSEND, ":No text to send")
|
|
return
|
|
}
|
|
|
|
target := parts[1]
|
|
message := strings.Join(parts[2:], " ")
|
|
if len(message) > 0 && message[0] == ':' {
|
|
message = message[1:]
|
|
}
|
|
|
|
if isChannelName(target) {
|
|
// Channel message
|
|
channel := c.server.GetChannel(target)
|
|
if channel == nil {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
|
|
return
|
|
}
|
|
|
|
if !c.IsInChannel(target) {
|
|
c.SendNumeric(ERR_CANNOTSENDTOCHAN, target+" :Cannot send to channel")
|
|
return
|
|
}
|
|
|
|
// Check if user can send messages to this channel (moderated mode check)
|
|
if !channel.CanSendMessage(c) {
|
|
c.SendNumeric(ERR_CANNOTSENDTOCHAN, target+" :Cannot send to channel (+m)")
|
|
return
|
|
}
|
|
|
|
msg := fmt.Sprintf(":%s PRIVMSG %s :%s", c.Prefix(), target, message)
|
|
channel.Broadcast(msg, c)
|
|
} else {
|
|
// Private message
|
|
targetClient := c.server.GetClient(target)
|
|
if targetClient == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel")
|
|
return
|
|
}
|
|
|
|
if targetClient.Away() != "" {
|
|
c.SendNumeric(RPL_AWAY, fmt.Sprintf("%s :%s", target, targetClient.Away()))
|
|
}
|
|
|
|
msg := fmt.Sprintf(":%s PRIVMSG %s :%s", c.Prefix(), target, message)
|
|
targetClient.SendMessage(msg)
|
|
}
|
|
}
|
|
|
|
// handleNotice handles NOTICE command
|
|
func (c *Client) handleNotice(parts []string) {
|
|
if !c.IsRegistered() {
|
|
return // NOTICE should not generate error responses
|
|
}
|
|
|
|
if len(parts) < 3 {
|
|
return
|
|
}
|
|
|
|
target := parts[1]
|
|
message := strings.Join(parts[2:], " ")
|
|
if len(message) > 0 && message[0] == ':' {
|
|
message = message[1:]
|
|
}
|
|
|
|
if isChannelName(target) {
|
|
// Channel notice
|
|
channel := c.server.GetChannel(target)
|
|
if channel == nil || !c.IsInChannel(target) {
|
|
return
|
|
}
|
|
|
|
msg := fmt.Sprintf(":%s NOTICE %s :%s", c.Prefix(), target, message)
|
|
channel.Broadcast(msg, c)
|
|
} else {
|
|
// Private notice
|
|
targetClient := c.server.GetClient(target)
|
|
if targetClient == nil {
|
|
return
|
|
}
|
|
|
|
msg := fmt.Sprintf(":%s NOTICE %s :%s", c.Prefix(), target, message)
|
|
targetClient.SendMessage(msg)
|
|
}
|
|
}
|
|
|
|
// handleWho handles WHO command
|
|
func (c *Client) handleWho(parts []string) {
|
|
if !c.IsRegistered() {
|
|
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "WHO :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
target := parts[1]
|
|
|
|
if isChannelName(target) {
|
|
channel := c.server.GetChannel(target)
|
|
if channel == nil {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
|
|
return
|
|
}
|
|
|
|
for _, client := range channel.GetClients() {
|
|
flags := ""
|
|
if client.IsOper() {
|
|
flags += "*"
|
|
}
|
|
if client.Away() != "" {
|
|
flags += "G"
|
|
} else {
|
|
flags += "H"
|
|
}
|
|
if channel.IsOperator(client) {
|
|
flags += "@"
|
|
} else if channel.IsVoice(client) {
|
|
flags += "+"
|
|
}
|
|
|
|
c.SendNumeric(352, fmt.Sprintf("%s %s %s %s %s %s :0 %s",
|
|
target, client.User(), client.Host(), c.server.config.Server.Name,
|
|
client.Nick(), flags, client.Realname()))
|
|
}
|
|
}
|
|
|
|
c.SendNumeric(315, target+" :End of /WHO list")
|
|
}
|
|
|
|
// handleWhois handles WHOIS command
|
|
func (c *Client) handleWhois(parts []string) {
|
|
if !c.IsRegistered() {
|
|
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "WHOIS :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
nick := parts[1]
|
|
target := c.server.GetClient(nick)
|
|
if target == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, nick+" :No such nick")
|
|
return
|
|
}
|
|
|
|
c.SendNumeric(RPL_WHOISUSER, fmt.Sprintf("%s %s %s * :%s",
|
|
target.Nick(), target.User(), target.Host(), target.Realname()))
|
|
|
|
c.SendNumeric(RPL_WHOISSERVER, fmt.Sprintf("%s %s :%s",
|
|
target.Nick(), c.server.config.Server.Name, c.server.config.Server.Description))
|
|
|
|
if target.IsOper() {
|
|
c.SendNumeric(RPL_WHOISOPERATOR, target.Nick()+" :is an IRC operator")
|
|
}
|
|
|
|
if target.Away() != "" {
|
|
c.SendNumeric(RPL_AWAY, fmt.Sprintf("%s :%s", target.Nick(), target.Away()))
|
|
}
|
|
|
|
// Send channels
|
|
var channels []string
|
|
for _, channel := range target.GetChannels() {
|
|
channelName := channel.Name()
|
|
if channel.IsOperator(target) {
|
|
channelName = "@" + channelName
|
|
} else if channel.IsVoice(target) {
|
|
channelName = "+" + channelName
|
|
}
|
|
channels = append(channels, channelName)
|
|
}
|
|
if len(channels) > 0 {
|
|
c.SendNumeric(RPL_WHOISCHANNELS, fmt.Sprintf("%s :%s", target.Nick(), strings.Join(channels, " ")))
|
|
}
|
|
|
|
// Show user modes if the requester is an operator or the target user
|
|
if c.IsOper() || c.Nick() == target.Nick() {
|
|
modes := target.GetModes()
|
|
if modes != "" {
|
|
c.SendMessage(fmt.Sprintf(":%s 379 %s %s :is using modes %s",
|
|
c.server.config.Server.Name, c.Nick(), target.Nick(), modes))
|
|
}
|
|
}
|
|
|
|
// Show SSL status
|
|
if target.IsSSL() {
|
|
c.SendMessage(fmt.Sprintf(":%s 671 %s %s :is using a secure connection",
|
|
c.server.config.Server.Name, c.Nick(), target.Nick()))
|
|
}
|
|
|
|
c.SendNumeric(RPL_ENDOFWHOIS, target.Nick()+" :End of /WHOIS list")
|
|
}
|
|
|
|
// handleNames handles NAMES command
|
|
func (c *Client) handleNames(parts []string) {
|
|
if !c.IsRegistered() {
|
|
c.SendNumeric(ERR_NOTREGISTERED, ":You have not registered")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
// Send names for all channels
|
|
for _, channel := range c.server.GetChannels() {
|
|
if c.IsInChannel(channel.Name()) {
|
|
c.sendNames(channel)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
channelNames := strings.Split(parts[1], ",")
|
|
for _, channelName := range channelNames {
|
|
channel := c.server.GetChannel(channelName)
|
|
if channel != nil && c.IsInChannel(channelName) {
|
|
c.sendNames(channel)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) sendNames(channel *Channel) {
|
|
var names []string
|
|
for _, client := range channel.GetClients() {
|
|
name := client.Nick()
|
|
if channel.IsOwner(client) {
|
|
name = "~" + name
|
|
} else if channel.IsOperator(client) {
|
|
name = "@" + name
|
|
} else if channel.IsHalfop(client) {
|
|
name = "%" + name
|
|
} else if channel.IsVoice(client) {
|
|
name = "+" + name
|
|
}
|
|
names = append(names, name)
|
|
}
|
|
|
|
symbol := "="
|
|
if channel.HasMode('s') {
|
|
symbol = "@"
|
|
} else if channel.HasMode('p') {
|
|
symbol = "*"
|
|
}
|
|
|
|
c.SendNumeric(RPL_NAMREPLY, fmt.Sprintf("%s %s :%s", symbol, channel.Name(), strings.Join(names, " ")))
|
|
c.SendNumeric(RPL_ENDOFNAMES, channel.Name()+" :End of /NAMES list")
|
|
}
|
|
|
|
// handleQuit handles QUIT command
|
|
func (c *Client) handleQuit(parts []string) {
|
|
reason := "Client quit"
|
|
if len(parts) > 1 {
|
|
reason = strings.Join(parts[1:], " ")
|
|
if len(reason) > 0 && reason[0] == ':' {
|
|
reason = reason[1:]
|
|
}
|
|
}
|
|
|
|
c.server.RemoveClient(c)
|
|
}
|
|
|
|
// handleMode handles MODE command
|
|
func (c *Client) handleMode(parts []string) {
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "MODE :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
target := parts[1]
|
|
|
|
// Handle user mode requests
|
|
if !isChannelName(target) {
|
|
if target != c.Nick() {
|
|
c.SendNumeric(ERR_USERSDONTMATCH, ":Cannot change mode for other users")
|
|
return
|
|
}
|
|
|
|
// If no mode changes specified, return current user modes
|
|
if len(parts) == 2 {
|
|
modes := c.GetModes()
|
|
if modes == "" {
|
|
modes = "+"
|
|
}
|
|
c.SendNumeric(RPL_UMODEIS, modes)
|
|
return
|
|
}
|
|
|
|
// Parse user mode changes
|
|
modeString := parts[2]
|
|
adding := true
|
|
var appliedModes []string
|
|
|
|
for _, char := range modeString {
|
|
switch char {
|
|
case '+':
|
|
adding = true
|
|
case '-':
|
|
adding = false
|
|
case 'i': // invisible
|
|
c.SetMode('i', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+i")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-i")
|
|
}
|
|
case 'w': // wallops
|
|
c.SetMode('w', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+w")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-w")
|
|
}
|
|
case 's': // server notices (requires oper)
|
|
if !c.IsOper() && adding {
|
|
continue // silently ignore for non-opers
|
|
}
|
|
c.SetMode('s', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+s")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-s")
|
|
}
|
|
case 'o': // operator (cannot be set manually)
|
|
if adding {
|
|
c.SendNumeric(ERR_UMODEUNKNOWNFLAG, ":Unknown MODE flag")
|
|
} else {
|
|
// Allow de-opering
|
|
c.SetOper(false)
|
|
c.SetMode('o', false)
|
|
appliedModes = append(appliedModes, "-o")
|
|
// Clear snomasks when de-opering
|
|
c.snomasks = make(map[rune]bool)
|
|
c.sendSnomask('o', fmt.Sprintf("%s is no longer an IRC operator", c.Nick()))
|
|
}
|
|
case 'r': // registered (cannot be set manually, services only)
|
|
c.SendNumeric(ERR_UMODEUNKNOWNFLAG, ":Unknown MODE flag")
|
|
case 'x': // host masking (TechIRCd special)
|
|
c.SetMode('x', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+x")
|
|
// TODO: Implement host masking
|
|
} else {
|
|
appliedModes = append(appliedModes, "-x")
|
|
}
|
|
case 'z': // SSL/TLS (automatic, cannot be manually set)
|
|
if c.IsSSL() {
|
|
c.SetMode('z', true)
|
|
}
|
|
// Ignore attempts to manually set/unset
|
|
case 'B': // bot flag (TechIRCd special)
|
|
c.SetMode('B', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+B")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-B")
|
|
}
|
|
default:
|
|
c.SendNumeric(ERR_UMODEUNKNOWNFLAG, ":Unknown MODE flag")
|
|
}
|
|
}
|
|
|
|
// Send mode changes back to user
|
|
if len(appliedModes) > 0 {
|
|
modeStr := strings.Join(appliedModes, "")
|
|
c.SendMessage(fmt.Sprintf(":%s MODE %s :%s", c.Nick(), c.Nick(), modeStr))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Handle channel mode requests
|
|
channel := c.server.GetChannel(target)
|
|
if channel == nil {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
|
|
return
|
|
}
|
|
|
|
if !c.IsInChannel(target) {
|
|
c.SendNumeric(ERR_NOTONCHANNEL, target+" :You're not on that channel")
|
|
return
|
|
}
|
|
|
|
// If no mode changes specified, return current channel modes
|
|
if len(parts) == 2 {
|
|
modes := channel.GetModes()
|
|
if modes == "" {
|
|
modes = "+"
|
|
}
|
|
c.SendNumeric(RPL_CHANNELMODEIS, fmt.Sprintf("%s %s", target, modes))
|
|
return
|
|
}
|
|
|
|
// Parse mode changes
|
|
modeString := parts[2]
|
|
args := parts[3:]
|
|
argIndex := 0
|
|
|
|
// Check if user has operator privileges (required for most mode changes)
|
|
if !channel.IsOwner(c) && !channel.IsOperator(c) && !channel.IsHalfop(c) && !c.IsOper() {
|
|
c.SendNumeric(ERR_CHANOPRIVSNEEDED, target+" :You're not channel operator")
|
|
return
|
|
}
|
|
|
|
adding := true
|
|
var appliedModes []string
|
|
var appliedArgs []string
|
|
|
|
for _, char := range modeString {
|
|
switch char {
|
|
case '+':
|
|
adding = true
|
|
case '-':
|
|
adding = false
|
|
case 'o': // operator
|
|
if argIndex >= len(args) {
|
|
continue
|
|
}
|
|
targetNick := args[argIndex]
|
|
argIndex++
|
|
|
|
targetClient := c.server.GetClient(targetNick)
|
|
if targetClient == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
|
|
continue
|
|
}
|
|
|
|
if !targetClient.IsInChannel(target) {
|
|
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", targetNick, target))
|
|
continue
|
|
}
|
|
|
|
channel.SetOperator(targetClient, adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+o")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-o")
|
|
}
|
|
appliedArgs = append(appliedArgs, targetNick)
|
|
|
|
case 'v': // voice
|
|
if argIndex >= len(args) {
|
|
continue
|
|
}
|
|
targetNick := args[argIndex]
|
|
argIndex++
|
|
|
|
targetClient := c.server.GetClient(targetNick)
|
|
if targetClient == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
|
|
continue
|
|
}
|
|
|
|
if !targetClient.IsInChannel(target) {
|
|
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", targetNick, target))
|
|
continue
|
|
}
|
|
|
|
channel.SetVoice(targetClient, adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+v")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-v")
|
|
}
|
|
appliedArgs = append(appliedArgs, targetNick)
|
|
|
|
case 'h': // halfop
|
|
if argIndex >= len(args) {
|
|
continue
|
|
}
|
|
targetNick := args[argIndex]
|
|
argIndex++
|
|
|
|
targetClient := c.server.GetClient(targetNick)
|
|
if targetClient == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
|
|
continue
|
|
}
|
|
|
|
if !targetClient.IsInChannel(target) {
|
|
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", targetNick, target))
|
|
continue
|
|
}
|
|
|
|
channel.SetHalfop(targetClient, adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+h")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-h")
|
|
}
|
|
appliedArgs = append(appliedArgs, targetNick)
|
|
|
|
case 'q': // owner/founder
|
|
if argIndex >= len(args) {
|
|
continue
|
|
}
|
|
targetNick := args[argIndex]
|
|
argIndex++
|
|
|
|
// Only existing owners can grant/remove owner status
|
|
if !channel.IsOwner(c) && !c.IsOper() {
|
|
c.SendNumeric(ERR_CHANOPRIVSNEEDED, target+" :You're not channel owner")
|
|
continue
|
|
}
|
|
|
|
targetClient := c.server.GetClient(targetNick)
|
|
if targetClient == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, targetNick+" :No such nick/channel")
|
|
continue
|
|
}
|
|
|
|
if !targetClient.IsInChannel(target) {
|
|
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", targetNick, target))
|
|
continue
|
|
}
|
|
|
|
channel.SetOwner(targetClient, adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+q")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-q")
|
|
}
|
|
appliedArgs = append(appliedArgs, targetNick)
|
|
|
|
case 'm': // moderated
|
|
channel.SetMode('m', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+m")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-m")
|
|
}
|
|
|
|
case 'n': // no external messages
|
|
channel.SetMode('n', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+n")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-n")
|
|
}
|
|
|
|
case 't': // topic restriction
|
|
channel.SetMode('t', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+t")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-t")
|
|
}
|
|
|
|
case 'i': // invite only
|
|
channel.SetMode('i', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+i")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-i")
|
|
}
|
|
|
|
case 's': // secret
|
|
channel.SetMode('s', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+s")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-s")
|
|
}
|
|
|
|
case 'p': // private
|
|
channel.SetMode('p', adding)
|
|
if adding {
|
|
appliedModes = append(appliedModes, "+p")
|
|
} else {
|
|
appliedModes = append(appliedModes, "-p")
|
|
}
|
|
|
|
case 'k': // key (password)
|
|
if adding {
|
|
if argIndex >= len(args) {
|
|
continue
|
|
}
|
|
key := args[argIndex]
|
|
argIndex++
|
|
channel.SetKey(key)
|
|
channel.SetMode('k', true)
|
|
appliedModes = append(appliedModes, "+k")
|
|
appliedArgs = append(appliedArgs, key)
|
|
} else {
|
|
channel.SetKey("")
|
|
channel.SetMode('k', false)
|
|
appliedModes = append(appliedModes, "-k")
|
|
}
|
|
|
|
case 'l': // limit
|
|
if adding {
|
|
if argIndex >= len(args) {
|
|
continue
|
|
}
|
|
limitStr := args[argIndex]
|
|
argIndex++
|
|
// Parse limit (simplified - should validate it's a number)
|
|
limit := 0
|
|
fmt.Sscanf(limitStr, "%d", &limit)
|
|
if limit > 0 {
|
|
channel.SetLimit(limit)
|
|
channel.SetMode('l', true)
|
|
appliedModes = append(appliedModes, "+l")
|
|
appliedArgs = append(appliedArgs, limitStr)
|
|
}
|
|
} else {
|
|
channel.SetLimit(0)
|
|
channel.SetMode('l', false)
|
|
appliedModes = append(appliedModes, "-l")
|
|
}
|
|
|
|
case 'b': // ban (enhanced with extended ban types)
|
|
if argIndex >= len(args) {
|
|
// List bans (TODO: implement ban list display)
|
|
continue
|
|
}
|
|
mask := args[argIndex]
|
|
argIndex++
|
|
|
|
// Check for extended ban types (e.g., ~q:nick!user@host for quiet)
|
|
if strings.HasPrefix(mask, "~") && len(mask) > 2 && mask[2] == ':' {
|
|
banType := mask[1] // The character after ~
|
|
banMask := mask[3:] // The mask after ~x:
|
|
|
|
switch banType {
|
|
case 'q': // Quiet ban
|
|
if adding {
|
|
// Add to quiet list
|
|
channel.quietList = append(channel.quietList, banMask)
|
|
appliedModes = append(appliedModes, "+b")
|
|
appliedArgs = append(appliedArgs, mask)
|
|
|
|
// Send snomask to opers
|
|
if c.IsOper() {
|
|
c.server.sendSnomask('x', fmt.Sprintf("%s set quiet ban %s on %s", c.Nick(), banMask, target))
|
|
}
|
|
} else {
|
|
// Remove from quiet list
|
|
for i, quiet := range channel.quietList {
|
|
if quiet == banMask {
|
|
channel.quietList = append(channel.quietList[:i], channel.quietList[i+1:]...)
|
|
appliedModes = append(appliedModes, "-b")
|
|
appliedArgs = append(appliedArgs, mask)
|
|
|
|
// Send snomask to opers
|
|
if c.IsOper() {
|
|
c.server.sendSnomask('x', fmt.Sprintf("%s removed quiet ban %s on %s", c.Nick(), banMask, target))
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
// Unknown extended ban type - treat as regular ban for now
|
|
if adding {
|
|
channel.banList = append(channel.banList, mask)
|
|
appliedModes = append(appliedModes, "+b")
|
|
} else {
|
|
for i, ban := range channel.banList {
|
|
if ban == mask {
|
|
channel.banList = append(channel.banList[:i], channel.banList[i+1:]...)
|
|
appliedModes = append(appliedModes, "-b")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
appliedArgs = append(appliedArgs, mask)
|
|
}
|
|
} else {
|
|
// Regular ban
|
|
if adding {
|
|
channel.banList = append(channel.banList, mask)
|
|
appliedModes = append(appliedModes, "+b")
|
|
} else {
|
|
for i, ban := range channel.banList {
|
|
if ban == mask {
|
|
channel.banList = append(channel.banList[:i], channel.banList[i+1:]...)
|
|
appliedModes = append(appliedModes, "-b")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
appliedArgs = append(appliedArgs, mask)
|
|
}
|
|
|
|
default:
|
|
// Unknown mode - ignore for now
|
|
}
|
|
}
|
|
|
|
// Broadcast mode changes to all channel members
|
|
if len(appliedModes) > 0 {
|
|
modeChangeMsg := fmt.Sprintf("MODE %s %s", target, strings.Join(appliedModes, ""))
|
|
if len(appliedArgs) > 0 {
|
|
modeChangeMsg += " " + strings.Join(appliedArgs, " ")
|
|
}
|
|
|
|
for _, client := range channel.GetClients() {
|
|
client.SendFrom(c.Prefix(), modeChangeMsg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleTopic handles TOPIC command
|
|
func (c *Client) handleTopic(parts []string) {
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "TOPIC :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
channelName := parts[1]
|
|
if !isChannelName(channelName) {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
|
|
return
|
|
}
|
|
|
|
channel := c.server.GetChannel(channelName)
|
|
if channel == nil {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
|
|
return
|
|
}
|
|
|
|
if !c.IsInChannel(channelName) {
|
|
c.SendNumeric(ERR_NOTONCHANNEL, channelName+" :You're not on that channel")
|
|
return
|
|
}
|
|
|
|
// If no topic provided, return current topic
|
|
if len(parts) == 2 {
|
|
topic := channel.Topic()
|
|
if topic == "" {
|
|
c.SendNumeric(RPL_NOTOPIC, channelName+" :No topic is set")
|
|
} else {
|
|
c.SendNumeric(RPL_TOPIC, fmt.Sprintf("%s :%s", channelName, topic))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check if user can set topic (for now, anyone in channel can)
|
|
// TODO: Add proper +t mode checking
|
|
newTopic := strings.Join(parts[2:], " ")
|
|
if len(newTopic) > 0 && newTopic[0] == ':' {
|
|
newTopic = newTopic[1:]
|
|
}
|
|
|
|
channel.SetTopic(newTopic, c.Nick())
|
|
|
|
// Broadcast topic change to all channel members
|
|
for _, client := range channel.GetClients() {
|
|
client.SendFrom(c.Prefix(), fmt.Sprintf("TOPIC %s :%s", channelName, newTopic))
|
|
}
|
|
}
|
|
|
|
// handleAway handles AWAY command
|
|
func (c *Client) handleAway(parts []string) {
|
|
if len(parts) == 1 {
|
|
// Remove away status
|
|
c.SetAway("")
|
|
c.SendNumeric(RPL_UNAWAY, ":You are no longer marked as being away")
|
|
return
|
|
}
|
|
|
|
// Set away message
|
|
awayMsg := strings.Join(parts[1:], " ")
|
|
if len(awayMsg) > 0 && awayMsg[0] == ':' {
|
|
awayMsg = awayMsg[1:]
|
|
}
|
|
|
|
c.SetAway(awayMsg)
|
|
c.SendNumeric(RPL_NOWAWAY, ":You have been marked as being away")
|
|
}
|
|
|
|
// handleList handles LIST command
|
|
func (c *Client) handleList(parts []string) {
|
|
c.SendNumeric(RPL_LISTSTART, "Channel :Users Name")
|
|
|
|
for _, channel := range c.server.GetChannels() {
|
|
// For now, show all channels (TODO: Add proper mode checking for secret channels)
|
|
userCount := len(channel.GetClients())
|
|
topic := channel.Topic()
|
|
if topic == "" {
|
|
topic = ""
|
|
}
|
|
c.SendNumeric(RPL_LIST, fmt.Sprintf("%s %d :%s", channel.Name(), userCount, topic))
|
|
}
|
|
|
|
c.SendNumeric(RPL_LISTEND, ":End of /LIST")
|
|
}
|
|
|
|
// handleInvite handles INVITE command
|
|
func (c *Client) handleInvite(parts []string) {
|
|
if len(parts) < 3 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "INVITE :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
nick := parts[1]
|
|
channelName := parts[2]
|
|
|
|
target := c.server.GetClient(nick)
|
|
if target == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, nick+" :No such nick/channel")
|
|
return
|
|
}
|
|
|
|
if !isChannelName(channelName) {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
|
|
return
|
|
}
|
|
|
|
channel := c.server.GetChannel(channelName)
|
|
if channel == nil {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
|
|
return
|
|
}
|
|
|
|
if !c.IsInChannel(channelName) {
|
|
c.SendNumeric(ERR_NOTONCHANNEL, channelName+" :You're not on that channel")
|
|
return
|
|
}
|
|
|
|
if target.IsInChannel(channelName) {
|
|
c.SendNumeric(ERR_USERONCHANNEL, fmt.Sprintf("%s %s :is already on channel", nick, channelName))
|
|
return
|
|
}
|
|
|
|
// TODO: Check if user has operator privileges for invite-only channels
|
|
|
|
// Send invite to target
|
|
target.SendFrom(c.Prefix(), fmt.Sprintf("INVITE %s %s", target.Nick(), channelName))
|
|
c.SendNumeric(RPL_INVITING, fmt.Sprintf("%s %s", target.Nick(), channelName))
|
|
}
|
|
|
|
// handleKick handles KICK command
|
|
func (c *Client) handleKick(parts []string) {
|
|
if len(parts) < 3 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "KICK :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
channelName := parts[1]
|
|
nick := parts[2]
|
|
reason := "No reason given"
|
|
if len(parts) > 3 {
|
|
reason = strings.Join(parts[3:], " ")
|
|
if len(reason) > 0 && reason[0] == ':' {
|
|
reason = reason[1:]
|
|
}
|
|
}
|
|
|
|
if !isChannelName(channelName) {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
|
|
return
|
|
}
|
|
|
|
channel := c.server.GetChannel(channelName)
|
|
if channel == nil {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, channelName+" :No such channel")
|
|
return
|
|
}
|
|
|
|
if !c.IsInChannel(channelName) {
|
|
c.SendNumeric(ERR_NOTONCHANNEL, channelName+" :You're not on that channel")
|
|
return
|
|
}
|
|
|
|
target := c.server.GetClient(nick)
|
|
if target == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, nick+" :No such nick/channel")
|
|
return
|
|
}
|
|
|
|
if !target.IsInChannel(channelName) {
|
|
c.SendNumeric(ERR_USERNOTINCHANNEL, fmt.Sprintf("%s %s :They aren't on that channel", nick, channelName))
|
|
return
|
|
}
|
|
|
|
// TODO: Check if user has operator privileges
|
|
// For now, allow anyone to kick (will fix with proper channel modes)
|
|
|
|
// Broadcast kick to all channel members
|
|
kickMsg := fmt.Sprintf("KICK %s %s :%s", channelName, target.Nick(), reason)
|
|
for _, client := range channel.GetClients() {
|
|
client.SendFrom(c.Prefix(), kickMsg)
|
|
}
|
|
|
|
// Remove target from channel
|
|
channel.RemoveClient(target)
|
|
target.RemoveChannel(channelName)
|
|
}
|
|
|
|
// handleKill handles KILL command (operator only)
|
|
func (c *Client) handleKill(parts []string) {
|
|
if !c.IsOper() {
|
|
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "KILL :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
nick := parts[1]
|
|
reason := "Killed by operator"
|
|
if len(parts) > 2 {
|
|
reason = strings.Join(parts[2:], " ")
|
|
if len(reason) > 0 && reason[0] == ':' {
|
|
reason = reason[1:]
|
|
}
|
|
}
|
|
|
|
target := c.server.GetClient(nick)
|
|
if target == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, nick+" :No such nick/channel")
|
|
return
|
|
}
|
|
|
|
// Can't kill other operators
|
|
if target.IsOper() {
|
|
c.SendNumeric(ERR_CANTKILLSERVER, ":You can't kill other operators")
|
|
return
|
|
}
|
|
|
|
// Send kill message to target and disconnect
|
|
target.SendMessage(fmt.Sprintf("ERROR :Killed (%s (%s))", c.Nick(), reason))
|
|
|
|
// Broadcast to other operators
|
|
for _, client := range c.server.GetClients() {
|
|
if client.IsOper() && client != c {
|
|
client.SendMessage(fmt.Sprintf(":%s WALLOPS :%s killed %s (%s)",
|
|
c.server.config.Server.Name, c.Nick(), target.Nick(), reason))
|
|
}
|
|
}
|
|
|
|
// Disconnect the target
|
|
target.conn.Close()
|
|
}
|
|
|
|
// handleOper handles OPER command
|
|
func (c *Client) handleOper(parts []string) {
|
|
if len(parts) < 3 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "OPER :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
if c.server == nil || c.server.config == nil {
|
|
c.SendNumeric(ERR_NOOPERHOST, ":No O-lines for your host")
|
|
return
|
|
}
|
|
|
|
name := parts[1]
|
|
password := parts[2]
|
|
|
|
// Check if opers are enabled
|
|
if !c.server.config.Features.EnableOper {
|
|
c.SendNumeric(ERR_NOOPERHOST, ":O-lines are disabled")
|
|
return
|
|
}
|
|
|
|
// Find matching oper configuration
|
|
for _, oper := range c.server.config.Opers {
|
|
if oper.Name == name && oper.Password == password {
|
|
// Check host mask (simplified - just check if it matches *@localhost for now)
|
|
if oper.Host == "*@localhost" || oper.Host == "*@*" {
|
|
c.SetOper(true)
|
|
|
|
// Set operator user mode
|
|
c.SetMode('o', true)
|
|
c.SetMode('s', true) // Enable server notices by default
|
|
c.SetMode('w', true) // Enable wallops by default
|
|
|
|
// Set default snomasks for new operators
|
|
c.SetSnomask('c', true) // Client connects/disconnects
|
|
c.SetSnomask('o', true) // Oper-up messages
|
|
c.SetSnomask('s', true) // Server messages
|
|
|
|
c.SendNumeric(RPL_YOUREOPER, ":You are now an IRC operator")
|
|
c.SendNumeric(RPL_SNOMASK, fmt.Sprintf("%s :Server notice mask", c.GetSnomasks()))
|
|
|
|
// Send mode change notification
|
|
c.SendMessage(fmt.Sprintf(":%s MODE %s :+osw", c.Nick(), c.Nick()))
|
|
|
|
// Send snomask to other operators
|
|
c.sendSnomask('o', fmt.Sprintf("%s (%s@%s) is now an IRC operator", c.Nick(), c.User(), c.Host()))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
c.SendNumeric(ERR_PASSWDMISMATCH, ":Password incorrect")
|
|
}
|
|
|
|
// handleSnomask handles SNOMASK command (server notice masks for operators)
|
|
func (c *Client) handleSnomask(parts []string) {
|
|
if !c.IsOper() {
|
|
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
// Show current snomasks
|
|
current := c.GetSnomasks()
|
|
if current == "" {
|
|
current = "+"
|
|
}
|
|
c.SendNumeric(RPL_SNOMASK, fmt.Sprintf("%s :Server notice mask", current))
|
|
return
|
|
}
|
|
|
|
modeString := parts[1]
|
|
adding := true
|
|
changed := false
|
|
|
|
for _, char := range modeString {
|
|
switch char {
|
|
case '+':
|
|
adding = true
|
|
case '-':
|
|
adding = false
|
|
case 'c': // Client connects/disconnects
|
|
c.SetSnomask('c', adding)
|
|
changed = true
|
|
case 'k': // Kill messages
|
|
c.SetSnomask('k', adding)
|
|
changed = true
|
|
case 'o': // Oper-up messages
|
|
c.SetSnomask('o', adding)
|
|
changed = true
|
|
case 'x': // X-line (ban) messages
|
|
c.SetSnomask('x', adding)
|
|
changed = true
|
|
case 'f': // Flood messages
|
|
c.SetSnomask('f', adding)
|
|
changed = true
|
|
case 'n': // Nick changes
|
|
c.SetSnomask('n', adding)
|
|
changed = true
|
|
case 's': // Server messages
|
|
c.SetSnomask('s', adding)
|
|
changed = true
|
|
case 'd': // Debug messages (TechIRCd special)
|
|
c.SetSnomask('d', adding)
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
if changed {
|
|
current := c.GetSnomasks()
|
|
if current == "" {
|
|
current = "+"
|
|
}
|
|
c.SendNumeric(RPL_SNOMASK, fmt.Sprintf("%s :Server notice mask", current))
|
|
}
|
|
}
|
|
|
|
// handleGlobalNotice handles GLOBALNOTICE command (TechIRCd special oper command)
|
|
func (c *Client) handleGlobalNotice(parts []string) {
|
|
if !c.IsOper() {
|
|
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "GLOBALNOTICE :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
message := strings.Join(parts[1:], " ")
|
|
if len(message) > 0 && message[0] == ':' {
|
|
message = message[1:]
|
|
}
|
|
|
|
// Send global notice to all users
|
|
for _, client := range c.server.GetClients() {
|
|
client.SendMessage(fmt.Sprintf(":%s NOTICE %s :[GLOBAL] %s",
|
|
c.server.config.Server.Name, client.Nick(), message))
|
|
}
|
|
|
|
// Send snomask to operators watching global notices
|
|
c.sendSnomask('s', fmt.Sprintf("Global notice from %s: %s", c.Nick(), message))
|
|
}
|
|
|
|
// handleWallops handles WALLOPS command (send to users with +w mode)
|
|
func (c *Client) handleWallops(parts []string) {
|
|
if !c.IsOper() {
|
|
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "WALLOPS :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
message := strings.Join(parts[1:], " ")
|
|
if len(message) > 0 && message[0] == ':' {
|
|
message = message[1:]
|
|
}
|
|
|
|
// Send to all users with +w mode
|
|
for _, client := range c.server.GetClients() {
|
|
if client.HasMode('w') {
|
|
client.SendMessage(fmt.Sprintf(":%s WALLOPS :%s", c.Nick(), message))
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleOperWall handles OPERWALL command (message to all operators)
|
|
func (c *Client) handleOperWall(parts []string) {
|
|
if !c.IsOper() {
|
|
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendNumeric(ERR_NEEDMOREPARAMS, "OPERWALL :Not enough parameters")
|
|
return
|
|
}
|
|
|
|
message := strings.Join(parts[1:], " ")
|
|
if len(message) > 0 && message[0] == ':' {
|
|
message = message[1:]
|
|
}
|
|
|
|
// Send to all operators
|
|
for _, client := range c.server.GetClients() {
|
|
if client.IsOper() {
|
|
client.SendMessage(fmt.Sprintf(":%s WALLOPS :%s", c.Nick(), message))
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleRehash handles REHASH command (reload configuration)
|
|
func (c *Client) handleRehash(parts []string) {
|
|
if !c.IsOper() {
|
|
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
|
|
return
|
|
}
|
|
|
|
// Reload configuration
|
|
if c.server != nil {
|
|
err := c.server.ReloadConfig()
|
|
if err != nil {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** REHASH failed: %s",
|
|
c.server.config.Server.Name, c.Nick(), err.Error()))
|
|
c.sendSnomask('s', fmt.Sprintf("REHASH failed by %s: %s", c.Nick(), err.Error()))
|
|
} else {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Configuration reloaded successfully",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
c.sendSnomask('s', fmt.Sprintf("Configuration reloaded by %s", c.Nick()))
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleTrace handles TRACE command (show server connection tree)
|
|
func (c *Client) handleTrace(parts []string) {
|
|
if !c.IsOper() {
|
|
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
|
|
return
|
|
}
|
|
|
|
// Show basic server info (simplified implementation)
|
|
c.SendMessage(fmt.Sprintf(":%s 200 %s Link %s %s %s",
|
|
c.server.config.Server.Name, c.Nick(),
|
|
c.server.config.Server.Version,
|
|
c.server.config.Server.Name,
|
|
"TechIRCd"))
|
|
|
|
clientCount := len(c.server.GetClients())
|
|
c.SendMessage(fmt.Sprintf(":%s 262 %s %s :End of TRACE with %d clients",
|
|
c.server.config.Server.Name, c.Nick(),
|
|
c.server.config.Server.Name, clientCount))
|
|
}
|
|
|
|
// handleSpy handles SPY command - covert surveillance and stealth operations
|
|
func (c *Client) handleSpy(parts []string) {
|
|
if !c.IsOper() {
|
|
c.SendNumeric(ERR_NOPRIVILEGES, ":Permission Denied- You're not an IRC operator")
|
|
return
|
|
}
|
|
|
|
if len(parts) < 2 {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** SPY Usage: SPY <hide|watch|track|listen|cloak|ghost|shadow>",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
command := strings.ToLower(parts[1])
|
|
|
|
switch command {
|
|
case "hide":
|
|
c.handleSpyHide(parts[2:])
|
|
case "watch":
|
|
c.handleSpyWatch(parts[2:])
|
|
case "track":
|
|
c.handleSpyTrack(parts[2:])
|
|
case "listen":
|
|
c.handleSpyListen(parts[2:])
|
|
case "cloak":
|
|
c.handleSpyCloak(parts[2:])
|
|
case "ghost":
|
|
c.handleSpyGhost(parts[2:])
|
|
case "shadow":
|
|
c.handleSpyShadow(parts[2:])
|
|
case "status":
|
|
c.handleSpyStatus()
|
|
default:
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Unknown SPY command: %s",
|
|
c.server.config.Server.Name, c.Nick(), command))
|
|
}
|
|
}
|
|
|
|
// handleSpyHide - become invisible to most commands and lists
|
|
func (c *Client) handleSpyHide(args []string) {
|
|
if len(args) == 0 || strings.ToLower(args[0]) == "on" {
|
|
c.SetMode('H', true) // Hidden mode
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You are now HIDDEN from WHO, WHOIS, and NAMES",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
c.sendSnomask('d', fmt.Sprintf("Operator %s has entered STEALTH mode", c.Nick()))
|
|
} else if strings.ToLower(args[0]) == "off" {
|
|
c.SetMode('H', false)
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You are now VISIBLE again",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
c.sendSnomask('d', fmt.Sprintf("Operator %s has left STEALTH mode", c.Nick()))
|
|
}
|
|
}
|
|
|
|
// handleSpyWatch - monitor a specific user's activities
|
|
func (c *Client) handleSpyWatch(args []string) {
|
|
if len(args) < 1 {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY WATCH <nickname|off>",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
target := args[0]
|
|
if strings.ToLower(target) == "off" {
|
|
// TODO: Remove from watch list
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Surveillance disabled",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
targetClient := c.server.GetClient(target)
|
|
if targetClient == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel")
|
|
return
|
|
}
|
|
|
|
// TODO: Add to watch list
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Now watching %s (%s@%s)",
|
|
c.server.config.Server.Name, c.Nick(), target, targetClient.User(), targetClient.Host()))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Target is in channels: %s",
|
|
c.server.config.Server.Name, c.Nick(), c.getChannelList(targetClient)))
|
|
}
|
|
|
|
// handleSpyTrack - get real-time location and movement tracking
|
|
func (c *Client) handleSpyTrack(args []string) {
|
|
if len(args) < 1 {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY TRACK <nickname>",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
target := args[0]
|
|
targetClient := c.server.GetClient(target)
|
|
if targetClient == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel")
|
|
return
|
|
}
|
|
|
|
// Show detailed tracking info
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** TRACKING %s",
|
|
c.server.config.Server.Name, c.Nick(), target))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Location: %s@%s",
|
|
c.server.config.Server.Name, c.Nick(), targetClient.User(), targetClient.Host()))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Status: %s",
|
|
c.server.config.Server.Name, c.Nick(), c.getUserStatus(targetClient)))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Channels: %s",
|
|
c.server.config.Server.Name, c.Nick(), c.getChannelList(targetClient)))
|
|
|
|
if targetClient.Away() != "" {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Away: %s",
|
|
c.server.config.Server.Name, c.Nick(), targetClient.Away()))
|
|
}
|
|
}
|
|
|
|
// handleSpyListen - tap into channel conversations invisibly
|
|
func (c *Client) handleSpyListen(args []string) {
|
|
if len(args) < 1 {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY LISTEN <#channel|off>",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
target := args[0]
|
|
if strings.ToLower(target) == "off" {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Wiretaps disabled",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
if !isChannelName(target) {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Invalid channel name",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
channel := c.server.GetChannel(target)
|
|
if channel == nil {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
|
|
return
|
|
}
|
|
|
|
// TODO: Add to wiretap list
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Now listening to %s (%d users)",
|
|
c.server.config.Server.Name, c.Nick(), target, channel.UserCount()))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Wiretap established - you will receive covert copies of all messages",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
}
|
|
|
|
// handleSpyCloak - disguise your identity
|
|
func (c *Client) handleSpyCloak(args []string) {
|
|
if len(args) < 1 {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY CLOAK <identity|off>",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
identity := args[0]
|
|
if strings.ToLower(identity) == "off" {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Identity cloak removed",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
// TODO: Implement identity cloaking
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Identity cloaked as: %s",
|
|
c.server.config.Server.Name, c.Nick(), identity))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Your true identity is hidden from WHOIS and other commands",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
}
|
|
|
|
// handleSpyGhost - become completely invisible in a channel
|
|
func (c *Client) handleSpyGhost(args []string) {
|
|
if len(args) < 1 {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY GHOST <#channel|off>",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
target := args[0]
|
|
if strings.ToLower(target) == "off" {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Ghost mode disabled",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
if !isChannelName(target) {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Invalid channel name",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
channel := c.server.GetChannel(target)
|
|
if channel == nil {
|
|
c.SendNumeric(ERR_NOSUCHCHANNEL, target+" :No such channel")
|
|
return
|
|
}
|
|
|
|
// Join channel invisibly
|
|
if !c.IsInChannel(target) {
|
|
channel.AddClient(c)
|
|
}
|
|
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You are now a GHOST in %s",
|
|
c.server.config.Server.Name, c.Nick(), target))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You can see everything but are invisible to users",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
}
|
|
|
|
// handleSpyShadow - follow a user invisibly across channels
|
|
func (c *Client) handleSpyShadow(args []string) {
|
|
if len(args) < 1 {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Usage: SPY SHADOW <nickname|off>",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
target := args[0]
|
|
if strings.ToLower(target) == "off" {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Shadow mode disabled",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
return
|
|
}
|
|
|
|
targetClient := c.server.GetClient(target)
|
|
if targetClient == nil {
|
|
c.SendNumeric(ERR_NOSUCHNICK, target+" :No such nick/channel")
|
|
return
|
|
}
|
|
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** Now shadowing %s",
|
|
c.server.config.Server.Name, c.Nick(), target))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** You will automatically follow them to any channel they join",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
}
|
|
|
|
// handleSpyStatus - show current spy operations
|
|
func (c *Client) handleSpyStatus() {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** === SPY STATUS ===",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
|
|
if c.HasMode('H') {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** STEALTH: Active (Hidden from WHO/WHOIS/NAMES)",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
} else {
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** STEALTH: Inactive",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
}
|
|
|
|
// TODO: Show other active spy operations
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** WATCH: None active",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** WIRETAPS: None active",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
c.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** SHADOWS: None active",
|
|
c.server.config.Server.Name, c.Nick()))
|
|
}
|
|
|
|
// Helper functions for spy operations
|
|
func (c *Client) getChannelList(target *Client) string {
|
|
channels := target.GetChannels()
|
|
var channelNames []string
|
|
for _, channel := range channels {
|
|
channelNames = append(channelNames, channel.Name())
|
|
}
|
|
if len(channelNames) == 0 {
|
|
return "None"
|
|
}
|
|
return strings.Join(channelNames, " ")
|
|
}
|
|
|
|
func (c *Client) getUserStatus(target *Client) string {
|
|
status := "Online"
|
|
if target.Away() != "" {
|
|
status = "Away"
|
|
}
|
|
if target.IsOper() {
|
|
status += " (Operator)"
|
|
}
|
|
if target.HasMode('i') {
|
|
status += " (Invisible)"
|
|
}
|
|
if target.HasMode('B') {
|
|
status += " (Bot)"
|
|
}
|
|
return status
|
|
}
|
|
|
|
// sendSnomask sends a server notice to operators watching a specific snomask
|
|
func (c *Client) sendSnomask(snomask rune, message string) {
|
|
if c.server == nil {
|
|
return
|
|
}
|
|
|
|
for _, client := range c.server.GetClients() {
|
|
if client.IsOper() && client.HasSnomask(snomask) {
|
|
client.SendMessage(fmt.Sprintf(":%s NOTICE %s :*** %s",
|
|
c.server.config.Server.Name, client.Nick(), message))
|
|
}
|
|
}
|
|
}
|
|
|
|
// isValidNickname checks if a nickname is valid
|
|
func isValidNickname(nick string) bool {
|
|
if len(nick) == 0 || len(nick) > 30 {
|
|
return false
|
|
}
|
|
|
|
// First character must be a letter or special char
|
|
first := nick[0]
|
|
if !((first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z') ||
|
|
first == '[' || first == ']' || first == '\\' || first == '`' ||
|
|
first == '_' || first == '^' || first == '{' || first == '|' || first == '}') {
|
|
return false
|
|
}
|
|
|
|
// Rest can be letters, digits, or special chars
|
|
for i := 1; i < len(nick); i++ {
|
|
c := nick[i]
|
|
if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
|
|
c == '[' || c == ']' || c == '\\' || c == '`' ||
|
|
c == '_' || c == '^' || c == '{' || c == '|' || c == '}' || c == '-') {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// isValidChannelName checks if a channel name is valid
|
|
func isValidChannelName(name string) bool {
|
|
if len(name) == 0 || len(name) > 50 {
|
|
return false
|
|
}
|
|
|
|
return name[0] == '#' || name[0] == '&' || name[0] == '!' || name[0] == '+'
|
|
}
|
|
|
|
// isChannelName checks if a name is a channel name
|
|
func isChannelName(name string) bool {
|
|
if len(name) == 0 {
|
|
return false
|
|
}
|
|
return name[0] == '#' || name[0] == '&' || name[0] == '!' || name[0] == '+'
|
|
}
|