mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-10 22:19:31 +01:00
initial persistent history implementation
This commit is contained in:
parent
0d5a4fd584
commit
33dac4c0ba
1
Makefile
1
Makefile
@ -25,6 +25,7 @@ test:
|
|||||||
cd irc/history && go test . && go vet .
|
cd irc/history && go test . && go vet .
|
||||||
cd irc/isupport && go test . && go vet .
|
cd irc/isupport && go test . && go vet .
|
||||||
cd irc/modes && go test . && go vet .
|
cd irc/modes && go test . && go vet .
|
||||||
|
cd irc/mysql && go test . && go vet .
|
||||||
cd irc/passwd && go test . && go vet .
|
cd irc/passwd && go test . && go vet .
|
||||||
cd irc/utils && go test . && go vet .
|
cd irc/utils && go test . && go vet .
|
||||||
./.check-gofmt.sh
|
./.check-gofmt.sh
|
||||||
|
@ -171,6 +171,12 @@ CAPDEFS = [
|
|||||||
url="https://github.com/ircv3/ircv3-specifications/pull/398",
|
url="https://github.com/ircv3/ircv3-specifications/pull/398",
|
||||||
standard="proposed IRCv3",
|
standard="proposed IRCv3",
|
||||||
),
|
),
|
||||||
|
CapDef(
|
||||||
|
identifier="Chathistory",
|
||||||
|
name="draft/chathistory",
|
||||||
|
url="https://github.com/ircv3/ircv3-specifications/pull/393",
|
||||||
|
standard="proposed IRCv3",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_defs():
|
def validate_defs():
|
||||||
|
12
go.mod
12
go.mod
@ -6,23 +6,19 @@ require (
|
|||||||
code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
|
code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
github.com/go-ldap/ldap/v3 v3.1.6
|
github.com/go-ldap/ldap/v3 v3.1.6
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
|
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
|
||||||
github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0
|
github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0
|
||||||
github.com/mattn/go-colorable v0.1.4
|
github.com/mattn/go-colorable v0.1.4
|
||||||
github.com/mattn/go-isatty v0.0.10 // indirect
|
github.com/mattn/go-isatty v0.0.10 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
|
||||||
|
github.com/onsi/ginkgo v1.12.0 // indirect
|
||||||
|
github.com/onsi/gomega v1.9.0 // indirect
|
||||||
github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
|
github.com/oragono/confusables v0.0.0-20190624102032-fe1cf31a24b0
|
||||||
github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
|
github.com/oragono/go-ident v0.0.0-20170110123031-337fed0fd21a
|
||||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
github.com/tidwall/buntdb v1.1.2
|
github.com/tidwall/buntdb v1.1.2
|
||||||
github.com/tidwall/gjson v1.3.4 // indirect
|
|
||||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
|
|
||||||
github.com/tidwall/match v1.0.1 // indirect
|
|
||||||
github.com/tidwall/pretty v1.0.0 // indirect
|
|
||||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
|
|
||||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
|
|
||||||
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
|
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
|
||||||
golang.org/x/sys v0.0.0-20191115151921-52ab43148777 // indirect
|
|
||||||
golang.org/x/text v0.3.2
|
golang.org/x/text v0.3.2
|
||||||
gopkg.in/yaml.v2 v2.2.5
|
gopkg.in/yaml.v2 v2.2.5
|
||||||
)
|
)
|
||||||
|
@ -33,7 +33,8 @@ const (
|
|||||||
keyAccountSettings = "account.settings %s"
|
keyAccountSettings = "account.settings %s"
|
||||||
keyAccountVHost = "account.vhost %s"
|
keyAccountVHost = "account.vhost %s"
|
||||||
keyCertToAccount = "account.creds.certfp %s"
|
keyCertToAccount = "account.creds.certfp %s"
|
||||||
keyAccountChannels = "account.channels %s"
|
keyAccountChannels = "account.channels %s" // channels registered to the account
|
||||||
|
keyAccountJoinedChannels = "account.joinedto %s" // channels a persistent client has joined
|
||||||
|
|
||||||
keyVHostQueueAcctToId = "vhostQueue %s"
|
keyVHostQueueAcctToId = "vhostQueue %s"
|
||||||
vhostRequestIdx = "vhostQueue"
|
vhostRequestIdx = "vhostQueue"
|
||||||
@ -71,6 +72,40 @@ func (am *AccountManager) Initialize(server *Server) {
|
|||||||
config := server.Config()
|
config := server.Config()
|
||||||
am.buildNickToAccountIndex(config)
|
am.buildNickToAccountIndex(config)
|
||||||
am.initVHostRequestQueue(config)
|
am.initVHostRequestQueue(config)
|
||||||
|
am.createAlwaysOnClients(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AccountManager) createAlwaysOnClients(config *Config) {
|
||||||
|
if config.Accounts.Bouncer.AlwaysOn == PersistentDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiedPrefix := fmt.Sprintf(keyAccountVerified, "")
|
||||||
|
|
||||||
|
am.serialCacheUpdateMutex.Lock()
|
||||||
|
defer am.serialCacheUpdateMutex.Unlock()
|
||||||
|
|
||||||
|
var accounts []string
|
||||||
|
|
||||||
|
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
err := tx.AscendGreaterOrEqual("", verifiedPrefix, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, verifiedPrefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
account := strings.TrimPrefix(key, verifiedPrefix)
|
||||||
|
accounts = append(accounts, account)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, accountName := range accounts {
|
||||||
|
account, err := am.LoadAccount(accountName)
|
||||||
|
if err == nil && account.Verified &&
|
||||||
|
persistenceEnabled(config.Accounts.Bouncer.AlwaysOn, account.Settings.AlwaysOn) {
|
||||||
|
am.server.AddAlwaysOnClient(account, am.loadChannels(accountName))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) buildNickToAccountIndex(config *Config) {
|
func (am *AccountManager) buildNickToAccountIndex(config *Config) {
|
||||||
@ -477,6 +512,28 @@ func (am *AccountManager) setPassword(account string, password string, hasPrivs
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (am *AccountManager) saveChannels(account string, channels []string) {
|
||||||
|
channelsStr := strings.Join(channels, ",")
|
||||||
|
key := fmt.Sprintf(keyAccountJoinedChannels, account)
|
||||||
|
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
tx.Set(key, channelsStr, nil)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AccountManager) loadChannels(account string) (channels []string) {
|
||||||
|
key := fmt.Sprintf(keyAccountJoinedChannels, account)
|
||||||
|
var channelsStr string
|
||||||
|
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
channelsStr, _ = tx.Get(key)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if channelsStr != "" {
|
||||||
|
return strings.Split(channelsStr, ",")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
|
func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
|
||||||
certfp, err = utils.NormalizeCertfp(certfp)
|
certfp, err = utils.NormalizeCertfp(certfp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -685,7 +742,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
|||||||
}
|
}
|
||||||
am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
|
am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
|
||||||
raw.Verified = true
|
raw.Verified = true
|
||||||
clientAccount, err := am.deserializeRawAccount(raw)
|
clientAccount, err := am.deserializeRawAccount(raw, casefoldedAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -892,13 +949,13 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err = am.deserializeRawAccount(raw)
|
result, err = am.deserializeRawAccount(raw, casefoldedAccount)
|
||||||
result.NameCasefolded = casefoldedAccount
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result ClientAccount, err error) {
|
func (am *AccountManager) deserializeRawAccount(raw rawClientAccount, cfName string) (result ClientAccount, err error) {
|
||||||
result.Name = raw.Name
|
result.Name = raw.Name
|
||||||
|
result.NameCasefolded = cfName
|
||||||
regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
|
regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
|
||||||
result.RegisteredAt = time.Unix(regTimeInt, 0).UTC()
|
result.RegisteredAt = time.Unix(regTimeInt, 0).UTC()
|
||||||
e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
|
e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
|
||||||
@ -976,6 +1033,7 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||||
vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
|
vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
|
||||||
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
||||||
|
joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount)
|
||||||
|
|
||||||
var clients []*Client
|
var clients []*Client
|
||||||
|
|
||||||
@ -1011,6 +1069,7 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
tx.Delete(vhostKey)
|
tx.Delete(vhostKey)
|
||||||
channelsStr, _ = tx.Get(channelsKey)
|
channelsStr, _ = tx.Get(channelsKey)
|
||||||
tx.Delete(channelsKey)
|
tx.Delete(channelsKey)
|
||||||
|
tx.Delete(joinedChannelsKey)
|
||||||
|
|
||||||
_, err := tx.Delete(vhostQueueKey)
|
_, err := tx.Delete(vhostQueueKey)
|
||||||
am.decrementVHostQueueCount(casefoldedAccount, err)
|
am.decrementVHostQueueCount(casefoldedAccount, err)
|
||||||
@ -1455,10 +1514,7 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
||||||
changed := client.SetAccountName(account.Name)
|
client.Login(account)
|
||||||
if !changed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client.nickTimer.Touch(nil)
|
client.nickTimer.Touch(nil)
|
||||||
|
|
||||||
@ -1468,9 +1524,6 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
|||||||
am.Lock()
|
am.Lock()
|
||||||
defer am.Unlock()
|
defer am.Unlock()
|
||||||
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
|
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
|
||||||
for _, client := range am.accountToClients[casefoldedAccount] {
|
|
||||||
client.SetAccountSettings(account.Settings)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) Logout(client *Client) {
|
func (am *AccountManager) Logout(client *Client) {
|
||||||
@ -1623,10 +1676,13 @@ func replayJoinsSettingFromString(str string) (result ReplayJoinsSetting, err er
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AccountSettings struct {
|
type AccountSettings struct {
|
||||||
AutoreplayLines *int
|
AutoreplayLines *int
|
||||||
NickEnforcement NickEnforcementMethod
|
NickEnforcement NickEnforcementMethod
|
||||||
AllowBouncer BouncerAllowedSetting
|
AllowBouncer BouncerAllowedSetting
|
||||||
ReplayJoins ReplayJoinsSetting
|
ReplayJoins ReplayJoinsSetting
|
||||||
|
AlwaysOn PersistentStatus
|
||||||
|
AutoreplayMissed bool
|
||||||
|
DMHistory HistoryStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientAccount represents a user account.
|
// ClientAccount represents a user account.
|
||||||
@ -1661,7 +1717,7 @@ func (am *AccountManager) logoutOfAccount(client *Client) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client.SetAccountName("")
|
client.Logout()
|
||||||
go client.nickTimer.Touch(nil)
|
go client.nickTimer.Touch(nil)
|
||||||
|
|
||||||
// dispatch account-notify
|
// dispatch account-notify
|
||||||
|
@ -7,7 +7,7 @@ package caps
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 26
|
numCapabs = 27
|
||||||
// length of the uint64 array that represents the bitset:
|
// length of the uint64 array that represents the bitset:
|
||||||
bitsetLen = 1
|
bitsetLen = 1
|
||||||
)
|
)
|
||||||
@ -37,6 +37,10 @@ const (
|
|||||||
// https://ircv3.net/specs/extensions/chghost-3.2.html
|
// https://ircv3.net/specs/extensions/chghost-3.2.html
|
||||||
ChgHost Capability = iota
|
ChgHost Capability = iota
|
||||||
|
|
||||||
|
// Chathistory is the proposed IRCv3 capability named "draft/chathistory":
|
||||||
|
// https://github.com/ircv3/ircv3-specifications/pull/393
|
||||||
|
Chathistory Capability = iota
|
||||||
|
|
||||||
// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
|
// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/362
|
// https://github.com/ircv3/ircv3-specifications/pull/362
|
||||||
EventPlayback Capability = iota
|
EventPlayback Capability = iota
|
||||||
@ -127,6 +131,7 @@ var (
|
|||||||
"batch",
|
"batch",
|
||||||
"cap-notify",
|
"cap-notify",
|
||||||
"chghost",
|
"chghost",
|
||||||
|
"draft/chathistory",
|
||||||
"draft/event-playback",
|
"draft/event-playback",
|
||||||
"draft/languages",
|
"draft/languages",
|
||||||
"draft/multiline",
|
"draft/multiline",
|
||||||
|
154
irc/channel.go
154
irc/channel.go
@ -24,6 +24,10 @@ const (
|
|||||||
histServMask = "HistServ!HistServ@localhost"
|
histServMask = "HistServ!HistServ@localhost"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ChannelSettings struct {
|
||||||
|
History HistoryStatus
|
||||||
|
}
|
||||||
|
|
||||||
// Channel represents a channel that clients can join.
|
// Channel represents a channel that clients can join.
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
flags modes.ModeSet
|
flags modes.ModeSet
|
||||||
@ -49,6 +53,7 @@ type Channel struct {
|
|||||||
joinPartMutex sync.Mutex // tier 3
|
joinPartMutex sync.Mutex // tier 3
|
||||||
ensureLoaded utils.Once // manages loading stored registration info from the database
|
ensureLoaded utils.Once // manages loading stored registration info from the database
|
||||||
dirtyBits uint
|
dirtyBits uint
|
||||||
|
settings ChannelSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannel creates a new channel from a `Server` and a `name`
|
// NewChannel creates a new channel from a `Server` and a `name`
|
||||||
@ -66,9 +71,10 @@ func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channe
|
|||||||
|
|
||||||
channel.initializeLists()
|
channel.initializeLists()
|
||||||
channel.writerSemaphore.Initialize(1)
|
channel.writerSemaphore.Initialize(1)
|
||||||
channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
|
channel.history.Initialize(0, 0)
|
||||||
|
|
||||||
if !registered {
|
if !registered {
|
||||||
|
channel.resizeHistory(config)
|
||||||
for _, mode := range config.Channels.defaultModes {
|
for _, mode := range config.Channels.defaultModes {
|
||||||
channel.flags.SetMode(mode, true)
|
channel.flags.SetMode(mode, true)
|
||||||
}
|
}
|
||||||
@ -106,8 +112,19 @@ func (channel *Channel) IsLoaded() bool {
|
|||||||
return channel.ensureLoaded.Done()
|
return channel.ensureLoaded.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) resizeHistory(config *Config) {
|
||||||
|
_, ephemeral, _ := channel.historyStatus(config)
|
||||||
|
if ephemeral {
|
||||||
|
channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
|
||||||
|
} else {
|
||||||
|
channel.history.Resize(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// read in channel state that was persisted in the DB
|
// read in channel state that was persisted in the DB
|
||||||
func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
|
func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
|
||||||
|
defer channel.resizeHistory(channel.server.Config())
|
||||||
|
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
defer channel.stateMutex.Unlock()
|
defer channel.stateMutex.Unlock()
|
||||||
|
|
||||||
@ -120,6 +137,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
|
|||||||
channel.createdTime = chanReg.RegisteredAt
|
channel.createdTime = chanReg.RegisteredAt
|
||||||
channel.key = chanReg.Key
|
channel.key = chanReg.Key
|
||||||
channel.userLimit = chanReg.UserLimit
|
channel.userLimit = chanReg.UserLimit
|
||||||
|
channel.settings = chanReg.Settings
|
||||||
|
|
||||||
for _, mode := range chanReg.Modes {
|
for _, mode := range chanReg.Modes {
|
||||||
channel.flags.SetMode(mode, true)
|
channel.flags.SetMode(mode, true)
|
||||||
@ -164,6 +182,10 @@ func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredCh
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if includeFlags&IncludeSettings != 0 {
|
||||||
|
info.Settings = channel.settings
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,7 +456,7 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
|
|||||||
if modeSet == nil {
|
if modeSet == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !isJoined && target.flags.HasMode(modes.Invisible) && !isOper {
|
if !isJoined && target.HasMode(modes.Invisible) && !isOper {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
prefix := modeSet.Prefixes(isMultiPrefix)
|
prefix := modeSet.Prefixes(isMultiPrefix)
|
||||||
@ -564,6 +586,48 @@ func (channel *Channel) IsEmpty() bool {
|
|||||||
return len(channel.members) == 0
|
return len(channel.members) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// figure out where history is being stored: persistent, ephemeral, or neither
|
||||||
|
// target is only needed if we're doing persistent history
|
||||||
|
func (channel *Channel) historyStatus(config *Config) (persistent, ephemeral bool, target string) {
|
||||||
|
if !config.History.Persistent.Enabled {
|
||||||
|
return false, config.History.Enabled, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.stateMutex.RLock()
|
||||||
|
target = channel.nameCasefolded
|
||||||
|
historyStatus := channel.settings.History
|
||||||
|
registered := channel.registeredFounder != ""
|
||||||
|
channel.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
historyStatus = historyEnabled(config.History.Persistent.RegisteredChannels, historyStatus)
|
||||||
|
|
||||||
|
// ephemeral history: either the channel owner explicitly set the ephemeral preference,
|
||||||
|
// or persistent history is disabled for unregistered channels
|
||||||
|
if registered {
|
||||||
|
ephemeral = (historyStatus == HistoryEphemeral)
|
||||||
|
persistent = (historyStatus == HistoryPersistent)
|
||||||
|
} else {
|
||||||
|
ephemeral = config.History.Enabled && !config.History.Persistent.UnregisteredChannels
|
||||||
|
persistent = config.History.Persistent.UnregisteredChannels
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) AddHistoryItem(item history.Item) (err error) {
|
||||||
|
if !item.IsStorable() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
persistent, ephemeral, target := channel.historyStatus(channel.server.Config())
|
||||||
|
if ephemeral {
|
||||||
|
channel.history.Add(item)
|
||||||
|
}
|
||||||
|
if persistent {
|
||||||
|
return channel.server.historyDB.AddChannelItem(target, item)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Join joins the given client to this channel (if they can be joined).
|
// Join joins the given client to this channel (if they can be joined).
|
||||||
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
|
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
@ -643,15 +707,18 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
|
|
||||||
channel.regenerateMembersCache()
|
channel.regenerateMembersCache()
|
||||||
|
|
||||||
message = utils.MakeMessage("")
|
// no history item for fake persistent joins
|
||||||
histItem := history.Item{
|
if rb != nil {
|
||||||
Type: history.Join,
|
message = utils.MakeMessage("")
|
||||||
Nick: details.nickMask,
|
histItem := history.Item{
|
||||||
AccountName: details.accountName,
|
Type: history.Join,
|
||||||
Message: message,
|
Nick: details.nickMask,
|
||||||
|
AccountName: details.accountName,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
histItem.Params[0] = details.realname
|
||||||
|
channel.AddHistoryItem(histItem)
|
||||||
}
|
}
|
||||||
histItem.Params[0] = details.realname
|
|
||||||
channel.history.Add(histItem)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}()
|
}()
|
||||||
@ -665,7 +732,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
|
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
for _, session := range member.Sessions() {
|
for _, session := range member.Sessions() {
|
||||||
if session == rb.session {
|
if rb != nil && session == rb.session {
|
||||||
continue
|
continue
|
||||||
} else if client == session.client {
|
} else if client == session.client {
|
||||||
channel.playJoinForSession(session)
|
channel.playJoinForSession(session)
|
||||||
@ -682,13 +749,13 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rb.session.capabilities.Has(caps.ExtendedJoin) {
|
if rb != nil && rb.session.capabilities.Has(caps.ExtendedJoin) {
|
||||||
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname)
|
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname)
|
||||||
} else {
|
} else {
|
||||||
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname)
|
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rb.session.client == client {
|
if rb != nil && rb.session.client == client {
|
||||||
// don't send topic and names for a SAJOIN of a different client
|
// don't send topic and names for a SAJOIN of a different client
|
||||||
channel.SendTopic(client, rb, false)
|
channel.SendTopic(client, rb, false)
|
||||||
channel.Names(client, rb)
|
channel.Names(client, rb)
|
||||||
@ -697,15 +764,29 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
|
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
|
||||||
rb.Flush(true)
|
rb.Flush(true)
|
||||||
|
|
||||||
channel.autoReplayHistory(client, rb, message.Msgid)
|
if rb != nil {
|
||||||
|
channel.autoReplayHistory(client, rb, message.Msgid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
|
func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
|
||||||
// autoreplay any messages as necessary
|
// autoreplay any messages as necessary
|
||||||
config := channel.server.Config()
|
|
||||||
var items []history.Item
|
var items []history.Item
|
||||||
|
|
||||||
|
var after, before time.Time
|
||||||
if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets.Has(channel.NameCasefolded())) {
|
if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets.Has(channel.NameCasefolded())) {
|
||||||
items, _ = channel.history.Between(rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before, false, config.History.ChathistoryMax)
|
after, before = rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before
|
||||||
|
} else if !rb.session.lastSignoff.IsZero() {
|
||||||
|
// we already checked for history caps in `playReattachMessages`
|
||||||
|
after = rb.session.lastSignoff
|
||||||
|
}
|
||||||
|
|
||||||
|
if !after.IsZero() || !before.IsZero() {
|
||||||
|
_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
|
||||||
|
if seq != nil {
|
||||||
|
zncMax := channel.server.Config().History.ZNCMax
|
||||||
|
items, _, _ = seq.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
|
||||||
|
}
|
||||||
} else if !rb.session.HasHistoryCaps() {
|
} else if !rb.session.HasHistoryCaps() {
|
||||||
var replayLimit int
|
var replayLimit int
|
||||||
customReplayLimit := client.AccountSettings().AutoreplayLines
|
customReplayLimit := client.AccountSettings().AutoreplayLines
|
||||||
@ -719,7 +800,10 @@ func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, sk
|
|||||||
replayLimit = channel.server.Config().History.AutoreplayOnJoin
|
replayLimit = channel.server.Config().History.AutoreplayOnJoin
|
||||||
}
|
}
|
||||||
if 0 < replayLimit {
|
if 0 < replayLimit {
|
||||||
items = channel.history.Latest(replayLimit)
|
_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
|
||||||
|
if seq != nil {
|
||||||
|
items, _, _ = seq.Between(history.Selector{}, history.Selector{}, replayLimit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// remove the client's own JOIN line from the replay
|
// remove the client's own JOIN line from the replay
|
||||||
@ -784,7 +868,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.history.Add(history.Item{
|
channel.AddHistoryItem(history.Item{
|
||||||
Type: history.Part,
|
Type: history.Part,
|
||||||
Nick: details.nickMask,
|
Nick: details.nickMask,
|
||||||
AccountName: details.accountName,
|
AccountName: details.accountName,
|
||||||
@ -799,10 +883,9 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
|||||||
// 2. Send JOIN and MODE lines to channel participants (including the new client)
|
// 2. Send JOIN and MODE lines to channel participants (including the new client)
|
||||||
// 3. Replay missed message history to the client
|
// 3. Replay missed message history to the client
|
||||||
func (channel *Channel) Resume(session *Session, timestamp time.Time) {
|
func (channel *Channel) Resume(session *Session, timestamp time.Time) {
|
||||||
now := time.Now().UTC()
|
|
||||||
channel.resumeAndAnnounce(session)
|
channel.resumeAndAnnounce(session)
|
||||||
if !timestamp.IsZero() {
|
if !timestamp.IsZero() {
|
||||||
channel.replayHistoryForResume(session, timestamp, now)
|
channel.replayHistoryForResume(session, timestamp, time.Time{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -852,7 +935,13 @@ func (channel *Channel) resumeAndAnnounce(session *Session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
|
func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
|
||||||
items, complete := channel.history.Between(after, before, false, 0)
|
var items []history.Item
|
||||||
|
var complete bool
|
||||||
|
afterS, beforeS := history.Selector{Time: after}, history.Selector{Time: before}
|
||||||
|
_, seq, _ := channel.server.GetHistorySequence(channel, session.client, "")
|
||||||
|
if seq != nil {
|
||||||
|
items, complete, _ = seq.Between(afterS, beforeS, channel.server.Config().History.ZNCMax)
|
||||||
|
}
|
||||||
rb := NewResponseBuffer(session)
|
rb := NewResponseBuffer(session)
|
||||||
channel.replayHistoryItems(rb, items, false)
|
channel.replayHistoryItems(rb, items, false)
|
||||||
if !complete && !session.resumeDetails.HistoryIncomplete {
|
if !complete && !session.resumeDetails.HistoryIncomplete {
|
||||||
@ -908,9 +997,9 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
|||||||
case history.Join:
|
case history.Join:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
if extendedJoin {
|
if extendedJoin {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname, item.AccountName, item.Params[0])
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname, item.AccountName, item.Params[0])
|
||||||
} else {
|
} else {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "JOIN", chname)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !playJoinsAsPrivmsg {
|
if !playJoinsAsPrivmsg {
|
||||||
@ -926,7 +1015,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
|||||||
}
|
}
|
||||||
case history.Part:
|
case history.Part:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "PART", chname, item.Message.Message)
|
||||||
} else {
|
} else {
|
||||||
if !playJoinsAsPrivmsg {
|
if !playJoinsAsPrivmsg {
|
||||||
continue // #474
|
continue // #474
|
||||||
@ -936,14 +1025,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
|||||||
}
|
}
|
||||||
case history.Kick:
|
case history.Kick:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "KICK", chname, item.Params[0], item.Message.Message)
|
||||||
} else {
|
} else {
|
||||||
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
|
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
|
||||||
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
case history.Quit:
|
case history.Quit:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "QUIT", item.Message.Message)
|
||||||
} else {
|
} else {
|
||||||
if !playJoinsAsPrivmsg {
|
if !playJoinsAsPrivmsg {
|
||||||
continue // #474
|
continue // #474
|
||||||
@ -953,7 +1042,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
|||||||
}
|
}
|
||||||
case history.Nick:
|
case history.Nick:
|
||||||
if eventPlayback {
|
if eventPlayback {
|
||||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0])
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "NICK", item.Params[0])
|
||||||
} else {
|
} else {
|
||||||
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
|
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
|
||||||
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
|
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
|
||||||
@ -1124,11 +1213,12 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
|||||||
// STATUSMSG
|
// STATUSMSG
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isCTCP && member.isTor {
|
|
||||||
continue // #753
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, session := range member.Sessions() {
|
for _, session := range member.Sessions() {
|
||||||
|
if isCTCP && session.isTor {
|
||||||
|
continue // #753
|
||||||
|
}
|
||||||
|
|
||||||
var tagsToUse map[string]string
|
var tagsToUse map[string]string
|
||||||
if session.capabilities.Has(caps.MessageTags) {
|
if session.capabilities.Has(caps.MessageTags) {
|
||||||
tagsToUse = clientOnlyTags
|
tagsToUse = clientOnlyTags
|
||||||
@ -1144,7 +1234,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.history.Add(history.Item{
|
channel.AddHistoryItem(history.Item{
|
||||||
Type: histType,
|
Type: histType,
|
||||||
Message: message,
|
Message: message,
|
||||||
Nick: nickmask,
|
Nick: nickmask,
|
||||||
@ -1266,7 +1356,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
|
|||||||
Message: message,
|
Message: message,
|
||||||
}
|
}
|
||||||
histItem.Params[0] = targetNick
|
histItem.Params[0] = targetNick
|
||||||
channel.history.Add(histItem)
|
channel.AddHistoryItem(histItem)
|
||||||
|
|
||||||
channel.Quit(target)
|
channel.Quit(target)
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ const (
|
|||||||
keyChannelModes = "channel.modes %s"
|
keyChannelModes = "channel.modes %s"
|
||||||
keyChannelAccountToUMode = "channel.accounttoumode %s"
|
keyChannelAccountToUMode = "channel.accounttoumode %s"
|
||||||
keyChannelUserLimit = "channel.userlimit %s"
|
keyChannelUserLimit = "channel.userlimit %s"
|
||||||
|
keyChannelSettings = "channel.settings %s"
|
||||||
|
|
||||||
keyChannelPurged = "channel.purged %s"
|
keyChannelPurged = "channel.purged %s"
|
||||||
)
|
)
|
||||||
@ -53,6 +54,7 @@ var (
|
|||||||
keyChannelModes,
|
keyChannelModes,
|
||||||
keyChannelAccountToUMode,
|
keyChannelAccountToUMode,
|
||||||
keyChannelUserLimit,
|
keyChannelUserLimit,
|
||||||
|
keyChannelSettings,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -63,6 +65,7 @@ const (
|
|||||||
IncludeTopic
|
IncludeTopic
|
||||||
IncludeModes
|
IncludeModes
|
||||||
IncludeLists
|
IncludeLists
|
||||||
|
IncludeSettings
|
||||||
)
|
)
|
||||||
|
|
||||||
// this is an OR of all possible flags
|
// this is an OR of all possible flags
|
||||||
@ -100,6 +103,8 @@ type RegisteredChannel struct {
|
|||||||
Excepts map[string]MaskInfo
|
Excepts map[string]MaskInfo
|
||||||
// Invites represents the invite exceptions set on the channel.
|
// Invites represents the invite exceptions set on the channel.
|
||||||
Invites map[string]MaskInfo
|
Invites map[string]MaskInfo
|
||||||
|
// Settings are the chanserv-modifiable settings
|
||||||
|
Settings ChannelSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChannelPurgeRecord struct {
|
type ChannelPurgeRecord struct {
|
||||||
@ -203,6 +208,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
|
|||||||
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
|
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
|
||||||
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
|
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
|
||||||
accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
|
accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
|
||||||
|
settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
|
||||||
|
|
||||||
modeSlice := make([]modes.Mode, len(modeString))
|
modeSlice := make([]modes.Mode, len(modeString))
|
||||||
for i, mode := range modeString {
|
for i, mode := range modeString {
|
||||||
@ -220,6 +226,9 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
|
|||||||
accountToUMode := make(map[string]modes.Mode)
|
accountToUMode := make(map[string]modes.Mode)
|
||||||
_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
|
_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
|
||||||
|
|
||||||
|
var settings ChannelSettings
|
||||||
|
_ = json.Unmarshal([]byte(settingsString), &settings)
|
||||||
|
|
||||||
info = RegisteredChannel{
|
info = RegisteredChannel{
|
||||||
Name: name,
|
Name: name,
|
||||||
RegisteredAt: time.Unix(regTimeInt, 0).UTC(),
|
RegisteredAt: time.Unix(regTimeInt, 0).UTC(),
|
||||||
@ -234,6 +243,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
|
|||||||
Invites: invitelist,
|
Invites: invitelist,
|
||||||
AccountToUMode: accountToUMode,
|
AccountToUMode: accountToUMode,
|
||||||
UserLimit: int(userLimit),
|
UserLimit: int(userLimit),
|
||||||
|
Settings: settings,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -357,6 +367,11 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
|
|||||||
accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
|
accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
|
||||||
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
|
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if includeFlags&IncludeSettings != 0 {
|
||||||
|
settingsString, _ := json.Marshal(channelInfo.Settings)
|
||||||
|
tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurgeChannel records a channel purge.
|
// PurgeChannel records a channel purge.
|
||||||
|
141
irc/chanserv.go
141
irc/chanserv.go
@ -137,6 +137,35 @@ INFO displays info about a registered channel.`,
|
|||||||
enabled: chanregEnabled,
|
enabled: chanregEnabled,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
|
"get": {
|
||||||
|
handler: csGetHandler,
|
||||||
|
help: `Syntax: $bGET #channel <setting>$b
|
||||||
|
|
||||||
|
GET queries the current values of the channel settings. For more information
|
||||||
|
on the settings and their possible values, see HELP SET.`,
|
||||||
|
helpShort: `$bGET$b queries the current values of a channel's settings`,
|
||||||
|
enabled: chanregEnabled,
|
||||||
|
minParams: 2,
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
handler: csSetHandler,
|
||||||
|
helpShort: `$bSET$b modifies a channel's settings`,
|
||||||
|
// these are broken out as separate strings so they can be translated separately
|
||||||
|
helpStrings: []string{
|
||||||
|
`Syntax $bSET #channel <setting> <value>$b
|
||||||
|
|
||||||
|
SET modifies a channel's settings. The following settings are available:`,
|
||||||
|
|
||||||
|
`$bHISTORY$b
|
||||||
|
'history' lets you control how channel history is stored. Your options are:
|
||||||
|
1. 'off' [no history]
|
||||||
|
2. 'ephemeral' [a limited amount of temporary history, not stored on disk]
|
||||||
|
3. 'on' [history stored in a permanent database, if available]
|
||||||
|
4. 'default' [use the server default]`,
|
||||||
|
},
|
||||||
|
enabled: chanregEnabled,
|
||||||
|
minParams: 3,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -320,6 +349,22 @@ func checkChanLimit(client *Client, rb *ResponseBuffer) (ok bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func csPrivsCheck(channel RegisteredChannel, client *Client, rb *ResponseBuffer) (success bool) {
|
||||||
|
founder := channel.Founder
|
||||||
|
if founder == "" {
|
||||||
|
csNotice(rb, client.t("That channel is not registered"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if client.HasRoleCapabs("chanreg") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if founder != client.Account() {
|
||||||
|
csNotice(rb, client.t("Insufficient privileges"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
channelName := params[0]
|
channelName := params[0]
|
||||||
var verificationCode string
|
var verificationCode string
|
||||||
@ -327,31 +372,18 @@ func csUnregisterHandler(server *Server, client *Client, command string, params
|
|||||||
verificationCode = params[1]
|
verificationCode = params[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
channelKey, err := CasefoldChannel(channelName)
|
channel := server.channels.Get(channelName)
|
||||||
if channelKey == "" || err != nil {
|
|
||||||
csNotice(rb, client.t("Channel name is not valid"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
channel := server.channels.Get(channelKey)
|
|
||||||
if channel == nil {
|
if channel == nil {
|
||||||
csNotice(rb, client.t("No such channel"))
|
csNotice(rb, client.t("No such channel"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
founder := channel.Founder()
|
|
||||||
if founder == "" {
|
|
||||||
csNotice(rb, client.t("That channel is not registered"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPrivs := client.HasRoleCapabs("chanreg") || founder == client.Account()
|
|
||||||
if !hasPrivs {
|
|
||||||
csNotice(rb, client.t("Insufficient privileges"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
info := channel.ExportRegistration(0)
|
info := channel.ExportRegistration(0)
|
||||||
|
channelKey := info.NameCasefolded
|
||||||
|
if !csPrivsCheck(info, client, rb) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
expectedCode := unregisterConfirmationCode(info.Name, info.RegisteredAt)
|
expectedCode := unregisterConfirmationCode(info.Name, info.RegisteredAt)
|
||||||
if expectedCode != verificationCode {
|
if expectedCode != verificationCode {
|
||||||
csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
|
csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
|
||||||
@ -359,7 +391,7 @@ func csUnregisterHandler(server *Server, client *Client, command string, params
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
server.channels.SetUnregistered(channelKey, founder)
|
server.channels.SetUnregistered(channelKey, info.Founder)
|
||||||
csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
|
csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,9 +409,7 @@ func csClearHandler(server *Server, client *Client, command string, params []str
|
|||||||
csNotice(rb, client.t("Channel does not exist"))
|
csNotice(rb, client.t("Channel does not exist"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
account := client.Account()
|
if !csPrivsCheck(channel.ExportRegistration(0), client, rb) {
|
||||||
if !(client.HasRoleCapabs("chanreg") || (account != "" && account == channel.Founder())) {
|
|
||||||
csNotice(rb, client.t("Insufficient privileges"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -573,3 +603,68 @@ func csInfoHandler(server *Server, client *Client, command string, params []stri
|
|||||||
csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
|
csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
|
||||||
csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
|
csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func displayChannelSetting(settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {
|
||||||
|
config := client.server.Config()
|
||||||
|
|
||||||
|
switch strings.ToLower(settingName) {
|
||||||
|
case "history":
|
||||||
|
effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
|
||||||
|
default:
|
||||||
|
csNotice(rb, client.t("Invalid params"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func csGetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
chname, setting := params[0], params[1]
|
||||||
|
channel := server.channels.Get(chname)
|
||||||
|
if channel == nil {
|
||||||
|
csNotice(rb, client.t("No such channel"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info := channel.ExportRegistration(IncludeSettings)
|
||||||
|
if !csPrivsCheck(info, client, rb) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
displayChannelSetting(setting, info.Settings, client, rb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func csSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
chname, setting, value := params[0], params[1], params[2]
|
||||||
|
channel := server.channels.Get(chname)
|
||||||
|
if channel == nil {
|
||||||
|
csNotice(rb, client.t("No such channel"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info := channel.ExportRegistration(IncludeSettings)
|
||||||
|
settings := info.Settings
|
||||||
|
if !csPrivsCheck(info, client, rb) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch strings.ToLower(setting) {
|
||||||
|
case "history":
|
||||||
|
settings.History, err = historyStatusFromString(value)
|
||||||
|
if err != nil {
|
||||||
|
err = errInvalidParams
|
||||||
|
break
|
||||||
|
}
|
||||||
|
channel.SetSettings(settings)
|
||||||
|
channel.resizeHistory(server.Config())
|
||||||
|
}
|
||||||
|
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
csNotice(rb, client.t("Successfully changed the channel settings"))
|
||||||
|
displayChannelSetting(setting, settings, client, rb)
|
||||||
|
case errInvalidParams:
|
||||||
|
csNotice(rb, client.t("Invalid parameters"))
|
||||||
|
default:
|
||||||
|
server.logger.Error("internal", "CS SET error:", err.Error())
|
||||||
|
csNotice(rb, client.t("An error occurred"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
318
irc/client.go
318
irc/client.go
@ -29,7 +29,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
|
// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
|
||||||
IdentTimeoutSeconds = 1.5
|
IdentTimeoutSeconds = 1.5
|
||||||
IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
|
IRCv3TimestampFormat = utils.IRCv3TimestampFormat
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResumeDetails is a place to stash data at various stages of
|
// ResumeDetails is a place to stash data at various stages of
|
||||||
@ -45,6 +45,7 @@ type ResumeDetails struct {
|
|||||||
type Client struct {
|
type Client struct {
|
||||||
account string
|
account string
|
||||||
accountName string // display name of the account: uncasefolded, '*' if not logged in
|
accountName string // display name of the account: uncasefolded, '*' if not logged in
|
||||||
|
accountRegDate time.Time
|
||||||
accountSettings AccountSettings
|
accountSettings AccountSettings
|
||||||
atime time.Time
|
atime time.Time
|
||||||
away bool
|
away bool
|
||||||
@ -55,12 +56,12 @@ type Client struct {
|
|||||||
ctime time.Time
|
ctime time.Time
|
||||||
destroyed bool
|
destroyed bool
|
||||||
exitedSnomaskSent bool
|
exitedSnomaskSent bool
|
||||||
flags modes.ModeSet
|
modes modes.ModeSet
|
||||||
hostname string
|
hostname string
|
||||||
invitedTo map[string]bool
|
invitedTo map[string]bool
|
||||||
isSTSOnly bool
|
isSTSOnly bool
|
||||||
isTor bool
|
|
||||||
languages []string
|
languages []string
|
||||||
|
lastSignoff time.Time // for always-on clients, the time their last session quit
|
||||||
loginThrottle connection_limits.GenericThrottle
|
loginThrottle connection_limits.GenericThrottle
|
||||||
nick string
|
nick string
|
||||||
nickCasefolded string
|
nickCasefolded string
|
||||||
@ -84,9 +85,12 @@ type Client struct {
|
|||||||
skeleton string
|
skeleton string
|
||||||
sessions []*Session
|
sessions []*Session
|
||||||
stateMutex sync.RWMutex // tier 1
|
stateMutex sync.RWMutex // tier 1
|
||||||
|
alwaysOn bool
|
||||||
username string
|
username string
|
||||||
vhost string
|
vhost string
|
||||||
history history.Buffer
|
history history.Buffer
|
||||||
|
dirtyBits uint
|
||||||
|
writerSemaphore utils.Semaphore // tier 1.5
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session is an individual client connection to the server (TCP connection
|
// Session is an individual client connection to the server (TCP connection
|
||||||
@ -102,6 +106,7 @@ type Session struct {
|
|||||||
realIP net.IP
|
realIP net.IP
|
||||||
proxiedIP net.IP
|
proxiedIP net.IP
|
||||||
rawHostname string
|
rawHostname string
|
||||||
|
isTor bool
|
||||||
|
|
||||||
idletimer IdleTimer
|
idletimer IdleTimer
|
||||||
fakelag Fakelag
|
fakelag Fakelag
|
||||||
@ -120,6 +125,7 @@ type Session struct {
|
|||||||
resumeID string
|
resumeID string
|
||||||
resumeDetails *ResumeDetails
|
resumeDetails *ResumeDetails
|
||||||
zncPlaybackTimes *zncPlaybackTimes
|
zncPlaybackTimes *zncPlaybackTimes
|
||||||
|
lastSignoff time.Time
|
||||||
|
|
||||||
batch MultilineBatch
|
batch MultilineBatch
|
||||||
}
|
}
|
||||||
@ -147,6 +153,13 @@ func (sd *Session) SetQuitMessage(message string) (set bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Session) IP() net.IP {
|
||||||
|
if s.proxiedIP != nil {
|
||||||
|
return s.proxiedIP
|
||||||
|
}
|
||||||
|
return s.realIP
|
||||||
|
}
|
||||||
|
|
||||||
// returns whether the session was actively destroyed (for example, by ping
|
// returns whether the session was actively destroyed (for example, by ping
|
||||||
// timeout or NS GHOST).
|
// timeout or NS GHOST).
|
||||||
// avoids a race condition between asynchronous idle-timing-out of sessions,
|
// avoids a race condition between asynchronous idle-timing-out of sessions,
|
||||||
@ -164,8 +177,7 @@ func (session *Session) SetDestroyed() {
|
|||||||
// returns whether the client supports a smart history replay cap,
|
// returns whether the client supports a smart history replay cap,
|
||||||
// and therefore autoreplay-on-join and similar should be suppressed
|
// and therefore autoreplay-on-join and similar should be suppressed
|
||||||
func (session *Session) HasHistoryCaps() bool {
|
func (session *Session) HasHistoryCaps() bool {
|
||||||
// TODO the chathistory cap will go here as well
|
return session.capabilities.Has(caps.Chathistory) || session.capabilities.Has(caps.ZNCPlayback)
|
||||||
return session.capabilities.Has(caps.ZNCPlayback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// generates a batch ID. the uniqueness requirements for this are fairly weak:
|
// generates a batch ID. the uniqueness requirements for this are fairly weak:
|
||||||
@ -231,7 +243,6 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
|
|||||||
channels: make(ChannelSet),
|
channels: make(ChannelSet),
|
||||||
ctime: now,
|
ctime: now,
|
||||||
isSTSOnly: conn.Config.STSOnly,
|
isSTSOnly: conn.Config.STSOnly,
|
||||||
isTor: conn.Config.Tor,
|
|
||||||
languages: server.Languages().Default(),
|
languages: server.Languages().Default(),
|
||||||
loginThrottle: connection_limits.GenericThrottle{
|
loginThrottle: connection_limits.GenericThrottle{
|
||||||
Duration: config.Accounts.LoginThrottling.Duration,
|
Duration: config.Accounts.LoginThrottling.Duration,
|
||||||
@ -253,6 +264,7 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
|
|||||||
ctime: now,
|
ctime: now,
|
||||||
atime: now,
|
atime: now,
|
||||||
realIP: realIP,
|
realIP: realIP,
|
||||||
|
isTor: conn.Config.Tor,
|
||||||
}
|
}
|
||||||
client.sessions = []*Session{session}
|
client.sessions = []*Session{session}
|
||||||
|
|
||||||
@ -272,7 +284,7 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
|
|||||||
client.rawHostname = session.rawHostname
|
client.rawHostname = session.rawHostname
|
||||||
} else {
|
} else {
|
||||||
remoteAddr := conn.Conn.RemoteAddr()
|
remoteAddr := conn.Conn.RemoteAddr()
|
||||||
if utils.AddrIsLocal(remoteAddr) {
|
if realIP.IsLoopback() || utils.IPInNets(realIP, config.Server.secureNets) {
|
||||||
// treat local connections as secure (may be overridden later by WEBIRC)
|
// treat local connections as secure (may be overridden later by WEBIRC)
|
||||||
client.SetMode(modes.TLS, true)
|
client.SetMode(modes.TLS, true)
|
||||||
}
|
}
|
||||||
@ -286,10 +298,65 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
|
|||||||
client.run(session, proxyLine)
|
client.run(session, proxyLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
config := server.Config()
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
atime: now,
|
||||||
|
channels: make(ChannelSet),
|
||||||
|
ctime: now,
|
||||||
|
languages: server.Languages().Default(),
|
||||||
|
server: server,
|
||||||
|
|
||||||
|
// TODO figure out how to set these on reattach?
|
||||||
|
username: "~user",
|
||||||
|
rawHostname: server.name,
|
||||||
|
realIP: utils.IPv4LoopbackAddress,
|
||||||
|
|
||||||
|
alwaysOn: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
client.SetMode(modes.TLS, true)
|
||||||
|
client.writerSemaphore.Initialize(1)
|
||||||
|
client.history.Initialize(0, 0)
|
||||||
|
client.brbTimer.Initialize(client)
|
||||||
|
|
||||||
|
server.accounts.Login(client, account)
|
||||||
|
|
||||||
|
client.resizeHistory(config)
|
||||||
|
|
||||||
|
_, err := server.clients.SetNick(client, nil, account.Name)
|
||||||
|
if err != nil {
|
||||||
|
server.logger.Error("internal", "could not establish always-on client", account.Name, err.Error())
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
server.logger.Debug("accounts", "established always-on client", account.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX set this last to avoid confusing SetNick:
|
||||||
|
client.registered = true
|
||||||
|
|
||||||
|
for _, chname := range chnames {
|
||||||
|
// XXX we're using isSajoin=true, to make these joins succeed even without channel key
|
||||||
|
// this is *probably* ok as long as the persisted memberships are accurate
|
||||||
|
server.channels.Join(client, chname, "", true, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) resizeHistory(config *Config) {
|
||||||
|
_, ephemeral := client.historyStatus(config)
|
||||||
|
if ephemeral {
|
||||||
|
client.history.Resize(config.History.ClientLength, config.History.AutoresizeWindow)
|
||||||
|
} else {
|
||||||
|
client.history.Resize(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
|
// resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
|
||||||
// and sending appropriate notices to the client
|
// and sending appropriate notices to the client
|
||||||
func (client *Client) lookupHostname(session *Session, overwrite bool) {
|
func (client *Client) lookupHostname(session *Session, overwrite bool) {
|
||||||
if client.isTor {
|
if session.isTor {
|
||||||
return
|
return
|
||||||
} // else: even if cloaking is enabled, look up the real hostname to show to operators
|
} // else: even if cloaking is enabled, look up the real hostname to show to operators
|
||||||
|
|
||||||
@ -384,14 +451,14 @@ const (
|
|||||||
authFailSaslRequired
|
authFailSaslRequired
|
||||||
)
|
)
|
||||||
|
|
||||||
func (client *Client) isAuthorized(config *Config) AuthOutcome {
|
func (client *Client) isAuthorized(config *Config, isTor bool) AuthOutcome {
|
||||||
saslSent := client.account != ""
|
saslSent := client.account != ""
|
||||||
// PASS requirement
|
// PASS requirement
|
||||||
if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) {
|
if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) {
|
||||||
return authFailPass
|
return authFailPass
|
||||||
}
|
}
|
||||||
// Tor connections may be required to authenticate with SASL
|
// Tor connections may be required to authenticate with SASL
|
||||||
if client.isTor && config.Server.TorListeners.RequireSasl && !saslSent {
|
if isTor && config.Server.TorListeners.RequireSasl && !saslSent {
|
||||||
return authFailTorSaslRequired
|
return authFailTorSaslRequired
|
||||||
}
|
}
|
||||||
// finally, enforce require-sasl
|
// finally, enforce require-sasl
|
||||||
@ -572,9 +639,13 @@ func (client *Client) run(session *Session, proxyLine string) {
|
|||||||
|
|
||||||
func (client *Client) playReattachMessages(session *Session) {
|
func (client *Client) playReattachMessages(session *Session) {
|
||||||
client.server.playRegistrationBurst(session)
|
client.server.playRegistrationBurst(session)
|
||||||
|
hasHistoryCaps := session.HasHistoryCaps()
|
||||||
for _, channel := range session.client.Channels() {
|
for _, channel := range session.client.Channels() {
|
||||||
channel.playJoinForSession(session)
|
channel.playJoinForSession(session)
|
||||||
// clients should receive autoreplay-on-join lines, if applicable;
|
// clients should receive autoreplay-on-join lines, if applicable:
|
||||||
|
if hasHistoryCaps {
|
||||||
|
continue
|
||||||
|
}
|
||||||
// if they negotiated znc.in/playback or chathistory, they will receive nothing,
|
// if they negotiated znc.in/playback or chathistory, they will receive nothing,
|
||||||
// because those caps disable autoreplay-on-join and they haven't sent the relevant
|
// because those caps disable autoreplay-on-join and they haven't sent the relevant
|
||||||
// *playback PRIVMSG or CHATHISTORY command yet
|
// *playback PRIVMSG or CHATHISTORY command yet
|
||||||
@ -582,6 +653,12 @@ func (client *Client) playReattachMessages(session *Session) {
|
|||||||
channel.autoReplayHistory(client, rb, "")
|
channel.autoReplayHistory(client, rb, "")
|
||||||
rb.Send(true)
|
rb.Send(true)
|
||||||
}
|
}
|
||||||
|
if !session.lastSignoff.IsZero() && !hasHistoryCaps {
|
||||||
|
rb := NewResponseBuffer(session)
|
||||||
|
zncPlayPrivmsgs(client, rb, session.lastSignoff, time.Time{})
|
||||||
|
rb.Send(true)
|
||||||
|
}
|
||||||
|
session.lastSignoff = time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -634,11 +711,6 @@ func (session *Session) tryResume() (success bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldClient.isTor != client.isTor {
|
|
||||||
session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection from Tor to non-Tor or vice versa"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := server.clients.Resume(oldClient, session)
|
err := server.clients.Resume(oldClient, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
|
session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
|
||||||
@ -657,37 +729,45 @@ func (session *Session) tryResume() (success bool) {
|
|||||||
func (session *Session) playResume() {
|
func (session *Session) playResume() {
|
||||||
client := session.client
|
client := session.client
|
||||||
server := client.server
|
server := client.server
|
||||||
|
config := server.Config()
|
||||||
|
|
||||||
friends := make(ClientSet)
|
friends := make(ClientSet)
|
||||||
oldestLostMessage := time.Now().UTC()
|
var oldestLostMessage time.Time
|
||||||
|
|
||||||
// work out how much time, if any, is not covered by history buffers
|
// work out how much time, if any, is not covered by history buffers
|
||||||
|
// assume that a persistent buffer covers the whole resume period
|
||||||
for _, channel := range client.Channels() {
|
for _, channel := range client.Channels() {
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
friends.Add(member)
|
friends.Add(member)
|
||||||
|
}
|
||||||
|
_, ephemeral, _ := channel.historyStatus(config)
|
||||||
|
if ephemeral {
|
||||||
lastDiscarded := channel.history.LastDiscarded()
|
lastDiscarded := channel.history.LastDiscarded()
|
||||||
if lastDiscarded.Before(oldestLostMessage) {
|
if oldestLostMessage.Before(lastDiscarded) {
|
||||||
oldestLostMessage = lastDiscarded
|
oldestLostMessage = lastDiscarded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
privmsgMatcher := func(item history.Item) bool {
|
_, cEphemeral := client.historyStatus(config)
|
||||||
return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
|
if cEphemeral {
|
||||||
|
lastDiscarded := client.history.LastDiscarded()
|
||||||
|
if oldestLostMessage.Before(lastDiscarded) {
|
||||||
|
oldestLostMessage = lastDiscarded
|
||||||
|
}
|
||||||
}
|
}
|
||||||
privmsgHistory := client.history.Match(privmsgMatcher, false, 0)
|
_, privmsgSeq, _ := server.GetHistorySequence(nil, client, "*")
|
||||||
lastDiscarded := client.history.LastDiscarded()
|
if privmsgSeq != nil {
|
||||||
if lastDiscarded.Before(oldestLostMessage) {
|
privmsgs, _, _ := privmsgSeq.Between(history.Selector{}, history.Selector{}, config.History.ClientLength)
|
||||||
oldestLostMessage = lastDiscarded
|
for _, item := range privmsgs {
|
||||||
}
|
sender := server.clients.Get(stripMaskFromNick(item.Nick))
|
||||||
for _, item := range privmsgHistory {
|
if sender != nil {
|
||||||
sender := server.clients.Get(stripMaskFromNick(item.Nick))
|
friends.Add(sender)
|
||||||
if sender != nil {
|
}
|
||||||
friends.Add(sender)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timestamp := session.resumeDetails.Timestamp
|
timestamp := session.resumeDetails.Timestamp
|
||||||
gap := lastDiscarded.Sub(timestamp)
|
gap := oldestLostMessage.Sub(timestamp)
|
||||||
session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
|
session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
|
||||||
gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
|
gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
|
||||||
|
|
||||||
@ -723,10 +803,12 @@ func (session *Session) playResume() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
|
if session.resumeDetails.HistoryIncomplete {
|
||||||
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))
|
if !timestamp.IsZero() {
|
||||||
} else {
|
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))
|
||||||
session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
|
} else {
|
||||||
|
session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
|
session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
|
||||||
@ -738,23 +820,26 @@ func (session *Session) playResume() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// replay direct PRIVSMG history
|
// replay direct PRIVSMG history
|
||||||
if !timestamp.IsZero() {
|
if !timestamp.IsZero() && privmsgSeq != nil {
|
||||||
now := time.Now().UTC()
|
after := history.Selector{Time: timestamp}
|
||||||
items, complete := client.history.Between(timestamp, now, false, 0)
|
items, complete, _ := privmsgSeq.Between(after, history.Selector{}, config.History.ZNCMax)
|
||||||
rb := NewResponseBuffer(client.Sessions()[0])
|
rb := NewResponseBuffer(session)
|
||||||
client.replayPrivmsgHistory(rb, items, complete)
|
client.replayPrivmsgHistory(rb, items, "", complete)
|
||||||
rb.Send(true)
|
rb.Send(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
session.resumeDetails = nil
|
session.resumeDetails = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
|
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, complete bool) {
|
||||||
var batchID string
|
var batchID string
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
nick := details.nick
|
nick := details.nick
|
||||||
if 0 < len(items) {
|
if 0 < len(items) {
|
||||||
batchID = rb.StartNestedHistoryBatch(nick)
|
if target == "" {
|
||||||
|
target = nick
|
||||||
|
}
|
||||||
|
batchID = rb.StartNestedHistoryBatch(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
allowTags := rb.session.capabilities.Has(caps.MessageTags)
|
allowTags := rb.session.capabilities.Has(caps.MessageTags)
|
||||||
@ -778,12 +863,12 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
|
|||||||
if allowTags {
|
if allowTags {
|
||||||
tags = item.Tags
|
tags = item.Tags
|
||||||
}
|
}
|
||||||
if item.Params[0] == "" {
|
if item.Params[0] == "" || item.Params[0] == nick {
|
||||||
// this message was sent *to* the client from another nick
|
// this message was sent *to* the client from another nick
|
||||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
|
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
|
||||||
} else {
|
} else {
|
||||||
// this message was sent *from* the client to another nick; the target is item.Params[0]
|
// this message was sent *from* the client to another nick; the target is item.Params[0]
|
||||||
// substitute the client's current nickmask in case they changed nick
|
// substitute client's current nickmask in case client changed nick
|
||||||
rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
|
rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -875,7 +960,7 @@ func (client *Client) HasRoleCapabs(capabs ...string) bool {
|
|||||||
|
|
||||||
// ModeString returns the mode string for this client.
|
// ModeString returns the mode string for this client.
|
||||||
func (client *Client) ModeString() (str string) {
|
func (client *Client) ModeString() (str string) {
|
||||||
return "+" + client.flags.String()
|
return "+" + client.modes.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Friends refers to clients that share a channel with this client.
|
// Friends refers to clients that share a channel with this client.
|
||||||
@ -1053,6 +1138,12 @@ func (client *Client) Quit(message string, session *Session) {
|
|||||||
// has no more sessions.
|
// has no more sessions.
|
||||||
func (client *Client) destroy(session *Session) {
|
func (client *Client) destroy(session *Session) {
|
||||||
var sessionsToDestroy []*Session
|
var sessionsToDestroy []*Session
|
||||||
|
var lastSignoff time.Time
|
||||||
|
if session != nil {
|
||||||
|
lastSignoff = session.idletimer.LastTouch()
|
||||||
|
} else {
|
||||||
|
lastSignoff = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
details := client.detailsNoMutex()
|
details := client.detailsNoMutex()
|
||||||
@ -1060,6 +1151,8 @@ func (client *Client) destroy(session *Session) {
|
|||||||
brbAt := client.brbTimer.brbAt
|
brbAt := client.brbTimer.brbAt
|
||||||
wasReattach := session != nil && session.client != client
|
wasReattach := session != nil && session.client != client
|
||||||
sessionRemoved := false
|
sessionRemoved := false
|
||||||
|
registered := client.registered
|
||||||
|
alwaysOn := client.alwaysOn
|
||||||
var remainingSessions int
|
var remainingSessions int
|
||||||
if session == nil {
|
if session == nil {
|
||||||
sessionsToDestroy = client.sessions
|
sessionsToDestroy = client.sessions
|
||||||
@ -1074,12 +1167,15 @@ func (client *Client) destroy(session *Session) {
|
|||||||
|
|
||||||
// should we destroy the whole client this time?
|
// should we destroy the whole client this time?
|
||||||
// BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
|
// BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
|
||||||
brbEligible := session != nil && (brbState == BrbEnabled || brbState == BrbSticky)
|
brbEligible := session != nil && (brbState == BrbEnabled || alwaysOn)
|
||||||
shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible
|
shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible
|
||||||
if shouldDestroy {
|
if shouldDestroy {
|
||||||
// if it's our job to destroy it, don't let anyone else try
|
// if it's our job to destroy it, don't let anyone else try
|
||||||
client.destroyed = true
|
client.destroyed = true
|
||||||
}
|
}
|
||||||
|
if alwaysOn && remainingSessions == 0 {
|
||||||
|
client.lastSignoff = lastSignoff
|
||||||
|
}
|
||||||
exitedSnomaskSent := client.exitedSnomaskSent
|
exitedSnomaskSent := client.exitedSnomaskSent
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
|
|
||||||
@ -1099,7 +1195,7 @@ func (client *Client) destroy(session *Session) {
|
|||||||
|
|
||||||
// remove from connection limits
|
// remove from connection limits
|
||||||
var source string
|
var source string
|
||||||
if client.isTor {
|
if session.isTor {
|
||||||
client.server.torLimiter.RemoveClient()
|
client.server.torLimiter.RemoveClient()
|
||||||
source = "tor"
|
source = "tor"
|
||||||
} else {
|
} else {
|
||||||
@ -1113,11 +1209,33 @@ func (client *Client) destroy(session *Session) {
|
|||||||
client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
|
client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decrement stats if we have no more sessions, even if the client will not be destroyed
|
||||||
|
if shouldDestroy || remainingSessions == 0 {
|
||||||
|
invisible := client.HasMode(modes.Invisible)
|
||||||
|
operator := client.HasMode(modes.LocalOperator) || client.HasMode(modes.Operator)
|
||||||
|
client.server.stats.Remove(registered, invisible, operator)
|
||||||
|
}
|
||||||
|
|
||||||
// do not destroy the client if it has either remaining sessions, or is BRB'ed
|
// do not destroy the client if it has either remaining sessions, or is BRB'ed
|
||||||
if !shouldDestroy {
|
if !shouldDestroy {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
splitQuitMessage := utils.MakeMessage(quitMessage)
|
||||||
|
quitItem := history.Item{
|
||||||
|
Type: history.Quit,
|
||||||
|
Nick: details.nickMask,
|
||||||
|
AccountName: details.accountName,
|
||||||
|
Message: splitQuitMessage,
|
||||||
|
}
|
||||||
|
var channels []*Channel
|
||||||
|
defer func() {
|
||||||
|
for _, channel := range channels {
|
||||||
|
// TODO it's dangerous to write to mysql while holding the destroy semaphore
|
||||||
|
channel.AddHistoryItem(quitItem)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// see #235: deduplicating the list of PART recipients uses (comparatively speaking)
|
// see #235: deduplicating the list of PART recipients uses (comparatively speaking)
|
||||||
// a lot of RAM, so limit concurrency to avoid thrashing
|
// a lot of RAM, so limit concurrency to avoid thrashing
|
||||||
client.server.semaphores.ClientDestroy.Acquire()
|
client.server.semaphores.ClientDestroy.Acquire()
|
||||||
@ -1127,7 +1245,6 @@ func (client *Client) destroy(session *Session) {
|
|||||||
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
|
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
|
||||||
}
|
}
|
||||||
|
|
||||||
registered := client.Registered()
|
|
||||||
if registered {
|
if registered {
|
||||||
client.server.whoWas.Append(client.WhoWas())
|
client.server.whoWas.Append(client.WhoWas())
|
||||||
}
|
}
|
||||||
@ -1141,18 +1258,12 @@ func (client *Client) destroy(session *Session) {
|
|||||||
// clean up monitor state
|
// clean up monitor state
|
||||||
client.server.monitorManager.RemoveAll(client)
|
client.server.monitorManager.RemoveAll(client)
|
||||||
|
|
||||||
splitQuitMessage := utils.MakeMessage(quitMessage)
|
|
||||||
// clean up channels
|
// clean up channels
|
||||||
// (note that if this is a reattach, client has no channels and therefore no friends)
|
// (note that if this is a reattach, client has no channels and therefore no friends)
|
||||||
friends := make(ClientSet)
|
friends := make(ClientSet)
|
||||||
for _, channel := range client.Channels() {
|
channels = client.Channels()
|
||||||
|
for _, channel := range channels {
|
||||||
channel.Quit(client)
|
channel.Quit(client)
|
||||||
channel.history.Add(history.Item{
|
|
||||||
Type: history.Quit,
|
|
||||||
Nick: details.nickMask,
|
|
||||||
AccountName: details.accountName,
|
|
||||||
Message: splitQuitMessage,
|
|
||||||
})
|
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
friends.Add(member)
|
friends.Add(member)
|
||||||
}
|
}
|
||||||
@ -1168,9 +1279,6 @@ func (client *Client) destroy(session *Session) {
|
|||||||
|
|
||||||
client.server.accounts.Logout(client)
|
client.server.accounts.Logout(client)
|
||||||
|
|
||||||
client.server.stats.Remove(registered, client.HasMode(modes.Invisible),
|
|
||||||
client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator))
|
|
||||||
|
|
||||||
// this happens under failure to return from BRB
|
// this happens under failure to return from BRB
|
||||||
if quitMessage == "" {
|
if quitMessage == "" {
|
||||||
if brbState == BrbDead && !brbAt.IsZero() {
|
if brbState == BrbDead && !brbAt.IsZero() {
|
||||||
@ -1196,11 +1304,10 @@ func (client *Client) destroy(session *Session) {
|
|||||||
// SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
|
// SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
|
||||||
// Adds account-tag to the line as well.
|
// Adds account-tag to the line as well.
|
||||||
func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
|
func (session *Session) sendSplitMsgFromClientInternal(blocking bool, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
|
||||||
// TODO no maxline support
|
|
||||||
if message.Is512() {
|
if message.Is512() {
|
||||||
session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
|
session.sendFromClientInternal(blocking, message.Time, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
|
||||||
} else {
|
} else {
|
||||||
if message.IsMultiline() && session.capabilities.Has(caps.Multiline) {
|
if session.capabilities.Has(caps.Multiline) {
|
||||||
for _, msg := range session.composeMultilineBatch(nickmask, accountName, tags, command, target, message) {
|
for _, msg := range session.composeMultilineBatch(nickmask, accountName, tags, command, target, message) {
|
||||||
session.SendRawMessage(msg, blocking)
|
session.SendRawMessage(msg, blocking)
|
||||||
}
|
}
|
||||||
@ -1366,13 +1473,23 @@ func (session *Session) Notice(text string) {
|
|||||||
func (client *Client) addChannel(channel *Channel) {
|
func (client *Client) addChannel(channel *Channel) {
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
client.channels[channel] = true
|
client.channels[channel] = true
|
||||||
|
alwaysOn := client.alwaysOn
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if alwaysOn {
|
||||||
|
client.markDirty(IncludeChannels)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) removeChannel(channel *Channel) {
|
func (client *Client) removeChannel(channel *Channel) {
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
delete(client.channels, channel)
|
delete(client.channels, channel)
|
||||||
|
alwaysOn := client.alwaysOn
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if alwaysOn {
|
||||||
|
client.markDirty(IncludeChannels)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Records that the client has been invited to join an invite-only channel
|
// Records that the client has been invited to join an invite-only channel
|
||||||
@ -1413,3 +1530,84 @@ func (client *Client) attemptAutoOper(session *Session) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) historyStatus(config *Config) (persistent, ephemeral bool) {
|
||||||
|
if !config.History.Enabled {
|
||||||
|
return false, false
|
||||||
|
} else if !config.History.Persistent.Enabled {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
client.stateMutex.RLock()
|
||||||
|
alwaysOn := client.alwaysOn
|
||||||
|
historyStatus := client.accountSettings.DMHistory
|
||||||
|
client.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
if !alwaysOn {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
historyStatus = historyEnabled(config.History.Persistent.DirectMessages, historyStatus)
|
||||||
|
ephemeral = (historyStatus == HistoryEphemeral)
|
||||||
|
persistent = (historyStatus == HistoryPersistent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// these are bit flags indicating what part of the client status is "dirty"
|
||||||
|
// and needs to be read from memory and written to the db
|
||||||
|
// TODO add a dirty flag for lastSignoff
|
||||||
|
const (
|
||||||
|
IncludeChannels uint = 1 << iota
|
||||||
|
)
|
||||||
|
|
||||||
|
func (client *Client) markDirty(dirtyBits uint) {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
alwaysOn := client.alwaysOn
|
||||||
|
client.dirtyBits = client.dirtyBits | dirtyBits
|
||||||
|
client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if alwaysOn {
|
||||||
|
client.wakeWriter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) wakeWriter() {
|
||||||
|
if client.writerSemaphore.TryAcquire() {
|
||||||
|
go client.writeLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) writeLoop() {
|
||||||
|
for {
|
||||||
|
client.performWrite()
|
||||||
|
client.writerSemaphore.Release()
|
||||||
|
|
||||||
|
client.stateMutex.RLock()
|
||||||
|
isDirty := client.dirtyBits != 0
|
||||||
|
client.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
if !isDirty || !client.writerSemaphore.TryAcquire() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) performWrite() {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
// TODO actually read dirtyBits in the future
|
||||||
|
client.dirtyBits = 0
|
||||||
|
account := client.account
|
||||||
|
client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if account == "" {
|
||||||
|
client.server.logger.Error("internal", "attempting to persist logged-out client", client.Nick())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channels := client.Channels()
|
||||||
|
channelNames := make([]string, len(channels))
|
||||||
|
for i, channel := range channels {
|
||||||
|
channelNames[i] = channel.Name()
|
||||||
|
}
|
||||||
|
client.server.accounts.saveChannels(account, channelNames)
|
||||||
|
}
|
||||||
|
@ -105,7 +105,8 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
|
|||||||
return errNickMissing
|
return errNickMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
if !oldClient.AddSession(session) {
|
success, _, _ := oldClient.AddSession(session)
|
||||||
|
if !success {
|
||||||
return errNickMissing
|
return errNickMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,32 +114,50 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetNick sets a client's nickname, validating it against nicknames in use
|
// SetNick sets a client's nickname, validating it against nicknames in use
|
||||||
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) error {
|
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) (setNick string, err error) {
|
||||||
if len(newNick) > client.server.Config().Limits.NickLen {
|
|
||||||
return errNicknameInvalid
|
|
||||||
}
|
|
||||||
newcfnick, err := CasefoldName(newNick)
|
newcfnick, err := CasefoldName(newNick)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errNicknameInvalid
|
return "", errNicknameInvalid
|
||||||
|
}
|
||||||
|
if len(newcfnick) > client.server.Config().Limits.NickLen {
|
||||||
|
return "", errNicknameInvalid
|
||||||
}
|
}
|
||||||
newSkeleton, err := Skeleton(newNick)
|
newSkeleton, err := Skeleton(newNick)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errNicknameInvalid
|
return "", errNicknameInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
|
if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
|
||||||
return errNicknameInvalid
|
return "", errNicknameInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
|
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
|
||||||
account := client.Account()
|
|
||||||
config := client.server.Config()
|
config := client.server.Config()
|
||||||
|
client.stateMutex.RLock()
|
||||||
|
account := client.account
|
||||||
|
accountName := client.accountName
|
||||||
|
settings := client.accountSettings
|
||||||
|
registered := client.registered
|
||||||
|
realname := client.realname
|
||||||
|
client.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
// recompute this (client.alwaysOn is not set for unregistered clients):
|
||||||
|
alwaysOn := account != "" && persistenceEnabled(config.Accounts.Bouncer.AlwaysOn, settings.AlwaysOn)
|
||||||
|
|
||||||
|
if alwaysOn && registered {
|
||||||
|
return "", errCantChangeNick
|
||||||
|
}
|
||||||
|
|
||||||
var bouncerAllowed bool
|
var bouncerAllowed bool
|
||||||
if config.Accounts.Bouncer.Enabled {
|
if config.Accounts.Bouncer.Enabled {
|
||||||
if session != nil && session.capabilities.Has(caps.Bouncer) {
|
if alwaysOn {
|
||||||
|
// ignore the pre-reg nick, force a reattach
|
||||||
|
newNick = accountName
|
||||||
|
newcfnick = account
|
||||||
|
bouncerAllowed = true
|
||||||
|
} else if session != nil && session.capabilities.Has(caps.Bouncer) {
|
||||||
bouncerAllowed = true
|
bouncerAllowed = true
|
||||||
} else {
|
} else {
|
||||||
settings := client.AccountSettings()
|
|
||||||
if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
|
if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
|
||||||
bouncerAllowed = true
|
bouncerAllowed = true
|
||||||
} else if settings.AllowBouncer == BouncerAllowedByUser {
|
} else if settings.AllowBouncer == BouncerAllowedByUser {
|
||||||
@ -154,28 +173,41 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
|||||||
// the client may just be changing case
|
// the client may just be changing case
|
||||||
if currentClient != nil && currentClient != client && session != nil {
|
if currentClient != nil && currentClient != client && session != nil {
|
||||||
// these conditions forbid reattaching to an existing session:
|
// these conditions forbid reattaching to an existing session:
|
||||||
if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
|
if registered || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
|
||||||
return errNicknameInUse
|
return "", errNicknameInUse
|
||||||
}
|
}
|
||||||
if !currentClient.AddSession(session) {
|
reattachSuccessful, numSessions, lastSignoff := currentClient.AddSession(session)
|
||||||
return errNicknameInUse
|
if !reattachSuccessful {
|
||||||
|
return "", errNicknameInUse
|
||||||
}
|
}
|
||||||
|
if numSessions == 1 {
|
||||||
|
invisible := client.HasMode(modes.Invisible)
|
||||||
|
operator := client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator)
|
||||||
|
client.server.stats.AddRegistered(invisible, operator)
|
||||||
|
}
|
||||||
|
session.lastSignoff = lastSignoff
|
||||||
|
// XXX SetNames only changes names if they are unset, so the realname change only
|
||||||
|
// takes effect on first attach to an always-on client (good), but the user/ident
|
||||||
|
// change is always a no-op (bad). we could make user/ident act the same way as
|
||||||
|
// realname, but then we'd have to send CHGHOST and i don't want to deal with that
|
||||||
|
// for performance reasons
|
||||||
|
currentClient.SetNames("user", realname, true)
|
||||||
// successful reattach!
|
// successful reattach!
|
||||||
return nil
|
return newNick, nil
|
||||||
}
|
}
|
||||||
// analogous checks for skeletons
|
// analogous checks for skeletons
|
||||||
skeletonHolder := clients.bySkeleton[newSkeleton]
|
skeletonHolder := clients.bySkeleton[newSkeleton]
|
||||||
if skeletonHolder != nil && skeletonHolder != client {
|
if skeletonHolder != nil && skeletonHolder != client {
|
||||||
return errNicknameInUse
|
return "", errNicknameInUse
|
||||||
}
|
}
|
||||||
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
|
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
|
||||||
return errNicknameReserved
|
return "", errNicknameReserved
|
||||||
}
|
}
|
||||||
clients.removeInternal(client)
|
clients.removeInternal(client)
|
||||||
clients.byNick[newcfnick] = client
|
clients.byNick[newcfnick] = client
|
||||||
clients.bySkeleton[newSkeleton] = client
|
clients.bySkeleton[newSkeleton] = client
|
||||||
client.updateNick(newNick, newcfnick, newSkeleton)
|
client.updateNick(newNick, newcfnick, newSkeleton)
|
||||||
return nil
|
return newNick, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (clients *ClientManager) AllClients() (result []*Client) {
|
func (clients *ClientManager) AllClients() (result []*Client) {
|
||||||
|
@ -54,6 +54,12 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
|
|||||||
return cmd.handler(server, client, msg, rb)
|
return cmd.handler(server, client, msg, rb)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// most servers do this only for PING/PONG, but we'll do it for any command:
|
||||||
|
if client.registered {
|
||||||
|
// touch even if `exiting`, so we record the time of a QUIT accurately
|
||||||
|
session.idletimer.Touch()
|
||||||
|
}
|
||||||
|
|
||||||
if exiting {
|
if exiting {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -63,11 +69,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
|
|||||||
exiting = server.tryRegister(client, session)
|
exiting = server.tryRegister(client, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// most servers do this only for PING/PONG, but we'll do it for any command:
|
|
||||||
if client.registered {
|
|
||||||
session.idletimer.Touch()
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.registered && !cmd.leaveClientIdle {
|
if client.registered && !cmd.leaveClientIdle {
|
||||||
client.Active(session)
|
client.Active(session)
|
||||||
}
|
}
|
||||||
@ -109,7 +110,7 @@ func init() {
|
|||||||
},
|
},
|
||||||
"CHATHISTORY": {
|
"CHATHISTORY": {
|
||||||
handler: chathistoryHandler,
|
handler: chathistoryHandler,
|
||||||
minParams: 3,
|
minParams: 4,
|
||||||
},
|
},
|
||||||
"DEBUG": {
|
"DEBUG": {
|
||||||
handler: debugHandler,
|
handler: debugHandler,
|
||||||
|
188
irc/config.go
188
irc/config.go
@ -61,6 +61,151 @@ type listenerConfig struct {
|
|||||||
ProxyBeforeTLS bool
|
ProxyBeforeTLS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PersistentStatus uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
PersistentUnspecified PersistentStatus = iota
|
||||||
|
PersistentDisabled
|
||||||
|
PersistentOptIn
|
||||||
|
PersistentOptOut
|
||||||
|
PersistentMandatory
|
||||||
|
)
|
||||||
|
|
||||||
|
func persistentStatusToString(status PersistentStatus) string {
|
||||||
|
switch status {
|
||||||
|
case PersistentUnspecified:
|
||||||
|
return "default"
|
||||||
|
case PersistentDisabled:
|
||||||
|
return "disabled"
|
||||||
|
case PersistentOptIn:
|
||||||
|
return "opt-in"
|
||||||
|
case PersistentOptOut:
|
||||||
|
return "opt-out"
|
||||||
|
case PersistentMandatory:
|
||||||
|
return "mandatory"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistentStatusFromString(status string) (PersistentStatus, error) {
|
||||||
|
switch strings.ToLower(status) {
|
||||||
|
case "default":
|
||||||
|
return PersistentUnspecified, nil
|
||||||
|
case "":
|
||||||
|
return PersistentDisabled, nil
|
||||||
|
case "opt-in":
|
||||||
|
return PersistentOptIn, nil
|
||||||
|
case "opt-out":
|
||||||
|
return PersistentOptOut, nil
|
||||||
|
case "mandatory":
|
||||||
|
return PersistentMandatory, nil
|
||||||
|
default:
|
||||||
|
b, err := utils.StringToBool(status)
|
||||||
|
if b {
|
||||||
|
return PersistentMandatory, err
|
||||||
|
} else {
|
||||||
|
return PersistentDisabled, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PersistentStatus) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var orig string
|
||||||
|
var err error
|
||||||
|
if err = unmarshal(&orig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result, err := persistentStatusFromString(orig)
|
||||||
|
if err == nil {
|
||||||
|
if result == PersistentUnspecified {
|
||||||
|
result = PersistentDisabled
|
||||||
|
}
|
||||||
|
*ps = result
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistenceEnabled(serverSetting, clientSetting PersistentStatus) (enabled bool) {
|
||||||
|
if serverSetting == PersistentDisabled {
|
||||||
|
return false
|
||||||
|
} else if serverSetting == PersistentMandatory {
|
||||||
|
return true
|
||||||
|
} else if clientSetting == PersistentDisabled {
|
||||||
|
return false
|
||||||
|
} else if clientSetting == PersistentMandatory {
|
||||||
|
return true
|
||||||
|
} else if serverSetting == PersistentOptOut {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HistoryStatus uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
HistoryDefault HistoryStatus = iota
|
||||||
|
HistoryDisabled
|
||||||
|
HistoryEphemeral
|
||||||
|
HistoryPersistent
|
||||||
|
)
|
||||||
|
|
||||||
|
func historyStatusFromString(str string) (status HistoryStatus, err error) {
|
||||||
|
switch strings.ToLower(str) {
|
||||||
|
case "default":
|
||||||
|
return HistoryDefault, nil
|
||||||
|
case "ephemeral":
|
||||||
|
return HistoryEphemeral, nil
|
||||||
|
case "persistent":
|
||||||
|
return HistoryPersistent, nil
|
||||||
|
default:
|
||||||
|
b, err := utils.StringToBool(str)
|
||||||
|
if b {
|
||||||
|
return HistoryPersistent, err
|
||||||
|
} else {
|
||||||
|
return HistoryDisabled, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func historyStatusToString(status HistoryStatus) string {
|
||||||
|
switch status {
|
||||||
|
case HistoryDefault:
|
||||||
|
return "default"
|
||||||
|
case HistoryDisabled:
|
||||||
|
return "disabled"
|
||||||
|
case HistoryEphemeral:
|
||||||
|
return "ephemeral"
|
||||||
|
case HistoryPersistent:
|
||||||
|
return "persistent"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func historyEnabled(serverSetting PersistentStatus, localSetting HistoryStatus) (result HistoryStatus) {
|
||||||
|
if serverSetting == PersistentDisabled {
|
||||||
|
return HistoryDisabled
|
||||||
|
} else if serverSetting == PersistentMandatory {
|
||||||
|
return HistoryPersistent
|
||||||
|
} else if serverSetting == PersistentOptOut {
|
||||||
|
if localSetting == HistoryDefault {
|
||||||
|
return HistoryPersistent
|
||||||
|
} else {
|
||||||
|
return localSetting
|
||||||
|
}
|
||||||
|
} else if serverSetting == PersistentOptIn {
|
||||||
|
if localSetting >= HistoryEphemeral {
|
||||||
|
return localSetting
|
||||||
|
} else {
|
||||||
|
return HistoryDisabled
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return HistoryDisabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AccountConfig struct {
|
type AccountConfig struct {
|
||||||
Registration AccountRegistrationConfig
|
Registration AccountRegistrationConfig
|
||||||
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
||||||
@ -79,7 +224,8 @@ type AccountConfig struct {
|
|||||||
NickReservation NickReservationConfig `yaml:"nick-reservation"`
|
NickReservation NickReservationConfig `yaml:"nick-reservation"`
|
||||||
Bouncer struct {
|
Bouncer struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
AllowedByDefault bool `yaml:"allowed-by-default"`
|
AllowedByDefault bool `yaml:"allowed-by-default"`
|
||||||
|
AlwaysOn PersistentStatus `yaml:"always-on"`
|
||||||
}
|
}
|
||||||
VHosts VHostConfig
|
VHosts VHostConfig
|
||||||
}
|
}
|
||||||
@ -340,6 +486,8 @@ type Config struct {
|
|||||||
isupport isupport.List
|
isupport isupport.List
|
||||||
IPLimits connection_limits.LimiterConfig `yaml:"ip-limits"`
|
IPLimits connection_limits.LimiterConfig `yaml:"ip-limits"`
|
||||||
Cloaks cloaks.CloakConfig `yaml:"ip-cloaking"`
|
Cloaks cloaks.CloakConfig `yaml:"ip-cloaking"`
|
||||||
|
SecureNetDefs []string `yaml:"secure-nets"`
|
||||||
|
secureNets []net.IPNet
|
||||||
supportedCaps *caps.Set
|
supportedCaps *caps.Set
|
||||||
capValues caps.Values
|
capValues caps.Values
|
||||||
Casemapping Casemapping
|
Casemapping Casemapping
|
||||||
@ -356,6 +504,14 @@ type Config struct {
|
|||||||
Datastore struct {
|
Datastore struct {
|
||||||
Path string
|
Path string
|
||||||
AutoUpgrade bool
|
AutoUpgrade bool
|
||||||
|
MySQL struct {
|
||||||
|
Enabled bool
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
HistoryDatabase string `yaml:"history-database"`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Accounts AccountConfig
|
Accounts AccountConfig
|
||||||
@ -395,6 +551,18 @@ type Config struct {
|
|||||||
AutoresizeWindow time.Duration `yaml:"autoresize-window"`
|
AutoresizeWindow time.Duration `yaml:"autoresize-window"`
|
||||||
AutoreplayOnJoin int `yaml:"autoreplay-on-join"`
|
AutoreplayOnJoin int `yaml:"autoreplay-on-join"`
|
||||||
ChathistoryMax int `yaml:"chathistory-maxmessages"`
|
ChathistoryMax int `yaml:"chathistory-maxmessages"`
|
||||||
|
ZNCMax int `yaml:"znc-maxmessages"`
|
||||||
|
Restrictions struct {
|
||||||
|
ExpireTime time.Duration `yaml:"expire-time"`
|
||||||
|
EnforceRegistrationDate bool `yaml:"enforce-registration-date"`
|
||||||
|
GracePeriod time.Duration `yaml:"grace-period"`
|
||||||
|
}
|
||||||
|
Persistent struct {
|
||||||
|
Enabled bool
|
||||||
|
UnregisteredChannels bool `yaml:"unregistered-channels"`
|
||||||
|
RegisteredChannels PersistentStatus `yaml:"registered-channels"`
|
||||||
|
DirectMessages PersistentStatus `yaml:"direct-messages"`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Filename string
|
Filename string
|
||||||
@ -717,7 +885,10 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !config.Accounts.Bouncer.Enabled {
|
if !config.Accounts.Bouncer.Enabled {
|
||||||
|
config.Accounts.Bouncer.AlwaysOn = PersistentDisabled
|
||||||
config.Server.supportedCaps.Disable(caps.Bouncer)
|
config.Server.supportedCaps.Disable(caps.Bouncer)
|
||||||
|
} else if config.Accounts.Bouncer.AlwaysOn >= PersistentOptOut {
|
||||||
|
config.Accounts.Bouncer.AllowedByDefault = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var newLogConfigs []logger.LoggingConfig
|
var newLogConfigs []logger.LoggingConfig
|
||||||
@ -786,6 +957,11 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
return nil, fmt.Errorf("Could not parse proxy-allowed-from nets: %v", err.Error())
|
return nil, fmt.Errorf("Could not parse proxy-allowed-from nets: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Server.secureNets, err = utils.ParseNetList(config.Server.SecureNetDefs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Could not parse secure-nets: %v\n", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
|
rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
|
||||||
if rawRegexp != "" {
|
if rawRegexp != "" {
|
||||||
regexp, err := regexp.Compile(rawRegexp)
|
regexp, err := regexp.Compile(rawRegexp)
|
||||||
@ -882,6 +1058,16 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
config.History.ClientLength = 0
|
config.History.ClientLength = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !config.History.Enabled || !config.History.Persistent.Enabled {
|
||||||
|
config.History.Persistent.UnregisteredChannels = false
|
||||||
|
config.History.Persistent.RegisteredChannels = PersistentDisabled
|
||||||
|
config.History.Persistent.DirectMessages = PersistentDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.History.ZNCMax == 0 {
|
||||||
|
config.History.ZNCMax = config.History.ChathistoryMax
|
||||||
|
}
|
||||||
|
|
||||||
config.Server.Cloaks.Initialize()
|
config.Server.Cloaks.Initialize()
|
||||||
if config.Server.Cloaks.Enabled {
|
if config.Server.Cloaks.Enabled {
|
||||||
if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
|
if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
|
||||||
|
@ -36,22 +36,6 @@ type SchemaChange struct {
|
|||||||
// maps an initial version to a schema change capable of upgrading it
|
// maps an initial version to a schema change capable of upgrading it
|
||||||
var schemaChanges map[string]SchemaChange
|
var schemaChanges map[string]SchemaChange
|
||||||
|
|
||||||
type incompatibleSchemaError struct {
|
|
||||||
currentVersion string
|
|
||||||
requiredVersion string
|
|
||||||
}
|
|
||||||
|
|
||||||
func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
|
|
||||||
return &incompatibleSchemaError{
|
|
||||||
currentVersion: currentVersion,
|
|
||||||
requiredVersion: latestDbSchema,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *incompatibleSchemaError) Error() string {
|
|
||||||
return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitDB creates the database, implementing the `oragono initdb` command.
|
// InitDB creates the database, implementing the `oragono initdb` command.
|
||||||
func InitDB(path string) {
|
func InitDB(path string) {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
@ -129,7 +113,7 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
|
|||||||
// successful autoupgrade, let's try this again:
|
// successful autoupgrade, let's try this again:
|
||||||
return openDatabaseInternal(config, false)
|
return openDatabaseInternal(config, false)
|
||||||
} else {
|
} else {
|
||||||
err = IncompatibleSchemaError(version)
|
err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,7 +157,7 @@ func UpgradeDB(config *Config) (err error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
// unable to upgrade to the desired version, roll back
|
// unable to upgrade to the desired version, roll back
|
||||||
return IncompatibleSchemaError(version)
|
return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
|
||||||
}
|
}
|
||||||
log.Println("attempting to update schema from version " + version)
|
log.Println("attempting to update schema from version " + version)
|
||||||
err := change.Changer(config, tx)
|
err := change.Changer(config, tx)
|
||||||
|
@ -33,6 +33,7 @@ var (
|
|||||||
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
||||||
errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`)
|
errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`)
|
||||||
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
||||||
|
errChannelNotRegistered = errors.New("Channel is not registered")
|
||||||
errChannelNameInUse = errors.New(`Channel name in use`)
|
errChannelNameInUse = errors.New(`Channel name in use`)
|
||||||
errInvalidChannelName = errors.New(`Invalid channel name`)
|
errInvalidChannelName = errors.New(`Invalid channel name`)
|
||||||
errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
|
errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
|
||||||
@ -40,6 +41,7 @@ var (
|
|||||||
errNicknameInvalid = errors.New("invalid nickname")
|
errNicknameInvalid = errors.New("invalid nickname")
|
||||||
errNicknameInUse = errors.New("nickname in use")
|
errNicknameInUse = errors.New("nickname in use")
|
||||||
errNicknameReserved = errors.New("nickname is reserved")
|
errNicknameReserved = errors.New("nickname is reserved")
|
||||||
|
errCantChangeNick = errors.New(`Always-on clients can't change nicknames`)
|
||||||
errNoExistingBan = errors.New("Ban does not exist")
|
errNoExistingBan = errors.New("Ban does not exist")
|
||||||
errNoSuchChannel = errors.New(`No such channel`)
|
errNoSuchChannel = errors.New(`No such channel`)
|
||||||
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
||||||
|
@ -61,7 +61,7 @@ func (wc *webircConfig) Populate() (err error) {
|
|||||||
func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls bool) (err error, quitMsg string) {
|
func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls bool) (err error, quitMsg string) {
|
||||||
// PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself
|
// PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself
|
||||||
// is whitelisted:
|
// is whitelisted:
|
||||||
if client.isTor {
|
if session.isTor {
|
||||||
return errBadProxyLine, ""
|
return errBadProxyLine, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,21 +91,27 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) AddSession(session *Session) (success bool) {
|
func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSignoff time.Time) {
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
|
|
||||||
// client may be dying and ineligible to receive another session
|
// client may be dying and ineligible to receive another session
|
||||||
if client.destroyed {
|
if client.destroyed {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
// success, attach the new session to the client
|
// success, attach the new session to the client
|
||||||
session.client = client
|
session.client = client
|
||||||
newSessions := make([]*Session, len(client.sessions)+1)
|
newSessions := make([]*Session, len(client.sessions)+1)
|
||||||
copy(newSessions, client.sessions)
|
copy(newSessions, client.sessions)
|
||||||
newSessions[len(newSessions)-1] = session
|
newSessions[len(newSessions)-1] = session
|
||||||
|
if len(client.sessions) == 0 && client.accountSettings.AutoreplayMissed {
|
||||||
|
// n.b. this is only possible if client is persistent and remained
|
||||||
|
// on the server with no sessions:
|
||||||
|
lastSignoff = client.lastSignoff
|
||||||
|
client.lastSignoff = time.Time{}
|
||||||
|
}
|
||||||
client.sessions = newSessions
|
client.sessions = newSessions
|
||||||
return true
|
return true, len(client.sessions), lastSignoff
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) removeSession(session *Session) (success bool, length int) {
|
func (client *Client) removeSession(session *Session) (success bool, length int) {
|
||||||
@ -189,6 +195,13 @@ func (client *Client) SetExitedSnomaskSent() {
|
|||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) AlwaysOn() (alwaysOn bool) {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
alwaysOn = client.alwaysOn
|
||||||
|
client.stateMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// uniqueIdentifiers returns the strings for which the server enforces per-client
|
// uniqueIdentifiers returns the strings for which the server enforces per-client
|
||||||
// uniqueness/ownership; no two clients can have colliding casefolded nicks or
|
// uniqueness/ownership; no two clients can have colliding casefolded nicks or
|
||||||
// skeletons.
|
// skeletons.
|
||||||
@ -264,23 +277,41 @@ func (client *Client) AccountName() string {
|
|||||||
return client.accountName
|
return client.accountName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) SetAccountName(account string) (changed bool) {
|
func (client *Client) Login(account ClientAccount) {
|
||||||
var casefoldedAccount string
|
alwaysOn := persistenceEnabled(client.server.Config().Accounts.Bouncer.AlwaysOn, account.Settings.AlwaysOn)
|
||||||
var err error
|
|
||||||
if account != "" {
|
|
||||||
if casefoldedAccount, err = CasefoldName(account); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
changed = client.account != casefoldedAccount
|
client.account = account.NameCasefolded
|
||||||
client.account = casefoldedAccount
|
client.accountName = account.Name
|
||||||
client.accountName = account
|
client.accountSettings = account.Settings
|
||||||
|
// check `registered` to avoid incorrectly marking a temporary (pre-reattach),
|
||||||
|
// SASL'ing client as always-on
|
||||||
|
if client.registered {
|
||||||
|
client.alwaysOn = alwaysOn
|
||||||
|
}
|
||||||
|
client.accountRegDate = account.RegisteredAt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) historyCutoff() (cutoff time.Time) {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
if client.account != "" {
|
||||||
|
cutoff = client.accountRegDate
|
||||||
|
} else {
|
||||||
|
cutoff = client.ctime
|
||||||
|
}
|
||||||
|
client.stateMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Logout() {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
client.account = ""
|
||||||
|
client.accountName = ""
|
||||||
|
client.alwaysOn = false
|
||||||
|
client.stateMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (client *Client) AccountSettings() (result AccountSettings) {
|
func (client *Client) AccountSettings() (result AccountSettings) {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
result = client.accountSettings
|
result = client.accountSettings
|
||||||
@ -289,8 +320,12 @@ func (client *Client) AccountSettings() (result AccountSettings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) SetAccountSettings(settings AccountSettings) {
|
func (client *Client) SetAccountSettings(settings AccountSettings) {
|
||||||
|
alwaysOn := persistenceEnabled(client.server.Config().Accounts.Bouncer.AlwaysOn, settings.AlwaysOn)
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
client.accountSettings = settings
|
client.accountSettings = settings
|
||||||
|
if client.registered {
|
||||||
|
client.alwaysOn = alwaysOn
|
||||||
|
}
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,11 +344,17 @@ func (client *Client) SetLanguages(languages []string) {
|
|||||||
|
|
||||||
func (client *Client) HasMode(mode modes.Mode) bool {
|
func (client *Client) HasMode(mode modes.Mode) bool {
|
||||||
// client.flags has its own synch
|
// client.flags has its own synch
|
||||||
return client.flags.HasMode(mode)
|
return client.modes.HasMode(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) SetMode(mode modes.Mode, on bool) bool {
|
func (client *Client) SetMode(mode modes.Mode, on bool) bool {
|
||||||
return client.flags.SetMode(mode, on)
|
return client.modes.SetMode(mode, on)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) SetRealname(realname string) {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
client.realname = realname
|
||||||
|
client.stateMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) Channels() (result []*Channel) {
|
func (client *Client) Channels() (result []*Channel) {
|
||||||
@ -410,3 +451,17 @@ func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
|
|||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
return clientModes.HighestChannelUserMode()
|
return clientModes.HighestChannelUserMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) Settings() (result ChannelSettings) {
|
||||||
|
channel.stateMutex.RLock()
|
||||||
|
result = channel.settings
|
||||||
|
channel.stateMutex.RUnlock()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) SetSettings(settings ChannelSettings) {
|
||||||
|
channel.stateMutex.Lock()
|
||||||
|
channel.settings = settings
|
||||||
|
channel.stateMutex.Unlock()
|
||||||
|
channel.MarkDirty(IncludeSettings)
|
||||||
|
}
|
||||||
|
389
irc/handlers.go
389
irc/handlers.go
@ -531,64 +531,49 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
|||||||
// CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
|
// CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
|
||||||
// e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
|
// e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
|
||||||
func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
|
func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
|
||||||
config := server.Config()
|
|
||||||
|
|
||||||
var items []history.Item
|
var items []history.Item
|
||||||
success := false
|
unknown_command := false
|
||||||
var hist *history.Buffer
|
var target string
|
||||||
var channel *Channel
|
var channel *Channel
|
||||||
|
var sequence history.Sequence
|
||||||
|
var err error
|
||||||
defer func() {
|
defer func() {
|
||||||
// successful responses are sent as a chathistory or history batch
|
// successful responses are sent as a chathistory or history batch
|
||||||
if success && 0 < len(items) {
|
if err == nil && 0 < len(items) {
|
||||||
if channel == nil {
|
if channel != nil {
|
||||||
client.replayPrivmsgHistory(rb, items, true)
|
|
||||||
} else {
|
|
||||||
channel.replayHistoryItems(rb, items, false)
|
channel.replayHistoryItems(rb, items, false)
|
||||||
|
} else {
|
||||||
|
client.replayPrivmsgHistory(rb, items, target, true)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// errors are sent either without a batch, or in a draft/labeled-response batch as usual
|
// errors are sent either without a batch, or in a draft/labeled-response batch as usual
|
||||||
// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
|
if unknown_command {
|
||||||
if hist == nil {
|
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "UNKNOWN_COMMAND", utils.SafeErrorParam(msg.Params[0]), client.t("Unknown command"))
|
||||||
rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL")
|
} else if err == utils.ErrInvalidParams {
|
||||||
} else if len(items) == 0 {
|
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMETERS", msg.Params[0], client.t("Invalid parameters"))
|
||||||
rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND")
|
} else if err != nil {
|
||||||
} else if !success {
|
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("Messages could not be retrieved"))
|
||||||
rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS")
|
} else if sequence == nil {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "NO_SUCH_CHANNEL", utils.SafeErrorParam(msg.Params[1]), client.t("No such channel"))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
target := msg.Params[0]
|
config := server.Config()
|
||||||
channel = server.channels.Get(target)
|
maxChathistoryLimit := config.History.ChathistoryMax
|
||||||
if channel != nil && channel.hasClient(client) {
|
if maxChathistoryLimit == 0 {
|
||||||
// "If [...] the user does not have permission to view the requested content, [...]
|
|
||||||
// NO_SUCH_CHANNEL SHOULD be returned"
|
|
||||||
hist = &channel.history
|
|
||||||
} else {
|
|
||||||
targetClient := server.clients.Get(target)
|
|
||||||
if targetClient != nil {
|
|
||||||
myAccount := client.Account()
|
|
||||||
targetAccount := targetClient.Account()
|
|
||||||
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
|
|
||||||
hist = &targetClient.history
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hist == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
preposition := strings.ToLower(msg.Params[1])
|
|
||||||
|
|
||||||
parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
|
parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
|
||||||
err = errInvalidParams
|
err = utils.ErrInvalidParams
|
||||||
pieces := strings.SplitN(param, "=", 2)
|
pieces := strings.SplitN(param, "=", 2)
|
||||||
if len(pieces) < 2 {
|
if len(pieces) < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
identifier, value := strings.ToLower(pieces[0]), pieces[1]
|
identifier, value := strings.ToLower(pieces[0]), pieces[1]
|
||||||
if identifier == "id" {
|
if identifier == "msgid" {
|
||||||
msgid, err = value, nil
|
msgid, err = value, nil
|
||||||
return
|
return
|
||||||
} else if identifier == "timestamp" {
|
} else if identifier == "timestamp" {
|
||||||
@ -598,10 +583,6 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
maxChathistoryLimit := config.History.ChathistoryMax
|
|
||||||
if maxChathistoryLimit == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
parseHistoryLimit := func(paramIndex int) (limit int) {
|
parseHistoryLimit := func(paramIndex int) (limit int) {
|
||||||
if len(msg.Params) < (paramIndex + 1) {
|
if len(msg.Params) < (paramIndex + 1) {
|
||||||
return maxChathistoryLimit
|
return maxChathistoryLimit
|
||||||
@ -613,140 +594,74 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: as currently implemented, almost all of thes queries are worst-case O(n)
|
preposition := strings.ToLower(msg.Params[0])
|
||||||
// in the number of stored history entries. Every one of them can be made O(1)
|
target = msg.Params[1]
|
||||||
// if necessary, without too much difficulty. Some ideas:
|
channel, sequence, err = server.GetHistorySequence(nil, client, target)
|
||||||
// * Ensure that the ring buffer is sorted by time, enabling binary search for times
|
if err != nil || sequence == nil {
|
||||||
// * Maintain a map from msgid to position in the ring buffer
|
|
||||||
|
|
||||||
if preposition == "between" {
|
|
||||||
if len(msg.Params) >= 5 {
|
|
||||||
startMsgid, startTimestamp, startErr := parseQueryParam(msg.Params[2])
|
|
||||||
endMsgid, endTimestamp, endErr := parseQueryParam(msg.Params[3])
|
|
||||||
ascending := msg.Params[4] == "+"
|
|
||||||
limit := parseHistoryLimit(5)
|
|
||||||
if startErr != nil || endErr != nil {
|
|
||||||
success = false
|
|
||||||
} else if startMsgid != "" && endMsgid != "" {
|
|
||||||
inInterval := false
|
|
||||||
matches := func(item history.Item) (result bool) {
|
|
||||||
result = inInterval
|
|
||||||
if item.HasMsgid(startMsgid) {
|
|
||||||
if ascending {
|
|
||||||
inInterval = true
|
|
||||||
} else {
|
|
||||||
inInterval = false
|
|
||||||
return false // interval is exclusive
|
|
||||||
}
|
|
||||||
} else if item.HasMsgid(endMsgid) {
|
|
||||||
if ascending {
|
|
||||||
inInterval = false
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
inInterval = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
items = hist.Match(matches, ascending, limit)
|
|
||||||
success = true
|
|
||||||
} else if !startTimestamp.IsZero() && !endTimestamp.IsZero() {
|
|
||||||
items, _ = hist.Between(startTimestamp, endTimestamp, ascending, limit)
|
|
||||||
if !ascending {
|
|
||||||
history.Reverse(items)
|
|
||||||
}
|
|
||||||
success = true
|
|
||||||
}
|
|
||||||
// else: mismatched params, success = false, fail
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// before, after, latest, around
|
roundUp := func(endpoint time.Time) (result time.Time) {
|
||||||
queryParam := msg.Params[2]
|
return endpoint.Truncate(time.Millisecond).Add(time.Millisecond)
|
||||||
msgid, timestamp, err := parseQueryParam(queryParam)
|
|
||||||
limit := parseHistoryLimit(3)
|
|
||||||
before := false
|
|
||||||
switch preposition {
|
|
||||||
case "before":
|
|
||||||
before = true
|
|
||||||
fallthrough
|
|
||||||
case "after":
|
|
||||||
var matches history.Predicate
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
} else if msgid != "" {
|
|
||||||
inInterval := false
|
|
||||||
matches = func(item history.Item) (result bool) {
|
|
||||||
result = inInterval
|
|
||||||
if item.HasMsgid(msgid) {
|
|
||||||
inInterval = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
matches = func(item history.Item) bool {
|
|
||||||
return before == item.Message.Time.Before(timestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items = hist.Match(matches, !before, limit)
|
|
||||||
success = true
|
|
||||||
case "latest":
|
|
||||||
if queryParam == "*" {
|
|
||||||
items = hist.Latest(limit)
|
|
||||||
} else if err != nil {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
var matches history.Predicate
|
|
||||||
if msgid != "" {
|
|
||||||
shouldStop := false
|
|
||||||
matches = func(item history.Item) bool {
|
|
||||||
if shouldStop {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
shouldStop = item.HasMsgid(msgid)
|
|
||||||
return !shouldStop
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
matches = func(item history.Item) bool {
|
|
||||||
return item.Message.Time.After(timestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items = hist.Match(matches, false, limit)
|
|
||||||
}
|
|
||||||
success = true
|
|
||||||
case "around":
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
var initialMatcher history.Predicate
|
|
||||||
if msgid != "" {
|
|
||||||
inInterval := false
|
|
||||||
initialMatcher = func(item history.Item) (result bool) {
|
|
||||||
if inInterval {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
inInterval = item.HasMsgid(msgid)
|
|
||||||
return inInterval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
initialMatcher = func(item history.Item) (result bool) {
|
|
||||||
return item.Message.Time.Before(timestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var halfLimit int
|
|
||||||
halfLimit = (limit + 1) / 2
|
|
||||||
firstPass := hist.Match(initialMatcher, false, halfLimit)
|
|
||||||
if len(firstPass) > 0 {
|
|
||||||
timeWindowStart := firstPass[0].Message.Time
|
|
||||||
items = hist.Match(func(item history.Item) bool {
|
|
||||||
return item.Message.Time.Equal(timeWindowStart) || item.Message.Time.After(timeWindowStart)
|
|
||||||
}, true, limit)
|
|
||||||
}
|
|
||||||
success = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var start, end history.Selector
|
||||||
|
var limit int
|
||||||
|
switch preposition {
|
||||||
|
case "between":
|
||||||
|
start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end.Msgid, end.Time, err = parseQueryParam(msg.Params[3])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// XXX preserve the ordering of the two parameters, since we might be going backwards,
|
||||||
|
// but round up the chronologically first one, whichever it is, to make it exclusive
|
||||||
|
if !start.Time.IsZero() && !end.Time.IsZero() {
|
||||||
|
if start.Time.Before(end.Time) {
|
||||||
|
start.Time = roundUp(start.Time)
|
||||||
|
} else {
|
||||||
|
end.Time = roundUp(end.Time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
limit = parseHistoryLimit(4)
|
||||||
|
case "before", "after", "around":
|
||||||
|
start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if preposition == "after" && !start.Time.IsZero() {
|
||||||
|
start.Time = roundUp(start.Time)
|
||||||
|
}
|
||||||
|
if preposition == "before" {
|
||||||
|
end = start
|
||||||
|
start = history.Selector{}
|
||||||
|
}
|
||||||
|
limit = parseHistoryLimit(3)
|
||||||
|
case "latest":
|
||||||
|
if msg.Params[2] != "*" {
|
||||||
|
end.Msgid, end.Time, err = parseQueryParam(msg.Params[2])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !end.Time.IsZero() {
|
||||||
|
end.Time = roundUp(end.Time)
|
||||||
|
}
|
||||||
|
start.Time = time.Now().UTC()
|
||||||
|
}
|
||||||
|
limit = parseHistoryLimit(3)
|
||||||
|
default:
|
||||||
|
unknown_command = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if preposition == "around" {
|
||||||
|
items, err = sequence.Around(start, limit)
|
||||||
|
} else {
|
||||||
|
items, _, err = sequence.Between(start, end, limit)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1006,6 +921,7 @@ Get an explanation of <argument>, or "index" for a list of help topics.`), rb)
|
|||||||
// HISTORY <target> [<limit>]
|
// HISTORY <target> [<limit>]
|
||||||
// e.g., HISTORY #ubuntu 10
|
// e.g., HISTORY #ubuntu 10
|
||||||
// HISTORY me 15
|
// HISTORY me 15
|
||||||
|
// HISTORY #darwin 1h
|
||||||
func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
if !config.History.Enabled {
|
if !config.History.Enabled {
|
||||||
@ -1014,53 +930,55 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
}
|
}
|
||||||
|
|
||||||
target := msg.Params[0]
|
target := msg.Params[0]
|
||||||
var hist *history.Buffer
|
if strings.ToLower(target) == "me" {
|
||||||
channel := server.channels.Get(target)
|
target = "*"
|
||||||
if channel != nil && channel.hasClient(client) {
|
}
|
||||||
hist = &channel.history
|
channel, sequence, err := server.GetHistorySequence(nil, client, target)
|
||||||
} else {
|
|
||||||
if strings.ToLower(target) == "me" {
|
if sequence == nil || err != nil {
|
||||||
hist = &client.history
|
// whatever
|
||||||
} else {
|
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
|
||||||
targetClient := server.clients.Get(target)
|
return false
|
||||||
if targetClient != nil {
|
}
|
||||||
myAccount, targetAccount := client.Account(), targetClient.Account()
|
|
||||||
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
|
var duration time.Duration
|
||||||
hist = &targetClient.history
|
maxChathistoryLimit := config.History.ChathistoryMax
|
||||||
}
|
limit := 100
|
||||||
|
if maxChathistoryLimit < limit {
|
||||||
|
limit = maxChathistoryLimit
|
||||||
|
}
|
||||||
|
if len(msg.Params) > 1 {
|
||||||
|
providedLimit, err := strconv.Atoi(msg.Params[1])
|
||||||
|
if err == nil && providedLimit != 0 {
|
||||||
|
limit = providedLimit
|
||||||
|
if maxChathistoryLimit < limit {
|
||||||
|
limit = maxChathistoryLimit
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
duration, err = time.ParseDuration(msg.Params[1])
|
||||||
|
if err == nil {
|
||||||
|
limit = maxChathistoryLimit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hist == nil {
|
var items []history.Item
|
||||||
if channel == nil {
|
if duration == 0 {
|
||||||
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
|
items, _, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
|
||||||
} else {
|
|
||||||
rb.Add(nil, server.name, ERR_NOTONCHANNEL, client.Nick(), target, client.t("You're not on that channel"))
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
limit := 10
|
|
||||||
maxChathistoryLimit := config.History.ChathistoryMax
|
|
||||||
if len(msg.Params) > 1 {
|
|
||||||
providedLimit, err := strconv.Atoi(msg.Params[1])
|
|
||||||
if providedLimit > maxChathistoryLimit {
|
|
||||||
providedLimit = maxChathistoryLimit
|
|
||||||
}
|
|
||||||
if err == nil && providedLimit != 0 {
|
|
||||||
limit = providedLimit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items := hist.Latest(limit)
|
|
||||||
|
|
||||||
if channel != nil {
|
|
||||||
channel.replayHistoryItems(rb, items, false)
|
|
||||||
} else {
|
} else {
|
||||||
client.replayPrivmsgHistory(rb, items, true)
|
now := time.Now().UTC()
|
||||||
|
start := history.Selector{Time: now}
|
||||||
|
end := history.Selector{Time: now.Add(-duration)}
|
||||||
|
items, _, err = sequence.Between(start, end, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
if channel != nil {
|
||||||
|
channel.replayHistoryItems(rb, items, false)
|
||||||
|
} else {
|
||||||
|
client.replayPrivmsgHistory(rb, items, "", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1944,7 +1862,7 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.isTor && utils.IsRestrictedCTCPMessage(message) {
|
if rb.session.isTor && utils.IsRestrictedCTCPMessage(message) {
|
||||||
// note that error replies are never sent for NOTICE
|
// note that error replies are never sent for NOTICE
|
||||||
if histType != history.Notice {
|
if histType != history.Notice {
|
||||||
rb.Notice(client.t("CTCP messages are disabled over Tor"))
|
rb.Notice(client.t("CTCP messages are disabled over Tor"))
|
||||||
@ -2001,21 +1919,22 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tnick := user.Nick()
|
tDetails := user.Details()
|
||||||
|
tnick := tDetails.nick
|
||||||
|
|
||||||
nickMaskString := client.NickMaskString()
|
details := client.Details()
|
||||||
accountName := client.AccountName()
|
nickMaskString := details.nickMask
|
||||||
|
accountName := details.accountName
|
||||||
// restrict messages appropriately when +R is set
|
// restrict messages appropriately when +R is set
|
||||||
// intentionally make the sending user think the message went through fine
|
// intentionally make the sending user think the message went through fine
|
||||||
allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount()
|
allowedPlusR := !user.HasMode(modes.RegisteredOnly) || details.account != ""
|
||||||
allowedTor := !user.isTor || !message.IsRestrictedCTCPMessage()
|
if allowedPlusR {
|
||||||
if allowedPlusR && allowedTor {
|
|
||||||
for _, session := range user.Sessions() {
|
for _, session := range user.Sessions() {
|
||||||
hasTagsCap := session.capabilities.Has(caps.MessageTags)
|
hasTagsCap := session.capabilities.Has(caps.MessageTags)
|
||||||
// don't send TAGMSG at all if they don't have the tags cap
|
// don't send TAGMSG at all if they don't have the tags cap
|
||||||
if histType == history.Tagmsg && hasTagsCap {
|
if histType == history.Tagmsg && hasTagsCap {
|
||||||
session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
|
session.sendFromClientInternal(false, message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
|
||||||
} else if histType != history.Tagmsg {
|
} else if histType != history.Tagmsg && !(session.isTor && message.IsRestrictedCTCPMessage()) {
|
||||||
tagsToSend := tags
|
tagsToSend := tags
|
||||||
if !hasTagsCap {
|
if !hasTagsCap {
|
||||||
tagsToSend = nil
|
tagsToSend = nil
|
||||||
@ -2053,17 +1972,37 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
|||||||
rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, user.AwayMessage())
|
rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, user.AwayMessage())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config := server.Config()
|
||||||
|
if !config.History.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
item := history.Item{
|
item := history.Item{
|
||||||
Type: histType,
|
Type: histType,
|
||||||
Message: message,
|
Message: message,
|
||||||
Nick: nickMaskString,
|
Nick: nickMaskString,
|
||||||
AccountName: accountName,
|
AccountName: accountName,
|
||||||
|
Tags: tags,
|
||||||
|
}
|
||||||
|
if !item.IsStorable() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetedItem := item
|
||||||
|
targetedItem.Params[0] = tnick
|
||||||
|
cPersistent, cEphemeral := client.historyStatus(config)
|
||||||
|
tPersistent, tEphemeral := user.historyStatus(config)
|
||||||
|
// add to ephemeral history
|
||||||
|
if cEphemeral {
|
||||||
|
targetedItem.CfCorrespondent = tDetails.nickCasefolded
|
||||||
|
client.history.Add(targetedItem)
|
||||||
|
}
|
||||||
|
if tEphemeral {
|
||||||
|
item.CfCorrespondent = details.nickCasefolded
|
||||||
|
user.history.Add(item)
|
||||||
|
}
|
||||||
|
if cPersistent || tPersistent {
|
||||||
|
item.CfCorrespondent = ""
|
||||||
|
server.historyDB.AddDirectMessage(details.nickCasefolded, user.NickCasefolded(), cPersistent, tPersistent, targetedItem)
|
||||||
}
|
}
|
||||||
// add to the target's history:
|
|
||||||
user.history.Add(item)
|
|
||||||
// add this to the client's history as well, recording the target:
|
|
||||||
item.Params[0] = tnick
|
|
||||||
client.history.Add(item)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2375,11 +2314,7 @@ func sceneHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
|||||||
// SETNAME <realname>
|
// SETNAME <realname>
|
||||||
func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
realname := msg.Params[0]
|
realname := msg.Params[0]
|
||||||
|
client.SetRealname(realname)
|
||||||
client.stateMutex.Lock()
|
|
||||||
client.realname = realname
|
|
||||||
client.stateMutex.Unlock()
|
|
||||||
|
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
|
|
||||||
// alert friends
|
// alert friends
|
||||||
|
@ -206,8 +206,9 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
|
|||||||
|
|
||||||
Replay message history. <target> can be a channel name, "me" to replay direct
|
Replay message history. <target> can be a channel name, "me" to replay direct
|
||||||
message history, or a nickname to replay another client's direct message
|
message history, or a nickname to replay another client's direct message
|
||||||
history (they must be logged into the same account as you). At most [limit]
|
history (they must be logged into the same account as you). [limit] can be
|
||||||
messages will be replayed.`,
|
either an integer (the maximum number of messages to replay), or a time
|
||||||
|
duration like 10m or 1h (the time window within which to replay messages).`,
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
text: `INFO
|
text: `INFO
|
||||||
|
@ -6,7 +6,6 @@ package history
|
|||||||
import (
|
import (
|
||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,9 +42,10 @@ type Item struct {
|
|||||||
// this is the uncasefolded account name, if there's no account it should be set to "*"
|
// this is the uncasefolded account name, if there's no account it should be set to "*"
|
||||||
AccountName string
|
AccountName string
|
||||||
// for non-privmsg items, we may stuff some other data in here
|
// for non-privmsg items, we may stuff some other data in here
|
||||||
Message utils.SplitMessage
|
Message utils.SplitMessage
|
||||||
Tags map[string]string
|
Tags map[string]string
|
||||||
Params [1]string
|
Params [1]string
|
||||||
|
CfCorrespondent string
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasMsgid tests whether a message has the message id `msgid`.
|
// HasMsgid tests whether a message has the message id `msgid`.
|
||||||
@ -53,20 +53,30 @@ func (item *Item) HasMsgid(msgid string) bool {
|
|||||||
return item.Message.Msgid == msgid
|
return item.Message.Msgid == msgid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (item *Item) isStorable() bool {
|
func (item *Item) IsStorable() bool {
|
||||||
if item.Type == Tagmsg {
|
switch item.Type {
|
||||||
|
case Tagmsg:
|
||||||
for name := range item.Tags {
|
for name := range item.Tags {
|
||||||
if !transientTags[name] {
|
if !transientTags[name] {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false // all tags were blacklisted
|
return false // all tags were blacklisted
|
||||||
} else {
|
case Privmsg, Notice:
|
||||||
|
// don't store CTCP other than ACTION
|
||||||
|
return !item.Message.IsRestrictedCTCPMessage()
|
||||||
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Predicate func(item Item) (matches bool)
|
type Predicate func(item *Item) (matches bool)
|
||||||
|
|
||||||
|
func Reverse(results []Item) {
|
||||||
|
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
results[i], results[j] = results[j], results[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Buffer is a ring buffer holding message/event history for a channel or user
|
// Buffer is a ring buffer holding message/event history for a channel or user
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
@ -81,8 +91,6 @@ type Buffer struct {
|
|||||||
|
|
||||||
lastDiscarded time.Time
|
lastDiscarded time.Time
|
||||||
|
|
||||||
enabled uint32
|
|
||||||
|
|
||||||
nowFunc func() time.Time
|
nowFunc func() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,8 +107,6 @@ func (hist *Buffer) Initialize(size int, window time.Duration) {
|
|||||||
hist.window = window
|
hist.window = window
|
||||||
hist.maximumSize = size
|
hist.maximumSize = size
|
||||||
hist.nowFunc = time.Now
|
hist.nowFunc = time.Now
|
||||||
|
|
||||||
hist.setEnabled(size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute the initial size for the buffer, taking into account autoresize
|
// compute the initial size for the buffer, taking into account autoresize
|
||||||
@ -115,31 +121,8 @@ func (hist *Buffer) initialSize(size int, window time.Duration) (result int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hist *Buffer) setEnabled(size int) {
|
|
||||||
var enabled uint32
|
|
||||||
if size != 0 {
|
|
||||||
enabled = 1
|
|
||||||
}
|
|
||||||
atomic.StoreUint32(&hist.enabled, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabled returns whether the buffer is currently storing messages
|
|
||||||
// (a disabled buffer blackholes everything it sees)
|
|
||||||
func (list *Buffer) Enabled() bool {
|
|
||||||
return atomic.LoadUint32(&list.enabled) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a history item to the buffer
|
// Add adds a history item to the buffer
|
||||||
func (list *Buffer) Add(item Item) {
|
func (list *Buffer) Add(item Item) {
|
||||||
// fast path without a lock acquisition for when we are not storing history
|
|
||||||
if !list.Enabled() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !item.isStorable() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.Message.Time.IsZero() {
|
if item.Message.Time.IsZero() {
|
||||||
item.Message.Time = time.Now().UTC()
|
item.Message.Time = time.Now().UTC()
|
||||||
}
|
}
|
||||||
@ -147,6 +130,10 @@ func (list *Buffer) Add(item Item) {
|
|||||||
list.Lock()
|
list.Lock()
|
||||||
defer list.Unlock()
|
defer list.Unlock()
|
||||||
|
|
||||||
|
if len(list.buffer) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
list.maybeExpand()
|
list.maybeExpand()
|
||||||
|
|
||||||
var pos int
|
var pos int
|
||||||
@ -170,55 +157,100 @@ func (list *Buffer) Add(item Item) {
|
|||||||
list.buffer[pos] = item
|
list.buffer[pos] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reverse reverses an []Item, in-place.
|
func (list *Buffer) lookup(msgid string) (result Item, found bool) {
|
||||||
func Reverse(results []Item) {
|
predicate := func(item *Item) bool {
|
||||||
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
|
return item.HasMsgid(msgid)
|
||||||
results[i], results[j] = results[j], results[i]
|
|
||||||
}
|
}
|
||||||
|
results := list.matchInternal(predicate, false, 1)
|
||||||
|
if len(results) != 0 {
|
||||||
|
return results[0], true
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Between returns all history items with a time `after` <= time <= `before`,
|
// Between returns all history items with a time `after` <= time <= `before`,
|
||||||
// with an indication of whether the results are complete or are missing items
|
// with an indication of whether the results are complete or are missing items
|
||||||
// because some of that period was discarded. A zero value of `before` is considered
|
// because some of that period was discarded. A zero value of `before` is considered
|
||||||
// higher than all other times.
|
// higher than all other times.
|
||||||
func (list *Buffer) Between(after, before time.Time, ascending bool, limit int) (results []Item, complete bool) {
|
func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Predicate, limit int) (results []Item, complete bool, err error) {
|
||||||
if !list.Enabled() {
|
var ascending bool
|
||||||
return
|
|
||||||
}
|
defer func() {
|
||||||
|
if !ascending {
|
||||||
|
Reverse(results)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
list.RLock()
|
list.RLock()
|
||||||
defer list.RUnlock()
|
defer list.RUnlock()
|
||||||
|
|
||||||
|
if len(list.buffer) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
after := start.Time
|
||||||
|
if start.Msgid != "" {
|
||||||
|
item, found := list.lookup(start.Msgid)
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
after = item.Message.Time
|
||||||
|
}
|
||||||
|
before := end.Time
|
||||||
|
if end.Msgid != "" {
|
||||||
|
item, found := list.lookup(end.Msgid)
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
before = item.Message.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
after, before, ascending = MinMaxAsc(after, before, cutoff)
|
||||||
|
|
||||||
complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
|
complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
|
||||||
|
|
||||||
satisfies := func(item Item) bool {
|
satisfies := func(item *Item) bool {
|
||||||
return (after.IsZero() || item.Message.Time.After(after)) && (before.IsZero() || item.Message.Time.Before(before))
|
return (after.IsZero() || item.Message.Time.After(after)) &&
|
||||||
|
(before.IsZero() || item.Message.Time.Before(before)) &&
|
||||||
|
(pred == nil || pred(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.matchInternal(satisfies, ascending, limit), complete
|
return list.matchInternal(satisfies, ascending, limit), complete, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match returns all history items such that `predicate` returns true for them.
|
// implements history.Sequence, emulating a single history buffer (for a channel,
|
||||||
// Items are considered in reverse insertion order if `ascending` is false, or
|
// a single user's DMs, or a DM conversation)
|
||||||
// in insertion order if `ascending` is true, up to a total of `limit` matches
|
type bufferSequence struct {
|
||||||
// if `limit` > 0 (unlimited otherwise).
|
list *Buffer
|
||||||
// `predicate` MAY be a closure that maintains its own state across invocations;
|
pred Predicate
|
||||||
// it MUST NOT acquire any locks or otherwise do anything weird.
|
cutoff time.Time
|
||||||
// Results are always returned in insertion order.
|
}
|
||||||
func (list *Buffer) Match(predicate Predicate, ascending bool, limit int) (results []Item) {
|
|
||||||
if !list.Enabled() {
|
func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequence {
|
||||||
return
|
var pred Predicate
|
||||||
|
if correspondent != "" {
|
||||||
|
pred = func(item *Item) bool {
|
||||||
|
return item.CfCorrespondent == correspondent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return &bufferSequence{
|
||||||
|
list: list,
|
||||||
|
pred: pred,
|
||||||
|
cutoff: cutoff,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
list.RLock()
|
func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, complete bool, err error) {
|
||||||
defer list.RUnlock()
|
return seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
|
||||||
|
}
|
||||||
|
|
||||||
return list.matchInternal(predicate, ascending, limit)
|
func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
|
||||||
|
return GenericAround(seq, start, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// you must be holding the read lock to call this
|
// you must be holding the read lock to call this
|
||||||
func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
|
func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
|
||||||
if list.start == -1 {
|
if list.start == -1 || len(list.buffer) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,7 +264,7 @@ func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if predicate(list.buffer[pos]) {
|
if predicate(&list.buffer[pos]) {
|
||||||
results = append(results, list.buffer[pos])
|
results = append(results, list.buffer[pos])
|
||||||
}
|
}
|
||||||
if pos == stop || (limit != 0 && len(results) == limit) {
|
if pos == stop || (limit != 0 && len(results) == limit) {
|
||||||
@ -245,18 +277,14 @@ func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO sort by time instead?
|
|
||||||
if !ascending {
|
|
||||||
Reverse(results)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Latest returns the items most recently added, up to `limit`. If `limit` is 0,
|
// latest returns the items most recently added, up to `limit`. If `limit` is 0,
|
||||||
// it returns all items.
|
// it returns all items.
|
||||||
func (list *Buffer) Latest(limit int) (results []Item) {
|
func (list *Buffer) latest(limit int) (results []Item) {
|
||||||
matchAll := func(item Item) bool { return true }
|
results, _, _ = list.betweenHelper(Selector{}, Selector{}, time.Time{}, nil, limit)
|
||||||
return list.Match(matchAll, false, limit)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// LastDiscarded returns the latest time of any entry that was evicted
|
// LastDiscarded returns the latest time of any entry that was evicted
|
||||||
@ -355,8 +383,6 @@ func (list *Buffer) Resize(maximumSize int, window time.Duration) {
|
|||||||
func (list *Buffer) resize(size int) {
|
func (list *Buffer) resize(size int) {
|
||||||
newbuffer := make([]Item, size)
|
newbuffer := make([]Item, size)
|
||||||
|
|
||||||
list.setEnabled(size)
|
|
||||||
|
|
||||||
if list.start == -1 {
|
if list.start == -1 {
|
||||||
// indices are already correct and nothing needs to be copied
|
// indices are already correct and nothing needs to be copied
|
||||||
} else if size == 0 {
|
} else if size == 0 {
|
||||||
|
@ -14,19 +14,21 @@ const (
|
|||||||
timeFormat = "2006-01-02 15:04:05Z"
|
timeFormat = "2006-01-02 15:04:05Z"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func betweenTimestamps(buf *Buffer, start, end time.Time, limit int) (result []Item, complete bool) {
|
||||||
|
result, complete, _ = buf.betweenHelper(Selector{Time: start}, Selector{Time: end}, time.Time{}, nil, limit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func TestEmptyBuffer(t *testing.T) {
|
func TestEmptyBuffer(t *testing.T) {
|
||||||
pastTime := easyParse(timeFormat)
|
pastTime := easyParse(timeFormat)
|
||||||
|
|
||||||
buf := NewHistoryBuffer(0, 0)
|
buf := NewHistoryBuffer(0, 0)
|
||||||
if buf.Enabled() {
|
|
||||||
t.Error("the buffer of size 0 must be considered disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.Add(Item{
|
buf.Add(Item{
|
||||||
Nick: "testnick",
|
Nick: "testnick",
|
||||||
})
|
})
|
||||||
|
|
||||||
since, complete := buf.Between(pastTime, time.Now(), false, 0)
|
since, complete := betweenTimestamps(buf, pastTime, time.Now(), 0)
|
||||||
if len(since) != 0 {
|
if len(since) != 0 {
|
||||||
t.Error("shouldn't be able to add to disabled buf")
|
t.Error("shouldn't be able to add to disabled buf")
|
||||||
}
|
}
|
||||||
@ -35,16 +37,13 @@ func TestEmptyBuffer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf.Resize(1, 0)
|
buf.Resize(1, 0)
|
||||||
if !buf.Enabled() {
|
since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
|
||||||
t.Error("the buffer of size 1 must be considered enabled")
|
|
||||||
}
|
|
||||||
since, complete = buf.Between(pastTime, time.Now(), false, 0)
|
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(len(since), 0, t)
|
assertEqual(len(since), 0, t)
|
||||||
buf.Add(Item{
|
buf.Add(Item{
|
||||||
Nick: "testnick",
|
Nick: "testnick",
|
||||||
})
|
})
|
||||||
since, complete = buf.Between(pastTime, time.Now(), false, 0)
|
since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
|
||||||
if len(since) != 1 {
|
if len(since) != 1 {
|
||||||
t.Error("should be able to store items in a nonempty buffer")
|
t.Error("should be able to store items in a nonempty buffer")
|
||||||
}
|
}
|
||||||
@ -58,7 +57,7 @@ func TestEmptyBuffer(t *testing.T) {
|
|||||||
buf.Add(Item{
|
buf.Add(Item{
|
||||||
Nick: "testnick2",
|
Nick: "testnick2",
|
||||||
})
|
})
|
||||||
since, complete = buf.Between(pastTime, time.Now(), false, 0)
|
since, complete = betweenTimestamps(buf, pastTime, time.Now(), 0)
|
||||||
if len(since) != 1 {
|
if len(since) != 1 {
|
||||||
t.Error("expect exactly 1 item")
|
t.Error("expect exactly 1 item")
|
||||||
}
|
}
|
||||||
@ -68,8 +67,7 @@ func TestEmptyBuffer(t *testing.T) {
|
|||||||
if since[0].Nick != "testnick2" {
|
if since[0].Nick != "testnick2" {
|
||||||
t.Error("retrieved junk data")
|
t.Error("retrieved junk data")
|
||||||
}
|
}
|
||||||
matchAll := func(item Item) bool { return true }
|
assertEqual(toNicks(buf.latest(0)), []string{"testnick2"}, t)
|
||||||
assertEqual(toNicks(buf.Match(matchAll, false, 0)), []string{"testnick2"}, t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toNicks(items []Item) (result []string) {
|
func toNicks(items []Item) (result []string) {
|
||||||
@ -110,27 +108,27 @@ func TestBuffer(t *testing.T) {
|
|||||||
|
|
||||||
buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z"))
|
buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z"))
|
||||||
|
|
||||||
since, complete := buf.Between(start, time.Now(), false, 0)
|
since, complete := betweenTimestamps(buf, start, time.Now(), 0)
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
|
assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
|
||||||
|
|
||||||
// add another item, evicting the first
|
// add another item, evicting the first
|
||||||
buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z"))
|
buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z"))
|
||||||
|
|
||||||
since, complete = buf.Between(start, time.Now(), false, 0)
|
since, complete = betweenTimestamps(buf, start, time.Now(), 0)
|
||||||
assertEqual(complete, false, t)
|
assertEqual(complete, false, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
|
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
|
||||||
// now exclude the time of the discarded entry; results should be complete again
|
// now exclude the time of the discarded entry; results should be complete again
|
||||||
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
|
since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), time.Now(), 0)
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
|
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
|
||||||
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"), false, 0)
|
since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"), 0)
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick1"}, t)
|
assertEqual(toNicks(since), []string{"testnick1"}, t)
|
||||||
|
|
||||||
// shrink the buffer, cutting off testnick1
|
// shrink the buffer, cutting off testnick1
|
||||||
buf.Resize(2, 0)
|
buf.Resize(2, 0)
|
||||||
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
|
since, complete = betweenTimestamps(buf, easyParse("2006-01-02 00:00:00Z"), time.Now(), 0)
|
||||||
assertEqual(complete, false, t)
|
assertEqual(complete, false, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
||||||
|
|
||||||
@ -138,18 +136,19 @@ func TestBuffer(t *testing.T) {
|
|||||||
buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
|
buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
|
||||||
buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
|
buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
|
||||||
buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
|
buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
|
||||||
since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), false, 0)
|
since, complete = betweenTimestamps(buf, easyParse("2006-01-03 00:00:00Z"), time.Now(), 0)
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
|
assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
|
||||||
|
|
||||||
// test ascending order
|
// test ascending order
|
||||||
since, _ = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), true, 2)
|
since, _ = betweenTimestamps(buf, easyParse("2006-01-03 00:00:00Z"), time.Time{}, 2)
|
||||||
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func autoItem(id int, t time.Time) (result Item) {
|
func autoItem(id int, t time.Time) (result Item) {
|
||||||
result.Message.Time = t
|
result.Message.Time = t
|
||||||
result.Nick = strconv.Itoa(id)
|
result.Nick = strconv.Itoa(id)
|
||||||
|
result.Message.Msgid = result.Nick
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +180,7 @@ func TestAutoresize(t *testing.T) {
|
|||||||
now = now.Add(time.Minute * 10)
|
now = now.Add(time.Minute * 10)
|
||||||
id += 1
|
id += 1
|
||||||
}
|
}
|
||||||
items := buf.Latest(0)
|
items := buf.latest(0)
|
||||||
assertEqual(len(items), initialAutoSize, t)
|
assertEqual(len(items), initialAutoSize, t)
|
||||||
assertEqual(atoi(items[0].Nick), 40, t)
|
assertEqual(atoi(items[0].Nick), 40, t)
|
||||||
assertEqual(atoi(items[len(items)-1].Nick), 71, t)
|
assertEqual(atoi(items[len(items)-1].Nick), 71, t)
|
||||||
@ -195,7 +194,7 @@ func TestAutoresize(t *testing.T) {
|
|||||||
// ok, 5 items from the first batch are still in the 1-hour window;
|
// ok, 5 items from the first batch are still in the 1-hour window;
|
||||||
// we should overwrite until only those 5 are left, then start expanding
|
// we should overwrite until only those 5 are left, then start expanding
|
||||||
// the buffer so that it retains those 5 and the 100 new items
|
// the buffer so that it retains those 5 and the 100 new items
|
||||||
items = buf.Latest(0)
|
items = buf.latest(0)
|
||||||
assertEqual(len(items), 105, t)
|
assertEqual(len(items), 105, t)
|
||||||
assertEqual(atoi(items[0].Nick), 67, t)
|
assertEqual(atoi(items[0].Nick), 67, t)
|
||||||
assertEqual(atoi(items[len(items)-1].Nick), 171, t)
|
assertEqual(atoi(items[len(items)-1].Nick), 171, t)
|
||||||
@ -207,7 +206,7 @@ func TestAutoresize(t *testing.T) {
|
|||||||
id += 1
|
id += 1
|
||||||
}
|
}
|
||||||
// should fill up to the maximum size of 128 and start overwriting
|
// should fill up to the maximum size of 128 and start overwriting
|
||||||
items = buf.Latest(0)
|
items = buf.latest(0)
|
||||||
assertEqual(len(items), 128, t)
|
assertEqual(len(items), 128, t)
|
||||||
assertEqual(atoi(items[0].Nick), 144, t)
|
assertEqual(atoi(items[0].Nick), 144, t)
|
||||||
assertEqual(atoi(items[len(items)-1].Nick), 271, t)
|
assertEqual(atoi(items[len(items)-1].Nick), 271, t)
|
||||||
@ -222,7 +221,7 @@ func TestEnabledByResize(t *testing.T) {
|
|||||||
buf.Resize(128, time.Hour)
|
buf.Resize(128, time.Hour)
|
||||||
// add an item and test that it is stored and retrievable
|
// add an item and test that it is stored and retrievable
|
||||||
buf.Add(autoItem(0, now))
|
buf.Add(autoItem(0, now))
|
||||||
items := buf.Latest(0)
|
items := buf.latest(0)
|
||||||
assertEqual(len(items), 1, t)
|
assertEqual(len(items), 1, t)
|
||||||
assertEqual(atoi(items[0].Nick), 0, t)
|
assertEqual(atoi(items[0].Nick), 0, t)
|
||||||
}
|
}
|
||||||
@ -232,13 +231,13 @@ func TestDisabledByResize(t *testing.T) {
|
|||||||
// enabled autoresizing buffer
|
// enabled autoresizing buffer
|
||||||
buf := NewHistoryBuffer(128, time.Hour)
|
buf := NewHistoryBuffer(128, time.Hour)
|
||||||
buf.Add(autoItem(0, now))
|
buf.Add(autoItem(0, now))
|
||||||
items := buf.Latest(0)
|
items := buf.latest(0)
|
||||||
assertEqual(len(items), 1, t)
|
assertEqual(len(items), 1, t)
|
||||||
assertEqual(atoi(items[0].Nick), 0, t)
|
assertEqual(atoi(items[0].Nick), 0, t)
|
||||||
|
|
||||||
// disable as during a rehash, confirm that nothing can be retrieved
|
// disable as during a rehash, confirm that nothing can be retrieved
|
||||||
buf.Resize(0, time.Hour)
|
buf.Resize(0, time.Hour)
|
||||||
items = buf.Latest(0)
|
items = buf.latest(0)
|
||||||
assertEqual(len(items), 0, t)
|
assertEqual(len(items), 0, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,3 +251,25 @@ func TestRoundUp(t *testing.T) {
|
|||||||
assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
|
assertEqual(roundUpToPowerOfTwo(1025), 2048, t)
|
||||||
assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
|
assertEqual(roundUpToPowerOfTwo(269435457), 536870912, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkInsert(b *testing.B) {
|
||||||
|
buf := NewHistoryBuffer(1024, 0)
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
buf.Add(Item{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMatch(b *testing.B) {
|
||||||
|
buf := NewHistoryBuffer(1024, 0)
|
||||||
|
var now time.Time
|
||||||
|
for i := 0; i < 1024; i += 1 {
|
||||||
|
buf.Add(autoItem(i, now))
|
||||||
|
now = now.Add(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
buf.lookup("512")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
71
irc/history/queries.go
Normal file
71
irc/history/queries.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package history
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Selector represents a parameter to a CHATHISTORY command;
|
||||||
|
// at most one of Msgid or Time may be nonzero
|
||||||
|
type Selector struct {
|
||||||
|
Msgid string
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sequence is an abstract sequence of history entries that can be queried;
|
||||||
|
// it encapsulates restrictions such as registration time cutoffs, or
|
||||||
|
// only looking at a single "query buffer" (DMs with a particular correspondent)
|
||||||
|
type Sequence interface {
|
||||||
|
Between(start, end Selector, limit int) (results []Item, complete bool, err error)
|
||||||
|
Around(start Selector, limit int) (results []Item, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
|
||||||
|
func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
|
||||||
|
var halfLimit int
|
||||||
|
halfLimit = (limit + 1) / 2
|
||||||
|
initialResults, _, err := seq.Between(Selector{}, start, halfLimit)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
} else if len(initialResults) == 0 {
|
||||||
|
// TODO: this fails if we're doing an AROUND on the first message in the buffer
|
||||||
|
// would be nice to fix this but whatever
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newStart := Selector{Time: initialResults[0].Message.Time}
|
||||||
|
results, _, err = seq.Between(newStart, Selector{}, limit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinMaxAsc converts CHATHISTORY arguments into time intervals, handling the most
|
||||||
|
// general case (BETWEEN going forwards or backwards) natively and the other ordering
|
||||||
|
// queries (AFTER, BEFORE, LATEST) as special cases.
|
||||||
|
func MinMaxAsc(after, before, cutoff time.Time) (min, max time.Time, ascending bool) {
|
||||||
|
startIsZero, endIsZero := after.IsZero(), before.IsZero()
|
||||||
|
if !startIsZero && endIsZero {
|
||||||
|
// AFTER
|
||||||
|
ascending = true
|
||||||
|
} else if startIsZero && !endIsZero {
|
||||||
|
// BEFORE
|
||||||
|
ascending = false
|
||||||
|
} else if !startIsZero && !endIsZero {
|
||||||
|
if before.Before(after) {
|
||||||
|
// BETWEEN going backwards
|
||||||
|
before, after = after, before
|
||||||
|
ascending = false
|
||||||
|
} else {
|
||||||
|
// BETWEEN going forwards
|
||||||
|
ascending = true
|
||||||
|
}
|
||||||
|
} else if startIsZero && endIsZero {
|
||||||
|
// LATEST
|
||||||
|
ascending = false
|
||||||
|
}
|
||||||
|
if after.IsZero() || after.Before(cutoff) {
|
||||||
|
// this may result in an impossible query, which is fine
|
||||||
|
after = cutoff
|
||||||
|
}
|
||||||
|
return after, before, ascending
|
||||||
|
}
|
@ -52,6 +52,7 @@ type IdleTimer struct {
|
|||||||
quitTimeout time.Duration
|
quitTimeout time.Duration
|
||||||
state TimerState
|
state TimerState
|
||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
|
lastTouch time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize sets up an IdleTimer and starts counting idle time;
|
// Initialize sets up an IdleTimer and starts counting idle time;
|
||||||
@ -61,9 +62,11 @@ func (it *IdleTimer) Initialize(session *Session) {
|
|||||||
it.registerTimeout = RegisterTimeout
|
it.registerTimeout = RegisterTimeout
|
||||||
it.idleTimeout, it.quitTimeout = it.recomputeDurations()
|
it.idleTimeout, it.quitTimeout = it.recomputeDurations()
|
||||||
registered := session.client.Registered()
|
registered := session.client.Registered()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
it.Lock()
|
it.Lock()
|
||||||
defer it.Unlock()
|
defer it.Unlock()
|
||||||
|
it.lastTouch = now
|
||||||
if registered {
|
if registered {
|
||||||
it.state = TimerActive
|
it.state = TimerActive
|
||||||
} else {
|
} else {
|
||||||
@ -82,7 +85,7 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
|
|||||||
}
|
}
|
||||||
|
|
||||||
idleTimeout = DefaultIdleTimeout
|
idleTimeout = DefaultIdleTimeout
|
||||||
if it.session.client.isTor {
|
if it.session.isTor {
|
||||||
idleTimeout = TorIdleTimeout
|
idleTimeout = TorIdleTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,10 +95,12 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
|
|||||||
|
|
||||||
func (it *IdleTimer) Touch() {
|
func (it *IdleTimer) Touch() {
|
||||||
idleTimeout, quitTimeout := it.recomputeDurations()
|
idleTimeout, quitTimeout := it.recomputeDurations()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
it.Lock()
|
it.Lock()
|
||||||
defer it.Unlock()
|
defer it.Unlock()
|
||||||
it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
|
it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
|
||||||
|
it.lastTouch = now
|
||||||
// a touch transitions TimerUnregistered or TimerIdle into TimerActive
|
// a touch transitions TimerUnregistered or TimerIdle into TimerActive
|
||||||
if it.state != TimerDead {
|
if it.state != TimerDead {
|
||||||
it.state = TimerActive
|
it.state = TimerActive
|
||||||
@ -103,6 +108,13 @@ func (it *IdleTimer) Touch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (it *IdleTimer) LastTouch() (result time.Time) {
|
||||||
|
it.Lock()
|
||||||
|
result = it.lastTouch
|
||||||
|
it.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (it *IdleTimer) processTimeout() {
|
func (it *IdleTimer) processTimeout() {
|
||||||
idleTimeout, quitTimeout := it.recomputeDurations()
|
idleTimeout, quitTimeout := it.recomputeDurations()
|
||||||
|
|
||||||
@ -322,9 +334,6 @@ const (
|
|||||||
// BrbDead is the state of a client after its timeout has expired; it will be removed
|
// BrbDead is the state of a client after its timeout has expired; it will be removed
|
||||||
// and therefore new sessions cannot be attached to it
|
// and therefore new sessions cannot be attached to it
|
||||||
BrbDead
|
BrbDead
|
||||||
// BrbSticky allows a client to remain online without sessions, with no timeout.
|
|
||||||
// This is not used yet.
|
|
||||||
BrbSticky
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BrbTimer struct {
|
type BrbTimer struct {
|
||||||
@ -345,16 +354,16 @@ func (bt *BrbTimer) Initialize(client *Client) {
|
|||||||
|
|
||||||
// attempts to enable BRB for a client, returns whether it succeeded
|
// attempts to enable BRB for a client, returns whether it succeeded
|
||||||
func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
|
func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
|
||||||
if !bt.client.Registered() || bt.client.ResumeID() == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO make this configurable
|
// TODO make this configurable
|
||||||
duration = ResumeableTotalTimeout
|
duration = ResumeableTotalTimeout
|
||||||
|
|
||||||
bt.client.stateMutex.Lock()
|
bt.client.stateMutex.Lock()
|
||||||
defer bt.client.stateMutex.Unlock()
|
defer bt.client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch bt.state {
|
switch bt.state {
|
||||||
case BrbDisabled, BrbEnabled:
|
case BrbDisabled, BrbEnabled:
|
||||||
bt.state = BrbEnabled
|
bt.state = BrbEnabled
|
||||||
@ -366,8 +375,6 @@ func (bt *BrbTimer) Enable() (success bool, duration time.Duration) {
|
|||||||
bt.brbAt = time.Now().UTC()
|
bt.brbAt = time.Now().UTC()
|
||||||
}
|
}
|
||||||
success = true
|
success = true
|
||||||
case BrbSticky:
|
|
||||||
success = true
|
|
||||||
default:
|
default:
|
||||||
// BrbDead
|
// BrbDead
|
||||||
success = false
|
success = false
|
||||||
@ -416,6 +423,10 @@ func (bt *BrbTimer) processTimeout() {
|
|||||||
bt.client.stateMutex.Lock()
|
bt.client.stateMutex.Lock()
|
||||||
defer bt.client.stateMutex.Unlock()
|
defer bt.client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if bt.client.alwaysOn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch bt.state {
|
switch bt.state {
|
||||||
case BrbDisabled, BrbEnabled:
|
case BrbDisabled, BrbEnabled:
|
||||||
if len(bt.client.sessions) == 0 {
|
if len(bt.client.sessions) == 0 {
|
||||||
@ -432,16 +443,3 @@ func (bt *BrbTimer) processTimeout() {
|
|||||||
}
|
}
|
||||||
bt.resetTimeout()
|
bt.resetTimeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
// sets a client to be "sticky", i.e., indefinitely exempt from removal for
|
|
||||||
// lack of sessions
|
|
||||||
func (bt *BrbTimer) SetSticky() (success bool) {
|
|
||||||
bt.client.stateMutex.Lock()
|
|
||||||
defer bt.client.stateMutex.Unlock()
|
|
||||||
if bt.state != BrbDead {
|
|
||||||
success = true
|
|
||||||
bt.state = BrbSticky
|
|
||||||
}
|
|
||||||
bt.resetTimeout()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
535
irc/mysql/history.go
Normal file
535
irc/mysql/history.go
Normal file
@ -0,0 +1,535 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/oragono/oragono/irc/history"
|
||||||
|
"github.com/oragono/oragono/irc/logger"
|
||||||
|
"github.com/oragono/oragono/irc/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// latest schema of the db
|
||||||
|
latestDbSchema = "1"
|
||||||
|
keySchemaVersion = "db.version"
|
||||||
|
cleanupRowLimit = 50
|
||||||
|
cleanupPauseTime = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type MySQL struct {
|
||||||
|
db *sql.DB
|
||||||
|
logger *logger.Manager
|
||||||
|
|
||||||
|
insertHistory *sql.Stmt
|
||||||
|
insertSequence *sql.Stmt
|
||||||
|
insertConversation *sql.Stmt
|
||||||
|
|
||||||
|
stateMutex sync.Mutex
|
||||||
|
expireTime time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) Initialize(logger *logger.Manager, expireTime time.Duration) {
|
||||||
|
mysql.logger = logger
|
||||||
|
mysql.expireTime = expireTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) SetExpireTime(expireTime time.Duration) {
|
||||||
|
mysql.stateMutex.Lock()
|
||||||
|
mysql.expireTime = expireTime
|
||||||
|
mysql.stateMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) getExpireTime() (expireTime time.Duration) {
|
||||||
|
mysql.stateMutex.Lock()
|
||||||
|
expireTime = mysql.expireTime
|
||||||
|
mysql.stateMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) Open(username, password, host string, port int, database string) (err error) {
|
||||||
|
// TODO: timeouts!
|
||||||
|
var address string
|
||||||
|
if port != 0 {
|
||||||
|
address = fmt.Sprintf("tcp(%s:%d)", host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
mysql.db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@%s/%s", username, password, address, database))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mysql.fixSchemas()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mysql.prepareStatements()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go mysql.cleanupLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) fixSchemas() (err error) {
|
||||||
|
_, err = mysql.db.Exec(`CREATE TABLE IF NOT EXISTS metadata (
|
||||||
|
key_name VARCHAR(32) primary key,
|
||||||
|
value VARCHAR(32) NOT NULL
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var schema string
|
||||||
|
err = mysql.db.QueryRow(`select value from metadata where key_name = ?;`, keySchemaVersion).Scan(&schema)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
err = mysql.createTables()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaVersion, latestDbSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if err == nil && schema != latestDbSchema {
|
||||||
|
// TODO figure out what to do about schema changes
|
||||||
|
return &utils.IncompatibleSchemaError{CurrentVersion: schema, RequiredVersion: latestDbSchema}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) createTables() (err error) {
|
||||||
|
_, err = mysql.db.Exec(`CREATE TABLE history (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
data BLOB NOT NULL,
|
||||||
|
msgid BINARY(16) NOT NULL,
|
||||||
|
KEY (msgid(4))
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mysql.db.Exec(`CREATE TABLE sequence (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
target VARBINARY(64) NOT NULL,
|
||||||
|
nanotime BIGINT UNSIGNED NOT NULL,
|
||||||
|
history_id BIGINT NOT NULL,
|
||||||
|
KEY (target, nanotime),
|
||||||
|
KEY (history_id)
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mysql.db.Exec(`CREATE TABLE conversations (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
lower_target VARBINARY(64) NOT NULL,
|
||||||
|
upper_target VARBINARY(64) NOT NULL,
|
||||||
|
nanotime BIGINT UNSIGNED NOT NULL,
|
||||||
|
history_id BIGINT NOT NULL,
|
||||||
|
KEY (lower_target, upper_target, nanotime),
|
||||||
|
KEY (history_id)
|
||||||
|
) CHARSET=ascii COLLATE=ascii_bin;`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) cleanupLoop() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
mysql.logger.Error("mysql",
|
||||||
|
fmt.Sprintf("Panic in cleanup routine: %v\n%s", r, debug.Stack()))
|
||||||
|
time.Sleep(cleanupPauseTime)
|
||||||
|
go mysql.cleanupLoop()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
expireTime := mysql.getExpireTime()
|
||||||
|
if expireTime != 0 {
|
||||||
|
for {
|
||||||
|
startTime := time.Now()
|
||||||
|
rowsDeleted, err := mysql.doCleanup(expireTime)
|
||||||
|
elapsed := time.Now().Sub(startTime)
|
||||||
|
mysql.logError("error during row cleanup", err)
|
||||||
|
// keep going as long as we're accomplishing significant work
|
||||||
|
// (don't busy-wait on small numbers of rows expiring):
|
||||||
|
if rowsDeleted < (cleanupRowLimit / 10) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// crude backpressure mechanism: if the database is slow,
|
||||||
|
// give it time to process other queries
|
||||||
|
time.Sleep(elapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(cleanupPauseTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) doCleanup(age time.Duration) (count int, err error) {
|
||||||
|
ids, maxNanotime, err := mysql.selectCleanupIDs(age)
|
||||||
|
if len(ids) == 0 {
|
||||||
|
mysql.logger.Debug("mysql", "found no rows to clean up")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mysql.logger.Debug("mysql", fmt.Sprintf("deleting %d history rows, max age %s", len(ids), utils.NanoToTimestamp(maxNanotime)))
|
||||||
|
|
||||||
|
// can't use ? binding for a variable number of arguments, build the IN clause manually
|
||||||
|
var inBuf bytes.Buffer
|
||||||
|
inBuf.WriteByte('(')
|
||||||
|
for i, id := range ids {
|
||||||
|
if i != 0 {
|
||||||
|
inBuf.WriteRune(',')
|
||||||
|
}
|
||||||
|
inBuf.WriteString(strconv.FormatInt(int64(id), 10))
|
||||||
|
}
|
||||||
|
inBuf.WriteRune(')')
|
||||||
|
|
||||||
|
_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM conversations WHERE history_id in %s;`, inBuf.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM sequence WHERE history_id in %s;`, inBuf.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = mysql.db.Exec(fmt.Sprintf(`DELETE FROM history WHERE id in %s;`, inBuf.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count = len(ids)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) selectCleanupIDs(age time.Duration) (ids []uint64, maxNanotime int64, err error) {
|
||||||
|
rows, err := mysql.db.Query(`
|
||||||
|
SELECT history.id, sequence.nanotime
|
||||||
|
FROM history
|
||||||
|
LEFT JOIN sequence ON history.id = sequence.history_id
|
||||||
|
ORDER BY history.id LIMIT ?;`, cleanupRowLimit)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// a history ID may have 0-2 rows in sequence: 1 for a channel entry,
|
||||||
|
// 2 for a DM, 0 if the data is inconsistent. therefore, deduplicate
|
||||||
|
// and delete anything that doesn't have a sequence entry:
|
||||||
|
idset := make(map[uint64]struct{}, cleanupRowLimit)
|
||||||
|
threshold := time.Now().Add(-age).UnixNano()
|
||||||
|
for rows.Next() {
|
||||||
|
var id uint64
|
||||||
|
var nanotime sql.NullInt64
|
||||||
|
err = rows.Scan(&id, &nanotime)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !nanotime.Valid || nanotime.Int64 < threshold {
|
||||||
|
idset[id] = struct{}{}
|
||||||
|
if nanotime.Valid && nanotime.Int64 > maxNanotime {
|
||||||
|
maxNanotime = nanotime.Int64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids = make([]uint64, len(idset))
|
||||||
|
i := 0
|
||||||
|
for id := range idset {
|
||||||
|
ids[i] = id
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) prepareStatements() (err error) {
|
||||||
|
mysql.insertHistory, err = mysql.db.Prepare(`INSERT INTO history
|
||||||
|
(data, msgid) VALUES (?, ?);`)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mysql.insertSequence, err = mysql.db.Prepare(`INSERT INTO sequence
|
||||||
|
(target, nanotime, history_id) VALUES (?, ?, ?);`)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mysql.insertConversation, err = mysql.db.Prepare(`INSERT INTO conversations
|
||||||
|
(lower_target, upper_target, nanotime, history_id) VALUES (?, ?, ?, ?);`)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) logError(context string, err error) (quit bool) {
|
||||||
|
if err != nil {
|
||||||
|
mysql.logger.Error("mysql", context, err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) AddChannelItem(target string, item history.Item) (err error) {
|
||||||
|
if mysql.db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if target == "" {
|
||||||
|
return utils.ErrInvalidParams
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := mysql.insertBase(item)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mysql.insertSequenceEntry(target, item.Message.Time, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) insertSequenceEntry(target string, messageTime time.Time, id int64) (err error) {
|
||||||
|
_, err = mysql.insertSequence.Exec(target, messageTime.UnixNano(), id)
|
||||||
|
mysql.logError("could not insert sequence entry", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) insertConversationEntry(sender, recipient string, messageTime time.Time, id int64) (err error) {
|
||||||
|
lower, higher := stringMinMax(sender, recipient)
|
||||||
|
_, err = mysql.insertConversation.Exec(lower, higher, messageTime.UnixNano(), id)
|
||||||
|
mysql.logError("could not insert conversations entry", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) insertBase(item history.Item) (id int64, err error) {
|
||||||
|
value, err := marshalItem(&item)
|
||||||
|
if mysql.logError("could not marshal item", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msgidBytes, err := decodeMsgid(item.Message.Msgid)
|
||||||
|
if mysql.logError("could not decode msgid", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := mysql.insertHistory.Exec(value, msgidBytes)
|
||||||
|
if mysql.logError("could not insert item", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err = result.LastInsertId()
|
||||||
|
if mysql.logError("could not insert item", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringMinMax(first, second string) (min, max string) {
|
||||||
|
if first < second {
|
||||||
|
return first, second
|
||||||
|
} else {
|
||||||
|
return second, first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) AddDirectMessage(sender, recipient string, senderPersistent, recipientPersistent bool, item history.Item) (err error) {
|
||||||
|
if mysql.db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(senderPersistent || recipientPersistent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sender == "" || recipient == "" {
|
||||||
|
return utils.ErrInvalidParams
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := mysql.insertBase(item)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if senderPersistent {
|
||||||
|
mysql.insertSequenceEntry(sender, item.Message.Time, id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipientPersistent && sender != recipient {
|
||||||
|
err = mysql.insertSequenceEntry(recipient, item.Message.Time, id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mysql.insertConversationEntry(sender, recipient, item.Message.Time, id)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) msgidToTime(msgid string) (result time.Time, err error) {
|
||||||
|
// in theory, we could optimize out a roundtrip to the database by using a subquery instead:
|
||||||
|
// sequence.nanotime > (
|
||||||
|
// SELECT sequence.nanotime FROM sequence, history
|
||||||
|
// WHERE sequence.history_id = history.id AND history.msgid = ?
|
||||||
|
// LIMIT 1)
|
||||||
|
// however, this doesn't handle the BETWEEN case with one or two msgids, where we
|
||||||
|
// don't initially know whether the interval is going forwards or backwards. to simplify
|
||||||
|
// the logic, resolve msgids to timestamps "manually" in all cases, using a separate query.
|
||||||
|
decoded, err := decodeMsgid(msgid)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row := mysql.db.QueryRow(`
|
||||||
|
SELECT sequence.nanotime FROM sequence
|
||||||
|
INNER JOIN history ON history.id = sequence.history_id
|
||||||
|
WHERE history.msgid = ? LIMIT 1;`, decoded)
|
||||||
|
var nanotime int64
|
||||||
|
err = row.Scan(&nanotime)
|
||||||
|
if mysql.logError("could not resolve msgid to time", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result = time.Unix(0, nanotime)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) selectItems(query string, args ...interface{}) (results []history.Item, err error) {
|
||||||
|
rows, err := mysql.db.Query(query, args...)
|
||||||
|
if mysql.logError("could not select history items", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var blob []byte
|
||||||
|
var item history.Item
|
||||||
|
err = rows.Scan(&blob)
|
||||||
|
if mysql.logError("could not scan history item", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = unmarshalItem(blob, &item)
|
||||||
|
if mysql.logError("could not unmarshal history item", err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
results = append(results, item)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) BetweenTimestamps(sender, recipient string, after, before, cutoff time.Time, limit int) (results []history.Item, err error) {
|
||||||
|
useSequence := true
|
||||||
|
var lowerTarget, upperTarget string
|
||||||
|
if sender != "" {
|
||||||
|
lowerTarget, upperTarget = stringMinMax(sender, recipient)
|
||||||
|
useSequence = false
|
||||||
|
}
|
||||||
|
|
||||||
|
table := "sequence"
|
||||||
|
if !useSequence {
|
||||||
|
table = "conversations"
|
||||||
|
}
|
||||||
|
|
||||||
|
after, before, ascending := history.MinMaxAsc(after, before, cutoff)
|
||||||
|
direction := "ASC"
|
||||||
|
if !ascending {
|
||||||
|
direction = "DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryBuf bytes.Buffer
|
||||||
|
|
||||||
|
args := make([]interface{}, 0, 6)
|
||||||
|
fmt.Fprintf(&queryBuf,
|
||||||
|
"SELECT history.data from history INNER JOIN %[1]s ON history.id = %[1]s.history_id WHERE", table)
|
||||||
|
if useSequence {
|
||||||
|
fmt.Fprintf(&queryBuf, " sequence.target = ?")
|
||||||
|
args = append(args, recipient)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&queryBuf, " conversations.lower_target = ? AND conversations.upper_target = ?")
|
||||||
|
args = append(args, lowerTarget)
|
||||||
|
args = append(args, upperTarget)
|
||||||
|
}
|
||||||
|
if !after.IsZero() {
|
||||||
|
fmt.Fprintf(&queryBuf, " AND %s.nanotime > ?", table)
|
||||||
|
args = append(args, after.UnixNano())
|
||||||
|
}
|
||||||
|
if !before.IsZero() {
|
||||||
|
fmt.Fprintf(&queryBuf, " AND %s.nanotime < ?", table)
|
||||||
|
args = append(args, before.UnixNano())
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&queryBuf, " ORDER BY %[1]s.nanotime %[2]s LIMIT ?;", table, direction)
|
||||||
|
args = append(args, limit)
|
||||||
|
|
||||||
|
results, err = mysql.selectItems(queryBuf.String(), args...)
|
||||||
|
if err == nil && !ascending {
|
||||||
|
history.Reverse(results)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) Close() {
|
||||||
|
// closing the database will close our prepared statements as well
|
||||||
|
if mysql.db != nil {
|
||||||
|
mysql.db.Close()
|
||||||
|
}
|
||||||
|
mysql.db = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements history.Sequence, emulating a single history buffer (for a channel,
|
||||||
|
// a single user's DMs, or a DM conversation)
|
||||||
|
type mySQLHistorySequence struct {
|
||||||
|
mysql *MySQL
|
||||||
|
sender string
|
||||||
|
recipient string
|
||||||
|
cutoff time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (results []history.Item, complete bool, err error) {
|
||||||
|
startTime := start.Time
|
||||||
|
if start.Msgid != "" {
|
||||||
|
startTime, err = s.mysql.msgidToTime(start.Msgid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endTime := end.Time
|
||||||
|
if end.Msgid != "" {
|
||||||
|
endTime, err = s.mysql.msgidToTime(end.Msgid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err = s.mysql.BetweenTimestamps(s.sender, s.recipient, startTime, endTime, s.cutoff, limit)
|
||||||
|
return results, (err == nil), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (results []history.Item, err error) {
|
||||||
|
return history.GenericAround(s, start, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql *MySQL) MakeSequence(sender, recipient string, cutoff time.Time) history.Sequence {
|
||||||
|
return &mySQLHistorySequence{
|
||||||
|
sender: sender,
|
||||||
|
recipient: recipient,
|
||||||
|
mysql: mysql,
|
||||||
|
cutoff: cutoff,
|
||||||
|
}
|
||||||
|
}
|
23
irc/mysql/serialization.go
Normal file
23
irc/mysql/serialization.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/oragono/oragono/irc/history"
|
||||||
|
"github.com/oragono/oragono/irc/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 123 / '{' is the magic number that means JSON;
|
||||||
|
// if we want to do a binary encoding later, we just have to add different magic version numbers
|
||||||
|
|
||||||
|
func marshalItem(item *history.Item) (result []byte, err error) {
|
||||||
|
return json.Marshal(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalItem(data []byte, result *history.Item) (err error) {
|
||||||
|
return json.Unmarshal(data, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMsgid(msgid string) ([]byte, error) {
|
||||||
|
return utils.B32Encoder.DecodeString(msgid)
|
||||||
|
}
|
@ -43,13 +43,15 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
|||||||
hadNick := target.HasNick()
|
hadNick := target.HasNick()
|
||||||
origNickMask := target.NickMaskString()
|
origNickMask := target.NickMaskString()
|
||||||
details := target.Details()
|
details := target.Details()
|
||||||
err := client.server.clients.SetNick(target, session, nickname)
|
assignedNickname, err := client.server.clients.SetNick(target, session, nickname)
|
||||||
if err == errNicknameInUse {
|
if err == errNicknameInUse {
|
||||||
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
|
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
|
||||||
} else if err == errNicknameReserved {
|
} else if err == errNicknameReserved {
|
||||||
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
|
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
|
||||||
} else if err == errNicknameInvalid {
|
} else if err == errNicknameInvalid {
|
||||||
rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, utils.SafeErrorParam(nickname), client.t("Erroneous nickname"))
|
rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, utils.SafeErrorParam(nickname), client.t("Erroneous nickname"))
|
||||||
|
} else if err == errCantChangeNick {
|
||||||
|
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, utils.SafeErrorParam(nickname), client.t(err.Error()))
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
|
rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
|
||||||
}
|
}
|
||||||
@ -64,26 +66,26 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
|||||||
AccountName: details.accountName,
|
AccountName: details.accountName,
|
||||||
Message: message,
|
Message: message,
|
||||||
}
|
}
|
||||||
histItem.Params[0] = nickname
|
histItem.Params[0] = assignedNickname
|
||||||
|
|
||||||
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, client.NickCasefolded()))
|
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, assignedNickname, client.NickCasefolded()))
|
||||||
if hadNick {
|
if hadNick {
|
||||||
if client == target {
|
if client == target {
|
||||||
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, nickname))
|
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, assignedNickname))
|
||||||
} else {
|
} else {
|
||||||
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("Operator %s changed nickname of $%s$r to %s"), client.Nick(), details.nick, nickname))
|
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("Operator %s changed nickname of $%s$r to %s"), client.Nick(), details.nick, assignedNickname))
|
||||||
}
|
}
|
||||||
target.server.whoWas.Append(details.WhoWas)
|
target.server.whoWas.Append(details.WhoWas)
|
||||||
rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
|
rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname)
|
||||||
for session := range target.Friends() {
|
for session := range target.Friends() {
|
||||||
if session != rb.session {
|
if session != rb.session {
|
||||||
session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
|
session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", assignedNickname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, channel := range client.Channels() {
|
for _, channel := range client.Channels() {
|
||||||
channel.history.Add(histItem)
|
channel.AddHistoryItem(histItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
if target.Registered() {
|
if target.Registered() {
|
||||||
|
@ -217,7 +217,7 @@ information on the settings and their possible values, see HELP SET.`,
|
|||||||
helpStrings: []string{
|
helpStrings: []string{
|
||||||
`Syntax $bSET <setting> <value>$b
|
`Syntax $bSET <setting> <value>$b
|
||||||
|
|
||||||
Set modifies your account settings. The following settings are available:`,
|
SET modifies your account settings. The following settings are available:`,
|
||||||
|
|
||||||
`$bENFORCE$b
|
`$bENFORCE$b
|
||||||
'enforce' lets you specify a custom enforcement mechanism for your registered
|
'enforce' lets you specify a custom enforcement mechanism for your registered
|
||||||
@ -247,6 +247,22 @@ lines for join and part. This provides more information about the context of
|
|||||||
messages, but may be spammy. Your options are 'always', 'never', and the default
|
messages, but may be spammy. Your options are 'always', 'never', and the default
|
||||||
of 'commands-only' (the messages will be replayed in /HISTORY output, but not
|
of 'commands-only' (the messages will be replayed in /HISTORY output, but not
|
||||||
during autoreplay).`,
|
during autoreplay).`,
|
||||||
|
`$bALWAYS-ON$b
|
||||||
|
'always-on' controls whether your nickname/identity will remain active
|
||||||
|
even while you are disconnected from the server. Your options are 'true',
|
||||||
|
'false', and 'default' (use the server default value).`,
|
||||||
|
`$bAUTOREPLAY-MISSED$b
|
||||||
|
'autoreplay-missed' is only effective for always-on clients. If enabled,
|
||||||
|
if you have at most one active session, the server will remember the time
|
||||||
|
you disconnect and then replay missed messages to you when you reconnect.
|
||||||
|
Your options are 'on' and 'off'.`,
|
||||||
|
`$bDM-HISTORY$b
|
||||||
|
'dm-history' is only effective for always-on clients. It lets you control
|
||||||
|
how the history of your direct messages is stored. Your options are:
|
||||||
|
1. 'off' [no history]
|
||||||
|
2. 'ephemeral' [a limited amount of temporary history, not stored on disk]
|
||||||
|
3. 'on' [history stored in a permanent database, if available]
|
||||||
|
4. 'default' [use the server default]`,
|
||||||
},
|
},
|
||||||
authRequired: true,
|
authRequired: true,
|
||||||
enabled: servCmdRequiresAccreg,
|
enabled: servCmdRequiresAccreg,
|
||||||
@ -349,6 +365,31 @@ func displaySetting(settingName string, settings AccountSettings, client *Client
|
|||||||
nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account"))
|
nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "always-on":
|
||||||
|
stored := settings.AlwaysOn
|
||||||
|
actual := client.AlwaysOn()
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("Your stored always-on setting is: %s"), persistentStatusToString(stored)))
|
||||||
|
if actual {
|
||||||
|
nsNotice(rb, client.t("Given current server settings, your client is always-on"))
|
||||||
|
} else {
|
||||||
|
nsNotice(rb, client.t("Given current server settings, your client is not always-on"))
|
||||||
|
}
|
||||||
|
case "autoreplay-missed":
|
||||||
|
stored := settings.AutoreplayMissed
|
||||||
|
if stored {
|
||||||
|
if client.AlwaysOn() {
|
||||||
|
nsNotice(rb, client.t("Autoreplay of missed messages is enabled"))
|
||||||
|
} else {
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nsNotice(rb, client.t("Your account is not configured to receive autoreplayed missed messages"))
|
||||||
|
}
|
||||||
|
case "dm-history":
|
||||||
|
effectiveValue := historyEnabled(config.History.Persistent.DirectMessages, settings.DMHistory)
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
|
||||||
|
|
||||||
default:
|
default:
|
||||||
nsNotice(rb, client.t("No such setting"))
|
nsNotice(rb, client.t("No such setting"))
|
||||||
}
|
}
|
||||||
@ -429,6 +470,37 @@ func nsSetHandler(server *Server, client *Client, command string, params []strin
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "always-on":
|
||||||
|
var newValue PersistentStatus
|
||||||
|
newValue, err = persistentStatusFromString(params[1])
|
||||||
|
// "opt-in" and "opt-out" don't make sense as user preferences
|
||||||
|
if err == nil && newValue != PersistentOptIn && newValue != PersistentOptOut {
|
||||||
|
munger = func(in AccountSettings) (out AccountSettings, err error) {
|
||||||
|
out = in
|
||||||
|
out.AlwaysOn = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "autoreplay-missed":
|
||||||
|
var newValue bool
|
||||||
|
newValue, err = utils.StringToBool(params[1])
|
||||||
|
if err == nil {
|
||||||
|
munger = func(in AccountSettings) (out AccountSettings, err error) {
|
||||||
|
out = in
|
||||||
|
out.AutoreplayMissed = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "dm-history":
|
||||||
|
var newValue HistoryStatus
|
||||||
|
newValue, err = historyStatusFromString(params[1])
|
||||||
|
if err == nil {
|
||||||
|
munger = func(in AccountSettings) (out AccountSettings, err error) {
|
||||||
|
out = in
|
||||||
|
out.DMHistory = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = errInvalidParams
|
err = errInvalidParams
|
||||||
}
|
}
|
||||||
@ -480,6 +552,9 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
|
|||||||
} else if ghost == client {
|
} else if ghost == client {
|
||||||
nsNotice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
|
nsNotice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
|
||||||
return
|
return
|
||||||
|
} else if ghost.AlwaysOn() {
|
||||||
|
nsNotice(rb, client.t("You can't GHOST an always-on client"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authorized := false
|
authorized := false
|
||||||
|
@ -58,6 +58,10 @@ func NewResponseBuffer(session *Session) *ResponseBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rb *ResponseBuffer) AddMessage(msg ircmsg.IrcMessage) {
|
func (rb *ResponseBuffer) AddMessage(msg ircmsg.IrcMessage) {
|
||||||
|
if rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if rb.finalized {
|
if rb.finalized {
|
||||||
rb.target.server.logger.Error("internal", "message added to finalized ResponseBuffer, undefined behavior")
|
rb.target.server.logger.Error("internal", "message added to finalized ResponseBuffer, undefined behavior")
|
||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
@ -80,12 +84,20 @@ func (rb *ResponseBuffer) setNestedBatchTag(msg *ircmsg.IrcMessage) {
|
|||||||
|
|
||||||
// Add adds a standard new message to our queue.
|
// Add adds a standard new message to our queue.
|
||||||
func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command string, params ...string) {
|
func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command string, params ...string) {
|
||||||
|
if rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...))
|
rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast adds a standard new message to our queue, then sends an unlabeled copy
|
// Broadcast adds a standard new message to our queue, then sends an unlabeled copy
|
||||||
// to all other sessions.
|
// to all other sessions.
|
||||||
func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, command string, params ...string) {
|
func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, command string, params ...string) {
|
||||||
|
if rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// can't reuse the IrcMessage object because of tag pollution :-\
|
// can't reuse the IrcMessage object because of tag pollution :-\
|
||||||
rb.Add(tags, prefix, command, params...)
|
rb.Add(tags, prefix, command, params...)
|
||||||
for _, session := range rb.session.client.Sessions() {
|
for _, session := range rb.session.client.Sessions() {
|
||||||
@ -97,6 +109,10 @@ func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, comma
|
|||||||
|
|
||||||
// AddFromClient adds a new message from a specific client to our queue.
|
// AddFromClient adds a new message from a specific client to our queue.
|
||||||
func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) {
|
func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) {
|
||||||
|
if rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
|
msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
|
||||||
if rb.session.capabilities.Has(caps.MessageTags) {
|
if rb.session.capabilities.Has(caps.MessageTags) {
|
||||||
msg.UpdateTags(tags)
|
msg.UpdateTags(tags)
|
||||||
@ -118,10 +134,14 @@ func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMa
|
|||||||
|
|
||||||
// AddSplitMessageFromClient adds a new split message from a specific client to our queue.
|
// AddSplitMessageFromClient adds a new split message from a specific client to our queue.
|
||||||
func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
|
func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
|
||||||
|
if rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if message.Is512() {
|
if message.Is512() {
|
||||||
rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
|
rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
|
||||||
} else {
|
} else {
|
||||||
if message.IsMultiline() && rb.session.capabilities.Has(caps.Multiline) {
|
if rb.session.capabilities.Has(caps.Multiline) {
|
||||||
batch := rb.session.composeMultilineBatch(fromNickMask, fromAccount, tags, command, target, message)
|
batch := rb.session.composeMultilineBatch(fromNickMask, fromAccount, tags, command, target, message)
|
||||||
rb.setNestedBatchTag(&batch[0])
|
rb.setNestedBatchTag(&batch[0])
|
||||||
rb.setNestedBatchTag(&batch[len(batch)-1])
|
rb.setNestedBatchTag(&batch[len(batch)-1])
|
||||||
@ -165,6 +185,10 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
|
|||||||
// Starts a nested batch (see the ResponseBuffer struct definition for a description of
|
// Starts a nested batch (see the ResponseBuffer struct definition for a description of
|
||||||
// how this works)
|
// how this works)
|
||||||
func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) {
|
func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) {
|
||||||
|
if rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
batchID = rb.session.generateBatchID()
|
batchID = rb.session.generateBatchID()
|
||||||
msgParams := make([]string, len(params)+2)
|
msgParams := make([]string, len(params)+2)
|
||||||
msgParams[0] = "+" + batchID
|
msgParams[0] = "+" + batchID
|
||||||
@ -194,6 +218,10 @@ func (rb *ResponseBuffer) EndNestedBatch(batchID string) {
|
|||||||
// Convenience to start a nested batch for history lines, at the highest level
|
// Convenience to start a nested batch for history lines, at the highest level
|
||||||
// supported by the client (`history`, `chathistory`, or no batch, in descending order).
|
// supported by the client (`history`, `chathistory`, or no batch, in descending order).
|
||||||
func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID string) {
|
func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID string) {
|
||||||
|
if rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var batchType string
|
var batchType string
|
||||||
if rb.session.capabilities.Has(caps.EventPlayback) {
|
if rb.session.capabilities.Has(caps.EventPlayback) {
|
||||||
batchType = "history"
|
batchType = "history"
|
||||||
@ -210,6 +238,10 @@ func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID str
|
|||||||
// Afterwards, the buffer is in an undefined state and MUST NOT be used further.
|
// Afterwards, the buffer is in an undefined state and MUST NOT be used further.
|
||||||
// If `blocking` is true you MUST be sending to the client from its own goroutine.
|
// If `blocking` is true you MUST be sending to the client from its own goroutine.
|
||||||
func (rb *ResponseBuffer) Send(blocking bool) error {
|
func (rb *ResponseBuffer) Send(blocking bool) error {
|
||||||
|
if rb == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return rb.flushInternal(true, blocking)
|
return rb.flushInternal(true, blocking)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,6 +250,10 @@ func (rb *ResponseBuffer) Send(blocking bool) error {
|
|||||||
// to ensure that the final `BATCH -` message is sent.
|
// to ensure that the final `BATCH -` message is sent.
|
||||||
// If `blocking` is true you MUST be sending to the client from its own goroutine.
|
// If `blocking` is true you MUST be sending to the client from its own goroutine.
|
||||||
func (rb *ResponseBuffer) Flush(blocking bool) error {
|
func (rb *ResponseBuffer) Flush(blocking bool) error {
|
||||||
|
if rb == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return rb.flushInternal(false, blocking)
|
return rb.flushInternal(false, blocking)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,5 +328,9 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
|||||||
|
|
||||||
// Notice sends the client the given notice from the server.
|
// Notice sends the client the given notice from the server.
|
||||||
func (rb *ResponseBuffer) Notice(text string) {
|
func (rb *ResponseBuffer) Notice(text string) {
|
||||||
rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.nick, text)
|
if rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.Nick(), text)
|
||||||
}
|
}
|
||||||
|
105
irc/server.go
105
irc/server.go
@ -24,8 +24,10 @@ import (
|
|||||||
"github.com/goshuirc/irc-go/ircfmt"
|
"github.com/goshuirc/irc-go/ircfmt"
|
||||||
"github.com/oragono/oragono/irc/caps"
|
"github.com/oragono/oragono/irc/caps"
|
||||||
"github.com/oragono/oragono/irc/connection_limits"
|
"github.com/oragono/oragono/irc/connection_limits"
|
||||||
|
"github.com/oragono/oragono/irc/history"
|
||||||
"github.com/oragono/oragono/irc/logger"
|
"github.com/oragono/oragono/irc/logger"
|
||||||
"github.com/oragono/oragono/irc/modes"
|
"github.com/oragono/oragono/irc/modes"
|
||||||
|
"github.com/oragono/oragono/irc/mysql"
|
||||||
"github.com/oragono/oragono/irc/sno"
|
"github.com/oragono/oragono/irc/sno"
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
)
|
)
|
||||||
@ -84,6 +86,7 @@ type Server struct {
|
|||||||
signals chan os.Signal
|
signals chan os.Signal
|
||||||
snomasks SnoManager
|
snomasks SnoManager
|
||||||
store *buntdb.DB
|
store *buntdb.DB
|
||||||
|
historyDB mysql.MySQL
|
||||||
torLimiter connection_limits.TorLimiter
|
torLimiter connection_limits.TorLimiter
|
||||||
whoWas WhoWasList
|
whoWas WhoWasList
|
||||||
stats Stats
|
stats Stats
|
||||||
@ -122,7 +125,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
|||||||
server.monitorManager.Initialize()
|
server.monitorManager.Initialize()
|
||||||
server.snomasks.Initialize()
|
server.snomasks.Initialize()
|
||||||
|
|
||||||
if err := server.applyConfig(config, true); err != nil {
|
if err := server.applyConfig(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +146,8 @@ func (server *Server) Shutdown() {
|
|||||||
if err := server.store.Close(); err != nil {
|
if err := server.store.Close(); err != nil {
|
||||||
server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
|
server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.historyDB.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the server.
|
// Run starts the server.
|
||||||
@ -316,7 +321,7 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
|||||||
|
|
||||||
// client MUST send PASS if necessary, or authenticate with SASL if necessary,
|
// client MUST send PASS if necessary, or authenticate with SASL if necessary,
|
||||||
// before completing the other registration commands
|
// before completing the other registration commands
|
||||||
authOutcome := c.isAuthorized(server.Config())
|
authOutcome := c.isAuthorized(server.Config(), session.isTor)
|
||||||
var quitMessage string
|
var quitMessage string
|
||||||
switch authOutcome {
|
switch authOutcome {
|
||||||
case authFailPass:
|
case authFailPass:
|
||||||
@ -376,7 +381,7 @@ func (server *Server) playRegistrationBurst(session *Session) {
|
|||||||
// continue registration
|
// continue registration
|
||||||
d := c.Details()
|
d := c.Details()
|
||||||
server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname))
|
server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname))
|
||||||
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))
|
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))
|
||||||
|
|
||||||
// send welcome text
|
// send welcome text
|
||||||
//NOTE(dan): we specifically use the NICK here instead of the nickmask
|
//NOTE(dan): we specifically use the NICK here instead of the nickmask
|
||||||
@ -550,7 +555,7 @@ func (server *Server) rehash() error {
|
|||||||
return fmt.Errorf("Error loading config file config: %s", err.Error())
|
return fmt.Errorf("Error loading config file config: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = server.applyConfig(config, false)
|
err = server.applyConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error applying config changes: %s", err.Error())
|
return fmt.Errorf("Error applying config changes: %s", err.Error())
|
||||||
}
|
}
|
||||||
@ -558,7 +563,10 @@ func (server *Server) rehash() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
func (server *Server) applyConfig(config *Config) (err error) {
|
||||||
|
oldConfig := server.Config()
|
||||||
|
initial := oldConfig == nil
|
||||||
|
|
||||||
if initial {
|
if initial {
|
||||||
server.configFilename = config.Filename
|
server.configFilename = config.Filename
|
||||||
server.name = config.Server.Name
|
server.name = config.Server.Name
|
||||||
@ -568,7 +576,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
|||||||
// enforce configs that can't be changed after launch:
|
// enforce configs that can't be changed after launch:
|
||||||
if server.name != config.Server.Name {
|
if server.name != config.Server.Name {
|
||||||
return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
|
return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
|
||||||
} else if server.Config().Datastore.Path != config.Datastore.Path {
|
} else if oldConfig.Datastore.Path != config.Datastore.Path {
|
||||||
return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
|
return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
|
||||||
} else if globalCasemappingSetting != config.Server.Casemapping {
|
} else if globalCasemappingSetting != config.Server.Casemapping {
|
||||||
return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
|
return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
|
||||||
@ -576,7 +584,6 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server.logger.Info("server", "Using config file", server.configFilename)
|
server.logger.Info("server", "Using config file", server.configFilename)
|
||||||
oldConfig := server.Config()
|
|
||||||
|
|
||||||
// first, reload config sections for functionality implemented in subpackages:
|
// first, reload config sections for functionality implemented in subpackages:
|
||||||
wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
|
wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO()
|
||||||
@ -609,14 +616,13 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
|||||||
if !oldConfig.Channels.Registration.Enabled {
|
if !oldConfig.Channels.Registration.Enabled {
|
||||||
server.channels.loadRegisteredChannels(config)
|
server.channels.loadRegisteredChannels(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resize history buffers as needed
|
// resize history buffers as needed
|
||||||
if oldConfig.History != config.History {
|
if oldConfig.History != config.History {
|
||||||
for _, channel := range server.channels.Channels() {
|
for _, channel := range server.channels.Channels() {
|
||||||
channel.history.Resize(config.History.ChannelLength, config.History.AutoresizeWindow)
|
channel.resizeHistory(config)
|
||||||
}
|
}
|
||||||
for _, client := range server.clients.AllClients() {
|
for _, client := range server.clients.AllClients() {
|
||||||
client.history.Resize(config.History.ClientLength, config.History.AutoresizeWindow)
|
client.resizeHistory(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -658,6 +664,10 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
|||||||
if err := server.loadDatastore(config); err != nil {
|
if err := server.loadDatastore(config); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if config.Datastore.MySQL.Enabled {
|
||||||
|
server.historyDB.SetExpireTime(config.History.Restrictions.ExpireTime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server.setupPprofListener(config)
|
server.setupPprofListener(config)
|
||||||
@ -778,6 +788,15 @@ func (server *Server) loadDatastore(config *Config) error {
|
|||||||
server.channels.Initialize(server)
|
server.channels.Initialize(server)
|
||||||
server.accounts.Initialize(server)
|
server.accounts.Initialize(server)
|
||||||
|
|
||||||
|
if config.Datastore.MySQL.Enabled {
|
||||||
|
server.historyDB.Initialize(server.logger, config.History.Restrictions.ExpireTime)
|
||||||
|
err = server.historyDB.Open(config.Datastore.MySQL.User, config.Datastore.MySQL.Password, config.Datastore.MySQL.Host, config.Datastore.MySQL.Port, config.Datastore.MySQL.HistoryDatabase)
|
||||||
|
if err != nil {
|
||||||
|
server.logger.Error("internal", "could not connect to mysql", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -835,6 +854,72 @@ func (server *Server) setupListeners(config *Config) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets the abstract sequence from which we're going to query history;
|
||||||
|
// we may already know the channel we're querying, or we may have
|
||||||
|
// to look it up via a string target. This function is responsible for
|
||||||
|
// privilege checking.
|
||||||
|
func (server *Server) GetHistorySequence(providedChannel *Channel, client *Client, target string) (channel *Channel, sequence history.Sequence, err error) {
|
||||||
|
config := server.Config()
|
||||||
|
var sender, recipient string
|
||||||
|
var hist *history.Buffer
|
||||||
|
if target == "*" {
|
||||||
|
if client.AlwaysOn() {
|
||||||
|
recipient = client.NickCasefolded()
|
||||||
|
} else {
|
||||||
|
hist = &client.history
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel = providedChannel
|
||||||
|
if channel == nil {
|
||||||
|
channel = server.channels.Get(target)
|
||||||
|
}
|
||||||
|
if channel != nil {
|
||||||
|
if !channel.hasClient(client) {
|
||||||
|
err = errInsufficientPrivs
|
||||||
|
return
|
||||||
|
}
|
||||||
|
persistent, ephemeral, cfTarget := channel.historyStatus(config)
|
||||||
|
if persistent {
|
||||||
|
recipient = cfTarget
|
||||||
|
} else if ephemeral {
|
||||||
|
hist = &channel.history
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sender = client.NickCasefolded()
|
||||||
|
var cfTarget string
|
||||||
|
cfTarget, err = CasefoldName(target)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recipient = cfTarget
|
||||||
|
if !client.AlwaysOn() {
|
||||||
|
hist = &client.history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cutoff time.Time
|
||||||
|
if config.History.Restrictions.ExpireTime != 0 {
|
||||||
|
cutoff = time.Now().UTC().Add(-config.History.Restrictions.ExpireTime)
|
||||||
|
}
|
||||||
|
if config.History.Restrictions.EnforceRegistrationDate {
|
||||||
|
regCutoff := client.historyCutoff()
|
||||||
|
regCutoff.Add(-config.History.Restrictions.GracePeriod)
|
||||||
|
// take the earlier of the two cutoffs
|
||||||
|
if regCutoff.After(cutoff) {
|
||||||
|
cutoff = regCutoff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hist != nil {
|
||||||
|
sequence = hist.MakeSequence(recipient, cutoff)
|
||||||
|
} else if recipient != "" {
|
||||||
|
sequence = server.historyDB.MakeSequence(sender, recipient, cutoff)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// elistMatcher takes and matches ELIST conditions
|
// elistMatcher takes and matches ELIST conditions
|
||||||
type elistMatcher struct {
|
type elistMatcher struct {
|
||||||
MinClientsActive bool
|
MinClientsActive bool
|
||||||
|
20
irc/stats.go
20
irc/stats.go
@ -26,15 +26,33 @@ func (s *Stats) Add() {
|
|||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activates a registered client, e.g., for the initial attach to a persistent client
|
||||||
|
func (s *Stats) AddRegistered(invisible, operator bool) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
if invisible {
|
||||||
|
s.Invisible += 1
|
||||||
|
}
|
||||||
|
if operator {
|
||||||
|
s.Operators += 1
|
||||||
|
}
|
||||||
|
s.Total += 1
|
||||||
|
s.setMax()
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Transition a client from unregistered to registered
|
// Transition a client from unregistered to registered
|
||||||
func (s *Stats) Register() {
|
func (s *Stats) Register() {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
s.Unknown -= 1
|
s.Unknown -= 1
|
||||||
s.Total += 1
|
s.Total += 1
|
||||||
|
s.setMax()
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stats) setMax() {
|
||||||
if s.Max < s.Total {
|
if s.Max < s.Total {
|
||||||
s.Max = s.Total
|
s.Max = s.Total
|
||||||
}
|
}
|
||||||
s.mutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modify the Invisible count
|
// Modify the Invisible count
|
||||||
|
@ -5,7 +5,13 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IRCv3TimestampFormat = "2006-01-02T15:04:05.000Z"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -45,9 +51,9 @@ func ArgsToStrings(maxLength int, arguments []string, delim string) []string {
|
|||||||
|
|
||||||
func StringToBool(str string) (result bool, err error) {
|
func StringToBool(str string) (result bool, err error) {
|
||||||
switch strings.ToLower(str) {
|
switch strings.ToLower(str) {
|
||||||
case "on", "true", "t", "yes", "y":
|
case "on", "true", "t", "yes", "y", "disabled":
|
||||||
result = true
|
result = true
|
||||||
case "off", "false", "f", "no", "n":
|
case "off", "false", "f", "no", "n", "enabled":
|
||||||
result = false
|
result = false
|
||||||
default:
|
default:
|
||||||
err = ErrInvalidParams
|
err = ErrInvalidParams
|
||||||
@ -63,3 +69,16 @@ func SafeErrorParam(param string) string {
|
|||||||
}
|
}
|
||||||
return param
|
return param
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IncompatibleSchemaError struct {
|
||||||
|
CurrentVersion string
|
||||||
|
RequiredVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *IncompatibleSchemaError) Error() string {
|
||||||
|
return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.RequiredVersion, err.CurrentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NanoToTimestamp(nanotime int64) string {
|
||||||
|
return time.Unix(0, nanotime).Format(IRCv3TimestampFormat)
|
||||||
|
}
|
||||||
|
@ -18,14 +18,6 @@ var (
|
|||||||
validHostnameLabelRegexp = regexp.MustCompile(`^[0-9A-Za-z.\-]+$`)
|
validHostnameLabelRegexp = regexp.MustCompile(`^[0-9A-Za-z.\-]+$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddrIsLocal returns whether the address is from a trusted local connection (loopback or unix).
|
|
||||||
func AddrIsLocal(addr net.Addr) bool {
|
|
||||||
if tcpaddr, ok := addr.(*net.TCPAddr); ok {
|
|
||||||
return tcpaddr.IP.IsLoopback()
|
|
||||||
}
|
|
||||||
return AddrIsUnix(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddrToIP returns the IP address for a net.Addr; unix domain sockets are treated as IPv4 loopback
|
// AddrToIP returns the IP address for a net.Addr; unix domain sockets are treated as IPv4 loopback
|
||||||
func AddrToIP(addr net.Addr) net.IP {
|
func AddrToIP(addr net.Addr) net.IP {
|
||||||
if tcpaddr, ok := addr.(*net.TCPAddr); ok {
|
if tcpaddr, ok := addr.(*net.TCPAddr); ok {
|
||||||
|
@ -23,9 +23,9 @@ type MessagePair struct {
|
|||||||
// SplitMessage represents a message that's been split for sending.
|
// SplitMessage represents a message that's been split for sending.
|
||||||
// Two possibilities:
|
// Two possibilities:
|
||||||
// (a) Standard message that can be relayed on a single 512-byte line
|
// (a) Standard message that can be relayed on a single 512-byte line
|
||||||
// (MessagePair contains the message, Wrapped == nil)
|
// (MessagePair contains the message, Split == nil)
|
||||||
// (b) multiline message that was split on the client side
|
// (b) multiline message that was split on the client side
|
||||||
// (Message == "", Wrapped contains the split lines)
|
// (Message == "", Split contains the split lines)
|
||||||
type SplitMessage struct {
|
type SplitMessage struct {
|
||||||
Message string
|
Message string
|
||||||
Msgid string
|
Msgid string
|
||||||
@ -36,7 +36,7 @@ type SplitMessage struct {
|
|||||||
func MakeMessage(original string) (result SplitMessage) {
|
func MakeMessage(original string) (result SplitMessage) {
|
||||||
result.Message = original
|
result.Message = original
|
||||||
result.Msgid = GenerateSecretToken()
|
result.Msgid = GenerateSecretToken()
|
||||||
result.Time = time.Now().UTC()
|
result.SetTime()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -52,7 +52,8 @@ func (sm *SplitMessage) Append(message string, concat bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SplitMessage) SetTime() {
|
func (sm *SplitMessage) SetTime() {
|
||||||
sm.Time = time.Now().UTC()
|
// strip the monotonic time, it's a potential source of problems:
|
||||||
|
sm.Time = time.Now().UTC().Round(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SplitMessage) LenLines() int {
|
func (sm *SplitMessage) LenLines() int {
|
||||||
@ -88,10 +89,6 @@ func (sm *SplitMessage) IsRestrictedCTCPMessage() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SplitMessage) IsMultiline() bool {
|
|
||||||
return sm.Message == "" && len(sm.Split) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sm *SplitMessage) Is512() bool {
|
func (sm *SplitMessage) Is512() bool {
|
||||||
return sm.Message != ""
|
return sm.Message != ""
|
||||||
}
|
}
|
||||||
|
18
irc/znc.go
18
irc/znc.go
@ -8,6 +8,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/oragono/oragono/irc/history"
|
||||||
)
|
)
|
||||||
|
|
||||||
type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
|
type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
|
||||||
@ -89,10 +91,8 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
|
|||||||
// 3.3 When the client sends a subsequent redundant JOIN line for those
|
// 3.3 When the client sends a subsequent redundant JOIN line for those
|
||||||
// channels; redundant JOIN is a complete no-op so we won't replay twice
|
// channels; redundant JOIN is a complete no-op so we won't replay twice
|
||||||
|
|
||||||
config := client.server.Config()
|
|
||||||
if params[1] == "*" {
|
if params[1] == "*" {
|
||||||
items, _ := client.history.Between(after, before, false, config.History.ChathistoryMax)
|
zncPlayPrivmsgs(client, rb, after, before)
|
||||||
client.replayPrivmsgHistory(rb, items, true)
|
|
||||||
} else {
|
} else {
|
||||||
targets = make(StringSet)
|
targets = make(StringSet)
|
||||||
// TODO actually handle nickname targets
|
// TODO actually handle nickname targets
|
||||||
@ -116,3 +116,15 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func zncPlayPrivmsgs(client *Client, rb *ResponseBuffer, after, before time.Time) {
|
||||||
|
_, sequence, _ := client.server.GetHistorySequence(nil, client, "*")
|
||||||
|
if sequence == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zncMax := client.server.Config().History.ZNCMax
|
||||||
|
items, _, err := sequence.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
|
||||||
|
if err == nil {
|
||||||
|
client.replayPrivmsgHistory(rb, items, "", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
64
oragono.yaml
64
oragono.yaml
@ -245,6 +245,15 @@ server:
|
|||||||
# all users will receive simply `netname` as their cloaked hostname.
|
# all users will receive simply `netname` as their cloaked hostname.
|
||||||
num-bits: 80
|
num-bits: 80
|
||||||
|
|
||||||
|
# secure-nets identifies IPs and CIDRs which are secure at layer 3,
|
||||||
|
# for example, because they are on a trusted internal LAN or a VPN.
|
||||||
|
# plaintext connections from these IPs and CIDRs will be considered
|
||||||
|
# secure (clients will receive the +Z mode and be allowed to resume
|
||||||
|
# or reattach to secure connections). note that loopback IPs are always
|
||||||
|
# considered secure:
|
||||||
|
secure-nets:
|
||||||
|
# - "10.0.0.0/8"
|
||||||
|
|
||||||
|
|
||||||
# account options
|
# account options
|
||||||
accounts:
|
accounts:
|
||||||
@ -351,6 +360,11 @@ accounts:
|
|||||||
# via nickserv
|
# via nickserv
|
||||||
allowed-by-default: true
|
allowed-by-default: true
|
||||||
|
|
||||||
|
# whether to allow clients that remain on the server even
|
||||||
|
# when they have no active connections. The possible values are:
|
||||||
|
# "disabled", "opt-in", "opt-out", or "mandatory".
|
||||||
|
always-on: "disabled"
|
||||||
|
|
||||||
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
|
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
|
||||||
# hostname/IP) by the HostServ service
|
# hostname/IP) by the HostServ service
|
||||||
vhosts:
|
vhosts:
|
||||||
@ -585,6 +599,16 @@ datastore:
|
|||||||
# up, and if the upgrade fails, the original database will be restored.
|
# up, and if the upgrade fails, the original database will be restored.
|
||||||
autoupgrade: true
|
autoupgrade: true
|
||||||
|
|
||||||
|
# connection information for MySQL (currently only used for persistent history):
|
||||||
|
mysql:
|
||||||
|
enabled: false
|
||||||
|
host: "localhost"
|
||||||
|
# port is unnecessary for connections via unix domain socket:
|
||||||
|
#port: 3306
|
||||||
|
user: "oragono"
|
||||||
|
password: "KOHw8WSaRwaoo-avo0qVpQ"
|
||||||
|
history-database: "oragono_history"
|
||||||
|
|
||||||
# languages config
|
# languages config
|
||||||
languages:
|
languages:
|
||||||
# whether to load languages
|
# whether to load languages
|
||||||
@ -657,7 +681,7 @@ fakelag:
|
|||||||
# message history tracking, for the RESUME extension and possibly other uses in future
|
# message history tracking, for the RESUME extension and possibly other uses in future
|
||||||
history:
|
history:
|
||||||
# should we store messages for later playback?
|
# should we store messages for later playback?
|
||||||
# the current implementation stores messages in RAM only; they do not persist
|
# by default, messages are stored in RAM only; they do not persist
|
||||||
# across server restarts. however, you should not enable this unless you understand
|
# across server restarts. however, you should not enable this unless you understand
|
||||||
# how it interacts with the GDPR and/or any data privacy laws that apply
|
# how it interacts with the GDPR and/or any data privacy laws that apply
|
||||||
# in your country and the countries of your users.
|
# in your country and the countries of your users.
|
||||||
@ -683,3 +707,41 @@ history:
|
|||||||
# maximum number of CHATHISTORY messages that can be
|
# maximum number of CHATHISTORY messages that can be
|
||||||
# requested at once (0 disables support for CHATHISTORY)
|
# requested at once (0 disables support for CHATHISTORY)
|
||||||
chathistory-maxmessages: 100
|
chathistory-maxmessages: 100
|
||||||
|
|
||||||
|
# maximum number of messages that can be replayed at once during znc emulation
|
||||||
|
# (znc.in/playback, or automatic replay on initial reattach to a persistent client):
|
||||||
|
znc-maxmessages: 2048
|
||||||
|
|
||||||
|
# options to delete old messages, or prevent them from being retrieved
|
||||||
|
restrictions:
|
||||||
|
# if this is set, messages older than this cannot be retrieved by anyone
|
||||||
|
# (and will eventually be deleted from persistent storage, if that's enabled)
|
||||||
|
#expire-time: 168h # 7 days
|
||||||
|
|
||||||
|
# if this is set, logged-in users cannot retrieve messages older than their
|
||||||
|
# account registration date, and logged-out users cannot retrieve messages
|
||||||
|
# older than their sign-on time (modulo grace-period, see below):
|
||||||
|
enforce-registration-date: false
|
||||||
|
|
||||||
|
# but if this is set, you can retrieve messages that are up to `grace-period`
|
||||||
|
# older than the above cutoff time. this is recommended to allow logged-out
|
||||||
|
# users to do session resumption / query history after disconnections.
|
||||||
|
grace-period: 1h
|
||||||
|
|
||||||
|
# options to store history messages in a persistent database (currently only MySQL):
|
||||||
|
persistent:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# store unregistered channel messages in the persistent database?
|
||||||
|
unregistered-channels: false
|
||||||
|
|
||||||
|
# for a registered channel, the channel owner can potentially customize
|
||||||
|
# the history storage setting. as the server operator, your options are
|
||||||
|
# 'disabled' (no persistent storage, regardless of per-channel setting),
|
||||||
|
# 'opt-in', 'opt-out', and 'mandatory' (force persistent storage, ignoring
|
||||||
|
# per-channel setting):
|
||||||
|
registered-channels: "opt-out"
|
||||||
|
|
||||||
|
# direct messages are only stored in the database for persistent clients;
|
||||||
|
# you can control how they are stored here (same options as above)
|
||||||
|
direct-messages: "opt-out"
|
||||||
|
Loading…
Reference in New Issue
Block a user