]> git.r.bdr.sh - rbdr/mobius/blob - hotline/client.go
35ec7fb488015c2c4ae00179cfea4468d8d53193
[rbdr/mobius] / hotline / 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 (
26 trackerListPage = "trackerList"
27 )
28
29 //go:embed banners/*.txt
30 var bannerDir embed.FS
31
32 type Bookmark struct {
33 Name string `yaml:"Name"`
34 Addr string `yaml:"Addr"`
35 Login string `yaml:"Login"`
36 Password string `yaml:"Password"`
37 }
38
39 type ClientPrefs struct {
40 Username string `yaml:"Username"`
41 IconID int `yaml:"IconID"`
42 Bookmarks []Bookmark `yaml:"Bookmarks"`
43 Tracker string `yaml:"Tracker"`
44 }
45
46 func (cp *ClientPrefs) IconBytes() []byte {
47 iconBytes := make([]byte, 2)
48 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
49 return iconBytes
50 }
51
52 func readConfig(cfgPath string) (*ClientPrefs, error) {
53 fh, err := os.Open(cfgPath)
54 if err != nil {
55 return nil, err
56 }
57
58 prefs := ClientPrefs{}
59 decoder := yaml.NewDecoder(fh)
60 decoder.SetStrict(true)
61 if err := decoder.Decode(&prefs); err != nil {
62 return nil, err
63 }
64 return &prefs, nil
65 }
66
67 type Client struct {
68 cfgPath string
69 DebugBuf *DebugBuffer
70 Connection net.Conn
71 Login *[]byte
72 Password *[]byte
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
92 type 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
104 func NewUI(c *Client) *UI {
105 app := tview.NewApplication()
106 chatBox := tview.NewTextView().
107 SetScrollable(true).
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).
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
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 })
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
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")
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
182 func (ui *UI) getTrackerList() *tview.List {
183 listing, err := GetListing(ui.HLClient.pref.Tracker)
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
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)
219 return err == nil
220 }, nil)
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"
226 }
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()
231
232 out, err := yaml.Marshal(&ui.HLClient.pref)
233 if err != nil {
234 // TODO: handle err
235 }
236 // TODO: handle err
237 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
238 if err != nil {
239 println(ui.HLClient.cfgPath)
240 panic(err)
241 }
242 ui.Pages.RemovePage("settings")
243 })
244 settingsForm.SetBorder(true)
245 settingsForm.SetCancelFunc(func() {
246 ui.Pages.RemovePage("settings")
247 })
248 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
249 settingsPage.Box.SetBorder(true).SetTitle("Settings")
250 settingsPage.AddItem(settingsForm, 0, 1, true)
251
252 centerFlex := tview.NewFlex().
253 AddItem(nil, 0, 1, false).
254 AddItem(tview.NewFlex().
255 SetDirection(tview.FlexRow).
256 AddItem(nil, 0, 1, false).
257 AddItem(settingsForm, 15, 1, true).
258 AddItem(nil, 0, 1, false), 40, 1, true).
259 AddItem(nil, 0, 1, false)
260
261 return centerFlex
262 }
263
264 // DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
265 type DebugBuffer struct {
266 TextView *tview.TextView
267 }
268
269 func (db *DebugBuffer) Write(p []byte) (int, error) {
270 return db.TextView.Write(p)
271 }
272
273 // Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
274 func (db *DebugBuffer) Sync() error {
275 return nil
276 }
277
278 func (ui *UI) joinServer(addr, login, password string) error {
279 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
280 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
281 }
282
283 go func() {
284 err := ui.HLClient.ReadLoop()
285 if err != nil {
286 ui.HLClient.Logger.Errorw("read error", "err", err)
287 }
288 }()
289 return nil
290 }
291
292 func (ui *UI) renderJoinServerForm(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
293 joinServerForm := tview.NewForm()
294 joinServerForm.
295 AddInputField("Server", server, 0, nil, nil).
296 AddInputField("Login", login, 0, nil, nil).
297 AddPasswordField("Password", password, 0, '*', nil).
298 AddCheckbox("Save", save, func(checked bool) {
299 // TODO: Implement bookmark saving
300 }).
301 AddButton("Cancel", func() {
302 ui.Pages.SwitchToPage(backPage)
303 }).
304 AddButton("Connect", func() {
305 err := ui.joinServer(
306 joinServerForm.GetFormItem(0).(*tview.InputField).GetText(),
307 joinServerForm.GetFormItem(1).(*tview.InputField).GetText(),
308 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
309 )
310 if err != nil {
311 ui.HLClient.Logger.Errorw("login error", "err", err)
312 loginErrModal := tview.NewModal().
313 AddButtons([]string{"Oh no"}).
314 SetText(err.Error()).
315 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
316 ui.Pages.SwitchToPage(backPage)
317 })
318
319 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
320 }
321
322 // Save checkbox
323 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
324 // TODO: implement bookmark saving
325 }
326 })
327
328 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
329 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
330 if event.Key() == tcell.KeyEscape {
331 ui.Pages.SwitchToPage(backPage)
332 }
333 return event
334 })
335
336 if defaultConnect {
337 joinServerForm.SetFocus(5)
338 }
339
340 joinServerPage := tview.NewFlex().
341 AddItem(nil, 0, 1, false).
342 AddItem(tview.NewFlex().
343 SetDirection(tview.FlexRow).
344 AddItem(nil, 0, 1, false).
345 AddItem(joinServerForm, 14, 1, true).
346 AddItem(nil, 0, 1, false), 40, 1, true).
347 AddItem(nil, 0, 1, false)
348
349 return joinServerPage
350 }
351
352 func randomBanner() string {
353 rand.Seed(time.Now().UnixNano())
354
355 bannerFiles, _ := bannerDir.ReadDir("banners")
356 file, _ := bannerDir.ReadFile("banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
357
358 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
359 }
360
361 func (ui *UI) renderServerUI() *tview.Flex {
362 commandList := tview.NewTextView().SetDynamicColors(true)
363 commandList.
364 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs\n").
365 SetBorder(true).
366 SetTitle("Keyboard Shortcuts")
367
368 modal := tview.NewModal().
369 SetText("Disconnect from the server?").
370 AddButtons([]string{"Cancel", "Exit"}).
371 SetFocus(1)
372 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
373 if buttonIndex == 1 {
374 _ = ui.HLClient.Disconnect()
375 ui.Pages.SwitchToPage("home")
376 } else {
377 ui.Pages.HidePage("modal")
378 }
379 })
380
381 serverUI := tview.NewFlex().
382 AddItem(tview.NewFlex().
383 SetDirection(tview.FlexRow).
384 AddItem(commandList, 4, 0, false).
385 AddItem(ui.chatBox, 0, 8, false).
386 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
387 AddItem(ui.userList, 25, 1, false)
388 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + "TODO" + " |").SetTitleAlign(tview.AlignLeft)
389 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
390 if event.Key() == tcell.KeyEscape {
391 ui.Pages.AddPage("modal", modal, false, true)
392 }
393
394 // Show News
395 if event.Key() == tcell.KeyCtrlN {
396 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
397 ui.HLClient.Logger.Errorw("err", "err", err)
398 }
399 }
400
401 // Post news
402 if event.Key() == tcell.KeyCtrlP {
403
404 newsFlex := tview.NewFlex()
405
406 newsPostTextArea := tview.NewTextView()
407 newsPostTextArea.SetBackgroundColor(tcell.ColorDimGray)
408 newsPostTextArea.SetChangedFunc(func() {
409 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
410 })
411 //newsPostTextArea.SetBorderPadding(0, 0, 1, 1)
412
413 newsPostForm := tview.NewForm().
414 SetButtonsAlign(tview.AlignRight).
415 AddButton("Post", nil)
416 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
417 switch event.Key() {
418 case tcell.KeyTab:
419 ui.App.SetFocus(newsPostTextArea)
420 case tcell.KeyEnter:
421 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
422 err := ui.HLClient.Send(
423 *NewTransaction(tranOldPostNews, nil,
424 NewField(fieldData, []byte(newsText)),
425 ),
426 )
427 if err != nil {
428 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
429 // TODO: display errModal to user
430 }
431 //newsInput.SetText("") // clear the input field after chat send
432 ui.Pages.RemovePage("newsInput")
433 }
434
435 return event
436 })
437
438 newsFlex.
439 SetDirection(tview.FlexRow).
440 SetBorder(true).
441 SetTitle("News Post")
442
443 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
444 ui.HLClient.Logger.Infow("key", "key", event.Key(), "rune", event.Rune())
445 switch event.Key() {
446 case tcell.KeyEscape:
447 ui.Pages.RemovePage("newsInput")
448 case tcell.KeyTab:
449 ui.App.SetFocus(newsPostForm)
450 case tcell.KeyEnter:
451 fmt.Fprintf(newsPostTextArea, "\n")
452 default:
453 switch event.Rune() {
454 case 127: // backspace
455 curTxt := newsPostTextArea.GetText(true)
456 if len(curTxt) > 0 {
457 curTxt = curTxt[:len(curTxt)-1]
458 newsPostTextArea.SetText(curTxt)
459 }
460 default:
461 fmt.Fprintf(newsPostTextArea, string(event.Rune()))
462 }
463 }
464
465 return event
466 })
467
468 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
469 newsFlex.AddItem(newsPostForm, 3, 0, false)
470
471 newsPostPage := tview.NewFlex().
472 AddItem(nil, 0, 1, false).
473 AddItem(tview.NewFlex().
474 SetDirection(tview.FlexRow).
475 AddItem(nil, 0, 1, false).
476 AddItem(newsFlex, 15, 1, true).
477 //AddItem(newsPostForm, 3, 0, false).
478 AddItem(nil, 0, 1, false), 40, 1, false).
479 AddItem(nil, 0, 1, false)
480
481 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
482 ui.App.SetFocus(newsPostTextArea)
483 }
484
485 return event
486 })
487 return serverUI
488 }
489
490 func (ui *UI) Start() {
491 home := tview.NewFlex().SetDirection(tview.FlexRow)
492 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
493 mainMenu := tview.NewList()
494
495 bannerItem := tview.NewTextView().
496 SetText(randomBanner()).
497 SetDynamicColors(true).
498 SetTextAlign(tview.AlignCenter)
499
500 home.AddItem(
501 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
502 14, 1, false)
503 home.AddItem(tview.NewFlex().
504 AddItem(nil, 0, 1, false).
505 AddItem(mainMenu, 0, 1, true).
506 AddItem(nil, 0, 1, false),
507 0, 1, true,
508 )
509
510 mainMenu.AddItem("Join Server", "", 'j', func() {
511 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
512 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
513 }).
514 AddItem("Bookmarks", "", 'b', func() {
515 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
516 }).
517 AddItem("Browse Tracker", "", 't', func() {
518 ui.trackerList = ui.getTrackerList()
519 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
520 }).
521 AddItem("Settings", "", 's', func() {
522 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
523 }).
524 AddItem("Quit", "", 'q', func() {
525 ui.App.Stop()
526 })
527
528 ui.Pages.AddPage("home", home, true, true)
529
530 // App level input capture
531 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
532 if event.Key() == tcell.KeyCtrlC {
533 ui.HLClient.Logger.Infow("Exiting")
534 ui.App.Stop()
535 os.Exit(0)
536 }
537 // Show Logs
538 if event.Key() == tcell.KeyCtrlL {
539 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
540 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
541 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
542 if key == tcell.KeyEscape {
543 ui.Pages.RemovePage("logs")
544 }
545 })
546
547 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
548 }
549 return event
550 })
551
552 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
553 ui.App.Stop()
554 os.Exit(1)
555 }
556 }
557
558 func NewClient(cfgPath string, logger *zap.SugaredLogger) *Client {
559 c := &Client{
560 cfgPath: cfgPath,
561 Logger: logger,
562 activeTasks: make(map[uint32]*Transaction),
563 Handlers: clientHandlers,
564 }
565 c.UI = NewUI(c)
566
567 prefs, err := readConfig(cfgPath)
568 if err != nil {
569 fmt.Printf("unable to read config file")
570 logger.Fatal("unable to read config file", "path", cfgPath)
571 }
572 c.pref = prefs
573
574 return c
575 }
576
577 type clientTransaction struct {
578 Name string
579 Handler func(*Client, *Transaction) ([]Transaction, error)
580 }
581
582 func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
583 return ch.Handler(cc, t)
584 }
585
586 type clientTHandler interface {
587 Handle(*Client, *Transaction) ([]Transaction, error)
588 }
589
590 type mockClientHandler struct {
591 mock.Mock
592 }
593
594 func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
595 args := mh.Called(cc, t)
596 return args.Get(0).([]Transaction), args.Error(1)
597 }
598
599 var clientHandlers = map[uint16]clientTHandler{
600 // Server initiated
601 tranChatMsg: clientTransaction{
602 Name: "tranChatMsg",
603 Handler: handleClientChatMsg,
604 },
605 tranLogin: clientTransaction{
606 Name: "tranLogin",
607 Handler: handleClientTranLogin,
608 },
609 tranShowAgreement: clientTransaction{
610 Name: "tranShowAgreement",
611 Handler: handleClientTranShowAgreement,
612 },
613 tranUserAccess: clientTransaction{
614 Name: "tranUserAccess",
615 Handler: handleClientTranUserAccess,
616 },
617 tranGetUserNameList: clientTransaction{
618 Name: "tranGetUserNameList",
619 Handler: handleClientGetUserNameList,
620 },
621 tranNotifyChangeUser: clientTransaction{
622 Name: "tranNotifyChangeUser",
623 Handler: handleNotifyChangeUser,
624 },
625 tranNotifyDeleteUser: clientTransaction{
626 Name: "tranNotifyDeleteUser",
627 Handler: handleNotifyDeleteUser,
628 },
629 tranGetMsgs: clientTransaction{
630 Name: "tranNotifyDeleteUser",
631 Handler: handleGetMsgs,
632 },
633 }
634
635 func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
636 newsText := string(t.GetField(fieldData).Data)
637 newsText = strings.ReplaceAll(newsText, "\r", "\n")
638
639 newsTextView := tview.NewTextView().
640 SetText(newsText).
641 SetDoneFunc(func(key tcell.Key) {
642 c.UI.Pages.SwitchToPage("serverUI")
643 c.UI.App.SetFocus(c.UI.chatInput)
644 })
645 newsTextView.SetBorder(true).SetTitle("News")
646
647 c.UI.Pages.AddPage("news", newsTextView, true, true)
648 c.UI.Pages.SwitchToPage("news")
649 c.UI.App.SetFocus(newsTextView)
650
651 c.UI.App.Draw()
652
653 return res, err
654 }
655
656 func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
657 newUser := User{
658 ID: t.GetField(fieldUserID).Data,
659 Name: string(t.GetField(fieldUserName).Data),
660 Icon: t.GetField(fieldUserIconID).Data,
661 Flags: t.GetField(fieldUserFlags).Data,
662 }
663
664 // Possible cases:
665 // user is new to the server
666 // user is already on the server but has a new name
667
668 var oldName string
669 var newUserList []User
670 updatedUser := false
671 for _, u := range c.UserList {
672 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
673 if bytes.Equal(newUser.ID, u.ID) {
674 oldName = u.Name
675 u.Name = newUser.Name
676 if u.Name != newUser.Name {
677 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
678 }
679 updatedUser = true
680 }
681 newUserList = append(newUserList, u)
682 }
683
684 if !updatedUser {
685 newUserList = append(newUserList, newUser)
686 }
687
688 c.UserList = newUserList
689
690 c.renderUserList()
691
692 return res, err
693 }
694
695 func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
696 exitUser := t.GetField(fieldUserID).Data
697
698 var newUserList []User
699 for _, u := range c.UserList {
700 if !bytes.Equal(exitUser, u.ID) {
701 newUserList = append(newUserList, u)
702 }
703 }
704
705 c.UserList = newUserList
706
707 c.renderUserList()
708
709 return res, err
710 }
711
712 const readBuffSize = 1024000 // 1KB - TODO: what should this be?
713
714 func (c *Client) ReadLoop() error {
715 tranBuff := make([]byte, 0)
716 tReadlen := 0
717 // Infinite loop where take action on incoming client requests until the connection is closed
718 for {
719 buf := make([]byte, readBuffSize)
720 tranBuff = tranBuff[tReadlen:]
721
722 readLen, err := c.Connection.Read(buf)
723 if err != nil {
724 return err
725 }
726 tranBuff = append(tranBuff, buf[:readLen]...)
727
728 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
729 // into a slice of transactions
730 var transactions []Transaction
731 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
732 c.Logger.Errorw("Error handling transaction", "err", err)
733 }
734
735 // iterate over all of the transactions that were parsed from the byte slice and handle them
736 for _, t := range transactions {
737 if err := c.HandleTransaction(&t); err != nil {
738 c.Logger.Errorw("Error handling transaction", "err", err)
739 }
740 }
741 }
742 }
743
744 func (c *Client) GetTransactions() error {
745 tranBuff := make([]byte, 0)
746 tReadlen := 0
747
748 buf := make([]byte, readBuffSize)
749 tranBuff = tranBuff[tReadlen:]
750
751 readLen, err := c.Connection.Read(buf)
752 if err != nil {
753 return err
754 }
755 tranBuff = append(tranBuff, buf[:readLen]...)
756
757 return nil
758 }
759
760 func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
761 var users []User
762 for _, field := range t.Fields {
763 // The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
764 // fields, but shxd sneaks in fieldChatSubject (115) so it's important to filter explicitly for the expected
765 // field type. Probably a good idea to do everywhere.
766 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
767 u, err := ReadUser(field.Data)
768 if err != nil {
769 return res, err
770 }
771 users = append(users, *u)
772 }
773 }
774 c.UserList = users
775
776 c.renderUserList()
777
778 return res, err
779 }
780
781 func (c *Client) renderUserList() {
782 c.UI.userList.Clear()
783 for _, u := range c.UserList {
784 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
785 if flagBitmap.Bit(userFlagAdmin) == 1 {
786 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
787 } else {
788 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
789 }
790 }
791 }
792
793 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
794 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
795
796 return res, err
797 }
798
799 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
800 c.UserAccess = t.GetField(fieldUserAccess).Data
801
802 return res, err
803 }
804
805 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
806 agreement := string(t.GetField(fieldData).Data)
807 agreement = strings.ReplaceAll(agreement, "\r", "\n")
808
809 c.UI.agreeModal = tview.NewModal().
810 SetText(agreement).
811 AddButtons([]string{"Agree", "Disagree"}).
812 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
813 if buttonIndex == 0 {
814 res = append(res,
815 *NewTransaction(
816 tranAgreed, nil,
817 NewField(fieldUserName, []byte(c.pref.Username)),
818 NewField(fieldUserIconID, c.pref.IconBytes()),
819 NewField(fieldUserFlags, []byte{0x00, 0x00}),
820 NewField(fieldOptions, []byte{0x00, 0x00}),
821 ),
822 )
823 c.Agreed = true
824 c.UI.Pages.HidePage("agreement")
825 c.UI.App.SetFocus(c.UI.chatInput)
826 } else {
827 _ = c.Disconnect()
828 c.UI.Pages.SwitchToPage("home")
829 }
830 },
831 )
832
833 c.Logger.Debug("show agreement page")
834 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
835
836 c.UI.Pages.ShowPage("agreement ")
837
838 c.UI.App.Draw()
839 return res, err
840 }
841
842 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
843 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
844 errMsg := string(t.GetField(fieldError).Data)
845 errModal := tview.NewModal()
846 errModal.SetText(errMsg)
847 errModal.AddButtons([]string{"Oh no"})
848 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
849 c.UI.Pages.RemovePage("errModal")
850 })
851 c.UI.Pages.RemovePage("joinServer")
852 c.UI.Pages.AddPage("errModal", errModal, false, true)
853
854 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
855
856 c.Logger.Error(string(t.GetField(fieldError).Data))
857 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
858 }
859 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
860 c.UI.App.SetFocus(c.UI.chatInput)
861
862 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
863 c.Logger.Errorw("err", "err", err)
864 }
865 return res, err
866 }
867
868 // JoinServer connects to a Hotline server and completes the login flow
869 func (c *Client) JoinServer(address, login, passwd string) error {
870 // Establish TCP connection to server
871 if err := c.connect(address); err != nil {
872 return err
873 }
874
875 // Send handshake sequence
876 if err := c.Handshake(); err != nil {
877 return err
878 }
879
880 // Authenticate (send tranLogin 107)
881 if err := c.LogIn(login, passwd); err != nil {
882 return err
883 }
884
885 return nil
886 }
887
888 // connect establishes a connection with a Server by sending handshake sequence
889 func (c *Client) connect(address string) error {
890 var err error
891 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
892 if err != nil {
893 return err
894 }
895 return nil
896 }
897
898 var ClientHandshake = []byte{
899 0x54, 0x52, 0x54, 0x50, // TRTP
900 0x48, 0x4f, 0x54, 0x4c, // HOTL
901 0x00, 0x01,
902 0x00, 0x02,
903 }
904
905 var ServerHandshake = []byte{
906 0x54, 0x52, 0x54, 0x50, // TRTP
907 0x00, 0x00, 0x00, 0x00, // ErrorCode
908 }
909
910 func (c *Client) Handshake() error {
911 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
912 //Sub-protocol ID 4 User defined
913 //Version 2 1 Currently 1
914 //Sub-version 2 User defined
915 if _, err := c.Connection.Write(ClientHandshake); err != nil {
916 return fmt.Errorf("handshake write err: %s", err)
917 }
918
919 replyBuf := make([]byte, 8)
920 _, err := c.Connection.Read(replyBuf)
921 if err != nil {
922 return err
923 }
924
925 //spew.Dump(replyBuf)
926 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
927 return nil
928 }
929 // In the case of an error, client and server close the connection.
930
931 return fmt.Errorf("handshake response err: %s", err)
932 }
933
934 func (c *Client) LogIn(login string, password string) error {
935 return c.Send(
936 *NewTransaction(
937 tranLogin, nil,
938 NewField(fieldUserName, []byte(c.pref.Username)),
939 NewField(fieldUserIconID, c.pref.IconBytes()),
940 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
941 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
942 NewField(fieldVersion, []byte{0, 2}),
943 ),
944 )
945 }
946
947 func (c *Client) Send(t Transaction) error {
948 requestNum := binary.BigEndian.Uint16(t.Type)
949 tID := binary.BigEndian.Uint32(t.ID)
950
951 //handler := TransactionHandlers[requestNum]
952
953 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
954 if t.IsReply == 0 {
955 c.activeTasks[tID] = &t
956 }
957
958 var n int
959 var err error
960 if n, err = c.Connection.Write(t.Payload()); err != nil {
961 return err
962 }
963 c.Logger.Debugw("Sent Transaction",
964 "IsReply", t.IsReply,
965 "type", requestNum,
966 "sentBytes", n,
967 )
968 return nil
969 }
970
971 func (c *Client) HandleTransaction(t *Transaction) error {
972 var origT Transaction
973 if t.IsReply == 1 {
974 requestID := binary.BigEndian.Uint32(t.ID)
975 origT = *c.activeTasks[requestID]
976 t.Type = origT.Type
977 }
978
979 requestNum := binary.BigEndian.Uint16(t.Type)
980 c.Logger.Infow(
981 "Received Transaction",
982 "RequestType", requestNum,
983 )
984
985 if handler, ok := c.Handlers[requestNum]; ok {
986 outT, _ := handler.Handle(c, t)
987 for _, t := range outT {
988 c.Send(t)
989 }
990 } else {
991 c.Logger.Errorw(
992 "Unimplemented transaction type received",
993 "RequestID", requestNum,
994 "TransactionID", t.ID,
995 )
996 }
997
998 return nil
999 }
1000
1001 func (c *Client) Connected() bool {
1002 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
1003 // c.Agreed == true &&
1004 if c.UserAccess != nil {
1005 return true
1006 }
1007 return false
1008 }
1009
1010 func (c *Client) Disconnect() error {
1011 err := c.Connection.Close()
1012 if err != nil {
1013 return err
1014 }
1015 return nil
1016 }