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