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