diff --git a/build.sh b/build.sh index 11e0b9d2..f03e1bb9 100755 --- a/build.sh +++ b/build.sh @@ -1,2 +1,2 @@ #!/bin/bash -env GOPATH="$PWD" go install ergonomadic +env GOPATH="$PWD" go install -v ergonomadic diff --git a/src/irc/channel.go b/src/irc/channel.go index f921c19d..83037c18 100644 --- a/src/irc/channel.go +++ b/src/irc/channel.go @@ -79,6 +79,10 @@ func (ch *Channel) Part(cl *Client, message string) { delete(ch.members, cl) delete(cl.channels, ch) + + if len(ch.members) == 0 { + ch.server.DeleteChannel(ch) + } } func (ch *Channel) PrivMsg(cl *Client, message string) { diff --git a/src/irc/client.go b/src/irc/client.go index 993a19e4..0a5b22e3 100644 --- a/src/irc/client.go +++ b/src/irc/client.go @@ -10,7 +10,6 @@ import ( type Client struct { conn net.Conn send chan<- Reply - recv <-chan string username string realname string hostname string @@ -27,39 +26,47 @@ type Client struct { type ClientSet map[*Client]bool func NewClient(server *Server, conn net.Conn) *Client { + read := StringReadChan(conn) + write := StringWriteChan(conn) + send := make(chan Reply) + client := &Client{ channels: make(ChannelSet), conn: conn, hostname: LookupHostname(conn.RemoteAddr()), - recv: StringReadChan(conn), server: server, + send: send, } - client.SetReplyToStringChan() + + // Connect the conn to the server. + go client.readConn(read) + + // Connect the reply channel to the conn. + go client.writeConn(write, send) + return client } -func (c *Client) SetReplyToStringChan() { - send := make(chan Reply) - write := StringWriteChan(c.conn) - go func() { - for reply := range send { - replyStr := reply.String(c) - log.Printf("%s <- %s", c.Id(), replyStr) - write <- replyStr - } - }() - c.send = send -} +func (c *Client) readConn(recv <-chan string) { + for str := range recv { + log.Printf("%s > %s", c.Id(), str) -// Adapt `chan string` to a `chan Message`. -func (c *Client) Communicate() { - for str := range c.recv { m, err := ParseMessage(str) if err != nil { // TODO handle error - return + continue } - c.server.recv <- &ClientMessage{c, m} + + m.SetClient(c) + c.server.recv <- m + } +} + +func (c *Client) writeConn(write chan<- string, send <-chan Reply) { + for reply := range send { + replyStr := reply.String(c) + log.Printf("%s < %s", c.Id(), replyStr) + write <- replyStr } } diff --git a/src/irc/commands.go b/src/irc/commands.go index d6ac5be1..b1340a6b 100644 --- a/src/irc/commands.go +++ b/src/irc/commands.go @@ -10,6 +10,8 @@ import ( type Message interface { Handle(s *Server, c *Client) + Client() *Client + SetClient(c *Client) } var ( @@ -17,9 +19,22 @@ var ( UModeUnknownFlagError = errors.New("unknown umode flag") ) -// unknown +type BaseMessage struct { + client *Client +} + +func (m *BaseMessage) Client() *Client { + return m.client +} + +func (m *BaseMessage) SetClient(c *Client) { + m.client = c +} + +// unknown [args...] type UnknownMessage struct { + *BaseMessage command string args []string } @@ -30,9 +45,10 @@ func (m *UnknownMessage) Handle(s *Server, c *Client) { c.send <- ErrUnknownCommand(s, m.command) } -// PING +// PING [ ] type PingMessage struct { + *BaseMessage server string server2 string } @@ -41,7 +57,10 @@ func NewPingMessage(args []string) (Message, error) { if len(args) < 1 { return nil, NotEnoughArgsError } - msg := &PingMessage{server: args[0]} + msg := &PingMessage{ + BaseMessage: &BaseMessage{}, + server: args[0], + } if len(args) > 1 { msg.server2 = args[1] } @@ -52,9 +71,10 @@ func (m *PingMessage) Handle(s *Server, c *Client) { c.send <- RplPong(s) } -// PONG +// PONG [ ] type PongMessage struct { + *BaseMessage server1 string server2 string } @@ -77,6 +97,7 @@ func (m *PongMessage) Handle(s *Server, c *Client) { // PASS type PassMessage struct { + *BaseMessage password string } @@ -85,7 +106,8 @@ func NewPassMessage(args []string) (Message, error) { return nil, NotEnoughArgsError } return &PassMessage{ - password: args[0], + BaseMessage: &BaseMessage{}, + password: args[0], }, nil } @@ -97,9 +119,10 @@ func (m *PassMessage) Handle(s *Server, c *Client) { } } -// NICK +// NICK type NickMessage struct { + *BaseMessage nickname string } @@ -107,16 +130,20 @@ func NewNickMessage(args []string) (Message, error) { if len(args) != 1 { return nil, NotEnoughArgsError } - return &NickMessage{args[0]}, nil + return &NickMessage{ + BaseMessage: &BaseMessage{}, + nickname: args[0], + }, nil } func (m *NickMessage) Handle(s *Server, c *Client) { s.ChangeNick(c, m.nickname) } -// USER +// USER type UserMessage struct { + *BaseMessage user string mode uint8 unused string @@ -128,9 +155,10 @@ func NewUserMessage(args []string) (Message, error) { return nil, NotEnoughArgsError } msg := &UserMessage{ - user: args[0], - unused: args[2], - realname: args[3], + BaseMessage: &BaseMessage{}, + user: args[0], + unused: args[2], + realname: args[3], } mode, err := strconv.ParseUint(args[1], 10, 8) if err == nil { @@ -146,11 +174,14 @@ func (m *UserMessage) Handle(s *Server, c *Client) { // QUIT [ ] type QuitMessage struct { + *BaseMessage message string } func NewQuitMessage(args []string) (Message, error) { - msg := &QuitMessage{} + msg := &QuitMessage{ + BaseMessage: &BaseMessage{}, + } if len(args) > 0 { msg.message = args[0] } @@ -164,6 +195,7 @@ func (m *QuitMessage) Handle(s *Server, c *Client) { // MODE *( ( "+" / "-" ) *( "i" / "w" / "o" / "O" / "r" ) ) type ModeMessage struct { + *BaseMessage nickname string modes []string } @@ -203,7 +235,8 @@ func NewModeMessage(args []string) (Message, error) { } msg := &ModeMessage{ - nickname: args[0], + BaseMessage: &BaseMessage{}, + nickname: args[0], } for _, arg := range args[1:] { if !MODE_RE.MatchString(arg) { @@ -238,13 +271,16 @@ func (m *ChannelModeMessage) Handle(s *Server, c *Client) { // JOIN ( *( "," ) [ *( "," ) ] ) / "0" type JoinMessage struct { + *BaseMessage channels []string keys []string zero bool } func NewJoinMessage(args []string) (Message, error) { - msg := &JoinMessage{} + msg := &JoinMessage{ + BaseMessage: &BaseMessage{}, + } if len(args) > 0 { if args[0] == "0" { msg.zero = true @@ -276,9 +312,10 @@ func (m *JoinMessage) Handle(s *Server, c *Client) { } } -// PART +// PART *( "," ) [ ] type PartMessage struct { + *BaseMessage channels []string message string } @@ -287,7 +324,11 @@ func NewPartMessage(args []string) (Message, error) { if len(args) < 1 { return nil, NotEnoughArgsError } - msg := &PartMessage{channels: strings.Split(args[0], ",")} + msg := &PartMessage{ + + BaseMessage: &BaseMessage{}, + channels: strings.Split(args[0], ","), + } if len(args) > 1 { msg.message = args[1] } @@ -307,9 +348,10 @@ func (m *PartMessage) Handle(s *Server, c *Client) { } } -// PRIVMSG +// PRIVMSG type PrivMsgMessage struct { + *BaseMessage target string message string } @@ -319,8 +361,9 @@ func NewPrivMsgMessage(args []string) (Message, error) { return nil, NotEnoughArgsError } return &PrivMsgMessage{ - target: args[0], - message: args[1], + BaseMessage: &BaseMessage{}, + target: args[0], + message: args[1], }, nil } @@ -350,6 +393,7 @@ func (m *PrivMsgMessage) Handle(s *Server, c *Client) { // TOPIC [newtopic] type TopicMessage struct { + *BaseMessage channel string topic string } @@ -358,7 +402,10 @@ func NewTopicMessage(args []string) (Message, error) { if len(args) < 1 { return nil, NotEnoughArgsError } - msg := &TopicMessage{channel: args[0]} + msg := &TopicMessage{ + BaseMessage: &BaseMessage{}, + channel: args[0], + } if len(args) > 1 { msg.topic = args[1] } @@ -378,27 +425,25 @@ func (m *TopicMessage) Handle(s *Server, c *Client) { } } -// OPER +// LOGIN -type OperMessage struct { - name string +type LoginMessage struct { + *BaseMessage + nick string password string } -func NewOperMessage(args []string) (Message, error) { +func NewLoginMessage(args []string) (Message, error) { if len(args) < 2 { return nil, NotEnoughArgsError } - return &OperMessage{ - name: args[0], - password: args[1], + return &LoginMessage{ + BaseMessage: &BaseMessage{}, + nick: args[0], + password: args[1], }, nil } -func (m *OperMessage) Handle(s *Server, c *Client) { - if s.operators[m.name] == m.password { - c.send <- RplYoureOper(s) - } else { - c.send <- ErrPasswdMismatch(s) - } +func (m *LoginMessage) Handle(s *Server, c *Client) { + // TODO } diff --git a/src/irc/constants.go b/src/irc/constants.go index efd2029f..5d3ce2ee 100644 --- a/src/irc/constants.go +++ b/src/irc/constants.go @@ -149,4 +149,5 @@ const ( RPL_PART = "PART" RPL_PONG = "PONG" RPL_PRIVMSG = "PRIVMSG" + RPL_QUIT = "QUIT" ) diff --git a/src/irc/parse.go b/src/irc/parse.go index 2bc74e82..61fe1049 100644 --- a/src/irc/parse.go +++ b/src/irc/parse.go @@ -12,6 +12,7 @@ var ( parseCommandFuncs = map[string]ParseFunc{ "JOIN": NewJoinMessage, "MODE": NewModeMessage, + "LOGIN": NewLoginMessage, "NICK": NewNickMessage, "PART": NewPartMessage, "PASS": NewPassMessage, @@ -21,7 +22,6 @@ var ( "QUIT": NewQuitMessage, "TOPIC": NewTopicMessage, "USER": NewUserMessage, - "OPER": NewOperMessage, } ) @@ -29,7 +29,11 @@ func ParseMessage(line string) (Message, error) { command, args := parseLine(line) constructor, ok := parseCommandFuncs[command] if !ok { - return &UnknownMessage{command, args}, nil + return &UnknownMessage{ + BaseMessage: &BaseMessage{}, + command: command, + args: args, + }, nil } return constructor(args) } diff --git a/src/irc/reply.go b/src/irc/reply.go index 53a96e0e..786d0bbe 100644 --- a/src/irc/reply.go +++ b/src/irc/reply.go @@ -20,175 +20,178 @@ type BasicReply struct { message string } +func NewBasicReply(source Identifier, code string, message string) *BasicReply { + fullMessage := fmt.Sprintf(":%s %s %s\r\n", source.Id(), code, message) + return &BasicReply{source, code, fullMessage} +} + func (reply *BasicReply) String(client *Client) string { + return reply.message +} + +type NumericReply struct { + *BasicReply +} + +func NewNumericReply(source Identifier, code string, message string) *NumericReply { + return &NumericReply{&BasicReply{source, code, message}} +} + +func (reply *NumericReply) String(client *Client) string { return fmt.Sprintf(":%s %s %s %s\r\n", reply.source.Id(), reply.code, client.Nick(), reply.message) } -type ChannelReply struct { - *BasicReply - channel *Channel -} - -func (reply *ChannelReply) String(client *Client) string { - return fmt.Sprintf(":%s %s %s %s\r\n", reply.source.Id(), reply.code, reply.channel.name, - reply.message) -} - -func NewReply(source Identifier, code string, message string) *BasicReply { - return &BasicReply{source, code, message} -} - // messaging func RplPrivMsg(source *Client, message string) Reply { - return NewReply(source, RPL_PRIVMSG, ":"+message) + return NewNumericReply(source, RPL_PRIVMSG, ":"+message) } func RplNick(client *Client, newNick string) Reply { - return NewReply(client, RPL_NICK, ":"+newNick) + return NewBasicReply(client, RPL_NICK, newNick) } -func RplInviteMsg(channel *Channel, inviter *Client) Reply { - return NewReply(inviter, RPL_INVITE, channel.name) +func RplPrivMsgChannel(channel *Channel, source *Client, message string) Reply { + return NewBasicReply(source, RPL_PRIVMSG, fmt.Sprintf("%s :%s", channel.name, message)) } -func RplInvitingMsg(channel *Channel, invitee *Client) Reply { - return NewReply(channel.server, RPL_INVITING, - fmt.Sprintf("%s %s", channel.name, invitee.Nick())) +func RplJoin(channel *Channel, client *Client) Reply { + return NewBasicReply(client, RPL_JOIN, channel.name) +} + +func RplPart(channel *Channel, client *Client, message string) Reply { + return NewBasicReply(client, RPL_PART, fmt.Sprintf("%s :%s", channel.name, message)) +} + +func RplPong(server *Server) Reply { + return NewBasicReply(server, RPL_PONG, server.Id()) +} + +func RplQuit(client *Client, message string) Reply { + return NewBasicReply(client, RPL_QUIT, ":"+message) } // Server Info func RplWelcome(source Identifier, client *Client) Reply { - return NewReply(source, RPL_WELCOME, + return NewNumericReply(source, RPL_WELCOME, "Welcome to the Internet Relay Network "+client.Id()) } func RplYourHost(server *Server, target *Client) Reply { - return NewReply(server, RPL_YOURHOST, + return NewNumericReply(server, RPL_YOURHOST, fmt.Sprintf("Your host is %s, running version %s", server.hostname, VERSION)) } func RplCreated(server *Server) Reply { - return NewReply(server, RPL_CREATED, + return NewNumericReply(server, RPL_CREATED, "This server was created "+server.ctime.Format(time.RFC1123)) } func RplMyInfo(server *Server) Reply { - return NewReply(server, RPL_MYINFO, + return NewNumericReply(server, RPL_MYINFO, fmt.Sprintf("%s %s w kn", server.name, VERSION)) } func RplUModeIs(server *Server, client *Client) Reply { - return NewReply(server, RPL_UMODEIS, client.UModeString()) + return NewNumericReply(server, RPL_UMODEIS, client.UModeString()) } -// channel operations - -func RplPrivMsgChannel(channel *Channel, source *Client, message string) Reply { - return &ChannelReply{NewReply(source, RPL_PRIVMSG, ":"+message), channel} -} - -func RplJoin(channel *Channel, client *Client) Reply { - return &ChannelReply{NewReply(client, RPL_JOIN, ""), channel} -} - -func RplPart(channel *Channel, client *Client, message string) Reply { - return &ChannelReply{NewReply(client, RPL_PART, ":"+message), channel} -} +// numeric replies func RplNoTopic(channel *Channel) Reply { - return NewReply(channel.server, RPL_NOTOPIC, channel.name+" :No topic is set") + return NewNumericReply(channel.server, RPL_NOTOPIC, channel.name+" :No topic is set") } func RplTopic(channel *Channel) Reply { - return NewReply(channel.server, RPL_TOPIC, fmt.Sprintf("%s :%s", channel.name, channel.topic)) + return NewNumericReply(channel.server, RPL_TOPIC, fmt.Sprintf("%s :%s", channel.name, channel.topic)) } -// server info +func RplInviteMsg(channel *Channel, inviter *Client) Reply { + return NewNumericReply(inviter, RPL_INVITE, channel.name) +} + +func RplInvitingMsg(channel *Channel, invitee *Client) Reply { + return NewNumericReply(channel.server, RPL_INVITING, + fmt.Sprintf("%s %s", channel.name, invitee.Nick())) +} func RplNamReply(channel *Channel) Reply { // TODO multiple names and splitting based on message size - return NewReply(channel.server, RPL_NAMREPLY, + return NewNumericReply(channel.server, RPL_NAMREPLY, fmt.Sprintf("= %s :%s", channel.name, strings.Join(channel.Nicks(), " "))) } func RplEndOfNames(source Identifier) Reply { - return NewReply(source, RPL_ENDOFNAMES, ":End of NAMES list") + return NewNumericReply(source, RPL_ENDOFNAMES, ":End of NAMES list") } -func RplPong(server *Server) Reply { - return NewReply(server, RPL_PONG, server.Id()) -} - -// server functions - func RplYoureOper(server *Server) Reply { - return NewReply(server, RPL_YOUREOPER, ":You are now an IRC operator") + return NewNumericReply(server, RPL_YOUREOPER, ":You are now an IRC operator") } // errors func ErrAlreadyRegistered(source Identifier) Reply { - return NewReply(source, ERR_ALREADYREGISTRED, ":You may not reregister") + return NewNumericReply(source, ERR_ALREADYREGISTRED, ":You may not reregister") } func ErrNickNameInUse(source Identifier, nick string) Reply { - return NewReply(source, ERR_NICKNAMEINUSE, + return NewNumericReply(source, ERR_NICKNAMEINUSE, nick+" :Nickname is already in use") } func ErrUnknownCommand(source Identifier, command string) Reply { - return NewReply(source, ERR_UNKNOWNCOMMAND, + return NewNumericReply(source, ERR_UNKNOWNCOMMAND, command+" :Unknown command") } func ErrUsersDontMatch(source Identifier) Reply { - return NewReply(source, ERR_USERSDONTMATCH, + return NewNumericReply(source, ERR_USERSDONTMATCH, ":Cannot change mode for other users") } func ErrNeedMoreParams(source Identifier, command string) Reply { - return NewReply(source, ERR_NEEDMOREPARAMS, + return NewNumericReply(source, ERR_NEEDMOREPARAMS, command+"%s :Not enough parameters") } func ErrNoSuchChannel(source Identifier, channel string) Reply { - return NewReply(source, ERR_NOSUCHCHANNEL, + return NewNumericReply(source, ERR_NOSUCHCHANNEL, channel+" :No such channel") } func ErrUserOnChannel(channel *Channel, member *Client) Reply { - return NewReply(channel.server, ERR_USERONCHANNEL, + return NewNumericReply(channel.server, ERR_USERONCHANNEL, fmt.Sprintf("%s %s :is already on channel", member.nick, channel.name)) } func ErrNotOnChannel(channel *Channel) Reply { - return NewReply(channel.server, ERR_NOTONCHANNEL, + return NewNumericReply(channel.server, ERR_NOTONCHANNEL, channel.name+" :You're not on that channel") } func ErrInviteOnlyChannel(channel *Channel) Reply { - return NewReply(channel.server, ERR_INVITEONLYCHAN, + return NewNumericReply(channel.server, ERR_INVITEONLYCHAN, channel.name+" :Cannot join channel (+i)") } func ErrBadChannelKey(channel *Channel) Reply { - return NewReply(channel.server, ERR_BADCHANNELKEY, + return NewNumericReply(channel.server, ERR_BADCHANNELKEY, channel.name+" :Cannot join channel (+k)") } func ErrNoSuchNick(source Identifier, nick string) Reply { - return NewReply(source, ERR_NOSUCHNICK, + return NewNumericReply(source, ERR_NOSUCHNICK, nick+" :No such nick/channel") } func ErrPasswdMismatch(server *Server) Reply { - return NewReply(server, ERR_PASSWDMISMATCH, ":Password incorrect") + return NewNumericReply(server, ERR_PASSWDMISMATCH, ":Password incorrect") } func ErrNoChanModes(channel *Channel) Reply { - return NewReply(channel.server, ERR_NOCHANMODES, + return NewNumericReply(channel.server, ERR_NOCHANMODES, channel.name+" :Channel doesn't support modes") } diff --git a/src/irc/server.go b/src/irc/server.go index 7b1b0c90..40abc3ff 100644 --- a/src/irc/server.go +++ b/src/irc/server.go @@ -7,23 +7,17 @@ import ( ) type Server struct { - hostname string - ctime time.Time - name string - recv chan<- *ClientMessage - nicks map[string]*Client - channels map[string]*Channel - password string - operators map[string]string -} - -type ClientMessage struct { - client *Client - message Message + hostname string + ctime time.Time + name string + recv chan<- Message + password string + nicks map[string]*Client + channels map[string]*Channel } func NewServer(name string) *Server { - recv := make(chan *ClientMessage) + recv := make(chan Message) server := &Server{ ctime: time.Now(), name: name, @@ -33,9 +27,8 @@ func NewServer(name string) *Server { } go func() { for m := range recv { - log.Printf("%s -> %T%+v", m.client.Id(), m.message, m.message) - m.client.atime = time.Now() - m.message.Handle(server, m.client) + m.Client().atime = time.Now() + m.Handle(server, m.Client()) } }() return server @@ -57,7 +50,7 @@ func (s *Server) Listen(addr string) { continue } log.Print("Server.Listen: accepted ", conn.RemoteAddr()) - go NewClient(s, conn).Communicate() + go NewClient(s, conn) } } @@ -72,13 +65,10 @@ func (s *Server) GetOrMakeChannel(name string) *Channel { return channel } -func (s *Server) AddOperator(name string, password string) { - s.operators[name] = password -} - // 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 := make(ClientSet) clients[fromClient] = true for channel := range fromClient.channels { for client := range channel.members { @@ -98,14 +88,12 @@ func (s *Server) ChangeNick(c *Client, newNick string) { c.send <- ErrNickNameInUse(s, newNick) return } + s.SendToInterestedClients(c, RplNick(c, newNick)) if c.nick != "" { delete(s.nicks, c.nick) } s.nicks[newNick] = c - - s.SendToInterestedClients(c, RplNick(c, newNick)) - c.nick = newNick s.tryRegister(c) @@ -136,7 +124,7 @@ func (s *Server) Quit(c *Client, message string) { channel.Part(c, message) } delete(s.nicks, c.nick) - + s.SendToInterestedClients(c, RplQuit(c, message)) c.conn.Close() } @@ -155,3 +143,7 @@ func (s *Server) ChangeUserMode(c *Client, modes []string) { func (s *Server) Id() string { return s.hostname } + +func (s *Server) DeleteChannel(channel *Channel) { + delete(s.channels, channel.name) +} diff --git a/src/irc/user.go b/src/irc/user.go new file mode 100644 index 00000000..a3e6ffb2 --- /dev/null +++ b/src/irc/user.go @@ -0,0 +1,5 @@ +package irc + +type User struct { + nick string +}