]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client.go
Troubleshoot build pipeline
[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
9174dbe8
JH
240func (c *Client) showErrMsg(msg string) {
241 time := time.Now().Format(time.RFC850)
242
243 title := "| Error |"
244
245 msgBox := tview.NewTextView().SetScrollable(true)
246 msgBox.SetText(msg).SetBackgroundColor(tcell.ColorDarkRed)
247 msgBox.SetTitle(title).SetBorder(true)
248 msgBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
249 switch event.Key() {
250 case tcell.KeyEscape:
251 c.UI.Pages.RemovePage("serverMsgModal" + time)
252 }
253 return event
254 })
255
256 centeredFlex := tview.NewFlex().
257 AddItem(nil, 0, 1, false).
258 AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
259 AddItem(nil, 0, 1, false).
260 AddItem(msgBox, 0, 2, true).
261 AddItem(nil, 0, 1, false), 0, 2, true).
262 AddItem(nil, 0, 1, false)
263
264 c.UI.Pages.AddPage("serverMsgModal"+time, centeredFlex, true, true)
265 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
266}
267
43ecc0f4 268func handleGetFileNameList(c *Client, t *Transaction) (res []Transaction, err error) {
9174dbe8
JH
269 if t.IsError() {
270 c.showErrMsg(string(t.GetField(fieldError).Data))
271 c.Logger.Infof("Error: %s", t.GetField(fieldError).Data)
272 return res, err
273 }
274
43ecc0f4
JH
275 fTree := tview.NewTreeView().SetTopLevel(1)
276 root := tview.NewTreeNode("Root")
277 fTree.SetRoot(root).SetCurrentNode(root)
278 fTree.SetBorder(true).SetTitle("| Files |")
279 fTree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
280 switch event.Key() {
281 case tcell.KeyEscape:
282 c.UI.Pages.RemovePage("files")
283 c.filePath = []string{}
284 case tcell.KeyEnter:
285 selectedNode := fTree.GetCurrentNode()
286
287 if selectedNode.GetText() == "<- Back" {
288 c.filePath = c.filePath[:len(c.filePath)-1]
289 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
290
291 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
292 c.UI.HLClient.Logger.Errorw("err", "err", err)
293 }
294 return event
295 }
296
297 entry := selectedNode.GetReference().(*FileNameWithInfo)
298
72dd37f1
JH
299 if bytes.Equal(entry.Type[:], []byte("fldr")) {
300 c.Logger.Infow("get new directory listing", "name", string(entry.name))
43ecc0f4 301
72dd37f1 302 c.filePath = append(c.filePath, string(entry.name))
43ecc0f4
JH
303 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
304
305 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
306 c.UI.HLClient.Logger.Errorw("err", "err", err)
307 }
308 } else {
309 // TODO: initiate file download
72dd37f1 310 c.Logger.Infow("download file", "name", string(entry.name))
43ecc0f4
JH
311 }
312 }
313
314 return event
315 })
316
317 if len(c.filePath) > 0 {
318 node := tview.NewTreeNode("<- Back")
319 root.AddChild(node)
320 }
321
43ecc0f4
JH
322 for _, f := range t.Fields {
323 var fn FileNameWithInfo
72dd37f1
JH
324 err = fn.UnmarshalBinary(f.Data)
325 if err != nil {
326 return nil, nil
327 }
43ecc0f4 328
72dd37f1
JH
329 if bytes.Equal(fn.Type[:], []byte("fldr")) {
330 node := tview.NewTreeNode(fmt.Sprintf("[blue::]📁 %s[-:-:-]", fn.name))
43ecc0f4
JH
331 node.SetReference(&fn)
332 root.AddChild(node)
333 } else {
72dd37f1 334 size := binary.BigEndian.Uint32(fn.FileSize[:]) / 1024
43ecc0f4 335
72dd37f1 336 node := tview.NewTreeNode(fmt.Sprintf(" %-40s %10v KB", fn.name, size))
43ecc0f4
JH
337 node.SetReference(&fn)
338 root.AddChild(node)
339 }
340
341 }
342
343 centerFlex := tview.NewFlex().
344 AddItem(nil, 0, 1, false).
345 AddItem(tview.NewFlex().
346 SetDirection(tview.FlexRow).
347 AddItem(nil, 0, 1, false).
348 AddItem(fTree, 20, 1, true).
246ed3a1 349 AddItem(nil, 0, 1, false), 60, 1, true).
43ecc0f4
JH
350 AddItem(nil, 0, 1, false)
351
352 c.UI.Pages.AddPage("files", centerFlex, true, true)
353 c.UI.App.Draw()
354
355 return res, err
6988a057
JH
356}
357
358func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
359 newsText := string(t.GetField(fieldData).Data)
360 newsText = strings.ReplaceAll(newsText, "\r", "\n")
361
362 newsTextView := tview.NewTextView().
363 SetText(newsText).
364 SetDoneFunc(func(key tcell.Key) {
40afb444 365 c.UI.Pages.SwitchToPage(serverUIPage)
6988a057
JH
366 c.UI.App.SetFocus(c.UI.chatInput)
367 })
368 newsTextView.SetBorder(true).SetTitle("News")
369
370 c.UI.Pages.AddPage("news", newsTextView, true, true)
aebc4d36
JH
371 // c.UI.Pages.SwitchToPage("news")
372 // c.UI.App.SetFocus(newsTextView)
6988a057
JH
373 c.UI.App.Draw()
374
375 return res, err
376}
377
378func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
379 newUser := User{
380 ID: t.GetField(fieldUserID).Data,
381 Name: string(t.GetField(fieldUserName).Data),
382 Icon: t.GetField(fieldUserIconID).Data,
383 Flags: t.GetField(fieldUserFlags).Data,
384 }
385
386 // Possible cases:
387 // user is new to the server
388 // user is already on the server but has a new name
389
390 var oldName string
391 var newUserList []User
392 updatedUser := false
393 for _, u := range c.UserList {
394 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
395 if bytes.Equal(newUser.ID, u.ID) {
396 oldName = u.Name
397 u.Name = newUser.Name
398 if u.Name != newUser.Name {
399 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
400 }
401 updatedUser = true
402 }
403 newUserList = append(newUserList, u)
404 }
405
406 if !updatedUser {
407 newUserList = append(newUserList, newUser)
408 }
409
410 c.UserList = newUserList
411
412 c.renderUserList()
413
414 return res, err
415}
416
417func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
418 exitUser := t.GetField(fieldUserID).Data
419
420 var newUserList []User
421 for _, u := range c.UserList {
422 if !bytes.Equal(exitUser, u.ID) {
423 newUserList = append(newUserList, u)
424 }
425 }
426
427 c.UserList = newUserList
428
429 c.renderUserList()
430
431 return res, err
432}
433
6988a057
JH
434func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
435 var users []User
436 for _, field := range t.Fields {
71c56068
JH
437 // The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
438 // fields, but shxd sneaks in fieldChatSubject (115) so it's important to filter explicitly for the expected
439 // field type. Probably a good idea to do everywhere.
440 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
441 u, err := ReadUser(field.Data)
442 if err != nil {
443 return res, err
444 }
445 users = append(users, *u)
446 }
6988a057
JH
447 }
448 c.UserList = users
449
450 c.renderUserList()
451
452 return res, err
453}
454
455func (c *Client) renderUserList() {
456 c.UI.userList.Clear()
457 for _, u := range c.UserList {
458 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
459 if flagBitmap.Bit(userFlagAdmin) == 1 {
5dd57308 460 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
6988a057 461 } else {
5dd57308 462 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
6988a057 463 }
b198b22b 464 // TODO: fade if user is away
6988a057
JH
465 }
466}
467
468func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
5dd57308 469 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
6988a057
JH
470
471 return res, err
472}
473
474func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
475 c.UserAccess = t.GetField(fieldUserAccess).Data
476
477 return res, err
478}
479
480func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
481 agreement := string(t.GetField(fieldData).Data)
482 agreement = strings.ReplaceAll(agreement, "\r", "\n")
483
72dd37f1 484 agreeModal := tview.NewModal().
6988a057
JH
485 SetText(agreement).
486 AddButtons([]string{"Agree", "Disagree"}).
487 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
488 if buttonIndex == 0 {
489 res = append(res,
490 *NewTransaction(
491 tranAgreed, nil,
492 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 493 NewField(fieldUserIconID, c.pref.IconBytes()),
6988a057
JH
494 NewField(fieldUserFlags, []byte{0x00, 0x00}),
495 NewField(fieldOptions, []byte{0x00, 0x00}),
496 ),
497 )
6988a057
JH
498 c.UI.Pages.HidePage("agreement")
499 c.UI.App.SetFocus(c.UI.chatInput)
500 } else {
f7e36225 501 _ = c.Disconnect()
6988a057
JH
502 c.UI.Pages.SwitchToPage("home")
503 }
504 },
505 )
506
72dd37f1 507 c.UI.Pages.AddPage("agreement", agreeModal, false, true)
b198b22b 508
6988a057
JH
509 return res, err
510}
511
512func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
513 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
514 errMsg := string(t.GetField(fieldError).Data)
515 errModal := tview.NewModal()
516 errModal.SetText(errMsg)
517 errModal.AddButtons([]string{"Oh no"})
518 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
519 c.UI.Pages.RemovePage("errModal")
520 })
521 c.UI.Pages.RemovePage("joinServer")
522 c.UI.Pages.AddPage("errModal", errModal, false, true)
523
524 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
525
526 c.Logger.Error(string(t.GetField(fieldError).Data))
527 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
528 }
40afb444 529 c.UI.Pages.AddAndSwitchToPage(serverUIPage, c.UI.renderServerUI(), true)
6988a057
JH
530 c.UI.App.SetFocus(c.UI.chatInput)
531
532 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
533 c.Logger.Errorw("err", "err", err)
534 }
535 return res, err
536}
537
538// JoinServer connects to a Hotline server and completes the login flow
539func (c *Client) JoinServer(address, login, passwd string) error {
540 // Establish TCP connection to server
541 if err := c.connect(address); err != nil {
542 return err
543 }
544
545 // Send handshake sequence
546 if err := c.Handshake(); err != nil {
547 return err
548 }
549
550 // Authenticate (send tranLogin 107)
551 if err := c.LogIn(login, passwd); err != nil {
552 return err
553 }
554
9d41bcdf
JH
555 // start keepalive go routine
556 go func() { _ = c.keepalive() }()
557
6988a057
JH
558 return nil
559}
560
9d41bcdf
JH
561func (c *Client) keepalive() error {
562 for {
563 time.Sleep(300 * time.Second)
564 _ = c.Send(*NewTransaction(tranKeepAlive, nil))
565 c.Logger.Infow("Sent keepalive ping")
566 }
567}
568
6988a057
JH
569// connect establishes a connection with a Server by sending handshake sequence
570func (c *Client) connect(address string) error {
571 var err error
572 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
573 if err != nil {
574 return err
575 }
576 return nil
577}
578
579var ClientHandshake = []byte{
580 0x54, 0x52, 0x54, 0x50, // TRTP
581 0x48, 0x4f, 0x54, 0x4c, // HOTL
582 0x00, 0x01,
583 0x00, 0x02,
584}
585
586var ServerHandshake = []byte{
587 0x54, 0x52, 0x54, 0x50, // TRTP
588 0x00, 0x00, 0x00, 0x00, // ErrorCode
589}
590
591func (c *Client) Handshake() error {
aebc4d36
JH
592 // Protocol ID 4 ‘TRTP’ 0x54 52 54 50
593 // Sub-protocol ID 4 User defined
594 // Version 2 1 Currently 1
595 // Sub-version 2 User defined
6988a057
JH
596 if _, err := c.Connection.Write(ClientHandshake); err != nil {
597 return fmt.Errorf("handshake write err: %s", err)
598 }
599
600 replyBuf := make([]byte, 8)
601 _, err := c.Connection.Read(replyBuf)
602 if err != nil {
603 return err
604 }
605
72dd37f1 606 if bytes.Equal(replyBuf, ServerHandshake) {
6988a057
JH
607 return nil
608 }
6988a057 609
b198b22b 610 // In the case of an error, client and server close the connection.
6988a057
JH
611 return fmt.Errorf("handshake response err: %s", err)
612}
613
614func (c *Client) LogIn(login string, password string) error {
615 return c.Send(
616 *NewTransaction(
617 tranLogin, nil,
618 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 619 NewField(fieldUserIconID, c.pref.IconBytes()),
b25c4a19
JH
620 NewField(fieldUserLogin, negateString([]byte(login))),
621 NewField(fieldUserPassword, negateString([]byte(password))),
6988a057
JH
622 ),
623 )
624}
625
6988a057
JH
626func (c *Client) Send(t Transaction) error {
627 requestNum := binary.BigEndian.Uint16(t.Type)
628 tID := binary.BigEndian.Uint32(t.ID)
629
aebc4d36 630 // handler := TransactionHandlers[requestNum]
6988a057
JH
631
632 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
633 if t.IsReply == 0 {
634 c.activeTasks[tID] = &t
635 }
636
637 var n int
638 var err error
72dd37f1
JH
639 b, err := t.MarshalBinary()
640 if err != nil {
641 return err
642 }
643 if n, err = c.Connection.Write(b); err != nil {
6988a057
JH
644 return err
645 }
646 c.Logger.Debugw("Sent Transaction",
647 "IsReply", t.IsReply,
648 "type", requestNum,
649 "sentBytes", n,
650 )
651 return nil
652}
653
654func (c *Client) HandleTransaction(t *Transaction) error {
655 var origT Transaction
656 if t.IsReply == 1 {
657 requestID := binary.BigEndian.Uint32(t.ID)
658 origT = *c.activeTasks[requestID]
659 t.Type = origT.Type
660 }
661
662 requestNum := binary.BigEndian.Uint16(t.Type)
0da28a1f 663 c.Logger.Debugw("Received Transaction", "RequestType", requestNum)
6988a057
JH
664
665 if handler, ok := c.Handlers[requestNum]; ok {
666 outT, _ := handler.Handle(c, t)
667 for _, t := range outT {
668 c.Send(t)
669 }
670 } else {
671 c.Logger.Errorw(
672 "Unimplemented transaction type received",
673 "RequestID", requestNum,
674 "TransactionID", t.ID,
675 )
676 }
677
678 return nil
679}
680
6988a057 681func (c *Client) Disconnect() error {
00d1ef67 682 return c.Connection.Close()
6988a057 683}