]> git.r.bdr.sh - rbdr/mobius/blame - client.go
Minor cleanup
[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
JH
212 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
213 settingsForm.AddInputField("IconID",iconStr, 0, func(idStr string, _ rune) bool {
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
252var (
253 srvIP string
254 srvLogin string
255 srvPass string
256)
257
258// DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
259type DebugBuffer struct {
260 TextView *tview.TextView
261}
262
263func (db *DebugBuffer) Write(p []byte) (int, error) {
264 return db.TextView.Write(p)
265}
266
267// Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
268func (db *DebugBuffer) Sync() error {
269 return nil
270}
271
272func (ui *UI) joinServer(addr, login, password string) error {
273 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
274 return errors.New(fmt.Sprintf("Error joining server: %v\n", err))
275 }
276
277 go func() {
278 err := ui.HLClient.ReadLoop()
279 if err != nil {
280 ui.HLClient.Logger.Errorw("read error", "err", err)
281 }
282 }()
283 return nil
284}
285
286func (ui *UI) renderJoinServerForm(server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
287 srvIP = server
288 joinServerForm := tview.NewForm()
289 joinServerForm.
290 AddInputField("Server", server, 20, nil, func(text string) {
291 srvIP = text
292 }).
293 AddInputField("Login", login, 20, nil, func(text string) {
294 l := []byte(text)
295 ui.HLClient.Login = &l
296 }).
297 AddPasswordField("Password", password, 20, '*', nil).
298 AddCheckbox("Save", save, func(checked bool) {
299 // TODO
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
352func randomBanner() string {
353 rand.Seed(time.Now().UnixNano())
354
355 bannerFiles, _ := bannerDir.ReadDir("client/banners")
8b53c840 356 file, _ := bannerDir.ReadFile("client/banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
6988a057
JH
357
358 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
359}
360
361func (ui *UI) renderServerUI() *tview.Flex {
362 commandList := tview.NewTextView().SetDynamicColors(true)
363 commandList.
364 SetText("[yellow]^n[-::]: Read 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 return event
402 })
403 return serverUI
404}
405
406func (ui *UI) Start() {
407 home := tview.NewFlex().SetDirection(tview.FlexRow)
408 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
409 mainMenu := tview.NewList()
410
411 bannerItem := tview.NewTextView().
412 SetText(randomBanner()).
413 SetDynamicColors(true).
414 SetTextAlign(tview.AlignCenter)
415
416 home.AddItem(
417 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
418 13, 1, false)
419 home.AddItem(tview.NewFlex().
420 AddItem(nil, 0, 1, false).
421 AddItem(mainMenu, 0, 1, true).
422 AddItem(nil, 0, 1, false),
423 0, 1, true,
424 )
425
6988a057 426 mainMenu.AddItem("Join Server", "", 'j', func() {
27e918a2 427 joinServerPage := ui.renderJoinServerForm("", GuestAccount, "", "home", false, false)
6988a057
JH
428 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
429 }).
430 AddItem("Bookmarks", "", 'b', func() {
431 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
432 }).
433 AddItem("Browse Tracker", "", 't', func() {
434 ui.trackerList = ui.getTrackerList()
435 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
436 }).
437 AddItem("Settings", "", 's', func() {
6988a057
JH
438 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
439 }).
440 AddItem("Quit", "", 'q', func() {
441 ui.App.Stop()
442 })
443
444 ui.Pages.AddPage("home", home, true, true)
445
446 // App level input capture
447 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
448 if event.Key() == tcell.KeyCtrlC {
449 ui.HLClient.Logger.Infow("Exiting")
450 ui.App.Stop()
451 os.Exit(0)
452 }
453 // Show Logs
454 if event.Key() == tcell.KeyCtrlL {
6988a057
JH
455 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
456 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
457 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
458 if key == tcell.KeyEscape {
6988a057
JH
459 ui.Pages.RemovePage("logs")
460 }
461 })
462
463 ui.Pages.AddAndSwitchToPage("logs", ui.HLClient.DebugBuf.TextView, true)
464 }
465 return event
466 })
467
468 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
12ace83f
JH
469 ui.App.Stop()
470 os.Exit(1)
6988a057
JH
471 }
472}
473
474func NewClient(username string, logger *zap.SugaredLogger) *Client {
475 c := &Client{
476 Icon: &[]byte{0x07, 0xd7},
477 Logger: logger,
478 activeTasks: make(map[uint32]*Transaction),
479 Handlers: clientHandlers,
480 }
481 c.UI = NewUI(c)
482
483 prefs, err := readConfig(clientConfigPath)
484 if err != nil {
485 return c
486 }
487 c.pref = prefs
488
489 return c
490}
491
492type clientTransaction struct {
493 Name string
494 Handler func(*Client, *Transaction) ([]Transaction, error)
495}
496
497func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
498 return ch.Handler(cc, t)
499}
500
501type clientTHandler interface {
502 Handle(*Client, *Transaction) ([]Transaction, error)
503}
504
505type mockClientHandler struct {
506 mock.Mock
507}
508
509func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
510 args := mh.Called(cc, t)
511 return args.Get(0).([]Transaction), args.Error(1)
512}
513
514var clientHandlers = map[uint16]clientTHandler{
515 // Server initiated
516 tranChatMsg: clientTransaction{
517 Name: "tranChatMsg",
518 Handler: handleClientChatMsg,
519 },
520 tranLogin: clientTransaction{
521 Name: "tranLogin",
522 Handler: handleClientTranLogin,
523 },
524 tranShowAgreement: clientTransaction{
525 Name: "tranShowAgreement",
526 Handler: handleClientTranShowAgreement,
527 },
528 tranUserAccess: clientTransaction{
529 Name: "tranUserAccess",
530 Handler: handleClientTranUserAccess,
531 },
532 tranGetUserNameList: clientTransaction{
533 Name: "tranGetUserNameList",
534 Handler: handleClientGetUserNameList,
535 },
536 tranNotifyChangeUser: clientTransaction{
537 Name: "tranNotifyChangeUser",
538 Handler: handleNotifyChangeUser,
539 },
540 tranNotifyDeleteUser: clientTransaction{
541 Name: "tranNotifyDeleteUser",
542 Handler: handleNotifyDeleteUser,
543 },
544 tranGetMsgs: clientTransaction{
545 Name: "tranNotifyDeleteUser",
546 Handler: handleGetMsgs,
547 },
548}
549
550func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
551 newsText := string(t.GetField(fieldData).Data)
552 newsText = strings.ReplaceAll(newsText, "\r", "\n")
553
554 newsTextView := tview.NewTextView().
555 SetText(newsText).
556 SetDoneFunc(func(key tcell.Key) {
557 c.UI.Pages.SwitchToPage("serverUI")
558 c.UI.App.SetFocus(c.UI.chatInput)
559 })
560 newsTextView.SetBorder(true).SetTitle("News")
561
562 c.UI.Pages.AddPage("news", newsTextView, true, true)
563 c.UI.Pages.SwitchToPage("news")
564 c.UI.App.SetFocus(newsTextView)
565
566 c.UI.App.Draw()
567
568 return res, err
569}
570
571func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
572 newUser := User{
573 ID: t.GetField(fieldUserID).Data,
574 Name: string(t.GetField(fieldUserName).Data),
575 Icon: t.GetField(fieldUserIconID).Data,
576 Flags: t.GetField(fieldUserFlags).Data,
577 }
578
579 // Possible cases:
580 // user is new to the server
581 // user is already on the server but has a new name
582
583 var oldName string
584 var newUserList []User
585 updatedUser := false
586 for _, u := range c.UserList {
587 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
588 if bytes.Equal(newUser.ID, u.ID) {
589 oldName = u.Name
590 u.Name = newUser.Name
591 if u.Name != newUser.Name {
592 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
593 }
594 updatedUser = true
595 }
596 newUserList = append(newUserList, u)
597 }
598
599 if !updatedUser {
600 newUserList = append(newUserList, newUser)
601 }
602
603 c.UserList = newUserList
604
605 c.renderUserList()
606
607 return res, err
608}
609
610func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
611 exitUser := t.GetField(fieldUserID).Data
612
613 var newUserList []User
614 for _, u := range c.UserList {
615 if !bytes.Equal(exitUser, u.ID) {
616 newUserList = append(newUserList, u)
617 }
618 }
619
620 c.UserList = newUserList
621
622 c.renderUserList()
623
624 return res, err
625}
626
627const readBuffSize = 1024000 // 1KB - TODO: what should this be?
628
629func (c *Client) ReadLoop() error {
630 tranBuff := make([]byte, 0)
631 tReadlen := 0
632 // Infinite loop where take action on incoming client requests until the connection is closed
633 for {
634 buf := make([]byte, readBuffSize)
635 tranBuff = tranBuff[tReadlen:]
636
637 readLen, err := c.Connection.Read(buf)
638 if err != nil {
639 return err
640 }
641 tranBuff = append(tranBuff, buf[:readLen]...)
642
643 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
644 // into a slice of transactions
645 var transactions []Transaction
646 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
647 c.Logger.Errorw("Error handling transaction", "err", err)
648 }
649
650 // iterate over all of the transactions that were parsed from the byte slice and handle them
651 for _, t := range transactions {
652 if err := c.HandleTransaction(&t); err != nil {
653 c.Logger.Errorw("Error handling transaction", "err", err)
654 }
655 }
656 }
657}
658
659func (c *Client) GetTransactions() error {
660 tranBuff := make([]byte, 0)
661 tReadlen := 0
662
663 buf := make([]byte, readBuffSize)
664 tranBuff = tranBuff[tReadlen:]
665
666 readLen, err := c.Connection.Read(buf)
667 if err != nil {
668 return err
669 }
670 tranBuff = append(tranBuff, buf[:readLen]...)
671
672 return nil
673}
674
675func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
676 var users []User
677 for _, field := range t.Fields {
678 u, _ := ReadUser(field.Data)
679 //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
680 //if flagBitmap.Bit(userFlagAdmin) == 1 {
681 // fmt.Fprintf(UserList, "[red::b]%s[-:-:-]\n", u.Name)
682 //} else {
683 // fmt.Fprintf(UserList, "%s\n", u.Name)
684 //}
685
686 users = append(users, *u)
687 }
688 c.UserList = users
689
690 c.renderUserList()
691
692 return res, err
693}
694
695func (c *Client) renderUserList() {
696 c.UI.userList.Clear()
697 for _, u := range c.UserList {
698 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
699 if flagBitmap.Bit(userFlagAdmin) == 1 {
700 fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
701 } else {
702 fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
703 }
704 }
705}
706
707func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
708 fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
709
710 return res, err
711}
712
713func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
714 c.UserAccess = t.GetField(fieldUserAccess).Data
715
716 return res, err
717}
718
719func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
720 agreement := string(t.GetField(fieldData).Data)
721 agreement = strings.ReplaceAll(agreement, "\r", "\n")
722
723 c.UI.agreeModal = tview.NewModal().
724 SetText(agreement).
725 AddButtons([]string{"Agree", "Disagree"}).
726 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
727 if buttonIndex == 0 {
728 res = append(res,
729 *NewTransaction(
730 tranAgreed, nil,
731 NewField(fieldUserName, []byte(c.pref.Username)),
732 NewField(fieldUserIconID, *c.Icon),
733 NewField(fieldUserFlags, []byte{0x00, 0x00}),
734 NewField(fieldOptions, []byte{0x00, 0x00}),
735 ),
736 )
737 c.Agreed = true
738 c.UI.Pages.HidePage("agreement")
739 c.UI.App.SetFocus(c.UI.chatInput)
740 } else {
741 c.Disconnect()
742 c.UI.Pages.SwitchToPage("home")
743 }
744 },
745 )
746
747 c.Logger.Debug("show agreement page")
748 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
749
750 c.UI.Pages.ShowPage("agreement ")
751
752 c.UI.App.Draw()
753 return res, err
754}
755
756func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
757 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
758 errMsg := string(t.GetField(fieldError).Data)
759 errModal := tview.NewModal()
760 errModal.SetText(errMsg)
761 errModal.AddButtons([]string{"Oh no"})
762 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
763 c.UI.Pages.RemovePage("errModal")
764 })
765 c.UI.Pages.RemovePage("joinServer")
766 c.UI.Pages.AddPage("errModal", errModal, false, true)
767
768 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
769
770 c.Logger.Error(string(t.GetField(fieldError).Data))
771 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
772 }
773 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
774 c.UI.App.SetFocus(c.UI.chatInput)
775
776 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
777 c.Logger.Errorw("err", "err", err)
778 }
779 return res, err
780}
781
782// JoinServer connects to a Hotline server and completes the login flow
783func (c *Client) JoinServer(address, login, passwd string) error {
784 // Establish TCP connection to server
785 if err := c.connect(address); err != nil {
786 return err
787 }
788
789 // Send handshake sequence
790 if err := c.Handshake(); err != nil {
791 return err
792 }
793
794 // Authenticate (send tranLogin 107)
795 if err := c.LogIn(login, passwd); err != nil {
796 return err
797 }
798
799 return nil
800}
801
802// connect establishes a connection with a Server by sending handshake sequence
803func (c *Client) connect(address string) error {
804 var err error
805 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
806 if err != nil {
807 return err
808 }
809 return nil
810}
811
812var ClientHandshake = []byte{
813 0x54, 0x52, 0x54, 0x50, // TRTP
814 0x48, 0x4f, 0x54, 0x4c, // HOTL
815 0x00, 0x01,
816 0x00, 0x02,
817}
818
819var ServerHandshake = []byte{
820 0x54, 0x52, 0x54, 0x50, // TRTP
821 0x00, 0x00, 0x00, 0x00, // ErrorCode
822}
823
824func (c *Client) Handshake() error {
825 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
826 //Sub-protocol ID 4 User defined
827 //Version 2 1 Currently 1
828 //Sub-version 2 User defined
829 if _, err := c.Connection.Write(ClientHandshake); err != nil {
830 return fmt.Errorf("handshake write err: %s", err)
831 }
832
833 replyBuf := make([]byte, 8)
834 _, err := c.Connection.Read(replyBuf)
835 if err != nil {
836 return err
837 }
838
839 //spew.Dump(replyBuf)
840 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
841 return nil
842 }
843 // In the case of an error, client and server close the connection.
844
845 return fmt.Errorf("handshake response err: %s", err)
846}
847
848func (c *Client) LogIn(login string, password string) error {
849 return c.Send(
850 *NewTransaction(
851 tranLogin, nil,
852 NewField(fieldUserName, []byte(c.pref.Username)),
853 NewField(fieldUserIconID, []byte{0x07, 0xd1}),
854 NewField(fieldUserLogin, []byte(NegatedUserString([]byte(login)))),
855 NewField(fieldUserPassword, []byte(NegatedUserString([]byte(password)))),
856 NewField(fieldVersion, []byte{0, 2}),
857 ),
858 )
859}
860
6988a057
JH
861func (c *Client) Send(t Transaction) error {
862 requestNum := binary.BigEndian.Uint16(t.Type)
863 tID := binary.BigEndian.Uint32(t.ID)
864
865 //handler := TransactionHandlers[requestNum]
866
867 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
868 if t.IsReply == 0 {
869 c.activeTasks[tID] = &t
870 }
871
872 var n int
873 var err error
874 if n, err = c.Connection.Write(t.Payload()); err != nil {
875 return err
876 }
877 c.Logger.Debugw("Sent Transaction",
878 "IsReply", t.IsReply,
879 "type", requestNum,
880 "sentBytes", n,
881 )
882 return nil
883}
884
885func (c *Client) HandleTransaction(t *Transaction) error {
886 var origT Transaction
887 if t.IsReply == 1 {
888 requestID := binary.BigEndian.Uint32(t.ID)
889 origT = *c.activeTasks[requestID]
890 t.Type = origT.Type
891 }
892
893 requestNum := binary.BigEndian.Uint16(t.Type)
894 c.Logger.Infow(
895 "Received Transaction",
896 "RequestType", requestNum,
897 )
898
899 if handler, ok := c.Handlers[requestNum]; ok {
900 outT, _ := handler.Handle(c, t)
901 for _, t := range outT {
902 c.Send(t)
903 }
904 } else {
905 c.Logger.Errorw(
906 "Unimplemented transaction type received",
907 "RequestID", requestNum,
908 "TransactionID", t.ID,
909 )
910 }
911
912 return nil
913}
914
915func (c *Client) Connected() bool {
916 fmt.Printf("Agreed: %v UserAccess: %v\n", c.Agreed, c.UserAccess)
917 // c.Agreed == true &&
918 if c.UserAccess != nil {
919 return true
920 }
921 return false
922}
923
924func (c *Client) Disconnect() error {
925 err := c.Connection.Close()
926 if err != nil {
927 return err
928 }
929 return nil
930}