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