53 Commits

Author SHA1 Message Date
4ea6cbb6f1 readme 2017-11-14 18:54:51 +13:00
b2ae91959e bump DEFAULT_CLIENT_VERSION to 0.15 2017-11-14 18:45:36 +13:00
f5fbce3ee6 add a fallback reconnection if no keepalives are recieved within ~24 hours 2017-11-14 18:45:06 +13:00
2cbc8f8496 fix regex matching protocol messages in the middle of multi-line public messages 2017-11-14 18:36:34 +13:00
5bf30ed95f update examples for StateChange member API change 2017-11-14 18:29:04 +13:00
a20fce3725 doc: add go-get tags to README 2017-10-28 12:32:06 +13:00
5c6435cf81 retag commits in vX.Y.Z format 2017-10-28 12:16:24 +13:00
b7dfcbc2f3 Added tag release-0.14 for changeset 6422ed687cd3 2017-02-11 13:56:27 +13:00
21847db9fd readme 2017-02-11 13:56:08 +13:00
b8122710f5 patch a crash on incomplete supplied UserIP2 2017-02-11 13:55:20 +13:00
f8c0fc54d8 Added tag release-0.13 for changeset 3ee0f4ea5142 2017-02-09 19:31:24 +13:00
4490444d1c readme 2017-02-09 19:31:19 +13:00
c5f655ff78 doc: update TODO 2017-02-09 19:28:58 +13:00
4415109174 support UserIP2 extension 2017-02-09 19:28:37 +13:00
0adc13ddfa move our is-connected one half roundtrip sooner 2017-02-09 19:19:17 +13:00
59c56fea1b doc: todo 2017-02-09 19:15:53 +13:00
7f8f98aa5a add ChatOnly to our $Supports 2017-02-08 19:30:53 +13:00
8536cb0a5c implement QuickList, save a network roundtrip on supported hubs (ptokax) 2017-02-08 19:22:07 +13:00
553ec20850 fix not emitting EVENT_USER_UPDATED_INFO on recieved MyINFO (!) 2017-02-08 18:58:14 +13:00
a403dac461 declare NoHello support (since our MyINFO handler will add unknown users) 2017-02-08 18:58:03 +13:00
59a37b975c more idiomatic error handling 2017-02-08 18:57:49 +13:00
a4e7d876cc Added tag release-0.12 for changeset 22b156a6fc2f 2017-02-05 22:37:24 +13:00
7c3069eabb readme 2017-02-05 22:35:18 +13:00
3cc31323ad more fine-grained locking around user map (fixes deadlock if callee uses Users() in response to processEvent) 2017-02-05 19:06:17 +13:00
7f898dff32 display a system message and disconnect if we're asked for a password but don't have one 2017-02-05 17:12:59 +13:00
58c1151278 readme for 0.11 2016-11-29 20:18:17 +13:00
d1c549f1f2 Added tag release-0.11 for changeset 5149ffe70ea8 2016-11-29 20:13:14 +13:00
9eb9cf0726 set a default clientversion of 0.11 2016-11-29 20:07:19 +13:00
f2abf8893c rename example functions for correct godoc appearance 2016-11-29 19:50:05 +13:00
732622f4db add some function comments 2016-11-29 19:49:53 +13:00
95311e1479 ConnectionState rename member function to satisfy Stringer interface 2016-11-29 19:44:45 +13:00
084b629ad7 rename NMDC{Escape,Unescape,Unlock} functions; don't export NMDCUnlock 2016-11-29 19:44:10 +13:00
5713b58c7c don't export checkIsNetTimeout 2016-11-29 19:43:32 +13:00
56c6b7b352 remove junk strings appearing in godoc HTML output 2016-11-29 19:41:44 +13:00
ea97afb01f move examples into a godoc-compatible Examples function 2016-11-29 19:40:08 +13:00
482b0d3ad8 existential fixes for the synchronous API 2016-11-29 19:39:47 +13:00
231bfeb247 sample: add a version using the synchronous API 2016-11-29 19:30:33 +13:00
bd0425d6d4 sample: use UserInfo constructor 2016-11-29 19:25:41 +13:00
5564eccf22 split structs to separate files 2016-11-29 19:24:31 +13:00
d373e9791a fix parsing connection modes in MyINFO 2016-11-29 19:23:00 +13:00
7e249acd6c remove errant printf 2016-11-29 19:22:45 +13:00
b592e8ef7e add ConnectionMode.String() helper 2016-11-29 19:22:28 +13:00
11564b8c32 test: add MyINFO parsing test cases 2016-11-29 19:22:15 +13:00
0a53963ec0 fix special characters appearing in recieved PM's 2016-11-04 19:01:01 +13:00
b63a240e9b Added tag libnmdc-r10 for changeset 3ecc037cf2d7 2016-10-08 16:32:16 +13:00
51b08dad3d readme 2016-10-08 16:32:10 +13:00
1a3e4fe072 support parsing usercommands 2016-10-08 15:19:21 +13:00
086281dab2 Added tag libnmdc-r9 for changeset e7c2c71ef24b 2016-08-27 17:39:00 +12:00
7392cbbea5 readme 2016-08-27 17:38:53 +12:00
265c0a43ce fix an issue not applying updated user profiles 2016-08-27 15:31:00 +12:00
417deff347 remove debug logging in previous 2016-08-27 15:24:18 +12:00
a996f1668c better myinfo parsing for zero-length speed strings 2016-08-27 15:22:39 +12:00
9b290ebb96 Added tag libnmdc-r8 for changeset b0e57a5fcffd 2016-05-10 19:18:11 +12:00
15 changed files with 465 additions and 150 deletions

21
.hgtags
View File

@@ -1,12 +1,19 @@
945ab4b16d05aa084f71bf5da9a3f687e0ec8bbd libnmdc-r1 945ab4b16d05aa084f71bf5da9a3f687e0ec8bbd v0.1.0
02a360e95480b97ddad83add5db48b2766339a99 nmdc-log-service-1.0.0 02a360e95480b97ddad83add5db48b2766339a99 nmdc-log-service-1.0.0
137c1b65039e03c80379826a6efdfd808f6fbc8f libnmdc-r2 137c1b65039e03c80379826a6efdfd808f6fbc8f v0.2.0
d8b64d5527c2a5e4d76872e5bc3d69f7646135c6 libnmdc-r3 d8b64d5527c2a5e4d76872e5bc3d69f7646135c6 v0.3.0
fca41372e400853775b02e951f9db91d87f41adb nmdc-log-service-1.0.1 fca41372e400853775b02e951f9db91d87f41adb nmdc-log-service-1.0.1
050b424a7c5d5a27c9323c8810f3afbead1f5b96 libnmdc-r4 050b424a7c5d5a27c9323c8810f3afbead1f5b96 v0.4.0
da9f123633f9c28be6435ed7898139665d4c39d9 nmdc-log-service-1.0.2 da9f123633f9c28be6435ed7898139665d4c39d9 nmdc-log-service-1.0.2
75a78f6a78f249a2cd8aa3d29f7e5e6319b4e03b libnmdc-r5 75a78f6a78f249a2cd8aa3d29f7e5e6319b4e03b v0.5.0
4116422bb10229d887f9296970a166fa1ef8c5fd nmdc-log-service-1.0.3 4116422bb10229d887f9296970a166fa1ef8c5fd nmdc-log-service-1.0.3
cb86f3a40115cc46f450c0c83fd9b9d3b740e820 nmdc-log-service-1.0.4 cb86f3a40115cc46f450c0c83fd9b9d3b740e820 nmdc-log-service-1.0.4
cb86f3a40115cc46f450c0c83fd9b9d3b740e820 libnmdc-r6 cb86f3a40115cc46f450c0c83fd9b9d3b740e820 v0.6.0
71343a2c641a438206d30ea7e75dc89a11dbef00 libnmdc-r7 71343a2c641a438206d30ea7e75dc89a11dbef00 v0.7.0
b0e57a5fcffdf4102d669db51a3648ddf66a0792 v0.8.0
e7c2c71ef24b386add728fad35fff4a996fccbac v0.9.0
3ecc037cf2d7080572fe87c2e39ecd153fb0e947 v0.10.0
5149ffe70ea8475e480b682345b31aa45a3352db v0.11.0
22b156a6fc2f6161765317f4ec9ab3731a26e0e2 v0.12.0
3ee0f4ea5142d66079a9500bdcd48a53bdcf362f v0.13.0
6422ed687cd308c339b6dc188bbe1034ed93f893 v0.14.0

29
ConnectionMode.go Normal file
View File

@@ -0,0 +1,29 @@
package libnmdc
import (
"fmt"
)
type ConnectionMode rune
const (
CONNECTIONMODE_ACTIVE ConnectionMode = 'A' // 65
CONNECTIONMODE_PASSIVE ConnectionMode = 'P' // 49
CONNECTIONMODE_SOCKS5 ConnectionMode = '5' // 53
)
func (this ConnectionMode) String() string {
switch this {
case CONNECTIONMODE_ACTIVE:
return "Active"
case CONNECTIONMODE_PASSIVE:
return "Passive"
case CONNECTIONMODE_SOCKS5:
return "SOCKS5"
default:
return fmt.Sprintf("ConnectionMode(\"%s\")", string(this))
}
}

View File

@@ -12,7 +12,7 @@ const (
CONNECTIONSTATE_CONNECTED = 3 CONNECTIONSTATE_CONNECTED = 3
) )
func (cs ConnectionState) Format() string { func (cs ConnectionState) String() string {
switch cs { switch cs {
case CONNECTIONSTATE_DISCONNECTED: case CONNECTIONSTATE_DISCONNECTED:
return "Disconnected" return "Disconnected"
@@ -25,7 +25,7 @@ func (cs ConnectionState) Format() string {
} }
} }
func CheckIsNetTimeout(err error) bool { func checkIsNetTimeout(err error) bool {
if err == nil { if err == nil {
return false return false
} }

58
Example_test.go Normal file
View File

@@ -0,0 +1,58 @@
package libnmdc
import (
"fmt"
)
func ExampleHubConnectionOptions_Connect() {
opts := HubConnectionOptions{
Address: "127.0.0.1",
Self: *NewUserInfo("slowpoke9"),
}
hub := opts.Connect()
for {
event := <-hub.OnEvent
switch event.EventType {
case EVENT_CONNECTION_STATE_CHANGED:
fmt.Printf("Connection -- %s (%s)\n", event.StateChange, event.Message)
case EVENT_PUBLIC:
fmt.Printf("Message from '%s': '%s'\n", event.Nick, event.Message)
if event.Message == "how are you" {
hub.SayPublic("good thanks!")
}
default:
fmt.Printf("%+v\n", event)
}
}
}
func ExampleHubConnectionOptions_ConnectSync() {
cb := func(hub *HubConnection, event HubEvent) {
switch event.EventType {
case EVENT_CONNECTION_STATE_CHANGED:
fmt.Printf("Connection -- %s (%s)\n", event.StateChange, event.Message)
case EVENT_PUBLIC:
fmt.Printf("Message from '%s': '%s'\n", event.Nick, event.Message)
if event.Message == "how are you" {
hub.SayPublic("good thanks!")
}
default:
fmt.Printf("%+v\n", event)
}
}
opts := HubConnectionOptions{
Address: "127.0.0.1",
Self: *NewUserInfo("slowpoke9"),
OnEventSync: cb,
}
opts.ConnectSync() // blocking
}

View File

@@ -2,7 +2,9 @@ package libnmdc
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"net" "net"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -27,8 +29,12 @@ type HubConnection struct {
connValid bool connValid bool
sentOurHello bool sentOurHello bool
autoReconnect bool autoReconnect bool
lastDataRecieved time.Time
supports map[string]struct{}
} }
// Thread-safe user accessor.
func (this *HubConnection) Users(cb func(*map[string]UserInfo) error) error { func (this *HubConnection) Users(cb func(*map[string]UserInfo) error) error {
this.userLock.Lock() this.userLock.Lock()
defer this.userLock.Unlock() defer this.userLock.Unlock()
@@ -37,11 +43,11 @@ func (this *HubConnection) Users(cb func(*map[string]UserInfo) error) error {
} }
func (this *HubConnection) SayPublic(message string) { func (this *HubConnection) SayPublic(message string) {
this.SayRaw("<" + this.Hco.Self.Nick + "> " + NMDCEscape(message) + "|") this.SayRaw("<" + this.Hco.Self.Nick + "> " + Escape(message) + "|")
} }
func (this *HubConnection) SayPrivate(recipient string, message string) { func (this *HubConnection) SayPrivate(recipient string, message string) {
this.SayRaw("$To: " + recipient + " From: " + this.Hco.Self.Nick + " $<" + this.Hco.Self.Nick + "> " + NMDCEscape(message) + "|") this.SayRaw("$To: " + recipient + " From: " + this.Hco.Self.Nick + " $<" + this.Hco.Self.Nick + "> " + Escape(message) + "|")
} }
func (this *HubConnection) SayInfo() { func (this *HubConnection) SayInfo() {
@@ -65,24 +71,31 @@ func (this *HubConnection) UserCount() int {
func (this *HubConnection) userJoined_NameOnly(nick string) { func (this *HubConnection) userJoined_NameOnly(nick string) {
if !this.UserExists(nick) { if !this.UserExists(nick) {
this.userLock.Lock()
defer this.userLock.Unlock()
this.userLock.Lock()
this.users[nick] = *NewUserInfo(nick) this.users[nick] = *NewUserInfo(nick)
this.userLock.Unlock() // Don't lock over a processEvent boundary
this.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: nick}) this.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: nick})
} }
} }
func (this *HubConnection) userJoined_Full(uinf *UserInfo) { func (this *HubConnection) userJoined_Full(uinf *UserInfo) {
if !this.UserExists(uinf.Nick) { // n.b. also called when we get a replacement MyINFO for someone
this.userLock.Lock() this.userLock.Lock()
defer this.userLock.Unlock() _, userExisted := this.users[uinf.Nick] // don't use UserExists as it would deadlock the mutex
this.users[uinf.Nick] = *uinf this.users[uinf.Nick] = *uinf
this.userLock.Unlock() // Don't lock over a processEvent boundary
if !userExisted {
this.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: uinf.Nick}) this.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: uinf.Nick})
} else {
this.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: uinf.Nick})
} }
} }
// SayRaw sends raw bytes over the TCP socket. Callers should add the protocol
// terminating character `|` themselves.
// Note that protocol messages are transmitted on the caller thread, not from // Note that protocol messages are transmitted on the caller thread, not from
// any internal libnmdc thread. // any internal libnmdc thread.
func (this *HubConnection) SayRaw(protocolCommand string) error { func (this *HubConnection) SayRaw(protocolCommand string) error {
@@ -106,14 +119,14 @@ func (this *HubConnection) processProtocolMessage(message string) {
// ``````````` // ```````````
if rx_publicChat.MatchString(message) { if rx_publicChat.MatchString(message) {
pubchat_parts := rx_publicChat.FindStringSubmatch(message) pubchat_parts := rx_publicChat.FindStringSubmatch(message)
this.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: pubchat_parts[1], Message: NMDCUnescape(pubchat_parts[2])}) this.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: pubchat_parts[1], Message: Unescape(pubchat_parts[2])})
return return
} }
// System messages // System messages
// ``````````````` // ```````````````
if message[0] != '$' { if message[0] != '$' {
this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Nick: this.HubName, Message: NMDCUnescape(message)}) this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Nick: this.HubName, Message: Unescape(message)})
return return
} }
@@ -124,9 +137,8 @@ func (this *HubConnection) processProtocolMessage(message string) {
switch commandParts[0] { switch commandParts[0] {
case "$Lock": case "$Lock":
this.SayRaw("$Supports NoGetINFO UserCommand UserIP2|" + this.SayRaw("$Supports NoHello NoGetINFO UserCommand UserIP2 QuickList ChatOnly|" +
"$Key " + NMDCUnlock([]byte(commandParts[1])) + "|" + "$Key " + unlock([]byte(commandParts[1])) + "|")
"$ValidateNick " + NMDCEscape(this.Hco.Self.Nick) + "|")
this.sentOurHello = false this.sentOurHello = false
case "$Hello": case "$Hello":
@@ -159,26 +171,32 @@ func (this *HubConnection) processProtocolMessage(message string) {
this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."}) this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."})
case "$GetPass": case "$GetPass":
this.SayRaw("$MyPass " + NMDCEscape(this.Hco.NickPassword) + "|") if len(this.Hco.NickPassword) == 0 {
// We've got a problem. MyPass with no arguments is a syntax error with no message = instant close
// Just drop the connection
this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "This account is passworded."})
this.Disconnect()
} else {
this.SayRaw("$MyPass " + Escape(this.Hco.NickPassword) + "|")
}
case "$Quit": case "$Quit":
func() {
this.userLock.Lock() this.userLock.Lock()
defer this.userLock.Unlock()
delete(this.users, commandParts[1]) delete(this.users, commandParts[1])
}() this.userLock.Unlock() // Don't lock over a processEvent boundary
this.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: commandParts[1]}) this.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: commandParts[1]})
case "$MyINFO": case "$MyINFO":
u := UserInfo{} u := UserInfo{}
err := u.fromMyINFO(commandParts[1]) err := u.fromMyINFO(commandParts[1])
if err == nil { if err != nil {
this.userJoined_Full(&u)
} else {
this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: err.Error()}) this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: err.Error()})
return
} }
this.userJoined_Full(&u)
case "$NickList": case "$NickList":
nicklist := strings.Split(commandParts[1], "$$") nicklist := strings.Split(commandParts[1], "$$")
for _, nick := range nicklist { for _, nick := range nicklist {
@@ -219,7 +237,7 @@ func (this *HubConnection) processProtocolMessage(message string) {
if rx_incomingTo.MatchString(commandParts[1]) { if rx_incomingTo.MatchString(commandParts[1]) {
txparts := rx_incomingTo.FindStringSubmatch(commandParts[1]) txparts := rx_incomingTo.FindStringSubmatch(commandParts[1])
if txparts[1] == this.Hco.Self.Nick && txparts[2] == txparts[3] { if txparts[1] == this.Hco.Self.Nick && txparts[2] == txparts[3] {
this.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: txparts[2], Message: txparts[4]}) this.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: txparts[2], Message: Unescape(txparts[4])})
valid = true valid = true
} }
} }
@@ -229,20 +247,94 @@ func (this *HubConnection) processProtocolMessage(message string) {
} }
case "$UserIP": case "$UserIP":
// Final message in PtokaX connection handshake - trigger connection callback. this.userLock.Lock()
// This might not be the case for other hubsofts, though
if this.State != CONNECTIONSTATE_CONNECTED { pairs := strings.Split(commandParts[1], "$$")
this.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED}) notifyOfUpdate := make([]string, 0, len(pairs))
this.State = CONNECTIONSTATE_CONNECTED
nextIPPair:
for _, pair := range pairs {
parts := strings.SplitN(pair, " ", 2)
if len(parts) != 2 {
// ????
continue nextIPPair
}
ip2nick := parts[0]
ip2addr := parts[1]
uinfo, ok := this.users[ip2nick]
if !ok {
this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Recieved IP '" + ip2addr + "' for unknown user '" + ip2nick + "'"})
continue nextIPPair
}
if uinfo.IPAddress != ip2addr {
uinfo.IPAddress = ip2addr
notifyOfUpdate = append(notifyOfUpdate, ip2nick)
this.users[ip2nick] = uinfo
}
}
this.userLock.Unlock()
for _, nick := range notifyOfUpdate {
this.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: nick})
} }
case "$ForceMove": case "$ForceMove":
this.Hco.Address = HubAddress(commandParts[1]) this.Hco.Address = HubAddress(commandParts[1])
this.conn.Close() // we'll reconnect onto the new address this.conn.Close() // we'll reconnect onto the new address
// IGNORABLE COMMANDS case "$UserCommand":
// $UserCommand 1 1 Group chat\New group chat$<%[mynick]> !groupchat_new&#124;|
if rx_userCommand.MatchString(commandParts[1]) {
usc := rx_userCommand.FindStringSubmatch(commandParts[1])
typeInt, _ := strconv.Atoi(usc[1])
contextInt, _ := strconv.Atoi(usc[2])
uscStruct := UserCommand{
Type: UserCommandType(typeInt),
Context: UserCommandContext(contextInt),
Message: usc[3],
Command: Unescape(usc[4]),
}
this.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uscStruct})
} else {
this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed usercommand '" + commandParts[1] + "'"})
}
case "$Supports": case "$Supports":
case "$UserCommand": // TODO $UserCommand 1 1 Group chat\New group chat$<%[mynick]> !groupchat_new&#124;| this.supports = make(map[string]struct{})
for _, s := range strings.Split(commandParts[1], " ") {
this.supports[s] = struct{}{}
}
if !this.sentOurHello {
// Need to log in.
// If the hub supports QuickList, we can skip one network roundtrip
if _, ok := this.supports["QuickList"]; ok {
this.SayInfo()
this.SayRaw("$GetNickList|")
} else {
this.SayRaw("$ValidateNick " + Escape(this.Hco.Self.Nick) + "|")
}
// This also counts as the end of the handshake from our POV. Consider
// ourselves logged in
this.sentOurHello = true
if this.State != CONNECTIONSTATE_CONNECTED {
this.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED})
this.State = CONNECTIONSTATE_CONNECTED
}
}
// IGNORABLE COMMANDS
case "$HubTopic": case "$HubTopic":
case "$Search": case "$Search":
case "$ConnectToMe": case "$ConnectToMe":
@@ -299,7 +391,7 @@ func (this *HubConnection) worker() {
this.conn.SetReadDeadline(time.Now().Add(SEND_KEEPALIVE_EVERY)) this.conn.SetReadDeadline(time.Now().Add(SEND_KEEPALIVE_EVERY))
nbytes, err = this.conn.Read(readBuff) nbytes, err = this.conn.Read(readBuff)
if CheckIsNetTimeout(err) { if checkIsNetTimeout(err) {
// No data before read deadline // No data before read deadline
err = nil err = nil
@@ -308,6 +400,7 @@ func (this *HubConnection) worker() {
} }
if nbytes > 0 { if nbytes > 0 {
this.lastDataRecieved = time.Now()
fullBuffer += string(readBuff[0:nbytes]) fullBuffer += string(readBuff[0:nbytes])
} }
} }
@@ -326,6 +419,10 @@ func (this *HubConnection) worker() {
} }
} }
if err == nil && time.Now().Sub(this.lastDataRecieved) > RECONNECT_IF_NO_DATA_RECIEVED_IN {
err = fmt.Errorf("No packets recieved since %s, connection presumed lost", this.lastDataRecieved.Format(time.RFC3339))
}
// Maybe we disconnected // Maybe we disconnected
// Perform this check *last*, to ensure we've had a final shot at // Perform this check *last*, to ensure we've had a final shot at
// clearing out any queued messages // clearing out any queued messages

View File

@@ -11,12 +11,18 @@ type HubConnectionOptions struct {
NumEventsToBuffer uint NumEventsToBuffer uint
// Returning messages in sync mode // Returning messages in sync mode
OnEventSync func(HubEvent) OnEventSync func(*HubConnection, HubEvent)
} }
func (this *HubConnectionOptions) prepareConnection() *HubConnection { func (this *HubConnectionOptions) prepareConnection() *HubConnection {
if this.Self.ClientTag == "" { if this.Self.ClientTag == "" {
this.Self.ClientTag = DEFAULT_CLIENT_TAG this.Self.ClientTag = DEFAULT_CLIENT_TAG
this.Self.ClientVersion = DEFAULT_CLIENT_VERSION
}
// Shouldn't be blank either
if this.Self.ClientVersion == "" {
this.Self.ClientVersion = "0"
} }
hc := HubConnection{ hc := HubConnection{
@@ -54,5 +60,9 @@ func (this *HubConnectionOptions) Connect() *HubConnection {
// Client code should supply an event handling function as hco.OnEventSync. // Client code should supply an event handling function as hco.OnEventSync.
func (this *HubConnectionOptions) ConnectSync() { func (this *HubConnectionOptions) ConnectSync() {
hc := this.prepareConnection() hc := this.prepareConnection()
hc.OnEvent = nil
hc.processEvent = func(ev HubEvent) {
this.OnEventSync(hc, ev)
}
hc.worker() hc.worker()
} }

View File

@@ -11,6 +11,7 @@ const (
EVENT_CONNECTION_STATE_CHANGED = 8 EVENT_CONNECTION_STATE_CHANGED = 8
EVENT_HUBNAME_CHANGED = 9 EVENT_HUBNAME_CHANGED = 9
EVENT_DEBUG_MESSAGE = 10 EVENT_DEBUG_MESSAGE = 10
EVENT_USERCOMMAND = 11
) )
type HubEventType int type HubEventType int
@@ -20,4 +21,5 @@ type HubEvent struct {
Nick string Nick string
Message string Message string
StateChange ConnectionState StateChange ConnectionState
UserCommand *UserCommand
} }

1
TODO.txt Normal file
View File

@@ -0,0 +1 @@
- Implement ZPipe ($ZOn)

26
UserCommand.go Normal file
View File

@@ -0,0 +1,26 @@
package libnmdc
type UserCommandType uint8
const (
USERCOMMAND_TYPE_SEPARATOR UserCommandType = 0
USERCOMMAND_TYPE_RAW UserCommandType = 1
USERCOMMAND_TYPE_NICKLIMITED UserCommandType = 2
USERCOMMAND_TYPE_CLEARALL UserCommandType = 255
)
type UserCommandContext uint8
const (
USERCOMMAND_CONTEXT_HUB UserCommandContext = 1
USERCOMMAND_CONTEXT_USER UserCommandContext = 2
USERCOMMAND_CONTEXT_SEARCH UserCommandContext = 4
USERCOMMAND_CONTEXT_FILELIST UserCommandContext = 8
)
type UserCommand struct {
Type UserCommandType
Context UserCommandContext
Message string
Command string
}

17
UserFlag.go Normal file
View File

@@ -0,0 +1,17 @@
package libnmdc
type UserFlag byte
const (
FLAG_NORMAL UserFlag = 1
FLAG_AWAY_1 UserFlag = 2
FLAG_AWAY_2 UserFlag = 3
FLAG_SERVER_1 UserFlag = 4
FLAG_SERVER_2 UserFlag = 5
FLAG_SERVER_AWAY_1 UserFlag = 6
FLAG_SERVER_AWAY_2 UserFlag = 7
FLAG_FIREBALL_1 UserFlag = 8
FLAG_FIREBALL_2 UserFlag = 9
FLAG_FIREBALL_AWAY_1 UserFlag = 10
FLAG_FIREBALL_AWAY_2 UserFlag = 11
)

View File

@@ -1,4 +1,3 @@
// libnmdc project UserInfo.go
package libnmdc package libnmdc
import ( import (
@@ -9,30 +8,6 @@ import (
"strings" "strings"
) )
type UserFlag byte
const (
FLAG_NORMAL UserFlag = 1
FLAG_AWAY_1 UserFlag = 2
FLAG_AWAY_2 UserFlag = 3
FLAG_SERVER_1 UserFlag = 4
FLAG_SERVER_2 UserFlag = 5
FLAG_SERVER_AWAY_1 UserFlag = 6
FLAG_SERVER_AWAY_2 UserFlag = 7
FLAG_FIREBALL_1 UserFlag = 8
FLAG_FIREBALL_2 UserFlag = 9
FLAG_FIREBALL_AWAY_1 UserFlag = 10
FLAG_FIREBALL_AWAY_2 UserFlag = 11
)
type ConnectionMode rune
const (
CONNECTIONMODE_ACTIVE ConnectionMode = 'A'
CONNECTIONMODE_PASSIVE ConnectionMode = 'P'
CONNECTIONMODE_SOCKS5 ConnectionMode = '5'
)
// This structure represents a user connected to a hub. // This structure represents a user connected to a hub.
type UserInfo struct { type UserInfo struct {
Nick string Nick string
@@ -49,30 +24,20 @@ type UserInfo struct {
HubsRegistered uint64 HubsRegistered uint64
HubsOperator uint64 HubsOperator uint64
IsOperator bool IsOperator bool
IPAddress string
} }
var rx_myinfo *regexp.Regexp var rx_myinfo *regexp.Regexp
var rx_myinfo_notag *regexp.Regexp var rx_myinfo_notag *regexp.Regexp
func init() { func init() {
// $ALL <nick> <description>$ $<connection><flag>$<e-mail>$<sharesize>$ // Format: $ALL <nick> <description>$ $<connection><flag>$<e-mail>$<sharesize>$
rx_myinfo = regexp.MustCompile("(?ms)^\\$ALL ([^ ]+) ([^<]*)<([^,]*)V:([^,]+),M:(.),H:([0-9]+)/([0-9]+)/([0-9]+),S:([0-9]+)>\\$.\\$(.+?)(.)\\$([^$]*)\\$([0-9]*)\\$$")
rx_myinfo_notag = regexp.MustCompile("(?ms)^\\$ALL ([^ ]+) ([^$]*)\\$.\\$(.*)(.)\\$([^$]*)\\$([0-9]*)\\$$") // Fallback for no tag
/* HEAD := `(?ms)^\$ALL ([^ ]+) `
sample := "$ALL Betty description<ApexDC++ V:1.4.3,M:P,H:9/0/2,S:1>$ $0.01\x01$xyz@xyz.com$53054999578$" FOOT := `\$.\$([^$]+)\$([^$]*)\$([0-9]*)\$$`
sample = "$ALL ivysaur80 $P$10A$$0$"
u := UserInfo{} rx_myinfo = regexp.MustCompile(HEAD + `([^<]*)<(.+?) V:([^,]+),M:(.),H:([0-9]+)/([0-9]+)/([0-9]+),S:([0-9]+)>` + FOOT)
err := u.fromMyINFO(sample) rx_myinfo_notag = regexp.MustCompile(HEAD + `([^$]*)` + FOOT) // Fallback for no tag
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Printf("%+v\n", u)
}
os.Exit(1)
*/
} }
func NewUserInfo(username string) *UserInfo { func NewUserInfo(username string) *UserInfo {
@@ -93,22 +58,27 @@ func maybeParse(str string, dest *uint64, default_val uint64) {
} }
func (this *UserInfo) fromMyINFO(protomsg string) error { func (this *UserInfo) fromMyINFO(protomsg string) error {
// Normal format (with tag in exact V/M/H/S order) // Normal format (with tag in exact V/M/H/S order)
matches := rx_myinfo.FindStringSubmatch(protomsg) matches := rx_myinfo.FindStringSubmatch(protomsg)
if matches != nil { if matches != nil {
this.Nick = matches[1] this.Nick = matches[1]
this.Description = NMDCUnescape(matches[2]) this.Description = Unescape(matches[2])
this.ClientTag = NMDCUnescape(matches[3]) this.ClientTag = Unescape(matches[3])
this.ClientVersion = matches[4] this.ClientVersion = matches[4]
this.ConnectionMode = ConnectionMode(matches[4][0]) this.ConnectionMode = ConnectionMode(matches[5][0])
maybeParse(matches[6], &this.HubsUnregistered, 0) maybeParse(matches[6], &this.HubsUnregistered, 0)
maybeParse(matches[7], &this.HubsRegistered, 0) maybeParse(matches[7], &this.HubsRegistered, 0)
maybeParse(matches[8], &this.HubsOperator, 0) maybeParse(matches[8], &this.HubsOperator, 0)
maybeParse(matches[9], &this.Slots, 0) maybeParse(matches[9], &this.Slots, 0)
this.Speed = matches[10] if len(matches[10]) > 1 {
this.Flag = UserFlag(matches[11][0]) this.Speed = matches[10][:len(matches[10])-2]
this.Email = NMDCUnescape(matches[12]) } else {
maybeParse(matches[13], &this.ShareSize, 0) this.Speed = ""
}
this.Flag = UserFlag(matches[10][len(matches[10])-1])
this.Email = Unescape(matches[11])
maybeParse(matches[12], &this.ShareSize, 0)
return nil return nil
} }
@@ -117,7 +87,7 @@ func (this *UserInfo) fromMyINFO(protomsg string) error {
matches = rx_myinfo_notag.FindStringSubmatch(protomsg) matches = rx_myinfo_notag.FindStringSubmatch(protomsg)
if matches != nil { if matches != nil {
this.Nick = matches[1] this.Nick = matches[1]
this.Description = NMDCUnescape(matches[2]) this.Description = Unescape(matches[2])
this.ClientTag = "" this.ClientTag = ""
this.ClientVersion = "0" this.ClientVersion = "0"
this.ConnectionMode = CONNECTIONMODE_PASSIVE this.ConnectionMode = CONNECTIONMODE_PASSIVE
@@ -125,10 +95,15 @@ func (this *UserInfo) fromMyINFO(protomsg string) error {
this.HubsRegistered = 0 this.HubsRegistered = 0
this.HubsOperator = 0 this.HubsOperator = 0
this.Slots = 0 this.Slots = 0
this.Speed = matches[3]
this.Flag = UserFlag(matches[4][0]) if len(matches[3]) > 1 {
this.Email = NMDCUnescape(matches[5]) this.Speed = matches[3][:len(matches[3])-2]
maybeParse(matches[6], &this.ShareSize, 0) } else {
this.Speed = ""
}
this.Flag = UserFlag(matches[3][len(matches[3])-1])
this.Email = Unescape(matches[4])
maybeParse(matches[5], &this.ShareSize, 0)
return nil return nil
} }

88
UserInfo_test.go Normal file
View File

@@ -0,0 +1,88 @@
package libnmdc
import (
"testing"
)
type myInfoTestPair struct {
in string
expect UserInfo
}
func TestMyINFOParse(t *testing.T) {
cases := []myInfoTestPair{
myInfoTestPair{
in: "$ALL Bxxxy description<ApexDC++ V:1.4.3,M:P,H:9/0/2,S:1>$ $0.01\x01$xyz@example.com$53054999578$",
expect: UserInfo{
Nick: "Bxxxy",
Description: "description",
ClientTag: "ApexDC++",
ClientVersion: "1.4.3",
Email: "xyz@example.com",
ShareSize: 53054999578,
ConnectionMode: CONNECTIONMODE_PASSIVE,
Flag: FLAG_NORMAL,
Slots: 1,
Speed: "0.0",
HubsUnregistered: 9,
HubsRegistered: 0,
HubsOperator: 2,
},
},
myInfoTestPair{
in: "$ALL ixxxxxxx0 $P$10A$$0$",
expect: UserInfo{
Nick: "ixxxxxxx0",
ClientVersion: "0", // Auto-inserted by the parser for short-format MyINFO strings
ConnectionMode: CONNECTIONMODE_PASSIVE,
Flag: UserFlag(rune('A')),
Speed: "1",
},
},
myInfoTestPair{
in: "$ALL SXXXX_XXXXXXR <ncdc V:1.19.1-12-g5561,M:P,H:1/0/0,S:10>$ $0.005Q$$0$",
expect: UserInfo{
Nick: "SXXXX_XXXXXXR",
ClientTag: "ncdc",
ClientVersion: "1.19.1-12-g5561",
ConnectionMode: CONNECTIONMODE_PASSIVE,
Flag: UserFlag(rune('Q')),
Slots: 10,
Speed: "0.00",
HubsUnregistered: 1,
},
},
myInfoTestPair{
in: "$ALL mxxxu desccccc<HexChat V:2.12.1,M:P,H:1/0/0,S:0>$ $p$$0$",
expect: UserInfo{
Nick: "mxxxu",
Description: "desccccc",
ClientTag: "HexChat",
ClientVersion: "2.12.1",
ConnectionMode: CONNECTIONMODE_PASSIVE,
Flag: UserFlag(rune('p')),
HubsUnregistered: 1,
Slots: 0,
},
},
}
for _, v := range cases {
got := UserInfo{}
err := got.fromMyINFO(v.in)
if err != nil {
t.Errorf("MyINFO parse warning (%s)", err.Error())
continue
}
if got != v.expect {
t.Errorf("MyINFO parse failure\nExpected:\n%+v\nGot:\n%+v\n", v.expect, got)
continue
}
}
}

View File

@@ -1,14 +1,51 @@
An NMDC client protocol library for Golang. An NMDC client protocol library for Golang.
- Offers both channel-based and synchronous APIs. This package offers both channel-based and synchronous APIs. Example is included.
- Includes a sample logging client using the channel-based API.
- This code hosting site isn't (yet) compatible with `go get`.
Written in golang Written in golang
Tags: nmdc Tags: nmdc
=GO GET=
This package can be installed via go get: `go get code.ivysaur.me/libnmdc`
[go-get]code.ivysaur.me/libnmdc git https://git.ivysaur.me/code.ivysaur.me/libnmdc.git[/go-get]
=CHANGELOG= =CHANGELOG=
2017-11-14 0.15
- Feature: Fallback reconnection if no data (not even keepalives) are recieved from the hub in 24 hours
- Fix an issue with detecting protocol messages inside multi-line chat messages
- Update examples and the default client version number
2017-02-09 0.14
- Fix an issue with crashing on malformed IP addresses supplied by the hub
2017-02-09 0.13
- Feature: Implement UserIP2 extension, to retrieve IP addresses of other users
- Enhancement: Implement QuickList extension (reduce one network roundtrip during initial connection)
- Enhancement: Implement NoHello extension (faster connection performance)
- Enhancement: Implement ChatOnly extension
- Fix an issue with not notifying client on all MyINFO updates
2017-02-05 0.12
- Fix an issue with mutex deadlock when accessing user information from a callback
- Fix an issue with silent disconnection if a password was required but not present
2016-11-29 0.11
- BREAKING: Remove some exported methods
- BREAKING: Fix an issue with missing sufficient parameters in the synchronous API
- Enhancement: Improve output under godoc
- Fix an issue with special characters appearing in recieved private messages
- Fix an issue with parsing active/passive connection modes
- Fix an issue with errors appearing on stdout
2016-10-08 r10
- Feature: Support `$UserCommand`
2016-08-27 r9
- Fix an issue with parsing MyINFO strings with zero-length speed descriptions
- Fix an issue with not storing updated profile information
2016-05-10 r8 2016-05-10 r8
- Enhancement: Separate `ClientTag` and `ClientVersion` in `UserInfo` structs - Enhancement: Separate `ClientTag` and `ClientVersion` in `UserInfo` structs

View File

@@ -1,4 +1,3 @@
// libnmdc project libnmdc.go
package libnmdc package libnmdc
import ( import (
@@ -11,35 +10,42 @@ import (
const ( const (
DEFAULT_CLIENT_TAG string = "libnmdc.go" DEFAULT_CLIENT_TAG string = "libnmdc.go"
DEFAULT_CLIENT_VERSION string = "0.15"
DEFAULT_HUB_NAME string = "(unknown)" DEFAULT_HUB_NAME string = "(unknown)"
SEND_KEEPALIVE_EVERY time.Duration = 29 * time.Second SEND_KEEPALIVE_EVERY time.Duration = 29 * time.Second
AUTO_RECONNECT_AFTER time.Duration = 30 * time.Second AUTO_RECONNECT_AFTER time.Duration = 30 * time.Second
RECONNECT_IF_NO_DATA_RECIEVED_IN time.Duration = 24 * time.Hour // we expect keepalives wayyyy more frequently than this
) )
var rx_protocolMessage *regexp.Regexp var rx_protocolMessage *regexp.Regexp
var rx_publicChat *regexp.Regexp var rx_publicChat *regexp.Regexp
var rx_incomingTo *regexp.Regexp var rx_incomingTo *regexp.Regexp
var rx_userCommand *regexp.Regexp
var ErrNotConnected error = errors.New("Not connected") var ErrNotConnected error = errors.New("Not connected")
func init() { func init() {
rx_protocolMessage = regexp.MustCompile("(?ms)^[^|]*\\|") // With the `m` flag, use \A instead of ^ to anchor to start
rx_publicChat = regexp.MustCompile("(?ms)^<([^>]*)> (.*)$") // This fixes accidentally finding a better match in the middle of a multi-line message
rx_incomingTo = regexp.MustCompile("(?ms)^([^ ]+) From: ([^ ]+) \\$<([^>]*)> (.*)")
rx_protocolMessage = regexp.MustCompile(`(?ms)\A[^|]*\|`)
rx_publicChat = regexp.MustCompile(`(?ms)\A<([^>]*)> (.*)$`)
rx_incomingTo = regexp.MustCompile(`(?ms)\A([^ ]+) From: ([^ ]+) \$<([^>]*)> (.*)`)
rx_userCommand = regexp.MustCompile(`(?ms)\A(\d+) (\d+)\s?([^\$]*)\$?(.*)`)
} }
func NMDCUnescape(encoded string) string { func Unescape(encoded string) string {
v1 := strings.Replace(encoded, "&#36;", "$", -1) v1 := strings.Replace(encoded, "&#36;", "$", -1)
v2 := strings.Replace(v1, "&#124;", "|", -1) v2 := strings.Replace(v1, "&#124;", "|", -1)
return strings.Replace(v2, "&amp;", "&", -1) return strings.Replace(v2, "&amp;", "&", -1)
} }
func NMDCEscape(plaintext string) string { func Escape(plaintext string) string {
v1 := strings.Replace(plaintext, "&", "&amp;", -1) v1 := strings.Replace(plaintext, "&", "&amp;", -1)
v2 := strings.Replace(v1, "|", "&#124;", -1) v2 := strings.Replace(v1, "|", "&#124;", -1)
return strings.Replace(v2, "$", "&#36;", -1) return strings.Replace(v2, "$", "&#36;", -1)
} }
func NMDCUnlock(lock []byte) string { func unlock(lock []byte) string {
nibble_swap := func(b byte) byte { nibble_swap := func(b byte) byte {
return ((b << 4) & 0xF0) | ((b >> 4) & 0x0F) return ((b << 4) & 0xF0) | ((b >> 4) & 0x0F)

View File

@@ -1,38 +0,0 @@
// +build ignore
// This sample file demonstrates use of the libnmdc.go library. It's excluded
// when building the library package, but you can run it via `go run libnmdc_sample.go`.
package main
import (
"fmt"
"libnmdc"
)
func main() {
opts := libnmdc.HubConnectionOptions{
Address: "127.0.0.1",
Self: libnmdc.UserInfo{Nick: "slowpoke9"},
}
hub := opts.Connect()
for {
event := <-hub.OnEvent
switch event.EventType {
case libnmdc.EVENT_CONNECTION_STATE_CHANGED:
fmt.Printf("Connection -- %s (%s)\n", event.StateChange.Format(), event.Message)
case libnmdc.EVENT_PUBLIC:
fmt.Printf("Message from '%s': '%s'\n", event.Nick, event.Message)
if event.Message == "how are you" {
hub.SayPublic("good thanks!")
}
default:
fmt.Printf("%+v\n", event)
}
}
}