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 err := ui.HLClient.Send(
414 *NewTransaction(tranOldPostNews, nil,
415 NewField(fieldData, []byte(newsPostTextArea.GetText(true))),
419 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
420 // TODO: display errModal to user
422 //newsInput.SetText("") // clear the input field after chat send
423 ui.Pages.RemovePage("newsInput")
430 SetDirection(tview.FlexRow).
432 SetTitle("News Post")
434 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
435 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
437 case tcell.KeyEscape:
438 ui.Pages.RemovePage("newsInput")
440 ui.App.SetFocus(newsPostForm)
442 fmt.Fprintf(newsPostTextArea, "\n")
444 switch event.Rune() {
446 curTxt := newsPostTextArea.GetText(true)
448 curTxt = curTxt[:len(curTxt)-1]
449 newsPostTextArea.SetText(curTxt)
452 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
459 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
460 newsFlex.AddItem(newsPostForm, 3, 0, false)
462 newsPostPage := tview.NewFlex().
463 AddItem(nil, 0, 1, false).
464 AddItem(tview.NewFlex().
465 SetDirection(tview.FlexRow).
466 AddItem(nil, 0, 1, false).
467 AddItem(newsFlex, 15, 1, true).
468 //AddItem(newsPostForm, 3, 0, false).
469 AddItem(nil, 0, 1, false), 40, 1, false).
470 AddItem(nil, 0, 1, false)
472 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
473 ui.App.SetFocus(newsPostTextArea)
481 func (ui *UI) Start() {
482 home := tview.NewFlex().SetDirection(tview.FlexRow)
483 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
484 mainMenu := tview.NewList()
486 bannerItem := tview.NewTextView().
487 SetText(randomBanner()).
488 SetDynamicColors(true).
489 SetTextAlign(tview.AlignCenter)
492 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
494 home.AddItem(tview.NewFlex().
495 AddItem(nil, 0, 1, false).
496 AddItem(mainMenu, 0, 1, true).
497 AddItem(nil, 0, 1, false),
501 mainMenu.AddItem("Join Server", "", 'j', func() {
502 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
503 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
505 AddItem("Bookmarks", "", 'b', func() {
506 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
508 AddItem("Browse Tracker", "", 't', func() {
509 ui.trackerList = ui.getTrackerList()
510 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
512 AddItem("Settings", "", 's', func() {
513 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
515 AddItem("Quit", "", 'q', func() {
519 ui.Pages.AddPage("home", home, true, true)
521 // App level input capture
522 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
523 if event.Key() == tcell.KeyCtrlC {
524 ui.HLClient.Logger.Infow("Exiting")
529 if event.Key() == tcell.KeyCtrlL {
530 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
531 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
532 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
533 if key == tcell.KeyEscape {
534 ui.Pages.RemovePage("logs")
538 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
543 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
549 func NewClient(username string, logger *zap.SugaredLogger) *Client {
552 activeTasks: make(map[uint32]*Transaction),
553 Handlers: clientHandlers,
557 prefs, err := readConfig(clientConfigPath)
566 type clientTransaction struct {
568 Handler func(*Client, *Transaction) ([]Transaction, error)
571 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
572 return ch.Handler(cc, t)
575 type clientTHandler interface {
576 Handle(*Client, *Transaction) ([]Transaction, error)
579 type mockClientHandler struct {
583 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
584 args := mh.Called(cc, t)
585 return args.Get(0).([]Transaction), args.Error(1)
588 var clientHandlers = map[uint16]clientTHandler{
590 tranChatMsg: clientTransaction{
592 Handler: handleClientChatMsg,
594 tranLogin: clientTransaction{
596 Handler: handleClientTranLogin,
598 tranShowAgreement: clientTransaction{
599 Name: "tranShowAgreement",
600 Handler: handleClientTranShowAgreement,
602 tranUserAccess: clientTransaction{
603 Name: "tranUserAccess",
604 Handler: handleClientTranUserAccess,
606 tranGetUserNameList: clientTransaction{
607 Name: "tranGetUserNameList",
608 Handler: handleClientGetUserNameList,
610 tranNotifyChangeUser: clientTransaction{
611 Name: "tranNotifyChangeUser",
612 Handler: handleNotifyChangeUser,
614 tranNotifyDeleteUser: clientTransaction{
615 Name: "tranNotifyDeleteUser",
616 Handler: handleNotifyDeleteUser,
618 tranGetMsgs: clientTransaction{
619 Name: "tranNotifyDeleteUser",
620 Handler: handleGetMsgs,
624 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
625 newsText := string(t.GetField(fieldData).Data)
626 newsText = strings.ReplaceAll(newsText, "\r", "\n")
628 newsTextView := tview.NewTextView().
630 SetDoneFunc(func(key tcell.Key) {
631 c.UI.Pages.SwitchToPage("serverUI")
632 c.UI.App.SetFocus(c.UI.chatInput)
634 newsTextView.SetBorder(true).SetTitle("News")
636 c.UI.Pages.AddPage("news", newsTextView, true, true)
637 c.UI.Pages.SwitchToPage("news")
638 c.UI.App.SetFocus(newsTextView)
645 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
647 ID: t.GetField(fieldUserID).Data,
648 Name: string(t.GetField(fieldUserName).Data),
649 Icon: t.GetField(fieldUserIconID).Data,
650 Flags: t.GetField(fieldUserFlags).Data,
654 // user is new to the server
655 // user is already on the server but has a new name
658 var newUserList []User
660 for _, u := range c.UserList {
661 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
662 if bytes.Equal(newUser.ID, u.ID) {
664 u.Name = newUser.Name
665 if u.Name != newUser.Name {
666 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
670 newUserList = append(newUserList, u)
674 newUserList = append(newUserList, newUser)
677 c.UserList = newUserList
684 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
685 exitUser := t.GetField(fieldUserID).Data
687 var newUserList []User
688 for _, u := range c.UserList {
689 if !bytes.Equal(exitUser, u.ID) {
690 newUserList = append(newUserList, u)
694 c.UserList = newUserList
701 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
703 func (c *Client) ReadLoop() error {
704 tranBuff := make([]byte, 0)
706 // Infinite loop where take action on incoming client requests until the connection is closed
708 buf := make([]byte, readBuffSize)
709 tranBuff = tranBuff[tReadlen:]
711 readLen, err := c.Connection.Read(buf)
715 tranBuff = append(tranBuff, buf[:readLen]...)
717 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
718 // into a slice of transactions
719 var transactions []Transaction
720 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
721 c.Logger.Errorw("Error handling transaction", "err", err)
724 // iterate over all of the transactions that were parsed from the byte slice and handle them
725 for _, t := range transactions {
726 if err := c.HandleTransaction(&t); err != nil {
727 c.Logger.Errorw("Error handling transaction", "err", err)
733 func (c *Client) GetTransactions() error {
734 tranBuff := make([]byte, 0)
737 buf := make([]byte, readBuffSize)
738 tranBuff = tranBuff[tReadlen:]
740 readLen, err := c.Connection.Read(buf)
744 tranBuff = append(tranBuff, buf[:readLen]...)
749 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
751 for _, field := range t.Fields {
752 u, _ := ReadUser(field.Data)
753 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
754 //if flagBitmap.Bit(userFlagAdmin) == 1 {
755 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
757 // fmt.Fprintf(UserList, "%s\n", u.Name)
760 users = append(users, *u)
769 func (c *Client) renderUserList() {
770 c.UI.userList.Clear()
771 for _, u := range c.UserList {
772 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
773 if flagBitmap.Bit(userFlagAdmin) == 1 {
774 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
776 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
781 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
782 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
787 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
788 c.UserAccess = t.GetField(fieldUserAccess).Data
793 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
794 agreement := string(t.GetField(fieldData).Data)
795 agreement = strings.ReplaceAll(agreement, "\r", "\n")
797 c.UI.agreeModal = tview.NewModal().
799 AddButtons([]string{"Agree", "Disagree"}).
800 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
801 if buttonIndex == 0 {
805 NewField(fieldUserName, []byte(c.pref.Username)),
806 NewField(fieldUserIconID, c.pref.IconBytes()),
807 NewField(fieldUserFlags, []byte{0x00, 0x00}),
808 NewField(fieldOptions, []byte{0x00, 0x00}),
812 c.UI.Pages.HidePage("agreement")
813 c.UI.App.SetFocus(c.UI.chatInput)
816 c.UI.Pages.SwitchToPage("home")
821 c.Logger.Debug("show agreement page")
822 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
824 c.UI.Pages.ShowPage("agreement ")
830 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
831 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
832 errMsg := string(t.GetField(fieldError).Data)
833 errModal := tview.NewModal()
834 errModal.SetText(errMsg)
835 errModal.AddButtons([]string{"Oh no"})
836 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
837 c.UI.Pages.RemovePage("errModal")
839 c.UI.Pages.RemovePage("joinServer")
840 c.UI.Pages.AddPage("errModal", errModal, false, true)
842 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
844 c.Logger.Error(string(t.GetField(fieldError).Data))
845 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
847 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
848 c.UI.App.SetFocus(c.UI.chatInput)
850 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
851 c.Logger.Errorw("err", "err", err)
856 // JoinServer connects to a Hotline server and completes the login flow
857 func (c *Client) JoinServer(address, login, passwd string) error {
858 // Establish TCP connection to server
859 if err := c.connect(address); err != nil {
863 // Send handshake sequence
864 if err := c.Handshake(); err != nil {
868 // Authenticate (send tranLogin 107)
869 if err := c.LogIn(login, passwd); err != nil {
876 // connect establishes a connection with a Server by sending handshake sequence
877 func (c *Client) connect(address string) error {
879 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
886 var ClientHandshake = []byte{
887 0x54, 0x52, 0x54, 0x50, // TRTP
888 0x48, 0x4f, 0x54, 0x4c, // HOTL
893 var ServerHandshake = []byte{
894 0x54, 0x52, 0x54, 0x50, // TRTP
895 0x00, 0x00, 0x00, 0x00, // ErrorCode
898 func (c *Client) Handshake() error {
899 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
900 //Sub-protocol ID 4 User defined
901 //Version 2 1 Currently 1
902 //Sub-version 2 User defined
903 if _, err := c.Connection.Write(ClientHandshake); err != nil {
904 return fmt.Errorf("handshake write err: %s", err)
907 replyBuf := make([]byte, 8)
908 _, err := c.Connection.Read(replyBuf)
913 //spew.Dump(replyBuf)
914 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
917 // In the case of an error, client and server close the connection.
919 return fmt.Errorf("handshake response err: %s", err)
922 func (c *Client) LogIn(login string, password string) error {
926 NewField(fieldUserName, []byte(c.pref.Username)),
927 NewField(fieldUserIconID, c.pref.IconBytes()),
928 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
929 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
930 NewField(fieldVersion, []byte{0, 2}),
935 func (c *Client) Send(t Transaction) error {
936 requestNum := binary.BigEndian.Uint16(t.Type)
937 tID := binary.BigEndian.Uint32(t.ID)
939 //handler := TransactionHandlers[requestNum]
941 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
943 c.activeTasks[tID] = &t
948 if n, err = c.Connection.Write(t.Payload()); err != nil {
951 c.Logger.Debugw("Sent Transaction",
952 "IsReply", t.IsReply,
959 func (c *Client) HandleTransaction(t *Transaction) error {
960 var origT Transaction
962 requestID := binary.BigEndian.Uint32(t.ID)
963 origT = *c.activeTasks[requestID]
967 requestNum := binary.BigEndian.Uint16(t.Type)
969 "Received Transaction",
970 "RequestType", requestNum,
973 if handler, ok := c.Handlers[requestNum]; ok {
974 outT, _ := handler.Handle(c, t)
975 for _, t := range outT {
980 "Unimplemented transaction type received",
981 "RequestID", requestNum,
982 "TransactionID", t.ID,
989 func (c *Client) Connected() bool {
990 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
991 // c.Agreed == true &&
992 if c.UserAccess != nil {
998 func (c *Client) Disconnect() error {
999 err := c.Connection.Close()