]> git.r.bdr.sh - rbdr/mobius/blob - client.go
Fix crash on shxd servers
[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 // The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
758 // fields, but shxd sneaks in fieldChatSubject (115) so it's important to filter explicitly for the expected
759 // field type. Probably a good idea to do everywhere.
760 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
761 u, err := ReadUser(field.Data)
762 if err != nil {
763 return res, err
764 }
765 users = append(users, *u)
766 }
767 }
768 c.UserList = users
769
770 c.renderUserList()
771
772 return res, err
773 }
774
775 func (c *Client) renderUserList() {
776 c.UI.userList.Clear()
777 for _, u := range c.UserList {
778 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
779 if flagBitmap.Bit(userFlagAdmin) == 1 {
780 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
781 } else {
782 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
783 }
784 }
785 }
786
787 func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
788 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
789
790 return res, err
791 }
792
793 func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
794 c.UserAccess = t.GetField(fieldUserAccess).Data
795
796 return res, err
797 }
798
799 func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
800 agreement := string(t.GetField(fieldData).Data)
801 agreement = strings.ReplaceAll(agreement, "\r", "\n")
802
803 c.UI.agreeModal = tview.NewModal().
804 SetText(agreement).
805 AddButtons([]string{"Agree", "Disagree"}).
806 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
807 if buttonIndex == 0 {
808 res = append(res,
809 *NewTransaction(
810 tranAgreed, nil,
811 NewField(fieldUserName, []byte(c.pref.Username)),
812 NewField(fieldUserIconID, c.pref.IconBytes()),
813 NewField(fieldUserFlags, []byte{0x00, 0x00}),
814 NewField(fieldOptions, []byte{0x00, 0x00}),
815 ),
816 )
817 c.Agreed = true
818 c.UI.Pages.HidePage("agreement")
819 c.UI.App.SetFocus(c.UI.chatInput)
820 } else {
821 _ = c.Disconnect()
822 c.UI.Pages.SwitchToPage("home")
823 }
824 },
825 )
826
827 c.Logger.Debug("show agreement page")
828 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
829
830 c.UI.Pages.ShowPage("agreement ")
831
832 c.UI.App.Draw()
833 return res, err
834 }
835
836 func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
837 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
838 errMsg := string(t.GetField(fieldError).Data)
839 errModal := tview.NewModal()
840 errModal.SetText(errMsg)
841 errModal.AddButtons([]string{"Oh no"})
842 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
843 c.UI.Pages.RemovePage("errModal")
844 })
845 c.UI.Pages.RemovePage("joinServer")
846 c.UI.Pages.AddPage("errModal", errModal, false, true)
847
848 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
849
850 c.Logger.Error(string(t.GetField(fieldError).Data))
851 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
852 }
853 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
854 c.UI.App.SetFocus(c.UI.chatInput)
855
856 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
857 c.Logger.Errorw("err", "err", err)
858 }
859 return res, err
860 }
861
862 // JoinServer connects to a Hotline server and completes the login flow
863 func (c *Client) JoinServer(address, login, passwd string) error {
864 // Establish TCP connection to server
865 if err := c.connect(address); err != nil {
866 return err
867 }
868
869 // Send handshake sequence
870 if err := c.Handshake(); err != nil {
871 return err
872 }
873
874 // Authenticate (send tranLogin 107)
875 if err := c.LogIn(login, passwd); err != nil {
876 return err
877 }
878
879 return nil
880 }
881
882 // connect establishes a connection with a Server by sending handshake sequence
883 func (c *Client) connect(address string) error {
884 var err error
885 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
886 if err != nil {
887 return err
888 }
889 return nil
890 }
891
892 var ClientHandshake = []byte{
893 0x54, 0x52, 0x54, 0x50, // TRTP
894 0x48, 0x4f, 0x54, 0x4c, // HOTL
895 0x00, 0x01,
896 0x00, 0x02,
897 }
898
899 var ServerHandshake = []byte{
900 0x54, 0x52, 0x54, 0x50, // TRTP
901 0x00, 0x00, 0x00, 0x00, // ErrorCode
902 }
903
904 func (c *Client) Handshake() error {
905 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
906 //Sub-protocol ID 4 User defined
907 //Version 2 1 Currently 1
908 //Sub-version 2 User defined
909 if _, err := c.Connection.Write(ClientHandshake); err != nil {
910 return fmt.Errorf("handshake write err: %s", err)
911 }
912
913 replyBuf := make([]byte, 8)
914 _, err := c.Connection.Read(replyBuf)
915 if err != nil {
916 return err
917 }
918
919 //spew.Dump(replyBuf)
920 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
921 return nil
922 }
923 // In the case of an error, client and server close the connection.
924
925 return fmt.Errorf("handshake response err: %s", err)
926 }
927
928 func (c *Client) LogIn(login string, password string) error {
929 return c.Send(
930 *NewTransaction(
931 tranLogin, nil,
932 NewField(fieldUserName, []byte(c.pref.Username)),
933 NewField(fieldUserIconID, c.pref.IconBytes()),
934 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
935 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
936 NewField(fieldVersion, []byte{0, 2}),
937 ),
938 )
939 }
940
941 func (c *Client) Send(t Transaction) error {
942 requestNum := binary.BigEndian.Uint16(t.Type)
943 tID := binary.BigEndian.Uint32(t.ID)
944
945 //handler := TransactionHandlers[requestNum]
946
947 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
948 if t.IsReply == 0 {
949 c.activeTasks[tID] = &t
950 }
951
952 var n int
953 var err error
954 if n, err = c.Connection.Write(t.Payload()); err != nil {
955 return err
956 }
957 c.Logger.Debugw("Sent Transaction",
958 "IsReply", t.IsReply,
959 "type", requestNum,
960 "sentBytes", n,
961 )
962 return nil
963 }
964
965 func (c *Client) HandleTransaction(t *Transaction) error {
966 var origT Transaction
967 if t.IsReply == 1 {
968 requestID := binary.BigEndian.Uint32(t.ID)
969 origT = *c.activeTasks[requestID]
970 t.Type = origT.Type
971 }
972
973 requestNum := binary.BigEndian.Uint16(t.Type)
974 c.Logger.Infow(
975 "Received Transaction",
976 "RequestType", requestNum,
977 )
978
979 if handler, ok := c.Handlers[requestNum]; ok {
980 outT, _ := handler.Handle(c, t)
981 for _, t := range outT {
982 c.Send(t)
983 }
984 } else {
985 c.Logger.Errorw(
986 "Unimplemented transaction type received",
987 "RequestID", requestNum,
988 "TransactionID", t.ID,
989 )
990 }
991
992 return nil
993 }
994
995 func (c *Client) Connected() bool {
996 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
997 // c.Agreed == true &&
998 if c.UserAccess != nil {
999 return true
1000 }
1001 return false
1002 }
1003
1004 func (c *Client) Disconnect() error {
1005 err := c.Connection.Close()
1006 if err != nil {
1007 return err
1008 }
1009 return nil
1010 }