]> git.r.bdr.sh - rbdr/mobius/blob - client.go
FIx icon ID
[rbdr/mobius] / client.go
1 package hotline
2
3 import (
4 "bytes"
5 "embed"
6 "encoding/binary"
7 "errors"
8 "fmt"
9 "github.com/davecgh/go-spew/spew"
10 "github.com/gdamore/tcell/v2"
11 "github.com/rivo/tview"
12 "github.com/stretchr/testify/mock"
13 "go.uber.org/zap"
14 "gopkg.in/yaml.v2"
15 "io/ioutil"
16 "math/big"
17 "math/rand"
18 "net"
19 "os"
20 "strconv"
21 "strings"
22 "time"
23 )
24
25 const clientConfigPath = "/usr/local/etc/mobius-client-config.yaml"
26 const (
27 trackerListPage = "trackerList"
28 )
29
30 //go:embed client/banners/*.txt
31 var bannerDir embed.FS
32
33 type Bookmark struct {
34 Name string `yaml:"Name"`
35 Addr string `yaml:"Addr"`
36 Login string `yaml:"Login"`
37 Password string `yaml:"Password"`
38 }
39
40 type ClientPrefs struct {
41 Username string `yaml:"Username"`
42 IconID int `yaml:"IconID"`
43 Bookmarks []Bookmark `yaml:"Bookmarks"`
44 Tracker string `yaml:"Tracker"`
45 }
46
47 func (cp *ClientPrefs) IconBytes() []byte {
48 iconBytes := make([]byte, 2)
49 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
50 return iconBytes
51 }
52
53 func readConfig(cfgPath string) (*ClientPrefs, error) {
54 fh, err := os.Open(cfgPath)
55 if err != nil {
56 return nil, err
57 }
58
59 prefs := ClientPrefs{}
60 decoder := yaml.NewDecoder(fh)
61 decoder.SetStrict(true)
62 if err := decoder.Decode(&prefs); err != nil {
63 return nil, err
64 }
65 return &prefs, nil
66 }
67
68 type Client struct {
69 DebugBuf *DebugBuffer
70 Connection net.Conn
71 UserName []byte
72 Login *[]byte
73 Password *[]byte
74 Icon *[]byte
75 Flags *[]byte
76 ID *[]byte
77 Version []byte
78 UserAccess []byte
79 Agreed bool
80 UserList []User
81 Logger *zap.SugaredLogger
82 activeTasks map[uint32]*Transaction
83
84 pref *ClientPrefs
85
86 Handlers map[uint16]clientTHandler
87
88 UI *UI
89
90 outbox chan *Transaction
91 Inbox chan *Transaction
92 }
93
94 type UI struct {
95 chatBox *tview.TextView
96 chatInput *tview.InputField
97 App *tview.Application
98 Pages *tview.Pages
99 userList *tview.TextView
100 agreeModal *tview.Modal
101 trackerList *tview.List
102 settingsPage *tview.Box
103 HLClient *Client
104 }
105
106 func NewUI(c *Client) *UI {
107 app := tview.NewApplication()
108 chatBox := tview.NewTextView().
109 SetScrollable(true).
110 SetDynamicColors(true).
111 SetWordWrap(true).
112 SetChangedFunc(func() {
113 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
114 })
115 chatBox.Box.SetBorder(true).SetTitle("Chat")
116
117 chatInput := tview.NewInputField()
118 chatInput.
119 SetLabel("> ").
120 SetFieldBackgroundColor(tcell.ColorDimGray).
121 SetDoneFunc(func(key tcell.Key) {
122 // skip send if user hit enter with no other text
123 if len(chatInput.GetText()) == 0 {
124 return
125 }
126
127 c.Send(
128 *NewTransaction(tranChatSend, nil,
129 NewField(fieldData, []byte(chatInput.GetText())),
130 ),
131 )
132 chatInput.SetText("") // clear the input field after chat send
133 })
134
135 chatInput.Box.SetBorder(true).SetTitle("Send")
136
137 userList := tview.
138 NewTextView().
139 SetDynamicColors(true).
140 SetChangedFunc(func() {
141 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
142 })
143 userList.Box.SetBorder(true).SetTitle("Users")
144
145 return &UI{
146 App: app,
147 chatBox: chatBox,
148 Pages: tview.NewPages(),
149 chatInput: chatInput,
150 userList: userList,
151 trackerList: tview.NewList(),
152 agreeModal: tview.NewModal(),
153 HLClient: c,
154 }
155 }
156
157 func (ui *UI) showBookmarks() *tview.List {
158 list := tview.NewList()
159 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
160 if event.Key() == tcell.KeyEsc {
161 ui.Pages.SwitchToPage("home")
162 }
163 return event
164 })
165 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
166
167 shortcut := 97 // rune for "a"
168 for i, srv := range ui.HLClient.pref.Bookmarks {
169 addr := srv.Addr
170 login := srv.Login
171 pass := srv.Password
172 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
173 ui.Pages.RemovePage("joinServer")
174
175 newJS := ui.renderJoinServerForm(addr, login, pass, "bookmarks", true, true)
176
177 ui.Pages.AddPage("joinServer", newJS, true, true)
178 })
179 }
180
181 return list
182 }
183
184 func (ui *UI) getTrackerList() *tview.List {
185 listing, err := GetListing(ui.HLClient.pref.Tracker)
186 if err != nil {
187 spew.Dump(err)
188 }
189
190 list := tview.NewList()
191 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
192 if event.Key() == tcell.KeyEsc {
193 ui.Pages.SwitchToPage("home")
194 }
195 return event
196 })
197 list.Box.SetBorder(true).SetTitle("| Servers |")
198
199 shortcut := 97 // rune for "a"
200 for i, srv := range listing {
201 addr := srv.Addr()
202 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
203 ui.Pages.RemovePage("joinServer")
204
205 newJS := ui.renderJoinServerForm(addr, GuestAccount, "", trackerListPage, false, true)
206
207 ui.Pages.AddPage("joinServer", newJS, true, true)
208 ui.Pages.ShowPage("joinServer")
209 })
210 }
211
212 return list
213 }
214
215 func (ui *UI) renderSettingsForm() *tview.Flex {
216 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
217 settingsForm := tview.NewForm()
218 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
219 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
220 _, err := strconv.Atoi(idStr)
221 return err == nil
222 }, nil)
223 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
224 settingsForm.AddButton("Save", func() {
225 ui.HLClient.pref.Username = settingsForm.GetFormItem(0).(*tview.InputField).GetText()
226 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
227 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
228 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
229
230 out, err := yaml.Marshal(&ui.HLClient.pref)
231 if err != nil {
232 // TODO: handle err
233 }
234 // TODO: handle err
235 _ = ioutil.WriteFile(clientConfigPath, out, 0666)
236 ui.Pages.RemovePage("settings")
237 })
238 settingsForm.SetBorder(true)
239 settingsForm.SetCancelFunc(func() {
240 ui.Pages.RemovePage("settings")
241 })
242 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
243 settingsPage.Box.SetBorder(true).SetTitle("Settings")
244 settingsPage.AddItem(settingsForm, 0, 1, true)
245
246 centerFlex := tview.NewFlex().
247 AddItem(nil, 0, 1, false).
248 AddItem(tview.NewFlex().
249 SetDirection(tview.FlexRow).
250 AddItem(nil, 0, 1, false).
251 AddItem(settingsForm, 15, 1, true).
252 AddItem(nil, 0, 1, false), 40, 1, true).
253 AddItem(nil, 0, 1, false)
254
255 return centerFlex
256 }
257
258 // DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
259 type DebugBuffer struct {
260 TextView *tview.TextView
261 }
262
263 func (db *DebugBuffer) Write(p []byte) (int, error) {
264 return db.TextView.Write(p)
265 }
266
267 // Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
268 func (db *DebugBuffer) Sync() error {
269 return nil
270 }
271
272 func (ui *UI) joinServer(addr, login, password string) error {
273 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
274 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
275 }
276
277 go func() {
278 err := ui.HLClient.ReadLoop()
279 if err != nil {
280 ui.HLClient.Logger.Errorw("read error", "err", err)
281 }
282 }()
283 return nil
284 }
285
286 func (ui *UI) renderJoinServerForm(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
287 joinServerForm := tview.NewForm()
288 joinServerForm.
289 AddInputField("Server", server, 0, nil, nil).
290 AddInputField("Login", login, 0, nil, nil).
291 AddPasswordField("Password", password, 0, '*', nil).
292 AddCheckbox("Save", save, func(checked bool) {
293 // TODO: Implement bookmark saving
294 }).
295 AddButton("Cancel", func() {
296 ui.Pages.SwitchToPage(backPage)
297 }).
298 AddButton("Connect", func() {
299 err := ui.joinServer(
300 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
301 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
302 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
303 )
304 if err != nil {
305 ui.HLClient.Logger.Errorw("login error", "err", err)
306 loginErrModal := tview.NewModal().
307 AddButtons([]string{"Oh no"}).
308 SetText(err.Error()).
309 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
310 ui.Pages.SwitchToPage(backPage)
311 })
312
313 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
314 }
315
316 // Save checkbox
317 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
318 // TODO: implement bookmark saving
319 }
320 })
321
322 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
323 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
324 if event.Key() == tcell.KeyEscape {
325 ui.Pages.SwitchToPage(backPage)
326 }
327 return event
328 })
329
330 if defaultConnect {
331 joinServerForm.SetFocus(5)
332 }
333
334 joinServerPage := tview.NewFlex().
335 AddItem(nil, 0, 1, false).
336 AddItem(tview.NewFlex().
337 SetDirection(tview.FlexRow).
338 AddItem(nil, 0, 1, false).
339 AddItem(joinServerForm, 14, 1, true).
340 AddItem(nil, 0, 1, false), 40, 1, true).
341 AddItem(nil, 0, 1, false)
342
343 return joinServerPage
344 }
345
346 func randomBanner() string {
347 rand.Seed(time.Now().UnixNano())
348
349 bannerFiles, _ := bannerDir.ReadDir("client/banners")
350 file, _ := bannerDir.ReadFile("client/banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
351
352 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
353 }
354
355 func (ui *UI) renderServerUI() *tview.Flex {
356 commandList := tview.NewTextView().SetDynamicColors(true)
357 commandList.
358 SetText("[yellow]^n[-::]: Read News\n[yellow]^l[-::]: View Logs\n").
359 SetBorder(true).
360 SetTitle("Keyboard Shortcuts")
361
362 modal := tview.NewModal().
363 SetText("Disconnect from the server?").
364 AddButtons([]string{"Cancel", "Exit"}).
365 SetFocus(1)
366 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
367 if buttonIndex == 1 {
368 _ = ui.HLClient.Disconnect()
369 ui.Pages.SwitchToPage("home")
370 } else {
371 ui.Pages.HidePage("modal")
372 }
373 })
374
375 serverUI := tview.NewFlex().
376 AddItem(tview.NewFlex().
377 SetDirection(tview.FlexRow).
378 AddItem(commandList, 4, 0, false).
379 AddItem(ui.chatBox, 0, 8, false).
380 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
381 AddItem(ui.userList, 25, 1, false)
382 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
383 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
384 if event.Key() == tcell.KeyEscape {
385 ui.Pages.AddPage("modal", modal, false, true)
386 }
387
388 // Show News
389 if event.Key() == tcell.KeyCtrlN {
390 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
391 ui.HLClient.Logger.Errorw("err", "err", err)
392 }
393 }
394
395 return event
396 })
397 return serverUI
398 }
399
400 func (ui *UI) Start() {
401 home := tview.NewFlex().SetDirection(tview.FlexRow)
402 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
403 mainMenu := tview.NewList()
404
405 bannerItem := tview.NewTextView().
406 SetText(randomBanner()).
407 SetDynamicColors(true).
408 SetTextAlign(tview.AlignCenter)
409
410 home.AddItem(
411 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
412 13, 1, false)
413 home.AddItem(tview.NewFlex().
414 AddItem(nil, 0, 1, false).
415 AddItem(mainMenu, 0, 1, true).
416 AddItem(nil, 0, 1, false),
417 0, 1, true,
418 )
419
420 mainMenu.AddItem("Join Server", "", 'j', func() {
421 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
422 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
423 }).
424 AddItem("Bookmarks", "", 'b', func() {
425 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
426 }).
427 AddItem("Browse Tracker", "", 't', func() {
428 ui.trackerList = ui.getTrackerList()
429 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
430 }).
431 AddItem("Settings", "", 's', func() {
432 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
433 }).
434 AddItem("Quit", "", 'q', func() {
435 ui.App.Stop()
436 })
437
438 ui.Pages.AddPage("home", home, true, true)
439
440 // App level input capture
441 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
442 if event.Key() == tcell.KeyCtrlC {
443 ui.HLClient.Logger.Infow("Exiting")
444 ui.App.Stop()
445 os.Exit(0)
446 }
447 // Show Logs
448 if event.Key() == tcell.KeyCtrlL {
449 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
450 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
451 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
452 if key == tcell.KeyEscape {
453 ui.Pages.RemovePage("logs")
454 }
455 })
456
457 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
458 }
459 return event
460 })
461
462 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
463 ui.App.Stop()
464 os.Exit(1)
465 }
466 }
467
468 func NewClient(username string, logger *zap.SugaredLogger) *Client {
469 c := &Client{
470 Icon: &[]byte{0x07, 0xd7},
471 Logger: logger,
472 activeTasks: make(map[uint32]*Transaction),
473 Handlers: clientHandlers,
474 }
475 c.UI = NewUI(c)
476
477 prefs, err := readConfig(clientConfigPath)
478 if err != nil {
479 return c
480 }
481 c.pref = prefs
482
483 return c
484 }
485
486 type clientTransaction struct {
487 Name string
488 Handler func(*Client, *Transaction) ([]Transaction, error)
489 }
490
491 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
492 return ch.Handler(cc, t)
493 }
494
495 type clientTHandler interface {
496 Handle(*Client, *Transaction) ([]Transaction, error)
497 }
498
499 type mockClientHandler struct {
500 mock.Mock
501 }
502
503 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
504 args := mh.Called(cc, t)
505 return args.Get(0).([]Transaction), args.Error(1)
506 }
507
508 var clientHandlers = map[uint16]clientTHandler{
509 // Server initiated
510 tranChatMsg: clientTransaction{
511 Name: "tranChatMsg",
512 Handler: handleClientChatMsg,
513 },
514 tranLogin: clientTransaction{
515 Name: "tranLogin",
516 Handler: handleClientTranLogin,
517 },
518 tranShowAgreement: clientTransaction{
519 Name: "tranShowAgreement",
520 Handler: handleClientTranShowAgreement,
521 },
522 tranUserAccess: clientTransaction{
523 Name: "tranUserAccess",
524 Handler: handleClientTranUserAccess,
525 },
526 tranGetUserNameList: clientTransaction{
527 Name: "tranGetUserNameList",
528 Handler: handleClientGetUserNameList,
529 },
530 tranNotifyChangeUser: clientTransaction{
531 Name: "tranNotifyChangeUser",
532 Handler: handleNotifyChangeUser,
533 },
534 tranNotifyDeleteUser: clientTransaction{
535 Name: "tranNotifyDeleteUser",
536 Handler: handleNotifyDeleteUser,
537 },
538 tranGetMsgs: clientTransaction{
539 Name: "tranNotifyDeleteUser",
540 Handler: handleGetMsgs,
541 },
542 }
543
544 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
545 newsText := string(t.GetField(fieldData).Data)
546 newsText = strings.ReplaceAll(newsText, "\r", "\n")
547
548 newsTextView := tview.NewTextView().
549 SetText(newsText).
550 SetDoneFunc(func(key tcell.Key) {
551 c.UI.Pages.SwitchToPage("serverUI")
552 c.UI.App.SetFocus(c.UI.chatInput)
553 })
554 newsTextView.SetBorder(true).SetTitle("News")
555
556 c.UI.Pages.AddPage("news", newsTextView, true, true)
557 c.UI.Pages.SwitchToPage("news")
558 c.UI.App.SetFocus(newsTextView)
559
560 c.UI.App.Draw()
561
562 return res, err
563 }
564
565 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
566 newUser := User{
567 ID: t.GetField(fieldUserID).Data,
568 Name: string(t.GetField(fieldUserName).Data),
569 Icon: t.GetField(fieldUserIconID).Data,
570 Flags: t.GetField(fieldUserFlags).Data,
571 }
572
573 // Possible cases:
574 // user is new to the server
575 // user is already on the server but has a new name
576
577 var oldName string
578 var newUserList []User
579 updatedUser := false
580 for _, u := range c.UserList {
581 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
582 if bytes.Equal(newUser.ID, u.ID) {
583 oldName = u.Name
584 u.Name = newUser.Name
585 if u.Name != newUser.Name {
586 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
587 }
588 updatedUser = true
589 }
590 newUserList = append(newUserList, u)
591 }
592
593 if !updatedUser {
594 newUserList = append(newUserList, newUser)
595 }
596
597 c.UserList = newUserList
598
599 c.renderUserList()
600
601 return res, err
602 }
603
604 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
605 exitUser := t.GetField(fieldUserID).Data
606
607 var newUserList []User
608 for _, u := range c.UserList {
609 if !bytes.Equal(exitUser, u.ID) {
610 newUserList = append(newUserList, u)
611 }
612 }
613
614 c.UserList = newUserList
615
616 c.renderUserList()
617
618 return res, err
619 }
620
621 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
622
623 func (c *Client) ReadLoop() error {
624 tranBuff := make([]byte, 0)
625 tReadlen := 0
626 // Infinite loop where take action on incoming client requests until the connection is closed
627 for {
628 buf := make([]byte, readBuffSize)
629 tranBuff = tranBuff[tReadlen:]
630
631 readLen, err := c.Connection.Read(buf)
632 if err != nil {
633 return err
634 }
635 tranBuff = append(tranBuff, buf[:readLen]...)
636
637 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
638 // into a slice of transactions
639 var transactions []Transaction
640 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
641 c.Logger.Errorw("Error handling transaction", "err", err)
642 }
643
644 // iterate over all of the transactions that were parsed from the byte slice and handle them
645 for _, t := range transactions {
646 if err := c.HandleTransaction(&t); err != nil {
647 c.Logger.Errorw("Error handling transaction", "err", err)
648 }
649 }
650 }
651 }
652
653 func (c *Client) GetTransactions() error {
654 tranBuff := make([]byte, 0)
655 tReadlen := 0
656
657 buf := make([]byte, readBuffSize)
658 tranBuff = tranBuff[tReadlen:]
659
660 readLen, err := c.Connection.Read(buf)
661 if err != nil {
662 return err
663 }
664 tranBuff = append(tranBuff, buf[:readLen]...)
665
666 return nil
667 }
668
669 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
670 var users []User
671 for _, field := range t.Fields {
672 u, _ := ReadUser(field.Data)
673 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
674 //if flagBitmap.Bit(userFlagAdmin) == 1 {
675 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
676 //} else {
677 // fmt.Fprintf(UserList, "%s\n", u.Name)
678 //}
679
680 users = append(users, *u)
681 }
682 c.UserList = users
683
684 c.renderUserList()
685
686 return res, err
687 }
688
689 func (c *Client) renderUserList() {
690 c.UI.userList.Clear()
691 for _, u := range c.UserList {
692 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
693 if flagBitmap.Bit(userFlagAdmin) == 1 {
694 fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
695 } else {
696 fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
697 }
698 }
699 }
700
701 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
702 fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
703
704 return res, err
705 }
706
707 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
708 c.UserAccess = t.GetField(fieldUserAccess).Data
709
710 return res, err
711 }
712
713 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
714 agreement := string(t.GetField(fieldData).Data)
715 agreement = strings.ReplaceAll(agreement, "\r", "\n")
716
717 c.UI.agreeModal = tview.NewModal().
718 SetText(agreement).
719 AddButtons([]string{"Agree", "Disagree"}).
720 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
721 if buttonIndex == 0 {
722 res = append(res,
723 *NewTransaction(
724 tranAgreed, nil,
725 NewField(fieldUserName, []byte(c.pref.Username)),
726 NewField(fieldUserIconID, c.pref.IconBytes()),
727 NewField(fieldUserFlags, []byte{0x00, 0x00}),
728 NewField(fieldOptions, []byte{0x00, 0x00}),
729 ),
730 )
731 c.Agreed = true
732 c.UI.Pages.HidePage("agreement")
733 c.UI.App.SetFocus(c.UI.chatInput)
734 } else {
735 _ = c.Disconnect()
736 c.UI.Pages.SwitchToPage("home")
737 }
738 },
739 )
740
741 c.Logger.Debug("show agreement page")
742 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
743
744 c.UI.Pages.ShowPage("agreement ")
745
746 c.UI.App.Draw()
747 return res, err
748 }
749
750 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
751 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
752 errMsg := string(t.GetField(fieldError).Data)
753 errModal := tview.NewModal()
754 errModal.SetText(errMsg)
755 errModal.AddButtons([]string{"Oh no"})
756 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
757 c.UI.Pages.RemovePage("errModal")
758 })
759 c.UI.Pages.RemovePage("joinServer")
760 c.UI.Pages.AddPage("errModal", errModal, false, true)
761
762 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
763
764 c.Logger.Error(string(t.GetField(fieldError).Data))
765 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
766 }
767 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
768 c.UI.App.SetFocus(c.UI.chatInput)
769
770 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
771 c.Logger.Errorw("err", "err", err)
772 }
773 return res, err
774 }
775
776 // JoinServer connects to a Hotline server and completes the login flow
777 func (c *Client) JoinServer(address, login, passwd string) error {
778 // Establish TCP connection to server
779 if err := c.connect(address); err != nil {
780 return err
781 }
782
783 // Send handshake sequence
784 if err := c.Handshake(); err != nil {
785 return err
786 }
787
788 // Authenticate (send tranLogin 107)
789 if err := c.LogIn(login, passwd); err != nil {
790 return err
791 }
792
793 return nil
794 }
795
796 // connect establishes a connection with a Server by sending handshake sequence
797 func (c *Client) connect(address string) error {
798 var err error
799 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
800 if err != nil {
801 return err
802 }
803 return nil
804 }
805
806 var ClientHandshake = []byte{
807 0x54, 0x52, 0x54, 0x50, // TRTP
808 0x48, 0x4f, 0x54, 0x4c, // HOTL
809 0x00, 0x01,
810 0x00, 0x02,
811 }
812
813 var ServerHandshake = []byte{
814 0x54, 0x52, 0x54, 0x50, // TRTP
815 0x00, 0x00, 0x00, 0x00, // ErrorCode
816 }
817
818 func (c *Client) Handshake() error {
819 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
820 //Sub-protocol ID 4 User defined
821 //Version 2 1 Currently 1
822 //Sub-version 2 User defined
823 if _, err := c.Connection.Write(ClientHandshake); err != nil {
824 return fmt.Errorf("handshake write err: %s", err)
825 }
826
827 replyBuf := make([]byte, 8)
828 _, err := c.Connection.Read(replyBuf)
829 if err != nil {
830 return err
831 }
832
833 //spew.Dump(replyBuf)
834 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
835 return nil
836 }
837 // In the case of an error, client and server close the connection.
838
839 return fmt.Errorf("handshake response err: %s", err)
840 }
841
842 func (c *Client) LogIn(login string, password string) error {
843 return c.Send(
844 *NewTransaction(
845 tranLogin, nil,
846 NewField(fieldUserName, []byte(c.pref.Username)),
847 NewField(fieldUserIconID, c.pref.IconBytes()),
848 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
849 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
850 NewField(fieldVersion, []byte{0, 2}),
851 ),
852 )
853 }
854
855 func (c *Client) Send(t Transaction) error {
856 requestNum := binary.BigEndian.Uint16(t.Type)
857 tID := binary.BigEndian.Uint32(t.ID)
858
859 //handler := TransactionHandlers[requestNum]
860
861 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
862 if t.IsReply == 0 {
863 c.activeTasks[tID] = &t
864 }
865
866 var n int
867 var err error
868 if n, err = c.Connection.Write(t.Payload()); err != nil {
869 return err
870 }
871 c.Logger.Debugw("Sent Transaction",
872 "IsReply", t.IsReply,
873 "type", requestNum,
874 "sentBytes", n,
875 )
876 return nil
877 }
878
879 func (c *Client) HandleTransaction(t *Transaction) error {
880 var origT Transaction
881 if t.IsReply == 1 {
882 requestID := binary.BigEndian.Uint32(t.ID)
883 origT = *c.activeTasks[requestID]
884 t.Type = origT.Type
885 }
886
887 requestNum := binary.BigEndian.Uint16(t.Type)
888 c.Logger.Infow(
889 "Received Transaction",
890 "RequestType", requestNum,
891 )
892
893 if handler, ok := c.Handlers[requestNum]; ok {
894 outT, _ := handler.Handle(c, t)
895 for _, t := range outT {
896 c.Send(t)
897 }
898 } else {
899 c.Logger.Errorw(
900 "Unimplemented transaction type received",
901 "RequestID", requestNum,
902 "TransactionID", t.ID,
903 )
904 }
905
906 return nil
907 }
908
909 func (c *Client) Connected() bool {
910 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
911 // c.Agreed == true &&
912 if c.UserAccess != nil {
913 return true
914 }
915 return false
916 }
917
918 func (c *Client) Disconnect() error {
919 err := c.Connection.Close()
920 if err != nil {
921 return err
922 }
923 return nil
924 }