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 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 // The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
758 // fields, but shxd sneaks in fieldChatSubject (115) so it's important to filter explicitly for the expected
759 // field type. Probably a good idea to do everywhere.
760 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
761 u, err := ReadUser(field.Data)
765 users = append(users, *u)
775 func (c *Client) renderUserList() {
776 c.UI.userList.Clear()
777 for _, u := range c.UserList {
778 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
779 if flagBitmap.Bit(userFlagAdmin) == 1 {
780 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
782 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
787 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
788 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
793 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
794 c.UserAccess = t.GetField(fieldUserAccess).Data
799 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
800 agreement := string(t.GetField(fieldData).Data)
801 agreement = strings.ReplaceAll(agreement, "\r", "\n")
803 c.UI.agreeModal = tview.NewModal().
805 AddButtons([]string{"Agree", "Disagree"}).
806 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
807 if buttonIndex == 0 {
811 NewField(fieldUserName, []byte(c.pref.Username)),
812 NewField(fieldUserIconID, c.pref.IconBytes()),
813 NewField(fieldUserFlags, []byte{0x00, 0x00}),
814 NewField(fieldOptions, []byte{0x00, 0x00}),
818 c.UI.Pages.HidePage("agreement")
819 c.UI.App.SetFocus(c.UI.chatInput)
822 c.UI.Pages.SwitchToPage("home")
827 c.Logger.Debug("show agreement page")
828 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
830 c.UI.Pages.ShowPage("agreement ")
836 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
837 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
838 errMsg := string(t.GetField(fieldError).Data)
839 errModal := tview.NewModal()
840 errModal.SetText(errMsg)
841 errModal.AddButtons([]string{"Oh no"})
842 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
843 c.UI.Pages.RemovePage("errModal")
845 c.UI.Pages.RemovePage("joinServer")
846 c.UI.Pages.AddPage("errModal", errModal, false, true)
848 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
850 c.Logger.Error(string(t.GetField(fieldError).Data))
851 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
853 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
854 c.UI.App.SetFocus(c.UI.chatInput)
856 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
857 c.Logger.Errorw("err", "err", err)
862 // JoinServer connects to a Hotline server and completes the login flow
863 func (c *Client) JoinServer(address, login, passwd string) error {
864 // Establish TCP connection to server
865 if err := c.connect(address); err != nil {
869 // Send handshake sequence
870 if err := c.Handshake(); err != nil {
874 // Authenticate (send tranLogin 107)
875 if err := c.LogIn(login, passwd); err != nil {
882 // connect establishes a connection with a Server by sending handshake sequence
883 func (c *Client) connect(address string) error {
885 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
892 var ClientHandshake = []byte{
893 0x54, 0x52, 0x54, 0x50, // TRTP
894 0x48, 0x4f, 0x54, 0x4c, // HOTL
899 var ServerHandshake = []byte{
900 0x54, 0x52, 0x54, 0x50, // TRTP
901 0x00, 0x00, 0x00, 0x00, // ErrorCode
904 func (c *Client) Handshake() error {
905 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
906 //Sub-protocol ID 4 User defined
907 //Version 2 1 Currently 1
908 //Sub-version 2 User defined
909 if _, err := c.Connection.Write(ClientHandshake); err != nil {
910 return fmt.Errorf("handshake write err: %s", err)
913 replyBuf := make([]byte, 8)
914 _, err := c.Connection.Read(replyBuf)
919 //spew.Dump(replyBuf)
920 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
923 // In the case of an error, client and server close the connection.
925 return fmt.Errorf("handshake response err: %s", err)
928 func (c *Client) LogIn(login string, password string) error {
932 NewField(fieldUserName, []byte(c.pref.Username)),
933 NewField(fieldUserIconID, c.pref.IconBytes()),
934 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
935 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
936 NewField(fieldVersion, []byte{0, 2}),
941 func (c *Client) Send(t Transaction) error {
942 requestNum := binary.BigEndian.Uint16(t.Type)
943 tID := binary.BigEndian.Uint32(t.ID)
945 //handler := TransactionHandlers[requestNum]
947 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
949 c.activeTasks[tID] = &t
954 if n, err = c.Connection.Write(t.Payload()); err != nil {
957 c.Logger.Debugw("Sent Transaction",
958 "IsReply", t.IsReply,
965 func (c *Client) HandleTransaction(t *Transaction) error {
966 var origT Transaction
968 requestID := binary.BigEndian.Uint32(t.ID)
969 origT = *c.activeTasks[requestID]
973 requestNum := binary.BigEndian.Uint16(t.Type)
975 "Received Transaction",
976 "RequestType", requestNum,
979 if handler, ok := c.Handlers[requestNum]; ok {
980 outT, _ := handler.Handle(c, t)
981 for _, t := range outT {
986 "Unimplemented transaction type received",
987 "RequestID", requestNum,
988 "TransactionID", t.ID,
995 func (c *Client) Connected() bool {
996 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
997 // c.Agreed == true &&
998 if c.UserAccess != nil {
1004 func (c *Client) Disconnect() error {
1005 err := c.Connection.Close()