From 07a57777f739bf660f4de804cc017e7deaaf7030 Mon Sep 17 00:00:00 2001 From: "." <.@.> Date: Sat, 7 May 2016 17:07:16 +1200 Subject: [PATCH] swap WHO for NAMES, ignore CAP requests, read NOTICEs, option to toggle autojoin, cosmetic fixes for hexchat --HG-- branch : nmdc-ircfrontend --- TODO.txt | 16 +++++- main.go | 5 ++ server.go | 149 +++++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 126 insertions(+), 44 deletions(-) diff --git a/TODO.txt b/TODO.txt index 01c2555..9b7a77f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -2,16 +2,26 @@ PRE-RELEASE =========== -- nick list +- hexchat: missing nick list -- part all nicks on server disconnection +- Atomic: fix crash on connection (even with autojoin disabled) + +- yaaic: doesn't recognise the room join (even with autojoin disabled) + +- andchat: WHO spam + +- part all nicks upon upstream server disconnection - test the current password auth + + WISHLIST ======== +- send client keepalives (PING) /and/ ensure we get a reply (PONG) + - client version sync (CTCP VERSION) - client descriptions (CTCP USERINFO) @@ -34,6 +44,8 @@ WISHLIST - threadsafe access to libnmdc hub options +- do we need to send the same payload in our PONG response? + diff --git a/main.go b/main.go index 93d1da6..8c7eccf 100644 --- a/main.go +++ b/main.go @@ -31,9 +31,13 @@ func main() { dcAddress := flag.String("upstream", "127.0.0.1:411", "Upstream NMDC server") serverName := flag.String("servername", "nmdc-ircfrontend", "Server name displayed to clients") verbose := flag.Bool("verbose", false, "Display debugging information") + autojoin := flag.Bool("autojoin", true, "Automatically join clients to the channel") flag.Parse() log.Printf("Listening on '%s'...", *ircAddress) + if *autojoin { + log.Printf("Clients will be automatically joined to the channel.") + } listener, err := net.Listen("tcp", *ircAddress) if err != nil { @@ -51,6 +55,7 @@ func main() { // Spin up a worker for the new user. server := NewServer(*serverName, libnmdc.HubAddress(*dcAddress), conn) server.verbose = *verbose + server.autojoin = *autojoin go server.RunWorker() } } diff --git a/server.go b/server.go index 2063a35..a688d7c 100644 --- a/server.go +++ b/server.go @@ -29,18 +29,27 @@ import ( "time" ) +type ClientState int + +const ( + CSUnregistered ClientState = iota + CSRegistered + CSJoined +) + type Server struct { name string motd string - clientConn net.Conn - clientRegistered bool + clientConn net.Conn + clientState ClientState upstreamLauncher libnmdc.HubConnectionOptions upstreamCloser chan struct{} upstream *libnmdc.HubConnection - verbose bool + verbose bool + autojoin bool lastMessage string // FIXME racey } @@ -50,9 +59,10 @@ func NewServer(name string, upstream libnmdc.HubAddress, conn net.Conn) *Server self.ClientTag = APP_DESCRIPTION return &Server{ - name: name, - clientConn: conn, - motd: "Connected to " + name, + name: name, + clientConn: conn, + clientState: CSUnregistered, + motd: "Connected to " + name + ". You /must/ join " + BLESSED_CHANNEL + " to continue.", upstreamLauncher: libnmdc.HubConnectionOptions{ Address: upstream, Self: *self, @@ -120,13 +130,12 @@ func (s *Server) RunWorker() { s.verboseln("Broken loop.") // Cleanup upstream - if s.clientRegistered { + if s.upstream != nil { s.upstreamCloser <- struct{}{} // always safe to do this /once/ } // Clean up ourselves s.DisconnectClient() // if not already done - s.clientRegistered = false } func (s *Server) upstreamWorker() { @@ -158,7 +167,7 @@ func (s *Server) upstreamWorker() { s.sendChannelTopic(hubEvent.Nick) case libnmdc.EVENT_PRIVATE: - s.reply(rplMsg, s.upstreamLauncher.Self.Nick, hubEvent.Nick, hubEvent.Message) + s.reply(rplMsg, hubEvent.Nick, s.upstreamLauncher.Self.Nick, hubEvent.Message) case libnmdc.EVENT_PUBLIC: if hubEvent.Nick == s.upstreamLauncher.Self.Nick && hubEvent.Message == s.lastMessage { @@ -168,6 +177,7 @@ func (s *Server) upstreamWorker() { } case libnmdc.EVENT_SYSTEM_MESSAGE_FROM_CONN, libnmdc.EVENT_SYSTEM_MESSAGE_FROM_HUB: + // FIXME blank names work well in hexchat, but not in yaaic s.reply(rplMsg, "", BLESSED_CHANNEL, hubEvent.Message) } @@ -183,12 +193,33 @@ func (s *Server) handleCommand(command string, args []string) { case "PING": s.reply(rplPong) + case "PONG": + // do nothing + case "INFO": s.reply(rplInfo, APP_DESCRIPTION) case "VERSION": s.reply(rplVersion, VERSION) + case "MOTD": + s.sendMOTD(s.motd) + + case "CAP": + return + /* + if len(args) < 1 { + s.reply(errMoreArgs) + return + } + + if args[0] == "LS" { + s.writeClient("CAP * LS :nmdc-ircfrontend") // no special IRCv3 capabilities available + } else { + s.writeClient(fmt.Sprintf(":%s 410 * %s :Invalid CAP command", s.name, args[0])) + } + */ + case "PASS": // RFC2812 registration. Stash the password for later if len(args) < 1 { @@ -215,7 +246,7 @@ func (s *Server) handleCommand(command string, args []string) { // This command sets altname, realname, ... none of which we use // It's the final step in a PASS/NICK/USER login handshake. - if s.clientRegistered == true { + if s.clientState != CSUnregistered { s.reply(rplKill, "You're already registered.", "") s.DisconnectClient() return @@ -228,18 +259,12 @@ func (s *Server) handleCommand(command string, args []string) { } s.reply(rplWelcome) - s.clientRegistered = true + s.clientState = CSRegistered - // Tell the user that they themselves joined the chat channel - s.reply(rplJoin, s.upstreamLauncher.Self.Nick, BLESSED_CHANNEL) - - // Send (initially just us) nicklist for the chat channel - s.reply(rplNames, BLESSED_CHANNEL, s.upstreamLauncher.Self.Nick) - s.reply(rplEndOfNames, BLESSED_CHANNEL) - - // Spawn upstream connection - s.upstream = s.upstreamLauncher.Connect() - go s.upstreamWorker() + // TODO tell the client that they /must/ join #chat + if s.autojoin { + s.handleCommand("JOIN", []string{BLESSED_CHANNEL}) + } // Send a CTCP VERSION request to the client. If the IRC client can // supply a client version string, we can replace our tag with it, @@ -252,7 +277,7 @@ func (s *Server) handleCommand(command string, args []string) { func (s *Server) handleRegisteredCommand(command string, args []string) { - if s.clientRegistered == false { + if s.clientState == CSUnregistered { s.reply(errNotReg) return } @@ -266,9 +291,30 @@ func (s *Server) handleRegisteredCommand(command string, args []string) { switch args[0] { case BLESSED_CHANNEL: - // Ignore, but they're already there + if s.clientState != CSJoined { + // Join for the first time + s.clientState = CSJoined + + // Acknowledge + s.reply(rplJoin, s.upstreamLauncher.Self.Nick, BLESSED_CHANNEL) + + // Send (initially just us) nicklist for the chat channel + s.sendNames() + + // Spawn upstream connection + s.upstream = s.upstreamLauncher.Connect() + go s.upstreamWorker() + + } else { + // They're already here, ignore + // Can happen if autojoin is enabled but the client already requested a login + } + case "0": - // Ignore, Intend to quit all channels but we don't allow that + // Quitting all channels? Drop client + s.reply(rplKill, "Bye.", "") + s.DisconnectClient() + default: s.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.", "") s.DisconnectClient() @@ -283,9 +329,10 @@ func (s *Server) handleRegisteredCommand(command string, args []string) { if args[0] == BLESSED_CHANNEL { // You can check out any time you like, but you can never leave s.reply(rplJoin, s.upstreamLauncher.Self.Nick, BLESSED_CHANNEL) + s.sendNames() } - case "PRIVMSG": + case "PRIVMSG", "NOTICE": if len(args) < 2 { s.reply(errMoreArgs) return @@ -357,24 +404,29 @@ func (s *Server) handleRegisteredCommand(command string, args []string) { return } - if args[0] == BLESSED_CHANNEL { - // always include ourselves - s.reply(rplWho, s.upstreamLauncher.Self.Nick, args[0]) + // Ignore what we're "supposed" to do + s.sendNames() - for nick, _ := range s.upstream.Users { - if nick != s.upstreamLauncher.Self.Nick { // but don't repeat ourselves - s.reply(rplWho, nick, args[0]) + /* + if args[0] == BLESSED_CHANNEL { + // always include ourselves + s.reply(rplWho, s.upstreamLauncher.Self.Nick, args[0]) + + for nick, _ := range s.upstream.Users { + if nick != s.upstreamLauncher.Self.Nick { // but don't repeat ourselves + s.reply(rplWho, nick, args[0]) + } + } + } else { + // argument is a filter + for nick, _ := range s.upstream.Users { + if strings.Contains(nick, args[0]) { + s.reply(rplWho, nick, args[0]) + } } } - } else { - // argument is a filter - for nick, _ := range s.upstream.Users { - if strings.Contains(nick, args[0]) { - s.reply(rplWho, nick, args[0]) - } - } - } - s.reply(rplEndOfWho, args[0]) + s.reply(rplEndOfWho, args[0]) + */ case "MODE": if len(args) < 1 { @@ -407,10 +459,23 @@ func (s *Server) DisconnectClient() { s.clientConn.Close() } s.clientConn = nil + s.clientState = CSUnregistered // Readloop will stop, which kills the upstream connection too } +func (s *Server) sendNames() { + names := s.upstreamLauncher.Self.Nick + if s.upstream != nil { + for nick, _ := range s.upstream.Users { + names += " " + nick + } + } + + s.reply(rplNames, BLESSED_CHANNEL, names) + s.reply(rplEndOfNames, BLESSED_CHANNEL) +} + func (s *Server) sendChannelTopic(topic string) { if len(topic) > 0 { s.reply(rplTopic, BLESSED_CHANNEL, s.upstream.HubName) @@ -485,7 +550,7 @@ func (s *Server) reply(code replyCode, args ...string) { case rplMOTD: s.writeClient(fmt.Sprintf(":%s 372 %s :- %s", s.name, s.upstreamLauncher.Self.Nick, args[0])) case rplEndOfMOTD: - s.writeClient(fmt.Sprintf(":%s 376 %s :End of MOTD Command", s.name, s.upstreamLauncher.Self.Nick)) + s.writeClient(fmt.Sprintf(":%s 376 %s :- End of MOTD", s.name, s.upstreamLauncher.Self.Nick)) case rplPong: s.writeClient(fmt.Sprintf(":%s PONG %s %s", s.name, s.upstreamLauncher.Self.Nick, s.name)) case errMoreArgs: @@ -503,7 +568,7 @@ func (s *Server) reply(code replyCode, args ...string) { case errUnknownCommand: s.writeClient(fmt.Sprintf(":%s 421 %s %s :Unknown command", s.name, s.upstreamLauncher.Self.Nick, args[0])) case errNotReg: - s.writeClient(fmt.Sprintf(":%s 451 :You have not registered", s.name)) + s.writeClient(fmt.Sprintf(":%s 451 :- You have not registered", s.name)) case errPassword: s.writeClient(fmt.Sprintf(":%s 464 %s :Error, password incorrect", s.name, s.upstreamLauncher.Self.Nick)) case errNoPriv: