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