Ver código fonte

initial persistent history implementation

tags/v2.0.0-rc1
Shivaram Lingamneni 4 anos atrás
pai
commit
33dac4c0ba

+ 1
- 0
Makefile Ver arquivo

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

+ 6
- 0
gencapdefs.py Ver arquivo

171
         url="https://github.com/ircv3/ircv3-specifications/pull/398",
171
         url="https://github.com/ircv3/ircv3-specifications/pull/398",
172
         standard="proposed IRCv3",
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
 def validate_defs():
182
 def validate_defs():

+ 4
- 8
go.mod Ver arquivo

6
 	code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
6
 	code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
7
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
7
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
8
 	github.com/go-ldap/ldap/v3 v3.1.6
8
 	github.com/go-ldap/ldap/v3 v3.1.6
9
+	github.com/go-sql-driver/mysql v1.5.0
9
 	github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
10
 	github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
10
 	github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0
11
 	github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0
11
 	github.com/mattn/go-colorable v0.1.4
12
 	github.com/mattn/go-colorable v0.1.4
12
 	github.com/mattn/go-isatty v0.0.10 // indirect
13
 	github.com/mattn/go-isatty v0.0.10 // indirect
13
 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
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
 	github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
17
 	github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
15
 	github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
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
 	github.com/tidwall/buntdb v1.1.2
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
 	golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
21
 	golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
25
-	golang.org/x/sys v0.0.0-20191115151921-52ab43148777 // indirect
26
 	golang.org/x/text v0.3.2
22
 	golang.org/x/text v0.3.2
27
 	gopkg.in/yaml.v2 v2.2.5
23
 	gopkg.in/yaml.v2 v2.2.5
28
 )
24
 )

+ 73
- 17
irc/accounts.go Ver arquivo

33
 	keyAccountSettings         = "account.settings %s"
33
 	keyAccountSettings         = "account.settings %s"
34
 	keyAccountVHost            = "account.vhost %s"
34
 	keyAccountVHost            = "account.vhost %s"
35
 	keyCertToAccount           = "account.creds.certfp %s"
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
 	keyVHostQueueAcctToId = "vhostQueue %s"
39
 	keyVHostQueueAcctToId = "vhostQueue %s"
39
 	vhostRequestIdx       = "vhostQueue"
40
 	vhostRequestIdx       = "vhostQueue"
71
 	config := server.Config()
72
 	config := server.Config()
72
 	am.buildNickToAccountIndex(config)
73
 	am.buildNickToAccountIndex(config)
73
 	am.initVHostRequestQueue(config)
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
 func (am *AccountManager) buildNickToAccountIndex(config *Config) {
111
 func (am *AccountManager) buildNickToAccountIndex(config *Config) {
477
 	return err
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
 func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
537
 func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
481
 	certfp, err = utils.NormalizeCertfp(certfp)
538
 	certfp, err = utils.NormalizeCertfp(certfp)
482
 	if err != nil {
539
 	if err != nil {
685
 	}
742
 	}
686
 	am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
743
 	am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
687
 	raw.Verified = true
744
 	raw.Verified = true
688
-	clientAccount, err := am.deserializeRawAccount(raw)
745
+	clientAccount, err := am.deserializeRawAccount(raw, casefoldedAccount)
689
 	if err != nil {
746
 	if err != nil {
690
 		return err
747
 		return err
691
 	}
748
 	}
892
 		return
949
 		return
893
 	}
950
 	}
894
 
951
 
895
-	result, err = am.deserializeRawAccount(raw)
896
-	result.NameCasefolded = casefoldedAccount
952
+	result, err = am.deserializeRawAccount(raw, casefoldedAccount)
897
 	return
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
 	result.Name = raw.Name
957
 	result.Name = raw.Name
958
+	result.NameCasefolded = cfName
902
 	regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
959
 	regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
903
 	result.RegisteredAt = time.Unix(regTimeInt, 0).UTC()
960
 	result.RegisteredAt = time.Unix(regTimeInt, 0).UTC()
904
 	e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
961
 	e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
976
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
1033
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
977
 	vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
1034
 	vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
978
 	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
1035
 	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
1036
+	joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount)
979
 
1037
 
980
 	var clients []*Client
1038
 	var clients []*Client
981
 
1039
 
1011
 		tx.Delete(vhostKey)
1069
 		tx.Delete(vhostKey)
1012
 		channelsStr, _ = tx.Get(channelsKey)
1070
 		channelsStr, _ = tx.Get(channelsKey)
1013
 		tx.Delete(channelsKey)
1071
 		tx.Delete(channelsKey)
1072
+		tx.Delete(joinedChannelsKey)
1014
 
1073
 
1015
 		_, err := tx.Delete(vhostQueueKey)
1074
 		_, err := tx.Delete(vhostQueueKey)
1016
 		am.decrementVHostQueueCount(casefoldedAccount, err)
1075
 		am.decrementVHostQueueCount(casefoldedAccount, err)
1455
 }
1514
 }
1456
 
1515
 
1457
 func (am *AccountManager) Login(client *Client, account ClientAccount) {
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
 	client.nickTimer.Touch(nil)
1519
 	client.nickTimer.Touch(nil)
1464
 
1520
 
1468
 	am.Lock()
1524
 	am.Lock()
1469
 	defer am.Unlock()
1525
 	defer am.Unlock()
1470
 	am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
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
 func (am *AccountManager) Logout(client *Client) {
1529
 func (am *AccountManager) Logout(client *Client) {
1623
 }
1676
 }
1624
 
1677
 
1625
 type AccountSettings struct {
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
 // ClientAccount represents a user account.
1688
 // ClientAccount represents a user account.
1661
 		return
1717
 		return
1662
 	}
1718
 	}
1663
 
1719
 
1664
-	client.SetAccountName("")
1720
+	client.Logout()
1665
 	go client.nickTimer.Touch(nil)
1721
 	go client.nickTimer.Touch(nil)
1666
 
1722
 
1667
 	// dispatch account-notify
1723
 	// dispatch account-notify

+ 6
- 1
irc/caps/defs.go Ver arquivo

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

+ 122
- 32
irc/channel.go Ver arquivo

24
 	histServMask = "HistServ!HistServ@localhost"
24
 	histServMask = "HistServ!HistServ@localhost"
25
 )
25
 )
26
 
26
 
27
+type ChannelSettings struct {
28
+	History HistoryStatus
29
+}
30
+
27
 // Channel represents a channel that clients can join.
31
 // Channel represents a channel that clients can join.
28
 type Channel struct {
32
 type Channel struct {
29
 	flags             modes.ModeSet
33
 	flags             modes.ModeSet
49
 	joinPartMutex     sync.Mutex      // tier 3
53
 	joinPartMutex     sync.Mutex      // tier 3
50
 	ensureLoaded      utils.Once      // manages loading stored registration info from the database
54
 	ensureLoaded      utils.Once      // manages loading stored registration info from the database
51
 	dirtyBits         uint
55
 	dirtyBits         uint
56
+	settings          ChannelSettings
52
 }
57
 }
53
 
58
 
54
 // NewChannel creates a new channel from a `Server` and a `name`
59
 // NewChannel creates a new channel from a `Server` and a `name`
66
 
71
 
67
 	channel.initializeLists()
72
 	channel.initializeLists()
68
 	channel.writerSemaphore.Initialize(1)
73
 	channel.writerSemaphore.Initialize(1)
69
-	channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
74
+	channel.history.Initialize(0, 0)
70
 
75
 
71
 	if !registered {
76
 	if !registered {
77
+		channel.resizeHistory(config)
72
 		for _, mode := range config.Channels.defaultModes {
78
 		for _, mode := range config.Channels.defaultModes {
73
 			channel.flags.SetMode(mode, true)
79
 			channel.flags.SetMode(mode, true)
74
 		}
80
 		}
106
 	return channel.ensureLoaded.Done()
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
 // read in channel state that was persisted in the DB
124
 // read in channel state that was persisted in the DB
110
 func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
125
 func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
126
+	defer channel.resizeHistory(channel.server.Config())
127
+
111
 	channel.stateMutex.Lock()
128
 	channel.stateMutex.Lock()
112
 	defer channel.stateMutex.Unlock()
129
 	defer channel.stateMutex.Unlock()
113
 
130
 
120
 	channel.createdTime = chanReg.RegisteredAt
137
 	channel.createdTime = chanReg.RegisteredAt
121
 	channel.key = chanReg.Key
138
 	channel.key = chanReg.Key
122
 	channel.userLimit = chanReg.UserLimit
139
 	channel.userLimit = chanReg.UserLimit
140
+	channel.settings = chanReg.Settings
123
 
141
 
124
 	for _, mode := range chanReg.Modes {
142
 	for _, mode := range chanReg.Modes {
125
 		channel.flags.SetMode(mode, true)
143
 		channel.flags.SetMode(mode, true)
164
 		}
182
 		}
165
 	}
183
 	}
166
 
184
 
185
+	if includeFlags&IncludeSettings != 0 {
186
+		info.Settings = channel.settings
187
+	}
188
+
167
 	return
189
 	return
168
 }
190
 }
169
 
191
 
434
 			if modeSet == nil {
456
 			if modeSet == nil {
435
 				continue
457
 				continue
436
 			}
458
 			}
437
-			if !isJoined && target.flags.HasMode(modes.Invisible) && !isOper {
459
+			if !isJoined && target.HasMode(modes.Invisible) && !isOper {
438
 				continue
460
 				continue
439
 			}
461
 			}
440
 			prefix := modeSet.Prefixes(isMultiPrefix)
462
 			prefix := modeSet.Prefixes(isMultiPrefix)
564
 	return len(channel.members) == 0
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
 // Join joins the given client to this channel (if they can be joined).
631
 // Join joins the given client to this channel (if they can be joined).
568
 func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
632
 func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
569
 	details := client.Details()
633
 	details := client.Details()
643
 
707
 
644
 		channel.regenerateMembersCache()
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
 		return
723
 		return
657
 	}()
724
 	}()
665
 
732
 
666
 	for _, member := range channel.Members() {
733
 	for _, member := range channel.Members() {
667
 		for _, session := range member.Sessions() {
734
 		for _, session := range member.Sessions() {
668
-			if session == rb.session {
735
+			if rb != nil && session == rb.session {
669
 				continue
736
 				continue
670
 			} else if client == session.client {
737
 			} else if client == session.client {
671
 				channel.playJoinForSession(session)
738
 				channel.playJoinForSession(session)
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
 		rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname)
753
 		rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname)
687
 	} else {
754
 	} else {
688
 		rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname)
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
 		// don't send topic and names for a SAJOIN of a different client
759
 		// don't send topic and names for a SAJOIN of a different client
693
 		channel.SendTopic(client, rb, false)
760
 		channel.SendTopic(client, rb, false)
694
 		channel.Names(client, rb)
761
 		channel.Names(client, rb)
697
 	// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
764
 	// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
698
 	rb.Flush(true)
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
 func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
772
 func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
704
 	// autoreplay any messages as necessary
773
 	// autoreplay any messages as necessary
705
-	config := channel.server.Config()
706
 	var items []history.Item
774
 	var items []history.Item
775
+
776
+	var after, before time.Time
707
 	if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets.Has(channel.NameCasefolded())) {
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
 	} else if !rb.session.HasHistoryCaps() {
790
 	} else if !rb.session.HasHistoryCaps() {
710
 		var replayLimit int
791
 		var replayLimit int
711
 		customReplayLimit := client.AccountSettings().AutoreplayLines
792
 		customReplayLimit := client.AccountSettings().AutoreplayLines
719
 			replayLimit = channel.server.Config().History.AutoreplayOnJoin
800
 			replayLimit = channel.server.Config().History.AutoreplayOnJoin
720
 		}
801
 		}
721
 		if 0 < replayLimit {
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
 	// remove the client's own JOIN line from the replay
809
 	// remove the client's own JOIN line from the replay
784
 		}
868
 		}
785
 	}
869
 	}
786
 
870
 
787
-	channel.history.Add(history.Item{
871
+	channel.AddHistoryItem(history.Item{
788
 		Type:        history.Part,
872
 		Type:        history.Part,
789
 		Nick:        details.nickMask,
873
 		Nick:        details.nickMask,
790
 		AccountName: details.accountName,
874
 		AccountName: details.accountName,
799
 // 2. Send JOIN and MODE lines to channel participants (including the new client)
883
 // 2. Send JOIN and MODE lines to channel participants (including the new client)
800
 // 3. Replay missed message history to the client
884
 // 3. Replay missed message history to the client
801
 func (channel *Channel) Resume(session *Session, timestamp time.Time) {
885
 func (channel *Channel) Resume(session *Session, timestamp time.Time) {
802
-	now := time.Now().UTC()
803
 	channel.resumeAndAnnounce(session)
886
 	channel.resumeAndAnnounce(session)
804
 	if !timestamp.IsZero() {
887
 	if !timestamp.IsZero() {
805
-		channel.replayHistoryForResume(session, timestamp, now)
888
+		channel.replayHistoryForResume(session, timestamp, time.Time{})
806
 	}
889
 	}
807
 }
890
 }
808
 
891
 
852
 }
935
 }
853
 
936
 
854
 func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
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
 	rb := NewResponseBuffer(session)
945
 	rb := NewResponseBuffer(session)
857
 	channel.replayHistoryItems(rb, items, false)
946
 	channel.replayHistoryItems(rb, items, false)
858
 	if !complete && !session.resumeDetails.HistoryIncomplete {
947
 	if !complete && !session.resumeDetails.HistoryIncomplete {
908
 		case history.Join:
997
 		case history.Join:
909
 			if eventPlayback {
998
 			if eventPlayback {
910
 				if extendedJoin {
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
 				} else {
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
 			} else {
1004
 			} else {
916
 				if !playJoinsAsPrivmsg {
1005
 				if !playJoinsAsPrivmsg {
926
 			}
1015
 			}
927
 		case history.Part:
1016
 		case history.Part:
928
 			if eventPlayback {
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
 			} else {
1019
 			} else {
931
 				if !playJoinsAsPrivmsg {
1020
 				if !playJoinsAsPrivmsg {
932
 					continue // #474
1021
 					continue // #474
936
 			}
1025
 			}
937
 		case history.Kick:
1026
 		case history.Kick:
938
 			if eventPlayback {
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
 			} else {
1029
 			} else {
941
 				message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
1030
 				message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
942
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
1031
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
943
 			}
1032
 			}
944
 		case history.Quit:
1033
 		case history.Quit:
945
 			if eventPlayback {
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
 			} else {
1036
 			} else {
948
 				if !playJoinsAsPrivmsg {
1037
 				if !playJoinsAsPrivmsg {
949
 					continue // #474
1038
 					continue // #474
953
 			}
1042
 			}
954
 		case history.Nick:
1043
 		case history.Nick:
955
 			if eventPlayback {
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
 			} else {
1046
 			} else {
958
 				message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
1047
 				message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
959
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
1048
 				rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
1124
 			// STATUSMSG
1213
 			// STATUSMSG
1125
 			continue
1214
 			continue
1126
 		}
1215
 		}
1127
-		if isCTCP && member.isTor {
1128
-			continue // #753
1129
-		}
1130
 
1216
 
1131
 		for _, session := range member.Sessions() {
1217
 		for _, session := range member.Sessions() {
1218
+			if isCTCP && session.isTor {
1219
+				continue // #753
1220
+			}
1221
+
1132
 			var tagsToUse map[string]string
1222
 			var tagsToUse map[string]string
1133
 			if session.capabilities.Has(caps.MessageTags) {
1223
 			if session.capabilities.Has(caps.MessageTags) {
1134
 				tagsToUse = clientOnlyTags
1224
 				tagsToUse = clientOnlyTags
1144
 		}
1234
 		}
1145
 	}
1235
 	}
1146
 
1236
 
1147
-	channel.history.Add(history.Item{
1237
+	channel.AddHistoryItem(history.Item{
1148
 		Type:        histType,
1238
 		Type:        histType,
1149
 		Message:     message,
1239
 		Message:     message,
1150
 		Nick:        nickmask,
1240
 		Nick:        nickmask,
1266
 		Message:     message,
1356
 		Message:     message,
1267
 	}
1357
 	}
1268
 	histItem.Params[0] = targetNick
1358
 	histItem.Params[0] = targetNick
1269
-	channel.history.Add(histItem)
1359
+	channel.AddHistoryItem(histItem)
1270
 
1360
 
1271
 	channel.Quit(target)
1361
 	channel.Quit(target)
1272
 }
1362
 }

+ 15
- 0
irc/channelreg.go Ver arquivo

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

+ 116
- 21
irc/chanserv.go Ver arquivo

137
 			enabled:   chanregEnabled,
137
 			enabled:   chanregEnabled,
138
 			minParams: 1,
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
 	return
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
 func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
368
 func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
324
 	channelName := params[0]
369
 	channelName := params[0]
325
 	var verificationCode string
370
 	var verificationCode string
327
 		verificationCode = params[1]
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
 	if channel == nil {
376
 	if channel == nil {
338
 		csNotice(rb, client.t("No such channel"))
377
 		csNotice(rb, client.t("No such channel"))
339
 		return
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
 		return
384
 		return
352
 	}
385
 	}
353
 
386
 
354
-	info := channel.ExportRegistration(0)
355
 	expectedCode := unregisterConfirmationCode(info.Name, info.RegisteredAt)
387
 	expectedCode := unregisterConfirmationCode(info.Name, info.RegisteredAt)
356
 	if expectedCode != verificationCode {
388
 	if expectedCode != verificationCode {
357
 		csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
389
 		csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
359
 		return
391
 		return
360
 	}
392
 	}
361
 
393
 
362
-	server.channels.SetUnregistered(channelKey, founder)
394
+	server.channels.SetUnregistered(channelKey, info.Founder)
363
 	csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
395
 	csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
364
 }
396
 }
365
 
397
 
377
 		csNotice(rb, client.t("Channel does not exist"))
409
 		csNotice(rb, client.t("Channel does not exist"))
378
 		return
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
 		return
413
 		return
384
 	}
414
 	}
385
 
415
 
573
 	csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
603
 	csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
574
 	csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
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 Ver arquivo

29
 const (
29
 const (
30
 	// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
30
 	// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
31
 	IdentTimeoutSeconds  = 1.5
31
 	IdentTimeoutSeconds  = 1.5
32
-	IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
32
+	IRCv3TimestampFormat = utils.IRCv3TimestampFormat
33
 )
33
 )
34
 
34
 
35
 // ResumeDetails is a place to stash data at various stages of
35
 // ResumeDetails is a place to stash data at various stages of
45
 type Client struct {
45
 type Client struct {
46
 	account            string
46
 	account            string
47
 	accountName        string // display name of the account: uncasefolded, '*' if not logged in
47
 	accountName        string // display name of the account: uncasefolded, '*' if not logged in
48
+	accountRegDate     time.Time
48
 	accountSettings    AccountSettings
49
 	accountSettings    AccountSettings
49
 	atime              time.Time
50
 	atime              time.Time
50
 	away               bool
51
 	away               bool
55
 	ctime              time.Time
56
 	ctime              time.Time
56
 	destroyed          bool
57
 	destroyed          bool
57
 	exitedSnomaskSent  bool
58
 	exitedSnomaskSent  bool
58
-	flags              modes.ModeSet
59
+	modes              modes.ModeSet
59
 	hostname           string
60
 	hostname           string
60
 	invitedTo          map[string]bool
61
 	invitedTo          map[string]bool
61
 	isSTSOnly          bool
62
 	isSTSOnly          bool
62
-	isTor              bool
63
 	languages          []string
63
 	languages          []string
64
+	lastSignoff        time.Time // for always-on clients, the time their last session quit
64
 	loginThrottle      connection_limits.GenericThrottle
65
 	loginThrottle      connection_limits.GenericThrottle
65
 	nick               string
66
 	nick               string
66
 	nickCasefolded     string
67
 	nickCasefolded     string
84
 	skeleton           string
85
 	skeleton           string
85
 	sessions           []*Session
86
 	sessions           []*Session
86
 	stateMutex         sync.RWMutex // tier 1
87
 	stateMutex         sync.RWMutex // tier 1
88
+	alwaysOn           bool
87
 	username           string
89
 	username           string
88
 	vhost              string
90
 	vhost              string
89
 	history            history.Buffer
91
 	history            history.Buffer
92
+	dirtyBits          uint
93
+	writerSemaphore    utils.Semaphore // tier 1.5
90
 }
94
 }
91
 
95
 
92
 // Session is an individual client connection to the server (TCP connection
96
 // Session is an individual client connection to the server (TCP connection
102
 	realIP      net.IP
106
 	realIP      net.IP
103
 	proxiedIP   net.IP
107
 	proxiedIP   net.IP
104
 	rawHostname string
108
 	rawHostname string
109
+	isTor       bool
105
 
110
 
106
 	idletimer IdleTimer
111
 	idletimer IdleTimer
107
 	fakelag   Fakelag
112
 	fakelag   Fakelag
120
 	resumeID         string
125
 	resumeID         string
121
 	resumeDetails    *ResumeDetails
126
 	resumeDetails    *ResumeDetails
122
 	zncPlaybackTimes *zncPlaybackTimes
127
 	zncPlaybackTimes *zncPlaybackTimes
128
+	lastSignoff      time.Time
123
 
129
 
124
 	batch MultilineBatch
130
 	batch MultilineBatch
125
 }
131
 }
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
 // returns whether the session was actively destroyed (for example, by ping
163
 // returns whether the session was actively destroyed (for example, by ping
151
 // timeout or NS GHOST).
164
 // timeout or NS GHOST).
152
 // avoids a race condition between asynchronous idle-timing-out of sessions,
165
 // avoids a race condition between asynchronous idle-timing-out of sessions,
164
 // returns whether the client supports a smart history replay cap,
177
 // returns whether the client supports a smart history replay cap,
165
 // and therefore autoreplay-on-join and similar should be suppressed
178
 // and therefore autoreplay-on-join and similar should be suppressed
166
 func (session *Session) HasHistoryCaps() bool {
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
 // generates a batch ID. the uniqueness requirements for this are fairly weak:
183
 // generates a batch ID. the uniqueness requirements for this are fairly weak:
231
 		channels:  make(ChannelSet),
243
 		channels:  make(ChannelSet),
232
 		ctime:     now,
244
 		ctime:     now,
233
 		isSTSOnly: conn.Config.STSOnly,
245
 		isSTSOnly: conn.Config.STSOnly,
234
-		isTor:     conn.Config.Tor,
235
 		languages: server.Languages().Default(),
246
 		languages: server.Languages().Default(),
236
 		loginThrottle: connection_limits.GenericThrottle{
247
 		loginThrottle: connection_limits.GenericThrottle{
237
 			Duration: config.Accounts.LoginThrottling.Duration,
248
 			Duration: config.Accounts.LoginThrottling.Duration,
253
 		ctime:      now,
264
 		ctime:      now,
254
 		atime:      now,
265
 		atime:      now,
255
 		realIP:     realIP,
266
 		realIP:     realIP,
267
+		isTor:      conn.Config.Tor,
256
 	}
268
 	}
257
 	client.sessions = []*Session{session}
269
 	client.sessions = []*Session{session}
258
 
270
 
272
 		client.rawHostname = session.rawHostname
284
 		client.rawHostname = session.rawHostname
273
 	} else {
285
 	} else {
274
 		remoteAddr := conn.Conn.RemoteAddr()
286
 		remoteAddr := conn.Conn.RemoteAddr()
275
-		if utils.AddrIsLocal(remoteAddr) {
287
+		if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) {
276
 			// treat local connections as secure (may be overridden later by WEBIRC)
288
 			// treat local connections as secure (may be overridden later by WEBIRC)
277
 			client.SetMode(modes.TLS, true)
289
 			client.SetMode(modes.TLS, true)
278
 		}
290
 		}
286
 	client.run(session, proxyLine)
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
 // resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
356
 // resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
290
 // and sending appropriate notices to the client
357
 // and sending appropriate notices to the client
291
 func (client *Client) lookupHostname(session *Session, overwrite bool) {
358
 func (client *Client) lookupHostname(session *Session, overwrite bool) {
292
-	if client.isTor {
359
+	if session.isTor {
293
 		return
360
 		return
294
 	} // else: even if cloaking is enabled, look up the real hostname to show to operators
361
 	} // else: even if cloaking is enabled, look up the real hostname to show to operators
295
 
362
 
384
 	authFailSaslRequired
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
 	saslSent := client.account != ""
455
 	saslSent := client.account != ""
389
 	// PASS requirement
456
 	// PASS requirement
390
 	if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) {
457
 	if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) {
391
 		return authFailPass
458
 		return authFailPass
392
 	}
459
 	}
393
 	// Tor connections may be required to authenticate with SASL
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
 		return authFailTorSaslRequired
462
 		return authFailTorSaslRequired
396
 	}
463
 	}
397
 	// finally, enforce require-sasl
464
 	// finally, enforce require-sasl
572
 
639
 
573
 func (client *Client) playReattachMessages(session *Session) {
640
 func (client *Client) playReattachMessages(session *Session) {
574
 	client.server.playRegistrationBurst(session)
641
 	client.server.playRegistrationBurst(session)
642
+	hasHistoryCaps := session.HasHistoryCaps()
575
 	for _, channel := range session.client.Channels() {
643
 	for _, channel := range session.client.Channels() {
576
 		channel.playJoinForSession(session)
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
 		// if they negotiated znc.in/playback or chathistory, they will receive nothing,
649
 		// if they negotiated znc.in/playback or chathistory, they will receive nothing,
579
 		// because those caps disable autoreplay-on-join and they haven't sent the relevant
650
 		// because those caps disable autoreplay-on-join and they haven't sent the relevant
580
 		// *playback PRIVMSG or CHATHISTORY command yet
651
 		// *playback PRIVMSG or CHATHISTORY command yet
582
 		channel.autoReplayHistory(client, rb, "")
653
 		channel.autoReplayHistory(client, rb, "")
583
 		rb.Send(true)
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
 		return
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
 	err := server.clients.Resume(oldClient, session)
714
 	err := server.clients.Resume(oldClient, session)
643
 	if err != nil {
715
 	if err != nil {
644
 		session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
716
 		session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
657
 func (session *Session) playResume() {
729
 func (session *Session) playResume() {
658
 	client := session.client
730
 	client := session.client
659
 	server := client.server
731
 	server := client.server
732
+	config := server.Config()
660
 
733
 
661
 	friends := make(ClientSet)
734
 	friends := make(ClientSet)
662
-	oldestLostMessage := time.Now().UTC()
735
+	var oldestLostMessage time.Time
663
 
736
 
664
 	// work out how much time, if any, is not covered by history buffers
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
 	for _, channel := range client.Channels() {
739
 	for _, channel := range client.Channels() {
666
 		for _, member := range channel.Members() {
740
 		for _, member := range channel.Members() {
667
 			friends.Add(member)
741
 			friends.Add(member)
742
+		}
743
+		_, ephemeral, _ := channel.historyStatus(config)
744
+		if ephemeral {
668
 			lastDiscarded := channel.history.LastDiscarded()
745
 			lastDiscarded := channel.history.LastDiscarded()
669
-			if lastDiscarded.Before(oldestLostMessage) {
746
+			if oldestLostMessage.Before(lastDiscarded) {
670
 				oldestLostMessage = lastDiscarded
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
 	timestamp := session.resumeDetails.Timestamp
769
 	timestamp := session.resumeDetails.Timestamp
690
-	gap := lastDiscarded.Sub(timestamp)
770
+	gap := oldestLostMessage.Sub(timestamp)
691
 	session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
771
 	session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
692
 	gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
772
 	gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
693
 
773
 
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
 	session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
814
 	session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
738
 	}
820
 	}
739
 
821
 
740
 	// replay direct PRIVSMG history
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
 		rb.Send(true)
828
 		rb.Send(true)
747
 	}
829
 	}
748
 
830
 
749
 	session.resumeDetails = nil
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
 	var batchID string
835
 	var batchID string
754
 	details := client.Details()
836
 	details := client.Details()
755
 	nick := details.nick
837
 	nick := details.nick
756
 	if 0 < len(items) {
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
 	allowTags := rb.session.capabilities.Has(caps.MessageTags)
845
 	allowTags := rb.session.capabilities.Has(caps.MessageTags)
778
 		if allowTags {
863
 		if allowTags {
779
 			tags = item.Tags
864
 			tags = item.Tags
780
 		}
865
 		}
781
-		if item.Params[0] == "" {
866
+		if item.Params[0] == "" || item.Params[0] == nick {
782
 			// this message was sent *to* the client from another nick
867
 			// this message was sent *to* the client from another nick
783
 			rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
868
 			rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
784
 		} else {
869
 		} else {
785
 			// this message was sent *from* the client to another nick; the target is item.Params[0]
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
 			rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
872
 			rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
788
 		}
873
 		}
789
 	}
874
 	}
875
 
960
 
876
 // ModeString returns the mode string for this client.
961
 // ModeString returns the mode string for this client.
877
 func (client *Client) ModeString() (str string) {
962
 func (client *Client) ModeString() (str string) {
878
-	return "+" + client.flags.String()
963
+	return "+" + client.modes.String()
879
 }
964
 }
880
 
965
 
881
 // Friends refers to clients that share a channel with this client.
966
 // Friends refers to clients that share a channel with this client.
1053
 // has no more sessions.
1138
 // has no more sessions.
1054
 func (client *Client) destroy(session *Session) {
1139
 func (client *Client) destroy(session *Session) {
1055
 	var sessionsToDestroy []*Session
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
 	client.stateMutex.Lock()
1148
 	client.stateMutex.Lock()
1058
 	details := client.detailsNoMutex()
1149
 	details := client.detailsNoMutex()
1060
 	brbAt := client.brbTimer.brbAt
1151
 	brbAt := client.brbTimer.brbAt
1061
 	wasReattach := session != nil && session.client != client
1152
 	wasReattach := session != nil && session.client != client
1062
 	sessionRemoved := false
1153
 	sessionRemoved := false
1154
+	registered := client.registered
1155
+	alwaysOn := client.alwaysOn
1063
 	var remainingSessions int
1156
 	var remainingSessions int
1064
 	if session == nil {
1157
 	if session == nil {
1065
 		sessionsToDestroy = client.sessions
1158
 		sessionsToDestroy = client.sessions
1074
 
1167
 
1075
 	// should we destroy the whole client this time?
1168
 	// should we destroy the whole client this time?
1076
 	// BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
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
 	shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible
1171
 	shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible
1079
 	if shouldDestroy {
1172
 	if shouldDestroy {
1080
 		// if it's our job to destroy it, don't let anyone else try
1173
 		// if it's our job to destroy it, don't let anyone else try
1081
 		client.destroyed = true
1174
 		client.destroyed = true
1082
 	}
1175
 	}
1176
+	if alwaysOn && remainingSessions == 0 {
1177
+		client.lastSignoff = lastSignoff
1178
+	}
1083
 	exitedSnomaskSent := client.exitedSnomaskSent
1179
 	exitedSnomaskSent := client.exitedSnomaskSent
1084
 	client.stateMutex.Unlock()
1180
 	client.stateMutex.Unlock()
1085
 
1181
 
1099
 
1195
 
1100
 		// remove from connection limits
1196
 		// remove from connection limits
1101
 		var source string
1197
 		var source string
1102
-		if client.isTor {
1198
+		if session.isTor {
1103
 			client.server.torLimiter.RemoveClient()
1199
 			client.server.torLimiter.RemoveClient()
1104
 			source = "tor"
1200
 			source = "tor"
1105
 		} else {
1201
 		} else {
1113
 		client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
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
 	// do not destroy the client if it has either remaining sessions, or is BRB'ed
1219
 	// do not destroy the client if it has either remaining sessions, or is BRB'ed
1117
 	if !shouldDestroy {
1220
 	if !shouldDestroy {
1118
 		return
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
 	// see #235: deduplicating the list of PART recipients uses (comparatively speaking)
1239
 	// see #235: deduplicating the list of PART recipients uses (comparatively speaking)
1122
 	// a lot of RAM, so limit concurrency to avoid thrashing
1240
 	// a lot of RAM, so limit concurrency to avoid thrashing
1123
 	client.server.semaphores.ClientDestroy.Acquire()
1241
 	client.server.semaphores.ClientDestroy.Acquire()
1127
 		client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
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
 	if registered {
1248
 	if registered {
1132
 		client.server.whoWas.Append(client.WhoWas())
1249
 		client.server.whoWas.Append(client.WhoWas())
1133
 	}
1250
 	}
1141
 	// clean up monitor state
1258
 	// clean up monitor state
1142
 	client.server.monitorManager.RemoveAll(client)
1259
 	client.server.monitorManager.RemoveAll(client)
1143
 
1260
 
1144
-	splitQuitMessage := utils.MakeMessage(quitMessage)
1145
 	// clean up channels
1261
 	// clean up channels
1146
 	// (note that if this is a reattach, client has no channels and therefore no friends)
1262
 	// (note that if this is a reattach, client has no channels and therefore no friends)
1147
 	friends := make(ClientSet)
1263
 	friends := make(ClientSet)
1148
-	for _, channel := range client.Channels() {
1264
+	channels = client.Channels()
1265
+	for _, channel := range channels {
1149
 		channel.Quit(client)
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
 		for _, member := range channel.Members() {
1267
 		for _, member := range channel.Members() {
1157
 			friends.Add(member)
1268
 			friends.Add(member)
1158
 		}
1269
 		}
1168
 
1279
 
1169
 	client.server.accounts.Logout(client)
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
 	// this happens under failure to return from BRB
1282
 	// this happens under failure to return from BRB
1175
 	if quitMessage == "" {
1283
 	if quitMessage == "" {
1176
 		if brbState == BrbDead && !brbAt.IsZero() {
1284
 		if brbState == BrbDead && !brbAt.IsZero() {
1196
 // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
1304
 // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
1197
 // Adds account-tag to the line as well.
1305
 // Adds account-tag to the line as well.
1198
 func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
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
 	if message.Is512() {
1307
 	if message.Is512() {
1201
 		session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
1308
 		session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
1202
 	} else {
1309
 	} else {
1203
-		if message.IsMultiline() && session.capabilities.Has(caps.Multiline) {
1310
+		if session.capabilities.Has(caps.Multiline) {
1204
 			for _, msg := range session.composeMultilineBatch(nickmask, accountName, tags, command, target, message) {
1311
 			for _, msg := range session.composeMultilineBatch(nickmask, accountName, tags, command, target, message) {
1205
 				session.SendRawMessage(msg, blocking)
1312
 				session.SendRawMessage(msg, blocking)
1206
 			}
1313
 			}
1366
 func (client *Client) addChannel(channel *Channel) {
1473
 func (client *Client) addChannel(channel *Channel) {
1367
 	client.stateMutex.Lock()
1474
 	client.stateMutex.Lock()
1368
 	client.channels[channel] = true
1475
 	client.channels[channel] = true
1476
+	alwaysOn := client.alwaysOn
1369
 	client.stateMutex.Unlock()
1477
 	client.stateMutex.Unlock()
1478
+
1479
+	if alwaysOn {
1480
+		client.markDirty(IncludeChannels)
1481
+	}
1370
 }
1482
 }
1371
 
1483
 
1372
 func (client *Client) removeChannel(channel *Channel) {
1484
 func (client *Client) removeChannel(channel *Channel) {
1373
 	client.stateMutex.Lock()
1485
 	client.stateMutex.Lock()
1374
 	delete(client.channels, channel)
1486
 	delete(client.channels, channel)
1487
+	alwaysOn := client.alwaysOn
1375
 	client.stateMutex.Unlock()
1488
 	client.stateMutex.Unlock()
1489
+
1490
+	if alwaysOn {
1491
+		client.markDirty(IncludeChannels)
1492
+	}
1376
 }
1493
 }
1377
 
1494
 
1378
 // Records that the client has been invited to join an invite-only channel
1495
 // Records that the client has been invited to join an invite-only channel
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 Ver arquivo

105
 		return errNickMissing
105
 		return errNickMissing
106
 	}
106
 	}
107
 
107
 
108
-	if !oldClient.AddSession(session) {
108
+	success, _, _ := oldClient.AddSession(session)
109
+	if !success {
109
 		return errNickMissing
110
 		return errNickMissing
110
 	}
111
 	}
111
 
112
 
113
 }
114
 }
114
 
115
 
115
 // SetNick sets a client's nickname, validating it against nicknames in use
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
 	newcfnick, err := CasefoldName(newNick)
118
 	newcfnick, err := CasefoldName(newNick)
121
 	if err != nil {
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
 	newSkeleton, err := Skeleton(newNick)
125
 	newSkeleton, err := Skeleton(newNick)
125
 	if err != nil {
126
 	if err != nil {
126
-		return errNicknameInvalid
127
+		return "", errNicknameInvalid
127
 	}
128
 	}
128
 
129
 
129
 	if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
130
 	if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
130
-		return errNicknameInvalid
131
+		return "", errNicknameInvalid
131
 	}
132
 	}
132
 
133
 
133
 	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
134
 	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
134
-	account := client.Account()
135
 	config := client.server.Config()
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
 	var bouncerAllowed bool
151
 	var bouncerAllowed bool
137
 	if config.Accounts.Bouncer.Enabled {
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
 			bouncerAllowed = true
159
 			bouncerAllowed = true
140
 		} else {
160
 		} else {
141
-			settings := client.AccountSettings()
142
 			if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
161
 			if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
143
 				bouncerAllowed = true
162
 				bouncerAllowed = true
144
 			} else if settings.AllowBouncer == BouncerAllowedByUser {
163
 			} else if settings.AllowBouncer == BouncerAllowedByUser {
154
 	// the client may just be changing case
173
 	// the client may just be changing case
155
 	if currentClient != nil && currentClient != client && session != nil {
174
 	if currentClient != nil && currentClient != client && session != nil {
156
 		// these conditions forbid reattaching to an existing session:
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
 		// successful reattach!
195
 		// successful reattach!
164
-		return nil
196
+		return newNick, nil
165
 	}
197
 	}
166
 	// analogous checks for skeletons
198
 	// analogous checks for skeletons
167
 	skeletonHolder := clients.bySkeleton[newSkeleton]
199
 	skeletonHolder := clients.bySkeleton[newSkeleton]
168
 	if skeletonHolder != nil && skeletonHolder != client {
200
 	if skeletonHolder != nil && skeletonHolder != client {
169
-		return errNicknameInUse
201
+		return "", errNicknameInUse
170
 	}
202
 	}
171
 	if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
203
 	if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
172
-		return errNicknameReserved
204
+		return "", errNicknameReserved
173
 	}
205
 	}
174
 	clients.removeInternal(client)
206
 	clients.removeInternal(client)
175
 	clients.byNick[newcfnick] = client
207
 	clients.byNick[newcfnick] = client
176
 	clients.bySkeleton[newSkeleton] = client
208
 	clients.bySkeleton[newSkeleton] = client
177
 	client.updateNick(newNick, newcfnick, newSkeleton)
209
 	client.updateNick(newNick, newcfnick, newSkeleton)
178
-	return nil
210
+	return newNick, nil
179
 }
211
 }
180
 
212
 
181
 func (clients *ClientManager) AllClients() (result []*Client) {
213
 func (clients *ClientManager) AllClients() (result []*Client) {

+ 7
- 6
irc/commands.go Ver arquivo

54
 		return cmd.handler(server, client, msg, rb)
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
 	if exiting {
63
 	if exiting {
58
 		return
64
 		return
59
 	}
65
 	}
63
 		exiting = server.tryRegister(client, session)
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
 	if client.registered && !cmd.leaveClientIdle {
72
 	if client.registered && !cmd.leaveClientIdle {
72
 		client.Active(session)
73
 		client.Active(session)
73
 	}
74
 	}
109
 		},
110
 		},
110
 		"CHATHISTORY": {
111
 		"CHATHISTORY": {
111
 			handler:   chathistoryHandler,
112
 			handler:   chathistoryHandler,
112
-			minParams: 3,
113
+			minParams: 4,
113
 		},
114
 		},
114
 		"DEBUG": {
115
 		"DEBUG": {
115
 			handler:   debugHandler,
116
 			handler:   debugHandler,

+ 187
- 1
irc/config.go Ver arquivo

61
 	ProxyBeforeTLS bool
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
 type AccountConfig struct {
209
 type AccountConfig struct {
65
 	Registration          AccountRegistrationConfig
210
 	Registration          AccountRegistrationConfig
66
 	AuthenticationEnabled bool `yaml:"authentication-enabled"`
211
 	AuthenticationEnabled bool `yaml:"authentication-enabled"`
79
 	NickReservation    NickReservationConfig `yaml:"nick-reservation"`
224
 	NickReservation    NickReservationConfig `yaml:"nick-reservation"`
80
 	Bouncer            struct {
225
 	Bouncer            struct {
81
 		Enabled          bool
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
 	VHosts VHostConfig
230
 	VHosts VHostConfig
85
 }
231
 }
340
 		isupport      isupport.List
486
 		isupport      isupport.List
341
 		IPLimits      connection_limits.LimiterConfig `yaml:"ip-limits"`
487
 		IPLimits      connection_limits.LimiterConfig `yaml:"ip-limits"`
342
 		Cloaks        cloaks.CloakConfig              `yaml:"ip-cloaking"`
488
 		Cloaks        cloaks.CloakConfig              `yaml:"ip-cloaking"`
489
+		SecureNetDefs []string                        `yaml:"secure-nets"`
490
+		secureNets    []net.IPNet
343
 		supportedCaps *caps.Set
491
 		supportedCaps *caps.Set
344
 		capValues     caps.Values
492
 		capValues     caps.Values
345
 		Casemapping   Casemapping
493
 		Casemapping   Casemapping
356
 	Datastore struct {
504
 	Datastore struct {
357
 		Path        string
505
 		Path        string
358
 		AutoUpgrade bool
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
 	Accounts AccountConfig
517
 	Accounts AccountConfig
395
 		AutoresizeWindow time.Duration `yaml:"autoresize-window"`
551
 		AutoresizeWindow time.Duration `yaml:"autoresize-window"`
396
 		AutoreplayOnJoin int           `yaml:"autoreplay-on-join"`
552
 		AutoreplayOnJoin int           `yaml:"autoreplay-on-join"`
397
 		ChathistoryMax   int           `yaml:"chathistory-maxmessages"`
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
 	Filename string
568
 	Filename string
717
 	}
885
 	}
718
 
886
 
719
 	if !config.Accounts.Bouncer.Enabled {
887
 	if !config.Accounts.Bouncer.Enabled {
888
+		config.Accounts.Bouncer.AlwaysOn = PersistentDisabled
720
 		config.Server.supportedCaps.Disable(caps.Bouncer)
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
 	var newLogConfigs []logger.LoggingConfig
894
 	var newLogConfigs []logger.LoggingConfig
786
 		return nil, fmt.Errorf("Could not parse proxy-allowed-from nets: %v", err.Error())
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
 	rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
965
 	rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
790
 	if rawRegexp != "" {
966
 	if rawRegexp != "" {
791
 		regexp, err := regexp.Compile(rawRegexp)
967
 		regexp, err := regexp.Compile(rawRegexp)
882
 		config.History.ClientLength = 0
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
 	config.Server.Cloaks.Initialize()
1071
 	config.Server.Cloaks.Initialize()
886
 	if config.Server.Cloaks.Enabled {
1072
 	if config.Server.Cloaks.Enabled {
887
 		if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
1073
 		if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {

+ 2
- 18
irc/database.go Ver arquivo

36
 // maps an initial version to a schema change capable of upgrading it
36
 // maps an initial version to a schema change capable of upgrading it
37
 var schemaChanges map[string]SchemaChange
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
 // InitDB creates the database, implementing the `oragono initdb` command.
39
 // InitDB creates the database, implementing the `oragono initdb` command.
56
 func InitDB(path string) {
40
 func InitDB(path string) {
57
 	_, err := os.Stat(path)
41
 	_, err := os.Stat(path)
129
 		// successful autoupgrade, let's try this again:
113
 		// successful autoupgrade, let's try this again:
130
 		return openDatabaseInternal(config, false)
114
 		return openDatabaseInternal(config, false)
131
 	} else {
115
 	} else {
132
-		err = IncompatibleSchemaError(version)
116
+		err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
133
 		return
117
 		return
134
 	}
118
 	}
135
 }
119
 }
173
 					break
157
 					break
174
 				}
158
 				}
175
 				// unable to upgrade to the desired version, roll back
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
 			log.Println("attempting to update schema from version " + version)
162
 			log.Println("attempting to update schema from version " + version)
179
 			err := change.Changer(config, tx)
163
 			err := change.Changer(config, tx)

+ 2
- 0
irc/errors.go Ver arquivo

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

+ 1
- 1
irc/gateways.go Ver arquivo

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

+ 71
- 16
irc/getters.go Ver arquivo

91
 	return
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
 	client.stateMutex.Lock()
95
 	client.stateMutex.Lock()
96
 	defer client.stateMutex.Unlock()
96
 	defer client.stateMutex.Unlock()
97
 
97
 
98
 	// client may be dying and ineligible to receive another session
98
 	// client may be dying and ineligible to receive another session
99
 	if client.destroyed {
99
 	if client.destroyed {
100
-		return false
100
+		return
101
 	}
101
 	}
102
 	// success, attach the new session to the client
102
 	// success, attach the new session to the client
103
 	session.client = client
103
 	session.client = client
104
 	newSessions := make([]*Session, len(client.sessions)+1)
104
 	newSessions := make([]*Session, len(client.sessions)+1)
105
 	copy(newSessions, client.sessions)
105
 	copy(newSessions, client.sessions)
106
 	newSessions[len(newSessions)-1] = session
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
 	client.sessions = newSessions
113
 	client.sessions = newSessions
108
-	return true
114
+	return true, len(client.sessions), lastSignoff
109
 }
115
 }
110
 
116
 
111
 func (client *Client) removeSession(session *Session) (success bool, length int) {
117
 func (client *Client) removeSession(session *Session) (success bool, length int) {
189
 	client.stateMutex.Unlock()
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
 // uniqueIdentifiers returns the strings for which the server enforces per-client
205
 // uniqueIdentifiers returns the strings for which the server enforces per-client
193
 // uniqueness/ownership; no two clients can have colliding casefolded nicks or
206
 // uniqueness/ownership; no two clients can have colliding casefolded nicks or
194
 // skeletons.
207
 // skeletons.
264
 	return client.accountName
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
 	client.stateMutex.Lock()
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
 	return
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
 func (client *Client) AccountSettings() (result AccountSettings) {
315
 func (client *Client) AccountSettings() (result AccountSettings) {
285
 	client.stateMutex.RLock()
316
 	client.stateMutex.RLock()
286
 	result = client.accountSettings
317
 	result = client.accountSettings
289
 }
320
 }
290
 
321
 
291
 func (client *Client) SetAccountSettings(settings AccountSettings) {
322
 func (client *Client) SetAccountSettings(settings AccountSettings) {
323
+	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Bouncer.AlwaysOn, settings.AlwaysOn)
292
 	client.stateMutex.Lock()
324
 	client.stateMutex.Lock()
293
 	client.accountSettings = settings
325
 	client.accountSettings = settings
326
+	if client.registered {
327
+		client.alwaysOn = alwaysOn
328
+	}
294
 	client.stateMutex.Unlock()
329
 	client.stateMutex.Unlock()
295
 }
330
 }
296
 
331
 
309
 
344
 
310
 func (client *Client) HasMode(mode modes.Mode) bool {
345
 func (client *Client) HasMode(mode modes.Mode) bool {
311
 	// client.flags has its own synch
346
 	// client.flags has its own synch
312
-	return client.flags.HasMode(mode)
347
+	return client.modes.HasMode(mode)
313
 }
348
 }
314
 
349
 
315
 func (client *Client) SetMode(mode modes.Mode, on bool) bool {
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
 func (client *Client) Channels() (result []*Channel) {
360
 func (client *Client) Channels() (result []*Channel) {
410
 	channel.stateMutex.RUnlock()
451
 	channel.stateMutex.RUnlock()
411
 	return clientModes.HighestChannelUserMode()
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 Ver arquivo

531
 // CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
531
 // CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
532
 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
532
 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
533
 func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
533
 func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
534
-	config := server.Config()
535
-
536
 	var items []history.Item
534
 	var items []history.Item
537
-	success := false
538
-	var hist *history.Buffer
535
+	unknown_command := false
536
+	var target string
539
 	var channel *Channel
537
 	var channel *Channel
538
+	var sequence history.Sequence
539
+	var err error
540
 	defer func() {
540
 	defer func() {
541
 		// successful responses are sent as a chathistory or history batch
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
 				channel.replayHistoryItems(rb, items, false)
544
 				channel.replayHistoryItems(rb, items, false)
545
+			} else {
546
+				client.replayPrivmsgHistory(rb, items, target, true)
547
 			}
547
 			}
548
 			return
548
 			return
549
 		}
549
 		}
550
 
550
 
551
 		// errors are sent either without a batch, or in a draft/labeled-response batch as usual
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
 		return
566
 		return
580
 	}
567
 	}
581
 
568
 
582
-	preposition := strings.ToLower(msg.Params[1])
583
-
584
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
569
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
585
-		err = errInvalidParams
570
+		err = utils.ErrInvalidParams
586
 		pieces := strings.SplitN(param, "=", 2)
571
 		pieces := strings.SplitN(param, "=", 2)
587
 		if len(pieces) < 2 {
572
 		if len(pieces) < 2 {
588
 			return
573
 			return
589
 		}
574
 		}
590
 		identifier, value := strings.ToLower(pieces[0]), pieces[1]
575
 		identifier, value := strings.ToLower(pieces[0]), pieces[1]
591
-		if identifier == "id" {
576
+		if identifier == "msgid" {
592
 			msgid, err = value, nil
577
 			msgid, err = value, nil
593
 			return
578
 			return
594
 		} else if identifier == "timestamp" {
579
 		} else if identifier == "timestamp" {
598
 		return
583
 		return
599
 	}
584
 	}
600
 
585
 
601
-	maxChathistoryLimit := config.History.ChathistoryMax
602
-	if maxChathistoryLimit == 0 {
603
-		return
604
-	}
605
 	parseHistoryLimit := func(paramIndex int) (limit int) {
586
 	parseHistoryLimit := func(paramIndex int) (limit int) {
606
 		if len(msg.Params) < (paramIndex + 1) {
587
 		if len(msg.Params) < (paramIndex + 1) {
607
 			return maxChathistoryLimit
588
 			return maxChathistoryLimit
613
 		return
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
 		return
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
 	switch preposition {
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
 		if err != nil {
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
 			} else {
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
 		if err != nil {
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
 	return
665
 	return
751
 }
666
 }
752
 
667
 
1006
 // HISTORY <target> [<limit>]
921
 // HISTORY <target> [<limit>]
1007
 // e.g., HISTORY #ubuntu 10
922
 // e.g., HISTORY #ubuntu 10
1008
 // HISTORY me 15
923
 // HISTORY me 15
924
+// HISTORY #darwin 1h
1009
 func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
925
 func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
1010
 	config := server.Config()
926
 	config := server.Config()
1011
 	if !config.History.Enabled {
927
 	if !config.History.Enabled {
1014
 	}
930
 	}
1015
 
931
 
1016
 	target := msg.Params[0]
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
 		return false
941
 		return false
1042
 	}
942
 	}
1043
 
943
 
1044
-	limit := 10
944
+	var duration time.Duration
1045
 	maxChathistoryLimit := config.History.ChathistoryMax
945
 	maxChathistoryLimit := config.History.ChathistoryMax
946
+	limit := 100
947
+	if maxChathistoryLimit < limit {
948
+		limit = maxChathistoryLimit
949
+	}
1046
 	if len(msg.Params) > 1 {
950
 	if len(msg.Params) > 1 {
1047
 		providedLimit, err := strconv.Atoi(msg.Params[1])
951
 		providedLimit, err := strconv.Atoi(msg.Params[1])
1048
-		if providedLimit > maxChathistoryLimit {
1049
-			providedLimit = maxChathistoryLimit
1050
-		}
1051
 		if err == nil && providedLimit != 0 {
952
 		if err == nil && providedLimit != 0 {
1052
 			limit = providedLimit
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
 	} else {
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
 	return false
982
 	return false
1065
 }
983
 }
1066
 
984
 
1944
 		return false
1862
 		return false
1945
 	}
1863
 	}
1946
 
1864
 
1947
-	if client.isTor && utils.IsRestrictedCTCPMessage(message) {
1865
+	if rb.session.isTor && utils.IsRestrictedCTCPMessage(message) {
1948
 		// note that error replies are never sent for NOTICE
1866
 		// note that error replies are never sent for NOTICE
1949
 		if histType != history.Notice {
1867
 		if histType != history.Notice {
1950
 			rb.Notice(client.t("CTCP messages are disabled over Tor"))
1868
 			rb.Notice(client.t("CTCP messages are disabled over Tor"))
2001
 			}
1919
 			}
2002
 			return
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
 		// restrict messages appropriately when +R is set
1928
 		// restrict messages appropriately when +R is set
2009
 		// intentionally make the sending user think the message went through fine
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
 			for _, session := range user.Sessions() {
1932
 			for _, session := range user.Sessions() {
2014
 				hasTagsCap := session.capabilities.Has(caps.MessageTags)
1933
 				hasTagsCap := session.capabilities.Has(caps.MessageTags)
2015
 				// don't send TAGMSG at all if they don't have the tags cap
1934
 				// don't send TAGMSG at all if they don't have the tags cap
2016
 				if histType == history.Tagmsg && hasTagsCap {
1935
 				if histType == history.Tagmsg && hasTagsCap {
2017
 					session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
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
 					tagsToSend := tags
1938
 					tagsToSend := tags
2020
 					if !hasTagsCap {
1939
 					if !hasTagsCap {
2021
 						tagsToSend = nil
1940
 						tagsToSend = nil
2053
 			rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, user.AwayMessage())
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
 		item := history.Item{
1979
 		item := history.Item{
2057
 			Type:        histType,
1980
 			Type:        histType,
2058
 			Message:     message,
1981
 			Message:     message,
2059
 			Nick:        nickMaskString,
1982
 			Nick:        nickMaskString,
2060
 			AccountName: accountName,
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
 // SETNAME <realname>
2314
 // SETNAME <realname>
2376
 func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2315
 func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2377
 	realname := msg.Params[0]
2316
 	realname := msg.Params[0]
2378
-
2379
-	client.stateMutex.Lock()
2380
-	client.realname = realname
2381
-	client.stateMutex.Unlock()
2382
-
2317
+	client.SetRealname(realname)
2383
 	details := client.Details()
2318
 	details := client.Details()
2384
 
2319
 
2385
 	// alert friends
2320
 	// alert friends

+ 3
- 2
irc/help.go Ver arquivo

206
 
206
 
207
 Replay message history. <target> can be a channel name, "me" to replay direct
207
 Replay message history. <target> can be a channel name, "me" to replay direct
208
 message history, or a nickname to replay another client's direct message
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
 	"info": {
213
 	"info": {
213
 		text: `INFO
214
 		text: `INFO

+ 97
- 71
irc/history/history.go Ver arquivo

6
 import (
6
 import (
7
 	"github.com/oragono/oragono/irc/utils"
7
 	"github.com/oragono/oragono/irc/utils"
8
 	"sync"
8
 	"sync"
9
-	"sync/atomic"
10
 	"time"
9
 	"time"
11
 )
10
 )
12
 
11
 
43
 	// this is the uncasefolded account name, if there's no account it should be set to "*"
42
 	// this is the uncasefolded account name, if there's no account it should be set to "*"
44
 	AccountName string
43
 	AccountName string
45
 	// for non-privmsg items, we may stuff some other data in here
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
 // HasMsgid tests whether a message has the message id `msgid`.
51
 // HasMsgid tests whether a message has the message id `msgid`.
53
 	return item.Message.Msgid == msgid
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
 		for name := range item.Tags {
59
 		for name := range item.Tags {
59
 			if !transientTags[name] {
60
 			if !transientTags[name] {
60
 				return true
61
 				return true
61
 			}
62
 			}
62
 		}
63
 		}
63
 		return false // all tags were blacklisted
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
 		return true
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
 // Buffer is a ring buffer holding message/event history for a channel or user
81
 // Buffer is a ring buffer holding message/event history for a channel or user
72
 type Buffer struct {
82
 type Buffer struct {
81
 
91
 
82
 	lastDiscarded time.Time
92
 	lastDiscarded time.Time
83
 
93
 
84
-	enabled uint32
85
-
86
 	nowFunc func() time.Time
94
 	nowFunc func() time.Time
87
 }
95
 }
88
 
96
 
99
 	hist.window = window
107
 	hist.window = window
100
 	hist.maximumSize = size
108
 	hist.maximumSize = size
101
 	hist.nowFunc = time.Now
109
 	hist.nowFunc = time.Now
102
-
103
-	hist.setEnabled(size)
104
 }
110
 }
105
 
111
 
106
 // compute the initial size for the buffer, taking into account autoresize
112
 // compute the initial size for the buffer, taking into account autoresize
115
 	return
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
 // Add adds a history item to the buffer
124
 // Add adds a history item to the buffer
133
 func (list *Buffer) Add(item Item) {
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
 	if item.Message.Time.IsZero() {
126
 	if item.Message.Time.IsZero() {
144
 		item.Message.Time = time.Now().UTC()
127
 		item.Message.Time = time.Now().UTC()
145
 	}
128
 	}
147
 	list.Lock()
130
 	list.Lock()
148
 	defer list.Unlock()
131
 	defer list.Unlock()
149
 
132
 
133
+	if len(list.buffer) == 0 {
134
+		return
135
+	}
136
+
150
 	list.maybeExpand()
137
 	list.maybeExpand()
151
 
138
 
152
 	var pos int
139
 	var pos int
170
 	list.buffer[pos] = item
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
 // Between returns all history items with a time `after` <= time <= `before`,
171
 // Between returns all history items with a time `after` <= time <= `before`,
181
 // with an indication of whether the results are complete or are missing items
172
 // with an indication of whether the results are complete or are missing items
182
 // because some of that period was discarded. A zero value of `before` is considered
173
 // because some of that period was discarded. A zero value of `before` is considered
183
 // higher than all other times.
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
 	list.RLock()
184
 	list.RLock()
190
 	defer list.RUnlock()
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
 	complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
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
 // you must be holding the read lock to call this
251
 // you must be holding the read lock to call this
220
 func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
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
 		return
254
 		return
223
 	}
255
 	}
224
 
256
 
232
 	}
264
 	}
233
 
265
 
234
 	for {
266
 	for {
235
-		if predicate(list.buffer[pos]) {
267
+		if predicate(&list.buffer[pos]) {
236
 			results = append(results, list.buffer[pos])
268
 			results = append(results, list.buffer[pos])
237
 		}
269
 		}
238
 		if pos == stop || (limit != 0 && len(results) == limit) {
270
 		if pos == stop || (limit != 0 && len(results) == limit) {
245
 		}
277
 		}
246
 	}
278
 	}
247
 
279
 
248
-	// TODO sort by time instead?
249
-	if !ascending {
250
-		Reverse(results)
251
-	}
252
 	return
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
 // it returns all items.
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
 // LastDiscarded returns the latest time of any entry that was evicted
290
 // LastDiscarded returns the latest time of any entry that was evicted
355
 func (list *Buffer) resize(size int) {
383
 func (list *Buffer) resize(size int) {
356
 	newbuffer := make([]Item, size)
384
 	newbuffer := make([]Item, size)
357
 
385
 
358
-	list.setEnabled(size)
359
-
360
 	if list.start == -1 {
386
 	if list.start == -1 {
361
 		// indices are already correct and nothing needs to be copied
387
 		// indices are already correct and nothing needs to be copied
362
 	} else if size == 0 {
388
 	} else if size == 0 {

+ 46
- 25
irc/history/history_test.go Ver arquivo

14
 	timeFormat = "2006-01-02 15:04:05Z"
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
 func TestEmptyBuffer(t *testing.T) {
22
 func TestEmptyBuffer(t *testing.T) {
18
 	pastTime := easyParse(timeFormat)
23
 	pastTime := easyParse(timeFormat)
19
 
24
 
20
 	buf := NewHistoryBuffer(0, 0)
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
 	buf.Add(Item{
27
 	buf.Add(Item{
26
 		Nick: "testnick",
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
 	if len(since) != 0 {
32
 	if len(since) != 0 {
31
 		t.Error("shouldn't be able to add to disabled buf")
33
 		t.Error("shouldn't be able to add to disabled buf")
32
 	}
34
 	}
35
 	}
37
 	}
36
 
38
 
37
 	buf.Resize(1, 0)
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
 	assertEqual(complete, true, t)
41
 	assertEqual(complete, true, t)
43
 	assertEqual(len(since), 0, t)
42
 	assertEqual(len(since), 0, t)
44
 	buf.Add(Item{
43
 	buf.Add(Item{
45
 		Nick: "testnick",
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
 	if len(since) != 1 {
47
 	if len(since) != 1 {
49
 		t.Error("should be able to store items in a nonempty buffer")
48
 		t.Error("should be able to store items in a nonempty buffer")
50
 	}
49
 	}
58
 	buf.Add(Item{
57
 	buf.Add(Item{
59
 		Nick: "testnick2",
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
 	if len(since) != 1 {
61
 	if len(since) != 1 {
63
 		t.Error("expect exactly 1 item")
62
 		t.Error("expect exactly 1 item")
64
 	}
63
 	}
68
 	if since[0].Nick != "testnick2" {
67
 	if since[0].Nick != "testnick2" {
69
 		t.Error("retrieved junk data")
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
 func toNicks(items []Item) (result []string) {
73
 func toNicks(items []Item) (result []string) {
110
 
108
 
111
 	buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z"))
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
 	assertEqual(complete, true, t)
112
 	assertEqual(complete, true, t)
115
 	assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
113
 	assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
116
 
114
 
117
 	// add another item, evicting the first
115
 	// add another item, evicting the first
118
 	buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z"))
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
 	assertEqual(complete, false, t)
119
 	assertEqual(complete, false, t)
122
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
120
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
123
 	// now exclude the time of the discarded entry; results should be complete again
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
 	assertEqual(complete, true, t)
123
 	assertEqual(complete, true, t)
126
 	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
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
 	assertEqual(complete, true, t)
126
 	assertEqual(complete, true, t)
129
 	assertEqual(toNicks(since), []string{"testnick1"}, t)
127
 	assertEqual(toNicks(since), []string{"testnick1"}, t)
130
 
128
 
131
 	// shrink the buffer, cutting off testnick1
129
 	// shrink the buffer, cutting off testnick1
132
 	buf.Resize(2, 0)
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
 	assertEqual(complete, false, t)
132
 	assertEqual(complete, false, t)
135
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
133
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
136
 
134
 
138
 	buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
136
 	buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
139
 	buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
137
 	buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
140
 	buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
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
 	assertEqual(complete, true, t)
140
 	assertEqual(complete, true, t)
143
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
141
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
144
 
142
 
145
 	// test ascending order
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
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
145
 	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
148
 }
146
 }
149
 
147
 
150
 func autoItem(id int, t time.Time) (result Item) {
148
 func autoItem(id int, t time.Time) (result Item) {
151
 	result.Message.Time = t
149
 	result.Message.Time = t
152
 	result.Nick = strconv.Itoa(id)
150
 	result.Nick = strconv.Itoa(id)
151
+	result.Message.Msgid = result.Nick
153
 	return
152
 	return
154
 }
153
 }
155
 
154
 
181
 		now = now.Add(time.Minute * 10)
180
 		now = now.Add(time.Minute * 10)
182
 		id += 1
181
 		id += 1
183
 	}
182
 	}
184
-	items := buf.Latest(0)
183
+	items := buf.latest(0)
185
 	assertEqual(len(items), initialAutoSize, t)
184
 	assertEqual(len(items), initialAutoSize, t)
186
 	assertEqual(atoi(items[0].Nick), 40, t)
185
 	assertEqual(atoi(items[0].Nick), 40, t)
187
 	assertEqual(atoi(items[len(items)-1].Nick), 71, t)
186
 	assertEqual(atoi(items[len(items)-1].Nick), 71, t)
195
 	// ok, 5 items from the first batch are still in the 1-hour window;
194
 	// ok, 5 items from the first batch are still in the 1-hour window;
196
 	// we should overwrite until only those 5 are left, then start expanding
195
 	// we should overwrite until only those 5 are left, then start expanding
197
 	// the buffer so that it retains those 5 and the 100 new items
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
 	assertEqual(len(items), 105, t)
198
 	assertEqual(len(items), 105, t)
200
 	assertEqual(atoi(items[0].Nick), 67, t)
199
 	assertEqual(atoi(items[0].Nick), 67, t)
201
 	assertEqual(atoi(items[len(items)-1].Nick), 171, t)
200
 	assertEqual(atoi(items[len(items)-1].Nick), 171, t)
207
 		id += 1
206
 		id += 1
208
 	}
207
 	}
209
 	// should fill up to the maximum size of 128 and start overwriting
208
 	// should fill up to the maximum size of 128 and start overwriting
210
-	items = buf.Latest(0)
209
+	items = buf.latest(0)
211
 	assertEqual(len(items), 128, t)
210
 	assertEqual(len(items), 128, t)
212
 	assertEqual(atoi(items[0].Nick), 144, t)
211
 	assertEqual(atoi(items[0].Nick), 144, t)
213
 	assertEqual(atoi(items[len(items)-1].Nick), 271, t)
212
 	assertEqual(atoi(items[len(items)-1].Nick), 271, t)
222
 	buf.Resize(128, time.Hour)
221
 	buf.Resize(128, time.Hour)
223
 	// add an item and test that it is stored and retrievable
222
 	// add an item and test that it is stored and retrievable
224
 	buf.Add(autoItem(0, now))
223
 	buf.Add(autoItem(0, now))
225
-	items := buf.Latest(0)
224
+	items := buf.latest(0)
226
 	assertEqual(len(items), 1, t)
225
 	assertEqual(len(items), 1, t)
227
 	assertEqual(atoi(items[0].Nick), 0, t)
226
 	assertEqual(atoi(items[0].Nick), 0, t)
228
 }
227
 }
232
 	// enabled autoresizing buffer
231
 	// enabled autoresizing buffer
233
 	buf := NewHistoryBuffer(128, time.Hour)
232
 	buf := NewHistoryBuffer(128, time.Hour)
234
 	buf.Add(autoItem(0, now))
233
 	buf.Add(autoItem(0, now))
235
-	items := buf.Latest(0)
234
+	items := buf.latest(0)
236
 	assertEqual(len(items), 1, t)
235
 	assertEqual(len(items), 1, t)
237
 	assertEqual(atoi(items[0].Nick), 0, t)
236
 	assertEqual(atoi(items[0].Nick), 0, t)
238
 
237
 
239
 	// disable as during a rehash, confirm that nothing can be retrieved
238
 	// disable as during a rehash, confirm that nothing can be retrieved
240
 	buf.Resize(0, time.Hour)
239
 	buf.Resize(0, time.Hour)
241
-	items = buf.Latest(0)
240
+	items = buf.latest(0)
242
 	assertEqual(len(items), 0, t)
241
 	assertEqual(len(items), 0, t)
243
 }
242
 }
244
 
243
 
252
 	assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
251
 	assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
253
 	assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
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 Ver arquivo

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 Ver arquivo

52
 	quitTimeout time.Duration
52
 	quitTimeout time.Duration
53
 	state       TimerState
53
 	state       TimerState
54
 	timer       *time.Timer
54
 	timer       *time.Timer
55
+	lastTouch   time.Time
55
 }
56
 }
56
 
57
 
57
 // Initialize sets up an IdleTimer and starts counting idle time;
58
 // Initialize sets up an IdleTimer and starts counting idle time;
61
 	it.registerTimeout = RegisterTimeout
62
 	it.registerTimeout = RegisterTimeout
62
 	it.idleTimeout, it.quitTimeout = it.recomputeDurations()
63
 	it.idleTimeout, it.quitTimeout = it.recomputeDurations()
63
 	registered := session.client.Registered()
64
 	registered := session.client.Registered()
65
+	now := time.Now().UTC()
64
 
66
 
65
 	it.Lock()
67
 	it.Lock()
66
 	defer it.Unlock()
68
 	defer it.Unlock()
69
+	it.lastTouch = now
67
 	if registered {
70
 	if registered {
68
 		it.state = TimerActive
71
 		it.state = TimerActive
69
 	} else {
72
 	} else {
82
 	}
85
 	}
83
 
86
 
84
 	idleTimeout = DefaultIdleTimeout
87
 	idleTimeout = DefaultIdleTimeout
85
-	if it.session.client.isTor {
88
+	if it.session.isTor {
86
 		idleTimeout = TorIdleTimeout
89
 		idleTimeout = TorIdleTimeout
87
 	}
90
 	}
88
 
91
 
92
 
95
 
93
 func (it *IdleTimer) Touch() {
96
 func (it *IdleTimer) Touch() {
94
 	idleTimeout, quitTimeout := it.recomputeDurations()
97
 	idleTimeout, quitTimeout := it.recomputeDurations()
98
+	now := time.Now().UTC()
95
 
99
 
96
 	it.Lock()
100
 	it.Lock()
97
 	defer it.Unlock()
101
 	defer it.Unlock()
98
 	it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
102
 	it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
103
+	it.lastTouch = now
99
 	// a touch transitions TimerUnregistered or TimerIdle into TimerActive
104
 	// a touch transitions TimerUnregistered or TimerIdle into TimerActive
100
 	if it.state != TimerDead {
105
 	if it.state != TimerDead {
101
 		it.state = TimerActive
106
 		it.state = TimerActive
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
 func (it *IdleTimer) processTimeout() {
118
 func (it *IdleTimer) processTimeout() {
107
 	idleTimeout, quitTimeout := it.recomputeDurations()
119
 	idleTimeout, quitTimeout := it.recomputeDurations()
108
 
120
 
322
 	// BrbDead is the state of a client after its timeout has expired; it will be removed
334
 	// BrbDead is the state of a client after its timeout has expired; it will be removed
323
 	// and therefore new sessions cannot be attached to it
335
 	// and therefore new sessions cannot be attached to it
324
 	BrbDead
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
 type BrbTimer struct {
339
 type BrbTimer struct {
345
 
354
 
346
 // attempts to enable BRB for a client, returns whether it succeeded
355
 // attempts to enable BRB for a client, returns whether it succeeded
347
 func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
356
 func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
348
-	if !bt.client.Registered() || bt.client.ResumeID() == "" {
349
-		return
350
-	}
351
-
352
 	// TODO make this configurable
357
 	// TODO make this configurable
353
 	duration = ResumeableTotalTimeout
358
 	duration = ResumeableTotalTimeout
354
 
359
 
355
 	bt.client.stateMutex.Lock()
360
 	bt.client.stateMutex.Lock()
356
 	defer bt.client.stateMutex.Unlock()
361
 	defer bt.client.stateMutex.Unlock()
357
 
362
 
363
+	if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
364
+		return
365
+	}
366
+
358
 	switch bt.state {
367
 	switch bt.state {
359
 	case BrbDisabled, BrbEnabled:
368
 	case BrbDisabled, BrbEnabled:
360
 		bt.state = BrbEnabled
369
 		bt.state = BrbEnabled
366
 			bt.brbAt = time.Now().UTC()
375
 			bt.brbAt = time.Now().UTC()
367
 		}
376
 		}
368
 		success = true
377
 		success = true
369
-	case BrbSticky:
370
-		success = true
371
 	default:
378
 	default:
372
 		// BrbDead
379
 		// BrbDead
373
 		success = false
380
 		success = false
416
 	bt.client.stateMutex.Lock()
423
 	bt.client.stateMutex.Lock()
417
 	defer bt.client.stateMutex.Unlock()
424
 	defer bt.client.stateMutex.Unlock()
418
 
425
 
426
+	if bt.client.alwaysOn {
427
+		return
428
+	}
429
+
419
 	switch bt.state {
430
 	switch bt.state {
420
 	case BrbDisabled, BrbEnabled:
431
 	case BrbDisabled, BrbEnabled:
421
 		if len(bt.client.sessions) == 0 {
432
 		if len(bt.client.sessions) == 0 {
432
 	}
443
 	}
433
 	bt.resetTimeout()
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 Ver arquivo

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 Ver arquivo

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 Ver arquivo

43
 	hadNick := target.HasNick()
43
 	hadNick := target.HasNick()
44
 	origNickMask := target.NickMaskString()
44
 	origNickMask := target.NickMaskString()
45
 	details := target.Details()
45
 	details := target.Details()
46
-	err := client.server.clients.SetNick(target, session, nickname)
46
+	assignedNickname, err := client.server.clients.SetNick(target, session, nickname)
47
 	if err == errNicknameInUse {
47
 	if err == errNicknameInUse {
48
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
48
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
49
 	} else if err == errNicknameReserved {
49
 	} else if err == errNicknameReserved {
50
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
50
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
51
 	} else if err == errNicknameInvalid {
51
 	} else if err == errNicknameInvalid {
52
 		rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, utils.SafeErrorParam(nickname), client.t("Erroneous nickname"))
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
 	} else if err != nil {
55
 	} else if err != nil {
54
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
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
 		AccountName: details.accountName,
66
 		AccountName: details.accountName,
65
 		Message:     message,
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
 	if hadNick {
72
 	if hadNick {
71
 		if client == target {
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
 		} else {
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
 		target.server.whoWas.Append(details.WhoWas)
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
 		for session := range target.Friends() {
80
 		for session := range target.Friends() {
79
 			if session != rb.session {
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
 	for _, channel := range client.Channels() {
87
 	for _, channel := range client.Channels() {
86
-		channel.history.Add(histItem)
88
+		channel.AddHistoryItem(histItem)
87
 	}
89
 	}
88
 
90
 
89
 	if target.Registered() {
91
 	if target.Registered() {

+ 76
- 1
irc/nickserv.go Ver arquivo

217
 			helpStrings: []string{
217
 			helpStrings: []string{
218
 				`Syntax $bSET <setting> <value>$b
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
 				`$bENFORCE$b
222
 				`$bENFORCE$b
223
 'enforce' lets you specify a custom enforcement mechanism for your registered
223
 'enforce' lets you specify a custom enforcement mechanism for your registered
247
 messages, but may be spammy. Your options are 'always', 'never', and the default
247
 messages, but may be spammy. Your options are 'always', 'never', and the default
248
 of 'commands-only' (the messages will be replayed in /HISTORY output, but not
248
 of 'commands-only' (the messages will be replayed in /HISTORY output, but not
249
 during autoreplay).`,
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
 			authRequired: true,
267
 			authRequired: true,
252
 			enabled:      servCmdRequiresAccreg,
268
 			enabled:      servCmdRequiresAccreg,
349
 				nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account"))
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
 	default:
393
 	default:
353
 		nsNotice(rb, client.t("No such setting"))
394
 		nsNotice(rb, client.t("No such setting"))
354
 	}
395
 	}
429
 				return
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
 	default:
504
 	default:
433
 		err = errInvalidParams
505
 		err = errInvalidParams
434
 	}
506
 	}
480
 	} else if ghost == client {
552
 	} else if ghost == client {
481
 		nsNotice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
553
 		nsNotice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
482
 		return
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
 	authorized := false
560
 	authorized := false

+ 42
- 2
irc/responsebuffer.go Ver arquivo

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

24
 	"github.com/goshuirc/irc-go/ircfmt"
24
 	"github.com/goshuirc/irc-go/ircfmt"
25
 	"github.com/oragono/oragono/irc/caps"
25
 	"github.com/oragono/oragono/irc/caps"
26
 	"github.com/oragono/oragono/irc/connection_limits"
26
 	"github.com/oragono/oragono/irc/connection_limits"
27
+	"github.com/oragono/oragono/irc/history"
27
 	"github.com/oragono/oragono/irc/logger"
28
 	"github.com/oragono/oragono/irc/logger"
28
 	"github.com/oragono/oragono/irc/modes"
29
 	"github.com/oragono/oragono/irc/modes"
30
+	"github.com/oragono/oragono/irc/mysql"
29
 	"github.com/oragono/oragono/irc/sno"
31
 	"github.com/oragono/oragono/irc/sno"
30
 	"github.com/tidwall/buntdb"
32
 	"github.com/tidwall/buntdb"
31
 )
33
 )
84
 	signals           chan os.Signal
86
 	signals           chan os.Signal
85
 	snomasks          SnoManager
87
 	snomasks          SnoManager
86
 	store             *buntdb.DB
88
 	store             *buntdb.DB
89
+	historyDB         mysql.MySQL
87
 	torLimiter        connection_limits.TorLimiter
90
 	torLimiter        connection_limits.TorLimiter
88
 	whoWas            WhoWasList
91
 	whoWas            WhoWasList
89
 	stats             Stats
92
 	stats             Stats
122
 	server.monitorManager.Initialize()
125
 	server.monitorManager.Initialize()
123
 	server.snomasks.Initialize()
126
 	server.snomasks.Initialize()
124
 
127
 
125
-	if err := server.applyConfig(config, true); err != nil {
128
+	if err := server.applyConfig(config); err != nil {
126
 		return nil, err
129
 		return nil, err
127
 	}
130
 	}
128
 
131
 
143
 	if err := server.store.Close(); err != nil {
146
 	if err := server.store.Close(); err != nil {
144
 		server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
147
 		server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
145
 	}
148
 	}
149
+
150
+	server.historyDB.Close()
146
 }
151
 }
147
 
152
 
148
 // Run starts the server.
153
 // Run starts the server.
316
 
321
 
317
 	// client MUST send PASS if necessary, or authenticate with SASL if necessary,
322
 	// client MUST send PASS if necessary, or authenticate with SASL if necessary,
318
 	// before completing the other registration commands
323
 	// before completing the other registration commands
319
-	authOutcome := c.isAuthorized(server.Config())
324
+	authOutcome := c.isAuthorized(server.Config(), session.isTor)
320
 	var quitMessage string
325
 	var quitMessage string
321
 	switch authOutcome {
326
 	switch authOutcome {
322
 	case authFailPass:
327
 	case authFailPass:
376
 	// continue registration
381
 	// continue registration
377
 	d := c.Details()
382
 	d := c.Details()
378
 	server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname))
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
 	// send welcome text
386
 	// send welcome text
382
 	//NOTE(dan): we specifically use the NICK here instead of the nickmask
387
 	//NOTE(dan): we specifically use the NICK here instead of the nickmask
550
 		return fmt.Errorf("Error loading config file config: %s", err.Error())
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
 	if err != nil {
559
 	if err != nil {
555
 		return fmt.Errorf("Error applying config changes: %s", err.Error())
560
 		return fmt.Errorf("Error applying config changes: %s", err.Error())
556
 	}
561
 	}
558
 	return nil
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
 	if initial {
570
 	if initial {
563
 		server.configFilename = config.Filename
571
 		server.configFilename = config.Filename
564
 		server.name = config.Server.Name
572
 		server.name = config.Server.Name
568
 		// enforce configs that can't be changed after launch:
576
 		// enforce configs that can't be changed after launch:
569
 		if server.name != config.Server.Name {
577
 		if server.name != config.Server.Name {
570
 			return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
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
 			return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
580
 			return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
573
 		} else if globalCasemappingSetting != config.Server.Casemapping {
581
 		} else if globalCasemappingSetting != config.Server.Casemapping {
574
 			return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
582
 			return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
576
 	}
584
 	}
577
 
585
 
578
 	server.logger.Info("server", "Using config file", server.configFilename)
586
 	server.logger.Info("server", "Using config file", server.configFilename)
579
-	oldConfig := server.Config()
580
 
587
 
581
 	// first, reload config sections for functionality implemented in subpackages:
588
 	// first, reload config sections for functionality implemented in subpackages:
582
 	wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
589
 	wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
609
 		if !oldConfig.Channels.Registration.Enabled {
616
 		if !oldConfig.Channels.Registration.Enabled {
610
 			server.channels.loadRegisteredChannels(config)
617
 			server.channels.loadRegisteredChannels(config)
611
 		}
618
 		}
612
-
613
 		// resize history buffers as needed
619
 		// resize history buffers as needed
614
 		if oldConfig.History != config.History {
620
 		if oldConfig.History != config.History {
615
 			for _, channel := range server.channels.Channels() {
621
 			for _, channel := range server.channels.Channels() {
616
-				channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
622
+				channel.resizeHistory(config)
617
 			}
623
 			}
618
 			for _, client := range server.clients.AllClients() {
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
 		if err := server.loadDatastore(config); err != nil {
664
 		if err := server.loadDatastore(config); err != nil {
659
 			return err
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
 	server.setupPprofListener(config)
673
 	server.setupPprofListener(config)
778
 	server.channels.Initialize(server)
788
 	server.channels.Initialize(server)
779
 	server.accounts.Initialize(server)
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
 	return nil
800
 	return nil
782
 }
801
 }
783
 
802
 
835
 	return
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
 // elistMatcher takes and matches ELIST conditions
923
 // elistMatcher takes and matches ELIST conditions
839
 type elistMatcher struct {
924
 type elistMatcher struct {
840
 	MinClientsActive bool
925
 	MinClientsActive bool

+ 19
- 1
irc/stats.go Ver arquivo

26
 	s.mutex.Unlock()
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
 // Transition a client from unregistered to registered
43
 // Transition a client from unregistered to registered
30
 func (s *Stats) Register() {
44
 func (s *Stats) Register() {
31
 	s.mutex.Lock()
45
 	s.mutex.Lock()
32
 	s.Unknown -= 1
46
 	s.Unknown -= 1
33
 	s.Total += 1
47
 	s.Total += 1
48
+	s.setMax()
49
+	s.mutex.Unlock()
50
+}
51
+
52
+func (s *Stats) setMax() {
34
 	if s.Max < s.Total {
53
 	if s.Max < s.Total {
35
 		s.Max = s.Total
54
 		s.Max = s.Total
36
 	}
55
 	}
37
-	s.mutex.Unlock()
38
 }
56
 }
39
 
57
 
40
 // Modify the Invisible count
58
 // Modify the Invisible count

+ 21
- 2
irc/utils/args.go Ver arquivo

5
 
5
 
6
 import (
6
 import (
7
 	"errors"
7
 	"errors"
8
+	"fmt"
8
 	"strings"
9
 	"strings"
10
+	"time"
11
+)
12
+
13
+const (
14
+	IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
9
 )
15
 )
10
 
16
 
11
 var (
17
 var (
45
 
51
 
46
 func StringToBool(str string) (result bool, err error) {
52
 func StringToBool(str string) (result bool, err error) {
47
 	switch strings.ToLower(str) {
53
 	switch strings.ToLower(str) {
48
-	case "on", "true", "t", "yes", "y":
54
+	case "on", "true", "t", "yes", "y", "disabled":
49
 		result = true
55
 		result = true
50
-	case "off", "false", "f", "no", "n":
56
+	case "off", "false", "f", "no", "n", "enabled":
51
 		result = false
57
 		result = false
52
 	default:
58
 	default:
53
 		err = ErrInvalidParams
59
 		err = ErrInvalidParams
63
 	}
69
 	}
64
 	return param
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 Ver arquivo

18
 	validHostnameLabelRegexp = regexp.MustCompile(`^[0-9A-Za-z.\-]+$`)
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
 // AddrToIP returns the IP address for a net.Addr; unix domain sockets are treated as IPv4 loopback
21
 // AddrToIP returns the IP address for a net.Addr; unix domain sockets are treated as IPv4 loopback
30
 func AddrToIP(addr net.Addr) net.IP {
22
 func AddrToIP(addr net.Addr) net.IP {
31
 	if tcpaddr, ok := addr.(*net.TCPAddr); ok {
23
 	if tcpaddr, ok := addr.(*net.TCPAddr); ok {

+ 5
- 8
irc/utils/text.go Ver arquivo

23
 // SplitMessage represents a message that's been split for sending.
23
 // SplitMessage represents a message that's been split for sending.
24
 // Two possibilities:
24
 // Two possibilities:
25
 // (a) Standard message that can be relayed on a single 512-byte line
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
 // (b) multiline message that was split on the client side
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
 type SplitMessage struct {
29
 type SplitMessage struct {
30
 	Message string
30
 	Message string
31
 	Msgid   string
31
 	Msgid   string
36
 func MakeMessage(original string) (result SplitMessage) {
36
 func MakeMessage(original string) (result SplitMessage) {
37
 	result.Message = original
37
 	result.Message = original
38
 	result.Msgid = GenerateSecretToken()
38
 	result.Msgid = GenerateSecretToken()
39
-	result.Time = time.Now().UTC()
39
+	result.SetTime()
40
 
40
 
41
 	return
41
 	return
42
 }
42
 }
52
 }
52
 }
53
 
53
 
54
 func (sm *SplitMessage) SetTime() {
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
 func (sm *SplitMessage) LenLines() int {
59
 func (sm *SplitMessage) LenLines() int {
88
 	return false
89
 	return false
89
 }
90
 }
90
 
91
 
91
-func (sm *SplitMessage) IsMultiline() bool {
92
-	return sm.Message == "" && len(sm.Split) != 0
93
-}
94
-
95
 func (sm *SplitMessage) Is512() bool {
92
 func (sm *SplitMessage) Is512() bool {
96
 	return sm.Message != ""
93
 	return sm.Message != ""
97
 }
94
 }

+ 15
- 3
irc/znc.go Ver arquivo

8
 	"strconv"
8
 	"strconv"
9
 	"strings"
9
 	"strings"
10
 	"time"
10
 	"time"
11
+
12
+	"github.com/oragono/oragono/irc/history"
11
 )
13
 )
12
 
14
 
13
 type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
15
 type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
89
 	//     3.3  When the client sends a subsequent redundant JOIN line for those
91
 	//     3.3  When the client sends a subsequent redundant JOIN line for those
90
 	//          channels; redundant JOIN is a complete no-op so we won't replay twice
92
 	//          channels; redundant JOIN is a complete no-op so we won't replay twice
91
 
93
 
92
-	config := client.server.Config()
93
 	if params[1] == "*" {
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
 	} else {
96
 	} else {
97
 		targets = make(StringSet)
97
 		targets = make(StringSet)
98
 		// TODO actually handle nickname targets
98
 		// TODO actually handle nickname targets
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 Ver arquivo

245
         # all users will receive simply `netname` as their cloaked hostname.
245
         # all users will receive simply `netname` as their cloaked hostname.
246
         num-bits: 80
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
 # account options
258
 # account options
250
 accounts:
259
 accounts:
351
         # via nickserv
360
         # via nickserv
352
         allowed-by-default: true
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
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
368
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
355
     # hostname/IP) by the HostServ service
369
     # hostname/IP) by the HostServ service
356
     vhosts:
370
     vhosts:
585
     # up, and if the upgrade fails, the original database will be restored.
599
     # up, and if the upgrade fails, the original database will be restored.
586
     autoupgrade: true
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
 # languages config
612
 # languages config
589
 languages:
613
 languages:
590
     # whether to load languages
614
     # whether to load languages
657
 # message history tracking, for the RESUME extension and possibly other uses in future
681
 # message history tracking, for the RESUME extension and possibly other uses in future
658
 history:
682
 history:
659
     # should we store messages for later playback?
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
     # across server restarts. however, you should not enable this unless you understand
685
     # across server restarts. however, you should not enable this unless you understand
662
     # how it interacts with the GDPR and/or any data privacy laws that apply
686
     # how it interacts with the GDPR and/or any data privacy laws that apply
663
     # in your country and the countries of your users.
687
     # in your country and the countries of your users.
683
     # maximum number of CHATHISTORY messages that can be
707
     # maximum number of CHATHISTORY messages that can be
684
     # requested at once (0 disables support for CHATHISTORY)
708
     # requested at once (0 disables support for CHATHISTORY)
685
     chathistory-maxmessages: 100
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"

Carregando…
Cancelar
Salvar