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 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
224 if len(usernameInput) == 0 {
225 usernameInput = "unnamed"
227 ui.HLClient.pref.Username = usernameInput
228 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
229 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
230 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
232 out, err := yaml.Marshal(&ui.HLClient.pref)
237 _ = ioutil.WriteFile(clientConfigPath, out, 0666)
238 ui.Pages.RemovePage("settings")
240 settingsForm.SetBorder(true)
241 settingsForm.SetCancelFunc(func() {
242 ui.Pages.RemovePage("settings")
244 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
245 settingsPage.Box.SetBorder(true).SetTitle("Settings")
246 settingsPage.AddItem(settingsForm, 0, 1, true)
248 centerFlex := tview.NewFlex().
249 AddItem(nil, 0, 1, false).
250 AddItem(tview.NewFlex().
251 SetDirection(tview.FlexRow).
252 AddItem(nil, 0, 1, false).
253 AddItem(settingsForm, 15, 1, true).
254 AddItem(nil, 0, 1, false), 40, 1, true).
255 AddItem(nil, 0, 1, false)
260 // DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
261 type DebugBuffer struct {
262 TextView *tview.TextView
265 func (db *DebugBuffer) Write(p []byte) (int, error) {
266 return db.TextView.Write(p)
269 // Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
270 func (db *DebugBuffer) Sync() error {
274 func (ui *UI) joinServer(addr, login, password string) error {
275 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
276 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
280 err := ui.HLClient.ReadLoop()
282 ui.HLClient.Logger.Errorw("read error", "err", err)
288 func (ui *UI) renderJoinServerForm(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
289 joinServerForm := tview.NewForm()
291 AddInputField("Server", server, 0, nil, nil).
292 AddInputField("Login", login, 0, nil, nil).
293 AddPasswordField("Password", password, 0, '*', nil).
294 AddCheckbox("Save", save, func(checked bool) {
295 // TODO: Implement bookmark saving
297 AddButton("Cancel", func() {
298 ui.Pages.SwitchToPage(backPage)
300 AddButton("Connect", func() {
301 err := ui.joinServer(
302 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
303 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
304 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
307 ui.HLClient.Logger.Errorw("login error", "err", err)
308 loginErrModal := tview.NewModal().
309 AddButtons([]string{"Oh no"}).
310 SetText(err.Error()).
311 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
312 ui.Pages.SwitchToPage(backPage)
315 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
319 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
320 // TODO: implement bookmark saving
324 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
325 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
326 if event.Key() == tcell.KeyEscape {
327 ui.Pages.SwitchToPage(backPage)
333 joinServerForm.SetFocus(5)
336 joinServerPage := tview.NewFlex().
337 AddItem(nil, 0, 1, false).
338 AddItem(tview.NewFlex().
339 SetDirection(tview.FlexRow).
340 AddItem(nil, 0, 1, false).
341 AddItem(joinServerForm, 14, 1, true).
342 AddItem(nil, 0, 1, false), 40, 1, true).
343 AddItem(nil, 0, 1, false)
345 return joinServerPage
348 func randomBanner() string {
349 rand.Seed(time.Now().UnixNano())
351 bannerFiles, _ := bannerDir.ReadDir("client/banners")
352 file, _ := bannerDir.ReadFile("client/banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
354 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
357 func (ui *UI) renderServerUI() *tview.Flex {
358 commandList := tview.NewTextView().SetDynamicColors(true)
360 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs\n").
362 SetTitle("Keyboard Shortcuts")
364 modal := tview.NewModal().
365 SetText("Disconnect from the server?").
366 AddButtons([]string{"Cancel", "Exit"}).
368 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
369 if buttonIndex == 1 {
370 _ = ui.HLClient.Disconnect()
371 ui.Pages.SwitchToPage("home")
373 ui.Pages.HidePage("modal")
377 serverUI := tview.NewFlex().
378 AddItem(tview.NewFlex().
379 SetDirection(tview.FlexRow).
380 AddItem(commandList, 4, 0, false).
381 AddItem(ui.chatBox, 0, 8, false).
382 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
383 AddItem(ui.userList, 25, 1, false)
384 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
385 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
386 if event.Key() == tcell.KeyEscape {
387 ui.Pages.AddPage("modal", modal, false, true)
391 if event.Key() == tcell.KeyCtrlN {
392 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
393 ui.HLClient.Logger.Errorw("err", "err", err)
398 if event.Key() == tcell.KeyCtrlP {
400 newsFlex := tview.NewFlex()
402 newsPostTextArea := tview.NewTextView()
403 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
404 newsPostTextArea.SetChangedFunc(func() {
405 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
407 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
409 newsPostForm := tview.NewForm().
410 SetButtonsAlign(tview.AlignRight).
411 AddButton("Post", nil)
412 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
415 ui.App.SetFocus(newsPostTextArea)
417 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
418 err := ui.HLClient.Send(
419 *NewTransaction(tranOldPostNews, nil,
420 NewField(fieldData, []byte(newsText)),
424 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
425 // TODO: display errModal to user
427 //newsInput.SetText("") // clear the input field after chat send
428 ui.Pages.RemovePage("newsInput")
435 SetDirection(tview.FlexRow).
437 SetTitle("News Post")
439 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
440 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
442 case tcell.KeyEscape:
443 ui.Pages.RemovePage("newsInput")
445 ui.App.SetFocus(newsPostForm)
447 fmt.Fprintf(newsPostTextArea, "\n")
449 switch event.Rune() {
450 case 127: // backspace
451 curTxt := newsPostTextArea.GetText(true)
453 curTxt = curTxt[:len(curTxt)-1]
454 newsPostTextArea.SetText(curTxt)
457 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
464 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
465 newsFlex.AddItem(newsPostForm, 3, 0, false)
467 newsPostPage := tview.NewFlex().
468 AddItem(nil, 0, 1, false).
469 AddItem(tview.NewFlex().
470 SetDirection(tview.FlexRow).
471 AddItem(nil, 0, 1, false).
472 AddItem(newsFlex, 15, 1, true).
473 //AddItem(newsPostForm, 3, 0, false).
474 AddItem(nil, 0, 1, false), 40, 1, false).
475 AddItem(nil, 0, 1, false)
477 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
478 ui.App.SetFocus(newsPostTextArea)
486 func (ui *UI) Start() {
487 home := tview.NewFlex().SetDirection(tview.FlexRow)
488 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
489 mainMenu := tview.NewList()
491 bannerItem := tview.NewTextView().
492 SetText(randomBanner()).
493 SetDynamicColors(true).
494 SetTextAlign(tview.AlignCenter)
497 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
499 home.AddItem(tview.NewFlex().
500 AddItem(nil, 0, 1, false).
501 AddItem(mainMenu, 0, 1, true).
502 AddItem(nil, 0, 1, false),
506 mainMenu.AddItem("Join Server", "", 'j', func() {
507 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
508 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
510 AddItem("Bookmarks", "", 'b', func() {
511 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
513 AddItem("Browse Tracker", "", 't', func() {
514 ui.trackerList = ui.getTrackerList()
515 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
517 AddItem("Settings", "", 's', func() {
518 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
520 AddItem("Quit", "", 'q', func() {
524 ui.Pages.AddPage("home", home, true, true)
526 // App level input capture
527 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
528 if event.Key() == tcell.KeyCtrlC {
529 ui.HLClient.Logger.Infow("Exiting")
534 if event.Key() == tcell.KeyCtrlL {
535 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
536 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
537 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
538 if key == tcell.KeyEscape {
539 ui.Pages.RemovePage("logs")
543 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
548 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
554 func NewClient(username string, logger *zap.SugaredLogger) *Client {
557 activeTasks: make(map[uint32]*Transaction),
558 Handlers: clientHandlers,
562 prefs, err := readConfig(clientConfigPath)
571 type clientTransaction struct {
573 Handler func(*Client, *Transaction) ([]Transaction, error)
576 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
577 return ch.Handler(cc, t)
580 type clientTHandler interface {
581 Handle(*Client, *Transaction) ([]Transaction, error)
584 type mockClientHandler struct {
588 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
589 args := mh.Called(cc, t)
590 return args.Get(0).([]Transaction), args.Error(1)
593 var clientHandlers = map[uint16]clientTHandler{
595 tranChatMsg: clientTransaction{
597 Handler: handleClientChatMsg,
599 tranLogin: clientTransaction{
601 Handler: handleClientTranLogin,
603 tranShowAgreement: clientTransaction{
604 Name: "tranShowAgreement",
605 Handler: handleClientTranShowAgreement,
607 tranUserAccess: clientTransaction{
608 Name: "tranUserAccess",
609 Handler: handleClientTranUserAccess,
611 tranGetUserNameList: clientTransaction{
612 Name: "tranGetUserNameList",
613 Handler: handleClientGetUserNameList,
615 tranNotifyChangeUser: clientTransaction{
616 Name: "tranNotifyChangeUser",
617 Handler: handleNotifyChangeUser,
619 tranNotifyDeleteUser: clientTransaction{
620 Name: "tranNotifyDeleteUser",
621 Handler: handleNotifyDeleteUser,
623 tranGetMsgs: clientTransaction{
624 Name: "tranNotifyDeleteUser",
625 Handler: handleGetMsgs,
629 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
630 newsText := string(t.GetField(fieldData).Data)
631 newsText = strings.ReplaceAll(newsText, "\r", "\n")
633 newsTextView := tview.NewTextView().
635 SetDoneFunc(func(key tcell.Key) {
636 c.UI.Pages.SwitchToPage("serverUI")
637 c.UI.App.SetFocus(c.UI.chatInput)
639 newsTextView.SetBorder(true).SetTitle("News")
641 c.UI.Pages.AddPage("news", newsTextView, true, true)
642 c.UI.Pages.SwitchToPage("news")
643 c.UI.App.SetFocus(newsTextView)
650 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
652 ID: t.GetField(fieldUserID).Data,
653 Name: string(t.GetField(fieldUserName).Data),
654 Icon: t.GetField(fieldUserIconID).Data,
655 Flags: t.GetField(fieldUserFlags).Data,
659 // user is new to the server
660 // user is already on the server but has a new name
663 var newUserList []User
665 for _, u := range c.UserList {
666 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
667 if bytes.Equal(newUser.ID, u.ID) {
669 u.Name = newUser.Name
670 if u.Name != newUser.Name {
671 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
675 newUserList = append(newUserList, u)
679 newUserList = append(newUserList, newUser)
682 c.UserList = newUserList
689 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
690 exitUser := t.GetField(fieldUserID).Data
692 var newUserList []User
693 for _, u := range c.UserList {
694 if !bytes.Equal(exitUser, u.ID) {
695 newUserList = append(newUserList, u)
699 c.UserList = newUserList
706 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
708 func (c *Client) ReadLoop() error {
709 tranBuff := make([]byte, 0)
711 // Infinite loop where take action on incoming client requests until the connection is closed
713 buf := make([]byte, readBuffSize)
714 tranBuff = tranBuff[tReadlen:]
716 readLen, err := c.Connection.Read(buf)
720 tranBuff = append(tranBuff, buf[:readLen]...)
722 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
723 // into a slice of transactions
724 var transactions []Transaction
725 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
726 c.Logger.Errorw("Error handling transaction", "err", err)
729 // iterate over all of the transactions that were parsed from the byte slice and handle them
730 for _, t := range transactions {
731 if err := c.HandleTransaction(&t); err != nil {
732 c.Logger.Errorw("Error handling transaction", "err", err)
738 func (c *Client) GetTransactions() error {
739 tranBuff := make([]byte, 0)
742 buf := make([]byte, readBuffSize)
743 tranBuff = tranBuff[tReadlen:]
745 readLen, err := c.Connection.Read(buf)
749 tranBuff = append(tranBuff, buf[:readLen]...)
754 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
756 for _, field := range t.Fields {
757 u, _ := ReadUser(field.Data)
758 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
759 //if flagBitmap.Bit(userFlagAdmin) == 1 {
760 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
762 // fmt.Fprintf(UserList, "%s\n", u.Name)
765 users = append(users, *u)
774 func (c *Client) renderUserList() {
775 c.UI.userList.Clear()
776 for _, u := range c.UserList {
777 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
778 if flagBitmap.Bit(userFlagAdmin) == 1 {
779 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
781 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
786 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
787 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
792 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
793 c.UserAccess = t.GetField(fieldUserAccess).Data
798 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
799 agreement := string(t.GetField(fieldData).Data)
800 agreement = strings.ReplaceAll(agreement, "\r", "\n")
802 c.UI.agreeModal = tview.NewModal().
804 AddButtons([]string{"Agree", "Disagree"}).
805 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
806 if buttonIndex == 0 {
810 NewField(fieldUserName, []byte(c.pref.Username)),
811 NewField(fieldUserIconID, c.pref.IconBytes()),
812 NewField(fieldUserFlags, []byte{0x00, 0x00}),
813 NewField(fieldOptions, []byte{0x00, 0x00}),
817 c.UI.Pages.HidePage("agreement")
818 c.UI.App.SetFocus(c.UI.chatInput)
821 c.UI.Pages.SwitchToPage("home")
826 c.Logger.Debug("show agreement page")
827 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
829 c.UI.Pages.ShowPage("agreement ")
835 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
836 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
837 errMsg := string(t.GetField(fieldError).Data)
838 errModal := tview.NewModal()
839 errModal.SetText(errMsg)
840 errModal.AddButtons([]string{"Oh no"})
841 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
842 c.UI.Pages.RemovePage("errModal")
844 c.UI.Pages.RemovePage("joinServer")
845 c.UI.Pages.AddPage("errModal", errModal, false, true)
847 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
849 c.Logger.Error(string(t.GetField(fieldError).Data))
850 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
852 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
853 c.UI.App.SetFocus(c.UI.chatInput)
855 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
856 c.Logger.Errorw("err", "err", err)
861 // JoinServer connects to a Hotline server and completes the login flow
862 func (c *Client) JoinServer(address, login, passwd string) error {
863 // Establish TCP connection to server
864 if err := c.connect(address); err != nil {
868 // Send handshake sequence
869 if err := c.Handshake(); err != nil {
873 // Authenticate (send tranLogin 107)
874 if err := c.LogIn(login, passwd); err != nil {
881 // connect establishes a connection with a Server by sending handshake sequence
882 func (c *Client) connect(address string) error {
884 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
891 var ClientHandshake = []byte{
892 0x54, 0x52, 0x54, 0x50, // TRTP
893 0x48, 0x4f, 0x54, 0x4c, // HOTL
898 var ServerHandshake = []byte{
899 0x54, 0x52, 0x54, 0x50, // TRTP
900 0x00, 0x00, 0x00, 0x00, // ErrorCode
903 func (c *Client) Handshake() error {
904 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
905 //Sub-protocol ID 4 User defined
906 //Version 2 1 Currently 1
907 //Sub-version 2 User defined
908 if _, err := c.Connection.Write(ClientHandshake); err != nil {
909 return fmt.Errorf("handshake write err: %s", err)
912 replyBuf := make([]byte, 8)
913 _, err := c.Connection.Read(replyBuf)
918 //spew.Dump(replyBuf)
919 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
922 // In the case of an error, client and server close the connection.
924 return fmt.Errorf("handshake response err: %s", err)
927 func (c *Client) LogIn(login string, password string) error {
931 NewField(fieldUserName, []byte(c.pref.Username)),
932 NewField(fieldUserIconID, c.pref.IconBytes()),
933 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
934 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
935 NewField(fieldVersion, []byte{0, 2}),
940 func (c *Client) Send(t Transaction) error {
941 requestNum := binary.BigEndian.Uint16(t.Type)
942 tID := binary.BigEndian.Uint32(t.ID)
944 //handler := TransactionHandlers[requestNum]
946 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
948 c.activeTasks[tID] = &t
953 if n, err = c.Connection.Write(t.Payload()); err != nil {
956 c.Logger.Debugw("Sent Transaction",
957 "IsReply", t.IsReply,
964 func (c *Client) HandleTransaction(t *Transaction) error {
965 var origT Transaction
967 requestID := binary.BigEndian.Uint32(t.ID)
968 origT = *c.activeTasks[requestID]
972 requestNum := binary.BigEndian.Uint16(t.Type)
974 "Received Transaction",
975 "RequestType", requestNum,
978 if handler, ok := c.Handlers[requestNum]; ok {
979 outT, _ := handler.Handle(c, t)
980 for _, t := range outT {
985 "Unimplemented transaction type received",
986 "RequestID", requestNum,
987 "TransactionID", t.ID,
994 func (c *Client) Connected() bool {
995 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
996 // c.Agreed == true &&
997 if c.UserAccess != nil {
1003 func (c *Client) Disconnect() error {
1004 err := c.Connection.Close()