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