]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client.go
Prune unused code
[rbdr/mobius] / hotline / client.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
4 "bytes"
5 "embed"
6 "encoding/binary"
7 "errors"
8 "fmt"
6988a057
JH
9 "github.com/gdamore/tcell/v2"
10 "github.com/rivo/tview"
11 "github.com/stretchr/testify/mock"
12 "go.uber.org/zap"
13 "gopkg.in/yaml.v2"
6988a057
JH
14 "math/big"
15 "math/rand"
16 "net"
17 "os"
18 "strings"
19 "time"
20)
21
4f3c459c
JH
22const (
23 trackerListPage = "trackerList"
e005c191 24 serverUIPage = "serverUI"
4f3c459c 25)
6988a057 26
22c599ab 27//go:embed banners/*.txt
6988a057
JH
28var bannerDir embed.FS
29
30type Bookmark struct {
31 Name string `yaml:"Name"`
32 Addr string `yaml:"Addr"`
33 Login string `yaml:"Login"`
34 Password string `yaml:"Password"`
35}
36
37type ClientPrefs struct {
38 Username string `yaml:"Username"`
39 IconID int `yaml:"IconID"`
40 Bookmarks []Bookmark `yaml:"Bookmarks"`
4f3c459c 41 Tracker string `yaml:"Tracker"`
6988a057
JH
42}
43
f7e36225
JH
44func (cp *ClientPrefs) IconBytes() []byte {
45 iconBytes := make([]byte, 2)
46 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
47 return iconBytes
48}
49
da1e0d79
JH
50func (cp *ClientPrefs) AddBookmark(name, addr, login, pass string) error {
51 cp.Bookmarks = append(cp.Bookmarks, Bookmark{Addr: addr, Login: login, Password: pass})
52
53 return nil
54}
55
6988a057
JH
56func readConfig(cfgPath string) (*ClientPrefs, error) {
57 fh, err := os.Open(cfgPath)
58 if err != nil {
59 return nil, err
60 }
61
62 prefs := ClientPrefs{}
63 decoder := yaml.NewDecoder(fh)
64 decoder.SetStrict(true)
65 if err := decoder.Decode(&prefs); err != nil {
66 return nil, err
67 }
68 return &prefs, nil
69}
70
71type Client struct {
95753255 72 cfgPath string
6988a057
JH
73 DebugBuf *DebugBuffer
74 Connection net.Conn
6988a057
JH
75 Login *[]byte
76 Password *[]byte
6988a057
JH
77 Flags *[]byte
78 ID *[]byte
79 Version []byte
80 UserAccess []byte
43ecc0f4 81 filePath []string
6988a057
JH
82 UserList []User
83 Logger *zap.SugaredLogger
84 activeTasks map[uint32]*Transaction
e005c191 85 serverName string
6988a057
JH
86
87 pref *ClientPrefs
88
89 Handlers map[uint16]clientTHandler
90
91 UI *UI
92
6988a057
JH
93 Inbox chan *Transaction
94}
95
b198b22b
JH
96func NewClient(cfgPath string, logger *zap.SugaredLogger) *Client {
97 c := &Client{
98 cfgPath: cfgPath,
99 Logger: logger,
100 activeTasks: make(map[uint32]*Transaction),
101 Handlers: clientHandlers,
6988a057 102 }
b198b22b 103 c.UI = NewUI(c)
6988a057 104
b198b22b 105 prefs, err := readConfig(cfgPath)
6988a057 106 if err != nil {
43ecc0f4
JH
107 fmt.Printf("unable to read config file %s", cfgPath)
108 os.Exit(1)
6988a057 109 }
b198b22b 110 c.pref = prefs
6988a057 111
b198b22b 112 return c
6988a057
JH
113}
114
6988a057
JH
115// DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
116type DebugBuffer struct {
117 TextView *tview.TextView
118}
119
120func (db *DebugBuffer) Write(p []byte) (int, error) {
121 return db.TextView.Write(p)
122}
123
124// Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
125func (db *DebugBuffer) Sync() error {
126 return nil
127}
128
6988a057
JH
129func randomBanner() string {
130 rand.Seed(time.Now().UnixNano())
131
ce348eb8
JH
132 bannerFiles, _ := bannerDir.ReadDir("banners")
133 file, _ := bannerDir.ReadFile("banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
6988a057
JH
134
135 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
136}
137
6988a057
JH
138type clientTransaction struct {
139 Name string
140 Handler func(*Client, *Transaction) ([]Transaction, error)
141}
142
143func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
144 return ch.Handler(cc, t)
145}
146
147type clientTHandler interface {
148 Handle(*Client, *Transaction) ([]Transaction, error)
149}
150
151type mockClientHandler struct {
152 mock.Mock
153}
154
155func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
156 args := mh.Called(cc, t)
157 return args.Get(0).([]Transaction), args.Error(1)
158}
159
160var clientHandlers = map[uint16]clientTHandler{
161 // Server initiated
162 tranChatMsg: clientTransaction{
163 Name: "tranChatMsg",
164 Handler: handleClientChatMsg,
165 },
166 tranLogin: clientTransaction{
167 Name: "tranLogin",
168 Handler: handleClientTranLogin,
169 },
170 tranShowAgreement: clientTransaction{
171 Name: "tranShowAgreement",
172 Handler: handleClientTranShowAgreement,
173 },
174 tranUserAccess: clientTransaction{
175 Name: "tranUserAccess",
176 Handler: handleClientTranUserAccess,
177 },
178 tranGetUserNameList: clientTransaction{
179 Name: "tranGetUserNameList",
180 Handler: handleClientGetUserNameList,
181 },
182 tranNotifyChangeUser: clientTransaction{
183 Name: "tranNotifyChangeUser",
184 Handler: handleNotifyChangeUser,
185 },
186 tranNotifyDeleteUser: clientTransaction{
187 Name: "tranNotifyDeleteUser",
188 Handler: handleNotifyDeleteUser,
189 },
190 tranGetMsgs: clientTransaction{
191 Name: "tranNotifyDeleteUser",
192 Handler: handleGetMsgs,
193 },
43ecc0f4
JH
194 tranGetFileNameList: clientTransaction{
195 Name: "tranGetFileNameList",
196 Handler: handleGetFileNameList,
197 },
3d2bd095
JH
198 tranServerMsg: clientTransaction{
199 Name: "tranServerMsg",
200 Handler: handleTranServerMsg,
201 },
202}
203
204func handleTranServerMsg(c *Client, t *Transaction) (res []Transaction, err error) {
205 time := time.Now().Format(time.RFC850)
206
207 msg := strings.ReplaceAll(string(t.GetField(fieldData).Data), "\r", "\n")
5c34f875 208 msg += "\n\nAt " + time
3d2bd095
JH
209 title := fmt.Sprintf("| Private Message From: %s |", t.GetField(fieldUserName).Data)
210
211 msgBox := tview.NewTextView().SetScrollable(true)
212 msgBox.SetText(msg).SetBackgroundColor(tcell.ColorDarkSlateBlue)
213 msgBox.SetTitle(title).SetBorder(true)
214 msgBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
215 switch event.Key() {
216 case tcell.KeyEscape:
217 c.UI.Pages.RemovePage("serverMsgModal" + time)
218 }
219 return event
220 })
221
222 centeredFlex := tview.NewFlex().
223 AddItem(nil, 0, 1, false).
224 AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
225 AddItem(nil, 0, 1, false).
226 AddItem(msgBox, 0, 2, true).
227 AddItem(nil, 0, 1, false), 0, 2, true).
228 AddItem(nil, 0, 1, false)
229
5c34f875 230 c.UI.Pages.AddPage("serverMsgModal"+time, centeredFlex, true, true)
3d2bd095
JH
231 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
232
233 return res, err
43ecc0f4
JH
234}
235
236func handleGetFileNameList(c *Client, t *Transaction) (res []Transaction, err error) {
237 fTree := tview.NewTreeView().SetTopLevel(1)
238 root := tview.NewTreeNode("Root")
239 fTree.SetRoot(root).SetCurrentNode(root)
240 fTree.SetBorder(true).SetTitle("| Files |")
241 fTree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
242 switch event.Key() {
243 case tcell.KeyEscape:
244 c.UI.Pages.RemovePage("files")
245 c.filePath = []string{}
246 case tcell.KeyEnter:
247 selectedNode := fTree.GetCurrentNode()
248
249 if selectedNode.GetText() == "<- Back" {
250 c.filePath = c.filePath[:len(c.filePath)-1]
251 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
252
253 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
254 c.UI.HLClient.Logger.Errorw("err", "err", err)
255 }
256 return event
257 }
258
259 entry := selectedNode.GetReference().(*FileNameWithInfo)
260
261 if bytes.Equal(entry.Type, []byte("fldr")) {
262 c.Logger.Infow("get new directory listing", "name", string(entry.Name))
263
264 c.filePath = append(c.filePath, string(entry.Name))
265 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
266
267 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
268 c.UI.HLClient.Logger.Errorw("err", "err", err)
269 }
270 } else {
271 // TODO: initiate file download
272 c.Logger.Infow("download file", "name", string(entry.Name))
273 }
274 }
275
276 return event
277 })
278
279 if len(c.filePath) > 0 {
280 node := tview.NewTreeNode("<- Back")
281 root.AddChild(node)
282 }
283
43ecc0f4
JH
284 for _, f := range t.Fields {
285 var fn FileNameWithInfo
286 _, _ = fn.Read(f.Data)
43ecc0f4
JH
287
288 if bytes.Equal(fn.Type, []byte("fldr")) {
289 node := tview.NewTreeNode(fmt.Sprintf("[blue::]📁 %s[-:-:-]", fn.Name))
290 node.SetReference(&fn)
291 root.AddChild(node)
292 } else {
293 size := binary.BigEndian.Uint32(fn.FileSize) / 1024
294
4c3b4c7f 295 node := tview.NewTreeNode(fmt.Sprintf(" %-40s %10v KB", fn.Name, size))
43ecc0f4
JH
296 node.SetReference(&fn)
297 root.AddChild(node)
298 }
299
300 }
301
302 centerFlex := tview.NewFlex().
303 AddItem(nil, 0, 1, false).
304 AddItem(tview.NewFlex().
305 SetDirection(tview.FlexRow).
306 AddItem(nil, 0, 1, false).
307 AddItem(fTree, 20, 1, true).
246ed3a1 308 AddItem(nil, 0, 1, false), 60, 1, true).
43ecc0f4
JH
309 AddItem(nil, 0, 1, false)
310
311 c.UI.Pages.AddPage("files", centerFlex, true, true)
312 c.UI.App.Draw()
313
314 return res, err
6988a057
JH
315}
316
317func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
318 newsText := string(t.GetField(fieldData).Data)
319 newsText = strings.ReplaceAll(newsText, "\r", "\n")
320
321 newsTextView := tview.NewTextView().
322 SetText(newsText).
323 SetDoneFunc(func(key tcell.Key) {
40afb444 324 c.UI.Pages.SwitchToPage(serverUIPage)
6988a057
JH
325 c.UI.App.SetFocus(c.UI.chatInput)
326 })
327 newsTextView.SetBorder(true).SetTitle("News")
328
329 c.UI.Pages.AddPage("news", newsTextView, true, true)
43ecc0f4
JH
330 //c.UI.Pages.SwitchToPage("news")
331 //c.UI.App.SetFocus(newsTextView)
6988a057
JH
332 c.UI.App.Draw()
333
334 return res, err
335}
336
337func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
338 newUser := User{
339 ID: t.GetField(fieldUserID).Data,
340 Name: string(t.GetField(fieldUserName).Data),
341 Icon: t.GetField(fieldUserIconID).Data,
342 Flags: t.GetField(fieldUserFlags).Data,
343 }
344
345 // Possible cases:
346 // user is new to the server
347 // user is already on the server but has a new name
348
349 var oldName string
350 var newUserList []User
351 updatedUser := false
352 for _, u := range c.UserList {
353 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
354 if bytes.Equal(newUser.ID, u.ID) {
355 oldName = u.Name
356 u.Name = newUser.Name
357 if u.Name != newUser.Name {
358 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
359 }
360 updatedUser = true
361 }
362 newUserList = append(newUserList, u)
363 }
364
365 if !updatedUser {
366 newUserList = append(newUserList, newUser)
367 }
368
369 c.UserList = newUserList
370
371 c.renderUserList()
372
373 return res, err
374}
375
376func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
377 exitUser := t.GetField(fieldUserID).Data
378
379 var newUserList []User
380 for _, u := range c.UserList {
381 if !bytes.Equal(exitUser, u.ID) {
382 newUserList = append(newUserList, u)
383 }
384 }
385
386 c.UserList = newUserList
387
388 c.renderUserList()
389
390 return res, err
391}
392
393const readBuffSize = 1024000 // 1KB - TODO: what should this be?
394
395func (c *Client) ReadLoop() error {
396 tranBuff := make([]byte, 0)
397 tReadlen := 0
398 // Infinite loop where take action on incoming client requests until the connection is closed
399 for {
400 buf := make([]byte, readBuffSize)
401 tranBuff = tranBuff[tReadlen:]
402
403 readLen, err := c.Connection.Read(buf)
404 if err != nil {
405 return err
406 }
407 tranBuff = append(tranBuff, buf[:readLen]...)
408
409 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
410 // into a slice of transactions
411 var transactions []Transaction
412 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
413 c.Logger.Errorw("Error handling transaction", "err", err)
414 }
415
416 // iterate over all of the transactions that were parsed from the byte slice and handle them
417 for _, t := range transactions {
418 if err := c.HandleTransaction(&t); err != nil {
419 c.Logger.Errorw("Error handling transaction", "err", err)
420 }
421 }
422 }
423}
424
425func (c *Client) GetTransactions() error {
426 tranBuff := make([]byte, 0)
427 tReadlen := 0
428
429 buf := make([]byte, readBuffSize)
430 tranBuff = tranBuff[tReadlen:]
431
432 readLen, err := c.Connection.Read(buf)
433 if err != nil {
434 return err
435 }
436 tranBuff = append(tranBuff, buf[:readLen]...)
437
438 return nil
439}
440
441func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
442 var users []User
443 for _, field := range t.Fields {
71c56068
JH
444 // The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
445 // fields, but shxd sneaks in fieldChatSubject (115) so it's important to filter explicitly for the expected
446 // field type. Probably a good idea to do everywhere.
447 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
448 u, err := ReadUser(field.Data)
449 if err != nil {
450 return res, err
451 }
452 users = append(users, *u)
453 }
6988a057
JH
454 }
455 c.UserList = users
456
457 c.renderUserList()
458
459 return res, err
460}
461
462func (c *Client) renderUserList() {
463 c.UI.userList.Clear()
464 for _, u := range c.UserList {
465 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
466 if flagBitmap.Bit(userFlagAdmin) == 1 {
5dd57308 467 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
6988a057 468 } else {
5dd57308 469 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
6988a057 470 }
b198b22b 471 // TODO: fade if user is away
6988a057
JH
472 }
473}
474
475func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
5dd57308 476 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
6988a057
JH
477
478 return res, err
479}
480
481func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
482 c.UserAccess = t.GetField(fieldUserAccess).Data
483
484 return res, err
485}
486
487func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
488 agreement := string(t.GetField(fieldData).Data)
489 agreement = strings.ReplaceAll(agreement, "\r", "\n")
490
491 c.UI.agreeModal = tview.NewModal().
492 SetText(agreement).
493 AddButtons([]string{"Agree", "Disagree"}).
494 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
495 if buttonIndex == 0 {
496 res = append(res,
497 *NewTransaction(
498 tranAgreed, nil,
499 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 500 NewField(fieldUserIconID, c.pref.IconBytes()),
6988a057
JH
501 NewField(fieldUserFlags, []byte{0x00, 0x00}),
502 NewField(fieldOptions, []byte{0x00, 0x00}),
503 ),
504 )
6988a057
JH
505 c.UI.Pages.HidePage("agreement")
506 c.UI.App.SetFocus(c.UI.chatInput)
507 } else {
f7e36225 508 _ = c.Disconnect()
6988a057
JH
509 c.UI.Pages.SwitchToPage("home")
510 }
511 },
512 )
513
514 c.Logger.Debug("show agreement page")
515 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
6988a057 516 c.UI.Pages.ShowPage("agreement ")
6988a057 517 c.UI.App.Draw()
b198b22b 518
6988a057
JH
519 return res, err
520}
521
522func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
523 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
524 errMsg := string(t.GetField(fieldError).Data)
525 errModal := tview.NewModal()
526 errModal.SetText(errMsg)
527 errModal.AddButtons([]string{"Oh no"})
528 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
529 c.UI.Pages.RemovePage("errModal")
530 })
531 c.UI.Pages.RemovePage("joinServer")
532 c.UI.Pages.AddPage("errModal", errModal, false, true)
533
534 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
535
536 c.Logger.Error(string(t.GetField(fieldError).Data))
537 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
538 }
40afb444 539 c.UI.Pages.AddAndSwitchToPage(serverUIPage, c.UI.renderServerUI(), true)
6988a057
JH
540 c.UI.App.SetFocus(c.UI.chatInput)
541
542 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
543 c.Logger.Errorw("err", "err", err)
544 }
545 return res, err
546}
547
548// JoinServer connects to a Hotline server and completes the login flow
549func (c *Client) JoinServer(address, login, passwd string) error {
550 // Establish TCP connection to server
551 if err := c.connect(address); err != nil {
552 return err
553 }
554
555 // Send handshake sequence
556 if err := c.Handshake(); err != nil {
557 return err
558 }
559
560 // Authenticate (send tranLogin 107)
561 if err := c.LogIn(login, passwd); err != nil {
562 return err
563 }
564
565 return nil
566}
567
568// connect establishes a connection with a Server by sending handshake sequence
569func (c *Client) connect(address string) error {
570 var err error
571 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
572 if err != nil {
573 return err
574 }
575 return nil
576}
577
578var ClientHandshake = []byte{
579 0x54, 0x52, 0x54, 0x50, // TRTP
580 0x48, 0x4f, 0x54, 0x4c, // HOTL
581 0x00, 0x01,
582 0x00, 0x02,
583}
584
585var ServerHandshake = []byte{
586 0x54, 0x52, 0x54, 0x50, // TRTP
587 0x00, 0x00, 0x00, 0x00, // ErrorCode
588}
589
590func (c *Client) Handshake() error {
591 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
592 //Sub-protocol ID 4 User defined
593 //Version 2 1 Currently 1
594 //Sub-version 2 User defined
595 if _, err := c.Connection.Write(ClientHandshake); err != nil {
596 return fmt.Errorf("handshake write err: %s", err)
597 }
598
599 replyBuf := make([]byte, 8)
600 _, err := c.Connection.Read(replyBuf)
601 if err != nil {
602 return err
603 }
604
6988a057
JH
605 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
606 return nil
607 }
6988a057 608
b198b22b 609 // In the case of an error, client and server close the connection.
6988a057
JH
610 return fmt.Errorf("handshake response err: %s", err)
611}
612
613func (c *Client) LogIn(login string, password string) error {
614 return c.Send(
615 *NewTransaction(
616 tranLogin, nil,
617 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 618 NewField(fieldUserIconID, c.pref.IconBytes()),
b25c4a19
JH
619 NewField(fieldUserLogin, negateString([]byte(login))),
620 NewField(fieldUserPassword, negateString([]byte(password))),
6988a057
JH
621 NewField(fieldVersion, []byte{0, 2}),
622 ),
623 )
624}
625
6988a057
JH
626func (c *Client) Send(t Transaction) error {
627 requestNum := binary.BigEndian.Uint16(t.Type)
628 tID := binary.BigEndian.Uint32(t.ID)
629
630 //handler := TransactionHandlers[requestNum]
631
632 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
633 if t.IsReply == 0 {
634 c.activeTasks[tID] = &t
635 }
636
637 var n int
638 var err error
639 if n, err = c.Connection.Write(t.Payload()); err != nil {
640 return err
641 }
642 c.Logger.Debugw("Sent Transaction",
643 "IsReply", t.IsReply,
644 "type", requestNum,
645 "sentBytes", n,
646 )
647 return nil
648}
649
650func (c *Client) HandleTransaction(t *Transaction) error {
651 var origT Transaction
652 if t.IsReply == 1 {
653 requestID := binary.BigEndian.Uint32(t.ID)
654 origT = *c.activeTasks[requestID]
655 t.Type = origT.Type
656 }
657
658 requestNum := binary.BigEndian.Uint16(t.Type)
659 c.Logger.Infow(
660 "Received Transaction",
661 "RequestType", requestNum,
662 )
663
664 if handler, ok := c.Handlers[requestNum]; ok {
665 outT, _ := handler.Handle(c, t)
666 for _, t := range outT {
667 c.Send(t)
668 }
669 } else {
670 c.Logger.Errorw(
671 "Unimplemented transaction type received",
672 "RequestID", requestNum,
673 "TransactionID", t.ID,
674 )
675 }
676
677 return nil
678}
679
680func (c *Client) Connected() bool {
6988a057
JH
681 // c.Agreed == true &&
682 if c.UserAccess != nil {
683 return true
684 }
685 return false
686}
687
688func (c *Client) Disconnect() error {
689 err := c.Connection.Close()
690 if err != nil {
691 return err
692 }
693 return nil
694}