1 // goircd -- minimalistic simple Internet Relay Chat (IRC) server
2 // Copyright (C) 2014-2024 Sergey Matveev <stargrave@stargrave.org>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, version 3 of the License.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
26 // Max deadline time of client's unresponsiveness
27 PingTimeout = time.Second * 180
28 // Max idle client's time before PING are sent
29 PingThreshold = time.Second * 90
32 func Processor(events chan ClientEvent, finished chan struct{}) {
34 ticker := time.NewTicker(10 * time.Second)
37 events <- ClientEvent{eventType: EventTick}
41 for e := range events {
47 for c := range clients {
48 if c.recvTimestamp.Add(PingTimeout).Before(now) {
50 log.Println(c, "ping timeout")
55 if c.sendTimestamp.Add(PingThreshold).Before(now) {
57 c.Msg("PING :" + *hostname)
58 c.sendTimestamp = time.Now()
61 log.Println(c, "ping timeout")
69 for rn, r := range rooms {
70 if *statedir == "" && len(r.members) == 0 {
72 log.Println(rn, "emptied room")
75 r.events <- ClientEvent{eventType: EventTerm}
84 clients[client] = struct{}{}
88 delete(clients, client)
91 for _, r := range rooms {
96 cols := strings.SplitN(e.text, " ", 2)
97 cmd := strings.ToUpper(cols[0])
99 log.Println(client, "command", cmd)
104 log.Println(client, "quit")
108 if !client.registered {
109 client.Register(cmd, cols)
113 client.recvTimestamp = now
119 client.ReplyNicknamed("305", "You are no longer marked as being away")
122 client.away = strings.TrimLeft(cols[1], ":")
123 client.ReplyNicknamed("306", "You have been marked as being away")
125 if len(cols) == 1 || len(cols[1]) < 1 {
126 client.ReplyNotEnoughParameters("JOIN")
131 client.SendList(cols)
135 if len(cols) == 1 || len(cols[1]) < 1 {
136 client.ReplyNotEnoughParameters("MODE")
139 cols = strings.SplitN(cols[1], " ", 2)
140 if cols[0] == client.username {
141 client.Msg("221 " + client.nickname + " +w")
142 // client.ReplyNicknamed("501", "Unknown MODE flag")
146 r, found := rooms[room]
148 client.ReplyNoChannel(room)
152 r.events <- ClientEvent{client, EventMode, ""}
154 r.events <- ClientEvent{client, EventMode, cols[1]}
159 rs := make([]*Room, len(cols))
162 for _, r := range rooms {
166 needed := make(map[string]struct{}, len(rs))
167 for _, r := range cols {
168 needed[r] = struct{}{}
170 for rn, r := range rooms {
171 if _, found := needed[rn]; found {
177 for _, r := range rs {
181 if len(cols) == 1 || len(cols[1]) < 1 {
182 client.ReplyNotEnoughParameters("PART")
185 rs := strings.Split(cols[1], " ")[0]
187 for _, room := range strings.Split(rs, ",") {
188 if r, found := rooms[room]; found {
189 r.events <- ClientEvent{client, EventDel, ""}
191 client.ReplyNoChannel(room)
197 client.ReplyNicknamed("409", "No origin specified")
200 client.Reply(fmt.Sprintf("PONG %s :%s", *hostname, cols[1]))
203 case "NOTICE", "PRIVMSG":
205 client.ReplyNicknamed("411", "No recipient given ("+cmd+")")
208 cols = strings.SplitN(cols[1], " ", 2)
210 client.ReplyNicknamed("412", "No text to send")
213 target := strings.ToLower(cols[0])
215 if r, found := rooms[target]; found {
216 r.events <- ClientEvent{
219 cmd + " " + strings.TrimLeft(cols[1], ":"),
227 for c := range clients {
228 if c.nickname != target {
231 msg = fmt.Sprintf(":%s %s %s %s", client, cmd, c.nickname, cols[1])
234 client.ReplyNicknamed("301", c.nickname, c.away)
238 clientsLock.RUnlock()
242 client.ReplyNoNickChan(target)
245 client.ReplyNotEnoughParameters("TOPIC")
248 cols = strings.SplitN(cols[1], " ", 2)
249 r, found := rooms[cols[0]]
251 client.ReplyNoChannel(cols[0])
258 r.events <- ClientEvent{client, EventTopic, change}
260 if len(cols) == 1 || len(cols[1]) < 1 {
261 client.ReplyNotEnoughParameters("WHO")
264 room := strings.Split(cols[1], " ")[0]
265 r, found := rooms[room]
267 r.events <- ClientEvent{client, EventWho, ""}
269 client.ReplyNoChannel(room)
272 if len(cols) == 1 || len(cols[1]) < 1 {
273 client.ReplyNotEnoughParameters("WHOIS")
276 cols := strings.Split(cols[1], " ")
277 nicknames := strings.Split(cols[len(cols)-1], ",")
278 client.SendWhois(nicknames)
280 if len(cols) == 1 || len(cols[1]) < 1 {
281 client.ReplyNotEnoughParameters("ISON")
284 nicknamesList := strings.Split(cols[1], " ")
285 nicknames := make(map[string]bool, len(nicknamesList))
286 for _, nickname := range nicknamesList {
287 nicknames[nickname] = false
290 for c := range clients {
291 if _, exists := nicknames[c.nickname]; exists {
292 nicknames[c.nickname] = true
295 clientsLock.RUnlock()
296 nicknamesList = nicknamesList[:0]
297 for n, exists := range nicknames {
299 nicknamesList = append(nicknamesList, n)
302 client.ReplyNicknamed("303", strings.Join(nicknamesList, " "))
305 client.ReplyNotEnoughParameters("WALLOPS")
308 cs := make([]*Client, 0, len(clients))
310 for c := range clients {
315 clientsLock.RUnlock()
316 for _, c := range cs {
317 c.Msg(fmt.Sprintf(":%s NOTICE %s %s", client, c.nickname, cols[1]))
324 client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", Version, debug, *hostname))
326 client.ReplyNicknamed("421", cmd, "Unknown command")
332 // Notify all clients about shutdown
334 for c := range clients {
336 ":%s NOTICE %s %s", *hostname, c.nickname,
337 ":Server is shutting down",
341 clientsLock.RUnlock()
343 // Read their EventDel
349 // Stop room processors
351 for _, r := range rooms {
352 r.events <- ClientEvent{eventType: EventTerm}
357 // Wait for either 5sec or all clients quitting
358 t := time.NewTimer(5 * time.Second)
359 clientsDone := make(chan struct{})