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)
252 // DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
253 type DebugBuffer struct {
254 TextView *tview.TextView
257 func (db *DebugBuffer) Write(p []byte) (int, error) {
258 return db.TextView.Write(p)
261 // Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
262 func (db *DebugBuffer) Sync() error {
266 func (ui *UI) joinServer(addr, login, password string) error {
267 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
268 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
272 err := ui.HLClient.ReadLoop()
274 ui.HLClient.Logger.Errorw("read error", "err", err)
280 func (ui *UI) renderJoinServerForm(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
281 joinServerForm := tview.NewForm()
283 AddInputField("Server", server, 0, nil, nil).
284 AddInputField("Login", login, 0, nil, nil).
285 AddPasswordField("Password", password, 0, '*', nil).
286 AddCheckbox("Save", save, func(checked bool) {
287 // TODO: Implement bookmark saving
289 AddButton("Cancel", func() {
290 ui.Pages.SwitchToPage(backPage)
292 AddButton("Connect", func() {
293 err := ui.joinServer(
294 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
295 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
296 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
299 ui.HLClient.Logger.Errorw("login error", "err", err)
300 loginErrModal := tview.NewModal().
301 AddButtons([]string{"Oh no"}).
302 SetText(err.Error()).
303 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
304 ui.Pages.SwitchToPage(backPage)
307 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
311 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
312 // TODO: implement bookmark saving
316 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
317 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
318 if event.Key() == tcell.KeyEscape {
319 ui.Pages.SwitchToPage(backPage)
325 joinServerForm.SetFocus(5)
328 joinServerPage := tview.NewFlex().
329 AddItem(nil, 0, 1, false).
330 AddItem(tview.NewFlex().
331 SetDirection(tview.FlexRow).
332 AddItem(nil, 0, 1, false).
333 AddItem(joinServerForm, 14, 1, true).
334 AddItem(nil, 0, 1, false), 40, 1, true).
335 AddItem(nil, 0, 1, false)
337 return joinServerPage
340 func randomBanner() string {
341 rand.Seed(time.Now().UnixNano())
343 bannerFiles, _ := bannerDir.ReadDir("client/banners")
344 file, _ := bannerDir.ReadFile("client/banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
346 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
349 func (ui *UI) renderServerUI() *tview.Flex {
350 commandList := tview.NewTextView().SetDynamicColors(true)
352 SetText("[yellow]^n[-::]: Read News\n[yellow]^l[-::]: View Logs\n").
354 SetTitle("Keyboard Shortcuts")
356 modal := tview.NewModal().
357 SetText("Disconnect from the server?").
358 AddButtons([]string{"Cancel", "Exit"}).
360 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
361 if buttonIndex == 1 {
362 _ = ui.HLClient.Disconnect()
363 ui.Pages.SwitchToPage("home")
365 ui.Pages.HidePage("modal")
369 serverUI := tview.NewFlex().
370 AddItem(tview.NewFlex().
371 SetDirection(tview.FlexRow).
372 AddItem(commandList, 4, 0, false).
373 AddItem(ui.chatBox, 0, 8, false).
374 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
375 AddItem(ui.userList, 25, 1, false)
376 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
377 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
378 if event.Key() == tcell.KeyEscape {
379 ui.Pages.AddPage("modal", modal, false, true)
383 if event.Key() == tcell.KeyCtrlN {
384 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
385 ui.HLClient.Logger.Errorw("err", "err", err)
394 func (ui *UI) Start() {
395 home := tview.NewFlex().SetDirection(tview.FlexRow)
396 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
397 mainMenu := tview.NewList()
399 bannerItem := tview.NewTextView().
400 SetText(randomBanner()).
401 SetDynamicColors(true).
402 SetTextAlign(tview.AlignCenter)
405 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
407 home.AddItem(tview.NewFlex().
408 AddItem(nil, 0, 1, false).
409 AddItem(mainMenu, 0, 1, true).
410 AddItem(nil, 0, 1, false),
414 mainMenu.AddItem("Join Server", "", 'j', func() {
415 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
416 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
418 AddItem("Bookmarks", "", 'b', func() {
419 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
421 AddItem("Browse Tracker", "", 't', func() {
422 ui.trackerList = ui.getTrackerList()
423 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
425 AddItem("Settings", "", 's', func() {
426 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
428 AddItem("Quit", "", 'q', func() {
432 ui.Pages.AddPage("home", home, true, true)
434 // App level input capture
435 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
436 if event.Key() == tcell.KeyCtrlC {
437 ui.HLClient.Logger.Infow("Exiting")
442 if event.Key() == tcell.KeyCtrlL {
443 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
444 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
445 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
446 if key == tcell.KeyEscape {
447 ui.Pages.RemovePage("logs")
451 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
456 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
462 func NewClient(username string, logger *zap.SugaredLogger) *Client {
464 Icon: &[]byte{0x07, 0xd7},
466 activeTasks: make(map[uint32]*Transaction),
467 Handlers: clientHandlers,
471 prefs, err := readConfig(clientConfigPath)
480 type clientTransaction struct {
482 Handler func(*Client, *Transaction) ([]Transaction, error)
485 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
486 return ch.Handler(cc, t)
489 type clientTHandler interface {
490 Handle(*Client, *Transaction) ([]Transaction, error)
493 type mockClientHandler struct {
497 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
498 args := mh.Called(cc, t)
499 return args.Get(0).([]Transaction), args.Error(1)
502 var clientHandlers = map[uint16]clientTHandler{
504 tranChatMsg: clientTransaction{
506 Handler: handleClientChatMsg,
508 tranLogin: clientTransaction{
510 Handler: handleClientTranLogin,
512 tranShowAgreement: clientTransaction{
513 Name: "tranShowAgreement",
514 Handler: handleClientTranShowAgreement,
516 tranUserAccess: clientTransaction{
517 Name: "tranUserAccess",
518 Handler: handleClientTranUserAccess,
520 tranGetUserNameList: clientTransaction{
521 Name: "tranGetUserNameList",
522 Handler: handleClientGetUserNameList,
524 tranNotifyChangeUser: clientTransaction{
525 Name: "tranNotifyChangeUser",
526 Handler: handleNotifyChangeUser,
528 tranNotifyDeleteUser: clientTransaction{
529 Name: "tranNotifyDeleteUser",
530 Handler: handleNotifyDeleteUser,
532 tranGetMsgs: clientTransaction{
533 Name: "tranNotifyDeleteUser",
534 Handler: handleGetMsgs,
538 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
539 newsText := string(t.GetField(fieldData).Data)
540 newsText = strings.ReplaceAll(newsText, "\r", "\n")
542 newsTextView := tview.NewTextView().
544 SetDoneFunc(func(key tcell.Key) {
545 c.UI.Pages.SwitchToPage("serverUI")
546 c.UI.App.SetFocus(c.UI.chatInput)
548 newsTextView.SetBorder(true).SetTitle("News")
550 c.UI.Pages.AddPage("news", newsTextView, true, true)
551 c.UI.Pages.SwitchToPage("news")
552 c.UI.App.SetFocus(newsTextView)
559 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
561 ID: t.GetField(fieldUserID).Data,
562 Name: string(t.GetField(fieldUserName).Data),
563 Icon: t.GetField(fieldUserIconID).Data,
564 Flags: t.GetField(fieldUserFlags).Data,
568 // user is new to the server
569 // user is already on the server but has a new name
572 var newUserList []User
574 for _, u := range c.UserList {
575 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
576 if bytes.Equal(newUser.ID, u.ID) {
578 u.Name = newUser.Name
579 if u.Name != newUser.Name {
580 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
584 newUserList = append(newUserList, u)
588 newUserList = append(newUserList, newUser)
591 c.UserList = newUserList
598 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
599 exitUser := t.GetField(fieldUserID).Data
601 var newUserList []User
602 for _, u := range c.UserList {
603 if !bytes.Equal(exitUser, u.ID) {
604 newUserList = append(newUserList, u)
608 c.UserList = newUserList
615 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
617 func (c *Client) ReadLoop() error {
618 tranBuff := make([]byte, 0)
620 // Infinite loop where take action on incoming client requests until the connection is closed
622 buf := make([]byte, readBuffSize)
623 tranBuff = tranBuff[tReadlen:]
625 readLen, err := c.Connection.Read(buf)
629 tranBuff = append(tranBuff, buf[:readLen]...)
631 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
632 // into a slice of transactions
633 var transactions []Transaction
634 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
635 c.Logger.Errorw("Error handling transaction", "err", err)
638 // iterate over all of the transactions that were parsed from the byte slice and handle them
639 for _, t := range transactions {
640 if err := c.HandleTransaction(&t); err != nil {
641 c.Logger.Errorw("Error handling transaction", "err", err)
647 func (c *Client) GetTransactions() error {
648 tranBuff := make([]byte, 0)
651 buf := make([]byte, readBuffSize)
652 tranBuff = tranBuff[tReadlen:]
654 readLen, err := c.Connection.Read(buf)
658 tranBuff = append(tranBuff, buf[:readLen]...)
663 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
665 for _, field := range t.Fields {
666 u, _ := ReadUser(field.Data)
667 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
668 //if flagBitmap.Bit(userFlagAdmin) == 1 {
669 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
671 // fmt.Fprintf(UserList, "%s\n", u.Name)
674 users = append(users, *u)
683 func (c *Client) renderUserList() {
684 c.UI.userList.Clear()
685 for _, u := range c.UserList {
686 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
687 if flagBitmap.Bit(userFlagAdmin) == 1 {
688 fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
690 fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
695 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
696 fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
701 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
702 c.UserAccess = t.GetField(fieldUserAccess).Data
707 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
708 agreement := string(t.GetField(fieldData).Data)
709 agreement = strings.ReplaceAll(agreement, "\r", "\n")
711 c.UI.agreeModal = tview.NewModal().
713 AddButtons([]string{"Agree", "Disagree"}).
714 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
715 if buttonIndex == 0 {
719 NewField(fieldUserName, []byte(c.pref.Username)),
720 NewField(fieldUserIconID, *c.Icon),
721 NewField(fieldUserFlags, []byte{0x00, 0x00}),
722 NewField(fieldOptions, []byte{0x00, 0x00}),
726 c.UI.Pages.HidePage("agreement")
727 c.UI.App.SetFocus(c.UI.chatInput)
730 c.UI.Pages.SwitchToPage("home")
735 c.Logger.Debug("show agreement page")
736 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
738 c.UI.Pages.ShowPage("agreement ")
744 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
745 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
746 errMsg := string(t.GetField(fieldError).Data)
747 errModal := tview.NewModal()
748 errModal.SetText(errMsg)
749 errModal.AddButtons([]string{"Oh no"})
750 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
751 c.UI.Pages.RemovePage("errModal")
753 c.UI.Pages.RemovePage("joinServer")
754 c.UI.Pages.AddPage("errModal", errModal, false, true)
756 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
758 c.Logger.Error(string(t.GetField(fieldError).Data))
759 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
761 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
762 c.UI.App.SetFocus(c.UI.chatInput)
764 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
765 c.Logger.Errorw("err", "err", err)
770 // JoinServer connects to a Hotline server and completes the login flow
771 func (c *Client) JoinServer(address, login, passwd string) error {
772 // Establish TCP connection to server
773 if err := c.connect(address); err != nil {
777 // Send handshake sequence
778 if err := c.Handshake(); err != nil {
782 // Authenticate (send tranLogin 107)
783 if err := c.LogIn(login, passwd); err != nil {
790 // connect establishes a connection with a Server by sending handshake sequence
791 func (c *Client) connect(address string) error {
793 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
800 var ClientHandshake = []byte{
801 0x54, 0x52, 0x54, 0x50, // TRTP
802 0x48, 0x4f, 0x54, 0x4c, // HOTL
807 var ServerHandshake = []byte{
808 0x54, 0x52, 0x54, 0x50, // TRTP
809 0x00, 0x00, 0x00, 0x00, // ErrorCode
812 func (c *Client) Handshake() error {
813 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
814 //Sub-protocol ID 4 User defined
815 //Version 2 1 Currently 1
816 //Sub-version 2 User defined
817 if _, err := c.Connection.Write(ClientHandshake); err != nil {
818 return fmt.Errorf("handshake write err: %s", err)
821 replyBuf := make([]byte, 8)
822 _, err := c.Connection.Read(replyBuf)
827 //spew.Dump(replyBuf)
828 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
831 // In the case of an error, client and server close the connection.
833 return fmt.Errorf("handshake response err: %s", err)
836 func (c *Client) LogIn(login string, password string) error {
840 NewField(fieldUserName, []byte(c.pref.Username)),
841 NewField(fieldUserIconID, []byte{0x07, 0xd1}),
842 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
843 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
844 NewField(fieldVersion, []byte{0, 2}),
849 func (c *Client) Send(t Transaction) error {
850 requestNum := binary.BigEndian.Uint16(t.Type)
851 tID := binary.BigEndian.Uint32(t.ID)
853 //handler := TransactionHandlers[requestNum]
855 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
857 c.activeTasks[tID] = &t
862 if n, err = c.Connection.Write(t.Payload()); err != nil {
865 c.Logger.Debugw("Sent Transaction",
866 "IsReply", t.IsReply,
873 func (c *Client) HandleTransaction(t *Transaction) error {
874 var origT Transaction
876 requestID := binary.BigEndian.Uint32(t.ID)
877 origT = *c.activeTasks[requestID]
881 requestNum := binary.BigEndian.Uint16(t.Type)
883 "Received Transaction",
884 "RequestType", requestNum,
887 if handler, ok := c.Handlers[requestNum]; ok {
888 outT, _ := handler.Handle(c, t)
889 for _, t := range outT {
894 "Unimplemented transaction type received",
895 "RequestID", requestNum,
896 "TransactionID", t.ID,
903 func (c *Client) Connected() bool {
904 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
905 // c.Agreed == true &&
906 if c.UserAccess != nil {
912 func (c *Client) Disconnect() error {
913 err := c.Connection.Close()