]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client.go
Fix panic when commenting file without existing info fork
[rbdr/mobius] / hotline / client.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
4 "bytes"
5 "embed"
6 "encoding/binary"
7 "errors"
8 "fmt"
6988a057
JH
9 "github.com/gdamore/tcell/v2"
10 "github.com/rivo/tview"
11 "github.com/stretchr/testify/mock"
12 "go.uber.org/zap"
0197c3f5 13 "gopkg.in/yaml.v3"
6988a057
JH
14 "math/big"
15 "math/rand"
16 "net"
17 "os"
18 "strings"
19 "time"
20)
21
4f3c459c
JH
22const (
23 trackerListPage = "trackerList"
e005c191 24 serverUIPage = "serverUI"
4f3c459c 25)
6988a057 26
22c599ab 27//go:embed banners/*.txt
6988a057
JH
28var bannerDir embed.FS
29
30type Bookmark struct {
31 Name string `yaml:"Name"`
32 Addr string `yaml:"Addr"`
33 Login string `yaml:"Login"`
34 Password string `yaml:"Password"`
35}
36
37type ClientPrefs struct {
38 Username string `yaml:"Username"`
39 IconID int `yaml:"IconID"`
40 Bookmarks []Bookmark `yaml:"Bookmarks"`
4f3c459c 41 Tracker string `yaml:"Tracker"`
6988a057
JH
42}
43
f7e36225
JH
44func (cp *ClientPrefs) IconBytes() []byte {
45 iconBytes := make([]byte, 2)
46 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
47 return iconBytes
48}
49
da1e0d79
JH
50func (cp *ClientPrefs) AddBookmark(name, addr, login, pass string) error {
51 cp.Bookmarks = append(cp.Bookmarks, Bookmark{Addr: addr, Login: login, Password: pass})
52
53 return nil
54}
55
6988a057
JH
56func readConfig(cfgPath string) (*ClientPrefs, error) {
57 fh, err := os.Open(cfgPath)
58 if err != nil {
59 return nil, err
60 }
61
62 prefs := ClientPrefs{}
63 decoder := yaml.NewDecoder(fh)
6988a057
JH
64 if err := decoder.Decode(&prefs); err != nil {
65 return nil, err
66 }
67 return &prefs, nil
68}
69
70type Client struct {
95753255 71 cfgPath string
6988a057
JH
72 DebugBuf *DebugBuffer
73 Connection net.Conn
6988a057
JH
74 Login *[]byte
75 Password *[]byte
6988a057
JH
76 Flags *[]byte
77 ID *[]byte
78 Version []byte
79 UserAccess []byte
43ecc0f4 80 filePath []string
6988a057
JH
81 UserList []User
82 Logger *zap.SugaredLogger
83 activeTasks map[uint32]*Transaction
e005c191 84 serverName string
6988a057
JH
85
86 pref *ClientPrefs
87
88 Handlers map[uint16]clientTHandler
89
90 UI *UI
91
72dd37f1 92 Inbox chan *Transaction
6988a057
JH
93}
94
b198b22b
JH
95func NewClient(cfgPath string, logger *zap.SugaredLogger) *Client {
96 c := &Client{
97 cfgPath: cfgPath,
98 Logger: logger,
99 activeTasks: make(map[uint32]*Transaction),
100 Handlers: clientHandlers,
6988a057 101 }
b198b22b 102 c.UI = NewUI(c)
6988a057 103
b198b22b 104 prefs, err := readConfig(cfgPath)
6988a057 105 if err != nil {
f4a69647 106 logger.Fatal(fmt.Sprintf("unable to read config file %s\n", cfgPath))
6988a057 107 }
b198b22b 108 c.pref = prefs
6988a057 109
b198b22b 110 return c
6988a057
JH
111}
112
6988a057
JH
113// DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
114type DebugBuffer struct {
115 TextView *tview.TextView
116}
117
118func (db *DebugBuffer) Write(p []byte) (int, error) {
119 return db.TextView.Write(p)
120}
121
7cd900d6 122// Sync is a noop function that dataFile to satisfy the zapcore.WriteSyncer interface
6988a057
JH
123func (db *DebugBuffer) Sync() error {
124 return nil
125}
126
6988a057
JH
127func randomBanner() string {
128 rand.Seed(time.Now().UnixNano())
129
ce348eb8
JH
130 bannerFiles, _ := bannerDir.ReadDir("banners")
131 file, _ := bannerDir.ReadFile("banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
6988a057
JH
132
133 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
134}
135
6988a057
JH
136type clientTransaction struct {
137 Name string
138 Handler func(*Client, *Transaction) ([]Transaction, error)
139}
140
141func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
142 return ch.Handler(cc, t)
143}
144
145type clientTHandler interface {
146 Handle(*Client, *Transaction) ([]Transaction, error)
147}
148
149type mockClientHandler struct {
150 mock.Mock
151}
152
153func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
154 args := mh.Called(cc, t)
155 return args.Get(0).([]Transaction), args.Error(1)
156}
157
158var clientHandlers = map[uint16]clientTHandler{
159 // Server initiated
160 tranChatMsg: clientTransaction{
161 Name: "tranChatMsg",
162 Handler: handleClientChatMsg,
163 },
164 tranLogin: clientTransaction{
165 Name: "tranLogin",
166 Handler: handleClientTranLogin,
167 },
168 tranShowAgreement: clientTransaction{
169 Name: "tranShowAgreement",
170 Handler: handleClientTranShowAgreement,
171 },
172 tranUserAccess: clientTransaction{
173 Name: "tranUserAccess",
174 Handler: handleClientTranUserAccess,
175 },
176 tranGetUserNameList: clientTransaction{
177 Name: "tranGetUserNameList",
178 Handler: handleClientGetUserNameList,
179 },
180 tranNotifyChangeUser: clientTransaction{
181 Name: "tranNotifyChangeUser",
182 Handler: handleNotifyChangeUser,
183 },
184 tranNotifyDeleteUser: clientTransaction{
185 Name: "tranNotifyDeleteUser",
186 Handler: handleNotifyDeleteUser,
187 },
188 tranGetMsgs: clientTransaction{
189 Name: "tranNotifyDeleteUser",
190 Handler: handleGetMsgs,
191 },
43ecc0f4
JH
192 tranGetFileNameList: clientTransaction{
193 Name: "tranGetFileNameList",
194 Handler: handleGetFileNameList,
195 },
3d2bd095
JH
196 tranServerMsg: clientTransaction{
197 Name: "tranServerMsg",
198 Handler: handleTranServerMsg,
199 },
9d41bcdf 200 tranKeepAlive: clientTransaction{
00d1ef67 201 Name: "tranKeepAlive",
9d41bcdf
JH
202 Handler: func(client *Client, transaction *Transaction) (t []Transaction, err error) {
203 return t, err
204 },
205 },
3d2bd095
JH
206}
207
208func handleTranServerMsg(c *Client, t *Transaction) (res []Transaction, err error) {
209 time := time.Now().Format(time.RFC850)
210
211 msg := strings.ReplaceAll(string(t.GetField(fieldData).Data), "\r", "\n")
5c34f875 212 msg += "\n\nAt " + time
3d2bd095
JH
213 title := fmt.Sprintf("| Private Message From: %s |", t.GetField(fieldUserName).Data)
214
215 msgBox := tview.NewTextView().SetScrollable(true)
216 msgBox.SetText(msg).SetBackgroundColor(tcell.ColorDarkSlateBlue)
217 msgBox.SetTitle(title).SetBorder(true)
218 msgBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
219 switch event.Key() {
220 case tcell.KeyEscape:
221 c.UI.Pages.RemovePage("serverMsgModal" + time)
222 }
223 return event
224 })
225
226 centeredFlex := tview.NewFlex().
227 AddItem(nil, 0, 1, false).
228 AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
229 AddItem(nil, 0, 1, false).
230 AddItem(msgBox, 0, 2, true).
231 AddItem(nil, 0, 1, false), 0, 2, true).
232 AddItem(nil, 0, 1, false)
233
5c34f875 234 c.UI.Pages.AddPage("serverMsgModal"+time, centeredFlex, true, true)
3d2bd095
JH
235 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
236
237 return res, err
43ecc0f4
JH
238}
239
240func handleGetFileNameList(c *Client, t *Transaction) (res []Transaction, err error) {
241 fTree := tview.NewTreeView().SetTopLevel(1)
242 root := tview.NewTreeNode("Root")
243 fTree.SetRoot(root).SetCurrentNode(root)
244 fTree.SetBorder(true).SetTitle("| Files |")
245 fTree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
246 switch event.Key() {
247 case tcell.KeyEscape:
248 c.UI.Pages.RemovePage("files")
249 c.filePath = []string{}
250 case tcell.KeyEnter:
251 selectedNode := fTree.GetCurrentNode()
252
253 if selectedNode.GetText() == "<- Back" {
254 c.filePath = c.filePath[:len(c.filePath)-1]
255 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
256
257 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
258 c.UI.HLClient.Logger.Errorw("err", "err", err)
259 }
260 return event
261 }
262
263 entry := selectedNode.GetReference().(*FileNameWithInfo)
264
72dd37f1
JH
265 if bytes.Equal(entry.Type[:], []byte("fldr")) {
266 c.Logger.Infow("get new directory listing", "name", string(entry.name))
43ecc0f4 267
72dd37f1 268 c.filePath = append(c.filePath, string(entry.name))
43ecc0f4
JH
269 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
270
271 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
272 c.UI.HLClient.Logger.Errorw("err", "err", err)
273 }
274 } else {
275 // TODO: initiate file download
72dd37f1 276 c.Logger.Infow("download file", "name", string(entry.name))
43ecc0f4
JH
277 }
278 }
279
280 return event
281 })
282
283 if len(c.filePath) > 0 {
284 node := tview.NewTreeNode("<- Back")
285 root.AddChild(node)
286 }
287
43ecc0f4
JH
288 for _, f := range t.Fields {
289 var fn FileNameWithInfo
72dd37f1
JH
290 err = fn.UnmarshalBinary(f.Data)
291 if err != nil {
292 return nil, nil
293 }
43ecc0f4 294
72dd37f1
JH
295 if bytes.Equal(fn.Type[:], []byte("fldr")) {
296 node := tview.NewTreeNode(fmt.Sprintf("[blue::]📁 %s[-:-:-]", fn.name))
43ecc0f4
JH
297 node.SetReference(&fn)
298 root.AddChild(node)
299 } else {
72dd37f1 300 size := binary.BigEndian.Uint32(fn.FileSize[:]) / 1024
43ecc0f4 301
72dd37f1 302 node := tview.NewTreeNode(fmt.Sprintf(" %-40s %10v KB", fn.name, size))
43ecc0f4
JH
303 node.SetReference(&fn)
304 root.AddChild(node)
305 }
306
307 }
308
309 centerFlex := tview.NewFlex().
310 AddItem(nil, 0, 1, false).
311 AddItem(tview.NewFlex().
312 SetDirection(tview.FlexRow).
313 AddItem(nil, 0, 1, false).
314 AddItem(fTree, 20, 1, true).
246ed3a1 315 AddItem(nil, 0, 1, false), 60, 1, true).
43ecc0f4
JH
316 AddItem(nil, 0, 1, false)
317
318 c.UI.Pages.AddPage("files", centerFlex, true, true)
319 c.UI.App.Draw()
320
321 return res, err
6988a057
JH
322}
323
324func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
325 newsText := string(t.GetField(fieldData).Data)
326 newsText = strings.ReplaceAll(newsText, "\r", "\n")
327
328 newsTextView := tview.NewTextView().
329 SetText(newsText).
330 SetDoneFunc(func(key tcell.Key) {
40afb444 331 c.UI.Pages.SwitchToPage(serverUIPage)
6988a057
JH
332 c.UI.App.SetFocus(c.UI.chatInput)
333 })
334 newsTextView.SetBorder(true).SetTitle("News")
335
336 c.UI.Pages.AddPage("news", newsTextView, true, true)
aebc4d36
JH
337 // c.UI.Pages.SwitchToPage("news")
338 // c.UI.App.SetFocus(newsTextView)
6988a057
JH
339 c.UI.App.Draw()
340
341 return res, err
342}
343
344func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
345 newUser := User{
346 ID: t.GetField(fieldUserID).Data,
347 Name: string(t.GetField(fieldUserName).Data),
348 Icon: t.GetField(fieldUserIconID).Data,
349 Flags: t.GetField(fieldUserFlags).Data,
350 }
351
352 // Possible cases:
353 // user is new to the server
354 // user is already on the server but has a new name
355
356 var oldName string
357 var newUserList []User
358 updatedUser := false
359 for _, u := range c.UserList {
360 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
361 if bytes.Equal(newUser.ID, u.ID) {
362 oldName = u.Name
363 u.Name = newUser.Name
364 if u.Name != newUser.Name {
365 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
366 }
367 updatedUser = true
368 }
369 newUserList = append(newUserList, u)
370 }
371
372 if !updatedUser {
373 newUserList = append(newUserList, newUser)
374 }
375
376 c.UserList = newUserList
377
378 c.renderUserList()
379
380 return res, err
381}
382
383func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
384 exitUser := t.GetField(fieldUserID).Data
385
386 var newUserList []User
387 for _, u := range c.UserList {
388 if !bytes.Equal(exitUser, u.ID) {
389 newUserList = append(newUserList, u)
390 }
391 }
392
393 c.UserList = newUserList
394
395 c.renderUserList()
396
397 return res, err
398}
399
400const readBuffSize = 1024000 // 1KB - TODO: what should this be?
401
402func (c *Client) ReadLoop() error {
403 tranBuff := make([]byte, 0)
404 tReadlen := 0
405 // Infinite loop where take action on incoming client requests until the connection is closed
406 for {
407 buf := make([]byte, readBuffSize)
408 tranBuff = tranBuff[tReadlen:]
409
410 readLen, err := c.Connection.Read(buf)
411 if err != nil {
412 return err
413 }
414 tranBuff = append(tranBuff, buf[:readLen]...)
415
416 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
417 // into a slice of transactions
418 var transactions []Transaction
419 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
420 c.Logger.Errorw("Error handling transaction", "err", err)
421 }
422
423 // iterate over all of the transactions that were parsed from the byte slice and handle them
424 for _, t := range transactions {
425 if err := c.HandleTransaction(&t); err != nil {
426 c.Logger.Errorw("Error handling transaction", "err", err)
427 }
428 }
429 }
430}
431
432func (c *Client) GetTransactions() error {
433 tranBuff := make([]byte, 0)
434 tReadlen := 0
435
436 buf := make([]byte, readBuffSize)
437 tranBuff = tranBuff[tReadlen:]
438
439 readLen, err := c.Connection.Read(buf)
440 if err != nil {
441 return err
442 }
443 tranBuff = append(tranBuff, buf[:readLen]...)
444
445 return nil
446}
447
448func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
449 var users []User
450 for _, field := range t.Fields {
71c56068
JH
451 // The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
452 // fields, but shxd sneaks in fieldChatSubject (115) so it's important to filter explicitly for the expected
453 // field type. Probably a good idea to do everywhere.
454 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
455 u, err := ReadUser(field.Data)
456 if err != nil {
457 return res, err
458 }
459 users = append(users, *u)
460 }
6988a057
JH
461 }
462 c.UserList = users
463
464 c.renderUserList()
465
466 return res, err
467}
468
469func (c *Client) renderUserList() {
470 c.UI.userList.Clear()
471 for _, u := range c.UserList {
472 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
473 if flagBitmap.Bit(userFlagAdmin) == 1 {
5dd57308 474 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
6988a057 475 } else {
5dd57308 476 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
6988a057 477 }
b198b22b 478 // TODO: fade if user is away
6988a057
JH
479 }
480}
481
482func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
5dd57308 483 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
6988a057
JH
484
485 return res, err
486}
487
488func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
489 c.UserAccess = t.GetField(fieldUserAccess).Data
490
491 return res, err
492}
493
494func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
495 agreement := string(t.GetField(fieldData).Data)
496 agreement = strings.ReplaceAll(agreement, "\r", "\n")
497
72dd37f1 498 agreeModal := tview.NewModal().
6988a057
JH
499 SetText(agreement).
500 AddButtons([]string{"Agree", "Disagree"}).
501 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
502 if buttonIndex == 0 {
503 res = append(res,
504 *NewTransaction(
505 tranAgreed, nil,
506 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 507 NewField(fieldUserIconID, c.pref.IconBytes()),
6988a057
JH
508 NewField(fieldUserFlags, []byte{0x00, 0x00}),
509 NewField(fieldOptions, []byte{0x00, 0x00}),
510 ),
511 )
6988a057
JH
512 c.UI.Pages.HidePage("agreement")
513 c.UI.App.SetFocus(c.UI.chatInput)
514 } else {
f7e36225 515 _ = c.Disconnect()
6988a057
JH
516 c.UI.Pages.SwitchToPage("home")
517 }
518 },
519 )
520
72dd37f1 521 c.UI.Pages.AddPage("agreement", agreeModal, false, true)
b198b22b 522
6988a057
JH
523 return res, err
524}
525
526func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
527 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
528 errMsg := string(t.GetField(fieldError).Data)
529 errModal := tview.NewModal()
530 errModal.SetText(errMsg)
531 errModal.AddButtons([]string{"Oh no"})
532 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
533 c.UI.Pages.RemovePage("errModal")
534 })
535 c.UI.Pages.RemovePage("joinServer")
536 c.UI.Pages.AddPage("errModal", errModal, false, true)
537
538 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
539
540 c.Logger.Error(string(t.GetField(fieldError).Data))
541 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
542 }
40afb444 543 c.UI.Pages.AddAndSwitchToPage(serverUIPage, c.UI.renderServerUI(), true)
6988a057
JH
544 c.UI.App.SetFocus(c.UI.chatInput)
545
546 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
547 c.Logger.Errorw("err", "err", err)
548 }
549 return res, err
550}
551
552// JoinServer connects to a Hotline server and completes the login flow
553func (c *Client) JoinServer(address, login, passwd string) error {
554 // Establish TCP connection to server
555 if err := c.connect(address); err != nil {
556 return err
557 }
558
559 // Send handshake sequence
560 if err := c.Handshake(); err != nil {
561 return err
562 }
563
564 // Authenticate (send tranLogin 107)
565 if err := c.LogIn(login, passwd); err != nil {
566 return err
567 }
568
9d41bcdf
JH
569 // start keepalive go routine
570 go func() { _ = c.keepalive() }()
571
6988a057
JH
572 return nil
573}
574
9d41bcdf
JH
575func (c *Client) keepalive() error {
576 for {
577 time.Sleep(300 * time.Second)
578 _ = c.Send(*NewTransaction(tranKeepAlive, nil))
579 c.Logger.Infow("Sent keepalive ping")
580 }
581}
582
6988a057
JH
583// connect establishes a connection with a Server by sending handshake sequence
584func (c *Client) connect(address string) error {
585 var err error
586 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
587 if err != nil {
588 return err
589 }
590 return nil
591}
592
593var ClientHandshake = []byte{
594 0x54, 0x52, 0x54, 0x50, // TRTP
595 0x48, 0x4f, 0x54, 0x4c, // HOTL
596 0x00, 0x01,
597 0x00, 0x02,
598}
599
600var ServerHandshake = []byte{
601 0x54, 0x52, 0x54, 0x50, // TRTP
602 0x00, 0x00, 0x00, 0x00, // ErrorCode
603}
604
605func (c *Client) Handshake() error {
aebc4d36
JH
606 // Protocol ID 4 ‘TRTP’ 0x54 52 54 50
607 // Sub-protocol ID 4 User defined
608 // Version 2 1 Currently 1
609 // Sub-version 2 User defined
6988a057
JH
610 if _, err := c.Connection.Write(ClientHandshake); err != nil {
611 return fmt.Errorf("handshake write err: %s", err)
612 }
613
614 replyBuf := make([]byte, 8)
615 _, err := c.Connection.Read(replyBuf)
616 if err != nil {
617 return err
618 }
619
72dd37f1 620 if bytes.Equal(replyBuf, ServerHandshake) {
6988a057
JH
621 return nil
622 }
6988a057 623
b198b22b 624 // In the case of an error, client and server close the connection.
6988a057
JH
625 return fmt.Errorf("handshake response err: %s", err)
626}
627
628func (c *Client) LogIn(login string, password string) error {
629 return c.Send(
630 *NewTransaction(
631 tranLogin, nil,
632 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 633 NewField(fieldUserIconID, c.pref.IconBytes()),
b25c4a19
JH
634 NewField(fieldUserLogin, negateString([]byte(login))),
635 NewField(fieldUserPassword, negateString([]byte(password))),
6988a057
JH
636 ),
637 )
638}
639
6988a057
JH
640func (c *Client) Send(t Transaction) error {
641 requestNum := binary.BigEndian.Uint16(t.Type)
642 tID := binary.BigEndian.Uint32(t.ID)
643
aebc4d36 644 // handler := TransactionHandlers[requestNum]
6988a057
JH
645
646 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
647 if t.IsReply == 0 {
648 c.activeTasks[tID] = &t
649 }
650
651 var n int
652 var err error
72dd37f1
JH
653 b, err := t.MarshalBinary()
654 if err != nil {
655 return err
656 }
657 if n, err = c.Connection.Write(b); err != nil {
6988a057
JH
658 return err
659 }
660 c.Logger.Debugw("Sent Transaction",
661 "IsReply", t.IsReply,
662 "type", requestNum,
663 "sentBytes", n,
664 )
665 return nil
666}
667
668func (c *Client) HandleTransaction(t *Transaction) error {
669 var origT Transaction
670 if t.IsReply == 1 {
671 requestID := binary.BigEndian.Uint32(t.ID)
672 origT = *c.activeTasks[requestID]
673 t.Type = origT.Type
674 }
675
676 requestNum := binary.BigEndian.Uint16(t.Type)
0da28a1f 677 c.Logger.Debugw("Received Transaction", "RequestType", requestNum)
6988a057
JH
678
679 if handler, ok := c.Handlers[requestNum]; ok {
680 outT, _ := handler.Handle(c, t)
681 for _, t := range outT {
682 c.Send(t)
683 }
684 } else {
685 c.Logger.Errorw(
686 "Unimplemented transaction type received",
687 "RequestID", requestNum,
688 "TransactionID", t.ID,
689 )
690 }
691
692 return nil
693}
694
6988a057 695func (c *Client) Disconnect() error {
00d1ef67 696 return c.Connection.Close()
6988a057 697}