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:
parent
114823b0cc
commit
07a57777f7
16
TODO.txt
16
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?
|
||||
|
||||
|
||||
|
||||
|
||||
|
5
main.go
5
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()
|
||||
}
|
||||
}
|
||||
|
149
server.go
149
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:
|
||||
|
Loading…
Reference in New Issue
Block a user