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