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 {
81 Logger *zap.SugaredLogger
82 activeTasks map[uint32]*Transaction
86 Handlers map[uint16]clientTHandler
90 outbox chan *Transaction
91 Inbox chan *Transaction
95 chatBox *tview.TextView
96 chatInput *tview.InputField
97 App *tview.Application
99 userList *tview.TextView
100 agreeModal *tview.Modal
101 trackerList *tview.List
102 settingsPage *tview.Box
106 func NewUI(c *Client) *UI {
107 app := tview.NewApplication()
108 chatBox := tview.NewTextView().
110 SetDynamicColors(true).
112 SetChangedFunc(func() {
113 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
115 chatBox.Box.SetBorder(true).SetTitle("Chat")
117 chatInput := tview.NewInputField()
120 SetFieldBackgroundColor(tcell.ColorDimGray).
121 SetDoneFunc(func(key tcell.Key) {
122 // skip send if user hit enter with no other text
123 if len(chatInput.GetText()) == 0 {
128 *NewTransaction(tranChatSend, nil,
129 NewField(fieldData, []byte(chatInput.GetText())),
132 chatInput.SetText("") // clear the input field after chat send
135 chatInput.Box.SetBorder(true).SetTitle("Send")
139 SetDynamicColors(true).
140 SetChangedFunc(func() {
141 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
143 userList.Box.SetBorder(true).SetTitle("Users")
148 Pages: tview.NewPages(),
149 chatInput: chatInput,
151 trackerList: tview.NewList(),
152 agreeModal: tview.NewModal(),
157 func (ui *UI) showBookmarks() *tview.List {
158 list := tview.NewList()
159 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
160 if event.Key() == tcell.KeyEsc {
161 ui.Pages.SwitchToPage("home")
165 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
167 shortcut := 97 // rune for "a"
168 for i, srv := range ui.HLClient.pref.Bookmarks {
172 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
173 ui.Pages.RemovePage("joinServer")
175 newJS := ui.renderJoinServerForm(addr, login, pass, "bookmarks", true, true)
177 ui.Pages.AddPage("joinServer", newJS, true, true)
184 func (ui *UI) getTrackerList() *tview.List {
185 listing, err := GetListing(ui.HLClient.pref.Tracker)
190 list := tview.NewList()
191 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
192 if event.Key() == tcell.KeyEsc {
193 ui.Pages.SwitchToPage("home")
197 list.Box.SetBorder(true).SetTitle("| Servers |")
199 shortcut := 97 // rune for "a"
200 for i, srv := range listing {
202 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
203 ui.Pages.RemovePage("joinServer")
205 newJS := ui.renderJoinServerForm(addr, GuestAccount, "", trackerListPage, false, true)
207 ui.Pages.AddPage("joinServer", newJS, true, true)
208 ui.Pages.ShowPage("joinServer")
215 func (ui *UI) renderSettingsForm() *tview.Flex {
216 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
217 settingsForm := tview.NewForm()
218 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
219 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
220 _, err := strconv.Atoi(idStr)
223 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
224 settingsForm.AddButton("Save", func() {
225 ui.HLClient.pref.Username = settingsForm.GetFormItem(0).(*tview.InputField).GetText()
226 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
227 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
228 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
230 out, err := yaml.Marshal(&ui.HLClient.pref)
235 _ = ioutil.WriteFile(clientConfigPath, out, 0666)
236 ui.Pages.RemovePage("settings")
238 settingsForm.SetBorder(true)
239 settingsForm.SetCancelFunc(func() {
240 ui.Pages.RemovePage("settings")
242 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
243 settingsPage.Box.SetBorder(true).SetTitle("Settings")
244 settingsPage.AddItem(settingsForm, 0, 1, true)
246 centerFlex := tview.NewFlex().
247 AddItem(nil, 0, 1, false).
248 AddItem(tview.NewFlex().
249 SetDirection(tview.FlexRow).
250 AddItem(nil, 0, 1, false).
251 AddItem(settingsForm, 15, 1, true).
252 AddItem(nil, 0, 1, false), 40, 1, true).
253 AddItem(nil, 0, 1, false)
258 // DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
259 type DebugBuffer struct {
260 TextView *tview.TextView
263 func (db *DebugBuffer) Write(p []byte) (int, error) {
264 return db.TextView.Write(p)
267 // Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
268 func (db *DebugBuffer) Sync() error {
272 func (ui *UI) joinServer(addr, login, password string) error {
273 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
274 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
278 err := ui.HLClient.ReadLoop()
280 ui.HLClient.Logger.Errorw("read error", "err", err)
286 func (ui *UI) renderJoinServerForm(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
287 joinServerForm := tview.NewForm()
289 AddInputField("Server", server, 0, nil, nil).
290 AddInputField("Login", login, 0, nil, nil).
291 AddPasswordField("Password", password, 0, '*', nil).
292 AddCheckbox("Save", save, func(checked bool) {
293 // TODO: Implement bookmark saving
295 AddButton("Cancel", func() {
296 ui.Pages.SwitchToPage(backPage)
298 AddButton("Connect", func() {
299 err := ui.joinServer(
300 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
301 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
302 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
305 ui.HLClient.Logger.Errorw("login error", "err", err)
306 loginErrModal := tview.NewModal().
307 AddButtons([]string{"Oh no"}).
308 SetText(err.Error()).
309 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
310 ui.Pages.SwitchToPage(backPage)
313 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
317 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
318 // TODO: implement bookmark saving
322 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
323 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
324 if event.Key() == tcell.KeyEscape {
325 ui.Pages.SwitchToPage(backPage)
331 joinServerForm.SetFocus(5)
334 joinServerPage := tview.NewFlex().
335 AddItem(nil, 0, 1, false).
336 AddItem(tview.NewFlex().
337 SetDirection(tview.FlexRow).
338 AddItem(nil, 0, 1, false).
339 AddItem(joinServerForm, 14, 1, true).
340 AddItem(nil, 0, 1, false), 40, 1, true).
341 AddItem(nil, 0, 1, false)
343 return joinServerPage
346 func randomBanner() string {
347 rand.Seed(time.Now().UnixNano())
349 bannerFiles, _ := bannerDir.ReadDir("client/banners")
350 file, _ := bannerDir.ReadFile("client/banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
352 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
355 func (ui *UI) renderServerUI() *tview.Flex {
356 commandList := tview.NewTextView().SetDynamicColors(true)
358 SetText("[yellow]^n[-::]: Read News\n[yellow]^l[-::]: View Logs\n").
360 SetTitle("Keyboard Shortcuts")
362 modal := tview.NewModal().
363 SetText("Disconnect from the server?").
364 AddButtons([]string{"Cancel", "Exit"}).
366 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
367 if buttonIndex == 1 {
368 _ = ui.HLClient.Disconnect()
369 ui.Pages.SwitchToPage("home")
371 ui.Pages.HidePage("modal")
375 serverUI := tview.NewFlex().
376 AddItem(tview.NewFlex().
377 SetDirection(tview.FlexRow).
378 AddItem(commandList, 4, 0, false).
379 AddItem(ui.chatBox, 0, 8, false).
380 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
381 AddItem(ui.userList, 25, 1, false)
382 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
383 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
384 if event.Key() == tcell.KeyEscape {
385 ui.Pages.AddPage("modal", modal, false, true)
389 if event.Key() == tcell.KeyCtrlN {
390 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
391 ui.HLClient.Logger.Errorw("err", "err", err)
400 func (ui *UI) Start() {
401 home := tview.NewFlex().SetDirection(tview.FlexRow)
402 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
403 mainMenu := tview.NewList()
405 bannerItem := tview.NewTextView().
406 SetText(randomBanner()).
407 SetDynamicColors(true).
408 SetTextAlign(tview.AlignCenter)
411 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
413 home.AddItem(tview.NewFlex().
414 AddItem(nil, 0, 1, false).
415 AddItem(mainMenu, 0, 1, true).
416 AddItem(nil, 0, 1, false),
420 mainMenu.AddItem("Join Server", "", 'j', func() {
421 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
422 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
424 AddItem("Bookmarks", "", 'b', func() {
425 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
427 AddItem("Browse Tracker", "", 't', func() {
428 ui.trackerList = ui.getTrackerList()
429 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
431 AddItem("Settings", "", 's', func() {
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 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
450 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
451 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
452 if key == tcell.KeyEscape {
453 ui.Pages.RemovePage("logs")
457 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
462 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
468 func NewClient(username string, logger *zap.SugaredLogger) *Client {
470 Icon: &[]byte{0x07, 0xd7},
472 activeTasks: make(map[uint32]*Transaction),
473 Handlers: clientHandlers,
477 prefs, err := readConfig(clientConfigPath)
486 type clientTransaction struct {
488 Handler func(*Client, *Transaction) ([]Transaction, error)
491 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
492 return ch.Handler(cc, t)
495 type clientTHandler interface {
496 Handle(*Client, *Transaction) ([]Transaction, error)
499 type mockClientHandler struct {
503 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
504 args := mh.Called(cc, t)
505 return args.Get(0).([]Transaction), args.Error(1)
508 var clientHandlers = map[uint16]clientTHandler{
510 tranChatMsg: clientTransaction{
512 Handler: handleClientChatMsg,
514 tranLogin: clientTransaction{
516 Handler: handleClientTranLogin,
518 tranShowAgreement: clientTransaction{
519 Name: "tranShowAgreement",
520 Handler: handleClientTranShowAgreement,
522 tranUserAccess: clientTransaction{
523 Name: "tranUserAccess",
524 Handler: handleClientTranUserAccess,
526 tranGetUserNameList: clientTransaction{
527 Name: "tranGetUserNameList",
528 Handler: handleClientGetUserNameList,
530 tranNotifyChangeUser: clientTransaction{
531 Name: "tranNotifyChangeUser",
532 Handler: handleNotifyChangeUser,
534 tranNotifyDeleteUser: clientTransaction{
535 Name: "tranNotifyDeleteUser",
536 Handler: handleNotifyDeleteUser,
538 tranGetMsgs: clientTransaction{
539 Name: "tranNotifyDeleteUser",
540 Handler: handleGetMsgs,
544 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
545 newsText := string(t.GetField(fieldData).Data)
546 newsText = strings.ReplaceAll(newsText, "\r", "\n")
548 newsTextView := tview.NewTextView().
550 SetDoneFunc(func(key tcell.Key) {
551 c.UI.Pages.SwitchToPage("serverUI")
552 c.UI.App.SetFocus(c.UI.chatInput)
554 newsTextView.SetBorder(true).SetTitle("News")
556 c.UI.Pages.AddPage("news", newsTextView, true, true)
557 c.UI.Pages.SwitchToPage("news")
558 c.UI.App.SetFocus(newsTextView)
565 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
567 ID: t.GetField(fieldUserID).Data,
568 Name: string(t.GetField(fieldUserName).Data),
569 Icon: t.GetField(fieldUserIconID).Data,
570 Flags: t.GetField(fieldUserFlags).Data,
574 // user is new to the server
575 // user is already on the server but has a new name
578 var newUserList []User
580 for _, u := range c.UserList {
581 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
582 if bytes.Equal(newUser.ID, u.ID) {
584 u.Name = newUser.Name
585 if u.Name != newUser.Name {
586 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
590 newUserList = append(newUserList, u)
594 newUserList = append(newUserList, newUser)
597 c.UserList = newUserList
604 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
605 exitUser := t.GetField(fieldUserID).Data
607 var newUserList []User
608 for _, u := range c.UserList {
609 if !bytes.Equal(exitUser, u.ID) {
610 newUserList = append(newUserList, u)
614 c.UserList = newUserList
621 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
623 func (c *Client) ReadLoop() error {
624 tranBuff := make([]byte, 0)
626 // Infinite loop where take action on incoming client requests until the connection is closed
628 buf := make([]byte, readBuffSize)
629 tranBuff = tranBuff[tReadlen:]
631 readLen, err := c.Connection.Read(buf)
635 tranBuff = append(tranBuff, buf[:readLen]...)
637 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
638 // into a slice of transactions
639 var transactions []Transaction
640 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
641 c.Logger.Errorw("Error handling transaction", "err", err)
644 // iterate over all of the transactions that were parsed from the byte slice and handle them
645 for _, t := range transactions {
646 if err := c.HandleTransaction(&t); err != nil {
647 c.Logger.Errorw("Error handling transaction", "err", err)
653 func (c *Client) GetTransactions() error {
654 tranBuff := make([]byte, 0)
657 buf := make([]byte, readBuffSize)
658 tranBuff = tranBuff[tReadlen:]
660 readLen, err := c.Connection.Read(buf)
664 tranBuff = append(tranBuff, buf[:readLen]...)
669 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
671 for _, field := range t.Fields {
672 u, _ := ReadUser(field.Data)
673 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
674 //if flagBitmap.Bit(userFlagAdmin) == 1 {
675 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
677 // fmt.Fprintf(UserList, "%s\n", u.Name)
680 users = append(users, *u)
689 func (c *Client) renderUserList() {
690 c.UI.userList.Clear()
691 for _, u := range c.UserList {
692 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
693 if flagBitmap.Bit(userFlagAdmin) == 1 {
694 fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
696 fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
701 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
702 fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
707 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
708 c.UserAccess = t.GetField(fieldUserAccess).Data
713 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
714 agreement := string(t.GetField(fieldData).Data)
715 agreement = strings.ReplaceAll(agreement, "\r", "\n")
717 c.UI.agreeModal = tview.NewModal().
719 AddButtons([]string{"Agree", "Disagree"}).
720 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
721 if buttonIndex == 0 {
725 NewField(fieldUserName, []byte(c.pref.Username)),
726 NewField(fieldUserIconID, c.pref.IconBytes()),
727 NewField(fieldUserFlags, []byte{0x00, 0x00}),
728 NewField(fieldOptions, []byte{0x00, 0x00}),
732 c.UI.Pages.HidePage("agreement")
733 c.UI.App.SetFocus(c.UI.chatInput)
736 c.UI.Pages.SwitchToPage("home")
741 c.Logger.Debug("show agreement page")
742 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
744 c.UI.Pages.ShowPage("agreement ")
750 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
751 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
752 errMsg := string(t.GetField(fieldError).Data)
753 errModal := tview.NewModal()
754 errModal.SetText(errMsg)
755 errModal.AddButtons([]string{"Oh no"})
756 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
757 c.UI.Pages.RemovePage("errModal")
759 c.UI.Pages.RemovePage("joinServer")
760 c.UI.Pages.AddPage("errModal", errModal, false, true)
762 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
764 c.Logger.Error(string(t.GetField(fieldError).Data))
765 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
767 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
768 c.UI.App.SetFocus(c.UI.chatInput)
770 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
771 c.Logger.Errorw("err", "err", err)
776 // JoinServer connects to a Hotline server and completes the login flow
777 func (c *Client) JoinServer(address, login, passwd string) error {
778 // Establish TCP connection to server
779 if err := c.connect(address); err != nil {
783 // Send handshake sequence
784 if err := c.Handshake(); err != nil {
788 // Authenticate (send tranLogin 107)
789 if err := c.LogIn(login, passwd); err != nil {
796 // connect establishes a connection with a Server by sending handshake sequence
797 func (c *Client) connect(address string) error {
799 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
806 var ClientHandshake = []byte{
807 0x54, 0x52, 0x54, 0x50, // TRTP
808 0x48, 0x4f, 0x54, 0x4c, // HOTL
813 var ServerHandshake = []byte{
814 0x54, 0x52, 0x54, 0x50, // TRTP
815 0x00, 0x00, 0x00, 0x00, // ErrorCode
818 func (c *Client) Handshake() error {
819 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
820 //Sub-protocol ID 4 User defined
821 //Version 2 1 Currently 1
822 //Sub-version 2 User defined
823 if _, err := c.Connection.Write(ClientHandshake); err != nil {
824 return fmt.Errorf("handshake write err: %s", err)
827 replyBuf := make([]byte, 8)
828 _, err := c.Connection.Read(replyBuf)
833 //spew.Dump(replyBuf)
834 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
837 // In the case of an error, client and server close the connection.
839 return fmt.Errorf("handshake response err: %s", err)
842 func (c *Client) LogIn(login string, password string) error {
846 NewField(fieldUserName, []byte(c.pref.Username)),
847 NewField(fieldUserIconID, c.pref.IconBytes()),
848 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
849 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
850 NewField(fieldVersion, []byte{0, 2}),
855 func (c *Client) Send(t Transaction) error {
856 requestNum := binary.BigEndian.Uint16(t.Type)
857 tID := binary.BigEndian.Uint32(t.ID)
859 //handler := TransactionHandlers[requestNum]
861 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
863 c.activeTasks[tID] = &t
868 if n, err = c.Connection.Write(t.Payload()); err != nil {
871 c.Logger.Debugw("Sent Transaction",
872 "IsReply", t.IsReply,
879 func (c *Client) HandleTransaction(t *Transaction) error {
880 var origT Transaction
882 requestID := binary.BigEndian.Uint32(t.ID)
883 origT = *c.activeTasks[requestID]
887 requestNum := binary.BigEndian.Uint16(t.Type)
889 "Received Transaction",
890 "RequestType", requestNum,
893 if handler, ok := c.Handlers[requestNum]; ok {
894 outT, _ := handler.Handle(c, t)
895 for _, t := range outT {
900 "Unimplemented transaction type received",
901 "RequestID", requestNum,
902 "TransactionID", t.ID,
909 func (c *Client) Connected() bool {
910 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
911 // c.Agreed == true &&
912 if c.UserAccess != nil {
918 func (c *Client) Disconnect() error {
919 err := c.Connection.Close()