3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-11 06:29:29 +01:00

Merge pull request #1496 from slingamn/jointime.1

fix #1490
This commit is contained in:
Shivaram Lingamneni 2021-01-21 01:20:45 -05:00 committed by GitHub
commit 2e7cf3cc1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 274 additions and 92 deletions

View File

@ -877,13 +877,18 @@ history:
# (and will eventually be deleted from persistent storage, if that's enabled) # (and will eventually be deleted from persistent storage, if that's enabled)
expire-time: 1w expire-time: 1w
# if this is set, logged-in users cannot retrieve messages older than their # this restricts access to channel history (it can be overridden by channel
# account registration date, and logged-out users cannot retrieve messages # owners). options are: 'none' (no restrictions), 'registration-time'
# older than their sign-on time (modulo grace-period, see below): # (logged-in users cannot retrieve messages older than their account
enforce-registration-date: false # registration date, and anonymous users cannot retrieve messages older than
# their sign-on time, modulo the grace-period described below), and
# 'join-time' (users cannot retrieve messages older than the time they
# joined the channel, so only always-on clients can view history).
query-cutoff: 'none'
# but if this is set, you can retrieve messages that are up to `grace-period` # if query-cutoff is set to 'registration-time', this allows retrieval
# older than the above cutoff time. this is recommended to allow logged-out # of messages that are up to 'grace-period' older than the above cutoff.
# if you use 'registration-time', this is recommended to allow logged-out
# users to do session resumption / query history after disconnections. # users to do session resumption / query history after disconnections.
grace-period: 1h grace-period: 1h

View File

@ -544,7 +544,12 @@ func (am *AccountManager) setPassword(account string, password string, hasPrivs
return err return err
} }
func (am *AccountManager) saveChannels(account string, channelToModes map[string]string) { type alwaysOnChannelStatus struct {
Modes string
JoinTime int64
}
func (am *AccountManager) saveChannels(account string, channelToModes map[string]alwaysOnChannelStatus) {
j, err := json.Marshal(channelToModes) j, err := json.Marshal(channelToModes)
if err != nil { if err != nil {
am.server.logger.Error("internal", "couldn't marshal channel-to-modes", account, err.Error()) am.server.logger.Error("internal", "couldn't marshal channel-to-modes", account, err.Error())
@ -558,7 +563,7 @@ func (am *AccountManager) saveChannels(account string, channelToModes map[string
}) })
} }
func (am *AccountManager) loadChannels(account string) (channelToModes map[string]string) { func (am *AccountManager) loadChannels(account string) (channelToModes map[string]alwaysOnChannelStatus) {
key := fmt.Sprintf(keyAccountChannelToModes, account) key := fmt.Sprintf(keyAccountChannelToModes, account)
var channelsStr string var channelsStr string
am.server.store.View(func(tx *buntdb.Tx) error { am.server.store.View(func(tx *buntdb.Tx) error {

View File

@ -20,7 +20,8 @@ import (
) )
type ChannelSettings struct { type ChannelSettings struct {
History HistoryStatus History HistoryStatus
QueryCutoff HistoryCutoff
} }
// Channel represents a channel that clients can join. // Channel represents a channel that clients can join.
@ -109,7 +110,7 @@ func (channel *Channel) IsLoaded() bool {
} }
func (channel *Channel) resizeHistory(config *Config) { func (channel *Channel) resizeHistory(config *Config) {
status, _ := channel.historyStatus(config) status, _, _ := channel.historyStatus(config)
if status == HistoryEphemeral { if status == HistoryEphemeral {
channel.history.Resize(config.History.ChannelLength, time.Duration(config.History.AutoresizeWindow)) channel.history.Resize(config.History.ChannelLength, time.Duration(config.History.AutoresizeWindow))
} else { } else {
@ -443,11 +444,11 @@ func (channel *Channel) regenerateMembersCache() {
// Names sends the list of users joined to the channel to the given client. // Names sends the list of users joined to the channel to the given client.
func (channel *Channel) Names(client *Client, rb *ResponseBuffer) { func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
clientModes, isJoined := channel.members[client] clientData, isJoined := channel.members[client]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
isOper := client.HasMode(modes.Operator) isOper := client.HasMode(modes.Operator)
respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper && respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper &&
(!isJoined || clientModes.HighestChannelUserMode() == modes.Mode(0)) (!isJoined || clientData.modes.HighestChannelUserMode() == modes.Mode(0))
isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix) isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames) isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
@ -463,8 +464,9 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
nick = target.Nick() nick = target.Nick()
} }
channel.stateMutex.RLock() channel.stateMutex.RLock()
modeSet := channel.members[target] memberData, _ := channel.members[target]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
modeSet := memberData.modes
if modeSet == nil { if modeSet == nil {
continue continue
} }
@ -519,7 +521,7 @@ func channelUserModeHasPrivsOver(clientMode modes.Mode, targetMode modes.Mode) b
// ClientIsAtLeast returns whether the client has at least the given channel privilege. // ClientIsAtLeast returns whether the client has at least the given channel privilege.
func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool { func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool {
channel.stateMutex.RLock() channel.stateMutex.RLock()
clientModes := channel.members[client] memberData := channel.members[client]
founder := channel.registeredFounder founder := channel.registeredFounder
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
@ -528,7 +530,7 @@ func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) b
} }
for _, mode := range modes.ChannelUserModes { for _, mode := range modes.ChannelUserModes {
if clientModes.HasMode(mode) { if memberData.modes.HasMode(mode) {
return true return true
} }
if mode == permission { if mode == permission {
@ -541,35 +543,37 @@ func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) b
func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) string { func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) string {
channel.stateMutex.RLock() channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock() defer channel.stateMutex.RUnlock()
modes, present := channel.members[client] memberData, present := channel.members[client]
if !present { if !present {
return "" return ""
} else { } else {
return modes.Prefixes(isMultiPrefix) return memberData.modes.Prefixes(isMultiPrefix)
} }
} }
func (channel *Channel) ClientStatus(client *Client) (present bool, cModes modes.Modes) { func (channel *Channel) ClientStatus(client *Client) (present bool, joinTimeSecs int64, cModes modes.Modes) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock() defer channel.stateMutex.RUnlock()
modes, present := channel.members[client] memberData, present := channel.members[client]
return present, modes.AllModes() return present, time.Unix(0, memberData.joinTime).Unix(), memberData.modes.AllModes()
} }
// helper for persisting channel-user modes for always-on clients; // helper for persisting channel-user modes for always-on clients;
// return the channel name and all channel-user modes for a client // return the channel name and all channel-user modes for a client
func (channel *Channel) nameAndModes(client *Client) (chname string, modeStr string) { func (channel *Channel) alwaysOnStatus(client *Client) (chname string, status alwaysOnChannelStatus) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock() defer channel.stateMutex.RUnlock()
chname = channel.name chname = channel.name
modeStr = channel.members[client].String() data := channel.members[client]
status.Modes = data.modes.String()
status.JoinTime = data.joinTime
return return
} }
// overwrite any existing channel-user modes with the stored ones // overwrite any existing channel-user modes with the stored ones
func (channel *Channel) setModesForClient(client *Client, modeStr string) { func (channel *Channel) setMemberStatus(client *Client, status alwaysOnChannelStatus) {
newModes := modes.NewModeSet() newModes := modes.NewModeSet()
for _, mode := range modeStr { for _, mode := range status.Modes {
newModes.SetMode(modes.Mode(mode), true) newModes.SetMode(modes.Mode(mode), true)
} }
channel.stateMutex.Lock() channel.stateMutex.Lock()
@ -577,14 +581,17 @@ func (channel *Channel) setModesForClient(client *Client, modeStr string) {
if _, ok := channel.members[client]; !ok { if _, ok := channel.members[client]; !ok {
return return
} }
channel.members[client] = newModes memberData := channel.members[client]
memberData.modes = newModes
memberData.joinTime = status.JoinTime
channel.members[client] = memberData
} }
func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool { func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
channel.stateMutex.RLock() channel.stateMutex.RLock()
founder := channel.registeredFounder founder := channel.registeredFounder
clientModes := channel.members[client] clientModes := channel.members[client].modes
targetModes := channel.members[target] targetModes := channel.members[target].modes
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
if founder != "" { if founder != "" {
@ -612,7 +619,7 @@ func (channel *Channel) modeStrings(client *Client) (result []string) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock() defer channel.stateMutex.RUnlock()
isMember := hasPrivs || channel.members[client] != nil isMember := hasPrivs || channel.members.Has(client)
showKey := isMember && (channel.key != "") showKey := isMember && (channel.key != "")
showUserLimit := channel.userLimit > 0 showUserLimit := channel.userLimit > 0
showForward := channel.forward != "" showForward := channel.forward != ""
@ -660,18 +667,38 @@ func (channel *Channel) IsEmpty() bool {
// figure out where history is being stored: persistent, ephemeral, or neither // figure out where history is being stored: persistent, ephemeral, or neither
// target is only needed if we're doing persistent history // target is only needed if we're doing persistent history
func (channel *Channel) historyStatus(config *Config) (status HistoryStatus, target string) { func (channel *Channel) historyStatus(config *Config) (status HistoryStatus, target string, restrictions HistoryCutoff) {
if !config.History.Enabled { if !config.History.Enabled {
return HistoryDisabled, "" return HistoryDisabled, "", HistoryCutoffNone
} }
channel.stateMutex.RLock() channel.stateMutex.RLock()
target = channel.nameCasefolded target = channel.nameCasefolded
historyStatus := channel.settings.History settings := channel.settings
registered := channel.registeredFounder != "" registered := channel.registeredFounder != ""
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
return channelHistoryStatus(config, registered, historyStatus), target restrictions = settings.QueryCutoff
if restrictions == HistoryCutoffDefault {
restrictions = config.History.Restrictions.queryCutoff
}
return channelHistoryStatus(config, registered, settings.History), target, restrictions
}
func (channel *Channel) joinTimeCutoff(client *Client) (present bool, cutoff time.Time) {
account := client.Account()
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
if data, ok := channel.members[client]; ok {
present = true
// report a cutoff of zero, i.e., no restriction, if the user is privileged
if !((account != "" && account == channel.registeredFounder) || data.modes.HasMode(modes.ChannelFounder) || data.modes.HasMode(modes.ChannelAdmin) || data.modes.HasMode(modes.ChannelOperator)) {
cutoff = time.Unix(0, data.joinTime)
}
}
return
} }
func channelHistoryStatus(config *Config, registered bool, storedStatus HistoryStatus) (result HistoryStatus) { func channelHistoryStatus(config *Config, registered bool, storedStatus HistoryStatus) (result HistoryStatus) {
@ -697,7 +724,7 @@ func (channel *Channel) AddHistoryItem(item history.Item, account string) (err e
return return
} }
status, target := channel.historyStatus(channel.server.Config()) status, target, _ := channel.historyStatus(channel.server.Config())
if status == HistoryPersistent { if status == HistoryPersistent {
err = channel.server.historyDB.AddChannelItem(target, item, account) err = channel.server.historyDB.AddChannelItem(target, item, account)
} else if status == HistoryEphemeral { } else if status == HistoryEphemeral {
@ -785,7 +812,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
givenMode = persistentMode givenMode = persistentMode
} }
if givenMode != 0 { if givenMode != 0 {
channel.members[client].SetMode(givenMode, true) channel.members[client].modes.SetMode(givenMode, true)
} }
}() }()
@ -825,9 +852,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
for _, member := range channel.Members() { for _, member := range channel.Members() {
if respectAuditorium { if respectAuditorium {
channel.stateMutex.RLock() channel.stateMutex.RLock()
memberModes, ok := channel.members[member] memberData, ok := channel.members[member]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
if !ok || memberModes.HighestChannelUserMode() == modes.Mode(0) { if !ok || memberData.modes.HighestChannelUserMode() == modes.Mode(0) {
continue continue
} }
} }
@ -955,7 +982,7 @@ func (channel *Channel) playJoinForSession(session *Session) {
func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) { func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
chname := channel.name chname := channel.name
clientModes, ok := channel.members[client] clientData, ok := channel.members[client]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
if !ok { if !ok {
@ -974,15 +1001,15 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
params = append(params, message) params = append(params, message)
} }
respectAuditorium := channel.flags.HasMode(modes.Auditorium) && respectAuditorium := channel.flags.HasMode(modes.Auditorium) &&
clientModes.HighestChannelUserMode() == modes.Mode(0) clientData.modes.HighestChannelUserMode() == modes.Mode(0)
var cache MessageCache var cache MessageCache
cache.Initialize(channel.server, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", params...) cache.Initialize(channel.server, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", params...)
for _, member := range channel.Members() { for _, member := range channel.Members() {
if respectAuditorium { if respectAuditorium {
channel.stateMutex.RLock() channel.stateMutex.RLock()
memberModes, ok := channel.members[member] memberData, ok := channel.members[member]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
if !ok || memberModes.HighestChannelUserMode() == modes.Mode(0) { if !ok || memberData.modes.HighestChannelUserMode() == modes.Mode(0) {
continue continue
} }
} }
@ -1022,12 +1049,12 @@ func (channel *Channel) Resume(session *Session, timestamp time.Time) {
func (channel *Channel) resumeAndAnnounce(session *Session) { func (channel *Channel) resumeAndAnnounce(session *Session) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
modeSet := channel.members[session.client] memberData, found := channel.members[session.client]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
if modeSet == nil { if !found {
return return
} }
oldModes := modeSet.String() oldModes := memberData.modes.String()
if 0 < len(oldModes) { if 0 < len(oldModes) {
oldModes = "+" + oldModes oldModes = "+" + oldModes
} }
@ -1271,8 +1298,9 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe
// CanSpeak returns true if the client can speak on this channel, otherwise it returns false along with the channel mode preventing the client from speaking. // CanSpeak returns true if the client can speak on this channel, otherwise it returns false along with the channel mode preventing the client from speaking.
func (channel *Channel) CanSpeak(client *Client) (bool, modes.Mode) { func (channel *Channel) CanSpeak(client *Client) (bool, modes.Mode) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
clientModes, hasClient := channel.members[client] memberData, hasClient := channel.members[client]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
clientModes := memberData.modes
if !hasClient && channel.flags.HasMode(modes.NoOutside) { if !hasClient && channel.flags.HasMode(modes.NoOutside) {
// TODO: enforce regular +b bans on -n channels? // TODO: enforce regular +b bans on -n channels?
@ -1347,9 +1375,9 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
if channel.flags.HasMode(modes.OpModerated) { if channel.flags.HasMode(modes.OpModerated) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
cuModes := channel.members[client] cuData := channel.members[client]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
if cuModes.HighestChannelUserMode() == modes.Mode(0) { if cuData.modes.HighestChannelUserMode() == modes.Mode(0) {
// max(statusmsg_minmode, halfop) // max(statusmsg_minmode, halfop)
if minPrefixMode == modes.Mode(0) || minPrefixMode == modes.Voice { if minPrefixMode == modes.Mode(0) || minPrefixMode == modes.Voice {
minPrefixMode = modes.Halfop minPrefixMode = modes.Halfop
@ -1402,9 +1430,9 @@ func (channel *Channel) applyModeToMember(client *Client, change modes.ModeChang
change.Arg = target.Nick() change.Arg = target.Nick()
channel.stateMutex.Lock() channel.stateMutex.Lock()
modeset, exists := channel.members[target] memberData, exists := channel.members[target]
if exists { if exists {
if modeset.SetMode(change.Mode, change.Op == modes.Add) { if memberData.modes.SetMode(change.Mode, change.Op == modes.Add) {
applied = true applied = true
result = change result = change
} }
@ -1590,19 +1618,19 @@ func (channel *Channel) auditoriumFriends(client *Client) (friends []*Client) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock() defer channel.stateMutex.RUnlock()
clientModes := channel.members[client] clientData, found := channel.members[client]
if clientModes == nil { if !found {
return // non-members have no friends return // non-members have no friends
} }
if !channel.flags.HasMode(modes.Auditorium) { if !channel.flags.HasMode(modes.Auditorium) {
return channel.membersCache // default behavior for members return channel.membersCache // default behavior for members
} }
if clientModes.HighestChannelUserMode() != modes.Mode(0) { if clientData.modes.HighestChannelUserMode() != modes.Mode(0) {
return channel.membersCache // +v and up can see everyone in the auditorium return channel.membersCache // +v and up can see everyone in the auditorium
} }
// without +v, your friends are those with +v and up // without +v, your friends are those with +v and up
for member, memberModes := range channel.members { for member, memberData := range channel.members {
if memberModes.HighestChannelUserMode() != modes.Mode(0) { if memberData.modes.HighestChannelUserMode() != modes.Mode(0) {
friends = append(friends, member) friends = append(friends, member)
} }
} }

View File

@ -171,6 +171,16 @@ SET modifies a channel's settings. The following settings are available:`,
2. 'ephemeral' [a limited amount of temporary history, not stored on disk] 2. 'ephemeral' [a limited amount of temporary history, not stored on disk]
3. 'on' [history stored in a permanent database, if available] 3. 'on' [history stored in a permanent database, if available]
4. 'default' [use the server default]`, 4. 'default' [use the server default]`,
`$bQUERY-CUTOFF$b
'query-cutoff' lets you restrict how much channel history can be retrieved
by unprivileged users. Your options are:
1. 'none' [no restrictions]
2. 'registration-time' [users can view history from after their account was
registered, plus a grace period]
3. 'join-time' [users can biew history from after they joined the
channel; note that history will be effectively
unavailable to clients that are not always-on]
4. 'default' [use the server default]`,
}, },
enabled: chanregEnabled, enabled: chanregEnabled,
minParams: 3, minParams: 3,
@ -340,7 +350,7 @@ func csDeopHandler(service *ircService, server *Server, client *Client, command
target = client target = client
} }
present, cumodes := channel.ClientStatus(target) present, _, cumodes := channel.ClientStatus(target)
if !present || len(cumodes) == 0 { if !present || len(cumodes) == 0 {
service.Notice(rb, client.t("Target has no privileges to remove")) service.Notice(rb, client.t("Target has no privileges to remove"))
return return
@ -764,6 +774,13 @@ func displayChannelSetting(service *ircService, settingName string, settings Cha
effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History) effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
service.Notice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History))) service.Notice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue))) service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
case "query-cutoff":
effectiveValue := settings.QueryCutoff
if effectiveValue == HistoryCutoffDefault {
effectiveValue = config.History.Restrictions.queryCutoff
}
service.Notice(rb, fmt.Sprintf(client.t("The stored channel history query cutoff setting is: %s"), historyCutoffToString(settings.QueryCutoff)))
service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history query cutoff setting is: %s"), historyCutoffToString(effectiveValue)))
default: default:
service.Notice(rb, client.t("Invalid params")) service.Notice(rb, client.t("Invalid params"))
} }
@ -807,6 +824,13 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
} }
channel.SetSettings(settings) channel.SetSettings(settings)
channel.resizeHistory(server.Config()) channel.resizeHistory(server.Config())
case "query-cutoff":
settings.QueryCutoff, err = historyCutoffFromString(value)
if err != nil {
err = errInvalidParams
break
}
channel.SetSettings(settings)
} }
switch err { switch err {

View File

@ -408,7 +408,7 @@ func (server *Server) RunClient(conn IRCConn) {
client.run(session) client.run(session)
} }
func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToModes map[string]string, lastSeen map[string]time.Time, uModes modes.Modes, realname string) { func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus map[string]alwaysOnChannelStatus, lastSeen map[string]time.Time, uModes modes.Modes, realname string) {
now := time.Now().UTC() now := time.Now().UTC()
config := server.Config() config := server.Config()
if lastSeen == nil && account.Settings.AutoreplayMissed { if lastSeen == nil && account.Settings.AutoreplayMissed {
@ -472,12 +472,12 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToModes ma
// XXX set this last to avoid confusing SetNick: // XXX set this last to avoid confusing SetNick:
client.registered = true client.registered = true
for chname, modeStr := range channelToModes { for chname, status := range channelToStatus {
// XXX we're using isSajoin=true, to make these joins succeed even without channel key // 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 // this is *probably* ok as long as the persisted memberships are accurate
server.channels.Join(client, chname, "", true, nil) server.channels.Join(client, chname, "", true, nil)
if channel := server.channels.Get(chname); channel != nil { if channel := server.channels.Get(chname); channel != nil {
channel.setModesForClient(client, modeStr) channel.setMemberStatus(client, status)
} else { } else {
server.logger.Error("internal", "could not create channel", chname) server.logger.Error("internal", "could not create channel", chname)
} }
@ -967,7 +967,7 @@ func (session *Session) playResume() {
for _, member := range channel.auditoriumFriends(client) { for _, member := range channel.auditoriumFriends(client) {
friends.Add(member) friends.Add(member)
} }
status, _ := channel.historyStatus(config) status, _, _ := channel.historyStatus(config)
if status == HistoryEphemeral { if status == HistoryEphemeral {
lastDiscarded := channel.history.LastDiscarded() lastDiscarded := channel.history.LastDiscarded()
if oldestLostMessage.Before(lastDiscarded) { if oldestLostMessage.Before(lastDiscarded) {
@ -2001,10 +2001,10 @@ func (client *Client) performWrite(additionalDirtyBits uint) {
if (dirtyBits & IncludeChannels) != 0 { if (dirtyBits & IncludeChannels) != 0 {
channels := client.Channels() channels := client.Channels()
channelToModes := make(map[string]string, len(channels)) channelToModes := make(map[string]alwaysOnChannelStatus, len(channels))
for _, channel := range channels { for _, channel := range channels {
chname, modes := channel.nameAndModes(client) chname, status := channel.alwaysOnStatus(client)
channelToModes[chname] = modes channelToModes[chname] = status
} }
client.server.accounts.saveChannels(account, channelToModes) client.server.accounts.saveChannels(account, channelToModes)
} }

View File

@ -62,6 +62,45 @@ type listenerConfigBlock struct {
HideSTS bool `yaml:"hide-sts"` HideSTS bool `yaml:"hide-sts"`
} }
type HistoryCutoff uint
const (
HistoryCutoffDefault HistoryCutoff = iota
HistoryCutoffNone
HistoryCutoffRegistrationTime
HistoryCutoffJoinTime
)
func historyCutoffToString(restriction HistoryCutoff) string {
switch restriction {
case HistoryCutoffDefault:
return "default"
case HistoryCutoffNone:
return "none"
case HistoryCutoffRegistrationTime:
return "registration-time"
case HistoryCutoffJoinTime:
return "join-time"
default:
return ""
}
}
func historyCutoffFromString(str string) (result HistoryCutoff, err error) {
switch strings.ToLower(str) {
case "default":
return HistoryCutoffDefault, nil
case "none", "disabled", "off", "false":
return HistoryCutoffNone, nil
case "registration-time":
return HistoryCutoffRegistrationTime, nil
case "join-time":
return HistoryCutoffJoinTime, nil
default:
return HistoryCutoffDefault, errInvalidParams
}
}
type PersistentStatus uint type PersistentStatus uint
const ( const (
@ -615,9 +654,12 @@ type Config struct {
ChathistoryMax int `yaml:"chathistory-maxmessages"` ChathistoryMax int `yaml:"chathistory-maxmessages"`
ZNCMax int `yaml:"znc-maxmessages"` ZNCMax int `yaml:"znc-maxmessages"`
Restrictions struct { Restrictions struct {
ExpireTime custime.Duration `yaml:"expire-time"` ExpireTime custime.Duration `yaml:"expire-time"`
EnforceRegistrationDate bool `yaml:"enforce-registration-date"` // legacy key, superceded by QueryCutoff:
GracePeriod custime.Duration `yaml:"grace-period"` EnforceRegistrationDate_ bool `yaml:"enforce-registration-date"`
QueryCutoff string `yaml:"query-cutoff"`
queryCutoff HistoryCutoff
GracePeriod custime.Duration `yaml:"grace-period"`
} }
Persistent struct { Persistent struct {
Enabled bool Enabled bool
@ -1358,6 +1400,19 @@ func LoadConfig(filename string) (config *Config, err error) {
config.History.ZNCMax = config.History.ChathistoryMax config.History.ZNCMax = config.History.ChathistoryMax
} }
if config.History.Restrictions.QueryCutoff != "" {
config.History.Restrictions.queryCutoff, err = historyCutoffFromString(config.History.Restrictions.QueryCutoff)
if err != nil {
return nil, fmt.Errorf("invalid value of history.query-restrictions: %w", err)
}
} else {
if config.History.Restrictions.EnforceRegistrationDate_ {
config.History.Restrictions.queryCutoff = HistoryCutoffRegistrationTime
} else {
config.History.Restrictions.queryCutoff = HistoryCutoffNone
}
}
config.Roleplay.addSuffix = utils.BoolDefaultTrue(config.Roleplay.AddSuffix) config.Roleplay.addSuffix = utils.BoolDefaultTrue(config.Roleplay.AddSuffix)
config.Datastore.MySQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime) config.Datastore.MySQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime)

View File

@ -24,7 +24,7 @@ const (
// 'version' of the database schema // 'version' of the database schema
keySchemaVersion = "db.version" keySchemaVersion = "db.version"
// latest schema of the db // latest schema of the db
latestDbSchema = 19 latestDbSchema = 20
keyCloakSecret = "crypto.cloak_secret" keyCloakSecret = "crypto.cloak_secret"
) )
@ -963,6 +963,51 @@ func schemaChangeV18To19(config *Config, tx *buntdb.Tx) error {
return nil return nil
} }
// #1490: start tracking join times for always-on clients
func schemaChangeV19To20(config *Config, tx *buntdb.Tx) error {
type joinData struct {
Modes string
JoinTime int64
}
var accounts []string
var data []string
now := time.Now().UnixNano()
prefix := "account.channeltomodes "
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
accounts = append(accounts, strings.TrimPrefix(key, prefix))
data = append(data, value)
return true
})
for i, account := range accounts {
var existingMap map[string]string
err := json.Unmarshal([]byte(data[i]), &existingMap)
if err != nil {
return err
}
newMap := make(map[string]joinData)
for channel, modeStr := range existingMap {
newMap[channel] = joinData{
Modes: modeStr,
JoinTime: now,
}
}
serialized, err := json.Marshal(newMap)
if err != nil {
return err
}
tx.Set(prefix+account, string(serialized), nil)
}
return nil
}
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) { func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
for _, change := range allChanges { for _, change := range allChanges {
if initialVersion == change.InitialVersion { if initialVersion == change.InitialVersion {
@ -1063,4 +1108,9 @@ var allChanges = []SchemaChange{
TargetVersion: 19, TargetVersion: 19,
Changer: schemaChangeV18To19, Changer: schemaChangeV18To19,
}, },
{
InitialVersion: 19,
TargetVersion: 20,
Changer: schemaChangeV19To20,
},
} }

View File

@ -528,7 +528,7 @@ func (channel *Channel) Founder() string {
func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) { func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
channel.stateMutex.RLock() channel.stateMutex.RLock()
clientModes := channel.members[client] clientModes := channel.members[client].modes
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
return clientModes.HighestChannelUserMode() return clientModes.HighestChannelUserMode()
} }

View File

@ -985,8 +985,8 @@ func extjwtHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
claims["channel"] = channel.Name() claims["channel"] = channel.Name()
claims["joined"] = 0 claims["joined"] = 0
claims["cmodes"] = []string{} claims["cmodes"] = []string{}
if present, cModes := channel.ClientStatus(client); present { if present, joinTimeSecs, cModes := channel.ClientStatus(client); present {
claims["joined"] = 1 claims["joined"] = joinTimeSecs
var modeStrings []string var modeStrings []string
for _, cMode := range cModes { for _, cMode := range cModes {
modeStrings = append(modeStrings, string(cMode)) modeStrings = append(modeStrings, string(cMode))
@ -2660,7 +2660,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
} }
config := server.Config() config := server.Config()
status, _ := channel.historyStatus(config) status, _, _ := channel.historyStatus(config)
if status == HistoryPersistent { if status == HistoryPersistent {
rb.Add(nil, server.name, "FAIL", "RENAME", "CANNOT_RENAME", oldName, utils.SafeErrorParam(newName), client.t("Channels with persistent history cannot be renamed")) rb.Add(nil, server.name, "FAIL", "RENAME", "CANNOT_RENAME", oldName, utils.SafeErrorParam(newName), client.t("Channels with persistent history cannot be renamed"))
return false return false

View File

@ -858,6 +858,7 @@ func (server *Server) GetHistorySequence(providedChannel *Channel, client *Clien
var status HistoryStatus var status HistoryStatus
var target, correspondent string var target, correspondent string
var hist *history.Buffer var hist *history.Buffer
restriction := HistoryCutoffNone
channel = providedChannel channel = providedChannel
if channel == nil { if channel == nil {
if strings.HasPrefix(query, "#") { if strings.HasPrefix(query, "#") {
@ -867,12 +868,15 @@ func (server *Server) GetHistorySequence(providedChannel *Channel, client *Clien
} }
} }
} }
var joinTimeCutoff time.Time
if channel != nil { if channel != nil {
if !channel.hasClient(client) { if present, cutoff := channel.joinTimeCutoff(client); present {
joinTimeCutoff = cutoff
} else {
err = errInsufficientPrivs err = errInsufficientPrivs
return return
} }
status, target = channel.historyStatus(config) status, target, restriction = channel.historyStatus(config)
switch status { switch status {
case HistoryEphemeral: case HistoryEphemeral:
hist = &channel.history hist = &channel.history
@ -904,15 +908,20 @@ func (server *Server) GetHistorySequence(providedChannel *Channel, client *Clien
cutoff = time.Now().UTC().Add(-time.Duration(config.History.Restrictions.ExpireTime)) cutoff = time.Now().UTC().Add(-time.Duration(config.History.Restrictions.ExpireTime))
} }
// #836: registration date cutoff is always enforced for DMs // #836: registration date cutoff is always enforced for DMs
if config.History.Restrictions.EnforceRegistrationDate || channel == nil { // either way, take the later of the two cutoffs
if restriction == HistoryCutoffRegistrationTime || channel == nil {
regCutoff := client.historyCutoff() regCutoff := client.historyCutoff()
// take the later of the two cutoffs
if regCutoff.After(cutoff) { if regCutoff.After(cutoff) {
cutoff = regCutoff cutoff = regCutoff
} }
} else if restriction == HistoryCutoffJoinTime {
if joinTimeCutoff.After(cutoff) {
cutoff = joinTimeCutoff
}
} }
// #836 again: grace period is never applied to DMs // #836 again: grace period is never applied to DMs
if !cutoff.IsZero() && channel != nil { if !cutoff.IsZero() && channel != nil && restriction != HistoryCutoffJoinTime {
cutoff = cutoff.Add(-time.Duration(config.History.Restrictions.GracePeriod)) cutoff = cutoff.Add(-time.Duration(config.History.Restrictions.GracePeriod))
} }
@ -966,7 +975,7 @@ func (server *Server) DeleteMessage(target, msgid, accountName string) (err erro
if target[0] == '#' { if target[0] == '#' {
channel := server.channels.Get(target) channel := server.channels.Get(target)
if channel != nil { if channel != nil {
if status, _ := channel.historyStatus(config); status == HistoryEphemeral { if status, _, _ := channel.historyStatus(config); status == HistoryEphemeral {
hist = &channel.history hist = &channel.history
} }
} }

View File

@ -5,7 +5,11 @@
package irc package irc
import "github.com/oragono/oragono/irc/modes" import (
"time"
"github.com/oragono/oragono/irc/modes"
)
type empty struct{} type empty struct{}
@ -28,12 +32,20 @@ func (clients ClientSet) Has(client *Client) bool {
return ok return ok
} }
type memberData struct {
modes *modes.ModeSet
joinTime int64
}
// MemberSet is a set of members with modes. // MemberSet is a set of members with modes.
type MemberSet map[*Client]*modes.ModeSet type MemberSet map[*Client]memberData
// Add adds the given client to this set. // Add adds the given client to this set.
func (members MemberSet) Add(member *Client) { func (members MemberSet) Add(member *Client) {
members[member] = modes.NewModeSet() members[member] = memberData{
modes: modes.NewModeSet(),
joinTime: time.Now().UnixNano(),
}
} }
// Remove removes the given client from this set. // Remove removes the given client from this set.
@ -47,15 +59,5 @@ func (members MemberSet) Has(member *Client) bool {
return ok return ok
} }
// AnyHasMode returns true if any of our clients has the given mode.
func (members MemberSet) AnyHasMode(mode modes.Mode) bool {
for _, modes := range members {
if modes.HasMode(mode) {
return true
}
}
return false
}
// ChannelSet is a set of channels. // ChannelSet is a set of channels.
type ChannelSet map[*Channel]empty type ChannelSet map[*Channel]empty

View File

@ -850,10 +850,14 @@ history:
# (and will eventually be deleted from persistent storage, if that's enabled) # (and will eventually be deleted from persistent storage, if that's enabled)
expire-time: 1w expire-time: 1w
# if this is set, logged-in users cannot retrieve messages older than their # this restricts access to channel history (it can be overridden by channel
# account registration date, and logged-out users cannot retrieve messages # owners). options are: 'none' (no restrictions), 'registration-time'
# older than their sign-on time (modulo grace-period, see below): # (logged-in users cannot retrieve messages older than their account
enforce-registration-date: false # registration date, and anonymous users cannot retrieve messages older than
# their sign-on time, modulo the grace-period described below), and
# 'join-time' (users cannot retrieve messages older than the time they
# joined the channel, so only always-on clients can view history).
query-cutoff: 'none'
# but if this is set, you can retrieve messages that are up to `grace-period` # 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 # older than the above cutoff time. this is recommended to allow logged-out