9 "github.com/davecgh/go-spew/spew"
10 "github.com/gdamore/tcell/v2"
11 "github.com/rivo/tview"
12 "github.com/stretchr/testify/mock"
24 const clientConfigPath = "/usr/local/etc/mobius-client-config.yaml"
26 //go:embed client/banners/*.txt
27 var bannerDir embed.FS
29 type Bookmark struct {
30 Name string `yaml:"Name"`
31 Addr string `yaml:"Addr"`
32 Login string `yaml:"Login"`
33 Password string `yaml:"Password"`
36 type ClientPrefs struct {
37 Username string `yaml:"Username"`
38 IconID int `yaml:"IconID"`
39 Bookmarks []Bookmark `yaml:"Bookmarks"`
42 func readConfig(cfgPath string) (*ClientPrefs, error) {
43 fh, err := os.Open(cfgPath)
48 prefs := ClientPrefs{}
49 decoder := yaml.NewDecoder(fh)
50 decoder.SetStrict(true)
51 if err := decoder.Decode(&prefs); err != nil {
70 Logger *zap.SugaredLogger
71 activeTasks map[uint32]*Transaction
75 Handlers map[uint16]clientTHandler
79 outbox chan *Transaction
80 Inbox chan *Transaction
84 chatBox *tview.TextView
85 chatInput *tview.InputField
86 App *tview.Application
88 userList *tview.TextView
89 agreeModal *tview.Modal
90 trackerList *tview.List
91 settingsPage *tview.Box
95 func NewUI(c *Client) *UI {
96 app := tview.NewApplication()
97 chatBox := tview.NewTextView().
100 SetDynamicColors(true).
102 SetChangedFunc(func() {
103 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
105 chatBox.Box.SetBorder(true).SetTitle("Chat")
107 chatInput := tview.NewInputField()
110 SetFieldBackgroundColor(tcell.ColorDimGray).
111 //SetFieldTextColor(tcell.ColorWhite).
112 SetDoneFunc(func(key tcell.Key) {
113 // skip send if user hit enter with no other text
114 if len(chatInput.GetText()) == 0 {
119 *NewTransaction(tranChatSend, nil,
120 NewField(fieldData, []byte(chatInput.GetText())),
123 chatInput.SetText("") // clear the input field after chat send
126 chatInput.Box.SetBorder(true).SetTitle("Send")
128 userList := tview.NewTextView().SetDynamicColors(true)
129 userList.SetChangedFunc(func() {
130 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
132 userList.Box.SetBorder(true).SetTitle("Users")
137 Pages: tview.NewPages(),
138 chatInput: chatInput,
140 trackerList: tview.NewList(),
141 agreeModal: tview.NewModal(),
146 const defaultUsername = "unnamed"
149 trackerListPage = "trackerList"
152 func (ui *UI) showBookmarks() *tview.List {
153 list := tview.NewList()
154 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
155 if event.Key() == tcell.KeyEsc {
156 ui.Pages.SwitchToPage("home")
160 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
162 shortcut := 97 // rune for "a"
163 for i, srv := range ui.HLClient.pref.Bookmarks {
167 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
168 ui.Pages.RemovePage("joinServer")
170 newJS := ui.renderJoinServerForm(addr, login, pass, "bookmarks", true, true)
172 ui.Pages.AddPage("joinServer", newJS, true, true)
179 func (ui *UI) getTrackerList() *tview.List {
180 listing, err := GetListing("hltracker.com:5498")
185 list := tview.NewList()
186 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
187 if event.Key() == tcell.KeyEsc {
188 ui.Pages.SwitchToPage("home")
192 list.Box.SetBorder(true).SetTitle("| Servers |")
194 shortcut := 97 // rune for "a"
195 for i, srv := range listing {
197 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
198 ui.Pages.RemovePage("joinServer")
200 newJS := ui.renderJoinServerForm(addr, GuestAccount, "", trackerListPage, false, true)
202 ui.Pages.AddPage("joinServer", newJS, true, true)
203 ui.Pages.ShowPage("joinServer")
210 func (ui *UI) renderSettingsForm() *tview.Flex {
211 settingsForm := tview.NewForm()
212 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 20, nil, nil)
213 settingsForm.AddButton("Save", func() {
214 ui.HLClient.pref.Username = settingsForm.GetFormItem(0).(*tview.InputField).GetText()
215 out, err := yaml.Marshal(&ui.HLClient.pref)
220 _ = ioutil.WriteFile(clientConfigPath, out, 0666)
221 ui.Pages.RemovePage("settings")
223 settingsForm.SetBorder(true)
224 settingsForm.SetCancelFunc(func() {
225 ui.Pages.RemovePage("settings")
227 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
228 settingsPage.Box.SetBorder(true).SetTitle("Settings")
229 settingsPage.AddItem(settingsForm, 0, 1, true)
231 centerFlex := tview.NewFlex().
232 AddItem(nil, 0, 1, false).
233 AddItem(tview.NewFlex().
234 SetDirection(tview.FlexRow).
235 AddItem(nil, 0, 1, false).
236 AddItem(settingsForm, 15, 1, true).
237 AddItem(nil, 0, 1, false), 40, 1, true).
238 AddItem(nil, 0, 1, false)
249 // DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
250 type DebugBuffer struct {
251 TextView *tview.TextView
254 func (db *DebugBuffer) Write(p []byte) (int, error) {
255 return db.TextView.Write(p)
258 // Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
259 func (db *DebugBuffer) Sync() error {
263 func (ui *UI) joinServer(addr, login, password string) error {
264 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
265 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
269 err := ui.HLClient.ReadLoop()
271 ui.HLClient.Logger.Errorw("read error", "err", err)
277 func (ui *UI) renderJoinServerForm(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
279 joinServerForm := tview.NewForm()
281 AddInputField("Server", server, 20, nil, func(text string) {
284 AddInputField("Login", login, 20, nil, func(text string) {
286 ui.HLClient.Login = &l
288 AddPasswordField("Password", password, 20, '*', nil).
289 AddCheckbox("Save", save, func(checked bool) {
292 AddButton("Cancel", func() {
293 ui.Pages.SwitchToPage(backPage)
295 AddButton("Connect", func() {
296 err := ui.joinServer(
297 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
298 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
299 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
302 ui.HLClient.Logger.Errorw("login error", "err", err)
303 loginErrModal := tview.NewModal().
304 AddButtons([]string{"Oh no"}).
305 SetText(err.Error()).
306 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
307 ui.Pages.SwitchToPage(backPage)
310 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
314 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
315 // TODO: implement bookmark saving
319 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
320 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
321 if event.Key() == tcell.KeyEscape {
322 ui.Pages.SwitchToPage(backPage)
328 joinServerForm.SetFocus(5)
331 joinServerPage := tview.NewFlex().
332 AddItem(nil, 0, 1, false).
333 AddItem(tview.NewFlex().
334 SetDirection(tview.FlexRow).
335 AddItem(nil, 0, 1, false).
336 AddItem(joinServerForm, 14, 1, true).
337 AddItem(nil, 0, 1, false), 40, 1, true).
338 AddItem(nil, 0, 1, false)
340 return joinServerPage
343 func randomBanner() string {
344 rand.Seed(time.Now().UnixNano())
346 bannerFiles, _ := bannerDir.ReadDir("client/banners")
347 file, _ := os.ReadFile("banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
349 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
352 func (ui *UI) renderServerUI() *tview.Flex {
353 commandList := tview.NewTextView().SetDynamicColors(true)
355 SetText("[yellow]^n[-::]: Read News\n[yellow]^l[-::]: View Logs\n").
357 SetTitle("Keyboard Shortcuts")
359 modal := tview.NewModal().
360 SetText("Disconnect from the server?").
361 AddButtons([]string{"Cancel", "Exit"}).
363 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
364 if buttonIndex == 1 {
365 _ = ui.HLClient.Disconnect()
366 ui.Pages.SwitchToPage("home")
368 ui.Pages.HidePage("modal")
372 serverUI := tview.NewFlex().
373 AddItem(tview.NewFlex().
374 SetDirection(tview.FlexRow).
375 AddItem(commandList, 4, 0, false).
376 AddItem(ui.chatBox, 0, 8, false).
377 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
378 AddItem(ui.userList, 25, 1, false)
379 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
380 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
381 if event.Key() == tcell.KeyEscape {
382 ui.Pages.AddPage("modal", modal, false, true)
386 if event.Key() == tcell.KeyCtrlN {
387 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
388 ui.HLClient.Logger.Errorw("err", "err", err)
397 func (ui *UI) Start() {
398 home := tview.NewFlex().SetDirection(tview.FlexRow)
399 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
400 mainMenu := tview.NewList()
402 bannerItem := tview.NewTextView().
403 SetText(randomBanner()).
404 SetDynamicColors(true).
405 SetTextAlign(tview.AlignCenter)
408 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
410 home.AddItem(tview.NewFlex().
411 AddItem(nil, 0, 1, false).
412 AddItem(mainMenu, 0, 1, true).
413 AddItem(nil, 0, 1, false),
417 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
419 mainMenu.AddItem("Join Server", "", 'j', func() {
420 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
422 AddItem("Bookmarks", "", 'b', func() {
423 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
425 AddItem("Browse Tracker", "", 't', func() {
426 ui.trackerList = ui.getTrackerList()
427 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
429 AddItem("Settings", "", 's', func() {
430 //ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, false)
432 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
434 AddItem("Quit", "", 'q', func() {
438 ui.Pages.AddPage("home", home, true, true)
440 // App level input capture
441 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
442 if event.Key() == tcell.KeyCtrlC {
443 ui.HLClient.Logger.Infow("Exiting")
448 if event.Key() == tcell.KeyCtrlL {
449 //curPage, _ := ui.Pages.GetFrontPage()
450 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
451 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
452 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
453 if key == tcell.KeyEscape {
454 //ui.Pages.SwitchToPage("serverUI")
455 ui.Pages.RemovePage("logs")
459 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
464 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
469 func NewClient(username string, logger *zap.SugaredLogger) *Client {
471 Icon: &[]byte{0x07, 0xd7},
473 activeTasks: make(map[uint32]*Transaction),
474 Handlers: clientHandlers,
478 prefs, err := readConfig(clientConfigPath)
487 type clientTransaction struct {
489 Handler func(*Client, *Transaction) ([]Transaction, error)
492 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
493 return ch.Handler(cc, t)
496 type clientTHandler interface {
497 Handle(*Client, *Transaction) ([]Transaction, error)
500 type mockClientHandler struct {
504 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
505 args := mh.Called(cc, t)
506 return args.Get(0).([]Transaction), args.Error(1)
509 var clientHandlers = map[uint16]clientTHandler{
511 tranChatMsg: clientTransaction{
513 Handler: handleClientChatMsg,
515 tranLogin: clientTransaction{
517 Handler: handleClientTranLogin,
519 tranShowAgreement: clientTransaction{
520 Name: "tranShowAgreement",
521 Handler: handleClientTranShowAgreement,
523 tranUserAccess: clientTransaction{
524 Name: "tranUserAccess",
525 Handler: handleClientTranUserAccess,
527 tranGetUserNameList: clientTransaction{
528 Name: "tranGetUserNameList",
529 Handler: handleClientGetUserNameList,
531 tranNotifyChangeUser: clientTransaction{
532 Name: "tranNotifyChangeUser",
533 Handler: handleNotifyChangeUser,
535 tranNotifyDeleteUser: clientTransaction{
536 Name: "tranNotifyDeleteUser",
537 Handler: handleNotifyDeleteUser,
539 tranGetMsgs: clientTransaction{
540 Name: "tranNotifyDeleteUser",
541 Handler: handleGetMsgs,
545 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
546 newsText := string(t.GetField(fieldData).Data)
547 newsText = strings.ReplaceAll(newsText, "\r", "\n")
549 newsTextView := tview.NewTextView().
551 SetDoneFunc(func(key tcell.Key) {
552 c.UI.Pages.SwitchToPage("serverUI")
553 c.UI.App.SetFocus(c.UI.chatInput)
555 newsTextView.SetBorder(true).SetTitle("News")
557 c.UI.Pages.AddPage("news", newsTextView, true, true)
558 c.UI.Pages.SwitchToPage("news")
559 c.UI.App.SetFocus(newsTextView)
566 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
568 ID: t.GetField(fieldUserID).Data,
569 Name: string(t.GetField(fieldUserName).Data),
570 Icon: t.GetField(fieldUserIconID).Data,
571 Flags: t.GetField(fieldUserFlags).Data,
575 // user is new to the server
576 // user is already on the server but has a new name
579 var newUserList []User
581 for _, u := range c.UserList {
582 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
583 if bytes.Equal(newUser.ID, u.ID) {
585 u.Name = newUser.Name
586 if u.Name != newUser.Name {
587 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
591 newUserList = append(newUserList, u)
595 newUserList = append(newUserList, newUser)
598 c.UserList = newUserList
605 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
606 exitUser := t.GetField(fieldUserID).Data
608 var newUserList []User
609 for _, u := range c.UserList {
610 if !bytes.Equal(exitUser, u.ID) {
611 newUserList = append(newUserList, u)
615 c.UserList = newUserList
622 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
624 func (c *Client) ReadLoop() error {
625 tranBuff := make([]byte, 0)
627 // Infinite loop where take action on incoming client requests until the connection is closed
629 buf := make([]byte, readBuffSize)
630 tranBuff = tranBuff[tReadlen:]
632 readLen, err := c.Connection.Read(buf)
636 tranBuff = append(tranBuff, buf[:readLen]...)
638 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
639 // into a slice of transactions
640 var transactions []Transaction
641 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
642 c.Logger.Errorw("Error handling transaction", "err", err)
645 // iterate over all of the transactions that were parsed from the byte slice and handle them
646 for _, t := range transactions {
647 if err := c.HandleTransaction(&t); err != nil {
648 c.Logger.Errorw("Error handling transaction", "err", err)
654 func (c *Client) GetTransactions() error {
655 tranBuff := make([]byte, 0)
658 buf := make([]byte, readBuffSize)
659 tranBuff = tranBuff[tReadlen:]
661 readLen, err := c.Connection.Read(buf)
665 tranBuff = append(tranBuff, buf[:readLen]...)
670 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
672 for _, field := range t.Fields {
673 u, _ := ReadUser(field.Data)
674 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
675 //if flagBitmap.Bit(userFlagAdmin) == 1 {
676 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
678 // fmt.Fprintf(UserList, "%s\n", u.Name)
681 users = append(users, *u)
690 func (c *Client) renderUserList() {
691 c.UI.userList.Clear()
692 for _, u := range c.UserList {
693 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
694 if flagBitmap.Bit(userFlagAdmin) == 1 {
695 fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
697 fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
702 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
703 fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
708 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
709 c.UserAccess = t.GetField(fieldUserAccess).Data
714 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
715 agreement := string(t.GetField(fieldData).Data)
716 agreement = strings.ReplaceAll(agreement, "\r", "\n")
718 c.UI.agreeModal = tview.NewModal().
720 AddButtons([]string{"Agree", "Disagree"}).
721 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
722 if buttonIndex == 0 {
726 NewField(fieldUserName, []byte(c.pref.Username)),
727 NewField(fieldUserIconID, *c.Icon),
728 NewField(fieldUserFlags, []byte{0x00, 0x00}),
729 NewField(fieldOptions, []byte{0x00, 0x00}),
733 c.UI.Pages.HidePage("agreement")
734 c.UI.App.SetFocus(c.UI.chatInput)
737 c.UI.Pages.SwitchToPage("home")
742 c.Logger.Debug("show agreement page")
743 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
745 c.UI.Pages.ShowPage("agreement ")
751 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
752 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
753 errMsg := string(t.GetField(fieldError).Data)
754 errModal := tview.NewModal()
755 errModal.SetText(errMsg)
756 errModal.AddButtons([]string{"Oh no"})
757 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
758 c.UI.Pages.RemovePage("errModal")
760 c.UI.Pages.RemovePage("joinServer")
761 c.UI.Pages.AddPage("errModal", errModal, false, true)
763 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
765 c.Logger.Error(string(t.GetField(fieldError).Data))
766 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
768 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
769 c.UI.App.SetFocus(c.UI.chatInput)
771 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
772 c.Logger.Errorw("err", "err", err)
777 // JoinServer connects to a Hotline server and completes the login flow
778 func (c *Client) JoinServer(address, login, passwd string) error {
779 // Establish TCP connection to server
780 if err := c.connect(address); err != nil {
784 // Send handshake sequence
785 if err := c.Handshake(); err != nil {
789 // Authenticate (send tranLogin 107)
790 if err := c.LogIn(login, passwd); err != nil {
797 // connect establishes a connection with a Server by sending handshake sequence
798 func (c *Client) connect(address string) error {
800 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
807 var ClientHandshake = []byte{
808 0x54, 0x52, 0x54, 0x50, // TRTP
809 0x48, 0x4f, 0x54, 0x4c, // HOTL
814 var ServerHandshake = []byte{
815 0x54, 0x52, 0x54, 0x50, // TRTP
816 0x00, 0x00, 0x00, 0x00, // ErrorCode
819 func (c *Client) Handshake() error {
820 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
821 //Sub-protocol ID 4 User defined
822 //Version 2 1 Currently 1
823 //Sub-version 2 User defined
824 if _, err := c.Connection.Write(ClientHandshake); err != nil {
825 return fmt.Errorf("handshake write err: %s", err)
828 replyBuf := make([]byte, 8)
829 _, err := c.Connection.Read(replyBuf)
834 //spew.Dump(replyBuf)
835 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
838 // In the case of an error, client and server close the connection.
840 return fmt.Errorf("handshake response err: %s", err)
843 func (c *Client) LogIn(login string, password string) error {
847 NewField(fieldUserName, []byte(c.pref.Username)),
848 NewField(fieldUserIconID, []byte{0x07, 0xd1}),
849 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
850 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
851 NewField(fieldVersion, []byte{0, 2}),
856 //// Agree agrees to the server agreement and sends user info, completing the login sequence
857 //func (c *Client) Agree() {
862 // NewField(fieldUserName, []byte("test")),
863 // NewField(fieldUserIconID, *c.Icon),
864 // NewField(fieldUserFlags, []byte{0x00, 0x00}),
869 // //// Block until we receive the agreement reply from the server
870 // //_ = c.WaitForTransaction(tranAgreed)
873 //func (c *Client) WaitForTransaction(id uint16) Transaction {
874 // var trans Transaction
876 // buf := make([]byte, 1400)
877 // readLen, err := c.Connection.Read(buf)
882 // transactions := ReadTransactions(buf[:readLen])
883 // tran, err := FindTransactions(id, transactions)
885 // fmt.Println("returning")
893 //func (c *Client) Read() error {
894 // // Main loop where we wait for and take action on client requests
896 // buf := make([]byte, 1400)
897 // readLen, err := c.Connection.Read(buf)
901 // transactions, _, _ := readTransactions(buf[:readLen])
903 // for _, t := range transactions {
904 // c.HandleTransaction(&t)
911 func (c *Client) Send(t Transaction) error {
912 requestNum := binary.BigEndian.Uint16(t.Type)
913 tID := binary.BigEndian.Uint32(t.ID)
915 //handler := TransactionHandlers[requestNum]
917 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
919 c.activeTasks[tID] = &t
924 if n, err = c.Connection.Write(t.Payload()); err != nil {
927 c.Logger.Debugw("Sent Transaction",
928 "IsReply", t.IsReply,
935 func (c *Client) HandleTransaction(t *Transaction) error {
936 var origT Transaction
938 requestID := binary.BigEndian.Uint32(t.ID)
939 origT = *c.activeTasks[requestID]
943 requestNum := binary.BigEndian.Uint16(t.Type)
945 "Received Transaction",
946 "RequestType", requestNum,
949 if handler, ok := c.Handlers[requestNum]; ok {
950 outT, _ := handler.Handle(c, t)
951 for _, t := range outT {
956 "Unimplemented transaction type received",
957 "RequestID", requestNum,
958 "TransactionID", t.ID,
965 func (c *Client) Connected() bool {
966 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
967 // c.Agreed == true &&
968 if c.UserAccess != nil {
974 func (c *Client) Disconnect() error {
975 err := c.Connection.Close()