diff --git a/ergonomadic.conf b/ergonomadic.conf index c638d3dd..fd08218f 100644 --- a/ergonomadic.conf +++ b/ergonomadic.conf @@ -9,3 +9,6 @@ password = "JDJhJDA0JHJzVFFlNXdOUXNhLmtkSGRUQVVEVHVYWXRKUmdNQ3FKVTRrczRSMTlSWGRP [operator "root"] password = "JDJhJDA0JEhkcm10UlNFRkRXb25iOHZuSDVLZXVBWlpyY0xyNkQ4dlBVc1VMWVk1LlFjWFpQbGxZNUtl" ; 'toor' + +[theater "#ghostbusters"] +password = "JDJhJDA0JG0yY1h4cTRFUHhkcjIzN2p1M2Nvb2VEYjAzSHh4eTB3YkZ0VFRLV1ZPVXdqeFBSRUtmRlBT" ; 'venkman' diff --git a/irc/channel.go b/irc/channel.go index fa18b62d..a771b8a0 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -6,14 +6,15 @@ import ( ) type Channel struct { - flags ChannelModeSet - lists map[ChannelMode]*UserMaskSet - key Text - members MemberSet - name Name - server *Server - topic Text - userLimit uint64 + flags ChannelModeSet + lists map[ChannelMode]*UserMaskSet + key Text + members MemberSet + name Name + server *Server + topic Text + userLimit uint64 + theaterUser *Client } // NewChannel creates a new channel from a `Server` and a `name` @@ -405,9 +406,11 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo return channel.applyModeMember(client, change.mode, change.op, NewName(change.arg)) + case Theater: + client.ErrConfiguredMode(change.mode) + default: client.ErrUnknownMode(change.mode, channel) - return false } return false } diff --git a/irc/client.go b/irc/client.go index 4e076b54..26634405 100644 --- a/irc/client.go +++ b/irc/client.go @@ -13,27 +13,28 @@ const ( ) type Client struct { - atime time.Time - authorized bool - awayMessage Text - capabilities CapabilitySet - capState CapState - channels ChannelSet - commands chan Command - ctime time.Time - flags map[UserMode]bool - hasQuit bool - hops uint - hostname Name - idleTimer *time.Timer - loginTimer *time.Timer - nick Name - quitTimer *time.Timer - realname Text - registered bool - server *Server - socket *Socket - username Name + atime time.Time + authorized bool + awayMessage Text + capabilities CapabilitySet + capState CapState + channels ChannelSet + commands chan Command + ctime time.Time + flags map[UserMode]bool + hasQuit bool + hops uint + hostname Name + idleTimer *time.Timer + loginTimer *time.Timer + nick Name + quitTimer *time.Timer + realname Text + registered bool + server *Server + socket *Socket + username Name + theaterChannels []*Channel } func NewClient(server *Server, conn net.Conn) *Client { @@ -259,6 +260,10 @@ func (client *Client) Quit(message Text) { return } + for _, channel := range client.theaterChannels { + delete(channel.flags, Theater) + } + client.Reply(RplError("connection closed")) client.hasQuit = true client.server.whoWas.Append(client) diff --git a/irc/commands.go b/irc/commands.go index 6c2e053a..c9a4550c 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -49,6 +49,7 @@ var ( PRIVMSG: NewPrivMsgCommand, PROXY: NewProxyCommand, QUIT: NewQuitCommand, + THEATER: NewTheaterCommand, // nonstandard TIME: NewTimeCommand, TOPIC: NewTopicCommand, USER: NewUserCommand, @@ -947,6 +948,31 @@ func NewInviteCommand(args []string) (Command, error) { }, nil } +func NewTheaterCommand(args []string) (Command, error) { + if len(args) < 1 { + return nil, NotEnoughArgsError + } else if upperSubCmd := strings.ToUpper(args[0]); upperSubCmd == "IDENTIFY" && len(args) == 3 { + return &TheaterIdentifyCommand{ + channel: NewName(args[1]), + PassCommand: PassCommand{password: []byte(args[2])}, + }, nil + } else if upperSubCmd == "PRIVMSG" && len(args) == 4 { + return &TheaterPrivMsgCommand{ + channel: NewName(args[1]), + asNick: NewName(args[2]), + message: NewText(args[3]), + }, nil + } else if upperSubCmd == "ACTION" && len(args) == 4 { + return &TheaterActionCommand{ + channel: NewName(args[1]), + asNick: NewName(args[2]), + action: NewText(args[3]), + }, nil + } else { + return nil, ErrParseCommand + } +} + type TimeCommand struct { BaseCommand target Name diff --git a/irc/config.go b/irc/config.go index 69bec631..5a3c2e44 100644 --- a/irc/config.go +++ b/irc/config.go @@ -29,6 +29,8 @@ type Config struct { } Operator map[string]*PassConfig + + Theater map[string]*PassConfig } func (conf *Config) Operators() map[Name][]byte { @@ -39,6 +41,18 @@ func (conf *Config) Operators() map[Name][]byte { return operators } +func (conf *Config) Theaters() map[Name][]byte { + theaters := make(map[Name][]byte) + for s, theaterConf := range conf.Theater { + name := NewName(s) + if !name.IsChannel() { + log.Fatal("config uses a non-channel for a theater!") + } + theaters[name] = theaterConf.PasswordBytes() + } + return theaters +} + func LoadConfig(filename string) (config *Config, err error) { config = &Config{} err = gcfg.ReadFileInto(config, filename) diff --git a/irc/constants.go b/irc/constants.go index 0fc6b0d1..955f3303 100644 --- a/irc/constants.go +++ b/irc/constants.go @@ -30,6 +30,7 @@ const ( PRIVMSG StringCode = "PRIVMSG" PROXY StringCode = "PROXY" QUIT StringCode = "QUIT" + THEATER StringCode = "THEATER" // nonstandard TIME StringCode = "TIME" TOPIC StringCode = "TOPIC" USER StringCode = "USER" diff --git a/irc/modes.go b/irc/modes.go index 7dfb3c08..44dd1607 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -83,6 +83,7 @@ const ( Quiet ChannelMode = 'q' // flag ReOp ChannelMode = 'r' // flag Secret ChannelMode = 's' // flag, deprecated + Theater ChannelMode = 'T' // flag arg, nonstandard UserLimit ChannelMode = 'l' // flag arg Voice ChannelMode = 'v' // arg ) @@ -90,7 +91,7 @@ const ( var ( SupportedChannelModes = ChannelModes{ BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside, - OpOnlyTopic, Persistent, Private, UserLimit, + OpOnlyTopic, Persistent, Private, Theater, UserLimit, } ) diff --git a/irc/reply.go b/irc/reply.go index 695ac9e5..040aedbf 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -548,6 +548,11 @@ func (target *Client) ErrUnknownMode(mode ChannelMode, channel *Channel) { "%s :is unknown mode char to me for %s", mode, channel) } +func (target *Client) ErrConfiguredMode(mode ChannelMode) { + target.NumericReply(ERR_UNKNOWNMODE, + "%s :can only change this mode in daemon configuration", mode) +} + func (target *Client) ErrChannelIsFull(channel *Channel) { target.NumericReply(ERR_CHANNELISFULL, "%s :Cannot join channel (+l)", channel) diff --git a/irc/server.go b/irc/server.go index 927d9b31..57f53d13 100644 --- a/irc/server.go +++ b/irc/server.go @@ -40,6 +40,7 @@ type Server struct { password []byte signals chan os.Signal whoWas *WhoWasList + theaters map[Name][]byte } var ( @@ -61,6 +62,7 @@ func NewServer(config *Config) *Server { operators: config.Operators(), signals: make(chan os.Signal, len(SERVER_SIGNALS)), whoWas: NewWhoWasList(100), + theaters: config.Theaters(), } if config.Server.Password != "" { diff --git a/irc/theater.go b/irc/theater.go new file mode 100644 index 00000000..43f1d8d0 --- /dev/null +++ b/irc/theater.go @@ -0,0 +1,121 @@ +package irc + +import ( + "fmt" +) + +type TheaterClient Name + +func (c TheaterClient) Id() Name { + return Name(c) +} + +func (c TheaterClient) Nick() Name { + return Name(c) +} + +type TheaterSubCommand string + +type theaterSubCommand interface { + String() string +} + +type TheaterIdentifyCommand struct { + PassCommand + channel Name +} + +func (m *TheaterIdentifyCommand) LoadPassword(s *Server) { + m.hash = s.theaters[m.channel] +} + +func (cmd *TheaterIdentifyCommand) String() string { + return fmt.Sprintf("THEATER_IDENTIFY(channel=%s)", cmd.channel) +} + +func (m *TheaterIdentifyCommand) HandleServer(s *Server) { + client := m.Client() + if !m.channel.IsChannel() { + client.ErrNoSuchChannel(m.channel) + return + } + + channel := s.channels.Get(m.channel) + if channel == nil { + client.ErrNoSuchChannel(m.channel) + return + } + + if (m.hash == nil) || (m.err != nil) { + client.ErrPasswdMismatch() + return + } + + if channel.theaterUser == nil { + client.theaterChannels = append(client.theaterChannels, channel) + channel.flags[Theater] = true + channel.theaterUser = client + } +} + +type TheaterPrivMsgCommand struct { + BaseCommand + channel Name + asNick Name + message Text +} + +func (cmd *TheaterPrivMsgCommand) String() string { + return fmt.Sprintf("THEATER_PRIVMSG(channel=%s, asNick=%s, message=%s)", cmd.channel, cmd.asNick, cmd.message) + +} +func (m *TheaterPrivMsgCommand) HandleServer(s *Server) { + client := m.Client() + if !m.channel.IsChannel() { + client.ErrNoSuchChannel(m.channel) + return + } + + channel := s.channels.Get(m.channel) + if channel == nil { + client.ErrNoSuchChannel(m.channel) + return + } + + if channel.theaterUser == client { + for member := range channel.members { + member.Reply(RplPrivMsg(TheaterClient(m.asNick), channel, m.message)) + } + } +} + +type TheaterActionCommand struct { + BaseCommand + channel Name + asNick Name + action Text +} + +func (cmd *TheaterActionCommand) String() string { + return fmt.Sprintf("THEATER_ACTION(channel=%s, asNick=%s, action=%s)", cmd.channel, cmd.asNick, cmd.action) +} + +func (m *TheaterActionCommand) HandleServer(s *Server) { + client := m.Client() + if m.channel.IsChannel() { + client.ErrNoSuchChannel(m.channel) + return + } + + channel := s.channels.Get(m.channel) + if channel == nil { + client.ErrNoSuchChannel(m.channel) + return + } + + if channel.theaterUser == client { + for member := range channel.members { + member.Reply(RplPrivMsg(TheaterClient(m.asNick), channel, NewText(fmt.Sprintf("\001ACTION %s\001", m.action)))) + } + } +}