/* goircd -- minimalistic simple Internet Relay Chat (IRC) server Copyright (C) 2014-2020 Sergey Matveev 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 the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package main import ( "fmt" "log" "regexp" "sort" "strings" "sync" ) type Room struct { name string topic string key string members map[*Client]struct{} events chan ClientEvent sync.RWMutex } var ( RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$") 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 (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 } 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") } 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) } r.RUnlock() } func (r *Room) StateSave() { stateSink <- StateEvent{r.name, r.topic, r.key} } func (r *Room) Processor(events <-chan ClientEvent) { for e := range events { c := e.client switch e.eventType { case EventTerm: roomsWG.Done() return case EventNew: r.Lock() r.members[c] = struct{}{} r.Unlock() if *verbose { log.Println(c, "joined", r.name) } 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: if _, subscribed := r.members[c]; !subscribed { c.ReplyNicknamed("442", r.name, "You are not on that channel") continue } 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: if _, subscribed := r.members[c]; !subscribed { c.ReplyParts("442", r.name, "You are not on that channel") continue } if e.text == "" { r.SendTopic(c) continue } 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: r.RLock() for m := range r.members { c.ReplyNicknamed( "352", r.name, m.username, m.Host(), *hostname, m.nickname, "H", "0 "+m.realname, ) } c.ReplyNicknamed("315", r.name, "End of /WHO list") r.RUnlock() case EventMode: if e.text == "" { mode := "+n" if r.key != "" { mode = mode + "k" } c.Msg(fmt.Sprintf("324 %s %s %s", c.nickname, r.name, mode)) continue } if strings.HasPrefix(e.text, "b") { c.ReplyNicknamed("368", r.name, "End of channel ban list") continue } 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 { c.ReplyNicknamed("472", e.text, "Unknown MODE flag") continue } var msg string var msgLog string if strings.HasPrefix(e.text, "+k") { cols := strings.Split(e.text, " ") if len(cols) == 1 { c.ReplyNotEnoughParameters("MODE") continue } r.key = cols[1] msg = fmt.Sprintf(":%s MODE %s +k %s", c, r.name, r.key) msgLog = "set channel key" } else { r.key = "" msg = fmt.Sprintf(":%s MODE %s -k", c, r.name) msgLog = "removed channel key" } r.Broadcast(msg) logSink <- LogEvent{r.name, c.nickname, msgLog, true} r.StateSave() case EventMsg: 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 }