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 readConfig(cfgPath string) (*ClientPrefs, error) {
48 fh, err := os.Open(cfgPath)
53 prefs := ClientPrefs{}
54 decoder := yaml.NewDecoder(fh)
55 decoder.SetStrict(true)
56 if err := decoder.Decode(&prefs); err != nil {
75 Logger *zap.SugaredLogger
76 activeTasks map[uint32]*Transaction
80 Handlers map[uint16]clientTHandler
84 outbox chan *Transaction
85 Inbox chan *Transaction
89 chatBox *tview.TextView
90 chatInput *tview.InputField
91 App *tview.Application
93 userList *tview.TextView
94 agreeModal *tview.Modal
95 trackerList *tview.List
96 settingsPage *tview.Box
100 func NewUI(c *Client) *UI {
101 app := tview.NewApplication()
102 chatBox := tview.NewTextView().
104 SetDynamicColors(true).
106 SetChangedFunc(func() {
107 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
109 chatBox.Box.SetBorder(true).SetTitle("Chat")
111 chatInput := tview.NewInputField()
114 SetFieldBackgroundColor(tcell.ColorDimGray).
115 SetDoneFunc(func(key tcell.Key) {
116 // skip send if user hit enter with no other text
117 if len(chatInput.GetText()) == 0 {
122 *NewTransaction(tranChatSend, nil,
123 NewField(fieldData, []byte(chatInput.GetText())),
126 chatInput.SetText("") // clear the input field after chat send
129 chatInput.Box.SetBorder(true).SetTitle("Send")
133 SetDynamicColors(true).
134 SetChangedFunc(func() {
135 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
137 userList.Box.SetBorder(true).SetTitle("Users")
142 Pages: tview.NewPages(),
143 chatInput: chatInput,
145 trackerList: tview.NewList(),
146 agreeModal: tview.NewModal(),
151 func (ui *UI) showBookmarks() *tview.List {
152 list := tview.NewList()
153 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
154 if event.Key() == tcell.KeyEsc {
155 ui.Pages.SwitchToPage("home")
159 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
161 shortcut := 97 // rune for "a"
162 for i, srv := range ui.HLClient.pref.Bookmarks {
166 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
167 ui.Pages.RemovePage("joinServer")
169 newJS := ui.renderJoinServerForm(addr, login, pass, "bookmarks", true, true)
171 ui.Pages.AddPage("joinServer", newJS, true, true)
178 func (ui *UI) getTrackerList() *tview.List {
179 listing, err := GetListing(ui.HLClient.pref.Tracker)
184 list := tview.NewList()
185 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
186 if event.Key() == tcell.KeyEsc {
187 ui.Pages.SwitchToPage("home")
191 list.Box.SetBorder(true).SetTitle("| Servers |")
193 shortcut := 97 // rune for "a"
194 for i, srv := range listing {
196 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
197 ui.Pages.RemovePage("joinServer")
199 newJS := ui.renderJoinServerForm(addr, GuestAccount, "", trackerListPage, false, true)
201 ui.Pages.AddPage("joinServer", newJS, true, true)
202 ui.Pages.ShowPage("joinServer")
209 func (ui *UI) renderSettingsForm() *tview.Flex {
210 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
211 settingsForm := tview.NewForm()
212 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
213 settingsForm.AddInputField("IconID",iconStr, 0, func(idStr string, _ rune) bool {
214 _, err := strconv.Atoi(idStr)
217 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
218 settingsForm.AddButton("Save", func() {
219 ui.HLClient.pref.Username = settingsForm.GetFormItem(0).(*tview.InputField).GetText()
220 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
221 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
222 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
224 out, err := yaml.Marshal(&ui.HLClient.pref)
229 _ = ioutil.WriteFile(clientConfigPath, out, 0666)
230 ui.Pages.RemovePage("settings")
232 settingsForm.SetBorder(true)
233 settingsForm.SetCancelFunc(func() {
234 ui.Pages.RemovePage("settings")
236 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
237 settingsPage.Box.SetBorder(true).SetTitle("Settings")
238 settingsPage.AddItem(settingsForm, 0, 1, true)
240 centerFlex := tview.NewFlex().
241 AddItem(nil, 0, 1, false).
242 AddItem(tview.NewFlex().
243 SetDirection(tview.FlexRow).
244 AddItem(nil, 0, 1, false).
245 AddItem(settingsForm, 15, 1, true).
246 AddItem(nil, 0, 1, false), 40, 1, true).
247 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 {
288 joinServerForm := tview.NewForm()
290 AddInputField("Server", server, 20, nil, func(text string) {
293 AddInputField("Login", login, 20, nil, func(text string) {
295 ui.HLClient.Login = &l
297 AddPasswordField("Password", password, 20, '*', nil).
298 AddCheckbox("Save", save, func(checked bool) {
301 AddButton("Cancel", func() {
302 ui.Pages.SwitchToPage(backPage)
304 AddButton("Connect", func() {
305 err := ui.joinServer(
306 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
307 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
308 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
311 ui.HLClient.Logger.Errorw("login error", "err", err)
312 loginErrModal := tview.NewModal().
313 AddButtons([]string{"Oh no"}).
314 SetText(err.Error()).
315 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
316 ui.Pages.SwitchToPage(backPage)
319 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
323 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
324 // TODO: implement bookmark saving
328 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
329 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
330 if event.Key() == tcell.KeyEscape {
331 ui.Pages.SwitchToPage(backPage)
337 joinServerForm.SetFocus(5)
340 joinServerPage := tview.NewFlex().
341 AddItem(nil, 0, 1, false).
342 AddItem(tview.NewFlex().
343 SetDirection(tview.FlexRow).
344 AddItem(nil, 0, 1, false).
345 AddItem(joinServerForm, 14, 1, true).
346 AddItem(nil, 0, 1, false), 40, 1, true).
347 AddItem(nil, 0, 1, false)
349 return joinServerPage
352 func randomBanner() string {
353 rand.Seed(time.Now().UnixNano())
355 bannerFiles, _ := bannerDir.ReadDir("client/banners")
356 file, _ := bannerDir.ReadFile("client/banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
358 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
361 func (ui *UI) renderServerUI() *tview.Flex {
362 commandList := tview.NewTextView().SetDynamicColors(true)
364 SetText("[yellow]^n[-::]: Read News\n[yellow]^l[-::]: View Logs\n").
366 SetTitle("Keyboard Shortcuts")
368 modal := tview.NewModal().
369 SetText("Disconnect from the server?").
370 AddButtons([]string{"Cancel", "Exit"}).
372 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
373 if buttonIndex == 1 {
374 _ = ui.HLClient.Disconnect()
375 ui.Pages.SwitchToPage("home")
377 ui.Pages.HidePage("modal")
381 serverUI := tview.NewFlex().
382 AddItem(tview.NewFlex().
383 SetDirection(tview.FlexRow).
384 AddItem(commandList, 4, 0, false).
385 AddItem(ui.chatBox, 0, 8, false).
386 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
387 AddItem(ui.userList, 25, 1, false)
388 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
389 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
390 if event.Key() == tcell.KeyEscape {
391 ui.Pages.AddPage("modal", modal, false, true)
395 if event.Key() == tcell.KeyCtrlN {
396 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
397 ui.HLClient.Logger.Errorw("err", "err", err)
406 func (ui *UI) Start() {
407 home := tview.NewFlex().SetDirection(tview.FlexRow)
408 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
409 mainMenu := tview.NewList()
411 bannerItem := tview.NewTextView().
412 SetText(randomBanner()).
413 SetDynamicColors(true).
414 SetTextAlign(tview.AlignCenter)
417 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
419 home.AddItem(tview.NewFlex().
420 AddItem(nil, 0, 1, false).
421 AddItem(mainMenu, 0, 1, true).
422 AddItem(nil, 0, 1, false),
426 mainMenu.AddItem("Join Server", "", 'j', func() {
427 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
428 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
430 AddItem("Bookmarks", "", 'b', func() {
431 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
433 AddItem("Browse Tracker", "", 't', func() {
434 ui.trackerList = ui.getTrackerList()
435 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
437 AddItem("Settings", "", 's', func() {
438 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
440 AddItem("Quit", "", 'q', func() {
444 ui.Pages.AddPage("home", home, true, true)
446 // App level input capture
447 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
448 if event.Key() == tcell.KeyCtrlC {
449 ui.HLClient.Logger.Infow("Exiting")
454 if event.Key() == tcell.KeyCtrlL {
455 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
456 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
457 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
458 if key == tcell.KeyEscape {
459 ui.Pages.RemovePage("logs")
463 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
468 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
474 func NewClient(username string, logger *zap.SugaredLogger) *Client {
476 Icon: &[]byte{0x07, 0xd7},
478 activeTasks: make(map[uint32]*Transaction),
479 Handlers: clientHandlers,
483 prefs, err := readConfig(clientConfigPath)
492 type clientTransaction struct {
494 Handler func(*Client, *Transaction) ([]Transaction, error)
497 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
498 return ch.Handler(cc, t)
501 type clientTHandler interface {
502 Handle(*Client, *Transaction) ([]Transaction, error)
505 type mockClientHandler struct {
509 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
510 args := mh.Called(cc, t)
511 return args.Get(0).([]Transaction), args.Error(1)
514 var clientHandlers = map[uint16]clientTHandler{
516 tranChatMsg: clientTransaction{
518 Handler: handleClientChatMsg,
520 tranLogin: clientTransaction{
522 Handler: handleClientTranLogin,
524 tranShowAgreement: clientTransaction{
525 Name: "tranShowAgreement",
526 Handler: handleClientTranShowAgreement,
528 tranUserAccess: clientTransaction{
529 Name: "tranUserAccess",
530 Handler: handleClientTranUserAccess,
532 tranGetUserNameList: clientTransaction{
533 Name: "tranGetUserNameList",
534 Handler: handleClientGetUserNameList,
536 tranNotifyChangeUser: clientTransaction{
537 Name: "tranNotifyChangeUser",
538 Handler: handleNotifyChangeUser,
540 tranNotifyDeleteUser: clientTransaction{
541 Name: "tranNotifyDeleteUser",
542 Handler: handleNotifyDeleteUser,
544 tranGetMsgs: clientTransaction{
545 Name: "tranNotifyDeleteUser",
546 Handler: handleGetMsgs,
550 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
551 newsText := string(t.GetField(fieldData).Data)
552 newsText = strings.ReplaceAll(newsText, "\r", "\n")
554 newsTextView := tview.NewTextView().
556 SetDoneFunc(func(key tcell.Key) {
557 c.UI.Pages.SwitchToPage("serverUI")
558 c.UI.App.SetFocus(c.UI.chatInput)
560 newsTextView.SetBorder(true).SetTitle("News")
562 c.UI.Pages.AddPage("news", newsTextView, true, true)
563 c.UI.Pages.SwitchToPage("news")
564 c.UI.App.SetFocus(newsTextView)
571 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
573 ID: t.GetField(fieldUserID).Data,
574 Name: string(t.GetField(fieldUserName).Data),
575 Icon: t.GetField(fieldUserIconID).Data,
576 Flags: t.GetField(fieldUserFlags).Data,
580 // user is new to the server
581 // user is already on the server but has a new name
584 var newUserList []User
586 for _, u := range c.UserList {
587 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
588 if bytes.Equal(newUser.ID, u.ID) {
590 u.Name = newUser.Name
591 if u.Name != newUser.Name {
592 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
596 newUserList = append(newUserList, u)
600 newUserList = append(newUserList, newUser)
603 c.UserList = newUserList
610 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
611 exitUser := t.GetField(fieldUserID).Data
613 var newUserList []User
614 for _, u := range c.UserList {
615 if !bytes.Equal(exitUser, u.ID) {
616 newUserList = append(newUserList, u)
620 c.UserList = newUserList
627 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
629 func (c *Client) ReadLoop() error {
630 tranBuff := make([]byte, 0)
632 // Infinite loop where take action on incoming client requests until the connection is closed
634 buf := make([]byte, readBuffSize)
635 tranBuff = tranBuff[tReadlen:]
637 readLen, err := c.Connection.Read(buf)
641 tranBuff = append(tranBuff, buf[:readLen]...)
643 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
644 // into a slice of transactions
645 var transactions []Transaction
646 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
647 c.Logger.Errorw("Error handling transaction", "err", err)
650 // iterate over all of the transactions that were parsed from the byte slice and handle them
651 for _, t := range transactions {
652 if err := c.HandleTransaction(&t); err != nil {
653 c.Logger.Errorw("Error handling transaction", "err", err)
659 func (c *Client) GetTransactions() error {
660 tranBuff := make([]byte, 0)
663 buf := make([]byte, readBuffSize)
664 tranBuff = tranBuff[tReadlen:]
666 readLen, err := c.Connection.Read(buf)
670 tranBuff = append(tranBuff, buf[:readLen]...)
675 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
677 for _, field := range t.Fields {
678 u, _ := ReadUser(field.Data)
679 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
680 //if flagBitmap.Bit(userFlagAdmin) == 1 {
681 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
683 // fmt.Fprintf(UserList, "%s\n", u.Name)
686 users = append(users, *u)
695 func (c *Client) renderUserList() {
696 c.UI.userList.Clear()
697 for _, u := range c.UserList {
698 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
699 if flagBitmap.Bit(userFlagAdmin) == 1 {
700 fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
702 fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
707 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
708 fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
713 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
714 c.UserAccess = t.GetField(fieldUserAccess).Data
719 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
720 agreement := string(t.GetField(fieldData).Data)
721 agreement = strings.ReplaceAll(agreement, "\r", "\n")
723 c.UI.agreeModal = tview.NewModal().
725 AddButtons([]string{"Agree", "Disagree"}).
726 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
727 if buttonIndex == 0 {
731 NewField(fieldUserName, []byte(c.pref.Username)),
732 NewField(fieldUserIconID, *c.Icon),
733 NewField(fieldUserFlags, []byte{0x00, 0x00}),
734 NewField(fieldOptions, []byte{0x00, 0x00}),
738 c.UI.Pages.HidePage("agreement")
739 c.UI.App.SetFocus(c.UI.chatInput)
742 c.UI.Pages.SwitchToPage("home")
747 c.Logger.Debug("show agreement page")
748 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
750 c.UI.Pages.ShowPage("agreement ")
756 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
757 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
758 errMsg := string(t.GetField(fieldError).Data)
759 errModal := tview.NewModal()
760 errModal.SetText(errMsg)
761 errModal.AddButtons([]string{"Oh no"})
762 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
763 c.UI.Pages.RemovePage("errModal")
765 c.UI.Pages.RemovePage("joinServer")
766 c.UI.Pages.AddPage("errModal", errModal, false, true)
768 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
770 c.Logger.Error(string(t.GetField(fieldError).Data))
771 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
773 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
774 c.UI.App.SetFocus(c.UI.chatInput)
776 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
777 c.Logger.Errorw("err", "err", err)
782 // JoinServer connects to a Hotline server and completes the login flow
783 func (c *Client) JoinServer(address, login, passwd string) error {
784 // Establish TCP connection to server
785 if err := c.connect(address); err != nil {
789 // Send handshake sequence
790 if err := c.Handshake(); err != nil {
794 // Authenticate (send tranLogin 107)
795 if err := c.LogIn(login, passwd); err != nil {
802 // connect establishes a connection with a Server by sending handshake sequence
803 func (c *Client) connect(address string) error {
805 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
812 var ClientHandshake = []byte{
813 0x54, 0x52, 0x54, 0x50, // TRTP
814 0x48, 0x4f, 0x54, 0x4c, // HOTL
819 var ServerHandshake = []byte{
820 0x54, 0x52, 0x54, 0x50, // TRTP
821 0x00, 0x00, 0x00, 0x00, // ErrorCode
824 func (c *Client) Handshake() error {
825 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
826 //Sub-protocol ID 4 User defined
827 //Version 2 1 Currently 1
828 //Sub-version 2 User defined
829 if _, err := c.Connection.Write(ClientHandshake); err != nil {
830 return fmt.Errorf("handshake write err: %s", err)
833 replyBuf := make([]byte, 8)
834 _, err := c.Connection.Read(replyBuf)
839 //spew.Dump(replyBuf)
840 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
843 // In the case of an error, client and server close the connection.
845 return fmt.Errorf("handshake response err: %s", err)
848 func (c *Client) LogIn(login string, password string) error {
852 NewField(fieldUserName, []byte(c.pref.Username)),
853 NewField(fieldUserIconID, []byte{0x07, 0xd1}),
854 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
855 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
856 NewField(fieldVersion, []byte{0, 2}),
861 func (c *Client) Send(t Transaction) error {
862 requestNum := binary.BigEndian.Uint16(t.Type)
863 tID := binary.BigEndian.Uint32(t.ID)
865 //handler := TransactionHandlers[requestNum]
867 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
869 c.activeTasks[tID] = &t
874 if n, err = c.Connection.Write(t.Payload()); err != nil {
877 c.Logger.Debugw("Sent Transaction",
878 "IsReply", t.IsReply,
885 func (c *Client) HandleTransaction(t *Transaction) error {
886 var origT Transaction
888 requestID := binary.BigEndian.Uint32(t.ID)
889 origT = *c.activeTasks[requestID]
893 requestNum := binary.BigEndian.Uint16(t.Type)
895 "Received Transaction",
896 "RequestType", requestNum,
899 if handler, ok := c.Handlers[requestNum]; ok {
900 outT, _ := handler.Handle(c, t)
901 for _, t := range outT {
906 "Unimplemented transaction type received",
907 "RequestID", requestNum,
908 "TransactionID", t.ID,
915 func (c *Client) Connected() bool {
916 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
917 // c.Agreed == true &&
918 if c.UserAccess != nil {
924 func (c *Client) Disconnect() error {
925 err := c.Connection.Close()