/*
goircd -- minimalistic simple Internet Relay Chat (IRC) server
-Copyright (C) 2014-2018 | wn Sergey Matveev <stargrave@stargrave.org>
+Copyright (C) 2014-2023 Sergey Matveev <stargrave@stargrave.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
"sync"
)
-var (
- RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
-)
-
-// Sanitize room's name. It can consist of 1 to 50 ASCII symbols
-// with some exclusions. All room names will have "#" prefix.
-func RoomNameValid(name string) bool {
- return RERoom.MatchString(name)
-}
-
type Room struct {
- name *string
- topic *string
- key *string
+ name string
+ topic string
+ key string
members map[*Client]struct{}
+ events chan ClientEvent
sync.RWMutex
}
-func (room *Room) String() (name string) {
- room.RLock()
- name = *room.name
- room.RUnlock()
- return
-}
+var (
+ RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
-func NewRoom(name string) *Room {
- topic := ""
- key := ""
- return &Room{
- name: &name,
- topic: &topic,
- key: &key,
- members: make(map[*Client]struct{}),
+ rooms map[string]*Room = make(map[string]*Room)
+ roomsLock sync.RWMutex
+ roomsWG sync.WaitGroup
+)
+
+func (r *Room) SendTopic(c *Client) {
+ t := r.topic
+ if t == "" {
+ c.ReplyNicknamed("331", r.name, "No topic is set")
+ } else {
+ c.ReplyNicknamed("332", r.name, t)
}
}
-func (room *Room) SendTopic(client *Client) {
- room.RLock()
- if *room.topic == "" {
- client.ReplyNicknamed("331", room.String(), "No topic is set")
- } else {
- client.ReplyNicknamed("332", room.String(), *room.topic)
+func (r *Room) SendNames(c *Client) {
+ allowed := false
+ if r.key == "" {
+ allowed = true
+ } else if _, isMember := r.members[c]; isMember {
+ allowed = true
+ }
+ if !allowed {
+ c.ReplyNicknamed("475", r.name, "Cannot join channel (+k)")
+ return
}
- room.RUnlock()
+ r.RLock()
+ nicknames := make([]string, 0, len(r.members))
+ for member := range r.members {
+ nicknames = append(nicknames, member.nickname)
+ }
+ r.RUnlock()
+ sort.Strings(nicknames)
+ maxLen := 512 - len(*hostname) - 2 - 2
+
+MoreNicknames:
+ lenAll := 0
+ lenName := 0
+ for i, n := range nicknames {
+ lenName = len(n) + 1
+ if lenAll+lenName >= maxLen {
+ c.ReplyNicknamed("353", "=", r.name, strings.Join(nicknames[:i-1], " "))
+ nicknames = nicknames[i:]
+ goto MoreNicknames
+ }
+ lenAll += lenName
+ }
+ if len(nicknames) > 0 {
+ c.ReplyNicknamed("353", "=", r.name, strings.Join(nicknames, " "))
+ }
+ c.ReplyNicknamed("366", r.name, "End of NAMES list")
}
-// Send message to all room's subscribers, possibly excluding someone.
-func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) {
- room.RLock()
- for member := range room.members {
- if (len(clientToIgnore) > 0) && member == clientToIgnore[0] {
+func (r *Room) Broadcast(msg string, excludes ...*Client) {
+ var exclude *Client
+ if len(excludes) > 0 {
+ exclude = excludes[0]
+ }
+ r.RLock()
+ for member := range r.members {
+ if member == exclude {
continue
}
member.Msg(msg)
}
- room.RUnlock()
+ r.RUnlock()
}
-func (room *Room) StateSave() {
- room.RLock()
- stateSink <- StateEvent{room.String(), *room.topic, *room.key}
- room.RUnlock()
+func (r *Room) StateSave() {
+ stateSink <- StateEvent{r.name, r.topic, r.key}
}
-func (room *Room) Processor(events <-chan ClientEvent) {
- var client *Client
- for event := range events {
- client = event.client
- switch event.eventType {
+func (r *Room) Processor(events <-chan ClientEvent) {
+ for e := range events {
+ c := e.client
+ switch e.eventType {
case EventTerm:
- roomsGroup.Done()
+ roomsWG.Done()
return
case EventNew:
- room.Lock()
- room.members[client] = struct{}{}
+ r.Lock()
+ r.members[c] = struct{}{}
+ r.Unlock()
if *verbose {
- log.Println(client, "joined", room.name)
+ log.Println(c, "joined", r.name)
}
- room.Unlock()
- room.SendTopic(client)
- room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, room.String()))
- logSink <- LogEvent{room.String(), *client.nickname, "joined", true}
- nicknames := make([]string, 0)
- room.RLock()
- for member := range room.members {
- nicknames = append(nicknames, *member.nickname)
- }
- room.RUnlock()
- sort.Strings(nicknames)
- client.ReplyNicknamed("353", "=", room.String(), strings.Join(nicknames, " "))
- client.ReplyNicknamed("366", room.String(), "End of NAMES list")
+ r.SendTopic(c)
+ r.Broadcast(fmt.Sprintf(":%s JOIN %s", c, r.name))
+ logSink <- LogEvent{r.name, c.nickname, "joined", true}
+ r.SendNames(c)
case EventDel:
- room.RLock()
- if _, subscribed := room.members[client]; !subscribed {
- client.ReplyNicknamed("442", room.String(), "You are not on that channel")
- room.RUnlock()
+ if _, subscribed := r.members[c]; !subscribed {
+ c.ReplyNicknamed("442", r.name, "You are not on that channel")
continue
}
- room.RUnlock()
- room.Lock()
- delete(room.members, client)
- room.Unlock()
- room.RLock()
- msg := fmt.Sprintf(":%s PART %s :%s", client, room.String(), *client.nickname)
- room.Broadcast(msg)
- logSink <- LogEvent{room.String(), *client.nickname, "left", true}
- room.RUnlock()
+ msg := fmt.Sprintf(":%s PART %s :%s", c, r.name, c.nickname)
+ r.Broadcast(msg)
+ r.Lock()
+ delete(r.members, c)
+ r.Unlock()
+ logSink <- LogEvent{r.name, c.nickname, "left", true}
+ if *verbose {
+ log.Println(c, "left", r.name)
+ }
case EventTopic:
- room.RLock()
- if _, subscribed := room.members[client]; !subscribed {
- client.ReplyParts("442", room.String(), "You are not on that channel")
- room.RUnlock()
+ if _, subscribed := r.members[c]; !subscribed {
+ c.ReplyParts("442", r.name, "You are not on that channel")
continue
}
- if event.text == "" {
- room.SendTopic(client)
- room.RUnlock()
+ if e.text == "" {
+ r.SendTopic(c)
continue
}
- room.RUnlock()
- topic := strings.TrimLeft(event.text, ":")
- room.Lock()
- room.topic = &topic
- room.Unlock()
- room.RLock()
- msg := fmt.Sprintf(":%s TOPIC %s :%s", client, room.String(), *room.topic)
- room.Broadcast(msg)
- logSink <- LogEvent{
- room.String(),
- *client.nickname,
- "set topic to " + *room.topic,
- true,
- }
- room.RUnlock()
- room.StateSave()
+ topic := strings.TrimLeft(e.text, ":")
+ r.topic = topic
+ msg := fmt.Sprintf(":%s TOPIC %s :%s", c, r.name, r.topic)
+ r.Broadcast(msg)
+ logSink <- LogEvent{r.name, c.nickname, "set topic to " + r.topic, true}
+ r.StateSave()
case EventWho:
- room.RLock()
- for m := range room.members {
- client.ReplyNicknamed(
+ r.RLock()
+ for m := range r.members {
+ c.ReplyNicknamed(
"352",
- room.String(),
- *m.username,
+ r.name,
+ m.username,
m.Host(),
*hostname,
- *m.nickname,
+ m.nickname,
"H",
- "0 "+*m.realname,
+ "0 "+m.realname,
)
}
- client.ReplyNicknamed("315", room.String(), "End of /WHO list")
- room.RUnlock()
+ c.ReplyNicknamed("315", r.name, "End of /WHO list")
+ r.RUnlock()
case EventMode:
- room.RLock()
- if event.text == "" {
- mode := "+"
- if *room.key != "" {
+ if e.text == "" {
+ mode := "+n"
+ if r.key != "" {
mode = mode + "k"
}
- client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, room.String(), mode))
- room.RUnlock()
+ c.Msg(fmt.Sprintf("324 %s %s %s", c.nickname, r.name, mode))
continue
}
- if strings.HasPrefix(event.text, "b") {
- client.ReplyNicknamed("368", room.String(), "End of channel ban list")
- room.RUnlock()
+ if strings.HasPrefix(e.text, "b") {
+ c.ReplyNicknamed("368", r.name, "End of channel ban list")
continue
}
- if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") {
- if _, subscribed := room.members[client]; !subscribed {
- client.ReplyParts("442", room.String(), "You are not on that channel")
- room.RUnlock()
+ if strings.HasPrefix(e.text, "-k") || strings.HasPrefix(e.text, "+k") {
+ if _, subscribed := r.members[c]; !subscribed {
+ c.ReplyParts("442", r.name, "You are not on that channel")
continue
}
} else {
- client.ReplyNicknamed("472", event.text, "Unknown MODE flag")
- room.RUnlock()
+ c.ReplyNicknamed("472", e.text, "Unknown MODE flag")
continue
}
- room.RUnlock()
var msg string
var msgLog string
- if strings.HasPrefix(event.text, "+k") {
- cols := strings.Split(event.text, " ")
+ if strings.HasPrefix(e.text, "+k") {
+ cols := strings.Split(e.text, " ")
if len(cols) == 1 {
- client.ReplyNotEnoughParameters("MODE")
+ c.ReplyNotEnoughParameters("MODE")
continue
}
- room.Lock()
- room.key = &cols[1]
- msg = fmt.Sprintf(":%s MODE %s +k %s", client, *room.name, *room.key)
- msgLog = "set channel key to " + *room.key
- room.Unlock()
+ r.key = cols[1]
+ msg = fmt.Sprintf(":%s MODE %s +k %s", c, r.name, r.key)
+ msgLog = "set channel key"
} else {
- key := ""
- room.Lock()
- room.key = &key
- msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name)
- room.Unlock()
+ r.key = ""
+ msg = fmt.Sprintf(":%s MODE %s -k", c, r.name)
msgLog = "removed channel key"
}
- room.Broadcast(msg)
- logSink <- LogEvent{room.String(), *client.nickname, msgLog, true}
- room.StateSave()
+ r.Broadcast(msg)
+ logSink <- LogEvent{r.name, c.nickname, msgLog, true}
+ r.StateSave()
case EventMsg:
- sep := strings.Index(event.text, " ")
- room.Broadcast(fmt.Sprintf(
- ":%s %s %s :%s",
- client,
- event.text[:sep],
- room.String(),
- event.text[sep+1:]),
- client,
- )
- logSink <- LogEvent{
- room.String(),
- *client.nickname,
- event.text[sep+1:],
- false,
- }
+ sep := strings.Index(e.text, " ")
+ r.Broadcast(fmt.Sprintf(
+ ":%s %s %s :%s", c, e.text[:sep], r.name, e.text[sep+1:],
+ ), c)
+ logSink <- LogEvent{r.name, c.nickname, e.text[sep+1:], false}
}
}
}
+
+func RoomRegister(name string) *Room {
+ r := &Room{
+ name: name,
+ members: make(map[*Client]struct{}),
+ events: make(chan ClientEvent),
+ }
+ roomsLock.Lock()
+ roomsWG.Add(1)
+ rooms[name] = r
+ roomsLock.Unlock()
+ go r.Processor(r.events)
+ return r
+}