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