Files
techircd/channel.go

837 lines
18 KiB
Go

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
}