diff --git a/ergonomadic.go b/ergonomadic.go index ab968579..698865c3 100644 --- a/ergonomadic.go +++ b/ergonomadic.go @@ -1,5 +1,4 @@ package main -// http://tools.ietf.org/html/rfc2812 import ( "irc" diff --git a/src/irc/client.go b/src/irc/client.go index 490d95f9..75b2e729 100644 --- a/src/irc/client.go +++ b/src/irc/client.go @@ -1,21 +1,13 @@ package irc import ( - "log" "net" - "strings" ) -type Message struct { - command string - args string - client *Client -} - type Client struct { addr net.Addr - send chan string - recv chan string + send chan<- string + recv <-chan string username string realname string nick string @@ -31,19 +23,12 @@ func NewClient(conn net.Conn) *Client { } // Adapt `chan string` to a `chan Message`. -func (c *Client) Communicate(server *Server) { - go func() { - for str := range c.recv { - parts := strings.SplitN(str, " ", 2) - server.Send(Message{parts[0], parts[1], c}) +func (c *Client) Communicate(server chan<- *ClientMessage) { + for str := range c.recv { + m := ParseMessage(str) + if m != nil { + server <- &ClientMessage{c, m} } - }() -} - -func (c *Client) Send(lines ...string) { - for _, line := range lines { - log.Printf("C <- S: %s", line) - c.send <- line } } diff --git a/src/irc/commands.go b/src/irc/commands.go new file mode 100644 index 00000000..2a222a1a --- /dev/null +++ b/src/irc/commands.go @@ -0,0 +1,158 @@ +package irc + +import ( + "strconv" + "strings" +) + +type Message interface { + Handle(s *Server, c *Client) +} + +type NewMessageFunc func([]string) Message + +type NickMessage struct { + nickname string +} + +func NewNickMessage(args []string) Message { + if len(args) != 1 { + return nil + } + return &NickMessage{args[0]} +} + +func (m *NickMessage) Handle(s *Server, c *Client) { + if s.nicks[m.nickname] != nil { + c.send <- ErrNickNameInUse(m.nickname) + return + } + if c.nick != "" { + delete(s.nicks, c.nick) + } + c.nick = m.nickname + s.nicks[c.nick] = c + tryRegister(s, c) +} + +type UserMessage struct { + user string + mode uint8 + unused string + realname string +} + +func NewUserMessage(args []string) Message { + if len(args) != 4 { + return nil + } + msg := new(UserMessage) + msg.user = args[0] + mode, err := strconv.ParseUint(args[1], 10, 8) + if err == nil { + msg.mode = uint8(mode) + } + msg.unused = args[2] + msg.realname = args[3] + return msg +} + +func (m *UserMessage) Handle(s *Server, c *Client) { + if c.username != "" { + c.send <- ErrAlreadyRegistered(c.Nick()) + return + } + c.username, c.realname = m.user, m.realname + tryRegister(s, c) +} + +type QuitMessage struct { + message string +} + +func NewQuitMessage(args []string) Message { + msg := QuitMessage{} + if len(args) > 0 { + msg.message = args[0] + } + return &msg +} + +func (m *QuitMessage) Handle(s *Server, c *Client) { + c.send <- MessageError() + delete(s.nicks, c.nick) +} + +type UnknownMessage struct { + command string +} + +func (m *UnknownMessage) Handle(s *Server, c *Client) { + c.send <- ErrUnknownCommand(c.Nick(), m.command) +} + +type PingMessage struct {} + +func NewPingMessage(args []string) Message { + return &PingMessage{} +} + +func (m *PingMessage) Handle(s *Server, c *Client) { + c.send <- MessagePong() +} + +func tryRegister(s *Server, c *Client) { + if (!c.registered && c.nick != "" && c.username != "") { + c.registered = true + c.send <- ReplyWelcome(c.Nick(), c.username, "localhost") + c.send <- ReplyYourHost(c.Nick(), "irc.jlatt.com") + c.send <- ReplyCreated(c.Nick(), "2012/04/07") + c.send <- ReplyMyInfo(c.Nick(), "irc.jlatt.com") + } +} + +func parseArg(line string) (string, string) { + if line == "" { + return "", "" + } + + if strings.HasPrefix(line, ":") { + return line[1:], "" + } + + parts := strings.SplitN(line, " ", 2) + arg := parts[0] + rest := "" + if len(parts) > 1 { + rest = parts[1] + } + return arg, rest +} + +func parseLine(line string) (string, []string) { + args := make([]string, 0) + for arg, rest := parseArg(line); arg != ""; arg, rest = parseArg(rest) { + args = append(args, arg) + } + return args[0], args[1:] +} + +var commands = map[string]NewMessageFunc { + "NICK": NewNickMessage, + "PING": NewPingMessage, + "QUIT": NewQuitMessage, + "USER": NewUserMessage, +} + +func ParseMessage(line string) Message { + command, args := parseLine(line) + constructor := commands[command] + var msg Message + if constructor != nil { + msg = constructor(args) + } + if msg == nil { + msg = &UnknownMessage{command} + } + return msg +} diff --git a/src/irc/net.go b/src/irc/net.go index 360dcd02..0bfc1bdd 100644 --- a/src/irc/net.go +++ b/src/irc/net.go @@ -7,18 +7,23 @@ import ( "net" ) +func readTrimmedLine(reader *bufio.Reader) (string, error) { + line, err := reader.ReadString('\n') + return strings.TrimSpace(line), err +} + // Adapt `net.Conn` to a `chan string`. -func StringReadChan(conn net.Conn) chan string { +func StringReadChan(conn net.Conn) <-chan string { ch := make(chan string) reader := bufio.NewReader(conn) go func() { for { - line, err := reader.ReadString('\n') + line, err := readTrimmedLine(reader) if (line != "") { - ch <- strings.TrimSpace(line) + ch <- line + log.Printf("%s -> %s", conn.RemoteAddr(), line) } if err != nil { - log.Print("StringReadChan[read]: ", err) break } } @@ -27,16 +32,16 @@ func StringReadChan(conn net.Conn) chan string { return ch } -func StringWriteChan(conn net.Conn) chan string { +func StringWriteChan(conn net.Conn) chan<- string { ch := make(chan string) writer := bufio.NewWriter(conn) go func() { for str := range ch { if _, err := writer.WriteString(str + "\r\n"); err != nil { - log.Print("StringWriteChan[write]: ", err) break } writer.Flush() + log.Printf("%s <- %s", conn.RemoteAddr(), str) } close(ch) }() diff --git a/src/irc/protocol.go b/src/irc/protocol.go index f32e7d3c..894ba02c 100644 --- a/src/irc/protocol.go +++ b/src/irc/protocol.go @@ -1,3 +1,4 @@ +// http://tools.ietf.org/html/rfc2812 package irc import ( diff --git a/src/irc/server.go b/src/irc/server.go index 008fbfe1..6188ce0d 100644 --- a/src/irc/server.go +++ b/src/irc/server.go @@ -3,19 +3,24 @@ package irc import ( "log" "net" - "regexp" - "strings" ) type Server struct { - ch chan Message + ch chan *ClientMessage nicks map[string]*Client } +type ClientMessage struct { + client *Client + message Message +} + func NewServer() *Server { - server := Server{make(chan Message), make(map[string]*Client)} + server := new(Server) + server.ch = make(chan *ClientMessage) + server.nicks = make(map[string]*Client) go server.Receive() - return &server + return server } func (s *Server) Listen(addr string) { @@ -30,71 +35,12 @@ func (s *Server) Listen(addr string) { log.Print("Server.Listen: ", err) continue } - go NewClient(conn).Communicate(s) + go NewClient(conn).Communicate(s.ch) } } func (s *Server) Receive() { - for message := range s.ch { - log.Printf("C -> S: %s %s", message.command, message.args) - switch message.command { - case "PING": - message.client.Send(MessagePong()) - case "USER": - s.UserCommand(message.client, message.args) - case "NICK": - s.NickCommand(message.client, message.args) - case "QUIT": - s.QuitCommand(message.client, message.args) - default: - message.client.Send(ErrUnknownCommand(message.client.Nick(), message.command)) - } + for m := range s.ch { + m.message.Handle(s, m.client) } } - -func (s *Server) Send(m Message) { - s.ch <- m -} - -// commands - -func (s *Server) UserCommand(c *Client, args string) { - parts := strings.SplitN(args, " ", 4) - username, _, _, realname := parts[0], parts[1], parts[2], parts[3] - if c.username != "" { - c.Send(ErrAlreadyRegistered(c.nick)) - return - } - c.username, c.realname = username, realname - s.TryRegister(c) -} - -func (s *Server) NickCommand(c *Client, nick string) { - if s.nicks[nick] != nil { - c.Send(ErrNickNameInUse(nick)) - return - } - c.nick = nick - s.nicks[nick] = c - s.TryRegister(c) -} - -func (s *Server) TryRegister(c *Client) { - if (!c.registered && c.nick != "" && c.username != "") { - c.registered = true - c.Send( - ReplyWelcome(c.Nick(), c.username, "localhost"), - ReplyYourHost(c.Nick(), "irc.jlatt.com"), - ReplyCreated(c.Nick(), "2012/04/07"), - ReplyMyInfo(c.Nick(), "irc.jlatt.com")) - } -} - -func (s *Server) QuitCommand(c *Client, args string) { - re := regexp.MustCompile("^" + RE_QUIT + "$") - matches := re.FindAllStringSubmatch(args, -1) - if matches != nil { - c.Send(MessageError()) - } - delete(s.nicks, c.nick) -}