swap WHO for NAMES, ignore CAP requests, read NOTICEs, option to toggle autojoin, cosmetic fixes for hexchat

--HG--
branch : nmdc-ircfrontend
This commit is contained in:
. 2016-05-07 17:07:16 +12:00
parent 114823b0cc
commit 07a57777f7
3 changed files with 126 additions and 44 deletions

View File

@ -2,16 +2,26 @@
PRE-RELEASE 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 - test the current password auth
WISHLIST WISHLIST
======== ========
- send client keepalives (PING) /and/ ensure we get a reply (PONG)
- client version sync (CTCP VERSION) - client version sync (CTCP VERSION)
- client descriptions (CTCP USERINFO) - client descriptions (CTCP USERINFO)
@ -34,6 +44,8 @@ WISHLIST
- threadsafe access to libnmdc hub options - threadsafe access to libnmdc hub options
- do we need to send the same payload in our PONG response?

View File

@ -31,9 +31,13 @@ func main() {
dcAddress := flag.String("upstream", "127.0.0.1:411", "Upstream NMDC server") dcAddress := flag.String("upstream", "127.0.0.1:411", "Upstream NMDC server")
serverName := flag.String("servername", "nmdc-ircfrontend", "Server name displayed to clients") serverName := flag.String("servername", "nmdc-ircfrontend", "Server name displayed to clients")
verbose := flag.Bool("verbose", false, "Display debugging information") verbose := flag.Bool("verbose", false, "Display debugging information")
autojoin := flag.Bool("autojoin", true, "Automatically join clients to the channel")
flag.Parse() flag.Parse()
log.Printf("Listening on '%s'...", *ircAddress) log.Printf("Listening on '%s'...", *ircAddress)
if *autojoin {
log.Printf("Clients will be automatically joined to the channel.")
}
listener, err := net.Listen("tcp", *ircAddress) listener, err := net.Listen("tcp", *ircAddress)
if err != nil { if err != nil {
@ -51,6 +55,7 @@ func main() {
// Spin up a worker for the new user. // Spin up a worker for the new user.
server := NewServer(*serverName, libnmdc.HubAddress(*dcAddress), conn) server := NewServer(*serverName, libnmdc.HubAddress(*dcAddress), conn)
server.verbose = *verbose server.verbose = *verbose
server.autojoin = *autojoin
go server.RunWorker() go server.RunWorker()
} }
} }

149
server.go
View File

@ -29,18 +29,27 @@ import (
"time" "time"
) )
type ClientState int
const (
CSUnregistered ClientState = iota
CSRegistered
CSJoined
)
type Server struct { type Server struct {
name string name string
motd string motd string
clientConn net.Conn clientConn net.Conn
clientRegistered bool clientState ClientState
upstreamLauncher libnmdc.HubConnectionOptions upstreamLauncher libnmdc.HubConnectionOptions
upstreamCloser chan struct{} upstreamCloser chan struct{}
upstream *libnmdc.HubConnection upstream *libnmdc.HubConnection
verbose bool verbose bool
autojoin bool
lastMessage string // FIXME racey lastMessage string // FIXME racey
} }
@ -50,9 +59,10 @@ func NewServer(name string, upstream libnmdc.HubAddress, conn net.Conn) *Server
self.ClientTag = APP_DESCRIPTION self.ClientTag = APP_DESCRIPTION
return &Server{ return &Server{
name: name, name: name,
clientConn: conn, clientConn: conn,
motd: "Connected to " + name, clientState: CSUnregistered,
motd: "Connected to " + name + ". You /must/ join " + BLESSED_CHANNEL + " to continue.",
upstreamLauncher: libnmdc.HubConnectionOptions{ upstreamLauncher: libnmdc.HubConnectionOptions{
Address: upstream, Address: upstream,
Self: *self, Self: *self,
@ -120,13 +130,12 @@ func (s *Server) RunWorker() {
s.verboseln("Broken loop.") s.verboseln("Broken loop.")
// Cleanup upstream // Cleanup upstream
if s.clientRegistered { if s.upstream != nil {
s.upstreamCloser <- struct{}{} // always safe to do this /once/ s.upstreamCloser <- struct{}{} // always safe to do this /once/
} }
// Clean up ourselves // Clean up ourselves
s.DisconnectClient() // if not already done s.DisconnectClient() // if not already done
s.clientRegistered = false
} }
func (s *Server) upstreamWorker() { func (s *Server) upstreamWorker() {
@ -158,7 +167,7 @@ func (s *Server) upstreamWorker() {
s.sendChannelTopic(hubEvent.Nick) s.sendChannelTopic(hubEvent.Nick)
case libnmdc.EVENT_PRIVATE: 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: case libnmdc.EVENT_PUBLIC:
if hubEvent.Nick == s.upstreamLauncher.Self.Nick && hubEvent.Message == s.lastMessage { 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: 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) s.reply(rplMsg, "", BLESSED_CHANNEL, hubEvent.Message)
} }
@ -183,12 +193,33 @@ func (s *Server) handleCommand(command string, args []string) {
case "PING": case "PING":
s.reply(rplPong) s.reply(rplPong)
case "PONG":
// do nothing
case "INFO": case "INFO":
s.reply(rplInfo, APP_DESCRIPTION) s.reply(rplInfo, APP_DESCRIPTION)
case "VERSION": case "VERSION":
s.reply(rplVersion, 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": case "PASS":
// RFC2812 registration. Stash the password for later // RFC2812 registration. Stash the password for later
if len(args) < 1 { 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 // This command sets altname, realname, ... none of which we use
// It's the final step in a PASS/NICK/USER login handshake. // 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.reply(rplKill, "You're already registered.", "")
s.DisconnectClient() s.DisconnectClient()
return return
@ -228,18 +259,12 @@ func (s *Server) handleCommand(command string, args []string) {
} }
s.reply(rplWelcome) s.reply(rplWelcome)
s.clientRegistered = true s.clientState = CSRegistered
// Tell the user that they themselves joined the chat channel // TODO tell the client that they /must/ join #chat
s.reply(rplJoin, s.upstreamLauncher.Self.Nick, BLESSED_CHANNEL) if s.autojoin {
s.handleCommand("JOIN", []string{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()
// Send a CTCP VERSION request to the client. If the IRC client can // 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, // 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) { func (s *Server) handleRegisteredCommand(command string, args []string) {
if s.clientRegistered == false { if s.clientState == CSUnregistered {
s.reply(errNotReg) s.reply(errNotReg)
return return
} }
@ -266,9 +291,30 @@ func (s *Server) handleRegisteredCommand(command string, args []string) {
switch args[0] { switch args[0] {
case BLESSED_CHANNEL: 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": 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: default:
s.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.", "") s.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.", "")
s.DisconnectClient() s.DisconnectClient()
@ -283,9 +329,10 @@ func (s *Server) handleRegisteredCommand(command string, args []string) {
if args[0] == BLESSED_CHANNEL { if args[0] == BLESSED_CHANNEL {
// You can check out any time you like, but you can never leave // You can check out any time you like, but you can never leave
s.reply(rplJoin, s.upstreamLauncher.Self.Nick, BLESSED_CHANNEL) s.reply(rplJoin, s.upstreamLauncher.Self.Nick, BLESSED_CHANNEL)
s.sendNames()
} }
case "PRIVMSG": case "PRIVMSG", "NOTICE":
if len(args) < 2 { if len(args) < 2 {
s.reply(errMoreArgs) s.reply(errMoreArgs)
return return
@ -357,24 +404,29 @@ func (s *Server) handleRegisteredCommand(command string, args []string) {
return return
} }
if args[0] == BLESSED_CHANNEL { // Ignore what we're "supposed" to do
// always include ourselves s.sendNames()
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 if args[0] == BLESSED_CHANNEL {
s.reply(rplWho, nick, args[0]) // 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 { s.reply(rplEndOfWho, args[0])
// 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])
case "MODE": case "MODE":
if len(args) < 1 { if len(args) < 1 {
@ -407,10 +459,23 @@ func (s *Server) DisconnectClient() {
s.clientConn.Close() s.clientConn.Close()
} }
s.clientConn = nil s.clientConn = nil
s.clientState = CSUnregistered
// Readloop will stop, which kills the upstream connection too // 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) { func (s *Server) sendChannelTopic(topic string) {
if len(topic) > 0 { if len(topic) > 0 {
s.reply(rplTopic, BLESSED_CHANNEL, s.upstream.HubName) s.reply(rplTopic, BLESSED_CHANNEL, s.upstream.HubName)
@ -485,7 +550,7 @@ func (s *Server) reply(code replyCode, args ...string) {
case rplMOTD: case rplMOTD:
s.writeClient(fmt.Sprintf(":%s 372 %s :- %s", s.name, s.upstreamLauncher.Self.Nick, args[0])) s.writeClient(fmt.Sprintf(":%s 372 %s :- %s", s.name, s.upstreamLauncher.Self.Nick, args[0]))
case rplEndOfMOTD: 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: case rplPong:
s.writeClient(fmt.Sprintf(":%s PONG %s %s", s.name, s.upstreamLauncher.Self.Nick, s.name)) s.writeClient(fmt.Sprintf(":%s PONG %s %s", s.name, s.upstreamLauncher.Self.Nick, s.name))
case errMoreArgs: case errMoreArgs:
@ -503,7 +568,7 @@ func (s *Server) reply(code replyCode, args ...string) {
case errUnknownCommand: case errUnknownCommand:
s.writeClient(fmt.Sprintf(":%s 421 %s %s :Unknown command", s.name, s.upstreamLauncher.Self.Nick, args[0])) s.writeClient(fmt.Sprintf(":%s 421 %s %s :Unknown command", s.name, s.upstreamLauncher.Self.Nick, args[0]))
case errNotReg: 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: case errPassword:
s.writeClient(fmt.Sprintf(":%s 464 %s :Error, password incorrect", s.name, s.upstreamLauncher.Self.Nick)) s.writeClient(fmt.Sprintf(":%s 464 %s :Error, password incorrect", s.name, s.upstreamLauncher.Self.Nick))
case errNoPriv: case errNoPriv: