diff --git a/README.md b/README.md index 6b9fd07b..6bef509f 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,9 @@ I wanted to learn Go. ### What's with the name? "Ergonomadic" is an anagram of "Go IRC Daemon". + +### Helpful Documentation + +- [IRC Channel Management](http://tools.ietf.org/html/rfc2811) +- [IRC Client Protocol](http://tools.ietf.org/html/rfc2812) +- [IRC Server Protocol](http://tools.ietf.org/html/rfc2813) diff --git a/src/irc/channel.go b/src/irc/channel.go index 124cb2dd..2b6c3657 100644 --- a/src/irc/channel.go +++ b/src/irc/channel.go @@ -1,5 +1,9 @@ package irc +import ( + "sort" +) + type Channel struct { name string key string @@ -12,10 +16,19 @@ type Channel struct { type ChannelSet map[*Channel]bool +// NewChannel creates a new channel from a `Server` and a `name` string, which +// must be unique on the server. func NewChannel(s *Server, name string) *Channel { - return &Channel{name: name, members: make(ClientSet), invites: make(map[string]bool), server: s} + return &Channel{ + name: name, + members: make(ClientSet), + invites: make(map[string]bool), + server: s, + } } +// Send a `Reply` to all `Client`s of the `Channel`. Skip `fromClient`, if it is +// provided. func (ch *Channel) Send(reply Reply, fromClient *Client) { for client := range ch.members { if client != fromClient { @@ -24,7 +37,20 @@ func (ch *Channel) Send(reply Reply, fromClient *Client) { } } +func (ch *Channel) Nicks() []string { + nicks := make([]string, len(ch.members)) + i := 0 + for member := range ch.members { + nicks[i] = member.Nick() + i++ + } + sort.Strings(nicks) + return nicks +} + +// // channel functionality +// func (ch *Channel) Join(cl *Client, key string) { if ch.key != key { @@ -42,10 +68,7 @@ func (ch *Channel) Join(cl *Client, key string) { ch.Send(RplJoin(ch, cl), nil) ch.GetTopic(cl) - - for member := range ch.members { - cl.send <- RplNamReply(ch, member) - } + cl.send <- RplNamReply(ch) cl.send <- RplEndOfNames(ch.server) } diff --git a/src/irc/client.go b/src/irc/client.go index 250c6b5a..3dee9c91 100644 --- a/src/irc/client.go +++ b/src/irc/client.go @@ -16,12 +16,19 @@ type Client struct { registered bool invisible bool channels ChannelSet + server *Server } type ClientSet map[*Client]bool -func NewClient(conn net.Conn) *Client { - client := &Client{conn: conn, recv: StringReadChan(conn), channels: make(ChannelSet), hostname: LookupHostname(conn.RemoteAddr())} +func NewClient(server *Server, conn net.Conn) *Client { + client := &Client{ + channels: make(ChannelSet), + conn: conn, + hostname: LookupHostname(conn.RemoteAddr()), + recv: StringReadChan(conn), + server: server, + } client.SetReplyToStringChan() return client } @@ -38,12 +45,14 @@ func (c *Client) SetReplyToStringChan() { } // Adapt `chan string` to a `chan Message`. -func (c *Client) Communicate(server *Server) { +func (c *Client) Communicate() { for str := range c.recv { - m := ParseMessage(str) - if m != nil { - server.recv <- &ClientMessage{c, m} + m, err := ParseMessage(str) + if err != nil { + // TODO handle error + return } + c.server.recv <- &ClientMessage{c, m} } } diff --git a/src/irc/commands.go b/src/irc/commands.go index 6039a8fe..de6f1195 100644 --- a/src/irc/commands.go +++ b/src/irc/commands.go @@ -1,15 +1,30 @@ package irc +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + type Message interface { Handle(s *Server, c *Client) } +var ( + ErrNotEnoughArgs = errors.New("not enough arguments") + ErrUModeUnknownFlag = errors.New("unknown umode flag") +) + // unknown type UnknownMessage struct { command string } +// NB: no constructor, created on demand in parser for invalid messages. + func (m *UnknownMessage) Handle(s *Server, c *Client) { c.send <- ErrUnknownCommand(s, m.command) } @@ -21,6 +36,17 @@ type PingMessage struct { server2 string } +func NewPingMessage(args []string) (Message, error) { + if len(args) < 1 { + return nil, ErrNotEnoughArgs + } + msg := &PingMessage{server: args[0]} + if len(args) > 1 { + msg.server2 = args[1] + } + return msg, nil +} + func (m *PingMessage) Handle(s *Server, c *Client) { c.send <- RplPong(s) } @@ -32,8 +58,19 @@ type PongMessage struct { server2 string } +func NewPongMessage(args []string) (Message, error) { + if len(args) < 1 { + return nil, ErrNotEnoughArgs + } + message := &PongMessage{server1: args[0]} + if len(args) > 1 { + message.server2 = args[1] + } + return message, nil +} + func (m *PongMessage) Handle(s *Server, c *Client) { - // TODO update client atime + // no-op } // NICK @@ -42,6 +79,13 @@ type NickMessage struct { nickname string } +func NewNickMessage(args []string) (Message, error) { + if len(args) != 1 { + return nil, ErrNotEnoughArgs + } + return &NickMessage{args[0]}, nil +} + func (m *NickMessage) Handle(s *Server, c *Client) { s.ChangeNick(c, m.nickname) } @@ -55,8 +99,24 @@ type UserMessage struct { realname string } +func NewUserMessage(args []string) (Message, error) { + if len(args) != 4 { + return nil, ErrNotEnoughArgs + } + msg := &UserMessage{ + user: args[0], + unused: args[2], + realname: args[3], + } + mode, err := strconv.ParseUint(args[1], 10, 8) + if err == nil { + msg.mode = uint8(mode) + } + return msg, nil +} + func (m *UserMessage) Handle(s *Server, c *Client) { - s.Register(c, m.user, m.realname) + s.UserLogin(c, m.user, m.realname) } // QUIT @@ -65,6 +125,14 @@ type QuitMessage struct { message string } +func NewQuitMessage(args []string) (Message, error) { + msg := &QuitMessage{} + if len(args) > 0 { + msg.message = args[0] + } + return msg, nil +} + func (m *QuitMessage) Handle(s *Server, c *Client) { s.Quit(c, m.message) } @@ -76,6 +144,28 @@ type ModeMessage struct { modes []string } +var MODE_RE = regexp.MustCompile("^[-+][a-zA-Z]+$") + +func NewModeMessage(args []string) (Message, error) { + if len(args) < 1 { + return nil, ErrNotEnoughArgs + } + msg := &ModeMessage{ + nickname: args[0], + } + for _, arg := range args[1:] { + if !MODE_RE.MatchString(arg) { + return nil, ErrUModeUnknownFlag + } + prefix := arg[0] + for _, c := range arg[1:] { + mode := fmt.Sprintf("%c%c", prefix, c) + msg.modes = append(msg.modes, mode) + } + } + return msg, nil +} + func (m *ModeMessage) Handle(s *Server, c *Client) { if m.nickname != c.nick { c.send <- ErrUsersDontMatch(s) @@ -92,6 +182,22 @@ type JoinMessage struct { zero bool } +func NewJoinMessage(args []string) (Message, error) { + msg := &JoinMessage{} + if len(args) > 0 { + if args[0] == "0" { + msg.zero = true + } else { + msg.channels = strings.Split(args[0], ",") + } + + if len(args) > 1 { + msg.keys = strings.Split(args[1], ",") + } + } + return msg, nil +} + func (m *JoinMessage) Handle(s *Server, c *Client) { if m.zero { for channel := range c.channels { @@ -116,6 +222,17 @@ type PartMessage struct { message string } +func NewPartMessage(args []string) (Message, error) { + if len(args) < 1 { + return nil, ErrNotEnoughArgs + } + msg := &PartMessage{channels: strings.Split(args[0], ",")} + if len(args) > 1 { + msg.message = args[1] + } + return msg, nil +} + func (m *PartMessage) Handle(s *Server, c *Client) { for _, chname := range m.channels { channel := s.channels[chname] @@ -136,6 +253,16 @@ type PrivMsgMessage struct { message string } +func NewPrivMsgMessage(args []string) (Message, error) { + if len(args) < 2 { + return nil, ErrNotEnoughArgs + } + return &PrivMsgMessage{ + target: args[0], + message: args[1], + }, nil +} + func (m *PrivMsgMessage) TargetIsChannel() bool { switch m.target[0] { case '&', '#', '+', '!': @@ -169,6 +296,17 @@ type TopicMessage struct { topic string } +func NewTopicMessage(args []string) (Message, error) { + if len(args) < 1 { + return nil, ErrNotEnoughArgs + } + msg := &TopicMessage{channel: args[0]} + if len(args) > 1 { + msg.topic = args[1] + } + return msg, nil +} + func (m *TopicMessage) Handle(s *Server, c *Client) { channel := s.channels[m.channel] if channel == nil { diff --git a/src/irc/constants.go b/src/irc/constants.go index a0ce3bf7..4826d036 100644 --- a/src/irc/constants.go +++ b/src/irc/constants.go @@ -1,6 +1,3 @@ -// channel management: http://tools.ietf.org/html/rfc2811 -// client protocol: http://tools.ietf.org/html/rfc2812 -// server protocol: http://tools.ietf.org/html/rfc2813 package irc const ( @@ -8,16 +5,19 @@ const ( ) const ( - RPL_WELCOME = "001" - RPL_YOURHOST = "002" - RPL_CREATED = "003" - RPL_MYINFO = "004" - RPL_UMODEIS = "221" - RPL_NOTOPIC = "331" - RPL_TOPIC = "332" - RPL_NAMREPLY = "353" - RPL_ENDOFNAMES = "366" - RPL_INFO = "371" + // # numeric codes + // ## reply codes + RPL_WELCOME = "001" + RPL_YOURHOST = "002" + RPL_CREATED = "003" + RPL_MYINFO = "004" + RPL_UMODEIS = "221" + RPL_NOTOPIC = "331" + RPL_TOPIC = "332" + RPL_NAMREPLY = "353" + RPL_ENDOFNAMES = "366" + RPL_INFO = "371" + // ## error codes ERR_NOSUCHNICK = "401" ERR_NOSUCHSERVER = "402" ERR_NOSUCHCHANNEL = "403" @@ -29,9 +29,10 @@ const ( ERR_INVITEONLYCHANNEL = "473" ERR_BADCHANNELKEY = "475" ERR_USERSDONTMATCH = "502" - RPL_JOIN = "JOIN" - RPL_NICK = "NICK" - RPL_PART = "PART" - RPL_PONG = "PONG" - RPL_PRIVMSG = "PRIVMSG" + // # message codes + RPL_JOIN = "JOIN" + RPL_NICK = "NICK" + RPL_PART = "PART" + RPL_PONG = "PONG" + RPL_PRIVMSG = "PRIVMSG" ) diff --git a/src/irc/parse.go b/src/irc/parse.go index 3c85f184..4a302e09 100644 --- a/src/irc/parse.go +++ b/src/irc/parse.go @@ -1,13 +1,11 @@ package irc import ( - "fmt" - "regexp" - "strconv" + "errors" "strings" ) -var commands = map[string]func([]string) Message{ +var commands = map[string]func([]string) (Message, error){ "JOIN": NewJoinMessage, "MODE": NewModeMessage, "NICK": NewNickMessage, @@ -20,169 +18,43 @@ var commands = map[string]func([]string) Message{ "USER": NewUserMessage, } -func ParseMessage(line string) Message { +var ( + ErrParseMessage = errors.New("failed to parse message") +) + +func ParseMessage(line string) (msg Message, err error) { command, args := parseLine(line) constructor, ok := commands[command] - var msg Message - if ok { - msg = constructor(args) - } - if msg == nil { + if !ok { msg = &UnknownMessage{command} + return } - return msg + msg, err = constructor(args) + return } -func parseArg(line string) (string, string) { +func parseArg(line string) (arg string, rest string) { if line == "" { - return "", "" + return } if strings.HasPrefix(line, ":") { - return line[1:], "" + arg = line[1:] + } else { + parts := strings.SplitN(line, " ", 2) + arg = parts[0] + if len(parts) > 1 { + rest = parts[1] + } } - - parts := strings.SplitN(line, " ", 2) - arg := parts[0] - rest := "" - if len(parts) > 1 { - rest = parts[1] - } - return arg, rest + return } -func parseLine(line string) (string, []string) { - args := make([]string, 0) +func parseLine(line string) (command string, args []string) { + args = make([]string, 0) for arg, rest := parseArg(line); arg != ""; arg, rest = parseArg(rest) { args = append(args, arg) } - return args[0], args[1:] -} - -// []string => Message constructors - -func NewNickMessage(args []string) Message { - if len(args) != 1 { - return nil - } - return &NickMessage{args[0]} -} - -func NewPingMessage(args []string) Message { - if len(args) < 1 { - return nil - } - message := &PingMessage{server: args[0]} - if len(args) > 1 { - message.server2 = args[1] - } - return message -} - -func NewPongMessage(args []string) Message { - if len(args) < 1 { - return nil - } - message := &PongMessage{server1: args[0]} - if len(args) > 1 { - message.server2 = args[1] - } - return message -} - -func NewQuitMessage(args []string) Message { - msg := QuitMessage{} - if len(args) > 0 { - msg.message = args[0] - } - return &msg -} - -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 -} - -var MODE_RE = regexp.MustCompile("^[-+][a-zA-Z]+$") - -func NewModeMessage(args []string) Message { - if len(args) < 1 { - return nil - } - msg := new(ModeMessage) - msg.nickname = args[0] - for _, arg := range args[1:] { - if !MODE_RE.MatchString(arg) { - // TODO invalid args - return nil - } - prefix := arg[0] - for _, c := range arg[1:] { - mode := fmt.Sprintf("%c%c", prefix, c) - msg.modes = append(msg.modes, mode) - } - } - return msg -} - -func NewJoinMessage(args []string) Message { - msg := new(JoinMessage) - - if len(args) > 0 { - if args[0] == "0" { - msg.zero = true - } else { - msg.channels = strings.Split(args[0], ",") - } - - if len(args) > 1 { - msg.keys = strings.Split(args[1], ",") - } - } - - return msg -} - -func NewPartMessage(args []string) Message { - if len(args) < 1 { - return nil - } - msg := new(PartMessage) - msg.channels = strings.Split(args[0], ",") - - if len(args) > 1 { - msg.message = args[1] - } - - return msg -} - -func NewPrivMsgMessage(args []string) Message { - if len(args) < 2 { - return nil - } - - return &PrivMsgMessage{target: args[0], message: args[1]} -} - -func NewTopicMessage(args []string) Message { - if len(args) < 1 { - return nil - } - - message := &TopicMessage{channel: args[0]} - if len(args) > 1 { - message.topic = args[1] - } - return message + command, args = args[0], args[1:] + return } diff --git a/src/irc/reply.go b/src/irc/reply.go index b4ee85bc..5a553ae3 100644 --- a/src/irc/reply.go +++ b/src/irc/reply.go @@ -2,6 +2,7 @@ package irc import ( "fmt" + "strings" "time" ) @@ -75,7 +76,7 @@ func RplCreated(server *Server) Reply { } func RplMyInfo(server *Server) Reply { - return NewReply(server, RPL_MYINFO, server.name+" i ik") + return NewReply(server, RPL_MYINFO, fmt.Sprintf("%s %s i ik", server.name, VERSION)) } func RplUModeIs(server *Server, client *Client) Reply { @@ -92,9 +93,9 @@ func RplTopic(channel *Channel) Reply { return &ChannelReply{NewReply(channel.server, RPL_TOPIC, fmt.Sprintf("%s :%s", channel.name, channel.topic)), channel} } -func RplNamReply(channel *Channel, client *Client) Reply { +func RplNamReply(channel *Channel) Reply { // TODO multiple names and splitting based on message size - return NewReply(channel.server, RPL_NAMREPLY, fmt.Sprintf("=%s :+%s", channel.name, client.Nick())) + return NewReply(channel.server, RPL_NAMREPLY, fmt.Sprintf("= %s :%s", channel.name, strings.Join(channel.Nicks(), " "))) } func RplEndOfNames(source Identifier) Reply { diff --git a/src/irc/server.go b/src/irc/server.go index 8c9cdd31..1cd7deab 100644 --- a/src/irc/server.go +++ b/src/irc/server.go @@ -22,7 +22,13 @@ type ClientMessage struct { func NewServer(name string) *Server { recv := make(chan *ClientMessage) - server := &Server{ctime: time.Now(), name: name, recv: recv, nicks: make(map[string]*Client), channels: make(map[string]*Channel)} + server := &Server{ + ctime: time.Now(), + name: name, + recv: recv, + nicks: make(map[string]*Client), + channels: make(map[string]*Channel), + } go func() { for m := range recv { m.message.Handle(server, m.client) @@ -47,7 +53,7 @@ func (s *Server) Listen(addr string) { continue } log.Print("Server.Listen: accepted ", conn.RemoteAddr()) - go NewClient(conn).Communicate(s) + go NewClient(s, conn).Communicate() } } @@ -65,6 +71,7 @@ func (s *Server) GetOrMakeChannel(name string) *Channel { // Send a message to clients of channels fromClient is a member. func (s *Server) SendToInterestedClients(fromClient *Client, reply Reply) { clients := make(map[*Client]bool) + clients[fromClient] = true for channel := range fromClient.channels { for client := range channel.members { clients[client] = true @@ -84,28 +91,29 @@ func (s *Server) ChangeNick(c *Client, newNick string) { return } - s.SendToInterestedClients(c, RplNick(c, newNick)) - if c.nick != "" { delete(s.nicks, c.nick) } - c.nick = newNick s.nicks[c.nick] = c - s.TryRegister(c) + s.SendToInterestedClients(c, RplNick(c, newNick)) + + c.nick = newNick + + s.tryRegister(c) } -func (s *Server) Register(c *Client, user string, realName string) { +func (s *Server) UserLogin(c *Client, user string, realName string) { if c.username != "" { c.send <- ErrAlreadyRegistered(s) return } c.username, c.realname = user, realName - s.TryRegister(c) + s.tryRegister(c) } -func (s *Server) TryRegister(c *Client) { +func (s *Server) tryRegister(c *Client) { if !c.registered && c.HasNick() && c.HasUser() { c.registered = true c.send <- RplWelcome(s, c)