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().
105 SetDynamicColors(true).
107 SetChangedFunc(func() {
108 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
110 chatBox.Box.SetBorder(true).SetTitle("Chat")
112 chatInput := tview.NewInputField()
115 SetFieldBackgroundColor(tcell.ColorDimGray).
116 //SetFieldTextColor(tcell.ColorWhite).
117 SetDoneFunc(func(key tcell.Key) {
118 // skip send if user hit enter with no other text
119 if len(chatInput.GetText()) == 0 {
124 *NewTransaction(tranChatSend, nil,
125 NewField(fieldData, []byte(chatInput.GetText())),
128 chatInput.SetText("") // clear the input field after chat send
131 chatInput.Box.SetBorder(true).SetTitle("Send")
133 userList := tview.NewTextView().SetDynamicColors(true)
134 userList.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 const defaultUsername = "unnamed"
154 trackerListPage = "trackerList"
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)
264 // DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
265 type DebugBuffer struct {
266 TextView *tview.TextView
269 func (db *DebugBuffer) Write(p []byte) (int, error) {
270 return db.TextView.Write(p)
273 // Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
274 func (db *DebugBuffer) Sync() error {
278 func (ui *UI) joinServer(addr, login, password string) error {
279 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
280 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
284 err := ui.HLClient.ReadLoop()
286 ui.HLClient.Logger.Errorw("read error", "err", err)
292 func (ui *UI) renderJoinServerForm(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
294 joinServerForm := tview.NewForm()
296 AddInputField("Server", server, 20, nil, func(text string) {
299 AddInputField("Login", login, 20, nil, func(text string) {
301 ui.HLClient.Login = &l
303 AddPasswordField("Password", password, 20, '*', nil).
304 AddCheckbox("Save", save, func(checked bool) {
307 AddButton("Cancel", func() {
308 ui.Pages.SwitchToPage(backPage)
310 AddButton("Connect", func() {
311 err := ui.joinServer(
312 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
313 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
314 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
317 ui.HLClient.Logger.Errorw("login error", "err", err)
318 loginErrModal := tview.NewModal().
319 AddButtons([]string{"Oh no"}).
320 SetText(err.Error()).
321 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
322 ui.Pages.SwitchToPage(backPage)
325 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
329 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
330 // TODO: implement bookmark saving
334 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
335 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
336 if event.Key() == tcell.KeyEscape {
337 ui.Pages.SwitchToPage(backPage)
343 joinServerForm.SetFocus(5)
346 joinServerPage := tview.NewFlex().
347 AddItem(nil, 0, 1, false).
348 AddItem(tview.NewFlex().
349 SetDirection(tview.FlexRow).
350 AddItem(nil, 0, 1, false).
351 AddItem(joinServerForm, 14, 1, true).
352 AddItem(nil, 0, 1, false), 40, 1, true).
353 AddItem(nil, 0, 1, false)
355 return joinServerPage
358 func randomBanner() string {
359 rand.Seed(time.Now().UnixNano())
361 bannerFiles, _ := bannerDir.ReadDir("client/banners")
362 file, _ := bannerDir.ReadFile("client/banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
364 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
367 func (ui *UI) renderServerUI() *tview.Flex {
368 commandList := tview.NewTextView().SetDynamicColors(true)
370 SetText("[yellow]^n[-::]: Read News\n[yellow]^l[-::]: View Logs\n").
372 SetTitle("Keyboard Shortcuts")
374 modal := tview.NewModal().
375 SetText("Disconnect from the server?").
376 AddButtons([]string{"Cancel", "Exit"}).
378 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
379 if buttonIndex == 1 {
380 _ = ui.HLClient.Disconnect()
381 ui.Pages.SwitchToPage("home")
383 ui.Pages.HidePage("modal")
387 serverUI := tview.NewFlex().
388 AddItem(tview.NewFlex().
389 SetDirection(tview.FlexRow).
390 AddItem(commandList, 4, 0, false).
391 AddItem(ui.chatBox, 0, 8, false).
392 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
393 AddItem(ui.userList, 25, 1, false)
394 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
395 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
396 if event.Key() == tcell.KeyEscape {
397 ui.Pages.AddPage("modal", modal, false, true)
401 if event.Key() == tcell.KeyCtrlN {
402 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
403 ui.HLClient.Logger.Errorw("err", "err", err)
412 func (ui *UI) Start() {
413 home := tview.NewFlex().SetDirection(tview.FlexRow)
414 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
415 mainMenu := tview.NewList()
417 bannerItem := tview.NewTextView().
418 SetText(randomBanner()).
419 SetDynamicColors(true).
420 SetTextAlign(tview.AlignCenter)
423 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
425 home.AddItem(tview.NewFlex().
426 AddItem(nil, 0, 1, false).
427 AddItem(mainMenu, 0, 1, true).
428 AddItem(nil, 0, 1, false),
432 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
434 mainMenu.AddItem("Join Server", "", 'j', func() {
435 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
437 AddItem("Bookmarks", "", 'b', func() {
438 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
440 AddItem("Browse Tracker", "", 't', func() {
441 ui.trackerList = ui.getTrackerList()
442 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
444 AddItem("Settings", "", 's', func() {
445 //ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, false)
447 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
449 AddItem("Quit", "", 'q', func() {
453 ui.Pages.AddPage("home", home, true, true)
455 // App level input capture
456 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
457 if event.Key() == tcell.KeyCtrlC {
458 ui.HLClient.Logger.Infow("Exiting")
463 if event.Key() == tcell.KeyCtrlL {
464 //curPage, _ := ui.Pages.GetFrontPage()
465 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
466 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
467 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
468 if key == tcell.KeyEscape {
469 //ui.Pages.SwitchToPage("serverUI")
470 ui.Pages.RemovePage("logs")
474 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
479 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
484 func NewClient(username string, logger *zap.SugaredLogger) *Client {
486 Icon: &[]byte{0x07, 0xd7},
488 activeTasks: make(map[uint32]*Transaction),
489 Handlers: clientHandlers,
493 prefs, err := readConfig(clientConfigPath)
502 type clientTransaction struct {
504 Handler func(*Client, *Transaction) ([]Transaction, error)
507 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
508 return ch.Handler(cc, t)
511 type clientTHandler interface {
512 Handle(*Client, *Transaction) ([]Transaction, error)
515 type mockClientHandler struct {
519 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
520 args := mh.Called(cc, t)
521 return args.Get(0).([]Transaction), args.Error(1)
524 var clientHandlers = map[uint16]clientTHandler{
526 tranChatMsg: clientTransaction{
528 Handler: handleClientChatMsg,
530 tranLogin: clientTransaction{
532 Handler: handleClientTranLogin,
534 tranShowAgreement: clientTransaction{
535 Name: "tranShowAgreement",
536 Handler: handleClientTranShowAgreement,
538 tranUserAccess: clientTransaction{
539 Name: "tranUserAccess",
540 Handler: handleClientTranUserAccess,
542 tranGetUserNameList: clientTransaction{
543 Name: "tranGetUserNameList",
544 Handler: handleClientGetUserNameList,
546 tranNotifyChangeUser: clientTransaction{
547 Name: "tranNotifyChangeUser",
548 Handler: handleNotifyChangeUser,
550 tranNotifyDeleteUser: clientTransaction{
551 Name: "tranNotifyDeleteUser",
552 Handler: handleNotifyDeleteUser,
554 tranGetMsgs: clientTransaction{
555 Name: "tranNotifyDeleteUser",
556 Handler: handleGetMsgs,
560 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
561 newsText := string(t.GetField(fieldData).Data)
562 newsText = strings.ReplaceAll(newsText, "\r", "\n")
564 newsTextView := tview.NewTextView().
566 SetDoneFunc(func(key tcell.Key) {
567 c.UI.Pages.SwitchToPage("serverUI")
568 c.UI.App.SetFocus(c.UI.chatInput)
570 newsTextView.SetBorder(true).SetTitle("News")
572 c.UI.Pages.AddPage("news", newsTextView, true, true)
573 c.UI.Pages.SwitchToPage("news")
574 c.UI.App.SetFocus(newsTextView)
581 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
583 ID: t.GetField(fieldUserID).Data,
584 Name: string(t.GetField(fieldUserName).Data),
585 Icon: t.GetField(fieldUserIconID).Data,
586 Flags: t.GetField(fieldUserFlags).Data,
590 // user is new to the server
591 // user is already on the server but has a new name
594 var newUserList []User
596 for _, u := range c.UserList {
597 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
598 if bytes.Equal(newUser.ID, u.ID) {
600 u.Name = newUser.Name
601 if u.Name != newUser.Name {
602 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
606 newUserList = append(newUserList, u)
610 newUserList = append(newUserList, newUser)
613 c.UserList = newUserList
620 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
621 exitUser := t.GetField(fieldUserID).Data
623 var newUserList []User
624 for _, u := range c.UserList {
625 if !bytes.Equal(exitUser, u.ID) {
626 newUserList = append(newUserList, u)
630 c.UserList = newUserList
637 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
639 func (c *Client) ReadLoop() error {
640 tranBuff := make([]byte, 0)
642 // Infinite loop where take action on incoming client requests until the connection is closed
644 buf := make([]byte, readBuffSize)
645 tranBuff = tranBuff[tReadlen:]
647 readLen, err := c.Connection.Read(buf)
651 tranBuff = append(tranBuff, buf[:readLen]...)
653 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
654 // into a slice of transactions
655 var transactions []Transaction
656 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
657 c.Logger.Errorw("Error handling transaction", "err", err)
660 // iterate over all of the transactions that were parsed from the byte slice and handle them
661 for _, t := range transactions {
662 if err := c.HandleTransaction(&t); err != nil {
663 c.Logger.Errorw("Error handling transaction", "err", err)
669 func (c *Client) GetTransactions() error {
670 tranBuff := make([]byte, 0)
673 buf := make([]byte, readBuffSize)
674 tranBuff = tranBuff[tReadlen:]
676 readLen, err := c.Connection.Read(buf)
680 tranBuff = append(tranBuff, buf[:readLen]...)
685 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
687 for _, field := range t.Fields {
688 u, _ := ReadUser(field.Data)
689 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
690 //if flagBitmap.Bit(userFlagAdmin) == 1 {
691 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
693 // fmt.Fprintf(UserList, "%s\n", u.Name)
696 users = append(users, *u)
705 func (c *Client) renderUserList() {
706 c.UI.userList.Clear()
707 for _, u := range c.UserList {
708 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
709 if flagBitmap.Bit(userFlagAdmin) == 1 {
710 fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
712 fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
717 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
718 fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
723 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
724 c.UserAccess = t.GetField(fieldUserAccess).Data
729 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
730 agreement := string(t.GetField(fieldData).Data)
731 agreement = strings.ReplaceAll(agreement, "\r", "\n")
733 c.UI.agreeModal = tview.NewModal().
735 AddButtons([]string{"Agree", "Disagree"}).
736 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
737 if buttonIndex == 0 {
741 NewField(fieldUserName, []byte(c.pref.Username)),
742 NewField(fieldUserIconID, *c.Icon),
743 NewField(fieldUserFlags, []byte{0x00, 0x00}),
744 NewField(fieldOptions, []byte{0x00, 0x00}),
748 c.UI.Pages.HidePage("agreement")
749 c.UI.App.SetFocus(c.UI.chatInput)
752 c.UI.Pages.SwitchToPage("home")
757 c.Logger.Debug("show agreement page")
758 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
760 c.UI.Pages.ShowPage("agreement ")
766 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
767 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
768 errMsg := string(t.GetField(fieldError).Data)
769 errModal := tview.NewModal()
770 errModal.SetText(errMsg)
771 errModal.AddButtons([]string{"Oh no"})
772 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
773 c.UI.Pages.RemovePage("errModal")
775 c.UI.Pages.RemovePage("joinServer")
776 c.UI.Pages.AddPage("errModal", errModal, false, true)
778 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
780 c.Logger.Error(string(t.GetField(fieldError).Data))
781 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
783 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
784 c.UI.App.SetFocus(c.UI.chatInput)
786 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
787 c.Logger.Errorw("err", "err", err)
792 // JoinServer connects to a Hotline server and completes the login flow
793 func (c *Client) JoinServer(address, login, passwd string) error {
794 // Establish TCP connection to server
795 if err := c.connect(address); err != nil {
799 // Send handshake sequence
800 if err := c.Handshake(); err != nil {
804 // Authenticate (send tranLogin 107)
805 if err := c.LogIn(login, passwd); err != nil {
812 // connect establishes a connection with a Server by sending handshake sequence
813 func (c *Client) connect(address string) error {
815 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
822 var ClientHandshake = []byte{
823 0x54, 0x52, 0x54, 0x50, // TRTP
824 0x48, 0x4f, 0x54, 0x4c, // HOTL
829 var ServerHandshake = []byte{
830 0x54, 0x52, 0x54, 0x50, // TRTP
831 0x00, 0x00, 0x00, 0x00, // ErrorCode
834 func (c *Client) Handshake() error {
835 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
836 //Sub-protocol ID 4 User defined
837 //Version 2 1 Currently 1
838 //Sub-version 2 User defined
839 if _, err := c.Connection.Write(ClientHandshake); err != nil {
840 return fmt.Errorf("handshake write err: %s", err)
843 replyBuf := make([]byte, 8)
844 _, err := c.Connection.Read(replyBuf)
849 //spew.Dump(replyBuf)
850 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
853 // In the case of an error, client and server close the connection.
855 return fmt.Errorf("handshake response err: %s", err)
858 func (c *Client) LogIn(login string, password string) error {
862 NewField(fieldUserName, []byte(c.pref.Username)),
863 NewField(fieldUserIconID, []byte{0x07, 0xd1}),
864 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
865 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
866 NewField(fieldVersion, []byte{0, 2}),
871 //// Agree agrees to the server agreement and sends user info, completing the login sequence
872 //func (c *Client) Agree() {
877 // NewField(fieldUserName, []byte("test")),
878 // NewField(fieldUserIconID, *c.Icon),
879 // NewField(fieldUserFlags, []byte{0x00, 0x00}),
884 // //// Block until we receive the agreement reply from the server
885 // //_ = c.WaitForTransaction(tranAgreed)
888 //func (c *Client) WaitForTransaction(id uint16) Transaction {
889 // var trans Transaction
891 // buf := make([]byte, 1400)
892 // readLen, err := c.Connection.Read(buf)
897 // transactions := ReadTransactions(buf[:readLen])
898 // tran, err := FindTransactions(id, transactions)
900 // fmt.Println("returning")
908 //func (c *Client) Read() error {
909 // // Main loop where we wait for and take action on client requests
911 // buf := make([]byte, 1400)
912 // readLen, err := c.Connection.Read(buf)
916 // transactions, _, _ := readTransactions(buf[:readLen])
918 // for _, t := range transactions {
919 // c.HandleTransaction(&t)
926 func (c *Client) Send(t Transaction) error {
927 requestNum := binary.BigEndian.Uint16(t.Type)
928 tID := binary.BigEndian.Uint32(t.ID)
930 //handler := TransactionHandlers[requestNum]
932 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
934 c.activeTasks[tID] = &t
939 if n, err = c.Connection.Write(t.Payload()); err != nil {
942 c.Logger.Debugw("Sent Transaction",
943 "IsReply", t.IsReply,
950 func (c *Client) HandleTransaction(t *Transaction) error {
951 var origT Transaction
953 requestID := binary.BigEndian.Uint32(t.ID)
954 origT = *c.activeTasks[requestID]
958 requestNum := binary.BigEndian.Uint16(t.Type)
960 "Received Transaction",
961 "RequestType", requestNum,
964 if handler, ok := c.Handlers[requestNum]; ok {
965 outT, _ := handler.Handle(c, t)
966 for _, t := range outT {
971 "Unimplemented transaction type received",
972 "RequestID", requestNum,
973 "TransactionID", t.ID,
980 func (c *Client) Connected() bool {
981 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
982 // c.Agreed == true &&
983 if c.UserAccess != nil {
989 func (c *Client) Disconnect() error {
990 err := c.Connection.Close()