9 "github.com/davecgh/go-spew/spew"
10 "github.com/gdamore/tcell/v2"
11 "github.com/rivo/tview"
12 "github.com/stretchr/testify/mock"
26 trackerListPage = "trackerList"
29 //go:embed banners/*.txt
30 var bannerDir embed.FS
32 type Bookmark struct {
33 Name string `yaml:"Name"`
34 Addr string `yaml:"Addr"`
35 Login string `yaml:"Login"`
36 Password string `yaml:"Password"`
39 type ClientPrefs struct {
40 Username string `yaml:"Username"`
41 IconID int `yaml:"IconID"`
42 Bookmarks []Bookmark `yaml:"Bookmarks"`
43 Tracker string `yaml:"Tracker"`
46 func (cp *ClientPrefs) IconBytes() []byte {
47 iconBytes := make([]byte, 2)
48 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
52 func readConfig(cfgPath string) (*ClientPrefs, error) {
53 fh, err := os.Open(cfgPath)
58 prefs := ClientPrefs{}
59 decoder := yaml.NewDecoder(fh)
60 decoder.SetStrict(true)
61 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 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
239 println(ui.HLClient.cfgPath)
242 ui.Pages.RemovePage("settings")
244 settingsForm.SetBorder(true)
245 settingsForm.SetCancelFunc(func() {
246 ui.Pages.RemovePage("settings")
248 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
249 settingsPage.Box.SetBorder(true).SetTitle("Settings")
250 settingsPage.AddItem(settingsForm, 0, 1, true)
252 centerFlex := tview.NewFlex().
253 AddItem(nil, 0, 1, false).
254 AddItem(tview.NewFlex().
255 SetDirection(tview.FlexRow).
256 AddItem(nil, 0, 1, false).
257 AddItem(settingsForm, 15, 1, true).
258 AddItem(nil, 0, 1, false), 40, 1, true).
259 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 {
293 joinServerForm := tview.NewForm()
295 AddInputField("Server", server, 0, nil, nil).
296 AddInputField("Login", login, 0, nil, nil).
297 AddPasswordField("Password", password, 0, '*', nil).
298 AddCheckbox("Save", save, func(checked bool) {
299 // TODO: Implement bookmark saving
301 AddButton("Cancel", func() {
302 ui.Pages.SwitchToPage(backPage)
304 AddButton("Connect", func() {
305 err := ui.joinServer(
306 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
307 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
308 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
311 ui.HLClient.Logger.Errorw("login error", "err", err)
312 loginErrModal := tview.NewModal().
313 AddButtons([]string{"Oh no"}).
314 SetText(err.Error()).
315 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
316 ui.Pages.SwitchToPage(backPage)
319 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
323 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
324 // TODO: implement bookmark saving
328 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
329 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
330 if event.Key() == tcell.KeyEscape {
331 ui.Pages.SwitchToPage(backPage)
337 joinServerForm.SetFocus(5)
340 joinServerPage := tview.NewFlex().
341 AddItem(nil, 0, 1, false).
342 AddItem(tview.NewFlex().
343 SetDirection(tview.FlexRow).
344 AddItem(nil, 0, 1, false).
345 AddItem(joinServerForm, 14, 1, true).
346 AddItem(nil, 0, 1, false), 40, 1, true).
347 AddItem(nil, 0, 1, false)
349 return joinServerPage
352 func randomBanner() string {
353 rand.Seed(time.Now().UnixNano())
355 bannerFiles, _ := bannerDir.ReadDir("banners")
356 file, _ := bannerDir.ReadFile("banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
358 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
361 func (ui *UI) renderServerUI() *tview.Flex {
362 commandList := tview.NewTextView().SetDynamicColors(true)
364 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs\n").
366 SetTitle("Keyboard Shortcuts")
368 modal := tview.NewModal().
369 SetText("Disconnect from the server?").
370 AddButtons([]string{"Cancel", "Exit"}).
372 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
373 if buttonIndex == 1 {
374 _ = ui.HLClient.Disconnect()
375 ui.Pages.SwitchToPage("home")
377 ui.Pages.HidePage("modal")
381 serverUI := tview.NewFlex().
382 AddItem(tview.NewFlex().
383 SetDirection(tview.FlexRow).
384 AddItem(commandList, 4, 0, false).
385 AddItem(ui.chatBox, 0, 8, false).
386 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
387 AddItem(ui.userList, 25, 1, false)
388 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
389 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
390 if event.Key() == tcell.KeyEscape {
391 ui.Pages.AddPage("modal", modal, false, true)
395 if event.Key() == tcell.KeyCtrlN {
396 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
397 ui.HLClient.Logger.Errorw("err", "err", err)
402 if event.Key() == tcell.KeyCtrlP {
404 newsFlex := tview.NewFlex()
406 newsPostTextArea := tview.NewTextView()
407 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
408 newsPostTextArea.SetChangedFunc(func() {
409 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
411 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
413 newsPostForm := tview.NewForm().
414 SetButtonsAlign(tview.AlignRight).
415 AddButton("Post", nil)
416 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
419 ui.App.SetFocus(newsPostTextArea)
421 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
422 err := ui.HLClient.Send(
423 *NewTransaction(tranOldPostNews, nil,
424 NewField(fieldData, []byte(newsText)),
428 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
429 // TODO: display errModal to user
431 //newsInput.SetText("") // clear the input field after chat send
432 ui.Pages.RemovePage("newsInput")
439 SetDirection(tview.FlexRow).
441 SetTitle("News Post")
443 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
444 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
446 case tcell.KeyEscape:
447 ui.Pages.RemovePage("newsInput")
449 ui.App.SetFocus(newsPostForm)
451 fmt.Fprintf(newsPostTextArea, "\n")
453 switch event.Rune() {
454 case 127: // backspace
455 curTxt := newsPostTextArea.GetText(true)
457 curTxt = curTxt[:len(curTxt)-1]
458 newsPostTextArea.SetText(curTxt)
461 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
468 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
469 newsFlex.AddItem(newsPostForm, 3, 0, false)
471 newsPostPage := tview.NewFlex().
472 AddItem(nil, 0, 1, false).
473 AddItem(tview.NewFlex().
474 SetDirection(tview.FlexRow).
475 AddItem(nil, 0, 1, false).
476 AddItem(newsFlex, 15, 1, true).
477 //AddItem(newsPostForm, 3, 0, false).
478 AddItem(nil, 0, 1, false), 40, 1, false).
479 AddItem(nil, 0, 1, false)
481 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
482 ui.App.SetFocus(newsPostTextArea)
490 func (ui *UI) Start() {
491 home := tview.NewFlex().SetDirection(tview.FlexRow)
492 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
493 mainMenu := tview.NewList()
495 bannerItem := tview.NewTextView().
496 SetText(randomBanner()).
497 SetDynamicColors(true).
498 SetTextAlign(tview.AlignCenter)
501 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
503 home.AddItem(tview.NewFlex().
504 AddItem(nil, 0, 1, false).
505 AddItem(mainMenu, 0, 1, true).
506 AddItem(nil, 0, 1, false),
510 mainMenu.AddItem("Join Server", "", 'j', func() {
511 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
512 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
514 AddItem("Bookmarks", "", 'b', func() {
515 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
517 AddItem("Browse Tracker", "", 't', func() {
518 ui.trackerList = ui.getTrackerList()
519 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
521 AddItem("Settings", "", 's', func() {
522 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
524 AddItem("Quit", "", 'q', func() {
528 ui.Pages.AddPage("home", home, true, true)
530 // App level input capture
531 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
532 if event.Key() == tcell.KeyCtrlC {
533 ui.HLClient.Logger.Infow("Exiting")
538 if event.Key() == tcell.KeyCtrlL {
539 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
540 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
541 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
542 if key == tcell.KeyEscape {
543 ui.Pages.RemovePage("logs")
547 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
552 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
558 func NewClient(cfgPath string, logger *zap.SugaredLogger) *Client {
562 activeTasks: make(map[uint32]*Transaction),
563 Handlers: clientHandlers,
567 prefs, err := readConfig(cfgPath)
569 fmt.Printf("unable to read config file")
570 logger.Fatal("unable to read config file", "path", cfgPath)
577 type clientTransaction struct {
579 Handler func(*Client, *Transaction) ([]Transaction, error)
582 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
583 return ch.Handler(cc, t)
586 type clientTHandler interface {
587 Handle(*Client, *Transaction) ([]Transaction, error)
590 type mockClientHandler struct {
594 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
595 args := mh.Called(cc, t)
596 return args.Get(0).([]Transaction), args.Error(1)
599 var clientHandlers = map[uint16]clientTHandler{
601 tranChatMsg: clientTransaction{
603 Handler: handleClientChatMsg,
605 tranLogin: clientTransaction{
607 Handler: handleClientTranLogin,
609 tranShowAgreement: clientTransaction{
610 Name: "tranShowAgreement",
611 Handler: handleClientTranShowAgreement,
613 tranUserAccess: clientTransaction{
614 Name: "tranUserAccess",
615 Handler: handleClientTranUserAccess,
617 tranGetUserNameList: clientTransaction{
618 Name: "tranGetUserNameList",
619 Handler: handleClientGetUserNameList,
621 tranNotifyChangeUser: clientTransaction{
622 Name: "tranNotifyChangeUser",
623 Handler: handleNotifyChangeUser,
625 tranNotifyDeleteUser: clientTransaction{
626 Name: "tranNotifyDeleteUser",
627 Handler: handleNotifyDeleteUser,
629 tranGetMsgs: clientTransaction{
630 Name: "tranNotifyDeleteUser",
631 Handler: handleGetMsgs,
635 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
636 newsText := string(t.GetField(fieldData).Data)
637 newsText = strings.ReplaceAll(newsText, "\r", "\n")
639 newsTextView := tview.NewTextView().
641 SetDoneFunc(func(key tcell.Key) {
642 c.UI.Pages.SwitchToPage("serverUI")
643 c.UI.App.SetFocus(c.UI.chatInput)
645 newsTextView.SetBorder(true).SetTitle("News")
647 c.UI.Pages.AddPage("news", newsTextView, true, true)
648 c.UI.Pages.SwitchToPage("news")
649 c.UI.App.SetFocus(newsTextView)
656 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
658 ID: t.GetField(fieldUserID).Data,
659 Name: string(t.GetField(fieldUserName).Data),
660 Icon: t.GetField(fieldUserIconID).Data,
661 Flags: t.GetField(fieldUserFlags).Data,
665 // user is new to the server
666 // user is already on the server but has a new name
669 var newUserList []User
671 for _, u := range c.UserList {
672 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
673 if bytes.Equal(newUser.ID, u.ID) {
675 u.Name = newUser.Name
676 if u.Name != newUser.Name {
677 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
681 newUserList = append(newUserList, u)
685 newUserList = append(newUserList, newUser)
688 c.UserList = newUserList
695 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
696 exitUser := t.GetField(fieldUserID).Data
698 var newUserList []User
699 for _, u := range c.UserList {
700 if !bytes.Equal(exitUser, u.ID) {
701 newUserList = append(newUserList, u)
705 c.UserList = newUserList
712 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
714 func (c *Client) ReadLoop() error {
715 tranBuff := make([]byte, 0)
717 // Infinite loop where take action on incoming client requests until the connection is closed
719 buf := make([]byte, readBuffSize)
720 tranBuff = tranBuff[tReadlen:]
722 readLen, err := c.Connection.Read(buf)
726 tranBuff = append(tranBuff, buf[:readLen]...)
728 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
729 // into a slice of transactions
730 var transactions []Transaction
731 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
732 c.Logger.Errorw("Error handling transaction", "err", err)
735 // iterate over all of the transactions that were parsed from the byte slice and handle them
736 for _, t := range transactions {
737 if err := c.HandleTransaction(&t); err != nil {
738 c.Logger.Errorw("Error handling transaction", "err", err)
744 func (c *Client) GetTransactions() error {
745 tranBuff := make([]byte, 0)
748 buf := make([]byte, readBuffSize)
749 tranBuff = tranBuff[tReadlen:]
751 readLen, err := c.Connection.Read(buf)
755 tranBuff = append(tranBuff, buf[:readLen]...)
760 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
762 for _, field := range t.Fields {
763 // The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
764 // fields, but shxd sneaks in fieldChatSubject (115) so it's important to filter explicitly for the expected
765 // field type. Probably a good idea to do everywhere.
766 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
767 u, err := ReadUser(field.Data)
771 users = append(users, *u)
781 func (c *Client) renderUserList() {
782 c.UI.userList.Clear()
783 for _, u := range c.UserList {
784 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
785 if flagBitmap.Bit(userFlagAdmin) == 1 {
786 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
788 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
793 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
794 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
799 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
800 c.UserAccess = t.GetField(fieldUserAccess).Data
805 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
806 agreement := string(t.GetField(fieldData).Data)
807 agreement = strings.ReplaceAll(agreement, "\r", "\n")
809 c.UI.agreeModal = tview.NewModal().
811 AddButtons([]string{"Agree", "Disagree"}).
812 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
813 if buttonIndex == 0 {
817 NewField(fieldUserName, []byte(c.pref.Username)),
818 NewField(fieldUserIconID, c.pref.IconBytes()),
819 NewField(fieldUserFlags, []byte{0x00, 0x00}),
820 NewField(fieldOptions, []byte{0x00, 0x00}),
824 c.UI.Pages.HidePage("agreement")
825 c.UI.App.SetFocus(c.UI.chatInput)
828 c.UI.Pages.SwitchToPage("home")
833 c.Logger.Debug("show agreement page")
834 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
836 c.UI.Pages.ShowPage("agreement ")
842 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
843 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
844 errMsg := string(t.GetField(fieldError).Data)
845 errModal := tview.NewModal()
846 errModal.SetText(errMsg)
847 errModal.AddButtons([]string{"Oh no"})
848 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
849 c.UI.Pages.RemovePage("errModal")
851 c.UI.Pages.RemovePage("joinServer")
852 c.UI.Pages.AddPage("errModal", errModal, false, true)
854 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
856 c.Logger.Error(string(t.GetField(fieldError).Data))
857 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
859 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
860 c.UI.App.SetFocus(c.UI.chatInput)
862 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
863 c.Logger.Errorw("err", "err", err)
868 // JoinServer connects to a Hotline server and completes the login flow
869 func (c *Client) JoinServer(address, login, passwd string) error {
870 // Establish TCP connection to server
871 if err := c.connect(address); err != nil {
875 // Send handshake sequence
876 if err := c.Handshake(); err != nil {
880 // Authenticate (send tranLogin 107)
881 if err := c.LogIn(login, passwd); err != nil {
888 // connect establishes a connection with a Server by sending handshake sequence
889 func (c *Client) connect(address string) error {
891 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
898 var ClientHandshake = []byte{
899 0x54, 0x52, 0x54, 0x50, // TRTP
900 0x48, 0x4f, 0x54, 0x4c, // HOTL
905 var ServerHandshake = []byte{
906 0x54, 0x52, 0x54, 0x50, // TRTP
907 0x00, 0x00, 0x00, 0x00, // ErrorCode
910 func (c *Client) Handshake() error {
911 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
912 //Sub-protocol ID 4 User defined
913 //Version 2 1 Currently 1
914 //Sub-version 2 User defined
915 if _, err := c.Connection.Write(ClientHandshake); err != nil {
916 return fmt.Errorf("handshake write err: %s", err)
919 replyBuf := make([]byte, 8)
920 _, err := c.Connection.Read(replyBuf)
925 //spew.Dump(replyBuf)
926 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
929 // In the case of an error, client and server close the connection.
931 return fmt.Errorf("handshake response err: %s", err)
934 func (c *Client) LogIn(login string, password string) error {
938 NewField(fieldUserName, []byte(c.pref.Username)),
939 NewField(fieldUserIconID, c.pref.IconBytes()),
940 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
941 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
942 NewField(fieldVersion, []byte{0, 2}),
947 func (c *Client) Send(t Transaction) error {
948 requestNum := binary.BigEndian.Uint16(t.Type)
949 tID := binary.BigEndian.Uint32(t.ID)
951 //handler := TransactionHandlers[requestNum]
953 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
955 c.activeTasks[tID] = &t
960 if n, err = c.Connection.Write(t.Payload()); err != nil {
963 c.Logger.Debugw("Sent Transaction",
964 "IsReply", t.IsReply,
971 func (c *Client) HandleTransaction(t *Transaction) error {
972 var origT Transaction
974 requestID := binary.BigEndian.Uint32(t.ID)
975 origT = *c.activeTasks[requestID]
979 requestNum := binary.BigEndian.Uint16(t.Type)
981 "Received Transaction",
982 "RequestType", requestNum,
985 if handler, ok := c.Handlers[requestNum]; ok {
986 outT, _ := handler.Handle(c, t)
987 for _, t := range outT {
992 "Unimplemented transaction type received",
993 "RequestID", requestNum,
994 "TransactionID", t.ID,
1001 func (c *Client) Connected() bool {
1002 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
1003 // c.Agreed == true &&
1004 if c.UserAccess != nil {
1010 func (c *Client) Disconnect() error {
1011 err := c.Connection.Close()