diff --git a/client.go b/client.go
index 9bcaf30..a43cc1e 100644
--- a/client.go
+++ b/client.go
@@ -23,11 +23,13 @@ import (
"log"
"net"
"strings"
+ "sync"
"time"
)
const (
- BufSize = 1500
+ BufSize = 1500
+ MaxOutBuf = 1 << 12
)
var (
@@ -35,35 +37,60 @@ var (
)
type Client struct {
- hostname *string
- conn net.Conn
- registered bool
- nickname string
- username string
- realname string
- password string
- away *string
+ conn net.Conn
+ registered bool
+ nickname *string
+ username *string
+ realname *string
+ password *string
+ away *string
+ recvTimestamp time.Time
+ sendTimestamp time.Time
+ outBuf chan string
+ alive bool
+ sync.Mutex
}
-type ClientAlivenessState struct {
- pingSent bool
- timestamp time.Time
+func (c Client) String() string {
+ return *c.nickname + "!" + *c.username + "@" + c.conn.RemoteAddr().String()
}
-func (client Client) String() string {
- return client.nickname + "!" + client.username + "@" + client.conn.RemoteAddr().String()
+func NewClient(conn net.Conn) *Client {
+ nickname := "*"
+ username := ""
+ c := Client{
+ conn: conn,
+ nickname: &nickname,
+ username: &username,
+ recvTimestamp: time.Now(),
+ sendTimestamp: time.Now(),
+ alive: true,
+ outBuf: make(chan string, MaxOutBuf),
+ }
+ go c.MsgSender()
+ return &c
}
-func NewClient(hostname *string, conn net.Conn) *Client {
- return &Client{hostname: hostname, conn: conn, nickname: "*", password: ""}
+func (c *Client) SetDead() {
+ close(c.outBuf)
+ c.alive = false
+}
+
+func (c *Client) Close() {
+ c.Lock()
+ c.conn.Close()
+ if c.alive {
+ c.SetDead()
+ }
+ c.Unlock()
}
// Client processor blockingly reads everything remote client sends,
// splits messages by CRLF and send them to Daemon gorouting for processing
// it futher. Also it can signalize that client is unavailable (disconnected).
-func (client *Client) Processor(sink chan<- ClientEvent) {
- sink <- ClientEvent{client, EventNew, ""}
- log.Println(client, "New client")
+func (c *Client) Processor(sink chan ClientEvent) {
+ sink <- ClientEvent{c, EventNew, ""}
+ log.Println(c, "New client")
buf := make([]byte, BufSize*2)
var n int
var prev int
@@ -71,14 +98,11 @@ func (client *Client) Processor(sink chan<- ClientEvent) {
var err error
for {
if prev == BufSize {
- log.Println(client, "buffer size exceeded, kicking him")
- sink <- ClientEvent{client, EventDel, ""}
- client.conn.Close()
+ log.Println(c, "input buffer size exceeded, kicking him")
break
}
- n, err = client.conn.Read(buf[prev:])
+ n, err = c.conn.Read(buf[prev:])
if err != nil {
- sink <- ClientEvent{client, EventDel, ""}
break
}
prev += n
@@ -87,50 +111,69 @@ func (client *Client) Processor(sink chan<- ClientEvent) {
if i == -1 {
continue
}
- sink <- ClientEvent{client, EventMsg, string(buf[:i])}
+ sink <- ClientEvent{c, EventMsg, string(buf[:i])}
copy(buf, buf[i+2:prev])
prev -= (i + 2)
goto CheckMore
}
+ c.Close()
+ sink <- ClientEvent{c, EventDel, ""}
+}
+
+func (c *Client) MsgSender() {
+ for msg := range c.outBuf {
+ c.conn.Write(append([]byte(msg), CRLF...))
+ }
}
// Send message as is with CRLF appended.
-func (client *Client) Msg(text string) {
- client.conn.Write(append([]byte(text), CRLF...))
+func (c *Client) Msg(text string) {
+ c.Lock()
+ defer c.Unlock()
+ if !c.alive {
+ return
+ }
+ if len(c.outBuf) == MaxOutBuf {
+ log.Println(c, "output buffer size exceeded, kicking him")
+ go c.Close()
+ c.SetDead()
+ return
+ }
+ c.outBuf <- text
}
// Send message from server. It has ": servername" prefix.
-func (client *Client) Reply(text string) {
- client.Msg(":" + *client.hostname + " " + text)
+func (c *Client) Reply(text string) {
+ c.Msg(":" + *hostname + " " + text)
}
// Send server message, concatenating all provided text parts and
// prefix the last one with ":".
-func (client *Client) ReplyParts(code string, text ...string) {
+func (c *Client) ReplyParts(code string, text ...string) {
parts := []string{code}
for _, t := range text {
parts = append(parts, t)
}
parts[len(parts)-1] = ":" + parts[len(parts)-1]
- client.Reply(strings.Join(parts, " "))
+ c.Reply(strings.Join(parts, " "))
}
// Send nicknamed server message. After servername it always has target
// client's nickname. The last part is prefixed with ":".
-func (client *Client) ReplyNicknamed(code string, text ...string) {
- client.ReplyParts(code, append([]string{client.nickname}, text...)...)
+func (c *Client) ReplyNicknamed(code string, text ...string) {
+ c.ReplyParts(code, append([]string{*c.nickname}, text...)...)
}
// Reply "461 not enough parameters" error for given command.
-func (client *Client) ReplyNotEnoughParameters(command string) {
- client.ReplyNicknamed("461", command, "Not enough parameters")
+func (c *Client) ReplyNotEnoughParameters(command string) {
+ c.ReplyNicknamed("461", command, "Not enough parameters")
}
// Reply "403 no such channel" error for specified channel.
-func (client *Client) ReplyNoChannel(channel string) {
- client.ReplyNicknamed("403", channel, "No such channel")
+func (c *Client) ReplyNoChannel(channel string) {
+ c.ReplyNicknamed("403", channel, "No such channel")
}
-func (client *Client) ReplyNoNickChan(channel string) {
- client.ReplyNicknamed("401", channel, "No such nick/channel")
+func (c *Client) ReplyNoNickChan(channel string) {
+ c.ReplyNicknamed("401", channel, "No such nick/channel")
}
diff --git a/client_test.go b/client_test.go
index 3735e1f..4c9682e 100644
--- a/client_test.go
+++ b/client_test.go
@@ -19,86 +19,17 @@ along with this program. If not, see .
package main
import (
- "net"
"testing"
- "time"
)
-// Testing network connection that satisfies net.Conn interface
-// Can send predefined messages and store all written ones
-type TestingConn struct {
- inbound chan string
- outbound chan string
- closed bool
-}
-
-func NewTestingConn() *TestingConn {
- inbound := make(chan string, 8)
- outbound := make(chan string, 8)
- return &TestingConn{inbound: inbound, outbound: outbound}
-}
-
-func (conn TestingConn) Error() string {
- return "i am finished"
-}
-
-func (conn *TestingConn) Read(b []byte) (n int, err error) {
- msg := <-conn.inbound
- if msg == "" {
- return 0, conn
- }
- for n, bt := range append([]byte(msg), CRLF...) {
- b[n] = bt
- }
- return len(msg)+2, nil
-}
-
-type MyAddr struct{}
-
-func (a MyAddr) String() string {
- return "someclient"
-}
-func (a MyAddr) Network() string {
- return "somenet"
-}
-
-func (conn *TestingConn) Write(b []byte) (n int, err error) {
- conn.outbound <- string(b)
- return len(b), nil
-}
-
-func (conn *TestingConn) Close() error {
- conn.closed = true
- return nil
-}
-
-func (conn TestingConn) LocalAddr() net.Addr {
- return nil
-}
-
-func (conn TestingConn) RemoteAddr() net.Addr {
- return MyAddr{}
-}
-
-func (conn TestingConn) SetDeadline(t time.Time) error {
- return nil
-}
-
-func (conn TestingConn) SetReadDeadline(t time.Time) error {
- return nil
-}
-
-func (conn TestingConn) SetWriteDeadline(t time.Time) error {
- return nil
-}
-
// New client creation test. It must send an event about new client,
// two predefined messages from it and deletion one
func TestNewClient(t *testing.T) {
conn := NewTestingConn()
sink := make(chan ClientEvent)
host := "foohost"
- client := NewClient(&host, conn)
+ hostname = &host
+ client := NewClient(conn)
go client.Processor(sink)
event := <-sink
@@ -126,8 +57,10 @@ func TestNewClient(t *testing.T) {
func TestClientReplies(t *testing.T) {
conn := NewTestingConn()
host := "foohost"
- client := NewClient(&host, conn)
- client.nickname = "мойник"
+ hostname = &host
+ client := NewClient(conn)
+ nickname := "мойник"
+ client.nickname = &nickname
client.Reply("hello")
if r := <-conn.outbound; r != ":foohost hello\r\n" {
diff --git a/common_test.go b/common_test.go
new file mode 100644
index 0000000..9c4cb05
--- /dev/null
+++ b/common_test.go
@@ -0,0 +1,93 @@
+/*
+goircd -- minimalistic simple Internet Relay Chat (IRC) server
+Copyright (C) 2014-2015 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, either version 3 of the License, or
+(at your option) any later version.
+
+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 (
+ "net"
+ "time"
+)
+
+// Testing network connection that satisfies net.Conn interface
+// Can send predefined messages and store all written ones
+type TestingConn struct {
+ inbound chan string
+ outbound chan string
+ closed bool
+}
+
+func NewTestingConn() *TestingConn {
+ inbound := make(chan string, 8)
+ outbound := make(chan string, 8)
+ return &TestingConn{inbound: inbound, outbound: outbound}
+}
+
+func (conn TestingConn) Error() string {
+ return "i am finished"
+}
+
+func (conn *TestingConn) Read(b []byte) (n int, err error) {
+ msg := <-conn.inbound
+ if msg == "" {
+ return 0, conn
+ }
+ for n, bt := range append([]byte(msg), CRLF...) {
+ b[n] = bt
+ }
+ return len(msg) + 2, nil
+}
+
+type MyAddr struct{}
+
+func (a MyAddr) String() string {
+ return "someclient"
+}
+func (a MyAddr) Network() string {
+ return "somenet"
+}
+
+func (conn *TestingConn) Write(b []byte) (n int, err error) {
+ conn.outbound <- string(b)
+ return len(b), nil
+}
+
+func (conn *TestingConn) Close() error {
+ conn.closed = true
+ close(conn.outbound)
+ return nil
+}
+
+func (conn TestingConn) LocalAddr() net.Addr {
+ return nil
+}
+
+func (conn TestingConn) RemoteAddr() net.Addr {
+ return MyAddr{}
+}
+
+func (conn TestingConn) SetDeadline(t time.Time) error {
+ return nil
+}
+
+func (conn TestingConn) SetReadDeadline(t time.Time) error {
+ return nil
+}
+
+func (conn TestingConn) SetWriteDeadline(t time.Time) error {
+ return nil
+}
diff --git a/daemon.go b/daemon.go
index 9145ba0..80a3229 100644
--- a/daemon.go
+++ b/daemon.go
@@ -26,56 +26,28 @@ import (
"regexp"
"sort"
"strings"
+ "sync"
"time"
)
const (
- // Max time deadline for client's unresponsiveness
+ // Max deadline time of client's unresponsiveness
PingTimeout = time.Second * 180
// Max idle client's time before PING are sent
PingThreshold = time.Second * 90
- // Client's aliveness check period
- AlivenessCheck = time.Second * 10
)
var (
RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$")
+
+ roomsGroup sync.WaitGroup
+
+ clients map[*Client]struct{} = make(map[*Client]struct{})
)
-type Daemon struct {
- Verbose bool
- version string
- hostname *string
- motd *string
- passwords *string
- clients map[*Client]struct{}
- clientAliveness map[*Client]*ClientAlivenessState
- rooms map[string]*Room
- roomSinks map[*Room]chan ClientEvent
- lastAlivenessCheck time.Time
- logSink chan<- LogEvent
- stateSink chan<- StateEvent
-}
-
-func NewDaemon(version string, hostname, motd, passwords *string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Daemon {
- daemon := Daemon{
- version: version,
- hostname: hostname,
- motd: motd,
- passwords: passwords,
- }
- daemon.clients = make(map[*Client]struct{})
- daemon.clientAliveness = make(map[*Client]*ClientAlivenessState)
- daemon.rooms = make(map[string]*Room)
- daemon.roomSinks = make(map[*Room]chan ClientEvent)
- daemon.logSink = logSink
- daemon.stateSink = stateSink
- return &daemon
-}
-
-func (daemon *Daemon) SendLusers(client *Client) {
+func SendLusers(client *Client) {
lusers := 0
- for client := range daemon.clients {
+ for client := range clients {
if client.registered {
lusers++
}
@@ -83,79 +55,87 @@ func (daemon *Daemon) SendLusers(client *Client) {
client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers))
}
-func (daemon *Daemon) SendMotd(client *Client) {
- if daemon.motd == nil || *daemon.motd == "" {
+func SendMotd(client *Client) {
+ if motd == nil {
client.ReplyNicknamed("422", "MOTD File is missing")
return
}
-
- motd, err := ioutil.ReadFile(*daemon.motd)
+ motdText, err := ioutil.ReadFile(*motd)
if err != nil {
- log.Printf("Can not read motd file %s: %v", *daemon.motd, err)
+ log.Printf("Can not read motd file %s: %v", *motd, err)
client.ReplyNicknamed("422", "Error reading MOTD File")
return
}
-
- client.ReplyNicknamed("375", "- "+*daemon.hostname+" Message of the day -")
- for _, s := range strings.Split(strings.Trim(string(motd), "\n"), "\n") {
- client.ReplyNicknamed("372", "- "+string(s))
+ client.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
+ for _, s := range strings.Split(strings.Trim(string(motdText), "\n"), "\n") {
+ client.ReplyNicknamed("372", "- "+s)
}
client.ReplyNicknamed("376", "End of /MOTD command")
}
-func (daemon *Daemon) SendWhois(client *Client, nicknames []string) {
+func SendWhois(client *Client, nicknames []string) {
+ var c *Client
+ var hostPort string
+ var err error
+ var subscriptions []string
+ var room *Room
+ var subscriber *Client
for _, nickname := range nicknames {
nickname = strings.ToLower(nickname)
- found := false
- for c := range daemon.clients {
- if strings.ToLower(c.nickname) != nickname {
- continue
+ for c = range clients {
+ if strings.ToLower(*c.nickname) == nickname {
+ goto Found
}
- found = true
- h := c.conn.RemoteAddr().String()
- h, _, err := net.SplitHostPort(h)
- if err != nil {
- log.Printf("Can't parse RemoteAddr %q: %v", h, err)
- h = "Unknown"
- }
- client.ReplyNicknamed("311", c.nickname, c.username, h, "*", c.realname)
- client.ReplyNicknamed("312", c.nickname, *daemon.hostname, *daemon.hostname)
- if c.away != nil {
- client.ReplyNicknamed("301", c.nickname, *c.away)
- }
- subscriptions := []string{}
- for _, room := range daemon.rooms {
- for subscriber := range room.members {
- if subscriber.nickname == nickname {
- subscriptions = append(subscriptions, room.name)
- }
+ }
+ client.ReplyNoNickChan(nickname)
+ continue
+ Found:
+ hostPort, _, err = net.SplitHostPort(c.conn.RemoteAddr().String())
+ if err != nil {
+ log.Printf("Can't parse RemoteAddr %q: %v", hostPort, err)
+ hostPort = "Unknown"
+ }
+ client.ReplyNicknamed("311", *c.nickname, *c.username, hostPort, "*", *c.realname)
+ client.ReplyNicknamed("312", *c.nickname, *hostname, *hostname)
+ if c.away != nil {
+ client.ReplyNicknamed("301", *c.nickname, *c.away)
+ }
+ subscriptions = make([]string, 0)
+ for _, room = range rooms {
+ for subscriber = range room.members {
+ if *subscriber.nickname == nickname {
+ subscriptions = append(subscriptions, *room.name)
}
}
- sort.Strings(subscriptions)
- client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " "))
- client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list")
- }
- if !found {
- client.ReplyNoNickChan(nickname)
}
+ sort.Strings(subscriptions)
+ client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " "))
+ client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list")
}
}
-func (daemon *Daemon) SendList(client *Client, cols []string) {
- var rooms []string
+func SendList(client *Client, cols []string) {
+ var rs []string
+ var r string
if (len(cols) > 1) && (cols[1] != "") {
- rooms = strings.Split(strings.Split(cols[1], " ")[0], ",")
+ rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
} else {
- rooms = []string{}
- for room := range daemon.rooms {
- rooms = append(rooms, room)
+ rs = make([]string, 0)
+ for r = range rooms {
+ rs = append(rs, r)
}
}
- sort.Strings(rooms)
- for _, room := range rooms {
- r, found := daemon.rooms[room]
- if found {
- client.ReplyNicknamed("322", room, fmt.Sprintf("%d", len(r.members)), r.topic)
+ sort.Strings(rs)
+ var room *Room
+ var found bool
+ for _, r = range rs {
+ if room, found = rooms[r]; found {
+ client.ReplyNicknamed(
+ "322",
+ r,
+ fmt.Sprintf("%d", len(room.members)),
+ *room.topic,
+ )
}
}
client.ReplyNicknamed("323", "End of /LIST")
@@ -166,14 +146,14 @@ func (daemon *Daemon) SendList(client *Client, cols []string) {
// * only QUIT, NICK and USER commands are processed
// * other commands are quietly ignored
// When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him.
-func (daemon *Daemon) ClientRegister(client *Client, command string, cols []string) {
- switch command {
+func ClientRegister(client *Client, cmd string, cols []string) {
+ switch cmd {
case "PASS":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("PASS")
return
}
- client.password = cols[1]
+ client.password = &cols[1]
case "NICK":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyParts("431", "No nickname given")
@@ -182,8 +162,8 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri
nickname := cols[1]
// Compatibility with some clients prepending colons to nickname
nickname = strings.TrimPrefix(nickname, ":")
- for existingClient := range daemon.clients {
- if existingClient.nickname == nickname {
+ for existingClient := range clients {
+ if *existingClient.nickname == nickname {
client.ReplyParts("433", "*", nickname, "Nickname is already in use")
return
}
@@ -192,7 +172,7 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri
client.ReplyParts("432", "*", cols[1], "Erroneous nickname")
return
}
- client.nickname = nickname
+ client.nickname = &nickname
case "USER":
if len(cols) == 1 {
client.ReplyNotEnoughParameters("USER")
@@ -203,66 +183,69 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri
client.ReplyNotEnoughParameters("USER")
return
}
- client.username = args[0]
- client.realname = strings.TrimLeft(args[3], ":")
+ client.username = &args[0]
+ realname := strings.TrimLeft(args[3], ":")
+ client.realname = &realname
}
- if client.nickname != "*" && client.username != "" {
- if daemon.passwords != nil && *daemon.passwords != "" {
- if client.password == "" {
+ if *client.nickname != "*" && *client.username != "" {
+ if passwords != nil && *passwords != "" {
+ if client.password == nil {
client.ReplyParts("462", "You may not register")
- client.conn.Close()
+ client.Close()
return
}
- contents, err := ioutil.ReadFile(*daemon.passwords)
+ contents, err := ioutil.ReadFile(*passwords)
if err != nil {
- log.Fatalf("Can no read passwords file %s: %s", *daemon.passwords, err)
+ log.Fatalf("Can no read passwords file %s: %s", *passwords, err)
return
}
for _, entry := range strings.Split(string(contents), "\n") {
if entry == "" {
continue
}
- if lp := strings.Split(entry, ":"); lp[0] == client.nickname && lp[1] != client.password {
+ if lp := strings.Split(entry, ":"); lp[0] == *client.nickname && lp[1] != *client.password {
client.ReplyParts("462", "You may not register")
- client.conn.Close()
+ client.Close()
return
}
}
}
-
client.registered = true
client.ReplyNicknamed("001", "Hi, welcome to IRC")
- client.ReplyNicknamed("002", "Your host is "+*daemon.hostname+", running goircd "+daemon.version)
+ client.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+version)
client.ReplyNicknamed("003", "This server was created sometime")
- client.ReplyNicknamed("004", *daemon.hostname+" goircd o o")
- daemon.SendLusers(client)
- daemon.SendMotd(client)
+ client.ReplyNicknamed("004", *hostname+" goircd o o")
+ SendLusers(client)
+ SendMotd(client)
log.Println(client, "logged in")
}
}
// Register new room in Daemon. Create an object, events sink, save pointers
// to corresponding daemon's places and start room's processor goroutine.
-func (daemon *Daemon) RoomRegister(name string) (*Room, chan<- ClientEvent) {
- roomNew := NewRoom(daemon.hostname, name, daemon.logSink, daemon.stateSink)
- roomNew.Verbose = daemon.Verbose
+func RoomRegister(name string) (*Room, chan ClientEvent) {
+ roomNew := NewRoom(name)
roomSink := make(chan ClientEvent)
- daemon.rooms[name] = roomNew
- daemon.roomSinks[roomNew] = roomSink
+ rooms[name] = roomNew
+ roomSinks[roomNew] = roomSink
go roomNew.Processor(roomSink)
+ roomsGroup.Add(1)
return roomNew, roomSink
}
-func (daemon *Daemon) HandlerJoin(client *Client, cmd string) {
+func HandlerJoin(client *Client, cmd string) {
args := strings.Split(cmd, " ")
- rooms := strings.Split(args[0], ",")
+ rs := strings.Split(args[0], ",")
var keys []string
if len(args) > 1 {
keys = strings.Split(args[1], ",")
} else {
- keys = []string{}
+ keys = make([]string, 0)
}
- for n, room := range rooms {
+ var roomExisting *Room
+ var roomSink chan ClientEvent
+ var roomNew *Room
+ for n, room := range rs {
if !RoomNameValid(room) {
client.ReplyNoChannel(room)
continue
@@ -273,97 +256,88 @@ func (daemon *Daemon) HandlerJoin(client *Client, cmd string) {
} else {
key = ""
}
- denied := false
- joined := false
- for roomExisting, roomSink := range daemon.roomSinks {
- if room == roomExisting.name {
- if (roomExisting.key != "") && (roomExisting.key != key) {
- denied = true
- } else {
- roomSink <- ClientEvent{client, EventNew, ""}
- joined = true
+ for roomExisting, roomSink = range roomSinks {
+ if room == *roomExisting.name {
+ if (roomExisting.key != nil) && (*roomExisting.key != key) {
+ goto Denied
}
- break
+ roomSink <- ClientEvent{client, EventNew, ""}
+ goto Joined
}
}
- if denied {
- client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key")
- }
- if denied || joined {
- continue
- }
- roomNew, roomSink := daemon.RoomRegister(room)
+ roomNew, roomSink = RoomRegister(room)
log.Println("Room", roomNew, "created")
if key != "" {
- roomNew.key = key
+ roomNew.key = &key
roomNew.StateSave()
}
roomSink <- ClientEvent{client, EventNew, ""}
+ continue
+ Denied:
+ client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key")
+ Joined:
}
}
-func (daemon *Daemon) Processor(events <-chan ClientEvent) {
+func Processor(events chan ClientEvent, finished chan struct{}) {
var now time.Time
+ go func() {
+ for {
+ time.Sleep(10 * time.Second)
+ events <- ClientEvent{eventType: EventTick}
+ }
+ }()
for event := range events {
now = time.Now()
client := event.client
-
- // Check for clients aliveness
- if daemon.lastAlivenessCheck.Add(AlivenessCheck).Before(now) {
- for c := range daemon.clients {
- aliveness, alive := daemon.clientAliveness[c]
- if !alive {
- continue
- }
- if aliveness.timestamp.Add(PingTimeout).Before(now) {
+ switch event.eventType {
+ case EventTick:
+ for c := range clients {
+ if c.recvTimestamp.Add(PingTimeout).Before(now) {
log.Println(c, "ping timeout")
- c.conn.Close()
+ c.Close()
continue
}
- if !aliveness.pingSent && aliveness.timestamp.Add(PingThreshold).Before(now) {
+ if c.sendTimestamp.Add(PingThreshold).Before(now) {
if c.registered {
- c.Msg("PING :" + *daemon.hostname)
- aliveness.pingSent = true
+ c.Msg("PING :" + *hostname)
+ c.sendTimestamp = time.Now()
} else {
log.Println(c, "ping timeout")
- c.conn.Close()
+ c.Close()
}
}
}
- daemon.lastAlivenessCheck = now
- }
-
- switch event.eventType {
- case EventNew:
- daemon.clients[client] = struct{}{}
- daemon.clientAliveness[client] = &ClientAlivenessState{
- pingSent: false,
- timestamp: now,
+ case EventTerm:
+ for _, sink := range roomSinks {
+ sink <- ClientEvent{eventType: EventTerm}
}
+ roomsGroup.Wait()
+ close(finished)
+ return
+ case EventNew:
+ clients[client] = struct{}{}
case EventDel:
- delete(daemon.clients, client)
- delete(daemon.clientAliveness, client)
- for _, roomSink := range daemon.roomSinks {
+ delete(clients, client)
+ for _, roomSink := range roomSinks {
roomSink <- event
}
case EventMsg:
cols := strings.SplitN(event.text, " ", 2)
- command := strings.ToUpper(cols[0])
- if daemon.Verbose {
- log.Println(client, "command", command)
+ cmd := strings.ToUpper(cols[0])
+ if *verbose {
+ log.Println(client, "command", cmd)
}
- if command == "QUIT" {
+ if cmd == "QUIT" {
log.Println(client, "quit")
- delete(daemon.clients, client)
- delete(daemon.clientAliveness, client)
- client.conn.Close()
+ client.Close()
continue
}
if !client.registered {
- daemon.ClientRegister(client, command, cols)
+ ClientRegister(client, cmd, cols)
continue
}
- switch command {
+ switch cmd {
case "AWAY":
if len(cols) == 1 {
client.away = nil
@@ -378,63 +352,63 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
client.ReplyNotEnoughParameters("JOIN")
continue
}
- daemon.HandlerJoin(client, cols[1])
+ HandlerJoin(client, cols[1])
case "LIST":
- daemon.SendList(client, cols)
+ SendList(client, cols)
case "LUSERS":
- daemon.SendLusers(client)
+ SendLusers(client)
case "MODE":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("MODE")
continue
}
cols = strings.SplitN(cols[1], " ", 2)
- if cols[0] == client.username {
+ if cols[0] == *client.username {
if len(cols) == 1 {
- client.Msg("221 " + client.nickname + " +")
+ client.Msg("221 " + *client.nickname + " +")
} else {
client.ReplyNicknamed("501", "Unknown MODE flag")
}
continue
}
room := cols[0]
- r, found := daemon.rooms[room]
+ r, found := rooms[room]
if !found {
client.ReplyNoChannel(room)
continue
}
if len(cols) == 1 {
- daemon.roomSinks[r] <- ClientEvent{client, EventMode, ""}
+ roomSinks[r] <- ClientEvent{client, EventMode, ""}
} else {
- daemon.roomSinks[r] <- ClientEvent{client, EventMode, cols[1]}
+ roomSinks[r] <- ClientEvent{client, EventMode, cols[1]}
}
case "MOTD":
- go daemon.SendMotd(client)
+ SendMotd(client)
case "PART":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("PART")
continue
}
- rooms := strings.Split(cols[1], " ")[0]
- for _, room := range strings.Split(rooms, ",") {
- r, found := daemon.rooms[room]
- if !found {
+ rs := strings.Split(cols[1], " ")[0]
+ for _, room := range strings.Split(rs, ",") {
+ if r, found := rooms[room]; found {
+ roomSinks[r] <- ClientEvent{client, EventDel, ""}
+ } else {
client.ReplyNoChannel(room)
continue
}
- daemon.roomSinks[r] <- ClientEvent{client, EventDel, ""}
}
case "PING":
if len(cols) == 1 {
client.ReplyNicknamed("409", "No origin specified")
continue
}
- client.Reply(fmt.Sprintf("PONG %s :%s", *daemon.hostname, cols[1]))
+ client.Reply(fmt.Sprintf("PONG %s :%s", *hostname, cols[1]))
case "PONG":
continue
case "NOTICE", "PRIVMSG":
if len(cols) == 1 {
- client.ReplyNicknamed("411", "No recipient given ("+command+")")
+ client.ReplyNicknamed("411", "No recipient given ("+cmd+")")
continue
}
cols = strings.SplitN(cols[1], " ", 2)
@@ -444,12 +418,12 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
}
msg := ""
target := strings.ToLower(cols[0])
- for c := range daemon.clients {
- if c.nickname == target {
- msg = fmt.Sprintf(":%s %s %s %s", client, command, c.nickname, cols[1])
+ for c := range clients {
+ if *c.nickname == target {
+ msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1])
c.Msg(msg)
if c.away != nil {
- client.ReplyNicknamed("301", c.nickname, *c.away)
+ client.ReplyNicknamed("301", *c.nickname, *c.away)
}
break
}
@@ -457,15 +431,14 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
if msg != "" {
continue
}
- r, found := daemon.rooms[target]
- if !found {
+ if r, found := rooms[target]; found {
+ roomSinks[r] <- ClientEvent{
+ client,
+ EventMsg,
+ cmd + " " + strings.TrimLeft(cols[1], ":"),
+ }
+ } else {
client.ReplyNoNickChan(target)
- continue
- }
- daemon.roomSinks[r] <- ClientEvent{
- client,
- EventMsg,
- command + " " + strings.TrimLeft(cols[1], ":"),
}
case "TOPIC":
if len(cols) == 1 {
@@ -473,7 +446,7 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
continue
}
cols = strings.SplitN(cols[1], " ", 2)
- r, found := daemon.rooms[cols[0]]
+ r, found := rooms[cols[0]]
if !found {
client.ReplyNoChannel(cols[0])
continue
@@ -484,19 +457,18 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
} else {
change = ""
}
- daemon.roomSinks[r] <- ClientEvent{client, EventTopic, change}
+ roomSinks[r] <- ClientEvent{client, EventTopic, change}
case "WHO":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHO")
continue
}
room := strings.Split(cols[1], " ")[0]
- r, found := daemon.rooms[room]
- if !found {
+ if r, found := rooms[room]; found {
+ roomSinks[r] <- ClientEvent{client, EventWho, ""}
+ } else {
client.ReplyNoChannel(room)
- continue
}
- daemon.roomSinks[r] <- ClientEvent{client, EventWho, ""}
case "WHOIS":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHOIS")
@@ -504,22 +476,21 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) {
}
cols := strings.Split(cols[1], " ")
nicknames := strings.Split(cols[len(cols)-1], ",")
- daemon.SendWhois(client, nicknames)
+ SendWhois(client, nicknames)
case "VERSION":
var debug string
- if daemon.Verbose {
+ if *verbose {
debug = "debug"
} else {
debug = ""
}
- client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", daemon.version, debug, *daemon.hostname))
+ client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", version, debug, *hostname))
default:
- client.ReplyNicknamed("421", command, "Unknown command")
+ client.ReplyNicknamed("421", cmd, "Unknown command")
}
}
- if aliveness, alive := daemon.clientAliveness[client]; alive {
- aliveness.timestamp = now
- aliveness.pingSent = false
+ if client != nil {
+ client.recvTimestamp = now
}
}
}
diff --git a/daemon_test.go b/daemon_test.go
index efdb39f..e48d22d 100644
--- a/daemon_test.go
+++ b/daemon_test.go
@@ -27,21 +27,24 @@ import (
func TestRegistrationWorkflow(t *testing.T) {
host := "foohost"
- daemon := NewDaemon("ver1", &host, nil, nil, nil, nil)
+ hostname = &host
events := make(chan ClientEvent)
- go daemon.Processor(events)
+ defer func() {
+ events <- ClientEvent{eventType: EventTerm}
+ }()
+ go Processor(events, make(chan struct{}))
conn := NewTestingConn()
- client := NewClient(&host, conn)
+ client := NewClient(conn)
go client.Processor(events)
- conn.inbound <- "UNEXISTENT CMD" // should recieve nothing on this
+ conn.inbound <- "UNEXISTENT CMD" // should receive nothing on this
conn.inbound <- "NICK"
if r := <-conn.outbound; r != ":foohost 431 :No nickname given\r\n" {
t.Fatal("431 for NICK", r)
}
- for _, n := range []string{"привет", " foo", "longlonglong", "#foo", "mein nick", "foo_bar"} {
+ for _, n := range []string{"привет", " foo", "#foo", "mein nick", "foo_bar"} {
conn.inbound <- "NICK " + n
if r := <-conn.outbound; r != ":foohost 432 * "+n+" :Erroneous nickname\r\n" {
t.Fatal("nickname validation", r)
@@ -52,7 +55,7 @@ func TestRegistrationWorkflow(t *testing.T) {
if r := <-conn.outbound; r != ":foohost 461 meinick USER :Not enough parameters\r\n" {
t.Fatal("461 for USER", r)
}
- if (client.nickname != "meinick") || client.registered {
+ if (*client.nickname != "meinick") || client.registered {
t.Fatal("NICK saved")
}
@@ -61,7 +64,7 @@ func TestRegistrationWorkflow(t *testing.T) {
t.Fatal("461 again for USER", r)
}
- daemon.SendLusers(client)
+ SendLusers(client)
if r := <-conn.outbound; !strings.Contains(r, "There are 0 users") {
t.Fatal("LUSERS", r)
}
@@ -85,7 +88,7 @@ func TestRegistrationWorkflow(t *testing.T) {
if r := <-conn.outbound; !strings.Contains(r, ":foohost 422") {
t.Fatal("422 after registration", r)
}
- if (client.username != "1") || (client.realname != "4 5") || !client.registered {
+ if (*client.username != "1") || (*client.realname != "4 5") || !client.registered {
t.Fatal("client register")
}
@@ -96,7 +99,7 @@ func TestRegistrationWorkflow(t *testing.T) {
t.Fatal("reply for unexistent command", r)
}
- daemon.SendLusers(client)
+ SendLusers(client)
if r := <-conn.outbound; !strings.Contains(r, "There are 1 users") {
t.Fatal("1 users logged in", r)
}
@@ -107,10 +110,6 @@ func TestRegistrationWorkflow(t *testing.T) {
}
conn.inbound <- "QUIT\r\nUNEXISTENT CMD"
- <-conn.outbound
- if !conn.closed {
- t.Fatal("closed connection on QUIT")
- }
}
func TestMotd(t *testing.T) {
@@ -123,11 +122,12 @@ func TestMotd(t *testing.T) {
conn := NewTestingConn()
host := "foohost"
- client := NewClient(&host, conn)
+ hostname = &host
+ client := NewClient(conn)
motdName := fd.Name()
- daemon := NewDaemon("ver1", &host, &motdName, nil, nil, nil)
+ motd = &motdName
- daemon.SendMotd(client)
+ SendMotd(client)
if r := <-conn.outbound; !strings.HasPrefix(r, ":foohost 375") {
t.Fatal("MOTD start", r)
}
diff --git a/events.go b/events.go
index bc659a6..9f3673b 100644
--- a/events.go
+++ b/events.go
@@ -34,10 +34,17 @@ const (
EventTopic = iota
EventWho = iota
EventMode = iota
+ EventTerm = iota
+ EventTick = iota
FormatMsg = "[%s] <%s> %s\n"
FormatMeta = "[%s] * %s %s\n"
)
+var (
+ logSink chan LogEvent = make(chan LogEvent)
+ stateSink chan StateEvent = make(chan StateEvent)
+)
+
// Client events going from each of client
// They can be either NEW, DEL or unparsed MSG
type ClientEvent struct {
@@ -70,7 +77,7 @@ func Logger(logdir string, events <-chan LogEvent) {
var fd *os.File
var err error
for event := range events {
- logfile = path.Join(logdir, event.where + ".log")
+ logfile = path.Join(logdir, event.where+".log")
fd, err = os.OpenFile(logfile, mode, perm)
if err != nil {
log.Println("Can not open logfile", logfile, err)
diff --git a/goircd.go b/goircd.go
index 95b463a..8bc5e3e 100644
--- a/goircd.go
+++ b/goircd.go
@@ -37,21 +37,19 @@ var (
logdir = flag.String("logdir", "", "Absolute path to directory for logs")
statedir = flag.String("statedir", "", "Absolute path to directory for states")
passwords = flag.String("passwords", "", "Optional path to passwords file")
-
- tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
- tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file")
-
- verbose = flag.Bool("v", false, "Enable verbose logging.")
+ tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
+ tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file")
+ verbose = flag.Bool("v", false, "Enable verbose logging.")
)
-func listenerLoop(sock net.Listener, events chan<- ClientEvent) {
+func listenerLoop(sock net.Listener, events chan ClientEvent) {
for {
conn, err := sock.Accept()
if err != nil {
log.Println("Error during accepting connection", err)
continue
}
- client := NewClient(hostname, conn)
+ client := NewClient(conn)
go client.Processor(events)
}
}
@@ -60,7 +58,6 @@ func Run() {
events := make(chan ClientEvent)
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
- logSink := make(chan LogEvent)
if *logdir == "" {
// Dummy logger
go func() {
@@ -75,10 +72,7 @@ func Run() {
log.Println(*logdir, "logger initialized")
}
- stateSink := make(chan StateEvent)
- daemon := NewDaemon(version, hostname, motd, passwords, logSink, stateSink)
- daemon.Verbose = *verbose
- log.Println("goircd " + daemon.version + " is starting")
+ log.Println("goircd " + version + " is starting")
if *statedir == "" {
// Dummy statekeeper
go func() {
@@ -98,14 +92,14 @@ func Run() {
if err != nil {
log.Fatalf("Can not read state %s: %v", state, err)
}
- room, _ := daemon.RoomRegister(path.Base(state))
+ room, _ := RoomRegister(path.Base(state))
contents := strings.Split(string(buf), "\n")
if len(contents) < 2 {
- log.Printf("State corrupted for %s: %q", room.name, contents)
+ log.Printf("State corrupted for %s: %q", *room.name, contents)
} else {
- room.topic = contents[0]
- room.key = contents[1]
- log.Println("Loaded state for room", room.name)
+ room.topic = &contents[0]
+ room.key = &contents[1]
+ log.Println("Loaded state for room", *room.name)
}
}
go StateKeeper(*statedir, stateSink)
@@ -133,8 +127,7 @@ func Run() {
log.Println("TLS listening on", *tlsBind)
go listenerLoop(listenerTLS, events)
}
-
- daemon.Processor(events)
+ Processor(events, make(chan struct{}))
}
func main() {
diff --git a/room.go b/room.go
index 4bdb4b2..3da5a3d 100644
--- a/room.go
+++ b/room.go
@@ -28,6 +28,10 @@ import (
var (
RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
+
+ rooms map[string]*Room = make(map[string]*Room)
+
+ roomSinks map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent)
)
// Sanitize room's name. It can consist of 1 to 50 ASCII symbols
@@ -37,40 +41,34 @@ func RoomNameValid(name string) bool {
}
type Room struct {
- Verbose bool
- name string
- topic string
- key string
- members map[*Client]bool
- hostname *string
- logSink chan<- LogEvent
- stateSink chan<- StateEvent
+ name *string
+ topic *string
+ key *string
+ members map[*Client]struct{}
}
func (room Room) String() string {
- return room.name
+ return *room.name
}
-func NewRoom(hostname *string, name string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Room {
- room := Room{name: name}
- room.members = make(map[*Client]bool)
- room.topic = ""
- room.key = ""
- room.hostname = hostname
- room.logSink = logSink
- room.stateSink = stateSink
- return &room
-}
-
-func (room *Room) SendTopic(client *Client) {
- if room.topic == "" {
- client.ReplyNicknamed("331", room.name, "No topic is set")
- } else {
- client.ReplyNicknamed("332", room.name, room.topic)
+func NewRoom(name string) *Room {
+ topic := ""
+ return &Room{
+ name: &name,
+ topic: &topic,
+ members: make(map[*Client]struct{}),
}
}
-// Send message to all room's subscribers, possibly excluding someone
+func (room *Room) SendTopic(client *Client) {
+ if *room.topic == "" {
+ client.ReplyNicknamed("331", *room.name, "No topic is set")
+ } else {
+ client.ReplyNicknamed("332", *room.name, *room.topic)
+ }
+}
+
+// Send message to all room's subscribers, possibly excluding someone.
func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) {
for member := range room.members {
if (len(clientToIgnore) > 0) && member == clientToIgnore[0] {
@@ -81,7 +79,11 @@ func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) {
}
func (room *Room) StateSave() {
- room.stateSink <- StateEvent{room.name, room.topic, room.key}
+ var key string
+ if room.key != nil {
+ key = *room.key
+ }
+ stateSink <- StateEvent{*room.name, *room.topic, key}
}
func (room *Room) Processor(events <-chan ClientEvent) {
@@ -89,46 +91,50 @@ func (room *Room) Processor(events <-chan ClientEvent) {
for event := range events {
client = event.client
switch event.eventType {
+ case EventTerm:
+ roomsGroup.Done()
+ return
case EventNew:
- room.members[client] = true
- if room.Verbose {
+ room.members[client] = struct{}{}
+ if *verbose {
log.Println(client, "joined", room.name)
}
room.SendTopic(client)
- room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, room.name))
- room.logSink <- LogEvent{room.name, client.nickname, "joined", true}
- nicknames := []string{}
+ room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, *room.name))
+ logSink <- LogEvent{*room.name, *client.nickname, "joined", true}
+ nicknames := make([]string, 0)
for member := range room.members {
- nicknames = append(nicknames, member.nickname)
+ nicknames = append(nicknames, *member.nickname)
}
sort.Strings(nicknames)
- client.ReplyNicknamed("353", "=", room.name, strings.Join(nicknames, " "))
- client.ReplyNicknamed("366", room.name, "End of NAMES list")
+ client.ReplyNicknamed("353", "=", *room.name, strings.Join(nicknames, " "))
+ client.ReplyNicknamed("366", *room.name, "End of NAMES list")
case EventDel:
if _, subscribed := room.members[client]; !subscribed {
- client.ReplyNicknamed("442", room.name, "You are not on that channel")
+ client.ReplyNicknamed("442", *room.name, "You are not on that channel")
continue
}
delete(room.members, client)
- msg := fmt.Sprintf(":%s PART %s :%s", client, room.name, client.nickname)
+ msg := fmt.Sprintf(":%s PART %s :%s", client, *room.name, *client.nickname)
room.Broadcast(msg)
- room.logSink <- LogEvent{room.name, client.nickname, "left", true}
+ logSink <- LogEvent{*room.name, *client.nickname, "left", true}
case EventTopic:
if _, subscribed := room.members[client]; !subscribed {
- client.ReplyParts("442", room.name, "You are not on that channel")
+ client.ReplyParts("442", *room.name, "You are not on that channel")
continue
}
if event.text == "" {
- go room.SendTopic(client)
+ room.SendTopic(client)
continue
}
- room.topic = strings.TrimLeft(event.text, ":")
- msg := fmt.Sprintf(":%s TOPIC %s :%s", client, room.name, room.topic)
- go room.Broadcast(msg)
- room.logSink <- LogEvent{
- room.name,
- client.nickname,
- "set topic to " + room.topic,
+ topic := strings.TrimLeft(event.text, ":")
+ room.topic = &topic
+ msg := fmt.Sprintf(":%s TOPIC %s :%s", client, *room.name, *room.topic)
+ room.Broadcast(msg)
+ logSink <- LogEvent{
+ *room.name,
+ *client.nickname,
+ "set topic to " + *room.topic,
true,
}
room.StateSave()
@@ -136,32 +142,32 @@ func (room *Room) Processor(events <-chan ClientEvent) {
for m := range room.members {
client.ReplyNicknamed(
"352",
- room.name,
- m.username,
+ *room.name,
+ *m.username,
m.conn.RemoteAddr().String(),
- *room.hostname,
- m.nickname,
+ *hostname,
+ *m.nickname,
"H",
- "0 "+m.realname,
+ "0 "+*m.realname,
)
}
- client.ReplyNicknamed("315", room.name, "End of /WHO list")
+ client.ReplyNicknamed("315", *room.name, "End of /WHO list")
case EventMode:
if event.text == "" {
mode := "+"
- if room.key != "" {
+ if room.key != nil {
mode = mode + "k"
}
- client.Msg(fmt.Sprintf("324 %s %s %s", client.nickname, room.name, mode))
+ client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, *room.name, mode))
continue
}
if strings.HasPrefix(event.text, "b") {
- client.ReplyNicknamed("368", room.name, "End of channel ban list")
+ client.ReplyNicknamed("368", *room.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.name, "You are not on that channel")
+ client.ReplyParts("442", *room.name, "You are not on that channel")
continue
}
} else {
@@ -176,16 +182,16 @@ func (room *Room) Processor(events <-chan ClientEvent) {
client.ReplyNotEnoughParameters("MODE")
continue
}
- room.key = cols[1]
- msg = fmt.Sprintf(":%s MODE %s +k %s", client, room.name, room.key)
- msgLog = "set channel key to " + room.key
- } else if strings.HasPrefix(event.text, "-k") {
- room.key = ""
- msg = fmt.Sprintf(":%s MODE %s -k", client, room.name)
+ room.key = &cols[1]
+ msg = fmt.Sprintf(":%s MODE %s +k %s", client, *room.name, *room.key)
+ msgLog = "set channel key to " + *room.key
+ } else {
+ room.key = nil
+ msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name)
msgLog = "removed channel key"
}
- go room.Broadcast(msg)
- room.logSink <- LogEvent{room.name, client.nickname, msgLog, true}
+ room.Broadcast(msg)
+ logSink <- LogEvent{*room.name, *client.nickname, msgLog, true}
room.StateSave()
case EventMsg:
sep := strings.Index(event.text, " ")
@@ -193,13 +199,13 @@ func (room *Room) Processor(events <-chan ClientEvent) {
":%s %s %s :%s",
client,
event.text[:sep],
- room.name,
+ *room.name,
event.text[sep+1:]),
client,
)
- room.logSink <- LogEvent{
- room.name,
- client.nickname,
+ logSink <- LogEvent{
+ *room.name,
+ *client.nickname,
event.text[sep+1:],
false,
}
diff --git a/room_test.go b/room_test.go
index d94e8d4..342e58c 100644
--- a/room_test.go
+++ b/room_test.go
@@ -42,17 +42,25 @@ func notEnoughParams(t *testing.T, c *TestingConn) {
}
func TestTwoUsers(t *testing.T) {
- logSink := make(chan LogEvent, 8)
- stateSink := make(chan StateEvent, 8)
+ logSink = make(chan LogEvent, 8)
+ stateSink = make(chan StateEvent, 8)
host := "foohost"
- daemon := NewDaemon("ver1", &host, nil, nil, logSink, stateSink)
+ hostname = &host
events := make(chan ClientEvent)
- go daemon.Processor(events)
+ rooms = make(map[string]*Room)
+ clients = make(map[*Client]struct{})
+ roomSinks = make(map[*Room]chan ClientEvent)
+ finished := make(chan struct{})
+ go Processor(events, finished)
+ defer func() {
+ events <- ClientEvent{eventType: EventTerm}
+ <-finished
+ }()
conn1 := NewTestingConn()
conn2 := NewTestingConn()
- client1 := NewClient(&host, conn1)
- client2 := NewClient(&host, conn2)
+ client1 := NewClient(conn1)
+ client2 := NewClient(conn2)
go client1.Processor(events)
go client2.Processor(events)
@@ -63,7 +71,7 @@ func TestTwoUsers(t *testing.T) {
<-conn2.outbound
}
- daemon.SendLusers(client1)
+ SendLusers(client1)
if r := <-conn1.outbound; !strings.Contains(r, "There are 2 users") {
t.Fatal("LUSERS", r)
}
@@ -105,27 +113,40 @@ func TestTwoUsers(t *testing.T) {
conn1.inbound <- "PRIVMSG nick2 :Hello"
conn1.inbound <- "PRIVMSG #foo :world"
conn1.inbound <- "NOTICE #foo :world"
- <-conn2.outbound
- if r := <-conn2.outbound; r != ":nick1!foo1@someclient PRIVMSG nick2 :Hello\r\n" {
- t.Fatal("first message", r)
+ m1 := <-conn2.outbound
+ m2 := <-conn2.outbound
+ mNeeded := ":nick1!foo1@someclient PRIVMSG nick2 :Hello\r\n"
+ if !(m1 == mNeeded || m2 == mNeeded) {
+ t.Fatal("first message", m1, m2)
}
- if r := <-conn2.outbound; r != ":nick1!foo1@someclient PRIVMSG #foo :world\r\n" {
- t.Fatal("second message", r)
+ if m2 == mNeeded {
+ m2 = <-conn2.outbound
}
- if r := <-conn2.outbound; r != ":nick1!foo1@someclient NOTICE #foo :world\r\n" {
- t.Fatal("third message", r)
+ if m2 != ":nick1!foo1@someclient PRIVMSG #foo :world\r\n" {
+ t.Fatal("second message", m2)
+ }
+ if m2 = <-conn2.outbound; m2 != ":nick1!foo1@someclient NOTICE #foo :world\r\n" {
+ t.Fatal("third message", m2)
}
}
func TestJoin(t *testing.T) {
- logSink := make(chan LogEvent, 8)
- stateSink := make(chan StateEvent, 8)
+ logSink = make(chan LogEvent, 8)
+ stateSink = make(chan StateEvent, 8)
host := "foohost"
- daemon := NewDaemon("ver1", &host, nil, nil, logSink, stateSink)
+ hostname = &host
events := make(chan ClientEvent)
- go daemon.Processor(events)
+ rooms = make(map[string]*Room)
+ clients = make(map[*Client]struct{})
+ roomSinks = make(map[*Room]chan ClientEvent)
+ finished := make(chan struct{})
+ go Processor(events, finished)
+ defer func() {
+ events <- ClientEvent{eventType: EventTerm}
+ <-finished
+ }()
conn := NewTestingConn()
- client := NewClient(&host, conn)
+ client := NewClient(conn)
go client.Processor(events)
conn.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2"
@@ -161,10 +182,10 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ {
<-conn.outbound
}
- if _, ok := daemon.rooms["#bar"]; !ok {
+ if _, ok := rooms["#bar"]; !ok {
t.Fatal("#bar does not exist")
}
- if _, ok := daemon.rooms["#baz"]; !ok {
+ if _, ok := rooms["#baz"]; !ok {
t.Fatal("#baz does not exist")
}
if r := <-logSink; (r.what != "joined") || (r.where != "#bar") || (r.who != "nick2") || (r.meta != true) {
@@ -178,10 +199,10 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ {
<-conn.outbound
}
- if daemon.rooms["#barenc"].key != "key1" {
+ if *rooms["#barenc"].key != "key1" {
t.Fatal("no room with key1")
}
- if daemon.rooms["#bazenc"].key != "key2" {
+ if *rooms["#bazenc"].key != "key2" {
t.Fatal("no room with key2")
}
if r := <-logSink; (r.what != "joined") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
@@ -201,7 +222,7 @@ func TestJoin(t *testing.T) {
if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc -k\r\n" {
t.Fatal("remove #barenc key", r)
}
- if daemon.rooms["#barenc"].key != "" {
+ if rooms["#barenc"].key != nil {
t.Fatal("removing key from #barenc")
}
if r := <-logSink; (r.what != "removed channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
@@ -253,5 +274,4 @@ func TestJoin(t *testing.T) {
if r := <-conn.outbound; r != ":foohost 315 nick2 #barenc :End of /WHO list\r\n" {
t.Fatal("end of WHO", r)
}
-
}