浏览代码

initial persistent history implementation

tags/v2.0.0-rc1
Shivaram Lingamneni 4 年前
父节点
当前提交
33dac4c0ba
共有 34 个文件被更改,包括 2206 次插入572 次删除
  1. 1
    0
      Makefile
  2. 6
    0
      gencapdefs.py
  3. 4
    8
      go.mod
  4. 73
    17
      irc/accounts.go
  5. 6
    1
      irc/caps/defs.go
  6. 122
    32
      irc/channel.go
  7. 15
    0
      irc/channelreg.go
  8. 116
    21
      irc/chanserv.go
  9. 258
    60
      irc/client.go
  10. 51
    19
      irc/client_lookup_set.go
  11. 7
    6
      irc/commands.go
  12. 187
    1
      irc/config.go
  13. 2
    18
      irc/database.go
  14. 2
    0
      irc/errors.go
  15. 1
    1
      irc/gateways.go
  16. 71
    16
      irc/getters.go
  17. 142
    207
      irc/handlers.go
  18. 3
    2
      irc/help.go
  19. 97
    71
      irc/history/history.go
  20. 46
    25
      irc/history/history_test.go
  21. 71
    0
      irc/history/queries.go
  22. 21
    23
      irc/idletimer.go
  23. 535
    0
      irc/mysql/history.go
  24. 23
    0
      irc/mysql/serialization.go
  25. 10
    8
      irc/nickname.go
  26. 76
    1
      irc/nickserv.go
  27. 42
    2
      irc/responsebuffer.go
  28. 95
    10
      irc/server.go
  29. 19
    1
      irc/stats.go
  30. 21
    2
      irc/utils/args.go
  31. 0
    8
      irc/utils/net.go
  32. 5
    8
      irc/utils/text.go
  33. 15
    3
      irc/znc.go
  34. 63
    1
      oragono.yaml

+ 1
- 0
Makefile 查看文件

@@ -25,6 +25,7 @@ test:
25 25
 	cd irc/history && go test . && go vet .
26 26
 	cd irc/isupport && go test . && go vet .
27 27
 	cd irc/modes && go test . && go vet .
28
+	cd irc/mysql && go test . && go vet .
28 29
 	cd irc/passwd && go test . && go vet .
29 30
 	cd irc/utils && go test . && go vet .
30 31
 	./.check-gofmt.sh

+ 6
- 0
gencapdefs.py 查看文件

@@ -171,6 +171,12 @@ CAPDEFS = [
171 171
         url="https://github.com/ircv3/ircv3-specifications/pull/398",
172 172
         standard="proposed IRCv3",
173 173
     ),
174
+    CapDef(
175
+        identifier="Chathistory",
176
+        name="draft/chathistory",
177
+        url="https://github.com/ircv3/ircv3-specifications/pull/393",
178
+        standard="proposed IRCv3",
179
+    ),
174 180
 ]
175 181
 
176 182
 def validate_defs():

+ 4
- 8
go.mod 查看文件

@@ -6,23 +6,19 @@ require (
6 6
 	code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
7 7
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
8 8
 	github.com/go-ldap/ldap/v3 v3.1.6
9
+	github.com/go-sql-driver/mysql v1.5.0
9 10
 	github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
10 11
 	github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0
11 12
 	github.com/mattn/go-colorable v0.1.4
12 13
 	github.com/mattn/go-isatty v0.0.10 // indirect
13 14
 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
15
+	github.com/onsi/ginkgo v1.12.0 // indirect
16
+	github.com/onsi/gomega v1.9.0 // indirect
14 17
 	github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
15 18
 	github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
16
-	github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
19
+	github.com/stretchr/testify v1.4.0 // indirect
17 20
 	github.com/tidwall/buntdb v1.1.2
18
-	github.com/tidwall/gjson v1.3.4 // indirect
19
-	github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
20
-	github.com/tidwall/match v1.0.1 // indirect
21
-	github.com/tidwall/pretty v1.0.0 // indirect
22
-	github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
23
-	github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
24 21
 	golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
25
-	golang.org/x/sys v0.0.0-20191115151921-52ab43148777 // indirect
26 22
 	golang.org/x/text v0.3.2
27 23
 	gopkg.in/yaml.v2 v2.2.5
28 24
 )

+ 73
- 17
irc/accounts.go 查看文件

@@ -33,7 +33,8 @@ const (
33 33
 	keyAccountSettings         = "account.settings %s"
34 34
 	keyAccountVHost            = "account.vhost %s"
35 35
 	keyCertToAccount           = "account.creds.certfp %s"
36
-	keyAccountChannels         = "account.channels %s"
36
+	keyAccountChannels         = "account.channels %s" // channels registered to the account
37
+	keyAccountJoinedChannels   = "account.joinedto %s" // channels a persistent client has joined
37 38
 
38 39
 	keyVHostQueueAcctToId = "vhostQueue %s"
39 40
 	vhostRequestIdx       = "vhostQueue"
@@ -71,6 +72,40 @@ func (am *AccountManager) Initialize(server *Server) {
71 72
 	config := server.Config()
72 73
 	am.buildNickToAccountIndex(config)
73 74
 	am.initVHostRequestQueue(config)
75
+	am.createAlwaysOnClients(config)
76
+}
77
+
78
+func (am *AccountManager) createAlwaysOnClients(config *Config) {
79
+	if config.Accounts.Bouncer.AlwaysOn == PersistentDisabled {
80
+		return
81
+	}
82
+
83
+	verifiedPrefix := fmt.Sprintf(keyAccountVerified, "")
84
+
85
+	am.serialCacheUpdateMutex.Lock()
86
+	defer am.serialCacheUpdateMutex.Unlock()
87
+
88
+	var accounts []string
89
+
90
+	am.server.store.View(func(tx *buntdb.Tx) error {
91
+		err := tx.AscendGreaterOrEqual("", verifiedPrefix, func(key, value string) bool {
92
+			if !strings.HasPrefix(key, verifiedPrefix) {
93
+				return false
94
+			}
95
+			account := strings.TrimPrefix(key, verifiedPrefix)
96
+			accounts = append(accounts, account)
97
+			return true
98
+		})
99
+		return err
100
+	})
101
+
102
+	for _, accountName := range accounts {
103
+		account, err := am.LoadAccount(accountName)
104
+		if err == nil && account.Verified &&
105
+			persistenceEnabled(config.Accounts.Bouncer.AlwaysOn, account.Settings.AlwaysOn) {
106
+			am.server.AddAlwaysOnClient(account, am.loadChannels(accountName))
107
+		}
108
+	}
74 109
 }
75 110
 
76 111
 func (am *AccountManager) buildNickToAccountIndex(config *Config) {
@@ -477,6 +512,28 @@ func (am *AccountManager) setPassword(account string, password string, hasPrivs
477 512
 	return err
478 513
 }
479 514
 
515
+func (am *AccountManager) saveChannels(account string, channels []string) {
516
+	channelsStr := strings.Join(channels, ",")
517
+	key := fmt.Sprintf(keyAccountJoinedChannels, account)
518
+	am.server.store.Update(func(tx *buntdb.Tx) error {
519
+		tx.Set(key, channelsStr, nil)
520
+		return nil
521
+	})
522
+}
523
+
524
+func (am *AccountManager) loadChannels(account string) (channels []string) {
525
+	key := fmt.Sprintf(keyAccountJoinedChannels, account)
526
+	var channelsStr string
527
+	am.server.store.View(func(tx *buntdb.Tx) error {
528
+		channelsStr, _ = tx.Get(key)
529
+		return nil
530
+	})
531
+	if channelsStr != "" {
532
+		return strings.Split(channelsStr, ",")
533
+	}
534
+	return
535
+}
536
+
480 537
 func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
481 538
 	certfp, err = utils.NormalizeCertfp(certfp)
482 539
 	if err != nil {
@@ -685,7 +742,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
685 742
 	}
686 743
 	am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
687 744
 	raw.Verified = true
688
-	clientAccount, err := am.deserializeRawAccount(raw)
745
+	clientAccount, err := am.deserializeRawAccount(raw, casefoldedAccount)
689 746
 	if err != nil {
690 747
 		return err
691 748
 	}
@@ -892,13 +949,13 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
892 949
 		return
893 950
 	}
894 951
 
895
-	result, err = am.deserializeRawAccount(raw)
896
-	result.NameCasefolded = casefoldedAccount
952
+	result, err = am.deserializeRawAccount(raw, casefoldedAccount)
897 953
 	return
898 954
 }
899 955
 
900
-func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result ClientAccount, err error) {
956
+func (am *AccountManager) deserializeRawAccount(raw rawClientAccount, cfName string) (result ClientAccount, err error) {
901 957
 	result.Name = raw.Name
958
+	result.NameCasefolded = cfName
902 959
 	regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
903 960
 	result.RegisteredAt = time.Unix(regTimeInt, 0).UTC()
904 961
 	e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
@@ -976,6 +1033,7 @@ func (am *AccountManager) Unregister(account string) error {
976 1033
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
977 1034
 	vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
978 1035
 	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
1036
+	joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount)
979 1037
 
980 1038
 	var clients []*Client
981 1039
 
@@ -1011,6 +1069,7 @@ func (am *AccountManager) Unregister(account string) error {
1011 1069
 		tx.Delete(vhostKey)
1012 1070
 		channelsStr, _ = tx.Get(channelsKey)
1013 1071
 		tx.Delete(channelsKey)
1072
+		tx.Delete(joinedChannelsKey)
1014 1073
 
1015 1074
 		_, err := tx.Delete(vhostQueueKey)
1016 1075
 		am.decrementVHostQueueCount(casefoldedAccount, err)
@@ -1455,10 +1514,7 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo)
1455 1514
 }
1456 1515
 
1457 1516
 func (am *AccountManager) Login(client *Client, account ClientAccount) {
1458
-	changed := client.SetAccountName(account.Name)
1459
-	if !changed {
1460
-		return
1461
-	}
1517
+	client.Login(account)
1462 1518
 
1463 1519
 	client.nickTimer.Touch(nil)
1464 1520
 
@@ -1468,9 +1524,6 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) {
1468 1524
 	am.Lock()
1469 1525
 	defer am.Unlock()
1470 1526
 	am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
1471
-	for _, client := range am.accountToClients[casefoldedAccount] {
1472
-		client.SetAccountSettings(account.Settings)
1473
-	}
1474 1527
 }
1475 1528
 
1476 1529
 func (am *AccountManager) Logout(client *Client) {
@@ -1623,10 +1676,13 @@ func replayJoinsSettingFromString(str string) (result ReplayJoinsSetting, err er
1623 1676
 }
1624 1677
 
1625 1678
 type AccountSettings struct {
1626
-	AutoreplayLines *int
1627
-	NickEnforcement NickEnforcementMethod
1628
-	AllowBouncer    BouncerAllowedSetting
1629
-	ReplayJoins     ReplayJoinsSetting
1679
+	AutoreplayLines  *int
1680
+	NickEnforcement  NickEnforcementMethod
1681
+	AllowBouncer     BouncerAllowedSetting
1682
+	ReplayJoins      ReplayJoinsSetting
1683
+	AlwaysOn         PersistentStatus
1684
+	AutoreplayMissed bool
1685
+	DMHistory        HistoryStatus
1630 1686
 }
1631 1687
 
1632 1688
 // ClientAccount represents a user account.
@@ -1661,7 +1717,7 @@ func (am *AccountManager) logoutOfAccount(client *Client) {
1661 1717
 		return
1662 1718
 	}
1663 1719
 
1664
-	client.SetAccountName("")
1720
+	client.Logout()
1665 1721
 	go client.nickTimer.Touch(nil)
1666 1722
 
1667 1723
 	// dispatch account-notify

+ 6
- 1
irc/caps/defs.go 查看文件

@@ -7,7 +7,7 @@ package caps
7 7
 
8 8
 const (
9 9
 	// number of recognized capabilities:
10
-	numCapabs = 26
10
+	numCapabs = 27
11 11
 	// length of the uint64 array that represents the bitset:
12 12
 	bitsetLen = 1
13 13
 )
@@ -37,6 +37,10 @@ const (
37 37
 	// https://ircv3.net/specs/extensions/chghost-3.2.html
38 38
 	ChgHost Capability = iota
39 39
 
40
+	// Chathistory is the proposed IRCv3 capability named "draft/chathistory":
41
+	// https://github.com/ircv3/ircv3-specifications/pull/393
42
+	Chathistory Capability = iota
43
+
40 44
 	// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
41 45
 	// https://github.com/ircv3/ircv3-specifications/pull/362
42 46
 	EventPlayback Capability = iota
@@ -127,6 +131,7 @@ var (
127 131
 		"batch",
128 132
 		"cap-notify",
129 133
 		"chghost",
134
+		"draft/chathistory",
130 135
 		"draft/event-playback",
131 136
 		"draft/languages",
132 137
 		"draft/multiline",

+ 122
- 32
irc/channel.go 查看文件

@@ -24,6 +24,10 @@ const (
24 24
 	histServMask = "HistServ!HistServ@localhost"
25 25
 )
26 26
 
27
+type ChannelSettings struct {
28
+	History HistoryStatus
29
+}
30
+
27 31
 // Channel represents a channel that clients can join.
28 32
 type Channel struct {
29 33
 	flags             modes.ModeSet
@@ -49,6 +53,7 @@ type Channel struct {
49 53
 	joinPartMutex     sync.Mutex      // tier 3
50 54
 	ensureLoaded      utils.Once      // manages loading stored registration info from the database
51 55
 	dirtyBits         uint
56
+	settings          ChannelSettings
52 57
 }
53 58
 
54 59
 // NewChannel creates a new channel from a `Server` and a `name`
@@ -66,9 +71,10 @@ func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channe
66 71
 
67 72
 	channel.initializeLists()
68 73
 	channel.writerSemaphore.Initialize(1)
69
-	channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
74
+	channel.history.Initialize(0, 0)
70 75
 
71 76
 	if !registered {
77
+		channel.resizeHistory(config)
72 78
 		for _, mode := range config.Channels.defaultModes {
73 79
 			channel.flags.SetMode(mode, true)
74 80
 		}
@@ -106,8 +112,19 @@ func (channel *Channel) IsLoaded() bool {
106 112
 	return channel.ensureLoaded.Done()
107 113
 }
108 114
 
115
+func (channel *Channel) resizeHistory(config *Config) {
116
+	_, ephemeral, _ := channel.historyStatus(config)
117
+	if ephemeral {
118
+		channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
119
+	} else {
120
+		channel.history.Resize(0, 0)
121
+	}
122
+}
123
+
109 124
 // read in channel state that was persisted in the DB
110 125
 func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
126
+	defer channel.resizeHistory(channel.server.Config())
127
+
111 128
 	channel.stateMutex.Lock()
112 129
 	defer channel.stateMutex.Unlock()
113 130
 
@@ -120,6 +137,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
120 137
 	channel.createdTime = chanReg.RegisteredAt
121 138
 	channel.key = chanReg.Key
122 139
 	channel.userLimit = chanReg.UserLimit
140
+	channel.settings = chanReg.Settings
123 141
 
124 142
 	for _, mode := range chanReg.Modes {
125 143
 		channel.flags.SetMode(mode, true)
@@ -164,6 +182,10 @@ func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredCh
164 182
 		}
165 183
 	}
166 184
 
185
+	if includeFlags&IncludeSettings != 0 {
186
+		info.Settings = channel.settings
187
+	}
188
+
167 189
 	return
168 190
 }
169 191
 
@@ -434,7 +456,7 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
434 456
 			if modeSet == nil {
435 457
 				continue
436 458
 			}
437
-			if !isJoined && target.flags.HasMode(modes.Invisible) && !isOper {
459
+			if !isJoined && target.HasMode(modes.Invisible) && !isOper {
438 460
 				continue
439 461
 			}
440 462
 			prefix := modeSet.Prefixes(isMultiPrefix)
@@ -564,6 +586,48 @@ func (channel *Channel) IsEmpty() bool {
564 586
 	return len(channel.members) == 0
565 587
 }
566 588
 
589
+// figure out where history is being stored: persistent, ephemeral, or neither
590
+// target is only needed if we're doing persistent history
591
+func (channel *Channel) historyStatus(config *Config) (persistent, ephemeral bool, target string) {
592
+	if !config.History.Persistent.Enabled {
593
+		return false, config.History.Enabled, ""
594
+	}
595
+
596
+	channel.stateMutex.RLock()
597
+	target = channel.nameCasefolded
598
+	historyStatus := channel.settings.History
599
+	registered := channel.registeredFounder != ""
600
+	channel.stateMutex.RUnlock()
601
+
602
+	historyStatus = historyEnabled(config.History.Persistent.RegisteredChannels, historyStatus)
603
+
604
+	// ephemeral history: either the channel owner explicitly set the ephemeral preference,
605
+	// or persistent history is disabled for unregistered channels
606
+	if registered {
607
+		ephemeral = (historyStatus == HistoryEphemeral)
608
+		persistent = (historyStatus == HistoryPersistent)
609
+	} else {
610
+		ephemeral = config.History.Enabled && !config.History.Persistent.UnregisteredChannels
611
+		persistent = config.History.Persistent.UnregisteredChannels
612
+	}
613
+	return
614
+}
615
+
616
+func (channel *Channel) AddHistoryItem(item history.Item) (err error) {
617
+	if !item.IsStorable() {
618
+		return
619
+	}
620
+
621
+	persistent, ephemeral, target := channel.historyStatus(channel.server.Config())
622
+	if ephemeral {
623
+		channel.history.Add(item)
624
+	}
625
+	if persistent {
626
+		return channel.server.historyDB.AddChannelItem(target, item)
627
+	}
628
+	return nil
629
+}
630
+
567 631
 // Join joins the given client to this channel (if they can be joined).
568 632
 func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
569 633
 	details := client.Details()
@@ -643,15 +707,18 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
643 707
 
644 708
 		channel.regenerateMembersCache()
645 709
 
646
-		message = utils.MakeMessage("")
647
-		histItem := history.Item{
648
-			Type:        history.Join,
649
-			Nick:        details.nickMask,
650
-			AccountName: details.accountName,
651
-			Message:     message,
710
+		// no history item for fake persistent joins
711
+		if rb != nil {
712
+			message = utils.MakeMessage("")
713
+			histItem := history.Item{
714
+				Type:        history.Join,
715
+				Nick:        details.nickMask,
716
+				AccountName: details.accountName,
717
+				Message:     message,
718
+			}
719
+			histItem.Params[0] = details.realname
720
+			channel.AddHistoryItem(histItem)
652 721
 		}
653
-		histItem.Params[0] = details.realname
654
-		channel.history.Add(histItem)
655 722
 
656 723
 		return
657 724
 	}()
@@ -665,7 +732,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
665 732
 
666 733
 	for _, member := range channel.Members() {
667 734
 		for _, session := range member.Sessions() {
668
-			if session == rb.session {
735
+			if rb != nil && session == rb.session {
669 736
 				continue
670 737
 			} else if client == session.client {
671 738
 				channel.playJoinForSession(session)
@@ -682,13 +749,13 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
682 749
 		}
683 750
 	}
684 751
 
685
-	if rb.session.capabilities.Has(caps.ExtendedJoin) {
752
+	if rb != nil && rb.session.capabilities.Has(caps.ExtendedJoin) {
686 753
 		rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname)
687 754
 	} else {
688 755
 		rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname)
689 756
 	}
690 757
 
691
-	if rb.session.client == client {
758
+	if rb != nil && rb.session.client == client {
692 759
 		// don't send topic and names for a SAJOIN of a different client
693 760
 		channel.SendTopic(client, rb, false)
694 761
 		channel.Names(client, rb)
@@ -697,15 +764,29 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
697 764
 	// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
698 765
 	rb.Flush(true)
699 766
 
700
-	channel.autoReplayHistory(client, rb, message.Msgid)
767
+	if rb != nil {
768
+		channel.autoReplayHistory(client, rb, message.Msgid)
769
+	}
701 770
 }
702 771
 
703 772
 func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
704 773
 	// autoreplay any messages as necessary
705
-	config := channel.server.Config()
706 774
 	var items []history.Item
775
+
776
+	var after, before time.Time
707 777
 	if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets.Has(channel.NameCasefolded())) {
708
-		items, _ = channel.history.Between(rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before, false, config.History.ChathistoryMax)
778
+		after, before = rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before
779
+	} else if !rb.session.lastSignoff.IsZero() {
780
+		// we already checked for history caps in `playReattachMessages`
781
+		after = rb.session.lastSignoff
782
+	}
783
+
784
+	if !after.IsZero() || !before.IsZero() {
785
+		_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
786
+		if seq != nil {
787
+			zncMax := channel.server.Config().History.ZNCMax
788
+			items, _, _ = seq.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
789
+		}
709 790
 	} else if !rb.session.HasHistoryCaps() {
710 791
 		var replayLimit int
711 792
 		customReplayLimit := client.AccountSettings().AutoreplayLines
@@ -719,7 +800,10 @@ func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, sk
719 800
 			replayLimit = channel.server.Config().History.AutoreplayOnJoin
720 801
 		}
721 802
 		if 0 < replayLimit {
722
-			items = channel.history.Latest(replayLimit)
803
+			_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
804
+			if seq != nil {
805
+				items, _, _ = seq.Between(history.Selector{}, history.Selector{}, replayLimit)
806
+			}
723 807
 		}
724 808
 	}
725 809
 	// remove the client's own JOIN line from the replay
@@ -784,7 +868,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
784 868
 		}
785 869
 	}
786 870
 
787
-	channel.history.Add(history.Item{
871
+	channel.AddHistoryItem(history.Item{
788 872
 		Type:        history.Part,
789 873
 		Nick:        details.nickMask,
790 874
 		AccountName: details.accountName,
@@ -799,10 +883,9 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
799 883
 // 2. Send JOIN and MODE lines to channel participants (including the new client)
800 884
 // 3. Replay missed message history to the client
801 885
 func (channel *Channel) Resume(session *Session, timestamp time.Time) {
802
-	now := time.Now().UTC()
803 886
 	channel.resumeAndAnnounce(session)
804 887
 	if !timestamp.IsZero() {
805
-		channel.replayHistoryForResume(session, timestamp, now)
888
+		channel.replayHistoryForResume(session, timestamp, time.Time{})
806 889
 	}
807 890
 }
808 891
 
@@ -852,7 +935,13 @@ func (channel *Channel) resumeAndAnnounce(session *Session) {
852 935
 }
853 936
 
854 937
 func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
855
-	items, complete := channel.history.Between(after, before, false, 0)
938
+	var items []history.Item
939
+	var complete bool
940
+	afterS, beforeS := history.Selector{Time: after}, history.Selector{Time: before}
941
+	_, seq, _ := channel.server.GetHistorySequence(channel, session.client, "")
942
+	if seq != nil {
943
+		items, complete, _ = seq.Between(afterS, beforeS, channel.server.Config().History.ZNCMax)
944
+	}
856 945
 	rb := NewResponseBuffer(session)
857 946
 	channel.replayHistoryItems(rb, items, false)
858 947
 	if !complete && !session.resumeDetails.HistoryIncomplete {
@@ -908,9 +997,9 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
908 997
 		case history.Join:
909 998
 			if eventPlayback {
910 999
 				if extendedJoin {
911
-					rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname, item.AccountName, item.Params[0])
1000
+					rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname, item.AccountName, item.Params[0])
912 1001
 				} else {
913
-					rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
1002
+					rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname)
914 1003
 				}
915 1004
 			} else {
916 1005
 				if !playJoinsAsPrivmsg {
@@ -926,7 +1015,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
926 1015
 			}
927 1016
 		case history.Part:
928 1017
 			if eventPlayback {
929
-				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
1018
+				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "PART", chname, item.Message.Message)
930 1019
 			} else {
931 1020
 				if !playJoinsAsPrivmsg {
932 1021
 					continue // #474
@@ -936,14 +1025,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
936 1025
 			}
937 1026
 		case history.Kick:
938 1027
 			if eventPlayback {
939
-				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message)
1028
+				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "KICK", chname, item.Params[0], item.Message.Message)
940 1029
 			} else {
941 1030
 				message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
942 1031
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
943 1032
 			}
944 1033
 		case history.Quit:
945 1034
 			if eventPlayback {
946
-				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
1035
+				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "QUIT", item.Message.Message)
947 1036
 			} else {
948 1037
 				if !playJoinsAsPrivmsg {
949 1038
 					continue // #474
@@ -953,7 +1042,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
953 1042
 			}
954 1043
 		case history.Nick:
955 1044
 			if eventPlayback {
956
-				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0])
1045
+				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "NICK", item.Params[0])
957 1046
 			} else {
958 1047
 				message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
959 1048
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
@@ -1124,11 +1213,12 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
1124 1213
 			// STATUSMSG
1125 1214
 			continue
1126 1215
 		}
1127
-		if isCTCP && member.isTor {
1128
-			continue // #753
1129
-		}
1130 1216
 
1131 1217
 		for _, session := range member.Sessions() {
1218
+			if isCTCP && session.isTor {
1219
+				continue // #753
1220
+			}
1221
+
1132 1222
 			var tagsToUse map[string]string
1133 1223
 			if session.capabilities.Has(caps.MessageTags) {
1134 1224
 				tagsToUse = clientOnlyTags
@@ -1144,7 +1234,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
1144 1234
 		}
1145 1235
 	}
1146 1236
 
1147
-	channel.history.Add(history.Item{
1237
+	channel.AddHistoryItem(history.Item{
1148 1238
 		Type:        histType,
1149 1239
 		Message:     message,
1150 1240
 		Nick:        nickmask,
@@ -1266,7 +1356,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
1266 1356
 		Message:     message,
1267 1357
 	}
1268 1358
 	histItem.Params[0] = targetNick
1269
-	channel.history.Add(histItem)
1359
+	channel.AddHistoryItem(histItem)
1270 1360
 
1271 1361
 	channel.Quit(target)
1272 1362
 }

+ 15
- 0
irc/channelreg.go 查看文件

@@ -33,6 +33,7 @@ const (
33 33
 	keyChannelModes          = "channel.modes %s"
34 34
 	keyChannelAccountToUMode = "channel.accounttoumode %s"
35 35
 	keyChannelUserLimit      = "channel.userlimit %s"
36
+	keyChannelSettings       = "channel.settings %s"
36 37
 
37 38
 	keyChannelPurged = "channel.purged %s"
38 39
 )
@@ -53,6 +54,7 @@ var (
53 54
 		keyChannelModes,
54 55
 		keyChannelAccountToUMode,
55 56
 		keyChannelUserLimit,
57
+		keyChannelSettings,
56 58
 	}
57 59
 )
58 60
 
@@ -63,6 +65,7 @@ const (
63 65
 	IncludeTopic
64 66
 	IncludeModes
65 67
 	IncludeLists
68
+	IncludeSettings
66 69
 )
67 70
 
68 71
 // this is an OR of all possible flags
@@ -100,6 +103,8 @@ type RegisteredChannel struct {
100 103
 	Excepts map[string]MaskInfo
101 104
 	// Invites represents the invite exceptions set on the channel.
102 105
 	Invites map[string]MaskInfo
106
+	// Settings are the chanserv-modifiable settings
107
+	Settings ChannelSettings
103 108
 }
104 109
 
105 110
 type ChannelPurgeRecord struct {
@@ -203,6 +208,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
203 208
 		exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
204 209
 		invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
205 210
 		accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
211
+		settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
206 212
 
207 213
 		modeSlice := make([]modes.Mode, len(modeString))
208 214
 		for i, mode := range modeString {
@@ -220,6 +226,9 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
220 226
 		accountToUMode := make(map[string]modes.Mode)
221 227
 		_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
222 228
 
229
+		var settings ChannelSettings
230
+		_ = json.Unmarshal([]byte(settingsString), &settings)
231
+
223 232
 		info = RegisteredChannel{
224 233
 			Name:           name,
225 234
 			RegisteredAt:   time.Unix(regTimeInt, 0).UTC(),
@@ -234,6 +243,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
234 243
 			Invites:        invitelist,
235 244
 			AccountToUMode: accountToUMode,
236 245
 			UserLimit:      int(userLimit),
246
+			Settings:       settings,
237 247
 		}
238 248
 		return nil
239 249
 	})
@@ -357,6 +367,11 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
357 367
 		accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
358 368
 		tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
359 369
 	}
370
+
371
+	if includeFlags&IncludeSettings != 0 {
372
+		settingsString, _ := json.Marshal(channelInfo.Settings)
373
+		tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil)
374
+	}
360 375
 }
361 376
 
362 377
 // PurgeChannel records a channel purge.

+ 116
- 21
irc/chanserv.go 查看文件

@@ -137,6 +137,35 @@ INFO displays info about a registered channel.`,
137 137
 			enabled:   chanregEnabled,
138 138
 			minParams: 1,
139 139
 		},
140
+		"get": {
141
+			handler: csGetHandler,
142
+			help: `Syntax: $bGET #channel <setting>$b
143
+
144
+GET queries the current values of the channel settings. For more information
145
+on the settings and their possible values, see HELP SET.`,
146
+			helpShort: `$bGET$b queries the current values of a channel's settings`,
147
+			enabled:   chanregEnabled,
148
+			minParams: 2,
149
+		},
150
+		"set": {
151
+			handler:   csSetHandler,
152
+			helpShort: `$bSET$b modifies a channel's settings`,
153
+			// these are broken out as separate strings so they can be translated separately
154
+			helpStrings: []string{
155
+				`Syntax $bSET #channel <setting> <value>$b
156
+
157
+SET modifies a channel's settings. The following settings are available:`,
158
+
159
+				`$bHISTORY$b
160
+'history' lets you control how channel history is stored. Your options are:
161
+1. 'off'        [no history]
162
+2. 'ephemeral'  [a limited amount of temporary history, not stored on disk]
163
+3. 'on'         [history stored in a permanent database, if available]
164
+4. 'default'    [use the server default]`,
165
+			},
166
+			enabled:   chanregEnabled,
167
+			minParams: 3,
168
+		},
140 169
 	}
141 170
 )
142 171
 
@@ -320,6 +349,22 @@ func checkChanLimit(client *Client, rb *ResponseBuffer) (ok bool) {
320 349
 	return
321 350
 }
322 351
 
352
+func csPrivsCheck(channel RegisteredChannel, client *Client, rb *ResponseBuffer) (success bool) {
353
+	founder := channel.Founder
354
+	if founder == "" {
355
+		csNotice(rb, client.t("That channel is not registered"))
356
+		return false
357
+	}
358
+	if client.HasRoleCapabs("chanreg") {
359
+		return true
360
+	}
361
+	if founder != client.Account() {
362
+		csNotice(rb, client.t("Insufficient privileges"))
363
+		return false
364
+	}
365
+	return true
366
+}
367
+
323 368
 func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
324 369
 	channelName := params[0]
325 370
 	var verificationCode string
@@ -327,31 +372,18 @@ func csUnregisterHandler(server *Server, client *Client, command string, params
327 372
 		verificationCode = params[1]
328 373
 	}
329 374
 
330
-	channelKey, err := CasefoldChannel(channelName)
331
-	if channelKey == "" || err != nil {
332
-		csNotice(rb, client.t("Channel name is not valid"))
333
-		return
334
-	}
335
-
336
-	channel := server.channels.Get(channelKey)
375
+	channel := server.channels.Get(channelName)
337 376
 	if channel == nil {
338 377
 		csNotice(rb, client.t("No such channel"))
339 378
 		return
340 379
 	}
341 380
 
342
-	founder := channel.Founder()
343
-	if founder == "" {
344
-		csNotice(rb, client.t("That channel is not registered"))
345
-		return
346
-	}
347
-
348
-	hasPrivs := client.HasRoleCapabs("chanreg") || founder == client.Account()
349
-	if !hasPrivs {
350
-		csNotice(rb, client.t("Insufficient privileges"))
381
+	info := channel.ExportRegistration(0)
382
+	channelKey := info.NameCasefolded
383
+	if !csPrivsCheck(info, client, rb) {
351 384
 		return
352 385
 	}
353 386
 
354
-	info := channel.ExportRegistration(0)
355 387
 	expectedCode := unregisterConfirmationCode(info.Name, info.RegisteredAt)
356 388
 	if expectedCode != verificationCode {
357 389
 		csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
@@ -359,7 +391,7 @@ func csUnregisterHandler(server *Server, client *Client, command string, params
359 391
 		return
360 392
 	}
361 393
 
362
-	server.channels.SetUnregistered(channelKey, founder)
394
+	server.channels.SetUnregistered(channelKey, info.Founder)
363 395
 	csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
364 396
 }
365 397
 
@@ -377,9 +409,7 @@ func csClearHandler(server *Server, client *Client, command string, params []str
377 409
 		csNotice(rb, client.t("Channel does not exist"))
378 410
 		return
379 411
 	}
380
-	account := client.Account()
381
-	if !(client.HasRoleCapabs("chanreg") || (account != "" && account == channel.Founder())) {
382
-		csNotice(rb, client.t("Insufficient privileges"))
412
+	if !csPrivsCheck(channel.ExportRegistration(0), client, rb) {
383 413
 		return
384 414
 	}
385 415
 
@@ -573,3 +603,68 @@ func csInfoHandler(server *Server, client *Client, command string, params []stri
573 603
 	csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
574 604
 	csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
575 605
 }
606
+
607
+func displayChannelSetting(settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {
608
+	config := client.server.Config()
609
+
610
+	switch strings.ToLower(settingName) {
611
+	case "history":
612
+		effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
613
+		csNotice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
614
+		csNotice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
615
+	default:
616
+		csNotice(rb, client.t("Invalid params"))
617
+	}
618
+}
619
+
620
+func csGetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
621
+	chname, setting := params[0], params[1]
622
+	channel := server.channels.Get(chname)
623
+	if channel == nil {
624
+		csNotice(rb, client.t("No such channel"))
625
+		return
626
+	}
627
+	info := channel.ExportRegistration(IncludeSettings)
628
+	if !csPrivsCheck(info, client, rb) {
629
+		return
630
+	}
631
+
632
+	displayChannelSetting(setting, info.Settings, client, rb)
633
+}
634
+
635
+func csSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
636
+	chname, setting, value := params[0], params[1], params[2]
637
+	channel := server.channels.Get(chname)
638
+	if channel == nil {
639
+		csNotice(rb, client.t("No such channel"))
640
+		return
641
+	}
642
+	info := channel.ExportRegistration(IncludeSettings)
643
+	settings := info.Settings
644
+	if !csPrivsCheck(info, client, rb) {
645
+		return
646
+	}
647
+
648
+	var err error
649
+	switch strings.ToLower(setting) {
650
+	case "history":
651
+		settings.History, err = historyStatusFromString(value)
652
+		if err != nil {
653
+			err = errInvalidParams
654
+			break
655
+		}
656
+		channel.SetSettings(settings)
657
+		channel.resizeHistory(server.Config())
658
+	}
659
+
660
+	switch err {
661
+	case nil:
662
+		csNotice(rb, client.t("Successfully changed the channel settings"))
663
+		displayChannelSetting(setting, settings, client, rb)
664
+	case errInvalidParams:
665
+		csNotice(rb, client.t("Invalid parameters"))
666
+	default:
667
+		server.logger.Error("internal", "CS SET error:", err.Error())
668
+		csNotice(rb, client.t("An error occurred"))
669
+	}
670
+}

+ 258
- 60
irc/client.go 查看文件

@@ -29,7 +29,7 @@ import (
29 29
 const (
30 30
 	// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
31 31
 	IdentTimeoutSeconds  = 1.5
32
-	IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
32
+	IRCv3TimestampFormat = utils.IRCv3TimestampFormat
33 33
 )
34 34
 
35 35
 // ResumeDetails is a place to stash data at various stages of
@@ -45,6 +45,7 @@ type ResumeDetails struct {
45 45
 type Client struct {
46 46
 	account            string
47 47
 	accountName        string // display name of the account: uncasefolded, '*' if not logged in
48
+	accountRegDate     time.Time
48 49
 	accountSettings    AccountSettings
49 50
 	atime              time.Time
50 51
 	away               bool
@@ -55,12 +56,12 @@ type Client struct {
55 56
 	ctime              time.Time
56 57
 	destroyed          bool
57 58
 	exitedSnomaskSent  bool
58
-	flags              modes.ModeSet
59
+	modes              modes.ModeSet
59 60
 	hostname           string
60 61
 	invitedTo          map[string]bool
61 62
 	isSTSOnly          bool
62
-	isTor              bool
63 63
 	languages          []string
64
+	lastSignoff        time.Time // for always-on clients, the time their last session quit
64 65
 	loginThrottle      connection_limits.GenericThrottle
65 66
 	nick               string
66 67
 	nickCasefolded     string
@@ -84,9 +85,12 @@ type Client struct {
84 85
 	skeleton           string
85 86
 	sessions           []*Session
86 87
 	stateMutex         sync.RWMutex // tier 1
88
+	alwaysOn           bool
87 89
 	username           string
88 90
 	vhost              string
89 91
 	history            history.Buffer
92
+	dirtyBits          uint
93
+	writerSemaphore    utils.Semaphore // tier 1.5
90 94
 }
91 95
 
92 96
 // Session is an individual client connection to the server (TCP connection
@@ -102,6 +106,7 @@ type Session struct {
102 106
 	realIP      net.IP
103 107
 	proxiedIP   net.IP
104 108
 	rawHostname string
109
+	isTor       bool
105 110
 
106 111
 	idletimer IdleTimer
107 112
 	fakelag   Fakelag
@@ -120,6 +125,7 @@ type Session struct {
120 125
 	resumeID         string
121 126
 	resumeDetails    *ResumeDetails
122 127
 	zncPlaybackTimes *zncPlaybackTimes
128
+	lastSignoff      time.Time
123 129
 
124 130
 	batch MultilineBatch
125 131
 }
@@ -147,6 +153,13 @@ func (sd *Session) SetQuitMessage(message string) (set bool) {
147 153
 	}
148 154
 }
149 155
 
156
+func (s *Session) IP() net.IP {
157
+	if s.proxiedIP != nil {
158
+		return s.proxiedIP
159
+	}
160
+	return s.realIP
161
+}
162
+
150 163
 // returns whether the session was actively destroyed (for example, by ping
151 164
 // timeout or NS GHOST).
152 165
 // avoids a race condition between asynchronous idle-timing-out of sessions,
@@ -164,8 +177,7 @@ func (session *Session) SetDestroyed() {
164 177
 // returns whether the client supports a smart history replay cap,
165 178
 // and therefore autoreplay-on-join and similar should be suppressed
166 179
 func (session *Session) HasHistoryCaps() bool {
167
-	// TODO the chathistory cap will go here as well
168
-	return session.capabilities.Has(caps.ZNCPlayback)
180
+	return session.capabilities.Has(caps.Chathistory) || session.capabilities.Has(caps.ZNCPlayback)
169 181
 }
170 182
 
171 183
 // generates a batch ID. the uniqueness requirements for this are fairly weak:
@@ -231,7 +243,6 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
231 243
 		channels:  make(ChannelSet),
232 244
 		ctime:     now,
233 245
 		isSTSOnly: conn.Config.STSOnly,
234
-		isTor:     conn.Config.Tor,
235 246
 		languages: server.Languages().Default(),
236 247
 		loginThrottle: connection_limits.GenericThrottle{
237 248
 			Duration: config.Accounts.LoginThrottling.Duration,
@@ -253,6 +264,7 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
253 264
 		ctime:      now,
254 265
 		atime:      now,
255 266
 		realIP:     realIP,
267
+		isTor:      conn.Config.Tor,
256 268
 	}
257 269
 	client.sessions = []*Session{session}
258 270
 
@@ -272,7 +284,7 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
272 284
 		client.rawHostname = session.rawHostname
273 285
 	} else {
274 286
 		remoteAddr := conn.Conn.RemoteAddr()
275
-		if utils.AddrIsLocal(remoteAddr) {
287
+		if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) {
276 288
 			// treat local connections as secure (may be overridden later by WEBIRC)
277 289
 			client.SetMode(modes.TLS, true)
278 290
 		}
@@ -286,10 +298,65 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
286 298
 	client.run(session, proxyLine)
287 299
 }
288 300
 
301
+func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string) {
302
+	now := time.Now().UTC()
303
+	config := server.Config()
304
+
305
+	client := &Client{
306
+		atime:     now,
307
+		channels:  make(ChannelSet),
308
+		ctime:     now,
309
+		languages: server.Languages().Default(),
310
+		server:    server,
311
+
312
+		// TODO figure out how to set these on reattach?
313
+		username:    "~user",
314
+		rawHostname: server.name,
315
+		realIP:      utils.IPv4LoopbackAddress,
316
+
317
+		alwaysOn: true,
318
+	}
319
+
320
+	client.SetMode(modes.TLS, true)
321
+	client.writerSemaphore.Initialize(1)
322
+	client.history.Initialize(0, 0)
323
+	client.brbTimer.Initialize(client)
324
+
325
+	server.accounts.Login(client, account)
326
+
327
+	client.resizeHistory(config)
328
+
329
+	_, err := server.clients.SetNick(client, nil, account.Name)
330
+	if err != nil {
331
+		server.logger.Error("internal", "could not establish always-on client", account.Name, err.Error())
332
+		return
333
+	} else {
334
+		server.logger.Debug("accounts", "established always-on client", account.Name)
335
+	}
336
+
337
+	// XXX set this last to avoid confusing SetNick:
338
+	client.registered = true
339
+
340
+	for _, chname := range chnames {
341
+		// XXX we're using isSajoin=true, to make these joins succeed even without channel key
342
+		// this is *probably* ok as long as the persisted memberships are accurate
343
+		server.channels.Join(client, chname, "", true, nil)
344
+	}
345
+}
346
+
347
+func (client *Client) resizeHistory(config *Config) {
348
+	_, ephemeral := client.historyStatus(config)
349
+	if ephemeral {
350
+		client.history.Resize(config.History.ClientLength, config.History.AutoresizeWindow)
351
+	} else {
352
+		client.history.Resize(0, 0)
353
+	}
354
+}
355
+
289 356
 // resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
290 357
 // and sending appropriate notices to the client
291 358
 func (client *Client) lookupHostname(session *Session, overwrite bool) {
292
-	if client.isTor {
359
+	if session.isTor {
293 360
 		return
294 361
 	} // else: even if cloaking is enabled, look up the real hostname to show to operators
295 362
 
@@ -384,14 +451,14 @@ const (
384 451
 	authFailSaslRequired
385 452
 )
386 453
 
387
-func (client *Client) isAuthorized(config *Config) AuthOutcome {
454
+func (client *Client) isAuthorized(config *Config, isTor bool) AuthOutcome {
388 455
 	saslSent := client.account != ""
389 456
 	// PASS requirement
390 457
 	if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) {
391 458
 		return authFailPass
392 459
 	}
393 460
 	// Tor connections may be required to authenticate with SASL
394
-	if client.isTor && config.Server.TorListeners.RequireSasl && !saslSent {
461
+	if isTor && config.Server.TorListeners.RequireSasl && !saslSent {
395 462
 		return authFailTorSaslRequired
396 463
 	}
397 464
 	// finally, enforce require-sasl
@@ -572,9 +639,13 @@ func (client *Client) run(session *Session, proxyLine string) {
572 639
 
573 640
 func (client *Client) playReattachMessages(session *Session) {
574 641
 	client.server.playRegistrationBurst(session)
642
+	hasHistoryCaps := session.HasHistoryCaps()
575 643
 	for _, channel := range session.client.Channels() {
576 644
 		channel.playJoinForSession(session)
577
-		// clients should receive autoreplay-on-join lines, if applicable;
645
+		// clients should receive autoreplay-on-join lines, if applicable:
646
+		if hasHistoryCaps {
647
+			continue
648
+		}
578 649
 		// if they negotiated znc.in/playback or chathistory, they will receive nothing,
579 650
 		// because those caps disable autoreplay-on-join and they haven't sent the relevant
580 651
 		// *playback PRIVMSG or CHATHISTORY command yet
@@ -582,6 +653,12 @@ func (client *Client) playReattachMessages(session *Session) {
582 653
 		channel.autoReplayHistory(client, rb, "")
583 654
 		rb.Send(true)
584 655
 	}
656
+	if !session.lastSignoff.IsZero() && !hasHistoryCaps {
657
+		rb := NewResponseBuffer(session)
658
+		zncPlayPrivmsgs(client, rb, session.lastSignoff, time.Time{})
659
+		rb.Send(true)
660
+	}
661
+	session.lastSignoff = time.Time{}
585 662
 }
586 663
 
587 664
 //
@@ -634,11 +711,6 @@ func (session *Session) tryResume() (success bool) {
634 711
 		return
635 712
 	}
636 713
 
637
-	if oldClient.isTor != client.isTor {
638
-		session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection from Tor to non-Tor or vice versa"))
639
-		return
640
-	}
641
-
642 714
 	err := server.clients.Resume(oldClient, session)
643 715
 	if err != nil {
644 716
 		session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
@@ -657,37 +729,45 @@ func (session *Session) tryResume() (success bool) {
657 729
 func (session *Session) playResume() {
658 730
 	client := session.client
659 731
 	server := client.server
732
+	config := server.Config()
660 733
 
661 734
 	friends := make(ClientSet)
662
-	oldestLostMessage := time.Now().UTC()
735
+	var oldestLostMessage time.Time
663 736
 
664 737
 	// work out how much time, if any, is not covered by history buffers
738
+	// assume that a persistent buffer covers the whole resume period
665 739
 	for _, channel := range client.Channels() {
666 740
 		for _, member := range channel.Members() {
667 741
 			friends.Add(member)
742
+		}
743
+		_, ephemeral, _ := channel.historyStatus(config)
744
+		if ephemeral {
668 745
 			lastDiscarded := channel.history.LastDiscarded()
669
-			if lastDiscarded.Before(oldestLostMessage) {
746
+			if oldestLostMessage.Before(lastDiscarded) {
670 747
 				oldestLostMessage = lastDiscarded
671 748
 			}
672 749
 		}
673 750
 	}
674
-	privmsgMatcher := func(item history.Item) bool {
675
-		return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
676
-	}
677
-	privmsgHistory := client.history.Match(privmsgMatcher, false, 0)
678
-	lastDiscarded := client.history.LastDiscarded()
679
-	if lastDiscarded.Before(oldestLostMessage) {
680
-		oldestLostMessage = lastDiscarded
751
+	_, cEphemeral := client.historyStatus(config)
752
+	if cEphemeral {
753
+		lastDiscarded := client.history.LastDiscarded()
754
+		if oldestLostMessage.Before(lastDiscarded) {
755
+			oldestLostMessage = lastDiscarded
756
+		}
681 757
 	}
682
-	for _, item := range privmsgHistory {
683
-		sender := server.clients.Get(stripMaskFromNick(item.Nick))
684
-		if sender != nil {
685
-			friends.Add(sender)
758
+	_, privmsgSeq, _ := server.GetHistorySequence(nil, client, "*")
759
+	if privmsgSeq != nil {
760
+		privmsgs, _, _ := privmsgSeq.Between(history.Selector{}, history.Selector{}, config.History.ClientLength)
761
+		for _, item := range privmsgs {
762
+			sender := server.clients.Get(stripMaskFromNick(item.Nick))
763
+			if sender != nil {
764
+				friends.Add(sender)
765
+			}
686 766
 		}
687 767
 	}
688 768
 
689 769
 	timestamp := session.resumeDetails.Timestamp
690
-	gap := lastDiscarded.Sub(timestamp)
770
+	gap := oldestLostMessage.Sub(timestamp)
691 771
 	session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
692 772
 	gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
693 773
 
@@ -723,10 +803,12 @@ func (session *Session) playResume() {
723 803
 		}
724 804
 	}
725 805
 
726
-	if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
727
-		session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
728
-	} else {
729
-		session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
806
+	if session.resumeDetails.HistoryIncomplete {
807
+		if !timestamp.IsZero() {
808
+			session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
809
+		} else {
810
+			session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
811
+		}
730 812
 	}
731 813
 
732 814
 	session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
@@ -738,23 +820,26 @@ func (session *Session) playResume() {
738 820
 	}
739 821
 
740 822
 	// replay direct PRIVSMG history
741
-	if !timestamp.IsZero() {
742
-		now := time.Now().UTC()
743
-		items, complete := client.history.Between(timestamp, now, false, 0)
744
-		rb := NewResponseBuffer(client.Sessions()[0])
745
-		client.replayPrivmsgHistory(rb, items, complete)
823
+	if !timestamp.IsZero() && privmsgSeq != nil {
824
+		after := history.Selector{Time: timestamp}
825
+		items, complete, _ := privmsgSeq.Between(after, history.Selector{}, config.History.ZNCMax)
826
+		rb := NewResponseBuffer(session)
827
+		client.replayPrivmsgHistory(rb, items, "", complete)
746 828
 		rb.Send(true)
747 829
 	}
748 830
 
749 831
 	session.resumeDetails = nil
750 832
 }
751 833
 
752
-func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
834
+func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, complete bool) {
753 835
 	var batchID string
754 836
 	details := client.Details()
755 837
 	nick := details.nick
756 838
 	if 0 < len(items) {
757
-		batchID = rb.StartNestedHistoryBatch(nick)
839
+		if target == "" {
840
+			target = nick
841
+		}
842
+		batchID = rb.StartNestedHistoryBatch(target)
758 843
 	}
759 844
 
760 845
 	allowTags := rb.session.capabilities.Has(caps.MessageTags)
@@ -778,12 +863,12 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
778 863
 		if allowTags {
779 864
 			tags = item.Tags
780 865
 		}
781
-		if item.Params[0] == "" {
866
+		if item.Params[0] == "" || item.Params[0] == nick {
782 867
 			// this message was sent *to* the client from another nick
783 868
 			rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
784 869
 		} else {
785 870
 			// this message was sent *from* the client to another nick; the target is item.Params[0]
786
-			// substitute the client's current nickmask in case they changed nick
871
+			// substitute client's current nickmask in case client changed nick
787 872
 			rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
788 873
 		}
789 874
 	}
@@ -875,7 +960,7 @@ func (client *Client) HasRoleCapabs(capabs ...string) bool {
875 960
 
876 961
 // ModeString returns the mode string for this client.
877 962
 func (client *Client) ModeString() (str string) {
878
-	return "+" + client.flags.String()
963
+	return "+" + client.modes.String()
879 964
 }
880 965
 
881 966
 // Friends refers to clients that share a channel with this client.
@@ -1053,6 +1138,12 @@ func (client *Client) Quit(message string, session *Session) {
1053 1138
 // has no more sessions.
1054 1139
 func (client *Client) destroy(session *Session) {
1055 1140
 	var sessionsToDestroy []*Session
1141
+	var lastSignoff time.Time
1142
+	if session != nil {
1143
+		lastSignoff = session.idletimer.LastTouch()
1144
+	} else {
1145
+		lastSignoff = time.Now().UTC()
1146
+	}
1056 1147
 
1057 1148
 	client.stateMutex.Lock()
1058 1149
 	details := client.detailsNoMutex()
@@ -1060,6 +1151,8 @@ func (client *Client) destroy(session *Session) {
1060 1151
 	brbAt := client.brbTimer.brbAt
1061 1152
 	wasReattach := session != nil && session.client != client
1062 1153
 	sessionRemoved := false
1154
+	registered := client.registered
1155
+	alwaysOn := client.alwaysOn
1063 1156
 	var remainingSessions int
1064 1157
 	if session == nil {
1065 1158
 		sessionsToDestroy = client.sessions
@@ -1074,12 +1167,15 @@ func (client *Client) destroy(session *Session) {
1074 1167
 
1075 1168
 	// should we destroy the whole client this time?
1076 1169
 	// BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
1077
-	brbEligible := session != nil && (brbState == BrbEnabled || brbState == BrbSticky)
1170
+	brbEligible := session != nil && (brbState == BrbEnabled || alwaysOn)
1078 1171
 	shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible
1079 1172
 	if shouldDestroy {
1080 1173
 		// if it's our job to destroy it, don't let anyone else try
1081 1174
 		client.destroyed = true
1082 1175
 	}
1176
+	if alwaysOn && remainingSessions == 0 {
1177
+		client.lastSignoff = lastSignoff
1178
+	}
1083 1179
 	exitedSnomaskSent := client.exitedSnomaskSent
1084 1180
 	client.stateMutex.Unlock()
1085 1181
 
@@ -1099,7 +1195,7 @@ func (client *Client) destroy(session *Session) {
1099 1195
 
1100 1196
 		// remove from connection limits
1101 1197
 		var source string
1102
-		if client.isTor {
1198
+		if session.isTor {
1103 1199
 			client.server.torLimiter.RemoveClient()
1104 1200
 			source = "tor"
1105 1201
 		} else {
@@ -1113,11 +1209,33 @@ func (client *Client) destroy(session *Session) {
1113 1209
 		client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
1114 1210
 	}
1115 1211
 
1212
+	// decrement stats if we have no more sessions, even if the client will not be destroyed
1213
+	if shouldDestroy || remainingSessions == 0 {
1214
+		invisible := client.HasMode(modes.Invisible)
1215
+		operator := client.HasMode(modes.LocalOperator) || client.HasMode(modes.Operator)
1216
+		client.server.stats.Remove(registered, invisible, operator)
1217
+	}
1218
+
1116 1219
 	// do not destroy the client if it has either remaining sessions, or is BRB'ed
1117 1220
 	if !shouldDestroy {
1118 1221
 		return
1119 1222
 	}
1120 1223
 
1224
+	splitQuitMessage := utils.MakeMessage(quitMessage)
1225
+	quitItem := history.Item{
1226
+		Type:        history.Quit,
1227
+		Nick:        details.nickMask,
1228
+		AccountName: details.accountName,
1229
+		Message:     splitQuitMessage,
1230
+	}
1231
+	var channels []*Channel
1232
+	defer func() {
1233
+		for _, channel := range channels {
1234
+			// TODO it's dangerous to write to mysql while holding the destroy semaphore
1235
+			channel.AddHistoryItem(quitItem)
1236
+		}
1237
+	}()
1238
+
1121 1239
 	// see #235: deduplicating the list of PART recipients uses (comparatively speaking)
1122 1240
 	// a lot of RAM, so limit concurrency to avoid thrashing
1123 1241
 	client.server.semaphores.ClientDestroy.Acquire()
@@ -1127,7 +1245,6 @@ func (client *Client) destroy(session *Session) {
1127 1245
 		client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
1128 1246
 	}
1129 1247
 
1130
-	registered := client.Registered()
1131 1248
 	if registered {
1132 1249
 		client.server.whoWas.Append(client.WhoWas())
1133 1250
 	}
@@ -1141,18 +1258,12 @@ func (client *Client) destroy(session *Session) {
1141 1258
 	// clean up monitor state
1142 1259
 	client.server.monitorManager.RemoveAll(client)
1143 1260
 
1144
-	splitQuitMessage := utils.MakeMessage(quitMessage)
1145 1261
 	// clean up channels
1146 1262
 	// (note that if this is a reattach, client has no channels and therefore no friends)
1147 1263
 	friends := make(ClientSet)
1148
-	for _, channel := range client.Channels() {
1264
+	channels = client.Channels()
1265
+	for _, channel := range channels {
1149 1266
 		channel.Quit(client)
1150
-		channel.history.Add(history.Item{
1151
-			Type:        history.Quit,
1152
-			Nick:        details.nickMask,
1153
-			AccountName: details.accountName,
1154
-			Message:     splitQuitMessage,
1155
-		})
1156 1267
 		for _, member := range channel.Members() {
1157 1268
 			friends.Add(member)
1158 1269
 		}
@@ -1168,9 +1279,6 @@ func (client *Client) destroy(session *Session) {
1168 1279
 
1169 1280
 	client.server.accounts.Logout(client)
1170 1281
 
1171
-	client.server.stats.Remove(registered, client.HasMode(modes.Invisible),
1172
-		client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator))
1173
-
1174 1282
 	// this happens under failure to return from BRB
1175 1283
 	if quitMessage == "" {
1176 1284
 		if brbState == BrbDead && !brbAt.IsZero() {
@@ -1196,11 +1304,10 @@ func (client *Client) destroy(session *Session) {
1196 1304
 // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
1197 1305
 // Adds account-tag to the line as well.
1198 1306
 func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
1199
-	// TODO no maxline support
1200 1307
 	if message.Is512() {
1201 1308
 		session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
1202 1309
 	} else {
1203
-		if message.IsMultiline() && session.capabilities.Has(caps.Multiline) {
1310
+		if session.capabilities.Has(caps.Multiline) {
1204 1311
 			for _, msg := range session.composeMultilineBatch(nickmask, accountName, tags, command, target, message) {
1205 1312
 				session.SendRawMessage(msg, blocking)
1206 1313
 			}
@@ -1366,13 +1473,23 @@ func (session *Session) Notice(text string) {
1366 1473
 func (client *Client) addChannel(channel *Channel) {
1367 1474
 	client.stateMutex.Lock()
1368 1475
 	client.channels[channel] = true
1476
+	alwaysOn := client.alwaysOn
1369 1477
 	client.stateMutex.Unlock()
1478
+
1479
+	if alwaysOn {
1480
+		client.markDirty(IncludeChannels)
1481
+	}
1370 1482
 }
1371 1483
 
1372 1484
 func (client *Client) removeChannel(channel *Channel) {
1373 1485
 	client.stateMutex.Lock()
1374 1486
 	delete(client.channels, channel)
1487
+	alwaysOn := client.alwaysOn
1375 1488
 	client.stateMutex.Unlock()
1489
+
1490
+	if alwaysOn {
1491
+		client.markDirty(IncludeChannels)
1492
+	}
1376 1493
 }
1377 1494
 
1378 1495
 // Records that the client has been invited to join an invite-only channel
@@ -1413,3 +1530,84 @@ func (client *Client) attemptAutoOper(session *Session) {
1413 1530
 		}
1414 1531
 	}
1415 1532
 }
1533
+
1534
+func (client *Client) historyStatus(config *Config) (persistent, ephemeral bool) {
1535
+	if !config.History.Enabled {
1536
+		return false, false
1537
+	} else if !config.History.Persistent.Enabled {
1538
+		return false, true
1539
+	}
1540
+
1541
+	client.stateMutex.RLock()
1542
+	alwaysOn := client.alwaysOn
1543
+	historyStatus := client.accountSettings.DMHistory
1544
+	client.stateMutex.RUnlock()
1545
+
1546
+	if !alwaysOn {
1547
+		return false, true
1548
+	}
1549
+
1550
+	historyStatus = historyEnabled(config.History.Persistent.DirectMessages, historyStatus)
1551
+	ephemeral = (historyStatus == HistoryEphemeral)
1552
+	persistent = (historyStatus == HistoryPersistent)
1553
+	return
1554
+}
1555
+
1556
+// these are bit flags indicating what part of the client status is "dirty"
1557
+// and needs to be read from memory and written to the db
1558
+// TODO add a dirty flag for lastSignoff
1559
+const (
1560
+	IncludeChannels uint = 1 << iota
1561
+)
1562
+
1563
+func (client *Client) markDirty(dirtyBits uint) {
1564
+	client.stateMutex.Lock()
1565
+	alwaysOn := client.alwaysOn
1566
+	client.dirtyBits = client.dirtyBits | dirtyBits
1567
+	client.stateMutex.Unlock()
1568
+
1569
+	if alwaysOn {
1570
+		client.wakeWriter()
1571
+	}
1572
+}
1573
+
1574
+func (client *Client) wakeWriter() {
1575
+	if client.writerSemaphore.TryAcquire() {
1576
+		go client.writeLoop()
1577
+	}
1578
+}
1579
+
1580
+func (client *Client) writeLoop() {
1581
+	for {
1582
+		client.performWrite()
1583
+		client.writerSemaphore.Release()
1584
+
1585
+		client.stateMutex.RLock()
1586
+		isDirty := client.dirtyBits != 0
1587
+		client.stateMutex.RUnlock()
1588
+
1589
+		if !isDirty || !client.writerSemaphore.TryAcquire() {
1590
+			return
1591
+		}
1592
+	}
1593
+}
1594
+
1595
+func (client *Client) performWrite() {
1596
+	client.stateMutex.Lock()
1597
+	// TODO actually read dirtyBits in the future
1598
+	client.dirtyBits = 0
1599
+	account := client.account
1600
+	client.stateMutex.Unlock()
1601
+
1602
+	if account == "" {
1603
+		client.server.logger.Error("internal", "attempting to persist logged-out client", client.Nick())
1604
+		return
1605
+	}
1606
+
1607
+	channels := client.Channels()
1608
+	channelNames := make([]string, len(channels))
1609
+	for i, channel := range channels {
1610
+		channelNames[i] = channel.Name()
1611
+	}
1612
+	client.server.accounts.saveChannels(account, channelNames)
1613
+}

+ 51
- 19
irc/client_lookup_set.go 查看文件

@@ -105,7 +105,8 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
105 105
 		return errNickMissing
106 106
 	}
107 107
 
108
-	if !oldClient.AddSession(session) {
108
+	success, _, _ := oldClient.AddSession(session)
109
+	if !success {
109 110
 		return errNickMissing
110 111
 	}
111 112
 
@@ -113,32 +114,50 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
113 114
 }
114 115
 
115 116
 // SetNick sets a client's nickname, validating it against nicknames in use
116
-func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) error {
117
-	if len(newNick) > client.server.Config().Limits.NickLen {
118
-		return errNicknameInvalid
119
-	}
117
+func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) (setNick string, err error) {
120 118
 	newcfnick, err := CasefoldName(newNick)
121 119
 	if err != nil {
122
-		return errNicknameInvalid
120
+		return "", errNicknameInvalid
121
+	}
122
+	if len(newcfnick) > client.server.Config().Limits.NickLen {
123
+		return "", errNicknameInvalid
123 124
 	}
124 125
 	newSkeleton, err := Skeleton(newNick)
125 126
 	if err != nil {
126
-		return errNicknameInvalid
127
+		return "", errNicknameInvalid
127 128
 	}
128 129
 
129 130
 	if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
130
-		return errNicknameInvalid
131
+		return "", errNicknameInvalid
131 132
 	}
132 133
 
133 134
 	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
134
-	account := client.Account()
135 135
 	config := client.server.Config()
136
+	client.stateMutex.RLock()
137
+	account := client.account
138
+	accountName := client.accountName
139
+	settings := client.accountSettings
140
+	registered := client.registered
141
+	realname := client.realname
142
+	client.stateMutex.RUnlock()
143
+
144
+	// recompute this (client.alwaysOn is not set for unregistered clients):
145
+	alwaysOn := account != "" && persistenceEnabled(config.Accounts.Bouncer.AlwaysOn, settings.AlwaysOn)
146
+
147
+	if alwaysOn && registered {
148
+		return "", errCantChangeNick
149
+	}
150
+
136 151
 	var bouncerAllowed bool
137 152
 	if config.Accounts.Bouncer.Enabled {
138
-		if session != nil && session.capabilities.Has(caps.Bouncer) {
153
+		if alwaysOn {
154
+			// ignore the pre-reg nick, force a reattach
155
+			newNick = accountName
156
+			newcfnick = account
157
+			bouncerAllowed = true
158
+		} else if session != nil && session.capabilities.Has(caps.Bouncer) {
139 159
 			bouncerAllowed = true
140 160
 		} else {
141
-			settings := client.AccountSettings()
142 161
 			if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
143 162
 				bouncerAllowed = true
144 163
 			} else if settings.AllowBouncer == BouncerAllowedByUser {
@@ -154,28 +173,41 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
154 173
 	// the client may just be changing case
155 174
 	if currentClient != nil && currentClient != client && session != nil {
156 175
 		// these conditions forbid reattaching to an existing session:
157
-		if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
158
-			return errNicknameInUse
176
+		if registered || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
177
+			return "", errNicknameInUse
178
+		}
179
+		reattachSuccessful, numSessions, lastSignoff := currentClient.AddSession(session)
180
+		if !reattachSuccessful {
181
+			return "", errNicknameInUse
159 182
 		}
160
-		if !currentClient.AddSession(session) {
161
-			return errNicknameInUse
183
+		if numSessions == 1 {
184
+			invisible := client.HasMode(modes.Invisible)
185
+			operator := client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator)
186
+			client.server.stats.AddRegistered(invisible, operator)
162 187
 		}
188
+		session.lastSignoff = lastSignoff
189
+		// XXX SetNames only changes names if they are unset, so the realname change only
190
+		// takes effect on first attach to an always-on client (good), but the user/ident
191
+		// change is always a no-op (bad). we could make user/ident act the same way as
192
+		// realname, but then we'd have to send CHGHOST and i don't want to deal with that
193
+		// for performance reasons
194
+		currentClient.SetNames("user", realname, true)
163 195
 		// successful reattach!
164
-		return nil
196
+		return newNick, nil
165 197
 	}
166 198
 	// analogous checks for skeletons
167 199
 	skeletonHolder := clients.bySkeleton[newSkeleton]
168 200
 	if skeletonHolder != nil && skeletonHolder != client {
169
-		return errNicknameInUse
201
+		return "", errNicknameInUse
170 202
 	}
171 203
 	if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
172
-		return errNicknameReserved
204
+		return "", errNicknameReserved
173 205
 	}
174 206
 	clients.removeInternal(client)
175 207
 	clients.byNick[newcfnick] = client
176 208
 	clients.bySkeleton[newSkeleton] = client
177 209
 	client.updateNick(newNick, newcfnick, newSkeleton)
178
-	return nil
210
+	return newNick, nil
179 211
 }
180 212
 
181 213
 func (clients *ClientManager) AllClients() (result []*Client) {

+ 7
- 6
irc/commands.go 查看文件

@@ -54,6 +54,12 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
54 54
 		return cmd.handler(server, client, msg, rb)
55 55
 	}()
56 56
 
57
+	// most servers do this only for PING/PONG, but we'll do it for any command:
58
+	if client.registered {
59
+		// touch even if `exiting`, so we record the time of a QUIT accurately
60
+		session.idletimer.Touch()
61
+	}
62
+
57 63
 	if exiting {
58 64
 		return
59 65
 	}
@@ -63,11 +69,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
63 69
 		exiting = server.tryRegister(client, session)
64 70
 	}
65 71
 
66
-	// most servers do this only for PING/PONG, but we'll do it for any command:
67
-	if client.registered {
68
-		session.idletimer.Touch()
69
-	}
70
-
71 72
 	if client.registered && !cmd.leaveClientIdle {
72 73
 		client.Active(session)
73 74
 	}
@@ -109,7 +110,7 @@ func init() {
109 110
 		},
110 111
 		"CHATHISTORY": {
111 112
 			handler:   chathistoryHandler,
112
-			minParams: 3,
113
+			minParams: 4,
113 114
 		},
114 115
 		"DEBUG": {
115 116
 			handler:   debugHandler,

+ 187
- 1
irc/config.go 查看文件

@@ -61,6 +61,151 @@ type listenerConfig struct {
61 61
 	ProxyBeforeTLS bool
62 62
 }
63 63
 
64
+type PersistentStatus uint
65
+
66
+const (
67
+	PersistentUnspecified PersistentStatus = iota
68
+	PersistentDisabled
69
+	PersistentOptIn
70
+	PersistentOptOut
71
+	PersistentMandatory
72
+)
73
+
74
+func persistentStatusToString(status PersistentStatus) string {
75
+	switch status {
76
+	case PersistentUnspecified:
77
+		return "default"
78
+	case PersistentDisabled:
79
+		return "disabled"
80
+	case PersistentOptIn:
81
+		return "opt-in"
82
+	case PersistentOptOut:
83
+		return "opt-out"
84
+	case PersistentMandatory:
85
+		return "mandatory"
86
+	default:
87
+		return ""
88
+	}
89
+}
90
+
91
+func persistentStatusFromString(status string) (PersistentStatus, error) {
92
+	switch strings.ToLower(status) {
93
+	case "default":
94
+		return PersistentUnspecified, nil
95
+	case "":
96
+		return PersistentDisabled, nil
97
+	case "opt-in":
98
+		return PersistentOptIn, nil
99
+	case "opt-out":
100
+		return PersistentOptOut, nil
101
+	case "mandatory":
102
+		return PersistentMandatory, nil
103
+	default:
104
+		b, err := utils.StringToBool(status)
105
+		if b {
106
+			return PersistentMandatory, err
107
+		} else {
108
+			return PersistentDisabled, err
109
+		}
110
+	}
111
+}
112
+
113
+func (ps *PersistentStatus) UnmarshalYAML(unmarshal func(interface{}) error) error {
114
+	var orig string
115
+	var err error
116
+	if err = unmarshal(&orig); err != nil {
117
+		return err
118
+	}
119
+	result, err := persistentStatusFromString(orig)
120
+	if err == nil {
121
+		if result == PersistentUnspecified {
122
+			result = PersistentDisabled
123
+		}
124
+		*ps = result
125
+	}
126
+	return err
127
+}
128
+
129
+func persistenceEnabled(serverSetting, clientSetting PersistentStatus) (enabled bool) {
130
+	if serverSetting == PersistentDisabled {
131
+		return false
132
+	} else if serverSetting == PersistentMandatory {
133
+		return true
134
+	} else if clientSetting == PersistentDisabled {
135
+		return false
136
+	} else if clientSetting == PersistentMandatory {
137
+		return true
138
+	} else if serverSetting == PersistentOptOut {
139
+		return true
140
+	} else {
141
+		return false
142
+	}
143
+}
144
+
145
+type HistoryStatus uint
146
+
147
+const (
148
+	HistoryDefault HistoryStatus = iota
149
+	HistoryDisabled
150
+	HistoryEphemeral
151
+	HistoryPersistent
152
+)
153
+
154
+func historyStatusFromString(str string) (status HistoryStatus, err error) {
155
+	switch strings.ToLower(str) {
156
+	case "default":
157
+		return HistoryDefault, nil
158
+	case "ephemeral":
159
+		return HistoryEphemeral, nil
160
+	case "persistent":
161
+		return HistoryPersistent, nil
162
+	default:
163
+		b, err := utils.StringToBool(str)
164
+		if b {
165
+			return HistoryPersistent, err
166
+		} else {
167
+			return HistoryDisabled, err
168
+		}
169
+	}
170
+}
171
+
172
+func historyStatusToString(status HistoryStatus) string {
173
+	switch status {
174
+	case HistoryDefault:
175
+		return "default"
176
+	case HistoryDisabled:
177
+		return "disabled"
178
+	case HistoryEphemeral:
179
+		return "ephemeral"
180
+	case HistoryPersistent:
181
+		return "persistent"
182
+	default:
183
+		return ""
184
+	}
185
+}
186
+
187
+func historyEnabled(serverSetting PersistentStatus, localSetting HistoryStatus) (result HistoryStatus) {
188
+	if serverSetting == PersistentDisabled {
189
+		return HistoryDisabled
190
+	} else if serverSetting == PersistentMandatory {
191
+		return HistoryPersistent
192
+	} else if serverSetting == PersistentOptOut {
193
+		if localSetting == HistoryDefault {
194
+			return HistoryPersistent
195
+		} else {
196
+			return localSetting
197
+		}
198
+	} else if serverSetting == PersistentOptIn {
199
+		if localSetting >= HistoryEphemeral {
200
+			return localSetting
201
+		} else {
202
+			return HistoryDisabled
203
+		}
204
+	} else {
205
+		return HistoryDisabled
206
+	}
207
+}
208
+
64 209
 type AccountConfig struct {
65 210
 	Registration          AccountRegistrationConfig
66 211
 	AuthenticationEnabled bool `yaml:"authentication-enabled"`
@@ -79,7 +224,8 @@ type AccountConfig struct {
79 224
 	NickReservation    NickReservationConfig `yaml:"nick-reservation"`
80 225
 	Bouncer            struct {
81 226
 		Enabled          bool
82
-		AllowedByDefault bool `yaml:"allowed-by-default"`
227
+		AllowedByDefault bool             `yaml:"allowed-by-default"`
228
+		AlwaysOn         PersistentStatus `yaml:"always-on"`
83 229
 	}
84 230
 	VHosts VHostConfig
85 231
 }
@@ -340,6 +486,8 @@ type Config struct {
340 486
 		isupport      isupport.List
341 487
 		IPLimits      connection_limits.LimiterConfig `yaml:"ip-limits"`
342 488
 		Cloaks        cloaks.CloakConfig              `yaml:"ip-cloaking"`
489
+		SecureNetDefs []string                        `yaml:"secure-nets"`
490
+		secureNets    []net.IPNet
343 491
 		supportedCaps *caps.Set
344 492
 		capValues     caps.Values
345 493
 		Casemapping   Casemapping
@@ -356,6 +504,14 @@ type Config struct {
356 504
 	Datastore struct {
357 505
 		Path        string
358 506
 		AutoUpgrade bool
507
+		MySQL       struct {
508
+			Enabled         bool
509
+			Host            string
510
+			Port            int
511
+			User            string
512
+			Password        string
513
+			HistoryDatabase string `yaml:"history-database"`
514
+		}
359 515
 	}
360 516
 
361 517
 	Accounts AccountConfig
@@ -395,6 +551,18 @@ type Config struct {
395 551
 		AutoresizeWindow time.Duration `yaml:"autoresize-window"`
396 552
 		AutoreplayOnJoin int           `yaml:"autoreplay-on-join"`
397 553
 		ChathistoryMax   int           `yaml:"chathistory-maxmessages"`
554
+		ZNCMax           int           `yaml:"znc-maxmessages"`
555
+		Restrictions     struct {
556
+			ExpireTime              time.Duration `yaml:"expire-time"`
557
+			EnforceRegistrationDate bool          `yaml:"enforce-registration-date"`
558
+			GracePeriod             time.Duration `yaml:"grace-period"`
559
+		}
560
+		Persistent struct {
561
+			Enabled              bool
562
+			UnregisteredChannels bool             `yaml:"unregistered-channels"`
563
+			RegisteredChannels   PersistentStatus `yaml:"registered-channels"`
564
+			DirectMessages       PersistentStatus `yaml:"direct-messages"`
565
+		}
398 566
 	}
399 567
 
400 568
 	Filename string
@@ -717,7 +885,10 @@ func LoadConfig(filename string) (config *Config, err error) {
717 885
 	}
718 886
 
719 887
 	if !config.Accounts.Bouncer.Enabled {
888
+		config.Accounts.Bouncer.AlwaysOn = PersistentDisabled
720 889
 		config.Server.supportedCaps.Disable(caps.Bouncer)
890
+	} else if config.Accounts.Bouncer.AlwaysOn >= PersistentOptOut {
891
+		config.Accounts.Bouncer.AllowedByDefault = true
721 892
 	}
722 893
 
723 894
 	var newLogConfigs []logger.LoggingConfig
@@ -786,6 +957,11 @@ func LoadConfig(filename string) (config *Config, err error) {
786 957
 		return nil, fmt.Errorf("Could not parse proxy-allowed-from nets: %v", err.Error())
787 958
 	}
788 959
 
960
+	config.Server.secureNets, err = utils.ParseNetList(config.Server.SecureNetDefs)
961
+	if err != nil {
962
+		return nil, fmt.Errorf("Could not parse secure-nets: %v\n", err.Error())
963
+	}
964
+
789 965
 	rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
790 966
 	if rawRegexp != "" {
791 967
 		regexp, err := regexp.Compile(rawRegexp)
@@ -882,6 +1058,16 @@ func LoadConfig(filename string) (config *Config, err error) {
882 1058
 		config.History.ClientLength = 0
883 1059
 	}
884 1060
 
1061
+	if !config.History.Enabled || !config.History.Persistent.Enabled {
1062
+		config.History.Persistent.UnregisteredChannels = false
1063
+		config.History.Persistent.RegisteredChannels = PersistentDisabled
1064
+		config.History.Persistent.DirectMessages = PersistentDisabled
1065
+	}
1066
+
1067
+	if config.History.ZNCMax == 0 {
1068
+		config.History.ZNCMax = config.History.ChathistoryMax
1069
+	}
1070
+
885 1071
 	config.Server.Cloaks.Initialize()
886 1072
 	if config.Server.Cloaks.Enabled {
887 1073
 		if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {

+ 2
- 18
irc/database.go 查看文件

@@ -36,22 +36,6 @@ type SchemaChange struct {
36 36
 // maps an initial version to a schema change capable of upgrading it
37 37
 var schemaChanges map[string]SchemaChange
38 38
 
39
-type incompatibleSchemaError struct {
40
-	currentVersion  string
41
-	requiredVersion string
42
-}
43
-
44
-func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
45
-	return &incompatibleSchemaError{
46
-		currentVersion:  currentVersion,
47
-		requiredVersion: latestDbSchema,
48
-	}
49
-}
50
-
51
-func (err *incompatibleSchemaError) Error() string {
52
-	return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
53
-}
54
-
55 39
 // InitDB creates the database, implementing the `oragono initdb` command.
56 40
 func InitDB(path string) {
57 41
 	_, err := os.Stat(path)
@@ -129,7 +113,7 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
129 113
 		// successful autoupgrade, let's try this again:
130 114
 		return openDatabaseInternal(config, false)
131 115
 	} else {
132
-		err = IncompatibleSchemaError(version)
116
+		err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
133 117
 		return
134 118
 	}
135 119
 }
@@ -173,7 +157,7 @@ func UpgradeDB(config *Config) (err error) {
173 157
 					break
174 158
 				}
175 159
 				// unable to upgrade to the desired version, roll back
176
-				return IncompatibleSchemaError(version)
160
+				return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
177 161
 			}
178 162
 			log.Println("attempting to update schema from version " + version)
179 163
 			err := change.Changer(config, tx)

+ 2
- 0
irc/errors.go 查看文件

@@ -33,6 +33,7 @@ var (
33 33
 	errChannelNotOwnedByAccount       = errors.New("Channel not owned by the specified account")
34 34
 	errChannelTransferNotOffered      = errors.New(`You weren't offered ownership of that channel`)
35 35
 	errChannelAlreadyRegistered       = errors.New("Channel is already registered")
36
+	errChannelNotRegistered           = errors.New("Channel is not registered")
36 37
 	errChannelNameInUse               = errors.New(`Channel name in use`)
37 38
 	errInvalidChannelName             = errors.New(`Invalid channel name`)
38 39
 	errMonitorLimitExceeded           = errors.New("Monitor limit exceeded")
@@ -40,6 +41,7 @@ var (
40 41
 	errNicknameInvalid                = errors.New("invalid nickname")
41 42
 	errNicknameInUse                  = errors.New("nickname in use")
42 43
 	errNicknameReserved               = errors.New("nickname is reserved")
44
+	errCantChangeNick                 = errors.New(`Always-on clients can't change nicknames`)
43 45
 	errNoExistingBan                  = errors.New("Ban does not exist")
44 46
 	errNoSuchChannel                  = errors.New(`No such channel`)
45 47
 	errChannelPurged                  = errors.New(`This channel was purged by the server operators and cannot be used`)

+ 1
- 1
irc/gateways.go 查看文件

@@ -61,7 +61,7 @@ func (wc *webircConfig) Populate() (err error) {
61 61
 func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls bool) (err error, quitMsg string) {
62 62
 	// PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself
63 63
 	// is whitelisted:
64
-	if client.isTor {
64
+	if session.isTor {
65 65
 		return errBadProxyLine, ""
66 66
 	}
67 67
 

+ 71
- 16
irc/getters.go 查看文件

@@ -91,21 +91,27 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat
91 91
 	return
92 92
 }
93 93
 
94
-func (client *Client) AddSession(session *Session) (success bool) {
94
+func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSignoff time.Time) {
95 95
 	client.stateMutex.Lock()
96 96
 	defer client.stateMutex.Unlock()
97 97
 
98 98
 	// client may be dying and ineligible to receive another session
99 99
 	if client.destroyed {
100
-		return false
100
+		return
101 101
 	}
102 102
 	// success, attach the new session to the client
103 103
 	session.client = client
104 104
 	newSessions := make([]*Session, len(client.sessions)+1)
105 105
 	copy(newSessions, client.sessions)
106 106
 	newSessions[len(newSessions)-1] = session
107
+	if len(client.sessions) == 0 && client.accountSettings.AutoreplayMissed {
108
+		// n.b. this is only possible if client is persistent and remained
109
+		// on the server with no sessions:
110
+		lastSignoff = client.lastSignoff
111
+		client.lastSignoff = time.Time{}
112
+	}
107 113
 	client.sessions = newSessions
108
-	return true
114
+	return true, len(client.sessions), lastSignoff
109 115
 }
110 116
 
111 117
 func (client *Client) removeSession(session *Session) (success bool, length int) {
@@ -189,6 +195,13 @@ func (client *Client) SetExitedSnomaskSent() {
189 195
 	client.stateMutex.Unlock()
190 196
 }
191 197
 
198
+func (client *Client) AlwaysOn() (alwaysOn bool) {
199
+	client.stateMutex.Lock()
200
+	alwaysOn = client.alwaysOn
201
+	client.stateMutex.Unlock()
202
+	return
203
+}
204
+
192 205
 // uniqueIdentifiers returns the strings for which the server enforces per-client
193 206
 // uniqueness/ownership; no two clients can have colliding casefolded nicks or
194 207
 // skeletons.
@@ -264,23 +277,41 @@ func (client *Client) AccountName() string {
264 277
 	return client.accountName
265 278
 }
266 279
 
267
-func (client *Client) SetAccountName(account string) (changed bool) {
268
-	var casefoldedAccount string
269
-	var err error
270
-	if account != "" {
271
-		if casefoldedAccount, err = CasefoldName(account); err != nil {
272
-			return
273
-		}
280
+func (client *Client) Login(account ClientAccount) {
281
+	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Bouncer.AlwaysOn, account.Settings.AlwaysOn)
282
+	client.stateMutex.Lock()
283
+	defer client.stateMutex.Unlock()
284
+	client.account = account.NameCasefolded
285
+	client.accountName = account.Name
286
+	client.accountSettings = account.Settings
287
+	// check `registered` to avoid incorrectly marking a temporary (pre-reattach),
288
+	// SASL'ing client as always-on
289
+	if client.registered {
290
+		client.alwaysOn = alwaysOn
274 291
 	}
292
+	client.accountRegDate = account.RegisteredAt
293
+	return
294
+}
275 295
 
296
+func (client *Client) historyCutoff() (cutoff time.Time) {
276 297
 	client.stateMutex.Lock()
277
-	defer client.stateMutex.Unlock()
278
-	changed = client.account != casefoldedAccount
279
-	client.account = casefoldedAccount
280
-	client.accountName = account
298
+	if client.account != "" {
299
+		cutoff = client.accountRegDate
300
+	} else {
301
+		cutoff = client.ctime
302
+	}
303
+	client.stateMutex.Unlock()
281 304
 	return
282 305
 }
283 306
 
307
+func (client *Client) Logout() {
308
+	client.stateMutex.Lock()
309
+	client.account = ""
310
+	client.accountName = ""
311
+	client.alwaysOn = false
312
+	client.stateMutex.Unlock()
313
+}
314
+
284 315
 func (client *Client) AccountSettings() (result AccountSettings) {
285 316
 	client.stateMutex.RLock()
286 317
 	result = client.accountSettings
@@ -289,8 +320,12 @@ func (client *Client) AccountSettings() (result AccountSettings) {
289 320
 }
290 321
 
291 322
 func (client *Client) SetAccountSettings(settings AccountSettings) {
323
+	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Bouncer.AlwaysOn, settings.AlwaysOn)
292 324
 	client.stateMutex.Lock()
293 325
 	client.accountSettings = settings
326
+	if client.registered {
327
+		client.alwaysOn = alwaysOn
328
+	}
294 329
 	client.stateMutex.Unlock()
295 330
 }
296 331
 
@@ -309,11 +344,17 @@ func (client *Client) SetLanguages(languages []string) {
309 344
 
310 345
 func (client *Client) HasMode(mode modes.Mode) bool {
311 346
 	// client.flags has its own synch
312
-	return client.flags.HasMode(mode)
347
+	return client.modes.HasMode(mode)
313 348
 }
314 349
 
315 350
 func (client *Client) SetMode(mode modes.Mode, on bool) bool {
316
-	return client.flags.SetMode(mode, on)
351
+	return client.modes.SetMode(mode, on)
352
+}
353
+
354
+func (client *Client) SetRealname(realname string) {
355
+	client.stateMutex.Lock()
356
+	client.realname = realname
357
+	client.stateMutex.Unlock()
317 358
 }
318 359
 
319 360
 func (client *Client) Channels() (result []*Channel) {
@@ -410,3 +451,17 @@ func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
410 451
 	channel.stateMutex.RUnlock()
411 452
 	return clientModes.HighestChannelUserMode()
412 453
 }
454
+
455
+func (channel *Channel) Settings() (result ChannelSettings) {
456
+	channel.stateMutex.RLock()
457
+	result = channel.settings
458
+	channel.stateMutex.RUnlock()
459
+	return result
460
+}
461
+
462
+func (channel *Channel) SetSettings(settings ChannelSettings) {
463
+	channel.stateMutex.Lock()
464
+	channel.settings = settings
465
+	channel.stateMutex.Unlock()
466
+	channel.MarkDirty(IncludeSettings)
467
+}

+ 142
- 207
irc/handlers.go 查看文件

@@ -531,64 +531,49 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
531 531
 // CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
532 532
 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
533 533
 func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
534
-	config := server.Config()
535
-
536 534
 	var items []history.Item
537
-	success := false
538
-	var hist *history.Buffer
535
+	unknown_command := false
536
+	var target string
539 537
 	var channel *Channel
538
+	var sequence history.Sequence
539
+	var err error
540 540
 	defer func() {
541 541
 		// successful responses are sent as a chathistory or history batch
542
-		if success && 0 < len(items) {
543
-			if channel == nil {
544
-				client.replayPrivmsgHistory(rb, items, true)
545
-			} else {
542
+		if err == nil && 0 < len(items) {
543
+			if channel != nil {
546 544
 				channel.replayHistoryItems(rb, items, false)
545
+			} else {
546
+				client.replayPrivmsgHistory(rb, items, target, true)
547 547
 			}
548 548
 			return
549 549
 		}
550 550
 
551 551
 		// errors are sent either without a batch, or in a draft/labeled-response batch as usual
552
-		// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
553
-		if hist == nil {
554
-			rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL")
555
-		} else if len(items) == 0 {
556
-			rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND")
557
-		} else if !success {
558
-			rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS")
552
+		if unknown_command {
553
+			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "UNKNOWN_COMMAND", utils.SafeErrorParam(msg.Params[0]), client.t("Unknown command"))
554
+		} else if err == utils.ErrInvalidParams {
555
+			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMETERS", msg.Params[0], client.t("Invalid parameters"))
556
+		} else if err != nil {
557
+			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("Messages could not be retrieved"))
558
+		} else if sequence == nil {
559
+			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "NO_SUCH_CHANNEL", utils.SafeErrorParam(msg.Params[1]), client.t("No such channel"))
559 560
 		}
560 561
 	}()
561 562
 
562
-	target := msg.Params[0]
563
-	channel = server.channels.Get(target)
564
-	if channel != nil && channel.hasClient(client) {
565
-		// "If [...] the user does not have permission to view the requested content, [...]
566
-		// NO_SUCH_CHANNEL SHOULD be returned"
567
-		hist = &channel.history
568
-	} else {
569
-		targetClient := server.clients.Get(target)
570
-		if targetClient != nil {
571
-			myAccount := client.Account()
572
-			targetAccount := targetClient.Account()
573
-			if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
574
-				hist = &targetClient.history
575
-			}
576
-		}
577
-	}
578
-	if hist == nil {
563
+	config := server.Config()
564
+	maxChathistoryLimit := config.History.ChathistoryMax
565
+	if maxChathistoryLimit == 0 {
579 566
 		return
580 567
 	}
581 568
 
582
-	preposition := strings.ToLower(msg.Params[1])
583
-
584 569
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
585
-		err = errInvalidParams
570
+		err = utils.ErrInvalidParams
586 571
 		pieces := strings.SplitN(param, "=", 2)
587 572
 		if len(pieces) < 2 {
588 573
 			return
589 574
 		}
590 575
 		identifier, value := strings.ToLower(pieces[0]), pieces[1]
591
-		if identifier == "id" {
576
+		if identifier == "msgid" {
592 577
 			msgid, err = value, nil
593 578
 			return
594 579
 		} else if identifier == "timestamp" {
@@ -598,10 +583,6 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
598 583
 		return
599 584
 	}
600 585
 
601
-	maxChathistoryLimit := config.History.ChathistoryMax
602
-	if maxChathistoryLimit == 0 {
603
-		return
604
-	}
605 586
 	parseHistoryLimit := func(paramIndex int) (limit int) {
606 587
 		if len(msg.Params) < (paramIndex + 1) {
607 588
 			return maxChathistoryLimit
@@ -613,140 +594,74 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
613 594
 		return
614 595
 	}
615 596
 
616
-	// TODO: as currently implemented, almost all of thes queries are worst-case O(n)
617
-	// in the number of stored history entries. Every one of them can be made O(1)
618
-	// if necessary, without too much difficulty. Some ideas:
619
-	// * Ensure that the ring buffer is sorted by time, enabling binary search for times
620
-	// * Maintain a map from msgid to position in the ring buffer
621
-
622
-	if preposition == "between" {
623
-		if len(msg.Params) >= 5 {
624
-			startMsgid, startTimestamp, startErr := parseQueryParam(msg.Params[2])
625
-			endMsgid, endTimestamp, endErr := parseQueryParam(msg.Params[3])
626
-			ascending := msg.Params[4] == "+"
627
-			limit := parseHistoryLimit(5)
628
-			if startErr != nil || endErr != nil {
629
-				success = false
630
-			} else if startMsgid != "" && endMsgid != "" {
631
-				inInterval := false
632
-				matches := func(item history.Item) (result bool) {
633
-					result = inInterval
634
-					if item.HasMsgid(startMsgid) {
635
-						if ascending {
636
-							inInterval = true
637
-						} else {
638
-							inInterval = false
639
-							return false // interval is exclusive
640
-						}
641
-					} else if item.HasMsgid(endMsgid) {
642
-						if ascending {
643
-							inInterval = false
644
-							return false
645
-						} else {
646
-							inInterval = true
647
-						}
648
-					}
649
-					return
650
-				}
651
-				items = hist.Match(matches, ascending, limit)
652
-				success = true
653
-			} else if !startTimestamp.IsZero() && !endTimestamp.IsZero() {
654
-				items, _ = hist.Between(startTimestamp, endTimestamp, ascending, limit)
655
-				if !ascending {
656
-					history.Reverse(items)
657
-				}
658
-				success = true
659
-			}
660
-			// else: mismatched params, success = false, fail
661
-		}
597
+	preposition := strings.ToLower(msg.Params[0])
598
+	target = msg.Params[1]
599
+	channel, sequence, err = server.GetHistorySequence(nil, client, target)
600
+	if err != nil || sequence == nil {
662 601
 		return
663 602
 	}
664 603
 
665
-	// before, after, latest, around
666
-	queryParam := msg.Params[2]
667
-	msgid, timestamp, err := parseQueryParam(queryParam)
668
-	limit := parseHistoryLimit(3)
669
-	before := false
604
+	roundUp := func(endpoint time.Time) (result time.Time) {
605
+		return endpoint.Truncate(time.Millisecond).Add(time.Millisecond)
606
+	}
607
+
608
+	var start, end history.Selector
609
+	var limit int
670 610
 	switch preposition {
671
-	case "before":
672
-		before = true
673
-		fallthrough
674
-	case "after":
675
-		var matches history.Predicate
611
+	case "between":
612
+		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
676 613
 		if err != nil {
677
-			break
678
-		} else if msgid != "" {
679
-			inInterval := false
680
-			matches = func(item history.Item) (result bool) {
681
-				result = inInterval
682
-				if item.HasMsgid(msgid) {
683
-					inInterval = true
684
-				}
685
-				return
686
-			}
687
-		} else {
688
-			matches = func(item history.Item) bool {
689
-				return before == item.Message.Time.Before(timestamp)
690
-			}
614
+			return
691 615
 		}
692
-		items = hist.Match(matches, !before, limit)
693
-		success = true
694
-	case "latest":
695
-		if queryParam == "*" {
696
-			items = hist.Latest(limit)
697
-		} else if err != nil {
698
-			break
699
-		} else {
700
-			var matches history.Predicate
701
-			if msgid != "" {
702
-				shouldStop := false
703
-				matches = func(item history.Item) bool {
704
-					if shouldStop {
705
-						return false
706
-					}
707
-					shouldStop = item.HasMsgid(msgid)
708
-					return !shouldStop
709
-				}
616
+		end.Msgid, end.Time, err = parseQueryParam(msg.Params[3])
617
+		if err != nil {
618
+			return
619
+		}
620
+		// XXX preserve the ordering of the two parameters, since we might be going backwards,
621
+		// but round up the chronologically first one, whichever it is, to make it exclusive
622
+		if !start.Time.IsZero() && !end.Time.IsZero() {
623
+			if start.Time.Before(end.Time) {
624
+				start.Time = roundUp(start.Time)
710 625
 			} else {
711
-				matches = func(item history.Item) bool {
712
-					return item.Message.Time.After(timestamp)
713
-				}
626
+				end.Time = roundUp(end.Time)
714 627
 			}
715
-			items = hist.Match(matches, false, limit)
716 628
 		}
717
-		success = true
718
-	case "around":
629
+		limit = parseHistoryLimit(4)
630
+	case "before", "after", "around":
631
+		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
719 632
 		if err != nil {
720
-			break
633
+			return
721 634
 		}
722
-		var initialMatcher history.Predicate
723
-		if msgid != "" {
724
-			inInterval := false
725
-			initialMatcher = func(item history.Item) (result bool) {
726
-				if inInterval {
727
-					return true
728
-				} else {
729
-					inInterval = item.HasMsgid(msgid)
730
-					return inInterval
731
-				}
635
+		if preposition == "after" && !start.Time.IsZero() {
636
+			start.Time = roundUp(start.Time)
637
+		}
638
+		if preposition == "before" {
639
+			end = start
640
+			start = history.Selector{}
641
+		}
642
+		limit = parseHistoryLimit(3)
643
+	case "latest":
644
+		if msg.Params[2] != "*" {
645
+			end.Msgid, end.Time, err = parseQueryParam(msg.Params[2])
646
+			if err != nil {
647
+				return
732 648
 			}
733
-		} else {
734
-			initialMatcher = func(item history.Item) (result bool) {
735
-				return item.Message.Time.Before(timestamp)
649
+			if !end.Time.IsZero() {
650
+				end.Time = roundUp(end.Time)
736 651
 			}
652
+			start.Time = time.Now().UTC()
737 653
 		}
738
-		var halfLimit int
739
-		halfLimit = (limit + 1) / 2
740
-		firstPass := hist.Match(initialMatcher, false, halfLimit)
741
-		if len(firstPass) > 0 {
742
-			timeWindowStart := firstPass[0].Message.Time
743
-			items = hist.Match(func(item history.Item) bool {
744
-				return item.Message.Time.Equal(timeWindowStart) || item.Message.Time.After(timeWindowStart)
745
-			}, true, limit)
746
-		}
747
-		success = true
654
+		limit = parseHistoryLimit(3)
655
+	default:
656
+		unknown_command = true
657
+		return
748 658
 	}
749 659
 
660
+	if preposition == "around" {
661
+		items, err = sequence.Around(start, limit)
662
+	} else {
663
+		items, _, err = sequence.Between(start, end, limit)
664
+	}
750 665
 	return
751 666
 }
752 667
 
@@ -1006,6 +921,7 @@ Get an explanation of <argument>, or "index" for a list of help topics.`), rb)
1006 921
 // HISTORY <target> [<limit>]
1007 922
 // e.g., HISTORY #ubuntu 10
1008 923
 // HISTORY me 15
924
+// HISTORY #darwin 1h
1009 925
 func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
1010 926
 	config := server.Config()
1011 927
 	if !config.History.Enabled {
@@ -1014,53 +930,55 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
1014 930
 	}
1015 931
 
1016 932
 	target := msg.Params[0]
1017
-	var hist *history.Buffer
1018
-	channel := server.channels.Get(target)
1019
-	if channel != nil && channel.hasClient(client) {
1020
-		hist = &channel.history
1021
-	} else {
1022
-		if strings.ToLower(target) == "me" {
1023
-			hist = &client.history
1024
-		} else {
1025
-			targetClient := server.clients.Get(target)
1026
-			if targetClient != nil {
1027
-				myAccount, targetAccount := client.Account(), targetClient.Account()
1028
-				if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
1029
-					hist = &targetClient.history
1030
-				}
1031
-			}
1032
-		}
933
+	if strings.ToLower(target) == "me" {
934
+		target = "*"
1033 935
 	}
936
+	channel, sequence, err := server.GetHistorySequence(nil, client, target)
1034 937
 
1035
-	if hist == nil {
1036
-		if channel == nil {
1037
-			rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
1038
-		} else {
1039
-			rb.Add(nil, server.name, ERR_NOTONCHANNEL, client.Nick(), target, client.t("You're not on that channel"))
1040
-		}
938
+	if sequence == nil || err != nil {
939
+		// whatever
940
+		rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
1041 941
 		return false
1042 942
 	}
1043 943
 
1044
-	limit := 10
944
+	var duration time.Duration
1045 945
 	maxChathistoryLimit := config.History.ChathistoryMax
946
+	limit := 100
947
+	if maxChathistoryLimit < limit {
948
+		limit = maxChathistoryLimit
949
+	}
1046 950
 	if len(msg.Params) > 1 {
1047 951
 		providedLimit, err := strconv.Atoi(msg.Params[1])
1048
-		if providedLimit > maxChathistoryLimit {
1049
-			providedLimit = maxChathistoryLimit
1050
-		}
1051 952
 		if err == nil && providedLimit != 0 {
1052 953
 			limit = providedLimit
954
+			if maxChathistoryLimit < limit {
955
+				limit = maxChathistoryLimit
956
+			}
957
+		} else if err != nil {
958
+			duration, err = time.ParseDuration(msg.Params[1])
959
+			if err == nil {
960
+				limit = maxChathistoryLimit
961
+			}
1053 962
 		}
1054 963
 	}
1055 964
 
1056
-	items := hist.Latest(limit)
1057
-
1058
-	if channel != nil {
1059
-		channel.replayHistoryItems(rb, items, false)
965
+	var items []history.Item
966
+	if duration == 0 {
967
+		items, _, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
1060 968
 	} else {
1061
-		client.replayPrivmsgHistory(rb, items, true)
969
+		now := time.Now().UTC()
970
+		start := history.Selector{Time: now}
971
+		end := history.Selector{Time: now.Add(-duration)}
972
+		items, _, err = sequence.Between(start, end, limit)
1062 973
 	}
1063 974
 
975
+	if err == nil {
976
+		if channel != nil {
977
+			channel.replayHistoryItems(rb, items, false)
978
+		} else {
979
+			client.replayPrivmsgHistory(rb, items, "", true)
980
+		}
981
+	}
1064 982
 	return false
1065 983
 }
1066 984
 
@@ -1944,7 +1862,7 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
1944 1862
 		return false
1945 1863
 	}
1946 1864
 
1947
-	if client.isTor && utils.IsRestrictedCTCPMessage(message) {
1865
+	if rb.session.isTor && utils.IsRestrictedCTCPMessage(message) {
1948 1866
 		// note that error replies are never sent for NOTICE
1949 1867
 		if histType != history.Notice {
1950 1868
 			rb.Notice(client.t("CTCP messages are disabled over Tor"))
@@ -2001,21 +1919,22 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
2001 1919
 			}
2002 1920
 			return
2003 1921
 		}
2004
-		tnick := user.Nick()
1922
+		tDetails := user.Details()
1923
+		tnick := tDetails.nick
2005 1924
 
2006
-		nickMaskString := client.NickMaskString()
2007
-		accountName := client.AccountName()
1925
+		details := client.Details()
1926
+		nickMaskString := details.nickMask
1927
+		accountName := details.accountName
2008 1928
 		// restrict messages appropriately when +R is set
2009 1929
 		// intentionally make the sending user think the message went through fine
2010
-		allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount()
2011
-		allowedTor := !user.isTor || !message.IsRestrictedCTCPMessage()
2012
-		if allowedPlusR && allowedTor {
1930
+		allowedPlusR := !user.HasMode(modes.RegisteredOnly) || details.account != ""
1931
+		if allowedPlusR {
2013 1932
 			for _, session := range user.Sessions() {
2014 1933
 				hasTagsCap := session.capabilities.Has(caps.MessageTags)
2015 1934
 				// don't send TAGMSG at all if they don't have the tags cap
2016 1935
 				if histType == history.Tagmsg && hasTagsCap {
2017 1936
 					session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
2018
-				} else if histType != history.Tagmsg {
1937
+				} else if histType != history.Tagmsg && !(session.isTor && message.IsRestrictedCTCPMessage()) {
2019 1938
 					tagsToSend := tags
2020 1939
 					if !hasTagsCap {
2021 1940
 						tagsToSend = nil
@@ -2053,17 +1972,37 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
2053 1972
 			rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, user.AwayMessage())
2054 1973
 		}
2055 1974
 
1975
+		config := server.Config()
1976
+		if !config.History.Enabled {
1977
+			return
1978
+		}
2056 1979
 		item := history.Item{
2057 1980
 			Type:        histType,
2058 1981
 			Message:     message,
2059 1982
 			Nick:        nickMaskString,
2060 1983
 			AccountName: accountName,
1984
+			Tags:        tags,
1985
+		}
1986
+		if !item.IsStorable() {
1987
+			return
1988
+		}
1989
+		targetedItem := item
1990
+		targetedItem.Params[0] = tnick
1991
+		cPersistent, cEphemeral := client.historyStatus(config)
1992
+		tPersistent, tEphemeral := user.historyStatus(config)
1993
+		// add to ephemeral history
1994
+		if cEphemeral {
1995
+			targetedItem.CfCorrespondent = tDetails.nickCasefolded
1996
+			client.history.Add(targetedItem)
1997
+		}
1998
+		if tEphemeral {
1999
+			item.CfCorrespondent = details.nickCasefolded
2000
+			user.history.Add(item)
2001
+		}
2002
+		if cPersistent || tPersistent {
2003
+			item.CfCorrespondent = ""
2004
+			server.historyDB.AddDirectMessage(details.nickCasefolded, user.NickCasefolded(), cPersistent, tPersistent, targetedItem)
2061 2005
 		}
2062
-		// add to the target's history:
2063
-		user.history.Add(item)
2064
-		// add this to the client's history as well, recording the target:
2065
-		item.Params[0] = tnick
2066
-		client.history.Add(item)
2067 2006
 	}
2068 2007
 }
2069 2008
 
@@ -2375,11 +2314,7 @@ func sceneHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
2375 2314
 // SETNAME <realname>
2376 2315
 func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2377 2316
 	realname := msg.Params[0]
2378
-
2379
-	client.stateMutex.Lock()
2380
-	client.realname = realname
2381
-	client.stateMutex.Unlock()
2382
-
2317
+	client.SetRealname(realname)
2383 2318
 	details := client.Details()
2384 2319
 
2385 2320
 	// alert friends

+ 3
- 2
irc/help.go 查看文件

@@ -206,8 +206,9 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
206 206
 
207 207
 Replay message history. <target> can be a channel name, "me" to replay direct
208 208
 message history, or a nickname to replay another client's direct message
209
-history (they must be logged into the same account as you). At most [limit]
210
-messages will be replayed.`,
209
+history (they must be logged into the same account as you). [limit] can be
210
+either an integer (the maximum number of messages to replay), or a time
211
+duration like 10m or 1h (the time window within which to replay messages).`,
211 212
 	},
212 213
 	"info": {
213 214
 		text: `INFO

+ 97
- 71
irc/history/history.go 查看文件

@@ -6,7 +6,6 @@ package history
6 6
 import (
7 7
 	"github.com/oragono/oragono/irc/utils"
8 8
 	"sync"
9
-	"sync/atomic"
10 9
 	"time"
11 10
 )
12 11
 
@@ -43,9 +42,10 @@ type Item struct {
43 42
 	// this is the uncasefolded account name, if there's no account it should be set to "*"
44 43
 	AccountName string
45 44
 	// for non-privmsg items, we may stuff some other data in here
46
-	Message utils.SplitMessage
47
-	Tags    map[string]string
48
-	Params  [1]string
45
+	Message         utils.SplitMessage
46
+	Tags            map[string]string
47
+	Params          [1]string
48
+	CfCorrespondent string
49 49
 }
50 50
 
51 51
 // HasMsgid tests whether a message has the message id `msgid`.
@@ -53,20 +53,30 @@ func (item *Item) HasMsgid(msgid string) bool {
53 53
 	return item.Message.Msgid == msgid
54 54
 }
55 55
 
56
-func (item *Item) isStorable() bool {
57
-	if item.Type == Tagmsg {
56
+func (item *Item) IsStorable() bool {
57
+	switch item.Type {
58
+	case Tagmsg:
58 59
 		for name := range item.Tags {
59 60
 			if !transientTags[name] {
60 61
 				return true
61 62
 			}
62 63
 		}
63 64
 		return false // all tags were blacklisted
64
-	} else {
65
+	case Privmsg, Notice:
66
+		// don't store CTCP other than ACTION
67
+		return !item.Message.IsRestrictedCTCPMessage()
68
+	default:
65 69
 		return true
66 70
 	}
67 71
 }
68 72
 
69
-type Predicate func(item Item) (matches bool)
73
+type Predicate func(item *Item) (matches bool)
74
+
75
+func Reverse(results []Item) {
76
+	for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
77
+		results[i], results[j] = results[j], results[i]
78
+	}
79
+}
70 80
 
71 81
 // Buffer is a ring buffer holding message/event history for a channel or user
72 82
 type Buffer struct {
@@ -81,8 +91,6 @@ type Buffer struct {
81 91
 
82 92
 	lastDiscarded time.Time
83 93
 
84
-	enabled uint32
85
-
86 94
 	nowFunc func() time.Time
87 95
 }
88 96
 
@@ -99,8 +107,6 @@ func (hist *Buffer) Initialize(size int, window time.Duration) {
99 107
 	hist.window = window
100 108
 	hist.maximumSize = size
101 109
 	hist.nowFunc = time.Now
102
-
103
-	hist.setEnabled(size)
104 110
 }
105 111
 
106 112
 // compute the initial size for the buffer, taking into account autoresize
@@ -115,31 +121,8 @@ func (hist *Buffer) initialSize(size int, window time.Duration) (result int) {
115 121
 	return
116 122
 }
117 123
 
118
-func (hist *Buffer) setEnabled(size int) {
119
-	var enabled uint32
120
-	if size != 0 {
121
-		enabled = 1
122
-	}
123
-	atomic.StoreUint32(&hist.enabled, enabled)
124
-}
125
-
126
-// Enabled returns whether the buffer is currently storing messages
127
-// (a disabled buffer blackholes everything it sees)
128
-func (list *Buffer) Enabled() bool {
129
-	return atomic.LoadUint32(&list.enabled) != 0
130
-}
131
-
132 124
 // Add adds a history item to the buffer
133 125
 func (list *Buffer) Add(item Item) {
134
-	// fast path without a lock acquisition for when we are not storing history
135
-	if !list.Enabled() {
136
-		return
137
-	}
138
-
139
-	if !item.isStorable() {
140
-		return
141
-	}
142
-
143 126
 	if item.Message.Time.IsZero() {
144 127
 		item.Message.Time = time.Now().UTC()
145 128
 	}
@@ -147,6 +130,10 @@ func (list *Buffer) Add(item Item) {
147 130
 	list.Lock()
148 131
 	defer list.Unlock()
149 132
 
133
+	if len(list.buffer) == 0 {
134
+		return
135
+	}
136
+
150 137
 	list.maybeExpand()
151 138
 
152 139
 	var pos int
@@ -170,55 +157,100 @@ func (list *Buffer) Add(item Item) {
170 157
 	list.buffer[pos] = item
171 158
 }
172 159
 
173
-// Reverse reverses an []Item, in-place.
174
-func Reverse(results []Item) {
175
-	for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
176
-		results[i], results[j] = results[j], results[i]
160
+func (list *Buffer) lookup(msgid string) (result Item, found bool) {
161
+	predicate := func(item *Item) bool {
162
+		return item.HasMsgid(msgid)
163
+	}
164
+	results := list.matchInternal(predicate, false, 1)
165
+	if len(results) != 0 {
166
+		return results[0], true
177 167
 	}
168
+	return
178 169
 }
179 170
 
180 171
 // Between returns all history items with a time `after` <= time <= `before`,
181 172
 // with an indication of whether the results are complete or are missing items
182 173
 // because some of that period was discarded. A zero value of `before` is considered
183 174
 // higher than all other times.
184
-func (list *Buffer) Between(after, before time.Time, ascending bool, limit int) (results []Item, complete bool) {
185
-	if !list.Enabled() {
186
-		return
187
-	}
175
+func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Predicate, limit int) (results []Item, complete bool, err error) {
176
+	var ascending bool
177
+
178
+	defer func() {
179
+		if !ascending {
180
+			Reverse(results)
181
+		}
182
+	}()
188 183
 
189 184
 	list.RLock()
190 185
 	defer list.RUnlock()
191 186
 
187
+	if len(list.buffer) == 0 {
188
+		return
189
+	}
190
+
191
+	after := start.Time
192
+	if start.Msgid != "" {
193
+		item, found := list.lookup(start.Msgid)
194
+		if !found {
195
+			return
196
+		}
197
+		after = item.Message.Time
198
+	}
199
+	before := end.Time
200
+	if end.Msgid != "" {
201
+		item, found := list.lookup(end.Msgid)
202
+		if !found {
203
+			return
204
+		}
205
+		before = item.Message.Time
206
+	}
207
+
208
+	after, before, ascending = MinMaxAsc(after, before, cutoff)
209
+
192 210
 	complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
193 211
 
194
-	satisfies := func(item Item) bool {
195
-		return (after.IsZero() || item.Message.Time.After(after)) && (before.IsZero() || item.Message.Time.Before(before))
212
+	satisfies := func(item *Item) bool {
213
+		return (after.IsZero() || item.Message.Time.After(after)) &&
214
+			(before.IsZero() || item.Message.Time.Before(before)) &&
215
+			(pred == nil || pred(item))
196 216
 	}
197 217
 
198
-	return list.matchInternal(satisfies, ascending, limit), complete
218
+	return list.matchInternal(satisfies, ascending, limit), complete, nil
199 219
 }
200 220
 
201
-// Match returns all history items such that `predicate` returns true for them.
202
-// Items are considered in reverse insertion order if `ascending` is false, or
203
-// in insertion order if `ascending` is true, up to a total of `limit` matches
204
-// if `limit` > 0 (unlimited otherwise).
205
-// `predicate` MAY be a closure that maintains its own state across invocations;
206
-// it MUST NOT acquire any locks or otherwise do anything weird.
207
-// Results are always returned in insertion order.
208
-func (list *Buffer) Match(predicate Predicate, ascending bool, limit int) (results []Item) {
209
-	if !list.Enabled() {
210
-		return
221
+// implements history.Sequence, emulating a single history buffer (for a channel,
222
+// a single user's DMs, or a DM conversation)
223
+type bufferSequence struct {
224
+	list   *Buffer
225
+	pred   Predicate
226
+	cutoff time.Time
227
+}
228
+
229
+func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequence {
230
+	var pred Predicate
231
+	if correspondent != "" {
232
+		pred = func(item *Item) bool {
233
+			return item.CfCorrespondent == correspondent
234
+		}
211 235
 	}
236
+	return &bufferSequence{
237
+		list:   list,
238
+		pred:   pred,
239
+		cutoff: cutoff,
240
+	}
241
+}
212 242
 
213
-	list.RLock()
214
-	defer list.RUnlock()
243
+func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, complete bool, err error) {
244
+	return seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
245
+}
215 246
 
216
-	return list.matchInternal(predicate, ascending, limit)
247
+func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
248
+	return GenericAround(seq, start, limit)
217 249
 }
218 250
 
219 251
 // you must be holding the read lock to call this
220 252
 func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
221
-	if list.start == -1 {
253
+	if list.start == -1 || len(list.buffer) == 0 {
222 254
 		return
223 255
 	}
224 256
 
@@ -232,7 +264,7 @@ func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int
232 264
 	}
233 265
 
234 266
 	for {
235
-		if predicate(list.buffer[pos]) {
267
+		if predicate(&list.buffer[pos]) {
236 268
 			results = append(results, list.buffer[pos])
237 269
 		}
238 270
 		if pos == stop || (limit != 0 && len(results) == limit) {
@@ -245,18 +277,14 @@ func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int
245 277
 		}
246 278
 	}
247 279
 
248
-	// TODO sort by time instead?
249
-	if !ascending {
250
-		Reverse(results)
251
-	}
252 280
 	return
253 281
 }
254 282
 
255
-// Latest returns the items most recently added, up to `limit`. If `limit` is 0,
283
+// latest returns the items most recently added, up to `limit`. If `limit` is 0,
256 284
 // it returns all items.
257
-func (list *Buffer) Latest(limit int) (results []Item) {
258
-	matchAll := func(item Item) bool { return true }
259
-	return list.Match(matchAll, false, limit)
285
+func (list *Buffer) latest(limit int) (results []Item) {
286
+	results, _, _ = list.betweenHelper(Selector{}, Selector{}, time.Time{}, nil, limit)
287
+	return
260 288
 }
261 289
 
262 290
 // LastDiscarded returns the latest time of any entry that was evicted
@@ -355,8 +383,6 @@ func (list *Buffer) Resize(maximumSize int, window time.Duration) {
355 383
 func (list *Buffer) resize(size int) {
356 384
 	newbuffer := make([]Item, size)
357 385
 
358
-	list.setEnabled(size)
359
-
360 386
 	if list.start == -1 {
361 387
 		// indices are already correct and nothing needs to be copied
362 388
 	} else if size == 0 {

+ 46
- 25
irc/history/history_test.go 查看文件

@@ -14,19 +14,21 @@ const (
14 14
 	timeFormat = "2006-01-02 15:04:05Z"
15 15
 )
16 16
 
17
+func betweenTimestamps(buf *Buffer, start, end time.Time, limit int) (result []Item, complete bool) {
18
+	result, complete, _ = buf.betweenHelper(Selector{Time: start}, Selector{Time: end}, time.Time{}, nil, limit)
19
+	return
20
+}
21
+
17 22
 func TestEmptyBuffer(t *testing.T) {
18 23
 	pastTime := easyParse(timeFormat)
19 24
 
20 25
 	buf := NewHistoryBuffer(0, 0)
21
-	if buf.Enabled() {
22
-		t.Error("the buffer of size 0 must be considered disabled")
23
-	}
24 26
 
25 27
 	buf.Add(Item{
26 28
 		Nick: "testnick",
27 29
 	})
28 30
 
29
-	since, complete := buf.Between(pastTime, time.Now(), false, 0)
31
+	since, complete := betweenTimestamps(buf, pastTime, time.Now(), 0)
30 32
 	if len(since) != 0 {
31 33
 		t.Error("shouldn't be able to add to disabled buf")
32 34
 	}
@@ -35,16 +37,13 @@ func TestEmptyBuffer(t *testing.T) {
35 37
 	}
36 38
 
37 39
 	buf.Resize(1, 0)
38
-	if !buf.Enabled() {
39
-		t.Error("the buffer of size 1 must be considered enabled")
40
-	}
41
-	since, complete = buf.Between(pastTime, time.Now(), false, 0)
40
+	since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
42 41
 	assertEqual(complete, true, t)
43 42
 	assertEqual(len(since), 0, t)
44 43
 	buf.Add(Item{
45 44
 		Nick: "testnick",
46 45
 	})
47
-	since, complete = buf.Between(pastTime, time.Now(), false, 0)
46
+	since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
48 47
 	if len(since) != 1 {
49 48
 		t.Error("should be able to store items in a nonempty buffer")
50 49
 	}
@@ -58,7 +57,7 @@ func TestEmptyBuffer(t *testing.T) {
58 57
 	buf.Add(Item{
59 58
 		Nick: "testnick2",
60 59
 	})
61
-	since, complete = buf.Between(pastTime, time.Now(), false, 0)
60
+	since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
62 61
 	if len(since) != 1 {
63 62
 		t.Error("expect exactly 1 item")
64 63
 	}
@@ -68,8 +67,7 @@ func TestEmptyBuffer(t *testing.T) {
68 67
 	if since[0].Nick != "testnick2" {
69 68
 		t.Error("retrieved junk data")
70 69
 	}
71
-	matchAll := func(item Item) bool { return true }
72
-	assertEqual(toNicks(buf.Match(matchAll, false, 0)), []string{"testnick2"}, t)
70
+	assertEqual(toNicks(buf.latest(0)), []string{"testnick2"}, t)
73 71
 }
74 72
 
75 73
 func toNicks(items []Item) (result []string) {
@@ -110,27 +108,27 @@ func TestBuffer(t *testing.T) {
110 108
 
111 109
 	buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z"))
112 110
 
113
-	since, complete := buf.Between(start, time.Now(), false, 0)
111
+	since, complete := betweenTimestamps(buf, start, time.Now(), 0)
114 112
 	assertEqual(complete, true, t)
115 113
 	assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
116 114
 
117 115
 	// add another item, evicting the first
118 116
 	buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z"))
119 117
 
120
-	since, complete = buf.Between(start, time.Now(), false, 0)
118
+	since, complete = betweenTimestamps(buf, start, time.Now(), 0)
121 119
 	assertEqual(complete, false, t)
122 120
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
123 121
 	// now exclude the time of the discarded entry; results should be complete again
124
-	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
122
+	since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), time.Now(), 0)
125 123
 	assertEqual(complete, true, t)
126 124
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
127
-	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"), false, 0)
125
+	since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"), 0)
128 126
 	assertEqual(complete, true, t)
129 127
 	assertEqual(toNicks(since), []string{"testnick1"}, t)
130 128
 
131 129
 	// shrink the buffer, cutting off testnick1
132 130
 	buf.Resize(2, 0)
133
-	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
131
+	since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), time.Now(), 0)
134 132
 	assertEqual(complete, false, t)
135 133
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
136 134
 
@@ -138,18 +136,19 @@ func TestBuffer(t *testing.T) {
138 136
 	buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
139 137
 	buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
140 138
 	buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
141
-	since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), false, 0)
139
+	since, complete = betweenTimestamps(buf, easyParse("2006-01-03 00:00:00Z"), time.Now(), 0)
142 140
 	assertEqual(complete, true, t)
143 141
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
144 142
 
145 143
 	// test ascending order
146
-	since, _ = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), true, 2)
144
+	since, _ = betweenTimestamps(buf, easyParse("2006-01-03 00:00:00Z"), time.Time{}, 2)
147 145
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
148 146
 }
149 147
 
150 148
 func autoItem(id int, t time.Time) (result Item) {
151 149
 	result.Message.Time = t
152 150
 	result.Nick = strconv.Itoa(id)
151
+	result.Message.Msgid = result.Nick
153 152
 	return
154 153
 }
155 154
 
@@ -181,7 +180,7 @@ func TestAutoresize(t *testing.T) {
181 180
 		now = now.Add(time.Minute * 10)
182 181
 		id += 1
183 182
 	}
184
-	items := buf.Latest(0)
183
+	items := buf.latest(0)
185 184
 	assertEqual(len(items), initialAutoSize, t)
186 185
 	assertEqual(atoi(items[0].Nick), 40, t)
187 186
 	assertEqual(atoi(items[len(items)-1].Nick), 71, t)
@@ -195,7 +194,7 @@ func TestAutoresize(t *testing.T) {
195 194
 	// ok, 5 items from the first batch are still in the 1-hour window;
196 195
 	// we should overwrite until only those 5 are left, then start expanding
197 196
 	// the buffer so that it retains those 5 and the 100 new items
198
-	items = buf.Latest(0)
197
+	items = buf.latest(0)
199 198
 	assertEqual(len(items), 105, t)
200 199
 	assertEqual(atoi(items[0].Nick), 67, t)
201 200
 	assertEqual(atoi(items[len(items)-1].Nick), 171, t)
@@ -207,7 +206,7 @@ func TestAutoresize(t *testing.T) {
207 206
 		id += 1
208 207
 	}
209 208
 	// should fill up to the maximum size of 128 and start overwriting
210
-	items = buf.Latest(0)
209
+	items = buf.latest(0)
211 210
 	assertEqual(len(items), 128, t)
212 211
 	assertEqual(atoi(items[0].Nick), 144, t)
213 212
 	assertEqual(atoi(items[len(items)-1].Nick), 271, t)
@@ -222,7 +221,7 @@ func TestEnabledByResize(t *testing.T) {
222 221
 	buf.Resize(128, time.Hour)
223 222
 	// add an item and test that it is stored and retrievable
224 223
 	buf.Add(autoItem(0, now))
225
-	items := buf.Latest(0)
224
+	items := buf.latest(0)
226 225
 	assertEqual(len(items), 1, t)
227 226
 	assertEqual(atoi(items[0].Nick), 0, t)
228 227
 }
@@ -232,13 +231,13 @@ func TestDisabledByResize(t *testing.T) {
232 231
 	// enabled autoresizing buffer
233 232
 	buf := NewHistoryBuffer(128, time.Hour)
234 233
 	buf.Add(autoItem(0, now))
235
-	items := buf.Latest(0)
234
+	items := buf.latest(0)
236 235
 	assertEqual(len(items), 1, t)
237 236
 	assertEqual(atoi(items[0].Nick), 0, t)
238 237
 
239 238
 	// disable as during a rehash, confirm that nothing can be retrieved
240 239
 	buf.Resize(0, time.Hour)
241
-	items = buf.Latest(0)
240
+	items = buf.latest(0)
242 241
 	assertEqual(len(items), 0, t)
243 242
 }
244 243
 
@@ -252,3 +251,25 @@ func TestRoundUp(t *testing.T) {
252 251
 	assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
253 252
 	assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
254 253
 }
254
+
255
+func BenchmarkInsert(b *testing.B) {
256
+	buf := NewHistoryBuffer(1024, 0)
257
+	b.ResetTimer()
258
+	for i := 0; i < b.N; i++ {
259
+		buf.Add(Item{})
260
+	}
261
+}
262
+
263
+func BenchmarkMatch(b *testing.B) {
264
+	buf := NewHistoryBuffer(1024, 0)
265
+	var now time.Time
266
+	for i := 0; i < 1024; i += 1 {
267
+		buf.Add(autoItem(i, now))
268
+		now = now.Add(time.Second)
269
+	}
270
+
271
+	b.ResetTimer()
272
+	for i := 0; i < b.N; i++ {
273
+		buf.lookup("512")
274
+	}
275
+}

+ 71
- 0
irc/history/queries.go 查看文件

@@ -0,0 +1,71 @@
1
+// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package history
5
+
6
+import (
7
+	"time"
8
+)
9
+
10
+// Selector represents a parameter to a CHATHISTORY command;
11
+// at most one of Msgid or Time may be nonzero
12
+type Selector struct {
13
+	Msgid string
14
+	Time  time.Time
15
+}
16
+
17
+// Sequence is an abstract sequence of history entries that can be queried;
18
+// it encapsulates restrictions such as registration time cutoffs, or
19
+// only looking at a single "query buffer" (DMs with a particular correspondent)
20
+type Sequence interface {
21
+	Between(start, end Selector, limit int) (results []Item, complete bool, err error)
22
+	Around(start Selector, limit int) (results []Item, err error)
23
+}
24
+
25
+// This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
26
+func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
27
+	var halfLimit int
28
+	halfLimit = (limit + 1) / 2
29
+	initialResults, _, err := seq.Between(Selector{}, start, halfLimit)
30
+	if err != nil {
31
+		return
32
+	} else if len(initialResults) == 0 {
33
+		// TODO: this fails if we're doing an AROUND on the first message in the buffer
34
+		// would be nice to fix this but whatever
35
+		return
36
+	}
37
+	newStart := Selector{Time: initialResults[0].Message.Time}
38
+	results, _, err = seq.Between(newStart, Selector{}, limit)
39
+	return
40
+}
41
+
42
+// MinMaxAsc converts CHATHISTORY arguments into time intervals, handling the most
43
+// general case (BETWEEN going forwards or backwards) natively and the other ordering
44
+// queries (AFTER, BEFORE, LATEST) as special cases.
45
+func MinMaxAsc(after, before, cutoff time.Time) (min, max time.Time, ascending bool) {
46
+	startIsZero, endIsZero := after.IsZero(), before.IsZero()
47
+	if !startIsZero && endIsZero {
48
+		// AFTER
49
+		ascending = true
50
+	} else if startIsZero && !endIsZero {
51
+		// BEFORE
52
+		ascending = false
53
+	} else if !startIsZero && !endIsZero {
54
+		if before.Before(after) {
55
+			// BETWEEN going backwards
56
+			before, after = after, before
57
+			ascending = false
58
+		} else {
59
+			// BETWEEN going forwards
60
+			ascending = true
61
+		}
62
+	} else if startIsZero && endIsZero {
63
+		// LATEST
64
+		ascending = false
65
+	}
66
+	if after.IsZero() || after.Before(cutoff) {
67
+		// this may result in an impossible query, which is fine
68
+		after = cutoff
69
+	}
70
+	return after, before, ascending
71
+}

+ 21
- 23
irc/idletimer.go 查看文件

@@ -52,6 +52,7 @@ type IdleTimer struct {
52 52
 	quitTimeout time.Duration
53 53
 	state       TimerState
54 54
 	timer       *time.Timer
55
+	lastTouch   time.Time
55 56
 }
56 57
 
57 58
 // Initialize sets up an IdleTimer and starts counting idle time;
@@ -61,9 +62,11 @@ func (it *IdleTimer) Initialize(session *Session) {
61 62
 	it.registerTimeout = RegisterTimeout
62 63
 	it.idleTimeout, it.quitTimeout = it.recomputeDurations()
63 64
 	registered := session.client.Registered()
65
+	now := time.Now().UTC()
64 66
 
65 67
 	it.Lock()
66 68
 	defer it.Unlock()
69
+	it.lastTouch = now
67 70
 	if registered {
68 71
 		it.state = TimerActive
69 72
 	} else {
@@ -82,7 +85,7 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
82 85
 	}
83 86
 
84 87
 	idleTimeout = DefaultIdleTimeout
85
-	if it.session.client.isTor {
88
+	if it.session.isTor {
86 89
 		idleTimeout = TorIdleTimeout
87 90
 	}
88 91
 
@@ -92,10 +95,12 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
92 95
 
93 96
 func (it *IdleTimer) Touch() {
94 97
 	idleTimeout, quitTimeout := it.recomputeDurations()
98
+	now := time.Now().UTC()
95 99
 
96 100
 	it.Lock()
97 101
 	defer it.Unlock()
98 102
 	it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
103
+	it.lastTouch = now
99 104
 	// a touch transitions TimerUnregistered or TimerIdle into TimerActive
100 105
 	if it.state != TimerDead {
101 106
 		it.state = TimerActive
@@ -103,6 +108,13 @@ func (it *IdleTimer) Touch() {
103 108
 	}
104 109
 }
105 110
 
111
+func (it *IdleTimer) LastTouch() (result time.Time) {
112
+	it.Lock()
113
+	result = it.lastTouch
114
+	it.Unlock()
115
+	return
116
+}
117
+
106 118
 func (it *IdleTimer) processTimeout() {
107 119
 	idleTimeout, quitTimeout := it.recomputeDurations()
108 120
 
@@ -322,9 +334,6 @@ const (
322 334
 	// BrbDead is the state of a client after its timeout has expired; it will be removed
323 335
 	// and therefore new sessions cannot be attached to it
324 336
 	BrbDead
325
-	// BrbSticky allows a client to remain online without sessions, with no timeout.
326
-	// This is not used yet.
327
-	BrbSticky
328 337
 )
329 338
 
330 339
 type BrbTimer struct {
@@ -345,16 +354,16 @@ func (bt *BrbTimer) Initialize(client *Client) {
345 354
 
346 355
 // attempts to enable BRB for a client, returns whether it succeeded
347 356
 func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
348
-	if !bt.client.Registered() || bt.client.ResumeID() == "" {
349
-		return
350
-	}
351
-
352 357
 	// TODO make this configurable
353 358
 	duration = ResumeableTotalTimeout
354 359
 
355 360
 	bt.client.stateMutex.Lock()
356 361
 	defer bt.client.stateMutex.Unlock()
357 362
 
363
+	if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
364
+		return
365
+	}
366
+
358 367
 	switch bt.state {
359 368
 	case BrbDisabled, BrbEnabled:
360 369
 		bt.state = BrbEnabled
@@ -366,8 +375,6 @@ func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
366 375
 			bt.brbAt = time.Now().UTC()
367 376
 		}
368 377
 		success = true
369
-	case BrbSticky:
370
-		success = true
371 378
 	default:
372 379
 		// BrbDead
373 380
 		success = false
@@ -416,6 +423,10 @@ func (bt *BrbTimer) processTimeout() {
416 423
 	bt.client.stateMutex.Lock()
417 424
 	defer bt.client.stateMutex.Unlock()
418 425
 
426
+	if bt.client.alwaysOn {
427
+		return
428
+	}
429
+
419 430
 	switch bt.state {
420 431
 	case BrbDisabled, BrbEnabled:
421 432
 		if len(bt.client.sessions) == 0 {
@@ -432,16 +443,3 @@ func (bt *BrbTimer) processTimeout() {
432 443
 	}
433 444
 	bt.resetTimeout()
434 445
 }
435
-
436
-// sets a client to be "sticky", i.e., indefinitely exempt from removal for
437
-// lack of sessions
438
-func (bt *BrbTimer) SetSticky() (success bool) {
439
-	bt.client.stateMutex.Lock()
440
-	defer bt.client.stateMutex.Unlock()
441
-	if bt.state != BrbDead {
442
-		success = true
443
-		bt.state = BrbSticky
444
-	}
445
-	bt.resetTimeout()
446
-	return
447
-}

+ 535
- 0
irc/mysql/history.go 查看文件

@@ -0,0 +1,535 @@
1
+package mysql
2
+
3
+import (
4
+	"bytes"
5
+	"database/sql"
6
+	"fmt"
7
+	"runtime/debug"
8
+	"strconv"
9
+	"sync"
10
+	"time"
11
+
12
+	_ "github.com/go-sql-driver/mysql"
13
+	"github.com/oragono/oragono/irc/history"
14
+	"github.com/oragono/oragono/irc/logger"
15
+	"github.com/oragono/oragono/irc/utils"
16
+)
17
+
18
+const (
19
+	// latest schema of the db
20
+	latestDbSchema   = "1"
21
+	keySchemaVersion = "db.version"
22
+	cleanupRowLimit  = 50
23
+	cleanupPauseTime = 10 * time.Minute
24
+)
25
+
26
+type MySQL struct {
27
+	db     *sql.DB
28
+	logger *logger.Manager
29
+
30
+	insertHistory      *sql.Stmt
31
+	insertSequence     *sql.Stmt
32
+	insertConversation *sql.Stmt
33
+
34
+	stateMutex sync.Mutex
35
+	expireTime time.Duration
36
+}
37
+
38
+func (mysql *MySQL) Initialize(logger *logger.Manager, expireTime time.Duration) {
39
+	mysql.logger = logger
40
+	mysql.expireTime = expireTime
41
+}
42
+
43
+func (mysql *MySQL) SetExpireTime(expireTime time.Duration) {
44
+	mysql.stateMutex.Lock()
45
+	mysql.expireTime = expireTime
46
+	mysql.stateMutex.Unlock()
47
+}
48
+
49
+func (mysql *MySQL) getExpireTime() (expireTime time.Duration) {
50
+	mysql.stateMutex.Lock()
51
+	expireTime = mysql.expireTime
52
+	mysql.stateMutex.Unlock()
53
+	return
54
+}
55
+
56
+func (mysql *MySQL) Open(username, password, host string, port int, database string) (err error) {
57
+	// TODO: timeouts!
58
+	var address string
59
+	if port != 0 {
60
+		address = fmt.Sprintf("tcp(%s:%d)", host, port)
61
+	}
62
+
63
+	mysql.db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@%s/%s", username, password, address, database))
64
+	if err != nil {
65
+		return err
66
+	}
67
+
68
+	err = mysql.fixSchemas()
69
+	if err != nil {
70
+		return err
71
+	}
72
+
73
+	err = mysql.prepareStatements()
74
+	if err != nil {
75
+		return err
76
+	}
77
+
78
+	go mysql.cleanupLoop()
79
+
80
+	return nil
81
+}
82
+
83
+func (mysql *MySQL) fixSchemas() (err error) {
84
+	_, err = mysql.db.Exec(`CREATE TABLE IF NOT EXISTS metadata (
85
+		key_name VARCHAR(32) primary key,
86
+		value VARCHAR(32) NOT NULL
87
+	) CHARSET=ascii COLLATE=ascii_bin;`)
88
+	if err != nil {
89
+		return err
90
+	}
91
+
92
+	var schema string
93
+	err = mysql.db.QueryRow(`select value from metadata where key_name = ?;`, keySchemaVersion).Scan(&schema)
94
+	if err == sql.ErrNoRows {
95
+		err = mysql.createTables()
96
+		if err != nil {
97
+			return
98
+		}
99
+		_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaVersion, latestDbSchema)
100
+		if err != nil {
101
+			return
102
+		}
103
+	} else if err == nil && schema != latestDbSchema {
104
+		// TODO figure out what to do about schema changes
105
+		return &utils.IncompatibleSchemaError{CurrentVersion: schema, RequiredVersion: latestDbSchema}
106
+	} else {
107
+		return err
108
+	}
109
+
110
+	return nil
111
+}
112
+
113
+func (mysql *MySQL) createTables() (err error) {
114
+	_, err = mysql.db.Exec(`CREATE TABLE history (
115
+		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
116
+		data BLOB NOT NULL,
117
+		msgid BINARY(16) NOT NULL,
118
+		KEY (msgid(4))
119
+	) CHARSET=ascii COLLATE=ascii_bin;`)
120
+	if err != nil {
121
+		return err
122
+	}
123
+
124
+	_, err = mysql.db.Exec(`CREATE TABLE sequence (
125
+		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
126
+		target VARBINARY(64) NOT NULL,
127
+		nanotime BIGINT UNSIGNED NOT NULL,
128
+		history_id BIGINT NOT NULL,
129
+		KEY (target, nanotime),
130
+		KEY (history_id)
131
+	) CHARSET=ascii COLLATE=ascii_bin;`)
132
+	if err != nil {
133
+		return err
134
+	}
135
+
136
+	_, err = mysql.db.Exec(`CREATE TABLE conversations (
137
+		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
138
+		lower_target VARBINARY(64) NOT NULL,
139
+		upper_target VARBINARY(64) NOT NULL,
140
+		nanotime BIGINT UNSIGNED NOT NULL,
141
+		history_id BIGINT NOT NULL,
142
+		KEY (lower_target, upper_target, nanotime),
143
+		KEY (history_id)
144
+	) CHARSET=ascii COLLATE=ascii_bin;`)
145
+	if err != nil {
146
+		return err
147
+	}
148
+
149
+	return nil
150
+}
151
+
152
+func (mysql *MySQL) cleanupLoop() {
153
+	defer func() {
154
+		if r := recover(); r != nil {
155
+			mysql.logger.Error("mysql",
156
+				fmt.Sprintf("Panic in cleanup routine: %v\n%s", r, debug.Stack()))
157
+			time.Sleep(cleanupPauseTime)
158
+			go mysql.cleanupLoop()
159
+		}
160
+	}()
161
+
162
+	for {
163
+		expireTime := mysql.getExpireTime()
164
+		if expireTime != 0 {
165
+			for {
166
+				startTime := time.Now()
167
+				rowsDeleted, err := mysql.doCleanup(expireTime)
168
+				elapsed := time.Now().Sub(startTime)
169
+				mysql.logError("error during row cleanup", err)
170
+				// keep going as long as we're accomplishing significant work
171
+				// (don't busy-wait on small numbers of rows expiring):
172
+				if rowsDeleted < (cleanupRowLimit / 10) {
173
+					break
174
+				}
175
+				// crude backpressure mechanism: if the database is slow,
176
+				// give it time to process other queries
177
+				time.Sleep(elapsed)
178
+			}
179
+		}
180
+		time.Sleep(cleanupPauseTime)
181
+	}
182
+}
183
+
184
+func (mysql *MySQL) doCleanup(age time.Duration) (count int, err error) {
185
+	ids, maxNanotime, err := mysql.selectCleanupIDs(age)
186
+	if len(ids) == 0 {
187
+		mysql.logger.Debug("mysql", "found no rows to clean up")
188
+		return
189
+	}
190
+
191
+	mysql.logger.Debug("mysql", fmt.Sprintf("deleting %d history rows, max age %s", len(ids), utils.NanoToTimestamp(maxNanotime)))
192
+
193
+	// can't use ? binding for a variable number of arguments, build the IN clause manually
194
+	var inBuf bytes.Buffer
195
+	inBuf.WriteByte('(')
196
+	for i, id := range ids {
197
+		if i != 0 {
198
+			inBuf.WriteRune(',')
199
+		}
200
+		inBuf.WriteString(strconv.FormatInt(int64(id), 10))
201
+	}
202
+	inBuf.WriteRune(')')
203
+
204
+	_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM conversations WHERE history_id in %s;`, inBuf.Bytes()))
205
+	if err != nil {
206
+		return
207
+	}
208
+	_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM sequence WHERE history_id in %s;`, inBuf.Bytes()))
209
+	if err != nil {
210
+		return
211
+	}
212
+	_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM history WHERE id in %s;`, inBuf.Bytes()))
213
+	if err != nil {
214
+		return
215
+	}
216
+
217
+	count = len(ids)
218
+	return
219
+}
220
+
221
+func (mysql *MySQL) selectCleanupIDs(age time.Duration) (ids []uint64, maxNanotime int64, err error) {
222
+	rows, err := mysql.db.Query(`
223
+		SELECT history.id, sequence.nanotime
224
+		FROM history
225
+		LEFT JOIN sequence ON history.id = sequence.history_id
226
+		ORDER BY history.id LIMIT ?;`, cleanupRowLimit)
227
+	if err != nil {
228
+		return
229
+	}
230
+	defer rows.Close()
231
+
232
+	// a history ID may have 0-2 rows in sequence: 1 for a channel entry,
233
+	// 2 for a DM, 0 if the data is inconsistent. therefore, deduplicate
234
+	// and delete anything that doesn't have a sequence entry:
235
+	idset := make(map[uint64]struct{}, cleanupRowLimit)
236
+	threshold := time.Now().Add(-age).UnixNano()
237
+	for rows.Next() {
238
+		var id uint64
239
+		var nanotime sql.NullInt64
240
+		err = rows.Scan(&id, &nanotime)
241
+		if err != nil {
242
+			return
243
+		}
244
+		if !nanotime.Valid || nanotime.Int64 < threshold {
245
+			idset[id] = struct{}{}
246
+			if nanotime.Valid && nanotime.Int64 > maxNanotime {
247
+				maxNanotime = nanotime.Int64
248
+			}
249
+		}
250
+	}
251
+	ids = make([]uint64, len(idset))
252
+	i := 0
253
+	for id := range idset {
254
+		ids[i] = id
255
+		i++
256
+	}
257
+	return
258
+}
259
+
260
+func (mysql *MySQL) prepareStatements() (err error) {
261
+	mysql.insertHistory, err = mysql.db.Prepare(`INSERT INTO history
262
+		(data, msgid) VALUES (?, ?);`)
263
+	if err != nil {
264
+		return
265
+	}
266
+	mysql.insertSequence, err = mysql.db.Prepare(`INSERT INTO sequence
267
+		(target, nanotime, history_id) VALUES (?, ?, ?);`)
268
+	if err != nil {
269
+		return
270
+	}
271
+	mysql.insertConversation, err = mysql.db.Prepare(`INSERT INTO conversations
272
+		(lower_target, upper_target, nanotime, history_id) VALUES (?, ?, ?, ?);`)
273
+	if err != nil {
274
+		return
275
+	}
276
+
277
+	return
278
+}
279
+
280
+func (mysql *MySQL) logError(context string, err error) (quit bool) {
281
+	if err != nil {
282
+		mysql.logger.Error("mysql", context, err.Error())
283
+		return true
284
+	}
285
+	return false
286
+}
287
+
288
+func (mysql *MySQL) AddChannelItem(target string, item history.Item) (err error) {
289
+	if mysql.db == nil {
290
+		return
291
+	}
292
+
293
+	if target == "" {
294
+		return utils.ErrInvalidParams
295
+	}
296
+
297
+	id, err := mysql.insertBase(item)
298
+	if err != nil {
299
+		return
300
+	}
301
+
302
+	err = mysql.insertSequenceEntry(target, item.Message.Time, id)
303
+	return
304
+}
305
+
306
+func (mysql *MySQL) insertSequenceEntry(target string, messageTime time.Time, id int64) (err error) {
307
+	_, err = mysql.insertSequence.Exec(target, messageTime.UnixNano(), id)
308
+	mysql.logError("could not insert sequence entry", err)
309
+	return
310
+}
311
+
312
+func (mysql *MySQL) insertConversationEntry(sender, recipient string, messageTime time.Time, id int64) (err error) {
313
+	lower, higher := stringMinMax(sender, recipient)
314
+	_, err = mysql.insertConversation.Exec(lower, higher, messageTime.UnixNano(), id)
315
+	mysql.logError("could not insert conversations entry", err)
316
+	return
317
+}
318
+
319
+func (mysql *MySQL) insertBase(item history.Item) (id int64, err error) {
320
+	value, err := marshalItem(&item)
321
+	if mysql.logError("could not marshal item", err) {
322
+		return
323
+	}
324
+
325
+	msgidBytes, err := decodeMsgid(item.Message.Msgid)
326
+	if mysql.logError("could not decode msgid", err) {
327
+		return
328
+	}
329
+
330
+	result, err := mysql.insertHistory.Exec(value, msgidBytes)
331
+	if mysql.logError("could not insert item", err) {
332
+		return
333
+	}
334
+	id, err = result.LastInsertId()
335
+	if mysql.logError("could not insert item", err) {
336
+		return
337
+	}
338
+
339
+	return
340
+}
341
+
342
+func stringMinMax(first, second string) (min, max string) {
343
+	if first < second {
344
+		return first, second
345
+	} else {
346
+		return second, first
347
+	}
348
+}
349
+
350
+func (mysql *MySQL) AddDirectMessage(sender, recipient string, senderPersistent, recipientPersistent bool, item history.Item) (err error) {
351
+	if mysql.db == nil {
352
+		return
353
+	}
354
+
355
+	if !(senderPersistent || recipientPersistent) {
356
+		return
357
+	}
358
+
359
+	if sender == "" || recipient == "" {
360
+		return utils.ErrInvalidParams
361
+	}
362
+
363
+	id, err := mysql.insertBase(item)
364
+	if err != nil {
365
+		return
366
+	}
367
+
368
+	if senderPersistent {
369
+		mysql.insertSequenceEntry(sender, item.Message.Time, id)
370
+		if err != nil {
371
+			return
372
+		}
373
+	}
374
+
375
+	if recipientPersistent && sender != recipient {
376
+		err = mysql.insertSequenceEntry(recipient, item.Message.Time, id)
377
+		if err != nil {
378
+			return
379
+		}
380
+	}
381
+
382
+	err = mysql.insertConversationEntry(sender, recipient, item.Message.Time, id)
383
+
384
+	return
385
+}
386
+
387
+func (mysql *MySQL) msgidToTime(msgid string) (result time.Time, err error) {
388
+	// in theory, we could optimize out a roundtrip to the database by using a subquery instead:
389
+	// sequence.nanotime > (
390
+	//     SELECT sequence.nanotime FROM sequence, history
391
+	//     WHERE sequence.history_id = history.id AND history.msgid = ?
392
+	//     LIMIT 1)
393
+	// however, this doesn't handle the BETWEEN case with one or two msgids, where we
394
+	// don't initially know whether the interval is going forwards or backwards. to simplify
395
+	// the logic,  resolve msgids to timestamps "manually" in all cases, using a separate query.
396
+	decoded, err := decodeMsgid(msgid)
397
+	if err != nil {
398
+		return
399
+	}
400
+	row := mysql.db.QueryRow(`
401
+		SELECT sequence.nanotime FROM sequence
402
+		INNER JOIN history ON history.id = sequence.history_id
403
+		WHERE history.msgid = ? LIMIT 1;`, decoded)
404
+	var nanotime int64
405
+	err = row.Scan(&nanotime)
406
+	if mysql.logError("could not resolve msgid to time", err) {
407
+		return
408
+	}
409
+	result = time.Unix(0, nanotime)
410
+	return
411
+}
412
+
413
+func (mysql *MySQL) selectItems(query string, args ...interface{}) (results []history.Item, err error) {
414
+	rows, err := mysql.db.Query(query, args...)
415
+	if mysql.logError("could not select history items", err) {
416
+		return
417
+	}
418
+
419
+	defer rows.Close()
420
+
421
+	for rows.Next() {
422
+		var blob []byte
423
+		var item history.Item
424
+		err = rows.Scan(&blob)
425
+		if mysql.logError("could not scan history item", err) {
426
+			return
427
+		}
428
+		err = unmarshalItem(blob, &item)
429
+		if mysql.logError("could not unmarshal history item", err) {
430
+			return
431
+		}
432
+		results = append(results, item)
433
+	}
434
+	return
435
+}
436
+
437
+func (mysql *MySQL) BetweenTimestamps(sender, recipient string, after, before, cutoff time.Time, limit int) (results []history.Item, err error) {
438
+	useSequence := true
439
+	var lowerTarget, upperTarget string
440
+	if sender != "" {
441
+		lowerTarget, upperTarget = stringMinMax(sender, recipient)
442
+		useSequence = false
443
+	}
444
+
445
+	table := "sequence"
446
+	if !useSequence {
447
+		table = "conversations"
448
+	}
449
+
450
+	after, before, ascending := history.MinMaxAsc(after, before, cutoff)
451
+	direction := "ASC"
452
+	if !ascending {
453
+		direction = "DESC"
454
+	}
455
+
456
+	var queryBuf bytes.Buffer
457
+
458
+	args := make([]interface{}, 0, 6)
459
+	fmt.Fprintf(&queryBuf,
460
+		"SELECT history.data from history INNER JOIN %[1]s ON history.id = %[1]s.history_id WHERE", table)
461
+	if useSequence {
462
+		fmt.Fprintf(&queryBuf, " sequence.target = ?")
463
+		args = append(args, recipient)
464
+	} else {
465
+		fmt.Fprintf(&queryBuf, " conversations.lower_target = ? AND conversations.upper_target = ?")
466
+		args = append(args, lowerTarget)
467
+		args = append(args, upperTarget)
468
+	}
469
+	if !after.IsZero() {
470
+		fmt.Fprintf(&queryBuf, " AND %s.nanotime > ?", table)
471
+		args = append(args, after.UnixNano())
472
+	}
473
+	if !before.IsZero() {
474
+		fmt.Fprintf(&queryBuf, " AND %s.nanotime < ?", table)
475
+		args = append(args, before.UnixNano())
476
+	}
477
+	fmt.Fprintf(&queryBuf, " ORDER BY %[1]s.nanotime %[2]s LIMIT ?;", table, direction)
478
+	args = append(args, limit)
479
+
480
+	results, err = mysql.selectItems(queryBuf.String(), args...)
481
+	if err == nil && !ascending {
482
+		history.Reverse(results)
483
+	}
484
+	return
485
+}
486
+
487
+func (mysql *MySQL) Close() {
488
+	// closing the database will close our prepared statements as well
489
+	if mysql.db != nil {
490
+		mysql.db.Close()
491
+	}
492
+	mysql.db = nil
493
+}
494
+
495
+// implements history.Sequence, emulating a single history buffer (for a channel,
496
+// a single user's DMs, or a DM conversation)
497
+type mySQLHistorySequence struct {
498
+	mysql     *MySQL
499
+	sender    string
500
+	recipient string
501
+	cutoff    time.Time
502
+}
503
+
504
+func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (results []history.Item, complete bool, err error) {
505
+	startTime := start.Time
506
+	if start.Msgid != "" {
507
+		startTime, err = s.mysql.msgidToTime(start.Msgid)
508
+		if err != nil {
509
+			return nil, false, err
510
+		}
511
+	}
512
+	endTime := end.Time
513
+	if end.Msgid != "" {
514
+		endTime, err = s.mysql.msgidToTime(end.Msgid)
515
+		if err != nil {
516
+			return nil, false, err
517
+		}
518
+	}
519
+
520
+	results, err = s.mysql.BetweenTimestamps(s.sender, s.recipient, startTime, endTime, s.cutoff, limit)
521
+	return results, (err == nil), err
522
+}
523
+
524
+func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (results []history.Item, err error) {
525
+	return history.GenericAround(s, start, limit)
526
+}
527
+
528
+func (mysql *MySQL) MakeSequence(sender, recipient string, cutoff time.Time) history.Sequence {
529
+	return &mySQLHistorySequence{
530
+		sender:    sender,
531
+		recipient: recipient,
532
+		mysql:     mysql,
533
+		cutoff:    cutoff,
534
+	}
535
+}

+ 23
- 0
irc/mysql/serialization.go 查看文件

@@ -0,0 +1,23 @@
1
+package mysql
2
+
3
+import (
4
+	"encoding/json"
5
+
6
+	"github.com/oragono/oragono/irc/history"
7
+	"github.com/oragono/oragono/irc/utils"
8
+)
9
+
10
+// 123 / '{' is the magic number that means JSON;
11
+// if we want to do a binary encoding later, we just have to add different magic version numbers
12
+
13
+func marshalItem(item *history.Item) (result []byte, err error) {
14
+	return json.Marshal(item)
15
+}
16
+
17
+func unmarshalItem(data []byte, result *history.Item) (err error) {
18
+	return json.Unmarshal(data, result)
19
+}
20
+
21
+func decodeMsgid(msgid string) ([]byte, error) {
22
+	return utils.B32Encoder.DecodeString(msgid)
23
+}

+ 10
- 8
irc/nickname.go 查看文件

@@ -43,13 +43,15 @@ func performNickChange(server *Server, client *Client, target *Client, session *
43 43
 	hadNick := target.HasNick()
44 44
 	origNickMask := target.NickMaskString()
45 45
 	details := target.Details()
46
-	err := client.server.clients.SetNick(target, session, nickname)
46
+	assignedNickname, err := client.server.clients.SetNick(target, session, nickname)
47 47
 	if err == errNicknameInUse {
48 48
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
49 49
 	} else if err == errNicknameReserved {
50 50
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
51 51
 	} else if err == errNicknameInvalid {
52 52
 		rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, utils.SafeErrorParam(nickname), client.t("Erroneous nickname"))
53
+	} else if err == errCantChangeNick {
54
+		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, utils.SafeErrorParam(nickname), client.t(err.Error()))
53 55
 	} else if err != nil {
54 56
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
55 57
 	}
@@ -64,26 +66,26 @@ func performNickChange(server *Server, client *Client, target *Client, session *
64 66
 		AccountName: details.accountName,
65 67
 		Message:     message,
66 68
 	}
67
-	histItem.Params[0] = nickname
69
+	histItem.Params[0] = assignedNickname
68 70
 
69
-	client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, client.NickCasefolded()))
71
+	client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, assignedNickname, client.NickCasefolded()))
70 72
 	if hadNick {
71 73
 		if client == target {
72
-			target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, nickname))
74
+			target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, assignedNickname))
73 75
 		} else {
74
-			target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("Operator %s changed nickname of $%s$r to %s"), client.Nick(), details.nick, nickname))
76
+			target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("Operator %s changed nickname of $%s$r to %s"), client.Nick(), details.nick, assignedNickname))
75 77
 		}
76 78
 		target.server.whoWas.Append(details.WhoWas)
77
-		rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
79
+		rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname)
78 80
 		for session := range target.Friends() {
79 81
 			if session != rb.session {
80
-				session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
82
+				session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname)
81 83
 			}
82 84
 		}
83 85
 	}
84 86
 
85 87
 	for _, channel := range client.Channels() {
86
-		channel.history.Add(histItem)
88
+		channel.AddHistoryItem(histItem)
87 89
 	}
88 90
 
89 91
 	if target.Registered() {

+ 76
- 1
irc/nickserv.go 查看文件

@@ -217,7 +217,7 @@ information on the settings and their possible values, see HELP SET.`,
217 217
 			helpStrings: []string{
218 218
 				`Syntax $bSET <setting> <value>$b
219 219
 
220
-Set modifies your account settings. The following settings are available:`,
220
+SET modifies your account settings. The following settings are available:`,
221 221
 
222 222
 				`$bENFORCE$b
223 223
 'enforce' lets you specify a custom enforcement mechanism for your registered
@@ -247,6 +247,22 @@ lines for join and part. This provides more information about the context of
247 247
 messages, but may be spammy. Your options are 'always', 'never', and the default
248 248
 of 'commands-only' (the messages will be replayed in /HISTORY output, but not
249 249
 during autoreplay).`,
250
+				`$bALWAYS-ON$b
251
+'always-on' controls whether your nickname/identity will remain active
252
+even while you are disconnected from the server. Your options are 'true',
253
+'false', and 'default' (use the server default value).`,
254
+				`$bAUTOREPLAY-MISSED$b
255
+'autoreplay-missed' is only effective for always-on clients. If enabled,
256
+if you have at most one active session, the server will remember the time
257
+you disconnect and then replay missed messages to you when you reconnect.
258
+Your options are 'on' and 'off'.`,
259
+				`$bDM-HISTORY$b
260
+'dm-history' is only effective for always-on clients. It lets you control
261
+how the history of your direct messages is stored. Your options are:
262
+1. 'off'        [no history]
263
+2. 'ephemeral'  [a limited amount of temporary history, not stored on disk]
264
+3. 'on'         [history stored in a permanent database, if available]
265
+4. 'default'    [use the server default]`,
250 266
 			},
251 267
 			authRequired: true,
252 268
 			enabled:      servCmdRequiresAccreg,
@@ -349,6 +365,31 @@ func displaySetting(settingName string, settings AccountSettings, client *Client
349 365
 				nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account"))
350 366
 			}
351 367
 		}
368
+	case "always-on":
369
+		stored := settings.AlwaysOn
370
+		actual := client.AlwaysOn()
371
+		nsNotice(rb, fmt.Sprintf(client.t("Your stored always-on setting is: %s"), persistentStatusToString(stored)))
372
+		if actual {
373
+			nsNotice(rb, client.t("Given current server settings, your client is always-on"))
374
+		} else {
375
+			nsNotice(rb, client.t("Given current server settings, your client is not always-on"))
376
+		}
377
+	case "autoreplay-missed":
378
+		stored := settings.AutoreplayMissed
379
+		if stored {
380
+			if client.AlwaysOn() {
381
+				nsNotice(rb, client.t("Autoreplay of missed messages is enabled"))
382
+			} else {
383
+				nsNotice(rb, client.t("You have enabled autoreplay of missed messages, but you can't receive them because your client isn't set to always-on"))
384
+			}
385
+		} else {
386
+			nsNotice(rb, client.t("Your account is not configured to receive autoreplayed missed messages"))
387
+		}
388
+	case "dm-history":
389
+		effectiveValue := historyEnabled(config.History.Persistent.DirectMessages, settings.DMHistory)
390
+		csNotice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
391
+		csNotice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
392
+
352 393
 	default:
353 394
 		nsNotice(rb, client.t("No such setting"))
354 395
 	}
@@ -429,6 +470,37 @@ func nsSetHandler(server *Server, client *Client, command string, params []strin
429 470
 				return
430 471
 			}
431 472
 		}
473
+	case "always-on":
474
+		var newValue PersistentStatus
475
+		newValue, err = persistentStatusFromString(params[1])
476
+		// "opt-in" and "opt-out" don't make sense as user preferences
477
+		if err == nil && newValue != PersistentOptIn && newValue != PersistentOptOut {
478
+			munger = func(in AccountSettings) (out AccountSettings, err error) {
479
+				out = in
480
+				out.AlwaysOn = newValue
481
+				return
482
+			}
483
+		}
484
+	case "autoreplay-missed":
485
+		var newValue bool
486
+		newValue, err = utils.StringToBool(params[1])
487
+		if err == nil {
488
+			munger = func(in AccountSettings) (out AccountSettings, err error) {
489
+				out = in
490
+				out.AutoreplayMissed = newValue
491
+				return
492
+			}
493
+		}
494
+	case "dm-history":
495
+		var newValue HistoryStatus
496
+		newValue, err = historyStatusFromString(params[1])
497
+		if err == nil {
498
+			munger = func(in AccountSettings) (out AccountSettings, err error) {
499
+				out = in
500
+				out.DMHistory = newValue
501
+				return
502
+			}
503
+		}
432 504
 	default:
433 505
 		err = errInvalidParams
434 506
 	}
@@ -480,6 +552,9 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
480 552
 	} else if ghost == client {
481 553
 		nsNotice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
482 554
 		return
555
+	} else if ghost.AlwaysOn() {
556
+		nsNotice(rb, client.t("You can't GHOST an always-on client"))
557
+		return
483 558
 	}
484 559
 
485 560
 	authorized := false

+ 42
- 2
irc/responsebuffer.go 查看文件

@@ -58,6 +58,10 @@ func NewResponseBuffer(session *Session) *ResponseBuffer {
58 58
 }
59 59
 
60 60
 func (rb *ResponseBuffer) AddMessage(msg ircmsg.IrcMessage) {
61
+	if rb == nil {
62
+		return
63
+	}
64
+
61 65
 	if rb.finalized {
62 66
 		rb.target.server.logger.Error("internal", "message added to finalized ResponseBuffer, undefined behavior")
63 67
 		debug.PrintStack()
@@ -80,12 +84,20 @@ func (rb *ResponseBuffer) setNestedBatchTag(msg *ircmsg.IrcMessage) {
80 84
 
81 85
 // Add adds a standard new message to our queue.
82 86
 func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command string, params ...string) {
87
+	if rb == nil {
88
+		return
89
+	}
90
+
83 91
 	rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...))
84 92
 }
85 93
 
86 94
 // Broadcast adds a standard new message to our queue, then sends an unlabeled copy
87 95
 // to all other sessions.
88 96
 func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, command string, params ...string) {
97
+	if rb == nil {
98
+		return
99
+	}
100
+
89 101
 	// can't reuse the IrcMessage object because of tag pollution :-\
90 102
 	rb.Add(tags, prefix, command, params...)
91 103
 	for _, session := range rb.session.client.Sessions() {
@@ -97,6 +109,10 @@ func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, comma
97 109
 
98 110
 // AddFromClient adds a new message from a specific client to our queue.
99 111
 func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) {
112
+	if rb == nil {
113
+		return
114
+	}
115
+
100 116
 	msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
101 117
 	if rb.session.capabilities.Has(caps.MessageTags) {
102 118
 		msg.UpdateTags(tags)
@@ -118,10 +134,14 @@ func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMa
118 134
 
119 135
 // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
120 136
 func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
137
+	if rb == nil {
138
+		return
139
+	}
140
+
121 141
 	if message.Is512() {
122 142
 		rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
123 143
 	} else {
124
-		if message.IsMultiline() && rb.session.capabilities.Has(caps.Multiline) {
144
+		if rb.session.capabilities.Has(caps.Multiline) {
125 145
 			batch := rb.session.composeMultilineBatch(fromNickMask, fromAccount, tags, command, target, message)
126 146
 			rb.setNestedBatchTag(&batch[0])
127 147
 			rb.setNestedBatchTag(&batch[len(batch)-1])
@@ -165,6 +185,10 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
165 185
 // Starts a nested batch (see the ResponseBuffer struct definition for a description of
166 186
 // how this works)
167 187
 func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) {
188
+	if rb == nil {
189
+		return
190
+	}
191
+
168 192
 	batchID = rb.session.generateBatchID()
169 193
 	msgParams := make([]string, len(params)+2)
170 194
 	msgParams[0] = "+" + batchID
@@ -194,6 +218,10 @@ func (rb *ResponseBuffer) EndNestedBatch(batchID string) {
194 218
 // Convenience to start a nested batch for history lines, at the highest level
195 219
 // supported by the client (`history`, `chathistory`, or no batch, in descending order).
196 220
 func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID string) {
221
+	if rb == nil {
222
+		return
223
+	}
224
+
197 225
 	var batchType string
198 226
 	if rb.session.capabilities.Has(caps.EventPlayback) {
199 227
 		batchType = "history"
@@ -210,6 +238,10 @@ func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID str
210 238
 // Afterwards, the buffer is in an undefined state and MUST NOT be used further.
211 239
 // If `blocking` is true you MUST be sending to the client from its own goroutine.
212 240
 func (rb *ResponseBuffer) Send(blocking bool) error {
241
+	if rb == nil {
242
+		return nil
243
+	}
244
+
213 245
 	return rb.flushInternal(true, blocking)
214 246
 }
215 247
 
@@ -218,6 +250,10 @@ func (rb *ResponseBuffer) Send(blocking bool) error {
218 250
 // to ensure that the final `BATCH -` message is sent.
219 251
 // If `blocking` is true you MUST be sending to the client from its own goroutine.
220 252
 func (rb *ResponseBuffer) Flush(blocking bool) error {
253
+	if rb == nil {
254
+		return nil
255
+	}
256
+
221 257
 	return rb.flushInternal(false, blocking)
222 258
 }
223 259
 
@@ -292,5 +328,9 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
292 328
 
293 329
 // Notice sends the client the given notice from the server.
294 330
 func (rb *ResponseBuffer) Notice(text string) {
295
-	rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.nick, text)
331
+	if rb == nil {
332
+		return
333
+	}
334
+
335
+	rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.Nick(), text)
296 336
 }

+ 95
- 10
irc/server.go 查看文件

@@ -24,8 +24,10 @@ import (
24 24
 	"github.com/goshuirc/irc-go/ircfmt"
25 25
 	"github.com/oragono/oragono/irc/caps"
26 26
 	"github.com/oragono/oragono/irc/connection_limits"
27
+	"github.com/oragono/oragono/irc/history"
27 28
 	"github.com/oragono/oragono/irc/logger"
28 29
 	"github.com/oragono/oragono/irc/modes"
30
+	"github.com/oragono/oragono/irc/mysql"
29 31
 	"github.com/oragono/oragono/irc/sno"
30 32
 	"github.com/tidwall/buntdb"
31 33
 )
@@ -84,6 +86,7 @@ type Server struct {
84 86
 	signals           chan os.Signal
85 87
 	snomasks          SnoManager
86 88
 	store             *buntdb.DB
89
+	historyDB         mysql.MySQL
87 90
 	torLimiter        connection_limits.TorLimiter
88 91
 	whoWas            WhoWasList
89 92
 	stats             Stats
@@ -122,7 +125,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
122 125
 	server.monitorManager.Initialize()
123 126
 	server.snomasks.Initialize()
124 127
 
125
-	if err := server.applyConfig(config, true); err != nil {
128
+	if err := server.applyConfig(config); err != nil {
126 129
 		return nil, err
127 130
 	}
128 131
 
@@ -143,6 +146,8 @@ func (server *Server) Shutdown() {
143 146
 	if err := server.store.Close(); err != nil {
144 147
 		server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
145 148
 	}
149
+
150
+	server.historyDB.Close()
146 151
 }
147 152
 
148 153
 // Run starts the server.
@@ -316,7 +321,7 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
316 321
 
317 322
 	// client MUST send PASS if necessary, or authenticate with SASL if necessary,
318 323
 	// before completing the other registration commands
319
-	authOutcome := c.isAuthorized(server.Config())
324
+	authOutcome := c.isAuthorized(server.Config(), session.isTor)
320 325
 	var quitMessage string
321 326
 	switch authOutcome {
322 327
 	case authFailPass:
@@ -376,7 +381,7 @@ func (server *Server) playRegistrationBurst(session *Session) {
376 381
 	// continue registration
377 382
 	d := c.Details()
378 383
 	server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname))
379
-	server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", d.nick, d.username, c.RawHostname(), c.IPString(), d.realname))
384
+	server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", d.nick, d.username, session.rawHostname, session.IP().String(), d.realname))
380 385
 
381 386
 	// send welcome text
382 387
 	//NOTE(dan): we specifically use the NICK here instead of the nickmask
@@ -550,7 +555,7 @@ func (server *Server) rehash() error {
550 555
 		return fmt.Errorf("Error loading config file config: %s", err.Error())
551 556
 	}
552 557
 
553
-	err = server.applyConfig(config, false)
558
+	err = server.applyConfig(config)
554 559
 	if err != nil {
555 560
 		return fmt.Errorf("Error applying config changes: %s", err.Error())
556 561
 	}
@@ -558,7 +563,10 @@ func (server *Server) rehash() error {
558 563
 	return nil
559 564
 }
560 565
 
561
-func (server *Server) applyConfig(config *Config, initial bool) (err error) {
566
+func (server *Server) applyConfig(config *Config) (err error) {
567
+	oldConfig := server.Config()
568
+	initial := oldConfig == nil
569
+
562 570
 	if initial {
563 571
 		server.configFilename = config.Filename
564 572
 		server.name = config.Server.Name
@@ -568,7 +576,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
568 576
 		// enforce configs that can't be changed after launch:
569 577
 		if server.name != config.Server.Name {
570 578
 			return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
571
-		} else if server.Config().Datastore.Path != config.Datastore.Path {
579
+		} else if oldConfig.Datastore.Path != config.Datastore.Path {
572 580
 			return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
573 581
 		} else if globalCasemappingSetting != config.Server.Casemapping {
574 582
 			return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
@@ -576,7 +584,6 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
576 584
 	}
577 585
 
578 586
 	server.logger.Info("server", "Using config file", server.configFilename)
579
-	oldConfig := server.Config()
580 587
 
581 588
 	// first, reload config sections for functionality implemented in subpackages:
582 589
 	wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
@@ -609,14 +616,13 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
609 616
 		if !oldConfig.Channels.Registration.Enabled {
610 617
 			server.channels.loadRegisteredChannels(config)
611 618
 		}
612
-
613 619
 		// resize history buffers as needed
614 620
 		if oldConfig.History != config.History {
615 621
 			for _, channel := range server.channels.Channels() {
616
-				channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
622
+				channel.resizeHistory(config)
617 623
 			}
618 624
 			for _, client := range server.clients.AllClients() {
619
-				client.history.Resize(config.History.ClientLength, config.History.AutoresizeWindow)
625
+				client.resizeHistory(config)
620 626
 			}
621 627
 		}
622 628
 	}
@@ -658,6 +664,10 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
658 664
 		if err := server.loadDatastore(config); err != nil {
659 665
 			return err
660 666
 		}
667
+	} else {
668
+		if config.Datastore.MySQL.Enabled {
669
+			server.historyDB.SetExpireTime(config.History.Restrictions.ExpireTime)
670
+		}
661 671
 	}
662 672
 
663 673
 	server.setupPprofListener(config)
@@ -778,6 +788,15 @@ func (server *Server) loadDatastore(config *Config) error {
778 788
 	server.channels.Initialize(server)
779 789
 	server.accounts.Initialize(server)
780 790
 
791
+	if config.Datastore.MySQL.Enabled {
792
+		server.historyDB.Initialize(server.logger, config.History.Restrictions.ExpireTime)
793
+		err = server.historyDB.Open(config.Datastore.MySQL.User, config.Datastore.MySQL.Password, config.Datastore.MySQL.Host, config.Datastore.MySQL.Port, config.Datastore.MySQL.HistoryDatabase)
794
+		if err != nil {
795
+			server.logger.Error("internal", "could not connect to mysql", err.Error())
796
+			return err
797
+		}
798
+	}
799
+
781 800
 	return nil
782 801
 }
783 802
 
@@ -835,6 +854,72 @@ func (server *Server) setupListeners(config *Config) (err error) {
835 854
 	return
836 855
 }
837 856
 
857
+// Gets the abstract sequence from which we're going to query history;
858
+// we may already know the channel we're querying, or we may have
859
+// to look it up via a string target. This function is responsible for
860
+// privilege checking.
861
+func (server *Server) GetHistorySequence(providedChannel *Channel, client *Client, target string) (channel *Channel, sequence history.Sequence, err error) {
862
+	config := server.Config()
863
+	var sender, recipient string
864
+	var hist *history.Buffer
865
+	if target == "*" {
866
+		if client.AlwaysOn() {
867
+			recipient = client.NickCasefolded()
868
+		} else {
869
+			hist = &client.history
870
+		}
871
+	} else {
872
+		channel = providedChannel
873
+		if channel == nil {
874
+			channel = server.channels.Get(target)
875
+		}
876
+		if channel != nil {
877
+			if !channel.hasClient(client) {
878
+				err = errInsufficientPrivs
879
+				return
880
+			}
881
+			persistent, ephemeral, cfTarget := channel.historyStatus(config)
882
+			if persistent {
883
+				recipient = cfTarget
884
+			} else if ephemeral {
885
+				hist = &channel.history
886
+			} else {
887
+				return
888
+			}
889
+		} else {
890
+			sender = client.NickCasefolded()
891
+			var cfTarget string
892
+			cfTarget, err = CasefoldName(target)
893
+			if err != nil {
894
+				return
895
+			}
896
+			recipient = cfTarget
897
+			if !client.AlwaysOn() {
898
+				hist = &client.history
899
+			}
900
+		}
901
+	}
902
+
903
+	var cutoff time.Time
904
+	if config.History.Restrictions.ExpireTime != 0 {
905
+		cutoff = time.Now().UTC().Add(-config.History.Restrictions.ExpireTime)
906
+	}
907
+	if config.History.Restrictions.EnforceRegistrationDate {
908
+		regCutoff := client.historyCutoff()
909
+		regCutoff.Add(-config.History.Restrictions.GracePeriod)
910
+		// take the earlier of the two cutoffs
911
+		if regCutoff.After(cutoff) {
912
+			cutoff = regCutoff
913
+		}
914
+	}
915
+	if hist != nil {
916
+		sequence = hist.MakeSequence(recipient, cutoff)
917
+	} else if recipient != "" {
918
+		sequence = server.historyDB.MakeSequence(sender, recipient, cutoff)
919
+	}
920
+	return
921
+}
922
+
838 923
 // elistMatcher takes and matches ELIST conditions
839 924
 type elistMatcher struct {
840 925
 	MinClientsActive bool

+ 19
- 1
irc/stats.go 查看文件

@@ -26,15 +26,33 @@ func (s *Stats) Add() {
26 26
 	s.mutex.Unlock()
27 27
 }
28 28
 
29
+// Activates a registered client, e.g., for the initial attach to a persistent client
30
+func (s *Stats) AddRegistered(invisible, operator bool) {
31
+	s.mutex.Lock()
32
+	if invisible {
33
+		s.Invisible += 1
34
+	}
35
+	if operator {
36
+		s.Operators += 1
37
+	}
38
+	s.Total += 1
39
+	s.setMax()
40
+	s.mutex.Unlock()
41
+}
42
+
29 43
 // Transition a client from unregistered to registered
30 44
 func (s *Stats) Register() {
31 45
 	s.mutex.Lock()
32 46
 	s.Unknown -= 1
33 47
 	s.Total += 1
48
+	s.setMax()
49
+	s.mutex.Unlock()
50
+}
51
+
52
+func (s *Stats) setMax() {
34 53
 	if s.Max < s.Total {
35 54
 		s.Max = s.Total
36 55
 	}
37
-	s.mutex.Unlock()
38 56
 }
39 57
 
40 58
 // Modify the Invisible count

+ 21
- 2
irc/utils/args.go 查看文件

@@ -5,7 +5,13 @@ package utils
5 5
 
6 6
 import (
7 7
 	"errors"
8
+	"fmt"
8 9
 	"strings"
10
+	"time"
11
+)
12
+
13
+const (
14
+	IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
9 15
 )
10 16
 
11 17
 var (
@@ -45,9 +51,9 @@ func ArgsToStrings(maxLength int, arguments []string, delim string) []string {
45 51
 
46 52
 func StringToBool(str string) (result bool, err error) {
47 53
 	switch strings.ToLower(str) {
48
-	case "on", "true", "t", "yes", "y":
54
+	case "on", "true", "t", "yes", "y", "disabled":
49 55
 		result = true
50
-	case "off", "false", "f", "no", "n":
56
+	case "off", "false", "f", "no", "n", "enabled":
51 57
 		result = false
52 58
 	default:
53 59
 		err = ErrInvalidParams
@@ -63,3 +69,16 @@ func SafeErrorParam(param string) string {
63 69
 	}
64 70
 	return param
65 71
 }
72
+
73
+type IncompatibleSchemaError struct {
74
+	CurrentVersion  string
75
+	RequiredVersion string
76
+}
77
+
78
+func (err *IncompatibleSchemaError) Error() string {
79
+	return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.RequiredVersion, err.CurrentVersion)
80
+}
81
+
82
+func NanoToTimestamp(nanotime int64) string {
83
+	return time.Unix(0, nanotime).Format(IRCv3TimestampFormat)
84
+}

+ 0
- 8
irc/utils/net.go 查看文件

@@ -18,14 +18,6 @@ var (
18 18
 	validHostnameLabelRegexp = regexp.MustCompile(`^[0-9A-Za-z.\-]+$`)
19 19
 )
20 20
 
21
-// AddrIsLocal returns whether the address is from a trusted local connection (loopback or unix).
22
-func AddrIsLocal(addr net.Addr) bool {
23
-	if tcpaddr, ok := addr.(*net.TCPAddr); ok {
24
-		return tcpaddr.IP.IsLoopback()
25
-	}
26
-	return AddrIsUnix(addr)
27
-}
28
-
29 21
 // AddrToIP returns the IP address for a net.Addr; unix domain sockets are treated as IPv4 loopback
30 22
 func AddrToIP(addr net.Addr) net.IP {
31 23
 	if tcpaddr, ok := addr.(*net.TCPAddr); ok {

+ 5
- 8
irc/utils/text.go 查看文件

@@ -23,9 +23,9 @@ type MessagePair struct {
23 23
 // SplitMessage represents a message that's been split for sending.
24 24
 // Two possibilities:
25 25
 // (a) Standard message that can be relayed on a single 512-byte line
26
-//     (MessagePair contains the message, Wrapped == nil)
26
+//     (MessagePair contains the message, Split == nil)
27 27
 // (b) multiline message that was split on the client side
28
-//     (Message == "", Wrapped contains the split lines)
28
+//     (Message == "", Split contains the split lines)
29 29
 type SplitMessage struct {
30 30
 	Message string
31 31
 	Msgid   string
@@ -36,7 +36,7 @@ type SplitMessage struct {
36 36
 func MakeMessage(original string) (result SplitMessage) {
37 37
 	result.Message = original
38 38
 	result.Msgid = GenerateSecretToken()
39
-	result.Time = time.Now().UTC()
39
+	result.SetTime()
40 40
 
41 41
 	return
42 42
 }
@@ -52,7 +52,8 @@ func (sm *SplitMessage) Append(message string, concat bool) {
52 52
 }
53 53
 
54 54
 func (sm *SplitMessage) SetTime() {
55
-	sm.Time = time.Now().UTC()
55
+	// strip the monotonic time, it's a potential source of problems:
56
+	sm.Time = time.Now().UTC().Round(0)
56 57
 }
57 58
 
58 59
 func (sm *SplitMessage) LenLines() int {
@@ -88,10 +89,6 @@ func (sm *SplitMessage) IsRestrictedCTCPMessage() bool {
88 89
 	return false
89 90
 }
90 91
 
91
-func (sm *SplitMessage) IsMultiline() bool {
92
-	return sm.Message == "" && len(sm.Split) != 0
93
-}
94
-
95 92
 func (sm *SplitMessage) Is512() bool {
96 93
 	return sm.Message != ""
97 94
 }

+ 15
- 3
irc/znc.go 查看文件

@@ -8,6 +8,8 @@ import (
8 8
 	"strconv"
9 9
 	"strings"
10 10
 	"time"
11
+
12
+	"github.com/oragono/oragono/irc/history"
11 13
 )
12 14
 
13 15
 type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
@@ -89,10 +91,8 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
89 91
 	//     3.3  When the client sends a subsequent redundant JOIN line for those
90 92
 	//          channels; redundant JOIN is a complete no-op so we won't replay twice
91 93
 
92
-	config := client.server.Config()
93 94
 	if params[1] == "*" {
94
-		items, _ := client.history.Between(after, before, false, config.History.ChathistoryMax)
95
-		client.replayPrivmsgHistory(rb, items, true)
95
+		zncPlayPrivmsgs(client, rb, after, before)
96 96
 	} else {
97 97
 		targets = make(StringSet)
98 98
 		// TODO actually handle nickname targets
@@ -116,3 +116,15 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
116 116
 		}
117 117
 	}
118 118
 }
119
+
120
+func zncPlayPrivmsgs(client *Client, rb *ResponseBuffer, after, before time.Time) {
121
+	_, sequence, _ := client.server.GetHistorySequence(nil, client, "*")
122
+	if sequence == nil {
123
+		return
124
+	}
125
+	zncMax := client.server.Config().History.ZNCMax
126
+	items, _, err := sequence.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
127
+	if err == nil {
128
+		client.replayPrivmsgHistory(rb, items, "", true)
129
+	}
130
+}

+ 63
- 1
oragono.yaml 查看文件

@@ -245,6 +245,15 @@ server:
245 245
         # all users will receive simply `netname` as their cloaked hostname.
246 246
         num-bits: 80
247 247
 
248
+    # secure-nets identifies IPs and CIDRs which are secure at layer 3,
249
+    # for example, because they are on a trusted internal LAN or a VPN.
250
+    # plaintext connections from these IPs and CIDRs will be considered
251
+    # secure (clients will receive the +Z mode and be allowed to resume
252
+    # or reattach to secure connections). note that loopback IPs are always
253
+    # considered secure:
254
+    secure-nets:
255
+        # - "10.0.0.0/8"
256
+
248 257
 
249 258
 # account options
250 259
 accounts:
@@ -351,6 +360,11 @@ accounts:
351 360
         # via nickserv
352 361
         allowed-by-default: true
353 362
 
363
+        # whether to allow clients that remain on the server even
364
+        # when they have no active connections. The possible values are:
365
+        # "disabled", "opt-in", "opt-out", or "mandatory".
366
+        always-on: "disabled"
367
+
354 368
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
355 369
     # hostname/IP) by the HostServ service
356 370
     vhosts:
@@ -585,6 +599,16 @@ datastore:
585 599
     # up, and if the upgrade fails, the original database will be restored.
586 600
     autoupgrade: true
587 601
 
602
+    # connection information for MySQL (currently only used for persistent history):
603
+    mysql:
604
+        enabled: false
605
+        host: "localhost"
606
+        # port is unnecessary for connections via unix domain socket:
607
+        #port: 3306
608
+        user: "oragono"
609
+        password: "KOHw8WSaRwaoo-avo0qVpQ"
610
+        history-database: "oragono_history"
611
+
588 612
 # languages config
589 613
 languages:
590 614
     # whether to load languages
@@ -657,7 +681,7 @@ fakelag:
657 681
 # message history tracking, for the RESUME extension and possibly other uses in future
658 682
 history:
659 683
     # should we store messages for later playback?
660
-    # the current implementation stores messages in RAM only; they do not persist
684
+    # by default, messages are stored in RAM only; they do not persist
661 685
     # across server restarts. however, you should not enable this unless you understand
662 686
     # how it interacts with the GDPR and/or any data privacy laws that apply
663 687
     # in your country and the countries of your users.
@@ -683,3 +707,41 @@ history:
683 707
     # maximum number of CHATHISTORY messages that can be
684 708
     # requested at once (0 disables support for CHATHISTORY)
685 709
     chathistory-maxmessages: 100
710
+
711
+    # maximum number of messages that can be replayed at once during znc emulation
712
+    # (znc.in/playback, or automatic replay on initial reattach to a persistent client):
713
+    znc-maxmessages: 2048
714
+
715
+    # options to delete old messages, or prevent them from being retrieved
716
+    restrictions:
717
+        # if this is set, messages older than this cannot be retrieved by anyone
718
+        # (and will eventually be deleted from persistent storage, if that's enabled)
719
+        #expire-time: 168h # 7 days
720
+
721
+        # if this is set, logged-in users cannot retrieve messages older than their
722
+        # account registration date, and logged-out users cannot retrieve messages
723
+        # older than their sign-on time (modulo grace-period, see below):
724
+        enforce-registration-date: false
725
+
726
+        # but if this is set, you can retrieve messages that are up to `grace-period`
727
+        # older than the above cutoff time. this is recommended to allow logged-out
728
+        # users to do session resumption / query history after disconnections.
729
+        grace-period: 1h
730
+
731
+    # options to store history messages in a persistent database (currently only MySQL):
732
+    persistent:
733
+        enabled: false
734
+
735
+        # store unregistered channel messages in the persistent database?
736
+        unregistered-channels: false
737
+
738
+        # for a registered channel, the channel owner can potentially customize
739
+        # the history storage setting. as the server operator, your options are
740
+        # 'disabled' (no persistent storage, regardless of per-channel setting),
741
+        # 'opt-in', 'opt-out', and 'mandatory' (force persistent storage, ignoring
742
+        # per-channel setting):
743
+        registered-channels: "opt-out"
744
+
745
+        # direct messages are only stored in the database for persistent clients;
746
+        # you can control how they are stored here (same options as above)
747
+        direct-messages: "opt-out"

正在加载...
取消
保存