59 Commits

Author SHA1 Message Date
d88c6248b8 doc: 1.3.0 changelog
--HG--
branch : nmdc-ircfrontend
2018-03-24 16:56:51 +13:00
4a34ccce26 doc: update compatibility list
--HG--
branch : nmdc-ircfrontend
2018-03-24 16:20:55 +13:00
9d9fcef954 change "end of NAMES list" message to more closely match RocketChat's regexp
--HG--
branch : nmdc-ircfrontend
2018-03-24 16:20:48 +13:00
2d19acb846 remove sendNamesOnWho quirk, use nickForGeneralMessages quirk on Yaaic as well as atomic
--HG--
branch : nmdc-ircfrontend
2018-03-24 15:40:20 +13:00
e993e8a32c dev server version is 0.0, not 1.x.x-dev (easier numeric handling)
--HG--
branch : nmdc-ircfrontend
2018-03-24 15:36:56 +13:00
541cbaabd2 client tags: assume atomic/yaaic are the latest abandonware versions
--HG--
branch : nmdc-ircfrontend
2018-03-24 15:36:41 +13:00
86ad1e1633 doc: update README
--HG--
branch : nmdc-ircfrontend
2018-03-24 15:25:09 +13:00
19e0120423 doc: fix bad numeric version for irssi, add clientTag test
--HG--
branch : nmdc-ircfrontend
2018-03-24 15:24:52 +13:00
e9ad8f6962 doc: update help text
--HG--
branch : nmdc-ircfrontend
2018-03-24 15:16:30 +13:00
ef36fc0c24 client tags: better detection for Revolution IRC and HoloIRC
--HG--
branch : nmdc-ircfrontend
2018-03-24 15:16:22 +13:00
458ab0a054 never send blank nicks (FIXES yaaic/atomic, Revolution IRC)
--HG--
branch : nmdc-ircfrontend
2018-03-24 15:04:09 +13:00
0e7e2b629c move chatformatting, quirks to separate package
--HG--
branch : nmdc-ircfrontend
2018-03-24 14:37:40 +13:00
371a7eb9e4 use full syntax for joins/parts (fixes userlist in liteirc)
--HG--
branch : nmdc-ircfrontend
2018-03-24 14:24:44 +13:00
485f68f5d1 move client tag parsing to separate file, add unit tests
--HG--
branch : nmdc-ircfrontend
2018-03-24 14:24:28 +13:00
d465d742c6 update help text to mention ADC support
--HG--
branch : nmdc-ircfrontend
2018-03-24 14:02:36 +13:00
37b62ff817 bump copyright year
--HG--
branch : nmdc-ircfrontend
2018-03-24 14:02:20 +13:00
fa13755311 libnmdc compatibility fixes (now requires 0.17++)
--HG--
branch : nmdc-ircfrontend
2018-03-24 13:22:34 +13:00
e83e2dc111 Added tag release-1.2.3 for changeset 129377245f80
--HG--
branch : nmdc-ircfrontend
2017-05-28 17:44:23 +12:00
a555dbd563 readme
--HG--
branch : nmdc-ircfrontend
2017-05-28 17:43:52 +12:00
f076aeaeda quirks support
--HG--
branch : nmdc-ircfrontend
2017-05-28 17:36:31 +12:00
1056493211 bump copyright year
--HG--
branch : nmdc-ircfrontend
2017-05-28 13:45:05 +12:00
ddcd65fa47 Added tag release-1.2.2 for changeset 111d6e41507d
--HG--
branch : nmdc-ircfrontend
2017-05-28 13:39:48 +12:00
d21b18fc75 fix bad patch for bounds check in previous
--HG--
branch : nmdc-ircfrontend
2017-05-28 13:38:56 +12:00
53b72e0cb0 readme
--HG--
branch : nmdc-ircfrontend
2017-05-27 14:00:25 +12:00
839dea016a chmod +x godist.sh
--HG--
branch : nmdc-ircfrontend
2017-05-27 13:58:12 +12:00
e235ee014e fix wrong bounds check causing panic when server gets scanned by weird clients
--HG--
branch : nmdc-ircfrontend
2017-05-27 13:57:37 +12:00
.
c54a271f17 Added tag release-1.2.1 for changeset 49dcc63e80e9
--HG--
branch : nmdc-ircfrontend
2016-11-29 20:21:29 +13:00
.
264ee8c61c readme
--HG--
branch : nmdc-ircfrontend
2016-11-29 20:21:22 +13:00
.
85e44756e3 libnmdc compatibility updates
--HG--
branch : nmdc-ircfrontend
2016-11-29 19:58:12 +13:00
.
e628bdcf91 update libnmdc import path
--HG--
branch : nmdc-ircfrontend
2016-11-29 19:55:50 +13:00
.
f929379569 fix build script not overwriting app_version
--HG--
branch : nmdc-ircfrontend
2016-08-27 17:53:04 +12:00
.
d5f331ec7c Added tag release-1.2.0 for changeset 3586b48a5abf
--HG--
branch : nmdc-ircfrontend
2016-08-27 17:42:34 +12:00
.
a609996484 readme
--HG--
branch : nmdc-ircfrontend
2016-08-27 17:42:24 +12:00
.
0ec1c20d7e whois: better output format
--HG--
branch : nmdc-ircfrontend
2016-08-27 15:27:50 +12:00
.
3cffacaa45 fixes for 3d8672ff5b9c
--HG--
branch : nmdc-ircfrontend
2016-08-27 14:32:16 +12:00
.
e0e5c742e3 add -version option to display version and quit
--HG--
branch : nmdc-ircfrontend
2016-08-27 14:20:27 +12:00
.
7494823b07 implant version number at build time
--HG--
branch : nmdc-ircfrontend
2016-08-27 14:18:20 +12:00
.
7b2ab6642b WHOIS support (needs testing)
--HG--
branch : nmdc-ircfrontend
2016-08-27 14:07:02 +12:00
.
f1051fbfc2 doc: update todo
--HG--
branch : nmdc-ircfrontend
2016-08-27 14:06:27 +12:00
.
73a4134b18 readme: demote liteirc to with-bugs section
--HG--
branch : nmdc-ircfrontend
2016-08-27 13:21:24 +12:00
.
962cc8dea0 reuse const
--HG--
branch : nmdc-ircfrontend
2016-08-27 12:38:21 +12:00
.
9bde052d9d todo
--HG--
branch : nmdc-ircfrontend
2016-05-10 19:45:28 +12:00
.
eabae2ce9b readme
--HG--
branch : nmdc-ircfrontend
2016-05-10 19:21:28 +12:00
.
10f4db39cc Added tag release-1.1.0 for changeset 34892054c343
--HG--
branch : nmdc-ircfrontend
2016-05-10 19:18:21 +12:00
.
6a7847f71b preparations for 1.1.0 release
--HG--
branch : nmdc-ircfrontend
2016-05-10 19:16:39 +12:00
.
7592bf6002 fix colon in mirc when renaming self
--HG--
branch : nmdc-ircfrontend
2016-05-10 19:08:26 +12:00
.
fb34cefc46 fix crash on renaming self
--HG--
branch : nmdc-ircfrontend
2016-05-10 19:08:19 +12:00
.
0df6237005 strip leading v from mirc version
--HG--
branch : nmdc-ircfrontend
2016-05-10 18:56:32 +12:00
.
c35062a3df support changing nicks (by reconnecting)
--HG--
branch : nmdc-ircfrontend
2016-05-09 18:57:04 +12:00
.
74e4f6cfa3 move mutex calls to outer function
--HG--
branch : nmdc-ircfrontend
2016-05-09 18:49:00 +12:00
.
7bf832322d hubsec nick opt, ctcp before join, delay join until ctcp version (with timeout), mirc/atomic-specific hack
--HG--
branch : nmdc-ircfrontend
2016-05-09 18:48:07 +12:00
.
169debea93 set proper clientversion, better version/tag extraction
--HG--
branch : nmdc-ircfrontend
2016-05-09 18:30:24 +12:00
.
9c2cad7986 doc update
--HG--
branch : nmdc-ircfrontend
2016-05-08 18:39:21 +12:00
.
32296eeffa doc update
--HG--
branch : nmdc-ircfrontend
2016-05-08 15:48:40 +12:00
.
3a00df8ef4 bugreport for mIRC 7
--HG--
branch : nmdc-ircfrontend
2016-05-08 15:26:05 +12:00
.
a948b457bd update readme
--HG--
branch : nmdc-ircfrontend
2016-05-08 14:51:06 +12:00
.
11dbafe387 update readme
--HG--
branch : nmdc-ircfrontend
2016-05-08 14:49:36 +12:00
.
a72f7be558 move readme file
--HG--
branch : nmdc-ircfrontend
2016-05-08 14:43:42 +12:00
.
637bfd6c78 Added tag release-1.0.0 for changeset da295cede46d
--HG--
branch : nmdc-ircfrontend
2016-05-08 14:42:50 +12:00
11 changed files with 527 additions and 180 deletions

6
.hgtags Normal file
View File

@@ -0,0 +1,6 @@
da295cede46d95848348292e04e54fa5a5713ae3 release-1.0.0
34892054c34384edeafa2b04a483697d7d8a73a3 release-1.1.0
3586b48a5abfdbdeef310f2e154b06f4d16d38bb release-1.2.0
49dcc63e80e98f8c2ce3bb029fe0c41a6426678f release-1.2.1
111d6e41507dd0f374860b936d18a651a7cb09ce release-1.2.2
129377245f8026414df6abc3f4292c10b30f0618 release-1.2.3

View File

@@ -1,4 +1,13 @@
BUGS
====
- /wrong/ password message shows up, but not if no password was given for a passworded nick
- tags not showing up in ncdc
WISHLIST WISHLIST
======== ========
@@ -6,19 +15,12 @@ WISHLIST
- automatic markdown bold/italic formatting - automatic markdown bold/italic formatting
- support changing nick (via reconnecting)
- support WHOIS
- respond to CTCP VERSION with the clienttag on behalf of other users
- support USERIP/KILL/KICK for ops - support USERIP/KILL/KICK for ops
- use CTCP chat to support irc-special characters in chat messages (colon, newline) - use CTCP chat to support irc-special characters in chat messages (colon, newline)
WONTFIX WONTFIX
======= =======

View File

@@ -8,7 +8,9 @@ TLS (SSL) support is not integrated. To host the IRC frontend over TLS, please u
This program uses some code from the AGPLv3 project https://github.com/eXeC64/Rosella , from which it inherits the Affero GPLv3 license. Anyone hosting a modified version of this software is required to release their changes under the terms of the AGPLv3. This program uses some code from the AGPLv3 project https://github.com/eXeC64/Rosella , from which it inherits the Affero GPLv3 license. Anyone hosting a modified version of this software is required to release their changes under the terms of the AGPLv3.
Tags: NMDC AGPLv3 Written in golang
Tags: nmdc, AGPLv3
=FEATURES= =FEATURES=
@@ -18,6 +20,7 @@ Tags: NMDC AGPLv3
- NMDC server's title exposed as IRC channel topic - NMDC server's title exposed as IRC channel topic
- Login with passworded nick (classic `PASS`, not `SASL`) - Login with passworded nick (classic `PASS`, not `SASL`)
- Full nick list integration including op status - Full nick list integration including op status
- Support IRC client changing nick via NMDC upstream reconnection
- Automatic join to single-enforced chatroom - Automatic join to single-enforced chatroom
- Multithreaded - Multithreaded
- Single binary deployment - Single binary deployment
@@ -29,35 +32,74 @@ Tags: NMDC AGPLv3
Automatically join clients to the channel (default true) Automatically join clients to the channel (default true)
-bind string -bind string
The address:port to bind to and listen for clients on (default ":6667") The address:port to bind to and listen for clients on (default ":6667")
-hubsecurity string
Nick used for administrative events (default "Hub-Security")
-servername string -servername string
Server name displayed to clients (default "nmdc-ircfrontend") Server name displayed to clients (default "nmdc-ircfrontend")
-upstream string -upstream string
Upstream NMDC server (default "127.0.0.1:411") Upstream NMDC/ADC server (default "127.0.0.1:411")
-verbose -verbose
Display debugging information` Display debugging information`
=COMPATIBILITY= =COMPATIBILITY=
*This section was last updated on or around the release of 1.3.0. Current compatibility may differ.*
NMDC's smaller community has standardised around comparatively few protocol implementations by means of necessity. In comparison, there are a lot of IRC client implementations with slightly differing interpretations of the protocol. NMDC's smaller community has standardised around comparatively few protocol implementations by means of necessity. In comparison, there are a lot of IRC client implementations with slightly differing interpretations of the protocol.
[b]Everything works:[/b] For passworded DC usernames, your IRC PASS username must be the same as your IRC nickname.
- Hexchat
- Mango IRC Tested working:
- AndChat
- AndroIRC - AndroIRC
- Lite IRC - Atomic
- Hexchat
- HoloIRC
- Irssi
- LiteIRC
- Mango IRC
- mIRC 7
- Mutter - Mutter
- Revolution IRC
- Weechat - Weechat
- Yaaic
[b]Usable, with bugs:[/b] Unusable:
- HoloIRC - Can't parse client tag, upstream bug https://github.com/tilal6991/HoloIRC/issues/140 - Rocket.chat 0.6x IRC plugin (can join room and recieve PMs after some regex tweaking)
- AndChat - Duplicate usernames appear, upstream bug https://github.com/znc/znc/issues/424
- Irssi - Ignorable warning "critical nicklist_set_host: assertion 'host != NULL' failed"
[b]Unusable:[/b]
- Yaaic - doesn't properly understand/parse the room join
- Atomic - doesn't properly understand/parse the room join, crashes on `PRIVMSG` with blank sender
=CHANGELOG= =CHANGELOG=
2018-03-24 1.3.0
- Feature: Support ADC hubs
- Compatibility: Fix missing userlist in LiteIRC (since 1.2.2)
- Compatibility: Avoid sending empty room list (fixes Yaaic, atomic, Revolution IRC)
- Update libnmdc to 0.17
2017-05-28 1.2.3
- Fix a regression with userlist display on HexChat (other IRC client compatibility unknown)
2017-05-27 1.2.2
- Update libnmdc to 0.14
- Fix a crash that could occur if the server is scanned by a non-irc client
2016-11-29 1.2.1
- Update libnmdc to 0.11
- Fix an issue with -devel version tag in 1.2.0 release binaries
2016-08-27 1.2.0
- Feature: Support WHOIS (display NMDC user's description + client software)
- Feature: `-version` command-line option
- Compatibility: Demote 'Lite IRC' to 'Usable with bugs' section
- Update libnmdc to r9 (fix protocol issues)
- Update golang to 1.7 (smaller binary size)
2016-05-10 1.1.0
- Feature: Support renaming own client during connection (`/nick`)
- Enhancement: Option to set Hub-Security nick (needed for initial CTCP, upgraded after upstream connection)
- Compatibility: Apply user-agent-specific hacks for mIRC and Atomic clients not nicely handling PRIVMSG with blank sender
- Compatibility: Better heuristic detection for client tag extraction
- Fix an issue causing the default client tag (nmdc-ircfrontend) to briefly appear in the NMDC user list, by (briefly) delaying the upstream join unless CTCP VERSION response comes in
- Fix an issue generating malformed version numbers in NMDC client tag
2016-05-08 1.0.0 2016-05-08 1.0.0
- Initial public release - Initial public release

33
chatFormatting.go Normal file
View File

@@ -0,0 +1,33 @@
package main
import (
"regexp"
"strings"
)
var (
rx_colorControlCode *regexp.Regexp = regexp.MustCompilePOSIX("\x03[0-9]+(,[0-9]+)?")
rx_formattingControlCode *regexp.Regexp = regexp.MustCompile("(\x02|\x1D|\x1F|\x16|\x0F)")
)
func reformatOutgoingMessageBody(body string) string {
// Strip color codes
noColors := rx_colorControlCode.ReplaceAllString(body, "")
// Strip formatting codes
return rx_formattingControlCode.ReplaceAllString(noColors, "")
}
func reformatIncomingMessageBody(body string) string {
// "Greentext" filter
// TODO markdown filters
if len(body) > 0 && body[0] == '>' {
return "\x033" + strings.Replace(body, "implying", "\x02implying\x02", -1)
} else {
return body
}
}

68
clientTag.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import (
"regexp"
"strings"
)
type clientTag struct {
AppName string
Version string
}
var (
rx_bestNumberPart = regexp.MustCompile(`[0-9\.]+`)
)
func parseVersion(ver string) clientTag {
// Try our best to turn the supplied text into a structured version
ret := clientTag{
AppName: APP_NAME,
Version: APP_VERSION,
}
// Special case: Some clients use a structured version AppName:Version:Metadata
// If we check for that, we can support clients with spaces in the name
if cParts := strings.Split(ver, ":"); len(cParts) == 3 {
ret.AppName = cParts[0]
ret.Version = cParts[1]
} else if ver == "Atomic - An IRC client for Android https://indrora.github.io/Atomic" {
ret.AppName = "Atomic"
ret.Version = "2.1" // Abandonware. Last available version
} else if ver == "Yaaic - Yet Another Android IRC Client - http://www.yaaic.org" {
ret.AppName = "Yaaic"
ret.Version = "1.1" // Abandonware. Last available version
} else {
// Special cases all failed, time for heuristics
// Turn colons to spaces; keep the first word, and the the first word containing digits
ver = strings.Trim(strings.Replace(ver, ":", " ", -1), " ")
words := strings.Split(ver, " ")
for _, word := range words[1:] {
if strings.ContainsAny(word, "0123456789") {
ret.AppName = words[0]
ret.Version = strings.Replace(word, "(", "", -1) // AndroIRC
break
}
}
}
// We only support digits, periods, and hyphens in the number part
// This removes the leading v from mIRC and the trailing deb** from irssi
if submatch := rx_bestNumberPart.FindStringSubmatch(ret.Version); len(submatch) == 1 {
ret.Version = submatch[0]
}
// Special case: "Relay" means "HoloIRC"
if ret.AppName == "Relay" && ret.Version == "1.0" && strings.Contains(ver, "Android") {
ret.AppName = "HoloIRC"
ret.Version = "4"
}
return ret
}

73
clientTag_test.go Normal file
View File

@@ -0,0 +1,73 @@
package main
import (
"testing"
)
func TestParseVersion(t *testing.T) {
tests := [][3]string{
[3]string{
"HexChat 2.12.1 [x64] / Microsoft Windows 10 Pro (x64) [Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz (3.60GHz)]",
"HexChat", "2.12.1",
},
[3]string{
"AndroIRC - Android IRC Client (5.2 - Build 6830152) - http://www.androirc.com",
"AndroIRC", "5.2",
},
[3]string{
"AndChat 1.4.3.2 http://www.andchat.ne",
"AndChat", "1.4.3.2",
},
[3]string{
"liteIRC for Android 1.1.8",
"liteIRC", "1.1.8",
},
[3]string{
":Relay:1.0:Android", // HoloIRC before 4.1.0
"HoloIRC", "4",
},
[3]string{
"Relay:1.0:Android", // HoloIRC after 4.1.0
"HoloIRC", "4",
},
[3]string{
"mIRC v7.45",
"mIRC", "7.45",
},
[3]string{
"Revolution IRC:0.3.2:Android",
"Revolution IRC", "0.3.2",
},
[3]string{
"irssi v1.0.2-1+deb9u3",
"irssi", "1.0.2",
},
[3]string{
"Atomic - An IRC client for Android https://indrora.github.io/Atomic",
"Atomic", "2.1",
},
[3]string{
"Yaaic - Yet Another Android IRC Client - http://www.yaaic.org",
"Yaaic", "1.1",
},
}
for _, test := range tests {
result := parseVersion(test[0])
if result.AppName != test[1] || result.Version != test[2] {
t.Fatalf("Got <%s,%s> expecting <%s,%s>\n", result.AppName, result.Version, test[1], test[2])
}
}
}

2
godist.sh Normal file → Executable file
View File

@@ -97,7 +97,7 @@ single_build() {
# Build. # Build.
# GOARCH/GOOS supplied in function env # GOARCH/GOOS supplied in function env
go build -a -ldflags '-s -w' -o "$(pathfix "${tmpdir}/${local_bin_name}")" go build -a -ldflags "-s -w -X main.APP_VERSION=${version}" -o "$(pathfix "${tmpdir}/${local_bin_name}")"
# Sanitise. # Sanitise.
sanitise "${tmpdir}/${local_bin_name}" sanitise "${tmpdir}/${local_bin_name}"

18
main.go
View File

@@ -1,7 +1,7 @@
package main package main
/* /*
Copyright (C) 2016 The `nmdc-ircfrontend' author(s) Copyright (C) 2016-2018 The `nmdc-ircfrontend' author(s)
Copyright (C) 2013 Harry Jeffery Copyright (C) 2013 Harry Jeffery
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@@ -20,20 +20,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import ( import (
"flag" "flag"
"libnmdc"
"log" "log"
"net" "net"
"code.ivysaur.me/libnmdc"
) )
func main() { func main() {
ircAddress := flag.String("bind", ":6667", "The address:port to bind to and listen for clients on") ircAddress := flag.String("bind", ":6667", "The address:port to bind to and listen for clients on")
dcAddress := flag.String("upstream", "127.0.0.1:411", "Upstream NMDC server") dcAddress := flag.String("upstream", "127.0.0.1:411", "Upstream NMDC/ADC server")
serverName := flag.String("servername", "nmdc-ircfrontend", "Server name displayed to clients") serverName := flag.String("servername", APP_NAME, "Server name displayed to clients")
hubsec := flag.String("hubsecurity", "Hub-Security", "Nick used for administrative events")
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") autojoin := flag.Bool("autojoin", true, "Automatically join clients to the channel")
version := flag.Bool("version", false, "Display version and exit")
flag.Parse() flag.Parse()
if *version {
log.Printf("%s version %s\n", APP_NAME, APP_VERSION)
return
}
log.Printf("Listening on '%s'...", *ircAddress) log.Printf("Listening on '%s'...", *ircAddress)
if *autojoin { if *autojoin {
log.Printf("Clients will be automatically joined to the channel.") log.Printf("Clients will be automatically joined to the channel.")
@@ -56,6 +65,7 @@ func main() {
server := NewServer(*serverName, libnmdc.HubAddress(*dcAddress), conn) server := NewServer(*serverName, libnmdc.HubAddress(*dcAddress), conn)
server.verbose = *verbose server.verbose = *verbose
server.autojoin = *autojoin server.autojoin = *autojoin
server.hubSecNick = *hubsec
go server.RunWorker() go server.RunWorker()
} }
} }

20
quirks.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"strings"
)
type Quirks struct {
RequireNickForGeneralMessages bool
}
func GetQuirksForClient(ver string) Quirks {
if strings.Contains(ver, "mIRC") || strings.Contains(ver, "Atomic") || strings.Contains(ver, "Yaaic") {
return Quirks{
RequireNickForGeneralMessages: true,
}
} else {
return Quirks{}
}
}

388
server.go
View File

@@ -1,7 +1,7 @@
package main package main
/* /*
Copyright (C) 2016 The `nmdc-ircfrontend' author(s) Copyright (C) 2016-2018 The `nmdc-ircfrontend' author(s)
Copyright (C) 2013 Harry Jeffery Copyright (C) 2013 Harry Jeffery
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@@ -22,12 +22,13 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"libnmdc"
"log" "log"
"net" "net"
"regexp"
"strings" "strings"
"sync"
"time" "time"
"code.ivysaur.me/libnmdc"
) )
type ClientState int type ClientState int
@@ -39,25 +40,34 @@ const (
) )
type Server struct { type Server struct {
name string name string
motd string motd string
hubSecNick string
clientConn net.Conn clientConn net.Conn
clientState ClientState clientState ClientState
ClientStateLock sync.Mutex
upstreamLauncher libnmdc.HubConnectionOptions upstreamLauncher libnmdc.HubConnectionOptions
upstreamCloser chan struct{} upstreamCloser chan struct{}
upstreamEvents chan libnmdc.HubEvent
upstream *libnmdc.HubConnection upstream *libnmdc.HubConnection
verbose bool verbose bool
autojoin bool autojoin bool
sentCtcpVersion bool recievedFirstServerMessage bool
recievedCtcpVersion bool
nickChangeAttempt int
sentFakeSelfJoin bool
quirks Quirks
} }
func NewServer(name string, upstream libnmdc.HubAddress, conn net.Conn) *Server { func NewServer(name string, upstream libnmdc.HubAddress, conn net.Conn) *Server {
self := libnmdc.NewUserInfo("") self := libnmdc.NewUserInfo("")
self.ClientTag = APP_DESCRIPTION self.ClientTag = APP_NAME
self.ClientVersion = APP_VERSION
return &Server{ return &Server{
name: name, name: name,
@@ -66,10 +76,12 @@ func NewServer(name string, upstream libnmdc.HubAddress, conn net.Conn) *Server
motd: "Connected to " + name + ". You /must/ join " + BLESSED_CHANNEL + " to continue.", 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,
SkipAutoReconnect: true, SkipAutoReconnect: true,
}, },
upstreamEvents: make(chan libnmdc.HubEvent, 0), // unbuffered
upstreamCloser: make(chan struct{}, 1), upstreamCloser: make(chan struct{}, 1),
quirks: Quirks{},
} }
} }
@@ -107,7 +119,7 @@ func (s *Server) RunWorker() {
} }
// If this was a /timeout/, send a KA and continue. // If this was a /timeout/, send a KA and continue.
if libnmdc.CheckIsNetTimeout(err) { if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
s.writeClient("PING :" + s.name) s.writeClient("PING :" + s.name)
continue continue
} }
@@ -126,12 +138,12 @@ func (s *Server) RunWorker() {
// Client sent a command // Client sent a command
fields := strings.Fields(string(line)) fields := strings.Fields(string(line))
if len(fields) < 1 { if len(fields) == 0 {
return return
} }
if strings.HasPrefix(fields[0], ":") { if strings.HasPrefix(fields[0], ":") {
fields = fields[1:] fields[0] = fields[0][1:]
} }
s.handleCommand(strings.ToUpper(fields[0]), fields[1:]) s.handleCommand(strings.ToUpper(fields[0]), fields[1:])
@@ -152,10 +164,14 @@ func (s *Server) RunWorker() {
func (s *Server) postGeneralMessageInRoom(msg string) { func (s *Server) postGeneralMessageInRoom(msg string) {
// FIXME blank names work well in hexchat, but not in yaaic var sendAs = ""
var BLANK_NICK = "" // "!@" + s.name // "PtokaX" // "\xC2\xA0"
// CTCP action conversion // Some clients can't handle blank nicks very well
if s.quirks.RequireNickForGeneralMessages {
sendAs = s.hubSecNick + "!" + s.hubSecNick + "@" + s.hubSecNick
}
// Detect pseudo-system message for potential CTCP ACTION conversion
words := strings.Split(msg, " ") words := strings.Split(msg, " ")
firstWord := words[0] firstWord := words[0]
remainder := strings.Join(words[1:], " ") remainder := strings.Join(words[1:], " ")
@@ -176,36 +192,11 @@ func (s *Server) postGeneralMessageInRoom(msg string) {
} else { } else {
// genuine system message // genuine system message
s.reply(rplMsg, BLANK_NICK, BLESSED_CHANNEL, msg) s.reply(rplMsg, sendAs, BLESSED_CHANNEL, msg)
} }
} }
var rx_colorControlCode *regexp.Regexp = regexp.MustCompilePOSIX("\x03[0-9]+(,[0-9]+)?")
var rx_formattingControlCode *regexp.Regexp = regexp.MustCompile("(\x02|\x1D|\x1F|\x16|\x0F)")
func reformatOutgoingMessageBody(body string) string {
// Strip color codes
noColors := rx_colorControlCode.ReplaceAllString(body, "")
// Strip formatting codes
return rx_formattingControlCode.ReplaceAllString(noColors, "")
}
func reformatIncomingMessageBody(body string) string {
// "Greentext" filter
// TODO markdown filters
if len(body) > 0 && body[0] == '>' {
return "\x033" + strings.Replace(body, "implying", "\x02implying\x02", -1)
} else {
return body
}
}
func (s *Server) upstreamWorker() { func (s *Server) upstreamWorker() {
// Read loop // Read loop
@@ -217,11 +208,15 @@ func (s *Server) upstreamWorker() {
s.upstream.Disconnect() s.upstream.Disconnect()
return return
case hubEvent := <-s.upstream.OnEvent: case hubEvent := <-s.upstreamEvents:
switch hubEvent.EventType { switch hubEvent.EventType {
case libnmdc.EVENT_USER_JOINED: case libnmdc.EVENT_USER_JOINED:
s.reply(rplJoin, hubEvent.Nick, BLESSED_CHANNEL) if hubEvent.Nick == s.clientNick() && s.sentFakeSelfJoin {
// If we want to JOIN with the full power of the supplied nick!user@host, then we'll need to actually remember the active client's USER parameters s.sentFakeSelfJoin = false
} else {
// If we want to JOIN with the full power of the supplied nick!user@host, then we'll need to actually remember the active client's USER parameters
s.reply(rplJoin, hubEvent.Nick, BLESSED_CHANNEL)
}
case libnmdc.EVENT_USER_PART: case libnmdc.EVENT_USER_PART:
s.reply(rplPart, hubEvent.Nick, BLESSED_CHANNEL, "Disconnected") s.reply(rplPart, hubEvent.Nick, BLESSED_CHANNEL, "Disconnected")
@@ -230,14 +225,24 @@ func (s *Server) upstreamWorker() {
// description change - no relevance for IRC users // description change - no relevance for IRC users
case libnmdc.EVENT_CONNECTION_STATE_CHANGED: case libnmdc.EVENT_CONNECTION_STATE_CHANGED:
s.postGeneralMessageInRoom("* Upstream: " + hubEvent.StateChange.Format()) s.postGeneralMessageInRoom("* Upstream: " + hubEvent.StateChange.String())
if hubEvent.StateChange == libnmdc.CONNECTIONSTATE_CONNECTED { if hubEvent.StateChange == libnmdc.CONNECTIONSTATE_CONNECTED {
s.sendNames() // delay doing this until now s.sendNames() // delay doing this until now
} }
if hubEvent.StateChange == libnmdc.CONNECTIONSTATE_DISCONNECTED { if hubEvent.StateChange == libnmdc.CONNECTIONSTATE_DISCONNECTED {
// Abandon thread. Don't try to autoreconnect at our level, the remote client can be responsible for that if s.nickChangeAttempt > 0 {
s.DisconnectClient() // If this was a nick change, reconnect /immediately/
s.upstream = nil
s.clientState = CSRegistered
s.maybeStartUpstream() // launches new goroutine
} else {
// Abandon thread. Don't try to autoreconnect at our level, the remote client can be responsible for that
s.DisconnectClient()
}
return
} }
case libnmdc.EVENT_HUBNAME_CHANGED: case libnmdc.EVENT_HUBNAME_CHANGED:
@@ -252,12 +257,10 @@ func (s *Server) upstreamWorker() {
} else { } else {
// nick!username@userhost, but for us all three of those are always identical // nick!username@userhost, but for us all three of those are always identical
s.reply(rplMsg, hubEvent.Nick+"!"+hubEvent.Nick+"@"+hubEvent.Nick, BLESSED_CHANNEL, reformatIncomingMessageBody(hubEvent.Message)) s.reply(rplMsg, hubEvent.Nick+"!"+hubEvent.Nick+"@"+hubEvent.Nick, BLESSED_CHANNEL, reformatIncomingMessageBody(hubEvent.Message))
}
if !s.sentCtcpVersion { if !s.recievedFirstServerMessage {
// Abuse this first username in order to also send a CTCP version request s.hubSecNick = hubEvent.Nick // Replace with the hub's real Hub-Security nick, although we shouldn't need it again
s.reply(rplMsg, hubEvent.Nick+"!"+hubEvent.Nick+"@"+hubEvent.Nick, BLESSED_CHANNEL, "\x01VERSION\x01")
s.sentCtcpVersion = true
}
} }
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:
@@ -280,10 +283,10 @@ func (s *Server) handleCommand(command string, args []string) {
// do nothing // do nothing
case "INFO": case "INFO":
s.reply(rplInfo, APP_DESCRIPTION) s.reply(rplInfo, APP_NAME+" v"+APP_VERSION)
case "VERSION": case "VERSION":
s.reply(rplVersion, VERSION) s.reply(rplVersion, APP_VERSION)
case "MOTD": case "MOTD":
s.sendMOTD(s.motd) s.sendMOTD(s.motd)
@@ -317,17 +320,38 @@ func (s *Server) handleCommand(command string, args []string) {
return return
} }
// mIRC puts a colon in first place when changing nick, hexchat doesn't
suppliedNick := args[0]
if len(suppliedNick) >= 2 && suppliedNick[0] == ':' {
suppliedNick = suppliedNick[1:]
}
if s.clientNick() == "" { if s.clientNick() == "" {
// allow set, as part of the login phase // allow set, as part of the login phase
s.upstreamLauncher.Self.Nick = args[0] s.upstreamLauncher.Self.Nick = suppliedNick
} else if args[0] == s.clientNick() { } else if suppliedNick == s.clientNick() {
// Ignore // Ignore
// Required for compatibility with Lite IRC, which sends USER/NICK in the wrong order // Required for compatibility with Lite IRC, which sends USER/NICK in the wrong order
} else { } else {
s.reply(rplKill, "Can't change nicks on this server.")
s.DisconnectClient() s.ClientStateLock.Lock()
defer s.ClientStateLock.Unlock()
if s.upstream == nil {
// Not yet connected, should be safe to change nick
s.upstreamLauncher.Self.Nick = suppliedNick
} else {
// Need to disconnect/reconnect the upstream
s.writeClient(fmt.Sprintf(":%s!%s@%s NICK %s", s.clientNick(), s.clientNick(), s.clientNick(), suppliedNick)) // notify client about what they've done
s.upstreamLauncher.Self.Nick = suppliedNick
s.nickChangeAttempt++
s.upstream.Disconnect()
}
//s.reply(rplKill, "Can't change nicks on this server.")
//s.DisconnectClient()
} }
case "USER": case "USER":
@@ -355,7 +379,9 @@ func (s *Server) handleCommand(command string, args []string) {
s.reply(rplWelcome) s.reply(rplWelcome)
s.clientState = CSRegistered s.clientState = CSRegistered
// TODO tell the client that they /must/ join #chat // Send CTCP VERSION request immediately
s.reply(rplMsg, s.hubSecNick+"!"+s.hubSecNick+"@"+s.hubSecNick, BLESSED_CHANNEL, "\x01VERSION\x01")
if s.autojoin { if s.autojoin {
s.handleCommand("JOIN", []string{BLESSED_CHANNEL}) s.handleCommand("JOIN", []string{BLESSED_CHANNEL})
} }
@@ -381,98 +407,14 @@ func (s *Server) handleRegisteredCommand(command string, args []string) {
} }
s.reply(rplListEnd) s.reply(rplListEnd)
case "JOIN":
if len(args) < 1 {
s.reply(errMoreArgs)
return
}
switch args[0] {
case BLESSED_CHANNEL:
if s.clientState != CSJoined {
// Join for the first time
s.clientState = CSJoined
// Acknowledge
s.reply(rplJoin, s.clientNick(), 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":
// Quitting all channels? Drop client
s.reply(rplKill, "Bye.")
s.DisconnectClient()
default:
s.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.")
s.DisconnectClient()
}
default:
s.handleJoinedCommand(command, args)
}
}
func (s *Server) SetClientSoftwareVersion(ver string) {
// "HexChat 2.12.1 [x64] / Microsoft Windows 10 Pro (x64) [Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz (3.60GHz)]"
// "AndroIRC - Android IRC Client (5.2 - Build 6830152) - http://www.androirc.com"
// "AndChat 1.4.3.2 http://www.andchat.net"
// "liteIRC for Android 1.1.8"
// ":Relay:1.0:Android"
// A bit long and unwieldy. Heuristic: stop after the first word containing digits
parts := regexp.MustCompilePOSIX("^([^0-9]*[0-9]+[^ ]*) ").FindStringSubmatch(ver)
if len(parts) > 0 {
ver = strings.TrimRight(parts[0], " ")
}
s.verbosef("Replacing client tag with '%s'...", ver)
s.upstreamLauncher.Self.ClientTag = ver
s.upstream.Hco.Self.ClientTag = ver
s.upstream.SayInfo()
}
func (s *Server) handleJoinedCommand(command string, args []string) {
if s.clientState != CSJoined {
s.reply(errNotReg)
return
}
switch command {
case "PART":
if len(args) < 1 {
s.reply(errMoreArgs)
return
}
if args[0] == BLESSED_CHANNEL {
// You can check out any time you like, but you can never leave
s.reply(rplJoin, s.clientNick(), BLESSED_CHANNEL)
s.sendNames()
}
case "PRIVMSG", "NOTICE": case "PRIVMSG", "NOTICE":
if len(args) < 2 { if len(args) < 2 {
s.reply(errMoreArgs) s.reply(errMoreArgs)
return return
} }
message := strings.Join(args[1:], " ")[1:] // strip leading colon // Strip leading colon (work around a bug in HoloIRC 4.1.0 and older)
message := strings.Join(args[1:], " ")[1:]
if strings.HasPrefix(message, "\x01VERSION ") { if strings.HasPrefix(message, "\x01VERSION ") {
// Not a real message - a reply to our internal request. Change the user's tag to match the actual client software // Not a real message - a reply to our internal request. Change the user's tag to match the actual client software
@@ -482,6 +424,7 @@ func (s *Server) handleJoinedCommand(command string, args []string) {
versionString := message[9:] versionString := message[9:]
versionString = versionString[:len(versionString)-1] versionString = versionString[:len(versionString)-1]
s.SetClientSoftwareVersion(versionString) s.SetClientSoftwareVersion(versionString)
s.quirks = GetQuirksForClient(versionString)
return return
} }
@@ -509,6 +452,105 @@ func (s *Server) handleJoinedCommand(command string, args []string) {
} }
case "JOIN":
if len(args) < 1 {
s.reply(errMoreArgs)
return
}
switch args[0] {
case BLESSED_CHANNEL:
// Give it a few seconds - it's better to hear the CTCP VERSION
// response first. Once we get that, it'll connect instantly
go func() {
<-time.After(WAIT_FOR_VERSION * time.Second)
s.ClientStateLock.Lock()
defer s.ClientStateLock.Unlock()
s.maybeStartUpstream()
}()
case "0":
// Quitting all channels? Drop client
s.reply(rplKill, "Bye.")
s.DisconnectClient()
default:
s.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.")
s.DisconnectClient()
}
default:
s.handleJoinedCommand(command, args)
}
}
func (s *Server) maybeStartUpstream() {
if s.clientState != CSJoined {
// Join for the first time
s.clientState = CSJoined
// Acknowledge
s.reply(rplJoin, s.clientNick(), BLESSED_CHANNEL)
// Spawn upstream connection
s.upstream = libnmdc.ConnectAsync(&s.upstreamLauncher, s.upstreamEvents)
go s.upstreamWorker()
} else {
// They're already here, ignore
// Can happen if autojoin is enabled but the client already requested a login
}
}
func (s *Server) SetClientSoftwareVersion(ver string) {
ct := parseVersion(ver)
s.verbosef("Replacing client tag with '%s' version '%s'", ct.AppName, ct.Version)
s.upstreamLauncher.Self.ClientTag = ct.AppName
s.upstreamLauncher.Self.ClientVersion = ct.Version
s.recievedCtcpVersion = true
s.ClientStateLock.Lock()
defer s.ClientStateLock.Unlock()
if s.upstream != nil {
s.upstream.Hco.Self.ClientTag = ct.AppName
s.upstream.Hco.Self.ClientVersion = ct.Version
s.upstream.SayInfo()
} else {
// Connected for the first time (order was CTCP VERSION --> JOIN)
s.maybeStartUpstream()
}
}
func (s *Server) handleJoinedCommand(command string, args []string) {
if s.clientState != CSJoined {
s.reply(errNotReg)
return
}
switch command {
case "PART":
if len(args) < 1 {
s.reply(errMoreArgs)
return
}
if args[0] == BLESSED_CHANNEL {
// You can check out any time you like, but you can never leave
s.reply(rplJoin, s.clientNick(), BLESSED_CHANNEL)
s.sendNames()
}
case "QUIT": case "QUIT":
s.DisconnectClient() s.DisconnectClient()
@@ -552,8 +594,7 @@ func (s *Server) handleJoinedCommand(command string, args []string) {
return return
} }
// s.sendWho(args[0]) // Ignore this command
// s.sendNames() // fixes hexchat, but andchat always sends WHO /immediately/ after NAMES end, causing an infinite loop
case "MODE": case "MODE":
if len(args) < 1 { if len(args) < 1 {
@@ -577,6 +618,35 @@ func (s *Server) handleJoinedCommand(command string, args []string) {
return return
case "WHOIS":
if len(args) < 1 {
s.reply(errMoreArgs)
return
}
// WHOIS [target] nick[,nick2[,nick...]]
nicklist := args[0] // Assume WHOIS ${nick} only,
if len(args) >= 2 {
nicklist = args[1] // It was WHOIS ${target} ${nick} instead
}
for _, targetnick := range strings.Split(nicklist, ",") {
// tell the client something about it
// The protocol does ostensibly support wildcard WHOIS, but we don't (yet)
s.upstream.Users(func(u *map[string]libnmdc.UserInfo) error {
for nick, nickinfo := range *u {
if nick == targetnick {
s.reply(rplWhoisUser, nick, nickinfo.Description+" <"+nickinfo.ClientTag+" V:"+nickinfo.ClientVersion+">")
if nickinfo.IsOperator {
s.reply(rplWhoisOperator, nick)
}
}
}
return nil
})
s.reply(rplEndOfWhois)
}
default: default:
s.reply(errUnknownCommand, command) s.reply(errUnknownCommand, command)
} }
@@ -609,6 +679,15 @@ func (s *Server) sendNames() {
}) })
} }
if len(nameList) == 0 {
// We don't have a nick list yet. Many clients can't handle a blank list
// We could delay until we do have a nick list
// Or, we could send our nick only, and filter it out of the next join
nameList = append(nameList, s.clientNick())
s.sentFakeSelfJoin = true
}
s.reply(rplNames, BLESSED_CHANNEL, strings.Join(nameList, " ")) s.reply(rplNames, BLESSED_CHANNEL, strings.Join(nameList, " "))
s.reply(rplEndOfNames, BLESSED_CHANNEL) s.reply(rplEndOfNames, BLESSED_CHANNEL)
} }
@@ -679,9 +758,9 @@ func (s *Server) reply(code replyCode, args ...string) {
s.writeClient(fmt.Sprintf(":%s 005 %s NAMESX CHANTYPES=# :are supported by this server", s.name, s.clientNick())) s.writeClient(fmt.Sprintf(":%s 005 %s NAMESX CHANTYPES=# :are supported by this server", s.name, s.clientNick()))
case rplJoin: case rplJoin:
s.writeClient(fmt.Sprintf(":%s JOIN %s", args[0], args[1])) s.writeClient(fmt.Sprintf(":%s!%s@%s JOIN %s", args[0], args[0], args[0], args[1]))
case rplPart: case rplPart:
s.writeClient(fmt.Sprintf(":%s PART %s %s", args[0], args[1], args[2])) s.writeClient(fmt.Sprintf(":%s!%s@%s PART %s %s", args[0], args[0], args[0], args[1], args[2]))
case rplTopic: case rplTopic:
s.writeClient(fmt.Sprintf(":%s 332 %s %s :%s", s.name, s.clientNick(), args[0], args[1])) s.writeClient(fmt.Sprintf(":%s 332 %s %s :%s", s.name, s.clientNick(), args[0], args[1]))
case rplNoTopic: case rplNoTopic:
@@ -690,7 +769,7 @@ func (s *Server) reply(code replyCode, args ...string) {
case rplNames: case rplNames:
s.writeClient(fmt.Sprintf(":%s 353 %s = %s :%s", s.name, s.clientNick(), args[0], args[1])) s.writeClient(fmt.Sprintf(":%s 353 %s = %s :%s", s.name, s.clientNick(), args[0], args[1]))
case rplEndOfNames: case rplEndOfNames:
s.writeClient(fmt.Sprintf(":%s 366 %s %s :End of NAMES list", s.name, s.clientNick(), args[0])) s.writeClient(fmt.Sprintf(":%s 366 %s %s :End of /NAMES list.", s.name, s.clientNick(), args[0]))
case rplWho: case rplWho:
s.writeClient(fmt.Sprintf(":%s 352 %s %s %s %s %s %s H :0 %s", s.name, s.clientNick(), args[1], args[0], args[0], s.name, args[0], args[0])) s.writeClient(fmt.Sprintf(":%s 352 %s %s %s %s %s %s H :0 %s", s.name, s.clientNick(), args[1], args[0], args[0], s.name, args[0], args[0]))
@@ -735,6 +814,13 @@ func (s *Server) reply(code replyCode, args ...string) {
case rplPong: case rplPong:
s.writeClient(fmt.Sprintf(":%s PONG %s %s", s.name, s.clientNick(), args[0])) s.writeClient(fmt.Sprintf(":%s PONG %s %s", s.name, s.clientNick(), args[0]))
case rplWhoisUser:
s.writeClient(fmt.Sprintf(":%s 311 %s %s %s %s * :%s", s.name, args[0], args[0], args[0], s.name, args[1])) // caller should supply nick,description
case rplWhoisOperator:
s.writeClient(fmt.Sprintf(":%s 313 %s :is an IRC operator", s.name, args[0]))
case rplEndOfWhois:
s.writeClient(fmt.Sprintf(":%s 318 :End of WHOIS list", s.name))
case errMoreArgs: case errMoreArgs:
s.writeClient(fmt.Sprintf(":%s 461 %s :Not enough params", s.name, s.clientNick())) s.writeClient(fmt.Sprintf(":%s 461 %s :Not enough params", s.name, s.clientNick()))
case errNoNick: case errNoNick:

View File

@@ -1,7 +1,7 @@
package main package main
/* /*
Copyright (C) 2016 The `nmdc-ircfrontend' author(s) Copyright (C) 2016-2018 The `nmdc-ircfrontend' author(s)
Copyright (C) 2013 Harry Jeffery Copyright (C) 2013 Harry Jeffery
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@@ -18,15 +18,19 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
var (
APP_VERSION = "0.0" // overridden with build ldflags
)
const ( const (
VERSION = "1.0.0" APP_NAME = "nmdc-ircfrontend"
APP_DESCRIPTION = "nmdc-ircfrontend v" + VERSION
BLESSED_CHANNEL = "#chat" // must be lowercase BLESSED_CHANNEL = "#chat" // must be lowercase
BLESSED_CHANNEL_MODE = "n" // means that you have to be in the channel to chat, but that's it BLESSED_CHANNEL_MODE = "n" // means that you have to be in the channel to chat, but that's it
NICKS_PER_PROTOMSG = 128 // Max number of nicks to send per message NICKS_PER_PROTOMSG = 128 // Max number of nicks to send per message
CLIENT_READ_BUFFSIZE = 512 CLIENT_READ_BUFFSIZE = 512
CLIENT_KEEPALIVE_EVERY = 60 // should be longer than the client's one, hexchat is 30s CLIENT_KEEPALIVE_EVERY = 60 // should be longer than the client's one, hexchat is 30s
WAIT_FOR_VERSION = 3 // Seconds to wait for the CTCP VERSION response before connecting with default MyINFO
) )
type replyCode int type replyCode int
@@ -55,6 +59,9 @@ const (
rplMOTD rplMOTD
rplEndOfMOTD rplEndOfMOTD
rplPong rplPong
rplWhoisUser
rplWhoisOperator
rplEndOfWhois
errMoreArgs errMoreArgs
errNoNick errNoNick
errInvalidNick errInvalidNick