Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d88c6248b8 | |||
| 4a34ccce26 | |||
| 9d9fcef954 | |||
| 2d19acb846 | |||
| e993e8a32c | |||
| 541cbaabd2 | |||
| 86ad1e1633 | |||
| 19e0120423 | |||
| e9ad8f6962 | |||
| ef36fc0c24 | |||
| 458ab0a054 | |||
| 0e7e2b629c | |||
| 371a7eb9e4 | |||
| 485f68f5d1 | |||
| d465d742c6 | |||
| 37b62ff817 | |||
| fa13755311 | |||
| e83e2dc111 | |||
| a555dbd563 | |||
| f076aeaeda | |||
| 1056493211 | |||
| ddcd65fa47 | |||
| d21b18fc75 | |||
| 53b72e0cb0 | |||
| 839dea016a | |||
| e235ee014e | |||
|
|
c54a271f17 | ||
|
|
264ee8c61c | ||
|
|
85e44756e3 | ||
|
|
e628bdcf91 | ||
|
|
f929379569 | ||
|
|
d5f331ec7c | ||
|
|
a609996484 | ||
|
|
0ec1c20d7e | ||
|
|
3cffacaa45 | ||
|
|
e0e5c742e3 | ||
|
|
7494823b07 | ||
|
|
7b2ab6642b | ||
|
|
f1051fbfc2 | ||
|
|
73a4134b18 | ||
|
|
962cc8dea0 | ||
|
|
9bde052d9d | ||
|
|
eabae2ce9b | ||
|
|
10f4db39cc | ||
|
|
6a7847f71b | ||
|
|
7592bf6002 | ||
|
|
fb34cefc46 | ||
|
|
0df6237005 | ||
|
|
c35062a3df | ||
|
|
74e4f6cfa3 | ||
|
|
7bf832322d | ||
|
|
169debea93 | ||
|
|
9c2cad7986 | ||
|
|
32296eeffa | ||
|
|
3a00df8ef4 | ||
|
|
a948b457bd | ||
|
|
11dbafe387 | ||
|
|
a72f7be558 | ||
|
|
637bfd6c78 |
6
.hgtags
Normal file
6
.hgtags
Normal 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
|
||||||
16
TODO.txt
16
TODO.txt
@@ -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
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
|||||||
@@ -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
33
chatFormatting.go
Normal 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
68
clientTag.go
Normal 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
73
clientTag_test.go
Normal 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
2
godist.sh
Normal file → Executable 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
18
main.go
@@ -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
20
quirks.go
Normal 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
388
server.go
@@ -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:
|
||||||
|
|||||||
13
typedefs.go
13
typedefs.go
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user