|
@@ -36,11 +36,8 @@ const (
|
36
|
36
|
// the resume process: when handling the RESUME command itself,
|
37
|
37
|
// when completing the registration, and when rejoining channels.
|
38
|
38
|
type ResumeDetails struct {
|
39
|
|
- OldClient *Client
|
40
|
39
|
PresentedToken string
|
41
|
40
|
Timestamp time.Time
|
42
|
|
- ResumedAt time.Time
|
43
|
|
- Channels []string
|
44
|
41
|
HistoryIncomplete bool
|
45
|
42
|
}
|
46
|
43
|
|
|
@@ -52,6 +49,7 @@ type Client struct {
|
52
|
49
|
atime time.Time
|
53
|
50
|
away bool
|
54
|
51
|
awayMessage string
|
|
52
|
+ brbTimer BrbTimer
|
55
|
53
|
certfp string
|
56
|
54
|
channels ChannelSet
|
57
|
55
|
ctime time.Time
|
|
@@ -75,7 +73,6 @@ type Client struct {
|
75
|
73
|
realname string
|
76
|
74
|
realIP net.IP
|
77
|
75
|
registered bool
|
78
|
|
- resumeDetails *ResumeDetails
|
79
|
76
|
resumeID string
|
80
|
77
|
saslInProgress bool
|
81
|
78
|
saslMechanism string
|
|
@@ -87,7 +84,7 @@ type Client struct {
|
87
|
84
|
stateMutex sync.RWMutex // tier 1
|
88
|
85
|
username string
|
89
|
86
|
vhost string
|
90
|
|
- history *history.Buffer
|
|
87
|
+ history history.Buffer
|
91
|
88
|
}
|
92
|
89
|
|
93
|
90
|
// Session is an individual client connection to the server (TCP connection
|
|
@@ -113,6 +110,9 @@ type Session struct {
|
113
|
110
|
maxlenRest uint32
|
114
|
111
|
capState caps.State
|
115
|
112
|
capVersion caps.Version
|
|
113
|
+
|
|
114
|
+ resumeID string
|
|
115
|
+ resumeDetails *ResumeDetails
|
116
|
116
|
}
|
117
|
117
|
|
118
|
118
|
// sets the session quit message, if there isn't one already
|
|
@@ -207,8 +207,9 @@ func (server *Server) RunClient(conn clientConn) {
|
207
|
207
|
nick: "*", // * is used until actual nick is given
|
208
|
208
|
nickCasefolded: "*",
|
209
|
209
|
nickMaskString: "*", // * is used until actual nick is given
|
210
|
|
- history: history.NewHistoryBuffer(config.History.ClientLength),
|
211
|
210
|
}
|
|
211
|
+ client.history.Initialize(config.History.ClientLength)
|
|
212
|
+ client.brbTimer.Initialize(client)
|
212
|
213
|
session := &Session{
|
213
|
214
|
client: client,
|
214
|
215
|
socket: socket,
|
|
@@ -339,7 +340,7 @@ func (client *Client) run(session *Session) {
|
339
|
340
|
}
|
340
|
341
|
}
|
341
|
342
|
// ensure client connection gets closed
|
342
|
|
- client.destroy(false, session)
|
|
343
|
+ client.destroy(session)
|
343
|
344
|
}()
|
344
|
345
|
|
345
|
346
|
session.idletimer.Initialize(session)
|
|
@@ -347,7 +348,13 @@ func (client *Client) run(session *Session) {
|
347
|
348
|
|
348
|
349
|
isReattach := client.Registered()
|
349
|
350
|
if isReattach {
|
350
|
|
- client.playReattachMessages(session)
|
|
351
|
+ if session.resumeDetails != nil {
|
|
352
|
+ session.playResume()
|
|
353
|
+ session.resumeDetails = nil
|
|
354
|
+ client.brbTimer.Disable()
|
|
355
|
+ } else {
|
|
356
|
+ client.playReattachMessages(session)
|
|
357
|
+ }
|
351
|
358
|
} else {
|
352
|
359
|
// don't reset the nick timer during a reattach
|
353
|
360
|
client.nickTimer.Initialize(client)
|
|
@@ -365,6 +372,9 @@ func (client *Client) run(session *Session) {
|
365
|
372
|
quitMessage = "readQ exceeded"
|
366
|
373
|
}
|
367
|
374
|
client.Quit(quitMessage, session)
|
|
375
|
+ // since the client did not actually send us a QUIT,
|
|
376
|
+ // give them a chance to resume or reattach if applicable:
|
|
377
|
+ client.brbTimer.Enable()
|
368
|
378
|
break
|
369
|
379
|
}
|
370
|
380
|
|
|
@@ -443,83 +453,66 @@ func (session *Session) Ping() {
|
443
|
453
|
}
|
444
|
454
|
|
445
|
455
|
// tryResume tries to resume if the client asked us to.
|
446
|
|
-func (client *Client) tryResume() (success bool) {
|
447
|
|
- server := client.server
|
448
|
|
- config := server.Config()
|
|
456
|
+func (session *Session) tryResume() (success bool) {
|
|
457
|
+ var oldResumeID string
|
449
|
458
|
|
450
|
459
|
defer func() {
|
451
|
|
- if !success {
|
452
|
|
- client.resumeDetails = nil
|
|
460
|
+ if success {
|
|
461
|
+ // "On a successful request, the server [...] terminates the old client's connection"
|
|
462
|
+ oldSession := session.client.GetSessionByResumeID(oldResumeID)
|
|
463
|
+ if oldSession != nil {
|
|
464
|
+ session.client.destroy(oldSession)
|
|
465
|
+ }
|
|
466
|
+ } else {
|
|
467
|
+ session.resumeDetails = nil
|
453
|
468
|
}
|
454
|
469
|
}()
|
455
|
470
|
|
456
|
|
- timestamp := client.resumeDetails.Timestamp
|
457
|
|
- var timestampString string
|
458
|
|
- if !timestamp.IsZero() {
|
459
|
|
- timestampString = timestamp.UTC().Format(IRCv3TimestampFormat)
|
460
|
|
- }
|
|
471
|
+ client := session.client
|
|
472
|
+ server := client.server
|
|
473
|
+ config := server.Config()
|
461
|
474
|
|
462
|
|
- oldClient := server.resumeManager.VerifyToken(client.resumeDetails.PresentedToken)
|
|
475
|
+ oldClient, oldResumeID := server.resumeManager.VerifyToken(client, session.resumeDetails.PresentedToken)
|
463
|
476
|
if oldClient == nil {
|
464
|
|
- client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, token is not valid"))
|
|
477
|
+ session.Send(nil, server.name, "FAIL", "RESUME", "INVALID_TOKEN", client.t("Cannot resume connection, token is not valid"))
|
465
|
478
|
return
|
466
|
479
|
}
|
467
|
|
- oldNick := oldClient.Nick()
|
468
|
|
- oldNickmask := oldClient.NickMaskString()
|
469
|
480
|
|
470
|
481
|
resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS))
|
471
|
482
|
if !resumeAllowed {
|
472
|
|
- client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, old and new clients must have TLS"))
|
|
483
|
+ session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection, old and new clients must have TLS"))
|
473
|
484
|
return
|
474
|
485
|
}
|
475
|
486
|
|
476
|
487
|
if oldClient.isTor != client.isTor {
|
477
|
|
- client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection from Tor to non-Tor or vice versa"))
|
478
|
|
- return
|
479
|
|
- }
|
480
|
|
-
|
481
|
|
- if 1 < len(oldClient.Sessions()) {
|
482
|
|
- client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume a client with multiple attached sessions"))
|
|
488
|
+ session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection from Tor to non-Tor or vice versa"))
|
483
|
489
|
return
|
484
|
490
|
}
|
485
|
491
|
|
486
|
|
- err := server.clients.Resume(client, oldClient)
|
|
492
|
+ err := server.clients.Resume(oldClient, session)
|
487
|
493
|
if err != nil {
|
488
|
|
- client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection"))
|
|
494
|
+ session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
|
489
|
495
|
return
|
490
|
496
|
}
|
491
|
497
|
|
492
|
498
|
success = true
|
|
499
|
+ client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", oldClient.Nick()))
|
493
|
500
|
|
494
|
|
- // this is a bit racey
|
495
|
|
- client.resumeDetails.ResumedAt = time.Now().UTC()
|
496
|
|
-
|
497
|
|
- client.nickTimer.Touch(nil)
|
498
|
|
-
|
499
|
|
- // resume successful, proceed to copy client state (nickname, flags, etc.)
|
500
|
|
- // after this, the server thinks that `newClient` owns the nickname
|
501
|
|
-
|
502
|
|
- client.resumeDetails.OldClient = oldClient
|
503
|
|
-
|
504
|
|
- // transfer monitor stuff
|
505
|
|
- server.monitorManager.Resume(client, oldClient)
|
506
|
|
-
|
507
|
|
- // record the names, not the pointers, of the channels,
|
508
|
|
- // to avoid dumb annoying race conditions
|
509
|
|
- channels := oldClient.Channels()
|
510
|
|
- client.resumeDetails.Channels = make([]string, len(channels))
|
511
|
|
- for i, channel := range channels {
|
512
|
|
- client.resumeDetails.Channels[i] = channel.Name()
|
513
|
|
- }
|
|
501
|
+ return
|
|
502
|
+}
|
514
|
503
|
|
515
|
|
- username := client.Username()
|
516
|
|
- hostname := client.Hostname()
|
|
504
|
+// playResume is called from the session's fresh goroutine after a resume;
|
|
505
|
+// it sends notifications to friends, then plays the registration burst and replays
|
|
506
|
+// stored history to the session
|
|
507
|
+func (session *Session) playResume() {
|
|
508
|
+ client := session.client
|
|
509
|
+ server := client.server
|
517
|
510
|
|
518
|
511
|
friends := make(ClientSet)
|
519
|
512
|
oldestLostMessage := time.Now().UTC()
|
520
|
513
|
|
521
|
514
|
// work out how much time, if any, is not covered by history buffers
|
522
|
|
- for _, channel := range channels {
|
|
515
|
+ for _, channel := range client.Channels() {
|
523
|
516
|
for _, member := range channel.Members() {
|
524
|
517
|
friends.Add(member)
|
525
|
518
|
lastDiscarded := channel.history.LastDiscarded()
|
|
@@ -531,8 +524,8 @@ func (client *Client) tryResume() (success bool) {
|
531
|
524
|
privmsgMatcher := func(item history.Item) bool {
|
532
|
525
|
return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
|
533
|
526
|
}
|
534
|
|
- privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0)
|
535
|
|
- lastDiscarded := oldClient.history.LastDiscarded()
|
|
527
|
+ privmsgHistory := client.history.Match(privmsgMatcher, false, 0)
|
|
528
|
+ lastDiscarded := client.history.LastDiscarded()
|
536
|
529
|
if lastDiscarded.Before(oldestLostMessage) {
|
537
|
530
|
oldestLostMessage = lastDiscarded
|
538
|
531
|
}
|
|
@@ -543,60 +536,61 @@ func (client *Client) tryResume() (success bool) {
|
543
|
536
|
}
|
544
|
537
|
}
|
545
|
538
|
|
|
539
|
+ timestamp := session.resumeDetails.Timestamp
|
546
|
540
|
gap := lastDiscarded.Sub(timestamp)
|
547
|
|
- client.resumeDetails.HistoryIncomplete = gap > 0
|
|
541
|
+ session.resumeDetails.HistoryIncomplete = gap > 0
|
548
|
542
|
gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
|
549
|
543
|
|
|
544
|
+ details := client.Details()
|
|
545
|
+ oldNickmask := details.nickMask
|
|
546
|
+ client.SetRawHostname(session.rawHostname)
|
|
547
|
+ hostname := client.Hostname() // may be a vhost
|
|
548
|
+ timestampString := session.resumeDetails.Timestamp.Format(IRCv3TimestampFormat)
|
|
549
|
+
|
550
|
550
|
// send quit/resume messages to friends
|
551
|
551
|
for friend := range friends {
|
552
|
|
- for _, session := range friend.Sessions() {
|
553
|
|
- if session.capabilities.Has(caps.Resume) {
|
|
552
|
+ if friend == client {
|
|
553
|
+ continue
|
|
554
|
+ }
|
|
555
|
+ for _, fSession := range friend.Sessions() {
|
|
556
|
+ if fSession.capabilities.Has(caps.Resume) {
|
554
|
557
|
if timestamp.IsZero() {
|
555
|
|
- session.Send(nil, oldNickmask, "RESUMED", username, hostname)
|
|
558
|
+ fSession.Send(nil, oldNickmask, "RESUMED", hostname)
|
556
|
559
|
} else {
|
557
|
|
- session.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString)
|
|
560
|
+ fSession.Send(nil, oldNickmask, "RESUMED", hostname, timestampString)
|
558
|
561
|
}
|
559
|
562
|
} else {
|
560
|
|
- if client.resumeDetails.HistoryIncomplete {
|
561
|
|
- session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
|
|
563
|
+ if session.resumeDetails.HistoryIncomplete {
|
|
564
|
+ fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
|
562
|
565
|
} else {
|
563
|
|
- session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
|
|
566
|
+ fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
|
564
|
567
|
}
|
565
|
568
|
}
|
566
|
569
|
}
|
567
|
570
|
}
|
568
|
571
|
|
569
|
|
- if client.resumeDetails.HistoryIncomplete {
|
570
|
|
- client.Send(nil, client.server.name, "RESUME", "WARN", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
|
|
572
|
+ if session.resumeDetails.HistoryIncomplete {
|
|
573
|
+ session.Send(nil, client.server.name, "RESUME", "WARN", "HISTORY_LOST", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
|
571
|
574
|
}
|
572
|
575
|
|
573
|
|
- client.Send(nil, client.server.name, "RESUME", "SUCCESS", oldNick)
|
574
|
|
-
|
575
|
|
- // after we send the rest of the registration burst, we'll try rejoining channels
|
576
|
|
- return
|
577
|
|
-}
|
|
576
|
+ session.Send(nil, client.server.name, "RESUME", details.nick)
|
578
|
577
|
|
579
|
|
-func (client *Client) tryResumeChannels() {
|
580
|
|
- details := client.resumeDetails
|
|
578
|
+ server.playRegistrationBurst(session)
|
581
|
579
|
|
582
|
|
- for _, name := range details.Channels {
|
583
|
|
- channel := client.server.channels.Get(name)
|
584
|
|
- if channel == nil {
|
585
|
|
- continue
|
586
|
|
- }
|
587
|
|
- channel.Resume(client, details.OldClient, details.Timestamp)
|
|
580
|
+ for _, channel := range client.Channels() {
|
|
581
|
+ channel.Resume(session, timestamp)
|
588
|
582
|
}
|
589
|
583
|
|
590
|
584
|
// replay direct PRIVSMG history
|
591
|
|
- if !details.Timestamp.IsZero() {
|
|
585
|
+ if !timestamp.IsZero() {
|
592
|
586
|
now := time.Now().UTC()
|
593
|
|
- items, complete := client.history.Between(details.Timestamp, now, false, 0)
|
|
587
|
+ items, complete := client.history.Between(timestamp, now, false, 0)
|
594
|
588
|
rb := NewResponseBuffer(client.Sessions()[0])
|
595
|
589
|
client.replayPrivmsgHistory(rb, items, complete)
|
596
|
590
|
rb.Send(true)
|
597
|
591
|
}
|
598
|
592
|
|
599
|
|
- details.OldClient.destroy(true, nil)
|
|
593
|
+ session.resumeDetails = nil
|
600
|
594
|
}
|
601
|
595
|
|
602
|
596
|
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
|
|
@@ -644,41 +638,6 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
|
644
|
638
|
}
|
645
|
639
|
}
|
646
|
640
|
|
647
|
|
-// copy applicable state from oldClient to client as part of a resume
|
648
|
|
-func (client *Client) copyResumeData(oldClient *Client) {
|
649
|
|
- oldClient.stateMutex.RLock()
|
650
|
|
- history := oldClient.history
|
651
|
|
- nick := oldClient.nick
|
652
|
|
- nickCasefolded := oldClient.nickCasefolded
|
653
|
|
- vhost := oldClient.vhost
|
654
|
|
- account := oldClient.account
|
655
|
|
- accountName := oldClient.accountName
|
656
|
|
- skeleton := oldClient.skeleton
|
657
|
|
- oldClient.stateMutex.RUnlock()
|
658
|
|
-
|
659
|
|
- // copy all flags, *except* TLS (in the case that the admins enabled
|
660
|
|
- // resume over plaintext)
|
661
|
|
- hasTLS := client.flags.HasMode(modes.TLS)
|
662
|
|
- temp := modes.NewModeSet()
|
663
|
|
- temp.Copy(&oldClient.flags)
|
664
|
|
- temp.SetMode(modes.TLS, hasTLS)
|
665
|
|
- client.flags.Copy(temp)
|
666
|
|
-
|
667
|
|
- client.stateMutex.Lock()
|
668
|
|
- defer client.stateMutex.Unlock()
|
669
|
|
-
|
670
|
|
- // reuse the old client's history buffer
|
671
|
|
- client.history = history
|
672
|
|
- // copy other data
|
673
|
|
- client.nick = nick
|
674
|
|
- client.nickCasefolded = nickCasefolded
|
675
|
|
- client.vhost = vhost
|
676
|
|
- client.account = account
|
677
|
|
- client.accountName = accountName
|
678
|
|
- client.skeleton = skeleton
|
679
|
|
- client.updateNickMaskNoMutex()
|
680
|
|
-}
|
681
|
|
-
|
682
|
641
|
// IdleTime returns how long this client's been idle.
|
683
|
642
|
func (client *Client) IdleTime() time.Duration {
|
684
|
643
|
client.stateMutex.RLock()
|
|
@@ -956,12 +915,13 @@ func (client *Client) Quit(message string, session *Session) {
|
956
|
915
|
// if `session` is nil, destroys the client unconditionally, removing all sessions;
|
957
|
916
|
// otherwise, destroys one specific session, only destroying the client if it
|
958
|
917
|
// has no more sessions.
|
959
|
|
-func (client *Client) destroy(beingResumed bool, session *Session) {
|
|
918
|
+func (client *Client) destroy(session *Session) {
|
960
|
919
|
var sessionsToDestroy []*Session
|
961
|
920
|
|
962
|
921
|
// allow destroy() to execute at most once
|
963
|
922
|
client.stateMutex.Lock()
|
964
|
923
|
details := client.detailsNoMutex()
|
|
924
|
+ brbState := client.brbTimer.state
|
965
|
925
|
wasReattach := session != nil && session.client != client
|
966
|
926
|
sessionRemoved := false
|
967
|
927
|
var remainingSessions int
|
|
@@ -977,10 +937,6 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
977
|
937
|
}
|
978
|
938
|
client.stateMutex.Unlock()
|
979
|
939
|
|
980
|
|
- if len(sessionsToDestroy) == 0 {
|
981
|
|
- return
|
982
|
|
- }
|
983
|
|
-
|
984
|
940
|
// destroy all applicable sessions:
|
985
|
941
|
var quitMessage string
|
986
|
942
|
for _, session := range sessionsToDestroy {
|
|
@@ -1010,8 +966,8 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
1010
|
966
|
client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
|
1011
|
967
|
}
|
1012
|
968
|
|
1013
|
|
- // ok, now destroy the client, unless it still has sessions:
|
1014
|
|
- if remainingSessions != 0 {
|
|
969
|
+ // do not destroy the client if it has either remaining sessions, or is BRB'ed
|
|
970
|
+ if remainingSessions != 0 || brbState == BrbEnabled || brbState == BrbSticky {
|
1015
|
971
|
return
|
1016
|
972
|
}
|
1017
|
973
|
|
|
@@ -1020,14 +976,12 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
1020
|
976
|
client.server.semaphores.ClientDestroy.Acquire()
|
1021
|
977
|
defer client.server.semaphores.ClientDestroy.Release()
|
1022
|
978
|
|
1023
|
|
- if beingResumed {
|
1024
|
|
- client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", details.nick))
|
1025
|
|
- } else if !wasReattach {
|
|
979
|
+ if !wasReattach {
|
1026
|
980
|
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
|
1027
|
981
|
}
|
1028
|
982
|
|
1029
|
983
|
registered := client.Registered()
|
1030
|
|
- if !beingResumed && registered {
|
|
984
|
+ if registered {
|
1031
|
985
|
client.server.whoWas.Append(client.WhoWas())
|
1032
|
986
|
}
|
1033
|
987
|
|
|
@@ -1045,15 +999,13 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
1045
|
999
|
// (note that if this is a reattach, client has no channels and therefore no friends)
|
1046
|
1000
|
friends := make(ClientSet)
|
1047
|
1001
|
for _, channel := range client.Channels() {
|
1048
|
|
- if !beingResumed {
|
1049
|
|
- channel.Quit(client)
|
1050
|
|
- channel.history.Add(history.Item{
|
1051
|
|
- Type: history.Quit,
|
1052
|
|
- Nick: details.nickMask,
|
1053
|
|
- AccountName: details.accountName,
|
1054
|
|
- Message: splitQuitMessage,
|
1055
|
|
- })
|
1056
|
|
- }
|
|
1002
|
+ channel.Quit(client)
|
|
1003
|
+ channel.history.Add(history.Item{
|
|
1004
|
+ Type: history.Quit,
|
|
1005
|
+ Nick: details.nickMask,
|
|
1006
|
+ AccountName: details.accountName,
|
|
1007
|
+ Message: splitQuitMessage,
|
|
1008
|
+ })
|
1057
|
1009
|
for _, member := range channel.Members() {
|
1058
|
1010
|
friends.Add(member)
|
1059
|
1011
|
}
|
|
@@ -1061,40 +1013,34 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
1061
|
1013
|
friends.Remove(client)
|
1062
|
1014
|
|
1063
|
1015
|
// clean up server
|
1064
|
|
- if !beingResumed {
|
1065
|
|
- client.server.clients.Remove(client)
|
1066
|
|
- }
|
|
1016
|
+ client.server.clients.Remove(client)
|
1067
|
1017
|
|
1068
|
1018
|
// clean up self
|
1069
|
1019
|
client.nickTimer.Stop()
|
|
1020
|
+ client.brbTimer.Disable()
|
1070
|
1021
|
|
1071
|
1022
|
client.server.accounts.Logout(client)
|
1072
|
1023
|
|
1073
|
1024
|
// send quit messages to friends
|
1074
|
|
- if !beingResumed {
|
1075
|
|
- if registered {
|
1076
|
|
- client.server.stats.ChangeTotal(-1)
|
1077
|
|
- }
|
1078
|
|
- if client.HasMode(modes.Invisible) {
|
1079
|
|
- client.server.stats.ChangeInvisible(-1)
|
1080
|
|
- }
|
1081
|
|
- if client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator) {
|
1082
|
|
- client.server.stats.ChangeOperators(-1)
|
1083
|
|
- }
|
|
1025
|
+ if registered {
|
|
1026
|
+ client.server.stats.ChangeTotal(-1)
|
|
1027
|
+ }
|
|
1028
|
+ if client.HasMode(modes.Invisible) {
|
|
1029
|
+ client.server.stats.ChangeInvisible(-1)
|
|
1030
|
+ }
|
|
1031
|
+ if client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator) {
|
|
1032
|
+ client.server.stats.ChangeOperators(-1)
|
|
1033
|
+ }
|
1084
|
1034
|
|
1085
|
|
- for friend := range friends {
|
1086
|
|
- if quitMessage == "" {
|
1087
|
|
- quitMessage = "Exited"
|
1088
|
|
- }
|
1089
|
|
- friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
|
|
1035
|
+ for friend := range friends {
|
|
1036
|
+ if quitMessage == "" {
|
|
1037
|
+ quitMessage = "Exited"
|
1090
|
1038
|
}
|
|
1039
|
+ friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
|
1091
|
1040
|
}
|
1092
|
|
- if !client.exitedSnomaskSent {
|
1093
|
|
- if beingResumed {
|
1094
|
|
- client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r is resuming their connection, old client has been destroyed"), client.nick))
|
1095
|
|
- } else if registered {
|
1096
|
|
- client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick))
|
1097
|
|
- }
|
|
1041
|
+
|
|
1042
|
+ if !client.exitedSnomaskSent && registered {
|
|
1043
|
+ client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick))
|
1098
|
1044
|
}
|
1099
|
1045
|
}
|
1100
|
1046
|
|