From 93f4b6859a64828f7f6540ba444fba56bc3a74b5 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Sat, 8 Feb 2014 22:06:10 -0800 Subject: [PATCH] more channel mode parsing and bad listing --- irc/channel.go | 25 ++++++++-- irc/commands.go | 117 +++++++++++++++++++++++++++++++---------------- irc/constants.go | 34 +++++++++++++- irc/net.go | 44 +++++++++++------- irc/reply.go | 13 +++++- irc/server.go | 3 +- irc/types.go | 34 ++++++++++++++ 7 files changed, 205 insertions(+), 65 deletions(-) create mode 100644 irc/types.go diff --git a/irc/channel.go b/irc/channel.go index 767509d2..06b73274 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -5,6 +5,7 @@ import ( ) type Channel struct { + banList []UserMask commands chan<- ChannelCommand key string members ClientSet @@ -47,11 +48,12 @@ func NewChannel(s *Server, name string) *Channel { commands := make(chan ChannelCommand) replies := make(chan Reply) channel := &Channel{ - name: name, - members: make(ClientSet), - server: s, + banList: make([]UserMask, 0), commands: commands, + members: make(ClientSet), + name: name, replies: replies, + server: s, } go channel.receiveCommands(commands) go channel.receiveReplies(replies) @@ -199,3 +201,20 @@ func (m *TopicCommand) HandleChannel(channel *Channel) { func (m *PrivMsgCommand) HandleChannel(channel *Channel) { channel.replies <- RplPrivMsg(m.Client(), channel, m.message) } + +func (msg *ChannelModeCommand) HandleChannel(channel *Channel) { + client := msg.Client() + + for _, modeOp := range msg.modeOps { + switch modeOp.mode { + case BanMask: + // TODO add/remove + for _, banMask := range channel.banList { + client.replies <- RplBanList(channel, banMask) + } + client.replies <- RplEndOfBanList(channel) + } + } + + client.replies <- RplChannelModeIs(channel) +} diff --git a/irc/commands.go b/irc/commands.go index 7f4e98b2..7e479a94 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -8,22 +8,17 @@ import ( "unicode/utf8" ) -type Command interface { - Client() *Client - Source() Identifier - Reply(Reply) - HandleServer(*Server) -} - -type EditableCommand interface { +type editableCommand interface { Command SetBase(*Client) } +type parseCommandFunc func([]string) (editableCommand, error) + var ( NotEnoughArgsError = errors.New("not enough arguments") ErrParseCommand = errors.New("failed to parse message") - parseCommandFuncs = map[string]func([]string) (EditableCommand, error){ + parseCommandFuncs = map[string]parseCommandFunc{ "JOIN": NewJoinCommand, "MODE": NewModeCommand, "NICK": NewNickCommand, @@ -60,7 +55,7 @@ func (command *BaseCommand) Reply(reply Reply) { command.client.Replies() <- reply } -func ParseCommand(line string) (EditableCommand, error) { +func ParseCommand(line string) (editableCommand, error) { command, args := parseLine(line) constructor := parseCommandFuncs[command] if constructor == nil { @@ -126,7 +121,7 @@ func (cmd *PingCommand) String() string { return fmt.Sprintf("PING(server=%s, server2=%s)", cmd.server, cmd.server2) } -func NewPingCommand(args []string) (EditableCommand, error) { +func NewPingCommand(args []string) (editableCommand, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -151,7 +146,7 @@ func (cmd *PongCommand) String() string { return fmt.Sprintf("PONG(server1=%s, server2=%s)", cmd.server1, cmd.server2) } -func NewPongCommand(args []string) (EditableCommand, error) { +func NewPongCommand(args []string) (editableCommand, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -175,7 +170,7 @@ func (cmd *PassCommand) String() string { return fmt.Sprintf("PASS(password=%s)", cmd.password) } -func NewPassCommand(args []string) (EditableCommand, error) { +func NewPassCommand(args []string) (editableCommand, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -195,7 +190,7 @@ func (m *NickCommand) String() string { return fmt.Sprintf("NICK(nickname=%s)", m.nickname) } -func NewNickCommand(args []string) (EditableCommand, error) { +func NewNickCommand(args []string) (editableCommand, error) { if len(args) != 1 { return nil, NotEnoughArgsError } @@ -219,7 +214,7 @@ func (cmd *UserMsgCommand) String() string { cmd.user, cmd.mode, cmd.unused, cmd.realname) } -func NewUserMsgCommand(args []string) (EditableCommand, error) { +func NewUserMsgCommand(args []string) (editableCommand, error) { if len(args) != 4 { return nil, NotEnoughArgsError } @@ -246,7 +241,7 @@ func (cmd *QuitCommand) String() string { return fmt.Sprintf("QUIT(message=%s)", cmd.message) } -func NewQuitCommand(args []string) (EditableCommand, error) { +func NewQuitCommand(args []string) (editableCommand, error) { msg := &QuitCommand{} if len(args) > 0 { msg.message = args[0] @@ -266,7 +261,7 @@ func (cmd *JoinCommand) String() string { return fmt.Sprintf("JOIN(channels=%s, zero=%t)", cmd.channels, cmd.zero) } -func NewJoinCommand(args []string) (EditableCommand, error) { +func NewJoinCommand(args []string) (editableCommand, error) { msg := &JoinCommand{ channels: make(map[string]string), } @@ -313,7 +308,7 @@ func (cmd *PartCommand) String() string { return fmt.Sprintf("PART(channels=%s, message=%s)", cmd.channels, cmd.message) } -func NewPartCommand(args []string) (EditableCommand, error) { +func NewPartCommand(args []string) (editableCommand, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -338,7 +333,7 @@ func (cmd *PrivMsgCommand) String() string { return fmt.Sprintf("PRIVMSG(target=%s, message=%s)", cmd.target, cmd.message) } -func NewPrivMsgCommand(args []string) (EditableCommand, error) { +func NewPrivMsgCommand(args []string) (editableCommand, error) { if len(args) < 2 { return nil, NotEnoughArgsError } @@ -364,7 +359,7 @@ func (cmd *TopicCommand) String() string { return fmt.Sprintf("TOPIC(channel=%s, topic=%s)", cmd.channel, cmd.topic) } -func NewTopicCommand(args []string) (EditableCommand, error) { +func NewTopicCommand(args []string) (editableCommand, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -377,20 +372,8 @@ func NewTopicCommand(args []string) (EditableCommand, error) { return msg, nil } -type Mode rune - -const ( - Away Mode = 'a' - Invisible Mode = 'i' - WallOps Mode = 'w' - Restricted Mode = 'r' - Operator Mode = 'o' - LocalOperator Mode = 'O' - ServerNotice Mode = 's' -) - type ModeChange struct { - mode Mode + mode UserMode add bool // false => remove } @@ -425,22 +408,68 @@ func stringToRunes(str string) <-chan rune { return runes } +type ChannelModeOp struct { + mode ChannelMode + op ModeOp + arg string +} + +func (op *ChannelModeOp) String() string { + return fmt.Sprintf("{%s %s %s}", op.op, op.mode, op.arg) +} + type ChannelModeCommand struct { BaseCommand channel string + modeOps []ChannelModeOp } // MODE *( ( "-" / "+" ) * * ) -func NewChannelModeCommand(args []string) (EditableCommand, error) { +func NewChannelModeCommand(args []string) (editableCommand, error) { cmd := &ChannelModeCommand{ channel: args[0], + modeOps: make([]ChannelModeOp, 0), } - // TODO implement channel mode changes + args = args[1:] + + for len(args) > 0 { + modeArg := args[0] + op := List + switch modeArg[0] { + case '+': + op = Add + modeArg = modeArg[1:] + case '-': + op = Remove + modeArg = modeArg[1:] + } + skipArgs := 1 + for mode := range stringToRunes(modeArg) { + modeOp := ChannelModeOp{ + mode: ChannelMode(mode), + op: op, + } + switch modeOp.mode { + case Key, BanMask, ExceptionMask, InviteMask: + if len(args) > skipArgs { + modeOp.arg = args[skipArgs] + skipArgs += 1 + } + } + cmd.modeOps = append(cmd.modeOps, modeOp) + } + args = args[skipArgs:] + } + return cmd, nil } +func (msg *ChannelModeCommand) String() string { + return fmt.Sprintf("MODE(channel=%s, modeOps=%s)", msg.channel, msg.modeOps) +} + // MODE *( ( "+" / "-" ) *( "i" / "w" / "o" / "O" / "r" ) ) -func NewUserModeCommand(args []string) (EditableCommand, error) { +func NewUserModeCommand(args []string) (editableCommand, error) { cmd := &ModeCommand{ nickname: args[0], changes: make([]ModeChange, @@ -458,7 +487,7 @@ func NewUserModeCommand(args []string) (EditableCommand, error) { add := sig == '+' for mode := range modeChange { cmd.changes[index] = ModeChange{ - mode: Mode(mode), + mode: UserMode(mode), add: add, } index += 1 @@ -468,7 +497,7 @@ func NewUserModeCommand(args []string) (EditableCommand, error) { return cmd, nil } -func NewModeCommand(args []string) (EditableCommand, error) { +func NewModeCommand(args []string) (editableCommand, error) { if len(args) == 0 { return nil, NotEnoughArgsError } @@ -487,7 +516,7 @@ type WhoisCommand struct { } // WHOIS [ ] *( "," ) -func NewWhoisCommand(args []string) (EditableCommand, error) { +func NewWhoisCommand(args []string) (editableCommand, error) { if len(args) < 1 { return nil, NotEnoughArgsError } @@ -508,6 +537,10 @@ func NewWhoisCommand(args []string) (EditableCommand, error) { }, nil } +func (msg *WhoisCommand) String() string { + return fmt.Sprintf("WHOIS(target=%s, masks=%s)", msg.target, msg.masks) +} + type WhoCommand struct { BaseCommand mask string @@ -515,7 +548,7 @@ type WhoCommand struct { } // WHO [ [ "o" ] ] -func NewWhoCommand(args []string) (EditableCommand, error) { +func NewWhoCommand(args []string) (editableCommand, error) { cmd := &WhoCommand{} if len(args) > 0 { @@ -528,3 +561,7 @@ func NewWhoCommand(args []string) (EditableCommand, error) { return cmd, nil } + +func (msg *WhoCommand) String() string { + return fmt.Sprintf("WHO(mask=%s, operatorOnly=%s)", msg.mask, msg.operatorOnly) +} diff --git a/irc/constants.go b/irc/constants.go index b78ea889..e460f29b 100644 --- a/irc/constants.go +++ b/irc/constants.go @@ -9,9 +9,7 @@ var ( const ( VERSION = "ergonomadic-1" -) -const ( // numeric codes RPL_WELCOME = 1 RPL_YOURHOST = 2 @@ -149,6 +147,7 @@ const ( ERR_NOOPERHOST = 491 ERR_UMODEUNKNOWNFLAG = 501 ERR_USERSDONTMATCH = 502 + // message codes RPL_ERROR = "ERROR" RPL_INVITE = "INVITE" @@ -158,4 +157,35 @@ const ( RPL_PONG = "PONG" RPL_PRIVMSG = "PRIVMSG" RPL_QUIT = "QUIT" + + List ModeOp = 'l' + Add ModeOp = 'a' + Remove ModeOp = 'r' + + Away UserMode = 'a' + Invisible UserMode = 'i' + WallOps UserMode = 'w' + Restricted UserMode = 'r' + Operator UserMode = 'o' + LocalOperator UserMode = 'O' + ServerNotice UserMode = 's' + + Anonymous ChannelMode = 'a' + InviteOnly ChannelMode = 'i' + Moderated ChannelMode = 'm' + NoOutside ChannelMode = 'n' + Quiet ChannelMode = 'q' + Private ChannelMode = 'p' + Secret ChannelMode = 's' + ReOp ChannelMode = 'r' + OpOnlyTopic ChannelMode = 't' + Key ChannelMode = 'k' + UserLimit ChannelMode = 'l' + BanMask ChannelMode = 'b' + ExceptionMask ChannelMode = 'e' + InviteMask ChannelMode = 'i' + + ChannelCreator UserChannelMode = 'O' + ChannelOperator UserChannelMode = 'o' + Voice UserChannelMode = 'v' ) diff --git a/irc/net.go b/irc/net.go index 23f780a7..54122f34 100644 --- a/irc/net.go +++ b/irc/net.go @@ -2,56 +2,68 @@ package irc import ( "bufio" + "io" "log" "net" "strings" ) -func readTrimmedLine(reader *bufio.Reader) (string, error) { - line, err := reader.ReadString('\n') - if err != nil { - return "", err - } - return strings.TrimSpace(line), nil -} - // Adapt `net.Conn` to a `chan string`. func StringReadChan(conn net.Conn) <-chan string { ch := make(chan string) reader := bufio.NewReader(conn) go func() { - defer conn.Close() defer close(ch) for { - line, err := readTrimmedLine(reader) + line, err := reader.ReadString('\n') if err != nil { - log.Printf("%s → %s error: %s", conn.RemoteAddr(), conn.LocalAddr(), err) + if err != io.EOF { + log.Printf("%s → %s error: %s", conn.RemoteAddr(), conn.LocalAddr(), err) + } break } if DEBUG_NET { log.Printf("%s → %s %s", conn.RemoteAddr(), conn.LocalAddr(), line) } - ch <- line + + ch <- strings.TrimSpace(line) } }() return ch } +const ( + CRLF = "\r\n" +) + +func maybeLogWriteError(conn net.Conn, err error) bool { + if err != nil { + if err != io.EOF { + log.Printf("%s ← %s error: %s", conn.RemoteAddr(), conn.LocalAddr(), err) + } + return true + } + return false +} + func StringWriteChan(conn net.Conn) chan<- string { ch := make(chan string) writer := bufio.NewWriter(conn) go func() { - defer conn.Close() defer close(ch) for str := range ch { if DEBUG_NET { log.Printf("%s ← %s %s", conn.RemoteAddr(), conn.LocalAddr(), str) } - if _, err := writer.WriteString(str + "\r\n"); err != nil { - log.Printf("%s ← %s error: %s", conn.RemoteAddr(), conn.LocalAddr(), err) + if _, err := writer.WriteString(str); maybeLogWriteError(conn, err) { + break + } + if _, err := writer.WriteString(CRLF); maybeLogWriteError(conn, err) { + break + } + if err := writer.Flush(); maybeLogWriteError(conn, err) { break } - writer.Flush() } }() return ch diff --git a/irc/reply.go b/irc/reply.go index a27e20ab..013ef65b 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -234,8 +234,8 @@ func RplEndOfWhois(server *Server) Reply { return NewNumericReply(server, RPL_ENDOFWHOIS, ":End of WHOIS list") } -func RplChannelModeIs(server *Server, channel *Channel) Reply { - return NewNumericReply(server, RPL_CHANNELMODEIS, "%s %s", +func RplChannelModeIs(channel *Channel) Reply { + return NewNumericReply(channel.server, RPL_CHANNELMODEIS, "%s %s", channel.name, channel.ModeString()) } @@ -252,6 +252,15 @@ func RplEndOfWho(server *Server, name string) Reply { return NewNumericReply(server, RPL_ENDOFWHO, "%s :End of WHO list", name) } +func RplBanList(channel *Channel, ban UserMask) Reply { + return NewNumericReply(channel.server, RPL_BANLIST, "%s %s", channel.name, ban) +} + +func RplEndOfBanList(channel *Channel) Reply { + return NewNumericReply(channel.server, RPL_ENDOFBANLIST, "%s :End of channel ban list", + channel.name) +} + // errors (also numeric) func ErrAlreadyRegistered(source Identifier) Reply { diff --git a/irc/server.go b/irc/server.go index 2137baf5..090e3af2 100644 --- a/irc/server.go +++ b/irc/server.go @@ -307,8 +307,7 @@ func (msg *ChannelModeCommand) HandleServer(server *Server) { client.replies <- ErrNoSuchChannel(server, msg.channel) return } - - client.replies <- RplChannelModeIs(server, channel) + channel.commands <- msg } func whoChannel(client *Client, server *Server, channel *Channel) { diff --git a/irc/types.go b/irc/types.go new file mode 100644 index 00000000..30966435 --- /dev/null +++ b/irc/types.go @@ -0,0 +1,34 @@ +package irc + +import ( + "fmt" +) + +// simple types + +type ModeOp rune +type UserMode rune +type ChannelMode rune +type UserChannelMode rune +type Mask string + +// interfaces + +type Command interface { + Client() *Client + Source() Identifier + Reply(Reply) + HandleServer(*Server) +} + +// structs + +type UserMask struct { + nickname Mask + username Mask + hostname Mask +} + +func (mask *UserMask) String() string { + return fmt.Sprintf("%s!%s@%s", mask.nickname, mask.username, mask.hostname) +}