9 "github.com/davecgh/go-spew/spew"
10 "github.com/gdamore/tcell/v2"
11 "github.com/rivo/tview"
12 "github.com/stretchr/testify/mock"
25 const clientConfigPath = "/usr/local/etc/mobius-client-config.yaml"
27 trackerListPage = "trackerList"
30 //go:embed client/banners/*.txt
31 var bannerDir embed.FS
33 type Bookmark struct {
34 Name string `yaml:"Name"`
35 Addr string `yaml:"Addr"`
36 Login string `yaml:"Login"`
37 Password string `yaml:"Password"`
40 type ClientPrefs struct {
41 Username string `yaml:"Username"`
42 IconID int `yaml:"IconID"`
43 Bookmarks []Bookmark `yaml:"Bookmarks"`
44 Tracker string `yaml:"Tracker"`
47 func (cp *ClientPrefs) IconBytes() []byte {
48 iconBytes := make([]byte, 2)
49 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
53 func readConfig(cfgPath string) (*ClientPrefs, error) {
54 fh, err := os.Open(cfgPath)
59 prefs := ClientPrefs{}
60 decoder := yaml.NewDecoder(fh)
61 decoder.SetStrict(true)
62 if err := decoder.Decode(&prefs); err != nil {
79 Logger *zap.SugaredLogger
80 activeTasks map[uint32]*Transaction
84 Handlers map[uint16]clientTHandler
88 outbox chan *Transaction
89 Inbox chan *Transaction
93 chatBox *tview.TextView
94 chatInput *tview.InputField
95 App *tview.Application
97 userList *tview.TextView
98 agreeModal *tview.Modal
99 trackerList *tview.List
100 settingsPage *tview.Box
104 func NewUI(c *Client) *UI {
105 app := tview.NewApplication()
106 chatBox := tview.NewTextView().
108 SetDynamicColors(true).
110 SetChangedFunc(func() {
111 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
113 chatBox.Box.SetBorder(true).SetTitle("Chat")
115 chatInput := tview.NewInputField()
118 SetFieldBackgroundColor(tcell.ColorDimGray).
119 SetDoneFunc(func(key tcell.Key) {
120 // skip send if user hit enter with no other text
121 if len(chatInput.GetText()) == 0 {
126 *NewTransaction(tranChatSend, nil,
127 NewField(fieldData, []byte(chatInput.GetText())),
130 chatInput.SetText("") // clear the input field after chat send
133 chatInput.Box.SetBorder(true).SetTitle("Send")
137 SetDynamicColors(true).
138 SetChangedFunc(func() {
139 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
141 userList.Box.SetBorder(true).SetTitle("Users")
146 Pages: tview.NewPages(),
147 chatInput: chatInput,
149 trackerList: tview.NewList(),
150 agreeModal: tview.NewModal(),
155 func (ui *UI) showBookmarks() *tview.List {
156 list := tview.NewList()
157 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
158 if event.Key() == tcell.KeyEsc {
159 ui.Pages.SwitchToPage("home")
163 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
165 shortcut := 97 // rune for "a"
166 for i, srv := range ui.HLClient.pref.Bookmarks {
170 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
171 ui.Pages.RemovePage("joinServer")
173 newJS := ui.renderJoinServerForm(addr, login, pass, "bookmarks", true, true)
175 ui.Pages.AddPage("joinServer", newJS, true, true)
182 func (ui *UI) getTrackerList() *tview.List {
183 listing, err := GetListing(ui.HLClient.pref.Tracker)
188 list := tview.NewList()
189 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
190 if event.Key() == tcell.KeyEsc {
191 ui.Pages.SwitchToPage("home")
195 list.Box.SetBorder(true).SetTitle("| Servers |")
197 shortcut := 97 // rune for "a"
198 for i, srv := range listing {
200 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
201 ui.Pages.RemovePage("joinServer")
203 newJS := ui.renderJoinServerForm(addr, GuestAccount, "", trackerListPage, false, true)
205 ui.Pages.AddPage("joinServer", newJS, true, true)
206 ui.Pages.ShowPage("joinServer")
213 func (ui *UI) renderSettingsForm() *tview.Flex {
214 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
215 settingsForm := tview.NewForm()
216 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
217 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
218 _, err := strconv.Atoi(idStr)
221 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
222 settingsForm.AddButton("Save", func() {
223 ui.HLClient.pref.Username = settingsForm.GetFormItem(0).(*tview.InputField).GetText()
224 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
225 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
226 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
228 out, err := yaml.Marshal(&ui.HLClient.pref)
233 _ = ioutil.WriteFile(clientConfigPath, out, 0666)
234 ui.Pages.RemovePage("settings")
236 settingsForm.SetBorder(true)
237 settingsForm.SetCancelFunc(func() {
238 ui.Pages.RemovePage("settings")
240 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
241 settingsPage.Box.SetBorder(true).SetTitle("Settings")
242 settingsPage.AddItem(settingsForm, 0, 1, true)
244 centerFlex := tview.NewFlex().
245 AddItem(nil, 0, 1, false).
246 AddItem(tview.NewFlex().
247 SetDirection(tview.FlexRow).
248 AddItem(nil, 0, 1, false).
249 AddItem(settingsForm, 15, 1, true).
250 AddItem(nil, 0, 1, false), 40, 1, true).
251 AddItem(nil, 0, 1, false)
256 // DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
257 type DebugBuffer struct {
258 TextView *tview.TextView
261 func (db *DebugBuffer) Write(p []byte) (int, error) {
262 return db.TextView.Write(p)
265 // Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
266 func (db *DebugBuffer) Sync() error {
270 func (ui *UI) joinServer(addr, login, password string) error {
271 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
272 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
276 err := ui.HLClient.ReadLoop()
278 ui.HLClient.Logger.Errorw("read error", "err", err)
284 func (ui *UI) renderJoinServerForm(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
285 joinServerForm := tview.NewForm()
287 AddInputField("Server", server, 0, nil, nil).
288 AddInputField("Login", login, 0, nil, nil).
289 AddPasswordField("Password", password, 0, '*', nil).
290 AddCheckbox("Save", save, func(checked bool) {
291 // TODO: Implement bookmark saving
293 AddButton("Cancel", func() {
294 ui.Pages.SwitchToPage(backPage)
296 AddButton("Connect", func() {
297 err := ui.joinServer(
298 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
299 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
300 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
303 ui.HLClient.Logger.Errorw("login error", "err", err)
304 loginErrModal := tview.NewModal().
305 AddButtons([]string{"Oh no"}).
306 SetText(err.Error()).
307 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
308 ui.Pages.SwitchToPage(backPage)
311 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
315 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
316 // TODO: implement bookmark saving
320 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
321 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
322 if event.Key() == tcell.KeyEscape {
323 ui.Pages.SwitchToPage(backPage)
329 joinServerForm.SetFocus(5)
332 joinServerPage := tview.NewFlex().
333 AddItem(nil, 0, 1, false).
334 AddItem(tview.NewFlex().
335 SetDirection(tview.FlexRow).
336 AddItem(nil, 0, 1, false).
337 AddItem(joinServerForm, 14, 1, true).
338 AddItem(nil, 0, 1, false), 40, 1, true).
339 AddItem(nil, 0, 1, false)
341 return joinServerPage
344 func randomBanner() string {
345 rand.Seed(time.Now().UnixNano())
347 bannerFiles, _ := bannerDir.ReadDir("client/banners")
348 file, _ := bannerDir.ReadFile("client/banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
350 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
353 func (ui *UI) renderServerUI() *tview.Flex {
354 commandList := tview.NewTextView().SetDynamicColors(true)
356 SetText("[yellow]^n[-::]: Read News\n[yellow]^l[-::]: View Logs\n").
358 SetTitle("Keyboard Shortcuts")
360 modal := tview.NewModal().
361 SetText("Disconnect from the server?").
362 AddButtons([]string{"Cancel", "Exit"}).
364 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
365 if buttonIndex == 1 {
366 _ = ui.HLClient.Disconnect()
367 ui.Pages.SwitchToPage("home")
369 ui.Pages.HidePage("modal")
373 serverUI := tview.NewFlex().
374 AddItem(tview.NewFlex().
375 SetDirection(tview.FlexRow).
376 AddItem(commandList, 4, 0, false).
377 AddItem(ui.chatBox, 0, 8, false).
378 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
379 AddItem(ui.userList, 25, 1, false)
380 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
381 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
382 if event.Key() == tcell.KeyEscape {
383 ui.Pages.AddPage("modal", modal, false, true)
387 if event.Key() == tcell.KeyCtrlN {
388 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
389 ui.HLClient.Logger.Errorw("err", "err", err)
398 func (ui *UI) Start() {
399 home := tview.NewFlex().SetDirection(tview.FlexRow)
400 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
401 mainMenu := tview.NewList()
403 bannerItem := tview.NewTextView().
404 SetText(randomBanner()).
405 SetDynamicColors(true).
406 SetTextAlign(tview.AlignCenter)
409 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
411 home.AddItem(tview.NewFlex().
412 AddItem(nil, 0, 1, false).
413 AddItem(mainMenu, 0, 1, true).
414 AddItem(nil, 0, 1, false),
418 mainMenu.AddItem("Join Server", "", 'j', func() {
419 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
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, true)
432 AddItem("Quit", "", 'q', func() {
436 ui.Pages.AddPage("home", home, true, true)
438 // App level input capture
439 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
440 if event.Key() == tcell.KeyCtrlC {
441 ui.HLClient.Logger.Infow("Exiting")
446 if event.Key() == tcell.KeyCtrlL {
447 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
448 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
449 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
450 if key == tcell.KeyEscape {
451 ui.Pages.RemovePage("logs")
455 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
460 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
466 func NewClient(username string, logger *zap.SugaredLogger) *Client {
469 activeTasks: make(map[uint32]*Transaction),
470 Handlers: clientHandlers,
474 prefs, err := readConfig(clientConfigPath)
483 type clientTransaction struct {
485 Handler func(*Client, *Transaction) ([]Transaction, error)
488 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
489 return ch.Handler(cc, t)
492 type clientTHandler interface {
493 Handle(*Client, *Transaction) ([]Transaction, error)
496 type mockClientHandler struct {
500 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
501 args := mh.Called(cc, t)
502 return args.Get(0).([]Transaction), args.Error(1)
505 var clientHandlers = map[uint16]clientTHandler{
507 tranChatMsg: clientTransaction{
509 Handler: handleClientChatMsg,
511 tranLogin: clientTransaction{
513 Handler: handleClientTranLogin,
515 tranShowAgreement: clientTransaction{
516 Name: "tranShowAgreement",
517 Handler: handleClientTranShowAgreement,
519 tranUserAccess: clientTransaction{
520 Name: "tranUserAccess",
521 Handler: handleClientTranUserAccess,
523 tranGetUserNameList: clientTransaction{
524 Name: "tranGetUserNameList",
525 Handler: handleClientGetUserNameList,
527 tranNotifyChangeUser: clientTransaction{
528 Name: "tranNotifyChangeUser",
529 Handler: handleNotifyChangeUser,
531 tranNotifyDeleteUser: clientTransaction{
532 Name: "tranNotifyDeleteUser",
533 Handler: handleNotifyDeleteUser,
535 tranGetMsgs: clientTransaction{
536 Name: "tranNotifyDeleteUser",
537 Handler: handleGetMsgs,
541 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
542 newsText := string(t.GetField(fieldData).Data)
543 newsText = strings.ReplaceAll(newsText, "\r", "\n")
545 newsTextView := tview.NewTextView().
547 SetDoneFunc(func(key tcell.Key) {
548 c.UI.Pages.SwitchToPage("serverUI")
549 c.UI.App.SetFocus(c.UI.chatInput)
551 newsTextView.SetBorder(true).SetTitle("News")
553 c.UI.Pages.AddPage("news", newsTextView, true, true)
554 c.UI.Pages.SwitchToPage("news")
555 c.UI.App.SetFocus(newsTextView)
562 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
564 ID: t.GetField(fieldUserID).Data,
565 Name: string(t.GetField(fieldUserName).Data),
566 Icon: t.GetField(fieldUserIconID).Data,
567 Flags: t.GetField(fieldUserFlags).Data,
571 // user is new to the server
572 // user is already on the server but has a new name
575 var newUserList []User
577 for _, u := range c.UserList {
578 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
579 if bytes.Equal(newUser.ID, u.ID) {
581 u.Name = newUser.Name
582 if u.Name != newUser.Name {
583 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
587 newUserList = append(newUserList, u)
591 newUserList = append(newUserList, newUser)
594 c.UserList = newUserList
601 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
602 exitUser := t.GetField(fieldUserID).Data
604 var newUserList []User
605 for _, u := range c.UserList {
606 if !bytes.Equal(exitUser, u.ID) {
607 newUserList = append(newUserList, u)
611 c.UserList = newUserList
618 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
620 func (c *Client) ReadLoop() error {
621 tranBuff := make([]byte, 0)
623 // Infinite loop where take action on incoming client requests until the connection is closed
625 buf := make([]byte, readBuffSize)
626 tranBuff = tranBuff[tReadlen:]
628 readLen, err := c.Connection.Read(buf)
632 tranBuff = append(tranBuff, buf[:readLen]...)
634 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
635 // into a slice of transactions
636 var transactions []Transaction
637 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
638 c.Logger.Errorw("Error handling transaction", "err", err)
641 // iterate over all of the transactions that were parsed from the byte slice and handle them
642 for _, t := range transactions {
643 if err := c.HandleTransaction(&t); err != nil {
644 c.Logger.Errorw("Error handling transaction", "err", err)
650 func (c *Client) GetTransactions() error {
651 tranBuff := make([]byte, 0)
654 buf := make([]byte, readBuffSize)
655 tranBuff = tranBuff[tReadlen:]
657 readLen, err := c.Connection.Read(buf)
661 tranBuff = append(tranBuff, buf[:readLen]...)
666 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
668 for _, field := range t.Fields {
669 u, _ := ReadUser(field.Data)
670 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
671 //if flagBitmap.Bit(userFlagAdmin) == 1 {
672 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
674 // fmt.Fprintf(UserList, "%s\n", u.Name)
677 users = append(users, *u)
686 func (c *Client) renderUserList() {
687 c.UI.userList.Clear()
688 for _, u := range c.UserList {
689 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
690 if flagBitmap.Bit(userFlagAdmin) == 1 {
691 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
693 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
698 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
699 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
704 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
705 c.UserAccess = t.GetField(fieldUserAccess).Data
710 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
711 agreement := string(t.GetField(fieldData).Data)
712 agreement = strings.ReplaceAll(agreement, "\r", "\n")
714 c.UI.agreeModal = tview.NewModal().
716 AddButtons([]string{"Agree", "Disagree"}).
717 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
718 if buttonIndex == 0 {
722 NewField(fieldUserName, []byte(c.pref.Username)),
723 NewField(fieldUserIconID, c.pref.IconBytes()),
724 NewField(fieldUserFlags, []byte{0x00, 0x00}),
725 NewField(fieldOptions, []byte{0x00, 0x00}),
729 c.UI.Pages.HidePage("agreement")
730 c.UI.App.SetFocus(c.UI.chatInput)
733 c.UI.Pages.SwitchToPage("home")
738 c.Logger.Debug("show agreement page")
739 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
741 c.UI.Pages.ShowPage("agreement ")
747 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
748 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
749 errMsg := string(t.GetField(fieldError).Data)
750 errModal := tview.NewModal()
751 errModal.SetText(errMsg)
752 errModal.AddButtons([]string{"Oh no"})
753 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
754 c.UI.Pages.RemovePage("errModal")
756 c.UI.Pages.RemovePage("joinServer")
757 c.UI.Pages.AddPage("errModal", errModal, false, true)
759 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
761 c.Logger.Error(string(t.GetField(fieldError).Data))
762 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
764 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
765 c.UI.App.SetFocus(c.UI.chatInput)
767 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
768 c.Logger.Errorw("err", "err", err)
773 // JoinServer connects to a Hotline server and completes the login flow
774 func (c *Client) JoinServer(address, login, passwd string) error {
775 // Establish TCP connection to server
776 if err := c.connect(address); err != nil {
780 // Send handshake sequence
781 if err := c.Handshake(); err != nil {
785 // Authenticate (send tranLogin 107)
786 if err := c.LogIn(login, passwd); err != nil {
793 // connect establishes a connection with a Server by sending handshake sequence
794 func (c *Client) connect(address string) error {
796 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
803 var ClientHandshake = []byte{
804 0x54, 0x52, 0x54, 0x50, // TRTP
805 0x48, 0x4f, 0x54, 0x4c, // HOTL
810 var ServerHandshake = []byte{
811 0x54, 0x52, 0x54, 0x50, // TRTP
812 0x00, 0x00, 0x00, 0x00, // ErrorCode
815 func (c *Client) Handshake() error {
816 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
817 //Sub-protocol ID 4 User defined
818 //Version 2 1 Currently 1
819 //Sub-version 2 User defined
820 if _, err := c.Connection.Write(ClientHandshake); err != nil {
821 return fmt.Errorf("handshake write err: %s", err)
824 replyBuf := make([]byte, 8)
825 _, err := c.Connection.Read(replyBuf)
830 //spew.Dump(replyBuf)
831 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
834 // In the case of an error, client and server close the connection.
836 return fmt.Errorf("handshake response err: %s", err)
839 func (c *Client) LogIn(login string, password string) error {
843 NewField(fieldUserName, []byte(c.pref.Username)),
844 NewField(fieldUserIconID, c.pref.IconBytes()),
845 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
846 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
847 NewField(fieldVersion, []byte{0, 2}),
852 func (c *Client) Send(t Transaction) error {
853 requestNum := binary.BigEndian.Uint16(t.Type)
854 tID := binary.BigEndian.Uint32(t.ID)
856 //handler := TransactionHandlers[requestNum]
858 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
860 c.activeTasks[tID] = &t
865 if n, err = c.Connection.Write(t.Payload()); err != nil {
868 c.Logger.Debugw("Sent Transaction",
869 "IsReply", t.IsReply,
876 func (c *Client) HandleTransaction(t *Transaction) error {
877 var origT Transaction
879 requestID := binary.BigEndian.Uint32(t.ID)
880 origT = *c.activeTasks[requestID]
884 requestNum := binary.BigEndian.Uint16(t.Type)
886 "Received Transaction",
887 "RequestType", requestNum,
890 if handler, ok := c.Handlers[requestNum]; ok {
891 outT, _ := handler.Handle(c, t)
892 for _, t := range outT {
897 "Unimplemented transaction type received",
898 "RequestID", requestNum,
899 "TransactionID", t.ID,
906 func (c *Client) Connected() bool {
907 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
908 // c.Agreed == true &&
909 if c.UserAccess != nil {
915 func (c *Client) Disconnect() error {
916 err := c.Connection.Close()