diff --git a/src/irc/channel.go b/src/irc/channel.go new file mode 100644 index 00000000..48c810e7 --- /dev/null +++ b/src/irc/channel.go @@ -0,0 +1,94 @@ +package irc + +type Channel struct { + name string + key string + topic string + members ClientSet + inviteOnly bool + invites map[string]bool + server *Server +} + +type ChannelSet map[*Channel]bool + +func NewChannel(s *Server, name string) *Channel { + return &Channel{name: name, members: make(ClientSet), invites: make(map[string]bool), server: s} +} + +func (ch *Channel) Send(reply Reply, fromClient *Client) { + for client := range ch.members { + if client != fromClient { + client.send <- reply + } + } +} + +// channel functionality + +func (ch *Channel) Join(cl *Client, key string) { + if ch.key != key { + cl.send <- ErrInviteOnlyChannel(ch) + return + } + + if ch.inviteOnly && !ch.invites[cl.nick] { + cl.send <- ErrBadChannelKey(ch) + return + } + + ch.members[cl] = true + cl.channels[ch] = true + + ch.Send(RplJoin(ch, cl), nil) + ch.GetTopic(cl) + + for member := range ch.members { + cl.send <- RplNamReply(ch, member) + } + cl.send <- RplEndOfNames(ch.server) +} + +func (ch *Channel) Part(cl *Client, message string) { + if !ch.members[cl] { + cl.send <- ErrNotOnChannel(ch) + return + } + + delete(ch.members, cl) + delete(cl.channels, ch) + + ch.Send(RplPart(ch, cl, message), nil) +} + +func (ch *Channel) PrivMsg(cl *Client, message string) { + ch.Send(RplPrivMsgChannel(ch, cl, message), cl) +} + +func (ch *Channel) GetTopic(cl *Client) { + if !ch.members[cl] { + cl.send <- ErrNotOnChannel(ch) + return + } + + if ch.topic != "" { + cl.send <- RplTopic(ch) + } else { + cl.send <- RplNoTopic(ch) + } +} + +func (ch *Channel) ChangeTopic(cl *Client, newTopic string) { + if !ch.members[cl] { + cl.send <- ErrNotOnChannel(ch) + return + } + + ch.topic = newTopic + + if ch.topic != "" { + ch.Send(RplTopic(ch), nil) + } else { + ch.Send(RplNoTopic(ch), nil) + } +} diff --git a/src/irc/client.go b/src/irc/client.go index 85ad6284..250c6b5a 100644 --- a/src/irc/client.go +++ b/src/irc/client.go @@ -3,34 +3,46 @@ package irc import ( "fmt" "net" - "strings" ) type Client struct { conn net.Conn - send chan<- string + hostname string + send chan<- Reply recv <-chan string username string realname string nick string registered bool invisible bool + channels ChannelSet } +type ClientSet map[*Client]bool + func NewClient(conn net.Conn) *Client { - client := new(Client) - client.conn = conn - client.send = StringWriteChan(conn) - client.recv = StringReadChan(conn) + client := &Client{conn: conn, recv: StringReadChan(conn), channels: make(ChannelSet), hostname: LookupHostname(conn.RemoteAddr())} + client.SetReplyToStringChan() return client } +func (c *Client) SetReplyToStringChan() { + send := make(chan Reply) + write := StringWriteChan(c.conn) + go func() { + for reply := range send { + write <- reply.String(c) + } + }() + c.send = send +} + // Adapt `chan string` to a `chan Message`. -func (c *Client) Communicate(server chan<- *ClientMessage) { +func (c *Client) Communicate(server *Server) { for str := range c.recv { m := ParseMessage(str) if m != nil { - server <- &ClientMessage{c, m} + server.recv <- &ClientMessage{c, m} } } } @@ -39,7 +51,7 @@ func (c *Client) Nick() string { if c.nick != "" { return c.nick } - return "" + return "*" } func (c *Client) UModeString() string { @@ -57,15 +69,10 @@ func (c *Client) HasUser() bool { return c.username != "" } -func (c *Client) Hostname() string { - addr := c.conn.RemoteAddr().String() - index := strings.LastIndex(addr, ":") - if index != -1 { - return addr[0:index] - } - return addr +func (c *Client) UserHost() string { + return fmt.Sprintf("%s!%s@%s", c.nick, c.username, c.hostname) } -func (c *Client) UserHost() string { - return fmt.Sprintf("%s!%s@%s", c.nick, c.username, c.Hostname()) +func (c *Client) Id() string { + return c.UserHost() } diff --git a/src/irc/commands.go b/src/irc/commands.go index 34bc8a96..6039a8fe 100644 --- a/src/irc/commands.go +++ b/src/irc/commands.go @@ -4,68 +4,180 @@ type Message interface { Handle(s *Server, c *Client) } -func (m *NickMessage) Handle(s *Server, c *Client) { - if s.nicks[m.nickname] != nil { - c.send <- ErrNickNameInUse(m.nickname) - return - } - oldNick := c.nick - if c.nick != "" { - delete(s.nicks, c.nick) - } - c.nick = m.nickname - s.nicks[c.nick] = c - if c.registered { - c.send <- ReplyNick(oldNick, c) - } else { - tryRegister(s, c) - } -} +// unknown -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) -} - -func (m *QuitMessage) Handle(s *Server, c *Client) { - c.send <- MessageError() - c.conn.Close() - 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) + c.send <- ErrUnknownCommand(s, m.command) +} + +// PING + +type PingMessage struct { + server string + server2 string } func (m *PingMessage) Handle(s *Server, c *Client) { - c.send <- MessagePong() + c.send <- RplPong(s) +} + +// PONG + +type PongMessage struct { + server1 string + server2 string +} + +func (m *PongMessage) Handle(s *Server, c *Client) { + // TODO update client atime +} + +// NICK + +type NickMessage struct { + nickname string +} + +func (m *NickMessage) Handle(s *Server, c *Client) { + s.ChangeNick(c, m.nickname) +} + +// USER + +type UserMessage struct { + user string + mode uint8 + unused string + realname string +} + +func (m *UserMessage) Handle(s *Server, c *Client) { + s.Register(c, m.user, m.realname) +} + +// QUIT + +type QuitMessage struct { + message string +} + +func (m *QuitMessage) Handle(s *Server, c *Client) { + s.Quit(c, m.message) +} + +// MODE + +type ModeMessage struct { + nickname string + modes []string } func (m *ModeMessage) Handle(s *Server, c *Client) { if m.nickname != c.nick { - c.send <- ErrUsersDontMatch(c.Nick()) + c.send <- ErrUsersDontMatch(s) return } - for _, mode := range m.modes { - if mode == "+i" { - c.invisible = true - } else if mode == "-i" { - c.invisible = false - } - } - c.send <- ReplyUModeIs(c) + s.ChangeUserMode(c, m.modes) } -func tryRegister(s *Server, c *Client) { - if (!c.registered && c.HasNick() && c.HasUser()) { - c.registered = true - c.send <- ReplyWelcome(c) - c.send <- ReplyYourHost(c.Nick(), s.name) - c.send <- ReplyCreated(c.Nick(), s.ctime) - c.send <- ReplyMyInfo(c.Nick(), s.name) +// JOIN + +type JoinMessage struct { + channels []string + keys []string + zero bool +} + +func (m *JoinMessage) Handle(s *Server, c *Client) { + if m.zero { + for channel := range c.channels { + channel.Part(c, "") + } + } else { + for i, name := range m.channels { + key := "" + if len(m.keys) > i { + key = m.keys[i] + } + + s.GetOrMakeChannel(name).Join(c, key) + } + } +} + +// PART + +type PartMessage struct { + channels []string + message string +} + +func (m *PartMessage) Handle(s *Server, c *Client) { + for _, chname := range m.channels { + channel := s.channels[chname] + + if channel == nil { + c.send <- ErrNoSuchChannel(s, chname) + continue + } + + channel.Part(c, m.message) + } +} + +// PRIVMSG + +type PrivMsgMessage struct { + target string + message string +} + +func (m *PrivMsgMessage) TargetIsChannel() bool { + switch m.target[0] { + case '&', '#', '+', '!': + return true + } + return false +} + +func (m *PrivMsgMessage) Handle(s *Server, c *Client) { + if m.TargetIsChannel() { + channel := s.channels[m.target] + if channel != nil { + channel.PrivMsg(c, m.message) + } else { + c.send <- ErrNoSuchNick(s, m.target) + } + } else { + client := s.nicks[m.target] + if client != nil { + client.send <- RplPrivMsg(client, m.message) + } else { + c.send <- ErrNoSuchNick(s, m.target) + } + } +} + +// TOPIC + +type TopicMessage struct { + channel string + topic string +} + +func (m *TopicMessage) Handle(s *Server, c *Client) { + channel := s.channels[m.channel] + if channel == nil { + c.send <- ErrNoSuchChannel(s, m.channel) + return + } + if m.topic == "" { + channel.GetTopic(c) + } else { + channel.ChangeTopic(c, m.topic) } } diff --git a/src/irc/constants.go b/src/irc/constants.go index 1a1b3a25..a0ce3bf7 100644 --- a/src/irc/constants.go +++ b/src/irc/constants.go @@ -1,4 +1,6 @@ -// http://tools.ietf.org/html/rfc2812 +// 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 ( @@ -6,22 +8,30 @@ const ( ) const ( - RPL_WELCOME = "001" - RPL_YOURHOST = "002" - RPL_CREATED = "003" - RPL_MYINFO = "004" - RPL_UMODEIS = "221" - RPL_INFO = "371" - RPL_NICK = "NICK" -) - -const ( - ERR_NOSUCHNICK = "401" - ERR_NOSUCHSERVER = "402" - ERR_NOSUCHCHANNEL = "403" - ERR_UNKNOWNCOMMAND = "421" - ERR_NICKNAMEINUSE = "433" - ERR_NEEDMOREPARAMS = "461" - ERR_ALREADYREGISTRED = "462" - ERR_USERSDONTMATCH = "502" + 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" + ERR_NOSUCHNICK = "401" + ERR_NOSUCHSERVER = "402" + ERR_NOSUCHCHANNEL = "403" + ERR_UNKNOWNCOMMAND = "421" + ERR_NICKNAMEINUSE = "433" + ERR_NOTONCHANNEL = "442" + ERR_NEEDMOREPARAMS = "461" + ERR_ALREADYREGISTRED = "462" + ERR_INVITEONLYCHANNEL = "473" + ERR_BADCHANNELKEY = "475" + ERR_USERSDONTMATCH = "502" + RPL_JOIN = "JOIN" + RPL_NICK = "NICK" + RPL_PART = "PART" + RPL_PONG = "PONG" + RPL_PRIVMSG = "PRIVMSG" ) diff --git a/src/irc/message.go b/src/irc/message.go deleted file mode 100644 index c3a7e944..00000000 --- a/src/irc/message.go +++ /dev/null @@ -1,27 +0,0 @@ -package irc - -type NickMessage struct { - nickname string -} - -type UserMessage struct { - user string - mode uint8 - unused string - realname string -} - -type QuitMessage struct { - message string -} - -type UnknownMessage struct { - command string -} - -type PingMessage struct {} - -type ModeMessage struct { - nickname string - modes []string -} diff --git a/src/irc/net.go b/src/irc/net.go index ca70c06e..c546e201 100644 --- a/src/irc/net.go +++ b/src/irc/net.go @@ -3,8 +3,8 @@ package irc import ( "bufio" "log" - "strings" "net" + "strings" ) func readTrimmedLine(reader *bufio.Reader) (string, error) { @@ -20,7 +20,7 @@ func StringReadChan(conn net.Conn) <-chan string { go func() { for { line, err := readTrimmedLine(reader) - if (line != "") { + if line != "" { ch <- line log.Printf("%s -> %s", addr, line) } @@ -50,3 +50,16 @@ func StringWriteChan(conn net.Conn) chan<- string { return ch } + +func LookupHostname(addr net.Addr) string { + addrStr := addr.String() + ipaddr, _, err := net.SplitHostPort(addrStr) + if err != nil { + return addrStr + } + names, err := net.LookupAddr(ipaddr) + if err != nil { + return ipaddr + } + return names[0] +} diff --git a/src/irc/parse.go b/src/irc/parse.go index 3fcda8e4..3c85f184 100644 --- a/src/irc/parse.go +++ b/src/irc/parse.go @@ -7,12 +7,17 @@ import ( "strings" ) -var commands = map[string]func([]string) Message { - "MODE": NewModeMessage, - "NICK": NewNickMessage, - "PING": NewPingMessage, - "QUIT": NewQuitMessage, - "USER": NewUserMessage, +var commands = map[string]func([]string) Message{ + "JOIN": NewJoinMessage, + "MODE": NewModeMessage, + "NICK": NewNickMessage, + "PART": NewPartMessage, + "PING": NewPingMessage, + "PONG": NewPongMessage, + "PRIVMSG": NewPrivMsgMessage, + "QUIT": NewQuitMessage, + "TOPIC": NewTopicMessage, + "USER": NewUserMessage, } func ParseMessage(line string) Message { @@ -54,7 +59,6 @@ func parseLine(line string) (string, []string) { return args[0], args[1:] } - // []string => Message constructors func NewNickMessage(args []string) Message { @@ -65,7 +69,25 @@ func NewNickMessage(args []string) Message { } func NewPingMessage(args []string) Message { - return &PingMessage{} + 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 { @@ -112,3 +134,55 @@ func NewModeMessage(args []string) Message { } 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 +} diff --git a/src/irc/reply.go b/src/irc/reply.go new file mode 100644 index 00000000..b4ee85bc --- /dev/null +++ b/src/irc/reply.go @@ -0,0 +1,148 @@ +package irc + +import ( + "fmt" + "time" +) + +type Identifier interface { + Id() string +} + +type Reply interface { + String(client *Client) string +} + +type BasicReply struct { + source Identifier + code string + message string +} + +func (reply *BasicReply) String(client *Client) string { + prefix := fmt.Sprintf(":%s %s %s ", reply.source.Id(), reply.code, client.Nick()) + return prefix + reply.message +} + +type ChannelReply struct { + *BasicReply + channel *Channel +} + +func (reply *ChannelReply) String(client *Client) string { + prefix := fmt.Sprintf(":%s %s %s ", reply.source.Id(), reply.code, reply.channel.name) + return prefix + 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) +} + +func RplNick(client *Client, newNick string) Reply { + return NewReply(client, RPL_NICK, ":"+newNick) +} + +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.name), channel} +} + +func RplPart(channel *Channel, client *Client, message string) Reply { + return &ChannelReply{NewReply(client, RPL_PART, ":"+message), channel} +} + +// Server Info + +func RplWelcome(source Identifier, client *Client) Reply { + return NewReply(source, RPL_WELCOME, "Welcome to the Internet Relay Network "+client.Id()) +} + +func RplYourHost(server *Server, target *Client) Reply { + return NewReply(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, "This server was created "+server.ctime.Format(time.RFC1123)) +} + +func RplMyInfo(server *Server) Reply { + return NewReply(server, RPL_MYINFO, server.name+" i ik") +} + +func RplUModeIs(server *Server, client *Client) Reply { + return NewReply(server, RPL_UMODEIS, client.UModeString()) +} + +// channel operations + +func RplNoTopic(channel *Channel) Reply { + return &ChannelReply{NewReply(channel.server, RPL_NOTOPIC, channel.name+" :No topic is set"), channel} +} + +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 { + // TODO multiple names and splitting based on message size + return NewReply(channel.server, RPL_NAMREPLY, fmt.Sprintf("=%s :+%s", channel.name, client.Nick())) +} + +func RplEndOfNames(source Identifier) Reply { + return NewReply(source, RPL_ENDOFNAMES, ":End of NAMES list") +} + +func RplPong(server *Server) Reply { + return NewReply(server, RPL_PONG, "") +} + +// errors + +func ErrAlreadyRegistered(source Identifier) Reply { + return NewReply(source, ERR_ALREADYREGISTRED, ":You may not reregister") +} + +func ErrNickNameInUse(source Identifier, nick string) Reply { + return NewReply(source, ERR_NICKNAMEINUSE, nick+" :Nickname is already in use") +} + +func ErrUnknownCommand(source Identifier, command string) Reply { + return NewReply(source, ERR_UNKNOWNCOMMAND, command+" :Unknown command") +} + +func ErrUsersDontMatch(source Identifier) Reply { + return NewReply(source, ERR_USERSDONTMATCH, ":Cannot change mode for other users") +} + +func ErrNeedMoreParams(source Identifier, command string) Reply { + return NewReply(source, ERR_NEEDMOREPARAMS, command+"%s :Not enough parameters") +} + +func ErrNoSuchChannel(source Identifier, channel string) Reply { + return NewReply(source, ERR_NOSUCHCHANNEL, channel+" :No such channel") +} + +func ErrNotOnChannel(channel *Channel) Reply { + return NewReply(channel.server, ERR_NOTONCHANNEL, channel.name+" :You're not on that channel") +} + +func ErrInviteOnlyChannel(channel *Channel) Reply { + return NewReply(channel.server, ERR_INVITEONLYCHANNEL, channel.name+" :Cannot join channel (+i)") +} + +func ErrBadChannelKey(channel *Channel) Reply { + return NewReply(channel.server, ERR_BADCHANNELKEY, channel.name+" :Cannot join channel (+k)") +} + +func ErrNoSuchNick(source Identifier, nick string) Reply { + return NewReply(source, ERR_NOSUCHNICK, nick+" :No such nick/channel") +} diff --git a/src/irc/responses.go b/src/irc/responses.go deleted file mode 100644 index b9a26206..00000000 --- a/src/irc/responses.go +++ /dev/null @@ -1,54 +0,0 @@ -package irc - -import ( - "fmt" - "time" -) - -func ReplyNick(oldNick string, c *Client) string { - return fmt.Sprintf(":%s!%s@%s %s :%s", oldNick, c.username, c.Hostname(), RPL_NICK, c.Nick()) -} - -func ReplyWelcome(c *Client) string { - return fmt.Sprintf("%s %s Welcome to the Internet Relay Network %s!%s@%s", RPL_WELCOME, c.Nick(), c.Nick(), c.username, c.Hostname()) -} - -func ReplyYourHost(nick string, server string) string { - return fmt.Sprintf("%s %s Your host is %s, running version %s", RPL_YOURHOST, nick, server, VERSION) -} - -func ReplyCreated(nick string, ctime time.Time) string { - return fmt.Sprintf("%s %s This server was created %s", RPL_CREATED, nick, ctime.Format(time.RFC1123)) -} - -func ReplyMyInfo(nick string, servername string) string { - return fmt.Sprintf("%s %s %s %s i ", RPL_MYINFO, nick, servername, VERSION) -} - -func ReplyUModeIs(c *Client) string { - return fmt.Sprintf("%s %s %s", RPL_UMODEIS, c.Nick(), c.UModeString()) -} - -func ErrAlreadyRegistered(nick string) string { - return fmt.Sprintf("%s %s :You may not reregister", ERR_ALREADYREGISTRED, nick) -} - -func ErrNickNameInUse(nick string) string { - return fmt.Sprintf("%s %s :Nickname is already in use", ERR_NICKNAMEINUSE, nick) -} - -func ErrUnknownCommand(nick string, command string) string { - return fmt.Sprintf("%s %s %s :Unknown command", ERR_UNKNOWNCOMMAND, nick, command) -} - -func ErrUsersDontMatch(nick string) string { - return fmt.Sprintf("%s %s :Cannot change mode for other users", ERR_USERSDONTMATCH, nick) -} - -func MessagePong() string { - return "PONG" -} - -func MessageError() string { - return "ERROR :Bye" -} diff --git a/src/irc/server.go b/src/irc/server.go index c7ea4107..8c9cdd31 100644 --- a/src/irc/server.go +++ b/src/irc/server.go @@ -7,24 +7,27 @@ import ( ) type Server struct { - ctime time.Time - name string - ch chan *ClientMessage - nicks map[string]*Client + hostname string + ctime time.Time + name string + recv chan<- *ClientMessage + nicks map[string]*Client + channels map[string]*Channel } type ClientMessage struct { - client *Client + client *Client message Message } func NewServer(name string) *Server { - server := new(Server) - server.ctime = time.Now() - server.name = name - server.ch = make(chan *ClientMessage) - server.nicks = make(map[string]*Client) - go server.Receive() + recv := make(chan *ClientMessage) + 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) + } + }() return server } @@ -34,18 +37,104 @@ func (s *Server) Listen(addr string) { log.Fatal("Server.Listen: ", err) } + s.hostname = LookupHostname(listener.Addr()) + log.Print("Server.Listen: listening on ", addr) + for { conn, err := listener.Accept() if err != nil { log.Print("Server.Listen: ", err) continue } - go NewClient(conn).Communicate(s.ch) + log.Print("Server.Listen: accepted ", conn.RemoteAddr()) + go NewClient(conn).Communicate(s) } } -func (s *Server) Receive() { - for m := range s.ch { - m.message.Handle(s, m.client) +func (s *Server) GetOrMakeChannel(name string) *Channel { + channel := s.channels[name] + + if channel == nil { + channel = NewChannel(s, name) + s.channels[name] = channel + } + + return 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) + for channel := range fromClient.channels { + for client := range channel.members { + clients[client] = true + } + } + + for client := range clients { + client.send <- reply } } + +// server functionality + +func (s *Server) ChangeNick(c *Client, newNick string) { + if s.nicks[newNick] != nil { + c.send <- ErrNickNameInUse(s, newNick) + 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) +} + +func (s *Server) Register(c *Client, user string, realName string) { + if c.username != "" { + c.send <- ErrAlreadyRegistered(s) + return + } + + c.username, c.realname = user, realName + s.TryRegister(c) +} + +func (s *Server) TryRegister(c *Client) { + if !c.registered && c.HasNick() && c.HasUser() { + c.registered = true + c.send <- RplWelcome(s, c) + c.send <- RplYourHost(s, c) + c.send <- RplCreated(s) + c.send <- RplMyInfo(s) + } +} + +func (s *Server) Quit(c *Client, message string) { + for channel := range c.channels { + channel.Part(c, message) + } + delete(s.nicks, c.nick) + + c.conn.Close() +} + +func (s *Server) ChangeUserMode(c *Client, modes []string) { + for _, mode := range modes { + if mode == "+i" { + c.invisible = true + } else if mode == "-i" { + c.invisible = false + } + } + c.send <- RplUModeIs(s, c) +} + +func (s *Server) Id() string { + return s.hostname +}