package main import ( "fmt" "path/filepath" "strings" "sync" "time" ) type Channel struct { name string topic string topicBy string topicTime time.Time clients map[string]*Client operators map[string]*Client halfops map[string]*Client voices map[string]*Client owners map[string]*Client admins map[string]*Client modes map[rune]bool key string limit int banList []string quietList []string // Users who can join but not speak (+q) exceptList []string // Users exempt from bans (+e) inviteList []string // Users who can join invite-only channels (+I) created time.Time // Advanced mode settings floodSettings string // Flood protection settings (e.g., "10:5") joinThrottle string // Join throttling settings (e.g., "3:10") mu sync.RWMutex } func NewChannel(name string) *Channel { return &Channel{ name: name, clients: make(map[string]*Client), operators: make(map[string]*Client), halfops: make(map[string]*Client), voices: make(map[string]*Client), owners: make(map[string]*Client), admins: make(map[string]*Client), modes: make(map[rune]bool), banList: make([]string, 0), quietList: make([]string, 0), exceptList: make([]string, 0), inviteList: make([]string, 0), created: time.Now(), } } func (ch *Channel) Name() string { ch.mu.RLock() defer ch.mu.RUnlock() return ch.name } func (ch *Channel) Topic() string { ch.mu.RLock() defer ch.mu.RUnlock() return ch.topic } func (ch *Channel) TopicBy() string { ch.mu.RLock() defer ch.mu.RUnlock() return ch.topicBy } func (ch *Channel) TopicTime() time.Time { ch.mu.RLock() defer ch.mu.RUnlock() return ch.topicTime } func (ch *Channel) SetTopic(topic, by string) { ch.mu.Lock() defer ch.mu.Unlock() ch.topic = topic ch.topicBy = by ch.topicTime = time.Now() } func (ch *Channel) AddClient(client *Client) { ch.mu.Lock() defer ch.mu.Unlock() nick := strings.ToLower(client.Nick()) // Check if client is already in the channel if _, exists := ch.clients[nick]; exists { // Client already in channel, don't add again return } ch.clients[nick] = client client.AddChannel(ch) // First user gets configured founder mode (default: operator) if len(ch.clients) == 1 { // Get the configured founder mode from server config founderMode := "o" // Default fallback if client.server != nil && client.server.config != nil { founderMode = client.server.config.Channels.FounderMode } // Apply the appropriate mode based on configuration switch founderMode { case "q": ch.owners[nick] = client case "a": ch.admins[nick] = client case "o": ch.operators[nick] = client case "h": ch.halfops[nick] = client case "v": ch.voices[nick] = client default: // Invalid config, fallback to operator ch.operators[nick] = client } } } func (ch *Channel) RemoveClient(client *Client) { ch.mu.Lock() defer ch.mu.Unlock() nick := strings.ToLower(client.Nick()) delete(ch.clients, nick) delete(ch.operators, nick) delete(ch.halfops, nick) delete(ch.voices, nick) delete(ch.owners, nick) delete(ch.admins, nick) client.RemoveChannel(ch.name) } func (ch *Channel) HasClient(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() _, exists := ch.clients[strings.ToLower(client.Nick())] return exists } func (ch *Channel) IsOperator(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() _, exists := ch.operators[strings.ToLower(client.Nick())] return exists } func (ch *Channel) IsVoice(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() _, exists := ch.voices[strings.ToLower(client.Nick())] return exists } func (ch *Channel) IsHalfop(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() _, exists := ch.halfops[strings.ToLower(client.Nick())] return exists } func (ch *Channel) IsOwner(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() _, exists := ch.owners[strings.ToLower(client.Nick())] return exists } func (ch *Channel) IsQuieted(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() return ch.isQuietedUnsafe(client) } func (ch *Channel) isQuietedUnsafe(client *Client) bool { hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host()) for _, quiet := range ch.quietList { if ch.matchesBanMask(client, quiet, hostmask) { return true } } return false } func (ch *Channel) SetOperator(client *Client, isOp bool) { ch.mu.Lock() defer ch.mu.Unlock() nick := strings.ToLower(client.Nick()) if isOp { ch.operators[nick] = client } else { delete(ch.operators, nick) } } func (ch *Channel) SetVoice(client *Client, hasVoice bool) { ch.mu.Lock() defer ch.mu.Unlock() nick := strings.ToLower(client.Nick()) if hasVoice { ch.voices[nick] = client } else { delete(ch.voices, nick) } } func (ch *Channel) SetHalfop(client *Client, isHalfop bool) { ch.mu.Lock() defer ch.mu.Unlock() nick := strings.ToLower(client.Nick()) if isHalfop { ch.halfops[nick] = client } else { delete(ch.halfops, nick) } } func (ch *Channel) SetOwner(client *Client, isOwner bool) { ch.mu.Lock() defer ch.mu.Unlock() nick := strings.ToLower(client.Nick()) if isOwner { ch.owners[nick] = client } else { delete(ch.owners, nick) } } func (ch *Channel) IsAdmin(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() _, exists := ch.admins[strings.ToLower(client.Nick())] return exists } func (ch *Channel) SetAdmin(client *Client, isAdmin bool) { ch.mu.Lock() defer ch.mu.Unlock() nick := strings.ToLower(client.Nick()) if isAdmin { ch.admins[nick] = client } else { delete(ch.admins, nick) } } func (ch *Channel) GetClients() []*Client { ch.mu.RLock() defer ch.mu.RUnlock() clients := make([]*Client, 0, len(ch.clients)) for _, client := range ch.clients { clients = append(clients, client) } return clients } func (ch *Channel) GetClientCount() int { ch.mu.RLock() defer ch.mu.RUnlock() return len(ch.clients) } func (ch *Channel) UserCount() int { return ch.GetClientCount() } func (ch *Channel) Broadcast(message string, exclude *Client) { ch.mu.RLock() defer ch.mu.RUnlock() for _, client := range ch.clients { if exclude != nil && client.Nick() == exclude.Nick() { continue } client.SendMessage(message) } } func (ch *Channel) BroadcastFrom(source, message string, exclude *Client) { ch.mu.RLock() defer ch.mu.RUnlock() for _, client := range ch.clients { if exclude != nil && client.Nick() == exclude.Nick() { continue } client.SendFrom(source, message) } } func (ch *Channel) HasMode(mode rune) bool { ch.mu.RLock() defer ch.mu.RUnlock() return ch.modes[mode] } func (ch *Channel) SetMode(mode rune, set bool) { ch.mu.Lock() defer ch.mu.Unlock() if set { ch.modes[mode] = true } else { delete(ch.modes, mode) } } func (ch *Channel) GetModes() string { ch.mu.RLock() defer ch.mu.RUnlock() var modes []rune for mode := range ch.modes { modes = append(modes, mode) } if len(modes) == 0 { return "" } return "+" + string(modes) } func (ch *Channel) CanSendMessage(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() // Check if user is quieted first if ch.isQuietedUnsafe(client) { // Only owners, operators, and halfops can speak when quieted nick := strings.ToLower(client.Nick()) _, isOwner := ch.owners[nick] _, isOp := ch.operators[nick] _, isHalfop := ch.halfops[nick] if !isOwner && !isOp && !isHalfop { return false } } // If channel is not moderated, anyone in the channel can send if !ch.modes['m'] { return true } // In moderated channels, only owners, operators, halfops and voiced users can send messages nick := strings.ToLower(client.Nick()) _, isOwner := ch.owners[nick] _, isOp := ch.operators[nick] _, isHalfop := ch.halfops[nick] _, hasVoice := ch.voices[nick] return isOwner || isOp || isHalfop || hasVoice } func (ch *Channel) Key() string { ch.mu.RLock() defer ch.mu.RUnlock() return ch.key } func (ch *Channel) SetKey(key string) { ch.mu.Lock() defer ch.mu.Unlock() ch.key = key } func (ch *Channel) Limit() int { ch.mu.RLock() defer ch.mu.RUnlock() return ch.limit } func (ch *Channel) SetLimit(limit int) { ch.mu.Lock() defer ch.mu.Unlock() ch.limit = limit } func (ch *Channel) GetNamesReply() string { ch.mu.RLock() defer ch.mu.RUnlock() var names []string for _, client := range ch.clients { prefix := "" if ch.IsOwner(client) { prefix = "~" } else if ch.IsOperator(client) { prefix = "@" } else if ch.IsHalfop(client) { prefix = "%" } else if ch.IsVoice(client) { prefix = "+" } names = append(names, prefix+client.Nick()) } return strings.Join(names, " ") } func (ch *Channel) CanSpeak(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() // If channel is not moderated, anyone can speak if !ch.modes['m'] { return true } // Operators and voiced users can always speak return ch.IsOperator(client) || ch.IsVoice(client) } func (ch *Channel) CanJoin(client *Client, key string) bool { ch.mu.RLock() defer ch.mu.RUnlock() // Check if invite-only if ch.modes['i'] { // Check invite list for _, mask := range ch.inviteList { if ch.matchesMask(client.Prefix(), mask) { return true } } return false } // Check key if ch.modes['k'] && ch.key != key { return false } // Check limit if ch.modes['l'] && len(ch.clients) >= ch.limit { return false } // Check ban list for _, mask := range ch.banList { if ch.matchesMask(client.Prefix(), mask) { // Check exception list for _, exceptMask := range ch.exceptList { if ch.matchesMask(client.Prefix(), exceptMask) { return true } } return false } } return true } func (ch *Channel) matchesMask(target, mask string) bool { // Simple mask matching - should be enhanced for production return strings.Contains(strings.ToLower(target), strings.ToLower(mask)) } func (ch *Channel) AddBan(mask string) { ch.mu.Lock() defer ch.mu.Unlock() ch.banList = append(ch.banList, mask) } func (ch *Channel) RemoveBan(mask string) { ch.mu.Lock() defer ch.mu.Unlock() for i, ban := range ch.banList { if ban == mask { ch.banList = append(ch.banList[:i], ch.banList[i+1:]...) break } } } func (ch *Channel) GetBans() []string { ch.mu.RLock() defer ch.mu.RUnlock() bans := make([]string, len(ch.banList)) copy(bans, ch.banList) return bans } // Extended ban list management func (ch *Channel) AddQuiet(mask string) { ch.mu.Lock() defer ch.mu.Unlock() ch.quietList = append(ch.quietList, mask) } func (ch *Channel) RemoveQuiet(mask string) { ch.mu.Lock() defer ch.mu.Unlock() for i, quiet := range ch.quietList { if quiet == mask { ch.quietList = append(ch.quietList[:i], ch.quietList[i+1:]...) break } } } func (ch *Channel) GetQuiets() []string { ch.mu.RLock() defer ch.mu.RUnlock() quiets := make([]string, len(ch.quietList)) copy(quiets, ch.quietList) return quiets } func (ch *Channel) AddExcept(mask string) { ch.mu.Lock() defer ch.mu.Unlock() ch.exceptList = append(ch.exceptList, mask) } func (ch *Channel) RemoveExcept(mask string) { ch.mu.Lock() defer ch.mu.Unlock() for i, except := range ch.exceptList { if except == mask { ch.exceptList = append(ch.exceptList[:i], ch.exceptList[i+1:]...) break } } } func (ch *Channel) GetExcepts() []string { ch.mu.RLock() defer ch.mu.RUnlock() excepts := make([]string, len(ch.exceptList)) copy(excepts, ch.exceptList) return excepts } func (ch *Channel) AddInviteException(mask string) { ch.mu.Lock() defer ch.mu.Unlock() ch.inviteList = append(ch.inviteList, mask) } func (ch *Channel) RemoveInviteException(mask string) { ch.mu.Lock() defer ch.mu.Unlock() for i, invite := range ch.inviteList { if invite == mask { ch.inviteList = append(ch.inviteList[:i], ch.inviteList[i+1:]...) break } } } func (ch *Channel) GetInviteExceptions() []string { ch.mu.RLock() defer ch.mu.RUnlock() invites := make([]string, len(ch.inviteList)) copy(invites, ch.inviteList) return invites } // IsExempt checks if a client is exempt from bans func (ch *Channel) IsExempt(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host()) for _, except := range ch.exceptList { if ch.matchesBanMask(client, except, hostmask) { return true } } return false } // CanJoinInviteOnly checks if a client can join an invite-only channel func (ch *Channel) CanJoinInviteOnly(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host()) for _, invite := range ch.inviteList { if ch.matchesBanMask(client, invite, hostmask) { return true } } return false } func (ch *Channel) Created() time.Time { ch.mu.RLock() defer ch.mu.RUnlock() return ch.created } // IsBanned checks if a client matches any ban mask in the channel func (ch *Channel) IsBanned(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() // Check exemptions first - if exempt, not banned if ch.isExemptUnsafe(client) { return false } hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host()) for _, ban := range ch.banList { if ch.matchesBanMask(client, ban, hostmask) { return true } } return false } // isExemptUnsafe checks exemptions without locking (internal use) func (ch *Channel) isExemptUnsafe(client *Client) bool { hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host()) for _, except := range ch.exceptList { if ch.matchesBanMask(client, except, hostmask) { return true } } return false } // matchesBanMask checks if a client matches a ban mask (supports extended bans) func (ch *Channel) matchesBanMask(client *Client, banMask, hostmask string) bool { // Check for extended ban format: ~type:parameter or ~type parameter if strings.HasPrefix(banMask, "~") { return ch.matchesExtendedBan(client, banMask) } // Traditional hostmask ban return matchWildcard(banMask, hostmask) } // matchesExtendedBan handles extended ban types func (ch *Channel) matchesExtendedBan(client *Client, extban string) bool { if len(extban) < 2 || extban[0] != '~' { return false } // Parse ~type:parameter or ~type parameter format var banType string var parameter string if strings.Contains(extban, ":") { // Format: ~type:parameter parts := strings.SplitN(extban[1:], ":", 2) banType = parts[0] if len(parts) > 1 { parameter = parts[1] } } else { // Format: ~type parameter (space separated) parts := strings.Fields(extban[1:]) if len(parts) > 0 { banType = parts[0] if len(parts) > 1 { parameter = strings.Join(parts[1:], " ") } } } switch banType { case "a": // Account ban: ~a:accountname or ~a accountname if parameter == "" { // ~a with no parameter bans unregistered users return client.account == "" } // ~a:account bans specific account return client.account == parameter case "c": // Channel ban: ~c:#channel - ban users in another channel if parameter == "" || !strings.HasPrefix(parameter, "#") { return false } targetChannel := client.server.GetChannel(parameter) return targetChannel != nil && targetChannel.HasClient(client) case "j": // Join prevent: ~j:#channel - prevent joining if in another channel if parameter == "" || !strings.HasPrefix(parameter, "#") { return false } targetChannel := client.server.GetChannel(parameter) return targetChannel != nil && targetChannel.HasClient(client) case "n": // Nick pattern ban: ~n:pattern if parameter == "" { return false } return matchWildcard(parameter, client.Nick()) case "q": // Quiet ban: ~q:mask - this is a special case for quiet functionality // When used in regular ban list, ~q acts as a quiet if parameter == "" { // ~q with no parameter quiets everyone return true } // ~q:mask quiets matching users - check against hostmask pattern hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host()) return matchWildcard(parameter, hostmask) case "r": // Real name ban: ~r:pattern if parameter == "" { return false } return matchWildcard(parameter, client.realname) case "s": // Server ban: ~s:servername if parameter == "" { return false } return matchWildcard(parameter, client.server.config.Server.Name) case "o": // Operator ban: ~o (bans all opers) return client.IsOper() case "z": // Non-SSL ban: ~z (bans non-SSL users) return !client.ssl case "Z": // SSL-only ban: ~Z (bans SSL users) return client.ssl case "u": // Username pattern ban: ~u:pattern if parameter == "" { return false } return matchWildcard(parameter, client.User()) case "h": // Hostname pattern ban: ~h:pattern if parameter == "" { return false } return matchWildcard(parameter, client.Host()) case "i": // IP ban: ~i:ip/cidr if parameter == "" { return false } // Simple IP matching for now (could be enhanced with CIDR) return matchWildcard(parameter, client.Host()) case "R": // Registered only: ~R (bans unregistered users) return client.account == "" case "m": // Mute ban: ~m:mask - similar to quiet if parameter == "" { return true } hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host()) return matchWildcard(parameter, hostmask) default: // Unknown extended ban type return false } } // IsInvited checks if a client is on the invite list for the channel func (ch *Channel) IsInvited(client *Client) bool { ch.mu.RLock() defer ch.mu.RUnlock() hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host()) for _, invite := range ch.inviteList { if matchWildcard(invite, hostmask) { return true } } return false } // matchWildcard checks if a pattern with wildcards (* and ?) matches a string func matchWildcard(pattern, str string) bool { matched, _ := filepath.Match(strings.ToLower(pattern), strings.ToLower(str)) return matched } // SetFloodSettings sets the flood protection settings func (ch *Channel) SetFloodSettings(settings string) { ch.mu.Lock() defer ch.mu.Unlock() ch.floodSettings = settings } // GetFloodSettings returns the flood protection settings func (ch *Channel) GetFloodSettings() string { ch.mu.RLock() defer ch.mu.RUnlock() return ch.floodSettings } // SetJoinThrottle sets the join throttling settings func (ch *Channel) SetJoinThrottle(settings string) { ch.mu.Lock() defer ch.mu.Unlock() ch.joinThrottle = settings } // GetJoinThrottle returns the join throttling settings func (ch *Channel) GetJoinThrottle() string { ch.mu.RLock() defer ch.mu.RUnlock() return ch.joinThrottle }