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