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 [yellow]^p[-::]: Post 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)
394 if event.Key() == tcell.KeyCtrlP {
396 newsFlex := tview.NewFlex()
398 newsPostTextArea := tview.NewTextView()
399 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
400 newsPostTextArea.SetChangedFunc(func() {
401 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
403 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
405 newsPostForm := tview.NewForm().
406 SetButtonsAlign(tview.AlignRight).
407 AddButton("Post", nil)
408 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
411 ui.App.SetFocus(newsPostTextArea)
413 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
414 err := ui.HLClient.Send(
415 *NewTransaction(tranOldPostNews, nil,
416 NewField(fieldData, []byte(newsText)),
420 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
421 // TODO: display errModal to user
423 //newsInput.SetText("") // clear the input field after chat send
424 ui.Pages.RemovePage("newsInput")
431 SetDirection(tview.FlexRow).
433 SetTitle("News Post")
435 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
436 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
438 case tcell.KeyEscape:
439 ui.Pages.RemovePage("newsInput")
441 ui.App.SetFocus(newsPostForm)
443 fmt.Fprintf(newsPostTextArea, "\n")
445 switch event.Rune() {
446 case 127: // backspace
447 curTxt := newsPostTextArea.GetText(true)
449 curTxt = curTxt[:len(curTxt)-1]
450 newsPostTextArea.SetText(curTxt)
453 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
460 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
461 newsFlex.AddItem(newsPostForm, 3, 0, false)
463 newsPostPage := tview.NewFlex().
464 AddItem(nil, 0, 1, false).
465 AddItem(tview.NewFlex().
466 SetDirection(tview.FlexRow).
467 AddItem(nil, 0, 1, false).
468 AddItem(newsFlex, 15, 1, true).
469 //AddItem(newsPostForm, 3, 0, false).
470 AddItem(nil, 0, 1, false), 40, 1, false).
471 AddItem(nil, 0, 1, false)
473 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
474 ui.App.SetFocus(newsPostTextArea)
482 func (ui *UI) Start() {
483 home := tview.NewFlex().SetDirection(tview.FlexRow)
484 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
485 mainMenu := tview.NewList()
487 bannerItem := tview.NewTextView().
488 SetText(randomBanner()).
489 SetDynamicColors(true).
490 SetTextAlign(tview.AlignCenter)
493 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
495 home.AddItem(tview.NewFlex().
496 AddItem(nil, 0, 1, false).
497 AddItem(mainMenu, 0, 1, true).
498 AddItem(nil, 0, 1, false),
502 mainMenu.AddItem("Join Server", "", 'j', func() {
503 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
504 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
506 AddItem("Bookmarks", "", 'b', func() {
507 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
509 AddItem("Browse Tracker", "", 't', func() {
510 ui.trackerList = ui.getTrackerList()
511 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
513 AddItem("Settings", "", 's', func() {
514 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
516 AddItem("Quit", "", 'q', func() {
520 ui.Pages.AddPage("home", home, true, true)
522 // App level input capture
523 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
524 if event.Key() == tcell.KeyCtrlC {
525 ui.HLClient.Logger.Infow("Exiting")
530 if event.Key() == tcell.KeyCtrlL {
531 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
532 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
533 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
534 if key == tcell.KeyEscape {
535 ui.Pages.RemovePage("logs")
539 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
544 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
550 func NewClient(username string, logger *zap.SugaredLogger) *Client {
553 activeTasks: make(map[uint32]*Transaction),
554 Handlers: clientHandlers,
558 prefs, err := readConfig(clientConfigPath)
567 type clientTransaction struct {
569 Handler func(*Client, *Transaction) ([]Transaction, error)
572 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
573 return ch.Handler(cc, t)
576 type clientTHandler interface {
577 Handle(*Client, *Transaction) ([]Transaction, error)
580 type mockClientHandler struct {
584 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
585 args := mh.Called(cc, t)
586 return args.Get(0).([]Transaction), args.Error(1)
589 var clientHandlers = map[uint16]clientTHandler{
591 tranChatMsg: clientTransaction{
593 Handler: handleClientChatMsg,
595 tranLogin: clientTransaction{
597 Handler: handleClientTranLogin,
599 tranShowAgreement: clientTransaction{
600 Name: "tranShowAgreement",
601 Handler: handleClientTranShowAgreement,
603 tranUserAccess: clientTransaction{
604 Name: "tranUserAccess",
605 Handler: handleClientTranUserAccess,
607 tranGetUserNameList: clientTransaction{
608 Name: "tranGetUserNameList",
609 Handler: handleClientGetUserNameList,
611 tranNotifyChangeUser: clientTransaction{
612 Name: "tranNotifyChangeUser",
613 Handler: handleNotifyChangeUser,
615 tranNotifyDeleteUser: clientTransaction{
616 Name: "tranNotifyDeleteUser",
617 Handler: handleNotifyDeleteUser,
619 tranGetMsgs: clientTransaction{
620 Name: "tranNotifyDeleteUser",
621 Handler: handleGetMsgs,
625 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
626 newsText := string(t.GetField(fieldData).Data)
627 newsText = strings.ReplaceAll(newsText, "\r", "\n")
629 newsTextView := tview.NewTextView().
631 SetDoneFunc(func(key tcell.Key) {
632 c.UI.Pages.SwitchToPage("serverUI")
633 c.UI.App.SetFocus(c.UI.chatInput)
635 newsTextView.SetBorder(true).SetTitle("News")
637 c.UI.Pages.AddPage("news", newsTextView, true, true)
638 c.UI.Pages.SwitchToPage("news")
639 c.UI.App.SetFocus(newsTextView)
646 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
648 ID: t.GetField(fieldUserID).Data,
649 Name: string(t.GetField(fieldUserName).Data),
650 Icon: t.GetField(fieldUserIconID).Data,
651 Flags: t.GetField(fieldUserFlags).Data,
655 // user is new to the server
656 // user is already on the server but has a new name
659 var newUserList []User
661 for _, u := range c.UserList {
662 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
663 if bytes.Equal(newUser.ID, u.ID) {
665 u.Name = newUser.Name
666 if u.Name != newUser.Name {
667 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
671 newUserList = append(newUserList, u)
675 newUserList = append(newUserList, newUser)
678 c.UserList = newUserList
685 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
686 exitUser := t.GetField(fieldUserID).Data
688 var newUserList []User
689 for _, u := range c.UserList {
690 if !bytes.Equal(exitUser, u.ID) {
691 newUserList = append(newUserList, u)
695 c.UserList = newUserList
702 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
704 func (c *Client) ReadLoop() error {
705 tranBuff := make([]byte, 0)
707 // Infinite loop where take action on incoming client requests until the connection is closed
709 buf := make([]byte, readBuffSize)
710 tranBuff = tranBuff[tReadlen:]
712 readLen, err := c.Connection.Read(buf)
716 tranBuff = append(tranBuff, buf[:readLen]...)
718 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
719 // into a slice of transactions
720 var transactions []Transaction
721 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
722 c.Logger.Errorw("Error handling transaction", "err", err)
725 // iterate over all of the transactions that were parsed from the byte slice and handle them
726 for _, t := range transactions {
727 if err := c.HandleTransaction(&t); err != nil {
728 c.Logger.Errorw("Error handling transaction", "err", err)
734 func (c *Client) GetTransactions() error {
735 tranBuff := make([]byte, 0)
738 buf := make([]byte, readBuffSize)
739 tranBuff = tranBuff[tReadlen:]
741 readLen, err := c.Connection.Read(buf)
745 tranBuff = append(tranBuff, buf[:readLen]...)
750 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
752 for _, field := range t.Fields {
753 u, _ := ReadUser(field.Data)
754 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
755 //if flagBitmap.Bit(userFlagAdmin) == 1 {
756 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
758 // fmt.Fprintf(UserList, "%s\n", u.Name)
761 users = append(users, *u)
770 func (c *Client) renderUserList() {
771 c.UI.userList.Clear()
772 for _, u := range c.UserList {
773 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
774 if flagBitmap.Bit(userFlagAdmin) == 1 {
775 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
777 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
782 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
783 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
788 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
789 c.UserAccess = t.GetField(fieldUserAccess).Data
794 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
795 agreement := string(t.GetField(fieldData).Data)
796 agreement = strings.ReplaceAll(agreement, "\r", "\n")
798 c.UI.agreeModal = tview.NewModal().
800 AddButtons([]string{"Agree", "Disagree"}).
801 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
802 if buttonIndex == 0 {
806 NewField(fieldUserName, []byte(c.pref.Username)),
807 NewField(fieldUserIconID, c.pref.IconBytes()),
808 NewField(fieldUserFlags, []byte{0x00, 0x00}),
809 NewField(fieldOptions, []byte{0x00, 0x00}),
813 c.UI.Pages.HidePage("agreement")
814 c.UI.App.SetFocus(c.UI.chatInput)
817 c.UI.Pages.SwitchToPage("home")
822 c.Logger.Debug("show agreement page")
823 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
825 c.UI.Pages.ShowPage("agreement ")
831 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
832 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
833 errMsg := string(t.GetField(fieldError).Data)
834 errModal := tview.NewModal()
835 errModal.SetText(errMsg)
836 errModal.AddButtons([]string{"Oh no"})
837 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
838 c.UI.Pages.RemovePage("errModal")
840 c.UI.Pages.RemovePage("joinServer")
841 c.UI.Pages.AddPage("errModal", errModal, false, true)
843 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
845 c.Logger.Error(string(t.GetField(fieldError).Data))
846 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
848 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
849 c.UI.App.SetFocus(c.UI.chatInput)
851 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
852 c.Logger.Errorw("err", "err", err)
857 // JoinServer connects to a Hotline server and completes the login flow
858 func (c *Client) JoinServer(address, login, passwd string) error {
859 // Establish TCP connection to server
860 if err := c.connect(address); err != nil {
864 // Send handshake sequence
865 if err := c.Handshake(); err != nil {
869 // Authenticate (send tranLogin 107)
870 if err := c.LogIn(login, passwd); err != nil {
877 // connect establishes a connection with a Server by sending handshake sequence
878 func (c *Client) connect(address string) error {
880 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
887 var ClientHandshake = []byte{
888 0x54, 0x52, 0x54, 0x50, // TRTP
889 0x48, 0x4f, 0x54, 0x4c, // HOTL
894 var ServerHandshake = []byte{
895 0x54, 0x52, 0x54, 0x50, // TRTP
896 0x00, 0x00, 0x00, 0x00, // ErrorCode
899 func (c *Client) Handshake() error {
900 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
901 //Sub-protocol ID 4 User defined
902 //Version 2 1 Currently 1
903 //Sub-version 2 User defined
904 if _, err := c.Connection.Write(ClientHandshake); err != nil {
905 return fmt.Errorf("handshake write err: %s", err)
908 replyBuf := make([]byte, 8)
909 _, err := c.Connection.Read(replyBuf)
914 //spew.Dump(replyBuf)
915 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
918 // In the case of an error, client and server close the connection.
920 return fmt.Errorf("handshake response err: %s", err)
923 func (c *Client) LogIn(login string, password string) error {
927 NewField(fieldUserName, []byte(c.pref.Username)),
928 NewField(fieldUserIconID, c.pref.IconBytes()),
929 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
930 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
931 NewField(fieldVersion, []byte{0, 2}),
936 func (c *Client) Send(t Transaction) error {
937 requestNum := binary.BigEndian.Uint16(t.Type)
938 tID := binary.BigEndian.Uint32(t.ID)
940 //handler := TransactionHandlers[requestNum]
942 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
944 c.activeTasks[tID] = &t
949 if n, err = c.Connection.Write(t.Payload()); err != nil {
952 c.Logger.Debugw("Sent Transaction",
953 "IsReply", t.IsReply,
960 func (c *Client) HandleTransaction(t *Transaction) error {
961 var origT Transaction
963 requestID := binary.BigEndian.Uint32(t.ID)
964 origT = *c.activeTasks[requestID]
968 requestNum := binary.BigEndian.Uint16(t.Type)
970 "Received Transaction",
971 "RequestType", requestNum,
974 if handler, ok := c.Handlers[requestNum]; ok {
975 outT, _ := handler.Handle(c, t)
976 for _, t := range outT {
981 "Unimplemented transaction type received",
982 "RequestID", requestNum,
983 "TransactionID", t.ID,
990 func (c *Client) Connected() bool {
991 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
992 // c.Agreed == true &&
993 if c.UserAccess != nil {
999 func (c *Client) Disconnect() error {
1000 err := c.Connection.Close()