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