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